diff --git a/.agents/README.md b/.agents/README.md index 863cef1db..fb1335061 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -7,43 +7,39 @@ 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, fleet-first: fleet files under `config/fleets/` (`--fleet`) primary; `NUM_ROBOTS` is the legacy homogeneous knob | +| [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 | +| [extract-module](skills/extract-module) | Extract an in-tree capability into a standalone module repo (TRUNK_REMOVAL.md pattern, sequencing rules, module CI) | +| [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 +58,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) -- **AI Agent Guide:** [docs/development/ai_agent_guide.md](../docs/development/ai_agent_guide.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/advanced/ai_agent_guide.md](../docs/development/advanced/ai_agent_guide.md) diff --git a/.agents/skills/add-behavior-tree-node/SKILL.md b/.agents/skills/add-behavior-tree-node/SKILL.md index 376307c47..36fb7d730 100644 --- a/.agents/skills/add-behavior-tree-node/SKILL.md +++ b/.agents/skills/add-behavior-tree-node/SKILL.md @@ -1,7 +1,7 @@ --- name: add-behavior-tree-node description: Create behavior tree nodes for high-level mission logic and decision-making. Use when implementing actions, conditions, or decorators for behavior trees. Covers BT node types, registration, and integration with behavior executive. -license: Apache-2.0 +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack diff --git a/.agents/skills/add-ros2-package/SKILL.md b/.agents/skills/add-ros2-package/SKILL.md index 4f0db5f8b..a457d84e4 100644 --- a/.agents/skills/add-ros2-package/SKILL.md +++ b/.agents/skills/add-ros2-package/SKILL.md @@ -1,7 +1,7 @@ --- name: add-ros2-package description: Create a new ROS 2 package for the AirStack autonomy stack. Use when implementing a new algorithm module (planner, controller, perception, world model, behavior node). Covers package structure, CMakeLists.txt, package.xml, launch files, and configuration. -license: Apache-2.0 +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -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 @@ -432,7 +439,7 @@ Build and test your package: ```bash # From outside the container -AUTOLAUNCH=false airstack up robot-desktop +airstack up robot-desktop --no-autolaunch # Build the specific package docker exec airstack-robot-desktop-1 bash -c "bws --packages-select your_package_name" 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..40dd2201e 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 @@ -197,11 +197,11 @@ ros2 launch your_package your_package.launch.xml \ The module is automatically launched when the autonomy stack starts: ```bash -# Full autonomy stack +# Full autonomy stack (autolaunches by default) airstack up robot-desktop -# Or with autolaunch -AUTOLAUNCH=true airstack up robot-desktop +# Or start idle and launch manually +airstack up robot-desktop --no-autolaunch ``` The module is integrated in: `_bringup/launch/.launch.xml` @@ -254,7 +254,7 @@ airstack up isaac-sim robot # Run test scenario... ``` -See [test_in_simulation.md](../../.agents/skills/test_in_simulation.md) for detailed testing procedures. +See the [test-in-simulation skill](../../../test-in-simulation/SKILL.md) for detailed testing procedures. ## Visualization @@ -351,8 +351,6 @@ Apache-2.0 (consistent with AirStack) - **Maintainer:** Your Name (your.email@example.com) - **Contributors:** List additional contributors here -## Changelog - -### Version 0.0.1 (YYYY-MM-DD) -- Initial implementation -- TODO: Add changelog entries as the module evolves + 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/add-task-executor/SKILL.md b/.agents/skills/add-task-executor/SKILL.md index ab91e6529..db525d0cb 100644 --- a/.agents/skills/add-task-executor/SKILL.md +++ b/.agents/skills/add-task-executor/SKILL.md @@ -1,7 +1,7 @@ --- name: add-task-executor description: Implement a new task executor as a ROS 2 action server in AirStack. Use when adding a new goal-directed task (coverage, search, counting, trajectory following, etc.) that a user can trigger with parameters, monitor via feedback, and cancel. Reference implementation is random_walk_planner (ExplorationTask). -license: Apache-2.0 +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -260,9 +260,9 @@ int main(int argc, char* argv[]) { Only switch to `MultiThreadedExecutor` if you have independent callbacks that genuinely need concurrent execution **and** all shared resources are thread-safe. Nodes that use OpenGL, CUDA, or other thread-affine resources **must** use `rclcpp::spin()` to keep callbacks serialized. -### 3. Add remap to bringup launch +### 3. Add remap in the stack entry file -In the layer bringup launch file (e.g., `global_bringup/launch/global.launch.xml`): +In the module's canonical launch file, declare the action endpoint as a topic arg with the canonical default; any deviation is wired in the stack entry file (e.g., `stacks/full_default/launch/stack.launch.xml` — the single-locus rule): ```xml ``` diff --git a/.agents/skills/add-unit-tests/SKILL.md b/.agents/skills/add-unit-tests/SKILL.md index 27aeb3a87..37a0108f0 100644 --- a/.agents/skills/add-unit-tests/SKILL.md +++ b/.agents/skills/add-unit-tests/SKILL.md @@ -1,7 +1,7 @@ --- name: add-unit-tests description: Add Python or C++ unit tests to an AirStack ROS 2 package. Covers the co-location pattern (test source in package/test/), registering the package in colcon_unit_test_packages.yaml so pytest tests/ and airstack test -m unit collect it, and how to extend to sim components. -license: MIT +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -68,8 +68,8 @@ Whether `colcon test` *also* picks up a package's Python tests depends on its bu | Package | Build type | Python tests under `colcon test` | |---|---|---| -| `natnet_ros2` | `ament_cmake` | **No** — `CMakeLists.txt` registers `ament_add_gtest` but no `ament_add_pytest_test` | | `lidar_point_cloud_filter` | `ament_python` | **Yes** — `setup.cfg` sets `testpaths = test`, so colcon's pytest runner finds them | +| any `ament_cmake` package | `ament_cmake` | **No** unless `CMakeLists.txt` registers `ament_add_pytest_test` (an `ament_add_gtest` alone covers only C++) | So a Python test in an `ament_cmake` package runs *only* via the root harness — which is fine, since that is what CI invokes. @@ -89,8 +89,8 @@ Good candidates are functions/classes with **no ROS or hardware dependencies**: - Data-structure converters - Any function that takes plain Python types and returns plain Python types -If the code imports ROS types, stub them out at the import boundary -(see `test_natnet_ros2.py` for the `sys.modules` stub pattern). +If the code imports ROS types, stub them out at the import boundary with +`sys.modules.setdefault(...)` before importing your module (pattern below). ### 2. Write the test source in the package @@ -145,7 +145,7 @@ sys.modules.setdefault("geometry_msgs.msg", MagicMock()) For `rclpy.node.Node` subclasses use a real dummy base class instead of a `MagicMock()` to ensure `__init_subclass__` fires and method bodies are defined -(see `test_natnet_ros2.py` for the full pattern). +(the asm_optitrack module's `test_natnet_ros2.py` shows the full pattern). ### 3. Register the package in colcon_unit_test_packages.yaml @@ -155,7 +155,6 @@ If the package isn't already listed, add it under the `robot` workspace in ```yaml robot: packages: - - natnet_ros2 - lidar_point_cloud_filter - # ← add here pytest_args: [] @@ -282,17 +281,18 @@ sim: | How do I run them? | `airstack test -m unit`, `cd tests && pytest -m unit`, or `pytest tests/ -m unit` | | What CI workflow runs them? | Python: `unit-tests.yml`; C++: the `build_packages` path in `system-tests.yml` — see §5 | | Do system tests (`liveliness`, etc.) run too? | No — `-m unit` filters to hermetic tests only | -| Does `colcon test` also run these? | Only if the package registers them. `ament_add_gtest` covers C++; a Python test needs `ament_add_pytest_test`, which `natnet_ros2` does **not** have — its Python tests run only under the root harness | +| Does `colcon test` also run these? | Only if the package registers them. `ament_add_gtest` covers C++; a Python test in an `ament_cmake` package needs `ament_add_pytest_test` — without it, its Python tests run only under the root harness | | Can I add pure C++ gtests? | Yes — `ament_add_gtest` in CMakeLists.txt | ## Reference Implementations | Package | Python test | What it covers | |---|---|---| -| `natnet_ros2` | `robot/ros_ws/src/perception/natnet_ros2/test/test_natnet_ros2.py` | `VisionPoseConverterNode._canonical_quaternion` (ROS-stubbed) | -| `natnet_ros2` (C++) | `robot/ros_ws/src/perception/natnet_ros2/test/test_natnet_logic.cpp` | `build_covariance_6x6`, `negotiate()`, `INatNetClient` seam | | `lidar_point_cloud_filter` | `robot/ros_ws/src/sensors/lidar_point_cloud_filter/test/test_validation_core.py` | Pure-numpy range validation rules | +For an `ament_cmake` example (ROS-stubbed Python tests + gtests with a client seam), see +`natnet_ros2` in the [asm_optitrack module](https://github.com/castacks/asm_optitrack). + Both are collected from their package `test/` dir. ## Files to Know diff --git a/.agents/skills/attach-gossip-payload/SKILL.md b/.agents/skills/attach-gossip-payload/SKILL.md index 0e726648b..86e5ef7a0 100644 --- a/.agents/skills/attach-gossip-payload/SKILL.md +++ b/.agents/skills/attach-gossip-payload/SKILL.md @@ -1,3 +1,12 @@ +--- +name: attach-gossip-payload +description: Broadcast any ROS message to all peer robots via the gossip protocol by attaching it as a PeerProfile payload — config-driven via gossip_payloads.yaml, with the peer_profile.py add_payload/get_payload API for consumers. Use when a module needs to share data (frontier maps, sensor summaries, task status) across robots on the gossip domain. +license: BSD-3-Clause-Clear +metadata: + author: AirLab CMU + repository: AirStack +--- + # Skill: Attach Custom Payload to PeerProfile (Gossip Protocol) ## When to use diff --git a/.agents/skills/bump-version-and-release/SKILL.md b/.agents/skills/bump-version-and-release/SKILL.md index a4a74c77e..61aa4d309 100644 --- a/.agents/skills/bump-version-and-release/SKILL.md +++ b/.agents/skills/bump-version-and-release/SKILL.md @@ -1,7 +1,7 @@ --- name: bump-version-and-release -description: Bump the AirStack VERSION in .env (semver) before merging a PR that changes Docker image content, and update CHANGELOG. Required to pass the check-version-increment gate and to trigger the docker-build release workflow. -license: Apache-2.0 +description: Bump the AirStack VERSION in .env (semver) before merging a PR that changes Docker image content, and record the change in the versioned Release Notes (docs/release_notes/index.md). Required to pass the check-version-increment gate and to trigger the docker-build release workflow. +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -17,7 +17,7 @@ Bump VERSION when the PR touches: - Any `Dockerfile` under `robot/`, `simulation/isaac-sim/`, `simulation/ms-airsim/`, `gcs/`, `common/`, or `tests/docker/` - `docker-compose.yaml` or any included sub-compose file (when the change affects what is built or installed into images) -- Code that is **baked into** an image (i.e., copied during build, not bind-mounted at runtime). For `DOCKER_IMAGE_BUILD_MODE="prebuilt"` this includes `robot/ros_ws/src/**`. For `DOCKER_IMAGE_BUILD_MODE="dev"` (the current default in `.env`) the workspace is bind-mounted, so source-only changes there do not strictly require a rebuild — but bumping is still safer if you are unsure. +- Code that is **baked into** an image (i.e., copied during build, not bind-mounted at runtime). The workspace is bind-mounted (`DOCKER_IMAGE_BUILD_MODE` is a tag discriminator only — a real `prebuilt` workspace-baked stage is future work), so source-only changes under `robot/ros_ws/src/**` do not strictly require a rebuild — but bumping is still safer if you are unsure. - Apt packages, pip requirements, ROS package manifests installed during the build - Entry-point scripts, tmux configs, or `.bashrc` snippets copied into images - Submodule pointer updates that affect image contents @@ -28,7 +28,7 @@ If in doubt, bump. The CI gate enforces a strict increment vs. the base branch The version-increment check still runs on every PR, but the **only** thing it requires is that VERSION be valid semver and strictly greater than the base. For documentation-only PRs you have two options: -- **Preferred:** still bump the patch (or pre-release counter) by one. It costs nothing, keeps the gate happy, and the docker-build only fires on push to `main`/`develop` *and* a VERSION change, so an extra alpha bump on a docs PR is cheap. +- **Preferred:** still bump the patch (or pre-release counter) by one. It costs nothing, keeps the gate happy, and the docker-build only fires on push to `main`/`develop` *and* a VERSION change, so an extra dev bump on a docs PR is cheap. - **If you really want to avoid a rebuild:** the docker-build workflow only triggers when `.env` is in the changed paths AND `VERSION=` differs from `HEAD~1`. So a docs-only PR that does not touch `.env` will not rebuild — but the PR will still fail the increment check unless you bump. There is no clean way to "opt out" of the check; the simplest path is to bump the pre-release counter. Do **not** bump for: @@ -47,17 +47,17 @@ Three workflows in `.github/workflows/` interact with `VERSION`: - **Trigger:** every `pull_request`. - **Logic:** runs a Python script that reads `VERSION=` from the PR's `.env`, reads the same line from `origin/:.env`, and validates: - - PR version matches the regex `^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.(\d+))?$` - - PR version is **strictly greater than** base version, with pre-release ordering `alpha < beta < rc < (no suffix / release)` + - PR version matches the regex `^(\d+)\.(\d+)\.(\d+)(?:-(dev|beta|rc)\.(\d+))?$` + - PR version is **strictly greater than** base version, with pre-release ordering `dev < beta < rc < (no suffix / release)` - **Accepted formats** (from the workflow's own error message): ``` MAJOR.MINOR.PATCH (e.g. 1.2.3) - MAJOR.MINOR.PATCH-alpha.N (e.g. 1.3.0-alpha.1) + MAJOR.MINOR.PATCH-dev.N (e.g. 1.3.0-dev.1) MAJOR.MINOR.PATCH-beta.N (e.g. 1.3.0-beta.2) MAJOR.MINOR.PATCH-rc.N (e.g. 1.3.0-rc.3) ``` -- **Rejected:** `1.2`, `1.2.3-rc1` (no dot before N), `1.2.3-dev`, `1.2.3+meta`, `v1.2.3`, anything with build metadata. -- **Comparison tuple:** `(major, minor, patch, pre_rank, pre_num)` where `pre_rank = alpha:0, beta:1, rc:2, release:3`. So `1.3.0-alpha.5 < 1.3.0-beta.1 < 1.3.0-rc.1 < 1.3.0`. A release version always sorts above any pre-release of the same `MAJOR.MINOR.PATCH`. +- **Rejected:** `1.2`, `1.2.3-rc1` (no dot before N), `1.2.3-dev` (missing the `.N` counter), `1.2.3-alpha.4` (the legacy `alpha` tag was replaced by `dev` in 0.20.0), `1.2.3+meta`, `v1.2.3`, anything with build metadata. +- **Comparison tuple:** `(major, minor, patch, pre_rank, pre_num)` where `pre_rank = dev:0, beta:1, rc:2, release:3`. So `1.3.0-dev.5 < 1.3.0-beta.1 < 1.3.0-rc.1 < 1.3.0`. A release version always sorts above any pre-release of the same `MAJOR.MINOR.PATCH`. ### 2. `docker-build.yml` — the publish trigger @@ -82,11 +82,11 @@ So the full release path is: bump `VERSION` → PR → merge to `main`/`develop` ## Choosing the Bump Type -Use this decision tree on the **current** version (currently `0.18.0-alpha.7`): +Use this decision tree on the **current** version (e.g. `0.21.0-dev.7`): ``` Is this a breaking API/topic/interface change? -├── yes → bump MAJOR, reset MINOR=0, PATCH=0 (e.g. 0.18.0-alpha.7 → 1.0.0-alpha.1 if pre-1.0) +├── yes → bump MAJOR, reset MINOR=0, PATCH=0 (e.g. 0.21.0-dev.7 → 1.0.0-dev.1 if pre-1.0) └── no ├── New feature / new module / new Docker image content? │ └── yes → bump MINOR, reset PATCH=0 (e.g. 0.18.0 → 0.19.0) @@ -94,16 +94,16 @@ Is this a breaking API/topic/interface change? └── yes → bump PATCH (e.g. 0.18.0 → 0.18.1) Are you mid-cycle on a pre-release line (suffix present)? -├── Same line, more iteration → increment N (0.18.0-alpha.7 → 0.18.0-alpha.8) -├── Promoting alpha → beta → reset N to 1 (0.18.0-alpha.7 → 0.18.0-beta.1) +├── Same line, more iteration → increment N (0.21.0-dev.7 → 0.21.0-dev.8) +├── Promoting dev → beta → reset N to 1 (0.21.0-dev.7 → 0.21.0-beta.1) ├── Promoting beta → rc → reset N to 1 (0.18.0-beta.4 → 0.18.0-rc.1) └── Promoting rc → release → drop suffix (0.18.0-rc.3 → 0.18.0) ``` Notes: -- AirStack is pre-1.0; many "breaking" changes still bump MINOR rather than MAJOR. Use judgment, and prefer pre-release suffixes (`-alpha.N`) for the active development line so feature PRs do not have to fight over MINOR numbers. -- The current pattern in git history is per-PR alpha bumps on the development line and a final un-suffixed bump at release time (e.g. `0.16.1-rc → 0.16.1`, `0.17.0-rc1 → 0.17.0` — note the older `-rc1` form predates the current validator and would be rejected today; use `-rc.1`). +- AirStack is pre-1.0; many "breaking" changes still bump MINOR rather than MAJOR. Use judgment, and prefer pre-release suffixes (`-dev.N`) for the active development line so feature PRs do not have to fight over MINOR numbers. +- The current pattern in git history is per-PR dev bumps on the development line and a final un-suffixed bump at release time. History before 0.21.0 used `-alpha.N` for this role, and even older releases used forms like `-rc1` — both predate the current validator and would be rejected today; use `-dev.N` / `-rc.N`. ## Bumping Steps @@ -111,7 +111,7 @@ Notes: ```bash airstack version -# → AirStack Version: 0.18.0-alpha.7 +# → AirStack Version: 0.21.0-dev.7 ``` (Equivalent: `grep '^VERSION=' .env`.) @@ -121,28 +121,33 @@ airstack version Open `/.env` and change exactly the `VERSION=` line. Keep the surrounding comments and quoting intact: ```diff -- VERSION="0.18.0-alpha.7" -+ VERSION="0.18.0-alpha.8" +- VERSION="0.21.0-dev.7" ++ VERSION="0.21.0-dev.8" ``` The validator strips surrounding `"` or `'`, so either quoting style works, but match the existing style (double quotes). -### 3. Update `CHANGELOG.md` +### 3. Update the Release Notes -Add an entry under `## [Unreleased]` describing your change (see "CHANGELOG Conventions" below). For a true release (no pre-release suffix), promote `[Unreleased]` to a new dated version section. +All change records live in the versioned Release Notes page, +`docs/release_notes/index.md` (there is no CHANGELOG.md — this page is the +single source). Add your bullets under the current version's `##` section +(see "Release Notes Conventions" below). For a true release (no pre-release +suffix), stamp that section's heading with the release date and open a fresh +`## (Unreleased)` section above it. ### 4. Verify locally ```bash airstack version # prints the new value grep '^VERSION=' .env # sanity-check the literal line -git diff .env CHANGELOG.md # review the diff +git diff .env docs/release_notes/index.md # review the diff ``` Optional regex preflight (mirrors the CI check): ```bash -python3 -c 'import re,sys; v=open(".env").read(); m=re.search(r"^VERSION\s*=\s*\"?([^\"#\s]+)", v, re.M); print(m.group(1)); assert re.fullmatch(r"^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.(\d+))?$", m.group(1)), "INVALID"' +python3 -c 'import re,sys; v=open(".env").read(); m=re.search(r"^VERSION\s*=\s*\"?([^\"#\s]+)", v, re.M); print(m.group(1)); assert re.fullmatch(r"^(\d+)\.(\d+)\.(\d+)(?:-(dev|beta|rc)\.(\d+))?$", m.group(1)), "INVALID"' ``` ### 5. Commit @@ -150,24 +155,42 @@ python3 -c 'import re,sys; v=open(".env").read(); m=re.search(r"^VERSION\s*=\s*\ Use a clear, conventional message: ``` -Bump version to 0.18.0-alpha.8 +Bump version to 0.21.0-dev.8 ``` Recent commits in this repo use exactly this phrasing (`Bump version to 0.17.0`, `Bump version to 0.16.1`). -## CHANGELOG Conventions - -`CHANGELOG.md` follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) and Semantic Versioning. The literal layout in the repo is: +## Release Notes Conventions + +`docs/release_notes/index.md` is the versioned change record rendered on +the docs site — one `##` section per version, newest first, with the current +in-progress version marked `(Unreleased)`. It is also the ONLY place +change-relative language ("changed from", "renamed", "removed", RFC/PR +references) is allowed; feature docs describe only the current system (see +the `write-mkdocs-documentation` skill). + +The source file keeps one section per version from 0.19.0 onward (notes for +0.18.0 and earlier live only on the GitHub releases page, which the page's +standing intro links to), but the rendered site shows +only the section matching the repo-root `.env` `VERSION` at build time: the +MkDocs hook `docs/hooks/release_notes_current_version.py` (registered under +`hooks:` in `mkdocs.yml`) trims the rest, so each mike-deployed docs version +carries only its own notes and readers reach older ones via the site's +version selector or the GitHub releases page. Section headings must +therefore start with the literal base semver (`## ...`) or the hook +won't match them; if VERSION has no matching section, the hook logs a +warning and publishes the page unfiltered. + +Layout: ```markdown -# Changelog +# Release Notes -All notable changes to this project will be documented in this file. + -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## (Unreleased) -## [Unreleased] + ### Added @@ -177,50 +200,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - -### Fixed +### Removed - -## [1.0.0] - 2024-12-19 - -First official public release. - -### Added - -- - ### Fixed -- - -### Changed - -- +- -### Removed +## -- +... ``` Rules: -- Use the H2 sections **Added**, **Changed**, **Fixed**, **Removed**, **Deprecated**, **Security** as needed (Keep a Changelog standard set). -- For pre-release bumps (`-alpha.N`, `-beta.N`, `-rc.N`), keep your bullets under `## [Unreleased]`. Do not create a section per alpha. -- For a release bump (no suffix), rename `[Unreleased]` to `## [] - ` and add a fresh empty `## [Unreleased]` above it. -- Use ISO date format `YYYY-MM-DD`. -- Write user-facing prose, not commit log dumps. Mention new modules, breaking changes, and notable behavior shifts. +- Use the H3 sections **Added**, **Changed**, **Fixed**, **Removed**, **Deprecated**, **Security** as needed; a release may also open with a short narrative and breaking-changes subsection. +- For pre-release bumps (`-dev.N`, `-beta.N`, `-rc.N`), keep your bullets under the current `(Unreleased)` section. Do not create a section per dev bump. +- For a release bump (no suffix), retitle the section to `## ` and open a fresh `## (Unreleased)` above it. +- Write user-facing prose, not commit log dumps. Mention new modules, breaking changes, and notable behavior shifts, with what changed FROM what. ## Common Pitfalls - **Forgetting the bump.** The `check-version-increment` job fails with `::error::VERSION must be strictly greater than the base branch version.` Bump and force-push the branch. -- **Invalid semver.** Forms like `1.2`, `1.2.3-rc1`, `1.2.3-dev`, `1.2.3+sha.abc`, `v1.2.3`, or empty strings fail with `::error::VERSION '' does not match the required format.` The only allowed pre-release tags are exactly `alpha`, `beta`, `rc`, each followed by a literal dot and an integer (e.g. `-rc.1`, never `-rc1`). +- **Invalid semver.** Forms like `1.2`, `1.2.3-rc1`, `1.2.3-dev` (no `.N`), `1.2.3-alpha.4` (legacy tag), `1.2.3+sha.abc`, `v1.2.3`, or empty strings fail with `::error::VERSION '' does not match the required format.` The only allowed pre-release tags are exactly `dev`, `beta`, `rc`, each followed by a literal dot and an integer (e.g. `-dev.1`, never `-dev1`). - **Going backwards.** `0.18.0 → 0.18.0-rc.1` looks like progress but is a regression: release > rc. Always move forward in the comparison tuple. - **Two PRs racing for the same number.** Whichever merges last wins; the loser's `check-version-increment` will start failing the moment the base advances past it. Rebase on the updated base branch and bump again. -- **Bumping but forgetting the CHANGELOG.** No CI gate enforces this, but reviewers will (and the release docs workflow lists what shipped per version, so missing entries become invisible history). -- **Bumping for pure docs PRs.** Wastes a registry tag. Prefer to keep docs-only changes off `.env` if possible — but if the gate is failing, an alpha bump is the path of least resistance. +- **Bumping but forgetting the Release Notes.** No CI gate enforces this, but reviewers will (and the versioned docs deploys snapshot the page per release, so missing entries become invisible history). +- **Bumping for pure docs PRs.** Wastes a registry tag. Prefer to keep docs-only changes off `.env` if possible — but if the gate is failing, a dev bump is the path of least resistance. - **Editing `VERSION=` quoting.** The extractor regex `^VERSION\s*=\s*["\']?([^"\'#\s]+)` handles double quotes, single quotes, or no quotes, and stops at `#`/whitespace. Don't add inline comments after the value (e.g. `VERSION="0.18.1" # bumped`) — the trailing `# bumped` will be stripped from the value but obscures intent; put comments on their own line above. - **Touching only sub-compose `.env` files.** The check looks at the **repo-root** `.env` only. `robot/docker/.env` and friends are container env files, not the version source of truth. -- **Force-pushing after merge to fix CHANGELOG.** Don't. Land a follow-up PR with the CHANGELOG correction (and, by the rules above, another tiny VERSION bump). +- **Force-pushing after merge to fix Release Notes.** Don't. Land a follow-up PR with the correction (docs-only, so no VERSION bump needed unless the gate demands one). ## Release Checklist @@ -229,8 +239,8 @@ For a normal feature/fix PR: 1. [ ] Confirm the PR changes Docker image content or otherwise warrants a bump (see "When to Use"). 2. [ ] Pick the bump type (see "Choosing the Bump Type"). 3. [ ] Edit `/.env` — change only the `VERSION=` line. -4. [ ] Update `CHANGELOG.md` under `## [Unreleased]`. -5. [ ] `airstack version` and `git diff .env CHANGELOG.md` to verify. +4. [ ] Add your bullets to `docs/release_notes/index.md` under the current `(Unreleased)` version section. +5. [ ] `airstack version` and `git diff .env docs/release_notes/index.md` to verify. 6. [ ] Commit (`Bump version to ` is the established style). 7. [ ] Push and open the PR. Confirm `Check VERSION Increment` passes green. 8. [ ] After review, merge into `develop` (or `main` per branch policy). @@ -240,7 +250,7 @@ For a true release (dropping the pre-release suffix): 1. [ ] Land final fixes on `develop` with `-rc.N` bumps. 2. [ ] Open a PR that bumps `VERSION="X.Y.Z-rc.N"` → `VERSION="X.Y.Z"`. -3. [ ] In the same PR, promote `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD` and add a fresh empty `## [Unreleased]`. +3. [ ] In the same PR, retitle the Release Notes section to `## X.Y.Z — YYYY-MM-DD` and open a fresh `## (Unreleased)` above it. 4. [ ] Merge to `main`. 5. [ ] Wait for `docker-build.yml` to push and sign all images. 6. [ ] Create a GitHub Release with tag `X.Y.Z` (matching `VERSION` exactly). Publishing the release fires `deploy_docs_from_release.yaml`, which runs `mike deploy --push --update-aliases X.Y.Z latest` and updates the versioned docs site. @@ -249,13 +259,12 @@ For a true release (dropping the pre-release suffix): ## References - [`/.env`](../../../.env) — source of truth for `VERSION=` -- [`/CHANGELOG.md`](../../../CHANGELOG.md) — release history +- [`/docs/release_notes/index.md`](../../../docs/release_notes/index.md) — the versioned Release Notes (release history) - [`/.github/workflows/check-version-increment.yml`](../../../.github/workflows/check-version-increment.yml) — the PR gate (semver regex lives here) - [`/.github/workflows/docker-build.yml`](../../../.github/workflows/docker-build.yml) — build/push/sign on tag change - [`/.github/workflows/deploy_docs_from_release.yaml`](../../../.github/workflows/deploy_docs_from_release.yaml) — versioned docs on release - [`/.github/workflows/deploy_docs_from_main.yaml`](../../../.github/workflows/deploy_docs_from_main.yaml) and [`deploy_docs_from_develop.yaml`](../../../.github/workflows/deploy_docs_from_develop.yaml) — branch-tracking docs aliases - [`/airstack.sh`](../../../airstack.sh) — defines `airstack version` and `get_VERSION` (used everywhere image tags are built) -- [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) - [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) ## Related Skills diff --git a/.agents/skills/capture-discovered-knowledge/SKILL.md b/.agents/skills/capture-discovered-knowledge/SKILL.md index d362a62ad..3fa2c0396 100644 --- a/.agents/skills/capture-discovered-knowledge/SKILL.md +++ b/.agents/skills/capture-discovered-knowledge/SKILL.md @@ -1,7 +1,7 @@ --- name: capture-discovered-knowledge description: Persist hard-won discoveries to AGENTS.md or a new/existing SKILL.md so future agents don't repeat the discovery cost. Trigger after any long context-discovery task (multi-minute grep / file-reading session, parallel research agents, debugging that took several iterations) or whenever you learn something critical, surprising, undocumented, or that contradicted prior assumptions in AGENTS.md or a skill. Decides between updating AGENTS.md, updating an existing skill, or creating a new skill. -license: Apache-2.0 +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack diff --git a/.agents/skills/configure-multi-robot/SKILL.md b/.agents/skills/configure-multi-robot/SKILL.md index a6ffacef1..04b1ff940 100644 --- a/.agents/skills/configure-multi-robot/SKILL.md +++ b/.agents/skills/configure-multi-robot/SKILL.md @@ -1,7 +1,7 @@ --- name: configure-multi-robot -description: Configure, name, and isolate multiple robots in AirStack. Use whenever launching multi-robot, multiple robots, swarm, or fleet scenarios; setting ROBOT_NAME; debugging cross-robot topic collisions; choosing a ROS_DOMAIN_ID; or namespacing topics, TF frames, and DDS bridges across robots. -license: Apache-2.0 +description: Configure, name, and isolate multiple robots in AirStack — fleet files (config/fleets/, airstack up --fleet) first, legacy NUM_ROBOTS second. Use whenever launching multi-robot, multiple robots, swarm, or fleet scenarios; mixing different stacks/vehicles per robot (heterogeneous fleets, split placement via hosts:); setting ROBOT_NAME; debugging cross-robot topic collisions; choosing a ROS_DOMAIN_ID; or namespacing topics, TF frames, and DDS bridges across robots. +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -13,7 +13,8 @@ metadata: Reach for this skill any time you: -- Spawn more than one robot in simulation (`NUM_ROBOTS > 1`) +- Spawn more than one robot in simulation (`--fleet ` or legacy `NUM_ROBOTS > 1`) +- Need robots that differ (stack, vehicle, or offboard placement) — a **heterogeneous fleet** - Deploy multiple physical aircraft (VOXL, Jetson, etc.) - Debug topic collisions, missing topics on `/robot_2/...`, or "two robots talking on the same topic" - Write a new launch file or YAML config that hardcodes a topic path @@ -29,9 +30,51 @@ If you only ever touch one robot, you can usually skip this skill — but the mo - Basic understanding of ROS 2 namespaces and TF frame names - You have already read [`docs/robot/docker/robot_identity.md`](../../../docs/robot/docker/robot_identity.md), or are willing to as you go — that file is the canonical reference for the resolution mechanism -## How ROBOT_NAME Flows Through the Stack +## Fleet-First: Declare the Whole Deployment in One File -`ROBOT_NAME` is **not** a single static value. It is computed per container at shell start by `robot/docker/.bashrc` and propagated into every ROS launch substitution. The full chain: +Since RFC #380 P6, the preferred way to run multiple robots is a **fleet file** +(`config/fleets/*.yaml`): who exists, which vehicle each flies, which stack +each runs, and which ground host runs each split stack's offboard half. Full +guide: [`docs/development/fleets.md`](../../../docs/development/fleets.md). + +```bash +airstack fleet list # what exists + shape +airstack up --fleet sim_one_default --sim isaac # 1 robot, today's defaults +airstack up --fleet sim_three_mixed --sim isaac # heterogeneous: 3 robots, 3 stacks + a split +``` + +What `--fleet ` does: + +- validates the fleet (named errors), exports `FLEET_CONFIG_FILE` (container + path), and **derives `NUM_ROBOTS`** from the robot count (explicit env + `NUM_ROBOTS` still wins, with a banner) +- on Isaac, switches an untouched-default `ISAAC_SIM_SCRIPT_NAME` to the + generic fleet spawner `fleet_spawn.py` (spawns/scene/sensors from the fleet + + vehicle files) +- **homogeneous** fleets (same vehicle + stack everywhere) keep + `deploy.replicas`; each replica resolves its own entry via + `tools/fleet/resolve_fleet.py` in `.bashrc` (opt-in: only when + `FLEET_CONFIG_FILE` is set) +- **heterogeneous** fleets get generated per-robot services + (`airstack fleet generate ` → + `.airstack/generated/docker-compose.fleet.yaml`, auto-included; the `fleet` + compose profile replaces `desktop`) +- a robot with `hosts: {offboard: gcs}` on a split stack gets its `onboard` + entry point, and the named ground host gets a service running the same + stack with `AIRSTACK_STACK_ENTRY=offboard` — the declared successor of the + `desktop_split` / `offboard` profiles + +Test harness: `airstack test -m liveliness --fleet sim_three_mixed ...` +passes `FLEET_CONFIG_FILE` + the derived `NUM_ROBOTS`; without `--fleet`, +`--num-robots` behaves exactly as before. + +Everything below — the legacy `NUM_ROBOTS` + `robot_name_map` path — remains +the default without a fleet and is still fully supported; the topic/TF +namespacing rules and pitfalls apply identically under both paths. + +## How ROBOT_NAME Flows Through the Stack (Legacy Path) + +`ROBOT_NAME` is **not** a single static value. It is computed per container at shell start by `robot/docker/.bashrc` and propagated into every ROS launch substitution. When `FLEET_CONFIG_FILE` is set, a fleet branch in `.bashrc` resolves the whole fleet entry first (name, domain, stack placement, vehicle — pre-set env still wins per variable, and failures fall back to the legacy resolver below). The legacy chain: ``` .env (ROBOT_NAME_MAP_CONFIG_FILE, NUM_ROBOTS) @@ -137,9 +180,9 @@ does work because `docker exec -e` sets it in the process environment: docker exec -e ROBOT_NAME=robot_5 -e ROS_DOMAIN_ID=5 -it airstack-robot-desktop-1 bash ``` -## Launching Multiple Robots +## Launching Multiple Robots (Legacy `NUM_ROBOTS` Path) -AirStack launches multiple robots as **replicas of the same container**, not as multiple namespaces inside one container. Look at [`robot/docker/docker-compose.yaml`](../../../robot/docker/docker-compose.yaml): +Prefer `airstack up --fleet ` (above). Without a fleet, AirStack launches multiple robots as **replicas of the same container**, not as multiple namespaces inside one container — which is also why replicas can only ever be *identical* robots (heterogeneous fleets need the generated per-robot services). Look at [`robot/docker/docker-compose.yaml`](../../../robot/docker/docker-compose.yaml): ```yaml robot-desktop: @@ -148,7 +191,7 @@ robot-desktop: replicas: ${NUM_ROBOTS:-1} ``` -So `NUM_ROBOTS=3 airstack up` produces **three** robot containers (`airstack-robot-desktop-1`, `-2`, `-3`), each with its own `ROBOT_NAME` and its own `ROS_DOMAIN_ID`. Each container runs the full autonomy stack independently. Cross-robot communication, when needed, goes through the DDS router (see [`onboard_all/config/dds_router.yaml`](../../../robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml)) which bridges allowlisted topics from each per-robot domain into a shared GCS domain. +So `NUM_ROBOTS=3` (set by `airstack up --robots 3`) produces **three** robot containers (`airstack-robot-desktop-1`, `-2`, `-3`), each with its own `ROBOT_NAME` and its own `ROS_DOMAIN_ID`. Each container runs the full autonomy stack independently. Cross-robot communication, when needed, goes through the DDS router (see the shared allowlist [`autonomy_bringup/config/dds_router.yaml`](../../../robot/ros_ws/src/autonomy_bringup/config/dds_router.yaml)) which bridges allowlisted topics from each per-robot domain into a shared GCS domain. ```bash airstack up --sim isaac --robots 3 # sets NUM_ROBOTS and the multi-drone Isaac script together @@ -160,16 +203,17 @@ docker ps --format '{{.Names}}' | grep robot-desktop The simulator side has to spawn matching vehicles — see [Sim-Side Robot Spawning](#sim-side-robot-spawning). -### `onboard_all` vs `onboard_local_offboard_global` +### Full vs. lite vs. split topologies (stacks) -[`autonomy_bringup`](../../../robot/ros_ws/src/autonomy_bringup/) ships two layouts, selected by the `role` arg / `AUTONOMY_ROLE` env var: +Topology is selected by **stack** — the legacy `AUTONOMY_ROLE` role dispatch was removed (a set `AUTONOMY_ROLE` is now a preflight error): `--stack full_default` (the no-stack default) runs everything on the machine, `--stack lite_default` runs the lite set, `--stack lite_offload_global:onboard|:offboard` is the split pair, and a fleet entry's `hosts: {offboard: }` declares the split *placement* (see Fleet-First above). -| Variant | Role values | What runs onboard | What runs offboard | When to use | -|--------|-------------|-------------------|--------------------|-------------| -| `onboard_all` | `role:=full` | interface, sensors, perception, local, **global**, behavior | nothing | Sim/dev desktop, autonomous Jetson with enough compute, single-machine deployments | -| `onboard_local_offboard_global` | `role:=onboard` (lite) + `role:=offboard` (GCS) | interface, sensors, perception, local, behavior | global planning + mapping | VOXL / lite Jetson where global planning is offloaded to a ground station; `desktop_split` profile for debugging the split | +| Stack | What runs onboard | What runs offboard | When to use | +|-------|-------------------|--------------------|-------------| +| `full_default` | interface, sensors, perception, local, **global**, behavior, logging | nothing | Sim/dev desktop, autonomous Jetson with enough compute, single-machine deployments | +| `lite_default` | interface, sensors, perception, local, behavior | nothing (no global anywhere) | Compute-constrained vehicle flying task-driven missions | +| `lite_offload_global` (`:onboard` + `:offboard`) | interface, sensors, perception, local, behavior | global planning + mapping | VOXL / lite Jetson where global planning is offloaded to a ground station; `desktop_split` profile for debugging the split | -The split is significant for multi-robot: with `onboard_local_offboard_global`, **one offboard container is launched per robot** (also via `replicas: ${NUM_ROBOTS}`), all on `ROS_DOMAIN_ID=0`, and each bridges into its own per-robot onboard domain via the domain bridge config in `onboard_local_offboard_global/config/dds_router.yaml`. See [`docs/robot/autonomy_modes.md`](../../../docs/robot/autonomy_modes.md) for the profile matrix. +The split is significant for multi-robot: with `lite_offload_global`, **one offboard container is launched per robot** (also via `replicas: ${NUM_ROBOTS}`), all on `ROS_DOMAIN_ID=0`, and each bridges into its own per-robot onboard domain via the DDS-router config generated from the stack's `bridge.yaml` (`python3 tools/gen_dds_router.py stacks/lite_offload_global/bridge.yaml` — the generated allowlist deliberately drops the legacy split's `set_trajectory_mode` crossing, doctor hard gate #2). See [`docs/robot/autonomy_modes.md`](../../../docs/robot/autonomy_modes.md) for the profile matrix. ## Topic and TF Namespacing @@ -246,6 +290,8 @@ The `ms-airsim` container's `entrypoint.sh` (in `simulation/ms-airsim/docker/`) ### Isaac Sim (Pegasus) +With a fleet, [`fleet_spawn.py`](../../../simulation/isaac-sim/launch_scripts/fleet_spawn.py) is selected automatically: spawn positions come from each robot's `spawn:`, the scene from `sim.scene`, and sensor toggles from the vehicle manifests (any `lidar*` sensor enables the RTX lidar subgraph — the per-vehicle `ENABLE_LIDAR` equivalent). The legacy path: + [`simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_launch_script.py`](../../../simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_launch_script.py) reads `NUM_ROBOTS` and calls `spawn_drone(i)` in a loop. Each drone is created with `robot_name=f"robot_{index}"`, `vehicle_id=index`, `domain_id=index`, and an X offset for spacing: ```python @@ -280,8 +326,13 @@ CLI passthrough: ```bash airstack test -m takeoff_hover_land --sim msairsim --num-robots 1,3 -v +airstack test -m liveliness --sim isaacsim --fleet sim_three_mixed -v # fleet-first ``` +With `--fleet`, the fixture sets `FLEET_CONFIG_FILE`, derives `NUM_ROBOTS` +from the fleet, and pins `ISAAC_SIM_SCRIPT_NAME=fleet_spawn.py` on Isaac; +`env["fleet"]` carries the fleet name for tests that need it. + ## Common Pitfalls ### 1. Hardcoding the robot name in topics @@ -394,11 +445,11 @@ Before merging a change that touches anything robot-namespaced: - [ ] Every cross-module topic uses `$(env ROBOT_NAME)` (in launch files) or a relative name remapped at launch time (in node code) - [ ] Every YAML config file that references `$(env ...)` is loaded with `allow_substs="true"` - [ ] TF frames in node code are either relative (`base_link`, `odom`) or built from `os.environ["ROBOT_NAME"]` -- [ ] If you added a new module to a layer bringup, you tested it with `NUM_ROBOTS=2` and confirmed both robots' namespaces look identical under `ros2 node list` +- [ ] If you added a new module to a layer bringup, you tested it with `--robots 2` and confirmed both robots' namespaces look identical under `ros2 node list` - [ ] If you added a sim launch script, it reads `NUM_ROBOTS` and spawns vehicles named `robot_1`, `robot_2`, … with matching `vehicle_id` / `domain_id` - [ ] If you added a system test that addresses a robot, it loops over `range(1, num_robots + 1)` and uses `domain_id=n` in `ros2_exec(...)` -- [ ] DDS router allowlists in `onboard_all/config/dds_router.yaml` (or the split equivalent) include any new cross-domain topic your module exposes — otherwise it will not appear on the GCS -- [ ] Verified end-to-end: `NUM_ROBOTS=3 airstack up`, then `docker exec airstack-robot-desktop-2 bash -c 'ros2 topic list | grep robot_2'` shows the same topics that `airstack-robot-desktop-1` shows under `robot_1` +- [ ] DDS router allowlists in `autonomy_bringup/config/dds_router.yaml` (or the split stack's `bridge.yaml`) include any new cross-domain topic your module exposes — otherwise it will not appear on the GCS +- [ ] Verified end-to-end: `airstack up --sim isaac --robots 3`, then `docker exec airstack-robot-desktop-2 bash -c 'ros2 topic list | grep robot_2'` shows the same topics that `airstack-robot-desktop-1` shows under `robot_1` ## Verification Commands @@ -427,11 +478,14 @@ docker exec -e ROS_DOMAIN_ID=1 airstack-robot-desktop-1 bash -c \ ## References -- [`docs/robot/docker/robot_identity.md`](../../../docs/robot/docker/robot_identity.md) — canonical reference for the resolution mechanism +- [`docs/development/fleets.md`](../../../docs/development/fleets.md) — fleets: hierarchy, file tour, split placement, migration table (fleet-first path) +- [`config/fleets/`](../../../config/fleets/) — `sim_one_default.yaml` (parity with legacy), `sim_three_mixed.yaml` (heterogeneous + split) +- [`tools/fleet/resolve_fleet.py`](../../../tools/fleet/resolve_fleet.py) — fleet-entry resolver (`--table` to inspect, `--validate` to check) +- [`docs/robot/docker/robot_identity.md`](../../../docs/robot/docker/robot_identity.md) — canonical reference for the legacy resolution mechanism - [`docs/robot/autonomy_modes.md`](../../../docs/robot/autonomy_modes.md) — profile matrix (`desktop`, `desktop_split`, `voxl`, `l4t`, `offboard`) - [`robot/docker/robot_name_map/`](../../../robot/docker/robot_name_map/) — mapping YAMLs and `resolve_robot_name.py` - [`robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml`](../../../robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml) — top-level `push_ros_namespace` -- [`robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml`](../../../robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml) — cross-domain allowlist pattern +- [`robot/ros_ws/src/autonomy_bringup/config/dds_router.yaml`](../../../robot/ros_ws/src/autonomy_bringup/config/dds_router.yaml) — cross-domain allowlist pattern - [`simulation/ms-airsim/config/generate_settings.py`](../../../simulation/ms-airsim/config/generate_settings.py) and [`settings.json.j2`](../../../simulation/ms-airsim/config/settings.json.j2) - [`simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_launch_script.py`](../../../simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_launch_script.py) - [`tests/conftest.py`](../../../tests/conftest.py) — `airstack_env` fixture and `--num-robots` parametrization diff --git a/.agents/skills/create-module/SKILL.md b/.agents/skills/create-module/SKILL.md new file mode 100644 index 000000000..ae3d4901e --- /dev/null +++ b/.agents/skills/create-module/SKILL.md @@ -0,0 +1,148 @@ +--- +name: create-module +description: Create a new AirStack module repository by hand — author the thin module.yaml manifest, lay out colcon packages with canonical-default launch args (never remaps), add test_stack/ and CI, and validate with tools/validate_module.py. Use when packaging a capability (planner, estimator, sim extension, vehicle data) as a standalone module repo per RFC #379. +license: BSD-3-Clause-Clear +metadata: + author: AirLab CMU + repository: AirStack +--- + +# Skill: Create an AirStack Module Repo + +## When to Use + +Creating a **new AirStack module repo** — a thin, standalone repository that +distributes an optional capability (a planner, state estimator, world model, Isaac +Sim extension, vehicle definition, …) without forking AirStack. Defined by +[RFC #379](https://github.com/castacks/AirStack/discussions/379) §2 (manifest) and +[RFC #385](https://github.com/castacks/AirStack/discussions/385) §2 (repo anatomy). + +Not this skill: adding a trunk-resident package (use `add-ros2-package`), or wiring +an existing module into a bringup (use `integrate-module-into-layer`). + +> **Status:** `airstack module create` scaffolding arrives in a later phase. Until +> then this skill documents the **by-hand procedure**; only the manifest schema and +> validator (`tools/validate_module.py`) exist today. + +## Module Repo Anatomy (RFC #385 §2) + +A module is a *thin* repo: ordinary colcon packages, a small manifest carrying deps +and identity only, and a `test_stack/` that is both its CI target and its living +install documentation. + +``` +my-module/ +├── module.yaml # the thin manifest — deps, identity, tests; NO wiring +├── my_module/ # ordinary colcon package(s) +│ ├── package.xml # rosdep keys live here (dep tier 1) +│ ├── src/ config/ test/ # co-located unit tests, standard colcon convention +│ └── launch/ +│ └── my_module.launch.xml # topic args DEFAULT to canonical names; never remaps +├── test_stack/ # reference-stack copy with this module wired in +│ ├── modules.repos # PINNED to tags/commits — never branches +│ ├── launch/stack.launch.xml # the ONE place this module is wired +│ ├── docker-compose.yaml +│ ├── wiring.md # generated in CI from the running graph +│ └── README.md +├── Dockerfile.module # optional dep tier 2 — against ARG BASE_IMAGE only +├── .github/workflows/ +│ └── ci.yml # ~10 lines: uses castacks/AirStack/.github/workflows/ +│ # module-system-tests.yml@vX.Y.Z (later phase) +└── README.md +``` + +## The Manifest (`module.yaml`) + +Schema + full field reference: [`common/module_schema/`](../../../common/module_schema/README.md). +Summary: + +- **Required:** `name` (snake_case), `description`, `maintainer` (email), + `license`, `type` (`isaac_extension` | `ros_package` | `data` | `platform`), + `airstack_compat` (semver range vs trunk `.env` `VERSION`, e.g. + `">=0.19.0 <0.21.0"` — never a branch name), `targets` + (non-empty: `robot` | `gcs` | `isaac-sim` | `ms-airsim`). +- **Optional:** `deps` `{apt, pip}`, `dockerfile` (path ending + `Dockerfile.module`), `overlay_image`, `compose`, `assets` + (`[{url (https), sha256, dest}]` — no Git LFS), `docs`, `foxglove`, `hooks` + (`host_setup`: idempotent, no sudo, writes only inside the module checkout), + `tests` (`packages` + `marks` from the known mark set). +- **Deliberately absent: wiring.** No slot, role, or topic metadata — unknown keys + are rejected. A module's interface is its launch file's declared args + (`ros2 launch --show-args`). + +Minimal working example: [`tests/fixtures/modules/hello_module/module.yaml`](../../../tests/fixtures/modules/hello_module/module.yaml). + +## The Canonical-Defaults Launch Rule + +The one interface convention that does the plug-and-play work (RFC #379 §2, §4): + +- Expose **every topic endpoint as a launch arg**, and **default it to the + canonical name** from the interface conventions spec (today: + `docs/robot/autonomy/integration_checklist.md`). +- **NEVER put `` in a module launch file**, and never hardcode a topic in + node code. All cross-module remaps live in the *stack's* entry launch file — + the single-locus wiring rule. + +```xml + + + + + + + + +``` + +(The `` *inside* the node block binds the node's internal name to the +declared arg — that is the mechanism, not a cross-module rewire. What is forbidden +is remapping other modules' topics or overriding canonical names in module launch +files: in a conventional stack, including the module must require **zero** remaps, +so only deviations appear in stack files.) + +## Steps (by hand, until `airstack module create` lands) + +1. Create the repo with the anatomy above; write `module.yaml` first. +2. Write the package(s) following `add-ros2-package` conventions (package.xml + format 3, co-located `test/`). +3. Author the module launch file under the canonical-defaults rule. +4. Validate: + + ```bash + python3 tools/validate_module.py path/to/my-module + ``` + + Exit 0 and `{"valid": true, "errors": []}` on stdout is the gate. Dir mode also + checks that declared `dockerfile`/`compose`/`hooks`/`docs` paths exist and warns + when `tests.packages` entries match no directory. +5. Copy a trunk reference stack into `test_stack/` and wire the module in its + `stack.launch.xml` (reference stacks land in a later phase; until then model it + on the bringup you tested against). +6. Add `ci.yml` calling the reusable `module-system-tests.yml` workflow (later + phase) with pinned `airstack_ref` and the marks for your module category + (RFC #379 §5: global planner → `waypoint_flight`,`autonomy`; state estimator → + `liveliness`,`sensors`,`takeoff_hover_land`; world model/perception → + `liveliness`,`sensors`; sim extension → `liveliness`). +7. **Register the module in the marketplace — this is the step that gets + missed.** It is TWO merges: a PR to `castacks/airstack-modules-index` + (`modules/.yaml`, plus `stacks/.yaml` for a consuming + reference stack) **and** the trunk PR (same entries copied into + `tests/meta/fixtures/modules_index/`, committed `docs/modules/` pages + regenerated with `tools/gen_docs_catalog.py`, mkdocs nav updated). The + docs deploy regenerates the catalog from the LIVE registry, so the trunk + PR alone leaves the published catalog without your module — merge the + registry PR first. The trunk PR is automated: after the registry PR + merges, dispatch the `sync-modules-index` workflow (also runs daily) and + merge the PR it opens; the develop docs deploy files a + `docs-catalog-drift` issue if the two ever disagree. Full checklist + + caveats: the [extract-module](../extract-module/SKILL.md) registration + step. + +## References + +- Manifest schema + validator: [`common/module_schema/`](../../../common/module_schema/README.md) +- Fixture module: [`tests/fixtures/modules/hello_module/`](../../../tests/fixtures/modules/hello_module/) +- Contract tests: [`tests/meta/test_module_manifest_contract.py`](../../../tests/meta/test_module_manifest_contract.py) +- RFC #379 (design), RFC #385 (directory atlas) +- Related skills: [add-ros2-package](../add-ros2-package), [write-launch-file](../write-launch-file), [run-system-tests](../run-system-tests) diff --git a/.agents/skills/create-stack/SKILL.md b/.agents/skills/create-stack/SKILL.md new file mode 100644 index 000000000..b3e3ce84d --- /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: BSD-3-Clause-Clear +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/debug-module/SKILL.md b/.agents/skills/debug-module/SKILL.md index 5d4dff05a..468cb6106 100644 --- a/.agents/skills/debug-module/SKILL.md +++ b/.agents/skills/debug-module/SKILL.md @@ -1,7 +1,7 @@ --- name: debug-module description: Systematically debug ROS 2 modules with autonomous diagnostic strategies. Use when a module is not working as expected. Covers node status, topic connections, data flow analysis, parameter checking, and performance profiling. -license: Apache-2.0 +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -497,8 +497,8 @@ Solution: Added scipy dependency - [Debugging with GDB](https://docs.ros.org/en/jazzy/Tutorials/Debugging/Debugging-CPP.html) - **AirStack:** - - [Integration Checklist](../docs/robot/autonomy/integration_checklist.md) - - [System Architecture](../docs/robot/autonomy/system_architecture.md) + - [Integration Checklist](../../../docs/robot/autonomy/integration_checklist.md) + - [System Architecture](../../../docs/robot/autonomy/system_architecture.md) - **Related Skills:** - [add-ros2-package](../add-ros2-package) diff --git a/.agents/skills/docker-build-profiles/SKILL.md b/.agents/skills/docker-build-profiles/SKILL.md index 3cff9e38c..1a68a6775 100644 --- a/.agents/skills/docker-build-profiles/SKILL.md +++ b/.agents/skills/docker-build-profiles/SKILL.md @@ -1,24 +1,74 @@ -# docker-build-profiles SKILL +--- +name: docker-build-profiles +description: Build-time validation and guidance for AirStack Docker compose profiles and build args — adding a robot profile, quoting numeric-like YAML args, the L4T/Jetson build chain, and how module-owned dependencies enter images via module layers (airstack module lock --build) now that per-capability SKIP_* build args are gone. Use when adding/updating a compose profile or debugging a Dockerfile.robot build. +license: BSD-3-Clause-Clear +metadata: + author: AirLab CMU + repository: AirStack +--- + +# Skill: Docker Build Profiles and Build Args Summary -- Purpose: Provide actionable build-time validation snippets and YAML guidance for AirStack Docker builds. Designed for Claude/GPT-style agents that automate repo changes, CI checks, or PR review suggestions. +- Purpose: Provide actionable build-time validation snippets and YAML guidance for AirStack Docker builds. Designed for agents that automate repo changes, CI checks, or PR review suggestions. - Location: .agents/skills/docker-build-profiles/SKILL.md When to use -- When adding or updating a `docker-compose` profile that passes `PYTHON_VERSION`, `ROS_DISTRO`, or other numeric-like build args. -- When an automated agent needs to verify a new profile will produce a correct `PYTHONPATH` and avoid YAML float-parsing bugs. +- When adding or updating a `docker-compose` profile / service that builds from `robot/docker/Dockerfile.robot`. +- When a build arg looks numeric (`PYTHON_VERSION`, versions like `36.4.0`) and YAML float parsing could corrupt it. +- When deciding where a new dependency belongs: the trunk Dockerfile vs a **module layer**. + +## Current profile landscape (robot/docker/docker-compose.yaml) + +Robot services select by compose profile: `desktop` (robot-desktop, the default dev +target), `desktop_split` / `offboard` (split-stack pairs), `simple`, `voxl` / +`voxl_onboard`, `l4t` (Jetson: `robot-l4t` + `robot-l4t-stack-base` + `zed-l4t`), +`l4t_lite`, and `test`. Build args passed by compose today are `BASE_IMAGE`, +`ROS_DISTRO` (jazzy), `REAL_ROBOT`, and (l4t) the stack-base image ref. +`PYTHON_VERSION` is an `ARG` **defaulted inside `Dockerfile.robot`** (currently +`3.12`) — compose does not pass it; only quote-and-pass it if a new profile genuinely +needs a different Python. + +## Module-owned dependencies: NOT build args anymore + +The per-capability skip args are **gone**: `SKIP_MACVO` and `SKIP_TENSORRT` no longer +exist in `Dockerfile.robot` or any compose file. MAC-VO (and with it the TensorRT apt +blocks, torch/onnx wheels, and model weights) was extracted to the external +`asm_macvo` module, whose own `Dockerfile.module` owns those deps. + +Module deps enter images via **module layers** (RFC #379 §6, `docs/development/modules.md`): + +- **Tier 1** — `module.yaml` `deps: {apt: [...], pip: [...]}` → one generated `RUN` + layer per module in `.airstack/generated/layers//Dockerfile.composed`. +- **Tier 2** — the module's `Dockerfile.module`, built with + `--build-arg BASE_IMAGE=` (always `ARG BASE_IMAGE`, never a + fixed base). +- **Tier 3** — a prebuilt `overlay_image` ref. + +`airstack module sync` (and `airstack module lock`) is **plan-only**: it writes +`.airstack/generated/layer_plan.json`, the composed Dockerfile, and `modules.lock`, +and never calls docker. To actually build the chain: + +```bash +airstack module lock --build # = tools/compose_module_layers.py --build +``` + +Rule of thumb: a dependency used by exactly one optional capability belongs in that +capability's module (tier 1 or 2), not in `Dockerfile.robot`. Trunk Dockerfiles thin +out as deps migrate into the modules that own them. Actions the agent can perform -1. Validate `docker-compose.yaml` args are quoted when numeric-like (e.g. `PYTHON_VERSION: "3.10"`). +1. Validate `docker-compose.yaml` args are quoted when numeric-like (e.g. `PYTHON_VERSION: "3.12"`). 2. Insert a build-time validation `RUN` into `robot/docker/Dockerfile.robot` to fail early when the ROS Python path does not exist. 3. Add or update a short test in documentation showing how to build the `builder` stage and check `ament_package` import. 4. Suggest `network: host` under `build:` for L4T/Jetson profiles only when necessary (kernel iptables workarounds). +5. Route new capability-specific deps to a module layer instead of a trunk build arg (see above). Snippets (copyable) - YAML-check rule (agent pseudocode): - - If a `build.args` key named `PYTHON_VERSION` exists and the value matches `/^\d+\.\d+$/`, ensure it's a quoted string in YAML; otherwise update to `""`. + - If a `build.args` key named `PYTHON_VERSION` (or any version-shaped value) exists and the value matches `/^\d+\.\d+$/`, ensure it's a quoted string in YAML; otherwise update to `""`. - Dockerfile validation snippet (recommended, place before using `PYTHON_VERSION` to compose `PYTHONPATH`): @@ -33,28 +83,30 @@ RUN test -d /opt/ros/${ROS_DISTRO}/lib/python${PYTHON_VERSION} \ DOCKER_BUILDKIT=1 docker build --target builder \ -f robot/docker/Dockerfile.robot \ --build-arg BASE_IMAGE= \ - --build-arg ROS_DISTRO= \ - --build-arg PYTHON_VERSION="" \ + --build-arg ROS_DISTRO=jazzy \ + --build-arg PYTHON_VERSION="3.12" \ -t airstack-builder-test:local robot/docker -docker run --rm -it airstack-builder-test:local bash -c "python3 -c 'import ament_package; print(ament_package.__file__)'" +docker run --rm airstack-builder-test:local bash -c "python3 -c 'import ament_package; print(ament_package.__file__)'" ``` Guidance for agents when editing the repo - Prefer making minimal, reversible changes: add the `RUN test -d ...` check early in the Dockerfile and gate it with informative message text. - When updating `docker-compose.yaml`, only quote the numeric-like values; do not change unrelated fields. +- Every service with a `build:` section needs **both** `cache_from` entries (the versioned tag and the floating `${CACHE_TAG:-cache}` tag) or its CI builds will always be cold (see AGENTS.md "Docker layer cache"). - If creating PRs, include a short note in the PR description instructing maintainers to run the builder-stage sanity build on both an amd64 desktop profile and an arm64 L4T profile. Troubleshooting notes - YAML quirk: unquoted `3.10` may be parsed as float `3.1` — this changes path strings and breaks imports (e.g., `python3.1` instead of `python3.10`). - Jetson/L4T builds may require `network: host` during the build to avoid kernel iptables/raw table missing-module errors. -- Jetson **`robot-l4t`** builds from **`robot-l4t-stack-base`** (`robot/docker/Dockerfile.l4t-stack-base`), not raw dustynv, so **`Dockerfile.robot` stays Ubuntu-shaped.** `airstack image-build --profile l4t robot-l4t` triggers **`robot-l4t-stack-base`** first (`airstack.sh`); bare `compose build robot-l4t` can still parallelize badly, so list stack-base explicitly if not using AirStack CLI. +- Jetson **`robot-l4t`** builds from **`robot-l4t-stack-base`** (`robot/docker/Dockerfile.l4t-stack-base`), not raw dustynv, so **`Dockerfile.robot` stays Ubuntu-shaped.** `airstack images build --profile l4t robot-l4t` triggers **`robot-l4t-stack-base`** first (`airstack.sh`); bare `compose build robot-l4t` can still parallelize badly, so list stack-base explicitly if not using AirStack CLI. - **dustynv `/ros_entrypoint.sh` shadows the apt Jazzy runtime (mavros symbol-lookup crash).** The dustynv base sources a prebuilt *source* ROS at `$ROS_ROOT/install` from PID 1, prepending its older libs (e.g. `fastcdr` 2.2.5) ahead of the apt Jazzy (2.2.7) that `Dockerfile.robot` layers on top — apt-built nodes like mavros then die with symbol-lookup errors under tmux autolaunch. `Dockerfile.l4t-stack-base` neutralizes it by overwriting `/ros_entrypoint.sh` with a `exec "$@"` passthrough; shells get ROS from `/opt/ros/jazzy/setup.bash` via `.bashrc`. If a Jetson node suddenly can't resolve symbols after a base-image bump, check whether the entrypoint passthrough is still in place. - **ZED SDK version is pinned across `zed/Dockerfile.zed-l4t`** — the `ZED_SDK_URL` (e.g. `.../zedsdk/5.2/...`) and the ROS dep args (`ZED_MSGS_VERSION`, `POINTCLOUD_TRANSPORT*_VERSION`, `BACKWARD_ROS_VERSION`) must move together; a mismatched `zed_msgs` vs SDK breaks the driver build. Bumping the SDK is camera-firmware-coupled, so confirm the target camera runs that SDK line before merging. -- **`pytest` is pinned to `7.4.*` in `Dockerfile.robot` — do not remove or bump it.** The builder-stage `pip3 install` pulls `pytest` transitively into `/usr/local` (copied into the runtime image), which shadows Jazzy's apt `python3-pytest` 7.4. `pytest` 8 removed the `path` argument from the `pytest_pycollect_makemodule` hook, which apt's `launch_pytest` plugin still declares — so an unpinned (>=8) pytest aborts **every** pytest run in the container at plugin registration. That breaks `colcon test` for `ament_python` packages (e.g. `lidar_point_cloud_filter` in `test_colcon_test_robot`), while `ament_cmake` gtest packages are unaffected. Keeping the pin at Jazzy's version keeps `launch_testing` / `launch_pytest` usable for launch-based tests. The `tests/docker` runner is a separate interpreter and is free to use a newer pytest. +- **`pytest` is pinned in `Dockerfile.robot` — do not remove or bump past 8.0.** The builder stage installs `pytest==7.4.*` and a later `RUN` constrains `pytest>=7.4,<8.1`: ROS Jazzy's `launch_testing` still implements `pytest_pycollect_makemodule(path=...)`, which pluggy rejects after pytest 8.1 removed the `py.path` hook argument — an unpinned pytest aborts **every** pytest run in the container at plugin registration, breaking `colcon test` for `ament_python` packages while `ament_cmake` gtest packages are unaffected. The `tests/docker` runner is a separate interpreter and is free to use a newer pytest. +- **Robot build suddenly missing a MAC-VO / TensorRT dep?** Those deps left trunk with the `asm_macvo` extraction. Add the module (`airstack module add https://github.com/castacks/asm_macvo --version `), then `airstack module lock --build`. Examples of agent prompts -- "Check `robot/docker/docker-compose.yaml` for `PYTHON_VERSION` entries and quote any unquoted numeric values; open a PR with the fixes and include a test log from a builder-stage build." +- "Check `robot/docker/docker-compose.yaml` for numeric-like build-arg values and quote any unquoted ones; open a PR with the fixes and include a test log from a builder-stage build." - "Insert a build-time validation `RUN` in `robot/docker/Dockerfile.robot` that ensures `/opt/ros/${ROS_DISTRO}/lib/python${PYTHON_VERSION}` exists; push as a separate small commit." Notes @@ -65,7 +117,7 @@ SKILL vs human docs - Keep SKILLs low-level and exact: this file contains raw `docker` commands and copyable build-time snippets intended for agents and automation. - Keep human-facing docs (`docs/`) showing the `airstack` CLI equivalents and higher-level workflows. This reduces cognitive load for maintainers while preserving exact commands in SKILLs for automation and debugging. -- For the robot profile, human docs should prefer `airstack image-build --target builder --progress=plain ` when showing how to inspect build output. +- For the robot profile, human docs should prefer `airstack images build --target builder --progress=plain ` when showing how to inspect build output. Creating a new profile (step-by-step) @@ -74,12 +126,12 @@ This section shows the minimal, recommended steps an agent or maintainer should 1. Pick a sensible service name and base image - Choose a service name that clearly indicates the platform, e.g. `robot-desktop`, `robot-l4t`, or `robot-myboard`. - - Select an appropriate `BASE_IMAGE` (amd64 desktop base or `nvcr.io/nvidia/l4t-jetpack:...` for Jetson). + - Select an appropriate `BASE_IMAGE` (amd64 desktop base or an L4T/JetPack base for Jetson; Jetson goes through `robot-l4t-stack-base`). 2. Add the profile with quoted numeric args - Add a service block in `robot/docker/docker-compose.yaml` (or an override file) and set `build.args` for the profile. - - Always quote `PYTHON_VERSION` values (e.g. `"3.10"`) so YAML does not convert them to floats. + - Quote any numeric-like values (e.g. `PYTHON_VERSION: "3.12"` if you must override it) so YAML does not convert them to floats. Example snippet to add: @@ -89,11 +141,11 @@ This section shows the minimal, recommended steps an agent or maintainer should context: ./robot/docker dockerfile: ./Dockerfile.robot args: - BASE_IMAGE: nvcr.io/nvidia/l4t-jetpack:r36.4.0 - ROS_DISTRO: humble - PYTHON_VERSION: "3.10" + BASE_IMAGE: + ROS_DISTRO: jazzy 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 §6) # for L4T builds only when necessary # network: host ``` @@ -104,22 +156,11 @@ This section shows the minimal, recommended steps an agent or maintainer should 4. Run the builder-stage sanity build - - Run the builder-target build locally (or in CI) to verify the image picks up the correct Python/ROS paths and that `ament_package` imports: - - ```bash - DOCKER_BUILDKIT=1 docker build --target builder \ - -f robot/docker/Dockerfile.robot \ - --build-arg BASE_IMAGE=nvcr.io/nvidia/l4t-jetpack:r36.4.0 \ - --build-arg ROS_DISTRO=humble \ - --build-arg PYTHON_VERSION="3.10" \ - -t airstack-builder-test:local robot/docker - - docker run --rm airstack-builder-test:local python3 -c "import ament_package; print('ok', ament_package.__file__)" - ``` + - Run the builder-target build locally (or in CI) to verify the image picks up the correct Python/ROS paths and that `ament_package` imports (see the snippet above). 5. Smoke-run the full compose build (optional but recommended) - - Use `docker compose -f robot/docker/docker-compose.yaml build robot-myboard` to ensure compose passes the args correctly. + - Use `airstack images build robot-myboard` (or `docker compose -f robot/docker/docker-compose.yaml build robot-myboard`) to ensure compose passes the args correctly. 6. Prepare the PR with clear validation notes @@ -134,4 +175,4 @@ Agent implementation tips - When automating the change, produce a single commit that updates only the new service block and, if needed, a second commit that adds the `RUN` check to `Dockerfile.robot`. - If the target is Jetson/L4T, add `network: host` under `build:` only when prior builds show iptables/kernel errors; do not enable it by default. -- If you detect a pre-existing unquoted `PYTHON_VERSION` in the repo, prefer to update that entry in-place and include an explanatory commit message about YAML float parsing. +- If you detect a pre-existing unquoted numeric-like build arg in the repo, prefer to update that entry in-place and include an explanatory commit message about YAML float parsing. diff --git a/.agents/skills/extract-module/SKILL.md b/.agents/skills/extract-module/SKILL.md new file mode 100644 index 000000000..fa209d52d --- /dev/null +++ b/.agents/skills/extract-module/SKILL.md @@ -0,0 +1,217 @@ +--- +name: extract-module +description: Extract an in-tree AirStack capability into a standalone module repo — choose history extraction vs plain copy, author and validate module.yaml, build test_stack/ from a reference stack, handle submodules and host-setup hooks, write a TRUNK_REMOVAL.md checklist, sequence the trunk-removal PR against the module overlay, and wire module CI. Use when graduating a trunk package (or fork research) into an asm_* module per RFC #379. +license: BSD-3-Clause-Clear +metadata: + author: AirLab CMU + repository: AirStack +--- + +# Skill: Extract an In-Tree Capability into a Module Repo + +Distilled from three completed extractions: `asm_macvo` (heavy Docker deps + +submodule), `asm_dfm2_disturbances` (Isaac extension from a fork), and +`asm_optitrack` (proprietary SDK via hooks). This skill covers the **extraction +procedure**; for authoring the module repo itself (manifest fields, +canonical-defaults launch rule, repo anatomy) defer to +[create-module](../create-module/SKILL.md) — don't duplicate it here. + +> `airstack module extract` automation is **future work** (RFC #379 §11 names it +> as the graduation step for `module create --in-tree` research). Today the +> extraction is manual; this skill is the manual. + +## 0. Scope the extraction + +- [ ] List every trunk artifact the capability touches — not just the package: + Dockerfile blocks and build args, compose args, bringup launch wiring, + keepalive/foxglove rows, wiring snapshots, docs pages, mkdocs excludes, + `.devcontainer` launch entries, rviz layouts. (The macvo extraction touched + all of these.) This list becomes `TRUNK_REMOVAL.md` (§6). +- [ ] Record the exact trunk ref and `VERSION` you extract from — `module.yaml` + `airstack_compat` and the checklist's line references pin against it. +- [ ] If the source lives in a **fork**, run `airstack module doctor --drift` + there first: module-contained changes move with you; trunk edits are + extraction debt to upstream, carry, or turn into a convention. + +## 1. History: `git filter-repo` vs plain copy + +- **`git filter-repo`** when the capability lives in one or two clean trunk + paths, its history is worth keeping, and the code moves mostly as-is: + + ```bash + git clone --no-local /path/to/AirStack asm_ && cd asm_ + git filter-repo --path robot/ros_ws/src// \ + --path-rename robot/ros_ws/src//: + ``` + +- **Plain copy into a fresh repo** when sources are scattered (the dfm2 case: + files spread across multiple fork branches, some only in dangling commits), + contain unresolved conflict markers, or need heavy rewriting to trunk + conventions anyway. Cite the source commits in the README / a friction log + instead of carrying history. +- Either way, keep a **FRICTION_LOG.md** (or notebook entry) of every step that + needed manual invention — it is the requirements list for the future tooling. + +## 2. Author + validate the manifest + +- [ ] Follow [create-module](../create-module/SKILL.md) for `module.yaml`. Key + extraction-specific choices seen in practice: + - Heavy image deps → put **everything** in `Dockerfile.module` (tier 2) and + keep `deps: {apt: [], pip: []}` empty so the layer story is unambiguous + (asm_macvo). + - Large pinned files (model weights) → `assets:` with `url` + `sha256` + + `dest`, no Git LFS. + - `airstack_compat`: a real semver range against the trunk `VERSION` you + tested (e.g. `">=0.21.0-dev.18 <0.22.0"`), never a branch. +- [ ] Validate: + + ```bash + python3 tools/validate_module.py /path/to/asm_ # exit 0, {"valid": true} + airstack module add /path/to/asm_ # local-path add + sync + airstack module doctor # manifests + overlay integrity + ``` + +## 3. Host-side SDK installs → `hooks.host_setup` + +Proprietary or license-gated SDKs (asm_optitrack's NatNet SDK) never go in the +image or in git: + +- [ ] Ship a download script and declare it: + + ```yaml + hooks: + host_setup: /scripts/download-.sh + ``` + +- [ ] The hook contract: **idempotent, no sudo, writes only inside the module + checkout** (gitignore the landed files). `airstack module sync` runs it; + `--no-hooks` skips it. + +## 4. Git submodules + +If the trunk package embeds a submodule (macvo's MAC-VO network): + +- [ ] The **module repo** carries the submodule now; `airstack module sync` uses + `vcs import --recursive`, so it clones automatically. +- [ ] Put the trunk-side removal in the checklist: `git submodule deinit -f + ` **before** `git rm -r`, plus deleting the `.gitmodules` entry. + +## 5. `test_stack/` from the reference stack + +- [ ] Copy the closest trunk reference stack (`airstack stack new ` + or copy by hand) into the module's `test_stack/` and wire the module in its + `launch/stack.launch.xml` — the **one** place the module is wired + (single-locus rule; the module's own launch file keeps canonical defaults + and zero remaps). +- [ ] Pin the module in `test_stack/modules.repos`. Before the repo has a + remote/tag the pin is a **placeholder** — track updating it on first + push/tag as a precondition in the checklist. +- [ ] Smoke it end-to-end from a trunk checkout: + + ```bash + airstack module add /path/to/asm_ + airstack module lock --build # only if the module has a Dockerfile.module + airstack up --sim isaac --robots 1 --headless --play --wait + airstack test -m liveliness --sim isaacsim --num-robots 1 -v + ``` + +## 6. Write `TRUNK_REMOVAL.md` in the module repo + +The pattern that made the macvo extraction reviewable: a checklist **in the +module repo** enumerating every trunk file/block the trunk-removal PR must +delete, written for the orchestrator of that PR. Include: + +- [ ] The package `git rm` (+ submodule/.gitmodules steps). +- [ ] Dockerfile blocks and build args to delete, **with a pip/apt consumer + audit**: for every dep you remove, `git grep -Iil ` outside the module + proves nothing else in the image uses it. Mark shared deps (numpy, matplotlib) + **keep** explicitly, with reasoning. +- [ ] Compose build args, bringup launch gates/remaps (the wiring moves into + stacks/module launch files), keepalive/foxglove rows, wiring snapshots to + regenerate (`airstack test -m wiring --stack `), docs/skills/mkdocs + references. +- [ ] Verification: clean-cache image build with before/after size, a + `git grep -in ` residue check, the trunk test suite without the module, + and a module dogfood run (add → sync → lock --build → up → liveliness). + +## 7. Sequencing rules (the ones that bite) + +- **Never have the same package in trunk and the module overlay at once.** A + duplicate colcon package makes builds nondeterministic. Remove the trunk copy + in the same PR that consumers start pinning the module — or gate the overlay + (don't `module add` until the trunk-removal PR merges). The macvo order: + trunk-removal PR merges → module CI can go green → trunk's stack pins the + module tag. +- **Stale colcon cache:** removed packages linger in the container's `install/` + (and `build/`) until a clean rebuild — a "deleted" package that still launches + is cache, not magic. `airstack clean` (host) or remove `build/ install/ log/` + in the container, then `bws`. +- **Placeholder pins rot silently:** every `modules.repos` pin written before + the first push/tag must be updated and is a checklist precondition, not a + footnote. + +## 8. CI for the module repo + +- [ ] Add `.github/workflows/ci.yml` calling trunk's reusable workflow — see + [docs/development/module_ci.md](../../../docs/development/module_ci.md): + + ```yaml + jobs: + system-tests: + uses: castacks/AirStack/.github/workflows/module-system-tests.yml@v0.19.0 + with: + airstack_ref: v0.19.0 + marks: liveliness # per module category — see module_ci.md + sim: msairsim # the cheap bring-up + ``` + + Pin the workflow ref and `airstack_ref` **together**. First-party policy: the + workflow hard-fails outside the `castacks` org. +- [ ] Known gap (tracked): the reusable workflow runs `module add`/`sync` + (layer *plan* only) but not `airstack module lock --build`, so a + `Dockerfile.module` is not built in CI — verify tier-2 builds manually until + that lands. +- [ ] Register the module in the index repo + (`castacks/airstack-modules-index`) once CI is green. **Registration is + TWO merges, and the registry one is the one that gets missed:** + 1. **Registry repo:** PR adding `modules/.yaml` (+ `stacks/.yaml` + for a consuming reference stack) to `castacks/airstack-modules-index`; + `tools/validate_entry.py` / its CI must pass. **This PR must be MERGED, + not just opened.** + 2. **Trunk repo:** copy the same entries into + `tests/meta/fixtures/modules_index/`, regenerate the committed catalog + (`python3 tools/gen_docs_catalog.py --index tests/meta/fixtures/modules_index + --modules-dir `), and add the module page + stack README to + the `mkdocs.yml` nav. + + Why both: the docs deploy workflows regenerate `docs/modules/` against the + **live registry** at build time — the committed pages are only the fallback + for an *unreachable* registry. If the trunk PR merges while the registry PR + sits unmerged, the deploy silently drops the module from the published + catalog even though `docs/modules/index.md` in git looks right (this + happened with `mighty`, 2026-08-29). Merge the registry PR **before or + with** the trunk PR; if it lands late, re-run the deploy: + `gh workflow run deploy_docs_from_develop.yaml --repo castacks/AirStack --ref develop`. + + **Automation (2026-08-29):** the trunk half no longer needs to be + hand-built — merging the registry PR dispatches the `sync-modules-index` + workflow (registry-side `trigger-trunk-sync`; also daily sweep + manual + dispatch), which opens the trunk sync PR (fixture mirror + regenerated + pages + VERSION bump). The develop docs deploy independently raises a + `docs-catalog-drift` issue whenever the committed catalog and the live + registry disagree, so a missed sync can no longer stay silent. Both + automations authenticate with the `REGISTRY_SYNC_TOKEN` secret (same + fine-grained PAT in both repos); if it is missing/expired, the registry + trigger no-ops with a warning (daily sweep covers it) and the bot PR falls + back to the workflow token, whose PRs don't trigger CI — close and reopen + such a PR to run the checks. + +## References + +- [create-module](../create-module/SKILL.md) — manifest, anatomy, canonical-defaults rule +- [create-stack](../create-stack/SKILL.md) — stack folders and wiring.md +- [docs/development/modules.md](../../../docs/development/modules.md) — overlay, dep tiers, `module lock` +- [docs/development/module_ci.md](../../../docs/development/module_ci.md) — the reusable CI caller +- Worked examples: `asm_macvo/TRUNK_REMOVAL.md` (trunk-removal checklist), + `asm_dfm2_disturbances/FRICTION_LOG.md` (fork archaeology + port decisions), + `asm_optitrack` (hooks.host_setup) diff --git a/.agents/skills/integrate-module-into-layer/SKILL.md b/.agents/skills/integrate-module-into-layer/SKILL.md index 46166877b..c328b8705 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. -license: Apache-2.0 +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: BSD-3-Clause-Clear 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 GONE.** The legacy layer bringup launch +> files (`local/perception/sensors/global/behavior *.launch.xml`) were +> deleted along with the AUTONOMY_ROLE dispatch — there is nothing left to +> edit there. The launch lint (`tests/meta/test_launch_single_locus.py`) +> forbids ``s outside `stacks/*/launch/`, and the grandfather +> allowlist (`tests/meta/launch_lint_allowlist.txt`) only shrinks. Integrate +> into a stack. + +## 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/.agents/skills/optitrack-development/SKILL.md b/.agents/skills/optitrack-development/SKILL.md deleted file mode 100644 index d5e656c9a..000000000 --- a/.agents/skills/optitrack-development/SKILL.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -name: optitrack-development -description: Develop and integrate OptiTrack NatNet in AirStack — robot client (natnet_ros2), Isaac Sim Motive emulator, wire-protocol handshake, and libNatNet 4.4 unicast behavior. Use when working on natnet_ros2, optitrack.natnet.emulator, LAUNCH_NATNET, or NatNet UDP protocol compatibility. -license: Apache-2.0 -metadata: - author: AirLab CMU - repository: AirStack ---- - -# Skill: OptiTrack / NatNet Development - -## When to Use - -- Implementing or debugging the **Motive emulator** in Isaac Sim - (`simulation/isaac-sim/extensions/optitrack.natnet.emulator/`) -- Integrating or testing **`natnet_ros2`** on the robot stack -- Understanding **NatNet wire protocol** (connect, model def, frame streaming) -- Capturing what **`libNatNet.so`** actually sends on the network -- Enabling OptiTrack in sim: `LAUNCH_NATNET=true`, `natnet_config.yaml`, Docker IPs -- Sim testing with mocap: bring the stack up with `overrides/isaac-optitrack-simulation.env`, which starts Isaac + the emulator and switches PX4 EKF2 to external-vision fusion (GPS/baro/range aiding off, so mocap is the only position source) - -## Architecture in AirStack - -```mermaid -flowchart LR - subgraph sim ["Isaac Sim (172.31.0.200)"] - Emulator["optitrack.natnet.emulator\n(NatNet UDP server)"] - end - subgraph robot ["Robot container"] - Node["natnet_ros2_node"] - SDK["libNatNet.so client"] - Node --> SDK - end - SDK -->|"UDP 1510 (unicast: cmd + frames)"| Emulator - Node --> Topics["/{ROBOT_NAME}/perception/optitrack/..."] -``` - -| Component | Path | Role | -|-----------|------|------| -| Robot client | [`robot/ros_ws/src/perception/natnet_ros2/`](../../../robot/ros_ws/src/perception/natnet_ros2/) | ROS 2 node; uses **official NatNet SDK** (`NatNetClient::Connect`) | -| SDK install | `natnet_ros2/lib/libNatNet.so`, `include/natnet/` | Download via `airstack setup --natnet` (proprietary, not in git) | -| Emulator (WIP) | [`simulation/isaac-sim/extensions/optitrack.natnet.emulator/`](../../../simulation/isaac-sim/extensions/optitrack.natnet.emulator/) | Python NatNet **server** for sim / integration tests | -| Integration tests | [`tests/integration/natnet/README.md`](../../../tests/integration/natnet/README.md) | End-to-end UDP tests against real SDK parser (mark: `integration`) | - -**Enable on robot:** `LAUNCH_NATNET=true` in `.env` → [`perception.launch.xml`](../../../robot/ros_ws/src/perception/perception_bringup/launch/perception.launch.xml) includes `natnet_ros2.launch.py`. - -**Enable in sim:** set ``ISAAC_SIM_SCRIPT_NAME`` to a NatNet Pegasus launch script (no env gate in the script — NatNet always starts): - -| Script | Use | -|--------|-----| -| [`example_one_px4_pegasus_natnet_launch_script.py`](../../../simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_natnet_launch_script.py) | Single drone + static ``Target`` | -| [`example_multi_px4_pegasus_natnet_launch_script.py`](../../../simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_natnet_launch_script.py) | ``NUM_ROBOTS`` drones + shared ``Target`` (pair with 3-profile ``natnet_config.yaml``) | - -Helpers: [`isaac/scene_setup.py`](../../../simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/scene_setup.py) (`start_drone_natnet_server`, `author_static_target`). Drone body: single = ``Drone`` (id 1); multi = ``Drone`` (id ``i``); target = ``Target`` (id 100). In the single-drone script these are overridable via ``NATNET_BODY_NAME``/``NATNET_TARGET_NAME`` env vars; in the multi script they are constants. Either way they must match the ``natnet_config.yaml`` profile — change both together. The client filters frames by numeric id, so a mismatch is silent: it connects and never publishes. Baseline Pegasus scripts (no NatNet) remain ``example_one_px4_pegasus_launch_script.py`` / ``example_multi_px4_pegasus_launch_script.py``. - -**Default client config:** unicast, `server_ip` → Motive/emulator (use `172.31.0.200` for Isaac container), ports 1510/1511. The config is per-robot: each `robots[$ROBOT_NAME]` profile lists the bodies it tracks (each a `rigid_body_name` + `id` mapped to a relative `topic`, with `pose`/`pose_cov` toggles and per-body covariance) and an optional `vision_pose` block that drives the MAVROS bridge. See [`natnet_config.yaml`](../../../robot/ros_ws/src/perception/natnet_ros2/config/natnet_config.yaml). - -## NatNet: Two UDP Channels - -| Port (server default) | Channel | Direction | -|----------------------|---------|-----------| -| **1510** | Command | Client → server: `NAT_CONNECT`, `NAT_REQUEST_MODELDEF`, keepalives. Server → client: `NAT_SERVERINFO`, `NAT_MODELDEF`, `NAT_RESPONSE` | -| **1511** | Data | Server → client: `NAT_FRAMEOFDATA` (mocap frames). Multicast group `239.255.42.99` when using multicast. **The server must send frames from a socket bound to the data port** (source port == `data_port`); see below. | - -**Critical rules (verified against the real `libNatNet.so` 4.4 unicast + `NatNet_SetLogCallback`):** - -- Command **responses** go to the client's endpoint from `recvfrom` on the server command listener (`1510`), sent via the **command** socket. -- **Frames must be sent from the server's DATA socket** (bound to `data_port`, e.g. `1511`) so the datagram **source port == `data_port`**. libNatNet routes inbound unicast datagrams by source port: frames from the **command** port are treated as command traffic and **silently dropped** (no error, no callback). This was the single biggest gotcha. -- **libNatNet 4.4 unicast uses one client UDP socket** (one ephemeral local port for command send/recv and frame recv). The client receives frames there regardless of the server's source port — but libNatNet only **dispatches** them to the frame callback when they came from the server's data port. Do **not** assume `data_port = cmd_port + 1`. -- **Every `NAT_FRAMEOFDATA` must end with a 4-byte end-of-data tag** (after the frame `params`). libNatNet's unpacker reads it; without it the unpacked length mismatches `nDataBytes` and the SDK drops the whole frame. (The lenient Python `NatNetClient` does not require it — always validate against the C SDK.) -- The **269-byte `NAT_CONNECT` payload does not include** the client port; the port is learned from the datagram **source address** on `NAT_CONNECT`. -- Do **not** trust `/proc`/`ss` alone for the client port — extra bound sockets may appear that do not match wire traffic. **`NAT_CONNECT` source `(ip, port)` is ground truth.** -- Do **not** parse connect payloads with in-memory `sNatNetClientConnectParams` (contains pointers). Use on-wire layouts below. - -## libNatNet 4.4 `NAT_CONNECT` (verified 2025-06) - -Observed against `127.0.0.1:1510` with the same unicast params as [`natnet_client_adapter.cpp`](../../../robot/ros_ws/src/perception/natnet_ros2/src/natnet_client_adapter.cpp). - -### What the client sends - -| Field | Observed value | -|-------|----------------| -| Message | `NAT_CONNECT` (0), `nDataBytes = 269`, total datagram 273 bytes | -| Payload layout | `sSender` (264 B) + `sConnectionOptions` (5 B) | -| `sSender.szName` | `"NatNetLib"` | -| `sSender.Version` | `[4, 4, 0, 0]` | -| `sSender.NatNetVersion` | `[4, 4, 0, 0]` | -| `subscribedDataOnly` | `0` | -| `BitstreamVersion` | `[0, 0, 0, 0]` → client defers to server version | -| Trailing port bytes | **None** (exactly 269 bytes; not PacketClient's optional +4) | -| UDP source port | Ephemeral (e.g. `41449`) — **client command + data port (same socket)** | - -Example hex (payload only, after 4-byte header): - -``` -NatNetLib\0 ... (256-byte name field) -04 04 00 00 (Version) -04 04 00 00 (NatNetVersion) -00 (subscribedDataOnly) -00 00 00 00 (BitstreamVersion) -``` - -## libNatNet 4.4 unicast: single client socket (verified 2025-06) - -Confirmed with wire capture on server `:1510`/`:1511`, `strace` on a minimal `NatNetClient::Connect()` binary, and `/proc//net/udp` cross-checks against the same `libNatNet.so` used by `natnet_ros2`. - -### What we observed - -| Signal | Result | -|--------|--------| -| Wire capture on server `:1510` | All client packets (`NAT_CONNECT`, `NAT_KEEPALIVE`, `NAT_REQUEST_MODELDEF`) from **one** source port | -| Wire capture on server `:1511` | **No** inbound packets from the client | -| strace on minimal client | **One** `bind()`, **one** fd for all `sendto` → server `:1510` and `recvfrom` ← server `:1510` | -| `NAT_CONNECT` payload | **No** trailing client port bytes (269 B total) | - -### Emulator rule (unicast + `natnet_ros2`) - -For libNatNet 4.4 unicast, treat the client as **single-endpoint**: - -```text -On NAT_CONNECT → store client_endpoint = (ip, port) from recvfrom -NAT_SERVERINFO → sendto(command_socket, client_endpoint) # source port = command_port -NAT_MODELDEF → sendto(command_socket, client_endpoint) # source port = command_port -NAT_FRAMEOFDATA → sendto(data_socket, client_endpoint) # source port = data_port (REQUIRED) -NAT_KEEPALIVE → no reply (client -> server only) -``` - -The client always learns its endpoint from the **`NAT_CONNECT` source address** (the -client uses a single socket), so the **destination** of frames is that endpoint. The -**source** of frames, however, must be the server's data port — bind a dedicated -`data_socket` to `('', data_port)` and `sendto` frames from it. - -`ConnectionDataPort = 1511` in `NAT_SERVERINFO` is required (the SDK uses it to -recognize the data channel — i.e. which source port valid frames arrive from). - -### When two client ports may still apply - -- **Multicast** clients (separate multicast data listener on `239.255.42.99:1511`) -- **PacketClient-style** samples that open explicit command + data sockets (optional +4 port bytes in connect) -- Other NatNet client implementations — always verify with protocol capture before assuming a two-socket model - -Do **not** assume `data_port = cmd_port + 1` for any client without capture. - -### What the server must reply (for `Connect()` + `GetServerDescription()`) - -1. **`NAT_SERVERINFO` (1)** on the **command port** to the connect datagram source. -2. Payload: packed **`sSender_Server`** (279 B), **not** `sServerDescription`. libNatNet - parses the `NAT_SERVERINFO` payload as `sSender_Server`; sending the larger - `sServerDescription` makes it misread the version/host. Fields: - - `Common.szName = "Motive"` (256-byte field) - - `Common.Version = {3, 1, 0, 0}` (Motive app), `Common.NatNetVersion = {4, 4, 0, 0}` - - `HighResClockFrequency`, `DataPort = 1511`, `IsMulticast = 0` (unicast) - -Pre-built in emulator: [`NatNetServer._build_connect_response_payload()`](../../../simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_server.py). - -### After connect (required for `natnet_ros2` topics) - -| SDK call | Server must handle | -|----------|-------------------| -| `GetDataDescriptionList()` | `NAT_REQUEST_MODELDEF` → `NAT_MODELDEF` with rigid body name/ID (e.g. `"Drone"`) | -| Frame callback | Stream `NAT_FRAMEOFDATA` to **`NAT_CONNECT` source `(ip, port)`** from the server **data socket** (source port = `data_port`); end each frame with the 4-byte EOD tag; set `rb.params & 0x01` (tracking valid) | -| Unicast keepalive | Accept `NAT_KEEPALIVE` on command port; **send no reply** | - -Verified end-to-end against the real `libNatNet.so` with a C probe that registers -`SetFrameReceivedCallback` + `NatNet_SetLogCallback`: with the data-port source, -EOD tag, `sSender_Server` reply, and no keepalive reply, the probe reports -`Server: Motive 3.1.0.0 NatNet 4.4.0.0`, `data descriptions: 1`, and ~74 Hz callbacks. - -## Wire format reference (do not confuse) - -| Client type | Connect payload | -|-------------|-----------------| -| **`libNatNet` / `natnet_ros2`** | `sSender` + `sConnectionOptions` (269 B observed) | -| **PacketClient sample** | Same + optional 4 trailing bytes (often zero in sample) | -| **Python NatNetClient sample** | Legacy 270-byte `"Ping"` blob — **not** used by `natnet_ros2` | - -API struct `sNatNetClientConnectParams` ([`NatNetTypes.h`](../../../simulation/isaac-sim/extensions/optitrack.natnet.emulator/NatNetClientSDK/NatNetSDK/include/NatNetTypes.h)) is for `Connect()` in process memory only — **not** the on-wire layout. - -## Protocol capture (optional, for debugging) - -Not part of the repo. If you need to re-verify wire behavior or debug a new client/server pairing, build a **minimal out-of-band harness**: - -1. **Minimal C++ client** — tiny binary linking `libNatNet.so` from `natnet_ros2`; call `NatNetClient::Connect()` with the same params as [`natnet_client_adapter.cpp`](../../../robot/ros_ws/src/perception/natnet_ros2/src/natnet_client_adapter.cpp). Optional: `GetDataDescriptionList()`, frame callback, `--hold-seconds` sleep. -2. **Python UDP stub server** — bind `:1510` (and optionally `:1511`); reply to `NAT_CONNECT` with canned `NAT_SERVERINFO`, to `NAT_REQUEST_MODELDEF` with `NAT_MODELDEF`, to `NAT_KEEPALIVE` with ack; log every `(ip, port)` and message id. -3. **Connect capture** — run the client against the stub; hex-dump the first datagram; confirm 269-byte `sSender` + `sConnectionOptions` payload and ephemeral source port. -4. **Endpoint discovery** — during a full connect + model-def fetch: - - `tcpdump -i any udp and host ` or the stub's packet log - - `strace -e trace=bind,sendto,recvfrom` on the client binary - - `/proc//net/udp` or `ss -uapn` (treat **`NAT_CONNECT` source port** as ground truth if they disagree) -5. **Frame delivery check** — confirm the client's frame callback fires. Register both `SetFrameReceivedCallback` **and** `NatNet_SetLogCallback` (the log callback surfaces silent drops). Frames must be sent from the server **data socket** (source port = `data_port`) and end with the 4-byte EOD tag, or the SDK drops them with no callback. - -Use the SDK's `NatNetTypes.h` and `PacketClient.cpp` for on-wire layouts — not in-memory `sNatNetClientConnectParams`. - -## Emulator implementation checklist - -1. **Command listener** on `0.0.0.0:1510` -2. **`NAT_CONNECT`** → register `client_endpoint` from `recvfrom`; reply `NAT_SERVERINFO` -3. **`NAT_REQUEST_MODELDEF`** → reply `NAT_MODELDEF` (match `body_name` in config) -4. **Frame loop** → `NAT_FRAMEOFDATA` to `client_endpoint` **from the data socket** (source port = `data_port`); end each frame with the 4-byte EOD tag -5. **Isaac integration** → sample drone pose → `sFrameOfMocapData` → `enqueue_mocap_data()` -6. **Docker** → emulator on `172.31.0.200`; robot `server_ip` points there - -## Testing levels - -| Level | Approach | Validates | -|-------|----------|-----------| -| Unit (no network) | `test_natnet_logic.cpp`, `FakeNatNetClient` | Negotiation logic, topic names | -| Protocol capture | Minimal client + UDP stub (see above) | Wire-format `NAT_CONNECT`, client endpoint model | -| Integration | `tests/integration/natnet/` | Full SDK parser + `natnet_ros2_node` (mark: `integration`) | -| System (future) | `airstack test -m sensors` | Topic Hz on `/perception/optitrack/...` | - -```bash -# Unit tests (robot container) -docker exec airstack-robot-desktop-1 bash -c "sws && colcon test --packages-select natnet_ros2 --event-handlers console_direct+" -``` - -## References - -- OptiTrack NatNet docs: https://docs.optitrack.com/developer-tools/natnet-sdk/natnet-4.0 -- SDK samples (wire format): `NatNet_SDK_*/Samples/PacketClient/`, `PythonClient/` (legacy connect in Python only) -- Integration test: [`tests/integration/natnet/README.md`](../../../tests/integration/natnet/README.md) diff --git a/.agents/skills/run-system-tests/SKILL.md b/.agents/skills/run-system-tests/SKILL.md index c2260d29e..262b4019f 100644 --- a/.agents/skills/run-system-tests/SKILL.md +++ b/.agents/skills/run-system-tests/SKILL.md @@ -1,7 +1,7 @@ --- name: run-system-tests -description: Run, interpret, and extend AirStack's pytest system test suite (build_packages, build_docker, liveliness, sensors, takeoff_hover_land, autonomy), trigger runs via /pytest PR comments, and read run_meta.json/metrics.json reports. Use for invoking tests, distinguishing infrastructure failures from policy regressions, or adding a new system test. -license: Apache-2.0 +description: Run, interpret, and extend AirStack's pytest system test suite (build_packages, build_docker, liveliness, wiring, sensors, takeoff_hover_land, autonomy, waypoint_flight), trigger runs via /pytest PR comments, and read run_meta.json/metrics.json reports. Use for invoking tests, distinguishing infrastructure failures from policy regressions, or adding a new system test. +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -24,8 +24,9 @@ This skill is about the **test harness itself** — pytest marks, fixtures, the The suite lives at `tests/` (repo root) and is fully pytest-based. Configuration is in `tests/pytest.ini` and shared infrastructure in `tests/conftest.py`. -- **`tests/system/`** — Docker stack integration tests. Marks: `build_docker`, `build_packages`, `liveliness`, `sensors`, `takeoff_hover_land`, `autonomy`. +- **`tests/system/`** — Docker stack integration tests. Marks: `build_docker`, `build_packages`, `liveliness`, `wiring`, `sensors`, `takeoff_hover_land`, `autonomy`, `waypoint_flight`. - **`tests/integration/`** — Cross-component tests (`integration` mark): robot container + a host-side component, no sim/GPU. +- **`tests/meta/`** — fast **contract tests** (`unit` mark, no Docker) that pin the CLI/docs/stack contracts: module manifest schema, module overlay, Docker layer plan, launch-intent flags, stack layout, single-locus launch rule, bridge safety, fleet resolution, docs catalog, doctor, wiring snapshot format, metrics reporting, and test collection. They run with `airstack test -m unit` and in `unit-tests.yml`; if you change one of those mechanisms, expect the matching `tests/meta/test_*_contract.py` to fail first. - **Unit tests** (`@pytest.mark.unit`) — Hermetic. Source is **co-located** with each ROS 2 package in its own `test/` dir (ROS 2 / colcon convention). `tests/colcon_unit_test_packages.yaml` lists which packages have unit tests; `conftest.py` resolves each to its `test/` dir and collects the non-linter `test_*.py` under `--import-mode=importlib`. ### Unit tests vs system tests @@ -51,14 +52,33 @@ For details on the co-located layout and adding new unit tests, see the | File | Mark | What it tests | Hardware required | |------|------|---------------|-------------------| -| `tests/system/test_build_docker.py` | `build_docker` | `airstack image-build` for `robot-desktop`, `gcs`, `isaac-sim`, `ms-airsim`; records image size to `metrics.json` | Docker daemon | +| `tests/system/test_build_docker.py` | `build_docker` | `airstack images build` for `robot-desktop`, `gcs`, `isaac-sim`, `ms-airsim`; records image size to `metrics.json` | Docker daemon | | `tests/system/test_build_packages.py` | `build_packages` | `colcon build` (`bws`) inside the robot, GCS, and ms-airsim ROS workspaces — brought up with `AUTOLAUNCH=false` | Docker daemon | | `tests/system/test_liveliness.py` | `liveliness` | Stack bring-up: containers Running, `/clock` readiness, tmux panes, sentinel ROS 2 nodes, compute, infra-only `test_stable` | Docker daemon, NVIDIA GPU + `nvidia-container-toolkit`, sim license / Omniverse creds | | `tests/system/test_sensors.py` | `sensors` | Topic Hz (Isaac: batched on sim + robot; LiDAR `echo-once` + cloud sanity), RTF, `test_sensor_streams_stable` | Docker daemon, NVIDIA GPU + `nvidia-container-toolkit`, sim license / Omniverse creds | | `tests/system/test_takeoff_hover_land.py` | `takeoff_hover_land` | 4-phase flight chain per `(sim, num_robots, iteration, velocity)`: `test_px4_ready` → `test_takeoff` → `test_hover` → `test_landing`. Records altitude error, overshoot, hover stability, landing accuracy, odometry drift | Docker daemon, NVIDIA GPU, sim license | | `tests/system/test_fixed_trajectory.py` | `autonomy` | 4-phase flight chain per `(sim, num_robots, iteration, trajectory_type)`: `test_px4_ready` → `test_takeoff` → `test_fixed_trajectory` → `test_landing`. Records cross-track error, path RMSE, trajectory success/time for Circle/Figure8/Racetrack/Line | Docker daemon, NVIDIA GPU, sim license | - -The marks are declared in `tests/pytest.ini`. **Do not invent new marks ad-hoc** — register any new mark there or pytest will warn about unknown marks. +| `tests/system/test_waypoint_flight.py` | `waypoint_flight` | 4-phase flight chain (PX4 ready → takeoff → NavigateTask waypoint route → land); pass/fail judged on the odometry track by `tests/waypoint_checker.py` (in-order corridor arrival, final goal tolerance, per-waypoint timeout) | Docker daemon, NVIDIA GPU, sim license | +| `tests/system/test_wiring_snapshot.py` | `wiring` | Observed wiring snapshot of the running ROS graph, drift-checked against the committed golden / the stack's `wiring.md` (use with `--stack ` to regenerate a stack's wiring) | Docker daemon, NVIDIA GPU, sim license | + +### The full mark set (`tests/pytest.ini`) + +All eleven registered marks — **do not invent new marks ad-hoc**; register any new +mark in `tests/pytest.ini` or pytest will warn about unknown marks: + +| Mark | Meaning | +|------|---------| +| `unit` | Fast hermetic tests (no Docker stack; numpy / pure Python) — includes `tests/meta/` contract tests | +| `build_docker` | Docker image build tests | +| `build_packages` | Colcon workspace build tests | +| `integration` | Cross-component integration tests (robot container + a host-side component; no sim/GPU) | +| `liveliness` | Container and process health (Docker, tmux, sentinel ROS 2 nodes) | +| `wiring` | Observed wiring snapshot of the running ROS graph, drift-checked against a committed golden (`test_wiring_snapshot.py`) | +| `sensors` | Sim and robot sensor topic rates, LiDAR validation, sim RTF | +| `takeoff_hover_land` | End-to-end takeoff / hover / land action tests | +| `autonomy` | Fixed-pattern trajectory path-tracker benchmark (`test_fixed_trajectory.py`) | +| `waypoint_flight` | Ordered-waypoint navigation judged on the odometry track (`test_waypoint_flight.py`) | +| `optitrack` | OptiTrack NatNet end-to-end — registered for `asm_optitrack` module CI (tests live in the module repo) | ### Test ordering (set by `pytest_collection_modifyitems`) @@ -98,7 +118,7 @@ The `system-tests.yml` workflow's `Parse pytest args` step automatically prepend - `/pytest -m takeoff_hover_land` → effectively runs `-m "build_packages or takeoff_hover_land"` - `/pytest` (no marks) → pytest defaults (everything) - `/pytest -m build_docker` → unchanged (the build_docker tests rebuild from scratch anyway) -- `/pytest -m build_packages` → **pull-only** (retag `cache_*`, no `image-build`, no Isaac). Add `--no-image-build` on other marks to skip the bake. +- `/pytest -m build_packages` → **pull-only** (retag `cache_*`, no `images build`, no Isaac). Add `--no-image-build` on other marks to skip the bake. This guarantees that ROS 2 workspaces are built inside the containers before any launch/liveliness test tries to source them. If you intentionally want to skip `build_packages` (e.g. you trust the prebuilt images), include it explicitly: `-m "liveliness and not build_packages"` would work, but the simpler path is to run locally where the prepend logic doesn't apply. @@ -160,6 +180,8 @@ The `airstack_env` fixture is parametrized over `(sim, num_robots, iteration)` t |------|---------|---------|---------| | `--sim` | `isaacsim` | `airstack_env` | One env-tuple per sim (`msairsim` opt-in) | | `--num-robots` | `1,3` | `airstack_env` | Cross-product with sim | +| `--stack` | `None` (= `full_default`) | `airstack_env` | Stack folder under `stacks/` to launch (sets `AIRSTACK_STACK_DIR` for `airstack up`); the `wiring` test drift-checks against `stacks//wiring.md` | +| `--fleet` | `None` (legacy `--num-robots` behavior) | `airstack_env` | Fleet preset under `config/fleets/` (e.g. `sim_three_mixed`); sets `FLEET_CONFIG_FILE` and derives `NUM_ROBOTS` from the fleet's robot count, **overriding `--num-robots`** | | `--stress-iterations` | `1` | `airstack_env` | Up/down cycles per `(sim, num_robots)` | | `--stable-duration` | `120` | `system.test_liveliness::test_stable` and `system.test_sensors::test_sensor_streams_stable` | Total seconds polled | | `--stable-interval` | `10` | `system.test_liveliness::test_stable` and `system.test_sensors::test_sensor_streams_stable` | Seconds between polls | @@ -174,7 +196,7 @@ Total parametrize cardinality for sim tests = `len(sims) × len(num_robots) × s - For `liveliness` / `sensors` / `takeoff_hover_land`: NVIDIA driver + `nvidia-container-toolkit` - For `isaacsim`: `simulation/isaac-sim/docker/omni_pass.env` populated with Omniverse credentials (CI generates a `guest`/`guest` version automatically) - `airstack setup` already run so `airstack` is on `PATH` -- All required compose images present locally — `airstack_env` calls `missing_images()` and fails fast otherwise. Build them first via `airstack test -m build_docker` or `airstack image-build `. +- All required compose images present locally — `airstack_env` calls `missing_images()` and fails fast otherwise. Build them first via `airstack test -m build_docker` or `airstack images build `. ## Running Tests via PR Comment @@ -210,7 +232,7 @@ The workflow: 2. Opens an in-progress GitHub Check Run on the PR's head SHA so the run shows up in the **Checks** tab (issue_comment events otherwise associate runs with the default branch) 3. Runs pytest on a freshly-spawned ephemeral OSMO GPU pod (`runs-on: [self-hosted, airstack-ephemeral]`) 4. Uploads `tests/results/` as artifact `test-results--` (90-day retention) -5. The downstream `report` job runs `parse_metrics.py`, compares only a matching complete simulation baseline, posts the result, and finalizes the PR-head Check Run +5. The downstream `report` job selects the newest matching complete simulation baseline, runs `parse_metrics.py`, posts the advisory comparison, and finalizes the PR-head Check Run 6. Closes the Check Run with the final conclusion ### Why fork PRs are blocked @@ -262,7 +284,7 @@ Keys follow `test_node_id → metric_key → {value, unit, direction, ...}`. Tim # Single-run report — markdown table, exits 0 always python tests/parse_metrics.py --current tests/results/2025-04-21_14-30-00/ -# Diff mode — side-by-side, exits 1 on regression +# Comparison mode — side-by-side; numeric deltas are advisory python tests/parse_metrics.py \ --current tests/results/2025-04-21_14-30-00/ \ --baseline tests/results/2025-04-20_09-00-00/ \ @@ -276,7 +298,11 @@ The report has three sections per test module: - **Sim publishing rates** — pivoted Hz aggregates per topic (`mean`, `start_mean`, `end_mean`, `min`, `max`) from the `sensors` mark (sim + robot streams) - **Compute usage** — pivoted CPU/mem/GPU per container -Regressions exceeding `--threshold` (default 20%) are flagged `:red_circle:`; improvements beyond threshold get `:green_circle:`. CI fails only when both artifacts are complete and have the same simulation campaign fingerprint. +Changes exceeding `--threshold` (default 20%) are flagged `:red_circle:` or +`:green_circle:` for review. Numeric deltas never fail CI. Pytest assertions, +infrastructure/prerequisite failures, missing artifacts, and report-parser +errors remain blocking. The fingerprint includes normalized tests and all +behavior-changing campaign options. When local-debugging a CI regression, download both artifacts (`test-results--` from the PR run and from the base branch's most recent run), unzip them under `tests/results/`, and run `parse_metrics.py` locally to see the same table the bot posted. @@ -290,8 +316,9 @@ If your test... - Builds a Docker image → reuse `build_docker` - Builds a colcon workspace → reuse `build_packages` -- Verifies the running stack → `liveliness` (infra); sensor topic rates / LiDAR / RTF → `sensors` -- Drives the autonomy stack to fly → reuse `takeoff_hover_land` +- Verifies the running stack → `liveliness` (infra); sensor topic rates / LiDAR / RTF → `sensors`; ROS-graph topology drift → `wiring` +- Drives the autonomy stack to fly → reuse `takeoff_hover_land`; fixed-pattern path tracking → `autonomy`; waypoint navigation → `waypoint_flight` +- Pins a CLI/docs/stack contract with no Docker stack → a `unit`-marked contract test in `tests/meta/` - Doesn't fit any of these → **register a new mark in `tests/pytest.ini`** before using it. Update the table in `tests/README.md` and the AGENTS.md "System Test Suite" table at the same time. ### 2. File location and naming @@ -428,8 +455,9 @@ python tests/parse_metrics.py \ - `tests/conftest.py` — pytest hooks + the `airstack_env` / `robot_autonomy_stack` fixtures (re-exports the harness API) - `tests/harness/` — helpers split by concern: `session`, `discovery`, `commands`, `containers`, `metrics` (`MetricsRecorder`), `run_meta`, `test_ids`, `sim`, `collection` (ordering) +- `tests/meta/` — fast contract tests (`unit` mark) pinning CLI/docs/stack contracts - `tests/pytest.ini` — mark registration, log format -- `tests/parse_metrics.py` — markdown reporter, regression diff +- `tests/parse_metrics.py` — markdown reporter and advisory comparison - `tests/README.md` — user-facing docs (CLI options, output layout, CI/CD orchestrator) - `.github/workflows/system-tests.yml` — CI workflow with `/pytest` comment trigger - `.github/orchestrator/README.md` — ephemeral OSMO runner setup and worker-debug procedure diff --git a/.agents/skills/test-in-simulation/SKILL.md b/.agents/skills/test-in-simulation/SKILL.md index c5882a312..fedda2194 100644 --- a/.agents/skills/test-in-simulation/SKILL.md +++ b/.agents/skills/test-in-simulation/SKILL.md @@ -1,7 +1,7 @@ --- name: test-in-simulation description: Test modules in Isaac Sim simulation environment end-to-end. Use after implementing and integrating a module to verify functionality. Covers test scenarios, monitoring, recording, and analysis of simulation tests. -license: Apache-2.0 +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -144,11 +144,10 @@ Plan specific tests for your module: ### 7. Execute Test Scenario -#### Method 1: Manual Commands via Behavior Tree GUI +#### Method 1: Manual Commands via the GCS ```bash -# If behavior tree GUI is available -# Send commands through rqt_behavior_tree_command +# Send commands from the Foxglove Robot Tasks panel (see the GCS docs) # Typical test sequence: # 1. Arm drone @@ -551,8 +550,8 @@ fi - [Isaac Sim Testing](https://docs.omniverse.nvidia.com/isaacsim/latest/index.html) - **AirStack:** - - [System Architecture](../docs/robot/autonomy/system_architecture.md) - - [Integration Checklist](../docs/robot/autonomy/integration_checklist.md) + - [System Architecture](../../../docs/robot/autonomy/system_architecture.md) + - [Integration Checklist](../../../docs/robot/autonomy/integration_checklist.md) - **Related Skills:** - [debug-module](../debug-module) - Debugging issues found in testing diff --git a/.agents/skills/update-documentation/SKILL.md b/.agents/skills/update-documentation/SKILL.md index 595d18981..4370112c6 100644 --- a/.agents/skills/update-documentation/SKILL.md +++ b/.agents/skills/update-documentation/SKILL.md @@ -1,7 +1,7 @@ --- name: update-documentation description: Document new modules and update mkdocs navigation. Use after implementing any new feature or module. Covers README templates, mkdocs.yml updates, mermaid diagrams, and documentation standards for AirStack. -license: Apache-2.0 +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -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 @@ -196,11 +196,11 @@ ros2 launch your_package your_package.launch.xml \ The module is automatically launched when the autonomy stack starts: ```bash -# Full autonomy stack +# Full autonomy stack (autolaunches by default) airstack up robot-desktop -# Or with specific configuration -AUTOLAUNCH=true airstack up robot-desktop +# Or start idle and launch manually +airstack up robot-desktop --no-autolaunch ``` The module is integrated in: `_bringup/launch/.launch.xml` @@ -333,14 +333,12 @@ Specify license (typically Apache-2.0 for AirStack). - **Maintainer:** Name (email) - **Contributors:** List contributors -## Changelog - -### Version X.Y.Z (YYYY-MM-DD) -- Feature: Added new capability -- Fix: Resolved issue with X -- Change: Modified behavior of Y ``` +Do not add a per-README "Changelog" section — change records live in the +versioned Release Notes (`docs/release_notes/index.md`), and READMEs +describe only the current system. + **Reference template:** `../add-ros2-package/assets/package_template/README.md` ### 2. Update mkdocs.yml Navigation @@ -353,15 +351,12 @@ Find the appropriate section based on your module type: ```yaml nav: - - Robot: - - Autonomy Modules: + - Reference: + - Autonomy Packages: - Local: - - Planning: - # Existing modules - - Trajectory Library: - - robot/ros_ws/src/local/planners/trajectory_library/README.md - - DROAN Local Planner: - - robot/ros_ws/src/local/planners/droan_local_planner/README.md + # Existing modules + - Trajectory Library: robot/ros_ws/src/local/planners/trajectory_library/README.md + - DROAN Local Planner: robot/ros_ws/src/local/planners/droan_local_planner/README.md # Add your module HERE - Your Module Name: - robot/ros_ws/src/local/planners/your_package/README.md @@ -585,10 +580,10 @@ class YourClass: If your module introduces breaking changes or deprecations: -**File:** `CHANGELOG.md` (repository root) +**File:** `docs/release_notes/index.md` (the versioned Release Notes — the only place change-relative language belongs; feature docs describe only the current system) ```markdown -## [Unreleased] +## (Unreleased) ### Added - New module: your_package for @@ -726,7 +721,7 @@ feat: Add YourModule local planner - Implement algorithm based on XYZ paper - Add configuration and launch files -- Integrate into local_bringup +- Integrate into the stack entry file (stacks/full_default) - Add comprehensive README documentation - Update mkdocs navigation ``` diff --git a/.agents/skills/use-airstack-cli/SKILL.md b/.agents/skills/use-airstack-cli/SKILL.md index 564ca5324..a13a54f12 100644 --- a/.agents/skills/use-airstack-cli/SKILL.md +++ b/.agents/skills/use-airstack-cli/SKILL.md @@ -1,7 +1,7 @@ --- name: use-airstack-cli -description: Operate AirStack via the airstack CLI and run commands inside containers using the non-interactive docker exec pattern. Use whenever you need to start/stop services, build the workspace, source the workspace, run ros2 commands, or inspect logs in any AirStack container. -license: Apache-2.0 +description: Operate AirStack via the airstack CLI — up/down with launch-intent flags (--sim, --robots, --stack, --fleet, --headless, --no-autolaunch, --dry-run, --wait), module management (add/list/sync/remove/create/lock/doctor), stack and fleet commands, doctor and ready checks — and run commands inside containers using the non-interactive docker exec pattern. Use whenever you need to start/stop services, build the workspace, source the workspace, run ros2 commands, or inspect logs in any AirStack container. +license: BSD-3-Clause-Clear metadata: author: AirLab CMU repository: AirStack @@ -14,6 +14,8 @@ metadata: Use this skill any time you need to: - Start, stop, or inspect AirStack services (robot, isaac-sim, ms-airsim, gcs, docs) +- Launch a specific **stack** (`--stack`) or **fleet** (`--fleet`) +- Manage **modules** (`airstack module add|list|sync|remove|create|lock|doctor`) - Build or source the ROS 2 workspace inside a container - Run `ros2` commands (node list, topic echo/hz, param get, launch, etc.) - Tail or grep container logs @@ -21,23 +23,29 @@ Use this skill any time you need to: - Run the system test suite or build the docs site This skill is the foundation for almost every other AirStack workflow — `debug-module`, -`test-in-simulation`, `add-ros2-package`, and `integrate-module-into-layer` all rely on -the patterns described here. +`test-in-simulation`, `add-ros2-package`, `create-module`, `create-stack`, and +`integrate-module-into-layer` all rely on the patterns described here. ## Why `airstack`, Not Raw `docker compose` Always prefer `airstack ` over `docker compose ...` directly: -- Runs a **containerized** docker-compose pinned to a known version (consistent across - hosts and CI runners). -- Loads `.env`, propagates host env overrides, and applies the right include set from - the top-level `docker-compose.yaml` (isaac-sim / ms-airsim / robot / gcs / docs). -- Resolves Compose **profiles** (`desktop`, `isaac-sim`, `ms-airsim`, etc.) from - `COMPOSE_PROFILES` in `.env` automatically. +- Resolves **launch-intent flags** (`--sim`, `--robots`, `--stack`, `--fleet`, ...) + into exported env vars before compose sees anything, prints the **effective launch + config**, and saves it under `.airstack/runs//effective_config.env`. +- Runs **preflight validation** (exactly one sim profile, URDF↔sim match, + NUM_ROBOTS↔Isaac-script consistency, missing images, removed `AUTONOMY_ROLE`) + before starting anything. `AIRSTACK_SKIP_PREFLIGHT=1` downgrades errors to warnings. +- Automatically includes the **generated overlays**: module mounts + (`.airstack/generated/docker-compose.modules.yaml`, opt out with + `AIRSTACK_NO_MODULE_COMPOSE=1`) and heterogeneous-fleet services + (`.airstack/generated/docker-compose.fleet.yaml`) — for both `up` and `down`. +- Loads `.env` via `--env-file` and applies the include set from the top-level + `docker-compose.yaml`; resolves compose **profiles** from `COMPOSE_PROFILES`. - Gives partial container-name matching for `connect` and `logs`. -Drop to raw `docker` only for: `docker exec bash -c ""` (CLI does not -wrap exec), `docker logs ` for raw streams, and `docker ps` to discover +Drop to raw `docker` only for: `docker exec bash -c ""` (the CLI does +not wrap exec), `docker logs ` for raw streams, and `docker ps` to discover container names. ## Container Lifecycle @@ -48,86 +56,171 @@ container names. # Install Docker Engine + NVIDIA Container Toolkit (skip if already installed) airstack install -# Configure AirStack: add `airstack` to PATH, set up shell completion, etc. +# Configure AirStack: add `airstack` to your shell profile, git submodules, +# Isaac/Nucleus/git-hooks config, and (if modules.repos exists) a module sync airstack setup ``` -`airstack install` is only needed once per host (and only if Docker / nvidia-container-toolkit -are missing). `airstack setup` is needed once per shell user; rerun if you switch shells -(bash <-> zsh) or if `~/.airstack.conf` is missing. +### Starting services: `airstack up` and its flags -### Starting services +`airstack up` takes AirStack **intent flags** (consumed before compose sees the args) +plus optional compose service names and passthrough flags (`--build`, `--recreate`): -The most common entrypoints: +| Flag | What it does | +|------|--------------| +| `--sim isaac\|airsim` | Selects the simulator: swaps the compose profile (`isaac-sim` / `ms-airsim`) and the matching `URDF_FILE` | +| `--robots N` | Exports `NUM_ROBOTS=N`; on Isaac also keeps `ISAAC_SIM_SCRIPT_NAME` consistent (auto-selects the multi-drone script for N>1) | +| `--stack NAME[:ENTRY]` | Launches `stacks/NAME/launch/ENTRY.launch.xml` (default entry: `stack`). Stacks are the **only** launch dispatch; no `--stack` = `full_default`. `NAME:onboard` / `NAME:offboard` select split-stack entries | +| `--fleet NAME` | Launches `config/fleets/NAME.yaml`: validates it, exports `FLEET_CONFIG_FILE`, derives `NUM_ROBOTS`, selects the Isaac fleet spawner (`fleet_spawn.py`), and for heterogeneous fleets includes the generated per-robot services. Mutually exclusive with `--robots` | +| `--headless` | Sets `ISAAC_SIM_HEADLESS=true`, `MS_AIRSIM_HEADLESS=true`, `QT_QPA_PLATFORM=offscreen` | +| `--play` / `--no-play` | Whether the sim auto-presses Play on start (`PLAY_SIM_ON_START`) | +| `--no-autolaunch` | Containers start **idle** (no tmuxinator launch sequence) — the development mode | +| `--wait` | Block until the stack is flight-ready (runs `airstack ready` after up) | +| `--dry-run` | Print + validate the resolved launch config, start nothing (shadows compose's own `up --dry-run`) | ```bash -# Start the default profile from .env (typically: desktop + isaac-sim) -airstack up +airstack up # default profile from .env +airstack up --sim isaac --robots 2 # Isaac, two robots, consistent sim script +airstack up --sim airsim --headless --play --wait +airstack up --stack lite_default --sim isaac # launch a specific stack +airstack up --stack lite_offload_global:offboard # split-stack ground half +airstack up --fleet sim_three_mixed --sim isaac # fleet launch (RFC #380) +airstack up --dry-run --sim isaac # validate config, start nothing +airstack up robot-desktop # one service only +``` -# Start a specific service (matches docker-compose service name) -airstack up robot-desktop -airstack up isaac-sim -airstack up ms-airsim -airstack up gcs +**Env vars still work.** Flags only export env vars (shell env has highest compose +precedence), so `AUTOLAUNCH=false airstack up` or `NUM_ROBOTS=2 airstack up` behave +exactly like `--no-autolaunch` / `--robots 2`. Prefer the flags — they validate input +and keep derived settings (profiles, URDF, Isaac script) consistent. -# Start multiple services -airstack up isaac-sim robot-desktop -``` +**`AUTONOMY_ROLE` was removed.** A set `AUTONOMY_ROLE` (env, `--env-file`, or `.env`) +is a preflight **error**. Migration: `full` → `full_default` (the no-stack default), +`onboard` → `lite_default`, onboard/offboard split → `lite_offload_global:onboard` / +`:offboard`. -### CRITICAL: `AUTOLAUNCH=false` for development +### CRITICAL: `--no-autolaunch` for development -By default, `AUTOLAUNCH="true"` in `.env`, which means a freshly started robot or sim -container immediately runs its tmuxinator launch sequence. **For development and -debugging, you almost always want this disabled** so the container starts idle and you -can iterate on launch files, rebuild packages, and start/stop nodes by hand: +By default `AUTOLAUNCH="true"` in `.env`, so a freshly started robot or sim container +immediately runs its tmuxinator launch sequence. **For development and debugging you +almost always want it disabled** so the container starts idle and you can iterate on +launch files, rebuild packages, and start/stop nodes by hand: ```bash # Start the robot container without autolaunching the autonomy stack -AUTOLAUNCH=false airstack up robot-desktop +airstack up --no-autolaunch robot-desktop -# Combine with other overrides -AUTOLAUNCH=false NUM_ROBOTS=2 airstack up robot-desktop isaac-sim +# Combine with other flags +airstack up --no-autolaunch --robots 2 --sim isaac ``` -Any variable defined in `.env` can be overridden this way (the wrapper exports each -`.env` key into the compose container). Common ones for agents: - -| Variable | What it controls | -|------------------------------|-----------------------------------------------------------| -| `AUTOLAUNCH` | Whether the container auto-runs the launch sequence | -| `NUM_ROBOTS` | How many robot containers spawn | -| `ROBOT_NAME` | Namespace prefix for ROS topics | -| `VERSION` | Docker image tag to use | -| `COMPOSE_PROFILES` | Which compose profiles are active | -| `ISAAC_SIM_USE_STANDALONE` | Run Isaac Sim as a standalone Python script | -| `ISAAC_SIM_SCRIPT_NAME` | Which Isaac Sim launch script to run | +(The legacy form `AUTOLAUNCH=false airstack up robot-desktop` still works.) -### Inspecting and stopping +### Waiting for readiness ```bash -# Show all running containers (with airstack container names) -airstack status +airstack ready # containers → sim /clock → autonomy nodes → PX4; blocks until green +airstack ready --json # machine-readable, for scripts +``` -# Tail logs for a single container (partial name matching works) -airstack logs robot-desktop -airstack logs isaac-sim +Or pass `--wait` to `airstack up` to run it automatically. -# Stop and remove containers (clean slate) -airstack down -airstack down robot-desktop +### Inspecting and stopping -# Stop, remove containers, and prune volumes/networks -airstack clean +```bash +airstack status # all containers with ROBOT_NAME + ROS_DOMAIN_ID columns +airstack logs robot-desktop # tail logs (partial name matching) +airstack down # stop everything (includes generated module/fleet services) +airstack down robot-desktop # stop one service +airstack clean # remove ALL ROS 2 build artifacts (build/, install/, log/, .egg-info, __pycache__) ``` +Note: `airstack clean` deletes host-side colcon build artifacts (forcing a full +rebuild on next up) — it does **not** touch containers, volumes, or networks. + ### Container naming convention Compose generates names of the form `--`. With the default `PROJECT_NAME="airstack"`: `airstack-robot-desktop-1`, `airstack-isaac-sim-1`, -`airstack-ms-airsim-1`, `airstack-gcs-1`, `airstack-docs-1`. With `NUM_ROBOTS=2` you +`airstack-ms-airsim-1`, `airstack-gcs-1`, `airstack-docs-1`. With `--robots 2` you also get `airstack-robot-desktop-2`. Always confirm with `airstack status` or `docker ps --format '{{.Names}}'` rather than guessing. +## Modules, Stacks, Fleets, Doctor (RFC #379/#380) + +### Modules + +Modules are thin external repos declared in `./modules.repos` (pinned to tags/SHAs — +branch refs are refused), synced into the gitignored `./modules/` dir and overlaid +into the checkout. Guide: `docs/development/modules.md`. + +```bash +airstack module add --version # pin + sync (branches refused) +airstack module add ../asm_optitrack # local path (recorded under x-local-modules) +airstack module add --version v0.1.0 --no-hooks # skip host_setup hooks +airstack module list # NAME / TYPE / VERSION/PIN / TARGETS / VALID +airstack module sync [--no-hooks] # (re)clone, validate, overlay, layer plan, hooks +airstack module remove # drop entry, checkout, overlay artifacts +airstack module create --in-tree # scaffold robot/ros_ws/src/modules// (fork research) +airstack module lock [--build] [--check-conflicts] # recompute layer_plan.json + modules.lock; --build runs the docker layer chain +airstack module doctor # validate manifests + overlay integrity +airstack module doctor --drift # classify fork changes: module-contained vs extraction debt (never blocks) +``` + +After sync, module mounts are included automatically by `airstack up` +(`.airstack/generated/docker-compose.modules.yaml`; opt out with +`AIRSTACK_NO_MODULE_COMPOSE=1`). Isaac module launch scripts are addressable as +`ISAAC_SIM_SCRIPT_NAME=modules// {% endblock %} diff --git a/docs/real_world/HITL/index.md b/docs/real_world/HITL/index.md index bf6057c70..e993113a4 100644 --- a/docs/real_world/HITL/index.md +++ b/docs/real_world/HITL/index.md @@ -1,25 +1,70 @@ + + # Hardware-In-The-Loop Simulation -We configure a multi-machine HITL simulation, where a powerful desktop computer runs Isaac Simulator and rendering, and one/multiple jetson compute boards run robot-specific programs (planning, mapping, etc.). -## Requirement -A desktop computer configured according to [here](/docs/getting_started). One/multiple ORIN AGX/NX configured according to [here](/docs/real_world/installation/). -## Communication -All machines should connect to the same network. In our test, all machines are connected to the same router with ethernet cables. Ensure that all machines are able to `ping` others' IP addresses. +Hardware-in-the-loop (HITL) testing runs the real onboard compute — a Jetson flying the same `robot-l4t` container it will fly in the field — against a simulator on a desktop machine, before any propellers spin. The desktop runs Isaac Sim (and optionally the GCS); the Jetson runs the autonomy stack; they talk ROS 2 over the LAN. -### Run -On the desktop computer, under your Airstack folder, run -``` -docker compose up isaac-sim-hitl +## Prerequisites + +- A desktop set up per [Getting Started](../../getting_started/index.md), with the Isaac Sim image built or pulled. +- One or more Jetson ORIN AGX/NX set up per the [Installation Guide](../installation/index.md), with the `robot-l4t` image pulled and the repo cloned. +- All machines on the same LAN (we test with everything wired into one router). Verify each machine can `ping` the others. +- Each Jetson's hostname set to the `robot-` convention (e.g. `hostnamectl set-hostname robot-1`) so [robot identity resolution](../../robot/docker/robot_identity.md) assigns `ROBOT_NAME=robot_1` / `ROS_DOMAIN_ID=1` — the fallback is silent, so check this first. + +## Desktop: simulator (+ GCS) + +The `hitl` compose profile selects `gcs-real` — the field variant of the GCS that runs with `network_mode: host` so its DDS participants sit directly on the LAN (see [GCS Docker Configuration](../../gcs/docker/index.md)). Combine it with the `isaac-sim` profile, overriding the `.env` default (`desktop,isaac-sim`) so no `robot-desktop` containers start on the desktop: + +```bash +COMPOSE_PROFILES="hitl,isaac-sim" airstack up ``` -You should see the isaac simulator being launched. -On the Jetson computer, run + +This starts `isaac-sim` and `gcs-real` only. If you don't want a ground station, use `COMPOSE_PROFILES="isaac-sim" airstack up` for the simulator alone. + +## Jetson: robot stack + +On each Jetson, use the `l4t` profile (same command as the [Installation Guide](../installation/index.md); on a Jetson clone, also set `COMPOSE_PROFILES=l4t` in `.env` so the desktop default `desktop,isaac-sim` profiles are not active): + +```bash +airstack --profile l4t up ``` -docker compose up robot_l4t + +`robot-l4t` runs with `network_mode: host` and autolaunches `robot.launch.xml sim:=false` with the `full_default` stack. To pick a different [stack](../../development/stacks.md), pass `--stack` (e.g. `airstack --profile l4t up --stack lite_default`), or add `--no-autolaunch` to start the container idle and launch manually. + +## Networking: what has to line up + +The old failure modes here are all networking. Check each of these: + +- **Domain IDs.** Isaac Sim publishes each drone's topics on `ROS_DOMAIN_ID = N` (drone `domain_id` convention in the Pegasus launch scripts), and the Jetson's hostname must resolve to the same domain via the [robot name map](../../robot/docker/robot_identity.md) — `robot-1` → domain 1 matches sim drone 1. Verify inside the container: `docker exec airstack-robot-l4t-1 bash -c 'echo "$ROBOT_NAME / $ROS_DOMAIN_ID"'`. +- **DDS transport.** All AirStack containers load `common/fastdds.xml`, which forces plain UDP and disables shared memory — required for topics to cross container and machine boundaries at all. +- **Sim container is on a Docker bridge.** The `isaac-sim` container joins the internal `airstack_network` bridge at the fixed address `172.31.0.200`, while the Jetson containers are host-networked on the LAN. DDS multicast discovery does not traverse the desktop's Docker bridge NAT by itself, and the repo does not ship a peer configuration for this topology. If the Jetson does not see sim topics (`ros2 topic list` empty apart from local nodes), you will need to configure cross-machine discovery yourself — e.g. a Fast DDS initial-peers profile or a discovery server pointing at the desktop's LAN IP — and verify it; this step is not currently automated or tested in CI. +- **Flight controller connection.** `robot-l4t` defaults to a real flight controller on serial (`FCU_URL=/dev/ttyTHS4:115200`) — with a physical FCU wired to the Jetson, MAVROS talks to real hardware while sensors come from the sim. If instead you want MAVROS to reach the PX4 SITL instance inside Isaac Sim, `FCU_URL` is env-overridable (unset, `interface_bringup`'s `interface.launch.py` computes `udp://:<14540+N>@$SIM_IP:<14580+N>`), but note `SIM_IP`'s default `172.31.0.200` is only reachable from the desktop's own Docker network, and the compose files do not publish PX4's UDP ports to the LAN — this cross-machine SITL path is untested; expect to forward those ports yourself. +- **GCS visibility.** With the `full_default` stack, the robot runs a [DDS router](../../robot/autonomy/dds_router.md) that bridges an allowlist of its topics into domain 0, where the host-networked `gcs-real` listens. + +## Verification + +```bash +airstack status # containers up on each machine +docker exec airstack-robot-l4t-1 bash -c "ros2 node list" # stack nodes present +docker exec airstack-robot-l4t-1 bash -c "ros2 topic hz /robot_1/sensors/front_stereo/left/image_rect" ``` -Once the scene is played in the Isaac simulator, the rviz GUI on the Jetson should start displaying sensor data, which means the connection is successful. -Screen record of desktop computer: +Once the scene is playing in Isaac Sim, sensor topics on the Jetson should tick at a steady rate. On the desktop, `gcs-real` launches Foxglove Studio (host networking: connect your own Foxglove to `ws://localhost:8765`) — sensor and odometry panels streaming live data confirm the LAN link end to end. See [GCS Foxglove Visualization](../../gcs/foxglove.md). Use `airstack logs robot-l4t` / `airstack connect robot-l4t` to debug the bringup session. + +!!! note "Tested configuration" + The demos below were recorded on an earlier AirStack release (raw `docker compose`, RViz-based verification). The commands on this page reflect the current CLI and compose profiles but this exact multi-machine topology is not covered by CI — treat the discovery and SITL caveats above as things to verify on your own network. + +## Demos + +Screen recording of the desktop machine: -Screen record of Jetson computer: - \ No newline at end of file +Screen recording of the Jetson: + diff --git a/docs/real_world/data_offloading/index.md b/docs/real_world/data_offloading/index.md index dfc63f649..d527ae329 100644 --- a/docs/real_world/data_offloading/index.md +++ b/docs/real_world/data_offloading/index.md @@ -56,7 +56,7 @@ Open a web browser to http://localhost:8091 (or the PORT you set). The default u ```bash cd /opt git clone https://github.com/castacks/storage_tools_device -cd stroage_tools_device +cd storage_tools_device ``` ### Update the config.yaml diff --git a/docs/real_world/deploying_to_hardware.md b/docs/real_world/deploying_to_hardware.md index f74b95bef..26533bf0a 100644 --- a/docs/real_world/deploying_to_hardware.md +++ b/docs/real_world/deploying_to_hardware.md @@ -1,27 +1,140 @@ # Deploying to Hardware -!!! note "Coming Soon" - This tutorial is a placeholder. Content will be added in a future release. +!!! danger "Safety first" + This tutorial ends with a real drone spinning real propellers. **Keep propellers off + until Step 8**, and arm only after every check in this page has passed. Fly with a + safety pilot holding an RC transmitter with a working manual override / kill switch, + where you are permitted to fly, per local regulations. -## Overview +This tutorial takes one path from a bench-top Jetson to a first autonomous flight: a Jetson +Orin running the `l4t` profile with the `full_default` stack, connected to a PX4 flight +controller over serial. Other platforms (e.g. ModalAI VOXL2 via the `voxl` profile) follow +the same shape — see [Autonomy Modes](../robot/autonomy_modes.md). -This tutorial will cover how to configure and deploy the AirStack autonomy stack onto a physical robot — either a Jetson-based system (L4T profile) or a ModalAI VOXL device (VOXL profile). +## 1. Prerequisites -## Prerequisites +- A Jetson ORIN AGX/NX flashed with JetPack (L4T, Ubuntu 22.04) — the tested platform per the [Installation Guide](installation/index.md). +- A PX4-based flight controller wired to a Jetson UART — default `/dev/ttyTHS4` at + 115200 baud; *verify the UART and baud on your hardware, wiring varies by carrier board*. +- You have completed [Getting Started](../getting_started/index.md) in simulation. +- An RC transmitter bound to the flight controller for manual takeover. -- Completed the [Getting Started](getting_started.md) tutorial. -- Physical robot hardware with a supported flight controller. -- Network access to the device (SSH or direct connection). +**Check:** on the Jetson, `ls -l /dev/ttyTHS4` shows the serial device (or note the device your FCU is actually wired to — you'll need it in Step 4). -## Steps +## 2. Install AirStack on the Jetson - +```bash +git clone --recursive -j8 git@github.com:castacks/AirStack.git +cd AirStack +./airstack.sh setup # adds `airstack` to PATH, installs Docker if needed +docker compose --profile l4t pull robot-l4t # pull the Jetson image +``` + +Also set `COMPOSE_PROFILES=l4t` in the repo's `.env` so the desktop defaults (`desktop,isaac-sim`) are not active on the Jetson. + +**Check:** `docker images | grep robot-l4t` lists the pulled image. + +## 3. Set the robot's identity + +Real-robot profiles resolve `ROBOT_NAME` and `ROS_DOMAIN_ID` from the **OS hostname** +via the [robot name map](../robot/docker/robot_identity.md). Name the device `robot-`: + +```bash +sudo hostnamectl set-hostname robot-1 +``` + +!!! warning "The fallback is silent" + A hostname that doesn't match the map does **not** error — the default map's catch-all + resolves it to `ROBOT_NAME=unknown_robot`, `ROS_DOMAIN_ID=0`, and the symptoms only + surface later (topics under `/unknown_robot`, the `zed-l4t` container on domain 1 + unable to see the stack). Check the mapping now. + +**Check:** the resolver maps your hostname as expected (re-verified in Step 6): + +```bash +python3 robot/docker/robot_name_map/resolve_robot_name.py $(hostname) \ + robot/docker/robot_name_map/default_robot_name_map.yaml # → ROBOT_NAME=robot_1 / ROS_DOMAIN_ID=1 +``` + +## 4. Configure the FCU connection + +The `robot-l4t` service (`robot/docker/docker-compose.yaml`) runs privileged with +`network_mode: host`, so the Jetson's serial devices are visible inside the container, +and it sets the MAVROS connection for you: `FCU_URL=${FCU_URL:-/dev/ttyTHS4:115200}` +and `TGT_SYSTEM=1`. If your FCU uses a different UART or baud rate, set `FCU_URL` in `.env` +(e.g. `FCU_URL=/dev/ttyTHS0:921600`) — a set `FCU_URL` environment variable is used directly by +`interface.launch.py` instead of deriving a simulation UDP URL (see [Robot Interface](../robot/autonomy/interface/index.md)). + +**Check:** `.env` reflects your serial device and baud rate if they differ from the default. MAVROS connectivity itself is verified in Step 6. + +## 5. Choose the stack (topology) + +The `l4t` profile defaults to the **`full_default`** stack — every autonomy module runs on +the Jetson, no ground station required. The alternative for compute-constrained vehicles is +the **`lite_offload_global`** split (lite modules onboard via `l4t_lite`, global planning on +a ground host via `offboard`) — see [Autonomy Modes](../robot/autonomy_modes.md). +**For a first flight, stay with `full_default`** — one machine, one container, nothing to bridge. + +**Check:** you have not set `AIRSTACK_STACK_DIR` or `--stack`, so the default applies. + +## 6. Bench test — PROPS OFF + +!!! danger "Propellers must be removed for this entire step" + +Power the FCU and bring the stack up on the Jetson, then verify in order: + +```bash +airstack --profile l4t up && airstack status # containers running +docker exec airstack-robot-l4t-1 bash -c 'echo "$ROBOT_NAME / $ROS_DOMAIN_ID"' # robot_1 / 1 +docker exec airstack-robot-l4t-1 bash -c "ros2 node list" # stack nodes present +docker exec airstack-robot-l4t-1 bash -c "ros2 topic echo /robot_1/interface/mavros/state --once" # connected: true +docker exec airstack-robot-l4t-1 bash -c "ros2 topic hz /robot_1/sensors/front_stereo/left/image_rect" # sensors ticking +``` + +The sensor topic above is the ZED front-stereo stream from the `zed-l4t` container — +substitute your own if your payload differs. Debug the bringup with `airstack logs robot-l4t` / +`airstack connect robot-l4t`. Finally, on a laptop on the same network, start the field GCS (`gcs-real`, host-networked): + +```bash +COMPOSE_PROFILES=deploy airstack up +``` + +**Check:** MAVROS reports `connected: true`, sensor topics tick at a steady rate, and the +robot appears in the GCS 3D view ([Operating the GCS](../gcs/usage/user_interface.md)). + +## 7. HITL rehearsal (recommended) + +Before flying, rehearse with the *real Jetson and real container* against a simulator: +[Hardware-In-The-Loop Simulation](HITL/index.md). HITL exercises the exact `robot-l4t` +image, identity resolution, networking, and GCS link you just configured — with zero flight risk. + +**Check:** the full Takeoff → Navigate → Land flow works end-to-end in HITL. + +## 8. First flight checks + +Only now, with every previous check green, install propellers. + +- **Safety monitor:** the `full_default` stack runs `drone_safety_monitor`, which watches + the state estimate and pauses the trajectory controller if it times out (it also accepts + `pause` / `resume` / `rewind` on its `command` topic). Confirm it is in `ros2 node list`. +- **Manual override:** confirm the safety pilot can take over and kill motors from the RC + transmitter at any time. *AirStack does not configure this — set up and test your FCU's + RC failsafe / kill switch per its documentation, and verify on your hardware.* +- **Telemetry:** GCS link live, odometry sane (a stationary drone shows a stationary pose), GPS fix acquired if you rely on it. +- **First command:** from the GCS Robot Tasks panel, send a **Takeoff** to a low altitude, + let it hover, then **Land**. Expand to Navigate missions only after a clean hover. + +## Congratulations + +Your robot has gone from a bench-top Jetson to an autonomous first flight. From here you can +grow the mission: waypoint routes and geofences from the GCS, split-stack topologies, and multi-robot fleets. + +## See Also + +- [Robot Identity](../robot/docker/robot_identity.md) — hostname → name/domain mapping in depth +- [Autonomy Modes](../robot/autonomy_modes.md) — profiles, stacks, and split topologies +- [HITL Testing](HITL/index.md) — the pre-flight rehearsal setup +- [Operating the GCS](../gcs/usage/user_interface.md) — commanding and monitoring from Foxglove +- [Data Offloading](data_offloading/index.md) — getting your flight data off the vehicle diff --git a/docs/real_world/index.md b/docs/real_world/index.md index 9d5d89a91..8126b2c81 100644 --- a/docs/real_world/index.md +++ b/docs/real_world/index.md @@ -26,10 +26,10 @@ Real-world deployment involves: ## Getting Started with Hardware ### Prerequisites -- Completed [Getting Started Tutorial](../getting_started.md) in simulation +- Completed [Getting Started Tutorial](../getting_started/index.md) in simulation - Understanding of [System Architecture](../robot/autonomy/system_architecture.md) - Access to supported hardware platform -- Familiarity with [Autonomy Modes](../tutorials/autonomy_modes.md) +- Familiarity with [Autonomy Modes](../robot/autonomy_modes.md) ### Deployment Process @@ -50,7 +50,7 @@ Real-world deployment involves: - Pre-flight checklist - Monitor during operation - Post-flight data collection - - See: [Deploying to Hardware Tutorial](../tutorials/deploying_to_hardware.md) + - See: [Deploying to Hardware Tutorial](deploying_to_hardware.md) 4. **Data Management** - Offload ROS bags and logs @@ -107,13 +107,14 @@ See: [Robot Identity Configuration](../robot/docker/robot_identity.md) ## Autonomy Modes for Real World -AirStack supports multiple autonomy modes for different scenarios: +AirStack supports multiple autonomy topologies (stacks) for different +scenarios: -- **`onboard_all`**: All processing on robot (no ground station needed) -- **`onboard_local`**: Local planning onboard, global planning offboard -- **`offboard_global`**: Heavy computation on ground station +- **`full_default`**: All processing on robot (no ground station needed) +- **`lite_default`**: Lite modules only — no global planning anywhere +- **`lite_offload_global`**: Split stack — local planning onboard, global planning on the ground station -See: [Autonomy Modes Tutorial](../tutorials/autonomy_modes.md) +See: [Onboard/Offboard Distributed Computing](../robot/autonomy_modes.md) and [Stacks](../development/stacks.md) ## Data Collection @@ -162,12 +163,12 @@ See: [Data Offloading](data_offloading/index.md) - [Installation on Hardware](installation/index.md) - [HITL Testing](HITL/index.md) - [Data Offloading](data_offloading/index.md) -- [Deploying to Hardware Tutorial](../tutorials/deploying_to_hardware.md) +- [Deploying to Hardware Tutorial](deploying_to_hardware.md) - [Robot Configuration](../robot/configuration/index.md) ## Next Steps -- **New to hardware deployment?** Start with [Deploying to Hardware Tutorial](../tutorials/deploying_to_hardware.md) +- **New to hardware deployment?** Start with [Deploying to Hardware Tutorial](deploying_to_hardware.md) - **Ready to install?** Follow [Installation Guide](installation/index.md) - **Need to test safely?** Set up [HITL Testing](HITL/index.md) - **Managing data?** Configure [Data Offloading](data_offloading/index.md) \ No newline at end of file diff --git a/docs/real_world/installation/index.md b/docs/real_world/installation/index.md index aa2a01c7c..7f485357a 100644 --- a/docs/real_world/installation/index.md +++ b/docs/real_world/installation/index.md @@ -1,29 +1,32 @@ # Installation on ORIN AGX/NX -We have tested installation and running robot container on Jetson ORIN AGX/NX and Ubuntu 22.04. +We have tested installation and running the robot container on Jetson ORIN AGX/NX with Ubuntu 22.04 (L4T / JetPack). ## Setup -Ensure you have docker installed. + +Ensure you have Docker installed (`airstack install` can install it for you). + ### Clone -``` +```bash git clone --recursive -j8 git@github.com:castacks/AirStack.git +cd AirStack ``` -Checkout to the correct branch: -``` -git checkout jkeller/jetson_36.4 -``` + ## Configure -Run `./configure.sh` and follow the instructions in the prompts to do an initial configuration of the repo. +Run `./airstack.sh setup` and follow the prompts to do an initial configuration of the repo (this also adds the `airstack` command to your PATH). Pull the correct image: -``` -docker compose pull robot_l4t + +```bash +docker compose --profile l4t pull robot-l4t ``` ## Run + +```bash +airstack --profile l4t up ``` -docker compose up robot_l4t -``` -You should be able to see the rviz GUI being launched. \ No newline at end of file + +The autonomy stack launches inside a tmux session in the `robot-l4t` container. Verify it with `airstack status` and follow the output with `airstack logs robot-l4t` (or `airstack connect robot-l4t` to attach to the tmux session). diff --git a/docs/real_world/supported_platforms.md b/docs/real_world/supported_platforms.md new file mode 100644 index 000000000..e734cc831 --- /dev/null +++ b/docs/real_world/supported_platforms.md @@ -0,0 +1,36 @@ +# Supported Platforms + +AirStack targets one compute platform per Docker Compose profile: each service in [`robot/docker/docker-compose.yaml`](https://github.com/castacks/AirStack/blob/main/robot/docker/docker-compose.yaml) extends the shared `robot_base` and pins the base image and platform toggles for its target. This page is the honest status matrix — what CI actually tests, what has been used in the field, and what merely exists as a profile. Deployment *topologies* (which autonomy stack runs where) are a separate axis, covered in [Autonomy Modes](../robot/autonomy_modes.md). + +## Platform Matrix + +| Platform | Compose profile → service(s) | Base image (from compose `build.args`) | Status | +| -------- | ---------------------------- | -------------------------------------- | ------ | +| x86-64 desktop/laptop (development + simulation) | `desktop` → `robot-desktop` (+ `gcs`) | `nvidia/cuda:13.0.2-base-ubuntu24.04` | **CI-tested (sim)**: `system-tests.yml` runs build, liveliness, sensor, and flight marks against this profile on ephemeral x86 GPU runners; `test_build_docker.py` builds the `robot-desktop`, `gcs`, `isaac-sim`, and `ms-airsim` images | +| x86-64 desktop, split-topology debugging | `desktop_split` → `robot-desktop-onboard` + `robot-offboard` (+ `gcs`) | Same image as `robot-desktop` | Profile exists; shares the CI-tested desktop image, but the split topology itself is not CI-exercised | +| x86-64 ground station (field, offboard half) | `offboard` → `robot-offboard` + `gcs-real` | Same image as `robot-desktop`; GCS from `osrf/ros:jazzy-desktop-full` | Profile exists for field use paired with `l4t_lite`/`voxl` vehicles; not CI-exercised | +| NVIDIA Jetson Orin AGX/NX (full stack onboard) | `l4t` → `robot-l4t` (+ `robot-l4t-stack-base`, `zed-l4t`) | `dustynv/ros:jazzy-ros-base-r36.4.0-cu128-24.04` via the intermediate `Dockerfile.l4t-stack-base` image | **Supported, field-used**: the [installation guide](installation/index.md) documents tested install and run on Jetson Orin AGX/NX with Ubuntu 22.04 (L4T / JetPack); not covered by CI | +| NVIDIA Jetson Orin, lite (global planning offloaded) | `l4t_lite` → `robot-l4t-onboard` | Same image as `robot-l4t` | Same support level as `l4t`; defaults to the `lite_default` stack | +| ModalAI VOXL 2 | `voxl` (alias `voxl_onboard`) → `robot-voxl-onboard` | `ubuntu:24.04` (aarch64, `REAL_ROBOT=true`, no CUDA) | Profile exists; docs in progress (per [About](../about.md#supported-platforms)); defaults to the `lite_default` stack (compute-constrained); do NOT assume test coverage | + +Companion services on the same profiles: `zed-l4t` (profile `l4t`, base `dustynv/ros:jazzy-desktop-r36.4.0-cu128-24.04`) runs only the ZED stereo camera driver next to `robot-l4t`; `simple-robot` (profile `simple`) and `robot-test` (profile `test`) are desktop-image variants for the lightweight simulator and colcon-test runs, not separate platforms. + +## OS / JetPack Requirements + +Only requirements actually stated by a doc or Dockerfile in this repo: + +| Platform | Stated requirement | Source | +| -------- | ------------------ | ------ | +| Development machine | Ubuntu 22.04 or 24.04, NVIDIA GPU (RTX 3070 minimum, RTX 4080+ recommended), 16GB+ RAM, ~100GB free storage | [About — FAQ](../about.md#faq) | +| Jetson Orin AGX/NX | Ubuntu 22.04 (L4T / JetPack); container stack pinned to L4T r36.4.0 | [Installation guide](installation/index.md); `r36.4.0` image tags in `robot/docker/docker-compose.yaml` | + +## What CI Does and Does Not Cover + +GPU simulation jobs (`system-tests.yml`) run on `[self-hosted, airstack-ephemeral]` x86 runners — every CI-verified result is the **desktop x86 simulation path only**. No CI job builds or runs the `l4t`, `l4t_lite`, or `voxl` images; their status above comes from the installation docs and profile definitions, not automated testing. + +## See Also + +- [Installation on Orin AGX/NX](installation/index.md) — the hardware install walkthrough +- [Autonomy Modes](../robot/autonomy_modes.md) — deployment topologies (which stack runs on which machine, per profile) +- [Docker Build Profiles](../development/intermediate/docker-build-profiles.md) — how compose build args map to image variants (including the L4T/Jetson build chain) +- [Docker Services](../robot/docker/index.md) — the full service hierarchy diff --git a/docs/release_notes/index.md b/docs/release_notes/index.md new file mode 100644 index 000000000..8d9d7e38c --- /dev/null +++ b/docs/release_notes/index.md @@ -0,0 +1,617 @@ +# Release Notes + +Feature docs deliberately describe only the system as it exists — never how +it got that way — so that every page reads standalone. This page is the one +place where change context lives: what changed, from what, and why. + +The published site shows only the notes for the docs version you are +viewing. To read another version's notes, switch versions with the selector +in the header. Versioned release-notes pages exist from **0.19.0** onward; +notes for older releases (0.18.0 and earlier) live on the +[GitHub releases page](https://github.com/castacks/AirStack/releases). + + + +## 0.21.0 (Unreleased) + +- Nothing yet. + +## 0.20.0 — 2026-08-29 + +- **Pre-release versioning terminology: `-alpha.N` → `-dev.N`.** The + development line on `develop` now uses `X.Y.Z-dev.N` pre-release versions + (first: `0.21.0-dev.0`) instead of `X.Y.Z-alpha.N`. The + `check-version-increment` PR gate accepts exactly `dev`, `beta`, `rc` + (ordering `dev < beta < rc < release`); the main→develop sync workflow + rolls develop forward to the next minor's `-dev.0` after a release. Docs, + skills, and the PR template were updated to match. Existing `-alpha.N` + versions in git history and in declared module-compat ranges remain valid + historical references. + +- **Single, revamped repo README.** The repo now has one `README.md` (at the + root — `docs/README.md` was removed), rewritten around the redesigned docs + home page: quickstart, one-command bring-up, sim-to-real parity, CI flight + campaigns, and agent-driven workflows. + +- **Module-catalog sync automation + drift alarm.** Registering a module is + two merges (registry PR to + [airstack-modules-index](https://github.com/castacks/airstack-modules-index) + + trunk fixture/catalog sync), and the docs deploy regenerates the + published catalog from the **live** registry — so a missed half used to + drop the module from the site silently. Now the develop docs deploy + raises a `docs-catalog-drift` issue whenever the committed catalog and + the live registry disagree, and the new `sync-modules-index` workflow + (daily + manual dispatch) opens the trunk sync PR automatically + (`.github/workflows/scripts/registry_sync.py`). + +- **Trustworthy system-test outcomes.** A red system-tests run now always + means the code under test got worse, never that CI infrastructure hiccuped: + `run_meta.json` (schema v2) classifies every failure as + `assertion` / `infrastructure` / `collection` / `ci_integrity`, and CI fails + only on those — comparable numeric metric deltas (Hz, CPU, error metrics) + are **advisory** in the report and no longer fail the PR. Metric + comparisons only happen between fingerprint-identical campaigns (same + tests **and** same behavior-changing CLI config: sim, robot count, + trajectories, velocities, tolerances), with the baseline selected from + recent base-branch artifacts by fingerprint instead of "newest artifact of + any shape". Bring-up/readiness failures fail fast when the simulator + process dies (instead of burning the full `/clock` timeout on an ephemeral + GPU pod) and capture a bounded, secret-free `diagnostics/` bundle before + the pod is destroyed. Maintainers can dispatch focused flight campaigns + (`trajectory_types`, `takeoff_velocities`) from `workflow_dispatch`. New + `airstack up --config-only`: dry-run restricted to logical launch-config + contracts (no Docker/credentials/image/submodule prerequisites). + +- **CI un-redded: Metrics Report and Unit Tests fixed.** Every PR had been + failing since ~2026-08-20 for reasons unrelated to the code under test. + The system-tests **Metrics Report** job installed only `tabulate`, so + `tests/parse_metrics.py` (which imports the `tests/harness` package) + crashed with `ModuleNotFoundError: No module named 'yaml'` — and the crash + was misreported as "Metric regression detected"; the job now installs + `tests/requirements.txt`, and the regression verdict additionally requires + a written `report.md` so a parser crash can never masquerade as a metric + regression. The **Unit Tests** workflow ran the `tests/meta/` contract + suite on a checkout with no submodules and no `omni_pass.env`, so every + `airstack up --dry-run --sim isaac` contract hard-failed preflight and the + docs-catalog contract missed the submodule-resident vdb_mapping_ros2 + README; the workflow now checks out submodules recursively and provisions + the same guest `omni_pass.env` stub that `module-system-tests.yml` uses + (preflight itself stays strict). + +- **New reference stack `full_mighty` + registered `mighty` module.** The + MIGHTY Hermite-spline local planner (MIT ACL, RA-L 2026) with its + acl-mapping voxel world model and a NavigateTask/trajectory_controller + bridge, packaged as the external + [asm_mighty](https://github.com/castacks/asm_mighty) module (pinned at + v0.1.1 in the stack's `modules.repos`; repo private until the AirStack + agent study concludes). [`full_mighty`](../../stacks/full_mighty/README.md) + is `full_default` with only the local-planner include swapped — the + module-swap demonstration for the modular architecture. Registered in the + [module catalog](../modules/index.md); validated on Isaac Sim (44/44 + vendored gtests, empty-world route flight, 7/7 pillar-field traversals, + 5/5 judged obstacle-route flights at 1.59–1.65 m min clearance vs a 1.0 m + gate). + +- Fixed: the docs search dropdown rendered behind the nav-tabs bar and the + version-selector text (custom z-indexes inside the header's stacking + context); the search subtree is now lifted above both. + +**Documentation overhaul (Diátaxis restructuring).** The docs site was +audited against the [Diátaxis](https://diataxis.fr) framework and +reorganized; page URLs are preserved (moves are covered by redirects): + +- The nav is now organized by document kind — **Tutorials / How-to Guides / + Reference / Concepts** tabs — replacing the difficulty-tier + ("Beginner/Intermediate/Advanced Tutorials") buckets, which contained no + tutorials. The doc-authoring standards (Documentation Guide, mkdocs + skills) now prescribe the quadrant taxonomy and include a decision tree. +- 18 verified doc/code mismatches fixed, including: a phantom + `--recreate` flag and missing `--scene` in the CLI reference; phantom + `ROBOT_LAUNCH_PACKAGE`/`ROBOT_LAUNCH_FILE` env vars (real: `LAUNCH_PACKAGE`); + nonexistent `airstack_msgs/TrajectorySegment`/`TrajectoryOverride` types in + doc templates (real: `TrajectoryXYZVYaw`); the MS-AirSim tmux window + ordering (bridges launch before PX4) and MAVLink port math; the Getting + Started Foxglove step (layout now auto-seeds; manual import retired). +- Removed superseded pages with redirects: the Ascent-era scene-setup pair, + the pre-harness testing-frameworks page, the orphaned tutorials index and + two stale duplicate pages (development_environment, airstack-cli index). + The git-hooks docker-versioning READMEs are now deprecation notices — the + hook they described conflicts with the semver `check-version-increment` + gate (note: `airstack config git-hooks` still installs it; CLI removal is + a follow-up). +- Pages that documented never-built or fabricated behavior were rewritten + from the code: global planning (the unimplemented Global-Manager/ + PlanRequest protocol is gone), the robot interface page (state flows via + `odometry_conversion`), robot configuration, HITL (now uses the + `gcs-real` `hitl` profile and Foxglove verification), rosbags, and the + robot-side data-offloading page (now points at the storage-tools + workflow). +- Hybrid pages split by audience: `system_architecture.md` (explanation + core; drifted topic tables replaced with links into the interface + conventions spec), CI/CD (new **Using CI** how-to), GCS Foxglove (new + **Extending the Visualizer**), Isaac Sim docker (new **Container + Workflows**). Duplicated hot tables (topics, CLI flags, pytest marks, + requirements) now live in one canonical home each. +- Previously off-site references added to the nav: the `vehicle.yaml` and + `module.yaml` schemas, local calibration contract, the RViz tasks/waypoint + panel manuals, the LiDAR point-cloud filter README, and the OSMO lab-admin + guide. New reference pages: the complete `.env` schema, the + `airstack_msgs` interface reference, the trajectory-library YAML format, + and a supported-platform matrix. New/rewritten onboarding: Deploying to + Hardware and Operating the GCS. +- The Interface Conventions Specification was bumped to **v1.0.1**: §8 now + lists all eight `task_msgs` actions (added `tasks/coverage` and + `tasks/chat`, both defined with no shipped executor). +- Six new how-to guides: Adding a State Estimator, Adding a Planner, + Creating a Multi-Agent Coordination Algorithm (grouped under a new + How-to → Autonomy section), Creating a Custom Stack Topology, Adding a + Vehicle Type/Unit/Platform, and Getting the Most out of Your Coding + Agent (the feature-notebook workflow). Each presents the in-tree + package vs `airstack module create --in-tree` module-scaffolding + choice. The Concepts tab now sits directly after Tutorials, and the + UE→Isaac export tutorial was refreshed (new walkthrough video, export + as Z-up in meters, note that UE Decals — paint markings, dirt, + puddles — do not export). +- Five new beginner tutorials completing the learning path: Fly a Mission + from the GCS, Change a Parameter (the edit→relaunch loop; config YAML is + symlink-installed from the bind-mounted source, so no rebuild), Write + Your First Module (`airstack module create --in-tree` scaffold, with a + fix-it note for the scaffold's double-namespace stub), Your First Fleet + (two-robot fleet file, per-robot Foxglove tabs), and Build and Fly Your + Own Scene (GUI stage → scene catalog → `--scene` flight → baked + `*.scene.usd`). New how-to: Adding a Controller (verified + trajectory_controller → pid_controller → interface command chain); + Adding a Planner expanded into Adding a World Model and Planner + (local world-model/planner matched pairs vs the spec'd global map + interchange). + +This release restructures AirStack from a monolith into **modules**, +**stacks**, and **fleets**, implementing +[RFC #379 (Modular AirStack)](https://github.com/castacks/AirStack/discussions/379) +scales 1–2 of +[RFC #380 (Heterogeneous AirStack)](https://github.com/castacks/AirStack/discussions/380), +and the stack-folder anatomy of +[RFC #385 (Directory Atlas)](https://github.com/castacks/AirStack/discussions/385). +Feature docs deliberately cite none of these — the design sources live here: + +- **Modules** are thin external repos with a small `module.yaml`, pulled on + demand: `airstack module add --version `. Three capabilities + were extracted from trunk into new repos: + [asm_macvo](https://github.com/castacks/asm_macvo) (with its torch/TensorRT + stack moved out of the base robot image — 17.1 GB → ~6 GB, −65%), + [asm_optitrack](https://github.com/castacks/asm_optitrack) (including the + Isaac Sim NatNet emulator), and + [asm_dfm2_disturbances](https://github.com/castacks/asm_dfm2_disturbances). + The registry lives at + [castacks/airstack-modules-index](https://github.com/castacks/airstack-modules-index). +- **Stacks** (`stacks/`) are self-contained topology folders — pinned + `modules.repos`, plain XML entry launch files, and a CI-observed + `wiring.md` graph baseline. Wiring that used to be spread across per-layer + `*_bringup` packages (`local_bringup`, `onboard_all`, and the + `local*.launch.xml` variant files) now lives in one place: the selected + stack's entry launch file. +- Module provenance: asm_dfm2_disturbances originates from the DFM2 + ("don't fool me twice") AirStack fork, hand-built as the pilot module + before the tooling existed (its FRICTION_LOG.md records every manual + step; the port drops `omni.isaac.dynamic_control` in favor of the PhysX + simulation interface). asm_optitrack was extracted from the trunk + OptiTrack PR series (#359/#374/#375/#376) with git history preserved; its + unit/integration/e2e tests run in the module's own CI. +- **Fleets** (`config/fleets/`) declare who exists, which vehicle, which + stack, and which ground hosts run split-stack halves: + `airstack up --fleet `. + +### Launch-path changes (breaking) + +- **`AUTONOMY_ROLE` is removed.** The env-var role dispatch + (`full`/`onboard`/`offboard`) is gone; a set `AUTONOMY_ROLE` is a preflight + hard error (as is setting it alongside `--stack`, which previously let the + stack silently win). Stacks are the only dispatch: `airstack up --stack + [:]`, defaulting to `full_default`. The per-role launch trees + (`onboard_all/`, `onboard_local_offboard_global/`) are deleted, the + wrap-then-flatten migration is complete (every reference stack composes + flat module-launch includes; the launch-lint allowlist is down to the two + deliberately wrapped blocks: `interface.launch.py` and the DDS-router/ + gossip helpers). Migration map: + + | Before | Now | + |---|---| + | no role set / `AUTONOMY_ROLE=full` | `full_default` (default; machine-proven graph-identical) | + | `full` + `local_droan_cpu.launch.xml` variant | `--stack full_droan_cpu` (variant file deleted; wiring captured from it before deletion) | + | `local_macvo_obstacle_avoidance.launch.xml` variant | `--stack full_macvo` — the variant was broken three ways (unprefixed args silently ignored, stale disparity topic, `launch_macvo` never enabled); the stack fixes all three | + | `AUTONOMY_ROLE=onboard` (no split) | `--stack lite_default` (on the desktop profile the onboard role was unreachable anyway — `robot-desktop` hardcoded `full`) | + | `onboard`/`offboard` split pair | `--stack lite_offload_global:onboard` / `:offboard` | +- The split onboard/offboard deployment is now the `lite_offload_global` + stack (`:onboard` / `:offboard` entries) bridged per its `bridge.yaml`; + the DDS-router config is generated by `tools/gen_dds_router.py` rather + than hand-maintained. The generated bridge deliberately drops the legacy + split's `set_trajectory_mode` crossing (control-mode topics may not cross + a bridge — `airstack doctor` hard gate). +- Module launch files are remap-free and declare prefixed, described + arguments with canonical defaults; generically named arguments (like + `config_file`) were renamed with per-module prefixes because ROS 2 launch + configurations are global across includes. +- Shared DDS-router configs moved out of the per-role `onboard_all/` tree up + to `autonomy_bringup/config/`. +- **OptiTrack activation changed:** trunk's + `overrides/isaac-optitrack-simulation.env` and the `LAUNCH_NATNET` toggle + are gone (a set `LAUNCH_NATNET` draws a preflight warning). Mocap is + brought up by a stack that includes `natnet_ros2` unconditionally — the + asm_optitrack module's `test_stack/` is the reference. +- `SKIP_MACVO` / `SKIP_TENSORRT` build args removed from `Dockerfile.robot` + along with the payload they gated; MAC-VO deps enter an image only via + `airstack module lock --build`. `stacks/full_macvo` includes the module's + own `macvo.launch.xml` (the in-tree `perception/macvo_ros2` copy is + deleted; the module preserves its git history, and fixes the previously + hardcoded `camera_info` subscription to honor its topic parameter). + +### Added + +- `airstack up --stack [:]` stack dispatch with 5 reference + stacks under `stacks/` (`full_default`, `full_droan_cpu`, `full_macvo`, + `lite_default`, `lite_offload_global`), each carrying pinned + `modules.repos` and a CI-observed `wiring.md` graph baseline +- Module CLI — `airstack module add --version ` (branches + refused; local paths allowed), `list|sync|remove|create --in-tree|doctor`, + workspace overlay of module packages, and auto-generated module compose + overrides included by `airstack up`; Docker module layers composed via + `modules.lock` (`airstack module lock --build`) +- Fleet system: `airstack up --fleet ` driven by + `config/fleets/*.yaml` (identity, vehicle from `config/vehicles/`, stack + selection, spawns) with `hosts:` split-stack placement onto ground hosts; + `airstack fleet list|generate` per-robot compose for heterogeneous fleets; + `airstack sync` reconciles `airstack.yaml` (modules, external stack repos, + fleet validation) +- `airstack doctor [--live|--snapshot] [--stack NAME]` — observe-and-report + checks with exactly two hard gates (module dependency-conflict gate; + bridge gate: no control-setpoint / trajectory-group topics may cross a + split-stack bridge); `--live` diffs the RUNNING ROS graph against the + stack's committed `wiring.md` +- `airstack stack list|new |diff ` (diff compares + generated wiring, not launch XML) +- `tests/meta/` contract-test tier (unit mark) pinning the CLI/docs/stack + contracts, plus the `wiring` system-test mark: an observed wiring snapshot + of the running graph drift-checked against the stack's committed + `stacks//wiring.md` +- Split-stack bridging: `stacks/lite_offload_global/bridge.yaml` explicitly + lists every boundary crossing and `tools/gen_dds_router.py` generates the + DDS-router config from it deterministically (`--check` enforces the bridge + hard gate) +- New docs: Modules, Stacks, Fleets, Module CI guides, the generated + Module & Stack Catalog marketplace, and the Modular AirStack Walkthrough +- Intent flags on `airstack up` — `--sim isaac|airsim|simple`, `--robots N`, + `--headless`, `--play`/`--no-play`, `--no-autolaunch`, `--wait`, + `--dry-run` — deriving the coordinated env-var sets as exported leaf + values, with a resolved-config banner and a per-run + `.airstack/runs//effective_config.env` dump; contract-tested +- `airstack up --scene `: simulator-agnostic scene selection via + a new catalog (`simulation/scenes.yaml`, resolved host-side by + `simulation/resolve_scene.py`). Isaac maps shortnames to Pegasus catalog + keys or Nucleus USD URLs (exported as `ISAAC_SIM_SCENE` + + `ISAAC_SIM_STAGE_SCALE`; the example launch scripts and `fleet_spawn.py` + now resolve their scene from these instead of hardcoding `env_url`); + MS AirSim maps to `fetch_scene.sh` keys (`MS_AIRSIM_SCENE` — the + entrypoint's auto-fetch, previously Blocks-only, now fetches any catalog + scene, and an interactive `airstack up` asks before a multi-GB download). + The `fetch_scene.sh` catalog was corrected to the UE4 binaries that + actually exist in the AirSim v1.8.1 release: `forest`, `soccerfield`, and + `building99` were removed (their zips were never published, or are empty), + `airsimnh` now downloads `AirSimNH.zip` (was the nonexistent + `Neighborhood.zip`), and `africasavannah` / `msbuild2018` were added; the + entrypoint resolves the UE launcher by glob after extraction, so zips whose + inner `.sh` doesn't match the folder name still boot. + Unknown scene = error + per-simulator availability table. Five stages were + published to the guest-readable + `omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/` + (AbandonedFactory, AbandonedWarehouse day/night, ChemicalPlant, + ConstructionSite, RetroNeighborhood) and cataloged with per-stage scale. + See [Simulation Scenes](../simulation/scenes.md) +- `airstack ready` (and `airstack up --wait`): staged flight-readiness gates + mirroring the system-test budgets — containers → sim `/clock` → per-robot + sentinel nodes → PX4 MAVROS-connected + `local_position/odom` streaming — + with per-gate diagnostics and `--json` for scripts +- Preflight validation in `airstack up` on resolved configuration (env > + `--env-file` > `.env`): one-simulator guard, `NUM_ROBOTS>1` vs + single-drone Isaac script as a named hard error, missing images listed + with an `image-pull` hint, missing `omni_pass.env` / empty Pegasus + submodule / Docker < 29 surfaced on the host + (`AIRSTACK_SKIP_PREFLIGHT=1` downgrades errors to warnings) +- tmux pane output mirrored to container stdout via shared `.tmux.conf` + hooks, so `docker logs` / `airstack logs` show colcon builds, + `ros2 launch` output, sim loading, and crashes +- simple-sim as a first-class simulator: `airstack up --sim simple` and a + `simple_sim` smoke-test mark (it had been broken since the ROS Jazzy + migration — its container sourced a Humble path — and is fixed) +- Automatic `unit-tests.yml` PR gate on `ubuntu-latest`, plus + `run_meta.json` outcome metadata so reports distinguish completed + simulation campaigns from collection errors, empty selections, timeouts, + and cancellations +- Feature notebook workflow (`use-feature-notebook` skill): gitignored + `notebook/NNN-feature-slug/` entries whose `design_spec.md` and + `results_summary.md` populate feature PR descriptions +- Battery and telemetry display in the GCS control panel (voltage and + percentage per robot when the MAVROS battery topic is bridged) +- `TARGET_ARCH` build arg (default `x86_64`) in `Dockerfile.robot`; + `docker-compose.yaml` passes `TARGET_ARCH: aarch64` to the `voxl` and + `l4t` real-robot image builds +- `ros-${ROS_DISTRO}-mavros-extras` in the robot image (provides the + vision_pose plugin used for external-pose deployments) +- `overrides/l4t-px4-realrobot.env` — site-agnostic deployment override for + a single real PX4 robot on a Jetson (aarch64/l4t) +- `integration` test tier (`tests/integration/`, `integration` mark) with a + shared `robot_autonomy_stack` fixture (robot container, no sim/GPU) +- `waypoint_flight` system test: takeoff → ordered waypoint route via + `NavigateTask` → land, judged on the odometry track by the standalone + `tests/waypoint_checker.py`; the standard acceptance check after + integrating or swapping a planner module +- Foxglove auto-loaded layout: `render_layout.py` seeds the rendered + `NUM_ROBOTS`-matched layout directly into the Foxglove desktop app's + local layout store and `gcs.launch.xml` selects it via the `layoutId` + deep link, so Foxglove opens on the right layout with no manual + **Import from file...** step; user-saved edits are hash-detected and + never overwritten (delete the layout in the UI to reset). Requires the + pinned Foxglove version — see Changed + +### Changed + +- **The simulator now starts playing by default**: `PLAY_SIM_ON_START` + defaults to `true` in `.env` (was `false`). Pass `airstack up --no-play` + to come up paused and press Play yourself; `--play` remains available as + an explicit override +- Container autolaunch (robot desktop/voxl/l4t and GCS) now runs through an + `autolaunch` shell helper instead of a bare `bws && sws && ros2 launch` + chain: a build failure or launch crash prints an unmissable red + "AUTOLAUNCH FAILED" banner in the tmux pane (mirrored to `docker logs` / + `airstack logs`) instead of silently returning to a prompt with the stack + down +- `Dockerfile.gcs` pins the Foxglove desktop version (`FOXGLOVE_VERSION` + build arg, currently 3.0.0) instead of installing `latest`, since the + layout auto-load writes the app's on-disk local-layout record format + directly (verified against 3.0.0; re-verify before bumping the pin) +- `robot-desktop` image slimmed 17.1 GB → ~6 GB (−65%) by moving MACVO's + torch/TensorRT/weights into the `asm_macvo` module Docker layer; a further + dependency purge removed unused apt/pip packages (−152 MB) and `droan_gl`'s + GL dependencies are declared explicitly +- Isaac launch scripts deduplicated onto a shared `pegasus_app.PegasusApp` + base: the scripts become scenario declarations (~40–170 lines each, net + −438 lines) with hooks for NatNet/scene-import extras; behavior verified + by full system-test parity. `ISAAC_SIM_HEADLESS` and + `ISAAC_SIM_LIVESTREAM` work uniformly in every launch script (each was + honored by only half of them before) +- Launch-workflow docs corrected against actual behavior: `ISAAC_SIM_SCENE` + (nonexistent) replaced by `ISAAC_SIM_SCRIPT_NAME`/`ISAAC_SIM_GUI`, + getting-started reflects the paused-by-default sim and Foxglove UI, isaac + docker.md defaults match `.env`, ms-airsim MAVROS ports/FOV/vehicle naming + fixed +- Unit-test documentation matches the co-located layout: C++ gtests run via + `colcon test` under the `build_packages` mark; Python via the root harness + (`conftest.py` applies the `unit` mark by file location) +- The CI orchestrator polls a `repos:` list (one instance covers trunk and + every asm_* module repo; the singular `repo:` key still works) — module CI + jobs on `airstack-ephemeral` no longer need per-repo orchestrator instances +- Ephemeral CI GPU runners spawn via NVIDIA OSMO as a drop-in replacement + for the earlier OpenStack-Nova backend: the GitHub side (labels, JIT + tokens, fork guard) is unchanged; only the spawn target moved. The OSMO + service-account token plays the old application-credential role, + `osmo workflow exec` replaces SSH-via-floating-IP debugging, and the + runner image prebakes what cloud-init used to install at boot +- Pegasus launch scripts drive lidar through the RTX OmniLidar API + (`add_rtx_lidar_subgraph`) in place of the Ouster graph path, with ROS + topics reconciled (raw cloud on `…/sensors/ouster/point_cloud_raw`, + filtered on `…/sensors/ouster/point_cloud`) +- The `airstack-osmo` SSH config block for OSMO IDE sessions is + `StrictHostKeyChecking no` + `UserKnownHostsFile /dev/null` (replacing + `accept-new`); users with the earlier block should replace it and run + `ssh-keygen -R "[localhost]:2200"` once +- Default system-test `--sim` is `isaacsim`; pass `--sim msairsim` to opt in +- `-m build_packages` CI runs pull `cache_*` images instead of baking sim + images; `docker-build.yml` retags unchanged images on VERSION bumps + (content fingerprint) instead of always rebuilding +- Automatic OSMO validation runs the pull-only `build_packages` gate on + every PR update; GPU simulation campaigns are selected through `/pytest` + or `workflow_dispatch` +- `robot-l4t` compose service knobs are env-overridable (`FCU_URL`, rosbag + path via `BAG_STORAGE_PATH`); `FCU_URL` unquoted so the literal serial + path reaches MAVROS +- `zed-l4t` image: ZED SDK 4.2 → 5.2 with coupled ROS deps (`zed_msgs` + 5.2.1, `point_cloud_transport(_plugins)` 4.x, `backward_ros`) +- Unit tests are defined by `tests/colcon_unit_test_packages.yaml` +- Repo-wide relicense to **BSD 3-Clause Clear** (vendored packages keep + their upstream licenses); `airstack_msgs` stabilized at 1.0.0; every + package.xml carries a real maintainer and description (contract-tested) +- `DOCKER_IMAGE_BUILD_MODE=prebuilt` is a tag discriminator only; a real + prebuilt-workspace image stage is future work +- The repository CHANGELOG.md is removed in favor of this page — all + change records live here, per version +- **Docs policy: standalone snapshots.** Feature docs describe only the + current system; all change-relative language (including RFC citations) + coalesces here. A full docs audit applied the policy and corrected pages + that had drifted from the code, notably: bag recording is NOT + auto-triggered at takeoff (the recorder starts idle; toggle via + `/{robot_name}/bag_record/set_recording_status`); `tracking_point` / + `look_ahead` carry `airstack_msgs/msg/Odometry` (older docs said + `geometry_msgs/PointStamped`) and trajectory topics are + `airstack_msgs/TrajectoryXYZVYaw`; MAVROSInterface targets any + MAVLink-compatible FC (the documented Ascent/Ardupilot specificity does + not exist in code); the RobotInterface command topic is `cmd_pose`; the + DDS-router allowlist table is regenerated from the real config; the + Jetson install flow is `./airstack.sh setup` + the `robot-l4t` service. + Two orphaned pages documenting the behavior-tree framework + (`behavior_tree`, `behavior_executive` — packages removed in an earlier + release, PR #332; only `behavior_tree_msgs` remains) were deleted; the + behavior layer is `drone_safety_monitor` with mission sequencing via + GCS-sent task goals + +### Removed + +An audit removed dead or superseded code wholesale. Anything here is +recoverable from git history, and hardware-specific capabilities return as +out-of-trunk modules: + +- The `AUTONOMY_ROLE` launch dispatch and its per-role launch trees — + stacks are the only launch path +- **MACVO extracted to** [castacks/asm_macvo](https://github.com/castacks/asm_macvo); + **OptiTrack/NatNet extracted to** + [castacks/asm_optitrack](https://github.com/castacks/asm_optitrack) + (client, PX4 external-vision fusion, NatNet emulator + Isaac wrapper, + e2e/integration tests, env overrides) +- `px4_interface` + vendored `px4_msgs` — a native PX4 uXRCE-DDS interface + is tracked as a fresh design in + [#387](https://github.com/castacks/AirStack/issues/387); MAVROS remains + the flight interface +- `waypoint_interface`, `attitude_controller`(+`_msgs`) — dead code +- The RQT/RViz GUI set: `rviz_behavior_tree_panel` (with the `xdot_cpp` + submodule), `rqt_behavior_tree_command`, `rqt_behavior_tree`, `rqt_gcs`, + `rqt_airstack_control_panel` — Foxglove is the GCS surface +- Sensors-layer hardware packages `camera_param_server`, + `gimbal_stabilizer`, `sensor_interfaces` + (`lidar_point_cloud_filter` remains) +- WinTAK / TAK integration (`ros2tak_tools`, CLI plumbing, GCS image + dependencies) — can return as a module +- Isaac Sim `standalone_examples` copies, stale robot-docker helpers, the + `ensemble_planner` skeleton, the Gazebo parallel-bringup tree, and + pre-co-location test scaffolding +- trajectory_library's vestigial rqt selector (catkin-era GUI source, + `plugin.xml`, `setup.py`, launcher script — never installed by its + CMakeLists), the orphaned behavior-tree docs images, and an empty + integration-testing stub page +- `tests/goldens/wiring/` — wiring baselines live per-stack as + `stacks//wiring.md` + +### Fixed + +- `airstack up` guards (one-simulator, URDF pairing) validated `.env` only + and were bypassed by `--env-file`; they now check the resolved + configuration +- `pytest tests/` collects the co-located unit tests before mark filtering + (CI previously collected 97 of 252 items, so the Python unit tests ran + nowhere); empty CI pytest arguments no longer recurse the repository; + non-comparable artifacts are reported instead of false 0% results +- `barebones_pegasus_launch.py` crashed with `NameError: os`; + `isaac-sim-livestream` produced a black stream with multi-drone scripts; + `NATNET_BODY_NAME`/`NATNET_TARGET_NAME` overrides now work +- Isaac Sim image: PX4 `ubuntu.sh` no longer fails dpkg configure on the + NVIDIA base; robot image pins `pytest<8.1` and disables `launch_testing` + for colcon unit tests +- Robot identity: a pre-set `ROBOT_NAME` is honored; the name-map catch-all + maps to `unknown_robot` (valid ROS namespace token) and logs a warning + naming both fixes; inert `ROBOT_NAME`/`ROS_DOMAIN_ID` lines dropped from + `overrides/l4t-px4-realrobot.env` +- l4t robot image: dustynv's `/ros_entrypoint.sh` replaced with a + passthrough so stale prebuilt `fastcdr` libs no longer crash apt-built + nodes like MAVROS; the GeographicLib `egm96-5` geoid is asserted at build + time (MAVROS dies at startup without it) +- Bag recording: `RECORD_BAGS=true` now actually starts the recorder on a + robot; recorder status is bridged in the correct direction so GCS + indicators work; `ros2 bag record --exclude` updated for Jazzy's + `--exclude-regex` (multiple excludes alternated into one regex) +- OptiTrack/NatNet: `NATNET_SERVER_IP` is forwarded to the robot container; + the default tracked body matches the emulator; EKF2 external-vision + parameters are passed as `PX4_PARAM_*` so PX4 actually fuses mocap; + external-vision tuning corrected from real-flight bags (`EKF2_EV_DELAY` + 7.0, `EKF2_EVP_NOISE` 0.05); an unrecognized `connection_type` fails at + startup instead of silently falling back; MODELDEF drone-body count is + cross-checked against `NUM_ROBOTS` after the handshake; the emulator is + installed as a Kit extension so launch scripts can import it + +### Landing page & simulation camera/lighting (alpha.13) + +- **Docs landing page redesigned** around four demonstrated pillars + (one-command bring-up, sim-to-vehicle code parity, full-stack CI, agent + readiness): real quickstart commands with copy buttons, the actual + `airstack ready` checklist, a live test-mark matrix, and a fresh + autoplay hero video (Isaac full-warehouse and office, Foxglove's live + 3-robot trajectory traces, MS AirSim neighborhood and ZhangJiajie) — + 40 s / 5.4 MB, replacing the 37 MB splash GIF. Per-simulator and Foxglove + clips are embedded in their docs sections, and the architecture image is + now an unedited full-system desktop capture (sim + GCS + CLI, live). +- **Isaac launch scripts grew camera/spawn/lighting env knobs** + (`pegasus_app.py`, plumbed through the isaac-sim compose service): + `ISAAC_SIM_FOLLOW_CAM` / `_OFFSET` — a smoothed viewport chase camera + tracking a drone via live Pegasus vehicle state (also fixes the black + viewport at spawn on cm-authored stages; on by default, `off` disables); + `ISAAC_SIM_SPAWN_XY` — recenter the spawn row away from a cluttered scene + origin; `ISAAC_SIM_LIGHT_BOOST` — multiply the scene's own lights + (de-instances light-bearing subtrees first, so e.g. the NVIDIA office's + 121 instanceable ceiling lights become editable); + `ISAAC_SIM_FOLLOW_CAM_LIGHT` — headlight riding the follow camera; + `ISAAC_SIM_DOME_LIGHT` — dome-light intensity/exposure override. + +## 0.19.0 — 2026-08-22 + +The launch-workflow and CI-infrastructure release preceding the modular +transition. + +### Added + +- Intent flags on `airstack up` — `--sim isaac|airsim`, `--robots N`, `--headless`, `--play`/`--no-play`, `--no-autolaunch`, `--wait`, `--dry-run` — deriving the coordinated env-var sets (compose profiles, URDF, single/multi Isaac launch script) as exported leaf values, with a resolved-config banner and a per-run `.airstack/runs//effective_config.env` dump; contract-tested in `tests/meta/test_launch_intent_contract.py` (unit mark) +- `airstack ready` (and `airstack up --wait`): staged flight-readiness gates mirroring the system-test budgets — containers → sim `/clock` → per-robot sentinel nodes → PX4 MAVROS-connected + `local_position/odom` streaming (the armable signal) — with per-gate diagnostics and `--json` for scripts +- Preflight validation in `airstack up` on **resolved** configuration (env > `--env-file` > `.env`): one-simulator guard no longer bypassed by `--env-file`; `NUM_ROBOTS>1` with the single-drone Isaac script is a named hard error; missing images are listed with an `image-pull` hint before compose starts an implicit build; missing `omni_pass.env` / empty Pegasus submodule / Docker < 29 surfaced on the host (`AIRSTACK_SKIP_PREFLIGHT=1` downgrades errors to warnings) +- tmux pane output is mirrored to container stdout via shared `.tmux.conf` hooks, so `docker logs` / `airstack logs` now show colcon builds, `ros2 launch` output, sim loading, and crashes +- Automatic `unit-tests.yml` PR gate on `ubuntu-latest`, plus `run_meta.json` outcome metadata so reports distinguish completed simulation campaigns from collection errors, empty selections, timeouts, and cancellations +- `overrides/isaac-optitrack-simulation.env` — brings up Isaac Sim with the NatNet emulator and PX4 flying on mocap EKF2 external vision (GPS/baro/range aiding off), i.e. the configuration `tests/system/test_optitrack_e2e.py` runs, reproducible by hand +- `overrides/l4t-optitrack-realrobot.env` — deployment override for a real Jetson robot flying on OptiTrack mocap (PX4 EKF2 external vision instead of GPS): the NatNet server/body settings, plus the multi-NIC and FCU-parameter notes that path needs +- Feature notebook workflow (`use-feature-notebook` skill): every agent-implemented feature gets a local, gitignored `notebook/NNN-feature-slug/` entry with a status-tracked `design_spec.md` (written before coding) and `results/` artifacts + self-contained `results_summary.md` that populate the feature's PR description +- Battery and telemetry display in GCS RQT control panel (voltage and percentage per robot when MAVROS battery topic is bridged) +- `TARGET_ARCH` build arg (default `x86_64`) in `Dockerfile.robot` to arch-parametrize `LD_LIBRARY_PATH`; `docker-compose.yaml` passes `TARGET_ARCH: aarch64` to the `voxl` and `l4t` real-robot image builds +- `ros-${ROS_DISTRO}-mavros-extras` in the robot image (provides the vision_pose plugin used for external-pose deployments) +- `overrides/l4t-px4-realrobot.env` — site-agnostic deployment override for a single real PX4 robot on a Jetson (aarch64/l4t) +- `integration` test tier (`tests/integration/`, `integration` mark) with a shared `robot_autonomy_stack` fixture (robot container, no sim/GPU) +- `waypoint_flight` system test (`tests/system/test_waypoint_flight.py`): takeoff → ordered waypoint route via `NavigateTask` (dispatched as a dense plan) → land, judged on the odometry track by the standalone stdlib-only `tests/waypoint_checker.py` (in-order corridor arrival within `--waypoint-tolerance`, final goal within `--goal-tolerance`, per-waypoint `--waypoint-timeout`); validated end-to-end in Isaac Sim; serves as the standard acceptance check after integrating or swapping a planner module +- Real-robot PX4 external-vision fusion in `natnet_ros2` (OptiTrack mocap → EKF2): `mavros_gp_origin` (geoid-corrected synthetic GPS origin so `local_position.z` == OptiTrack z, fixing the ~36 m boot offset), `vision_pose_converter`, and a PX4 param **checker** (`px4_param_setter`, `auto_set` off by default; `on_mismatch` warn/halt) — setup guide at `docs/robot/px4_external_vision.md` +- NatNet server emulator (`optitrack.natnet.emulator`, protocol core) — pure-Python OptiTrack Motive server emulation so `natnet_ros2` can be driven without hardware; host integration tests (`tests/integration/natnet/`) wire it to the robot client +- Isaac wrapper for the NatNet emulator (USD scene → server) + natnet Pegasus launch scripts, and a dedicated OptiTrack sim e2e test (`optitrack` mark, `tests/system/test_optitrack_e2e.py`) that flies a **Circle trajectory on mocap EKF2 fusion** — GPS, baro and range aiding are disabled for the run, so the OptiTrack stream is the vehicle's only position source and cross-track error scores the whole chain + +### Changed + +- Isaac launch scripts deduplicated onto a shared `pegasus_app.PegasusApp` base (`simulation/isaac-sim/launch_scripts/pegasus_app.py`): the six scripts become scenario declarations (~40–170 lines each, net −438 lines) with hooks for NatNet/scene-import extras; behavior verified by full system-test parity (liveliness, sensors, takeoff/hover/land on Isaac). `ISAAC_SIM_HEADLESS` and `ISAAC_SIM_LIVESTREAM` now work uniformly in **every** launch script (previously each was honored by only half of them) +- Launch-workflow docs corrected against actual behavior: `ISAAC_SIM_SCENE` (nonexistent) replaced by `ISAAC_SIM_SCRIPT_NAME`/`ISAAC_SIM_GUI`, getting-started reflects the paused-by-default sim and Foxglove UI, isaac docker.md defaults table matches `.env`, ms-airsim MAVROS ports/FOV/vehicle naming fixed, AGENTS.md uses the real `down`/`image-build` command names +- Unit-test documentation now matches the co-located layout: the `add-unit-tests` and `run-system-tests` skills and the testing docs record which runner each language uses (C++ gtests via `colcon test` under the `build_packages` mark; Python via the root harness, plus `colcon test` for `ament_python` packages), and stop instructing authors to write `@pytest.mark.unit` by hand — `conftest.py` applies it by file location +- Ephemeral CI GPU runners spawn via NVIDIA OSMO (not OpenStack); `system-tests.yml` / `docker-build.yml` still use `airstack-ephemeral` +- Default system-test `--sim` is `isaacsim`; pass `--sim msairsim` to opt in to Microsoft AirSim +- `-m build_packages` CI runs pull `cache_*` images instead of baking sim images +- `docker-build.yml` retags unchanged images on VERSION bumps (content fingerprint) instead of always rebuilding; floating `cache_*` tags still seed PR layer cache +- Automatic OSMO validation runs the pull-only `build_packages` gate whenever a PR is opened, updated, or reopened; GPU-intensive simulation campaigns (including OptiTrack) are selected through `/pytest` or `workflow_dispatch` +- `robot-l4t` compose service knobs are now env-overridable (`AUTONOMY_ROLE`, `FCU_URL`, and the rosbag path via `BAG_STORAGE_PATH`); `FCU_URL` unquoted so the literal serial path reaches MAVROS +- `zed-l4t` image: ZED SDK 4.2 → 5.2 with the coupled ROS deps (`zed_msgs` 5.2.1, `point_cloud_transport(_plugins)` 4.x, add `backward_ros`) +- Unit tests are defined by `tests/colcon_unit_test_packages.yaml`: `conftest.py` collects each listed package's co-located `test/` dir under `--import-mode=importlib` and marks it `unit` (ament lint files are skipped and run under `colcon test`) + +### Removed + +- Pre-co-location unit-test scaffolding: the six per-layer stub READMEs under `tests/robot/` (which instructed authors to add tests in directories tests no longer live in) and `tests/sim/motive_emulator/README.md` (superseded by `simulation/isaac-sim/extensions/optitrack.natnet.emulator/` and `tests/integration/natnet/`) + +### Fixed + +- `barebones_pegasus_launch.py` (the documented template script) crashed with `NameError: os` on construction +- `isaac-sim-livestream` compose service silently produced a black stream when `ISAAC_SIM_SCRIPT_NAME` was a multi-drone script (livestream setup existed only in the single-drone scripts) +- `NATNET_BODY_NAME`/`NATNET_TARGET_NAME` env overrides documented by the single-drone NatNet script now actually work +- `airstack up` guards (one-simulator, URDF pairing) validated `.env` only and were bypassed by `--env-file overrides/...`; they now check the resolved configuration +- `pytest tests/` now collects the co-located unit tests before mark filtering. The old guard skipped injection whenever any path was on the command line, and `tests/` is a path — CI collected 97 of 252 items and the Python unit tests ran nowhere. Narrowing (`pytest tests/system/test_x.py`) still skips injection; repository-root and empty-path collection are rejected +- Empty CI pytest arguments no longer become `pytest tests/ ""` and recurse through the repository; collection/import, setup/teardown, partial, and interrupted artifacts are reported as non-comparable instead of false 0% simulation-policy results, and metric regression runs only for an identical simulation campaign fingerprint +- Isaac Sim image: PX4 `ubuntu.sh` no longer fails dpkg configure on the NVIDIA base (`ca-certificates` / `software-properties-common`); use `--no-nuttx --no-sim-tools` like ms-airsim +- Robot image: pin `pytest<8.1` and disable `launch_testing` for colcon unit tests so ROS Jazzy's outdated pytest hook does not abort `colcon test` +- Robot name resolution now honors a pre-set `ROBOT_NAME` (e.g. injected via docker compose) instead of always overriding it from the container/hostname mapping (`robot/docker/.bashrc`) +- Robot name-map catch-all fallback now maps to `unknown_robot` (valid ROS namespace token) instead of `unknown-robot` (`default_robot_name_map.yaml`) +- l4t robot image: replace dustynv's `/ros_entrypoint.sh` with a passthrough so its prebuilt source-ROS libs (older `fastcdr`) no longer shadow the apt Jazzy runtime and crash apt-built nodes like MAVROS +- `RECORD_BAGS=true` never brought the bag recorder up on a robot: `logging.launch.xml` hardcoded `record_bag=false` and `onboard_autonomy_all.launch.xml` includes it with no arguments, so the variable was forwarded into the container and read by nobody (only `gcs.launch.xml` consumed it). With no `bag_record` node running, the GCS control panel's `set_recording_status` toggle had nothing to reach despite being bridged in `domain_bridge.yaml` / `dds_router.yaml`. It now reads `RECORD_BAGS` and selects its topic set via `LOG_CONFIG` +- Falling back to `unknown_robot` / domain 0 now logs a warning naming both fixes (rename the device `robot-` on the host, or supply a `ROBOT_NAME_MAP_CONFIG_FILE` matching your hostnames). The fallback itself is unchanged — it deliberately keeps an unidentified robot out of every real robot's namespace — but it used to resolve silently, so the symptoms surfaced far from the cause +- Dropped `ROBOT_NAME` / `ROS_DOMAIN_ID` from `overrides/l4t-px4-realrobot.env`: no compose service declares either, so an env file could never set them and the lines were inert +- `bag_record/bag_recording_status` was bridged GCS -> robot in `domain_bridge.yaml`, the same direction as the command it answers, so recorder status never reached the GCS and every recording indicator stayed blank +- `bag_record_node` passed `--exclude` to `ros2 bag record`, which Jazzy renamed to `--exclude-regex`. It is now an ambiguous prefix of four options, so argparse rejected the command and any section using `exclude:` (including `log.yaml`'s `airstack` section, i.e. everything but the cameras) recorded nothing — surfacing only as a usage dump in the node's stdout. Multiple `exclude:` entries are now alternated into one regex instead of repeating a single-valued flag, which had silently kept only the last +- `natnet_config.yaml`'s `$(env NATNET_SERVER_IP ...)` could never resolve: no compose service declared the variable, so the NatNet client always fell back to its hardcoded default and could reach neither the in-sim emulator nor a real Motive host. It is now forwarded in `robot-base-docker-compose.yaml`, defaulting to the in-sim emulator +- The NatNet rigid body tracked by `robot_1` defaulted to a site-specific body (id 1146) that no emulator streams; since the client filters frames by numeric id, that produced a connected client that never published. It now defaults to the emulator's body (`Drone`, id 1). Per-robot bodies are configured in each robot's profile in `natnet_config.yaml`, selected by `ROBOT_NAME` +- OptiTrack external-vision tuning corrected from real-flight bags: `EKF2_EV_DELAY` 8.0 → 7.0 and `EKF2_EVP_NOISE` 0.01 → 0.05. The old 0.01 gave a 5 cm innovation gate (`EKF2_EVP_GATE` × 5σ) that rejected valid mocap updates and blocked arming; `px4_params.yaml` now records the supporting measurements and the drift-and-snap misdiagnosis so neither is repeated +- The synthetic GPS origin now places the mocap floor at the shared world datum (`desired_floor_amsl: 36.0`, i.e. 90 m ellipsoidal in AMSL) rather than at sea level, so a mocap robot's reported global altitude agrees with sim and the GCS. `local_position.z` still equals the OptiTrack height either way +- The robot image could ship without the GeographicLib `egm96-5` geoid: mavros' `install_geographiclib_datasets.sh` swallows a failed download and still exits 0, so the `RUN` layer succeeded either way, and `geographiclib-tools` was only ever a transitive dependency. MAVROS builds that geoid in its UAS core before any plugin loads and throws if it is missing, so `mavros_node` died at startup on affected images. `Dockerfile.robot` now pins the tool and asserts the file exists, failing the build instead +- An unrecognised `connection_type` in `natnet_config.yaml` silently fell back to `unicast`, so a typo produced a client that connected on the wrong transport and never received frames. `validate_connection_type` now throws and `natnet_ros2_node` fails at startup naming the offending value +- `natnet_ros2_node` on `robot_1` now compares the NatNet server's MODELDEF drone-body count (`Drone` / `Drone1`…`DroneN`, excluding `Target` and skeleton bones) against `NUM_ROBOTS` after the handshake and logs an error on mismatch, so a sim launch script and `natnet_config.yaml` that disagree about how many drones exist is caught at startup rather than as a robot that silently never receives frames. `NUM_ROBOTS` is forwarded into the robot container for it +- Isaac Sim PX4 never fused the mocap stream: `EKF2_EV_CTRL` defaults to 0 and the isaac compose set no PX4 parameters, so the emulator could stream perfectly while PX4 flew on sim GPS. The compose now passes the EKF2 external-vision set as `PX4_PARAM_*` (applied by PX4 SITL's `rcS` at boot), each defaulting to PX4's own default so non-mocap sims are unaffected; the mocap path opts in +- The NatNet emulator hardcoded the drone's streaming id to 1 while the client reads `NATNET_BODY_ID`, so a real Motive id desynced the two into a connected client that never published (`example_one_px4_pegasus_natnet_launch_script.py`) +- `test_optitrack_e2e.py::test_px4_fuses_vision` asserted only that `local_position/pose` publishes, which it does off GPS — the check passed with external vision disabled. It is now the pre-flight gate (an estimate exists) and the Circle flight is the actual proof of fusion +- The NatNet emulator is now installed as a Kit extension: `Dockerfile.isaac-ros` pip-installs it editable into the Isaac python and bind-mounts the repo copy over it (the same pattern as `pegasus.simulator`), and the natnet launch scripts `enable_extension` it before importing. Being on a Kit `--ext-folder` search path only makes Kit *aware* of an extension — it does not put the package on `sys.path` — so the scripts previously died with `ModuleNotFoundError: No module named 'optitrack'` + diff --git a/docs/robot/autonomy/adding_a_controller.md b/docs/robot/autonomy/adding_a_controller.md new file mode 100644 index 000000000..3cb97db93 --- /dev/null +++ b/docs/robot/autonomy/adding_a_controller.md @@ -0,0 +1,83 @@ +# Adding a Controller + +AirStack splits control into two roles ([Controls overview](local/controls/index.md)), and the first decision is which one you are replacing: + +- **Trajectory controller** — a pure-pursuit trajectory *manager*, not itself a feedback controller. It **owns the [trajectory group (spec §5)](interface_conventions.md#5-trajectory-group-the-trajectory-controllers-contract-onboard-only)**: it consumes `trajectory_controller/trajectory_segment_to_add` and `trajectory_override` (`airstack_msgs/msg/TrajectoryXYZVYaw`), serves the `set_trajectory_mode` service, and emits `tracking_point` and `look_ahead` (`airstack_msgs/msg/Odometry` — not `nav_msgs`). Reference: [Trajectory Controller](../../../robot/ros_ws/src/local/controls/trajectory_controller/README.md). +- **Feedback controller** — closes the loop between the tracking point and the vehicle's actual state and emits the [`control_setpoint` (spec §6)](interface_conventions.md#6-control_setpoint-controller-interface-command-onboard-only) command into the interface. Reference: `pid_controller` (`robot/ros_ws/src/local/controls/pid_controller` — no README; the cascaded position→velocity PID is described in the [Trajectory Controller README's Control Architecture section](../../../robot/ros_ws/src/local/controls/trajectory_controller/README.md#control-architecture)). + +The verified chain in every reference stack (`full_default`, `full_droan_cpu`, `full_macvo`, `full_mighty`, `lite_default`, `lite_offload_global` onboard) is: + +```text +trajectory_controller/tracking_point (airstack_msgs/Odometry, §5) + │ + odometry_conversion/odometry (§2) + ▼ +control/pid_controller ──► interface/cmd_roll_pitch_yawrate_thrust + (mav_msgs/RollPitchYawrateThrust, §6) ──► robot_interface → MAVROS/PX4 +``` + +**Both roles are onboard-only.** Spec §5 and §6 names may never appear in a split stack's `bridge.yaml` — `airstack doctor` hard-errors on it. The rationale is the spec's safety floor: command authority flows through the trajectory controller (arming, safety monitoring, takeover come for free to anything publishing `trajectory_override`), and that floor collapses if control crosses a link that can drop. A controller can never run offboard. + +This guide assumes you know the [layered architecture](index.md) and have flown a stack in sim. Link the [Interface Conventions Specification](interface_conventions.md) from your README instead of restating its tables. + +## Package or module? + +Decide early where the controller lives: + +- **In-tree package** — a package under `robot/ros_ws/src/local/controls/`, or a scaffolded module boundary in your fork via `airstack module create --in-tree `. +- **Module repo** — shareable, version-pinned, own CI and Docker dependency layer, added with `airstack module add --version `. See [AirStack Modules](../../development/modules.md) and the [create-module skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/create-module/SKILL.md). + +The wiring steps below are identical either way. + +## Path A: replace the feedback controller (the common case) + +Swap `pid_controller` for your own attitude/velocity controller. The §5 surface stays owned by the stock trajectory controller — you only consume its setpoint. + +Conventions worth copying from the reference (all in `pid_controller.cpp` / its launch file): + +- Runs as node `pid_controller` under the `control` namespace (node path `/robot_1/control/pid_controller`); gains load from a params YAML passed with `allow_substs="true"`. +- Gains are **dynamic parameters** (`airstack::dynamic_param` from `airstack_common`) — per-axis `p/i/d/ff/min/max/constant` plus a `_d_alpha` derivative filter — tunable at runtime with `ros2 param set`, no rebuild between tuning iterations. +- It exposes a `reset_integrators` subscription (`std_msgs/msg/Empty`, relative name in its namespace) so flight phases can clear integral windup; keep an equivalent if your controller integrates. +- It is control-rate agnostic: it computes on every `tracking_point` message (the trajectory controller ticks at 20 Hz) rather than running its own timer. + +### 1. Create the package + +Follow the [add-ros2-package skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-ros2-package/SKILL.md) and the [Module Integration Checklist](integration_checklist.md), under `robot/ros_ws/src/local/controls/`. Declare every topic endpoint as a launch argument defaulting to its canonical spec name — copy the pattern from `pid_controller/launch/pid_controller.launch.xml` (`pid_controller_odometry_topic`, `pid_controller_tracking_point_topic`, `pid_controller_command_topic`). + +**Verify:** `docker exec airstack-robot-desktop-1 bash -c "bws --packages-select "` exits cleanly. + +### 2. Conform to the interchange + +Inputs: `trajectory_controller/tracking_point` (§5, `airstack_msgs/msg/Odometry` — pose, velocity, acceleration, jerk along the trajectory) and `odometry_conversion/odometry` ([§2](interface_conventions.md#2-odometry-primary-state-estimate), `nav_msgs/msg/Odometry`). Output: one §6 command dialect — `interface/cmd_roll_pitch_yawrate_thrust` (`mav_msgs/msg/RollPitchYawrateThrust`, the blessed publisher slot the PID fills today) or the alternates `interface/cmd_pose` / `interface/cmd_velocity`. Never publish `tracking_point` or `look_ahead` yourself — that is impersonating the trajectory controller, and `doctor --live` flags it. + +**Verify:** with the node running under a full stack, `ros2 topic info /robot_1/interface/cmd_roll_pitch_yawrate_thrust` lists your node as the only publisher, and `ros2 topic info /robot_1/trajectory_controller/tracking_point` lists it as a subscriber. + +### 3. Wire it into a custom stack + +Controller variants are named stacks (see [Creating a Custom Stack Topology](../../development/creating_a_stack.md) and the [single-locus rule](../../development/stacks.md#the-single-locus-rule-and-its-lint)): `airstack stack new full_default full_my_controller`, then in `stacks/full_my_controller/launch/stack.launch.xml` replace the `pid_controller.launch.xml` include with your controller's include. Canonical arg defaults mean a conforming controller needs no include args. + +**Verify:** `airstack up --stack full_my_controller --sim isaac --robots 1 && airstack ready` succeeds and `ros2 node list` shows your controller in place of `control/pid_controller`. + +### 4. Verify with wiring and a flight + +1. `airstack test -m wiring --stack full_my_controller` regenerates `wiring.md`; `airstack stack diff full_default full_my_controller` should show exactly the controller swap. `airstack doctor --live --stack full_my_controller` must be clean. +2. Fly it: `airstack test -m takeoff_hover_land --sim isaacsim --num-robots 1 -v` — every command the vehicle receives in all four phases flows through your feedback controller, so this is the cheapest full-chain exercise (`waypoint_flight` adds planner behavior, not controller coverage). Then `airstack test -m autonomy --trajectory-types Circle,Figure8` for tracking *quality*: it flies fixed trajectories straight through the controllers and records cross-track error and path RMSE — the numbers your gains actually move. + +## Path B: replace the trajectory controller itself + +This is a much bigger lift: you take over the **entire §5 surface**, and every task server, the local planner, the feedback controller, and the safety monitor are your clients. Study the [Trajectory Controller README](../../../robot/ros_ws/src/local/controls/trajectory_controller/README.md) end-to-end before writing code. Your replacement must: + +- **Consume** `trajectory_controller/trajectory_segment_to_add` (appended segments from the local planner, stitched into the live trajectory near the current tracking position) and `trajectory_override` (complete replacement trajectories from takeoff/land and fixed-trajectory task servers). +- **Serve** `trajectory_controller/set_trajectory_mode` (`airstack_msgs/srv/TrajectoryMode`) with all five modes — `ROBOT_POSE`, `TRACK`, `ADD_SEGMENT`, `PAUSE`, `REWIND` — including the transition semantics in the README. Clients include `takeoff_landing_planner`, the fixed-trajectory task, the local planner, and `drone_safety_monitor`. +- **Publish** `tracking_point` and `look_ahead` (`airstack_msgs/msg/Odometry`), keeping `look_ahead` far enough ahead for the planner's cycle, plus `trajectory_completion_percentage` (`std_msgs/msg/Float32`), which task servers use to judge goal completion. +- **Broadcast** the four TF frames (`tracking_point`, `look_ahead_point`, and their `_stabilized` variants) the README documents. +- **Honor the safety integration**: the safety monitor commands `PAUSE`/`REWIND` through your mode service on state-estimate timeout — this path is why §5 is a spec, and it must work before anything else does. + +Wire it the same way as Path A step 3 (replace the `trajectory_controller.launch.xml` include; the `fixed_trajectory_task.launch.xml` include comes from the same package — replace or keep it deliberately), then verify as in Path A step 4 — but fly with `airstack test -m waypoint_flight --sim isaacsim --num-robots 1 -v`: its chain (takeoff → NavigateTask route → land) exercises the whole surface — `TRACK`/override for takeoff and landing, `ADD_SEGMENT` stitching under a continuously replanning local planner, and the mode transitions between them — where `takeoff_hover_land` never enters `ADD_SEGMENT`. + +## See also + +- [Interface Conventions Specification](interface_conventions.md) — §2 odometry, §5 trajectory group, §6 control_setpoint +- [Controls overview](local/controls/index.md) · [Trajectory Controller README](../../../robot/ros_ws/src/local/controls/trajectory_controller/README.md) +- [Creating a Custom Stack Topology](../../development/creating_a_stack.md) · [AirStack Stacks](../../development/stacks.md) +- [Module Integration Checklist](integration_checklist.md) +- Skills: [add-ros2-package](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-ros2-package/SKILL.md) · [create-module](https://github.com/castacks/AirStack/blob/develop/.agents/skills/create-module/SKILL.md) diff --git a/docs/robot/autonomy/adding_a_world_model_and_planner.md b/docs/robot/autonomy/adding_a_world_model_and_planner.md new file mode 100644 index 000000000..6c403ebf4 --- /dev/null +++ b/docs/robot/autonomy/adding_a_world_model_and_planner.md @@ -0,0 +1,118 @@ +# Adding a World Model and Planner + +AirStack has two planner slots — and behind each, a world-model slot. The first decision is which one you are filling: a local planner (Path A), a global planner (Path B), or a world model paired with a planner (Path C): + +- **Local planner** — a perpetual node plus a `NavigateTask` server that consumes the [`global_plan` (spec §4)](interface_conventions.md#4-global_plan-global-waypoint-path), a world model input (disparity or point clouds), and the trajectory controller's `look_ahead`/`tracking_point`, and emits short collision-free segments on the [trajectory-controller surface (spec §5)](interface_conventions.md#5-trajectory-group-the-trajectory-controllers-contract-onboard-only) (`trajectory_controller/trajectory_segment_to_add`, `airstack_msgs/msg/TrajectoryXYZVYaw`). Reference: [DROAN](../../../robot/ros_ws/src/local/planners/droan_local_planner/README.md) ([overview](local/planning/index.md)). Spec §5 is **onboard-only** — a local planner can never run offboard. +- **Global planner** — a [task executor](tasks.md): an action server at `tasks/` ([spec §8](interface_conventions.md#8-tasks-task-action-servers)) that plans only while a goal is active, publishes the coarse path on [`global_plan` (spec §4)](interface_conventions.md#4-global_plan-global-waypoint-path), and delegates flying to the local planner via `tasks/navigate`. Reference: [Random Walk](../../../robot/ros_ws/src/global/planners/random_walk/README.md) ([overview](global/planning/index.md)). `global_plan` is the one interchange that may cross a machine boundary, so a global planner may run offboard (`lite_offload_global`). + +- **World model** — the representation a planner plans against. This is *two different contracts* — read the next section before picking a lane. + +## World models: a matched pair locally, a spec'd interchange globally + +**Local world models** feed the local planner a fast short-range obstacle representation. The reference is the disparity pipeline ([overview](local/world_model/index.md)): [disparity_expansion](../../../robot/ros_ws/src/local/world_models/disparity_expansion/README.md) (C-space expansion of stereo disparity by the robot radius) → [disparity_graph](../../../robot/ros_ws/src/local/world_models/disparity_graph/README.md) (rolling window of expanded-disparity keyframes with camera poses) → [disparity_graph_cost_map](../../../robot/ros_ws/src/local/world_models/disparity_graph_cost_map/README.md) (a `cost_map_interface` plugin the CPU DROAN planner loads via its `cost_map` parameter to score candidate trajectories). The GPU planner `droan_gl` does the expansion and graph internally on the GPU and consumes raw disparity directly. + +**Be honest about the local contract: there isn't a spec-level one.** Unlike `global_plan` or the trajectory group, the local world-model ↔ planner interface is **not** an interchange in the [Interface Conventions Specification](interface_conventions.md) — a local planner and its world model are a **matched pair**, wired together in the stack entry. Compare the reference stacks: `full_default` includes `droan_gl` alone (disparity in from `perception/stereo_image_proc/disparity`, world model internal), while `full_droan_cpu` includes `droan_local_planner` **plus** `disparity_expansion`, the planner consuming the expansion clouds by relative name in the shared `droan` namespace. Adding a new local world model therefore usually means adapting a planner to consume it (e.g. implementing the `cost_map_interface` plugin API) or bringing a paired planner with it. + +**Global world models** *are* spec'd: [`global_map` (spec §3)](interface_conventions.md#3-global_map-global-world-model) — today the [vdb_mapping_ros2](../../../robot/ros_ws/src/global/world_models/vdb_mapping_ros2/README.md) topics (`vdb_mapping/vdb_map_visualization` is the de-facto interchange the reference global planner consumes, plus the update-grid and point-cloud exports). A new global world model that produces the §3 surface drops in for the global planner without touching it. + +This guide assumes you know the [layered architecture](index.md) and have flown a stack in sim. Link the [Interface Conventions Specification](interface_conventions.md) from your README instead of restating its tables. + +## Package or module? + +Decide early where the new code lives: + +- **In-tree package** — fastest for trunk work: a package under `robot/ros_ws/src/local/planners/`, `robot/ros_ws/src/global/planners/`, or the matching `world_models/` directory, or a scaffolded module boundary in your fork via `airstack module create --in-tree ` (lands under `robot/ros_ws/src/modules/`). +- **Module repo** — shareable, version-pinned, own CI and Docker dependency layer, added with `airstack module add --version `. See [AirStack Modules](../../development/modules.md) (the [researcher fork → module workflow](../../development/modules.md#the-researcher-workflow-fork-module)) and the [create-module skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/create-module/SKILL.md); [asm_macvo](../../modules/macvo.md) is the worked precedent for a capability shipped this way. + +The wiring steps below are identical either way — a module's launch file is included by a stack entry file exactly like a trunk package's. + +## Path A: local planner + +### 1. Create the package + +Follow the [add-ros2-package skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-ros2-package/SKILL.md) and the [Module Integration Checklist](integration_checklist.md), under `robot/ros_ws/src/local/planners/`. Declare every topic endpoint as a launch argument defaulting to its canonical spec name — a conventional stack then includes you with zero remaps. + +**Verify:** `docker exec airstack-robot-desktop-1 bash -c "bws --packages-select "` exits cleanly. + +### 2. Conform to the interchange + +Inputs: `global_plan` (§4, `nav_msgs/Path`, `map` frame), your world model topic, `odometry_conversion/odometry` ([§2](interface_conventions.md#2-odometry-primary-state-estimate)), and the controller's `look_ahead` (§5 — plan from the look-ahead point, not the current pose). Output: `trajectory_controller/trajectory_segment_to_add` (§5). Serve `NavigateTask` at `tasks/navigate` (goal/feedback/result fields in [Task Executors → NavigateTask](tasks.md#navigatetask)); the [add-task-executor skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-task-executor/SKILL.md) covers the four action callbacks. Emitting `trajectory_segment_to_add` (rather than commanding the interface directly) is what buys you arming, safety monitoring, and takeover for free — the spec's safety floor. For candidate-trajectory generation, scoring helpers, and `TrajectoryXYZVYaw` conversion, use the [trajectory_library](../../../robot/ros_ws/src/local/planners/trajectory_library/README.md) instead of rolling your own. + +**Verify:** with the node running under a full stack, `docker exec airstack-robot-desktop-1 bash -c "sws && ros2 topic info /robot_1/trajectory_controller/trajectory_segment_to_add"` lists your node as a publisher and the trajectory controller as the subscriber, and `ros2 action list` shows `/robot_1/tasks/navigate`. + +### 3. Wire it into a stack + +Planner variants are named stacks, not launch arguments — the [single-locus rule](../../development/stacks.md#the-single-locus-rule-and-its-lint) puts every wiring deviation in the stack entry file. The worked swap example is [full_droan_cpu](../../../stacks/full_droan_cpu/README.md): byte-identical to `full_default` except the local-planner block, where `stacks/full_droan_cpu/launch/stack.launch.xml` replaces the single `droan_gl.launch.xml` include with two lines: + +```xml + + +``` + +Do the same for yours: `airstack stack new full_default full_my_planner`, then in `stacks/full_my_planner/launch/stack.launch.xml` replace the DROAN include with your planner's include (plus any world-model include it needs). Only deviations from canonical names appear as include args — see how `full_macvo` passes exactly one (`droan_gl_disparity_topic`). For a planner that ships as an **external module**, the worked example is [full_mighty](../../../stacks/full_mighty/README.md): its `modules.repos` pins the [mighty module](../../modules/mighty.md) (planner + voxel world model + bridge), and the local-planner block is one `mighty_module.launch.xml` include. + +**Verify:** `airstack up --stack full_my_planner --sim isaac --robots 1 && airstack ready` succeeds and `ros2 node list` shows your planner in place of DROAN. + +### 4. Verify with wiring and a flight + +1. Snapshot and diff the wiring: `airstack test -m wiring --stack full_my_planner` regenerates `stacks/full_my_planner/wiring.md`; the diff against `full_default` should be exactly your planner block. `airstack stack diff full_default full_my_planner` compares the generated wiring directly. +2. Live check: `airstack doctor --live --stack full_my_planner` — doctor flags anything but the trajectory controller publishing `look_ahead`/`tracking_point`, and hard-errors if §5 names ever appear in a split stack's `bridge.yaml`. +3. Fly it: `airstack test -m waypoint_flight --sim isaacsim --num-robots 1 --stress-iterations 1 -v` drives a waypoint route through your `tasks/navigate` server and judges the odometry track. For a manual flight, take off from the GCS and send a `NavigateTask` goal (`ros2 action send_goal --feedback /robot_1/tasks/navigate task_msgs/action/NavigateTask ...` with a `global_plan` path and `goal_tolerance_m`), or use the [GCS waypoint editor](../../gcs/waypoints_and_geofences.md). (`airstack test -m autonomy` flies fixed trajectories straight through the controller — it checks the stack still flies, but never touches your planner.) + +## Path B: global planner + +### 1. Create the package + +Same as Path A step 1, under `robot/ros_ws/src/global/planners/`. Keep planning logic in a ROS-free class ([Global Planning](global/planning/index.md) explains why); the node wraps it. + +**Verify:** `bws --packages-select ` exits cleanly. + +### 2. Implement it as a task executor + +Follow [Adding a New Task Executor](tasks.md#adding-a-new-task-executor) and the [add-task-executor skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-task-executor/SKILL.md): pick or add a `.action` type in `task_msgs`, implement the four action callbacks, and remap the server to `tasks/` in your module launch file (§8). Subscribe to the map ([`global_map`, spec §3](interface_conventions.md#3-global_map-global-world-model) — today the VDB visualization topic) and odometry (§2); publish `global_plan` (§4, `nav_msgs/Path` in the `map` frame, last pose = goal) and delegate flying by sending `NavigateTask` goals to `tasks/navigate` while your goal is active — exactly the [random_walk cascade](../../../robot/ros_ws/src/global/planners/random_walk/README.md). + +**Verify:** `ros2 action list` shows `/robot_1/tasks/`, and `ros2 topic info /robot_1/global_plan` lists your node as publisher and the local planner as subscriber. + +### 3. Wire it into a stack + +Swapping the global planner is replacing one include: in your copy of the stack entry file (`airstack stack new full_default full_my_global`), replace the `random_walk_planner.launch.xml` include with yours — the same one-line swap the [exploration planner](global/planning/index.md#example-planners) documents. The node keeps its canonical names, so no include args are needed unless you deviate. + +**Verify:** `airstack up --stack full_my_global --sim isaac --robots 1 && airstack ready`; `ros2 node list` shows your planner and no `random_walk_node`. + +### 4. Verify with wiring and a flight + +1. `airstack test -m wiring --stack full_my_global` then `airstack stack diff full_default full_my_global` — the only delta is the global-planner swap; `airstack doctor --live --stack full_my_global` is clean. +2. Fly it: take off, then activate your task, e.g. for an exploration-type planner `ros2 action send_goal /robot_1/tasks/exploration task_msgs/action/ExplorationTask '{...}' --feedback` (a full goal example is in [Task Executors](tasks.md#explorationtask)), and watch `ros2 topic echo /robot_1/global_plan --once` update and the drone follow it. `airstack test -m takeoff_hover_land` confirms you have not disturbed the base flight chain. + +## Path C: new world model + +### 1. Create the package + +Same as Path A step 1, under `robot/ros_ws/src/local/world_models/` or `robot/ros_ws/src/global/world_models/`. + +**Verify:** `bws --packages-select ` exits cleanly. + +### 2. Produce the right surface + +- **Global:** produce the [`global_map` (spec §3)](interface_conventions.md#3-global_map-global-world-model) surface — at minimum the visualization-topic interchange the reference global planner consumes today — from your sensor input (the VDB map takes the filtered LiDAR cloud). Conform and the existing global planner needs no changes. +- **Local:** there is no spec surface to hit — produce the representation your **paired planner** consumes, and treat the disparity pipeline as the worked example of the pairing: `disparity_expansion` publishes expansion clouds the CPU DROAN planner reads by relative name, while the graph + cost map reach the planner as a `cost_map_interface` plugin selected by its `cost_map` parameter. If you keep DROAN, implementing that plugin API is the smallest integration; a different planner means adapting it to your representation (or writing one — Path A). + +**Verify (local):** with the pair running, `ros2 topic info` on your world-model output lists the planner as a subscriber (or the planner logs loading your cost-map plugin). **Verify (global):** `ros2 topic info /robot_1/vdb_mapping/vdb_map_visualization`-equivalent shows your node as publisher and the global planner as subscriber. + +### 3. Wire the pair into a stack + +A local world model and its planner are swapped **together** — [full_droan_cpu](../../../stacks/full_droan_cpu/README.md) is the template, replacing `full_default`'s single `droan_gl` include with the `droan_local_planner` + `disparity_expansion` pair (the two-line swap shown in Path A step 3). Do the same: `airstack stack new full_default full_my_wm`, then swap in your world-model include plus its paired planner's include in `stacks/full_my_wm/launch/stack.launch.xml`. For a global world model, replace the `vdb_mapping_ros2` include (and its `config` arg) with yours. + +**Verify:** `airstack up --stack full_my_wm --sim isaac --robots 1 && airstack ready`; `ros2 node list` shows the new pair. + +### 4. Verify with wiring and a flight + +Same as Path A step 4: `airstack test -m wiring --stack full_my_wm`, `airstack stack diff full_default full_my_wm` (the delta is exactly the pair swap), `airstack doctor --live --stack full_my_wm`, then `airstack test -m waypoint_flight` — a waypoint route through obstacles is what actually consults the world model. For a global world model, also confirm `ros2 topic echo /robot_1/global_plan --once` updates while an exploration-type task runs against your map. + +## See also + +- [Interface Conventions Specification](interface_conventions.md) — §2 odometry, §3 global_map, §4 global_plan, §5 trajectory group, §8 tasks +- [Module Integration Checklist](integration_checklist.md) — package structure, launch conventions, integration testing commands +- [Local Planning](local/planning/index.md) · [Global Planning](global/planning/index.md) · [Local World Model](local/world_model/index.md) — layer overviews and references +- [Task Executors](tasks.md) — action types and the task cascade +- Skills: [add-ros2-package](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-ros2-package/SKILL.md) · [add-task-executor](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-task-executor/SKILL.md) · [create-module](https://github.com/castacks/AirStack/blob/develop/.agents/skills/create-module/SKILL.md) diff --git a/docs/robot/autonomy/behavior/behavior_executive.md b/docs/robot/autonomy/behavior/behavior_executive.md deleted file mode 100644 index 61d849279..000000000 --- a/docs/robot/autonomy/behavior/behavior_executive.md +++ /dev/null @@ -1,19 +0,0 @@ -# Behavior Executive - -The behavior executive reads which actions are active from the behavior tree and implements the behavior which these actions should perform and sets the status of the actions to SUCCESS, RUNNING, or FAILURE. It also sets the status of conditions as either SUCCESS or FAILURE. - -A typical way of implementing the behavior for an action is the following in the 20 Hz timer callback: - -``` -if(action->is_active()){ - if(action->active_has_changed()){ - // This is only true when the when the action transitions between active/inactive - // so this block of code will only run once whenever the action goes from being inactive to active. - // You might put a service call here and then call action->set_success() or action->set_failure() - // based on the result returned by the service call. - } - - // Code here will get executed each iteration. - // You might call action->set_running() while you are doing work here. -} -``` \ No newline at end of file diff --git a/docs/robot/autonomy/behavior/behavior_tree.md b/docs/robot/autonomy/behavior/behavior_tree.md deleted file mode 100644 index bd7577539..000000000 --- a/docs/robot/autonomy/behavior/behavior_tree.md +++ /dev/null @@ -1,95 +0,0 @@ -# Behavior Trees - -Defines how a task in terms of conditions and actions which the user -implements. - -Other types of nodes, control flow and decorator nodes, control which -conditions will be checked and which actions will be activated. - -Nodes have statuses of either SUCCESS, RUNNING or FAILURE. - -![](./media/image1.png) - -## Why Behavior Trees? - -Maintainable - Easy to modify - -Scalable - Parts of sub-trees are modular and can be encapsulated - -Reusable - Sub-trees can be reused in different places - -Clear visualization and interpretation - -## Types of Nodes - -- **Execution Nodes** - - Condition Nodes - - Action Nodes -- **Decorator Nodes** - - Not Node -- **Control Flow Nodes** - - Sequence Nodes - - Fallback Nodes - -### Execution Nodes - Condition Nodes - -Condition nodes have a status of either SUCCESS or FAILURE - -![](./media/image2.png) ![](./media/image3.png) - -### Execution Nodes - Action Nodes - -Action nodes can either be active or inactive - -An inactive node's status is not checked by the behavior tree, it is -shown in white - -below - -An active node's status is checked, it can either be SUCCESS (green), -RUNNING (blue) or FAILURE (red) - -![green](./media/image4.png) ![white](./media/image5.png)![blue](./media/image6.png)![red](./media/image7.png) - -### Decorator Nodes - Not Nodes - -The not node must have one condition node has a child and inverts the -status of the child. - -If the child's status is SUCCESS, the not node's status will be FAILURE. - -If the child's status is FAILURE, the not node's status will be SUCCESS. - -![](./media/image8.png) - -### Control Flow Nodes - Fallback Nodes - -These nodes are shown with a ? - -This node returns FAILURE if and only if all of its children return -FAILURE - -If one of its children return RUNNING or SUCCESS, it returns RUNNING or -SUCCESS and no subsequent children's statuses are check - -Below shows a typical example, where an action will only be performed if -all of the preceding conditions are false. In this case a drone will only be -armed if it is not already armed, it is in offboard mode and it is stationary - -![](./media/image8.png) - -### Control Flow Nodes - Sequence Nodes - -These nodes are shown with a "-\>" - -This node returns SUCCESS if and only if all of its children return -SUCCESS - -If one of its children return RUNNING or FAILURE, it returns RUNNING or -FAILURE and no subsequent children's statuses are check - -Below shows a typical example where preceding conditions must be true in -order for an action to be performed. In this case the drone will land if the IMU -times out and it is in offboard mode - -![](./media/image8.png) diff --git a/docs/robot/autonomy/behavior/index.md b/docs/robot/autonomy/behavior/index.md index 17bf102f6..eae6be9c1 100644 --- a/docs/robot/autonomy/behavior/index.md +++ b/docs/robot/autonomy/behavior/index.md @@ -1,8 +1,21 @@ # Behavior -The behavior module is responsible for the high-level decision making of the robot. This includes deciding what actions to take based on the current state of the robot and the world around it. The behavior module is responsible for coordinating the actions of the local and global modules to achieve the robot's goals. + +The behavior layer, as shipped, is a **safety executive**: it watches the health of the running stack and issues safety commands when something goes wrong. High-level mission sequencing does not live here — task goals (takeoff, land, explore, navigate) are sent by the operator from the GCS to the [task executors](../tasks.md) hosted in the global and local layers; see [System Architecture — Task Cascade](../system_architecture.md#task-cascade). ## Launch -Launch files are under `src/robot/autonomy/behavior/behavior_bringup/launch`. -The main launch command is `ros2 launch behavior_bringup behavior.launch.xml`. +Behavior modules ship their own canonical launch files and are composed by +the selected stack's entry launch file, e.g. +`ros2 launch drone_safety_monitor drone_safety_monitor.launch.xml` — see +`stacks/full_default/launch/stack.launch.xml` for the composed wiring. + +## Modules + +- **`drone_safety_monitor`** (`robot/ros_ws/src/behavior/drone_safety_monitor`) — the safety executive: watches the state + estimate for timeouts and issues safety commands. It runs onboard so the + robot can failsafe even if every ground link is lost. + +## Key Interchanges +- [`safety` (§9)](../interface_conventions.md#9-safety-safety-executive-onboard-only) — the safety executive's onboard-only topics (`state_estimate_timed_out`, `command`); these may never cross a split-stack bridge +- [`tasks/*` (§8)](../interface_conventions.md#8-tasks-task-action-servers) — where mission-level goals actually enter the stack diff --git a/docs/robot/autonomy/behavior/media/image1.png b/docs/robot/autonomy/behavior/media/image1.png deleted file mode 100644 index 135407aed..000000000 Binary files a/docs/robot/autonomy/behavior/media/image1.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image10.png b/docs/robot/autonomy/behavior/media/image10.png deleted file mode 100644 index 81ecbd255..000000000 Binary files a/docs/robot/autonomy/behavior/media/image10.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image2.png b/docs/robot/autonomy/behavior/media/image2.png deleted file mode 100644 index efd5b5942..000000000 Binary files a/docs/robot/autonomy/behavior/media/image2.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image3.png b/docs/robot/autonomy/behavior/media/image3.png deleted file mode 100644 index 6b7f368d3..000000000 Binary files a/docs/robot/autonomy/behavior/media/image3.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image4.png b/docs/robot/autonomy/behavior/media/image4.png deleted file mode 100644 index ba20ef619..000000000 Binary files a/docs/robot/autonomy/behavior/media/image4.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image5.png b/docs/robot/autonomy/behavior/media/image5.png deleted file mode 100644 index 35647b040..000000000 Binary files a/docs/robot/autonomy/behavior/media/image5.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image6.png b/docs/robot/autonomy/behavior/media/image6.png deleted file mode 100644 index 3aefd3b41..000000000 Binary files a/docs/robot/autonomy/behavior/media/image6.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image7.png b/docs/robot/autonomy/behavior/media/image7.png deleted file mode 100644 index ae0061e02..000000000 Binary files a/docs/robot/autonomy/behavior/media/image7.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image8.png b/docs/robot/autonomy/behavior/media/image8.png deleted file mode 100644 index c6a313d6c..000000000 Binary files a/docs/robot/autonomy/behavior/media/image8.png and /dev/null differ diff --git a/docs/robot/autonomy/behavior/media/image9.png b/docs/robot/autonomy/behavior/media/image9.png deleted file mode 100644 index 83265b267..000000000 Binary files a/docs/robot/autonomy/behavior/media/image9.png and /dev/null differ diff --git a/docs/robot/autonomy/coordination/creating_coordination_algorithms.md b/docs/robot/autonomy/coordination/creating_coordination_algorithms.md new file mode 100644 index 000000000..90f473849 --- /dev/null +++ b/docs/robot/autonomy/coordination/creating_coordination_algorithms.md @@ -0,0 +1,78 @@ +# Creating a Multi-Agent Coordination Algorithm + +Each robot runs on its own DDS domain ([robot N gets `ROS_DOMAIN_ID=N`](../../docker/robot_identity.md)), so robots cannot see each other's topics directly — a node on `robot_1` will never discover `/robot_2/odometry`. The supported cross-robot channel is the [coordination layer](index.md): each robot's `gossip_node` broadcasts a `PeerProfile` (GPS, heading, current waypoint, plus arbitrary payloads) on the shared gossip domain (default **99**), bridged from the robot's own domain by a dedicated DDS router. A coordination algorithm in AirStack is therefore: attach your state as a gossip payload, consume the peer registry, decide, and act through the stack's normal interfaces. + +Canonical names, types, and QoS for the gossip channel are in the [Interface Conventions Spec §10](../interface_conventions.md#10-gossip--multi-robot-coordination). + +## Step 1 — Decide what state must cross robots, and attach it as a payload + +The `PeerProfile` already carries GPS position, heading, and the current waypoint for free — many coordination schemes (spatial dispersion, follow-the-leader, deconfliction) need nothing more. If your algorithm needs additional per-robot state (a frontier map, a task bid, a mode string), publish it as a normal topic on the robot's own domain and declare it in `gossip_payloads.yaml` — the gossip node serializes the latest message onto every 1 Hz profile tick, transforming `MarkerArray`/`PointCloud2` payloads into global ENU on the way out. The full recipe is [Payloads & Foxglove Visualization](payloads.md) (or run the [attach-gossip-payload skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/attach-gossip-payload/SKILL.md)); don't re-derive it here. Keep payloads small — they ride inside every profile message at the gossip rate. + +**Verify:** `ros2 topic echo /gossip/peers --field payloads` shows an entry with your `payload_type` string. + +## Step 2 — Write the coordination node that consumes peer profiles + +Your node runs on the robot's **own** domain, like every other stack node. Two verified inputs: + +- **`/{robot_name}/coordination/peer_registry`** (`coordination_msgs/msg/PeerProfile`, RELIABLE + TRANSIENT_LOCAL) — the gossip node's latest-wins snapshot of every known peer, republished on the robot's domain each time a peer updates. TRANSIENT_LOCAL means a late-joining node receives the current registry immediately. This is the right input for in-stack consumers: no domain gymnastics, dedup and monotonic-timestamp filtering already done by `gossip_node`. +- **`/gossip/peers`** (same type, BEST_EFFORT) — the raw bus, but only visible on domain 99. Use it for out-of-stack tooling (this is what `peer_registry_monitor` subscribes to), not for stack nodes. + +Deserialize payloads with the helper API from `common/ros_packages/coordination/coordination_bringup/coordination_bringup/peer_profile.py`: + +```python +from coordination_msgs.msg import PeerProfile as PeerProfileMsg +from coordination_bringup.peer_profile import PeerProfile + +def on_peer(self, msg: PeerProfileMsg): + profile = PeerProfile.from_ros_msg(msg) + bid = profile.get_payload("std_msgs/msg/String") # by type + cloud = profile.get_payload_by_name("raw_frontiers") # by topic tail +``` + +For outputs, use the stack's existing verified command mechanisms rather than inventing a side channel: publish a `global_plan` (`nav_msgs/msg/Path` — [the interchange](../interface_conventions.md#4-global_plan--global-waypoint-path) the local planner consumes), or call a task action server under `tasks/*` (e.g. `tasks/navigate` / `task_msgs/action/NavigateTask`, [§8](../interface_conventions.md#8-tasks--task-action-servers)). To share your decision back to peers, attach it as another payload (Step 1). + +**Package or module?** The node can live in-tree as a normal ROS 2 package ([add-ros2-package skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-ros2-package/SKILL.md)), or ship as a standalone module repo pinned by whichever stacks use it — the same pattern as `asm_macvo`. See [AirStack Modules](../../../development/modules.md) and the [create-module skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/create-module/SKILL.md); `airstack module create --in-tree ` scaffolds the module boundary in a fork. + +**Verify:** with the stack running, `docker exec airstack-robot-desktop-1 bash -c "ros2 topic echo /robot_1/coordination/peer_registry --once"` returns a `PeerProfile`, and your node appears in `ros2 node list`. + +## Step 3 — Wire it into a stack + +The gossip layer itself is already included in every reference stack — each entry launch file includes `coordination_bringup`'s `gossip.launch.xml` (e.g. `stacks/full_default/launch/stack.launch.xml`, "Gossip coordination layer" block, passing `gossip_domain`/`gossip_publish_rate`). You do **not** add anything to make peer profiles flow. + +Your coordination node is a new module include in the stack entry file — one `` with your topic args, per the single-locus rule. If you're modifying a topology, make your own stack first: see [Creating a Custom Stack Topology](../../../development/creating_a_stack.md) and the [integrate-module-into-layer skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/integrate-module-into-layer/SKILL.md). Do not touch `coordination_bringup` or `autonomy_bringup`. + +**Verify:** `airstack up --stack --sim isaac` brings the node up (`ros2 node list` inside the robot container shows it alongside `gossip_node`). + +## Step 4 — Test with multiple robots + +```bash +airstack up --sim isaac --robots 3 # replicas: airstack-robot-desktop-1/-2/-3, domains 1/2/3 +# or, for heterogeneous fleets / split stacks: +airstack up --fleet --sim isaac # config/fleets/.yaml +airstack ready +``` + +`--robots N` also selects the multi-robot Isaac spawn script; fleet identity/placement details are in [AirStack Fleets](../../../development/fleets.md). + +Watch the peer registry converge — every robot should list every other: + +```bash +ROS_DOMAIN_ID=99 ros2 run coordination_bringup peer_registry_monitor # the shared bus view +ROS_DOMAIN_ID=1 ros2 run coordination_bringup peer_registry_monitor # what robot_1 actually receives +ros2 topic echo /gossip/peers # raw messages +``` + +(Run these inside a robot container via `docker exec ... bash -c "..."`.) Then exercise your algorithm and confirm the decisions land: the acted-on robot's `global_plan` changes, or the `tasks/*` action goal is accepted. + +**Verify:** the monitor shows all N robots with fresh timestamps and your `payload_type` listed per peer, and your node's output topic/action responds on each robot. + +## What the platform does not give you yet + +Read `common/ros_packages/coordination/README.md` before designing around these: + +- **No relay/multi-hop.** The `source`/`relay_hops` wire fields are reserved; relay logic is not active. Every robot must reach the shared gossip domain directly. +- **No payload delta suppression.** Payloads are re-serialized and re-sent on every publish tick even when unchanged (payload version hashing is a future plan) — budget bandwidth accordingly. +- **Delivery is BEST_EFFORT and at-least-once.** The seen-set (`(robot_name, stamp)` dedup, 50-entry FIFO) prevents re-processing, not loss; design decisions to be idempotent under a 1 Hz latest-wins stream. +- **Registry entries are never evicted.** A crashed peer stays in the registry until the node restarts — check profile timestamps yourself if liveness matters. + +Consensus, task allocation, and leader election are yours to build on top of this bus; the platform provides the state-sharing substrate, not the algorithms. 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..5072cee3c 100644 --- a/docs/robot/autonomy/dds_router.md +++ b/docs/robot/autonomy/dds_router.md @@ -46,7 +46,7 @@ All topics are **bidirectional** by default. ## Launch file: `interpolate_dds_router.launch.py` -**Location:** [`robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py`](../../../../robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py) +**Location:** [`robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py`](../../../robot/ros_ws/src/autonomy_bringup/launch/interpolate_dds_router.launch.py) DDS Router consumes a plain YAML file, but the router configs in AirStack need runtime values (domain IDs, robot names) and shared base configs. `interpolate_dds_router.launch.py` adds three capabilities on top of plain YAML before handing the file to `ddsrouter`: @@ -57,19 +57,27 @@ 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 "Prefixed launch arguments" + The canonical arguments are the prefixed `dds_router_config_file` / + `dds_router_args` — generic names like `config_file` leak across sibling + includes in the same launch scope, because ROS 2 launch configurations + are global. The generic `config_file` / `args` names are accepted as + **deprecated aliases** (the prefixed name wins when both are set); + prefer the prefixed names in all callers. + ### 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: @@ -100,11 +108,12 @@ some_key: !reset ## Config files -### `onboard_all/config/dds_router.yaml` — base config +### `autonomy_bringup/config/dds_router.yaml` — shared allowlist -**Location:** [`robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml`](../../../../robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml) +**Location:** [`robot/ros_ws/src/autonomy_bringup/config/dds_router.yaml`](../../../robot/ros_ws/src/autonomy_bringup/config/dds_router.yaml) -Used when the robot role is `full` or `onboard` and both robot and GCS share the same physical machine or are bridged by this router. +Selected by the `full_*` stacks and `lite_default` (their entry files pass it +to `interpolate_dds_router.launch.py`). **Participants:** @@ -117,50 +126,45 @@ Used when the robot role is `full` or `onboard` and both robot and GCS share the | Topic / Service | |---| +| `rt//sensors/ouster/point_cloud` | +| `rt//vdb_mapping/vdb_map_visualization` | +| `rt//sensors/front_stereo/{left,right}/image_rect` + `camera_info` | +| `rt//perception/stereo_image_proc/point_cloud` | | `rt//odometry_conversion/odometry` | -| `rt//interface/mavros/global_position/raw/fix` | -| `rt//behavior/behavior_tree_commands` | -| `rt//behavior/behavior_tree_graphviz` | +| `rt//interface/mavros/global_position/global` | +| `rt//trajectory_controller/trajectory_vis` | +| `rt//global_plan` | | `rq+rr//interface/robot_command` | | `rq+rr//trajectory_controller/set_trajectory_mode` | | `rq+rr//takeoff_landing_planner/set_takeoff_landing_command` | | `rq+rr//behavior/global_plan_toggle` | | `rt//bag_record/bag_recording_status` | | `rt//bag_record/set_recording_status` | -| `rt//fixed_trajectory_generator/fixed_trajectory_command` | - - ---- - -### `onboard_local_offboard_global/config/dds_router.yaml` — extended config - -**Location:** [`robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/config/dds_router.yaml`](../../../../robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/config/dds_router.yaml) -Used for the `desktop_split`, `l4t_lite + offboard`, and `voxl + offboard` deployment profiles where local planning runs onboard and global planning runs on the GCS. +Gossip peer profiles are deliberately **not** in this allowlist — they are +bridged by the dedicated gossip DDS router on domain 99 (bridging them here +too would cause message amplification). -This config **inherits from `onboard_all`** via `extends:` and appends additional topics: -```yaml -extends: "$(find-pkg-share autonomy_bringup)/onboard_all/config/dds_router.yaml" -``` - -**Additional topics (appended to the base allowlist):** +--- -| Topic | -|---| -| `rt//sensors/front_stereo/left/image_rect` | -| `rt//sensors/front_stereo/left/camera_info` | -| `rt//sensors/front_stereo/right/image_rect` | -| `rt//sensors/front_stereo/right/camera_info` | -| `rt//global_plan` | +### Split-stack router config — generated from `bridge.yaml` +The split stack (`stacks/lite_offload_global`) does NOT use a hand-written +router config: its [`bridge.yaml`](../../../stacks/lite_offload_global/bridge.yaml) +is the authoritative boundary document, and `tools/gen_dds_router.py` +generates `.airstack/generated/dds_router.lite_offload_global.yaml` from it +(loaded by the stack's `onboard` entry). -The stereo image topics let the global planner on the GCS observe the robot's environment. `global_plan` carries the resulting path back to the onboard local planner. +The generated config deliberately contains no `set_trajectory_mode` +crossing: command authority stays onboard — control-mode and +trajectory-group names may never cross a split-stack bridge +(`airstack doctor` hard gate #2). --- ## Adding a new bridged topic -1. Decide which config applies (`onboard_all` for all roles, `onboard_local_offboard_global` for split-only). -2. Add the topic to the appropriate `allowlist`, using the correct DDS prefix (`rt/`, `rq/`, `rr/`, etc.) and the `$(env ROBOT_NAME)` substitution for the robot namespace. +1. Decide where it belongs: the shared `autonomy_bringup/config/dds_router.yaml` allowlist (full/lite stacks), or the split stack's `bridge.yaml` (then regenerate with `tools/gen_dds_router.py`; the doctor hard gate rejects control-setpoint / trajectory-group names). +2. For the shared allowlist, add the topic using the correct DDS prefix (`rt/`, `rq/`, `rr/`, etc.) and the `$(env ROBOT_NAME)` substitution for the robot namespace. 3. If overriding inherited list entries is needed, use the `!override` tag on the list. diff --git a/docs/robot/autonomy/global/index.md b/docs/robot/autonomy/global/index.md index eaffb424b..6d803d8bb 100644 --- a/docs/robot/autonomy/global/index.md +++ b/docs/robot/autonomy/global/index.md @@ -1,10 +1,22 @@ # Global Packages -The global packages include global world models and planners. +The **global** layer gives the robot memory and direction beyond sensor range: a persistent 3D map of everywhere it has been, and a global planner that reasons over that map to produce a coarse waypoint path. Its output — `global_plan` — is the handoff to the [local layer](../local/index.md), which refines it into collision-free trajectory segments. Because global planning is not flight-critical, it is the one part of the stack that may run off-vehicle: the `lite_offload_global` split stack moves it to a ground host. +## Sub-layers + +- [**World Model**](world_model/index.md) — the persistent VDB voxel map built from filtered sensor clouds +- [**Planning**](planning/index.md) — global planners (reference implementation: random walk exploration) ## Launch -Launch files are under `src/robot/autonomy/global/global_bringup/launch`. -The main launch command is `ros2 launch global_bringup global.launch.xml`. +The global layer is composed by the selected stack's entry launch file (`stacks//launch/*.launch.xml`): the trunk stacks include `vdb_mapping_ros2.py` and `random_walk_planner.launch.xml` directly — see `stacks/full_default/launch/stack.launch.xml`. The `global_bringup` package (`robot/ros_ws/src/global/global_bringup`) owns the cross-package VDB config (`config/vdb_params.yaml`), which the stack entry file passes to the mapper. + +## Key Interchanges + +- [`global_map` (§3)](../interface_conventions.md#3-global_map-global-world-model) — the VDB map topics the global planner consumes +- [`global_plan` (§4)](../interface_conventions.md#4-global_plan-global-waypoint-path) — the waypoint path handed to the local planner; the one interchange that may cross a machine boundary in split stacks + +## See Also +- [System Architecture — Global Layer](../system_architecture.md#global-layer) +- [Task Executors](../tasks.md) — the exploration task server lives in the global planner diff --git a/docs/robot/autonomy/global/planning/index.md b/docs/robot/autonomy/global/planning/index.md index ba0aa1a49..2da25a8e2 100644 --- a/docs/robot/autonomy/global/planning/index.md +++ b/docs/robot/autonomy/global/planning/index.md @@ -1,96 +1,113 @@ [//]: # "global" # Planning -![global_trajectory_diagram](global_trajectory.png) +Global planners output a high-level, coarse path for the robot to follow. -Global planners output a high level, coarse trajectory for the robot to follow. +The global planner should make a path that is collision-free according to the +global map ([`global_map`, spec §3](../../interface_conventions.md#3-global_map-global-world-model)). +However, avoiding fine obstacles is delegated to the local planner, which +operates at a faster rate. -A **trajectory** is a spatial path plus a schedule. -This means each waypoint in the trajectory has a time associated with it, indicating when the robot should reach that waypoint. -These timestamps are fed to the local planner and controller to determine velocity and acceleration. +For the structure of the package, the global planner node should not include +any logic to generate the path. This should be located in a separate logic +class, separated from ROS. This allows more modularity for testing and easy +interface changes. -If a waypoint's header timestamp is empty, the local planner should assume there's no time constraint and follow the trajectory at its own pace. - -The global planner should make a trajectory that is collision-free according to the global map. -However, avoiding fine obstacles is delegated to the local planner that operates at a faster rate. - -For the structure of the package, the global planner node should not include any logic to generate the path. This should be located in a seperate logic class and be seperated from ROS. This will allow more modularity in the future for testing and easy interface changes. - -We intend the global planners to be modular. _AirStack_ implements a basic Random Walk planner as a baseline. +We intend the global planners to be modular. _AirStack_ implements a basic +Random Walk planner as a baseline, plus a frontier-based Exploration planner. Feel free to implement your own through the following interfaces. ## ROS Interfaces -Global planners are meant to be modules that can be swapped out easily. -They can be thought of as different high level behaviors for the robot to follow. -Consider that multiple global planners may be run in parallel, for example by some ensemble planner node that chooses the best plan for the current situation. - -As such, the global planner should be implemented as a ROS2 node that accepts runtime mission parameters in a custom `PlanRequest.msg` and -publishes a plan to its local `~/global_plan` topic. - -The best global plan should then be forwarded or remapped to `/$(env ROBOT_NAME)/global_plan` for the local planner to follow. - -``` mermaid -sequenceDiagram - autonumber - Global Manager->>Global Planner: ~/plan_request (your_planner/PlanRequest.msg) - loop Planning - Global Planner-->>Global Manager: heartbeat feedback - end - Global Planner->>Global Manager: ~/global_plan (nav_msgs/Path.msg) - Global Manager->>Local Planner: /$ROBOT_NAME/global_plan_reference (nav_msgs/Path.msg) - Local Planner->>Global Manager: /$ROBOT_NAME/global_plan_eta (nav_msgs/Path.msg) -``` +Global planners are meant to be modules that can be swapped out easily. +They can be thought of as different high-level behaviors for the robot to +follow. -### Subscribe: Plan Request -Your custom `PlanRequest.msg` defines the parameters that your global planner needs to generate a plan. -It will be sent on the `~/plan_request` topic. +A global planner sits at the top of the [task cascade](../../tasks.md#task-cascade): +it is a **task executor** — a ROS 2 action server that only plans while a goal +is active — invoked by the operator from the GCS (Foxglove robot-commands +panel or RViz Tasks Panel), and it delegates navigation to the local planner. -Some common parameters may be the following: +```mermaid +graph TD + GCS[GCS operator] -->|"task goal, e.g. ExplorationTask (tasks/exploration)"| GP[Global planner] + GP -->|"global_plan (nav_msgs/Path, map frame)"| LP[Local planner] + GP -->|"NavigateTask goal (tasks/navigate)"| LP ``` -# PlanRequest.msg -std_msgs/Duration timeout # maximum time to spend planning -geometry_msgs/Polygon bounds # boundary that the plan must stay within -``` - - -### Publish: Global Plan -The global planner must publish a message of type `nav_msgs/Path` to `~/global_plan`. -The message defines high level waypoints to reach by a given time. -The `nav_msgs/Path` message type contains a `header` field and `poses` field. +### Task-executor action server -- The top level header of `nav_msgs/Path` message should contain the coordinate frame of the trajectory, and its timestamp should indicate when the trajectory was published. -- Within the `poses` field, each `geometry_msgs/PoseStamped`'s header should contain a timestamp that indicates when that waypoint should be reached +Each global planner exposes its action server at the canonical +`/{robot_name}/tasks/` name — see +[`tasks/*` — task action servers (spec §8)](../../interface_conventions.md#8-tasks-task-action-servers). +Action types live in the shared `task_msgs` package; the per-task goal, +feedback, and result fields are documented in +[Task Executors](../../tasks.md#task-action-types). +For example, the random walk planner serves +`tasks/exploration` (`task_msgs/action/ExplorationTask`) and, while the goal +is active, sends the generated path to the local planner as a +`NavigateTask` goal on `tasks/navigate`. -``` -nav_msgs/Path.msg - - std_msgs/Header header - - time stamp: when the trajectory was generated - - frame_id: the coordinate frame of the trajectory - - geometry_msgs/PoseStamped[] poses: the trajectory - - geometry_msgs/PoseStamped pose - - std_msgs/Header header - - time stamp: when the waypoint should be reached - - string frame_id: the coordinate frame of the waypoint - - geometry_msgs/Pose pose: the position and orientation of the waypoint -``` -### Publish: Heartbeat -For long-running global planners, it's recommended to publish a heartbeat message to `~/heartbeat`. This way the calling node can know that the global planner is still running and hasn't crashed. - -### Additional Subscribers -In general, the global planner needs to access components of the world model such as the map and drone state. - -The most common map is Occupancy Grids that is published by {==TODO==} node. - -The global planner can also access the robot's current state and expected state in the future. For example, if the global planner takes 20 seconds to plan a trajectory, -it can query where the robot expects to be in 20 seconds. This ROS2 service is available under {==TODO==}. +### Publish: Global Plan -The global planner can do whatever it wants internally with this information. +The global planner publishes its path as a `nav_msgs/Path` on its local +`~/global_plan` topic, remapped in the module launch file to the canonical +`/{robot_name}/global_plan` — see +[`global_plan` — global waypoint path (spec §4)](../../interface_conventions.md#4-global_plan-global-waypoint-path). +The path is in the `map` frame (ENU, meters), and its last pose is the +navigation goal. The local planner consumes it and handles fine obstacle +avoidance along the way. `global_plan` is the one interchange that may cross +a machine boundary, which is what makes the global-offload split stack +(`lite_offload_global`) possible. + +Where applicable, plan publication can be toggled at runtime: the exploration +planner exposes a `~/global_plan_toggle` service (`std_srvs/Trigger`, +remapped to `/{robot_name}/behavior/global_plan_toggle`) to turn planning on +and off from the RViz Tasks Panel or the GCS. + +### Subscribe: World Model and State + +In general, the global planner needs the global map and the robot state: + +- **Map:** today's de facto map interchange is the VDB map visualization + topic published by `vdb_mapping` — see + [`global_map` (spec §3)](../../interface_conventions.md#3-global_map-global-world-model). + The random walk planner collision-checks its segments against it. +- **State estimate:** the canonical odometry topic + `odometry_conversion/odometry` — see + [`odometry` (spec §2)](../../interface_conventions.md#2-odometry-primary-state-estimate). + +Both are declared as launch arguments (defaulting to the canonical names) in +the module launch file, so a stack entry file can rewire them without +touching the module. ## Example Planners ### Random Walk planner -The random walk planner replans when the robot is getting close to the goal. The random walk planner is a trivial planner that generates a plan by randomly selecting a direction to move in. The random walk planner is useful for testing the robot's ability to follow a plan. - +The [random walk planner](../../../../../robot/ros_ws/src/global/planners/random_walk/README.md) +replans when the robot is getting close to the goal. It is a trivial planner +that generates a plan by randomly selecting a direction to move in, and is +useful for testing the robot's ability to follow a plan. It is the reference +task-executor implementation, serving `tasks/exploration`. + +### Exploration planner + +The [exploration planner](../../../../../robot/ros_ws/src/global/planners/exploration/README.md) +(`robot/ros_ws/src/global/planners/exploration`) is a frontier-based +geometric exploration planner — an alternative to `random_walk`. To use it, +swap the `random_walk_planner.launch.xml` include in your stack's entry +launch file for `exploration_launch.xml`, as described in its README. + +## Writing Your Own Global Planner + +1. Follow the [Module Integration Checklist](../../integration_checklist.md) + for package structure, launch-file conventions, and wiring. +2. Implement the planner as a task executor: see + [Adding a New Task Executor](../../tasks.md#adding-a-new-task-executor) + and the + [add-task-executor](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-task-executor/SKILL.md) + skill for the step-by-step action-server pattern. +3. Publish `global_plan` and delegate navigation to `tasks/navigate` + per the interfaces above, defaulting every endpoint to its canonical name + from the [Interface Conventions Specification](../../interface_conventions.md). diff --git a/docs/robot/autonomy/global/world_model/index.md b/docs/robot/autonomy/global/world_model/index.md index 7d1feb6cf..712cebbf6 100644 --- a/docs/robot/autonomy/global/world_model/index.md +++ b/docs/robot/autonomy/global/world_model/index.md @@ -1,6 +1,23 @@ [//]: # "global" # World Model -Global world models are responsible for maintaining a representation of the world that is used by the global planner to generate a plan. This representation is typically a map of the environment, but can also include other information such as the location of other robots, obstacles, and goals. +The global world model maintains a persistent 3D representation of everywhere the robot has sensed — the memory the [global planner](../planning/index.md) plans over. AirStack's shipped default is **VDB Mapping**: an OpenVDB-based voxel map, built in the `map` frame from the filtered LiDAR cloud, vendored in-tree with the config owned by `global_bringup`. -The current placeholder world model is a voxelized map representation called [VDB Mapping](https://github.com/fzi-forschungszentrum-informatik/vdb_mapping_ros2). \ No newline at end of file +## Packages + +- [**VDB Mapping ROS 2**](../../../../../robot/ros_ws/src/global/world_models/vdb_mapping_ros2/README.md) (`vdb_mapping_ros2`, in-tree) — ROS 2 wrapper around the FZI [VDB Mapping](https://github.com/fzi-forschungszentrum-informatik/vdb_mapping) library; upstream wrapper repo: [vdb_mapping_ros2](https://github.com/fzi-forschungszentrum-informatik/vdb_mapping_ros2) + +## Launch + +The mapper is composed by the selected stack's entry launch file (`stacks//launch/*.launch.xml`), which includes `vdb_mapping_ros2.py` with `global_bringup/config/vdb_params.yaml` — see `stacks/full_default/launch/stack.launch.xml`. + +## Key Interchanges + +All map topics are specified in [`global_map` (§3)](../../interface_conventions.md#3-global_map-global-world-model): `vdb_map_visualization` (today's de facto map interchange, consumed by the reference global planner), the `vdb_map_updates` / `_sections` / `_overwrites` grids for remote/split map synchronization, and the `vdb_map_pointcloud` export. The map lives in the `map` frame. + +In the `lite_offload_global` split stack, VDB mapping runs **offboard** on the ground host: the filtered sensor cloud crosses the bridge (per the stack's `bridge.yaml`) and the map is built where the global planner consumes it. + +## See Also + +- [Global layer overview](../index.md) +- [System Architecture — Global Layer](../../system_architecture.md#global-layer) diff --git a/docs/robot/autonomy/index.md b/docs/robot/autonomy/index.md index bfd8cf6c8..25d81b0fb 100644 --- a/docs/robot/autonomy/index.md +++ b/docs/robot/autonomy/index.md @@ -1,22 +1,32 @@ # Autonomy Modules -## Overview +The onboard autonomy stack is organized into **layers**: data flows from sensors through perception and world models into planners, then down through controllers to the hardware interface. Each layer is a set of swappable ROS 2 packages that meet at the narrow interchange points defined in the [Interface Conventions Specification](interface_conventions.md), so an individual module (a planner, a controller, a mapper) can be replaced without rewiring its neighbors. -The AirStack autonomy stack is organized into modular layers that work together to enable autonomous operation. Each layer has specific responsibilities and communicates with adjacent layers through well-defined ROS 2 interfaces. +## The Six Layers -## Modules +- [**Interface**](interface/index.md) — the bridge to the flight controller: command authority, arming, and MAVLink/MAVROS translation +- [**Sensors**](sensors/index.md) — driver/bridge topic normalization and robot-side preprocessing (e.g. LiDAR near-range filtering) +- [**Perception**](perception/index.md) — state estimation: the odometry every downstream module consumes +- [**Local**](local/index.md) — short-range world model, reactive local planner, and the trajectory + PID controllers +- [**Global**](global/index.md) — persistent 3D mapping (VDB) and coarse global planning +- [**Behavior**](behavior/index.md) — the onboard safety executive -- [**Interface**](interface/index.md) - Hardware interface and safety systems -- [**Sensors**](sensors/index.md) - Sensor integration and data processing -- [**Perception**](perception/index.md) - State estimation and environment understanding -- [**Local**](local/index.md) - Local planning, world models, and control -- [**Global**](global/index.md) - Global planning and mapping -- [**Behavior**](behavior/index.md) - High-level mission execution and decision making +Alongside the layers, [**Coordination**](coordination/index.md) lets robots gossip state to each other and the GCS. + +## Perpetual Nodes vs Task Executors + +Modules run in one of two styles. **Perpetual nodes** (state estimation, world models, controllers) run continuously from launch to shutdown. **Task executors** are action servers that only work when the operator sends a goal from the GCS — takeoff, land, navigate, explore — cascading from global-layer to local-layer executors. See [System Architecture — Node Types](system_architecture.md#node-types-perpetual-vs-task-executor) and [Task Executors](tasks.md). + +## Where Stacks Fit + +Which modules run, and how they are wired, is decided by the selected **stack**: each layer's modules are composed by the stack's entry launch file (`stacks//launch/*.launch.xml`, e.g. `stacks/full_default/launch/stack.launch.xml`). See [Stacks](../../development/stacks.md). ## Key Resources -- [**System Architecture**](system_architecture.md) - Detailed architecture diagrams and data flow -- [**Integration Checklist**](integration_checklist.md) - Guide for adding new modules +- [**System Architecture**](system_architecture.md) — architecture diagrams and data flow +- [**Interface Conventions Specification**](interface_conventions.md) — the versioned contract at every module boundary +- [**Integration Checklist**](integration_checklist.md) — guide for adding new modules ## System Diagram + ![AirStack System Diagram](../airstack_system_diagram.png) diff --git a/docs/robot/autonomy/integration_checklist.md b/docs/robot/autonomy/integration_checklist.md index 24c045767..16abfd5ce 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 live in the versioned +> [Interface Conventions Specification](interface_conventions.md)** — +> 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: @@ -49,115 +54,34 @@ integration in one place. --- -## Standard Topic Patterns - -AirStack uses standardized topic naming conventions to ensure consistent communication between modules. - -### Topic Naming Convention - -Topics follow this pattern: -``` -/[robot_name]/[layer]/[module]/[data_type] -``` - -Examples: - -- `/drone1/perception/macvo/odometry` -- `/drone1/local_planner/droan/trajectory` -- `/drone1/trajectory_controller/tracking_point` - -### Common Standard Topics - -These topics are used across multiple modules and should be used when applicable: - -| Topic | Type | Purpose | Layer | -|-------|------|---------|-------| -| `/[robot]/odometry` | nav_msgs/Odometry | Primary state estimate | Perception → All | -| `/[robot]/global_plan` | nav_msgs/Path | Global waypoint path | Global → Local | -| `/[robot]/trajectory_controller/trajectory_segment_to_add` | airstack_msgs/TrajectorySegment | Local trajectory commands | Local Planner → Controller | -| `/[robot]/trajectory_controller/trajectory_override` | airstack_msgs/TrajectoryOverride | Direct trajectory override | Behavior → Controller | -| `/[robot]/trajectory_controller/look_ahead` | geometry_msgs/PointStamped | Look-ahead point for planning | Controller → Local Planner | -| `/[robot]/trajectory_controller/tracking_point` | geometry_msgs/PointStamped | Current tracking point | Controller → All | -| `/[robot]/trajectory_controller/trajectory_completion_percentage` | std_msgs/Float32 | Trajectory progress | Controller → Planners | -| `/[robot]/interface/mavros/cmd/takeoff` | mavros_msgs/CommandTOL | Takeoff command | Behavior → Interface | -| `/[robot]/interface/cmd_vel` | geometry_msgs/Twist | Low-level velocity commands | Controller → Interface | - -### Layer-Specific Topic Patterns - -#### Interface Layer -- **Inputs:** Commands from control layer -- **Outputs:** Robot state, sensor raw data -- **Topics:** - - - `/[robot]/interface/mavros/state` - - `/[robot]/interface/mavros/local_position/pose` - - `/[robot]/interface/battery_state` +## Standard Interfaces -#### Sensors Layer -- **Inputs:** Raw sensor data from interface -- **Outputs:** Processed sensor data -- **Topics:** +Canonical topic/service/action names, message types, QoS profiles, and +frames for every interchange point are defined in the versioned +[Interface Conventions Specification](interface_conventions.md) — do not +copy its tables here. The sections you will cite most while integrating: - - `/[robot]/sensors/[sensor_name]/[data_type]` - - Example: `/[robot]/sensors/front_stereo/left/image` - - Example: `/[robot]/sensors/front_stereo/disparity` +- [State estimation (`odometry`)](interface_conventions.md#2-odometry-primary-state-estimate) +- [Trajectory controller surface](interface_conventions.md#5-trajectory-group-the-trajectory-controllers-contract-onboard-only) +- [Interface commands (`control_setpoint`)](interface_conventions.md#6-control_setpoint-controller-interface-command-onboard-only) + and [interface status](interface_conventions.md#7-interface_status-group-vehicle-state-out-of-the-interface-layer) +- [Task action servers (`tasks/*`)](interface_conventions.md#8-tasks-task-action-servers) -#### Perception Layer -- **Inputs:** Sensor data -- **Outputs:** Odometry, environment understanding -- **Topics:** +What the checklist adds on top of the spec: - - `/[robot]/perception/[module]/odometry` - - `/[robot]/perception/[module]/depth` - - `/[robot]/odometry` (aggregated/primary odometry) +- **Every input/output topic must be remappable via launch arguments.** + Default each one to its canonical name from the spec so a conventional + stack needs zero remaps; only deviations belong in the stack entry file. +- **Task action servers** must be remapped to + `/{robot_name}/tasks/{task_name}` in the module's launch file: -#### Local Layer -- **Inputs:** Odometry, local sensor data, global plan -- **Outputs:** Local trajectories, cost maps -- **Topics:** - - World Models: `/[robot]/local/[module]/cost_map` - - Planners: `/[robot]/local/[module]/trajectory` - - Controllers: `/[robot]/trajectory_controller/cmd` - -#### Global Layer -- **Inputs:** Global map, robot pose, goal -- **Outputs:** Global plan, map updates -- **Topics:** - - Mapping: `/[robot]/global/[module]/map` - - Planning: `/[robot]/global_plan` - -#### Behavior Layer - -- **Inputs:** Mission commands, autonomy state -- **Outputs:** High-level commands, mode changes -- **Topics:** - - - `/[robot]/behavior/mission_state` - - `/[robot]/behavior/bt_status` - -### Task Action Server Naming Convention - -All task action servers must be remapped to: - -```text -/{robot_name}/tasks/{task_name} -``` - -Examples: - -- `/{robot_name}/tasks/exploration` — ExplorationTask -- `/{robot_name}/tasks/navigate` — NavigateTask -- `/{robot_name}/tasks/coverage` — CoverageTask - -Add the remap in the layer bringup launch file: - -```xml - -``` + ```xml + + ``` -The `~/` prefix expands to the node's private namespace at runtime, -making the action name configurable without hardcoding. + The `~/` prefix expands to the node's private namespace at runtime, + making the action name configurable without hardcoding. See [Task Executors](tasks.md) for the complete list of defined task action types. @@ -196,7 +120,7 @@ Use this checklist when integrating a new module: - [ ] Cancel flag checked before completion condition inside the `execute()` loop - [ ] Action server remapped to `/{robot_name}/tasks/{name}` - in the layer bringup launch file + in the module's launch file - [ ] `rclcpp::spin()` used in `main()` unless callbacks genuinely need concurrent execution **and** all shared resources are thread-safe (see skill for guidance) @@ -214,15 +138,13 @@ Use this checklist when integrating a new module: ### 4. Launch Integration - [ ] Module launch file created with topic remapping arguments -- [ ] Module added to appropriate layer bringup package +- [ ] Module included in the stack entry file (`stacks//launch/*.launch.xml`) - [ ] Launch arguments use `$(env ROBOT_NAME)` for multi-robot support - [ ] Module namespace properly configured -- [ ] Module included in `autonomy_bringup` launch flow ### 5. Dependencies - [ ] All dependencies listed in `package.xml` -- [ ] Bringup package depends on your package - [ ] External dependencies documented in README - [ ] Dependencies available in Docker image (or documented for addition) @@ -545,6 +467,6 @@ docker stats airstack-robot-desktop-1 - [add-task-executor](../../../.agents/skills/add-task-executor) — Implementing a task executor action server - [integrate-module-into-layer](./../../../.agents/skills/integrate-module-into-layer) - — Adding a module to layer bringup + — Integrating a module into a stack - [test-in-simulation](../../../.agents/skills/test-in-simulation) — Testing procedures diff --git a/docs/robot/autonomy/interface/index.md b/docs/robot/autonomy/interface/index.md index e3aab7241..23c6bf700 100644 --- a/docs/robot/autonomy/interface/index.md +++ b/docs/robot/autonomy/interface/index.md @@ -3,16 +3,36 @@ The interface defines the communication between the autonomy stack running on the onboard computer and the robot's control unit. For example, for drones it converts the control commands from the autonomy stack into MAVLink messages for the flight controller. -==TODO: This is not our diagram, must replace.== -![Interface Diagram](https://404warehouse.net/wp-content/uploads/2016/08/softwareoverview.png?w=800) +```mermaid +graph LR + subgraph Autonomy stack + C[Controllers] + O["odometry_conversion node"] + end + subgraph Interface layer + RI["robot_interface node
(MAVROSInterface)"] + M[MAVROS] + end + FCU["Flight controller (FCU)
PX4 via MAVLink"] + + C -->|"interface/cmd_velocity, interface/cmd_pose,
interface/cmd_roll_pitch_yawrate_thrust, ... (spec §6)"| RI + RI --> M + M <--> FCU + M -->|"interface/mavros/* state (spec §7)"| C + M -->|interface/mavros/local_position/odom| O + O -->|"odometry_conversion/odometry (spec §2)"| C +``` + +Command topics are the [`control_setpoint` interchange (spec §6)](../interface_conventions.md#6-control_setpoint-controller-interface-command-onboard-only); vehicle state comes back out through the [`interface_status` group (spec §7)](../interface_conventions.md#7-interface_status-group-vehicle-state-out-of-the-interface-layer) and the canonical [`odometry` topic (spec §2)](../interface_conventions.md#2-odometry-primary-state-estimate). -The code is located under `AirStack/ros_ws/src/robot/autonomy/interface/`. +The code is located under `robot/ros_ws/src/interface/`. ## Launch -Launch files are under `src/autonomy/interface/interface_bringup/launch`. +Launch files are under `robot/ros_ws/src/interface/interface_bringup/launch`. The main launch command is `ros2 launch interface_bringup interface.launch.py`. +It starts MAVROS (under the `interface` namespace), the `robot_interface` node, the position setpoint publisher, and the `odometry_conversion` node. ### FCU URL and Target System @@ -34,15 +54,16 @@ MAVROS is skipped entirely when `SIM_TYPE=simple`. ## RobotInterface Package `robot_interface` is a ROS2 node that interfaces with the robot's hardware. -The `RobotInterface` _gets robot state_ and forwards it to the autonomy stack, -and also _translates control commands_ from the autonomy stack into the command for the underlying hardware. +The `RobotInterface` _translates control commands_ from the autonomy stack into the command for the underlying hardware, and reports arming/control status back to the stack. Note the base class is unimplemented. Specific implementations should extend `class RobotInterface` in `robot_interface.hpp`, for example `class MAVROSInterface`. ### State -The `RobotInterface` class broadcasts the robot's pose as a TF2 transform. -It also publishes the robot's odometry as a `nav_msgs/Odometry` message to `$(env ROBOT_NAME)/interface/robot_interface/odometry`. +Vehicle state flows out of the interface layer on two paths: + +- The `robot_interface` node publishes `interface/is_armed` and `interface/has_control` (`std_msgs/Bool`), and MAVROS itself publishes the vehicle state topics under `interface/mavros/*` (e.g. `state`, `extended_state`, `global_position/global`) — see [spec §7](../interface_conventions.md#7-interface_status-group-vehicle-state-out-of-the-interface-layer). +- The canonical odometry is **not** produced by `RobotInterface` implementations. A separate `odometry_conversion` node (also in the `robot_interface` package) subscribes to `interface/mavros/local_position/odom`, republishes it as the canonical `odometry_conversion/odometry` (`nav_msgs/Odometry`, `map` frame — see [spec §2](../interface_conventions.md#2-odometry-primary-state-estimate)), and broadcasts the corresponding `map → base_link` TF (plus a stabilized variant). ### Commands @@ -56,55 +77,39 @@ The RobotInterface node subscribes to: - `/$(env ROBOT_NAME)/interface/cmd_roll_pitch_yawrate_thrust` of type `mav_msgs/RollPitchYawrateThrust.msg` - `/$(env ROBOT_NAME)/interface/cmd_torque_thrust` of type `mav_msgs/TorqueThrust.msg` - `/$(env ROBOT_NAME)/interface/cmd_velocity` of type `geometry_msgs/TwistStamped.msg` -- `/$(env ROBOT_NAME)/interface/cmd_position` of type `geometry_msgs/PoseStamped.msg` +- `/$(env ROBOT_NAME)/interface/cmd_pose` of type `geometry_msgs/PoseStamped.msg` -All messages are in the robot's body frame, except `velocity` and `position` which use the frame specified by the message header. +All messages are in the robot's body frame, except `cmd_velocity` and `cmd_pose` which use the frame specified by the message header. ## MAVROSInterface -The available implementation in AirStack is called `MAVROSInterface` implemented in `mavros_interface.cpp`. It simply forwards the control commands to the Ascent flight controller (based on Ardupilot) using MAVROS. +The available implementation in AirStack is called `MAVROSInterface` implemented in `mavros_interface.cpp`. It forwards the control commands over MAVROS to any MAVLink-compatible flight controller (PX4 in simulation). ## Custom Robot Interface If you're using a different robot control unit with its own custom API, then you need to create an associated RobotInterface. Implementations should do the following: -### Broadcast State - -Implementations of `RobotInterface` should obtain the robot's pose and broadcast it as a TF2 transform. - -Should look something like: - -```c++ -// callback function triggered by some loop -void your_callback_function(){ - // ... - geometry_msgs::msg::TransformStamped t; - // populate the transform, e.g.: - t.header = // some header - t.transform.translation.x = // some value - t.transform.translation.y = // some value - t.transform.translation.z = // some value - t.transform.rotation = // some quaternion - // Send the transformation - this->tf_broadcaster_->sendTransform(t); - // ... -} -``` +### Provide State -==TODO: our code doesn't currently do it like this, it instead uses an external odometry_conversion node.== +Your interface (or its underlying driver, as MAVROS does) should publish the robot's native odometry. +Do not broadcast pose TF or publish the canonical odometry topic from the interface itself — that is the job of the existing `odometry_conversion` node. +Instead, point the `interface_odometry_in_topic` launch argument of `interface.launch.py` (default: `/$(env ROBOT_NAME)/interface/mavros/local_position/odom`) at your interface's odometry output. +The `odometry_conversion` node then produces the canonical `odometry_conversion/odometry` in the `map` frame and broadcasts the `map → base_link` transform, so every downstream consumer works unchanged. ### Override Command Handling Should override all `virtual` functions in `robot_interface.hpp`: -- `cmd_attitude_thrust_callback` -- `cmd_rate_thrust_callback` -- `cmd_roll_pitch_yawrate_thrust_callback` -- `cmd_torque_thrust_callback` -- `cmd_velocity_callback` -- `cmd_position_callback` +- `attitude_thrust_callback` +- `rate_thrust_callback` +- `roll_pitch_yawrate_thrust_callback` +- `torque_thrust_callback` +- `velocity_callback` +- `pose_callback` - `request_control` - `arm` - `disarm` - `is_armed` - `has_control` +- `takeoff` +- `land` diff --git a/docs/robot/autonomy/interface_conventions.md b/docs/robot/autonomy/interface_conventions.md new file mode 100644 index 000000000..1e5c3f7ea --- /dev/null +++ b/docs/robot/autonomy/interface_conventions.md @@ -0,0 +1,247 @@ +# Interface Conventions Specification + +**Spec version: v1.0.1** (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. It +is the citable contract behind the topic tables of the +[Module Integration Checklist](integration_checklist.md); 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 any +other document disagrees with a column here, 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: + 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 are first-class in the vehicle manifest, 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`** — +a doctor hard gate: `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, and not `PointStamped`) | +| `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) | +| `tasks/coverage` | `task_msgs/action/CoverageTask` | (defined in `task_msgs`; no shipped executor) | +| `tasks/chat` | `task_msgs/action/ChatTask` | (defined in `task_msgs`; no shipped executor) | + +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**: 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. +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 **written proposal in the registry repo** (`rfcs/` — the + deprecation registry; until the registry repo exists, proposals live as + GitHub Discussions on the AirStack repo). + +Additions (new interchange points) are semver-minor and are discovered +through drift reports: three forks patching the same tap point = a missing +convention. The `doctor` hard-gate list (dep conflicts; +control/trajectory names in `bridge.yaml`) grows only through the same +proposal process. + +## Change log + +| Spec | Date | Change | +|---|---|---| +| v1.0.1 | 2026-08-25 | §8: added `tasks/coverage` and `tasks/chat` rows so the table covers all eight `task_msgs` actions; both are defined in `task_msgs` with no shipped executor. Documentation-only. | +| v1.0.0 | 2026-08-20 | Initial versioned spec, recorded from `full_default`'s observed wiring. Known v2 candidates: plain `odometry` as the canonical state topic; a structured `global_map` interchange. | diff --git a/docs/robot/autonomy/local/controls/index.md b/docs/robot/autonomy/local/controls/index.md index 1ffe7da95..8aab04daf 100644 --- a/docs/robot/autonomy/local/controls/index.md +++ b/docs/robot/autonomy/local/controls/index.md @@ -1,7 +1,10 @@ # Controls -Controls dictate the actuation of the robot. They are responsible for taking in sensor data and producing control commands. +Controls dictate the actuation of the robot: they close the loop between the planned trajectory and the robot's actual state, and publish control commands to the topics defined by the [Robot Interface](../../interface/index.md). -The controller should publish control commands directly to topics defined by the [Robot Interface](../../interface/index.md). +AirStack splits control into two nodes: -Currently the AirStack uses a custom controller called "Trajectory Controller". +- [**Trajectory Controller**](../../../../../robot/ros_ws/src/local/controls/trajectory_controller/README.md) (`trajectory_controller`) — a pure-pursuit trajectory manager that advances a **tracking point** and **look-ahead point** along the current trajectory (it is not itself a feedback controller) +- **PID Controller** (`pid_controller`) — a cascaded position/velocity PID that drives the drone toward the tracking point and publishes roll/pitch/yaw-rate/thrust commands to the interface + +Both are perpetual nodes and run onboard only — control never crosses a machine boundary (see the [Interface Conventions Specification](../../interface_conventions.md)). diff --git a/docs/robot/autonomy/local/index.md b/docs/robot/autonomy/local/index.md index cae708c0e..011eea596 100644 --- a/docs/robot/autonomy/local/index.md +++ b/docs/robot/autonomy/local/index.md @@ -1,8 +1,24 @@ # Local Packages -The local module includes packages that are specific to the local autonomy of the robot. This includes local mapping, planning, and control. + +The **local** layer closes the robot's short-range sense-plan-act loop: a fast local world model built from live sensor data, a reactive local planner that avoids obstacles the global map is too slow or too coarse to capture, and the controllers that turn planned trajectories into commands for the [interface](../interface/index.md). The loop is coupled through the trajectory controller's **look-ahead point** — the local planner plans forward from where the controller will soon be, and streams trajectory segments back to it. + +## Sub-layers + +- [**World Model**](world_model/index.md) — disparity-based C-space obstacle representation for fast collision queries +- [**Planning**](planning/index.md) — the DROAN local planner: turns the global plan into short, collision-free trajectory segments +- [**Controls**](controls/index.md) — trajectory controller (tracking/look-ahead point management) and PID controller (attitude/thrust commands) ## Launch -Launch files are under `src/robot/autonomy/local/local_bringup/launch`. -The main launch command is `ros2 launch local_bringup local.launch.xml`. +Local modules ship their own canonical launch files and are composed flat by the selected stack's entry launch file (`stacks//launch/*.launch.xml`) — the trunk stacks include `takeoff_landing_planner`, the trajectory controller, `droan_gl`, and the PID controller directly; see `stacks/full_default/launch/stack.launch.xml` for the composed wiring. + +## Key Interchanges + +- [`global_plan` (§4)](../interface_conventions.md#4-global_plan-global-waypoint-path) — the coarse path handed down from the global layer; the local planner's main input +- [`trajectory` group (§5)](../interface_conventions.md#5-trajectory-group-the-trajectory-controllers-contract-onboard-only) — the trajectory controller's onboard-only contract: `trajectory_segment_to_add`, `trajectory_override`, `tracking_point`, `look_ahead` +- [`control_setpoint` (§6)](../interface_conventions.md#6-control_setpoint-controller-interface-command-onboard-only) — the PID controller's command into the interface layer + +## See Also +- [System Architecture — Local Layer](../system_architecture.md#local-layer) +- [Global](../global/index.md) — the upstream producer of `global_plan` diff --git a/docs/robot/autonomy/local/planning/index.md b/docs/robot/autonomy/local/planning/index.md index a59789807..96face1c3 100644 --- a/docs/robot/autonomy/local/planning/index.md +++ b/docs/robot/autonomy/local/planning/index.md @@ -1,7 +1,12 @@ # Local Planning -Part of the local planner is the Waypoint Manager. +Local planners turn the coarse global plan into short, collision-free trajectory segments, reacting to obstacles the global map is too slow or too coarse to capture. They plan from the trajectory controller's look-ahead point and feed segments to it continuously. -The Waypoint Manager subscribes to the global waypoints and the drone's current position and publishes the next waypoint to the local planner. +AirStack's baseline local planner is DROAN, in two implementations: -We plan for this baseline to be DROAN. \ No newline at end of file +- [**DROAN GL**](../../../../../robot/ros_ws/src/local/planners/droan_gl/README.md) (`droan_gl`) — GPU-accelerated, true-sphere disparity expansion via OpenGL shaders; the default in the `full_default` stack +- [**DROAN Local Planner**](../../../../../robot/ros_ws/src/local/planners/droan_local_planner/README.md) (`droan_local_planner`) — the CPU implementation, selected by the `full_droan_cpu` stack + +Both are task executors serving `NavigateTask` at `/{robot_name}/tasks/navigate`. + +Specialized maneuvers are handled by the [Takeoff Landing Planner](../../../../../robot/ros_ws/src/local/planners/takeoff_landing_planner/README.md), and candidate trajectories come from the [Trajectory Library](../../../../../robot/ros_ws/src/local/planners/trajectory_library/README.md). diff --git a/docs/robot/autonomy/local/world_model/index.md b/docs/robot/autonomy/local/world_model/index.md index 5051a0efd..8429e8285 100644 --- a/docs/robot/autonomy/local/world_model/index.md +++ b/docs/robot/autonomy/local/world_model/index.md @@ -1 +1,9 @@ -# Local World Model \ No newline at end of file +# Local World Model + +Local world models give the local planner a fast, short-range obstacle representation built directly from sensor data — cheaper and lower-latency than the global map, at the cost of limited spatial extent. AirStack's local world model is disparity-based: + +- [**Disparity Expansion**](../../../../../robot/ros_ws/src/local/world_models/disparity_expansion/README.md) — C-space expansion of stereo disparity images by the robot radius +- [**Disparity Graph**](../../../../../robot/ros_ws/src/local/world_models/disparity_graph/README.md) — rolling window of expanded-disparity keyframes with their camera poses +- [**Disparity Graph Cost Map**](../../../../../robot/ros_ws/src/local/world_models/disparity_graph_cost_map/README.md) — cost-map plugin answering collision-cost queries for the DROAN local planner + +The GPU planner `droan_gl` performs the expansion and graph internally on the GPU; the CPU pipeline uses these packages as separate nodes. diff --git a/docs/robot/autonomy/perception/adding_a_state_estimator.md b/docs/robot/autonomy/perception/adding_a_state_estimator.md new file mode 100644 index 000000000..3972b0901 --- /dev/null +++ b/docs/robot/autonomy/perception/adding_a_state_estimator.md @@ -0,0 +1,77 @@ +# Adding a State Estimator + +A state estimator in AirStack is any node that produces the robot's primary state estimate — the odometry surface every downstream consumer (safety monitor, PID controller, DROAN, random_walk, trajectory controller, task servers) subscribes to. The contract is [Interface Conventions §2](../interface_conventions.md#2-odometry-primary-state-estimate): `nav_msgs/Odometry` on `odometry_conversion/odometry` (RELIABLE QoS), `pose` in the `map` frame (ENU, meters), `twist` in the body frame (`child_frame_id`), plus the `map → base_link` TF. This guide assumes you have run the stack before and know the [layered architecture](../index.md); it swaps the estimator, not the consumers. + +Today's default estimator path is PX4's EKF: MAVROS publishes `/{robot_name}/interface/mavros/local_position/odom`, and the `odometry_conversion` node (from the `robot_interface` package, launched inside `robot/ros_ws/src/interface/interface_bringup/launch/interface.launch.py`) normalizes it onto the canonical surface — it restamps `frame_id`/`child_frame_id` to `map`/`base_link`, republishes on `odometry_conversion/odometry`, and broadcasts the `map → base_link` TF (`convert_odometry_to_transform: true`). Your estimator replaces the *input* to that node, not the node itself. + +## Package or module? + +Decide early where the estimator lives: + +- **In-tree package** — fastest for trunk work: a normal ROS 2 package under `robot/ros_ws/src/perception/`, or a scaffolded module boundary in your fork via `airstack module create --in-tree ` (lands under `robot/ros_ws/src/modules/`). +- **Module repo** — shareable, version-pinned, with its own CI and Docker dependency layer: a thin external repo added with `airstack module add --version `. See [AirStack Modules](../../../development/modules.md) (especially [the researcher workflow](../../../development/modules.md#the-researcher-workflow-fork-module)) and the [create-module skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/create-module/SKILL.md). The precedent for a state estimator shipped this way is [asm_macvo](../../../modules/macvo.md) — MAC-VO learned stereo visual odometry, consumed by the [full_macvo](../../../../stacks/full_macvo/README.md) stack. + +The steps below are the same either way; only step 5 differs. + +## Steps + +### 1. Create the package + +Follow the [add-ros2-package skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/add-ros2-package/SKILL.md) and the [Module Integration Checklist](../integration_checklist.md). Put it under `robot/ros_ws/src/perception/` (or use the module scaffold above). Declare every topic endpoint as a launch argument defaulting to its canonical name — that is what lets a stack include it with zero remaps. + +**Verify:** it builds inside the robot container — `docker exec airstack-robot-desktop-1 bash -c "bws --packages-select "` exits cleanly. + +### 2. Conform to the spec §2 surface + +Publish `nav_msgs/Odometry` with a RELIABLE publisher; link the [spec table](../interface_conventions.md#2-odometry-primary-state-estimate) from your README rather than restating it. Frames per the spec's [TF table](../interface_conventions.md#tf-frames-and-units): `pose` in `map` (ENU, meters), `twist` in the body frame named by `child_frame_id`, yaw right-handed about +Z. + +The recommended integration is to publish your estimate on your own namespaced topic (e.g. `perception//odometry`) and route it *through* `odometry_conversion` — `interface.launch.py` declares the `interface_odometry_in_topic` launch argument exactly for this. You then keep frame normalization and the `map → base_link` TF broadcast for free. If you instead bypass `odometry_conversion` and publish the canonical topic directly, **you** must broadcast `map → base_link` — `odometry_conversion` is the node that publishes it in the default graph, and it is launched unconditionally by `interface.launch.py`, so bypassing also means forking that launch file to avoid two publishers on the same surface. Route through it. + +**Verify:** with your node running, `docker exec airstack-robot-desktop-1 bash -c "sws && ros2 topic echo /$ROBOT_NAME/perception//odometry --once"` shows a sane pose and the frame ids you expect. + +### 3. Wire it into a stack + +The [single-locus rule](../../../development/stacks.md#the-single-locus-rule-and-its-lint): all wiring deviations live in one place — the stack entry launch file. Never edit `interface.launch.py` or another module's launch file to point at your estimator. Copy a reference stack and change the include lines: + +```bash +airstack stack new full_default full_my_estimator +``` + +Then in `stacks/full_my_estimator/launch/stack.launch.xml`, (a) include your estimator under the `perception` namespace, and (b) pass the interface's odometry-input arg. This is the same pattern [full_macvo](../../../../stacks/full_macvo/README.md) uses to add MAC-VO under `perception/` and rewire one consumer with a single include arg (`stacks/full_macvo/launch/stack.launch.xml` — there the deviation is DROAN's disparity input; here it is the interface's odometry input): + +```xml + + + + + + + + + + +``` + +**Verify:** `airstack up --stack full_my_estimator --sim isaac --robots 1`, then `airstack ready`, then `docker exec airstack-robot-desktop-1 bash -c "ros2 node list"` shows your estimator node alongside `odometry_conversion`. + +### 4. Verify the running graph + +1. Rate and content: `docker exec airstack-robot-desktop-1 bash -c "sws && ros2 topic hz /robot_1/odometry_conversion/odometry"` reports a `state`-class rate (~10–100 Hz), and `ros2 topic echo ... --once` shows `frame_id: map`, `child_frame_id: base_link`. +2. Snapshot the wiring and diff it: `airstack test -m wiring --stack full_my_estimator` regenerates `stacks/full_my_estimator/wiring.md`; review that the only deviations from `full_default`'s wiring are your estimator and the odometry input. +3. Live drift check: `airstack doctor --live --stack full_my_estimator` reports the running graph against the committed `wiring.md` with no unexplained diffs. + +A best-effort/reliable QoS mismatch here fails *silently* (consumers receive nothing) — the spec calls this out; `ros2 topic hz` from step 1 is the check that catches it. + +### 5. Optional: package it as a module + +If the estimator should be shareable and pinnable outside your fork, graduate it to a module repo following [AirStack Modules](../../../development/modules.md) and the [create-module skill](https://github.com/castacks/AirStack/blob/develop/.agents/skills/create-module/SKILL.md), with [asm_macvo](../../../modules/macvo.md) as the worked precedent (heavy deps in `Dockerfile.module`, a consuming reference stack, module CI). Your stack's `modules.repos` then pins it, exactly as `stacks/full_macvo/modules.repos` pins `asm_macvo`. + +**Verify:** `airstack module add --version ` followed by `airstack module doctor` passes, and `airstack up --stack full_my_estimator` brings the graph up from a clean checkout. + +## See also + +- [Interface Conventions Specification](../interface_conventions.md) — the citable contract (§2 odometry, TF frames) +- [Module Integration Checklist](../integration_checklist.md) — package-level integration steps +- [Perception Packages](index.md) — where estimators live in the layer +- [AirStack Stacks](../../../development/stacks.md) — stack anatomy, wiring.md, doctor diff --git a/docs/robot/autonomy/perception/index.md b/docs/robot/autonomy/perception/index.md index 87a113c15..e1206760c 100644 --- a/docs/robot/autonomy/perception/index.md +++ b/docs/robot/autonomy/perception/index.md @@ -12,19 +12,18 @@ Perception forms the foundation of the autonomy stack by: ## Launch -Launch files are located under `robot/ros_ws/src/perception/perception_bringup/launch/`. - -The main launch command is: +Module launch files are located under +`robot/ros_ws/src/perception/perception_bringup/launch/`. The stack entry +files include them directly: ```bash -ros2 launch perception_bringup perception.launch.xml +ros2 launch perception_bringup stereo_image_proc.launch.xml +ros2 launch perception_bringup topic_keepalive.launch.xml ``` ## Key Topics ### Outputs -- `/{robot_name}/odometry` - Best estimate of robot state (position, orientation, velocities) -- `/{robot_name}/pose` - Current robot pose -- `/{robot_name}/imu/data` - Processed IMU data +- `/{robot_name}/odometry_conversion/odometry` (`nav_msgs/Odometry`) - Best estimate of robot state (position, orientation, velocities). This is the v1 canonical state topic — see [Interface Conventions §2](../interface_conventions.md); plain `/{robot_name}/odometry` is the intended v2 name. ### Inputs - Raw sensor data from sensors layer (cameras, IMU, GPS, depth sensors) @@ -35,11 +34,11 @@ State estimation and related perception packages live under `robot/ros_ws/src/pe ### External pose (motion capture) -- [**NatNet (OptiTrack)**](../../../../robot/ros_ws/src/perception/natnet_ros2/README.md) — Receives rigid-body poses from an external Motive PC over NatNet UDP and publishes `/{robot_name}/perception/optitrack/...` topics. Optional MAVROS bridge for PX4 vision pose. Enabled with `LAUNCH_NATNET=true` in `.env` (off by default). +- [**OptiTrack (asm_optitrack module)**](../../optitrack.md) — NatNet mocap support (rigid-body poses from a Motive PC, PX4 external-vision fusion bridges), provided by the [asm_optitrack module](https://github.com/castacks/asm_optitrack). Add it with `airstack module add https://github.com/castacks/asm_optitrack --version `; see [AirStack Modules](../../../development/modules.md). ## Configuration -Perception parameters are configured in `perception_bringup/config/` directory. Common parameters include: +Perception parameters live in each module package's own `config/` YAML files, overridden per-stack via launch arguments in the stack entry file (`stacks//launch/*.launch.xml`). Common parameters include: - Sensor topics to subscribe to - Fusion algorithm parameters diff --git a/docs/robot/autonomy/sensors/index.md b/docs/robot/autonomy/sensors/index.md index a5142333a..66c12c74f 100644 --- a/docs/robot/autonomy/sensors/index.md +++ b/docs/robot/autonomy/sensors/index.md @@ -12,15 +12,15 @@ The sensors layer is responsible for: ## Launch -Launch files are located under `robot/ros_ws/src/sensors/sensors_bringup/launch/`. - -The main launch command is: +Sensor modules ship their own canonical launch files and are composed by the +stack entry file under the `sensors` namespace. E.g. the trunk stacks +include: ```bash -ros2 launch sensors_bringup sensors.launch.xml +ros2 launch lidar_point_cloud_filter lidar_point_cloud_filter.launch.xml ``` -The bringup group uses the `sensors` namespace under each robot; see that package for which nodes are started. +See `stacks/full_default/launch/stack.launch.xml` for the composed wiring. ## Key Topics @@ -31,16 +31,16 @@ The bringup group uses the `sensors` namespace under each robot; see that packag ### Inputs - `/{robot_name}/sensors/ouster/point_cloud_raw` — Raw cloud from the simulator or driver (typical input to the LiDAR filter) -- Other hardware- or bridge-specific topics as wired in `sensors_bringup` +- Other hardware- or bridge-specific topics as wired in the stack entry file (e.g. `stacks/full_default/launch/stack.launch.xml`; the observed graph is recorded in the stack's `wiring.md`) Topic strings are parameterized with `$(env ROBOT_NAME)` in YAML; override `input_topic` / `output_topic` in the filter config if your stack uses different names. ## Modules - [**LiDAR point cloud filter**](#lidar-point-cloud-filter) (`lidar_point_cloud_filter`) — near-range sphere filter for `PointCloud2` -- [**Gimbal stabilizer**](gimbal.md) — gimbal extension usage in simulation +- [**Gimbal (simulation)**](gimbal.md) — gimbal extension usage in simulation (documentation only; no robot-side package) -## LiDAR point cloud filter (`lidar_point_cloud_filter`){#lidar-point-cloud-filter} +## LiDAR point cloud filter **Package:** `robot/ros_ws/src/sensors/lidar_point_cloud_filter` @@ -58,7 +58,8 @@ Topic strings are parameterized with `$(env ROBOT_NAME)` in YAML; override `inpu ## Configuration -- **Bringup:** `robot/ros_ws/src/sensors/sensors_bringup/config/` and launch XML under `sensors_bringup/launch/` +- **Module launch files:** each sensor package ships its own canonical launch file — `robot/ros_ws/src/sensors/lidar_point_cloud_filter/launch/lidar_point_cloud_filter.launch.xml` +- **Stack wiring:** the stack entry files include these under the `sensors` namespace (`stacks//launch/*.launch.xml`, e.g. `stacks/full_default/launch/stack.launch.xml`); the observed graph is recorded in `stacks/full_default/wiring.md` - **LiDAR filter:** `robot/ros_ws/src/sensors/lidar_point_cloud_filter/config/lidar_point_cloud_filter.yaml` (`near_range_m`, topics, QoS) ## See Also diff --git a/docs/robot/autonomy/system_architecture.md b/docs/robot/autonomy/system_architecture.md index 8e199a07d..ac8b1205b 100644 --- a/docs/robot/autonomy/system_architecture.md +++ b/docs/robot/autonomy/system_architecture.md @@ -1,6 +1,6 @@ # System Architecture -This document provides a comprehensive overview of the AirStack autonomy system architecture, data flow, and module interactions. +This document explains the AirStack autonomy architecture: how the layers relate, the two kinds of nodes the stack is built from, and how data and task goals flow through the system. ## Overview @@ -43,7 +43,7 @@ They receive data over topics and publish results immediately — there is no external activation step. Most of the autonomy stack consists of perpetual nodes. -Examples: state estimator, VDB mapper, disparity expander, trajectory controller, behavior tree tick loop. +Examples: state estimator, VDB mapper, disparity expander, trajectory controller, safety monitor. ### Task Executors @@ -71,21 +71,22 @@ All task action servers are remapped to `/{robot_name}/tasks/{task_name}` by con ### Task Cascade -High-level tasks (sent by the behavior layer) cascade down through the stack: +High-level task goals (sent by the operator from the GCS — the Foxglove +robot-commands panel or the RViz Tasks Panel) cascade down through the stack: ```mermaid graph TD - BE[behavior_executive] -->|ExplorationTask| RW[random_walk_planner] + GCS[GCS operator] -->|ExplorationTask| RW[random_walk_planner] RW -->|NavigateTask| DG[droan_gl] DG -->|trajectory_segment_to_add| TC[trajectory_controller] - style BE fill:#cce5ff + style GCS fill:#cce5ff style TC fill:#cce5ff style RW fill:#d4edda style DG fill:#d4edda ``` -*Blue = perpetual node, green = task executor.* +*Blue = perpetual node / external client, green = task executor.* The global-layer task executor (e.g. `random_walk_planner`) decides *where* to go and delegates the actual flying to the local-layer task @@ -136,327 +137,124 @@ graph LR ## Detailed Layer Architecture -### Interface Layer - -**Purpose:** Abstract hardware/simulation and provide safety monitoring. +The exact topic names, message types, and QoS settings that connect the layers +are specified once, normatively, in the +[Interface Conventions Specification](interface_conventions.md) — the +subsections below describe each layer's *role* and link to its documentation +rather than restating that data. -```mermaid -graph TB - MAVROS[MAVROS Interface] - Safety[Safety Monitor] - RobotIF[Robot Interface] - - PX4[PX4 Flight Controller] --> MAVROS - MAVROS --> Safety - Safety --> RobotIF - - RobotIF -->|State| Perception - RobotIF -->|Safety Status| Behavior - - Control[Trajectory Controller] -->|Commands| MAVROS - MAVROS -->|Actuator Commands| PX4 -``` - -**Key Modules:** - -- `mavros_interface`: MAVLink communication with flight controller -- `drone_safety_monitor`: Safety checks and emergency handling -- `robot_interface`: High-level robot state abstraction - -**Topics:** +### Interface Layer -- **Published:** - - `/[robot]/interface/mavros/state` - - `/[robot]/interface/mavros/local_position/pose` - - `/[robot]/interface/battery_state` +The interface layer abstracts the flight controller behind a stable +command/status boundary: it converts controller setpoints into vehicle +commands (MAVROS ↔ PX4 in the trunk) and publishes vehicle state — arming, +flight mode, battery — back up to the rest of the stack. Nothing above this +layer talks to hardware directly, which is what makes simulation and real +vehicles interchangeable. -- **Subscribed:** - - `/[robot]/trajectory_controller/cmd_vel` +See the [Interface layer documentation](interface/index.md); the +command and status contracts are +[`control_setpoint` (§6)](interface_conventions.md#6-control_setpoint-controller-interface-command-onboard-only) +and the +[`interface_status` group (§7)](interface_conventions.md#7-interface_status-group-vehicle-state-out-of-the-interface-layer). ### Sensors Layer -**Purpose:** Process and calibrate sensor data. - -```mermaid -graph LR - Camera[Camera Sensors] --> ImgProc[Image Processing] - Stereo[Stereo Cameras] --> Disparity[Disparity Computation] - Gimbal[Gimbal] --> Stabilizer[Gimbal Stabilizer] - - ImgProc --> Perception - Disparity --> Perception - Stabilizer --> Camera -``` - -**Key Modules:** - -- `camera_param_server`: Camera calibration management -- `gimbal_stabilizer`: Gimbal control and stabilization -- Sensor drivers and processors +The sensors layer wraps drivers and low-level processing (e.g. point-cloud +filtering) so that downstream layers consume calibrated, consistently named +streams instead of device-specific topics. -**Topics:** - -- **Published:** - - `/[robot]/sensors/[sensor_name]/image` - - `/[robot]/sensors/[sensor_name]/camera_info` - - `/[robot]/sensors/front_stereo/disparity` +See the [Sensors layer documentation](sensors/index.md) and the +[`sensors/*` naming convention (§1)](interface_conventions.md#1-sensors-sensor-naming-convention). ### Perception Layer -**Purpose:** Estimate robot state and understand environment. - -```mermaid -graph TB - subgraph "State Estimation" - VIO[Visual-Inertial Odometry] - Fusion[Sensor Fusion] - end - - subgraph "Environment Perception" - Depth[Depth Estimation] - Features[Feature Detection] - Tracking[Object Tracking] - end - - Sensors -->|Images + IMU| VIO - Sensors -->|Multi-sensor| Fusion - VIO --> Odometry[Odometry Output] - Fusion --> Odometry - - Sensors -->|Stereo| Depth - Sensors -->|Images| Features - Features --> Tracking - - Odometry --> Local - Odometry --> Global - Depth --> Local -``` +The perception layer turns sensor streams into the robot's estimate of itself +and its surroundings: the primary state estimate (`odometry`) and depth / +point-cloud products consumed by the world models. The trunk default stereo +pipeline is `stereo_image_proc`; learned visual odometry (MAC-VO) is available +as the external [asm_macvo](https://github.com/castacks/asm_macvo) module +rather than in the trunk. -**Key Modules:** - -- `macvo_ros2`: Visual-inertial odometry system - -**Topics:** - -- **Published:** - - `/[robot]/odometry` - Primary state estimate - - `/[robot]/perception/macvo/depth` - - `/[robot]/perception/macvo/features` - -- **Subscribed:** - - `/[robot]/sensors/*/image` - - `/[robot]/sensors/*/camera_info` - - `/[robot]/interface/mavros/imu` +See the [Perception layer documentation](perception/index.md) and the +[`odometry` contract (§2)](interface_conventions.md#2-odometry-primary-state-estimate). ### Local Layer -**Purpose:** Reactive obstacle avoidance and trajectory control. - -The local layer has three sub-layers: - -```mermaid -graph TB - subgraph "Local World Models" - Disparity[Disparity Expansion] - Graph[Disparity Graph] - CostMap[Cost Map] - - Disparity --> Graph - Graph --> CostMap - end - - subgraph "Local Planners" - DROAN[DROAN Planner] - TakeoffLanding[Takeoff/Landing] - - CostMap --> DROAN - end - - subgraph "Controllers" - TrajControl[Trajectory Controller] - AttControl[Attitude Controller] - - DROAN --> TrajControl - TakeoffLanding --> TrajControl - TrajControl --> AttControl - end - - Perception -->|Odometry| DROAN - Perception -->|Disparity| Disparity - Global -->|Global Plan| DROAN - - AttControl -->|Commands| Interface -``` - -**Key Modules:** - -- **World Models:** - - - `disparity_expansion`: Obstacle detection from stereo - - `disparity_graph`: Graph-based obstacle representation - - `disparity_graph_cost_map`: Cost map generation - -- **Planners:** - - - `droan_local_planner`: DROAN obstacle avoidance - - `takeoff_landing_planner`: Specialized maneuvers - - `trajectory_library`: Trajectory generation utilities - -- **Controllers:** - - - `trajectory_controller`: Trajectory tracking - - `attitude_controller`: Attitude control - -**Topics:** - -- **Subscribed:** - - - `/[robot]/odometry` - - `/[robot]/global_plan` - - `/[robot]/sensors/front_stereo/disparity` +The local layer is the reactive, short-horizon part of the stack, organized in +three sub-layers: **world models** (e.g. disparity expansion) maintain an +obstacle representation around the robot, **planners** (DROAN, takeoff/landing) +generate collision-free trajectories through it, and **controllers** +(trajectory controller, PID controller) track those trajectories at high rate +and hand setpoints to the interface layer. -- **Published:** - - - `/[robot]/trajectory_controller/trajectory_segment_to_add` - - `/[robot]/trajectory_controller/look_ahead` - - `/[robot]/trajectory_controller/tracking_point` - - `/[robot]/local/cost_map` +See the [Local layer documentation](local/index.md); the handoff between +planners and the trajectory controller is the +[`trajectory` group (§5)](interface_conventions.md#5-trajectory-group-the-trajectory-controllers-contract-onboard-only). ### Global Layer -**Purpose:** Strategic path planning and global mapping. - -```mermaid -graph TB - subgraph "Global World Models" - VDBMap[VDB Mapping] - Occupancy[Occupancy Grid] - end - - subgraph "Global Planners" - RandomWalk[Random Walk Explorer] - Ensemble[Ensemble Planner] - end - - Perception -->|Pose| VDBMap - Sensors -->|Point Clouds| VDBMap - VDBMap --> Occupancy - - Occupancy --> RandomWalk - Occupancy --> Ensemble - Behavior -->|Goals| RandomWalk - Behavior -->|Goals| Ensemble - - RandomWalk --> GlobalPlan[Global Plan] - Ensemble --> GlobalPlan - GlobalPlan --> Local -``` - -**Key Modules:** - -- **World Models:** - - - `vdb_mapping_ros2`: VDB-based 3D mapping - -- **Planners:** +The global layer is the strategic counterpart: it maintains a persistent 3D +map of everywhere the robot has been (VDB mapping) and decides where to go +next — exploration and global path planning — handing global plans down to the +local layer for execution. - - `random_walk`: Random exploration planner - - `ensemble_planner`: Multi-planner coordination - -**Topics:** - -- **Subscribed:** - - - `/[robot]/odometry` - - `/[robot]/sensors/*/pointcloud` - - `/[robot]/behavior/mission_goal` - -- **Published:** - - - `/[robot]/global_plan` - - `/[robot]/global/map` - - `/[robot]/global/occupancy` +See the [Global layer documentation](global/index.md) and the +[`global_plan` contract (§4)](interface_conventions.md#4-global_plan-global-waypoint-path). ### Behavior Layer -**Purpose:** High-level mission execution and decision making. - -```mermaid -graph TB - Mission[Mission Manager] -->|Goals| BT[Behavior Tree] - BT -->|Evaluate| Conditions{Conditions} - Conditions -->|True| Actions[Actions] - Conditions -->|False| Fallback[Fallback] - - Actions -->|Global Goals| Global - Actions -->|Local Commands| Local - Actions -->|Mode Changes| Interface - - subgraph "Behavior Tree Components" - BT - Conditions - Actions - Fallback - end - - GCS[Ground Control Station] -->|Commands| Mission - Autonomy[Autonomy State] --> BT -``` - -**Key Modules:** +Onboard safety supervision. Mission-level sequencing is driven +by the operator from the GCS through [task executors](tasks.md); the +behavior layer's job is the part that must never depend on a ground link — +watching the robot's health and forcing a safe reaction when something +breaks (the `drone_safety_monitor` watches the state estimate and issues +safety commands when it times out). -- `behavior_tree`: Behavior tree framework -- `behavior_executive`: Mission execution engine -- `rqt_behavior_tree_command`: GUI for behavior tree control - -**Topics:** - -- **Subscribed:** - - - `/[robot]/odometry` - - `/[robot]/interface/mavros/state` - - `/[robot]/trajectory_controller/trajectory_completion_percentage` - -- **Published:** - - - `/[robot]/global/goal` - - `/[robot]/trajectory_controller/trajectory_override` - - `/[robot]/behavior/mission_state` +See the [Behavior layer documentation](behavior/index.md) and the +[`safety` contract (§9)](interface_conventions.md#9-safety-safety-executive-onboard-only). ## Complete Data Flow ### Autonomous Flight Scenario -Here's the complete data flow for an autonomous flight with obstacle avoidance: +An autonomous flight is a [task cascade](tasks.md#task-cascade). The GCS +operator is an **action client**: they send a task goal (e.g. +`ExplorationTask`) to a global-layer task executor, which decides where to go +and delegates the flying to the local-layer task executor via `NavigateTask`. +The local planner feeds trajectory segments to the perpetual trajectory +controller, which produces setpoints for the interface layer. Each action +**result returns to the client that sent the goal** — `NavigateTask` results +to the global executor, and the top-level task result (with ~1 Hz feedback +along the way) to the GCS: ```mermaid sequenceDiagram - participant BEH as Behavior - participant GLO as Global Planner - participant LOC as Local Planner - participant CTL as Controller + participant GCS as GCS (action client) + participant GLO as Global Task Executor
(random_walk_planner) + participant LOC as Local Task Executor
(droan_gl) + participant CTL as Trajectory Controller participant IF as Interface participant HW as Hardware/Sim - Note over BEH: Mission: Navigate to waypoint - BEH->>GLO: Goal Position - GLO->>GLO: Plan global path - GLO->>LOC: Global Plan + GCS->>GLO: ExplorationTask goal + GLO->>GLO: Choose next goal point + GLO->>LOC: NavigateTask goal (global plan) loop Obstacle Avoidance - HW->>IF: Sensor Data - IF->>LOC: Disparity Image + HW->>LOC: Sensor data (via sensors + perception) LOC->>LOC: Detect obstacles LOC->>LOC: Generate local trajectory LOC->>CTL: Trajectory Segment - CTL->>CTL: Compute control commands - CTL->>IF: Velocity Commands + CTL->>CTL: Compute control setpoint + CTL->>IF: Control setpoint IF->>HW: Actuator Commands end - Note over CTL: Waypoint reached - CTL->>BEH: Completion notification - BEH->>BEH: Next waypoint or mission complete + Note over LOC: Goal reached + LOC-->>GLO: NavigateTask result + GLO->>GLO: Next goal point, or done + GLO-->>GCS: ExplorationTask result ``` ## Module Communication Patterns @@ -500,94 +298,19 @@ This enables: ## Coordinate Frames -### Frame Hierarchy - -```mermaid -graph TB - World[world] --> Map[map] - Map --> Odom[odom] - Odom --> BaseLink[base_link] - BaseLink --> BaseLinkStab[base_link_stabilized] - BaseLinkStab --> Camera[camera_link] - BaseLinkStab --> Lidar[lidar_link] - BaseLink --> LookAhead[look_ahead_point] -``` - -**Standard Frames:** - -- `world`: Fixed world frame -- `map`: Global map frame (may drift from world) -- `odom`: Odometry frame (continuous, may drift) -- `base_link`: Robot body frame -- `base_link_stabilized`: Stabilized body frame (yaw-only) -- `camera_link`: Camera sensor frame -- `look_ahead_point`: Trajectory tracking reference - -## Performance Characteristics - -### Typical Update Rates - -| Layer | Module | Rate | Latency | -|-------|--------|------|---------| -| Interface | MAVROS | 50 Hz | <5 ms | -| Sensors | Camera | 30 Hz | <10 ms | -| Sensors | Disparity | 15 Hz | <30 ms | -| Perception | VIO | 30 Hz | <20 ms | -| Local Planner | DROAN | 10 Hz | <50 ms | -| Local Controller | Trajectory | 50 Hz | <10 ms | -| Global Planner | Path | 1 Hz | <500 ms | -| Behavior | BT Tick | 10 Hz | <5 ms | - -### Resource Usage (Typical) - -| Component | CPU | Memory | GPU | -|-----------|-----|--------|-----| -| Full Stack | 60-80% | 4-6 GB | 20-40% | -| Perception | 15-20% | 500 MB | 10-20% | -| Local Planning | 10-15% | 300 MB | 5-10% | -| Global Planning | 5-10% | 200 MB | 0% | -| Simulation | 30-40% | 2-3 GB | 60-80% | - -## Module Integration Guidelines - -When adding a new module, follow these integration patterns: - -### 1. Determine Layer Placement - -Place module in appropriate layer based on its function: - -- Real-time obstacle avoidance? → Local planning -- State estimation? → Perception -- Path planning? → Global planning -- Mission logic? → Behavior - -### 2. Define Interfaces - -Specify input and output topics: - -- Use standard topics when available -- Create custom topics with appropriate namespaces -- Document expected message rates and latencies - -### 3. Configure Launch Integration - -Add module to layer bringup with: - -- Topic remapping -- Namespace configuration -- Parameter loading -- Conditional launching (if needed) - -### 4. Test Integration - -Verify: +The frame tree, units, and the ENU convention are specified normatively in +[Interface Conventions — TF frames and units](interface_conventions.md#tf-frames-and-units). +For the reasoning behind the conventions — including the NED↔ENU conversion at +the PX4/MAVROS boundary and Isaac Sim's FLU convention — see the +[Frame Conventions](../../development/intermediate/frame_conventions.md) +concept page. -- Topics connect correctly -- Data flows as expected -- Performance meets requirements -- Works with other modules +## Integrating a New Module -See [Integration Checklist](integration_checklist.md) for detailed steps. +When adding a new module to the stack — choosing its layer, defining its topic +interfaces, wiring it into a stack entry launch file, and verifying the +connections — follow the [Integration Checklist](integration_checklist.md), +which is the canonical step-by-step guide. ## Multi-Robot Architecture @@ -642,8 +365,9 @@ graph TB ## References +- [Interface Conventions Specification](interface_conventions.md) - Normative topic, action, and frame contracts - [Integration Checklist](integration_checklist.md) - Module integration guidelines -- [AI Agent Guide](../../development/ai_agent_guide.md) - Guide for AI agents +- [AI Agent Guide](../../development/advanced/ai_agent_guide.md) - Guide for AI agents - [Layer Documentation](index.md) - Detailed layer descriptions - Skills: diff --git a/docs/robot/autonomy/tasks.md b/docs/robot/autonomy/tasks.md index 20345ea92..5ef549f92 100644 --- a/docs/robot/autonomy/tasks.md +++ b/docs/robot/autonomy/tasks.md @@ -8,16 +8,16 @@ See [System Architecture — Node Types](system_architecture.md#node-types-perpe ## Task Cascade -Behavior sends high-level task goals to global-layer task executors, which in turn delegate navigation to local-layer task executors: +The operator sends high-level task goals from the GCS (Foxglove robot-commands panel or RViz Tasks Panel) to global-layer task executors, which in turn delegate navigation to local-layer task executors: ```mermaid graph TD - BE[behavior_executive] -->|ExplorationTask| RW[random_walk_planner] + GCS[GCS operator] -->|ExplorationTask| RW[random_walk_planner] RW -->|NavigateTask| DG[droan_gl / droan_local_planner] DG -->|TrajectoryXYZVYaw| TC[trajectory_controller] ``` -All task action servers are remapped to `/{robot_name}/tasks/{task_name}` in the bringup launch files. +All task action servers are remapped to `/{robot_name}/tasks/{task_name}` in their module launch files. ## Task Action Types @@ -41,11 +41,73 @@ geometry_msgs/Point current_position --- +### TakeoffTask + +**File:** `action/TakeoffTask.action` +**Action server:** `/{robot_name}/tasks/takeoff` +**Implemented by:** `takeoff_landing_planner` + +Take off to a target altitude. The goal is rejected if the robot is not armed, offboard control is not active, or the state estimate has timed out. + +#### Goal + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `target_altitude_m` | float32 | Altitude to climb to (m) | +| `velocity_m_s` | float32 | Ascent velocity (m/s) | + +#### Result + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `success` | bool | True if the target altitude was reached; false if rejected, canceled, or error | +| `message` | string | Completion reason | + +#### Feedback + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `status` | string | Task status string | +| `current_altitude_m` | float32 | Current altitude (m) | +| `target_altitude_m` | float32 | Target altitude (m) | + +--- + +### LandTask + +**File:** `action/LandTask.action` +**Action server:** `/{robot_name}/tasks/land` +**Implemented by:** `takeoff_landing_planner` + +Land the robot at its current position. + +#### Goal + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `velocity_m_s` | float32 | Descent velocity (m/s); `0.0` = use default from config | + +#### Result + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `success` | bool | True if the robot landed; false if canceled or error | +| `message` | string | Completion reason | + +#### Feedback + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `status` | string | Task status string | +| `current_altitude_m` | float32 | Current altitude (m) | + +--- + ### FixedTrajectoryTask **File:** `action/FixedTrajectoryTask.action` **Action server:** `/{robot_name}/tasks/fixed_trajectory` -**Implemented by:** *(not yet implemented)* +**Implemented by:** `trajectory_controller` package (`fixed_trajectory_task` node) Follow a pre-defined trajectory specified by shape type and parameters. With `loop: true`, the trajectory repeats until the task is canceled. @@ -272,7 +334,7 @@ See the [add-task-executor](../../../.agents/skills/add-task-executor) skill for 2. Create a new package under `robot/ros_ws/src/global/planners/` (or `local/planners/` if it is a navigation executor) 3. Implement the four action server callbacks: `handle_goal`, `handle_cancel`, `handle_accepted`, `execute` 4. In `execute()`, delegate navigation to `/{robot_name}/tasks/navigate` (NavigateTask) via an action client -5. Add a remap in the layer bringup launch file: `` +5. Add a remap in your module's launch file (included by the stack entry file): `` 6. Document the node with a **Task Executor** section in its `README.md` Reference implementation: [random_walk_planner](../../../robot/ros_ws/src/global/planners/random_walk/README.md) diff --git a/docs/robot/autonomy_modes.md b/docs/robot/autonomy_modes.md index 00f11a2ce..b1536658f 100644 --- a/docs/robot/autonomy_modes.md +++ b/docs/robot/autonomy_modes.md @@ -1,13 +1,25 @@ -# Onboard/Offboard Distributed Computing +# Onboard/Offboard Distributed Computing -AirStack uses a **role** system to control which planning modules launch inside each container. -The role is hardcoded per compose service — no environment variables need to be set by hand. +AirStack uses **stacks** ([docs/development/stacks.md](../development/stacks.md)) +to control which autonomy modules launch inside each container. Each compose +service carries a default stack — no environment variables need to be set by +hand. (Stacks are the only dispatch mechanism; a set `AUTONOMY_ROLE` +environment variable is a preflight error.) -| Role | Value | What runs | -|---|---|---| -| **Full** | `full` | Every autonomy module: interface, sensors, perception, local planning, global planning, behavior | -| **Onboard** | `onboard` | Lite modules only: interface, sensors, perception, local planning, behavior — no global planner | -| **Offboard** | `offboard` | Global planner only — runs on the GCS paired with onboard robots | +| Stack | What runs | +|---|---| +| **`full_default`** | Every autonomy module: interface, sensors, perception, local planning, global planning, behavior, logging — the default when no stack is selected | +| **`lite_default`** | Lite modules only: interface, sensors, perception, local planning, behavior — no global planner | +| **`lite_offload_global:onboard`** | The lite set on the vehicle, bridged to an offboard global half per the stack's `bridge.yaml` | +| **`lite_offload_global:offboard`** | Global planner + world model only — runs on the GCS paired with onboard robots | +| **`full_droan_cpu`** | `full_default` with the CPU DROAN local planner (`droan_local_planner` + `disparity_expansion`) instead of the GPU `droan_gl` node | +| **`full_macvo`** | `full_default` with MAC-VO as the disparity source — requires `airstack module add asm_macvo` first | +| **`full_mighty`** | `full_default` with the MIGHTY map-based local planner (`asm_mighty` module: planner + acl-mapping voxel world model + NavigateTask bridge) in place of `droan_gl` | + +Instead of picking a stack per container, `airstack up --fleet ` launches +a whole **fleet**: `config/fleets/.yaml` declares who exists, which +vehicle each robot flies, which stack it runs, and which ground hosts run each +split stack's offboard half — see [Fleets](../development/fleets.md). --- @@ -17,21 +29,24 @@ Profiles are split into **deployment** and **simulator** categories. **Deployment profiles:** -| Profile | Machine | Services started | Role(s) | +| Profile | Machine | Services started | Default stack(s) | |---|---|---|---| -| `desktop` | Dev desktop | `robot-desktop` + `gcs` | `full` | -| `desktop_split` | Dev desktop | `robot-desktop-onboard` + `robot-offboard` + `gcs` | `onboard` + `offboard` | -| `l4t` | Jetson | `robot-l4t` + `zed-l4t` | `full` | -| `l4t_lite` | Jetson | `robot-l4t-onboard` + `zed-l4t` | `onboard` | -| `voxl` | VOXL2 | `robot-voxl-onboard` | `onboard` (always) | -| `offboard` | Ground station | `robot-offboard` ×N + `gcs-real` | `offboard` | +| `desktop` | Dev desktop | `robot-desktop` + `gcs` | `full_default` | +| `desktop_split` | Dev desktop | `robot-desktop-onboard` + `robot-offboard` + `gcs` | `lite_default` + `lite_offload_global:offboard` | +| `l4t` | Jetson | `robot-l4t` + `zed-l4t` | `full_default` | +| `l4t_lite` | Jetson | `robot-l4t-onboard` | `lite_default` | +| `voxl` (alias `voxl_onboard`) | VOXL2 | `robot-voxl-onboard` | `lite_default` (compute-constrained) | +| `offboard` | Ground station | `robot-offboard` ×N + `gcs-real` | `lite_offload_global:offboard` | + +The hardware-profile defaults are redefinable per deployment (env / +`--env-file` / `--stack`). **Simulator profiles (mutually exclusive, `desktop`/`desktop_split` only):** | Profile | Simulator | |---|---| | `isaac-sim` | NVIDIA Isaac Sim (Pegasus) | -| `ms-ms-airsim` | Microsoft AirSim (legacy) (UE4) | +| `ms-airsim` | Microsoft AirSim (legacy) (UE4) | | `simple` | Simple Sim | Only one simulator profile can be active at a time. `airstack up` will error if multiple are set. @@ -46,19 +61,19 @@ Combine with a simulator profile. ``` Dev desktop ├── simulator (isaac-sim / ms-airsim / simple) -├── robot-desktop × N [role: full] +├── robot-desktop × N [stack: full_default] └── gcs ``` ```bash -# Isaac Sim (set in .env: COMPOSE_PROFILES="desktop,isaac-sim"): -airstack up +# Isaac Sim: +airstack up --sim isaac # Microsoft AirSim (legacy): -COMPOSE_PROFILES="desktop,ms-airsim" airstack up +airstack up --sim airsim # Multiple simulated robots: -NUM_ROBOTS=3 airstack up +airstack up --sim isaac --robots 3 ``` Each replica gets a unique `ROBOT_NAME` (`robot_1`, `robot_2`, `robot_3`) and `ROS_DOMAIN_ID` (1, 2, 3) @@ -76,8 +91,8 @@ Use this to debug the split configuration and domain bridge without needing phys ``` Dev desktop ├── simulator (isaac-sim / ms-airsim / simple) -├── robot-desktop-onboard × N [role: onboard, ROS_DOMAIN_ID = 1..N] -├── robot-offboard × N [role: offboard, ROS_DOMAIN_ID = 0] +├── robot-desktop-onboard × N [stack: lite_default, ROS_DOMAIN_ID = 1..N] +├── robot-offboard × N [stack: lite_offload_global:offboard, ROS_DOMAIN_ID = 0] └── gcs [domain 0] ``` @@ -91,8 +106,11 @@ airstack --profile desktop_split --profile isaac-sim up !!! note "Domain isolation" Onboard containers run on `ROS_DOMAIN_ID` 1, 2, 3… (one per robot). All offboard containers and the GCS share `ROS_DOMAIN_ID=0`. - A `domain_bridge` node inside each `robot-offboard` container bridges only the - necessary topics across the domain boundary to avoid flooding the radio link. + The DDS router bridges only the topics listed in the split stack's + `bridge.yaml` across the domain boundary to avoid flooding the radio + link — generate its config first: + `python3 tools/gen_dds_router.py stacks/lite_offload_global/bridge.yaml` + (or `airstack fleet generate `). --- @@ -116,8 +134,8 @@ Lite modules run on the Jetson; global planning runs on the ground station. # On the Jetson: airstack --profile l4t_lite up -# On the ground station (set NUM_ROBOTS to match fleet size): -NUM_ROBOTS=3 airstack --profile offboard up +# On the ground station (--robots must match the fleet size): +airstack --profile offboard up --robots 3 ``` --- @@ -132,7 +150,7 @@ for global planning. Global planning must always run on the GCS. airstack --profile voxl up # On the ground station: -NUM_ROBOTS=3 airstack --profile offboard up +airstack --profile offboard up --robots 3 ``` --- @@ -142,18 +160,23 @@ NUM_ROBOTS=3 airstack --profile offboard up If `AUTOLAUNCH=false`, containers start idle. Launch manually inside the container: ```bash -# Full role (desktop or l4t): -ros2 launch autonomy_bringup robot.launch.xml role:=full sim:=false +# Full stack (desktop or l4t) — also the default with no stack args: +ros2 launch autonomy_bringup robot.launch.xml sim:=false \ + stack_dir:=/root/AirStack/stacks/full_default -# Onboard role (VOXL, l4t_lite, desktop_split onboard): -ros2 launch autonomy_bringup robot.launch.xml role:=onboard sim:=false +# Lite stack (VOXL, l4t_lite, desktop_split onboard): +ros2 launch autonomy_bringup robot.launch.xml sim:=false \ + stack_dir:=/root/AirStack/stacks/lite_default -# Offboard role (GCS): -ros2 launch autonomy_bringup robot.launch.xml role:=offboard sim:=false +# Offboard half of the split stack (GCS): +ros2 launch autonomy_bringup robot.launch.xml sim:=false \ + stack_dir:=/root/AirStack/stacks/lite_offload_global stack_entry:=offboard ``` -`desktop_bringup` wraps the above and adds RViz (only when `sim:=true`): +`desktop_bringup` wraps the above and adds RViz (only when `sim:=true`); +the stack selection flows through the `AIRSTACK_STACK_DIR` / +`AIRSTACK_STACK_ENTRY` env vars: ```bash -ros2 launch desktop_bringup robot.launch.xml role:=full sim:=true +ros2 launch desktop_bringup robot.launch.xml sim:=true ``` diff --git a/docs/robot/configuration/environment_variables.md b/docs/robot/configuration/environment_variables.md new file mode 100644 index 000000000..02886d0f4 --- /dev/null +++ b/docs/robot/configuration/environment_variables.md @@ -0,0 +1,90 @@ +# Environment Variable Reference (`.env`) + +The top-level [`.env`](https://github.com/castacks/AirStack/blob/main/.env) file sets Docker Compose **interpolation variables** — image tags, profile selection, replica counts, and per-container launch switches. These variables do **not** automatically become environment variables inside the containers: a variable only reaches a container if a compose file forwards it through an `environment:` entry (see each *Consumed by* column). `airstack up` reads `.env` automatically; launch-intent flags (`--sim`, `--robots`, `--stack`, ...) override it by exporting the same variables before compose runs. This page is the complete schema; the [Docker guide](../docker/index.md#environment-variables) summarizes the subset forwarded into robot containers, and the [Configuration overview](index.md) explains where non-compose configuration lives. + +## Project & Images + +These variables assemble every image tag as `${PROJECT_DOCKER_REGISTRY}/${PROJECT_NAME}:v${VERSION}_` (robot images additionally append `_${DOCKER_IMAGE_BUILD_MODE}`). + +| Variable | Purpose | Default | Consumed by | +| -------- | ------- | ------- | ----------- | +| `PROJECT_NAME` | Repository name for Docker images and part of every image tag | `"airstack"` | `image:` tags in all compose files (`robot/docker/`, `gcs/docker/`, `simulation/*/docker/`) | +| `VERSION` | Semver image version; bumped per release, so the value in `.env` is always the current release (e.g. `0.21.0-dev.15`) | current release semver | `image:` tags in all compose files; CI version gate (`check-version-increment.yml`) | +| `DOCKER_IMAGE_BUILD_MODE` | Image-tag discriminator **only** — no Dockerfile consumes it. Keep `dev` (mounted code, built live); a real `prebuilt` workspace-baked stage is future work | `"dev"` | Tag suffix of the robot images (`robot/docker/docker-compose.yaml`) | +| `PROJECT_DOCKER_REGISTRY` | Registry to push/pull images from | `"airlab-docker.andrew.cmu.edu/airstack"` | `image:` tags in all compose files | +| `COMPOSE_PROFILES` | Default compose profiles when none are passed explicitly | `"desktop,isaac-sim"` | Docker Compose profile selection; rewritten by `airstack up --sim ` (swaps the simulator profile) and `--fleet` (heterogeneous fleets swap `desktop` for `fleet`) | + +## Launch Behavior + +| Variable | Purpose | Default | Consumed by | +| -------- | ------- | ------- | ----------- | +| `AUTOLAUNCH` | If `false`, containers spawn idle with no launch command (tmux session still created) | `"true"` | Container `command:` of every robot service, `isaac-sim`, `ms-airsim`, and `gcs` (each gates its tmux autolaunch on it) | +| `NUM_ROBOTS` | Number of robot containers to launch (compose replicas) | `"1"` | `deploy.replicas` of `robot-desktop`/`robot-offboard`; forwarded into `isaac-sim`, `ms-airsim`, and `gcs` containers (drone spawn count / peer list). Overridden by `airstack up --robots N`; derived from the fleet file with `--fleet` | +| `RECORD_BAGS` | Start the bag recorder node with the stack (see [Rosbags](../logging/rosbags.md)) | `"false"` | `robot_base` and `gcs` `environment:` → `bag_recorder_pid` via the logging bringup | + +## Isaac Sim + +All four are forwarded into (or read by the `command:` of) the `isaac-sim` service in `simulation/isaac-sim/docker/docker-compose.yaml`. + +| Variable | Purpose | Default | Consumed by | +| -------- | ------- | ------- | ----------- | +| `ISAAC_SIM_GUI` | USD scene path for the **non-standalone** launch path | `/isaac-sim/AirStack/simulation/isaac-sim/assets/scenes/simple_pegasus.scene.usd` | `ros2 launch isaacsim run_isaacsim.launch.py gui:=...` branch of the `isaac-sim` command (only when `ISAAC_SIM_USE_STANDALONE` is not `true`) | +| `ISAAC_SIM_USE_STANDALONE` | `true` = launch Isaac Sim via a standalone Python script; `false` = load the `ISAAC_SIM_GUI` USD file via `run_isaacsim.launch.py` | `"true"` | Branch selector in the `isaac-sim` container command | +| `ISAAC_SIM_SCRIPT_NAME` | Standalone launch script, resolved under `/AirStack/simulation/isaac-sim/launch_scripts/`. The default spawns exactly **one** drone; multi-robot needs `example_multi_px4_pegasus_launch_script.py` (auto-selected by `airstack up --robots N>1`) and fleets use `fleet_spawn.py` (auto-selected by `--fleet`) | `"example_one_px4_pegasus_launch_script.py"` | Standalone branch of the `isaac-sim` container command | +| `PLAY_SIM_ON_START` | Start the sim **playing** instead of paused (`airstack up --no-play` to come up paused) | `"true"` | `isaac-sim` `environment:` → the Pegasus launch scripts (`pegasus_app.py` and the example scripts); passed as `play_sim_on_start:=` in the USD launch path | + +## Robot Identity & Description + +| Variable | Purpose | Default | Consumed by | +| -------- | ------- | ------- | ----------- | +| `ROBOT_NAME_MAP_CONFIG_FILE` | Mapping file (in `robot/docker/robot_name_map/`) that resolves each container to `ROBOT_NAME` + `ROS_DOMAIN_ID` — see [Robot Identity](../docker/robot_identity.md) | `"default_robot_name_map.yaml"` | `robot_base` `environment:` → `resolve_robot_name.py`, run by `robot/docker/.bashrc` at container startup | +| `URDF_FILE` | Robot description, relative to the workspace `robot_descriptions/` install. Swapped automatically by `airstack up --sim isaac\|airsim` to the matching sensor URDF | `robot_descriptions/iris/urdf/iris_with_sensors.pegasus.robot.urdf` | `robot_base` `environment:` → `autonomy_bringup/launch/robot.launch.xml` (robot state publisher) | +| `DEBUG_RVIZ` | If `true`, launches RViz alongside the robot | `"false"` | `robot_base` `environment:` → `desktop_bringup/launch/robot.launch.xml` | + +## Ports + +| Variable | Purpose | Default | Consumed by | +| -------- | ------- | ------- | ----------- | +| `OFFBOARD_BASE_PORT` | Base UDP port for offboard (API-out) MAVLink streams; offset per robot so multi-agent FCU communication doesn't collide | `14540` | `robot_base` `environment:` → `interface_bringup/launch/interface.launch.py` (MAVROS `fcu_url` calculation) | +| `ONBOARD_BASE_PORT` | Base UDP port for onboard MAVLink streams, offset per robot | `14580` | Same as above | + +## Variables Exported by `airstack up` (Not Set in `.env`) + +`airstack up` parses its launch-intent flags (`parse_launch_intent` / `apply_launch_intent` in `airstack.sh`) and exports these before invoking compose. Set them by flag, not by editing `.env` — though explicit env / `--env-file` values take precedence (leaf-value precedence, with an override banner for fleet conflicts). + +| Variable | Set by | Purpose | Consumed by | +| -------- | ------ | ------- | ----------- | +| `AIRSTACK_STACK_DIR` | `--stack ` (always exported; no stack = `/root/AirStack/stacks/full_default`) | Container path of the stack folder whose entry launch file defines the autonomy topology — see [Stacks](../../development/stacks.md) | `robot_base` `environment:` → `autonomy_bringup/launch/robot.launch.xml` dispatch | +| `AIRSTACK_STACK_ENTRY` | `--stack :` (default `stack`) | Entry launch file name: `launch/.launch.xml` (split stacks use `onboard`/`offboard`) | Same as above | +| `FLEET_CONFIG_FILE` | `--fleet ` | Container path of the fleet file (`/root/AirStack/config/fleets/...`); empty = legacy `robot_name_map` resolution — see [Fleets](../../development/fleets.md) | `robot_base` and `isaac-sim` `environment:` → `robot/docker/.bashrc` (per-container identity/stack via `tools/fleet/resolve_fleet.py`) and `fleet_spawn.py` | +| `NUM_ROBOTS` | `--robots N`, or derived from the fleet file with `--fleet` (mutually exclusive flags) | Overrides the `.env` value above | As in Launch Behavior | +| `PLAY_SIM_ON_START` | `--play` / `--no-play` | Overrides the `.env` value above | As in Isaac Sim | +| `AUTOLAUNCH` | `--no-autolaunch` | Overrides the `.env` value above (sets `false`) | As in Launch Behavior | +| `ISAAC_SIM_HEADLESS` | `--headless` | Run Isaac Sim without a window | `isaac-sim` `environment:` (default `false`; forced `true` by the `isaac-sim-livestream` service) | +| `MS_AIRSIM_HEADLESS` | `--headless` | Run the UE4 binary off-screen | `ms-airsim` `environment:` | +| `QT_QPA_PLATFORM` | `--headless` (sets `offscreen`) | Keeps Qt tools (RViz etc.) from requiring a display | `robot_base` `environment:` | +| `COMPOSE_PROFILES` | `--sim isaac\|airsim\|simple` (swaps the simulator profile; `simple` also drops `desktop`), `--fleet` (heterogeneous: swaps `desktop` for `fleet`) | Overrides the `.env` value above | Docker Compose profile selection | +| `URDF_FILE` | `--sim isaac\|airsim` | Swaps to the simulator-matched sensor URDF | As in Robot Identity & Description | +| `ISAAC_SIM_SCRIPT_NAME` | `--robots` (one ↔ multi example script) and `--fleet` (→ `fleet_spawn.py`); an explicit env value always wins | Overrides the `.env` value above | As in Isaac Sim | +| `ISAAC_SIM_SCENE`, `ISAAC_SIM_STAGE_SCALE`, `MS_AIRSIM_SCENE` | `--scene ` via `simulation/resolve_scene.py` (`simulation/scenes.yaml` catalog) | Scene selection for the active simulator — see [Scenes](../../simulation/scenes.md) | `isaac-sim` / `ms-airsim` `environment:` → the launch scripts / `entrypoint.sh` | + +## Notable Optional Variables + +These appear commented-out in `.env` (or are read by compose with a built-in default) and are documented in full on their own pages. + +| Variable | Purpose | Documented in | +| -------- | ------- | ------------- | +| `ISAAC_SIM_SCENE`, `ISAAC_SIM_STAGE_SCALE` | Direct scene override / stage scale for cm-authored stages (normally set via `--scene`) | [Scenes](../../simulation/scenes.md) | +| `MS_AIRSIM_SCENE`, `MS_AIRSIM_ENV_DIR`, `MS_AIRSIM_BINARY_PATH`, `MS_AIRSIM_HEADLESS`, `MS_AIRSIM_PX4_START_DELAY` | UE4 scene fetching/selection, binary override, off-screen rendering, PX4 start delay | [MS AirSim Docker](../../simulation/ms-airsim/docker.md) | +| `BAG_STORAGE_PATH` | Host directory mounted at `/bags` on the `l4t` profile (default `/media/airlab/Storage/airstack_collection`) | [Rosbags](../logging/rosbags.md) | +| `LOG_CONFIG` | Bag recorder topic-selection file in `logging_bringup/config/` (default `log.yaml`) | [Rosbags](../logging/rosbags.md) | +| `ISAAC_SIM_FOLLOW_CAM`, `ISAAC_SIM_FOLLOW_CAM_OFFSET`, `ISAAC_SIM_FOLLOW_CAM_LIGHT` | Viewport follow-camera: drone domain id to chase, world-frame offset, headlight for unlit interiors | [Isaac Sim](../../simulation/isaac_sim/index.md) (read by `pegasus_app.py`) | +| `SIM_IP` | Address robot containers use to reach the simulator (default `172.31.0.200`; every sim service binds this fixed address) | `robot/docker/docker-compose.yaml`, `simulation/isaac-sim/docker/docker-compose.yaml` | +| `FCU_URL` | MAVROS flight-controller URL on real hardware (default `/dev/ttyTHS4:115200` on `l4t`); desktop/sim derive it from the base ports instead | `robot/docker/docker-compose.yaml` (`robot-l4t`) | +| `CACHE_TAG` | Floating Docker-layer-cache image tag (default `cache`) — CI only | [CI/CD](../../development/intermediate/testing/ci_cd.md) | + +## See Also + +- [Docker Services — Environment Variables](../docker/index.md#environment-variables) — the subset forwarded into robot containers +- [Robot Configuration](index.md) — where non-compose configuration lives (stacks, module parameters, fleets) +- [AirStack CLI](../../development/beginner/airstack-cli/index.md) — the `airstack up` flags that export these variables diff --git a/docs/robot/configuration/index.md b/docs/robot/configuration/index.md index d6d527b73..9a591348a 100644 --- a/docs/robot/configuration/index.md +++ b/docs/robot/configuration/index.md @@ -1,52 +1,75 @@ # Robot Configuration -Configure robot-specific parameters, sensor calibrations, and system settings for AirStack deployment. +Configure robot identity, stack selection, and module parameters for AirStack deployment. -## Overview +## Where Configuration Actually Lives -Robot configuration includes: +There is no single configuration file — settings live at the level they affect: -- **Robot Identity**: Unique identification (name, ROS_DOMAIN_ID) -- **Sensor Configuration**: Calibration parameters and topic mappings -- **Network Settings**: Communication and connectivity -- **Hardware Parameters**: Platform-specific settings (Jetson, VOXL) -- **Autonomy Parameters**: Behavior and performance tuning +| Level | Location | What it controls | +| ----- | -------- | ---------------- | +| Compose / containers | top-level `.env` | Image tags, `NUM_ROBOTS`, `AUTOLAUNCH`, sim selection, bag recording | +| Stack (launch topology) | `stacks//launch/*.launch.xml` | Which modules run and how their topics are wired (launch args, remaps) | +| Module parameters | each package's `config/*.yaml` (`robot/ros_ws/src//...//config/`) | Algorithm-specific ROS 2 parameters | +| Fleet era (RFC #380) | `config/vehicles/` and `config/fleets/` | Vehicle definitions; who exists, which vehicle, which stack, which ground hosts | +| Robot identity | `robot/docker/robot_name_map/` | Container/hostname → `ROBOT_NAME` + `ROS_DOMAIN_ID` mapping | -## Configuration Files +## Stack Selection -### Environment Variables +The autonomy topology is selected by a **stack** — stacks are the only dispatch +(the legacy `AUTONOMY_ROLE` role dispatch was removed). `airstack up --stack +[:]` exports `AIRSTACK_STACK_DIR` (the container path of the stack +folder, `/root/AirStack/stacks/`) and `AIRSTACK_STACK_ENTRY` (the entry +launch file name, default `stack`). Unset, the trunk reference stack +`full_default` is used. -Key environment variables configured in `robot/docker/.env`: +See [Stacks](../../development/stacks.md) for the reference stacks and how to +create your own. -```bash -# Robot Identity -ROBOT_NAME=robot1 -ROS_DOMAIN_ID=0 +## Environment Variables + +Key variables in the top-level `.env` file (compose-level configuration): +```bash # Launch Configuration -AUTOLAUNCH=true -AUTONOMY_MODE=onboard_all +AUTOLAUNCH="true" # false = spawn idle containers with no launch command + +# Multi-robot +NUM_ROBOTS="1" # Number of robot containers (compose replicas) -# Sensor Configuration -ENABLE_CAMERA=true -ENABLE_LIDAR=false -CAMERA_TOPIC=/camera/image_raw +# Robot identity mapping (name → ROBOT_NAME + ROS_DOMAIN_ID) +ROBOT_NAME_MAP_CONFIG_FILE="default_robot_name_map.yaml" + +# Logging +RECORD_BAGS="false" # Start the bag recorder node (see Logging docs) ``` -### ROS 2 Parameters +Stack selection (`AIRSTACK_STACK_DIR`, `AIRSTACK_STACK_ENTRY`) and fleet +selection (`FLEET_CONFIG_FILE`) are exported by `airstack up --stack` / +`--fleet` rather than set by hand in `.env`. + +`ROBOT_NAME` and `ROS_DOMAIN_ID` are **not** set in `.env` — each container +resolves them at startup from `ROBOT_NAME_SOURCE` and the mapping config; see +[Robot Identity](../docker/robot_identity.md). -Module-specific parameters in YAML files: +The full table of variables forwarded into the robot containers is in the +[Docker guide](../docker/index.md#environment-variables). -**Location**: `robot/ros_ws/src//_bringup/config/` +## ROS 2 Parameters -**Example** (`perception_bringup/config/state_estimation.yaml`): +Module-specific parameters live in YAML files in each module package's own +`config/` directory (`robot/ros_ws/src//...//config/`); stacks +override them via launch arguments in their entry files +(`stacks//launch/*.launch.xml`). + +**Example** (`robot/ros_ws/src/sensors/lidar_point_cloud_filter/config/lidar_point_cloud_filter.yaml`): ```yaml -state_estimator: +/**: ros__parameters: - publish_rate: 100.0 - use_gps: true - imu_topic: /imu/data - gps_topic: /gps/fix + near_range_m: 0.75 + input_topic: "/$(env ROBOT_NAME)/sensors/ouster/point_cloud_raw" + output_topic: "/$(env ROBOT_NAME)/sensors/ouster/point_cloud" + qos_reliable: true ``` ## Robot Identity @@ -57,308 +80,17 @@ See: [Robot Identity Guide](../docker/robot_identity.md) **Key Settings**: -- **ROBOT_NAME**: Namespace for all topics (`/robot1/...`) +- **ROBOT_NAME**: Namespace for all topics (`/robot_1/...`) - **ROS_DOMAIN_ID**: Isolate ROS 2 communication (0-101) -- **Hostname**: Unique network identifier - -## Sensor Configuration - -### Camera Configuration - -Configure camera parameters: - -```yaml -camera: - ros__parameters: - frame_id: camera_link - width: 1280 - height: 720 - fps: 30 - encoding: rgb8 -``` - -### IMU Configuration - -IMU calibration and orientation: - -```yaml -imu: - ros__parameters: - frame_id: imu_link - accel_stddev: 0.01 - gyro_stddev: 0.005 - orientation_covariance: [0.01, 0, 0, - 0, 0.01, 0, - 0, 0, 0.01] -``` - -### Depth Sensor Configuration - -Depth camera/stereo parameters: - -```yaml -depth_camera: - ros__parameters: - frame_id: depth_camera_link - min_range: 0.5 - max_range: 10.0 - fov_horizontal: 87.0 - fov_vertical: 58.0 -``` - -## Autonomy Configuration - -### Local Planning Parameters - -Tune local planner behavior: - -```yaml -local_planner: - ros__parameters: - planning_horizon: 5.0 - max_velocity: 2.0 - max_acceleration: 1.0 - obstacle_margin: 0.5 -``` - -### Global Planning Parameters - -Configure global planner: - -```yaml -global_planner: - ros__parameters: - planning_rate: 1.0 - goal_tolerance: 0.5 - path_resolution: 0.1 -``` - -### Controller Parameters - -Trajectory controller tuning: - -```yaml -trajectory_controller: - ros__parameters: - kp_position: 1.0 - kd_position: 0.5 - kp_velocity: 0.8 - max_thrust: 20.0 -``` - -## Network Configuration - -### WiFi Configuration - -For onboard computer (Jetson/VOXL): - -```bash -# /etc/netplan/01-netcfg.yaml -network: - version: 2 - wifis: - wlan0: - dhcp4: yes - access-points: - "YourSSID": - password: "YourPassword" -``` - -### Static IP (Optional) - -For reliable communication: - -```yaml -network: - version: 2 - ethernets: - eth0: - addresses: [192.168.1.100/24] - gateway4: 192.168.1.1 - nameservers: - addresses: [8.8.8.8, 8.8.4.4] -``` - -## Platform-Specific Configuration - -### NVIDIA Jetson - -Power mode settings: - -```bash -# Maximum performance -sudo nvpmodel -m 0 -sudo jetson_clocks - -# Balanced mode -sudo nvpmodel -m 2 -``` - -Configure fan control: - -```bash -# /etc/systemd/system/jetson-fan.service -[Unit] -Description=Jetson Fan Control - -[Service] -Type=simple -ExecStart=/usr/bin/jetson_fan.py - -[Install] -WantedBy=multi-user.target -``` - -### ModalAI VOXL - -VOXL-specific configuration via `voxl-configure-*` tools: - -```bash -# Configure cameras -voxl-configure-cameras - -# Configure MPA -voxl-configure-mpa - -# Configure vision -voxl-configure-vision -``` - -## Parameter Tuning Workflow - -### 1. Baseline Configuration - -Start with default parameters from reference implementation. - -### 2. Simulation Testing - -Test parameter changes in Isaac Sim: - -```bash -# Launch with custom parameters -ros2 launch my_module_bringup my_module.launch.xml param_file:=config/tuned_params.yaml -``` - -### 3. HITL Validation - -Validate on hardware-in-the-loop setup before field deployment. - -See: [HITL Testing](../../real_world/HITL/index.md) - -### 4. Field Tuning - -Fine-tune based on real-world performance: - -- Monitor performance metrics -- Adjust parameters incrementally -- Document changes and rationale -- Test thoroughly after each change - -## Configuration Management - -### Version Control - -Track configuration files in Git: - -```bash -git add robot/ros_ws/src/*/config/*.yaml -git commit -m "Tune planner parameters for outdoor operation" -``` - -### Robot-Specific Configs - -For multiple robots with different configurations: - -``` -robot/ros_ws/src/my_module/config/ -├── default.yaml # Default parameters -├── robot1.yaml # Robot 1 overrides -├── robot2.yaml # Robot 2 overrides -└── outdoor.yaml # Environment-specific -``` - -Load appropriate config: - -```xml - - - -``` - -### Configuration Validation - -Validate configuration before deployment: - -```python -#!/usr/bin/env python3 -import yaml - -def validate_config(config_file): - with open(config_file) as f: - config = yaml.safe_load(f) - - # Check required parameters exist - assert 'max_velocity' in config - assert 'planning_horizon' in config - - # Check parameter ranges - assert 0 < config['max_velocity'] <= 5.0 - assert config['planning_horizon'] > 0 - - print(f"✓ Configuration {config_file} is valid") - -if __name__ == "__main__": - validate_config("config/my_params.yaml") -``` - -## Dynamic Reconfiguration - -Some parameters can be changed at runtime without restart: - -```bash -# Get current parameter value -ros2 param get /my_node my_parameter - -# Set new parameter value -ros2 param set /my_node my_parameter 2.5 - -# Dump all parameters -ros2 param dump /my_node > current_params.yaml -``` - -## Troubleshooting - -**Parameter changes not taking effect**: - -- Verify parameter file path in launch file -- Check for typos in parameter names -- Rebuild package if C++ parameters changed -- Restart nodes after parameter changes - -**Invalid parameter values**: - -- Check parameter validation in node code -- Review error messages for allowed ranges -- Verify YAML syntax (indentation, types) - -**Configuration conflicts**: - -- Check for multiple parameter files being loaded -- Verify launch file parameter precedence -- Use `ros2 param dump` to see actual loaded values - -## Best Practices -- **Document parameters**: Add comments in YAML files -- **Use reasonable defaults**: Safe, conservative values -- **Validate inputs**: Check parameter ranges in code -- **Version control**: Track configuration changes -- **Test incrementally**: Change one parameter at a time -- **Keep backups**: Save known-good configurations +Both are resolved at container startup by +`robot/docker/robot_name_map/resolve_robot_name.py` from the mapping file +selected by `ROBOT_NAME_MAP_CONFIG_FILE`. ## See Also - [Robot Identity](../docker/robot_identity.md) - Configuring robot identification -- [Autonomy Modes](../../tutorials/autonomy_modes.md) - Different operation modes -- [HITL Testing](../../real_world/HITL/index.md) - Testing configuration on hardware +- [Stacks](../../development/stacks.md) - Stack folders and entry launch files +- [Fleets](../../development/fleets.md) - Fleet files, vehicles, and placement +- [Autonomy Modes](../autonomy_modes.md) - Different operation modes - [Integration Checklist](../autonomy/integration_checklist.md) - Module configuration requirements diff --git a/docs/robot/docker/index.md b/docs/robot/docker/index.md index 1502fdeb6..c775625e5 100644 --- a/docs/robot/docker/index.md +++ b/docs/robot/docker/index.md @@ -15,16 +15,19 @@ robot/docker/ All robot services inherit from the shared `robot_base` service defined in `robot-base-docker-compose.yaml`. Platform-specific services then extend `robot_base` and override only what differs for that target. -``` +```text robot_base (robot-base-docker-compose.yaml) │ -├── robot-desktop (profile: desktop) x86-64 desktop / simulation -│ └── simple-robot (profile: simple) desktop + simple sim override -│ └── robot-test (profile: test) desktop + colcon test override +├── robot-desktop (profile: desktop) x86-64 desktop / simulation +│ ├── robot-desktop-onboard (profile: desktop_split) desktop, lite stack (simulated onboard computer) +│ ├── simple-robot (profile: simple) desktop + simple sim override +│ └── robot-test (profile: test) desktop + colcon test override │ -├── robot-voxl (profile: voxl) ModalAI VOXL platform -├── robot-l4t (profile: l4t) NVIDIA Jetson (Linux for Tegra) -└── zed-l4t (profile: l4t) ZED camera driver on Jetson +├── robot-offboard (profiles: desktop_split, offboard) offboard/global half on a ground host +├── robot-voxl-onboard (profiles: voxl, voxl_onboard) ModalAI VOXL platform (lite stack) +├── robot-l4t (profile: l4t) NVIDIA Jetson (Linux for Tegra) +├── robot-l4t-onboard (profile: l4t_lite) Jetson, lite stack (global offloaded) +└── zed-l4t (profile: l4t) ZED camera driver on Jetson ``` ## Base Service (`robot_base`) @@ -39,7 +42,7 @@ robot_base (robot-base-docker-compose.yaml) | **ROS workspace** | `common/ros_packages` is mounted into the ROS 2 workspace `src/common` | | **Shell config** | `.bashrc` and `inputrc` are bind-mounted so the developer experience is consistent across rebuilds | | **Bags** | `robot/bags/` is mounted at `/bags` for recording and playback | -| **Launch variables** | All `*_LAUNCH_PACKAGE` / `*_LAUNCH_FILE` environment variables are forwarded from the host `.env` file | +| **Launch variables** | `LAUNCH_PACKAGE` is set per service in the compose files; the launch file is `robot.launch.xml` (hardcoded in the compose command) | ## Platform Profiles @@ -52,7 +55,7 @@ Select a profile by passing `--profile ` to `docker compose` (or via the ` This is the DEFAULT profile as specified by `COMPOSE_PROFILES=desktop` in the root level `.env` file. It will run by default if no profile is passed or with `airstack up --profile desktop`. Use this profile when developing or running simulations on an x86-64 Linux workstation. - **Image:** `...:v_robot-x86-64_` -- **Base image:** `nvidia/cuda:13.0.2-base-ubuntu22.04` +- **Base image:** `nvidia/cuda:13.0.2-base-ubuntu24.04` - **Network:** isolated `airstack_network` bridge (prevents conflicts with other developers on the same LAN) - **SSH:** host ports `2223–2243` forwarded to port `22` in each container, one port per robot replica - **Scaling:** `NUM_ROBOTS` env var controls the number of replicas (default: 1) @@ -65,11 +68,11 @@ Runs with `airstack up --profile simple`. Extends `desktop` with `SIM_TYPE=simpl ### `voxl` — ModalAI VOXL -Runs with `airstack up --profile voxl`. Use this profile when deploying on a ModalAI VOXL flight computer. +Runs with `airstack up --profile voxl` (service `robot-voxl-onboard`). Use this profile when deploying on a ModalAI VOXL flight computer. - **Image:** `...:v_robot-voxl_` -- **Base image:** `ubuntu:22.04` (no CUDA; VOXL has its own compute stack) -- **Skipped components:** OpenVDB, MACVO, TensorRT +- **Base image:** `ubuntu:24.04` (no CUDA; VOXL has its own compute stack) +- **Skipped components:** OpenVDB (MAC-VO and TensorRT are not part of 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` @@ -78,7 +81,7 @@ Runs with `airstack up --profile voxl`. Use this profile when deploying on a Mod Runs with `airstack up --profile l4t`. Use this profile when deploying on an NVIDIA Jetson device running L4T. - **Image:** `...:v_robot-l4t_` -- **Base image:** `nvcr.io/nvidia/l4t-jetpack:r36.4.0` +- **Base image:** `dustynv/ros:jazzy-ros-base-r36.4.0-cu128-24.04` (via `Dockerfile.l4t-stack-base`) - **Network:** `host` - **IPC:** `host` (needed for shared-memory DDS transports on Jetson) - **Storage:** `/media/airlab/Storage/airstack_collection` mounted at `/bags` @@ -97,12 +100,12 @@ Key variables are set in the project's `.env` file and forwarded into the contai | Variable | Description | |---|---| | `VERSION` | Image version tag | -| `DOCKER_IMAGE_BUILD_MODE` | Build mode (e.g. `release`, `dev`) | +| `DOCKER_IMAGE_BUILD_MODE` | Image-tag discriminator only (`dev` today; a real `prebuilt` workspace-baked stage is future work) | | `PROJECT_DOCKER_REGISTRY` | Docker registry prefix | | `PROJECT_NAME` | Project / image name | | `NUM_ROBOTS` | Number of robot replicas (desktop only, default `1`) | | `AUTOLAUNCH` | Whether to auto-start the ROS 2 stack on container start (default `true`) | -| `ROBOT_LAUNCH_PACKAGE` / `ROBOT_LAUNCH_FILE` | Top-level ROS 2 launch target | +| `LAUNCH_PACKAGE` | Top-level ROS 2 launch package: `desktop_bringup` (adds RViz; desktop/sim) or `autonomy_bringup` (real robots / headless). The launch file is always `robot.launch.xml`, hardcoded in the compose command | | `OFFBOARD_BASE_PORT` / `ONBOARD_BASE_PORT` | MAVLink UDP port base values (desktop/sim only) | | `ROBOT_NAME_MAP_CONFIG_FILE` | YAML mapping config used to resolve a name to `ROBOT_NAME` and `ROS_DOMAIN_ID` (default: `default_robot_name_map.yaml`) | | `DEBUG_RVIZ` | If `true`, launches RViz alongside the robot via `desktop_bringup/robot.launch.xml` (default: `false`) | diff --git a/docs/robot/docker/robot_identity.md b/docs/robot/docker/robot_identity.md index dde74696f..d2efbd243 100644 --- a/docs/robot/docker/robot_identity.md +++ b/docs/robot/docker/robot_identity.md @@ -46,7 +46,7 @@ Each rule has a `pattern` (Python `re.fullmatch` regex), a `robot` template, and ```yaml mappings: - - pattern: '.*robot-.*(\ d+)' + - pattern: '.*robot-\D*(\d+)' robot: 'robot_{1}' domain_id: '{1}' @@ -63,7 +63,7 @@ To customize the mapping for your deployment, create a new YAML file in `robot/d Used by the **`desktop`** and **`simple`** profiles. -In simulation, Docker Compose names containers after the service, appending a replica number (e.g. `airstack-robot-desktop-1`, `airstack-robot-2`). The `.bashrc` resolves the container's hostname back to its Docker name: +In simulation, Docker Compose names containers after the service, appending a replica number (e.g. `airstack-robot-desktop-1`, `airstack-robot-desktop-2`). The `.bashrc` resolves the container's hostname back to its Docker name: ```bash name_to_map=$(host $(host $(hostname) | awk '{print $NF}') | awk '{print $NF}' | awk -F . '{print $1}') @@ -75,7 +75,7 @@ Because simulation robots get their identity from the container name (which is c ```bash NUM_ROBOTS=3 docker compose --profile desktop up -# → containers: airstack-robot-desktop-1, airstack-robot-2, airstack-robot-3 +# → containers: airstack-robot-desktop-1, airstack-robot-desktop-2, airstack-robot-desktop-3 # → ROBOT_NAME: robot_1, robot_2, robot_3 # → ROS_DOMAIN_ID: 1, 2, 3 ``` @@ -114,7 +114,7 @@ export FCU_URL="/dev/ttyTHS4:115200" `ROBOT_NAME=unknown_robot`, `ROS_DOMAIN_ID=0` with no error and a clean boot. The symptoms surface later — topics under `/unknown_robot`, per-robot config lookups keyed on `ROBOT_NAME` finding no profile, and containers pinned to another domain - (`zed-l4t` hardcodes `ROS_DOMAIN_ID=1`) no longer seeing the stack. + (`zed-l4t` hardcodes `ROS_DOMAIN_ID=1`) unable to see the stack. Make sure every physical robot has a hostname that matches a rule before deployment. Run the following to set the hostname on a device: diff --git a/docs/robot/index.md b/docs/robot/index.md index 2351d77e2..0030c64bf 100644 --- a/docs/robot/index.md +++ b/docs/robot/index.md @@ -21,28 +21,26 @@ robot/ ├── ros_ws/ # ROS 2 workspace │ └── src/ # Source packages (layered architecture) │ ├── autonomy_bringup/ # Top-level launch orchestration +│ ├── common/ # Shared packages & utilities │ ├── interface/ # Hardware interface & safety │ │ ├── interface_bringup/ │ │ ├── mavros_interface/ -│ │ └── ... +│ │ └── robot_interface/ │ ├── sensors/ # Sensor integration -│ │ ├── sensors_bringup/ -│ │ └── ... +│ │ └── lidar_point_cloud_filter/ │ ├── perception/ # State estimation & perception -│ │ ├── perception_bringup/ -│ │ └── ... +│ │ └── perception_bringup/ │ ├── local/ # Local planning, control, world models -│ │ ├── local_bringup/ │ │ ├── planners/ -│ │ ├── c_controls/ +│ │ ├── controls/ │ │ └── world_models/ │ ├── global/ # Global planning & mapping │ │ ├── global_bringup/ │ │ ├── planners/ │ │ └── world_models/ -│ └── behavior/ # High-level decision making -│ ├── behavior_bringup/ -│ └── ... +│ ├── behavior/ # High-level decision making +│ │ └── drone_safety_monitor/ +│ └── modules/ # Synced external modules └── bags/ # ROS 2 bag recordings ``` @@ -58,20 +56,24 @@ The robot autonomy stack is launched via Docker Compose. The configuration is in ### Launch Command Hierarchy -The Docker `command:` attribute launches the top-level ROS 2 launch file, which cascades through autonomy layers: +The Docker `command:` attribute launches the top-level ROS 2 launch file, +which runs a shared preamble and then the selected **stack** entry file — +the stack is the only dispatch mechanism (see +[Stacks](../development/stacks.md)): ``` -robot.launch.xml # Entry point (robot_bringup) - └── autonomy.launch.xml # Autonomy orchestration (autonomy_bringup) - ├── interface.launch.xml # Hardware interface - ├── sensors.launch.xml # Sensor drivers - ├── perception.launch.xml # State estimation - ├── local.launch.xml # Local planning & control - ├── global.launch.xml # Global planning & mapping - └── behavior.launch.xml # Mission execution +robot.launch.xml # Entry point (autonomy_bringup): + ├── (preamble: namespace, use_sim_time, robot_state_publisher, world→map TF) + └── stacks//launch/.launch.xml # The stack entry file — + ├── interface.launch.py # a flat list of module + ├── lidar_point_cloud_filter.launch.xml # includes; every connection + ├── stereo_image_proc.launch.xml # is written down here + ├── ... (local, global, behavior modules) + └── interpolate_dds_router / gossip ``` -Each `*_bringup` package contains launch files that orchestrate modules in that layer. +No stack selected = `stacks/full_default`. Each module package ships its own +canonical launch file; the stack entry file is the single wiring locus. ### Quick Reference @@ -80,10 +82,10 @@ Each `*_bringup` package contains launch files that orchestrate modules in that airstack up robot-desktop # Start without auto-launch (for development) -AUTOLAUNCH=false airstack up robot-desktop +airstack up robot-desktop --no-autolaunch # Multiple robots -NUM_ROBOTS=3 airstack up robot-desktop +airstack up robot-desktop --robots 3 # Different platforms (profiles) airstack up --profile l4t # NVIDIA Jetson @@ -98,19 +100,19 @@ airstack up --profile voxl # ModalAI VOXL ## Common Topics -Standard ROS 2 topics used across the autonomy stack: +The canonical topic/service/action names, message types, and QoS profiles for every interchange point live in the versioned [Interface Conventions Specification](autonomy/interface_conventions.md). A few examples (types per the spec): -| Topic | Type | Description | +| Topic (example) | Type | Description | |-------|------|-------------| -| `/$ROBOT_NAME/odometry` | [nav_msgs/Odometry](https://docs.ros.org/en/rolling/p/nav_msgs/interfaces/msg/Odometry.html) | Best estimate of robot state | -| `/$ROBOT_NAME/global_plan` | [nav_msgs/Path](https://docs.ros.org/en/rolling/p/nav_msgs/interfaces/msg/Path.html) | Target global trajectory | -| `/$ROBOT_NAME/trajectory_controller/trajectory_override` | airstack_msgs/TrajectoryOverride | Direct trajectory commands | -| `/$ROBOT_NAME/trajectory_controller/look_ahead` | geometry_msgs/PointStamped | Look-ahead point for planning | +| `/$ROBOT_NAME/odometry_conversion/odometry` | [nav_msgs/msg/Odometry](https://docs.ros.org/en/rolling/p/nav_msgs/interfaces/msg/Odometry.html) | Primary state estimate | +| `/$ROBOT_NAME/global_plan` | [nav_msgs/msg/Path](https://docs.ros.org/en/rolling/p/nav_msgs/interfaces/msg/Path.html) | Global waypoint path | +| `/$ROBOT_NAME/trajectory_controller/trajectory_override` | airstack_msgs/msg/TrajectoryXYZVYaw | Direct trajectory commands | **See also:** +- [Interface Conventions Specification](autonomy/interface_conventions.md) - The full topic/service/action reference - [System Architecture](autonomy/system_architecture.md) - Complete data flow diagrams -- [Integration Checklist](autonomy/integration_checklist.md) - Full topic reference +- [Integration Checklist](autonomy/integration_checklist.md) - Step-by-step module integration ## Next Steps diff --git a/docs/robot/logging/data_offloading.md b/docs/robot/logging/data_offloading.md index bb9f006e0..0187a037d 100644 --- a/docs/robot/logging/data_offloading.md +++ b/docs/robot/logging/data_offloading.md @@ -1,323 +1,23 @@ # Data Offloading -Automatic transfer of ROS bags, logs, and other data from robots to ground stations or storage servers. Critical for managing limited onboard storage and enabling post-mission analysis. +The supported workflow for offloading ROS bags and logs from robots is the +storage-tools pair — [storage_tools_server](https://github.com/castacks/storage_tools_server) +on the receiving machine and storage_tools_device on the robot. Setup and usage +are documented in the [Real World Data Offloading guide](../../real_world/data_offloading/index.md). -## Overview +## Quick Manual Copy -Data offloading in AirStack: +For a one-off manual transfer, plain rsync works. Bags land in `robot/bags/` +on the host (mounted at `/bags` in the robot container); on Jetson (`l4t` +profile) they land in `${BAG_STORAGE_PATH}` (default +`/media/airlab/Storage/airstack_collection`): -- **Automatic synchronization** when robot connects to network -- **Bandwidth-aware transfers** to avoid interfering with operations -- **Compression** to reduce transfer time -- **Verification** to ensure data integrity -- **Storage management** to free onboard space after successful transfer - -## Architecture - -```mermaid -graph LR - A[Robot Onboard Storage] -->|WiFi/Cellular| B[Ground Station] - A -->|SSH/rsync| C[Storage Server] - B --> D[Archive Storage] - C --> D -``` - -Data flows from robot to either: - -1. **Ground Control Station** during or after mission -2. **Storage Server** for long-term archival -3. **Cloud Storage** for team-wide access - -## Quick Start - -### Basic Offload - -Manual offload via rsync: -```bash -# From robot to ground station -rsync -avz --progress /opt/airstack/bags/ user@groundstation:/data/robot1/ -``` - -### Automatic Offload - -Configure automatic offloading by setting up: - -1. **SSH key authentication** (no password required) -2. **Offload script** that runs on network connection -3. **Cron job** or systemd timer for periodic sync - -## Configuration - -### Setting Up SSH Keys - -On robot: -```bash -ssh-keygen -t ed25519 -f ~/.ssh/id_offload -ssh-copy-id -i ~/.ssh/id_offload.pub user@groundstation -``` - -### Offload Script - -Create `/opt/airstack/scripts/offload_data.sh`: - -```bash -#!/bin/bash -# Offload data from robot to ground station - -ROBOT_NAME=${ROBOT_NAME:-"robot1"} -GROUND_STATION="user@groundstation" -REMOTE_DIR="/data/${ROBOT_NAME}" -LOCAL_BAGS="/opt/airstack/bags" -LOCAL_LOGS="/opt/airstack/logs" - -# Check if ground station is reachable -if ! ping -c 1 -W 5 groundstation > /dev/null 2>&1; then - echo "Ground station not reachable, skipping offload" - exit 0 -fi - -# Sync bags -echo "Syncing bags..." -rsync -avz --progress --remove-source-files \ - ${LOCAL_BAGS}/ \ - ${GROUND_STATION}:${REMOTE_DIR}/bags/ - -# Sync logs -echo "Syncing logs..." -rsync -avz --progress \ - ${LOCAL_LOGS}/ \ - ${GROUND_STATION}:${REMOTE_DIR}/logs/ - -echo "Offload complete" -``` - -Make executable: -```bash -chmod +x /opt/airstack/scripts/offload_data.sh -``` - -### Automatic Scheduling - -**Option 1: Cron (periodic)** -```bash -# Run every hour -0 * * * * /opt/airstack/scripts/offload_data.sh >> /var/log/offload.log 2>&1 -``` - -**Option 2: Systemd (on network up)** - -Create `/etc/systemd/system/airstack-offload.service`: -```ini -[Unit] -Description=AirStack Data Offload -After=network-online.target -Wants=network-online.target - -[Service] -Type=oneshot -ExecStart=/opt/airstack/scripts/offload_data.sh -User=airstack -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target -``` - -Enable: -```bash -sudo systemctl enable airstack-offload.service -sudo systemctl start airstack-offload.service -``` - -## Storage Management - -### Monitoring Disk Space - -Check available space: -```bash -df -h /opt/airstack -``` - -Monitor during mission: -```bash -watch -n 10 "df -h /opt/airstack | tail -1" -``` - -### Automatic Cleanup - -After successful offload, free space: - -```bash -# Remove successfully transferred bags (already done if using --remove-source-files) -# Or delete bags older than 7 days after verification -find /opt/airstack/bags -name "*.db3" -mtime +7 -delete -``` - -### Storage Quotas - -On Jetson/VOXL with limited storage: - -- **Reserve 10GB minimum** free space for system -- **Set bag size limits** in recording configuration -- **Prioritize critical topics** over full recording -- **Enable automatic offload** to prevent filling disk - -## Bandwidth Optimization - -### Compression - -Compress before transfer: -```bash -# Compress bags -cd /opt/airstack/bags -tar -czf bags_$(date +%Y%m%d_%H%M%S).tar.gz *.db3 - -# Transfer compressed archive -rsync -avz --progress bags_*.tar.gz user@groundstation:/data/robot1/ -``` - -### Transfer Scheduling - -Avoid transferring during active operations: - -- **Pre-flight**: Offload before mission -- **Post-flight**: Offload after mission completes -- **Off-hours**: Schedule large transfers overnight -- **Bandwidth limiting**: Use `rsync --bwlimit=1000` (KB/s) - -### Delta Sync - -Only transfer new/changed files: -```bash -rsync -avz --update --progress /opt/airstack/bags/ user@groundstation:/data/robot1/bags/ -``` - -## Security Considerations - -- **Use SSH keys** instead of passwords -- **Restrict key permissions**: `chmod 600 ~/.ssh/id_offload` -- **Limit SSH key scope** using `command=` in authorized_keys -- **Use VPN** for remote offloading over internet -- **Encrypt sensitive data** before transfer - -## Multi-Robot Scenarios - -For multiple robots offloading to same ground station: - -### Unique Robot Directories - -```bash -ROBOT_NAME="robot1" -REMOTE_DIR="/data/${ROBOT_NAME}" -rsync -avz /opt/airstack/bags/ user@groundstation:${REMOTE_DIR}/bags/ -``` - -### Coordinated Transfers - -Prevent bandwidth saturation: - -```bash -# Robot 1: offload immediately after landing -# Robot 2: offload 10 minutes after Robot 1 -# Robot 3: offload 10 minutes after Robot 2 -``` - -Use file locks to serialize: -```bash -flock /var/lock/offload.lock /opt/airstack/scripts/offload_data.sh -``` - -## Ground Station Setup - -### Receiving Data - -On ground station, create directory structure: -```bash -sudo mkdir -p /data/{robot1,robot2,robot3}/{bags,logs} -sudo chown -R user:user /data -``` - -### Archive Management - -Organize by date and mission: -```bash -/data/ -├── robot1/ -│ ├── bags/ -│ │ ├── 2024-03-17_mission1/ -│ │ ├── 2024-03-18_mission2/ -│ │ └── ... -│ └── logs/ -└── robot2/ - └── ... -``` - -Automated archival script: -```bash -#!/bin/bash -# Archive and compress old mission data - -SOURCE="/data/robot1/bags" -ARCHIVE="/archive/robot1" -DAYS_OLD=30 - -find ${SOURCE} -name "*.db3" -mtime +${DAYS_OLD} -exec tar -czf {}.tar.gz {} \; -delete -mv ${SOURCE}/*.tar.gz ${ARCHIVE}/ -``` - -## Troubleshooting - -**Transfer fails with SSH error**: - -- Verify SSH keys are set up correctly -- Test manual SSH connection: `ssh user@groundstation` -- Check network connectivity - -**Transfer is too slow**: - -- Use compression: `tar -czf` before transfer -- Check network bandwidth and latency -- Use `--bwlimit` to avoid saturating connection -- Transfer during off-peak hours - -**Disk full on robot**: - -- Manually offload immediately -- Delete old/unnecessary bags -- Reduce recording topic list -- Increase offload frequency - -**Data corruption during transfer**: - -- Use rsync's built-in checksums -- Verify file sizes after transfer -- Use `--checksum` flag for rsync -- Implement post-transfer validation script - -## Monitoring and Alerts - -### Check Offload Status - -View offload logs: -```bash -journalctl -u airstack-offload.service -f -``` - -### Disk Space Alerts - -Alert when disk is >80% full: ```bash -#!/bin/bash -USAGE=$(df /opt/airstack | tail -1 | awk '{print $5}' | sed 's/%//') -if [ $USAGE -gt 80 ]; then - echo "WARNING: Disk usage at ${USAGE}% on $(hostname)" | mail -s "Disk Alert" ops@example.com -fi +rsync -avz --progress /media/airlab/Storage/airstack_collection/ user@groundstation:/data/robot_1/bags/ ``` ## See Also +- [Real World Data Offloading](../../real_world/data_offloading/index.md) - The supported storage-tools workflow +- [Logging Overview](index.md) - Where bags land and how recording is configured - [ROS Bags](rosbags.md) - Recording data -- [Logging Overview](index.md) - AirStack logging infrastructure -- [Real World Data Offloading](../../real_world/data_offloading/index.md) - Field-specific offload procedures -- [Robot Configuration](../configuration/index.md) - Configuring robot identity and network \ No newline at end of file diff --git a/docs/robot/logging/index.md b/docs/robot/logging/index.md index e2b378b8c..9443c26ec 100644 --- a/docs/robot/logging/index.md +++ b/docs/robot/logging/index.md @@ -2,14 +2,29 @@ ## Bag Recording -AirStack provides automated bag recording capabilities for capturing ROS2 topic data during flights. The main node for bag recording is located at [common/ros_packages/bag_recorder_pid](../../common/ros_packages/bag_recorder_pid). For detailed configuration options and implementation details, please consult the README in that directory. +AirStack provides automated bag recording capabilities for capturing ROS 2 topic data during flights. The main node for bag recording is located at [common/ros_packages/logging/bag_recorder_pid](../../../common/ros_packages/logging/bag_recorder_pid/README.md). For detailed configuration options and implementation details, please consult the README in that directory. ### Enabling Bag Recording -To enable bag recording, prepend `RECORD_BAGS=true` to the airstack up command: +To start the recorder node, prepend `RECORD_BAGS=true` to the airstack up command: ```bash RECORD_BAGS=true airstack up robot-desktop ``` -The BehaviorTree will automatically trigger topic recording once the drone takes off. Recorded bags will appear in the `./robot/bags` directory. \ No newline at end of file +The stack entry file includes `logging_bringup/launch/logging.launch.xml`, which starts the `bag_record` node only when `RECORD_BAGS=true`. The topic set to record is selected with `LOG_CONFIG` (a filename in `logging_bringup/config/`, default `log.yaml`). + +The recorder starts **idle**. Toggle recording at runtime by publishing to its control topic (bridged to the GCS by the DDS router): + +```bash +# Start recording +ros2 topic pub --once /$ROBOT_NAME/bag_record/set_recording_status std_msgs/msg/Bool "{data: true}" + +# Stop recording +ros2 topic pub --once /$ROBOT_NAME/bag_record/set_recording_status std_msgs/msg/Bool "{data: false}" + +# Watch recording status (published at 2 Hz) +ros2 topic echo /$ROBOT_NAME/bag_record/bag_recording_status +``` + +Recorded bags (MCAP format) are written to `/bags` inside the container, which is the mounted `./robot/bags` directory on the host. diff --git a/docs/robot/logging/rosbags.md b/docs/robot/logging/rosbags.md index e3dae568e..2877d4f11 100644 --- a/docs/robot/logging/rosbags.md +++ b/docs/robot/logging/rosbags.md @@ -2,205 +2,85 @@ ROS bags are the primary method for recording data during robot operation. They capture ROS 2 topics for later analysis, debugging, and algorithm development. -## Overview +## Managed Recording -ROS bag recording in AirStack: - -- **Automatic recording** via bag_recorder_pid package -- **Selective topic recording** to manage storage -- **Integration with logging infrastructure** -- **Support for onboard and offboard recording** - -## Quick Start - -### Manual Recording - -Record specific topics: -```bash -ros2 bag record /robot1/odometry /robot1/camera/image_raw -``` - -Record all topics: -```bash -ros2 bag record -a -``` - -Record with storage limit: -```bash -ros2 bag record -a --max-bag-size 1000 # 1GB per file -``` - -### Automatic Recording - -AirStack can automatically record bags using the [bag_recorder_pid](../../../common/ros_packages/bag_recorder_pid/README.md) package. - -Configure in the robot launch files to automatically start recording when the autonomy stack launches. +AirStack manages recording with the [bag_recorder_pid](../../../common/ros_packages/logging/bag_recorder_pid/README.md) package: launch the stack with `RECORD_BAGS=true` to start the recorder node, then toggle recording via its `bag_record/set_recording_status` topic (`std_msgs/Bool`, in the robot namespace). See the [Logging overview](index.md) for the full workflow. ## Configuration ### Topic Selection -Choose topics based on mission objectives: +Choose topics based on mission objectives. The recorder's config file (selected with `LOG_CONFIG`, in `logging_bringup/config/`, default `log.yaml`) groups topics into named **sections**; relative topic names are prefixed with the robot namespace: **Minimal set** (state and commands): ```yaml -topics: - - /{robot_name}/odometry - - /{robot_name}/global_plan - - /{robot_name}/trajectory_controller/trajectory_segment_to_add -``` - -**Standard set** (add sensor data): -```yaml -topics: - - /{robot_name}/odometry - - /{robot_name}/global_plan - - /{robot_name}/camera/image_raw/compressed - - /{robot_name}/depth/image_raw - - /{robot_name}/imu/data +sections: + state: + mcap_qos: mcap_qos.yaml + args: [] + topics: + - odometry_conversion/odometry + - global_plan + - trajectory_controller/trajectory_segment_to_add ``` -**Full set** (everything for debugging): +**Full set** (everything for debugging — record all topics except an exclude list): ```yaml -topics: - - ".*" # Record all topics +sections: + everything: + mcap_qos: mcap_qos.yaml + args: [] + exclude: + - /tf + - /tf_static ``` -### Storage Management - -On resource-constrained platforms (Jetson, VOXL): - -- **Limit bag size**: Use `--max-bag-size` to split large bags -- **Selective recording**: Only record topics needed for mission -- **Compression**: Use compressed image topics when available -- **Automatic offload**: Configure [data offloading](data_offloading.md) to free space +Section `args` are passed through to `ros2 bag record`, so storage limits like +`-b 4000000000` (split at ~4 GB) or `--max-cache-size` go there — see the +shipped `log.yaml` for a working example. ## Storage Locations ### Development (Docker) -- Bags stored in mounted volume: `robot/bags/` +- Bags stored in mounted volume: `robot/bags/` (mounted at `/bags` in the container) - Persists across container restarts ### Hardware Deployment -- Default location: `/opt/airstack/bags/` or local SSD -- Configure via environment variable: `ROSBAG_DIR` +- Jetson (`l4t` profile): `${BAG_STORAGE_PATH}` on the device (default `/media/airlab/Storage/airstack_collection`) is mounted at `/bags` +- The recorder's target directory is its `output_dir` parameter (set in `logging.launch.xml`, default `/bags`) -## Playback and Analysis +## Manual Recording and Playback -### Basic Playback +For one-off captures the standard CLI works as usual inside the robot container — see the [ROS 2 bag documentation](https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Recording-And-Playing-Back-Data/Recording-And-Playing-Back-Data.html) for the full reference: -Play back a recorded bag: ```bash +ros2 bag record /robot_1/odometry_conversion/odometry /robot_1/global_plan ros2 bag play path/to/bagfile -``` - -Play at different speed: -```bash -ros2 bag play path/to/bagfile --rate 0.5 # Half speed -``` - -Play in loop: -```bash -ros2 bag play path/to/bagfile --loop -``` - -### Bag Information - -Get bag metadata: -```bash ros2 bag info path/to/bagfile ``` -Example output: -``` -Files: rosbag2_2024_03_17-14_30_00.db3 -Bag size: 1.2 GB -Storage id: sqlite3 -Duration: 300.5s -Start: Mar 17 2024 14:30:00.123 -End: Mar 17 2024 14:35:00.623 -Messages: 45123 -Topic information: - Topic: /robot1/odometry | Type: nav_msgs/msg/Odometry | Count: 3005 | Serialization Format: cdr - Topic: /robot1/camera/image_raw/compressed | Type: sensor_msgs/msg/CompressedImage | Count: 1500 | Serialization Format: cdr - ... -``` - -### Extract Specific Topics +Topics are namespaced by robot name (`/robot_1/...` with the default robot +name map). -Convert to a new bag with only specific topics: +To extract specific topics into a new bag, use `ros2 bag convert` with an output spec: ```bash -ros2 bag filter input_bag -o output_bag --topics /robot1/odometry /robot1/camera/image_raw +ros2 bag convert -i input_bag -o out_spec.yaml ``` -## Common Workflows - -### Debug Mission Issues - -1. Record full topic set during mission -2. Play back locally in simulation -3. Analyze behavior with rviz or custom tools -4. Iterate on algorithms offline - -### Algorithm Development - -1. Record sensor data in real environment -2. Play back during development -3. Test new algorithms against real data -4. Validate before hardware deployment - -### Performance Analysis - -1. Record timestamped topics -2. Analyze latencies and frequencies -3. Identify bottlenecks -4. Optimize performance - -## Best Practices - -- **Test recording setup** before important missions -- **Monitor disk space** during operation -- **Use compression** for image topics -- **Document bag contents** with descriptive names -- **Archive important bags** with mission metadata -- **Regular cleanup** of old/unnecessary bags - -## Troubleshooting - -**Bag recording fails to start**: - -- Check disk space availability -- Verify write permissions to bag directory -- Check if bag_recorder_pid is running - -**Bags too large**: - -- Use topic filtering to record only necessary data -- Enable compression for image topics -- Use `--max-bag-size` to split files -- Consider reducing sensor publishing rates - -**Playback issues**: - -- Ensure ROS 2 version matches recording system -- Check topic names and types match expectations -- Verify clock synchronization settings - -**Missing data in bags**: - -- Verify topics were being published during recording -- Check bag info to confirm topics recorded -- Ensure recording started before mission began +```yaml +# out_spec.yaml +output_bags: + - uri: output_bag + topics: [/robot_1/odometry_conversion/odometry, /robot_1/global_plan] +``` ## Integration with Data Offloading -For automatic transfer of bags from robot to ground station or storage server: - -See: [Data Offloading Guide](data_offloading.md) +For transferring bags from robot to a storage server, see the [Data Offloading Guide](data_offloading.md). ## See Also -- [bag_recorder_pid Package](../../../common/ros_packages/bag_recorder_pid/README.md) - Automatic recording package +- [bag_recorder_pid Package](../../../common/ros_packages/logging/bag_recorder_pid/README.md) - Automatic recording package - [Data Offloading](data_offloading.md) - Transfer bags from robot - [Logging Overview](index.md) - AirStack logging infrastructure -- [ROS 2 Bag Documentation](https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Recording-And-Playing-Back-Data/Recording-And-Playing-Back-Data.html) \ No newline at end of file +- [ROS 2 Bag Documentation](https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Recording-And-Playing-Back-Data/Recording-And-Playing-Back-Data.html) diff --git a/docs/robot/optitrack.md b/docs/robot/optitrack.md new file mode 100644 index 000000000..3c4d52c9b --- /dev/null +++ b/docs/robot/optitrack.md @@ -0,0 +1,9 @@ +# OptiTrack (asm_optitrack module) + +OptiTrack support — the `natnet_ros2` client, the PX4 external-vision fusion bridges, and the NatNet server emulator for Isaac Sim — ships as the standalone [asm_optitrack module](https://github.com/castacks/asm_optitrack) rather than in the AirStack trunk. To use it, add the module to your checkout: + +```bash +airstack module add https://github.com/castacks/asm_optitrack --version +``` + +See [AirStack Modules](../development/modules.md) for how modules are declared, synced, and overlaid, and the module's own README for setup (including the host-side NatNet SDK download, which runs automatically as the module's `host_setup` hook). diff --git a/docs/robot/px4_external_vision.md b/docs/robot/px4_external_vision.md deleted file mode 100644 index 4c7be444b..000000000 --- a/docs/robot/px4_external_vision.md +++ /dev/null @@ -1,216 +0,0 @@ -# PX4 External-Vision (OptiTrack) Setup - -Runbook for flying a PX4 vehicle (Cube Orange) on **OptiTrack mocap as the sole -position source** — no GNSS, no magnetometer — with an onboard companion -computer (Jetson) running the AirStack robot stack. - -It covers three things that must all be right: - -1. **EKF2 parameters** — tell PX4 to fuse external vision instead of GPS/baro/mag. -2. **Companion MAVLink link** — how the Jetson talks to the Cube (see the PX4 docs). -3. **Vision pose pipeline** — how a mocap pose becomes a `VISION_POSITION_ESTIMATE`, - and how PX4 gets a global position without GNSS. - -> Scope: PX4 ≥ 1.14 (the `EKF2_EV_CTRL` / `EKF2_GPS_CTRL` era). For older -> firmware use `EKF2_AID_MASK: 24` and `EKF2_HGT_MODE: 3` instead of the bitmask -> params below. - ---- - -## 1. EKF2 parameters (external vision) - -These are enforced automatically at startup by the `px4_param_setter` node (see -below), sourced from -[`robot/ros_ws/src/perception/natnet_ros2/config/px4_params.yaml`](../../robot/ros_ws/src/perception/natnet_ros2/config/px4_params.yaml). -You can also set them by hand in QGroundControl — PX4 persists parameters, so -either way it's a one-time thing per airframe. - -| Parameter | Value | Meaning | -|---|---|---| -| `EKF2_EV_CTRL` | `11` | Fuse vision **horizontal pos (1) + vertical pos (2) + yaw (8)**. Add bit **4** (velocity) only if a vision *speed* source is also streamed. | -| `EKF2_HGT_REF` | `3` | Vision is the primary height reference (not baro / GPS). | -| `EKF2_GPS_CTRL` | `0` | No GPS fusion. | -| `EKF2_MAG_TYPE` | `5` | Magnetometer disabled — yaw comes from vision. | -| `EKF2_BARO_CTRL` | `0` | No baro fusion; height is pure vision. Set to `1` to keep baro as a backup height source. | -| `EKF2_EV_DELAY` | `7.0` | OptiTrack→EKF2 latency (ms): ~0.7 ms measured LAN transport + a ~5 ms *estimated* FCU hop. **Raising this does not compensate for apparent lag — it makes the estimate run ahead of truth.** | -| `EKF2_EV_NOISE_MD` | `1` | Use the `EKF2_EV*_NOISE` floors below instead of the message covariance (which is `1e-6` — too optimistic to fuse safely). | -| `EKF2_EVP_NOISE` | `0.05` | Vision **position** noise floor (m). Not marker precision — it also sets the innovation gate, `EKF2_EVP_GATE` (default 5) sigma wide, so this is a 25 cm gate. | -| `EKF2_EVA_NOISE` | `0.05` | Vision **angle** noise floor (rad). | -| `COM_ARM_WO_GPS` | `1` | Allow arming without GPS. | - -**Type matters.** Integers are written bare (`11`); floats need a decimal point -(`7.0`) so the MAVLink param type matches the FCU's declaration. Getting this -wrong makes the set silently reject. - -### Troubleshooting tips - -If you see drift-and-snap, **check the Motive PC rigid-body definition first** and ensure the x axis points forward. Then, make sure that Motive is streaming the position with z-axis up. - -### The latency figure is only partly measured - -`EKF2_EV_DELAY` is currently `7.0` ms: roughly `0.7` measured plus a `5.0` estimate -(`cube_orange_latency_ms` in `natnet_config.yaml`). Only the first part is empirically measured currently. - -- **Measured:** `natnet_ros2_node` derives transport latency from the NatNet - `TransmitTimestamp` — i.e. from *server transmit* to client receipt. It does not - include Motive's own capture→transmit pipeline (exposure, centroiding, solving), - which is typically several ms and happens before that clock starts. -- **Estimated:** `cube_orange_latency_ms` models the MAVROS → MAVLink → uORB → EKF2 hop. - It is **estimated only**. - -**Reboot after any change.** Fusion-source (`EKF2_*`) params are safest applied -from a clean estimator start — reboot the flight controller before flying. The -param setter prints a warning whenever it actually changes something. - ---- - -## 2. The param checker (`px4_param_setter`) - -Set the table above **once in QGroundControl**. To catch a mis-configured FCU -before flight, the stack runs a one-shot node at startup that **checks** the live -params against the desired set. **By default it only checks and flags — it does not -write to the FCU.** - -- **Node:** [`px4_param_setter_node.py`](../../robot/ros_ws/src/perception/natnet_ros2/src/px4_param_setter_node.py) -- **Config:** [`config/px4_params.yaml`](../../robot/ros_ws/src/perception/natnet_ros2/config/px4_params.yaml) - (everything under `params.` is a desired FCU parameter) -- **Launch:** [`launch/px4_param_setter.launch.xml`](../../robot/ros_ws/src/perception/natnet_ros2/launch/px4_param_setter.launch.xml), - included from `natnet_ros2.launch.py` when the robot's `vision_pose` block is enabled. - -Two safety flags in `px4_params.yaml`: - -| Flag | Default | Behaviour | -|------|---------|-----------| -| `auto_set` | `false` | `false`: read + compare only, never write. `true`: also push mismatched params via `param/set` and verify (the legacy enforce path). | -| `on_mismatch` | `warn` | With `auto_set: false`, on a wrong param — `warn`: log the diffs, keep the stack up. `halt`: log fatal + exit non-zero so a `required` launch node tears the stack down before flight. | - -Per parameter it waits for an FCU connection + `settle_sec` (default 10 s), reads -the current value, and compares (float32 tolerance). A clean run logs -`10 already correct, 0 mismatched`. A mismatch under the default (`auto_set: false`, -`on_mismatch: warn`) logs, e.g., `EKF2_HGT_REF: FCU has 1, expected 3 (not set — -auto_set=false). Fix in QGroundControl.` - -Disable it entirely with `enabled: false`. - -> **The checker does NOT configure the companion link** (`MAV_*` / `SER_*` -> params in section 3) — those are set once in QGC. - ---- - -## 3. Companion MAVLink link (Jetson ↔ Cube) - -`mavros` reaches the FCU over the serial link named by `FCU_URL` in the deployment env. -Configuring that link is standard PX4 setup, not AirStack-specific — see the PX4 docs: - -- [Companion computer setup](https://docs.px4.io/main/en/companion_computer/) -- [MAVLink peripherals (`MAV_n_CONFIG`, `MAV_n_MODE`)](https://docs.px4.io/main/en/peripherals/mavlink_peripherals.html) -- [Serial port configuration](https://docs.px4.io/main/en/peripherals/serial_configuration.html) - -Use the **TELEM2 UART** for the companion link rather than USB. On Cube Orange the USB -CDC-ACM path intermittently stalls outbound transfers for 10–30 s at a time — visible as -`DROPPED Message-Id 102 … TX queue overflow` — which starves EKF2 of vision updates and -makes it dead-reckon between bursts. It is not a bandwidth problem and rate-limiting the -vision stream does not help. - -> In compose list-syntax `environment:`, values are literal — write `FCU_URL=/dev/ttyTHS1:115200` -> bare. Quoting it passes the quotes through and breaks MAVROS URL parsing. - - -## 4. Vision pose pipeline (mocap → PX4) - -``` -Motive (OptiTrack, 100 Hz) - → natnet_ros2_node publishes the rigid body as a ROS pose (ENU) - → vision_pose_converter rate-limit + quaternion canonicalize (passthrough) - → mavros vision_pose converts ENU→NED, sends VISION_POSITION_ESTIMATE (msg 102) - → PX4 EKF2 fuses per the params in section 1 -``` - -**Frame convention — the thing to get right.** MAVROS's `vision_pose` plugin -expects **ROS ENU** and converts to PX4 NED internally. The -[`vision_pose_converter_node.py`](../../robot/ros_ws/src/perception/natnet_ros2/src/vision_pose_converter_node.py) -does **no coordinate transform** — it only rewrites `frame_id`, optionally -canonicalizes the quaternion sign (`qw ≥ 0`), and rate-limits. **So -`natnet_ros2_node` must already publish ENU.** If position/yaw come out rotated -or axis-swapped, fix it there, not in the converter. - -**Rate limiting.** `max_rate_hz` (default 50 in -[`vision_pose_converter.yaml`](../../robot/ros_ws/src/perception/natnet_ros2/config/vision_pose_converter.yaml)) -caps the stream to MAVROS. EKF2 only needs 30–50 Hz. Note this is about not -saturating a healthy serial link — it does **not** fix the USB CDC stall in -section 3. - -### Injecting a global position — `mavros_gp_origin` - -Vision gives PX4 a valid *local* position, but with GNSS disabled it has no *global* -one, and modes that require a global position (e.g. `AUTO.LOITER`) refuse to arm. - -[`mavros_gp_origin_node.py`](../../robot/ros_ws/src/perception/natnet_ros2/src/mavros_gp_origin_node.py) -publishes a **synthetic GPS origin** once at startup, which lets PX4 derive a global -position from the fused vision estimate. It waits for MAVROS to connect, listens for an -existing origin, and only publishes if none is present — so a GNSS-equipped vehicle is -left untouched. Location and behaviour come from -[`mavros_gp_origin.yaml`](../../robot/ros_ws/src/perception/natnet_ros2/config/mavros_gp_origin.yaml); -the defaults match the AirStack shared world datum so sim, the GCS, and the robot agree -on where world origin sits on Earth. - -!!! note "Real deployments: the origin altitude needs a geoid correction" - - `geographic_msgs/GeoPoint.altitude` is a height above the **WGS-84 ellipsoid**, and - MAVROS converts it to AMSL with the **egm96-5 geoid** before handing it to PX4. Send - the shared datum's literal `90.0` and PX4 anchors its vertical frame at - `AMSL = 90 − N ≈ 36 m`, while OptiTrack says the floor is `z = 0` — the drone reads - ~36 m of altitude sitting on the floor. The gap is exactly the geoid undulation `N`. - - With `use_geoid_altitude: true` the node publishes `N + desired_floor_amsl` instead, - computing `N` at runtime via `GeoidEval` with the same egm96-5 model MAVROS uses, so - the conversion cancels out. - - `desired_floor_amsl` chooses what AMSL the mocap floor reports; `local_position.z` - equals the OptiTrack height either way. We use **36.0**, the shared datum in AMSL, so - the robot's global altitude agrees with sim and the GCS. - - **Not needed in sim.** The geoid path is skipped when `use_sim_time: true`: sim's - synthetic GPS is self-consistent with the spawn and uses the literal datum altitude - on both ends, so there is no ellipsoidal-vs-AMSL mismatch to correct. - - -## 5. Verify it's actually fusing - -**Live, in the QGC MAVLink _Console_** (not the Inspector — it can't see -companion→FCU messages): - -``` -listener vehicle_visual_odometry # should be steady ~50 Hz, not gappy -listener estimator_status -``` - -On the ROS side: `/{ROBOT_NAME}/interface/mavros/local_position/pose` should -publish and track the mocap. Hand-lift test: raise the vehicle, Z should go up -(Motive Z-up correct); translate it and check the sign/axis match. - -**Definitive, from the SD-card ulog** ([Flight Review](https://logs.px4.io) or -PlotJuggler): - -- `estimator_innovations` → **`ev_hpos` / `ev_vpos` / `ev_yaw`** and their - **test ratios**. Ratio > 1 ⇒ EKF2 is *rejecting* the measurement - (frame / timing / covariance). Near-zero with occasional gaps ⇒ fusing fine - but starved by dropped messages (section 3). -- `estimator_status_flags` → **`cs_ev_pos` / `cs_ev_yaw`** — confirms EV fusion - is actually active. If unset, EKF2 isn't fusing vision regardless of params. -- `vehicle_visual_odometry` rate in the log quantifies how many `102`s actually - arrived. - ---- - -## Troubleshooting quick reference - -| Symptom | Likely cause | Where to look | -|---|---|---| -| `DROPPED Message-Id 102 … TX queue overflow` | Cube USB CDC OUT stall | Section 3 → move to TELEM2 | -| mavros local pos drifts away from mocap over time | Dropped `102`s starving EKF2 | Fix link first, then recheck | -| Constant rotation between mocap and EKF2 pose | Yaw/frame misalignment | `natnet_ros2_node` frame (must be ENU); `EKF2_EV_CTRL` yaw bit | -| Axes swapped / uncorrelated | Wrong frame convention | `natnet_ros2_node`, not the converter | -| Param set "rejected or readback mismatch" | Wrong literal type (int vs float) | Section 1 — floats need a decimal point | -| Won't arm | GPS still required | `COM_ARM_WO_GPS: 1`, reboot | -| EV innovation test ratio > 1 | EKF2 rejecting vision | Retune `EKF2_EV_DELAY`, `EKF2_EVP_NOISE` / `EKF2_EVA_NOISE` | diff --git a/docs/robot/static_transforms/index.md b/docs/robot/static_transforms/index.md index 4040b7336..33bb3a95a 100644 --- a/docs/robot/static_transforms/index.md +++ b/docs/robot/static_transforms/index.md @@ -1,7 +1,22 @@ +# Static Transforms -## Frame Conventions +Static transforms pin down the fixed geometric relationships between frames — where sensors sit on the body, and how each robot's `map` frame relates to the `world` frame — so that every module can transform data into a common frame without per-module configuration. On the robot they come from two places, both set up by the launch preamble in [`autonomy_bringup/launch/robot.launch.xml`](https://github.com/castacks/AirStack/blob/develop/robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml), which runs for every stack. -Each robot has its own **map** frame that represents the starting position of the robot. -The **map** frame is expected to be in ENU (East-North-Up) convention. +## The Frame Tree -The robot is in the **base_link** frame. +- **`world`** — fixed origin +- **`map`** — child of `world` (static identity); ENU, meters — the planning/state frame +- **`base_link`** — the body frame, positioned by the state estimate; sensor frames hang off it via the URDF + +## `world` → `map` + +The preamble publishes a **static identity transform** `world → map` via `tf2_ros static_transform_publisher` (node `world_to_map_broadcaster`). `world` is the fixed origin; `map` is the ENU planning/state frame in which odometry pose, the global plan, and the map live. The dynamic `map → base_link` relationship comes from the state estimate, not a static transform. + +## Sensor Extrinsics (URDF) + +Sensor and body-frame extrinsics are published by `robot_state_publisher` from the robot URDF: the preamble includes `robot_descriptions/launch/robot_state_publisher.launch.py` with the `urdf_file` launch argument, which defaults to the `URDF_FILE` environment variable. `URDF_FILE` is set in the top-level `.env` (default: `robot_descriptions/iris/urdf/iris_with_sensors.pegasus.robot.urdf`) and passed into the robot container by `robot/docker/robot-base-docker-compose.yaml`; fleet files can override it per robot via their vehicle definition. + +## See Also + +- [Interface Conventions — TF frames and units](../autonomy/interface_conventions.md#tf-frames-and-units) — the canonical frame table (`world`, `map`, `base_link`) and unit conventions +- [Frame Conventions](../../development/intermediate/frame_conventions.md) — the concept page explaining the frame tree diff --git a/docs/simulation/index.md b/docs/simulation/index.md index 994d74c08..3378258e5 100644 --- a/docs/simulation/index.md +++ b/docs/simulation/index.md @@ -1,6 +1,8 @@ # Simulation -AirStack provides high-fidelity simulation environments for developing and testing autonomous systems before deploying to hardware. Simulation enables rapid iteration, safe testing of edge cases, and multi-robot scenarios. +AirStack provides simulation environments for developing and testing autonomous systems before deploying to hardware. Simulation enables rapid iteration, safe testing of edge cases, and multi-robot scenarios. + +Three simulators are supported because no single one covers every development need: **Isaac Sim** (primary) for high-fidelity rendering, physics, and full sensor suites; **Microsoft AirSim (legacy)** for native PX4-in-the-loop testing with pre-built Unreal Engine scenes and no Omniverse dependency; and **Simple Sim** for fast, lightweight iteration on planning and perception code without PX4 or a heavyweight GPU workload. Pick the lightest simulator that exercises what you're working on. ## Directory Structure @@ -12,11 +14,10 @@ simulation/ │ ├── docker/ # Isaac Sim containerization │ │ ├── docker-compose.yaml # Main launch configuration │ │ └── Dockerfile.isaac-ros # Image definition -│ ├── assets/ # 3D models and props -│ ├── config/ # Simulation configurations +│ ├── assets/ # Scenes, 3D models and props │ ├── extensions/ # Custom Isaac Sim extensions │ ├── launch_scripts/ # Python launch scripts -│ └── standalone_examples/ # Example scenes and scripts +│ └── utils/ # Shared helpers ├── ms-airsim/ │ ├── docker/ # Microsoft AirSim (legacy) containerization │ │ ├── docker-compose.yaml # Launch configuration @@ -42,8 +43,8 @@ Simulation components are launched via Docker Compose. Each simulator has its ow - **Launch command:** `airstack up --sim isaac` (or `airstack up isaac-sim` to start only the sim service) - **Main process:** The `command:` in docker-compose.yaml starts the simulator -- **Scene selection (Isaac Sim):** the standalone launch script named by `ISAAC_SIM_SCRIPT_NAME` (in `.env`) defines the scene and drones; with `ISAAC_SIM_USE_STANDALONE=false`, `ISAAC_SIM_GUI` points at a USD file to open instead -- **Auto-play:** Controlled by `PLAY_SIM_ON_START` (or `airstack up --play`) +- **Scene selection:** `airstack up --scene ` picks the environment for whichever simulator is active — see [Simulation Scenes](scenes.md). The standalone launch script named by `ISAAC_SIM_SCRIPT_NAME` (in `.env`) defines the drones and honors the selected scene; with `ISAAC_SIM_USE_STANDALONE=false`, `ISAAC_SIM_GUI` points at a USD file to open instead +- **Auto-play:** On by default — controlled by `PLAY_SIM_ON_START` (`airstack up --no-play` starts paused) **Example:** ```bash @@ -73,7 +74,6 @@ Isaac Sim is our primary simulation platform, offering: - [Isaac Sim Overview](isaac_sim/index.md) - [Pegasus Scene Setup](isaac_sim/pegasus_scene_setup.md) -- [Ascent SITL Extension](isaac_sim/ascent_sitl_extension.md) - [Export from Unreal Engine](isaac_sim/export_stages_from_unreal.md) ### Microsoft AirSim (legacy) (Unreal Engine) @@ -86,22 +86,21 @@ An open-source drone simulator built on Unreal Engine with native PX4 SITL integ - Depth-based obstacle avoidance testing (DROAN) - Environments from the Unreal Engine ecosystem -**Launch:** `airstack up --env-file overrides/ms-airsim.env` +**Launch:** `airstack up --sim airsim` **Location:** `simulation/ms-airsim/` ### Simple Sim (Lightweight) -A lightweight 2D/3D simulator for basic testing and development when full Isaac Sim fidelity isn't needed. +A lightweight kinematic simulator (single ROS 2 node, no PX4/MAVROS — it mocks the MAVROS interface directly) for fast iteration when full Isaac Sim fidelity isn't needed. Single robot only. See [Simple Sim](simple_sim/index.md). **Use cases:** -- Quick algorithm prototyping -- CI/CD testing -- Lower hardware requirements +- Quick algorithm prototyping (planning/control/stereo perception) +- Machines without an Isaac-class GPU or Omniverse credentials - Faster iteration cycles -**Launch:** `airstack up simple-sim` +**Launch:** `airstack up --sim simple` **Location:** `simulation/simple-sim/` @@ -157,7 +156,7 @@ Key environment variables for simulation (set in `.env` or at runtime): | `ISAAC_SIM_SCRIPT_NAME` | Standalone launch script (scene + drones) in `simulation/isaac-sim/launch_scripts/` | `example_one_px4_pegasus_launch_script.py` | | `ISAAC_SIM_USE_STANDALONE` | `true`: run the launch script; `false`: open the USD in `ISAAC_SIM_GUI` | `true` | | `ISAAC_SIM_GUI` | USD file to open when not using a standalone script | `simple_pegasus.scene.usd` | -| `PLAY_SIM_ON_START` | Auto-start simulation | `false` | +| `PLAY_SIM_ON_START` | Auto-start simulation | `true` | | `NUM_ROBOTS` | Number of robot containers (use `--robots` so the sim matches) | `1` | **Example:** @@ -165,14 +164,11 @@ Key environment variables for simulation (set in `.env` or at runtime): # Custom launch script ISAAC_SIM_SCRIPT_NAME=my_custom_scene.py airstack up --sim isaac -# Auto-play on start -airstack up --sim isaac --play +# Start paused (press Play in the sim window yourself) +airstack up --sim isaac --no-play ``` -**Pre-built scenes:** Located in `scenes/` directory - -- `two_drone_fire_new.usd` - Fire academy scenario -- `two_drone_RetroNeighborhood.usd` - Urban neighborhood +**Pre-built scenes:** Located in `simulation/isaac-sim/assets/scenes/` (e.g. `simple_pegasus.scene.usd`); standalone launch scripts in `simulation/isaac-sim/launch_scripts/` build scenes programmatically. **Learn more:** [Docker Workflow](../development/beginner/airstack-cli/docker_usage.md#docker-compose-variable-overrides) diff --git a/docs/simulation/isaac_sim/ascent_node.png b/docs/simulation/isaac_sim/ascent_node.png deleted file mode 100644 index bb3020b90..000000000 Binary files a/docs/simulation/isaac_sim/ascent_node.png and /dev/null differ diff --git a/docs/simulation/isaac_sim/ascent_sitl_extension.md b/docs/simulation/isaac_sim/ascent_sitl_extension.md deleted file mode 100644 index 7eb63ef09..000000000 --- a/docs/simulation/isaac_sim/ascent_sitl_extension.md +++ /dev/null @@ -1,21 +0,0 @@ -# AirLab AirStack Extension - -The AirStack extension for IsaacSim does two main things. It creates an Ascent Omnigraph Node which runs the Ascent SITL and updates the position of a drone model in IsaacSim based on the SITL. It also creates a panel for listing, attaching to, and killing tmux sessions. - -## Ascent OmniGraph Node (Deprecated as of November 2025) - -The Ascent OmniGraph node takes as input a domain id, node namespace and drone prim. It runs the Ascent SITL, mavproxy, and mavros and takes care of keeping the SITL time synced with IsaacSim's time. Mavros is run using the inputted domain id and node namespace. The drone prim's position is set based off of the position of the drone in the SITL. The drone prim doesn't do collision and will pass through objects in the IsaacSim world. - -The way the SITL is synced with IsaacSim is by running the SITL in gdb with a breakpoint on the functin that advances the SITL time. Every time this function is called, our code is run by injecting a library using the LD_PRELOAD trick. Our code runs a client socket that talks to a server socket running in the AirStack IsaacSim extension which tells it how long to sleep based off the current SITL and IsaacSim time. - -The Ascent OmniGraph node is shown below: - -![Ascent OmniGraph Node](ascent_node.png) - -## TMUX Panel - -This is a panel for listing, attaching to, and killing any running TMUX sessions. The Ascent SITL, mavproxy, and mavros are run in a TMUX sesion, so this is mainly for debugging those and probably doesn't need to be interacted with by most users. A list of TMUX sessions is displayed in the panel. It doesn't auto refresh so you have to manually click the refresh button to display any changes in the list of sessions. For each session, there is an `Attach` button and a `Kill` button. The `Attach` button will bring up an `xterm` window with the TMUX session. The `Kill` button will kill the TMUX session. - -The TMUX panel is shown below: - -![TMUX Panel](tmux_panel.png) \ No newline at end of file diff --git a/docs/simulation/isaac_sim/container_workflows.md b/docs/simulation/isaac_sim/container_workflows.md new file mode 100644 index 000000000..da4bd1db1 --- /dev/null +++ b/docs/simulation/isaac_sim/container_workflows.md @@ -0,0 +1,272 @@ +# Isaac Sim Container Workflows + +Working procedures for the Isaac Sim container: launching it in different modes, accessing it, iterating on scenes and scripts, managing images, and troubleshooting. + +**Reference for this container:** [Isaac Sim Docker Configuration](docker.md) — file structure, service architecture, launch configuration, environment variables, networking, GPU access, and volume mounts. + +## Launch Modes + +Isaac Sim supports multiple launch modes: + +### 1. Standard Launch (ROS 2 Integration) + +Default mode with ROS 2 bridge: + +```bash +airstack up isaac-sim +``` + +**What happens:** + +- Launches Isaac Sim with ROS 2 bridge +- Runs the launch script named by `ISAAC_SIM_SCRIPT_NAME` (or, with `ISAAC_SIM_USE_STANDALONE=false`, opens the USD in `ISAAC_SIM_GUI`) +- Publishes sensor topics to ROS 2 +- Optionally auto-plays simulation (via `PLAY_SIM_ON_START`) + +### 2. Standalone Python Launch + +Launch with standalone Python script: + +```bash +ISAAC_SIM_USE_STANDALONE=true ISAAC_SIM_SCRIPT_NAME=my_script.py airstack up isaac-sim +``` + +**Use cases:** + +- Custom simulation logic +- Advanced scene setup +- Programmatic control + +### 3. GUI-Only Mode (`isaac-sim-gui`) + +The `isaac-sim-gui` compose service opens Isaac Sim's **full GUI editor** +(`runapp.sh` — no Pegasus launch script, no drones) for USD/scene editing on +any asset: + +```bash +airstack up --profile isaac-sim-gui isaac-sim-gui +``` + +(The service sits behind its own `isaac-sim-gui` profile, so both the +`--profile` flag and the service name are needed; it does not conflict with +the one-active-simulator rule.) + +**Use cases:** + +- Authoring/editing USD scenes and assets (see [Pegasus Scene Setup](pegasus_scene_setup.md)) +- Inspecting assets without bringing up the robot stack + +**Not for flying:** the service is deliberately **not** on `airstack_network` +(`networks: !reset null` in `simulation/isaac-sim/docker/docker-compose.yaml`), +so no DDS traffic reaches the robot containers and no PX4 is launched. To fly, +use `airstack up --sim isaac`. + +### Example overrides + +```bash +# Launch without a window (headless) +airstack up --sim isaac --headless + +# Don't auto-play simulation +airstack up isaac-sim --no-play + +# Launch with standalone script +ISAAC_SIM_USE_STANDALONE=true ISAAC_SIM_SCRIPT_NAME=custom_scene.py airstack up isaac-sim +``` + +See the [environment variables table](docker.md#environment-variables) for the full list of configuration variables. + +## Omniverse Credentials + +Isaac Sim requires NVIDIA Omniverse credentials. + +### Setup + +1. **Create credentials file:** + ```bash + cp simulation/isaac-sim/docker/omni_pass_TEMPLATE.env simulation/isaac-sim/docker/omni_pass.env + ``` + +2. **Edit with your credentials:** + ```bash + # omni_pass.env + OMNI_USER=your_username + OMNI_PASS=your_password + ``` + +3. **File is git-ignored** (don't commit credentials!) + +### Getting Credentials + +1. Create account at [NVIDIA Omniverse](https://www.nvidia.com/en-us/omniverse/) +2. Use your NVIDIA account credentials +3. Required for downloading assets and extensions + +## Accessing Isaac Sim + +### Via GUI (Default) + +If `DISPLAY` is configured: + +```bash +airstack up isaac-sim +# Isaac Sim GUI opens on host display +``` + +### Via tmux Session + +Connect to the container and attach to tmux: + +```bash +# Connect to container +airstack connect isaac-sim + +# Attach to Isaac Sim tmux session +tmux a -t isaac +``` + +**Useful tmux commands:** + +- `Ctrl-b d` - Detach from session +- `Ctrl-b [` - Scroll mode (arrow keys to scroll logs) +- `Ctrl-c` - Stop Isaac Sim + +### Via Streaming (Headless) + +For remote access, use Isaac Sim streaming: + +**WebRTC streaming:** +```bash +# Inside container +./runheadless.webrtc.sh +``` + +Access via web browser. + +## Development Workflow + +### Iterating on Scenes + +1. **Edit scene files** on host (they're mounted): + ``` + simulation/isaac-sim/assets/scenes/my_scene.usd + ``` + +2. **Reload in Isaac Sim:** + - File → Open + - Or restart container with new scene + +3. **Changes persist** (files on host) + +### Testing Standalone Scripts + +1. **Create script:** + ``` + simulation/isaac-sim/launch_scripts/test_script.py + ``` + +2. **Launch:** + ```bash + ISAAC_SIM_USE_STANDALONE=true ISAAC_SIM_SCRIPT_NAME=test_script.py airstack up isaac-sim + ``` + +3. **View output:** + ```bash + airstack logs isaac-sim # tmux pane output is mirrored to docker logs + airstack connect isaac-sim # attach to the tmux session interactively + ``` + +### Debugging + +**Enable debug logging:** + +Edit `user.config.json` to increase log verbosity. + +**View logs:** + +```bash +# Container logs +airstack logs isaac-sim + +# Isaac Sim logs +ls $HOME/docker/isaac-sim/logs/ +``` + +## Image Management + +### Pulling Pre-built Images + +```bash +# Login to AirLab registry +docker login airlab-docker.andrew.cmu.edu + +# Pull Isaac Sim image +docker compose -f simulation/isaac-sim/docker/docker-compose.yaml pull +``` + +### Building from Source + +```bash +# Build Isaac Sim image +docker compose -f simulation/isaac-sim/docker/docker-compose.yaml build + +# Build with no cache +docker compose -f simulation/isaac-sim/docker/docker-compose.yaml build --no-cache +``` + +**Note:** Isaac Sim base image is large (~20GB). Initial build takes time. + +## Troubleshooting + +**Isaac Sim won't start:** + +- Check GPU: `nvidia-smi` on host +- Verify NVIDIA Container Toolkit: `docker run --rm --gpus all nvidia/cuda:11.8.0-base-ubuntu22.04 nvidia-smi` +- Check disk space: `df -h` (need 25GB+ free) +- Review logs: `airstack logs isaac-sim` + +**GUI not displaying:** + +- Check `DISPLAY`: `echo $DISPLAY` (should be `:0` or `:1`) +- Allow X11: `xhost +local:docker` +- Verify X11 socket mounted: Check docker-compose volumes + +**ROS 2 topics not visible:** + +- Verify containers on same network: `docker network inspect airstack_network` +- Check ROS 2 domain IDs match +- Inspect DDS: `fastdds.xml` configuration +- Test connection: `ros2 topic list` in Isaac Sim container + +**`rclpy` / `_rclpy_pybind11` warnings when starting Kit with `python.sh`:** + +!!! note "Why this happens: Kit-Python vs system Python ABI" + Jazzy’s `setup.bash` puts **Python 3.12** ROS packages on `PYTHONPATH`. Isaac’s `python.sh` uses **Kit Python (~3.10)**. Importing system `rclpy` from the wrong interpreter causes ABI errors in the log (topics from Omnigraph may still work). + + Standalone launch uses `PYTHONPATH="$ISAAC_SIM_PYTHONPATH"` in the **tmux** command (`$$ISAAC_SIM_PYTHONPATH` in `docker-compose.yaml` so Compose does not treat it as a host variable). See container `.bashrc` and `docker-compose.yaml`: it drops `lib/python3.12/site-packages` and appends the bridge’s internal `rclpy` path. + +**Performance issues:** + +- Reduce scene complexity +- Lower physics timestep +- Disable raytracing (Settings → Rendering) +- Close other GPU-intensive applications + +**Omniverse login fails:** + +- Verify credentials in `omni_pass.env` +- Check network connectivity +- Ensure NVIDIA account is active + +**Extension not loading:** + +- Verify `user.config.json` enables extension +- Check extension path in volume mounts +- Review Isaac Sim logs for extension errors + +## See Also + +- [Isaac Sim Docker Configuration](docker.md) - Container reference: files, services, environment variables, mounts +- [Isaac Sim Overview](index.md) - Isaac Sim capabilities and features +- [Pegasus Scene Setup](pegasus_scene_setup.md) - Creating custom scenes +- [Docker Workflow](../../development/beginner/airstack-cli/docker_usage.md) - General Docker operations diff --git a/docs/simulation/isaac_sim/content_browser.png b/docs/simulation/isaac_sim/content_browser.png deleted file mode 100644 index 656c85624..000000000 Binary files a/docs/simulation/isaac_sim/content_browser.png and /dev/null differ diff --git a/docs/simulation/isaac_sim/docker.md b/docs/simulation/isaac_sim/docker.md index 6ab78c426..486aa32b1 100644 --- a/docs/simulation/isaac_sim/docker.md +++ b/docs/simulation/isaac_sim/docker.md @@ -1,6 +1,8 @@ # Isaac Sim Docker Configuration -Isaac Sim runs in a Docker container with NVIDIA GPU support and full integration with the AirStack ecosystem. +Isaac Sim runs in a Docker container with NVIDIA GPU support and full integration with the AirStack ecosystem. This page is the **reference** for the container: file structure, service architecture, launch configuration, environment variables, networking, GPU access, and volume mounts. + +**Working procedures:** [Isaac Sim Container Workflows](container_workflows.md) — launch modes, credentials setup, accessing Isaac Sim, development workflow, image management, and troubleshooting. ## File Structure @@ -30,67 +32,17 @@ The Isaac Sim service is defined in `simulation/isaac-sim/docker/docker-compose. | **ROS 2 Bridge** | Native ROS 2 topic publishing/subscribing | | **GPU Acceleration** | NVIDIA GPU for rendering and physics | -## Launch Modes - -Isaac Sim supports multiple launch modes: - -### 1. Standard Launch (ROS 2 Integration) - -Default mode with ROS 2 bridge: - -```bash -airstack up isaac-sim -``` - -**What happens:** - -- Launches Isaac Sim with ROS 2 bridge -- Runs the launch script named by `ISAAC_SIM_SCRIPT_NAME` (or, with `ISAAC_SIM_USE_STANDALONE=false`, opens the USD in `ISAAC_SIM_GUI`) -- Publishes sensor topics to ROS 2 -- Optionally auto-plays simulation (via `PLAY_SIM_ON_START`) - -### 2. Standalone Python Launch - -Launch with standalone Python script: - -```bash -ISAAC_SIM_USE_STANDALONE=true ISAAC_SIM_SCRIPT_NAME=my_script.py airstack up isaac-sim -``` - -**Use cases:** - -- Custom simulation logic -- Advanced scene setup -- Programmatic control - -### 3. GUI-Only Mode - -Launch just the Isaac Sim GUI (no ROS 2): - -```bash -airstack up --profile isaac-sim-gui isaac-sim-gui -``` - -**Use cases:** - -- Scene authoring -- Testing without robot stack -- Visual debugging - ## Launch Configuration -The container command in docker-compose.yaml: +The container command in `simulation/isaac-sim/docker/docker-compose.yaml` (excerpt — see the compose file for the full command): ```yaml command: > bash -c " tmux new -d -s isaac; if [ $$AUTOLAUNCH = 'true' ]; then - if [ \"${ISAAC_SIM_USE_STANDALONE}\" = 'true' ]; then - tmux send-keys -t isaac 'PYTHONPATH="$$ISAAC_SIM_PYTHONPATH" /isaac-sim/python.sh /isaac-sim/AirStack/simulation/isaac-sim/launch_scripts/${ISAAC_SIM_SCRIPT_NAME} --ext-folder ~/.local/share/ov/data/documents/Kit/shared/exts' ENTER - else - tmux send-keys -t isaac 'ros2 launch isaacsim run_isaacsim.launch.py install_path:=/isaac-sim gui:=\"${ISAAC_SIM_GUI}\" play_sim_on_start:=\"${PLAY_SIM_ON_START}\"' ENTER - fi + ... # standalone: python.sh + ISAAC_SIM_SCRIPT_NAME + # otherwise: ros2 launch isaacsim run_isaacsim.launch.py fi; sleep infinity" ``` @@ -102,6 +54,8 @@ command: > 3. Chooses standalone or ROS 2 mode based on `ISAAC_SIM_USE_STANDALONE` 4. Keeps container alive with `sleep infinity` +For the launch mode recipes (standard, standalone script, GUI-only editor), see [Container Workflows → Launch Modes](container_workflows.md#launch-modes). + ## Environment Variables Key variables for Isaac Sim configuration: @@ -111,28 +65,17 @@ Key variables for Isaac Sim configuration: | `AUTOLAUNCH` | Auto-start Isaac Sim on container launch | `true` | | `ISAAC_SIM_USE_STANDALONE` | `true`: run `ISAAC_SIM_SCRIPT_NAME`; `false`: open the `ISAAC_SIM_GUI` USD | `true` | | `ISAAC_SIM_SCRIPT_NAME` | Standalone launch script in `simulation/isaac-sim/launch_scripts/` | `example_one_px4_pegasus_launch_script.py` | -| `ISAAC_SIM_GUI` | Path to a USD scene file (used only when `ISAAC_SIM_USE_STANDALONE=false`) | `simple_pegasus.scene.usd` | -| `PLAY_SIM_ON_START` | Auto-play simulation on start (`airstack up --play/--no-play`) | `false` | +| `ISAAC_SIM_GUI` | Path to a USD scene file (used only when `ISAAC_SIM_USE_STANDALONE=false`) | `simulation/isaac-sim/assets/scenes/simple_pegasus.scene.usd` | +| `PLAY_SIM_ON_START` | Auto-play simulation on start (`airstack up --play/--no-play`) | `true` | | `ISAAC_SIM_HEADLESS` | Run without a window (`airstack up --headless`) | unset (`false`) | -| `PX4_PHYSICS_HZ` | Physics step rate for PX4 SITL — also sets PX4 `IMU_INTEG_RATE` | `250` | -| `PX4_RENDERING_HZ` | Rendering frame rate for PX4 profiles (independent of physics) | `60` | +| `PX4_PHYSICS_HZ` | Physics step rate for PX4 SITL — also sets PX4 `IMU_INTEG_RATE` | `100` | +| `PX4_RENDERING_HZ` | Rendering frame rate for PX4 profiles (independent of physics) | `30` | | `ARDUPILOT_PHYSICS_HZ` | Physics step rate for ArduPilot SITL | `800` | | `ARDUPILOT_RENDERING_HZ` | Rendering frame rate for ArduPilot profiles | `120` | -`PX4_PHYSICS_HZ` and `PX4_RENDERING_HZ` are set in the isaac-sim compose file (defaults 100/60 there; the Pegasus code default is 250). AirStack runs PX4 at **100 Hz** for near-real-time performance. See [Pegasus Scene Setup → Physics Rate](pegasus_scene_setup.md) for valid values and the full configuration flow. +`PX4_PHYSICS_HZ` and `PX4_RENDERING_HZ` default to 100/30 in the isaac-sim compose file (the Pegasus code default is 250 Hz physics). AirStack runs PX4 at **100 Hz** for near-real-time performance. See [Pegasus Scene Setup → Physics Rate](pegasus_scene_setup.md) for valid values and the full configuration flow. -**Example overrides:** - -```bash -# Launch without GUI (headless) -ISAAC_SIM_GUI=false airstack up isaac-sim - -# Don't auto-play simulation -PLAY_SIM_ON_START=false airstack up isaac-sim - -# Launch with standalone script -ISAAC_SIM_USE_STANDALONE=true ISAAC_SIM_SCRIPT_NAME=custom_scene.py airstack up isaac-sim -``` +For example command-line overrides of these variables, see [Container Workflows → Launch Modes](container_workflows.md#launch-modes). ## Networking @@ -170,6 +113,20 @@ deploy: nvidia-smi ``` +### Multi-GPU Setup + +For multi-GPU systems: + +```yaml +deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0', '1'] # Use specific GPUs + capabilities: [gpu] +``` + ## Volume Mounts Isaac Sim container mounts several directories: @@ -206,7 +163,7 @@ Enables GUI display on host. - ../extensions/PegasusSimulator/extensions/pegasus.simulator:/isaac-sim/.local/share/ov/data/documents/Kit/shared/exts/pegasus.simulator/:rw ``` -Mounts the Pegasus multi-rotor simulator extension. +Mounts the Pegasus multi-rotor simulator extension. Custom Isaac Sim extensions follow the same pattern: place the extension in `simulation/isaac-sim/extensions/`, mount it in docker-compose.yaml, and enable it in `user.config.json`. ### AirStack Code @@ -225,232 +182,11 @@ Mounts entire AirStack repository for access to scenes, scripts, and launch file **user.config.json:** Enables Pegasus extension and other custom settings. -## Omniverse Credentials - -Isaac Sim requires NVIDIA Omniverse credentials. - -### Setup - -1. **Create credentials file:** - ```bash - cp simulation/isaac-sim/docker/omni_pass_TEMPLATE.env simulation/isaac-sim/docker/omni_pass.env - ``` - -2. **Edit with your credentials:** - ```bash - # omni_pass.env - OMNI_USER=your_username - OMNI_PASS=your_password - ``` - -3. **File is git-ignored** (don't commit credentials!) - -### Getting Credentials - -1. Create account at [NVIDIA Omniverse](https://www.nvidia.com/en-us/omniverse/) -2. Use your NVIDIA account credentials -3. Required for downloading assets and extensions - -## Accessing Isaac Sim - -### Via GUI (Default) - -If `DISPLAY` is configured: - -```bash -airstack up isaac-sim -# Isaac Sim GUI opens on host display -``` - -### Via tmux Session - -Connect to the container and attach to tmux: - -```bash -# Connect to container -airstack connect isaac-sim - -# Attach to Isaac Sim tmux session -tmux a -t isaac -``` - -**Useful tmux commands:** - -- `Ctrl-b d` - Detach from session -- `Ctrl-b [` - Scroll mode (arrow keys to scroll logs) -- `Ctrl-c` - Stop Isaac Sim - -### Via Streaming (Headless) - -For remote access, use Isaac Sim streaming: - -**Native streaming:** -```bash -# Inside container -./runheadless.native.sh -``` - -Connect with [Omniverse Streaming Client](https://docs.omniverse.nvidia.com/streaming-client/latest/user-manual.html). - -**WebRTC streaming:** -```bash -# Inside container -./runheadless.webrtc.sh -``` - -Access via web browser. - -## Development Workflow - -### Iterating on Scenes - -1. **Edit scene files** on host (they're mounted): - ``` - simulation/isaac-sim/scenes/my_scene.usd - ``` - -2. **Reload in Isaac Sim:** - - File → Open - - Or restart container with new scene - -3. **Changes persist** (files on host) - -### Testing Standalone Scripts - -1. **Create script:** - ``` - simulation/isaac-sim/launch_scripts/test_script.py - ``` - -2. **Launch:** - ```bash - ISAAC_SIM_USE_STANDALONE=true ISAAC_SIM_SCRIPT_NAME=test_script.py airstack up isaac-sim - ``` - -3. **View output:** - ```bash - airstack logs isaac-sim # tmux pane output is mirrored to docker logs - airstack connect isaac-sim # attach to the tmux session interactively - ``` - -### Debugging - -**Enable debug logging:** - -Edit `user.config.json` to increase log verbosity. - -**View logs:** - -```bash -# Container logs -airstack logs isaac-sim - -# Isaac Sim logs -ls $HOME/docker/isaac-sim/logs/ -``` - -## Image Management - -### Pulling Pre-built Images - -```bash -# Login to AirLab registry -docker login airlab-docker.andrew.cmu.edu - -# Pull Isaac Sim image -docker compose -f simulation/isaac-sim/docker/docker-compose.yaml pull -``` - -### Building from Source - -```bash -# Build Isaac Sim image -docker compose -f simulation/isaac-sim/docker/docker-compose.yaml build - -# Build with no cache -docker compose -f simulation/isaac-sim/docker/docker-compose.yaml build --no-cache -``` - -**Note:** Isaac Sim base image is large (~20GB). Initial build takes time. - -## Troubleshooting - -**Isaac Sim won't start:** - -- Check GPU: `nvidia-smi` on host -- Verify NVIDIA Container Toolkit: `docker run --rm --gpus all nvidia/cuda:11.8.0-base-ubuntu22.04 nvidia-smi` -- Check disk space: `df -h` (need 25GB+ free) -- Review logs: `airstack logs isaac-sim` - -**GUI not displaying:** - -- Check `DISPLAY`: `echo $DISPLAY` (should be `:0` or `:1`) -- Allow X11: `xhost +local:docker` -- Verify X11 socket mounted: Check docker-compose volumes - -**ROS 2 topics not visible:** - -- Verify containers on same network: `docker network inspect airstack_network` -- Check ROS 2 domain IDs match -- Inspect DDS: `fastdds.xml` configuration -- Test connection: `ros2 topic list` in Isaac Sim container - -**`rclpy` / `_rclpy_pybind11` warnings when starting Kit with `python.sh`:** - -- Jazzy’s `setup.bash` puts **Python 3.12** ROS packages on `PYTHONPATH`. Isaac’s `python.sh` uses **Kit Python (~3.10)**. Importing system `rclpy` from the wrong interpreter causes ABI errors in the log (topics from Omnigraph may still work). -- Standalone launch uses `PYTHONPATH="$ISAAC_SIM_PYTHONPATH"` in the **tmux** command (`$$ISAAC_SIM_PYTHONPATH` in `docker-compose.yaml` so Compose does not treat it as a host variable). See container `.bashrc` and `docker-compose.yaml`: it drops `lib/python3.12/site-packages` and appends the bridge’s internal `rclpy` path. - -**Performance issues:** - -- Reduce scene complexity -- Lower physics timestep -- Disable raytracing (Settings → Rendering) -- Close other GPU-intensive applications - -**Omniverse login fails:** - -- Verify credentials in `omni_pass.env` -- Check network connectivity -- Ensure NVIDIA account is active - -**Extension not loading:** - -- Verify `user.config.json` enables extension -- Check extension path in volume mounts -- Review Isaac Sim logs for extension errors - -## Advanced Configuration - -### Custom Extensions - -Add custom Isaac Sim extensions: - -1. Place extension in `simulation/isaac-sim/extensions/` -2. Mount in docker-compose.yaml -3. Enable in `user.config.json` - -### Multi-GPU Setup - -For multi-GPU systems: - -```yaml -deploy: - resources: - reservations: - devices: - - driver: nvidia - device_ids: ['0', '1'] # Use specific GPUs - capabilities: [gpu] -``` - -### Persistent Nucleus Server - -For team collaboration, set up persistent Nucleus server: - -Edit `omniverse.toml` with your Nucleus server URL. +**omniverse.toml:** Omniverse settings. For team collaboration with a persistent Nucleus server, edit it with your Nucleus server URL. ## See Also +- [Isaac Sim Container Workflows](container_workflows.md) - Working procedures: launch modes, access, development, troubleshooting - [Isaac Sim Overview](index.md) - Isaac Sim capabilities and features - [Pegasus Scene Setup](pegasus_scene_setup.md) - Creating custom scenes - [Simulation Overview](../index.md) - Main simulation documentation diff --git a/docs/simulation/isaac_sim/export_stages_from_unreal.md b/docs/simulation/isaac_sim/export_stages_from_unreal.md index 273022e8d..37d5ec880 100644 --- a/docs/simulation/isaac_sim/export_stages_from_unreal.md +++ b/docs/simulation/isaac_sim/export_stages_from_unreal.md @@ -9,7 +9,10 @@ Generally, Unreal Engine environments can be found on Epic Games' [Fab Marketpla The below video explains how to export an Unreal Engine environment to an Isaac Sim stage. - + + +!!! warning "Two settings to change from the video" + Unlike what the video shows, set the export **Up Axis to Z-up** and the **scale to meters**. AirStack and Isaac Sim work in Z-up meters (see [Frame Conventions](../../development/intermediate/frame_conventions.md)); exporting this way avoids the unit/orientation fix-ups later on this page. You can save this file as `[YOUR_ENVIRONMENT_NAME].stage.usd`. @@ -24,9 +27,11 @@ Omniverse doesn't perform well with large amounts of vegetation. Anything with c That said you can still achieve photorealism by substituting complex geometries for high quality textures. Isaac seems to do fine with high quality textures. -**Optimization:** After exporting, edit the file with [USD Composer](https://docs.omniverse.nvidia.com/composer/latest/index.html) and run the [Scene Optimizer extension](https://docs.omniverse.nvidia.com/extensions/latest/ext_scene-optimizer.html) for faster performance. USD Composer can be installed via [Omniverse Launcher](https://docs.omniverse.nvidia.com/launcher/latest/index.html). +**Decals don't export:** Unreal Engine Decals do not export well to USD, so any decorations applied as decals — paint markings, road lines, dirt, puddles, and similar surface details — won't come across. If those details matter for your scene, bake them into the surface textures instead. + +**Optimization:** After exporting, edit the file with [USD Composer](https://docs.omniverse.nvidia.com/composer/latest/index.html) and run the [Scene Optimizer extension](https://docs.omniverse.nvidia.com/extensions/latest/ext_scene-optimizer.html) for faster performance. USD Composer / USD Explorer can be installed from NVIDIA's current distribution channel (the Omniverse Launcher has been discontinued). -**Verify the Scale:** The Omniverse exporter exports in centimeters, but Isaac Sim natively works in meters. For consistency, follow these steps to [change the scene units to be meters](https://forums.developer.nvidia.com/t/how-to-change-units-of-the-grid-from-centimeters-to-meters/301285#:~:text=Find%20the%20%E2%80%9CMeters%20Per%20Unit%E2%80%9D%20property%20and%20set%20it%20to%201%20for%20meters). +**Verify the Scale:** Isaac Sim natively works in meters. If you set the export scale to meters as instructed above, this is already correct — but if a stage was exported in centimeters (the exporter's old default), follow these steps to [change the scene units to be meters](https://forums.developer.nvidia.com/t/how-to-change-units-of-the-grid-from-centimeters-to-meters/301285#:~:text=Find%20the%20%E2%80%9CMeters%20Per%20Unit%E2%80%9D%20property%20and%20set%20it%20to%201%20for%20meters). To check the scale of the scene, you can add a cube in Isaac Sim and compare it to the exported scene. The cube is 1m x 1m x 1m. @@ -35,4 +40,4 @@ To check the scale of the scene, you can add a cube in Isaac Sim and compare it Adding physics to the stage is as simple as adding a `Physics` property with the "Colliders Preset", as described in the [Isaac docs](https://docs.omniverse.nvidia.com/isaacsim/latest/gui_tutorials/tutorial_intro_simple_objects.html#adding-physics-properties). Then save the scene as `[YOUR_ENVIRONMENT_NAME].scene.usd` to clarify that it's a physics-enabled scene. -You're now ready to add robots to the scene on the next page. \ No newline at end of file +You're now ready to add robots to the scene — see [Spawning Drones](spawning_drones.md). \ No newline at end of file diff --git a/docs/simulation/isaac_sim/index.md b/docs/simulation/isaac_sim/index.md index 5f11f3ef5..9331479e1 100644 --- a/docs/simulation/isaac_sim/index.md +++ b/docs/simulation/isaac_sim/index.md @@ -5,6 +5,11 @@ We chose Isaac Sim as the best balance between photorealism and physics simulati Isaac Sim is built on [NVIDIA Omniverse](https://developer.nvidia.com/omniverse), which provides a physically-based rendering engine and accurate rigid-body physics. This combination allows us to create realistic scenes that behave and look close to the real world. + +*Three drones running the full AirStack autonomy stack in Isaac Sim (`airstack up --sim isaac --robots 3 --scene full-warehouse`): parallel `TakeoffTask` actions followed by concurrent Circle and Figure-8 `FixedTrajectoryTask` patterns, viewed from the built-in follow camera (`ISAAC_SIM_FOLLOW_CAM`).* + ## Why Isaac Sim ### ROS 2 Integration diff --git a/docs/simulation/isaac_sim/natnet_emulator.md b/docs/simulation/isaac_sim/natnet_emulator.md deleted file mode 100644 index 4d6cb12da..000000000 --- a/docs/simulation/isaac_sim/natnet_emulator.md +++ /dev/null @@ -1,305 +0,0 @@ -# NatNet Emulator (OptiTrack Simulation) - -The `optitrack.natnet.emulator` Isaac Sim extension lets you test the full -[`natnet_ros2`](../../../robot/ros_ws/src/perception/natnet_ros2/README.md) -perception stack in simulation without a physical OptiTrack system. It runs a -Motive-compatible NatNet UDP server inside Isaac Sim, streams rigid-body poses -sampled from USD prim world transforms, and presents the same wire protocol -that the real Motive software uses. - -## How it works - -``` -Isaac Sim (physics step) - ↓ sample prim world pose -NatNetServerManager (/World/NatNetInterface USD prim) - ↓ encode sFrameOfMocapData (NatNet 4.1 wire format) -NatNetUnicastServer ──UDP 1510/1511──► natnet_ros2_node (robot container) - ↓ - /robot_N/perception/optitrack/{body} - /robot_N/interface/mavros/vision_pose/pose -``` - -Configuration lives on a `/World/NatNetInterface` USD prim with `natnet:*` -attributes. Because it is USD, the config **persists when you save the stage** — -re-opening a `.usd` file restores the catalog and server settings without -re-running any script. - -Each physics step the extension: - -1. Reads the world transform of each tracked prim. -2. Packs a `sFrameOfMocapData` frame (one `sRigidBodyData` entry per body). -3. Flushes the frame immediately on the physics-step thread (no background timer). - -Bodies whose target prim is missing emit a **lost** frame (NaN position, -tracking-invalid bit clear) until the prim appears — this handles Pegasus drones -that are spawned on the first Play tick. - -Optional **sensor noise** (`pose_noise_std_meters`, `pose_noise_rotation_deg`) -adds Gaussian position and orientation perturbation to simulate real OptiTrack -measurement uncertainty. - ---- - -## Using the pre-built launch scripts - -The easiest way to start is with the provided Pegasus launch scripts. Set -`ISAAC_SIM_SCRIPT_NAME` in your environment or use the convenience override: - -```bash -# Single drone, NatNet emulator + PX4 flying on external vision -airstack up --env-file overrides/isaac-optitrack-simulation.env -``` - -`overrides/isaac-optitrack-simulation.env` sets: - -| Variable | Value | -|---|---| -| `NUM_ROBOTS` | `1` | -| `LAUNCH_NATNET` | `true` | -| `PX4_PARAM_SET` | `external-vision` | -| `ISAAC_SIM_SCRIPT_NAME` | `example_one_px4_pegasus_natnet_launch_script.py` | - -`PX4_PARAM_SET` selects `simulation/isaac-sim/docker/px4-params/.env`, whose -`PX4_PARAM_*` entries PX4's rcS applies at boot. It defaults to `default`, which is empty, -so every other Isaac Sim run keeps PX4's firmware defaults. Add a file there to save your -own parameter set. - -### Available NatNet launch scripts - -| Script | Use case | -|---|---| -| `example_one_px4_pegasus_natnet_launch_script.py` | Single drone + static `Target` body | -| `example_multi_px4_pegasus_natnet_launch_script.py` | `NUM_ROBOTS` drones + shared `Target` body | - -Both scripts set up GPS origins (via `gps_utils.py`) so the GCS datum matches -PX4, author the NatNet interface, and play the simulation automatically. - -!!! note "Baseline scripts have no NatNet" - `example_one_px4_pegasus_launch_script.py` and - `example_multi_px4_pegasus_launch_script.py` do **not** include NatNet. Use - the `*_natnet_*` variants above when you need mocap simulation. - -### Body naming - -| `NUM_ROBOTS` | Body names streamed | -|---|---| -| 1 | `Drone`, `Target` | -| N > 1 | `Drone1`, `Drone2`, …, `DroneN`, `Target` | - -### Changing which body is streamed - -The streamed body name and streaming id are **constants in the launch script** -(`NATNET_BODY_NAME` / `NATNET_BODY_ID` / `NATNET_TARGET_NAME`), not environment -variables. They must match a body entry in the robot's profile in -[`natnet_config.yaml`](../../../robot/ros_ws/src/perception/natnet_ros2/config/natnet_config.yaml), -which is the only place the client reads its bodies from — that is what lets each robot -in a multi-robot scene track a different body. - -To retarget, edit **both** together: - -| Where | What | -|---|---| -| `example_one_px4_pegasus_natnet_launch_script.py` | `NATNET_BODY_NAME`, `NATNET_BODY_ID` | -| `natnet_config.yaml` → `robots..bodies[]` | `rigid_body_name`, `id` | - -!!! warning "A mismatch fails silently" - The NatNet client filters incoming frames by **numeric id**. If the ids disagree, the - client connects, the emulator streams, and the pose topic never publishes — with no - error on either side. When debugging a silent stream, check the id first. - ---- - -## Adding NatNet to your own launch script - -Call `author_drone_natnet_interface` after your Pegasus drones are spawned and -before you start the timeline. The extension builds the server from the prim on -Play. - -```python -from isaacsim.core.utils.extensions import enable_extension - -# Enable the NatNet emulator extension through Kit extension manager. -enable_extension("optitrack.natnet.emulator") - -from optitrack.natnet.emulator.isaac import ( - author_drone_natnet_interface, - author_static_target, - DEFAULT_TARGET_PATH, - DEFAULT_TARGET_STREAMING_ID, -) - -stage = omni.usd.get_context().get_stage() - -# Optional: add a static target body the robot can navigate toward. -author_static_target(stage, DEFAULT_TARGET_PATH, position=(2.0, 0.0, 1.0)) - -# One entry per drone: (rigid_body_name, streaming_id, target_prim_path) -drones = [ - ("Drone", 1, "/World/drone1/base_link/body"), -] - -author_drone_natnet_interface( - stage, - drones=drones, - server_ip="172.31.0.200", # Isaac container IP on AirStack bridge network - pose_noise_enabled=True, - pose_noise_std_meters=0.0005, - pose_noise_rotation_deg=0.05, -) -``` - -For multiple drones, add one tuple per drone: - -```python -drones = [ - ("Drone1", 1, "/World/drone1/base_link/body"), - ("Drone2", 2, "/World/drone2/base_link/body"), - ("Drone3", 3, "/World/drone3/base_link/body"), -] -``` - -`author_drone_natnet_interface` also accepts the static target as a body — include -it explicitly if you want it: - -```python -from optitrack.natnet.emulator.isaac import DEFAULT_TARGET_STREAMING_ID, DEFAULT_TARGET_PATH - -drones = [ - ("Drone", 1, "/World/drone1/base_link/body"), - ("Target", DEFAULT_TARGET_STREAMING_ID, DEFAULT_TARGET_PATH), -] -``` - ---- - -## Using the Kit UI panel - -The extension registers a docked panel under **Window → NatNet Interface** in -the Isaac Sim menu bar (appears alongside the Pegasus panel). - -### Opening the panel - -Open Isaac Sim, load your scene, then go to **Window → NatNet Interface**. The -panel docks next to the Property panel in the bottom-right. - -### Panel controls - -| Button | Action | -|---|---| -| **Create Interface** | Author a fresh `/World/NatNetInterface` prim with current settings | -| **Save** | Push the form fields into the USD prim on the stage | -| **Load from Stage** | Pull the existing prim's values back into the form | -| **Print config** | Log the current config to the console | - -**The server's lifetime follows the simulation:** Play builds it from the prim, -Stop shuts it down. The panel's `Server:` label reports which state it is in. - -When edits take effect after **Save**: - -| Setting | Takes effect | -|---|---| -| Bodies — added, removed, renamed, retargeted | Next frame; the server re-reads the interface as it samples | -| `upAxis`, pose noise | Next frame | -| `serverIp`, ports, `mode` | Next **Play**; these are bound when the server is built | - -!!! warning "Restart the robot stack after each Play" - Clients register with the server instance they connect to, and `natnet_ros2` - handshakes only until its first success. A client connected during an earlier - run is unknown to the server built by the next Play and receives no frames; the - console shows `[Command Handler] Ignoring message N from unregistered client`. - - Assume one client connection per server lifetime: restart the robot container - after each Stop → Play cycle. - -### Server settings - -| Field | Default | Description | -|---|---|---| -| Server enabled | `true` | Uncheck to stop the server starting on Play | -| Server IP | `172.31.0.200` | IP the UDP socket binds to (Isaac container address) | -| Mode | `unicast` | `unicast` for direct; `multicast` for broadcast | -| Command port | `1510` | NatNet command channel | -| Data port | `1511` | NatNet data channel (frame stream) | -| Publish rate (Hz) | `120` | Target frame rate | -| Up axis | `Z` | `Z` passes poses through unchanged; `Y` re-axes for Y-up Motive | -| Pose noise enabled | `true` | Add Gaussian noise to simulate real sensor uncertainty | -| Pose noise std (m) | `0.0005` | Position noise std dev (0.5 mm, matching OptiTrack spec) | -| Pose noise rotation (deg) | `0.05` | Orientation noise std dev | - -### Adding tracked bodies - -1. In the Stage tree, **select the prim** you want to track (e.g. `/World/drone1/base_link/body`). -2. Click **Add body (from selection)** in the panel. -3. Fill in the **rigid body name** (must match the `rigid_body_name` in `natnet_config.yaml`) and **streaming ID**. -4. Click **Save**. The body starts streaming on the next frame. - -Each body row shows a live readout of the prim's current world position with a -colour-coded status indicator: - -- Green dot — server running, prim found, pose valid -- Grey dot — prim found but server not running -- Red — prim missing or NaN position - -### Persistence - -After configuring the panel, save your USD stage (**File → Save**). The -`natnet:*` attributes are written into the `.usd` file. Re-opening the stage -restores the full catalog automatically — no script or panel interaction needed -unless you want to change the config. - ---- - -## Configuration reference - -`author_drone_natnet_interface` and `build_drone_config` accept these keyword -arguments (all optional): - -| Parameter | Default | Description | -|---|---|---| -| `server_ip` | `"172.31.0.200"` | IP to bind the UDP server to | -| `mode` | `"unicast"` | `"unicast"` or `"multicast"` | -| `command_port` | `1510` | NatNet command port | -| `data_port` | `1511` | NatNet data port | -| `publish_rate` | `120.0` | Frame streaming rate (Hz) | -| `up_axis` | `"Z"` | Axis convention (`"Z"` or `"Y"`) | -| `pose_noise_enabled` | `True` | Enable sensor noise | -| `pose_noise_std_meters` | `0.0005` | Position noise std dev (m) | -| `pose_noise_rotation_deg` | `0.05` | Orientation noise std dev (degrees) | - ---- - -## Troubleshooting - -**`natnet_ros2` connects but no pose topics appear** - -- Check that `rigid_body_name` in `natnet_config.yaml` matches exactly what the - emulator is streaming (case-sensitive). Run `ros2 topic list` inside the robot - container and look for `/robot_N/perception/optitrack/...`. - -**Server starts but no data arrives in `natnet_ros2`** - -- Confirm the server IP matches the Isaac container's address (`172.31.0.200` - on the AirStack bridge). Check with `docker network inspect airstack_network`. -- The NatNet data port (1511) must be bound to the *data* socket — frames sent - from the command socket are silently dropped by libNatNet 4.4. - -**Data stopped after stopping and replaying the simulation** - -- Expected: Stop destroys the server, so the client's registration goes with it, - and `natnet_ros2` does not re-handshake after its first successful connect. The - console shows `Ignoring message N from unregistered client`. Restart the robot - container to force a fresh `NAT_CONNECT`. See the warning under - [Panel controls](#panel-controls). - -**Emulator streams but `vision_pose` is empty** - -- `vision_pose` forwarding requires `vision_pose.enabled: true` in the robot's - `natnet_config.yaml` profile and `SITL_PARAM_PROFILE=px4-vision` so PX4 - accepts external vision instead of GPS. - -**Body shows red / NaN in the UI panel** - -- The target prim doesn't exist yet. This is normal before pressing Play (Pegasus - spawns the drone `base_link` prim on the first physics tick). After Play the - indicator should turn green within one frame. diff --git a/docs/simulation/isaac_sim/omnigraph_config.png b/docs/simulation/isaac_sim/omnigraph_config.png deleted file mode 100644 index 874d098f0..000000000 Binary files a/docs/simulation/isaac_sim/omnigraph_config.png and /dev/null differ diff --git a/docs/simulation/isaac_sim/pegasus_scene_setup.md b/docs/simulation/isaac_sim/pegasus_scene_setup.md index 68473dd3d..b4be1262d 100644 --- a/docs/simulation/isaac_sim/pegasus_scene_setup.md +++ b/docs/simulation/isaac_sim/pegasus_scene_setup.md @@ -20,16 +20,14 @@ Through this approach, AirStack leverages Pegasus to create a flexible, reusable ## Launch Configuration -Launch Configuration - At the top level of the AirStack simulation environment, a `.env` file controls how Pegasus and Isaac Sim are launched: ```bash -ISAAC_SIM_GUI="omniverse://airlab-nucleus.andrew.cmu.edu/Library/Assets/Pegasus/iris_with_sensors.pegasus.robot.usd" +ISAAC_SIM_GUI="/isaac-sim/AirStack/simulation/isaac-sim/assets/scenes/simple_pegasus.scene.usd" # Set to "true" to launch Isaac Sim using a standalone Python script instead of a USD file -ISAAC_SIM_USE_STANDALONE="false" # "true" or "false" +ISAAC_SIM_USE_STANDALONE="true" # "true" or "false" # Script name (must be in /AirStack/simulation/isaac-sim/launch_scripts/) ISAAC_SIM_SCRIPT_NAME="example_one_px4_pegasus_launch_script.py" -PLAY_SIM_ON_START="false" # honored in both modes; `airstack up --play` overrides +PLAY_SIM_ON_START="true" # honored in both modes; `airstack up --no-play` overrides ``` There are *two modes* for launching Pegasus simulations: @@ -54,15 +52,7 @@ Example scripts are provided in `simulation/isaac-sim/launch_scripts/`. They are **Location:** `simulation/isaac-sim/utils/scene_prep.py` -`scene_prep.py` provides helpers that are shared across all example launch scripts: - -| Function | Purpose | -|----------|---------| -| `scale_stage_prim(stage, prim_path, scale_factor)` | Applies a uniform XYZ scale transform to the prim at `prim_path`, clearing any existing xform ops first. Use `0.01` for Nucleus assets authored in centimeters; use `1.0` for assets already in meters. | -| `add_colliders(stage_prim)` | Recursively walks every child of `stage_prim` and applies `UsdPhysics.CollisionAPI` to each `UsdGeom.Mesh`. **Must be called or drones fall through the floor.** Skips prims that already have the API. | -| `add_dome_light(stage, intensity=3500, exposure=-3)` | Adds a hemisphere light at `/World/DomeLight` (or updates it if it already exists). Pass `intensity` / `exposure` keyword arguments to override the defaults. | -| `save_scene_as_contained_usd(source_usd_url, output_dir)` | Copies the stage and all its dependencies (textures, MDL materials) from a Nucleus `omniverse://` URL into a local directory via `omni.kit.usd.collect.Collector`. Set `SAVE_SCENE_TO = None` in your script to skip this step. | -| `get_stage_meters_per_unit(stage)` | Returns `(meters_per_unit, scene_scale_factor)`. Multiply metric coordinates by `scene_scale_factor` to convert them into stage-space units. Useful for computing drone spawn heights when `STAGE_SCALE != 1.0`. | +`scene_prep.py` provides helpers that are shared across all example launch scripts — scaling, colliders, dome lighting, self-contained saving, and stage-unit conversion. The full per-function reference lives in [Spawning Drones → Scene prep helpers](spawning_drones.md#scene-prep-helpers). #### Loading `scene_prep` @@ -112,13 +102,13 @@ Scripts must live in `simulation/isaac-sim/launch_scripts/`. Set `ISAAC_SIM_SCRI ## RTX OmniLidar and near range (`min_range`) — known limitation {#rtx-lidar-near-range} -AirStack’s Pegasus fork (Isaac Sim **5.1+**) wires **RTX OmniLidar** through OmniGraph helpers such as `add_rtx_lidar_subgraph` in `pegasus.simulator.ogn.api.spawn_rtx_lidar` (used from `simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py`, `example_multi_px4_pegasus_launch_script.py`, etc.). Recent work in this repo switched those scripts from the legacy Ouster graph path to this **RTX** API and reconciled ROS topic names (e.g. raw cloud on `…/sensors/ouster/point_cloud_raw`, filtered consumer topic `…/sensors/ouster/point_cloud`). +AirStack’s Pegasus fork (Isaac Sim **5.1+**) wires **RTX OmniLidar** through OmniGraph helpers such as `add_rtx_lidar_subgraph` in `pegasus.simulator.ogn.api.spawn_rtx_lidar` (used from `simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py`, `example_multi_px4_pegasus_launch_script.py`, etc.). The lidar publishes the raw cloud on `…/sensors/ouster/point_cloud_raw`; consumers read the filtered topic `…/sensors/ouster/point_cloud`. ### `min_range` → `nearRangeM` in simulation The spawn code maps the Python argument **`min_range`** to the OmniLidar prim attribute **`omni:sensor:Core:nearRangeM`** when it exists, and logs a warning when it does not. The module docstring in `spawn_rtx_lidar.py` states the reality: some Kit builds only express echo spacing in the **vendor lidar JSON profile**, not as a writable **Core** prim attribute, so **setting near range in Isaac does not consistently remove short-range returns**. -**Known bug / policy:** Do **not** rely on Isaac-only `min_range` / `nearRangeM` as your primary near-field cleanup. Use the **robot-side** package **`lidar_point_cloud_filter`** (see [Sensors — LiDAR filter](../../../robot/autonomy/sensors/index.md#lidar-point-cloud-filter)), which applies a configurable **`near_range_m`** sphere filter in the ROS graph and publishes a stable cloud for VDB, exploration, and RViz. +**Known bug / policy:** Do **not** rely on Isaac-only `min_range` / `nearRangeM` as your primary near-field cleanup. Use the **robot-side** package **`lidar_point_cloud_filter`** (see [Sensors — LiDAR filter](../../robot/autonomy/sensors/index.md#lidar-point-cloud-filter)), which applies a configurable **`near_range_m`** sphere filter in the ROS graph and publishes a stable cloud for VDB, exploration, and RViz. ## Known bugs and workarounds for Scripted Scene Generation diff --git a/docs/simulation/isaac_sim/scene_setup.md b/docs/simulation/isaac_sim/scene_setup.md deleted file mode 100644 index 5c3771a45..000000000 --- a/docs/simulation/isaac_sim/scene_setup.md +++ /dev/null @@ -1,52 +0,0 @@ -# AirStack Scene Setup - -## Creating a New Scene with Robots -The easiest way to create a scene is to copy and customize an existing scene. - -Example scenes are located on the AirLab Nucleus Server under [Projects > AirStack](https://airlab-nucleus.andrew.cmu.edu/omni/web3/omniverse://airlab-nucleus.andrew.cmu.edu/Projects/AirStack/). -This can be opened in Isaac's Content Browser: -![Image of content browser](content_browser.png) - -For example, `simple_tree_one_drone.scene.usd` looks like this: -![scene setup](scene_setup.png) - -The example scenes are setup with the following: - -- A "World" prim, which is the root of the scene -- The Root layer is set to use meters as the unit of length -- Prims that make up the scene. Scene prims should have collision physics enabled with Colliders Preset (Property > Add > Physics > Collider Preset) -- Robot instances, added to the scene as a reference to the robot USD file. Currently this file is [Library > Assets > Ascent_Aerosystems > TEMPLATE_spirit_uav.robot.usd](https://airlab-nucleus.andrew.cmu.edu/omni/web3/omniverse://airlab-nucleus.andrew.cmu.edu/Library/Assets/Ascent_Aerosystems/Spirit_UAV/TEMPLATE_spirit_uav.robot.usd) - - The robot has default sensors added, including a LiDAR and stereo cameras - - Sensors publish to ROS using the attached ActionGraph - - Robot dynamics are controlled by the [AirStack Extension](ascent_sitl_extension.md) - - -### Configure Robot Name and ROS_DOMAIN_ID - -Under the Spirit drone prim is an `ActionGraph` component, which is an [Omnigraph](https://docs.omniverse.nvidia.com/extensions/latest/ext_omnigraph.html). This component is used to configure the ROS publishers for the robot. The `ActionGraph` component has the following fields to configure: - -- `ROBOT_NAME`: The name of the robot. This is used as the top-level namespace for ROS topics. -- `ROS_DOMAIN_ID`: The ROS domain ID. This sets the `ROS_DOMAIN_ID` environment variable for DDS networking. - -The Omnigraph has subgraphs for each ROS publisher type. For example, TFs, Images, and PointClouds. The top-level `robot_name` and `domain_id` fields get fed into the subgraphs. - -To create a new robot, duplicate the drone prim instance and adjust the `ROBOT_NAME` and `ROS_DOMAIN_ID` fields to be unique. - - - -### Customizing the Omnigraph - -Common pre-built graphs for ROS may be added through the top menu bar: `Isaac Utils > Common OmniGraphs`. -This is helpful for creating various sensor publishers. - -We recommend organizing your work into sub-graphs. -Copy your omnigraph template them into the top-level `Omnigraph` component, named "ActionGraph". Connect the `robot_name` and `domain_id` fields to your workflow. Then, select all the nodes in your workflow, right-click, and create a subgraph. - - -## Frame Conventions -Isaac Sim uses Forward-Left-Up (FLU) coordinate frame conventions. However, MAVROS and AirStack use East-North-Up (ENU). - -To address this, the origin of the robot lives under a prim called `map_FLU`. Then AirStack publishes a static transform (`static_transforms.launch.xml`) from `map_FLU` to `map`, which is in ENU. The transform is a 90 degree rotation about the Z-axis. - -The resulting TF tree looks like this: -![Image of tf tree](tf_tree.png) \ No newline at end of file diff --git a/docs/simulation/isaac_sim/scene_setup.png b/docs/simulation/isaac_sim/scene_setup.png deleted file mode 100644 index 5bee41a7b..000000000 Binary files a/docs/simulation/isaac_sim/scene_setup.png and /dev/null differ diff --git a/docs/simulation/isaac_sim/spawning_drones.md b/docs/simulation/isaac_sim/spawning_drones.md index 4a5930016..e23d58c10 100644 --- a/docs/simulation/isaac_sim/spawning_drones.md +++ b/docs/simulation/isaac_sim/spawning_drones.md @@ -7,8 +7,8 @@ All launch scripts under `simulation/isaac-sim/launch_scripts/` are thin scenari | `barebones_pegasus_launch.py` | Smallest possible scenario: an environment, no drones. Copy this as the template for new launch scripts. | | `example_one_px4_pegasus_launch_script.py` | One PX4 drone with the standard sensor stack (ZED stereo + Ouster lidar) in the default environment. | | `example_multi_px4_pegasus_launch_script.py` | `NUM_ROBOTS` drones spawned in a row (`row_spawn_configs`). Each drone gets its own ROS domain id (`1..N`). Lidar gated on `ENABLE_LIDAR`. | -| `example_one/multi_px4_pegasus_natnet_launch_script.py` | Same, plus an OptiTrack NatNet mocap server authored in a `post_spawn` hook. | | `example_multi_drone_scene_import.py` | Explicit `DRONE_CONFIGS` in an **imported scene** (USD from a Nucleus server) with per-drone GPS homes. Use this as the starting point for any custom scene. | +| `fleet_spawn.py` | Generic fleet spawner: spawn positions, per-robot sensor toggles, and the scene come from the fleet file named by `FLEET_CONFIG_FILE`. Selected automatically by `airstack up --fleet ` — not meant to be copied as a template. | ## Writing a launch script with `PegasusApp` @@ -44,7 +44,7 @@ Key constructor kwargs (see the docstrings in `pegasus_app.py` for the full list | `stage_scale` | Uniform scale applied to `/World/stage` (`0.01` for cm-authored assets). | | `enable_camera` / `enable_lidar` + offsets | Standard ZED stereo + RTX lidar sensor stack per drone. | | `dome_light` | `True` (defaults), `False`, or a kwargs dict for `add_dome_light`. | -| `world_gps_origin` | If set, calls `gps_utils.set_gps_origins(drone_configs, world_origin=…)` before PX4 boots. | +| `world_gps_origin` | If set, calls `gps_utils.set_gps_origins(drone_configs, world_origin=…)` before PX4 boots. Default `None`: multi-drone spawns (more than one config) auto-anchor at `gps_utils.DEFAULT_WORLD_ORIGIN`; single-drone spawns keep the PX4/Pegasus default home. | | `scale_spawn_positions` | Convert spawn meters into stage units (imported non-metric scenes). | | `save_scene_to` | Export the prepared scene as a self-contained USD package. | @@ -52,9 +52,9 @@ For anything beyond declarations, subclass and override the hooks — each recei - `pre_scene_prep(stage)` — after the environment loads, before scale/colliders (e.g. `dedupe_physics_scenes`, `reference_root_prims_under_world`) - `post_scene_prep(stage)` — after stage prep, before drones spawn (e.g. the overhead map camera) -- `post_spawn(stage)` — after all drones spawn (e.g. authoring the NatNet mocap interface) +- `post_spawn(stage)` — after all drones spawn (e.g. authoring extra scene-level prims such as a mocap interface — the [asm_optitrack module](https://github.com/castacks/asm_optitrack)'s launch scripts use this hook) -`example_multi_drone_scene_import.py` (hooks, Nucleus scene, explicit poses) and the `_natnet_` scripts (`post_spawn`) are the reference subclasses. +`example_multi_drone_scene_import.py` (hooks, Nucleus scene, explicit poses) is the reference subclass. To run your script: put it in `simulation/isaac-sim/launch_scripts/`, then `ISAAC_SIM_SCRIPT_NAME=my_script.py airstack up --sim isaac` (see [Docker](docker.md)). @@ -74,14 +74,14 @@ DRONE_CONFIGS = [ | `domain_id` | ROS domain id and (by default) PX4 vehicle id — MAVLink port is `14540 + vehicle_id`. The robot container with `ROS_DOMAIN_ID=1` will see this drone. | | `x_m`, `y_m`, `z_m` | World-frame spawn position in meters. Convention: `+X = East`, `+Y = North`, `+Z = Up`. | | `orient` | Spawn orientation quaternion `[x, y, z, w]` (default identity). | -| `prim`, `node_name` | Override the drone's root prim / OmniGraph node name (single-drone scenes use the historical `/World/base_link` / `PX4Multirotor`). | -| `lidar`, `lidar_min_range`, `camera_offset` | Per-drone sensor overrides of the app-level settings. | +| `prim`, `node_name` | Override the drone's root prim / OmniGraph node name (single-drone scenes default to `/World/base_link` / `PX4Multirotor`). | +| `camera`, `lidar`, `lidar_min_range`, `camera_offset` | Per-drone sensor overrides of the app-level settings. | To add another drone, append an entry with a fresh `domain_id` and a non-overlapping spawn position, and launch the matching number of robot containers (`airstack up --sim isaac --robots N` keeps `NUM_ROBOTS` and the launch script consistent). ## Per-drone GPS home — `gps_utils` -PX4 needs a GPS home per vehicle. `simulation/isaac-sim/launch_scripts/gps_utils.py` derives one from each drone's world-frame spawn position so all drones share a consistent geographic anchor and end up at distinct GPS coordinates spaced according to their spawn offsets. Pass `world_gps_origin=` to `PegasusApp` (as `example_multi_drone_scene_import.py` does) and the base class makes this call for you before PX4 boots; the underlying helper is: +PX4 needs a GPS home per vehicle. `simulation/isaac-sim/launch_scripts/gps_utils.py` derives one from each drone's world-frame spawn position so all drones share a consistent geographic anchor and end up at distinct GPS coordinates spaced according to their spawn offsets. `PegasusApp` makes this call for you before PX4 boots: whenever a scene spawns **more than one** drone it anchors at `gps_utils.DEFAULT_WORLD_ORIGIN` automatically, and passing `world_gps_origin=` (as `example_multi_drone_scene_import.py` does) overrides the anchor. Single-drone scenes only get per-drone GPS homes if you pass `world_gps_origin=` explicitly — otherwise they keep the PX4/Pegasus default home. The underlying helper is: ```python from gps_utils import set_gps_origins, DEFAULT_WORLD_ORIGIN @@ -104,7 +104,7 @@ set_gps_origins(DRONE_CONFIGS, world_origin=(40.4433, -79.9436, 280.0)) # Pitts The anchor only affects the geographic location reported via GPS; nothing in the scene moves. Pick something close to where you want the drones to "be" — Foxglove's Map panel will center on it, and any GPS-referenced inputs to your stack will be relative to it. -## Scene prep helpers — `scene_prep.py` +## Scene prep helpers — `scene_prep.py` {#scene-prep-helpers} `simulation/isaac-sim/utils/scene_prep.py` is the small toolbox of stage preparation helpers `example_multi_drone_scene_import.py` uses inside its post-load callback (after the stage is loaded, before drones spawn). The full file has more — what's documented here is what you'll reach for in 95% of scenes. @@ -204,7 +204,7 @@ The collected folder contains a standalone root USD with relative references — | All drones share one GPS coordinate | `domain_id` collision in `DRONE_CONFIGS` | Give each drone a unique `domain_id` | | Map panel centers on the wrong city | Wrong `world_origin` | Override the second arg to `set_gps_origins` | | Drone position drifts in the wrong compass direction | Stage axis mismatch | Swap `x_m` ↔ `y_m` in `gps_utils.compute_gps_origin` | -| Robot container can't see the drone's topics | `ROS_DOMAIN_ID` ≠ `domain_id` in DRONE_CONFIGS | Match them, or set `NUM_ROBOTS` correctly | +| Robot container can't see the drone's topics | `ROS_DOMAIN_ID` ≠ `domain_id` in DRONE_CONFIGS | Match them, or pass the right `--robots` count | ## See also diff --git a/docs/simulation/isaac_sim/tf_tree.png b/docs/simulation/isaac_sim/tf_tree.png deleted file mode 100644 index 3be9699d4..000000000 Binary files a/docs/simulation/isaac_sim/tf_tree.png and /dev/null differ diff --git a/docs/simulation/isaac_sim/tmux_panel.png b/docs/simulation/isaac_sim/tmux_panel.png deleted file mode 100644 index 3550fd7aa..000000000 Binary files a/docs/simulation/isaac_sim/tmux_panel.png and /dev/null differ diff --git a/docs/simulation/ms-airsim/docker.md b/docs/simulation/ms-airsim/docker.md index 16a515663..118cf83d8 100644 --- a/docs/simulation/ms-airsim/docker.md +++ b/docs/simulation/ms-airsim/docker.md @@ -39,29 +39,28 @@ The Microsoft AirSim (legacy) service is defined in `simulation/ms-airsim/docker ### Starting Microsoft AirSim (legacy) -Microsoft AirSim (legacy) is gated behind a Docker Compose profile: +For the user-facing launch path (scene selection + `airstack up --sim airsim`), see the [Quick Start in the overview](index.md#quick-start). At the container level, the service is gated behind a Docker Compose profile: ```bash -# Start alongside the robot stack +# Explicit-profile form (equivalent to `airstack up --sim airsim`) airstack up --profile ms-airsim --profile desktop - -# Build the image first -airstack image-build --profile ms-airsim ``` Alternatively, set `COMPOSE_PROFILES=ms-airsim,desktop` in `.env` and run `airstack up`. ### What happens on startup -The container runs `entrypoint.sh`, which: +The container runs `entrypoint.sh` (`simulation/ms-airsim/docker/entrypoint.sh`), which: 1. Generates `settings.json` from the Jinja2 template using current environment variables -2. Creates a tmux session named `ms-airsim` -3. Builds the ROS 2 bridge workspace (`colcon build`) -4. In the `airsim` window: if `MS_AIRSIM_BINARY_PATH` is unset, runs `fetch_scene.sh blocks` to download + extract the default scene, then launches the UE4 binary as the `ms-airsim` user (UE4 refuses to run as root) -5. Launches one bridge node per robot, each with `ROS_DOMAIN_ID=` -6. Waits for the AirSim API to become available (TCP port 41451) -7. Spawns one PX4 SITL instance per robot, each in its own tmux window +2. Creates a tmux session named `ms-airsim` with a first window named `airsim` +3. Resolves the scene: an explicit `MS_AIRSIM_BINARY_PATH` wins (and must exist); otherwise `MS_AIRSIM_SCENE` (default `blocks`) selects a `fetch_scene.sh` key +4. Builds the ROS 2 bridge workspace (`colcon build`) +5. In the `airsim` window: auto-fetches the selected scene if it isn't downloaded yet (so progress is visible), then launches the UE4 binary as the `ms-airsim` user (UE4 refuses to run as root) +6. Creates one bridge window per robot (`robot__bridge`), each running the bridge node with `ROS_DOMAIN_ID=` +7. Waits for the AirSim API to become available (TCP port 41451) +8. Sleeps `MS_AIRSIM_PX4_START_DELAY` seconds (default 3) so AirSim sensors settle before PX4's EKF snapshots a local origin +9. Creates one PX4 SITL window per robot (`robot__px4`), each running `px4 ... -i ` ## Environment Variables @@ -71,34 +70,24 @@ The container runs `entrypoint.sh`, which: | `MS_AIRSIM_BINARY_PATH` | _(unset → auto-fetch Blocks)_ | Path to UE4 binary inside container. If unset, the entrypoint fetches Blocks into the mounted scenes dir and points at it. | | `MS_AIRSIM_ENV_DIR` | `../assets/scenes` | Host path to extracted UE4 scenes | | `MS_AIRSIM_HEADLESS` | `false` | Run UE4 without a window (`-RenderOffScreen -nosound`) | +| `MS_AIRSIM_SCENE` | _(empty → `blocks`)_ | Scene shortname (a `fetch_scene.sh` key, set by `airstack up --scene `); ignored when `MS_AIRSIM_BINARY_PATH` is set | | `MS_AIRSIM_PX4_START_DELAY` | `3` | Seconds to wait after AirSim becomes ready before starting PX4, so sensors settle before the EKF snapshots a local origin | | `NUM_ROBOTS` | `1` | Number of vehicles and PX4 SITL instances | | `SIM_IP` | `172.31.0.200` | Simulator IP on `airstack_network` | -**Camera template variables** (override in `.env` to regenerate `settings.json`): - -| Variable | Default | Description | -|----------|---------|-------------| -| `AIRSIM_CAM_WIDTH` | `480` | Camera image width (px) | -| `AIRSIM_CAM_HEIGHT` | `300` | Camera image height (px) | -| `AIRSIM_CAM_FOV` | `90` | Camera horizontal FOV (degrees) | -| `AIRSIM_CAM_X` | `0.4` | Camera X offset from body center (m) | -| `AIRSIM_CAM_Y` | `0.06` | Camera Y half-baseline (m) | -| `AIRSIM_CAM_Z` | `0.0` | Camera Z offset from body center (m) | -| `AIRSIM_CAM_PITCH` | `0.0` | Camera pitch angle (degrees) | -| `AIRSIM_SPAWN_SPACING` | `3.0` | Y-axis spacing between robots (m) | +The camera template variables (`AIRSIM_CAM_*`) and their defaults are documented in the [camera configuration reference](index.md#cameras); `AIRSIM_SPAWN_SPACING` (default `3.0`) sets the Y-axis spacing between spawned robots in meters. **Example overrides:** ```bash # Two robots -NUM_ROBOTS=2 airstack up --profile ms-airsim +airstack up --sim airsim --robots 2 # Headless (no GUI, uses UE4's -RenderOffScreen) -MS_AIRSIM_HEADLESS=true airstack up --profile ms-airsim +airstack up --sim airsim --headless -# Custom scene binary -MS_AIRSIM_ENV_DIR=/data/airsim_envs MS_AIRSIM_BINARY_PATH=/ms-airsim-env/CityEnviron/LinuxNoEditor/CityEnviron.sh airstack up --profile ms-airsim +# Custom scene binary (no flag equivalent) +MS_AIRSIM_ENV_DIR=/data/airsim_envs MS_AIRSIM_BINARY_PATH=/ms-airsim-env/CityEnviron/LinuxNoEditor/CityEnviron.sh airstack up --sim airsim ``` ## Settings Generation @@ -131,9 +120,9 @@ NUM_ROBOTS=2 python3 generate_settings.py | Port | Protocol | Purpose | |------|----------|---------| | 41451 | TCP | AirSim Python API | -| 4561–456N | TCP | PX4 lockstep (one per robot, N = robot index) | -| 24541–2454N | UDP | MAVLink offboard (one per robot) | -| 24581–2458N | UDP | MAVLink onboard (one per robot) | +| `4560 + i` | TCP | PX4 lockstep (`TcpPort`, one per robot `i` = 1..N) | +| `24540 + i` | UDP | AirSim MAVLink control channel, local (`ControlPortLocal`, one per robot) | +| `24580 + i` | UDP | AirSim MAVLink control channel, remote (`ControlPortRemote`, one per robot) | ## GPU Access @@ -210,13 +199,13 @@ airstack connect ms-airsim tmux a -t ms-airsim ``` -**Tmux windows layout:** +**Tmux windows layout** (`1 + 2*NUM_ROBOTS` windows, in creation order — see [What happens on startup](#what-happens-on-startup)): -| Window | Contents | -|--------|---------| -| 0 | AirSim UE4 binary | -| 1..N | PX4 SITL instance for robot 1..N | -| N+1..2N | ROS 2 bridge node for robot 1..N | +| Window | Name | Contents | +|--------|------|----------| +| 0 | `airsim` | Scene fetch (if needed) + AirSim UE4 binary | +| 1..N | `robot__bridge` | ROS 2 bridge node for robot `i` = 1..N | +| N+1..2N | `robot__px4` | PX4 SITL instance for robot `i` = 1..N (created only after the AirSim API is ready + `MS_AIRSIM_PX4_START_DELAY`) | **Useful tmux commands:** @@ -232,7 +221,8 @@ airstack logs ms-airsim ## Multi-Robot Support -Set `NUM_ROBOTS` to spawn multiple vehicles. Each robot gets: +Use `airstack up --robots N` (which sets `NUM_ROBOTS`) to spawn multiple +vehicles. Each robot gets: - A named vehicle in `settings.json` (`robot_1`, `robot_2`, …) - Its own PX4 SITL instance with unique ports @@ -241,7 +231,7 @@ Set `NUM_ROBOTS` to spawn multiple vehicles. Each robot gets: ```bash # Launch with 3 robots -NUM_ROBOTS=3 airstack up --profile ms-airsim --profile desktop +airstack up --sim airsim --robots 3 ``` ## Image Management @@ -260,7 +250,7 @@ docker compose -f simulation/ms-airsim/docker/docker-compose.yaml pull ```bash # Build image -airstack image-build --profile ms-airsim +airstack images build --profile ms-airsim # Or directly docker compose -f simulation/ms-airsim/docker/docker-compose.yaml build @@ -285,7 +275,7 @@ docker exec ms-airsim bash -c "cd /root/ros_ws && colcon build --symlink-install Change camera or vehicle parameters via environment variables and restart the container — `settings.json` is regenerated each time. ```bash -AIRSIM_CAM_FOV=120 airstack up --profile ms-airsim +AIRSIM_CAM_FOV=120 airstack up --sim airsim ``` ## Troubleshooting @@ -297,39 +287,23 @@ AIRSIM_CAM_FOV=120 airstack up --profile ms-airsim - Verify `DISPLAY` is set and X11 socket is mounted: `echo $DISPLAY`, `xhost +local:docker` - Check disk space: pre-built environments are 3–10 GB -**Bridge can't connect to AirSim API:** - -- Ensure AirSim binary started successfully (check tmux window 0) -- The entrypoint retries until the API is ready; check for connection errors in the container logs -- Verify `ms_airsim_ip` in `bridge.yaml` matches where AirSim is running (default: `127.0.0.1` — same container) - **PX4 SITL won't connect:** -- Confirm `settings.json` was generated with correct `TcpPort` values +- Confirm `settings.json` was generated with correct `TcpPort` values (`4560 + i`) - Check AirSim console for "Waiting for TCP connection" messages - Verify the PX4 lockstep port is not blocked by a firewall -**MAVROS won't connect (robot container):** - -- Verify `SIM_IP=172.31.0.200` is set in `.env` -- Ensure PX4 SITL has started (look for `[mavlink]` output in the PX4 tmux window) -- Check MAVLink ports match: offboard `24541+i`, onboard `24581+i` - -**No depth images on ROS 2 topics:** - -- Verify the camera names in `settings.json` match those in `bridge.yaml` -- Check bridge node output for connection errors (tmux bridge window) -- Echo the topic: `ros2 topic echo /robot_1/sensors/front_stereo/depth --once` - **ROS 2 topics not visible from robot container:** - Confirm both containers are on `airstack_network`: `docker network inspect airstack_network` - Check `ROS_DOMAIN_ID` is consistent between containers - Verify DDS multicast is working: `ros2 topic list` from inside each container +For user-facing issues (bridge can't connect to AirSim, no depth images, MAVROS won't connect), see the [overview → Troubleshooting](index.md#troubleshooting). + ## See Also -- [Microsoft AirSim (legacy) Overview](index.md) — capabilities, configuration, and architecture +- [Microsoft AirSim (legacy) Overview](index.md) — quick start, settings/camera/bridge configuration, published topics - [Simulation Overview](../index.md) — Choosing between simulators - [Isaac Sim Docker](../isaac_sim/docker.md) — Isaac Sim container reference - [Docker Workflow](../../development/beginner/airstack-cli/docker_usage.md) — General Docker operations diff --git a/docs/simulation/ms-airsim/index.md b/docs/simulation/ms-airsim/index.md index d0806f03d..5fca23a31 100644 --- a/docs/simulation/ms-airsim/index.md +++ b/docs/simulation/ms-airsim/index.md @@ -2,6 +2,11 @@ [Microsoft AirSim (legacy)](https://microsoft.github.io/AirSim/) is an open-source simulator for drones built on Unreal Engine, with built-in PX4 SITL integration. + +*The AirStack autonomy stack flying `TakeoffTask` + `FixedTrajectoryTask` patterns in the AirSimNH neighborhood scene (`airstack up --sim airsim --robots 2 --scene neighborhood`).* + ## Overview Microsoft AirSim (legacy) provides an alternative simulation backend for AirStack, offering: @@ -18,19 +23,29 @@ Microsoft AirSim (legacy) provides an alternative simulation backend for AirStac - Archived project (no new features, but stable) - UE 4.27 only (older engine) +## Project status + +Microsoft archived AirSim, which is why AirStack labels it "legacy": it remains a stable, supported simulation backend here, but the upstream project receives no new features. For a maintained successor, see [Project AirSim](https://github.com/iamaisim/ProjectAirSim) (UE5, new API). + ## Quick Start ### 1. Scene (auto-fetched on first launch) -If `MS_AIRSIM_BINARY_PATH` is unset, the container's entrypoint auto-downloads the Blocks scene (~200 MB) into `simulation/ms-airsim/assets/scenes/Blocks/` inside the `airsim` tmux window on first launch. Progress and any errors are visible there. +If `MS_AIRSIM_BINARY_PATH` is unset, the container's entrypoint auto-downloads the selected scene (Blocks by default, ~200 MB) into `simulation/ms-airsim/assets/scenes/` inside the `airsim` tmux window on first launch. Progress and any errors are visible there. + +The easiest way to pick a scene is the launch flag — it maps a shortname to the right UE4 binary and, when the scene isn't downloaded yet, asks before fetching it (see [Simulation Scenes](../scenes.md) for the full catalog): + +```bash +airstack up --sim airsim --scene neighborhood +``` To pre-fetch (e.g. before CI) or pick a different scene, run the helper directly: ```bash ./simulation/ms-airsim/assets/scenes/fetch_scene.sh # blocks (default) -./simulation/ms-airsim/assets/scenes/fetch_scene.sh airsimnh # or: abandonedpark, forest, - # landscapemountains, soccerfield, - # building99, zhangjiajie +./simulation/ms-airsim/assets/scenes/fetch_scene.sh airsimnh # or: abandonedpark, + # landscapemountains, zhangjiajie, + # africasavannah, msbuild2018 ``` To use a scene that isn't one of the presets, extract it yourself into `simulation/ms-airsim/assets/scenes/` and set `MS_AIRSIM_BINARY_PATH` to its `.sh` path inside the container. @@ -40,31 +55,19 @@ Scenes are pulled from the [AirSim Linux releases](https://github.com/microsoft/ ### 2. Launch Microsoft AirSim (legacy) + Robot ```bash -airstack up --env-file overrides/ms-airsim.env +airstack up --sim airsim ``` -To build the images first: - -```bash -airstack image-build --profile ms-airsim -``` +(Equivalently: `airstack up --env-file overrides/ms-airsim.env`, which sets the same compose profiles and URDF.) -The container runs `1 + 2*NUM_ROBOTS` tmux windows: -- **Window 0**: AirSim binary (Unreal Engine rendering) -- **Windows 1..N**: one PX4 SITL instance per robot -- **Windows N+1..2N**: one ROS 2 bridge node per robot (depth + stereo RGB + camera_info) +To build or pull the images first, see [Docker reference → Image Management](docker.md#image-management). -To attach to the tmux session: +To attach to the container's tmux session (window layout and startup sequence are detailed in the [Docker reference](docker.md#accessing-the-container)): ```bash airstack connect ms-airsim ``` -A video is below: - - - - ## Architecture ``` @@ -107,10 +110,10 @@ Key settings: | `ClockType` | `SteppableClock` | Lockstep with PX4 | | `VehicleType` | `PX4Multirotor` | PX4 SITL vehicle | | `TcpPort` | `4560 + i` | PX4 lockstep connection (per robot `i`) | -| `ControlPortLocal` | `24540 + i` | AirSim MAVLink proxy local port (moved off `14540+i` so it doesn't intercept PX4 ↔ MAVROS traffic) | +| `ControlPortLocal` | `24540 + i` | AirSim MAVLink proxy local port (deliberately offset from `14540+i` so the proxy doesn't intercept PX4 ↔ MAVROS traffic) | | `ControlPortRemote` | `24580 + i` | AirSim MAVLink proxy remote port | -`settings.json` is generated at container start from [`settings.json.j2`](https://github.com/.../simulation/ms-airsim/config/settings.json.j2) via [`generate_settings.py`](https://github.com/.../simulation/ms-airsim/config/generate_settings.py), which expands per-robot port offsets, spawn positions, and camera parameters. +`settings.json` is generated at container start from [`settings.json.j2`](https://github.com/castacks/AirStack/blob/main/simulation/ms-airsim/config/settings.json.j2) via [`generate_settings.py`](https://github.com/castacks/AirStack/blob/main/simulation/ms-airsim/config/generate_settings.py), which expands per-robot port offsets, spawn positions, and camera parameters. ### Cameras @@ -122,13 +125,14 @@ The default configuration is a forward-facing **stereo pair** (left + right) plu | FOV | 90° | `AIRSIM_CAM_FOV` | | Baseline (2 × Y offset) | 0.12 m | `AIRSIM_CAM_Y` | | Forward (X) offset | 0.4 m | `AIRSIM_CAM_X` | +| Vertical (Z) offset | 0 m | `AIRSIM_CAM_Z` | | Pitch | 0° | `AIRSIM_CAM_PITCH` | -Cameras are defined per vehicle in the generated `settings.json` under `Vehicles.robot_.Cameras`. +Cameras are defined per vehicle in the generated `settings.json` under `Vehicles.robot_.Cameras`. Override the `.env` variables and restart the container to regenerate `settings.json` (see [Docker reference → Settings Generation](docker.md#settings-generation)). ### Bridge node parameters -Located at `simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/config/bridge.yaml`: +Declared (with these defaults) in `simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/ms_airsim_ros_bridge/bridge_node.py`; the entrypoint starts each bridge with `ros2 run ... --ros-args -p robot_name:=robot_`: | Parameter | Default | Description | |-----------|---------|-------------| @@ -139,11 +143,7 @@ Located at `simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/config/bridge.y ### Environment variables -| Variable | Default | Description | -|----------|---------|-------------| -| `SIM_IP` | `172.31.0.200` | Simulation container IP | -| `MS_AIRSIM_ENV_DIR` | `simulation/ms-airsim/assets/scenes` | Host path to extracted AirSim scenes | -| `MS_AIRSIM_BINARY_PATH` | _(unset → auto-fetch Blocks)_ | Path to binary inside container. If unset, the entrypoint fetches Blocks and points at it. | +Container-level environment variables (`AUTOLAUNCH`, `NUM_ROBOTS`, `SIM_IP`, `MS_AIRSIM_*`) are documented in the [Docker reference → Environment Variables](docker.md#environment-variables). ## Published ROS 2 Topics @@ -156,24 +156,30 @@ Located at `simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/config/bridge.y | `/{robot_name}/sensors/front_stereo/depth` | `sensor_msgs/Image` | Depth image (32FC1, meters) | | `/clock` | `rosgraph_msgs/Clock` | Simulation clock from AirSim | -### Project status - -Microsoft archived AirSim. For a maintained successor, see [Project AirSim](https://github.com/iamaisim/ProjectAirSim) (UE5, new API — integration planned as a future AirStack feature). - ## Troubleshooting **Bridge can't connect to Microsoft AirSim (legacy):** -- Ensure the AirSim binary is running and `settings.json` is loaded -- Check that `ms_airsim_ip` parameter matches where AirSim is running +- Ensure the AirSim binary is running (`airsim` tmux window) and `settings.json` is loaded +- The entrypoint retries until the AirSim API is ready; check for connection errors in the container logs +- Check that the bridge node's `ms_airsim_ip` parameter matches where AirSim is running (default: `127.0.0.1` — same container) **No depth images:** -- Verify the camera name in `settings.json` matches `bridge.yaml` +- Verify the camera names in the generated `settings.json` are `front_left` / `front_right` — the names the bridge node requests images by - Check AirSim console for rendering errors +- Echo the topic: `ros2 topic echo /robot_1/sensors/front_stereo/depth --once` **MAVROS won't connect:** - Verify `SIM_IP=172.31.0.200` is set in `.env` (default) -- Ensure PX4 SITL has started (check AirSim console for MAVLink messages) -- Check port configuration: offboard=24540+i, onboard=24580+i +- Ensure PX4 SITL has started (look for `[mavlink]` output in the `robot__px4` tmux window) +- Check port configuration: offboard `14540 + ROS_DOMAIN_ID` (see Data flow above); AirSim's own control channel uses `24540+i`/`24580+i` + +For container-level issues (UE4 binary won't launch, GPU/Vulkan access, PX4 lockstep connection, DDS topic visibility across containers), see the [Docker reference → Troubleshooting](docker.md#troubleshooting). + +## See Also + +- [Docker Configuration](docker.md) — container reference: services, env vars, networking, tmux layout, startup sequence +- [Simulation Scenes](../scenes.md) — scene catalog and fetch helper +- [Simulation Overview](../index.md) — choosing between simulators diff --git a/docs/simulation/scenes.md b/docs/simulation/scenes.md new file mode 100644 index 000000000..4cca0f56b --- /dev/null +++ b/docs/simulation/scenes.md @@ -0,0 +1,213 @@ +# Simulation Scenes + +Every simulator names its environments differently — Isaac Sim loads Pegasus +catalog entries or USD stages from an Omniverse Nucleus server, while +Microsoft AirSim (legacy) runs pre-built Unreal Engine binaries. So that +developers don't have to know each simulator's addressing scheme, AirStack +keeps one simulator-agnostic **scene catalog** and a single launch flag: +`airstack up --scene ` maps the shortname to whatever the selected +simulator understands. + +## Quick Example + +```bash +# Isaac Sim: fly in the construction site stage from the AirLab Nucleus server +airstack up --sim isaac --scene construction-site + +# Isaac Sim: an NVIDIA warehouse from the Pegasus catalog +airstack up --sim isaac --scene warehouse + +# Microsoft AirSim (legacy): the Blocks UE4 scene +airstack up --sim airsim --scene blocks +``` + +Passing an unknown scene — or one the selected simulator doesn't have — +prints a table of every available scene per simulator: + +```bash +airstack up --sim isaac --scene blocks +# ERROR: scene 'blocks' is not available for Isaac (only: MS AirSim). Available scenes: +# SCENE ISAAC MS AIRSIM +# abandoned-factory nucleus:AbandonedFactory.stage.usd - +# blocks - blocks +# ... +``` + +The same table is available any time via: + +```bash +python3 simulation/resolve_scene.py --table +``` + +## How It Works + +The catalog lives in [`simulation/scenes.yaml`](https://github.com/castacks/AirStack/blob/main/simulation/scenes.yaml). +Each shortname declares the scene reference per simulator, so the same name can +exist for both simulators without ambiguity — resolution always follows the +simulator selected by `--sim` (or the active compose profile): + +```yaml +scenes: + construction-site: + isaac: + ref: omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/ConstructionSite/ConstructionSite.stage.usd + stage_scale: 0.01 # this stage is authored in centimeters + warehouse: + isaac: Warehouse # bare string = Pegasus catalog key (or a USD URL) + blocks: + msairsim: blocks # fetch_scene.sh key (pre-built UE4 binary) +``` + +`airstack up --scene` resolves the shortname host-side +(`simulation/resolve_scene.py`) and exports plain environment variables; the +simulator containers do the final loading: + +```mermaid +flowchart LR + A["airstack up --sim isaac --scene construction-site"] --> B["resolve_scene.py
reads simulation/scenes.yaml"] + B -->|Isaac| C["ISAAC_SIM_SCENE
ISAAC_SIM_STAGE_SCALE"] + B -->|MS AirSim| D["MS_AIRSIM_SCENE"] + C --> E["Pegasus launch script
loads catalog key or USD URL"] + D --> F["ms-airsim entrypoint
fetches + runs the UE4 binary"] +``` + +The exported variables are ordinary launch config — they appear in the +effective launch config printout and can also be set directly (in `.env`, an +`--env-file`, or the shell) without using `--scene` at all: + +```bash +# Bypass the catalog entirely: any Nucleus/HTTP USD URL works +ISAAC_SIM_SCENE="omniverse://my-server/My/Stage.usd" airstack up --sim isaac +``` + +| Variable | Simulator | Meaning | +|----------|-----------|---------| +| `ISAAC_SIM_SCENE` | Isaac | A Pegasus `SIMULATION_ENVIRONMENTS` key (e.g. `Warehouse`) or a USD reference (`omniverse://`/`https://` URL or `*.usd` path). Empty = the launch script's default (`Default Environment`). | +| `ISAAC_SIM_STAGE_SCALE` | Isaac | Scale applied to the loaded `/World/stage` prim. `0.01` converts centimeter-authored stages to meters; default `1.0`. | +| `MS_AIRSIM_SCENE` | MS AirSim | A `fetch_scene.sh` catalog key (e.g. `blocks`, `airsimnh`). Empty = `blocks`. Ignored when `MS_AIRSIM_BINARY_PATH` is set. | + +Fleets can pin a scene in their fleet file (`sim.scene`); a `--scene` flag on +the command line overrides it, with a log line noting the override. + +## Scene Catalog + +### Isaac Sim — Pegasus catalog + +These map to Pegasus Simulator's built-in `SIMULATION_ENVIRONMENTS` (NVIDIA +Isaac assets, streamed from the Nucleus mount configured in `omni_pass.env`): + +| Shortname | Pegasus environment | +|-----------|---------------------| +| `default` | Default Environment | +| `black-gridroom`, `curved-gridroom` | Black / Curved Gridroom | +| `hospital`, `office`, `simple-room` | Hospital, Office, Simple Room | +| `warehouse`, `warehouse-forklifts`, `warehouse-shelves`, `full-warehouse` | Warehouse variants | +| `flat-plane`, `rough-plane`, `slope-plane`, `stairs-plane` | Terrain planes | +| `exhibition-hall` | Exhibition Hall (NVIDIA cloud asset) | + +### Isaac Sim — AirLab public stages + +Full environments exported from Unreal Engine, hosted on the AirLab Nucleus +server under a **publicly readable** folder: + +``` +omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/ +``` + +Anyone can browse and load these with the guest account (username `guest`, +password `guest`) — the default `omni_pass.env` credentials work out of the +box. Log in at to browse +from a web browser. + +| Shortname | Stage USD | Size | Units | +|-----------|-----------|------|-------| +| `abandoned-factory` | `AbandonedFactory/AbandonedFactory.stage.usd` | 38 MB | m | +| `abandoned-warehouse-night` | `AbandonedWarehouse/Warehouse_01_night.stage.usd` | 7 MB | cm | +| `abandoned-warehouse-day` | `AbandonedWarehouse/Warehouse_02_day.stage.usd` | 4 MB | cm | +| `chemical-plant` | `ChemicalPlant/Map_ChemicalPlant_2.stage.usd` | 549 MB | m | +| `construction-site` | `ConstructionSite/ConstructionSite.stage.usd` | 44 MB | cm | +| `retro-neighborhood` | `RetroNeighborhood/RetroNeighborhood.stage.usd` | 193 KB (+props) | cm | + +!!! note "Units and `stage_scale`" + Each catalog entry's `stage_scale` mirrors the stage's authored + `metersPerUnit` (`0.01` for centimeter-authored stages, `1.0` for + meter-authored ones), so scenes load at real-world scale without manual + tuning. When loading a stage by raw URL instead of shortname, set + `ISAAC_SIM_STAGE_SCALE` yourself. + +!!! note "Internet access for skies" + These stages reference dome-light skies on NVIDIA's public S3 bucket + (`omniverse-content-production.s3.us-west-2.amazonaws.com`). All geometry + and materials are self-contained on the AirLab server; only the sky needs + general internet access. On an air-gapped machine the stage still loads, + minus the sky. + +### Microsoft AirSim (legacy) — pre-built UE4 scenes + +These map to `fetch_scene.sh` keys; the pre-built binaries come from the +[AirSim v1.8.1 release](https://github.com/microsoft/AirSim/releases/tag/v1.8.1): + +| Shortname | UE4 scene | Download size | +|-----------|-----------|---------------| +| `blocks` (also `default`) | Blocks | 135 MB | +| `neighborhood` | AirSimNH | 2.0 GB | +| `abandoned-park` | AbandonedPark | 1.6 GB | +| `landscape-mountains` | LandscapeMountains | 1.1 GB | +| `zhangjiajie` | ZhangJiajie | 840 MB | +| `africa-savannah` | Africa_Savannah | 1.1 GB | +| `msbuild2018` | MSBuild2018 | 754 MB | + +Scenes download on first use into `simulation/ms-airsim/assets/scenes/` +(bind-mounted into the container). When `--scene` selects a scene that isn't +present locally, an interactive `airstack up` asks before downloading (the +binaries are large — hundreds of MB to several GB); a non-interactive run +proceeds and the container auto-fetches it inside the `airsim` tmux window. +You can also pre-fetch manually: + +```bash +./simulation/ms-airsim/assets/scenes/fetch_scene.sh blocks +``` + +An explicit `MS_AIRSIM_BINARY_PATH` (pointing at any extracted UE4 binary) +always wins over `--scene`. + +## Using a Stage Directly in a Launch Script + +The example Pegasus launch scripts resolve their scene from the environment, +so `--scene` needs no code changes. A custom launch script can do the same, or +pass any USD reference verbatim: + +```python +from pegasus.simulator.params import SIMULATION_ENVIRONMENTS +from pegasus_app import PegasusApp, resolve_scene_from_env + +# Honor `airstack up --scene` / ISAAC_SIM_SCENE, with a custom default: +env_url, stage_scale = resolve_scene_from_env( + SIMULATION_ENVIRONMENTS, default_key="Warehouse") + +# ...or hardcode a public stage by URL: +PegasusApp( + env_url="omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/ConstructionSite/ConstructionSite.stage.usd", + stage_scale=0.01, # match the stage's metersPerUnit + ... +) +``` + +## Adding a Scene to the Catalog + +1. For Isaac: host the stage somewhere reachable by the container's Nucleus + credentials — for team-wide scenes, a folder under + `Public/AirStack/Stages/` keeps it guest-accessible. See + [Export Stages from Unreal](isaac_sim/export_stages_from_unreal.md) for + producing the USD. +2. Add an entry to `simulation/scenes.yaml`. Set `stage_scale` to the stage's + `metersPerUnit` (`0.01` if authored in centimeters). +3. Verify: `python3 simulation/resolve_scene.py --sim isaac --scene `, + then `airstack up --dry-run --sim isaac --scene `. + +For MS AirSim, the catalog can only reference scenes `fetch_scene.sh` knows +how to download (the v1.8.1 release binaries); add the key to both files. + +**Learn more:** [Pegasus scene setup](isaac_sim/pegasus_scene_setup.md) · +[Spawning drones](isaac_sim/spawning_drones.md) · +[Microsoft AirSim setup](ms-airsim/index.md) diff --git a/docs/simulation/simple_sim/docker.md b/docs/simulation/simple_sim/docker.md index 389692ca2..721c27ed4 100644 --- a/docs/simulation/simple_sim/docker.md +++ b/docs/simulation/simple_sim/docker.md @@ -1,368 +1,75 @@ # Simple Sim Docker Configuration -Simple Sim runs in a lightweight Docker container optimized for quick startup and low resource usage. +The simulator runs in one container defined in +[`simulation/simple-sim/docker/docker-compose.yaml`](https://github.com/castacks/AirStack/blob/main/simulation/simple-sim/docker/docker-compose.yaml). -## File Structure +## Service -``` -simulation/simple-sim/docker/ -├── docker-compose.yaml # Service definition -├── Dockerfile.sim # Image definition -├── bashrc # Bash configuration -└── inputrc # Input configuration -``` - -## Service Architecture - -The Simple Sim service is defined in `simulation/simple-sim/docker/docker-compose.yaml`. - -**Key components:** - -| Component | Purpose | -|-----------|---------| -| **Simple Sim Core** | Lightweight 2D/3D simulator | -| **ROS 2 Native** | Direct ROS 2 topic integration | -| **Basic Physics** | Simplified flight dynamics | -| **Minimal Dependencies** | Fast startup, low overhead | - -## Launch Configuration - -The container command in docker-compose.yaml: - -```yaml -command: > - bash -c "ssh service restart; - tmux new -d -s sim - && tmux send-keys -t sim - 'cd /models && ./download.sh && cd ~/ros_ws/ && colcon build --symlink-install && source install/setup.bash && ROS_DOMAIN_ID=1 ros2 launch sim sim.launch.xml' ENTER - && sleep infinity" -``` - -**Launch sequence:** - -1. Restarts SSH service (for remote access) -2. Creates tmux session named `sim` -3. Downloads required models -4. Builds Simple Sim ROS workspace -5. Launches simulator with `ROS_DOMAIN_ID=1` -6. Keeps container alive - -## Profiles - -Simple Sim uses the `simple` profile: - -```bash -# Launch Simple Sim standalone -airstack up --profile simple simple-sim - -# Launch with robot (uses simple profile for robot too) -airstack up --profile simple simple-robot -``` - -**Profile configuration:** - -- Activates Simple Sim service -- Configures robot for simple sim mode (if launched together) - -## Networking - -**Network configuration:** -- **Network:** `airstack_network` (172.31.0.0/24) -- **Fixed IP:** 172.31.0.200 (same as Isaac Sim - mutually exclusive) -- **ROS_DOMAIN_ID:** 1 (matches robot containers) - -**Why same IP as Isaac Sim?** Simple Sim and Isaac Sim are mutually exclusive - only one runs at a time. - -## GPU Access - -Simple Sim supports GPU but doesn't require high-end hardware: - -```yaml -deploy: - resources: - reservations: - devices: - - driver: nvidia - count: 1 - capabilities: [gpu] -``` - -**GPU usage:** - -- **Optional** - Works with integrated graphics -- **Improves** visualization performance if available -- **Not required** for algorithm testing - -**Test without GPU:** -```bash -# Works even without dedicated GPU -airstack up --profile simple simple-robot -``` - -## Volume Mounts - -Simple Sim mounts minimal volumes for fast startup: - -### Display (X11) - -```yaml -- $HOME/.Xauthority:/root/.Xauthority -- /tmp/.X11-unix:/tmp/.X11-unix -``` - -Enables GUI visualization (optional). - -### Configuration - -```yaml -- ./bashrc:/root/.bashrc:rw -- /var/run/docker.sock:/var/run/docker.sock -``` - -**Docker socket:** Allows container to query its own name for `ROBOT_NAME`. - -### Models - -```yaml -- ../models:/models/:rw -``` - -3D models for visualization. Downloaded on first launch via `download.sh`. - -### ROS Workspace - -```yaml -- ../ros_ws:/root/ros_ws:rw -``` - -Simple Sim ROS 2 packages. Edit on host, rebuild in container. - -## Environment Variables - -Simple Sim uses minimal environment variables: - -| Variable | Description | Default | -|----------|-------------|---------| -| `DISPLAY` | X11 display for GUI | (from host) | -| `ROS_DOMAIN_ID` | ROS 2 domain (hardcoded in command) | `1` | -| `NVIDIA_DRIVER_CAPABILITIES` | GPU capabilities | `all` | - -**Note:** Most configuration is in ROS 2 launch files, not environment variables. - -## Accessing Simple Sim - -### Via tmux Session - -Connect to the container and attach to tmux: - -```bash -# Connect to container -airstack connect simple-sim - -# Attach to Simple Sim tmux session -tmux a -t sim -``` +| Property | Value | +|---|---| +| Service / container name | `simple-sim` | +| Compose profile | `simple` (activated by `airstack up --sim simple`) | +| Image | `osrf/ros:jazzy-desktop-full` + GLFW/GLM/Assimp and dev tools (`Dockerfile.sim`) | +| Network | `airstack_network`, fixed IP **172.31.0.200** — the same `SIM_IP` address every sim service binds, so only one simulator can run at a time | +| Paired robot service | `simple-robot` (in `robot/docker/docker-compose.yaml`): extends `robot-desktop` with `SIM_TYPE=simple`, same `simple` profile | -**View logs:** -```bash -# Detach from tmux first (Ctrl-b d) -# Then view logs -airstack logs simple-sim -``` - -### Via RViz Visualization - -If running with GUI, launch RViz to visualize: - -```bash -# In robot container -docker exec airstack-simple-robot-1 bash -c "rviz2" - -# Add visualization topics -``` +## Startup sequence -## Development Workflow +The container command runs, inside a tmux session named `sim`: -### Modifying Simple Sim +1. `cd /models && ./download.sh` — fetch the world mesh on first start +2. `colcon build --symlink-install` in `/root/ros_ws` (the workspace is + bind-mounted from `simulation/simple-sim/ros_ws`, so it builds at container + start, not at image build) +3. `ROS_DOMAIN_ID=1 ros2 launch sim sim.launch.xml` -1. **Edit simulator code** on host: - ``` - simulation/simple-sim/ros_ws/src/sim/ - ``` +Expect the sim's topics (`/clock`, mock-MAVROS state/odom, stereo images — +see the [overview](index.md)) to appear only after the build finishes. -2. **Rebuild in container:** - ```bash - docker exec airstack-simple-sim-1 bash -c "cd ~/ros_ws && colcon build --packages-select sim" - ``` +## Requirements -3. **Restart simulator:** - ```bash - docker exec airstack-simple-sim-1 bash -c "tmux send-keys -t sim C-c 'source install/setup.bash && ros2 launch sim sim.launch.xml' ENTER" - ``` +- **X display + OpenGL:** the sim renders the stereo pair with GLFW; the + compose file mounts `~/.Xauthority` and `/tmp/.X11-unix` from the host. +- **NVIDIA container runtime:** the service reserves one GPU + (`deploy.resources.reservations.devices`). The rendering itself is plain + OpenGL — far below Isaac Sim's requirements — but as written the compose + file will not start without the nvidia runtime. -### Adding Models - -1. **Place models** in `simulation/simple-sim/models/` - -2. **Update download script:** - ```bash - # Edit models/download.sh - ``` - -3. **Restart container** to download new models - -### Testing with Robot Stack - -**Full integration test:** +## Working with the container ```bash -# Launch both Simple Sim and robot -airstack up --profile simple simple-robot - -# Robot autonomy connects to Simple Sim automatically -``` - -## Image Management +airstack connect simple-sim # shell in; `tmux a -t sim` for the sim pane +airstack logs simple-sim # tmux output mirrored to docker logs -### Pulling Pre-built Images +# Rebuild the sim package after editing simulation/simple-sim/ros_ws/src/sim/ +docker exec simple-sim bash -c "cd ~/ros_ws && colcon build --symlink-install --packages-select sim" -```bash -# Login to AirLab registry -docker login airlab-docker.andrew.cmu.edu - -# Pull Simple Sim image -docker compose -f simulation/simple-sim/docker/docker-compose.yaml pull -``` - -### Building from Source - -```bash -# Build Simple Sim image (fast - minimal dependencies) -docker compose -f simulation/simple-sim/docker/docker-compose.yaml build - -# Build with no cache -docker compose -f simulation/simple-sim/docker/docker-compose.yaml build --no-cache +# See what the sim is publishing (domain 1) +docker exec simple-sim bash -c "source /opt/ros/jazzy/setup.bash && ROS_DOMAIN_ID=1 ros2 topic list" ``` -**Build time:** ~2-5 minutes (much faster than Isaac Sim) +Scene parameters (camera FOV/resolution/baseline, world mesh path, scale, +offsets) are plain ROS parameters in +`simulation/simple-sim/ros_ws/src/sim/launch/sim.launch.xml`. ## Troubleshooting -**Simple Sim won't start:** - -- Check logs: `airstack logs simple-sim` -- Verify network available: `docker network ls | grep airstack` -- Check ROS 2 domain: Should be `ROS_DOMAIN_ID=1` - -**No visualization:** - -- Verify `DISPLAY` set: `echo $DISPLAY` -- Allow X11: `xhost +local:docker` -- Check X11 socket mounted in docker-compose - -**ROS 2 topics not visible from robot:** - -- Verify both on `airstack_network`: `docker network inspect airstack_network` -- Check `ROS_DOMAIN_ID=1` in both containers -- Test directly: `docker exec airstack-simple-sim-1 bash -c "ros2 topic list"` - -**Models not downloading:** - -- Check network connectivity -- Verify `/models` writable in container -- Run download manually: `docker exec airstack-simple-sim-1 bash -c "cd /models && ./download.sh"` - -**Build fails:** +- **No topics after several minutes** — the startup colcon build failed or the + model download stalled: `airstack logs simple-sim`. +- **GLFW / display errors** — no usable X display in the container: check + `echo $DISPLAY` on the host and `xhost +local:docker`. +- **Robot sees nothing** — the stack must run as `robot_1` on domain 1 (the + sim hardcodes both); verify the robot container is + `airstack-simple-robot-1` and was started via `airstack up --sim simple`, + not alongside a `desktop`-profile robot. -- Check ROS 2 dependencies in workspace -- Verify `simulation/simple-sim/ros_ws/src/sim/package.xml` -- Review colcon build output - -## Performance Optimization - -Simple Sim is already lightweight, but you can optimize further: - -### Headless Mode - -Disable GUI for faster execution: - -```bash -# Don't mount display -# Edit docker-compose.yaml and comment out X11 volumes -``` - -### Reduce Visualization - -In launch files, disable unnecessary visualization topics: - -```xml - - -``` - -### Increase Physics Rate - -For faster simulation: - -```yaml -# In sim config -physics_rate_hz: 100 # Increase from default -``` - -**Trade-off:** Higher CPU usage but faster-than-real-time simulation. - -## Comparison with Isaac Sim Docker - -| Aspect | Isaac Sim | Simple Sim | -|--------|-----------|------------| -| **Image size** | ~20GB | ~2GB | -| **Startup time** | 30-60 seconds | 5-10 seconds | -| **Build time** | 20-30 minutes | 2-5 minutes | -| **GPU requirement** | Required | Optional | -| **Volume mounts** | Many (cache, config, extensions) | Minimal | -| **Dependencies** | NVIDIA Omniverse stack | ROS 2 + basic physics | - -## Advanced Configuration - -### Custom Physics - -Implement custom dynamics in `simulation/simple-sim/ros_ws/src/sim/`: - -```python -# Example: Add wind effects -class DroneSimulator: - def apply_wind(self, wind_vector): - # Custom physics - pass -``` - -### Multi-Robot Testing - -Launch multiple Simple Sim instances: +## Smoke test ```bash -# Requires docker-compose modifications -# Each instance needs unique IP and ROS_DOMAIN_ID -``` - -**Note:** Currently configured for single simulator instance. - -### CI/CD Integration - -Example GitHub Actions workflow: - -```yaml -- name: Test with Simple Sim - run: | - airstack up --profile simple simple-robot & - sleep 10 # Wait for startup - docker exec airstack-simple-robot-1 bash -c "colcon test" - airstack down +airstack test -m simple_sim --sim simplesim --num-robots 1 -v ``` ## See Also -- [Simple Sim Overview](index.md) - Features and use cases -- [Isaac Sim Docker](../isaac_sim/docker.md) - High-fidelity simulator -- [Simulation Overview](../index.md) - Main simulation documentation -- [Docker Workflow](../../development/beginner/airstack-cli/docker_usage.md) - General Docker operations +- [Simple Sim Overview](index.md) +- [Isaac Sim Docker](../isaac_sim/docker.md) diff --git a/docs/simulation/simple_sim/index.md b/docs/simulation/simple_sim/index.md index d8caada94..2f53b715b 100644 --- a/docs/simulation/simple_sim/index.md +++ b/docs/simulation/simple_sim/index.md @@ -1,177 +1,67 @@ # Simple Sim -Simple Sim is a lightweight 2D/3D simulator for basic testing and development when full Isaac Sim fidelity isn't needed. - -## Overview - -Simple Sim provides a faster, more resource-efficient alternative to Isaac Sim for: - -- **Quick algorithm prototyping** - Faster iteration cycles -- **CI/CD testing** - Lightweight enough for automated testing pipelines -- **Lower hardware requirements** - Works on systems without high-end GPUs -- **Basic flight dynamics** - Sufficient for many planning and control algorithms - -**Trade-offs:** - -- ✅ Faster startup and execution -- ✅ Lower computational requirements -- ✅ Simpler scene setup -- ❌ Less realistic physics -- ❌ Limited sensor simulation -- ❌ Basic graphics (no photorealism) - -## Use Cases - -### Algorithm Development - -Test planning and control algorithms without full simulation overhead: - -```bash -airstack up simple-sim robot -``` - -Your ROS 2 autonomy stack connects to Simple Sim just like Isaac Sim. - -### Continuous Integration - -Run automated tests in CI pipelines: - -```bash -# In CI script -airstack up --profile simple simple-robot -# Run tests... -airstack down -``` - -### Resource-Constrained Environments - -Develop on laptops or systems without high-end GPUs: - -- Works with integrated graphics -- Lower RAM requirements (~4GB vs 16GB+) -- Faster container startup - -## Architecture - -Simple Sim is built on: - -- **ROS 2 native** - Direct ROS 2 integration -- **Lightweight physics** - Basic dynamics simulation -- **2D/3D visualization** - RViz-compatible -- **Configurable dynamics** - Tune flight characteristics - -### Comparison with Isaac Sim - -| Feature | Isaac Sim | Simple Sim | -|---------|-----------|------------| -| **Graphics** | Photorealistic raytracing | Basic 3D rendering | -| **Physics** | NVIDIA PhysX (high-fidelity) | Simplified dynamics | -| **Sensors** | Full suite (cameras, LiDAR, etc.) | Basic sensors | -| **Startup time** | 30-60 seconds | 5-10 seconds | -| **GPU requirements** | RTX 3070+ | Integrated graphics OK | -| **RAM requirements** | 16GB+ | 4GB+ | -| **Scene authoring** | USD format (Omniverse) | Configuration files | -| **Multi-robot** | Full support | Full support | +Simple Sim is AirStack's lightweight kinematic simulator: a single C++ ROS 2 +node that flies a simplified drone model through one OpenGL-rendered mesh +world — **no PX4, no MAVROS, no Isaac Sim**. It is actively used by core +maintainer John Keller for fast planner/perception iteration. ## Quick Start -### Launch Simple Sim - ```bash -# Launch Simple Sim only -airstack up --profile simple simple-sim - -# Launch with robot stack -airstack up --profile simple simple-robot +airstack up --sim simple ``` -### Verify Connection +This starts two containers: -```bash -# Check ROS 2 topics -docker exec airstack-simple-robot-1 bash -c "ros2 topic list | grep simple" -``` +- **`simple-sim`** — the simulator ([`simulation/simple-sim/`](https://github.com/castacks/AirStack/tree/main/simulation/simple-sim)). On first start it downloads the world mesh (`models/download.sh`), colcon-builds its small workspace, then runs `ros2 launch sim sim.launch.xml` on `ROS_DOMAIN_ID=1`. +- **`airstack-simple-robot-1`** — the autonomy stack. The `simple-robot` compose service extends `robot-desktop` with `SIM_TYPE=simple`, which makes the interface layer **skip MAVROS** (`interface_bringup/launch/interface.launch.py`); everything else in the stack launches as usual. -## Configuration +`--sim simple` deliberately drops the `desktop` profile: `simple-robot` +*replaces* `robot-desktop` (both would otherwise claim `robot_1` on domain 1), +and no GCS container is started. -Simple Sim configuration is in `simulation/simple-sim/ros_ws/`: +## How it works -``` -simulation/simple-sim/ -├── docker/ # Docker configuration -│ ├── docker-compose.yaml -│ └── Dockerfile.sim -├── models/ # 3D models -│ └── download.sh # Model download script -└── ros_ws/ # Simple Sim ROS workspace - └── src/ - └── sim/ # Simulator package - ├── config/ # Configuration files - └── launch/ # Launch files -``` +The sim node (`MavrosMockNode`, launched as `/sim`) **impersonates the MAVROS +surface** the autonomy stack talks to, so the stack runs unmodified minus +MAVROS itself: -### Customizing Flight Dynamics +| Direction | Interface | +|---|---| +| Serves | `/robot_1/interface/mavros/set_mode`, `.../cmd/arming`, `.../cmd/takeoff` | +| Subscribes | `/robot_1/interface/mavros/setpoint_raw/attitude` (attitude + thrust setpoints) | +| Publishes | `/robot_1/interface/mavros/state`, `/robot_1/interface/mavros/local_position/odom`, `/clock` | +| Publishes | `/robot_1/sensors/front_stereo/{left,right}/image_rect` + `camera_info` (OpenGL-rendered stereo pair of the FBX world) | -Edit configuration in `simulation/simple-sim/ros_ws/src/sim/config/`: - -```yaml -# Example: drone dynamics parameters -mass: 1.5 # kg -max_thrust: 20.0 # N -drag_coefficient: 0.1 -``` +Static TFs for the stereo pair (`base_link` → camera links → optical frames) +are published by the sim's launch file. After a takeoff service call the sim +briefly runs in a fast-forward mode to skip the stack's post-takeoff wait. -## Development Workflow +**Single robot only:** all topics and services are hardcoded to `robot_1` on +`ROS_DOMAIN_ID=1`. There is no multi-robot support. -### Testing Algorithm Changes +## When to use it -1. **Start Simple Sim:** - ```bash - airstack up --profile simple simple-robot - ``` +- Fast iteration on planning / control / stereo-perception code — startup is a + small colcon build plus a mesh load, not an Isaac Sim boot. +- Machines without an Isaac-class GPU or Omniverse credentials. (The container + still needs OpenGL: an X display is mounted in, and the compose file + reserves an NVIDIA GPU via the nvidia container runtime.) +- Not for: PX4/MAVROS behavior, LiDAR, physics fidelity, multi-robot, or + final validation — use [Isaac Sim](../isaac_sim/index.md) for those. -2. **Make changes** to autonomy code on host +## Smoke test -3. **Rebuild in container:** - ```bash - docker exec airstack-simple-robot-1 bash -c "bws --packages-select my_planner" - ``` - -4. **Test immediately** (faster than Isaac Sim restart) - -### Transitioning to Isaac Sim - -Once algorithms work in Simple Sim, test in Isaac Sim: +A dedicated system test verifies the simple-sim bring-up (containers, +`/clock`, mock-MAVROS odometry reaching the robot, and the `SIM_TYPE=simple` +sentinel nodes — MAVROS intentionally absent): ```bash -# Stop Simple Sim -airstack down - -# Start Isaac Sim -airstack up robot isaac-sim +airstack test -m simple_sim --sim simplesim --num-robots 1 -v ``` -Code changes are minimal - same ROS 2 topics and interfaces. - -## Limitations - -**What Simple Sim can't do:** - -- Photorealistic rendering -- Complex sensor simulation (cameras, LiDAR) -- Accurate aerodynamic effects -- Detailed collision physics -- Custom 3D environments (limited to basic models) - -**When to use Isaac Sim instead:** - -- Visual perception algorithm development -- Realistic sensor simulation needed -- Complex environment interactions -- Final validation before hardware deployment - ## See Also -- [Docker Configuration](docker.md) - Simple Sim container setup -- [Isaac Sim](../isaac_sim/index.md) - High-fidelity simulation alternative -- [Simulation Overview](../index.md) - Main simulation documentation +- [Docker Configuration](docker.md) — the `simple-sim` container in detail +- [Isaac Sim](../isaac_sim/index.md) — high-fidelity alternative +- [Simulation Overview](../index.md) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 1b7d703f0..05aa18642 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,17 +1,138 @@ +/* ========================================================================== + AirStack documentation theme + Modern, professional skin on top of mkdocs-material. + ========================================================================== */ + +/* -------------------------------------------------------------------------- + 1. Design tokens + -------------------------------------------------------------------------- */ + :root { - --md-primary-fg-color: #EE0F0F; - --md-primary-fg-color--light: #ECB7B7; - --md-primary-fg-color--dark: #90030C; + /* CMU official palette (brand.cmu.edu/visual-identity/colors) + Core: Carnegie Red is the primary brand color and must dominate. + Supporting reds: Skibo Red (Campus palette) as the dark step, + Scots Rose (Tartan palette) as the bright step for dark mode. */ + --as-red: #c41230; /* Carnegie Red, PMS 187 C */ + --as-red-dark: #941120; /* Skibo Red, PMS 7623 C */ + --as-red-darker: #6a0c17; /* derived deep step (no official darker red) */ + --as-red-light: #ef3a47; /* Scots Rose, PMS Red 032 C */ + --as-red-tint: rgba(196, 18, 48, 0.08); + + /* Core neutrals */ + --as-iron-gray: #6d6e71; /* Cool Gray 10 C — supporting text */ + --as-steel-gray: #e0e0e0; /* Cool Gray 4 C — borders, rules */ + --as-border: var(--as-steel-gray); + --as-text-secondary: var(--as-iron-gray); + + --md-primary-fg-color: var(--as-red); + --md-primary-fg-color--light: var(--as-red-light); + --md-primary-fg-color--dark: var(--as-red-dark); + --md-accent-fg-color: var(--as-red-dark); + --md-accent-fg-color--transparent: var(--as-red-tint); + + /* Shape + motion */ + --as-radius: 0.6rem; + --as-radius-sm: 0.35rem; + --as-shadow-sm: 0 1px 2px rgba(15, 18, 25, 0.06), 0 1px 3px rgba(15, 18, 25, 0.08); + --as-shadow-md: 0 4px 14px rgba(15, 18, 25, 0.10); + --as-shadow-lg: 0 12px 32px rgba(15, 18, 25, 0.16); + --as-transition: 180ms cubic-bezier(0.2, 0, 0, 1); +} + +/* Links in dark mode need a brighter red for contrast */ +[data-md-color-scheme="slate"] { + --md-hue: 215; + + --md-primary-fg-color: var(--as-red); + --md-accent-fg-color: var(--as-red-light); + --md-accent-fg-color--transparent: rgba(239, 58, 71, 0.12); + --md-typeset-a-color: var(--as-red-light); + + /* Deeper, less washed-out dark background */ + --md-default-bg-color: hsla(var(--md-hue), 22%, 10%, 1); + --md-code-bg-color: hsla(var(--md-hue), 20%, 14%, 1); + --md-footer-bg-color: hsla(var(--md-hue), 22%, 8%, 1); + --md-footer-bg-color--dark: hsla(var(--md-hue), 22%, 6%, 1); + + --as-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.35); + --as-shadow-md: 0 4px 14px rgba(0, 0, 0, 0.4); + --as-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5); + + /* Gray brand neutrals don't work on slate; fall back to theme grays */ + --as-border: var(--md-default-fg-color--lightest); + --as-text-secondary: var(--md-default-fg-color--light); } -[data-md-color-scheme="slate"] { - --md-hue: 210; +[data-md-color-scheme="default"] { + --md-typeset-a-color: var(--as-red); +} + +/* -------------------------------------------------------------------------- + 2. Typography + -------------------------------------------------------------------------- */ + +.md-typeset h1, +.md-typeset h2, +.md-typeset h3, +.md-typeset h4 { + font-weight: 700; + letter-spacing: -0.015em; +} + +.md-typeset h1 { + color: var(--md-default-fg-color); + margin-bottom: 1.2em; +} + +.md-typeset h2 { + margin-top: 2em; +} + +/* Slightly larger, more readable body text */ +.md-typeset { + font-size: 0.78rem; + line-height: 1.65; +} + +/* -------------------------------------------------------------------------- + 3. Header and navigation tabs + -------------------------------------------------------------------------- */ + +.md-header { + background: linear-gradient(120deg, var(--as-red) 0%, var(--as-red-dark) 100%); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08) inset, var(--as-shadow-sm); } -/* Ensure navigation tabs are above splash content */ .md-tabs { position: relative; - z-index: 3; + z-index: 3; /* keep tabs above splash content */ + background: linear-gradient(120deg, var(--as-red-dark) 0%, var(--as-red-darker) 100%); +} + +.md-tabs__link { + font-weight: 500; + opacity: 0.75; + transition: opacity var(--as-transition); +} + +.md-tabs__link--active, +.md-tabs__link:hover { + opacity: 1; +} + +.md-tabs__item--active .md-tabs__link { + position: relative; +} + +.md-tabs__item--active .md-tabs__link::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -0.4rem; + height: 2px; + border-radius: 2px; + background: #fff; } .md-version * { @@ -19,42 +140,194 @@ z-index: 5; } -/* Splash container and background */ +/* Search: keep the results dropdown above the tabs bar (z 3) and the + version selector text (z 5), which share the header's stacking context. + Material's .md-search__output is only z 1, so without this the dropdown + renders behind them. .md-search is already position:relative upstream. */ +.md-search { + z-index: 6; +} + +/* Search field: soft pill */ +.md-search__form { + border-radius: 0.45rem; + background-color: rgba(255, 255, 255, 0.14); + transition: background-color var(--as-transition); +} + +.md-search__form:hover { + background-color: rgba(255, 255, 255, 0.22); +} + +/* -------------------------------------------------------------------------- + 4. Sidebar navigation + -------------------------------------------------------------------------- */ + +.md-nav__item .md-nav__link--active, +.md-nav__item .md-nav__link--active code { + font-weight: 600; +} + +.md-nav__link { + transition: color var(--as-transition); +} + +/* -------------------------------------------------------------------------- + 5. Content elements: code, tables, admonitions, buttons, images + -------------------------------------------------------------------------- */ + +/* Code blocks */ +.md-typeset pre > code { + border-radius: var(--as-radius-sm); +} + +.md-typeset .highlight { + border-radius: var(--as-radius-sm); +} + +.md-typeset code { + border-radius: 0.25rem; + font-size: 0.85em; +} + +/* Tables */ +.md-typeset table:not([class]) { + border-radius: var(--as-radius-sm); + border: 1px solid var(--as-border); + box-shadow: none; + overflow: hidden; + font-size: 0.72rem; +} + +.md-typeset table:not([class]) th { + background-color: var(--md-default-fg-color--lightest); + color: var(--md-default-fg-color); + font-weight: 600; +} + +.md-typeset table:not([class]) tr:hover { + background-color: var(--md-accent-fg-color--transparent); + transition: background-color var(--as-transition); +} + +/* Admonitions: flat modern style */ +.md-typeset .admonition, +.md-typeset details { + border-radius: var(--as-radius-sm); + border-width: 0 0 0 3px; + box-shadow: var(--as-shadow-sm); + font-size: 0.72rem; +} + +.md-typeset .admonition-title, +.md-typeset summary { + border-radius: 0; +} + +/* Buttons */ +.md-typeset .md-button { + border-radius: 2rem; + font-weight: 600; + letter-spacing: 0.01em; + transition: transform var(--as-transition), + box-shadow var(--as-transition), + background-color var(--as-transition), + border-color var(--as-transition), + color var(--as-transition); +} + +.md-typeset .md-button:hover { + transform: translateY(-1px); + box-shadow: var(--as-shadow-md); +} + +.md-typeset .md-button--primary { + background-color: var(--as-red); + border-color: var(--as-red); + color: #fff; +} + +.md-typeset .md-button--primary:hover { + background-color: var(--as-red-dark); + border-color: var(--as-red-dark); +} + +/* Images and mermaid diagrams */ +.md-typeset img { + border-radius: var(--as-radius-sm); +} + +/* Footer */ +.md-footer-meta { + font-size: 0.65rem; +} + +/* Scrollbar (WebKit) */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-thumb { + background: var(--md-default-fg-color--lighter); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--md-default-fg-color--light); +} + +/* Text selection */ +::selection { + background: var(--as-red-tint); +} + +/* -------------------------------------------------------------------------- + 6. Landing page: splash / hero + -------------------------------------------------------------------------- */ + .splash-container { position: relative; width: 100%; - height: calc(100vh - 98px); /* Subtract header height to prevent overlap */ - min-height: 600px; + /* Grow with the hero content: a fixed height + overflow:hidden clipped + the CTAs/footnote on short laptop viewports. */ + height: auto; + min-height: calc(100vh - 98px); overflow: hidden; - margin-top: 0; /* Remove negative margin */ - padding-top: 1rem; /* Add some padding from the navigation */ + margin-top: 0; + padding-top: 1rem; + display: flex; + flex-direction: column; +} + +.splash-container > .md-grid { + flex: 1; + display: flex; + width: 100%; + min-width: 0; /* flex items default to min-width:auto → pre overflows on phones */ } .splash-background { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + inset: 0; z-index: 1; } .media-overlay { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); /* Darkens the background media */ + inset: 0; + /* Layered gradient: red brand tint + darkening + bottom fade into the page. + Kept light so the demo video stays visible; the fade into the page bg + only starts near the bottom edge. */ + background: + linear-gradient(180deg, rgba(10, 12, 18, 0.30) 0%, rgba(10, 12, 18, 0.10) 45%, rgba(10, 12, 18, 0) 85%, var(--md-default-bg-color) 100%), + linear-gradient(120deg, rgba(148, 17, 32, 0.18) 0%, rgba(15, 18, 25, 0.06) 60%); z-index: 2; } .media-container { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + inset: 0; z-index: 1; } @@ -62,9 +335,10 @@ width: 100%; height: 100%; object-fit: cover; + filter: saturate(0.9); } -/* Hero section */ +/* Hero content */ .mdx-container { padding: 1rem; margin: 0 auto; @@ -74,171 +348,536 @@ position: relative; z-index: 3; margin: 0; - height: 100%; + flex: 1; display: flex; align-items: center; justify-content: center; text-align: center; color: white; - padding-top: 7rem; /* Add padding to push content down */ + padding: 4rem 0 2.5rem; + min-width: 0; + width: 100%; +} + +.mdx-hero__content { + min-width: 0; + max-width: 100%; +} + +.mdx-hero__badge { + display: inline-block; + padding: 0.3rem 0.9rem; + margin-bottom: 1.4rem; + border: 1px solid rgba(255, 255, 255, 0.35); + border-radius: 2rem; + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.92); } .mdx-hero h1 { - font-size: 3.5rem; - font-weight: 700; + font-size: clamp(2.2rem, 5.5vw, 3.6rem); + font-weight: 800; + letter-spacing: -0.03em; margin-bottom: 1rem; - line-height: 1.15; + line-height: 1.1; color: #ffffff; - text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); + text-shadow: 0 2px 16px rgba(0, 0, 0, 0.55), 0 1px 3px rgba(0, 0, 0, 0.4); } .mdx-hero p { - font-size: 1.4rem; - font-weight: 300; - margin-bottom: 2rem; - color: #ffffff; - text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); - max-width: 35rem; - margin-left: auto; - margin-right: auto; + font-size: clamp(1rem, 2vw, 1.3rem); + font-weight: 400; + margin: 0 auto 2.2rem; + color: rgba(255, 255, 255, 0.92); + text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55); + max-width: 36rem; + line-height: 1.5; } .mdx-hero .md-button { - background-color: #ffffffAA; - margin: 0.5rem; - font-size: 1rem; - padding: 0.625em 2em; + margin: 0.4rem; + font-size: 0.85rem; + font-weight: 600; + padding: 0.7em 2.2em; + border-radius: 2rem; + border: 1px solid rgba(255, 255, 255, 0.45); + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + color: #fff; + transition: transform var(--as-transition), + box-shadow var(--as-transition), + background var(--as-transition), + border-color var(--as-transition); +} + +.mdx-hero .md-button:hover { + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.7); + box-shadow: var(--as-shadow-lg); + color: #fff; } .mdx-hero .md-button--primary { - background-color: var(--md-primary-fg-color); - border-color: var(--md-primary-fg-color); - color: var(--md-primary-bg-color); + background: var(--as-red); + border-color: var(--as-red); + color: #fff; + box-shadow: 0 6px 24px rgba(196, 18, 48, 0.45); } -/* What is section */ -.what-is-section { - margin: 4rem 0; +.mdx-hero .md-button--primary:hover { + background: var(--as-red-dark); + border-color: var(--as-red-dark); + box-shadow: 0 10px 32px rgba(196, 18, 48, 0.55); } -.what-is-section h2 { - font-size: 2.5rem; - font-weight: 600; - margin-bottom: 1.5rem; +/* -------------------------------------------------------------------------- + 7. Landing page: sections (.as-*) + -------------------------------------------------------------------------- */ + +.as-landing { + max-width: 61rem; + margin: 0 auto; + padding: 0 0.8rem; + /* Belt and braces: wide artifacts scroll inside their own cards; the + landing column itself never scrolls horizontally. */ + overflow-x: clip; +} + +.as-section { + margin: 5rem 0; +} + +.as-landing h2 { + font-size: 1.6rem; + font-weight: 750; + letter-spacing: -0.02em; + line-height: 1.2; + margin: 0 0 1rem; color: var(--md-default-fg-color); } -.what-is-section p { - font-size: 1.1rem; - line-height: 1.6; - margin-bottom: 1.5rem; +.as-landing p { + font-size: 0.82rem; + line-height: 1.65; + color: var(--as-text-secondary); } -.feature-list { - list-style: none; - padding: 0; - margin: 2rem 0; +.as-lede { + max-width: 42rem; + margin: 0 0 1.8rem; } -.feature-list li { - font-size: 1.1rem; - margin-bottom: 1rem; - padding-left: 1.5rem; - position: relative; +.as-proof { + border-left: 3px solid var(--as-red); + padding: 0.5rem 0 0.5rem 0.9rem; + background: var(--as-red-tint); + border-radius: 0 var(--as-radius-sm) var(--as-radius-sm) 0; } -.feature-list li strong { - color: var(--md-primary-fg-color); +.as-proof strong { + color: var(--md-default-fg-color); } -/* Feature grid */ -.feature-grid { +/* Two-column pillar layout */ +.as-section__grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); /* Increased minimum width */ - gap: 2rem; - margin: 2rem 0 4rem; + grid-template-columns: 1fr 1fr; + gap: 2.6rem; + align-items: center; } -.feature-card { - background-color: var(--md-default-bg-color); - border: 1px solid var(--md-default-fg-color--lightest); - border-radius: 8px; - padding: 2rem; - transition: all 0.3s ease; - text-align: center; +.as-section__copy h2 { + margin-top: 0; } -.feature-card:hover { - transform: translateY(-5px); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +/* Grid/flex items default to min-width:auto, letting wide
 content
+   blow the column past the viewport on phones. */
+.as-section__grid > * {
+    min-width: 0;
 }
 
-.feature-icon {
-    font-size: 2.5rem;
-    margin-bottom: 1rem;
+.as-section__artifact {
+    min-width: 0;
+}
+
+/* Terminal + snippet cards */
+.as-terminal,
+.as-snippet {
+    border-radius: var(--as-radius);
+    overflow: hidden;
+    border: 1px solid rgba(255, 255, 255, 0.08);
+    background: #14181f;
+    box-shadow: var(--as-shadow-md);
+    text-align: left;
+    min-width: 0;
+    max-width: 100%;
+}
+
+.as-terminal__bar,
+.as-snippet__bar {
+    display: flex;
+    align-items: center;
+    gap: 0.6rem;
+    padding: 0.45rem 0.8rem;
+    background: rgba(255, 255, 255, 0.05);
+    border-bottom: 1px solid rgba(255, 255, 255, 0.07);
+    font-size: 0.62rem;
+    letter-spacing: 0.04em;
+    color: rgba(255, 255, 255, 0.55);
+}
+
+.as-snippet__bar code {
+    background: none;
+    color: inherit;
+    padding: 0;
+    font-size: inherit;
+}
+
+.as-terminal__dots {
+    display: inline-flex;
+    gap: 0.3rem;
+}
+
+.as-terminal__dots i {
+    width: 0.55rem;
+    height: 0.55rem;
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.18);
 }
 
-.feature-card h3 {
-    font-size: 1.5rem;
+.as-terminal__title {
+    flex: 1;
+}
+
+.as-snippet__bar span {
+    flex: 1;
+}
+
+.as-terminal pre,
+.as-snippet pre {
+    margin: 0;
+    padding: 0.85rem 1rem;
+    overflow-x: auto;
+}
+
+.as-terminal code,
+.as-snippet code {
+    display: block;
+    background: none;
+    padding: 0;
+    font-size: 0.68rem;
+    line-height: 1.75;
+    color: #e8edf4;
+    white-space: pre;
+}
+
+/* mkdocs-material injects its own copy icon into pre>code; our cards have one */
+.as-terminal .md-clipboard,
+.as-snippet .md-clipboard {
+    display: none;
+}
+
+.as-prompt {
+    color: var(--as-red-light);
+    font-weight: 700;
+}
+
+.as-ok {
+    color: #4ade80;
+    font-weight: 700;
+}
+
+.as-dim {
+    color: rgba(232, 237, 244, 0.45);
+}
+
+.as-copy {
+    font: inherit;
+    font-size: 0.62rem;
     font-weight: 600;
-    margin: 1rem 0;
-    color: var(--md-primary-fg-color);
-    white-space: nowrap; /* Prevent text wrapping */
+    letter-spacing: 0.04em;
+    color: rgba(255, 255, 255, 0.75);
+    background: rgba(255, 255, 255, 0.08);
+    border: 1px solid rgba(255, 255, 255, 0.18);
+    border-radius: 0.9rem;
+    padding: 0.12rem 0.7rem;
+    cursor: pointer;
+    transition: background var(--as-transition), color var(--as-transition);
 }
 
-.feature-card p {
-    font-size: 1rem;
-    line-height: 1.5;
-    color: var(--md-default-fg-color--light);
+.as-copy:hover {
+    background: rgba(255, 255, 255, 0.18);
+    color: #fff;
+}
+
+.as-snippet--center {
+    max-width: 36rem;
+    margin: 0 auto;
 }
 
-/* Architecture section */
-.architecture-section {
-    margin: 4rem 0;
+/* Media artifacts */
+.as-video {
+    width: 100%;
+    display: block;
+    border-radius: var(--as-radius);
+    border: 1px solid var(--as-border);
+    box-shadow: var(--as-shadow-md);
+    background: #000;
+}
+
+.as-caption {
+    font-size: 0.7rem;
+    line-height: 1.55;
+    color: var(--as-text-secondary);
+    margin: 0.7rem 0 0;
+}
+
+.as-caption--center {
     text-align: center;
+    margin-top: 1rem;
+}
+
+/* CI test matrix */
+.as-matrix {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+    gap: 0.7rem;
+    margin: 0 0 1.8rem;
+}
+
+.as-matrix__item {
+    display: flex;
+    flex-direction: column;
+    gap: 0.15rem;
+    padding: 0.7rem 0.9rem;
+    border: 1px solid var(--as-border);
+    border-radius: var(--as-radius-sm);
+    background: var(--md-code-bg-color);
+}
+
+.as-matrix__item code {
+    background: none;
+    padding: 0;
+    font-size: 0.7rem;
+    font-weight: 700;
+    color: var(--md-default-fg-color);
+}
+
+.as-matrix__item span {
+    font-size: 0.65rem;
+    color: var(--as-text-secondary);
+}
+
+/* Agent skills card */
+.as-skills {
+    border: 1px solid var(--as-border);
+    border-radius: var(--as-radius);
+    background: var(--md-code-bg-color);
+    box-shadow: var(--as-shadow-sm);
+    overflow: hidden;
+}
+
+.as-skills__bar {
+    padding: 0.5rem 0.9rem;
+    font-family: var(--md-code-font-family, monospace);
+    font-size: 0.65rem;
+    color: var(--as-text-secondary);
+    border-bottom: 1px solid var(--as-border);
 }
 
-.architecture-image {
+.as-skills__grid {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0.45rem;
+    padding: 0.9rem;
+}
+
+.as-skills__grid span {
+    font-family: var(--md-code-font-family, monospace);
+    font-size: 0.62rem;
+    padding: 0.18rem 0.6rem;
+    border-radius: 1rem;
+    border: 1px solid var(--as-border);
+    background: var(--md-default-bg-color);
+    color: var(--md-default-fg-color);
+}
+
+.as-skills__grid .as-skills__more {
+    border-style: dashed;
+    color: var(--as-text-secondary);
+}
+
+/* Full-system screenshot */
+.as-arch {
+    border: 1px solid var(--as-border);
+    border-radius: var(--as-radius);
+    box-shadow: var(--as-shadow-lg);
+    overflow: hidden;
+}
+
+.as-arch img {
     max-width: 100%;
     height: auto;
-    border-radius: 8px;
-    margin-bottom: 1rem;
+    display: block;
 }
 
-.image-caption {
-    font-size: 1rem;
-    color: var(--md-default-fg-color--light);
+/* Provenance + final CTA */
+.as-provenance .as-lede {
+    max-width: 48rem;
 }
 
-/* Responsive adjustments */
-@media screen and (max-width: 76.1875em) {
+.as-cta {
+    margin: 5rem 0 3rem;
+    text-align: center;
+}
+
+.as-cta h2 {
+    font-size: 1.8rem;
+    margin-bottom: 1.4rem;
+}
+
+.as-cta .md-button--primary {
+    background: var(--as-red);
+    border-color: var(--as-red);
+    color: #fff;
+}
+
+.as-cta .md-button {
+    margin: 0.3rem;
+}
+
+/* Hero additions */
+.mdx-hero__actions {
+    margin-top: 1.6rem;
+}
+
+.mdx-hero__footnote {
+    font-size: 0.62rem !important;
+    color: rgba(255, 255, 255, 0.55) !important;
+    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
+    margin: 1.4rem auto 0 !important;
+    max-width: 34rem;
+}
+
+.as-terminal--hero {
+    max-width: min(40rem, 100%);
+    margin: 0 auto;
+    text-align: left;
+    background: rgba(13, 16, 22, 0.82);
+    backdrop-filter: blur(10px);
+    -webkit-backdrop-filter: blur(10px);
+    border: 1px solid rgba(255, 255, 255, 0.14);
+}
+
+.as-terminal--hero code {
+    font-size: 0.64rem;
+}
+
+/* Short laptop viewports (13" MacBooks etc.): tighten the hero so the
+   quickstart, CTAs, and footnote all fit without clipping or scrolling. */
+@media screen and (max-height: 900px) {
+    .mdx-hero {
+        padding: 2.5rem 0 2rem;
+    }
+
     .mdx-hero h1 {
-        font-size: 2.5rem;
+        font-size: clamp(1.9rem, 4.5vw, 2.7rem);
+        margin-bottom: 0.7rem;
     }
-    
+
     .mdx-hero p {
-        font-size: 1.2rem;
+        font-size: clamp(0.9rem, 1.6vw, 1.05rem);
+        margin-bottom: 1.4rem;
+    }
+
+    .mdx-hero__badge {
+        margin-bottom: 1rem;
+    }
+
+    .as-terminal--hero code {
+        font-size: 0.6rem;
+        line-height: 1.6;
+    }
+
+    .mdx-hero__actions {
+        margin-top: 1.1rem;
     }
 
-    .what-is-section h2 {
-        font-size: 2rem;
+    .mdx-hero__footnote {
+        margin-top: 0.9rem !important;
     }
+}
 
-    .feature-grid {
-        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+/* --------------------------------------------------------------------------
+   8. Responsive adjustments
+   -------------------------------------------------------------------------- */
+
+@media screen and (max-width: 76.1875em) {
+    .as-landing h2 {
+        font-size: 1.4rem;
+    }
+}
+
+@media screen and (max-width: 900px) {
+    .as-section {
+        margin: 3.5rem 0;
+    }
+
+    .as-section__grid {
+        grid-template-columns: 1fr;
+        gap: 1.4rem;
+    }
+
+    /* keep prose above its artifact when stacked */
+    .as-section__grid .as-section__copy {
+        order: 0;
+    }
+
+    .as-section__grid .as-section__artifact {
+        order: 1;
     }
 }
 
 @media screen and (max-width: 600px) {
-    .mdx-hero h1 {
-        font-size: 2rem;
+    .splash-container {
+        height: auto;
+        min-height: calc(100svh - 98px);
     }
-    
-    .mdx-hero p {
-        font-size: 1.1rem;
+
+    .mdx-hero h1 br {
+        display: none;
+    }
+
+    .as-terminal--hero code {
+        font-size: 0.56rem;
+    }
+
+    .as-matrix {
+        grid-template-columns: 1fr 1fr;
+    }
+
+    .as-matrix__item code {
+        font-size: 0.62rem;
+    }
+}
+
+/* Respect reduced-motion preferences */
+@media (prefers-reduced-motion: reduce) {
+    .md-typeset .md-button,
+    .mdx-hero .md-button {
+        transition: none;
     }
 
-    .what-is-section h2 {
-        font-size: 1.75rem;
+    .md-typeset .md-button:hover,
+    .mdx-hero .md-button:hover {
+        transform: none;
     }
 }
diff --git a/docs/tutorials/airstack_on_osmo.md b/docs/tutorials/airstack_on_osmo.md
index e9cfa8974..14daeb880 100644
--- a/docs/tutorials/airstack_on_osmo.md
+++ b/docs/tutorials/airstack_on_osmo.md
@@ -1,12 +1,14 @@
 # AirStack on OSMO — Recommended Remote Development Workflow
 
-This is AirStack's recommended day-to-day development path going forward.
+This is AirStack's recommended **remote** development path — for Mac and
+Windows users, machines without a local NVIDIA GPU, or anyone who wants to
+develop against the lab's shared GPU pool.
 You submit one OSMO workflow that spins up a GPU pod running the full
 three-container AirStack stack (Isaac Sim, robot-desktop, GCS), attach VS
 Code or Cursor to it over Remote-SSH, and stream Isaac Sim and the GCS
 Foxglove dashboard back to your browser.
 
-Why this is the recommended path:
+Why this is the recommended remote path:
 
 - **Pooled GPUs.** A lab's GPUs are shared on-demand across the whole team
   instead of pinned one-per-desktop. Onboarding doesn't require buying
@@ -18,20 +20,20 @@ Why this is the recommended path:
   Docker images that the system tests and deployed robots run, so your
   dev environment can't drift away from production.
 - **One-command onboarding.** A new student goes from zero to "Isaac Sim
-  streaming into my browser" with `airstack osmo:setup` followed by
-  `airstack osmo:up` — no install marathon.
+  streaming into my browser" with `airstack osmo setup` followed by
+  `airstack osmo up` — no install marathon.
 - **Hardware bigger than your laptop.** The pod has more CPU/RAM/GPU than
   most dev laptops, even if you have a GPU laptop.
 
-> **Still want local development on a Linux+GPU desktop?** It works and
-> can be faster for tight inner loops — see
-> [Getting Started](../getting_started/index.md). It just isn't the
-> recommended default anymore.
+> **Have a Linux+GPU desktop?** Local development is the golden path —
+> it's faster for tight inner loops. See
+> [Getting Started](../getting_started/index.md). OSMO is the recommended
+> alternative when local isn't an option.
 
 ## Who is this for?
 
-Anyone developing AirStack — Mac, Windows, or Linux, with or without a
-local GPU.
+Anyone developing AirStack remotely — Mac, Windows, or Linux, especially
+without a local GPU.
 
 You're comfortable using `git` from a terminal, you have an SSH key
 (`~/.ssh/id_ed25519` or similar), and you have either VS Code or Cursor
@@ -39,7 +41,7 @@ installed. That's the entire local-machine bar.
 
 ## Architecture in a sentence
 
-`airstack osmo:up` (which wraps `osmo workflow submit`) spins up a GPU pod
+`airstack osmo up` (which wraps `osmo workflow submit`) spins up a GPU pod
 that runs sshd plus a Docker-in-Docker daemon. Inside that pod, `airstack
 up` brings up the familiar three AirStack containers (Isaac Sim,
 robot-desktop, GCS). Your IDE attaches over Remote-SSH; Isaac Sim and
@@ -107,7 +109,7 @@ From your AirStack clone, run:
 ```bash
 git clone https://github.com/castacks/AirStack.git
 cd AirStack
-./airstack.sh osmo:setup
+./airstack.sh osmo setup
 ```
 
 This prompts for your Andrew ID, AirLab Docker password, and Nucleus API
@@ -138,12 +140,12 @@ osmo credential list
 
 You should see all three (`airlab-docker-registry`, `airlab-docker-login`,
 `airlab-nucleus`). To rotate any of them later, just re-run
-`./airstack.sh osmo:setup`.
+`./airstack.sh osmo setup`.
 
 
Under the hood — the three raw `osmo credential set` calls -`airstack osmo:setup` (defined in +`airstack osmo setup` (defined in [`.airstack/modules/osmo.sh`](https://github.com/castacks/AirStack/blob/main/.airstack/modules/osmo.sh) as `cmd_osmo_setup`) is equivalent to running these three commands by hand — useful for debugging or rotating one credential at a time: @@ -196,7 +198,7 @@ Host airstack-osmo User root # Every OSMO workflow boots a fresh pod with a fresh sshd host key, so # any saved fingerprint for [localhost]:2200 will be wrong on the next - # `airstack osmo:up`. Skip the host-key check here: this alias only + # `airstack osmo up`. Skip the host-key check here: this alias only # connects via the local port-forward, so the security boundary is # OSMO's authenticated control-plane tunnel — not the SSH fingerprint. # /dev/null keeps known_hosts clean (no stale entries pile up); LogLevel @@ -221,17 +223,17 @@ EOF The `localhost:2200` is what we'll port-forward to in step 4. -> **Already added the old block?** If your `~/.ssh/config` still has -> `StrictHostKeyChecking accept-new` for `airstack-osmo` from an earlier -> setup, replace it with the three lines above. As a one-time cleanup of -> the stale fingerprint left behind by previous pods, also run: +> **`~/.ssh/config` already has a different `airstack-osmo` block?** If +> your config carries `StrictHostKeyChecking accept-new` for +> `airstack-osmo`, replace it with the three lines above, and clean out +> any saved fingerprint for the port-forward once: > > ```bash > ssh-keygen -R "[localhost]:2200" > ``` > -> `airstack osmo:ide` does this scrub for you on every run, so you only -> need it once when migrating. +> `airstack osmo ide` does this scrub for you on every run, so at most you +> need it once. > **Smoke-test the agent forward** once the pod is up: SSH in and run > `ssh-add -l` — you should see your local key listed. If you see "The @@ -243,7 +245,7 @@ The `localhost:2200` is what we'll port-forward to in step 4. From the AirStack clone: ```bash -./airstack.sh osmo:up --pool airstack +./airstack.sh osmo up --pool airstack ``` This submits @@ -263,11 +265,11 @@ with two things injected: > **The pod clones from GitHub, not your laptop.** Local edits (and > commits you haven't pushed) won't make it into the pod. `airstack -> osmo:up` warns you up-front if your branch is ahead of origin or has +> osmo up` warns you up-front if your branch is ahead of origin or has > uncommitted changes — `git push` first if you want the pod to pick > them up. -`airstack osmo:up` prints a workflow id like `airstack-dev-1` and stores +`airstack osmo up` prints a workflow id like `airstack-dev-1` and stores it in `~/.airstack/osmo-state`, so the rest of the `airstack osmo:*` commands in this tutorial pick it up automatically — no `export WF=...` needed. To target a specific workflow for a single invocation, export @@ -276,7 +278,7 @@ needed. To target a specific workflow for a single invocation, export
Under the hood — raw `osmo workflow submit` -`airstack osmo:up` (defined in +`airstack osmo up` (defined in [`.airstack/modules/osmo.sh`](https://github.com/castacks/AirStack/blob/main/.airstack/modules/osmo.sh) as `cmd_osmo_up`) is equivalent to: @@ -296,7 +298,7 @@ substitute it for `airstack osmo:*` in the rest of the tutorial. Tail the lead task's logs and watch for milestones: ```bash -./airstack.sh osmo:logs +./airstack.sh osmo logs ``` Expected milestones, in order (each is one line in the log): @@ -315,7 +317,7 @@ spinning up — the bring-up will continue in the background.
Under the hood — raw `osmo workflow logs` -`airstack osmo:logs` (defined in +`airstack osmo logs` (defined in [`.airstack/modules/osmo.sh`](https://github.com/castacks/AirStack/blob/main/.airstack/modules/osmo.sh) as `cmd_osmo_logs`) just exec's: @@ -336,7 +338,7 @@ stop. Override the task / tail length with `OSMO_LOGS_TASK` / In one terminal, run: ```bash -./airstack.sh osmo:ide +./airstack.sh osmo ide ``` This (a) starts the `localhost:2200 → pod:22` port-forward with a 24h @@ -367,7 +369,7 @@ You should see four containers: `airstack-isaac-sim-livestream-1`,
Under the hood — raw port-forward + manual IDE attach -`airstack osmo:ide` (defined in +`airstack osmo ide` (defined in [`.airstack/modules/osmo.sh`](https://github.com/castacks/AirStack/blob/main/.airstack/modules/osmo.sh) as `cmd_osmo_ide`) is equivalent to running the port-forward by hand: @@ -381,7 +383,7 @@ osmo workflow port-forward $WF workspace --port 2200:22 --connect-timeout 86400 `airstack-osmo`. - **Cursor:** the same flow under its remote-development menu. -Add `--no-open` to `airstack osmo:ide` to only run the port-forward and +Add `--no-open` to `airstack osmo ide` to only run the port-forward and attach the IDE manually.
@@ -415,7 +417,7 @@ Isaac Sim runs headless inside the pod with the Kit `isaac-sim-livestream` Compose profile). To view it locally: ```bash -./airstack.sh osmo:webrtc +./airstack.sh osmo webrtc ``` This spawns the UDP port-forward (media, `49099`) in the background and @@ -429,7 +431,7 @@ way it would on a local Linux desktop.
Under the hood — raw TCP + UDP port-forwards -`airstack osmo:webrtc` (defined in +`airstack osmo webrtc` (defined in [`.airstack/modules/osmo.sh`](https://github.com/castacks/AirStack/blob/main/.airstack/modules/osmo.sh) as `cmd_osmo_webrtc`) is equivalent to running the two raw port-forwards in separate terminals — Kit's WebRTC needs both TCP signaling and UDP @@ -454,7 +456,7 @@ AirStack Foxglove extensions locally and forward the websocket in one step: ```bash -./airstack.sh osmo:foxglove +./airstack.sh osmo foxglove ``` This copies the AirStack Foxglove extensions (Robot Tasks, Waypoint @@ -477,12 +479,12 @@ Desktop): The full Foxglove flow — layout import, panel customisation, DDS bridge naming — is documented at [Foxglove Visualization](../gcs/foxglove.md). The only OSMO-specific -difference is the `osmo:foxglove` line in front of it. +difference is the `osmo foxglove` line in front of it.
Under the hood — raw `osmo workflow port-forward` -`airstack osmo:foxglove` (defined in +`airstack osmo foxglove` (defined in [`.airstack/modules/osmo.sh`](https://github.com/castacks/AirStack/blob/main/.airstack/modules/osmo.sh) as `cmd_osmo_foxglove`) wraps the extension install plus: @@ -521,7 +523,7 @@ laptop, a fresh pod tomorrow, a colleague's machine. When you're done: ```bash -./airstack.sh osmo:down +./airstack.sh osmo down ``` This prints a 5-second warning then cancels the workflow stored in @@ -537,7 +539,7 @@ by accident.
Under the hood — raw `osmo workflow cancel` -`airstack osmo:down` (defined in +`airstack osmo down` (defined in [`.airstack/modules/osmo.sh`](https://github.com/castacks/AirStack/blob/main/.airstack/modules/osmo.sh) as `cmd_osmo_down`) is equivalent to: @@ -551,19 +553,19 @@ osmo workflow cancel $WF | Symptom | Likely cause | Fix | |---|---|---| -| `Remote-SSH: Connection refused` after a working session | Port-forward died (laptop slept, network blip) | Re-run `./airstack.sh osmo:ide` | -| `Permission denied (publickey)` on Remote-SSH | The pod authorised a different pubkey than the one your local SSH client is offering | Confirm `cat ~/.ssh/id_ed25519.pub` matches the key that was injected at submit time. Re-submit with `./airstack.sh osmo:down && ./airstack.sh osmo:up --pool airstack`. | -| `airstack osmo:logs` shows `ERROR: SSH_PUB_KEY not set` | The submit didn't inject a pubkey (e.g. you ran raw `osmo workflow submit` without `--set-env`) | `./airstack.sh osmo:down`, then resubmit with `./airstack.sh osmo:up --pool airstack` (it injects `SSH_PUB_KEY` automatically). | -| `docker pull` fails inside the pod with `unauthorized` | Your `airlab-docker-login` credential is missing or has the wrong Andrew ID/password | Re-run `./airstack.sh osmo:setup`. | +| `Remote-SSH: Connection refused` after a working session | Port-forward died (laptop slept, network blip) | Re-run `./airstack.sh osmo ide` | +| `Permission denied (publickey)` on Remote-SSH | The pod authorised a different pubkey than the one your local SSH client is offering | Confirm `cat ~/.ssh/id_ed25519.pub` matches the key that was injected at submit time. Re-submit with `./airstack.sh osmo down && ./airstack.sh osmo up --pool airstack`. | +| `airstack osmo logs` shows `ERROR: SSH_PUB_KEY not set` | The submit didn't inject a pubkey (e.g. you ran raw `osmo workflow submit` without `--set-env`) | `./airstack.sh osmo down`, then resubmit with `./airstack.sh osmo up --pool airstack` (it injects `SSH_PUB_KEY` automatically). | +| `docker pull` fails inside the pod with `unauthorized` | Your `airlab-docker-login` credential is missing or has the wrong Andrew ID/password | Re-run `./airstack.sh osmo setup`. | | Logs show `WARN: airlab-nucleus OSMO credential not set` and Isaac Sim asset loads fail, **or** Isaac Sim shows "Login Required: Unable to connect server omniverse://airlab-nucleus..." with the auth-service log showing `InternalCredentials.auth … 'username': '' … status: 'DENIED'` (no `Tokens.auth_with_api_token` call) | The pod is doing **password auth** instead of **API-token auth**. Inside the pod, `simulation/isaac-sim/docker/omni_pass.env` must have `OMNI_USER=$$omni-api-token` (literal `$$`, the sentinel for API-token auth — docker-compose v2 collapses `$$` to `$` on its way to the container). The OSMO entrypoint sets this automatically when `OMNI_PASS` looks like a JWT; if you see `OMNI_USER=` in the file, recreate the container with `docker compose --profile desktop --profile isaac-sim-livestream up -d isaac-sim-livestream` (`restart` does NOT re-read `env_file`). | -| Logs show `WARN: airlab-nucleus OSMO credential not set` and Isaac Sim asset loads fail, **or** Isaac Sim shows "Login Required: Unable to connect server omniverse://airlab-nucleus..." with the auth-service log showing `Tokens.auth_with_api_token … status: 'DENIED'` | Your `airlab-nucleus` API token is missing, expired, or revoked (rotation invalidates the predecessor). Confirm by SSH'ing the Nucleus host and running `sudo docker logs --tail 200 base_stack-nucleus-auth-1`. Regenerate the token at , then `./airstack.sh osmo:setup` and `./airstack.sh osmo:down && ./airstack.sh osmo:up --pool airstack` to resubmit (or live-edit `simulation/isaac-sim/docker/omni_pass.env` in the pod and recreate the `isaac-sim-livestream` container — see row above). | +| Logs show `WARN: airlab-nucleus OSMO credential not set` and Isaac Sim asset loads fail, **or** Isaac Sim shows "Login Required: Unable to connect server omniverse://airlab-nucleus..." with the auth-service log showing `Tokens.auth_with_api_token … status: 'DENIED'` | Your `airlab-nucleus` API token is missing, expired, or revoked (rotation invalidates the predecessor). Confirm by SSH'ing the Nucleus host and running `sudo docker logs --tail 200 base_stack-nucleus-auth-1`. Regenerate the token at , then `./airstack.sh osmo setup` and `./airstack.sh osmo down && ./airstack.sh osmo up --pool airstack` to resubmit (or live-edit `simulation/isaac-sim/docker/omni_pass.env` in the pod and recreate the `isaac-sim-livestream` container — see row above). | | Isaac Sim container restarts repeatedly | GPU not visible to the inner Docker daemon (toolkit not configured on the node) | Lab admin task. From inside the pod: `docker info \| grep -i runtime` should list `nvidia`. | | Isaac Sim is up but the WebRTC stream is blank | The Pegasus script isn't getting `--/app/livestream/enabled=true`, or the wrong Compose profile is active | In the integrated terminal: `docker logs airstack-isaac-sim-livestream-1`. Confirm `ISAAC_SIM_LIVESTREAM=true` and that the `isaac-sim-livestream` profile is the one running (`docker ps`). | -| Foxglove "no connection" | Port-forward died, GCS container hasn't started yet, or browser is caching an old connection | Re-run `./airstack.sh osmo:foxglove`; check `docker ps` shows `airstack-gcs-1` Up; try `ws://127.0.0.1:8766` instead of `ws://localhost:8766`. | +| Foxglove "no connection" | Port-forward died, GCS container hasn't started yet, or browser is caching an old connection | Re-run `./airstack.sh osmo foxglove`; check `docker ps` shows `airstack-gcs-1` Up; try `ws://127.0.0.1:8766` instead of `ws://localhost:8766`. | | First Remote-SSH connect takes forever | VS Code / Cursor downloading its remote server (~50 MB) into the fresh pod | Wait it out the first time. Subsequent connects to the same pod hit the cache. | -| **I forgot to push before tearing down** | The pod is still up; cancel hasn't fired yet | Don't run `./airstack.sh osmo:down`. SSH in via the existing port-forward (`./airstack.sh osmo:ide --no-open` if the tunnel is gone), push from the IDE terminal, *then* tear down. If the workflow has already terminated and the pod is gone, the work is gone — git is the only persistence layer. | +| **I forgot to push before tearing down** | The pod is still up; cancel hasn't fired yet | Don't run `./airstack.sh osmo down`. SSH in via the existing port-forward (`./airstack.sh osmo ide --no-open` if the tunnel is gone), push from the IDE terminal, *then* tear down. If the workflow has already terminated and the pod is gone, the work is gone — git is the only persistence layer. | -## What survives `airstack osmo:down`? +## What survives `airstack osmo down`? | Artifact | Lives in | Survives? | |---|---|---| @@ -583,7 +585,7 @@ git-tracked sense.** The Source Control panel is the persistence boundary. — lab-admin reference (pool prerequisites, OSMO credential registration, workspace image build, validation stages). - [Foxglove Visualization](../gcs/foxglove.md) — full layout import + - panel-customisation flow once your `airstack osmo:foxglove` is up. + panel-customisation flow once your `airstack osmo foxglove` is up. - [AGENTS.md](https://github.com/castacks/AirStack/blob/main/AGENTS.md) — inside-the-pod workflow once you're attached: `bws`, `sws`, `docker exec`, ROS 2 commands. diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md deleted file mode 100644 index 08dbbdaee..000000000 --- a/docs/tutorials/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# Tutorials - -Step-by-step guides for common AirStack workflows. If you are new, start with **Getting Started**. - -| Tutorial | Description | -|---|---| -| [Getting Started](../getting_started.md) | Install AirStack, pull Docker images, launch a simulated robot, and fly it for the first time. | -| [AirStack on OSMO (Mac/Windows OK)](airstack_on_osmo.md) | Develop on AirStack from a Mac, Windows, or no-GPU Linux laptop using NVIDIA OSMO + VS Code/Cursor Remote-SSH. No local Docker or local `airstack install`; use a local repo clone for the `airstack osmo:*` wrappers and workflow YAML. | -| [Multi-Robot Simulation](multi_robot_simulation.md) | Spin up multiple simulated robots in Isaac Sim and verify independent ROS 2 namespaces. | -| [Autonomy Modes](autonomy_modes.md) | Understand `onboard_all`, `onboard_local`, and `offboard_global` modes and the commands to run each. | -| [Deploying to Hardware](deploying_to_hardware.md) | Flash a Jetson or VOXL device, configure the robot hostname, and run the autonomy stack on a real drone. | diff --git a/gcs/docker/.bash_history b/gcs/docker/.bash_history deleted file mode 100644 index c656cdd8b..000000000 --- a/gcs/docker/.bash_history +++ /dev/null @@ -1,10 +0,0 @@ -tmux a -tmux ls -cd ~/ros_ws -ros2 launch gcs_bringup gcs.launch.xml -cws -bws -sws -bws --packages-select gcs_bringup -mosquitto_sub -h localhost -t healthcheck -u airlab -mosquitto_sub -h localhost -t to_tak -u airlab diff --git a/gcs/docker/.bashrc b/gcs/docker/.bashrc index ba851a34a..e8415b72d 100644 --- a/gcs/docker/.bashrc +++ b/gcs/docker/.bashrc @@ -60,6 +60,31 @@ function cws(){ fi } +# Build → source → launch, with an unmissable banner when a stage fails. +# Used by the docker-compose AUTOLAUNCH tmux commands: a bare +# `bws && sws && ros2 launch ...` dies silently on a build failure — the tmux +# pane just returns to a prompt and `docker logs` shows nothing — so bringup +# failures went unnoticed. Also fine to use interactively. +function _autolaunch_banner(){ + local line='!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + printf '\n\033[1;97;41m%s\033[0m\n' "$line" " AUTOLAUNCH FAILED on $(hostname) (GCS)" " $1" " Scroll up in this tmux pane (airstack connect) or 'airstack logs'" " for the first error." "$line" + # Plain repeat so the message survives log processors that strip ANSI. + printf '%s\n' "AUTOLAUNCH FAILED: $1" +} +function autolaunch(){ + if ! bws; then + _autolaunch_banner "colcon build (bws) failed — the stack was NOT launched" + return 1 + fi + sws + ros2 launch "$@" + local rc=$? + if [ $rc -ne 0 ]; then + _autolaunch_banner "ros2 launch $* exited with code $rc — the stack is DOWN" + return $rc + fi +} + source /opt/ros/jazzy/setup.bash sws # source the ROS2 workspace by default diff --git a/gcs/docker/.env b/gcs/docker/.env deleted file mode 100644 index bd5e60580..000000000 --- a/gcs/docker/.env +++ /dev/null @@ -1,39 +0,0 @@ -PROJECT_NAME="airstack" -PROJECT_VERSION="0.12.0" -PROJECT_DOCKER_REGISTRY="airlab-storage.andrew.cmu.edu:442/airstack" - -# ROS -------------------------------------------------------- -ROS_WS_DIR=/home/gcs/ros_ws - - - - - -# ROS2TAK_TOOLS -------------------------------------------------------- - -# NOTE: Update the config file -ROS2TAK_TOOLS_CONFIG_DIR=src/ros2tak_tools/config -ROS2TAK_TOOLS_CONFIG_FILENAME=config.yaml - -TAK_PUBLISHER_FILEPATH=src/ros2tak_tools/scripts/tak_publisher.py -TAK_SUBSCRIBER_FILEPATH=src/ros2tak_tools/scripts/tak_subscriber.py - -MQTT_USERNAME= # Enter the MQTT username -MQTT_PASSWORD= # Enter the MQTT password - -# Chat interface configuration ----------------------------------------- -AI_AGENT_NAME=aerolens.ai (BOT) - -# GSTREAMER TO ROS NODES ----------------------------------------------- - -# CAMERA 1 -CAMERA1_STREAM_IP=rtsp://10.4.1.33:554/vio -CAMERA1_ROS_TOPIC=/view1/image_raw - -# CAMERA 2 -CAMERA2_STREAM_IP=rtsp:// -CAMERA2_ROS_TOPIC=/view2/image_raw - - -# ROS VARIABLES ------------------------------------------------------- -ROS_DOMAIN_ID=200 \ No newline at end of file diff --git a/gcs/docker/Dockerfile.gcs b/gcs/docker/Dockerfile.gcs index 63bccb34d..1320aa769 100644 --- a/gcs/docker/Dockerfile.gcs +++ b/gcs/docker/Dockerfile.gcs @@ -11,12 +11,7 @@ RUN apt-get -o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngrad ros-dev-tools ros-jazzy-mavros ros-jazzy-tf2* ros-jazzy-stereo-image-proc \ ros-jazzy-image-view ros-jazzy-topic-tools ros-jazzy-grid-map \ ros-jazzy-domain-bridge ros-jazzy-ros2cli python3-colcon-common-extensions \ - libglib2.0-dev libcgal-dev mosquitto mosquitto-clients \ - libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ - libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base \ - gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ - gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools \ - gstreamer1.0-x gstreamer1.0-alsa openssh-server \ + libglib2.0-dev libcgal-dev openssh-server \ xterm gnome-terminal libcanberra-gtk-module libcanberra-gtk3-module dbus-x11 \ ros-jazzy-rosbag2-storage-mcap ros-jazzy-plotjuggler \ && rm -rf /var/lib/apt/lists/* @@ -40,8 +35,6 @@ RUN pip3 install --break-system-packages \ six \ toml \ scipy \ - pytak \ - paho-mqtt \ sphinx \ utm @@ -58,7 +51,11 @@ RUN git clone --depth 1 https://github.com/tmux-plugins/tpm /root/.tmux/plugins/ (sleep 5 && git clone --depth 1 https://github.com/tmux-plugins/tpm /root/.tmux/plugins/tpm) # install FoxGloveStudio and ros-jazzy-foxglove-bridge -RUN wget -q https://get.foxglove.dev/desktop/latest/foxglove-studio-latest-linux-amd64.deb -O /tmp/foxglove-studio.deb \ +# Pinned (not "latest"): gcs/foxglove_extensions/render_layout.py seeds layouts +# into the app's local layout store, whose on-disk format we verified against +# this exact version. Re-verify the store format before bumping. +ARG FOXGLOVE_VERSION=3.0.0 +RUN wget -q https://get.foxglove.dev/desktop/v${FOXGLOVE_VERSION}/foxglove-studio-${FOXGLOVE_VERSION}-linux-amd64.deb -O /tmp/foxglove-studio.deb \ && apt -o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngradeToInsecureRepositories=true update \ && apt -o APT::Get::AllowUnauthenticated=true install -y --no-install-recommends /tmp/foxglove-studio.deb ros-jazzy-foxglove-bridge \ && rm /tmp/foxglove-studio.deb \ diff --git a/gcs/docker/docker-compose.yaml b/gcs/docker/docker-compose.yaml index b73f9bc82..4dac5ec02 100644 --- a/gcs/docker/docker-compose.yaml +++ b/gcs/docker/docker-compose.yaml @@ -1,7 +1,7 @@ services: gcs: profiles: - - desktop # standard sim/dev: GCS alongside robot-desktop (full role) + - desktop # standard sim/dev: GCS alongside robot-desktop (unsplit stacks, e.g. full_default) - desktop_split # split sim/dev: GCS alongside robot-desktop-onboard + robot-offboard extends: file: ./gcs-base-docker-compose.yaml @@ -25,5 +25,3 @@ services: network_mode: host volumes: - $HOME/bags:/bags - # TODO: Update rviz config location if needed - - ../plot:/plot diff --git a/gcs/docker/gcs-base-docker-compose.yaml b/gcs/docker/gcs-base-docker-compose.yaml index 8894bcb9f..c8bb6d654 100644 --- a/gcs/docker/gcs-base-docker-compose.yaml +++ b/gcs/docker/gcs-base-docker-compose.yaml @@ -13,12 +13,12 @@ services: - *gcs_cache command: > bash -c " - ssh service restart; + service ssh restart; python3 /root/AirStack/gcs/foxglove_extensions/install.py; python3 /root/AirStack/gcs/foxglove_extensions/render_layout.py; tmux new -d -s bringup; if [ $$AUTOLAUNCH = 'true' ]; then - tmux send-keys -t bringup:0.0 'bws && sws; ros2 launch desktop_bringup gcs.launch.xml' ENTER; + tmux send-keys -t bringup:0.0 'autolaunch desktop_bringup gcs.launch.xml' ENTER; fi; sleep infinity" # Interactive shell @@ -36,8 +36,14 @@ services: - DISPLAY=${DISPLAY} - QT_X11_NO_MITSHM=1 - NVIDIA_DRIVER_CAPABILITIES=all - # Number of robots (used by action_relay to spawn per-robot relays) + # Robot roster for per-robot relays/layouts. Fleet-first: when + # FLEET_CONFIG_FILE is set (airstack up --fleet ), action_relay + # derives (robot_name, domain) pairs from the fleet file; otherwise the + # legacy NUM_ROBOTS count spawns robot_1..robot_N on domains 1..N. + # ROBOT_RELAY_MAP ("name:domain,...") overrides both. - NUM_ROBOTS=${NUM_ROBOTS:-1} + - FLEET_CONFIG_FILE=${FLEET_CONFIG_FILE:-} + - ROBOT_RELAY_MAP=${ROBOT_RELAY_MAP:-} # Record bags - RECORD_BAGS=${RECORD_BAGS} image: ${PROJECT_DOCKER_REGISTRY}/${PROJECT_NAME}:v${VERSION}_gcs @@ -66,5 +72,9 @@ services: - ../../common/ros_packages:/root/AirStack/gcs/ros_ws/src/common:rw - ../../common/fastdds.xml:/root/AirStack/gcs/ros_ws/src/fastdds.xml - ../ros_ws:/root/AirStack/gcs/ros_ws:rw + # fleet + vehicle configs and the fleet resolver (read by action_relay + # when FLEET_CONFIG_FILE points at /root/AirStack/config/fleets/.yaml) + - ../../config:/root/AirStack/config:ro + - ../../tools/fleet:/root/AirStack/tools/fleet:ro # bags - ../bags:/bags:rw \ No newline at end of file diff --git a/gcs/docker/resources/fastrtps-profile.xml b/gcs/docker/resources/fastrtps-profile.xml deleted file mode 100644 index edacf5020..000000000 --- a/gcs/docker/resources/fastrtps-profile.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - CustomUdpTransport - UDPv4 - - - - - - - CustomUdpTransport - - - false - - - diff --git a/gcs/foxglove_extensions/render_layout.py b/gcs/foxglove_extensions/render_layout.py index 103736aa9..b51abc0d7 100644 --- a/gcs/foxglove_extensions/render_layout.py +++ b/gcs/foxglove_extensions/render_layout.py @@ -1,15 +1,30 @@ #!/usr/bin/env python3 -"""Render airstack_layout_custom.json with NUM_ROBOTS tabs from a single-robot template. +"""Render and auto-load a NUM_ROBOTS-tab Foxglove layout from a single-robot template. Foxglove layout JSON has no native templating, so we generate it at GCS startup based on the NUM_ROBOTS env var. Tab[0] of the input file is treated as the canonical robot_1 template; we replicate it for robots 1..NUM_ROBOTS, mint unique panel IDs per tab, and patch the 3D panel's per-robot transforms / topics / namespaces to cover the same range. + +Besides the manual-import copy in /root/, the rendered layout is seeded +directly into the Foxglove desktop app's local layout store +(/studio-datastores/layouts-local/ — one plain-JSON file +per layout; verified against foxglove-studio 3.0.0) under the deterministic id +``airstack_default__robots``. gcs.launch.xml then opens Foxglove with a +``layoutId=airstack_default__robots`` deep link, so the right layout is +active on startup with no manual import. + +User edits are preserved: a sidecar state file (/airstack/ +seed_state.json) records the hash of what we last seeded, and a store record +whose baseline no longer matches that hash (i.e. the user saved their own +changes) is never overwritten. Deleting the layout in the Foxglove UI resets +it — the next container start re-seeds a fresh copy. """ import argparse import copy +import hashlib import json import os import re @@ -161,6 +176,87 @@ def expand_layout(template_json: dict, num_robots: int) -> dict: return out +def layout_id(num_robots: int) -> str: + """Deterministic local-layout id; must match the layoutId deep link in + desktop_bringup gcs.launch.xml.""" + return f'airstack_default_{num_robots}_robots' + + +def _hash_layout(data: dict) -> str: + return hashlib.sha256( + json.dumps(data, sort_keys=True).encode()).hexdigest() + + +def _write_json_atomic(path: str, obj) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = path + '.tmp' + with open(tmp, 'w') as f: + json.dump(obj, f, indent=2) + os.replace(tmp, path) + + +def seed_layout_store(rendered: dict, num_robots: int, userdata: str) -> None: + """Seed the rendered layout into Foxglove desktop's local layout store. + + Store format (foxglove-studio 3.0.0): one JSON file per layout at + /studio-datastores/layouts-local/ with + {id, name, permission, baseline:{data, savedAt}}. The dir is enumerated by + readdir, so nothing but layout records may live in it — the seeder's own + state goes to /airstack/seed_state.json instead. + """ + from datetime import datetime, timezone + + lid = layout_id(num_robots) + store_dir = os.path.join(userdata, 'studio-datastores', 'layouts-local') + record_path = os.path.join(store_dir, lid) + state_path = os.path.join(userdata, 'airstack', 'seed_state.json') + + state = {} + if os.path.exists(state_path): + try: + with open(state_path) as f: + state = json.load(f) + except (json.JSONDecodeError, OSError): + state = {} + + new_hash = _hash_layout(rendered) + + existing_baseline_hash = None + if os.path.exists(record_path): + try: + with open(record_path) as f: + existing_baseline_hash = _hash_layout( + json.load(f)['baseline']['data']) + except (json.JSONDecodeError, KeyError, OSError, TypeError): + existing_baseline_hash = None # corrupt record → reseed + + if existing_baseline_hash is not None: + last_seeded = state.get(lid) + if existing_baseline_hash != last_seeded: + print(f'layout "{lid}" has user-saved edits — leaving it alone ' + '(delete it in Foxglove\'s Layouts menu to reset to the ' + 'generated default)') + return + if existing_baseline_hash == new_hash: + print(f'layout "{lid}" already up to date in {store_dir}') + return + + record = { + 'id': lid, + 'name': f'AirStack default ({num_robots} ' + f'robot{"s" if num_robots != 1 else ""})', + 'permission': 'CREATOR_WRITE', + 'baseline': { + 'data': rendered, + 'savedAt': datetime.now(timezone.utc).isoformat(), + }, + } + _write_json_atomic(record_path, record) + state[lid] = new_hash + _write_json_atomic(state_path, state) + print(f'seeded layout "{lid}" → {record_path}') + + def main(): ap = argparse.ArgumentParser() ap.add_argument('--input', help='Source template JSON (LAYOUT_TEMPLATE env)', @@ -173,6 +269,12 @@ def main(): default=os.environ.get('LAYOUT_OUTPUT')) ap.add_argument('--num-robots', type=int, default=int(os.environ.get('NUM_ROBOTS', '1'))) + ap.add_argument('--foxglove-userdata', + help='Foxglove desktop userData dir to seed the layout ' + 'into (FOXGLOVE_USERDATA env); pass an empty string to ' + 'skip seeding.', + default=os.environ.get('FOXGLOVE_USERDATA', + '/root/.config/Foxglove')) args = ap.parse_args() if args.output is None: args.output = f'/root/airstack_layout_num_robots_{args.num_robots}.json' @@ -181,13 +283,12 @@ def main(): template = json.load(f) rendered = expand_layout(template, args.num_robots) - os.makedirs(os.path.dirname(args.output), exist_ok=True) - tmp = args.output + '.tmp' - with open(tmp, 'w') as f: - json.dump(rendered, f, indent=2) - os.replace(tmp, args.output) + _write_json_atomic(args.output, rendered) print(f'rendered {args.num_robots}-robot layout → {args.output}') + if args.foxglove_userdata: + seed_layout_store(rendered, args.num_robots, args.foxglove_userdata) + if __name__ == '__main__': main() diff --git a/gcs/foxglove_extensions/robot-commands.foxe b/gcs/foxglove_extensions/robot-commands.foxe deleted file mode 100644 index f14869ba2..000000000 Binary files a/gcs/foxglove_extensions/robot-commands.foxe and /dev/null differ diff --git a/gcs/ros_ws/src/action_relay/action_relay/relay_node.py b/gcs/ros_ws/src/action_relay/action_relay/relay_node.py index 00491633c..efd7ec25b 100644 --- a/gcs/ros_ws/src/action_relay/action_relay/relay_node.py +++ b/gcs/ros_ws/src/action_relay/action_relay/relay_node.py @@ -47,11 +47,18 @@ ExplorationTask, ) -# ── Map-frame ENU origin (must match gcs_visualizer/gcs_utils.py) ──────────── +# ── Map-frame ENU origin — Lisbon (must match gcs_visualizer/gcs_utils.py) ─── # Foxglove panels publish waypoints/polygons in this same global ENU frame, and # the gcs_visualizer renders the robot at gps_to_enu(...) - boot. The robot's # task executors expect coordinates relative to its own boot pose, so we # subtract the robot's boot ENU position before forwarding. +# +# KEEP IN SYNC: the same anchor lives in three other files (single-sourcing +# across container/mount boundaries is deferred — documented in the audit): +# - common/ros_packages/coordination/coordination_bringup/coordination_bringup/frame_utils.py +# - gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/gcs_utils.py +# - simulation/isaac-sim/launch_scripts/gps_utils.py (isaac container) +# If you change the anchor here, change all four together. ORIGIN_LAT = 38.736832 ORIGIN_LON = -9.137977 diff --git a/gcs/ros_ws/src/action_relay/launch/action_relay.launch.py b/gcs/ros_ws/src/action_relay/launch/action_relay.launch.py index 270a73a8b..f390c0ecb 100644 --- a/gcs/ros_ws/src/action_relay/launch/action_relay.launch.py +++ b/gcs/ros_ws/src/action_relay/launch/action_relay.launch.py @@ -1,15 +1,53 @@ """Launch one action_relay node per robot. -Defaults to robot_1..robot_N with domain IDs 1..N (NUM_ROBOTS env, default 1). -Override with ROBOT_RELAY_MAP="robot_1:1,robot_2:2,..." when the -robot_name -> domain mapping in default_robot_name_map.yaml has been customized. +Robot roster resolution (first match wins): + +1. ROBOT_RELAY_MAP="robot_1:1,robot_2:2,..." — explicit override for custom + robot_name -> domain mappings. +2. FLEET_CONFIG_FILE (set by ``airstack up --fleet ``) — the fleet file + names the robots; ``network.domain_policy: auto`` assigns robot N (1-based + file order) -> domain N, the same rule as tools/fleet/resolve_fleet.py. +3. Legacy fallback: NUM_ROBOTS (default 1) -> robot_1..robot_N with domain + IDs 1..N, matching default_robot_name_map.yaml. """ import os +import sys + from launch import LaunchDescription from launch_ros.actions import Node +def _fleet_robots(fleet_config_file): + """(robot_name, domain) pairs from a fleet file (RFC #380). + + Prefers importing the canonical resolver from the checkout mounted at + ``/tools/fleet`` (the fleet file lives at + ``/config/fleets/.yaml``, so ```` is derived from the + file's own path — /root/AirStack inside the GCS container). If that mount + is missing, falls back to reading the YAML directly with the same + ``domain_policy: auto`` rule (robot N, 1-based file order -> domain N). + """ + fleet_path = os.path.abspath(fleet_config_file) + root = os.path.dirname(os.path.dirname(os.path.dirname(fleet_path))) + tools_fleet = os.path.join(root, 'tools', 'fleet') + if tools_fleet not in sys.path: + sys.path.insert(0, tools_fleet) + try: + from resolve_fleet import load_fleet # single source of fleet parsing + fleet = load_fleet(fleet_path) + except ImportError: + # tools/fleet isn't mounted in this container — parse the YAML inline + # (same schema, same auto domain rule; keep the two in sync). + import yaml + with open(fleet_path, encoding='utf-8') as f: + fleet = yaml.safe_load(f) or {} + robots = fleet.get('robots') or {} + if not robots: + raise ValueError(f'fleet file {fleet_path} has no robots:') + return [(name, i) for i, name in enumerate(robots, start=1)] + + def _parse_robots(): override = os.environ.get('ROBOT_RELAY_MAP', '').strip() if override: @@ -20,6 +58,9 @@ def _parse_robots(): raise ValueError(f"ROBOT_RELAY_MAP entry '{entry}' must be name:domain") out.append((name, int(domain))) return out + fleet_config_file = os.environ.get('FLEET_CONFIG_FILE', '').strip() + if fleet_config_file and os.path.isfile(fleet_config_file): + return _fleet_robots(fleet_config_file) n = int(os.environ.get('NUM_ROBOTS', '1')) return [(f'robot_{i}', i) for i in range(1, n + 1)] diff --git a/gcs/ros_ws/src/action_relay/package.xml b/gcs/ros_ws/src/action_relay/package.xml index f9fe0fdbc..24e9aa16d 100644 --- a/gcs/ros_ws/src/action_relay/package.xml +++ b/gcs/ros_ws/src/action_relay/package.xml @@ -4,8 +4,8 @@ action_relay 0.0.1 Relay ROS 2 actions across DDS domains (GCS domain 0 to robot domain N) - AirLab CMU - MIT + Andrew Jong + BSD-3-Clause-Clear rclpy task_msgs diff --git a/gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/gcs_utils.py b/gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/gcs_utils.py index e9da61fc8..870c3d489 100644 --- a/gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/gcs_utils.py +++ b/gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/gcs_utils.py @@ -11,6 +11,13 @@ transform_point_cloud2 as _transform_pc2, ) +# ── Global ENU world origin — Lisbon (the Pegasus configs.yaml default) ────── +# KEEP IN SYNC: the same anchor lives in three other files (single-sourcing +# across container/mount boundaries is deferred — documented in the audit): +# - common/ros_packages/coordination/coordination_bringup/coordination_bringup/frame_utils.py +# - gcs/ros_ws/src/action_relay/action_relay/relay_node.py +# - simulation/isaac-sim/launch_scripts/gps_utils.py (isaac container) +# If you change the anchor here, change all four together. ORIGIN_LAT = 38.736832 ORIGIN_LON = -9.137977 ORIGIN_ALT = 90.0 diff --git a/gcs/ros_ws/src/gcs_visualizer/package.xml b/gcs/ros_ws/src/gcs_visualizer/package.xml index 4b4d65d0a..76b8f7852 100644 --- a/gcs/ros_ws/src/gcs_visualizer/package.xml +++ b/gcs/ros_ws/src/gcs_visualizer/package.xml @@ -4,8 +4,8 @@ gcs_visualizer 0.0.1 GCS visualization node: publishes drone mesh markers in a global ENU frame for Foxglove - AirLab CMU - MIT + Andrew Jong + BSD-3-Clause-Clear rclpy sensor_msgs diff --git a/gcs/ros_ws/src/ros2tak_tools/README.md b/gcs/ros_ws/src/ros2tak_tools/README.md deleted file mode 100644 index 4c4a1208e..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# TAK Tools ROS 2 Package - -`ros2tak_tools` is a ROS 2 package designed for integrating TAK (Tactical Assault Kit) functionalities within ROS 2. It includes tools for publishing and subscribing to Cursor-On-Target (CoT) events, interfacing with TAK servers, and setting up search and rescue missions. - - -## Features -- **ROS 2 to TAK Communication**: Send ROS 2 messages to TAK using CoT events. -- **CoT to ROS 2 Communication**: Receive CoT events from TAK and publish as ROS 2 messages. -- **Mission Planning**: Custom tools to create and manage search missions using CoT and ROS. - - diff --git a/gcs/ros_ws/src/ros2tak_tools/config/.gitignore b/gcs/ros_ws/src/ros2tak_tools/config/.gitignore deleted file mode 100644 index 5b6b0720c..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/config/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.yaml diff --git a/gcs/ros_ws/src/ros2tak_tools/config/demo_config.yaml b/gcs/ros_ws/src/ros2tak_tools/config/demo_config.yaml deleted file mode 100644 index 2ec3da59e..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/config/demo_config.yaml +++ /dev/null @@ -1,110 +0,0 @@ -# Configuration for the Basestation Syetem interacting ROS and TAK server. - -# Preliminary: -# Tak-server: Server that receives CoT messages and sends them to the TAK clients (e.g., ATAK, WinTAK, etc.) -# COT (Cursor on Target) message: A message format used by TAK clients to share location and other information with/without Tak-server. - -# Instructions: -# (1) Please read the comments in the configuration file to understand the configuration parameters. -# (2) The confguration key names are case-sensitive. Please use the same key names as mentioned in the comments. - -# Maintainer: Aditya Rauniyar (rauniyar@cmu.edu) - - -project: - name: Airstack # Name of the project. This will be used to name the services and set the UUID of all the COT messages. - -gps_streaming: - - name: 'drone1' - type: 'uav' # Type of the robot (e.g., uav, quadruped, offroad) - topicname: '/robot_1/interface/mavros/global_position/raw/fix' - frequency: 1 # Frequency at which the GPS data should be published to TAK server in Hz. - -trackers: - - name: 'base' # Note stable GPS reading - ip: '10.223.132.129' - input_port: 2947 - - name: 'target2' # Name of the tracker. This can be the target or the robot streaming GPS data. - ip: '10.223.118.110' # IP address of the tracker. (Testing using Doodle labs mesh rider) - input_port: 2947 # Port of the Radio link to receive GPS data. - - -tak_server: - cot_url: # URL for the TAK server where CoT events are sent. - pytak_tls_client_cert: # Path to the client certificate for TLS connection. - pytak_tls_client_key: # Path to the client key for TLS connection. - - -mqtt: - host: localhost - port: 1883 - username: airlab - password: - -services: - host: '127.0.0.1' # Host settings can be specified here (e.g., localhost or specific IP address). - - # NOTE: - # (1) The publishers and subscribers are in reference to the TAK server. - # (2) The name of the service would be in the format of _ (e.g., dsta_tak_publisher). - publisher: - tak_publisher: - # this serivce is used to publish CoT messages from HOSTIP:PORT to the TAK server. - topic_name: to_tak - - mediator: - ros2cot_agent: - # this service is used to generate COT messages from ROS messages and send them to HOSTIP:PORT. - topic_name: to_tak - cot2ros_agent: - # this service is used to generate ROS messages from HOSTIP:PORT to ROS topics. - # TAK_Subscriber (below) service has more information on the ROS topics. - topic_name: from_tak # Port for the ROS publisher service. - cot2planner_agent: - # this service is used to generate ROS messages from HOSTIP:PORT to ROS topics. - # TAK_Subscriber (below) service has more information on the ROS topics. - topic_name: planner_events # Topic name at MQTT for the subscriber service that sends COT messages subscribed from the TAK server. - - chat2ros_agent: - mqtt_subcribe_topic: dsta-operator # Topic name at MQTT for the subscriber service that sends COT messages subscribed from the TAK server. - ros_query_text_topic: '/query/text' # ROS Topic name to publish the chat queries. - ros_query_response_topic: '/query/response' # ROS Topic name to publish the chat responses. - filter_name: dsta-operator - - ros2casevac_agent: - # this service is used to generate ROS messages from HOSTIP:PORT to ROS topics. - # TAK_Subscriber (below) service has more information on the ROS topics. - topic_name: to_tak # MQTT topic name to send the COT messages to. - ros_casualty_meta_topic_name: '/casualty/meta' # ROS Topic name to publish the casevac messages. - ros_casualty_image_topic_name: '/casualty/image' # ROS message type for the casevac messages. - - subscriber: - tak_subscriber: - # this service is used to subscribe to CoT messages from TAK server and send them to HOSTIP:PORT. - filter_messages: # Type of messages to subscribe to. Options: - - name: 'target' - # ROS Topic name to publish the target messages. Use {n} as a placeholder for the robot number. - # If provided, the topic name will be formatted with the robot number extracted from the message name. - ros_topic_name: '/target{n}/gps/gt' - ros_msg_type: NavSatFix # ROS message type for the target messages. - mqtt_topic_name: target_from_tak # Topic name at MQTT for the subscriber service that sends COT messages subscribed from the TAK server. - - name: 'iphone' - # ROS Topic name to publish the target messages. Use {n} as a placeholder for the robot number. - # If provided, the topic name will be formatted with the robot number extracted from the message name. - ros_topic_name: '/iphone{n}/gps/gt' - ros_msg_type: NavSatFix # ROS message type for the target messages. - mqtt_topic_name: iphone_from_tak # Topic name at MQTT for the subscriber service that sends COT messages subscribed from the TAK server. - - name: 'base' - ros_topic_name: '/basestation/gps' - ros_msg_type: NavSatFix # ROS message type for the target messages. - mqtt_topic_name: base_from_tak # Topic name at MQTT for the subscriber service that sends COT messages subscribed from the TAK server. - - name: 'planner' - ros_topic_name: '/planner/planconfig' # ROS Topic name to publish the shapes messages. - ros_msg_type: MarkerArray # ROS message type for the shapes messages. - mqtt_topic_name: dsta-operator # Topic name at MQTT for the subscriber service that sends COT messages subscribed from the TAK server. - - name: 'dsta-operator' - ros_topic_name: 'NA' # ROS Topic name to publish the shapes messages. - ros_msg_type: NA # ROS message type for the shapes messages. - mqtt_topic_name: dsta-operator # Topic name at MQTT for the subscriber service that sends COT messages subscribed from the TAK server. - # target: Target messages - # planner: Planner messages \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/launch/tak.launch.xml b/gcs/ros_ws/src/ros2tak_tools/launch/tak.launch.xml deleted file mode 100644 index 8e5f95deb..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/launch/tak.launch.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gcs/ros_ws/src/ros2tak_tools/mosquitto/config/mosquitto.conf b/gcs/ros_ws/src/ros2tak_tools/mosquitto/config/mosquitto.conf deleted file mode 100755 index 9cec7c979..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/mosquitto/config/mosquitto.conf +++ /dev/null @@ -1,905 +0,0 @@ -# Config file for mosquitto -# -# See mosquitto.conf(5) for more information. -# -# Default values are shown, uncomment to change. -# -# Use the # character to indicate a comment, but only if it is the -# very first character on the line. - -# ================================================================= -# General configuration -# ================================================================= - -# Use per listener security settings. -# -# It is recommended this option be set before any other options. -# -# If this option is set to true, then all authentication and access control -# options are controlled on a per listener basis. The following options are -# affected: -# -# acl_file -# allow_anonymous -# allow_zero_length_clientid -# auto_id_prefix -# password_file -# plugin -# plugin_opt_* -# psk_file -# -# Note that if set to true, then a durable client (i.e. with clean session set -# to false) that has disconnected will use the ACL settings defined for the -# listener that it was most recently connected to. -# -# The default behaviour is for this to be set to false, which maintains the -# setting behaviour from previous versions of mosquitto. -#per_listener_settings false - - -# This option controls whether a client is allowed to connect with a zero -# length client id or not. This option only affects clients using MQTT v3.1.1 -# and later. If set to false, clients connecting with a zero length client id -# are disconnected. If set to true, clients will be allocated a client id by -# the broker. This means it is only useful for clients with clean session set -# to true. -#allow_zero_length_clientid true - -# If allow_zero_length_clientid is true, this option allows you to set a prefix -# to automatically generated client ids to aid visibility in logs. -# Defaults to 'auto-' -#auto_id_prefix auto- - -# This option affects the scenario when a client subscribes to a topic that has -# retained messages. It is possible that the client that published the retained -# message to the topic had access at the time they published, but that access -# has been subsequently removed. If check_retain_source is set to true, the -# default, the source of a retained message will be checked for access rights -# before it is republished. When set to false, no check will be made and the -# retained message will always be published. This affects all listeners. -#check_retain_source true - -# QoS 1 and 2 messages will be allowed inflight per client until this limit -# is exceeded. Defaults to 0. (No maximum) -# See also max_inflight_messages -#max_inflight_bytes 0 - -# The maximum number of QoS 1 and 2 messages currently inflight per -# client. -# This includes messages that are partway through handshakes and -# those that are being retried. Defaults to 20. Set to 0 for no -# maximum. Setting to 1 will guarantee in-order delivery of QoS 1 -# and 2 messages. -#max_inflight_messages 20 - -# For MQTT v5 clients, it is possible to have the server send a "server -# keepalive" value that will override the keepalive value set by the client. -# This is intended to be used as a mechanism to say that the server will -# disconnect the client earlier than it anticipated, and that the client should -# use the new keepalive value. The max_keepalive option allows you to specify -# that clients may only connect with keepalive less than or equal to this -# value, otherwise they will be sent a server keepalive telling them to use -# max_keepalive. This only applies to MQTT v5 clients. The default, and maximum -# value allowable, is 65535. -# -# Set to 0 to allow clients to set keepalive = 0, which means no keepalive -# checks are made and the client will never be disconnected by the broker if no -# messages are received. You should be very sure this is the behaviour that you -# want. -# -# For MQTT v3.1.1 and v3.1 clients, there is no mechanism to tell the client -# what keepalive value they should use. If an MQTT v3.1.1 or v3.1 client -# specifies a keepalive time greater than max_keepalive they will be sent a -# CONNACK message with the "identifier rejected" reason code, and disconnected. -# -#max_keepalive 65535 - -# For MQTT v5 clients, it is possible to have the server send a "maximum packet -# size" value that will instruct the client it will not accept MQTT packets -# with size greater than max_packet_size bytes. This applies to the full MQTT -# packet, not just the payload. Setting this option to a positive value will -# set the maximum packet size to that number of bytes. If a client sends a -# packet which is larger than this value, it will be disconnected. This applies -# to all clients regardless of the protocol version they are using, but v3.1.1 -# and earlier clients will of course not have received the maximum packet size -# information. Defaults to no limit. Setting below 20 bytes is forbidden -# because it is likely to interfere with ordinary client operation, even with -# very small payloads. -#max_packet_size 0 - -# QoS 1 and 2 messages above those currently in-flight will be queued per -# client until this limit is exceeded. Defaults to 0. (No maximum) -# See also max_queued_messages. -# If both max_queued_messages and max_queued_bytes are specified, packets will -# be queued until the first limit is reached. -#max_queued_bytes 0 - -# Set the maximum QoS supported. Clients publishing at a QoS higher than -# specified here will be disconnected. -#max_qos 2 - -# The maximum number of QoS 1 and 2 messages to hold in a queue per client -# above those that are currently in-flight. Defaults to 1000. Set -# to 0 for no maximum (not recommended). -# See also queue_qos0_messages. -# See also max_queued_bytes. -#max_queued_messages 1000 -# -# This option sets the maximum number of heap memory bytes that the broker will -# allocate, and hence sets a hard limit on memory use by the broker. Memory -# requests that exceed this value will be denied. The effect will vary -# depending on what has been denied. If an incoming message is being processed, -# then the message will be dropped and the publishing client will be -# disconnected. If an outgoing message is being sent, then the individual -# message will be dropped and the receiving client will be disconnected. -# Defaults to no limit. -#memory_limit 0 - -# This option sets the maximum publish payload size that the broker will allow. -# Received messages that exceed this size will not be accepted by the broker. -# The default value is 0, which means that all valid MQTT messages are -# accepted. MQTT imposes a maximum payload size of 268435455 bytes. -#message_size_limit 0 - -# This option allows the session of persistent clients (those with clean -# session set to false) that are not currently connected to be removed if they -# do not reconnect within a certain time frame. This is a non-standard option -# in MQTT v3.1. MQTT v3.1.1 and v5.0 allow brokers to remove client sessions. -# -# Badly designed clients may set clean session to false whilst using a randomly -# generated client id. This leads to persistent clients that connect once and -# never reconnect. This option allows these clients to be removed. This option -# allows persistent clients (those with clean session set to false) to be -# removed if they do not reconnect within a certain time frame. -# -# The expiration period should be an integer followed by one of h d w m y for -# hour, day, week, month and year respectively. For example -# -# persistent_client_expiration 2m -# persistent_client_expiration 14d -# persistent_client_expiration 1y -# -# The default if not set is to never expire persistent clients. -#persistent_client_expiration - -# Write process id to a file. Default is a blank string which means -# a pid file shouldn't be written. -# This should be set to /var/run/mosquitto/mosquitto.pid if mosquitto is -# being run automatically on boot with an init script and -# start-stop-daemon or similar. -#pid_file - -# Set to true to queue messages with QoS 0 when a persistent client is -# disconnected. These messages are included in the limit imposed by -# max_queued_messages and max_queued_bytes -# Defaults to false. -# This is a non-standard option for the MQTT v3.1 spec but is allowed in -# v3.1.1. -#queue_qos0_messages false - -# Set to false to disable retained message support. If a client publishes a -# message with the retain bit set, it will be disconnected if this is set to -# false. -#retain_available true - -# Disable Nagle's algorithm on client sockets. This has the effect of reducing -# latency of individual messages at the potential cost of increasing the number -# of packets being sent. -#set_tcp_nodelay false - -# Time in seconds between updates of the $SYS tree. -# Set to 0 to disable the publishing of the $SYS tree. -#sys_interval 10 - -# The MQTT specification requires that the QoS of a message delivered to a -# subscriber is never upgraded to match the QoS of the subscription. Enabling -# this option changes this behaviour. If upgrade_outgoing_qos is set true, -# messages sent to a subscriber will always match the QoS of its subscription. -# This is a non-standard option explicitly disallowed by the spec. -#upgrade_outgoing_qos false - -# When run as root, drop privileges to this user and its primary -# group. -# Set to root to stay as root, but this is not recommended. -# If set to "mosquitto", or left unset, and the "mosquitto" user does not exist -# then it will drop privileges to the "nobody" user instead. -# If run as a non-root user, this setting has no effect. -# Note that on Windows this has no effect and so mosquitto should be started by -# the user you wish it to run as. -#user mosquitto - -# ================================================================= -# Listeners -# ================================================================= - -# Listen on a port/ip address combination. By using this variable -# multiple times, mosquitto can listen on more than one port. If -# this variable is used and neither bind_address nor port given, -# then the default listener will not be started. -# The port number to listen on must be given. Optionally, an ip -# address or host name may be supplied as a second argument. In -# this case, mosquitto will attempt to bind the listener to that -# address and so restrict access to the associated network and -# interface. By default, mosquitto will listen on all interfaces. -# Note that for a websockets listener it is not possible to bind to a host -# name. -# -# On systems that support Unix Domain Sockets, it is also possible -# to create a # Unix socket rather than opening a TCP socket. In -# this case, the port number should be set to 0 and a unix socket -# path must be provided, e.g. -# listener 0 /tmp/mosquitto.sock -# -# listener port-number [ip address/host name/unix socket path] -listener 1883 0.0.0.0 -listener 9001 0.0.0.0 - -# By default, a listener will attempt to listen on all supported IP protocol -# versions. If you do not have an IPv4 or IPv6 interface you may wish to -# disable support for either of those protocol versions. In particular, note -# that due to the limitations of the websockets library, it will only ever -# attempt to open IPv6 sockets if IPv6 support is compiled in, and so will fail -# if IPv6 is not available. -# -# Set to `ipv4` to force the listener to only use IPv4, or set to `ipv6` to -# force the listener to only use IPv6. If you want support for both IPv4 and -# IPv6, then do not use the socket_domain option. -# -#socket_domain - -# Bind the listener to a specific interface. This is similar to -# the [ip address/host name] part of the listener definition, but is useful -# when an interface has multiple addresses or the address may change. If used -# with the [ip address/host name] part of the listener definition, then the -# bind_interface option will take priority. -# Not available on Windows. -# -# Example: bind_interface eth0 -#bind_interface - -# When a listener is using the websockets protocol, it is possible to serve -# http data as well. Set http_dir to a directory which contains the files you -# wish to serve. If this option is not specified, then no normal http -# connections will be possible. -#http_dir - -# The maximum number of client connections to allow. This is -# a per listener setting. -# Default is -1, which means unlimited connections. -# Note that other process limits mean that unlimited connections -# are not really possible. Typically the default maximum number of -# connections possible is around 1024. -#max_connections -1 - -# The listener can be restricted to operating within a topic hierarchy using -# the mount_point option. This is achieved be prefixing the mount_point string -# to all topics for any clients connected to this listener. This prefixing only -# happens internally to the broker; the client will not see the prefix. -#mount_point - -# Choose the protocol to use when listening. -# This can be either mqtt or websockets. -# Certificate based TLS may be used with websockets, except that only the -# cafile, certfile, keyfile, ciphers, and ciphers_tls13 options are supported. -protocol websockets - -# Set use_username_as_clientid to true to replace the clientid that a client -# connected with with its username. This allows authentication to be tied to -# the clientid, which means that it is possible to prevent one client -# disconnecting another by using the same clientid. -# If a client connects with no username it will be disconnected as not -# authorised when this option is set to true. -# Do not use in conjunction with clientid_prefixes. -# See also use_identity_as_username. -# This does not apply globally, but on a per-listener basis. -#use_username_as_clientid - -# Change the websockets headers size. This is a global option, it is not -# possible to set per listener. This option sets the size of the buffer used in -# the libwebsockets library when reading HTTP headers. If you are passing large -# header data such as cookies then you may need to increase this value. If left -# unset, or set to 0, then the default of 1024 bytes will be used. -#websockets_headers_size - -# ----------------------------------------------------------------- -# Certificate based SSL/TLS support -# ----------------------------------------------------------------- -# The following options can be used to enable certificate based SSL/TLS support -# for this listener. Note that the recommended port for MQTT over TLS is 8883, -# but this must be set manually. -# -# See also the mosquitto-tls man page and the "Pre-shared-key based SSL/TLS -# support" section. Only one of certificate or PSK encryption support can be -# enabled for any listener. - -# Both of certfile and keyfile must be defined to enable certificate based -# TLS encryption. - -# Path to the PEM encoded server certificate. -#certfile - -# Path to the PEM encoded keyfile. -#keyfile - -# If you wish to control which encryption ciphers are used, use the ciphers -# option. The list of available ciphers can be optained using the "openssl -# ciphers" command and should be provided in the same format as the output of -# that command. This applies to TLS 1.2 and earlier versions only. Use -# ciphers_tls1.3 for TLS v1.3. -#ciphers - -# Choose which TLS v1.3 ciphersuites are used for this listener. -# Defaults to "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256" -#ciphers_tls1.3 - -# If you have require_certificate set to true, you can create a certificate -# revocation list file to revoke access to particular client certificates. If -# you have done this, use crlfile to point to the PEM encoded revocation file. -#crlfile - -# To allow the use of ephemeral DH key exchange, which provides forward -# security, the listener must load DH parameters. This can be specified with -# the dhparamfile option. The dhparamfile can be generated with the command -# e.g. "openssl dhparam -out dhparam.pem 2048" -#dhparamfile - -# By default an TLS enabled listener will operate in a similar fashion to a -# https enabled web server, in that the server has a certificate signed by a CA -# and the client will verify that it is a trusted certificate. The overall aim -# is encryption of the network traffic. By setting require_certificate to true, -# the client must provide a valid certificate in order for the network -# connection to proceed. This allows access to the broker to be controlled -# outside of the mechanisms provided by MQTT. -#require_certificate false - -# cafile and capath define methods of accessing the PEM encoded -# Certificate Authority certificates that will be considered trusted when -# checking incoming client certificates. -# cafile defines the path to a file containing the CA certificates. -# capath defines a directory that will be searched for files -# containing the CA certificates. For capath to work correctly, the -# certificate files must have ".crt" as the file ending and you must run -# "openssl rehash " each time you add/remove a certificate. -#cafile -#capath - - -# If require_certificate is true, you may set use_identity_as_username to true -# to use the CN value from the client certificate as a username. If this is -# true, the password_file option will not be used for this listener. -#use_identity_as_username false - -# ----------------------------------------------------------------- -# Pre-shared-key based SSL/TLS support -# ----------------------------------------------------------------- -# The following options can be used to enable PSK based SSL/TLS support for -# this listener. Note that the recommended port for MQTT over TLS is 8883, but -# this must be set manually. -# -# See also the mosquitto-tls man page and the "Certificate based SSL/TLS -# support" section. Only one of certificate or PSK encryption support can be -# enabled for any listener. - -# The psk_hint option enables pre-shared-key support for this listener and also -# acts as an identifier for this listener. The hint is sent to clients and may -# be used locally to aid authentication. The hint is a free form string that -# doesn't have much meaning in itself, so feel free to be creative. -# If this option is provided, see psk_file to define the pre-shared keys to be -# used or create a security plugin to handle them. -#psk_hint - -# When using PSK, the encryption ciphers used will be chosen from the list of -# available PSK ciphers. If you want to control which ciphers are available, -# use the "ciphers" option. The list of available ciphers can be optained -# using the "openssl ciphers" command and should be provided in the same format -# as the output of that command. -#ciphers - -# Set use_identity_as_username to have the psk identity sent by the client used -# as its username. Authentication will be carried out using the PSK rather than -# the MQTT username/password and so password_file will not be used for this -# listener. -#use_identity_as_username false - - -# ================================================================= -# Persistence -# ================================================================= - -# If persistence is enabled, save the in-memory database to disk -# every autosave_interval seconds. If set to 0, the persistence -# database will only be written when mosquitto exits. See also -# autosave_on_changes. -# Note that writing of the persistence database can be forced by -# sending mosquitto a SIGUSR1 signal. -#autosave_interval 1800 - -# If true, mosquitto will count the number of subscription changes, retained -# messages received and queued messages and if the total exceeds -# autosave_interval then the in-memory database will be saved to disk. -# If false, mosquitto will save the in-memory database to disk by treating -# autosave_interval as a time in seconds. -#autosave_on_changes false - -# Save persistent message data to disk (true/false). -# This saves information about all messages, including -# subscriptions, currently in-flight messages and retained -# messages. -# retained_persistence is a synonym for this option. -persistence true - -# The filename to use for the persistent database, not including -# the path. -#persistence_file mosquitto.db - -# Location for persistent database. -# Default is an empty string (current directory). -# Set to e.g. /var/lib/mosquitto if running as a proper service on Linux or -# similar. -persistence_location /mosquitto/data - - -# ================================================================= -# Logging -# ================================================================= - -# Places to log to. Use multiple log_dest lines for multiple -# logging destinations. -# Possible destinations are: stdout stderr syslog topic file dlt -# -# stdout and stderr log to the console on the named output. -# -# syslog uses the userspace syslog facility which usually ends up -# in /var/log/messages or similar. -# -# topic logs to the broker topic '$SYS/broker/log/', -# where severity is one of D, E, W, N, I, M which are debug, error, -# warning, notice, information and message. Message type severity is used by -# the subscribe/unsubscribe log_types and publishes log messages to -# $SYS/broker/log/M/susbcribe or $SYS/broker/log/M/unsubscribe. -# -# The file destination requires an additional parameter which is the file to be -# logged to, e.g. "log_dest file /var/log/mosquitto.log". The file will be -# closed and reopened when the broker receives a HUP signal. Only a single file -# destination may be configured. -# -# The dlt destination is for the automotive `Diagnostic Log and Trace` tool. -# This requires that Mosquitto has been compiled with DLT support. -# -# Note that if the broker is running as a Windows service it will default to -# "log_dest none" and neither stdout nor stderr logging is available. -# Use "log_dest none" if you wish to disable logging. -# log_dest file /mosquitto/log/mosquitto.log - -# Types of messages to log. Use multiple log_type lines for logging -# multiple types of messages. -# Possible types are: debug, error, warning, notice, information, -# none, subscribe, unsubscribe, websockets, all. -# Note that debug type messages are for decoding the incoming/outgoing -# network packets. They are not logged in "topics". -#log_type error -#log_type warning -#log_type notice -#log_type information - - -# If set to true, client connection and disconnection messages will be included -# in the log. -#connection_messages true - -# If using syslog logging (not on Windows), messages will be logged to the -# "daemon" facility by default. Use the log_facility option to choose which of -# local0 to local7 to log to instead. The option value should be an integer -# value, e.g. "log_facility 5" to use local5. -#log_facility - -# If set to true, add a timestamp value to each log message. -#log_timestamp true - -# Set the format of the log timestamp. If left unset, this is the number of -# seconds since the Unix epoch. -# This is a free text string which will be passed to the strftime function. To -# get an ISO 8601 datetime, for example: -# log_timestamp_format %Y-%m-%dT%H:%M:%S -#log_timestamp_format - -# Change the websockets logging level. This is a global option, it is not -# possible to set per listener. This is an integer that is interpreted by -# libwebsockets as a bit mask for its lws_log_levels enum. See the -# libwebsockets documentation for more details. "log_type websockets" must also -# be enabled. -#websockets_log_level 0 - - -# ================================================================= -# Security -# ================================================================= - -# If set, only clients that have a matching prefix on their -# clientid will be allowed to connect to the broker. By default, -# all clients may connect. -# For example, setting "secure-" here would mean a client "secure- -# client" could connect but another with clientid "mqtt" couldn't. -#clientid_prefixes - -# Boolean value that determines whether clients that connect -# without providing a username are allowed to connect. If set to -# false then a password file should be created (see the -# password_file option) to control authenticated client access. -# -# Defaults to false, unless there are no listeners defined in the configuration -# file, in which case it is set to true, but connections are only allowed from -# the local machine. -# allow_anonymous false - -# ----------------------------------------------------------------- -# Default authentication and topic access control -# ----------------------------------------------------------------- - -# Control access to the broker using a password file. This file can be -# generated using the mosquitto_passwd utility. If TLS support is not compiled -# into mosquitto (it is recommended that TLS support should be included) then -# plain text passwords are used, in which case the file should be a text file -# with lines in the format: -# username:password -# The password (and colon) may be omitted if desired, although this -# offers very little in the way of security. -# -# See the TLS client require_certificate and use_identity_as_username options -# for alternative authentication options. If a plugin is used as well as -# password_file, the plugin check will be made first. -password_file /mosquitto/config/pwfile - -# Access may also be controlled using a pre-shared-key file. This requires -# TLS-PSK support and a listener configured to use it. The file should be text -# lines in the format: -# identity:key -# The key should be in hexadecimal format without a leading "0x". -# If an plugin is used as well, the plugin check will be made first. -#psk_file - -# Control access to topics on the broker using an access control list -# file. If this parameter is defined then only the topics listed will -# have access. -# If the first character of a line of the ACL file is a # it is treated as a -# comment. -# Topic access is added with lines of the format: -# -# topic [read|write|readwrite|deny] -# -# The access type is controlled using "read", "write", "readwrite" or "deny". -# This parameter is optional (unless contains a space character) - if -# not given then the access is read/write. can contain the + or # -# wildcards as in subscriptions. -# -# The "deny" option can used to explicity deny access to a topic that would -# otherwise be granted by a broader read/write/readwrite statement. Any "deny" -# topics are handled before topics that grant read/write access. -# -# The first set of topics are applied to anonymous clients, assuming -# allow_anonymous is true. User specific topic ACLs are added after a -# user line as follows: -# -user mosquitto -# -# The username referred to here is the same as in password_file. It is -# not the clientid. -# -# -# If is also possible to define ACLs based on pattern substitution within the -# topic. The patterns available for substition are: -# -# %c to match the client id of the client -# %u to match the username of the client -# -# The substitution pattern must be the only text for that level of hierarchy. -# -# The form is the same as for the topic keyword, but using pattern as the -# keyword. -# Pattern ACLs apply to all users even if the "user" keyword has previously -# been given. -# -# If using bridges with usernames and ACLs, connection messages can be allowed -# with the following pattern: -# pattern write $SYS/broker/connection/%c/state -# -# pattern [read|write|readwrite] -# -# Example: -# -# pattern write sensor/%u/data -# -# If an plugin is used as well as acl_file, the plugin check will be -# made first. -#acl_file - -# ----------------------------------------------------------------- -# External authentication and topic access plugin options -# ----------------------------------------------------------------- - -# External authentication and access control can be supported with the -# plugin option. This is a path to a loadable plugin. See also the -# plugin_opt_* options described below. -# -# The plugin option can be specified multiple times to load multiple -# plugins. The plugins will be processed in the order that they are specified -# here. If the plugin option is specified alongside either of -# password_file or acl_file then the plugin checks will be made first. -# -# If the per_listener_settings option is false, the plugin will be apply to all -# listeners. If per_listener_settings is true, then the plugin will apply to -# the current listener being defined only. -# -# This option is also available as `auth_plugin`, but this use is deprecated -# and will be removed in the future. -# -#plugin - -# If the plugin option above is used, define options to pass to the -# plugin here as described by the plugin instructions. All options named -# using the format plugin_opt_* will be passed to the plugin, for example: -# -# This option is also available as `auth_opt_*`, but this use is deprecated -# and will be removed in the future. -# -# plugin_opt_db_host -# plugin_opt_db_port -# plugin_opt_db_username -# plugin_opt_db_password - - -# ================================================================= -# Bridges -# ================================================================= - -# A bridge is a way of connecting multiple MQTT brokers together. -# Create a new bridge using the "connection" option as described below. Set -# options for the bridges using the remaining parameters. You must specify the -# address and at least one topic to subscribe to. -# -# Each connection must have a unique name. -# -# The address line may have multiple host address and ports specified. See -# below in the round_robin description for more details on bridge behaviour if -# multiple addresses are used. Note that if you use an IPv6 address, then you -# are required to specify a port. -# -# The direction that the topic will be shared can be chosen by -# specifying out, in or both, where the default value is out. -# The QoS level of the bridged communication can be specified with the next -# topic option. The default QoS level is 0, to change the QoS the topic -# direction must also be given. -# -# The local and remote prefix options allow a topic to be remapped when it is -# bridged to/from the remote broker. This provides the ability to place a topic -# tree in an appropriate location. -# -# For more details see the mosquitto.conf man page. -# -# Multiple topics can be specified per connection, but be careful -# not to create any loops. -# -# If you are using bridges with cleansession set to false (the default), then -# you may get unexpected behaviour from incoming topics if you change what -# topics you are subscribing to. This is because the remote broker keeps the -# subscription for the old topic. If you have this problem, connect your bridge -# with cleansession set to true, then reconnect with cleansession set to false -# as normal. -#connection -#address [:] [[:]] -#topic [[[out | in | both] qos-level] local-prefix remote-prefix] - -# If you need to have the bridge connect over a particular network interface, -# use bridge_bind_address to tell the bridge which local IP address the socket -# should bind to, e.g. `bridge_bind_address 192.168.1.10` -#bridge_bind_address - -# If a bridge has topics that have "out" direction, the default behaviour is to -# send an unsubscribe request to the remote broker on that topic. This means -# that changing a topic direction from "in" to "out" will not keep receiving -# incoming messages. Sending these unsubscribe requests is not always -# desirable, setting bridge_attempt_unsubscribe to false will disable sending -# the unsubscribe request. -#bridge_attempt_unsubscribe true - -# Set the version of the MQTT protocol to use with for this bridge. Can be one -# of mqttv50, mqttv311 or mqttv31. Defaults to mqttv311. -#bridge_protocol_version mqttv311 - -# Set the clean session variable for this bridge. -# When set to true, when the bridge disconnects for any reason, all -# messages and subscriptions will be cleaned up on the remote -# broker. Note that with cleansession set to true, there may be a -# significant amount of retained messages sent when the bridge -# reconnects after losing its connection. -# When set to false, the subscriptions and messages are kept on the -# remote broker, and delivered when the bridge reconnects. -#cleansession false - -# Set the amount of time a bridge using the lazy start type must be idle before -# it will be stopped. Defaults to 60 seconds. -#idle_timeout 60 - -# Set the keepalive interval for this bridge connection, in -# seconds. -#keepalive_interval 60 - -# Set the clientid to use on the local broker. If not defined, this defaults to -# 'local.'. If you are bridging a broker to itself, it is important -# that local_clientid and clientid do not match. -#local_clientid - -# If set to true, publish notification messages to the local and remote brokers -# giving information about the state of the bridge connection. Retained -# messages are published to the topic $SYS/broker/connection//state -# unless the notification_topic option is used. -# If the message is 1 then the connection is active, or 0 if the connection has -# failed. -# This uses the last will and testament feature. -#notifications true - -# Choose the topic on which notification messages for this bridge are -# published. If not set, messages are published on the topic -# $SYS/broker/connection//state -#notification_topic - -# Set the client id to use on the remote end of this bridge connection. If not -# defined, this defaults to 'name.hostname' where name is the connection name -# and hostname is the hostname of this computer. -# This replaces the old "clientid" option to avoid confusion. "clientid" -# remains valid for the time being. -#remote_clientid - -# Set the password to use when connecting to a broker that requires -# authentication. This option is only used if remote_username is also set. -# This replaces the old "password" option to avoid confusion. "password" -# remains valid for the time being. -#remote_password - -# Set the username to use when connecting to a broker that requires -# authentication. -# This replaces the old "username" option to avoid confusion. "username" -# remains valid for the time being. -#remote_username - -# Set the amount of time a bridge using the automatic start type will wait -# until attempting to reconnect. -# This option can be configured to use a constant delay time in seconds, or to -# use a backoff mechanism based on "Decorrelated Jitter", which adds a degree -# of randomness to when the restart occurs. -# -# Set a constant timeout of 20 seconds: -# restart_timeout 20 -# -# Set backoff with a base (start value) of 10 seconds and a cap (upper limit) of -# 60 seconds: -# restart_timeout 10 30 -# -# Defaults to jitter with a base of 5 and cap of 30 -#restart_timeout 5 30 - -# If the bridge has more than one address given in the address/addresses -# configuration, the round_robin option defines the behaviour of the bridge on -# a failure of the bridge connection. If round_robin is false, the default -# value, then the first address is treated as the main bridge connection. If -# the connection fails, the other secondary addresses will be attempted in -# turn. Whilst connected to a secondary bridge, the bridge will periodically -# attempt to reconnect to the main bridge until successful. -# If round_robin is true, then all addresses are treated as equals. If a -# connection fails, the next address will be tried and if successful will -# remain connected until it fails -#round_robin false - -# Set the start type of the bridge. This controls how the bridge starts and -# can be one of three types: automatic, lazy and once. Note that RSMB provides -# a fourth start type "manual" which isn't currently supported by mosquitto. -# -# "automatic" is the default start type and means that the bridge connection -# will be started automatically when the broker starts and also restarted -# after a short delay (30 seconds) if the connection fails. -# -# Bridges using the "lazy" start type will be started automatically when the -# number of queued messages exceeds the number set with the "threshold" -# parameter. It will be stopped automatically after the time set by the -# "idle_timeout" parameter. Use this start type if you wish the connection to -# only be active when it is needed. -# -# A bridge using the "once" start type will be started automatically when the -# broker starts but will not be restarted if the connection fails. -#start_type automatic - -# Set the number of messages that need to be queued for a bridge with lazy -# start type to be restarted. Defaults to 10 messages. -# Must be less than max_queued_messages. -#threshold 10 - -# If try_private is set to true, the bridge will attempt to indicate to the -# remote broker that it is a bridge not an ordinary client. If successful, this -# means that loop detection will be more effective and that retained messages -# will be propagated correctly. Not all brokers support this feature so it may -# be necessary to set try_private to false if your bridge does not connect -# properly. -#try_private true - -# Some MQTT brokers do not allow retained messages. MQTT v5 gives a mechanism -# for brokers to tell clients that they do not support retained messages, but -# this is not possible for MQTT v3.1.1 or v3.1. If you need to bridge to a -# v3.1.1 or v3.1 broker that does not support retained messages, set the -# bridge_outgoing_retain option to false. This will remove the retain bit on -# all outgoing messages to that bridge, regardless of any other setting. -#bridge_outgoing_retain true - -# If you wish to restrict the size of messages sent to a remote bridge, use the -# bridge_max_packet_size option. This sets the maximum number of bytes for -# the total message, including headers and payload. -# Note that MQTT v5 brokers may provide their own maximum-packet-size property. -# In this case, the smaller of the two limits will be used. -# Set to 0 for "unlimited". -#bridge_max_packet_size 0 - - -# ----------------------------------------------------------------- -# Certificate based SSL/TLS support -# ----------------------------------------------------------------- -# Either bridge_cafile or bridge_capath must be defined to enable TLS support -# for this bridge. -# bridge_cafile defines the path to a file containing the -# Certificate Authority certificates that have signed the remote broker -# certificate. -# bridge_capath defines a directory that will be searched for files containing -# the CA certificates. For bridge_capath to work correctly, the certificate -# files must have ".crt" as the file ending and you must run "openssl rehash -# " each time you add/remove a certificate. -#bridge_cafile -#bridge_capath - - -# If the remote broker has more than one protocol available on its port, e.g. -# MQTT and WebSockets, then use bridge_alpn to configure which protocol is -# requested. Note that WebSockets support for bridges is not yet available. -#bridge_alpn - -# When using certificate based encryption, bridge_insecure disables -# verification of the server hostname in the server certificate. This can be -# useful when testing initial server configurations, but makes it possible for -# a malicious third party to impersonate your server through DNS spoofing, for -# example. Use this option in testing only. If you need to resort to using this -# option in a production environment, your setup is at fault and there is no -# point using encryption. -#bridge_insecure false - -# Path to the PEM encoded client certificate, if required by the remote broker. -#bridge_certfile - -# Path to the PEM encoded client private key, if required by the remote broker. -#bridge_keyfile - -# ----------------------------------------------------------------- -# PSK based SSL/TLS support -# ----------------------------------------------------------------- -# Pre-shared-key encryption provides an alternative to certificate based -# encryption. A bridge can be configured to use PSK with the bridge_identity -# and bridge_psk options. These are the client PSK identity, and pre-shared-key -# in hexadecimal format with no "0x". Only one of certificate and PSK based -# encryption can be used on one -# bridge at once. -#bridge_identity -#bridge_psk - - -# ================================================================= -# External config files -# ================================================================= - -# External configuration files may be included by using the -# include_dir option. This defines a directory that will be searched -# for config files. All files that end in '.conf' will be loaded as -# a configuration file. It is best to have this as the last option -# in the main file. This option will only be processed from the main -# configuration file. The directory specified must not contain the -# main configuration file. -# Files within include_dir will be loaded sorted in case-sensitive -# alphabetical order, with capital letters ordered first. If this option is -# given multiple times, all of the files from the first instance will be -# processed before the next instance. See the man page for examples. -#include_dir diff --git a/gcs/ros_ws/src/ros2tak_tools/package.xml b/gcs/ros_ws/src/ros2tak_tools/package.xml deleted file mode 100644 index c45ad27f5..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/package.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - ros2tak_tools - 0.0.0 - TODO: Package description - mission-operator - TODO: License declaration - - rclpy - std_msgs - geometry_msgs - paho-mqtt - pytak - pyyaml - - ament_copyright - ament_flake8 - ament_pep257 - python3-pytest - - - ament_python - - diff --git a/gcs/ros_ws/src/ros2tak_tools/resource/ros2tak_tools b/gcs/ros_ws/src/ros2tak_tools/resource/ros2tak_tools deleted file mode 100644 index e69de29bb..000000000 diff --git a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/__init__.py b/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/__init__.py deleted file mode 100755 index e69de29bb..000000000 diff --git a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/chat2ros_agent.py b/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/chat2ros_agent.py deleted file mode 100755 index 95fd25357..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/chat2ros_agent.py +++ /dev/null @@ -1,263 +0,0 @@ -import os -import xml.etree.ElementTree as ET -import rclpy -from rclpy.node import Node -from std_msgs.msg import String -import paho.mqtt.client as mqtt -from tak_helper.create_cot_msgs import create_chat_COT, create_gps_COT, create_polygon_COT -from tak_helper.logger import setup_logger -from airstack_msgs.msg import TextQueryResponse -import yaml -import uuid -import logging -import sys -from threading import Lock -from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy - -HELP_MESSAGE = ( - "Welcome to Fleet Control!\n\n" - "Available commands:\n" - "1. **robot {robot_name} find {area}**\n" - " - Instruct a robot to locate a specified area.\n" - " - `robot_name` should be a single word.\n" - "2. **help**\n" - " - Display this help message." -) - -# Shape of the CoT message: -POLYGON = "u-d-f" - - -class AiAgent(Node): - def __init__(self): - super().__init__('gcs_ai_agent') - - # Initialize a basic logger before loading the config - self.logger = logging.getLogger("GCSAIAgent") - - # Read config from config filename from the ros2 parameters - self.declare_parameter("config_file_path", "") - self.config_filepath = self.get_parameter("config_file_path").get_parameter_value().string_value - - # Read credentials directory (for compatibility with ros2_cot_agent) - self.declare_parameter("creds_dir", "") - self.creds_dir = self.get_parameter("creds_dir").get_parameter_value().string_value - - self.get_logger().info(f"Loading configuration from {self.config_filepath}") - self.get_logger().info(f"Credentials directory: {self.creds_dir}") - - # Load the configuration - try: - with open(self.config_filepath, 'r') as file: - config = yaml.safe_load(file) - - # Setup logger based on config - log_level = config.get('logging', {}).get('level', 'INFO') - self.logger = setup_logger(self, log_level) - self.logger.info(f"Logger configured with level: {log_level}") - - except Exception as e: - self.get_logger().error(f"Failed to load configuration: {e}") - sys.exit(1) - - # Read environment variables for configuration - self.project_name = config.get("project", {}).get("name", os.getenv("PROJECT_NAME", "airlab")) - self.ai_agent_name = os.getenv("AI_AGENT_NAME", "aerolens.ai") - - # MQTT Configurations - mqtt_config = config['mqtt'] - self.mqtt_broker = mqtt_config.get('host', "localhost") - self.mqtt_port = int(mqtt_config['port']) - self.mqtt_username = mqtt_config['username'] - self.mqtt_pwd = mqtt_config['password'] - self.mqtt2tak_topic = config['services']['publisher']['tak_publisher']['topic_name'] - self.mqtt_subscribe_topic = config["services"]["mediator"]["chat2ros_agent"]["mqtt_subcribe_topic"] - - # ROS Configurations - self.ros_robot_query_txt_topic = config["services"]["mediator"]["chat2ros_agent"]["ros_query_text_topic"] - self.ros_robot_query_response_topic = config["services"]["mediator"]["chat2ros_agent"][ - "ros_query_response_topic"] - self.robot_publisher = {} - - # Set up MQTT client - self.mqtt_client = mqtt.Client() - self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_pwd) - self.mqtt_client.on_message = self._on_mqtt_message - - # Create a QoS profile for ROS subscriptions - self.qos_profile = QoSProfile( - reliability=ReliabilityPolicy.BEST_EFFORT, - durability=DurabilityPolicy.VOLATILE, - history=HistoryPolicy.KEEP_LAST, - depth=10, - ) - - # Connect to MQTT broker and subscribe to topic - try: - self.logger.info(f"Connecting to MQTT broker at {self.mqtt_broker}:{self.mqtt_port}") - self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port, keepalive=65535) - self.mqtt_client.subscribe(self.mqtt_subscribe_topic) - self.mqtt_client.loop_start() - self.logger.info( - f"Connected and subscribed to MQTT topic '{self.mqtt_subscribe_topic}' on broker {self.mqtt_broker}:{self.mqtt_port}") - except Exception as e: - self.logger.error(f"Failed to connect or subscribe to MQTT: {e}") - self.logger.error(f"Exception type: {type(e)}") - - def _get_robot_text_query(self, query): - return f"{query}" - - def _on_mqtt_message(self, client, userdata, msg): - # Parse the XML message - try: - root = ET.fromstring(msg.payload.decode('utf-8')) - remarks_tag = root.find(".//remarks") - remarks = remarks_tag.text.lower() - self.logger.info(f"Received message: {remarks}") - self.process_remarks(remarks) - # Capture NoneType error - except AttributeError as e: - self.logger.warning(f"Failed to parse message: {e}") - except Exception as e: - self.logger.error(f"Failed to process message: {e}") - self.logger.error(f"Exception type: {type(e)}") - - def process_remarks(self, remarks): - """Process the remarks and act accordingly.""" - if remarks.startswith("robot"): - # Example: "robot {robot_name} find {area}" - parts = remarks.split(" ") - if len(parts) >= 4 and parts[2] == "find": - robot_name = parts[1] - area = " ".join(parts[3:]) - self.publish_txt_query_to_robot(robot_name, area) - else: - self.logger.warning(f"Invalid robot command format: {remarks}") - help_cot_message = create_chat_COT(uuid=str(uuid.uuid4()), callsign=self.ai_agent_name, - message=HELP_MESSAGE) - self.send_message_to_TAK(cot_message=help_cot_message) - elif remarks.lower() == "help": - help_cot_message = create_chat_COT(uuid=str(uuid.uuid4()), callsign=self.ai_agent_name, - message=HELP_MESSAGE) - self.send_message_to_TAK(cot_message=help_cot_message) - else: - self.logger.info(f"Unrecognized command: {remarks}") - - def publish_txt_query_to_robot(self, robot_name, area): - """Publish the area to the ROS robot's query topic.""" - message = self._get_robot_text_query(area) - msg = String() - msg.data = message - - request_topic_name = f"/{robot_name}{self.ros_robot_query_txt_topic}" - response_topic_name = f"/{robot_name}{self.ros_robot_query_response_topic}" - - if robot_name not in self.robot_publisher: - self.robot_publisher[robot_name] = self.create_publisher(String, request_topic_name, 10) - self.robot_publisher[robot_name].publish(msg) - self.logger.info(f"Sent command to {request_topic_name} to find {area}") - - # Send a chat message - message = f"Sent command to {robot_name} to find '{area}'" - cot_message = create_chat_COT(uuid=str(uuid.uuid4()), callsign=self.ai_agent_name, message=message) - self.send_message_to_TAK(cot_message=cot_message) - - # Create a subscriber to listen for the response - self.create_subscription( - TextQueryResponse, - response_topic_name, - lambda msg: self._on_robot_response(msg, robot_name), - self.qos_profile - ) - - def _on_robot_response(self, msg, robot_name): - """Callback for processing robot response and sending CoT messages.""" - log_prefix = f"GCSAIAgent.{robot_name}" - robot_logger = logging.getLogger(log_prefix) - - # Extracting the header information - header = msg.header - robot_logger.info(f"Header information: seq={header.seq}, stamp={header.stamp}, frame_id={header.frame_id}") - - # Extract the tag name - tag_name = msg.tag_name - robot_logger.info(f"Tag name: {tag_name}") - - # Extracting the geofence data (which is an array of NavSatFix) - geofence_data = msg.geofence - geofence_info = "" - - # Create a list of GPS points - gps_points = [] - - for i, gps_fix in enumerate(geofence_data): - # Create a list of GPS points with {"lat": str, "lon": str, "hae": str} - gps_point = (gps_fix.latitude, gps_fix.longitude) - gps_points.append(gps_point) - geofence_info += f"Point {i}: lat={gps_fix.latitude}, lon={gps_fix.longitude}\n" - - polygon_cot_message = create_polygon_COT(uuid=tag_name, callsign=self.ai_agent_name, gps_coordinates=gps_points) - robot_logger.info(f"Geofence data:\n{geofence_info}") - - # Send the polygon CoT message - self.send_message_to_TAK(polygon_cot_message) - - # Send confirmation chat message - cot_chat_message = create_chat_COT( - uuid=str(uuid.uuid4()), - callsign=self.ai_agent_name, - message=f"Response received! Please check the geofence data for {tag_name} near {robot_name}" - ) - self.send_message_to_TAK(cot_chat_message) - - def send_message_to_TAK(self, cot_message): - """Send a message to the TAK topic.""" - mqtt_logger = logging.getLogger("GCSAIAgent.MQTT") - try: - self.mqtt_client.publish(self.mqtt2tak_topic, cot_message) - mqtt_logger.debug(f"Sent message to topic {self.mqtt2tak_topic}") - except Exception as e: - mqtt_logger.error(f"Failed to send message: {e}") - mqtt_logger.error(f"Exception type: {type(e)}") - - def destroy_node(self): - """Stop MQTT loop and destroy the node.""" - self.logger.info("Shutting down GCS AI Agent") - self.mqtt_client.loop_stop() - self.mqtt_client.disconnect() # Explicit disconnect - self.logger.info("MQTT client disconnected") - super().destroy_node() - - -def main(args=None): - # Basic logger setup before config is loaded - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(name)s - %(message)s' - ) - startup_logger = logging.getLogger("GCSAIAgent.startup") - startup_logger.info("Starting GCS AI Agent") - - rclpy.init(args=args) - - try: - # Create the AI agent node - startup_logger.info("Creating AI Agent node") - ai_agent = AiAgent() - - # Spin the node - startup_logger.info("Node initialized successfully, starting spin") - rclpy.spin(ai_agent) - except Exception as e: - startup_logger.critical(f"Fatal error occurred: {e}") - finally: - # Shutdown and cleanup - startup_logger.info("Shutting down node") - if 'ai_agent' in locals(): - ai_agent.destroy_node() - rclpy.shutdown() - startup_logger.info("Node has shut down cleanly") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/cot2planner_agent.py b/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/cot2planner_agent.py deleted file mode 100755 index dda4a5e9a..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/cot2planner_agent.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 - -""" -Description: -This script listens to the MQTT messages from the TAK server and converts them to ROS 2 messages. -Mainly used for Global Planner for getting Search Bounds, Search Priors, and Keep Out Zones. - -Usage: -ros2 run ros2tak_tools cot2planner_agent --config - -Author: -Aditya Rauniyar (2024) - -""" - -import rclpy -from rclpy.node import Node -import xml.etree.ElementTree as ET -import paho.mqtt.client as mqtt -from airstack_msgs.msg import SearchMissionRequest, SearchPrior, KeepOutZone # Import the custom message -from std_msgs.msg import Header -from geometry_msgs.msg import Polygon, Point32 -from rclpy.qos import QoSProfile -import yaml -from enum import Enum -import re - -# Constants and Enums -DEFAULT_FRAME_ID = 'map' - -# Define event types using Enum for better type safety -class EventType(Enum): - POLYGON = "u-d-f" - ROUTE = "b-m-r" - POINT = "u-d-c-p" - CIRCLE = "u-d-c-c" - -def extract_polygon_points(root): - """Extract points for the polygon based on link coordinates.""" - points32 = [] - - # Find all elements and extract their 'point' attribute - for link in root.findall('.//link'): - point_str = link.attrib.get('point') - # Check if point attribute exists and is non-empty. - # WinTAK sometimes generates blank points, ATAK is fine. - if point_str: - try: - # Split the single point into latitude and longitude - lat, lon = map(float, point_str.split(',')) - - # Create a Point32 object with lat and lon, set z=0.0 (altitude is not used here) - point = Point32() - point.x = lat - point.y = lon - point.z = 0.0 # Default z value (altitude not available) - - # Add the point to the list - points32.append(point) - except ValueError as e: - print(f"Warning: Skipping invalid point: {point_str} - Error: {e}") - - return points32 - -def extract_value_priority(callsign): - """Extract value and priority from a callsign like 'planner_sp_v0.2_p3.4'.""" - match = re.search(r'v([0-9.]+)_p([0-9.]+)', callsign) - if match: - value = float(match.group(1)) - priority = float(match.group(2)) - return value, priority - return 0.0, 1.0 # Default values if no match - - -def _process_polygons(root): - """Process XML for 'u-d-f' type and convert to SearchBound and SearchPrior.""" - points32 = extract_polygon_points(root) - # Create ROS message - polygon = Polygon() - - # Set the points for the polygon - polygon.points = points32 - - return polygon - - -class Cot2Planner(Node): - def __init__(self, config_file): - super().__init__('cot2planner') - - # Load configuration - with open(config_file, 'r') as file: - config = yaml.safe_load(file) - - # MQTT Configurations - mqtt_config = config['mqtt'] - self.mqtt_broker = mqtt_config['host'] - self.mqtt_port = int(mqtt_config['port']) - self.mqtt_username = mqtt_config['username'] - self.mqtt_password = mqtt_config['password'] - - # ROS Configurations - # Reading the ros_topic_name for the planner under filter_messages -> planner - planner_config = next( - item for item in config['services']['subscriber']['tak_subscriber']['filter_messages'] - if item['name'] == 'planner' - ) - self.ros_topic = planner_config['ros_topic_name'] # Read the ros_topic_name dynamically - self.mqtt_topicname = planner_config['mqtt_topic_name'] # Read the MQTT topic name for planner events - - # ROS 2 Publisher - qos_profile = QoSProfile(depth=10) - self.publisher = self.create_publisher(SearchMissionRequest, self.ros_topic, qos_profile) - - # Initialize MQTT Client - self.mqtt_client = mqtt.Client() - self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_password) - self.mqtt_client.on_message = self._on_mqtt_message - - # Connect to MQTT broker and start loop - self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port, keepalive=65535) - self.mqtt_client.subscribe(self.mqtt_topicname) # Subscribe to the dynamic planner topic - self.mqtt_client.loop_start() - - # Planner message request to be sent - self.plan_msg_request = self._initialize_ros_message_header(SearchMissionRequest(), frame_id=DEFAULT_FRAME_ID) - - self.get_logger().info(f"Subscribed to MQTT topic '{self.mqtt_topicname}' on broker '{self.mqtt_broker}:{self.mqtt_port}'") - - def _initialize_ros_message_header(self, message, frame_id=DEFAULT_FRAME_ID): - """Initialize the ROS message with header info.""" - message.header = Header() - message.header.stamp = self.get_clock().now().to_msg() - message.header.frame_id = frame_id - return message - - def _on_mqtt_message(self, client, userdata, msg): - """Callback for incoming MQTT messages.""" - try: - # Parse the XML message - root = ET.fromstring(msg.payload.decode('utf-8')) - - # Extract event type from the XML - event_type = root.attrib.get('type') - - # Extract the callsign from the XML - callsign = root.find('.//contact').attrib.get('callsign', '') - - # Handle event types using Enum values - if event_type == EventType.POLYGON.value or event_type == EventType.ROUTE.value or event_type == EventType.POINT.value: - polygon = _process_polygons(root) - - # Check if the callsign contains 'sb' for search bounds - if 'sb' in callsign.lower(): - self.plan_msg_request.search_bounds = polygon - self.get_logger().info(f"Added SearchBounds with callsign '{callsign}' to the SearchMissionRequest") - - event_shape_type = "polygon" if event_type == EventType.POLYGON.value else \ - "route" if event_type == EventType.ROUTE.value else "point" - - # Process as search prior - self._process_search_priors(polygon, call_sign=callsign, shape_type=event_shape_type) - - elif event_type == EventType.CIRCLE.value: - self._process_keep_out_zones(root) - self.get_logger().info(f"Added KeepOutZones with callsign '{callsign}' to the SearchMissionRequest") - else: - self.get_logger().info(f"Event type '{event_type}' not supported") - - # Print the current self.plan_msg_request - # self.get_logger().info(f"Current SearchMissionRequest: {self.plan_msg_request}") - - # Check if the xml contains word "end" in the remarks field mostly the remarks field is kept empty - remarks = root.find('.//remarks').text - if remarks and 'end' in remarks.lower(): - # Publish the SearchMissionRequest message - self.publisher.publish(self.plan_msg_request) - self.get_logger().info(f"Published SearchMissionRequest to '{self.ros_topic}'") - # Reset the plan_msg_request for next mission - self.plan_msg_request = self._initialize_ros_message_header(SearchMissionRequest(), frame_id=DEFAULT_FRAME_ID) - - except (ET.ParseError, mqtt.MQTTException) as e: - self.get_logger().error(f"Error processing MQTT message: {e}") - - def _process_search_priors(self, polygon, call_sign="", shape_type="polygon"): - """Process XML for 'b-m-r' type and convert to SearchBound and SearchPrior.""" - - # Extract the value and priority from the callsign - value, priority = extract_value_priority(call_sign) - - # Create a SearchPrior message - search_prior = self._initialize_ros_message_header(SearchPrior(), frame_id=DEFAULT_FRAME_ID) - - # Set the prior type as polygon - if shape_type == "polygon": - search_prior.grid_prior_type = SearchPrior.POLYGON_PRIOR - elif shape_type == "route": - search_prior.grid_prior_type = SearchPrior.LINE_SEG_PRIOR - elif shape_type == "point": - search_prior.grid_prior_type = SearchPrior.POINT_PRIOR - - # set polygon, value, and priority - search_prior.points_list = polygon - search_prior.value = [value] - search_prior.priority = [priority] - - # Update the plan_msg_request with the search_prior - self.plan_msg_request.search_priors.append(search_prior) - self.get_logger().info(f"Added SearchPriors to the SearchMissionRequest with callSign={call_sign}, " - f"type={shape_type}, value={value}, priority={priority}") - - - def _process_keep_out_zones(self, root): - """Process XML for 'u-d-c-c' type and convert to KeepOutZone.""" - # Extract data from XML - point = root.find('.//point') - if point is not None: - lat = float(point.attrib.get('lat', 0.0)) - lon = float(point.attrib.get('lon', 0.0)) - else: - lat = lon = 0.0 - - ellipse = root.find('.//ellipse') - major_ellipse = float(ellipse.attrib.get('major', 0.0)) if ellipse is not None else 0.0 - - # Create ROS message - message = self._initialize_ros_message_header(KeepOutZone(), frame_id=DEFAULT_FRAME_ID) - - # Set the x, y, z_min, z_max, and radius fields - message.x = lat - message.y = lon - message.z_min = 0.0 - message.z_max = 0.0 - message.radius = major_ellipse # Radius is the major ellipse axis in meters - - # Append the KeepOutZone message to the plan_msg_request - self.plan_msg_request.keep_out_zones.append(message) - - def destroy_node(self): - """Stop MQTT loop and destroy the node.""" - self.mqtt_client.loop_stop() - self.mqtt_client.disconnect() # Explicit disconnect - super().destroy_node() - -def main(args=None): - rclpy.init(args=args) - - import argparse - parser = argparse.ArgumentParser(description="COT to Planner") - parser.add_argument('--config', type=str, required=True, help='Path to the config YAML file.') - args = parser.parse_args() - - cot2planner = Cot2Planner(args.config) - rclpy.spin(cot2planner) - cot2planner.destroy_node() - rclpy.shutdown() - -if __name__ == '__main__': - main() diff --git a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/ros2casevac_agent.py b/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/ros2casevac_agent.py deleted file mode 100755 index b9eb356f9..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/ros2casevac_agent.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python3 - -""" -ROS 2 CASEVAC Agent - -Subscribes to 2 topics that has casualty meta data and image data for the casualty. - -Author: Aditya Rauniyar (rauniyar@cmu.edu) - -Usage: - 1. Run the script with the following command, specifying the path to the config file: - ros2 run your_package ros2casevac_agent --ros-args -p config_file_path:=config.yaml -p creds_dir:=/path/to/creds -""" - -import rclpy -from rclpy.node import Node -import paho.mqtt.client as mqtt -import argparse -import yaml -import logging -import sys -from straps_msgs.msg import CasualtyMeta, Injury, Critical, Vitals -from tak_helper.Casualty import CasualtyCOT, create_casualty_id -import pytak -from tak_helper.logger import setup_logger - - -def load_config(file_path): - """Load configuration from a YAML file.""" - with open(file_path, "r") as f: - return yaml.safe_load(f) - - -############################################################################################################ -""" -Global dictionary to store the casualty meta data. -""" -CASUALTY_META_DATA = {} -############################################################################################################ - - -class ROS2COTPublisher(Node): - def __init__(self): - super().__init__("ros2casevac_agent") - self.subscribers = [] - - # Read config from config filename from the ros2 parameters - self.declare_parameter("config_file_path", "") - self.config_filepath = self.get_parameter("config_file_path").get_parameter_value().string_value - - # Initialize a basic logger before loading the config - self.logger = logging.getLogger("ROS2CASEVAC") - - self.get_logger().info(f"Loading configuration from {self.config_filepath}") - - # Load the configuration - try: - config = load_config(self.config_filepath) - - # Setup logger based on config - log_level = config.get('logging', {}).get('level', 'INFO') - self.logger = setup_logger(self, log_level) - self.logger.info(f"Logger configured with level: {log_level}") - - except Exception as e: - self.get_logger().error(f"Failed to load configuration: {e}") - sys.exit(1) - - # Read the credentials dir from the ros2 parameters - self.declare_parameter("creds_dir", "") - self.creds_dir = self.get_parameter("creds_dir").get_parameter_value().string_value - self.logger.info(f"Loading credentials from {self.creds_dir}") - - # Get host and port from the config - self.host = config["services"]["host"] - self.project_name = config["project"]["name"] - - # MQTT related configs - try: - self.mqtt_broker = config["mqtt"]["host"] - self.mqtt_port = config["mqtt"]["port"] - self.mqtt_username = config["mqtt"]["username"] - self.mqtt_pwd = config["mqtt"]["password"] - self.mqtt_topicname = config["services"]["mediator"]["ros2casevac_agent"]["topic_name"] - - self.ros_casualty_meta_topic_name = config["services"]["mediator"]["ros2casevac_agent"]["ros_casualty_meta_topic_name"] - self.ros_casualty_image_topic_name = config["services"]["mediator"]["ros2casevac_agent"]["ros_casualty_image_topic_name"] - - self.logger.info( - f"MQTT CONFIG: Broker={self.mqtt_broker}, Port={self.mqtt_port}, Topic={self.mqtt_topicname}") - except KeyError as e: - self.logger.error(f"Missing required configuration key: {e}") - sys.exit(1) - - # Setting MQTT - self.mqtt_client = mqtt.Client() - # Set the username and password - self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_pwd) - try: - self.logger.info(f"Attempting to connect to MQTT broker at {self.mqtt_broker}:{self.mqtt_port}") - self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port, keepalive=65535) - self.mqtt_client.loop_start() # Start MQTT loop in background thread - self.logger.info(f"Connected to MQTT broker at {self.mqtt_broker}:{self.mqtt_port}") - except Exception as e: - self.logger.error(f"Failed to connect to MQTT broker: {e}") - self.logger.error(f"Exception type: {type(e)}") - - self.logger.info(f"Starting ROS2CASEVAC_AGENT for project: {self.project_name}") - - # Subscribe to the casualty meta data topic with msg type CasualtyMeta - self.casualty_meta_subscriber = self.create_subscription( - CasualtyMeta, - self.ros_casualty_meta_topic_name, - self.casualty_meta_callback, - 10, - ) - self.logger.info(f"Subscribed to {self.ros_casualty_meta_topic_name} topic") - - def casualty_meta_callback(self, msg): - """Callback for the casualty meta data subscriber""" - global CASUALTY_META_DATA - - self.logger.info(f"Received CasualtyMeta message: {msg}") - - # get the casualty id from the message - casualty_id = create_casualty_id(msg.casualty_id) - - if casualty_id not in CASUALTY_META_DATA: - # create a new CasualtyCOT object - CASUALTY_META_DATA[casualty_id] = CasualtyCOT(msg.casualty_id) - self.logger.info(f"Created new CasualtyCOT object for casualty: {casualty_id}") - - # update the CasualtyCOT object with the new data - CASUALTY_META_DATA[casualty_id].update_casualty_metadata(msg) - self.logger.info(f"Updated CasualtyCOT object for casualty: {casualty_id}") - - # send the updated CoT event over MQTT if the GPS data is available - if CASUALTY_META_DATA[casualty_id].gps.status: - self.send_cot_event_over_mqtt( - CASUALTY_META_DATA[casualty_id].generate_cot_event(), - casualty_id - ) - self.logger.info(f"Sent CoT event for casualty: {casualty_id}") - - def send_cot_event_over_mqtt(self, cot_event, casualty_id=None): - """Send CoT event over the MQTT network""" - log_prefix = f"ROS2CASEVAC.MQTT.{casualty_id}" if casualty_id else "ROS2CASEVAC.MQTT" - logger = logging.getLogger(log_prefix) - - try: - self.mqtt_client.publish(self.mqtt_topicname, cot_event) - logger.debug(f"CoT event published to topic '{self.mqtt_topicname}'") - except Exception as e: - logger.error(f"Failed to publish to MQTT: {e}") - logger.error(f"Exception type: {type(e)}") - - -def main(args=None): - # Initialize logger - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(name)s - %(message)s' - ) - logger = logging.getLogger("ROS2CASEVAC.main") - logger.info("Initializing ROS 2 Python client library") - - # Initialize ROS 2 Python client library - rclpy.init(args=args) - - try: - # Create an instance of the ROS2COTPublisher node - logger.info("Creating ROS2CASEVAC_AGENT node") - casevac_agent = ROS2COTPublisher() - - # Keep the node running to listen to incoming messages - logger.info("Node initialized successfully, starting spin") - rclpy.spin(casevac_agent) - except Exception as e: - logger.critical(f"Fatal error occurred: {e}") - finally: - # Shutdown and cleanup - logger.info("Shutting down node") - if 'casevac_agent' in locals(): - # Stop the MQTT client loop - if hasattr(casevac_agent, 'mqtt_client'): - casevac_agent.mqtt_client.loop_stop() - casevac_agent.mqtt_client.disconnect() - casevac_agent.destroy_node() - rclpy.shutdown() - logger.info("Node has shut down cleanly") - - -if __name__ == "__main__": - # Basic logger setup before config is loaded - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(name)s - %(message)s' - ) - logger = logging.getLogger("ROS2CASEVAC.startup") - logger.info("Starting ROS 2 CASEVAC Agent") - - main() \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/ros2cot_agent.py b/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/ros2cot_agent.py deleted file mode 100755 index 6a46e93e2..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/ros2tak_tools/ros2cot_agent.py +++ /dev/null @@ -1,346 +0,0 @@ -#!/usr/bin/env python3 - -""" -ROS 2 GPS to CoT Event Publisher - -Author: Aditya Rauniyar (rauniyar@cmu.edu) - -This script acts as a ROS 2 node that subscribes to GPS data from multiple robots -and converts that data into Cursor-On-Target (CoT) events. The CoT events are then sent -over MQTT to a designated topic. The configuration for the MQTT connection, -as well as the robot streaming configuration, is loaded from a YAML configuration file. - -The script now supports publishing GPS data at a specified frequency for each robot. - -Usage: - 1. Ensure you have Python 3.x installed with the necessary packages: - pip install rclpy sensor_msgs paho-mqtt pytak pyyaml - - 2. Create a YAML configuration file (e.g., config.yaml) with the following structure: - project: - name: test - logging: - level: 'INFO' # Logging level for the services. Options: DEBUG, INFO, WARNING, ERROR, CRITICAL. - gps_streaming: - - name: 'drone1' - type: 'uav' - topicname: '/robot_1/interface/mavros/global_position/global' - frequency: 1 # Frequency in Hz - - name: 'drone2' - type: 'uav' - topicname: '/robot_2/interface/mavros/global_position/global' - frequency: 1 # Frequency in Hz - mqtt: - host: '127.0.0.1' - port: 1883 - username: 'user' - password: 'pass' - services: - host: '127.0.0.1' - mediator: - ros2cot_agent: - topic_name: 'ros2cot/events' - - 3. Run the script with the following command, specifying the path to the config file: - ros2 run your_package your_script --ros-args -p config_file_path:=config.yaml -p creds_dir:=/path/to/creds -""" - -import rclpy -from rclpy.node import Node -from sensor_msgs.msg import NavSatFix -import paho.mqtt.client as mqtt -import pytak -import socket -import yaml -import logging -import sys -import time -from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy -from threading import Lock -from tak_helper.create_cot_msgs import create_gps_COT -from tak_helper.logger import setup_logger - - -def load_config(file_path): - """Load configuration from a YAML file.""" - with open(file_path, "r") as f: - return yaml.safe_load(f) - - -class RobotGPSData: - """Class to store the latest GPS data for a robot.""" - - def __init__(self, robot_name, robot_type, frequency): - self.robot_name = robot_name - self.robot_type = robot_type - self.frequency = frequency - self.latitude = 0.0 - self.longitude = 0.0 - self.altitude = 0.0 - self.last_update_time = 0.0 - self.last_publish_time = 0.0 - self.has_new_data = False - self.lock = Lock() # For thread safety - - def update_data(self, latitude, longitude, altitude): - """Update the GPS data for this robot.""" - with self.lock: - self.latitude = latitude - self.longitude = longitude - self.altitude = altitude - self.last_update_time = time.time() - self.has_new_data = True - - def should_publish(self): - """Check if it's time to publish data based on frequency.""" - current_time = time.time() - # Only publish if: - # 1. We have new data since the last publish - # 2. The publish interval has elapsed (1/frequency seconds) - with self.lock: - if not self.has_new_data: - return False - - time_since_last_publish = current_time - self.last_publish_time - should_publish = time_since_last_publish >= (1.0 / self.frequency) - - if should_publish: - # Update the last publish time and mark data as published - self.last_publish_time = current_time - self.has_new_data = False - - return should_publish - - def get_data(self): - """Get the current GPS data.""" - with self.lock: - return { - "latitude": self.latitude, - "longitude": self.longitude, - "altitude": self.altitude - } - - -class ROS2COTPublisher(Node): - def __init__(self): - super().__init__("ros2cot_publisher") - self.subscribers = [] - self.robot_data = {} # Dictionary to store the latest data for each robot - - # Read config from config filename from the ros2 parameters - self.declare_parameter("config_file_path", "") - self.config_filepath = self.get_parameter("config_file_path").get_parameter_value().string_value - - # Initialize a basic logger before loading the config - self.logger = logging.getLogger("ROS2COT") - - self.get_logger().info(f"Loading configuration from {self.config_filepath}") - - # Load the configuration - try: - config = load_config(self.config_filepath) - - # Setup logger based on config - log_level = config.get('logging', {}).get('level', 'INFO') - self.logger = setup_logger(self, log_level) - self.logger.info(f"Logger configured with level: {log_level}") - - except Exception as e: - self.get_logger().error(f"Failed to load configuration: {e}") - sys.exit(1) - - # Read the credentials dir from the ros2 parameters - self.declare_parameter("creds_dir", "") - self.creds_dir = self.get_parameter("creds_dir").get_parameter_value().string_value - self.logger.info(f"Loading credentials from {self.creds_dir}") - - # Get host and port from the config - self.host = config["services"]["host"] - self.project_name = config["project"]["name"] - - # Get GPS streaming configuration - self.gps_streaming = config.get("gps_streaming", []) - - if not self.gps_streaming: - self.logger.warning("No GPS streaming configurations found in config file") - - # MQTT related configs - try: - self.mqtt_broker = config["mqtt"]["host"] - self.mqtt_port = config["mqtt"]["port"] - self.mqtt_username = config["mqtt"]["username"] - self.mqtt_pwd = config["mqtt"]["password"] - self.mqtt_topicname = config["services"]["mediator"]["ros2cot_agent"]["topic_name"] - - self.logger.info( - f"MQTT CONFIG: Broker={self.mqtt_broker}, Port={self.mqtt_port}, Topic={self.mqtt_topicname}") - except KeyError as e: - self.logger.error(f"Missing required MQTT configuration key: {e}") - sys.exit(1) - - # Setting MQTT - self.mqtt_client = mqtt.Client() - # Set the username and password - self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_pwd) - try: - self.logger.info(f"Attempting to connect to MQTT broker at {self.mqtt_broker}:{self.mqtt_port}") - self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port, keepalive=65535) - self.mqtt_client.loop_start() # Start MQTT loop in background thread - self.logger.info(f"Connected to MQTT broker at {self.mqtt_broker}:{self.mqtt_port}") - except Exception as e: - self.logger.error(f"Failed to connect to MQTT broker: {e}") - self.logger.error(f"Exception type: {type(e)}") - - self.logger.info(f"Starting ROS2COTPublisher for project: {self.project_name}") - - # Create a QoS profile that matches the publisher - self.qos_profile = QoSProfile( - reliability=ReliabilityPolicy.BEST_EFFORT, # Match the publisher's BEST_EFFORT - durability=DurabilityPolicy.VOLATILE, # Match the publisher's VOLATILE - history=HistoryPolicy.KEEP_LAST, - depth=10, - ) - - # Subscribe to GPS topics based on the configuration - for robot_config in self.gps_streaming: - robot_name = robot_config.get("name") - robot_type = robot_config.get("type") - topic_name = robot_config.get("topicname") - frequency = robot_config.get("frequency", 1.0) # Default to 1Hz if not specified - - if not robot_name or not topic_name: - self.logger.warning(f"Skipping invalid robot config: {robot_config}") - continue - - # Create a data structure to hold GPS data for this robot - self.robot_data[robot_name] = RobotGPSData(robot_name, robot_type, frequency) - - subscriber = self.create_subscription( - NavSatFix, - topic_name, - lambda msg, name=robot_name: self.gps_callback(msg, name), - self.qos_profile - ) - self.subscribers.append(subscriber) - self.logger.info(f"Subscribed to GPS topic for {robot_name}: {topic_name}, publishing at {frequency} Hz") - - # Create a timer to check and publish data at regular intervals - # Use the shortest interval possible (0.01 seconds) to check all robots - self.publisher_timer = self.create_timer(0.01, self.publish_timer_callback) - - def gps_callback(self, msg, robot_name): - """Callback for processing GPS data.""" - logger = logging.getLogger(f"ROS2COT.{robot_name}") - - latitude = msg.latitude - longitude = msg.longitude - altitude = msg.altitude - - # Log the received GPS data - logger.debug( - f"Received GPS data: Lat {latitude:.6f}, Lon {longitude:.6f}, Alt {altitude:.2f}" - ) - - # Update the stored data for this robot - if robot_name in self.robot_data: - self.robot_data[robot_name].update_data(latitude, longitude, altitude) - else: - logger.warning(f"Received data for unknown robot: {robot_name}") - - def publish_timer_callback(self): - """Timer callback to check and publish data for all robots based on their frequency.""" - for robot_name, robot_data in self.robot_data.items(): - if robot_data.should_publish(): - # Get the current data - data = robot_data.get_data() - - # Create a CoT event - cot_event = create_gps_COT( - f"{self.project_name}_{robot_name}", - data["latitude"], - data["longitude"], - data["altitude"], - "COT_Event", - robot_data.robot_type - ) - - # Send the CoT event over MQTT - self.send_cot_event_over_mqtt(cot_event, robot_name) - - def send_cot_event_over_mqtt(self, cot_event, robot_name=None): - """Send CoT event over the MQTT network""" - log_prefix = f"ROS2COT.MQTT.{robot_name}" if robot_name else "ROS2COT.MQTT" - logger = logging.getLogger(log_prefix) - - try: - self.mqtt_client.publish(self.mqtt_topicname, cot_event) - logger.debug(f"CoT event published to topic '{self.mqtt_topicname}'") - except Exception as e: - logger.error(f"Failed to publish to MQTT: {e}") - logger.error(f"Exception type: {type(e)}") - - def send_cot_event_over_network(self, cot_event, host=None, port=None): - """Send CoT event over a TCP socket to the configured host.""" - logger = logging.getLogger("ROS2COT.TCP") - - # Use provided host/port or fall back to class attributes - host = host or self.host - port = port or getattr(self, 'port', None) - - if not port: - logger.error("No port configured for TCP connection") - return - - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.connect((host, port)) - s.sendall(cot_event) - logger.info(f"Sent CoT event to {host}:{port}") - except ConnectionRefusedError: - logger.error(f"Connection to {host}:{port} refused. Ensure the server is running.") - except Exception as e: - logger.error(f"Failed to send CoT event via TCP: {e}") - logger.error(f"Exception type: {type(e)}") - - -def main(args=None): - # Initialize logger - logger = logging.getLogger("ROS2COT.main") - logger.info("Initializing ROS 2 Python client library") - - # Initialize ROS 2 Python client library - rclpy.init(args=args) - - try: - # Create an instance of the ROS2COTPublisher node - logger.info("Creating ROS2COTPublisher node") - gps_cot_publisher = ROS2COTPublisher() - - # Keep the node running to listen to incoming messages - logger.info("Node initialized successfully, starting spin") - rclpy.spin(gps_cot_publisher) - except Exception as e: - logger.critical(f"Fatal error occurred: {e}") - finally: - # Shutdown and cleanup - logger.info("Shutting down node") - if 'gps_cot_publisher' in locals(): - # Stop the MQTT client loop - if hasattr(gps_cot_publisher, 'mqtt_client'): - gps_cot_publisher.mqtt_client.loop_stop() - gps_cot_publisher.mqtt_client.disconnect() - gps_cot_publisher.destroy_node() - rclpy.shutdown() - logger.info("Node has shut down cleanly") - - -if __name__ == "__main__": - # Basic logger setup before config is loaded - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(name)s - %(message)s' - ) - logger = logging.getLogger("ROS2COT.startup") - logger.info("Starting ROS 2 GPS to CoT Publisher") - - main() \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/scripts/tak_publisher.py b/gcs/ros_ws/src/ros2tak_tools/scripts/tak_publisher.py deleted file mode 100755 index 9463ecf92..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/scripts/tak_publisher.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python3 - -""" -TAK Publisher Script with Enhanced Logging - -Author: Aditya Rauniyar (rauniyar@cmu.edu) -""" - -import asyncio -from pathlib import Path -import sys -import logging -import logging.handlers -import multiprocessing -import argparse -import yaml -from configparser import ConfigParser -import paho.mqtt.client as mqtt -import pytak - - -def setup_logger(log_level): - """ - Set up the logger with appropriate log level and formatting. - - Args: - log_level: The log level from config (DEBUG, INFO, WARNING, ERROR, CRITICAL) - - Returns: - Configured logger object - """ - # Convert string log level to logging constants - level_map = { - 'DEBUG': logging.DEBUG, - 'INFO': logging.INFO, - 'WARNING': logging.WARNING, - 'ERROR': logging.ERROR, - 'CRITICAL': logging.CRITICAL - } - - # Default to INFO if level not recognized - numeric_level = level_map.get(log_level, logging.INFO) - - # Configure root logger - logger = logging.getLogger() - logger.setLevel(numeric_level) - - # Console handler with improved formatting - console = logging.StreamHandler() - console.setLevel(numeric_level) - - # Format: timestamp - level - component - message - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s') - console.setFormatter(formatter) - - # Add handler to logger - logger.addHandler(console) - - return logger - - -class MySender(pytak.QueueWorker): - def __init__(self, tx_queue, config, event_loop): - self.logger = logging.getLogger("MySender") - self.logger.debug("Initializing MySender class") - super().__init__(tx_queue, config["mycottool"]) - - # MQTT parameters - self.mqtt_broker = config['mqtt']['host'] - self.mqtt_port = int(config['mqtt']['port']) - self.mqtt_username = config['mqtt']['username'] - self.mqtt_pwd = config['mqtt']['password'] - self.mqtt_topicname = config['mqtt']['topicname'] - - # Capture the main event loop - self.event_loop = event_loop - - # Log MQTT connection details - self.logger.info( - f"MQTT config - Broker: {self.mqtt_broker}, Port: {self.mqtt_port}, Topic: {self.mqtt_topicname}") - - # Set up MQTT client with explicit callbacks - self.logger.debug("Creating MQTT client") - self.mqtt_client = mqtt.Client(client_id="tak_publisher_client", protocol=mqtt.MQTTv311) - - # Set callbacks - self.logger.debug("Setting up MQTT callbacks") - self.mqtt_client.on_connect = self._on_connect - self.mqtt_client.on_disconnect = self._on_disconnect - self.mqtt_client.on_message = self._on_message_sync - - # Set credentials if provided - if self.mqtt_username: - self.logger.debug(f"Setting up MQTT credentials for user: {self.mqtt_username}") - self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_pwd) - - # Connect to MQTT broker - self.logger.info(f"Attempting to connect to MQTT broker at {self.mqtt_broker}:{self.mqtt_port}") - try: - self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port, keepalive=60) - self.logger.debug("MQTT connect() method completed without exception") - except Exception as e: - self.logger.error(f"MQTT Connection failed with exception: {e}") - self.logger.error(f"Exception type: {type(e)}") - - self.logger.debug("MySender initialization completed") - - def _on_connect(self, client, userdata, flags, rc): - """Callback for when the client connects to the broker""" - connection_responses = { - 0: "Connection successful", - 1: "Connection refused - incorrect protocol version", - 2: "Connection refused - invalid client identifier", - 3: "Connection refused - server unavailable", - 4: "Connection refused - bad username or password", - 5: "Connection refused - not authorized" - } - - status = connection_responses.get(rc, f"Unknown error code: {rc}") - self.logger.info(f"MQTT CONNECTION STATUS: {status} (code={rc})") - - if rc == 0: - self.logger.info(f"Connected to MQTT broker at {self.mqtt_broker}:{self.mqtt_port}") - # Subscribe to topic - result, mid = self.mqtt_client.subscribe(self.mqtt_topicname) - self.logger.debug(f"MQTT Subscribe result: {result}, Message ID: {mid}") - if result == 0: - self.logger.info(f"Subscribed to MQTT topic: {self.mqtt_topicname}") - else: - self.logger.error(f"Failed to subscribe to MQTT topic: {self.mqtt_topicname}") - else: - self.logger.error(f"MQTT connection failed: {status}") - - def _on_disconnect(self, client, userdata, rc): - """Callback for when the client disconnects from the broker""" - if rc == 0: - self.logger.info("Disconnected from broker cleanly") - else: - self.logger.warning(f"Unexpected disconnect with code {rc}") - - def start_mqtt_loop(self): - """Start MQTT loop in a separate thread.""" - self.logger.debug("Starting MQTT client loop") - self.mqtt_client.loop_start() - self.logger.debug("MQTT loop started") - - def _on_message_sync(self, client, userdata, message): - """Synchronous wrapper for MQTT on_message to run handle_data in the main event loop.""" - self.logger.debug(f"Message received on topic: {message.topic}") - asyncio.run_coroutine_threadsafe(self.handle_data(client, userdata, message), self.event_loop) - - async def handle_data(self, client, userdata, message): - """Handle incoming MQTT data and put it on the async queue.""" - event = message.payload - await self.put_queue(event) - self.logger.debug(f"Processed message from topic '{message.topic}' and queued for transmission") - - async def run(self, number_of_iterations=-1): - self.logger.debug("MySender.run() method started") - self.start_mqtt_loop() - self.logger.debug("MQTT loop started, now waiting in run loop") - try: - while True: - await asyncio.sleep(10) # Keep the loop running, check every 10 seconds - self.logger.debug("Still running in the MySender.run() loop") - except asyncio.CancelledError: - self.logger.debug("CancelledError caught, stopping MQTT loop") - self.mqtt_client.loop_stop() - self.logger.debug("MQTT loop stopped") - except Exception as e: - self.logger.error(f"Unexpected exception in MySender.run(): {e}") - raise - - -async def main(config): - logger = logging.getLogger("main") - logger.debug("main() function started") - loop = asyncio.get_running_loop() # Capture the main event loop - clitool = pytak.CLITool(config["mycottool"]) - await clitool.setup() - logger.debug("Adding MySender task to CLITool") - clitool.add_task(MySender(clitool.tx_queue, config, loop)) # Pass the loop - logger.debug("Running CLITool") - await clitool.run() - - -def run_main_in_process(config): - logger = logging.getLogger("process") - logger.debug("run_main_in_process() started") - loop = asyncio.get_event_loop() - logger.debug("Event loop created, running main()") - loop.run_until_complete(main(config)) - - -if __name__ == "__main__": - logger = logging.getLogger("startup") - logger.info("Script main block executing") - - parser = argparse.ArgumentParser(description="TAK Publisher Script") - parser.add_argument('--config_file_path', type=str, required=True, help='Path to the config YAML file.') - parser.add_argument('--creds_path', type=str, required=True, help='Path to the creds directory.') - - args = parser.parse_args() - logger.info(f"Args parsed - config_file_path: {args.config_file_path}, creds_path: {args.creds_path}") - - # Load the YAML configuration - try: - with open(args.config_file_path, 'r') as file: - logger.info(f"Loading configuration from {args.config_file_path}") - config_data = yaml.safe_load(file) - logger.info("Configuration loaded successfully") - - # Setup logger based on config - log_level = config_data.get('logging', {}).get('level', 'INFO') - logger = setup_logger(log_level) - logger.info(f"Logger configured with level: {log_level}") - - except Exception as e: - logger.error(f"Failed to load configuration: {e}") - sys.exit(1) - - # Extract necessary parameters - try: - cot_url = config_data['tak_server']['cot_url'] - pytak_tls_client_cert = config_data['tak_server']['pytak_tls_client_cert'] - # Add the creds_path to the pytak_tls_client_cert - pytak_tls_client_cert = Path(args.creds_path) / pytak_tls_client_cert - pytak_tls_client_key = config_data['tak_server']['pytak_tls_client_key'] - # Add the creds_path to the pytak_tls_client_key - pytak_tls_client_key = Path(args.creds_path) / pytak_tls_client_key - - host = config_data['services']['host'] - - # MQTT params - mqtt_broker = config_data['mqtt']['host'] - mqtt_port = config_data['mqtt']['port'] - mqtt_username = config_data['mqtt']['username'] - mqtt_pwd = config_data['mqtt']['password'] - mqtt_topicname = config_data['services']['publisher']['tak_publisher']['topic_name'] - - logger.info(f"MQTT CONFIG: Broker={mqtt_broker}, Port={mqtt_port}, Topic={mqtt_topicname}") - except KeyError as e: - logger.error(f"Missing required configuration key: {e}") - sys.exit(1) - - # Setup config for pytak - config = ConfigParser() - config["mycottool"] = { - "COT_URL": cot_url, - "PYTAK_TLS_CLIENT_CERT": str(pytak_tls_client_cert), - "PYTAK_TLS_CLIENT_KEY": str(pytak_tls_client_key), - "PYTAK_TLS_CLIENT_PASSWORD": "atakatak", - "PYTAK_TLS_DONT_VERIFY": "1" - } - config["service"] = { - "host": host, - } - - config["mqtt"] = { - "host": mqtt_broker, - "port": str(mqtt_port), # Convert to string - "username": mqtt_username, - "password": mqtt_pwd or "", # Handle empty password - "topicname": mqtt_topicname - } - - # Start the asyncio event loop in a separate process - logger.info("Starting TAK publisher process") - process = multiprocessing.Process(target=run_main_in_process, args=(config,)) - process.start() - - logger.info("Main() is now running in a separate process") - process.join() - logger.info("Process completed") \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/scripts/tak_subscriber.py b/gcs/ros_ws/src/ros2tak_tools/scripts/tak_subscriber.py deleted file mode 100644 index 10eb4dfd0..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/scripts/tak_subscriber.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 - -""" -COT Subscriber from TAK Server - -Author: Aditya Rauniyar (rauniyar@cmu.edu) - -Description: -This script acts as a receiver for Cursor-On-Target (CoT) messages. It handles incoming CoT data from a TAK server -and processes it according to the defined logic. The received messages are then sent to a specified -host IP and port defined in the configuration file. - -Usage: -1. Ensure you have Python 3.x installed with the necessary packages: - pip install pytak - -2. Create a configuration file with the parameters specified. - -3. Run the script: - python your_script.py --config path/to/config.yaml -""" - -import asyncio -import xml.etree.ElementTree as ET -import pytak -import argparse -import yaml -from configparser import ConfigParser -import logging -import paho.mqtt.client as mqtt -import sys -from pathlib import Path - -# Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL -LOG_LEVEL = "DEBUG" - - - -def load_config(file_path): - """Load configuration from a YAML file.""" - with open(file_path, 'r') as f: - return yaml.safe_load(f) - - -class MyReceiver(pytak.QueueWorker): - """Defines how you will handle events from RX Queue.""" - - def __init__(self, rx_queue, config, filter_names2topic): - super().__init__(rx_queue, config["mycottool"]) - self.host = config["service"]["host"] - - # MQTT parameters - self.mqtt_broker = config['mqtt']['host'] - self.mqtt_port = int(config['mqtt']['port']) - self.mqtt_username = config['mqtt']['username'] - self.mqtt_pwd = config['mqtt']['password'] - - self._logger.info(f"Sending data to {self.host}:{self.mqtt_port}") - self.filter_names2topic = filter_names2topic - self.total_filters = len(self.filter_names2topic) - print(f"Filter messages number: {self.total_filters}") - - # Set up logging config to print debug message - self._logger.setLevel(logging.DEBUG) - - # Set up MQTT client - self.mqtt_client = mqtt.Client() - self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_pwd) - - # Connect to MQTT broker and subscribe to topic - try: - self._logger.info(f"Connecting to {self.mqtt_broker}:{self.mqtt_port}") - self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port, keepalive=65535) - self._logger.info(f"Connected and subscribed to MQTT on broker {self.mqtt_broker}:{self.mqtt_port}") - except Exception as e: - self._logger.error(f"Failed to connect or subscribe to MQTT: {e}") - - - async def handle_data(self, data): - """Handle data from the receive queue.""" - self._logger.debug("Received:\n%s\n", data.decode()) - - # Parse the CoT message to extract necessary fields - try: - root = ET.fromstring(data.decode()) - uuid = root.get("uid") - - self._logger.info(f"Recevied Message: {data}") - mqtt_topic = self.should_send_message(root) - - # Add your filter conditions here - if mqtt_topic: - # Send received data to the specified host and port - # self._logger.info("Sending data to %s:%s", self.host, self.port) - await self.send_to_mqtt(data, mqtt_topic=mqtt_topic) - else: - # self._logger.info("Filtered out message with UID: %s", uuid) - self._logger.debug(ET.tostring(root, encoding='unicode', method='xml')) - - except ET.ParseError as e: - # self._logger.error("Failed to parse CoT message: %s", e) - pass - - def should_send_message(self, root): - """Determine whether to send the message based on filtering criteria.""" - # Iterate over the filter messages and check if their name exists in the XML - for ii in range(self.total_filters): - filter_name = self.filter_names2topic[ii]["name"] - # Check if the filter name exists anywhere in the root element - if self.is_message_relevant(root, filter_name): - mqtt_topic = self.filter_names2topic[ii]["mqtt_topic"] - return mqtt_topic - - return None - - def is_message_relevant(self, root, filter_name): - """Helper function to check if a filter name exists in the XML string.""" - # Convert the XML to a string - xml_string = ET.tostring(root, encoding='unicode', method='xml') - - self._logger.info(f"Checking with filter: {filter_name}") - - # Check if the filter name exists in the XML string - if filter_name in xml_string: - self._logger.debug("Found filter match in XML string: %s", filter_name) - return True - - return False - - async def send_to_mqtt(self, data, mqtt_topic): - """Send CoT event over the MQTT network""" - try: - self.mqtt_client.publish(mqtt_topic, data) - self._logger.info(f"Message published to topic {mqtt_topic}") - self._logger.debug(f"Message: '{data}'") - except: - self._logger.info(f"Failed to publish.") - - async def run(self): # pylint: disable=arguments-differ - """Read from the receive queue, put data onto handler.""" - while True: - data = await self.queue.get() # Get received CoT from rx_queue - await self.handle_data(data) - - -async def main(): - parser = argparse.ArgumentParser(description="TAK Subscriber Script") - parser.add_argument('--config_file_path', type=str, required=True, help='Path to the config YAML file.') - parser.add_argument('--creds_path', type=str, required=True, help='Path to the creds directory.') - - args = parser.parse_args() - print(f"STARTUP: Args parsed - config_file_path: {args.config_file_path}, creds_path: {args.creds_path}", - flush=True) - - # Load the YAML configuration - try: - with open(args.config_file_path, 'r') as file: - print(f"STARTUP: Loading configuration from {args.config_file_path}", flush=True) - config_data = yaml.safe_load(file) - print("STARTUP: Configuration loaded successfully", flush=True) - except Exception as e: - print(f"ERROR: Failed to load configuration: {e}", flush=True) - sys.exit(1) - - # Extract necessary parameters from the configuration - try: - cot_url = config_data['tak_server']['cot_url'] - pytak_tls_client_cert = config_data['tak_server']['pytak_tls_client_cert'] - # Add the creds_path to the pytak_tls_client_cert - pytak_tls_client_cert = Path(args.creds_path) / pytak_tls_client_cert - pytak_tls_client_key = config_data['tak_server']['pytak_tls_client_key'] - # Add the creds_path to the pytak_tls_client_key - pytak_tls_client_key = Path(args.creds_path) / pytak_tls_client_key - - host = config_data['services']['host'] - filter_messages = config_data['services']['subscriber']['tak_subscriber']['filter_messages'] - # Extract the filter name and corresponding mqtt topic_name - message_name2topic = [{"name": msg['name'], "mqtt_topic": msg["mqtt_topic_name"]} for msg in filter_messages] - - # MQTT params - mqtt_broker = config_data['mqtt']['host'] - # mqtt_broker = "localhost" # Uncomment if running from the host - mqtt_port = config_data['mqtt']['port'] - mqtt_username = config_data['mqtt']['username'] - mqtt_pwd = config_data['mqtt']['password'] - except KeyError as e: - print(f"ERROR: Missing parameter in configuration: {e}", flush=True) - sys.exit(1) - - # Setup config for pytak - config = ConfigParser() - config["mycottool"] = { - "COT_URL": cot_url, - "PYTAK_TLS_CLIENT_CERT": str(pytak_tls_client_cert), - "PYTAK_TLS_CLIENT_KEY": str(pytak_tls_client_key), - "PYTAK_TLS_CLIENT_PASSWORD": "atakatak", - "PYTAK_TLS_DONT_VERIFY": "1" - } - - config["service"] = { - "host": host - } - - config["mqtt"] = { - "host": mqtt_broker, - "port": str(mqtt_port), - "username": mqtt_username, - "password": mqtt_pwd or "" - } - - # Initialize worker queues and tasks. - clitool = pytak.CLITool(config["mycottool"]) - await clitool.setup() - - clitool.add_task(MyReceiver(clitool.rx_queue, config, message_name2topic)) - # Start all tasks. - await clitool.run() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/gcs/ros_ws/src/ros2tak_tools/setup.cfg b/gcs/ros_ws/src/ros2tak_tools/setup.cfg deleted file mode 100644 index 23d12fb81..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/ros2tak_tools -[install] -install_scripts=$base/lib/ros2tak_tools \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/setup.py b/gcs/ros_ws/src/ros2tak_tools/setup.py deleted file mode 100644 index a29746d5d..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -from setuptools import find_packages, setup -import os -from glob import glob - -package_name = 'ros2tak_tools' - -setup( - name=package_name, - version='1.0.0', - packages=find_packages(exclude=['test']) + ['tak_helper'], - data_files=[ - ('share/ament_index/resource_index/packages', - ['resource/' + package_name]), - ('share/' + package_name, ['package.xml']), - # Include all files in config directory - ('share/' + package_name + '/config', glob('config/*.*')), - # Include all files in the launch directory - ('share/' + package_name + '/launch', glob('launch/*.*')), - # Include all files in scripts directory - ('share/' + package_name + '/scripts', glob('scripts/*.*')), - # Create lib directory for executables - ('lib/' + package_name, []), - ] + [ - # This will recursively include all files in creds directory and its subdirectories - (os.path.join('share', package_name, os.path.dirname(p)), [p]) - for p in glob('creds/**/*', recursive=True) - if os.path.isfile(p) - ], - install_requires=['setuptools'], - zip_safe=True, - maintainer='Adi', - maintainer_email='rauniyar@cmu.edu', - description='TODO: Package description', - license='BSD-3', - tests_require=['pytest'], - entry_points={ - 'console_scripts': [ - 'ros2cot_agent = ros2tak_tools.ros2cot_agent:main', - 'cot2ros_agent = ros2tak_tools.cot2ros_agent:main', - 'cot2planner_agent = ros2tak_tools.cot2planner_agent:main', - 'ros2casevac_agent = ros2tak_tools.ros2casevac_agent:main', - 'chat2ros_agent = ros2tak_tools.chat2ros_agent:main', - ], - }, -) \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/tak_helper/Casualty.py b/gcs/ros_ws/src/ros2tak_tools/tak_helper/Casualty.py deleted file mode 100755 index a5514aa42..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/tak_helper/Casualty.py +++ /dev/null @@ -1,545 +0,0 @@ -from distutils.core import setup - -from sphinx.addnodes import index -from straps_msgs.msg import CasualtyMeta, Injury, Critical, Vitals -from xml.etree.ElementTree import Element, SubElement, tostring -import uuid -from datetime import datetime, timedelta -import pytak -from enum import Enum - - -""" -Enums to represent the different types of injuries and vitals. -""" - -class TraumaType(Enum): - TRAUMA_HEAD = Injury.TRAUMA_HEAD - TRAUMA_TORSO = Injury.TRAUMA_TORSO - TRAUMA_LOWER_EXT = Injury.TRAUMA_LOWER_EXT - TRAUMA_UPPER_EXT = Injury.TRAUMA_UPPER_EXT - ALERTNESS_OCULAR = Injury.ALERTNESS_OCULAR - ALERTNESS_VERBAL = Injury.ALERTNESS_VERBAL - ALERTNESS_MOTOR = Injury.ALERTNESS_MOTOR - -class TraumaSeverity(Enum): - TRAUMA_NORMAL = Injury.TRAUMA_NORMAL - TRAUMA_WOUND = Injury.TRAUMA_WOUND - TRAUMA_AMPUTATION = Injury.TRAUMA_AMPUTATION - -class OcularAlertness(Enum): - OCULAR_OPEN = Injury.OCULAR_OPEN - OCULAR_CLOSED = Injury.OCULAR_CLOSED - OCULAR_NOT_TESTABLE = Injury.OCULAR_NOT_TESTABLE - -class AlertnessLevel(Enum): - ALERTNESS_NORMAL = Injury.ALERTNESS_NORMAL - ALERTNESS_ABNORMAL = Injury.ALERTNESS_ABNORMAL - ALERTNESS_ABSENT = Injury.ALERTNESS_ABSENT - ALERTNESS_NOT_TESTABLE = Injury.ALERTNESS_NOT_TESTABLE - -class VitalType(Enum): - HEART_RATE = Vitals.HEART_RATE - RESPIRATORY_RATE = Vitals.RESPIRATORY_RATE - TEMPERATURE = Vitals.TEMPERATURE - - -class ConditionType(Enum): - SEVERE_HEMORRHAGE = Critical.SEVERE_HEMORRHAGE - RESPIRATORY_DISTRESS = Critical.RESPIRATORY_DISTRESS - -class ConditionStatus(Enum): - ABSENT = Critical.ABSENT - PRESENT = Critical.PRESENT - -""" -Functions to validate the type and value of the injury. -""" -def is_valid_type_injury_value(trauma_type, value): - """Validates that the value matches the type based on the rules.""" - if trauma_type in [TraumaType.TRAUMA_HEAD, TraumaType.TRAUMA_TORSO]: - # TRAUMA_HEAD and TRAUMA_TORSO should have values TRAUMA_NORMAL or TRAUMA_WOUND - return value in [TraumaSeverity.TRAUMA_NORMAL, TraumaSeverity.TRAUMA_WOUND] - - elif trauma_type in [TraumaType.TRAUMA_LOWER_EXT, TraumaType.TRAUMA_UPPER_EXT]: - # TRAUMA_LOWER_EXT and TRAUMA_UPPER_EXT should have values TRAUMA_NORMAL, TRAUMA_WOUND, or TRAUMA_AMPUTATION - return value in [TraumaSeverity.TRAUMA_NORMAL, TraumaSeverity.TRAUMA_WOUND, TraumaSeverity.TRAUMA_AMPUTATION] - - elif trauma_type == TraumaType.ALERTNESS_OCULAR: - # ALERTNESS_OCULAR should have values OCULAR_OPEN, OCULAR_CLOSED, or OCULAR_NOT_TESTABLE - return value in [OcularAlertness.OCULAR_OPEN, OcularAlertness.OCULAR_CLOSED, OcularAlertness.OCULAR_NOT_TESTABLE] - - elif trauma_type in [TraumaType.ALERTNESS_VERBAL, TraumaType.ALERTNESS_MOTOR]: - # ALERTNESS_VERBAL and ALERTNESS_MOTOR should have values ALERTNESS_NORMAL, ALERTNESS_ABNORMAL, ALERTNESS_ABSENT, or ALERTNESS_NOT_TESTABLE - return value in [AlertnessLevel.ALERTNESS_NORMAL, AlertnessLevel.ALERTNESS_ABNORMAL, AlertnessLevel.ALERTNESS_ABSENT, AlertnessLevel.ALERTNESS_NOT_TESTABLE] - - return False - -# Function to create a unique casualty ID -def create_casualty_id(casualty_id:int): - return f"casualty-{casualty_id}" - -""" -Classes to represent a Casualty object with all the necessary information for triage. -""" - -class GPSCOT: - """ - GPS class to store the GPS coordinates of the casualty. - """ - # Define the types - status: bool - latitude: float - longitude: float - altitude: float - - def __init__(self): - self.status = False - self.latitude = 0.0 - self.longitude = 0.0 - self.altitude = 0.0 - - def set_gps(self, latitude, longitude, altitude): - self.latitude = latitude - self.longitude = longitude - self.altitude = altitude - self.status = True - - -class VitalsCOT: - """ - Vitals class to store the vitals - """ - # Define the class types - vitals_name: str - status: bool # True if diagnosis has been made, False otherwise - system: str - type: VitalType - value: float - time_ago: float - confidence: float - - # initialize the class with default values - def __init__(self, vitals_name="", system='', type_=None, value=None, time_ago=None, confidence=0.0): - self.vitals_name = vitals_name - self.status = False - self.system = system - self.type = type_ - self.value = value - self.time_ago = time_ago - self.confidence = confidence - - def set_vitals(self, system, type_, value, time_ago, confidence): - # Ensuring that type is either HEART_RATE or RESPIRATORY_RATE - if type_ not in [VitalType.HEART_RATE, VitalType.RESPIRATORY_RATE]: - raise ValueError("Type must be either HEART_RATE (2) or RESPIRATORY_RATE (3)") - - self.system = system - self.type = type_ - self.value = value - self.time_ago = time_ago - self.confidence = confidence - self.status = True - - def __repr__(self): - return f"{self.vitals_name}(Confidence={self.confidence*100:0.2f}%)" - -class InjuryCOT: - """ - Injury class to store the injury status - """ - # Define the class types - injury_name: str - status: bool # True if diagnosis has been made, False otherwise - system: str - type: TraumaType - value: TraumaSeverity - confidence: float - - def __init__(self, injury_name="", system='', type_=None, value=None, confidence=0.0): - self.injury_name = injury_name - self.status = False - self.system = system - self.type_ = type_ - self.value = value - self.confidence = confidence - - def set_status(self, system, type_, value, confidence): - """Sets the status of the injury, with validation for type and value.""" - if not is_valid_type_injury_value(type_, value): - raise ValueError(f"Invalid value for type {type_}: {value}") - - self.system = system - self.type_ = type_ - self.value = value - self.confidence = confidence - self.status = True - - def __repr__(self): - return f"{self.injury_name}(Confidence={self.confidence*100:0.2f}%)" - - -class CriticalCOT: - """ - Critical class to store the critical condition status - """ - # Define the class types - critical_name: str - status: bool # True if diagnosis has been made, False otherwise - system: str - type: ConditionType - value: ConditionStatus - confidence: float - - def __init__(self, critical_name="", system='', type_=None, value=None, confidence=0.0): - self.critical_name = critical_name - self.status = False - self.system = system - self.type = type_ - self.value = value - self.confidence = confidence - - def set_status(self, system: str, type_: ConditionType, value: ConditionStatus, confidence: float): - """Sets the status of the injury, with validation for type and value.""" - self.system = system - self.type = type_ - self.value = value - self.confidence = confidence - self.status = True - - def __repr__(self): - return f"{self.critical_name}(Confidence={self.confidence*100:0.2f}%)" - - -class CasualtyCOT: - """ - Casualty class to store all the information of a casualty. - """ - # Define the class types - gps: GPSCOT - stamp: str - casualty_id: str - # Critical conditions - severe_hemorrhage: CriticalCOT - respiratory_distress: CriticalCOT - # Vitals - heart_rate: VitalsCOT - respiratory_rate: VitalsCOT - # Injuries - trauma_head: InjuryCOT - trauma_torso: InjuryCOT - trauma_lower_ext: InjuryCOT - trauma_upper_ext: InjuryCOT - # Alertness - alertness_ocular: InjuryCOT - alertness_verbal: InjuryCOT - alertness_motor: InjuryCOT - - def __init__(self, casualty_id:int): - self.stamp = pytak.cot_time() - - self.casualty_id = create_casualty_id(casualty_id) - - self.gps = GPSCOT() - - self.severe_hemorrhage = CriticalCOT("Severe Hemorrhage") - self.respiratory_distress = CriticalCOT("Respiratory Distress") - - self.heart_rate = VitalsCOT("Heart Rate") - self.respiratory_rate = VitalsCOT("Respiratory Rate") - self.temperature = VitalsCOT("Temperature") - - self.trauma_head = InjuryCOT("Trauma Head") - self.trauma_torso = InjuryCOT("Trauma Torso") - self.trauma_lower_ext = InjuryCOT("Trauma Lower Extremity") - self.trauma_upper_ext = InjuryCOT("Trauma Upper Extremity") - - self.alertness_ocular = InjuryCOT("Alertness Ocular") - self.alertness_verbal = InjuryCOT("Alertness Verbal") - self.alertness_motor = InjuryCOT("Alertness Motor") - - # ZMIST Report Fields: - # Z: Zap Number – A unique identifier for the casualty. - # M: Mechanism of Injury – Describes how the injury occurred (e.g., explosion, gunshot wound). - # I: Injuries Sustained – Specifies the injuries observed (e.g., right leg amputation). - # S: Signs and Symptoms – Details vital signs and symptoms (e.g., massive hemorrhage, no radial pulse). - # T: Treatments Rendered – Lists the medical interventions provided (e.g., tourniquet applied, pain medication administered). - - def get_zap_num(self): - return f"{self.casualty_id}" - - def get_mechanism(self): - return "Unknown" - - def get_injuries(self): - """Returns the injuries sustained for preset status""" - - injuries = [] - if self.trauma_head.status: - injuries.append(repr(self.trauma_head)) - if self.trauma_torso.status: - injuries.append(repr(self.trauma_torso)) - if self.trauma_lower_ext.status: - injuries.append(repr(self.trauma_lower_ext)) - if self.trauma_upper_ext.status: - injuries.append(repr(self.trauma_upper_ext)) - return ", ".join(injuries) - - - def get_signs_symptoms(self): - """Returns the signs and symptoms for preset status""" - - signs = [] - if self.severe_hemorrhage.status: - signs.append(repr(self.severe_hemorrhage)) - if self.respiratory_distress.status: - signs.append(repr(self.respiratory_distress)) - if self.heart_rate.status: - signs.append(repr(self.heart_rate)) - if self.respiratory_rate.status: - signs.append(repr(self.respiratory_rate)) - return ", ".join(signs) - - def get_treatments(self): - return "Unknown" - - def update_casualty_metadata(self, msg: CasualtyMeta): - """Updates the casualty metadata with the message data.""" - # Update GPS coordinates - if msg.gps: # Check if the array is not empty - try: - for gps_data in msg.gps: - self.gps.set_gps(gps_data.latitude, gps_data.longitude, gps_data.altitude) - except Exception as e: - print(f"Error updating GPS data: {e}") - - # Update critical conditions - if msg.severe_hemorrhage: # Check if the array is not empty - try: - for hemorrhage_data in msg.severe_hemorrhage: - self.severe_hemorrhage.set_status( - system="Circulatory", - type_=ConditionType.SEVERE_HEMORRHAGE, - value=ConditionStatus(hemorrhage_data.value), - confidence=hemorrhage_data.confidence - ) - except Exception as e: - print(f"Error updating severe hemorrhage data: {e}") - - if msg.respiratory_distress: # Check if the array is not empty - try: - for distress_data in msg.respiratory_distress: - self.respiratory_distress.set_status( - system="Respiratory", - type_=ConditionType.RESPIRATORY_DISTRESS, - value=ConditionStatus(distress_data.value), - confidence=distress_data.confidence - ) - except Exception as e: - print(f"Error updating respiratory distress data: {e}") - - # Update vitals - if msg.heart_rate: # Check if the array is not empty - try: - for heart_rate_data in msg.heart_rate: - self.heart_rate.set_vitals( - system="Cardiovascular", - type_=VitalType.HEART_RATE, - value=heart_rate_data.value, - time_ago=heart_rate_data.time_ago, - confidence=heart_rate_data.confidence - ) - except Exception as e: - print(f"Error updating heart rate data: {e}") - - if msg.respiratory_rate: # Check if the array is not empty - try: - for resp_rate_data in msg.respiratory_rate: - self.respiratory_rate.set_vitals( - system="Respiratory", - type_=VitalType.RESPIRATORY_RATE, - value=resp_rate_data.value, - time_ago=resp_rate_data.time_ago, - confidence=resp_rate_data.confidence - ) - except Exception as e: - print(f"Error updating respiratory rate data: {e}") - - if msg.temperature: # Check if the array is not empty - try: - for temp_data in msg.temperature: - self.temperature.set_vitals( - system="Body", - type_=VitalType.TEMPERATURE, - value=temp_data.value, - time_ago=temp_data.time_ago, - confidence=temp_data.confidence - ) - except Exception as e: - print(f"Error updating temperature data: {e}") - - # Update injuries - if msg.trauma_head: # Check if the array is not empty - try: - for trauma_data in msg.trauma_head: - self.trauma_head.set_status( - system="Head", - type_=TraumaType.TRAUMA_HEAD, - value=TraumaSeverity(trauma_data.value), - confidence=trauma_data.confidence - ) - except Exception as e: - print(f"Error updating trauma head data: {e}") - - if msg.trauma_torso: # Check if the array is not empty - try: - for trauma_data in msg.trauma_torso: - self.trauma_torso.set_status( - system="Torso", - type_=TraumaType.TRAUMA_TORSO, - value=TraumaSeverity(trauma_data.value), - confidence=trauma_data.confidence - ) - except Exception as e: - print(f"Error updating trauma torso data: {e}") - - if msg.trauma_lower_ext: # Check if the array is not empty - try: - for trauma_data in msg.trauma_lower_ext: - self.trauma_lower_ext.set_status( - system="Lower Extremity", - type_=TraumaType.TRAUMA_LOWER_EXT, - value=TraumaSeverity(trauma_data.value), - confidence=trauma_data.confidence - ) - except Exception as e: - print(f"Error updating trauma lower extremity data: {e}") - - if msg.trauma_upper_ext: # Check if the array is not empty - try: - for trauma_data in msg.trauma_upper_ext: - self.trauma_upper_ext.set_status( - system="Upper Extremity", - type_=TraumaType.TRAUMA_UPPER_EXT, - value=TraumaSeverity(trauma_data.value), - confidence=trauma_data.confidence - ) - except Exception as e: - print(f"Error updating trauma upper extremity data: {e}") - - # Update alertness levels - if msg.alertness_ocular: # Check if the array is not empty - try: - for alertness_data in msg.alertness_ocular: - self.alertness_ocular.set_status( - system="Neurological", - type_=TraumaType.ALERTNESS_OCULAR, - value=OcularAlertness(alertness_data.value), - confidence=alertness_data.confidence - ) - except Exception as e: - print(f"Error updating alertness ocular data: {e}") - - if msg.alertness_verbal: # Check if the array is not empty - try: - for alertness_data in msg.alertness_verbal: - self.alertness_verbal.set_status( - system="Neurological", - type_=TraumaType.ALERTNESS_VERBAL, - value=AlertnessLevel(alertness_data.value), - confidence=alertness_data.confidence - ) - except Exception as e: - print(f"Error updating alertness verbal data: {e}") - - if msg.alertness_motor: # Check if the array is not empty - try: - for alertness_data in msg.alertness_motor: - self.alertness_motor.set_status( - system="Neurological", - type_=TraumaType.ALERTNESS_MOTOR, - value=AlertnessLevel(alertness_data.value), - confidence=alertness_data.confidence - ) - except Exception as e: - print(f"Error updating alertness motor data: {e}") - - - def generate_cot_event(self): - # Create root event element - event = Element('event', { - 'version': "2.0", - 'uid': str(uuid.uuid4()), # Generate a unique UID - 'type': "b-r-f-h-c", - 'how': "h-g-i-g-o", - 'time': self.stamp, - 'start': self.stamp, - 'stale': pytak.cot_time(2400) - }) - - # Create point element - point = SubElement(event, 'point', { - 'lat': f"{self.gps.latitude}", - 'lon': f"{self.gps.longitude}", - 'hae': "9999999.0", - 'ce': "9999999.0", - 'le': "9999999.0" - }) - - # Create detail element - detail = SubElement(event, 'detail') - - # Add contact element - contact = SubElement(detail, 'contact', { - 'callsign': self.casualty_id - }) - - # Add link element - link = SubElement(detail, 'link', { - 'type': "a-f-G-U-C-I", - 'uid': "S-1-5-21-942292099-3747883346-3641641706-1000", - 'parent_callsign': self.casualty_id, - 'relation': "p-p", - 'production_time': self.stamp - }) - - # Add archive and status elements - SubElement(detail, 'archive') - SubElement(detail, 'status', {'readiness': "false"}) - SubElement(detail, 'remarks') - - # Create _medevac_ element with nested zMistsMap and zMist - medevac = SubElement(detail, '_medevac_', { - 'title': "MED.12.201008", - 'casevac': "false", - 'freq': "0.0", - 'equipment_none': "true", - 'security': "0", - 'hlz_marking': "3", - 'terrain_none': "true", - 'obstacles': "None", - 'medline_remarks': "", - 'zone_prot_selection': "0" - }) - - zMistsMap = SubElement(medevac, 'zMistsMap') - zMist = SubElement(zMistsMap, 'zMist', { - 'z': self.get_zap_num(), - 'm': self.get_mechanism(), - 'i': self.get_injuries(), - 's': self.get_signs_symptoms(), - 't': self.get_treatments(), - 'title': "ZMIST1" - }) - - # Add _flow-tags_ element - flow_tags = SubElement(detail, '_flow-tags_', { - 'TAK-Server-f6edbf55ccfa4af1b4cfa2d7f177ea67': f"chiron-tak_subscriber: {datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}" - }) - - # Convert to a string - return tostring(event, encoding='utf-8').decode('utf-8') - - - - diff --git a/gcs/ros_ws/src/ros2tak_tools/tak_helper/__init__.py b/gcs/ros_ws/src/ros2tak_tools/tak_helper/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/gcs/ros_ws/src/ros2tak_tools/tak_helper/create_cot_msgs.py b/gcs/ros_ws/src/ros2tak_tools/tak_helper/create_cot_msgs.py deleted file mode 100644 index a14367ec6..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/tak_helper/create_cot_msgs.py +++ /dev/null @@ -1,309 +0,0 @@ -import xml.etree.ElementTree as ET -import pytak -from datetime import datetime -import uuid -from typing import List, Tuple -import logging - -def create_gps_COT(uuid, latitude, longitude, altitude, logger_name, device_type=None): - """Create a CoT event based on the GPS data.""" - logger = logging.getLogger(logger_name) - - # Define CoT type based on robot type - cot_type = "a-f-G" # Default generic - if device_type: - if device_type.lower() == 'uav': - cot_type = "a-f-A" # Aircraft - elif device_type.lower() in ['ugv', 'quadruped', 'offroad']: - cot_type = "a-f-G" # Ground robot - - logger.debug(f"Creating CoT event for {uuid} with type {cot_type}") - - root = ET.Element("event") - root.set("version", "2.0") - root.set("type", cot_type) - root.set("uid", uuid) # Use robot name as UID for identification - root.set("how", "m-g") - root.set("time", pytak.cot_time()) - root.set("start", pytak.cot_time()) - root.set("stale", pytak.cot_time(3600)) - - pt_attr = { - "lat": str(latitude), - "lon": str(longitude), - "hae": str(altitude), - "ce": "10", - "le": "10", - } - - ET.SubElement(root, "point", attrib=pt_attr) - - # Adding detail section - detail = ET.SubElement(root, "detail") - - # Add robot type information if provided - if device_type: - contact = ET.SubElement(detail, "contact") - contact.set("callsign", uuid) - - takv = ET.SubElement(detail, "takv") - takv.set("device", device_type) - - logger.debug(f"CoT event created successfully for {uuid}") - return ET.tostring(root, encoding="utf-8") - -def create_casevac_COT(uuid, casualty_id, gps, zap_num, mechanism, injury, signs_symptoms, treatments, physician_name): - # Create root event element - event = ET.Element( - "event", - { - "version": "2.0", - "uid": casualty_id, - "type": "b-r-f-h-c", - "how": "h-g-i-g-o", - "time": pytak.cot_time(), - "start": pytak.cot_time(), - "stale": pytak.cot_time(3600), - }, - ) - - # Create point element - point = ET.SubElement( - event, - "point", - { - "lat": f"{gps.latitude}", - "lon": f"{gps.longitude}", - "hae": "9999999.0", - "ce": "9999999.0", - "le": "9999999.0", - }, - ) - - # Create detail element - detail = ET.SubElement(event, "detail") - - # Add contact element - contact = ET.SubElement(detail, "contact", {"callsign": casualty_id}) - - # Add link element - link = ET.SubElement( - detail, - "link", - { - "type": "a-f-G-U-C-I", - "uid": "S-1-5-21-942292099-3747883346-3641641706-1000", - "parent_callsign": casualty_id, - "relation": "p-p", - "production_time": pytak.cot_time(), - }, - ) - - # Add archive and status elements - ET.SubElement(detail, "archive") - ET.SubElement(detail, "status", {"readiness": "false"}) - ET.SubElement(detail, "remarks") - - # Create _medevac_ element with nested zMistsMap and zMist - medevac = ET.SubElement( - detail, - "_medevac_", - { - "title": casualty_id.upper(), - "casevac": "false", - "freq": "0.0", - "equipment_none": "true", - "security": "0", - "hlz_marking": "3", - "terrain_none": "true", - "obstacles": "None", - "medline_remarks": "", - "zone_prot_selection": "0", - }, - ) - - zMistsMap = ET.SubElement(medevac, "zMistsMap") - zMist = ET.SubElement( - zMistsMap, - "zMist", - { - "z": zap_num, - "m": mechanism, - "i": injury, - "s": signs_symptoms, - "t": treatments, - "title": physician_name, - }, - ) - - # Add _flow-tags_ element - flow_tags = ET.SubElement( - detail, - "_flow-tags_", - { - "TAK-Server-f6edbf55ccfa4af1b4cfa2d7f177ea67": f"chiron-tak_subscriber: {datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}" - }, - ) - - # Convert to a string - return ET.tostring(event, encoding="utf-8").decode("utf-8") - -def create_chat_COT(uuid, callsign: str, message: str) -> str: - - # Create root event element - event = ET.Element( - "event", - { - "version": "2.0", - "uid": f"GeoChat.{uuid}", - "type": "b-t-f", - "how": "h-g-i-g-o", - "time": pytak.cot_time(), - "start": pytak.cot_time(), - "stale": pytak.cot_time(3600), - } - ) - - # Create point element - point = ET.SubElement( - event, - "point", - { - "lat": "0.0", - "lon": "0.0", - "hae": "9999999.0", - "ce": "9999999.0", - "le": "9999999.0", - }, - ) - - # Create detail element - detail = ET.SubElement(event, "detail") - - # Create __chat element - chat = ET.SubElement( - detail, - "__chat", - { - "id": "All Chat Rooms", - "chatroom": "All Chat Rooms", - "senderCallsign": callsign, - "groupOwner": "false", - } - ) - - # Add chatgrp element - chatgrp = ET.SubElement( - chat, - "chatgrp", - { - "id": "All Chat Rooms", - "uid0": uuid, - "uid1": "All Chat Rooms", - }, - ) - - # Add link element - link = ET.SubElement( - detail, - "link", - { - "uid": uuid, - "type": "a-f-G-U-C-I", - "relation": "p-p", - }, - ) - - # Add remarks element - remarks = ET.SubElement( - detail, - "remarks", - { - "source": f"BAO.F.AIRLAB_CLI_MANAGER_{uuid}", - "sourceID": uuid, - "to": "All Chat Rooms", - "time": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), - } - ) - remarks.text = message - - # Add _flow-tags_ element - flow_tags = ET.SubElement( - detail, - "_flow-tags_", - { - "TAK-Server-f6edbf55ccfa4af1b4cfa2d7f177ea67": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), - }, - ) - - # Convert to a string - return ET.tostring(event, encoding="utf-8").decode("utf-8") - -from datetime import datetime, timedelta - -def create_polygon_COT( - uuid: str, - callsign: str, - gps_coordinates: List[Tuple[float, float]], - fill_color: int = 16777215, -) -> bytes: - """ - Creates a CoT message for a polygon using GPS coordinates. - - Args: - gps_coordinates (list of tuples): List of (latitude, longitude) coordinates. - uuid (str): Unique identifier for the CoT event. - callsign (str): Callsign for the contact. - fill_color (int, optional): Fill color value. Default is 16777215. - fill_color = (Red (0-255) << 16) + (Green (0-255) << 8) + Blue (0-255) - - - Returns: - bytes: CoT message as a UTF-8 encoded XML byte string. - """ - if not gps_coordinates: - raise ValueError("GPS coordinates list cannot be empty.") - - # Ensure the polygon is closed - if gps_coordinates[0] != gps_coordinates[-1]: - gps_coordinates.append(gps_coordinates[0]) - - # Create the root element - root = ET.Element("event", { - "version": "2.0", - "uid": uuid, - "type": "u-d-f", - "how": "h-e", - "time": pytak.cot_time(), - "start": pytak.cot_time(), - "stale": pytak.cot_time(3600), - }) - - # Add the point element - first_point = gps_coordinates[0] - ET.SubElement(root, "point", { - "lat": str(first_point[0]), - "lon": str(first_point[1]), - "hae": "9999999.0", - "ce": "9999999.0", - "le": "9999999.0", - }) - - # Add the detail element - detail = ET.SubElement(root, "detail") - ET.SubElement(detail, "contact", {"callsign": callsign}) - ET.SubElement(detail, "strokeColor", {"value": "-1"}) - ET.SubElement(detail, "fillColor", {"value": str(fill_color)}) - ET.SubElement(detail, "remarks") - ET.SubElement(detail, "height", {"value": "0.00"}) - ET.SubElement(detail, "height_unit", {"value": "4"}) - ET.SubElement(detail, "archive") - - # Add link elements for each GPS point - for lat, lon in gps_coordinates: - ET.SubElement(detail, "link", {"point": f"{lat},{lon}"}) - - ET.SubElement(detail, "archive") - - # Generate the XML string - return ET.tostring(root, encoding="utf-8", xml_declaration=True) \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/tak_helper/logger.py b/gcs/ros_ws/src/ros2tak_tools/tak_helper/logger.py deleted file mode 100644 index 5da67aabc..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/tak_helper/logger.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging - - -def setup_logger(node, log_level): - """ - Set up the logger with appropriate log level and formatting. - - Args: - node: ROS2 node instance for logging through ROS system - log_level: The log level from config (DEBUG, INFO, WARNING, ERROR, CRITICAL) - - Returns: - Configured logger object - """ - # Convert string log level to logging constants - level_map = { - 'DEBUG': logging.DEBUG, - 'INFO': logging.INFO, - 'WARNING': logging.WARNING, - 'ERROR': logging.ERROR, - 'CRITICAL': logging.CRITICAL - } - - # Default to INFO if level not recognized - numeric_level = level_map.get(log_level, logging.INFO) - - # Configure root logger - logger = logging.getLogger() - logger.setLevel(numeric_level) - - # Remove any existing handlers to avoid duplicates - for handler in logger.handlers[:]: - logger.removeHandler(handler) - - # Console handler with improved formatting - console = logging.StreamHandler() - console.setLevel(numeric_level) - - # Format: timestamp - level - component - message - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s') - console.setFormatter(formatter) - - # Add handler to logger - logger.addHandler(console) - - # Map Python logging levels to ROS2 logging levels - def log_bridge(record): - if record.levelno >= logging.CRITICAL: - node.get_logger().fatal(record.getMessage()) - elif record.levelno >= logging.ERROR: - node.get_logger().error(record.getMessage()) - elif record.levelno >= logging.WARNING: - node.get_logger().warn(record.getMessage()) - elif record.levelno >= logging.INFO: - node.get_logger().info(record.getMessage()) - elif record.levelno >= logging.DEBUG: - node.get_logger().debug(record.getMessage()) - - # Create a handler that bridges Python logging to ROS2 logging - class ROS2LogHandler(logging.Handler): - def emit(self, record): - log_bridge(record) - - # Add ROS2 log handler - ros2_handler = ROS2LogHandler() - ros2_handler.setLevel(numeric_level) - logger.addHandler(ros2_handler) - - return logger \ No newline at end of file diff --git a/gcs/ros_ws/src/ros2tak_tools/test/test_copyright.py b/gcs/ros_ws/src/ros2tak_tools/test/test_copyright.py deleted file mode 100644 index 97a39196e..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/test/test_copyright.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_copyright.main import main -import pytest - - -# Remove the `skip` decorator once the source file(s) have a copyright header -@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') -@pytest.mark.copyright -@pytest.mark.linter -def test_copyright(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found errors' diff --git a/gcs/ros_ws/src/ros2tak_tools/test/test_flake8.py b/gcs/ros_ws/src/ros2tak_tools/test/test_flake8.py deleted file mode 100644 index 27ee1078f..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/test/test_flake8.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2017 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_flake8.main import main_with_errors -import pytest - - -@pytest.mark.flake8 -@pytest.mark.linter -def test_flake8(): - rc, errors = main_with_errors(argv=[]) - assert rc == 0, \ - 'Found %d code style errors / warnings:\n' % len(errors) + \ - '\n'.join(errors) diff --git a/gcs/ros_ws/src/ros2tak_tools/test/test_pep257.py b/gcs/ros_ws/src/ros2tak_tools/test/test_pep257.py deleted file mode 100644 index b234a3840..000000000 --- a/gcs/ros_ws/src/ros2tak_tools/test/test_pep257.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_pep257.main import main -import pytest - - -@pytest.mark.linter -@pytest.mark.pep257 -def test_pep257(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found code style errors / warnings' diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/CHANGELOG.rst b/gcs/ros_ws/src/rqt_airstack_control_panel/CHANGELOG.rst deleted file mode 100644 index 0a789b786..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/CHANGELOG.rst +++ /dev/null @@ -1,149 +0,0 @@ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Changelog for package rqt_py_console -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1.0.2 (2021-08-31) ------------------- -* Fix modern setuptools warning about dashes instead of underscores (`#11 `_) -* Contributors: Chris Lalancette - -1.0.1 (2021-04-27) ------------------- -* Changed the build type to ament_python and fixed package to run with ros2 run (`#8 `_) -* Contributors: Alejandro Hernández Cordero - -1.0.0 (2018-12-11) ------------------- -* spyderlib -> spyder (`#5 `_) -* ros2 port (`#3 `_) -* autopep8 (`#2 `_) -* Contributors: Mike Lautman - -0.4.8 (2017-04-28) ------------------- - -0.4.7 (2017-03-02) ------------------- - -0.4.6 (2017-02-27) ------------------- - -0.4.5 (2017-02-03) ------------------- - -0.4.4 (2017-01-24) ------------------- -* use Python 3 compatible syntax (`#421 `_) - -0.4.3 (2016-11-02) ------------------- - -0.4.2 (2016-09-19) ------------------- - -0.4.1 (2016-05-16) ------------------- - -0.4.0 (2016-04-27) ------------------- -* Support Qt 5 (in Kinetic and higher) as well as Qt 4 (in Jade and earlier) (`#359 `_) - -0.3.13 (2016-03-08) -------------------- - -0.3.12 (2015-07-24) -------------------- - -0.3.11 (2015-04-30) -------------------- - -0.3.10 (2014-10-01) -------------------- -* update plugin scripts to use full name to avoid future naming collisions - -0.3.9 (2014-08-18) ------------------- - -0.3.8 (2014-07-15) ------------------- - -0.3.7 (2014-07-11) ------------------- -* export architecture_independent flag in package.xml (`#254 `_) - -0.3.6 (2014-06-02) ------------------- - -0.3.5 (2014-05-07) ------------------- - -0.3.4 (2014-01-28) ------------------- - -0.3.3 (2014-01-08) ------------------- -* add groups for rqt plugins, renamed some plugins (`#167 `_) - -0.3.2 (2013-10-14) ------------------- - -0.3.1 (2013-10-09) ------------------- - -0.3.0 (2013-08-28) ------------------- - -0.2.17 (2013-07-04) -------------------- - -0.2.16 (2013-04-09 13:33) -------------------------- - -0.2.15 (2013-04-09 00:02) -------------------------- - -0.2.14 (2013-03-14) -------------------- - -0.2.13 (2013-03-11 22:14) -------------------------- - -0.2.12 (2013-03-11 13:56) -------------------------- - -0.2.11 (2013-03-08) -------------------- - -0.2.10 (2013-01-22) -------------------- - -0.2.9 (2013-01-17) ------------------- - -0.2.8 (2013-01-11) ------------------- - -0.2.7 (2012-12-24) ------------------- - -0.2.6 (2012-12-23) ------------------- - -0.2.5 (2012-12-21 19:11) ------------------------- - -0.2.4 (2012-12-21 01:13) ------------------------- - -0.2.3 (2012-12-21 00:24) ------------------------- - -0.2.2 (2012-12-20 18:29) ------------------------- - -0.2.1 (2012-12-20 17:47) ------------------------- - -0.2.0 (2012-12-20 17:39) ------------------------- -* first release of this package into groovy diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/README.md b/gcs/ros_ws/src/rqt_airstack_control_panel/README.md deleted file mode 100644 index e13fe32eb..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# RQT Python AirstackControlPanel - -If you `colcon build` this package in a workspace and then run "rqt --force-discover" after sourcing the workspace, the plugin should show up as "Airstack Control Panel" in "Miscellaneous Tools" in the "Plugins" menu. - -You can use the `generate_rqt_py_package.sh` script to generate a new package by doing the following from the rqt_airstack_control_panel directory - -``` -./generate_rqt_py_package.sh [package name] [class name] [plugin title] -``` - -[package name] will be the name of the package and a directory with this name will be created above `rqt_airstack_control_panel/`. [class name] is the name of the class in `src/[package name]/template.py`. [plugin title] is what the plugin will be called in the "Miscellaneous Tools" menu. - -For example, - -``` -cd rqt_airstack_control_panel/ -./generate_rqt_py_package.sh new_rqt_package ClassName "Plugin Title" -``` - diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/package.xml b/gcs/ros_ws/src/rqt_airstack_control_panel/package.xml deleted file mode 100644 index 2d18cf404..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/package.xml +++ /dev/null @@ -1,29 +0,0 @@ - - rqt_airstack_control_panel - 1.0.2 - rqt_airstack_control_panel is a Python GUI template. - John Keller - - BSD - - - - - - John Keller - - ament_index_python - python_qt_binding - qt_gui - qt_gui_py_common - rclpy - rqt_gui - sensor_msgs - rqt_gui_py - - - - - ament_python - - diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/plugin.xml b/gcs/ros_ws/src/rqt_airstack_control_panel/plugin.xml deleted file mode 100644 index 00e02b055..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/plugin.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - A Python GUI plugin providing an interactive Python console. - - - - - folder - Plugins related to miscellaneous tools. - - - applications-python - A Python RQT GUI template. - - - diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/resource/py_console_widget.ui b/gcs/ros_ws/src/rqt_airstack_control_panel/resource/py_console_widget.ui deleted file mode 100644 index 12810f1c5..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/resource/py_console_widget.ui +++ /dev/null @@ -1,53 +0,0 @@ - - - PyConsole - - - - 0 - 0 - 276 - 212 - - - - PyConsole - - - - 0 - - - 0 - - - 0 - - - 3 - - - 0 - - - - - 0 - - - - - - - - - - - PyConsoleTextEdit - QTextEdit -
rqt_py_console.py_console_text_edit
-
-
- - -
diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/resource/rqt_airstack_control_panel b/gcs/ros_ws/src/rqt_airstack_control_panel/resource/rqt_airstack_control_panel deleted file mode 100644 index e69de29bb..000000000 diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/setup.cfg b/gcs/ros_ws/src/rqt_airstack_control_panel/setup.cfg deleted file mode 100644 index 7f42fcca6..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/rqt_airstack_control_panel -[install] -install_scripts=$base/lib/rqt_airstack_control_panel diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/setup.py b/gcs/ros_ws/src/rqt_airstack_control_panel/setup.py deleted file mode 100644 index fe6b7c2f3..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -from setuptools import setup - -package_name = 'rqt_airstack_control_panel' - -setup( - name=package_name, - version='1.0.2', - packages=[package_name], - package_dir={'': 'src'}, - data_files=[ - ('share/ament_index/resource_index/packages', - ['resource/' + package_name]), - ('share/' + package_name + '/resource', - ['resource/py_console_widget.ui']), - ('share/' + package_name, ['package.xml']), - ('share/' + package_name, ['plugin.xml']), - ], - install_requires=['setuptools'], - zip_safe=True, - author='', - maintainer='', - maintainer_email='', - keywords=['ROS'], - classifiers=[ - '', - '', - '', - '', - ], - description=( - 'rqt_airstack_control_panel' - ), - license='BSD', - entry_points={ - 'console_scripts': [ - 'rqt_airstack_control_panel = ' + package_name + '.main:main', - ], - }, -) diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/__init__.py b/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/drag_and_drop.py b/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/drag_and_drop.py deleted file mode 100644 index 4e3a1e5fd..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/drag_and_drop.py +++ /dev/null @@ -1,166 +0,0 @@ -# adapted from https://www.pythonguis.com/faq/pyqt-drag-drop-widgets/ -from PyQt5 import QtCore -from PyQt5.QtCore import QMimeData, Qt, pyqtSignal -from PyQt5.QtGui import QDrag, QPixmap -from PyQt5.QtWidgets import ( - QApplication, - QHBoxLayout, - QLabel, - QMainWindow, - QVBoxLayout, - QGridLayout, - QWidget, - QComboBox, - QLineEdit -) - -class DragTargetIndicator(QLabel): - def __init__(self, parent=None): - super().__init__(parent) - #self.setContentsMargins(25, 5, 25, 5) - #self.setStyleSheet("QLabel { background-color: #ccc; border: 1px solid black; }") - - def set_size(self, size): - self.setFixedSize(size) - - -class DragItem(QWidget): - def __init__(self, w): - super().__init__() - self.widget = w - self.widget.destroyed.connect(self.child_destroyed) - self.setObjectName('main') - self.layout = QVBoxLayout() - self.setLayout(self.layout) - self.setAttribute(QtCore.Qt.WA_StyledBackground, True) - #self.setStyleSheet('QWidget#main {background-color: lightcyan; border: 1px solid black;}') - - self.layout.addWidget(w) - - def set_data(self, data): - self.data = data - - def child_destroyed(self): - self.deleteLater() - - def mouseMoveEvent(self, e): - if e.buttons() == Qt.LeftButton: - drag = QDrag(self) - mime = QMimeData() - drag.setMimeData(mime) - - pixmap = QPixmap(self.size()) - self.render(pixmap) - drag.setPixmap(pixmap) - - drag.exec_(Qt.MoveAction) - self.show() # Show this widget again, if it's dropped outside. - - -class DragWidget(QWidget): - """ - Generic list sorting handler. - """ - - orderChanged = pyqtSignal(list) - - def __init__(self, *args, orientation=Qt.Orientation.Horizontal, **kwargs): - super().__init__() - self.setAcceptDrops(True) - - # Store the orientation for drag checks later. - self.orientation = orientation - - if self.orientation == Qt.Orientation.Vertical: - self.blayout = QVBoxLayout() - else: - self.blayout = QHBoxLayout() - - # Add the drag target indicator. This is invisible by default, - # we show it and move it around while the drag is active. - self._drag_target_indicator = DragTargetIndicator() - self.blayout.addWidget(self._drag_target_indicator) - self._drag_target_indicator.hide() - - self.setLayout(self.blayout) - - def dragEnterEvent(self, e): - e.accept() - - def dragLeaveEvent(self, e): - self._drag_target_indicator.hide() - e.accept() - - def getWidgets(self): - widgets = [] - for i in range(self.blayout.count()): - try: - widgets.append(self.blayout.itemAt(i).widget().widget) - except: - pass - return widgets - - def dragMoveEvent(self, e): - # Find the correct location of the drop target, so we can move it there. - index = self._find_drop_location(e) - if index is not None: - # Inserting moves the item if its alreaady in the layout. - self.blayout.insertWidget(index, self._drag_target_indicator) - # Hide the item being dragged. - e.source().hide() - # Show the target. - self._drag_target_indicator.set_size(e.source().size()) - self._drag_target_indicator.show() - e.accept() - - def dropEvent(self, e): - widget = e.source() - # Use drop target location for destination, then remove it. - self._drag_target_indicator.hide() - index = self.blayout.indexOf(self._drag_target_indicator) - if index is not None: - self.blayout.insertWidget(index, widget) - self.orderChanged.emit(self.get_item_data()) - widget.show() - self.blayout.activate() - e.accept() - - def _find_drop_location(self, e): - pos = e.pos() - spacing = self.blayout.spacing() / 2 - - for n in range(self.blayout.count()): - # Get the widget at each index in turn. - w = self.blayout.itemAt(n).widget() - - if self.orientation == Qt.Orientation.Vertical: - # Drag drop vertically. - drop_here = ( - pos.y() >= w.y() - spacing - and pos.y() <= w.y() + w.size().height() + spacing - ) - else: - # Drag drop horizontally. - drop_here = ( - pos.x() >= w.x() - spacing - and pos.x() <= w.x() + w.size().width() + spacing - ) - - if drop_here: - # Drop over this target. - break - - return n - - def add_item(self, item): - self.blayout.addWidget(item) - - def get_item_data(self): - data = [] - for n in range(self.blayout.count()): - # Get the widget at each index in turn. - w = self.blayout.itemAt(n).widget() - if hasattr(w, "data"): - # The target indicator has no data. - data.append(w.data) - return data diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/main.py b/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/main.py deleted file mode 100755 index 9a5c9376e..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/main.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys - -from rqt_gui.main import Main - - -def main(): - main = Main() - sys.exit(main.main(sys.argv, standalone='rqt_py_console.py_console.PyConsole')) - - -if __name__ == '__main__': - main() diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/py_console_text_edit.py b/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/py_console_text_edit.py deleted file mode 100644 index dc9ce1a0a..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/py_console_text_edit.py +++ /dev/null @@ -1,69 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import sys -from code import InteractiveInterpreter - -from python_qt_binding import QT_BINDING, QT_BINDING_VERSION -from python_qt_binding.QtCore import Qt, Signal - -from qt_gui_py_common.console_text_edit import ConsoleTextEdit - - -class PyConsoleTextEdit(ConsoleTextEdit): - _color_stdin = Qt.darkGreen - _multi_line_char = ':' - _multi_line_indent = ' ' - _prompt = ('>>> ', '... ') # prompt for single and multi line - exit = Signal() - - def __init__(self, parent=None): - super(PyConsoleTextEdit, self).__init__(parent) - - self._interpreter_locals = {} - self._interpreter = InteractiveInterpreter(self._interpreter_locals) - - self._comment_writer.write('Python %s on %s\n' % - (sys.version.replace('\n', ''), sys.platform)) - self._comment_writer.write( - 'Qt bindings: %s version %s\n' % (QT_BINDING, QT_BINDING_VERSION)) - - self._add_prompt() - - def update_interpreter_locals(self, newLocals): - self._interpreter_locals.update(newLocals) - - def _exec_code(self, code): - try: - self._interpreter.runsource(code) - except SystemExit: # catch sys.exit() calls, so they don't close the whole gui - self.exit.emit() diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/py_console_widget.py b/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/py_console_widget.py deleted file mode 100644 index e69bde34c..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/py_console_widget.py +++ /dev/null @@ -1,59 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import os -from ament_index_python.resources import get_resource - -from python_qt_binding import loadUi -from python_qt_binding.QtWidgets import QWidget -from rqt_py_console.py_console_text_edit import PyConsoleTextEdit - - -class PyConsoleWidget(QWidget): - - def __init__(self, context=None): - super(PyConsoleWidget, self).__init__() - - _, package_path = get_resource('packages', 'rqt_py_console') - ui_file = os.path.join( - package_path, 'share', 'rqt_py_console', 'resource', 'py_console_widget.ui') - - loadUi(ui_file, self, {'PyConsoleTextEdit': PyConsoleTextEdit}) - self.setObjectName('PyConsoleWidget') - - my_locals = { - 'context': context - } - self.py_console.update_interpreter_locals(my_locals) - self.py_console.print_message( - 'The variable "context" is set to the PluginContext of this plugin.') - self.py_console.exit.connect(context.close_plugin) diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/spyder_console_widget.py b/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/spyder_console_widget.py deleted file mode 100644 index 374ef7a5d..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/spyder_console_widget.py +++ /dev/null @@ -1,60 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from python_qt_binding.QtGui import QFont - -from spyder.widgets.internalshell import InternalShell -from spyder.utils.module_completion import moduleCompletion - -class SpyderConsoleWidget(InternalShell): - - def __init__(self, context=None): - my_locals = { - 'context': context - } - super(SpyderConsoleWidget, self).__init__(namespace=my_locals) - self.setObjectName('SpyderConsoleWidget') - self.set_pythonshell_font(QFont('Mono')) - self.interpreter.restore_stds() - - def get_module_completion(self, objtxt): - """Return module completion list associated to object name""" - return moduleCompletion(objtxt) - - def run_command(self, *args): - self.interpreter.redirect_stds() - super(SpyderConsoleWidget, self).run_command(*args) - self.flush() - self.interpreter.restore_stds() - - def shutdown(self): - self.exit_interpreter() diff --git a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/template.py b/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/template.py deleted file mode 100644 index 01ee10b38..000000000 --- a/gcs/ros_ws/src/rqt_airstack_control_panel/src/rqt_airstack_control_panel/template.py +++ /dev/null @@ -1,554 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from python_qt_binding.QtWidgets import QVBoxLayout, QWidget -from rqt_gui_py.plugin import Plugin -from qt_gui_py_common.simple_settings_dialog import SimpleSettingsDialog -from rqt_py_console.py_console_widget import PyConsoleWidget - -import python_qt_binding.QtWidgets as qt -import python_qt_binding.QtWidgets as QtWidgets -import python_qt_binding.QtGui as QtGui -import python_qt_binding.QtCore as QtCore - -import subprocess -import threading -import time -import copy - -from std_msgs.msg import Bool -from sensor_msgs.msg import BatteryState - -from .drag_and_drop import DragWidget, DragItem - -try: - from rqt_py_console.spyder_console_widget import SpyderConsoleWidget - _has_spyderlib = True -except ImportError: - _has_spyderlib = False - - -logger = None - -def xor_encrypt_decrypt(data, key=983476): - return ''.join(chr(ord(c) ^ key) for c in data) - -class InfoConfigDialog(qt.QDialog): - def __init__(self, settings): - super().__init__() - - self.setWindowTitle('Configuration') - layout = qt.QVBoxLayout() - - self.name_label = qt.QLabel('Name:') - layout.addWidget(self.name_label) - self.name_entry = qt.QLineEdit() - self.name_entry.setText(settings['name']) - layout.addWidget(self.name_entry) - - self.username_label = qt.QLabel('Username:') - layout.addWidget(self.username_label) - self.username_entry = qt.QLineEdit() - self.username_entry.setText(settings['username']) - layout.addWidget(self.username_entry) - - self.password_label = qt.QLabel('Password:') - layout.addWidget(self.password_label) - self.password_entry = qt.QLineEdit() - self.password_entry.setEchoMode(qt.QLineEdit.Password) - self.password_entry.setText(settings['password']) - layout.addWidget(self.password_entry) - - self.hostname_label = qt.QLabel('Hostname:') - layout.addWidget(self.hostname_label) - self.hostname_entry = qt.QLineEdit() - self.hostname_entry.setText(settings['hostname']) - layout.addWidget(self.hostname_entry) - - self.namespace_label = qt.QLabel('Namespace:') - layout.addWidget(self.namespace_label) - self.namespace_entry = qt.QLineEdit() - self.namespace_entry.setText(settings['namespace']) - layout.addWidget(self.namespace_entry) - - self.path_label = qt.QLabel('Docker Compose Path:') - layout.addWidget(self.path_label) - self.path_entry = qt.QLineEdit() - self.path_entry.setText(settings['path']) - layout.addWidget(self.path_entry) - - self.services_label = qt.QLabel('Services to Hide (comma separated):') - layout.addWidget(self.services_label) - self.services_entry = qt.QLineEdit() - self.services_entry.setText(', '.join(settings['excluded_services'])) - layout.addWidget(self.services_entry) - - self.enable_display_checkbox = qt.QCheckBox('Enable Display') - self.enable_display_checkbox.setChecked(settings['enable_display']) - layout.addWidget(self.enable_display_checkbox) - - self.submit_button = qt.QPushButton('Submit') - self.submit_button.clicked.connect(self.submit) - layout.addWidget(self.submit_button) - - self.setLayout(layout) - - self.result = None - - def submit(self): - name = self.name_entry.text() - hostname = self.hostname_entry.text() - username = self.username_entry.text() - password = self.password_entry.text() - namespace = self.namespace_entry.text() - path = self.path_entry.text() - services = list(map(lambda x:x.strip(), self.services_entry.text().split(','))) - try: - services.remove('') - except: - pass - enable_display = self.enable_display_checkbox.isChecked() - self.result = {'name': name, 'username': username, 'password': password, 'hostname': hostname, 'namespace': namespace, 'path': path, 'excluded_services': services, 'enable_display': enable_display} - self.accept() - -class CommandThread(QtCore.QThread): - output_signal = QtCore.pyqtSignal(str) - - def __init__(self, command, callback=None, wait_until_finished=False, timeout=None): - super().__init__() - self.command = command - if callback != None: - self.output_signal.connect(callback) - self.wait_until_finished = wait_until_finished - self.timeout = timeout - self.running = True - self.start() - - def run(self): - process = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True) - - if not self.wait_until_finished: - while self.running: - line = process.stdout.readline() - if not line: - break - self.output_signal.emit(line.strip()) - if self.wait_until_finished: - try: - process.wait(timeout=self.timeout) - self.output_signal.emit(process.stdout.read()) - except subprocess.TimeoutExpired: - process.kill() - - process.terminate() - - def stop(self): - self.running = False - self.terminate() - -class InfoWidget(qt.QWidget): - def __init__(self, node, settings={'name': 'Name', 'username': 'airlab', 'password': 'passme24', 'hostname': 'localhost', 'namespace': 'none', 'path': '~/airstack', 'excluded_services': [], 'enable_display': False}): - super(qt.QWidget, self).__init__() - self.layout = qt.QVBoxLayout(self) - #self.setObjectName('info_widget') - #self.setStyleSheet('#info_widget {border: 2px solid black;}') - - self.node = node - self.recording_sub = None - self.recording_pub = None - self.battery_sub = None - - self.setAttribute(QtCore.Qt.WA_StyledBackground, True) - self.setObjectName('info_widget') - self.stylesheet = 'border: 2px solid lightgrey; border-radius: 10px;' - self.setStyleSheet('QWidget#info_widget { ' + self.stylesheet + ' }') - - self.settings = settings - - self.ping_thread = None - self.connected = False - self.services = {} - - # info panel - self.info_widget = qt.QWidget() - self.info_layout = qt.QHBoxLayout(self.info_widget) - self.layout.addWidget(self.info_widget) - - self.name_label = qt.QLabel(self.settings['name']) - self.name_label.setStyleSheet('font-weight: bold;') - self.info_layout.addWidget(self.name_label) - - self.hostname_label = qt.QLabel('Hostname: ' + self.settings['hostname']) - self.info_layout.addWidget(self.hostname_label) - - self.ping_label = qt.QLabel('Ping:') - self.info_layout.addWidget(self.ping_label) - - self.recording_label = qt.QLabel('Recording:') - self.info_layout.addWidget(self.recording_label) - - self.battery_label = qt.QLabel('Battery: --') - self.info_layout.addWidget(self.battery_label) - - line = qt.QFrame() - line.setFrameShape(qt.QFrame.HLine) - line.setFrameShadow(qt.QFrame.Plain) - line.setStyleSheet('QFrame {background-color: #cccccc; max-height: 1px; border: none; }') - self.layout.addWidget(line) - - # command panel - self.command_widget = qt.QWidget() - self.command_layout = qt.QHBoxLayout(self.command_widget) - self.layout.addWidget(self.command_widget) - - self.refresh_button = qt.QPushButton(text='Refresh') - self.refresh_button.clicked.connect(self.refresh_docker) - self.command_layout.addWidget(self.refresh_button) - - self.restart_button = qt.QPushButton(text='Restart') - self.restart_button.clicked.connect(lambda :self.docker_command('restart')) - self.command_layout.addWidget(self.restart_button) - - self.up_button = qt.QPushButton(text='Up') - self.up_button.clicked.connect(lambda :self.docker_command('up')) - self.command_layout.addWidget(self.up_button) - - self.down_button = qt.QPushButton(text='Down') - self.down_button.clicked.connect(lambda :self.docker_command('down')) - self.command_layout.addWidget(self.down_button) - - self.record_button = qt.QPushButton(text='Record') - self.record_button.setCheckable(True) - self.record_button.clicked.connect(self.record) - self.command_layout.addWidget(self.record_button) - - self.ssh_button = qt.QPushButton(text='ssh') - self.ssh_button.clicked.connect(self.ssh) - self.command_layout.addWidget(self.ssh_button) - - self.config_button = qt.QPushButton(text='Config') - self.config_button.clicked.connect(self.config_clicked) - self.command_layout.addStretch(1) - self.command_layout.addWidget(self.config_button) - - self.delete_button = qt.QPushButton(text='X') - self.delete_button.clicked.connect(self.delete_clicked) - self.command_layout.addWidget(self.delete_button) - - line = qt.QFrame() - line.setFrameShape(qt.QFrame.HLine) - line.setFrameShadow(qt.QFrame.Plain) - line.setStyleSheet('QFrame {background-color: #cccccc; max-height: 1px; border: none; }') - self.layout.addWidget(line) - - # docker panel - self.docker_widget = qt.QWidget() - self.docker_layout = qt.QHBoxLayout(self.docker_widget) - self.layout.addWidget(self.docker_widget) - - self.update_info() - #self.refresh_docker() - - def config_clicked(self): - dialog = InfoConfigDialog(self.settings) - if dialog.exec(): - self.settings = dialog.result - self.update_info() - - def delete_clicked(self): - self.delete_function() - - def record(self): - if self.recording_pub != None: - msg = Bool() - msg.data = self.record_button.isChecked() - self.recording_pub.publish(msg) - - - def update_info(self): - self.name_label.setText(self.settings['name']) - self.hostname_label.setText('Hostname: ' + self.settings['hostname']) - - if self.ping_thread != None: - self.ping_thread.stop() - del self.ping_thread - self.ping_thread = None - #self.ping_thread = CommandThread('ping ' + self.settings['hostname']) - self.ping_thread = CommandThread('HOST="' + self.settings['hostname'] + '"; while true; do OUTPUT=$(ping -c 1 -w 3 $HOST 2>&1); if echo "$OUTPUT" | grep -q "time="; then PING_TIME=$(echo "$OUTPUT" | grep -oP "time=\K[\d.]+"); echo "$PING_TIME ms"; else echo "failed"; fi; sleep 1; done', self.handle_ping) - - if self.recording_sub is not None: - self.node.destroy_subscription(self.recording_sub) - self.recording_sub = self.node.create_subscription( - Bool, - self.settings['namespace'] + '/bag_record/bag_recording_status', - self.recording_callback, - 1, - ) - self.recording_pub = self.node.create_publisher( - Bool, - self.settings['namespace'] + '/bag_record/set_recording_status', - 1, - ) - - if self.battery_sub is not None: - self.node.destroy_subscription(self.battery_sub) - self.battery_sub = self.node.create_subscription( - BatteryState, - self.settings['namespace'] + '/interface/mavros/battery', - self.battery_callback, - 1, - ) - - def battery_callback(self, msg): - pct = msg.percentage - if pct < 0 or pct > 1: - pct = -1.0 - if pct >= 0: - self.battery_label.setText( - 'Battery: {:.1f} V ({:.0f}%)'.format(msg.voltage, pct * 100.0) - ) - else: - self.battery_label.setText('Battery: {:.1f} V'.format(msg.voltage)) - - def recording_callback(self, msg): - if msg.data: - self.recording_label.setText('Recording: YES') - else: - self.recording_label.setText('Recording: NO') - - def ssh_t(self, command): - ssh = 'ssh -t -o StrictHostKeyChecking=no' - if self.settings['enable_display']: - ssh += ' -X' - - display = '' - if self.settings['enable_display']: - display = 'export DISPLAY=:1; ' - - return 'sshpass -p "' + self.settings['password'] + '" ' + ssh + ' ' + \ - self.settings['username'] + '@' + self.settings['hostname'] + ' "' + display + command + '"' - - def refresh_docker(self): - command = self.ssh_t('cd ' + self.settings['path'] + '; docker compose --profile \'*\' config --services && echo SPLIT && docker compose ps --format {{.Service}}') - - #result = subprocess.run(command, capture_output=True, text=True, check=True, shell=True) - - self.refresh_thread = CommandThread(command, self.handle_refresh_docker, True, 5) - - def handle_refresh_docker(self, stdout): - if 'SPLIT' not in stdout: - return - - services = sorted(stdout.split('SPLIT')[0].split('\n')) - running = stdout.split('SPLIT')[1].split('\n') - - for i in reversed(range(self.docker_layout.count())): - self.docker_layout.itemAt(i).widget().setParent(None) - - for service in services: - if service != '' and service not in self.settings['excluded_services']: - button = qt.QPushButton(text=service) - if service in running: - button.setStyleSheet('background-color: lightgreen;') - button.setCheckable(True) - if service in self.services: - button.setChecked(self.services[service]) - else: - self.services[service] = False - def get_click_function(s, b): - def click_function(): - self.services[s] = b.isChecked() - return click_function - button.clicked.connect(get_click_function(service, button)) - - def get_open_menu_function(s, b): - def menu_triggered(action): - if action.text() == 'bash': - self.docker_exec(s, 'bash') - elif action.text() == 'tmux': - self.docker_exec(s, 'tmux a') - - def open_menu_function(): - menu = qt.QMenu(self) - action1 = menu.addAction('bash') - action2 = menu.addAction('tmux') - menu.triggered.connect(menu_triggered) - menu.popup(b.mapToGlobal(b.rect().topLeft())) - return open_menu_function - - button.setContextMenuPolicy(3) - button.customContextMenuRequested.connect(get_open_menu_function(service, button)) - - self.docker_layout.addWidget(button) - - def docker_command(self, command): - services_str = ' '.join(self.get_selected_services()) - - logger.info('docker command: ' + command + ' ' + services_str) - - if len(services_str) > 0: - command = 'dbus-launch gnome-terminal --wait -- bash -c \'' + \ - self.ssh_t('cd ' + self.settings['path'] + '; docker compose ' + command + ' ' + services_str + \ - (' -d' if command == 'up' else '')) + '\'' - logger.info('docker_command: ' + command) - self.docker_command_thread = CommandThread(command, lambda :self.refresh_docker(), True) - - def docker_exec(self, service, command): - proc = f''' - set -x - mapfile -t names <<< $(sshpass -p "{self.settings['password']}" \ - ssh -t -o StrictHostKeyChecking=no {self.settings['username']}@{self.settings['hostname']} \ - "cd {self.settings['path']}; docker ps -f name={service} --format '{{{{.Names}}}}'"); - for item in ${{names[@]}}; do - item=$(echo $item| tr -d '\\r') - command="dbus-launch gnome-terminal -- bash -c 'sshpass -p \\"{self.settings['password']}\\" \ - ssh -t -o StrictHostKeyChecking=no \ - {self.settings['username']}@{self.settings['hostname']} \ - \\"cd {self.settings['path']}; docker exec -it $item {command};\\"';" - output=$(eval "$command" 2>&1) - - while grep -q "Error creating terminal" <<< "$output"; do - output=$(eval "$command" 2>&1) - done - done - ''' - - logger.info(proc) - p = subprocess.Popen(proc, shell=True, executable='/usr/bin/bash') - out, err = p.communicate(timeout=2) - logger.info('out ' + str(out)) - logger.info('err ' + str(err)) - - def ssh(self): - proc = f'''dbus-launch gnome-terminal -- bash -c 'sshpass -p "{self.settings['password']}" \ - ssh -o StrictHostKeyChecking=no \ - {self.settings['username']}@{self.settings['hostname']}' ''' - logger.info(proc) - subprocess.Popen(proc, shell=True, executable='/usr/bin/bash') - - def handle_ping(self, text): - if text == 'failed': - self.setStyleSheet('QWidget#info_widget { ' + self.stylesheet + 'background-color: rgb(255, 144, 144) }') - self.connected = False - else: - self.setStyleSheet('QWidget#info_widget { ' + self.stylesheet + 'background-color: rgb(239, 239, 239) }') - if not self.connected: - self.refresh_docker() - self.connected = True - - self.ping_label.setText('Ping: ' + text) - - def get_dct(self): - return self.settings - - def get_selected_services(self): - selected_services = [] - for service, selected in self.services.items(): - if selected: - selected_services.append(service) - return selected_services - - -class AirstackControlPanel(Plugin): - def __init__(self, context): - super(AirstackControlPanel, self).__init__(context) - self.setObjectName('AirstackControlPanel') - - self.context = context - # to access ros2 node use self.context.node - - global logger - logger = self.context.node.get_logger() - - self.widget = qt.QWidget() - self.layout = qt.QVBoxLayout(self.widget) - - self.info_widget = qt.QWidget() - self.info_layout = qt.QVBoxLayout(self.info_widget) - - self.drag_widget = DragWidget(orientation=QtCore.Qt.Orientation.Vertical) - self.info_layout.addWidget(self.drag_widget) - - self.info_scroll_area = qt.QScrollArea() - self.info_scroll_area.setWidget(self.info_widget) - self.info_scroll_area.setWidgetResizable(True) - self.info_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.info_scroll_area.setMinimumHeight(800) - self.layout.addWidget(self.info_scroll_area) - - self.add_button = qt.QPushButton('+') - self.add_button.clicked.connect(lambda x:self.add_info_widget()) - self.layout.addWidget(self.add_button) - self.layout.addStretch(1) - - self.context.add_widget(self.widget) - - def add_info_widget(self, settings=None): - if settings == None: - info_widget = DragItem(InfoWidget(self.context.node)) - else: - info_widget = DragItem(InfoWidget(self.context.node, settings)) - info_widget.widget.delete_function = info_widget.deleteLater - self.drag_widget.add_item(info_widget) - - def save_settings(self, plugin_settings, instance_settings): - info_dcts = [copy.deepcopy(w.get_dct()) for w in self.drag_widget.getWidgets()] - for settings in info_dcts: - settings['password'] = xor_encrypt_decrypt(settings['password']) - instance_settings.set_value('info_dcts', info_dcts) - - def restore_settings(self, plugin_settings, instance_settings): - info_dcts = instance_settings.value('info_dcts', {}) - for settings in info_dcts: - settings['password'] = xor_encrypt_decrypt(settings['password']) - self.add_info_widget(settings) - - def trigger_configuration(self): - options = [ - {'title': 'Option 1', - 'description': 'Description of option 1.', - 'enabled': True}, - {'title': 'Option 2', - 'description': 'Description of option 2.'}, - ] - dialog = SimpleSettingsDialog(title='Options') - dialog.add_exclusive_option_group(title='List of options:', options=options, selected_index=0) - selected_index = dialog.get_settings()[0] - if selected_index != None: - selected_index = selected_index['selected_index'] - print('selected_index ', selected_index) - - def shutdown_console_widget(self): - pass - - def shutdown_plugin(self): - self.shutdown_console_widget() diff --git a/gcs/ros_ws/src/rqt_gcs/CHANGELOG.rst b/gcs/ros_ws/src/rqt_gcs/CHANGELOG.rst deleted file mode 100644 index 0a789b786..000000000 --- a/gcs/ros_ws/src/rqt_gcs/CHANGELOG.rst +++ /dev/null @@ -1,149 +0,0 @@ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Changelog for package rqt_py_console -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1.0.2 (2021-08-31) ------------------- -* Fix modern setuptools warning about dashes instead of underscores (`#11 `_) -* Contributors: Chris Lalancette - -1.0.1 (2021-04-27) ------------------- -* Changed the build type to ament_python and fixed package to run with ros2 run (`#8 `_) -* Contributors: Alejandro Hernández Cordero - -1.0.0 (2018-12-11) ------------------- -* spyderlib -> spyder (`#5 `_) -* ros2 port (`#3 `_) -* autopep8 (`#2 `_) -* Contributors: Mike Lautman - -0.4.8 (2017-04-28) ------------------- - -0.4.7 (2017-03-02) ------------------- - -0.4.6 (2017-02-27) ------------------- - -0.4.5 (2017-02-03) ------------------- - -0.4.4 (2017-01-24) ------------------- -* use Python 3 compatible syntax (`#421 `_) - -0.4.3 (2016-11-02) ------------------- - -0.4.2 (2016-09-19) ------------------- - -0.4.1 (2016-05-16) ------------------- - -0.4.0 (2016-04-27) ------------------- -* Support Qt 5 (in Kinetic and higher) as well as Qt 4 (in Jade and earlier) (`#359 `_) - -0.3.13 (2016-03-08) -------------------- - -0.3.12 (2015-07-24) -------------------- - -0.3.11 (2015-04-30) -------------------- - -0.3.10 (2014-10-01) -------------------- -* update plugin scripts to use full name to avoid future naming collisions - -0.3.9 (2014-08-18) ------------------- - -0.3.8 (2014-07-15) ------------------- - -0.3.7 (2014-07-11) ------------------- -* export architecture_independent flag in package.xml (`#254 `_) - -0.3.6 (2014-06-02) ------------------- - -0.3.5 (2014-05-07) ------------------- - -0.3.4 (2014-01-28) ------------------- - -0.3.3 (2014-01-08) ------------------- -* add groups for rqt plugins, renamed some plugins (`#167 `_) - -0.3.2 (2013-10-14) ------------------- - -0.3.1 (2013-10-09) ------------------- - -0.3.0 (2013-08-28) ------------------- - -0.2.17 (2013-07-04) -------------------- - -0.2.16 (2013-04-09 13:33) -------------------------- - -0.2.15 (2013-04-09 00:02) -------------------------- - -0.2.14 (2013-03-14) -------------------- - -0.2.13 (2013-03-11 22:14) -------------------------- - -0.2.12 (2013-03-11 13:56) -------------------------- - -0.2.11 (2013-03-08) -------------------- - -0.2.10 (2013-01-22) -------------------- - -0.2.9 (2013-01-17) ------------------- - -0.2.8 (2013-01-11) ------------------- - -0.2.7 (2012-12-24) ------------------- - -0.2.6 (2012-12-23) ------------------- - -0.2.5 (2012-12-21 19:11) ------------------------- - -0.2.4 (2012-12-21 01:13) ------------------------- - -0.2.3 (2012-12-21 00:24) ------------------------- - -0.2.2 (2012-12-20 18:29) ------------------------- - -0.2.1 (2012-12-20 17:47) ------------------------- - -0.2.0 (2012-12-20 17:39) ------------------------- -* first release of this package into groovy diff --git a/gcs/ros_ws/src/rqt_gcs/README.md b/gcs/ros_ws/src/rqt_gcs/README.md deleted file mode 100644 index 3a8773e9e..000000000 --- a/gcs/ros_ws/src/rqt_gcs/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# RQT Python GroundControlStation - -If you `colcon build` this package in a workspace and then run "rqt --force-discover" after sourcing the workspace, the plugin should show up as "Ground Control Station" in "Miscellaneous Tools" in the "Plugins" menu. - -You can use the `generate_rqt_py_package.sh` script to generate a new package by doing the following from the rqt_gcs directory - -``` -./generate_rqt_py_package.sh [package name] [class name] [plugin title] -``` - -[package name] will be the name of the package and a directory with this name will be created above `rqt_gcs/`. [class name] is the name of the class in `src/[package name]/template.py`. [plugin title] is what the plugin will be called in the "Miscellaneous Tools" menu. - -For example, - -``` -cd rqt_gcs/ -./generate_rqt_py_package.sh new_rqt_package ClassName "Plugin Title" -``` diff --git a/gcs/ros_ws/src/rqt_gcs/config/fixed_trajectories.yaml b/gcs/ros_ws/src/rqt_gcs/config/fixed_trajectories.yaml deleted file mode 100644 index 2c8622ede..000000000 --- a/gcs/ros_ws/src/rqt_gcs/config/fixed_trajectories.yaml +++ /dev/null @@ -1,39 +0,0 @@ -trajectories: - - Figure8: - attributes: - - frame_id - - velocity - - max_acceleration - - length - - width - - height - - Racetrack: - attributes: - - frame_id - - velocity - - turn_velocity - - max_acceleration - - length - - width - - height - - Circle: - attributes: - - frame_id - - velocity - - radius - - Line: - attributes: - - frame_id - - velocity - - max_acceleration - - length - - width - - height - - Point: - attributes: - - frame_id - - velocity - - max_acceleration - - x - - y - - height diff --git a/gcs/ros_ws/src/rqt_gcs/config/gcs.perspective b/gcs/ros_ws/src/rqt_gcs/config/gcs.perspective deleted file mode 100644 index 856123b61..000000000 --- a/gcs/ros_ws/src/rqt_gcs/config/gcs.perspective +++ /dev/null @@ -1,128 +0,0 @@ -{ - "keys": {}, - "groups": { - "mainwindow": { - "keys": { - "geometry": { - "repr(QByteArray.hex)": "QtCore.QByteArray(b'01d9d0cb00030000000001ed00000086000009bb000004c5000001ed000000ab000009bb000004c500000000000000000a00000001ed000000ab000009bb000004c5')", - "type": "repr(QByteArray.hex)", - "pretty-print": " " - }, - "state": { - "repr(QByteArray.hex)": "QtCore.QByteArray(b'000000ff00000000fd0000000100000003000007cf000003f1fc0100000003fb0000006a007200710074005f00670072006f0075006e0064005f0063006f006e00740072006f006c005f00730074006100740069006f006e005f005f00470072006f0075006e00640043006f006e00740072006f006c00530074006100740069006f006e005f005f0031005f005f0100000000000003e90000000000000000fc0000000000000428000000d000fffffffa000000010200000002fb00000042007200710074005f006200650068006100760069006f0072005f0074007200650065005f005f005000790043006f006e0073006f006c0065005f005f0031005f005f0100000000ffffffff0000004a00fffffffb0000006a007200710074005f0061006900720073007400610063006b005f0063006f006e00740072006f006c005f00700061006e0065006c005f005f0041006900720073007400610063006b0043006f006e00740072006f006c00500061006e0065006c005f005f0031005f005f0100000000ffffffff0000036300fffffffb00000044007200710074005f006700630073005f005f00470072006f0075006e00640043006f006e00740072006f006c00530074006100740069006f006e005f005f0031005f005f010000042e000003a10000036200ffffff000007cf0000000000000004000000040000000800000008fc00000001000000030000000100000036004d0069006e0069006d0069007a006500640044006f0063006b00570069006400670065007400730054006f006f006c0062006100720000000000ffffffff0000000000000000')", - "type": "repr(QByteArray.hex)", - "pretty-print": " jrqt_ground_control_station__GroundControlStation__1__ ( J c . b " - } - }, - "groups": { - "toolbar_areas": { - "keys": { - "MinimizedDockWidgetsToolbar": { - "repr": "8", - "type": "repr" - } - }, - "groups": {} - } - } - }, - "pluginmanager": { - "keys": { - "running-plugins": { - "repr": "{'rqt_airstack_control_panel/AirstackControlPanel': [1], 'rqt_behavior_tree/PyConsole': [1], 'rqt_gcs/GroundControlStation': [1]}", - "type": "repr" - } - }, - "groups": { - "plugin__rqt_airstack_control_panel__AirstackControlPanel__1": { - "keys": {}, - "groups": { - "dock_widget__": { - "keys": { - "dock_widget_title": { - "repr": "'Control Panel'", - "type": "repr" - }, - "dockable": { - "repr": "True", - "type": "repr" - }, - "parent": { - "repr": "None", - "type": "repr" - } - }, - "groups": {} - }, - "plugin": { - "keys": { - "info_dcts": { - "repr": "[{'enable_display': True, 'excluded_services': [], 'hostname': '172.17.0.1', 'name': 'Localhost', 'namespace': 'none', 'password': '\\U000f01de\\U000f01db\\U000f01dc\\U000f01da', 'path': '~/airstack', 'username': 'john'}, {'enable_display': False, 'excluded_services': ['docs', 'gcs', 'gcs-real', 'isaac-sim', 'robot'], 'hostname': '10.4.1.11', 'name': 'NX 1', 'namespace': 'robot_1', 'password': '\\U000f01c4\\U000f01d5\\U000f01c7\\U000f01c7\\U000f01d9\\U000f01d1\\U000f0186\\U000f0180', 'path': '~/airstack', 'username': 'airlab'}, {'enable_display': False, 'excluded_services': ['docs', 'gcs', 'gcs-real', 'isaac-sim', 'robot'], 'hostname': '10.4.1.12', 'name': 'AGX 1', 'namespace': 'robot_1', 'password': '\\U000f01c4\\U000f01d5\\U000f01c7\\U000f01c7\\U000f01d9\\U000f01d1\\U000f0186\\U000f0180', 'path': '~/airstack', 'username': 'airlab'}, {'enable_display': False, 'excluded_services': ['docs', 'gcs', 'gcs-real', 'isaac-sim', 'robot'], 'hostname': '10.4.1.21', 'name': 'NX 2', 'namespace': 'robot_2', 'password': '\\U000f01c4\\U000f01d5\\U000f01c7\\U000f01c7\\U000f01d9\\U000f01d1\\U000f0186\\U000f0180', 'path': '~/airstack', 'username': 'airlab'}, {'enable_display': False, 'excluded_services': ['docs', 'gcs', 'gcs-real', 'isaac-sim', 'robot'], 'hostname': '10.4.1.22', 'name': 'AGX 2', 'namespace': 'robot_2', 'password': '\\U000f01c4\\U000f01d5\\U000f01c7\\U000f01c7\\U000f01d9\\U000f01d1\\U000f0186\\U000f0180', 'path': '~/airstack', 'username': 'airlab'}, {'enable_display': False, 'excluded_services': ['docs', 'gcs', 'gcs-real', 'isaac-sim', 'robot'], 'hostname': '10.4.1.31', 'name': 'NX 3', 'namespace': 'robot_3', 'password': '\\U000f01c4\\U000f01d5\\U000f01c7\\U000f01c7\\U000f01d9\\U000f01d1\\U000f0186\\U000f0180', 'path': '~/airstack', 'username': 'airlab'}, {'enable_display': False, 'excluded_services': ['docs', 'gcs', 'gcs-real', 'isaac-sim', 'robot'], 'hostname': '10.4.1.32', 'name': 'AGX 3', 'namespace': 'robot_3', 'password': '\\U000f01c4\\U000f01d5\\U000f01c7\\U000f01c7\\U000f01d9\\U000f01d1\\U000f0186\\U000f0180', 'path': '~/airstack', 'username': 'airlab'}]", - "type": "repr" - } - }, - "groups": {} - } - } - }, - "plugin__rqt_behavior_tree__PyConsole__1": { - "keys": {}, - "groups": { - "dock_widget__": { - "keys": { - "dock_widget_title": { - "repr": "'Behavior Tree'", - "type": "repr" - }, - "dockable": { - "repr": "True", - "type": "repr" - }, - "parent": { - "repr": "None", - "type": "repr" - } - }, - "groups": {} - } - } - }, - "plugin__rqt_gcs__GroundControlStation__1": { - "keys": {}, - "groups": { - "dock_widget__": { - "keys": { - "dock_widget_title": { - "repr": "''", - "type": "repr" - }, - "dockable": { - "repr": "True", - "type": "repr" - }, - "parent": { - "repr": "None", - "type": "repr" - } - }, - "groups": {} - }, - "plugin": { - "keys": { - "command_config_filename": { - "repr": "'/root/AirStack/gcs/ros_ws/install/rqt_gcs/share/rqt_gcs/config/gui_config.yaml'", - "type": "repr" - }, - "trajectory_config_filename": { - "repr": "'/root/AirStack/gcs/ros_ws/install/rqt_gcs/share/rqt_gcs/config/fixed_trajectories.yaml'", - "type": "repr" - } - }, - "groups": {} - } - } - } - } - } - } -} \ No newline at end of file diff --git a/gcs/ros_ws/src/rqt_gcs/config/gui_config.yaml b/gcs/ros_ws/src/rqt_gcs/config/gui_config.yaml deleted file mode 100644 index 8c388715d..000000000 --- a/gcs/ros_ws/src/rqt_gcs/config/gui_config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -groups: - - commands: - - Arm and Takeoff: - condition_name: Auto Takeoff Commanded - - Fixed Trajectory: - condition_name: Fixed Trajectory Commanded - - Global Plan: - condition_name: Global Plan Commanded - - Pause: - condition_name: Pause Commanded - - Rewind: - condition_name: Rewind Commanded - - Disarm: - condition_name: Disarm Commanded - - Land: - condition_name: Land Commanded - - Autonomously Explore: - condition_name: Autonomously Explore Commanded -robots: # these should match a robot's namespace - - robot_1 - - robot_2 - - robot_3 \ No newline at end of file diff --git a/gcs/ros_ws/src/rqt_gcs/package.xml b/gcs/ros_ws/src/rqt_gcs/package.xml deleted file mode 100644 index be695e970..000000000 --- a/gcs/ros_ws/src/rqt_gcs/package.xml +++ /dev/null @@ -1,28 +0,0 @@ - - rqt_gcs - 1.0.2 - rqt_gcs is a Python GUI template. - John Keller - - BSD - - - - - - John Keller - - ament_index_python - python_qt_binding - qt_gui - qt_gui_py_common - rclpy - rqt_gui - rqt_gui_py - - - - - ament_python - - \ No newline at end of file diff --git a/gcs/ros_ws/src/rqt_gcs/plugin.xml b/gcs/ros_ws/src/rqt_gcs/plugin.xml deleted file mode 100644 index d9f5a464c..000000000 --- a/gcs/ros_ws/src/rqt_gcs/plugin.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - A Python GUI plugin providing an interactive Python console. - - - - - folder - Plugins related to miscellaneous tools. - - - applications-python - A Python RQT GUI template. - - - \ No newline at end of file diff --git a/gcs/ros_ws/src/rqt_gcs/resource/py_console_widget.ui b/gcs/ros_ws/src/rqt_gcs/resource/py_console_widget.ui deleted file mode 100644 index 12810f1c5..000000000 --- a/gcs/ros_ws/src/rqt_gcs/resource/py_console_widget.ui +++ /dev/null @@ -1,53 +0,0 @@ - - - PyConsole - - - - 0 - 0 - 276 - 212 - - - - PyConsole - - - - 0 - - - 0 - - - 0 - - - 3 - - - 0 - - - - - 0 - - - - - - - - - - - PyConsoleTextEdit - QTextEdit -
rqt_py_console.py_console_text_edit
-
-
- - -
diff --git a/gcs/ros_ws/src/rqt_gcs/resource/rqt_gcs b/gcs/ros_ws/src/rqt_gcs/resource/rqt_gcs deleted file mode 100644 index e69de29bb..000000000 diff --git a/gcs/ros_ws/src/rqt_gcs/setup.cfg b/gcs/ros_ws/src/rqt_gcs/setup.cfg deleted file mode 100644 index 83453313d..000000000 --- a/gcs/ros_ws/src/rqt_gcs/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/rqt_gcs -[install] -install_scripts=$base/lib/rqt_gcs diff --git a/gcs/ros_ws/src/rqt_gcs/setup.py b/gcs/ros_ws/src/rqt_gcs/setup.py deleted file mode 100644 index 4c3ccf358..000000000 --- a/gcs/ros_ws/src/rqt_gcs/setup.py +++ /dev/null @@ -1,37 +0,0 @@ -from setuptools import setup - -package_name = "rqt_gcs" -import glob - -setup( - name=package_name, - version="1.0.2", - packages=[package_name], - package_dir={"": "src"}, - data_files=[ - ("share/ament_index/resource_index/packages", ["resource/" + package_name]), - ("share/" + package_name + "/resource", ["resource/py_console_widget.ui"]), - ("share/" + package_name, ["package.xml"]), - ("share/" + package_name, ["plugin.xml"]), - ("share/" + package_name + "/config/", glob.glob("config/*")), - ], - install_requires=["setuptools"], - zip_safe=True, - author="", - maintainer="", - maintainer_email="", - keywords=["ROS"], - classifiers=[ - "", - "", - "", - "", - ], - description=("rqt_gcs"), - license="BSD", - entry_points={ - "console_scripts": [ - "rqt_gcs = " + package_name + ".main:main", - ], - }, -) diff --git a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/__init__.py b/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/drag_and_drop.py b/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/drag_and_drop.py deleted file mode 100644 index 0041927fb..000000000 --- a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/drag_and_drop.py +++ /dev/null @@ -1,157 +0,0 @@ -# adapted from https://www.pythonguis.com/faq/pyqt-drag-drop-widgets/from PyQt5 import QtCore -from PyQt5 import QtCore -from PyQt5.QtCore import QMimeData, Qt, pyqtSignal -from PyQt5.QtGui import QDrag, QPixmap -from PyQt5.QtWidgets import ( - QApplication, - QHBoxLayout, - QLabel, - QMainWindow, - QVBoxLayout, - QGridLayout, - QWidget, - QComboBox, - QLineEdit -) - -class DragTargetIndicator(QLabel): - def __init__(self, parent=None): - super().__init__(parent) - #self.setContentsMargins(25, 5, 25, 5) - self.setStyleSheet("QLabel { background-color: #ccc; border: 1px solid black; }") - - def set_size(self, size): - self.setFixedSize(size) - - -class DragItem(QWidget): - def __init__(self, w): - super().__init__() - self.widget = w - self.widget.destroyed.connect(self.child_destroyed) - self.setObjectName('main') - self.layout = QVBoxLayout() - self.setLayout(self.layout) - self.setAttribute(QtCore.Qt.WA_StyledBackground, True) - self.setStyleSheet('QWidget#main {background-color: lightcyan; border: 1px solid black;}') - - self.layout.addWidget(w) - - def set_data(self, data): - self.data = data - - def child_destroyed(self): - self.deleteLater() - - def mouseMoveEvent(self, e): - if e.buttons() == Qt.LeftButton: - drag = QDrag(self) - mime = QMimeData() - drag.setMimeData(mime) - - pixmap = QPixmap(self.size()) - self.render(pixmap) - drag.setPixmap(pixmap) - - drag.exec_(Qt.MoveAction) - self.show() # Show this widget again, if it's dropped outside. - - -class DragWidget(QWidget): - """ - Generic list sorting handler. - """ - - orderChanged = pyqtSignal(list) - - def __init__(self, *args, orientation=Qt.Orientation.Horizontal, **kwargs): - super().__init__() - self.setAcceptDrops(True) - - # Store the orientation for drag checks later. - self.orientation = orientation - - if self.orientation == Qt.Orientation.Vertical: - self.blayout = QVBoxLayout() - else: - self.blayout = QHBoxLayout() - - # Add the drag target indicator. This is invisible by default, - # we show it and move it around while the drag is active. - self._drag_target_indicator = DragTargetIndicator() - self.blayout.addWidget(self._drag_target_indicator) - self._drag_target_indicator.hide() - - self.setLayout(self.blayout) - - def dragEnterEvent(self, e): - e.accept() - - def dragLeaveEvent(self, e): - self._drag_target_indicator.hide() - e.accept() - - def dragMoveEvent(self, e): - # Find the correct location of the drop target, so we can move it there. - index = self._find_drop_location(e) - if index is not None: - # Inserting moves the item if its alreaady in the layout. - self.blayout.insertWidget(index, self._drag_target_indicator) - # Hide the item being dragged. - e.source().hide() - # Show the target. - self._drag_target_indicator.set_size(e.source().size()) - self._drag_target_indicator.show() - e.accept() - - def dropEvent(self, e): - widget = e.source() - # Use drop target location for destination, then remove it. - self._drag_target_indicator.hide() - index = self.blayout.indexOf(self._drag_target_indicator) - if index is not None: - self.blayout.insertWidget(index, widget) - self.orderChanged.emit(self.get_item_data()) - widget.show() - self.blayout.activate() - e.accept() - - def _find_drop_location(self, e): - pos = e.pos() - spacing = self.blayout.spacing() / 2 - - for n in range(self.blayout.count()): - # Get the widget at each index in turn. - w = self.blayout.itemAt(n).widget() - - if self.orientation == Qt.Orientation.Vertical: - # Drag drop vertically. - drop_here = ( - pos.y() >= w.y() - spacing - and pos.y() <= w.y() + w.size().height() + spacing - ) - else: - # Drag drop horizontally. - drop_here = ( - pos.x() >= w.x() - spacing - and pos.x() <= w.x() + w.size().width() + spacing - ) - - if drop_here: - # Drop over this target. - break - - return n - - def add_item(self, item): - self.blayout.addWidget(item) - - def get_item_data(self): - data = [] - for n in range(self.blayout.count()): - # Get the widget at each index in turn. - w = self.blayout.itemAt(n).widget() - if hasattr(w, "data"): - # The target indicator has no data. - data.append(w.data) - return data diff --git a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/main.py b/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/main.py deleted file mode 100755 index 9a5c9376e..000000000 --- a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/main.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys - -from rqt_gui.main import Main - - -def main(): - main = Main() - sys.exit(main.main(sys.argv, standalone='rqt_py_console.py_console.PyConsole')) - - -if __name__ == '__main__': - main() diff --git a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/py_console_text_edit.py b/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/py_console_text_edit.py deleted file mode 100644 index dc9ce1a0a..000000000 --- a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/py_console_text_edit.py +++ /dev/null @@ -1,69 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import sys -from code import InteractiveInterpreter - -from python_qt_binding import QT_BINDING, QT_BINDING_VERSION -from python_qt_binding.QtCore import Qt, Signal - -from qt_gui_py_common.console_text_edit import ConsoleTextEdit - - -class PyConsoleTextEdit(ConsoleTextEdit): - _color_stdin = Qt.darkGreen - _multi_line_char = ':' - _multi_line_indent = ' ' - _prompt = ('>>> ', '... ') # prompt for single and multi line - exit = Signal() - - def __init__(self, parent=None): - super(PyConsoleTextEdit, self).__init__(parent) - - self._interpreter_locals = {} - self._interpreter = InteractiveInterpreter(self._interpreter_locals) - - self._comment_writer.write('Python %s on %s\n' % - (sys.version.replace('\n', ''), sys.platform)) - self._comment_writer.write( - 'Qt bindings: %s version %s\n' % (QT_BINDING, QT_BINDING_VERSION)) - - self._add_prompt() - - def update_interpreter_locals(self, newLocals): - self._interpreter_locals.update(newLocals) - - def _exec_code(self, code): - try: - self._interpreter.runsource(code) - except SystemExit: # catch sys.exit() calls, so they don't close the whole gui - self.exit.emit() diff --git a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/py_console_widget.py b/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/py_console_widget.py deleted file mode 100644 index e69bde34c..000000000 --- a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/py_console_widget.py +++ /dev/null @@ -1,59 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import os -from ament_index_python.resources import get_resource - -from python_qt_binding import loadUi -from python_qt_binding.QtWidgets import QWidget -from rqt_py_console.py_console_text_edit import PyConsoleTextEdit - - -class PyConsoleWidget(QWidget): - - def __init__(self, context=None): - super(PyConsoleWidget, self).__init__() - - _, package_path = get_resource('packages', 'rqt_py_console') - ui_file = os.path.join( - package_path, 'share', 'rqt_py_console', 'resource', 'py_console_widget.ui') - - loadUi(ui_file, self, {'PyConsoleTextEdit': PyConsoleTextEdit}) - self.setObjectName('PyConsoleWidget') - - my_locals = { - 'context': context - } - self.py_console.update_interpreter_locals(my_locals) - self.py_console.print_message( - 'The variable "context" is set to the PluginContext of this plugin.') - self.py_console.exit.connect(context.close_plugin) diff --git a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/spyder_console_widget.py b/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/spyder_console_widget.py deleted file mode 100644 index 374ef7a5d..000000000 --- a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/spyder_console_widget.py +++ /dev/null @@ -1,60 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from python_qt_binding.QtGui import QFont - -from spyder.widgets.internalshell import InternalShell -from spyder.utils.module_completion import moduleCompletion - -class SpyderConsoleWidget(InternalShell): - - def __init__(self, context=None): - my_locals = { - 'context': context - } - super(SpyderConsoleWidget, self).__init__(namespace=my_locals) - self.setObjectName('SpyderConsoleWidget') - self.set_pythonshell_font(QFont('Mono')) - self.interpreter.restore_stds() - - def get_module_completion(self, objtxt): - """Return module completion list associated to object name""" - return moduleCompletion(objtxt) - - def run_command(self, *args): - self.interpreter.redirect_stds() - super(SpyderConsoleWidget, self).run_command(*args) - self.flush() - self.interpreter.restore_stds() - - def shutdown(self): - self.exit_interpreter() diff --git a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/template.py b/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/template.py deleted file mode 100644 index 781f23c7b..000000000 --- a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/template.py +++ /dev/null @@ -1,894 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from python_qt_binding.QtWidgets import QVBoxLayout, QWidget -from rqt_gui_py.plugin import Plugin -from qt_gui_py_common.simple_settings_dialog import SimpleSettingsDialog -from rqt_py_console.py_console_widget import PyConsoleWidget - -import python_qt_binding.QtWidgets as qt -import python_qt_binding.QtWidgets as QtWidgets -import python_qt_binding.QtGui as gui -import python_qt_binding.QtCore as core - -from ament_index_python.packages import get_package_share_directory -import yaml -import os -import collections -import copy -import inspect -import time -import pickle - -from behavior_tree_msgs.msg import Status, BehaviorTreeCommand, BehaviorTreeCommands -from airstack_msgs.msg import FixedTrajectory -from diagnostic_msgs.msg import KeyValue -from std_msgs.msg import String - -from .drag_and_drop import DragWidget, DragItem -from .trajectory_dialog import TrajectoryDialog -import rclpy - -logger = None - - -class GroundControlStation(Plugin): - - def __init__(self, context): - super(GroundControlStation, self).__init__(context) - self.setObjectName("GroundControlStation") - - self.settings = { - "command_config_filename": None, - "trajectory_config_filename": None, - "robots": [], - "groups": {"commands": []}, - "publishers": {}, - } - - # self.config_filename = '' - self.button_groups = {} - - self.context = context - self.node = self.context.node - global logger - logger = self.node.get_logger() - - latched_qos = rclpy.qos.QoSProfile( - history=rclpy.qos.HistoryPolicy.KEEP_LAST, - depth=1, - durability=rclpy.qos.DurabilityPolicy.TRANSIENT_LOCAL, - ) - self.robot_selection_pub = self.node.create_publisher( - String, "robot_selection", latched_qos - ) - - # main layout - self.widget = QWidget() - self.vbox = qt.QVBoxLayout() - self.widget.setLayout(self.vbox) - context.add_widget(self.widget) - - # config widget - self.config_widget = qt.QWidget() - # self.config_widget.setStyleSheet('QWidget{margin-left:-1px;}') - self.config_layout = qt.QHBoxLayout() - self.config_widget.setLayout(self.config_layout) - self.config_widget.setFixedHeight(50) - - self.pause_all_button = qt.QPushButton("PAUSE ALL ROBOTS") - self.pause_all_button.clicked.connect(self.pause_all) - self.config_layout.addWidget(self.pause_all_button) - - self.robot_selection_label = qt.QLabel("Robot to command:") - self.config_layout.addWidget(self.robot_selection_label) - - self.robot_combo_box = qt.QComboBox() - self.robot_combo_box.currentIndexChanged.connect(self.robot_selection_change) - self.config_layout.addWidget(self.robot_combo_box) - - self.vbox.addWidget(self.config_widget) - - spacer = qt.QSpacerItem( - 20, 40, qt.QSizePolicy.Minimum, qt.QSizePolicy.Expanding - ) - self.vbox.addItem(spacer) - - # button widget - self.button_widget = qt.QWidget() - self.button_layout = qt.QVBoxLayout() - self.button_widget.setLayout(self.button_layout) - self.vbox.addWidget(self.button_widget) - - spacer = qt.QSpacerItem( - 20, 40, qt.QSizePolicy.Minimum, qt.QSizePolicy.Expanding - ) - self.vbox.addItem(spacer) - - # timeline widget - self.timeline_widget = qt.QWidget() - self.timeline_layout = qt.QHBoxLayout() - self.timeline_widget.setLayout(self.timeline_layout) - - self.timeline_button_widget = qt.QWidget() - self.timeline_button_layout = qt.QVBoxLayout(self.timeline_button_widget) - self.timeline_layout.addWidget(self.timeline_button_widget) - - self.default_button_style = "QPushButton{font-size: 40px; font-weight: bold}" - self.green_button_style = ( - "QPushButton{font-size: 40px; font-weight: bold; background-color: green}" - ) - self.yellow_button_style = ( - "QPushButton{font-size: 40px; font-weight: bold; background-color: yellow}" - ) - self.red_button_style = ( - "QPushButton{font-size: 40px; font-weight: bold; background-color: red}" - ) - self.timeline_play_button = AspectRatioButton("▶") - self.timeline_play_button.setStyleSheet(self.default_button_style) - self.timeline_play_button.clicked.connect(self.play) - self.timeline_button_layout.addWidget(self.timeline_play_button) - - self.timeline_pause_button = AspectRatioButton("❚❚") - self.timeline_pause_button.setStyleSheet(self.default_button_style) - self.timeline_pause_button.clicked.connect(self.pause) - self.timeline_button_layout.addWidget(self.timeline_pause_button) - - self.timeline_stop_button = AspectRatioButton("◼") - self.timeline_stop_button.setStyleSheet(self.red_button_style) - self.timeline_stop_button.clicked.connect(self.stop) - self.timeline_button_layout.addWidget(self.timeline_stop_button) - - self.timeline_drag_container_widget = qt.QWidget() - self.timeline_drag_container_layout = qt.QHBoxLayout( - self.timeline_drag_container_widget - ) - - self.timeline_drag_widget = DragWidget() - self.timeline_drag_container_layout.addWidget(self.timeline_drag_widget) - self.timeline_drag_container_layout.addStretch(1) - # self.timeline_layout.addWidget(self.timeline_drag_widget) - self.timeline_scroll_area = qt.QScrollArea() - self.timeline_scroll_area.setWidgetResizable(True) - self.timeline_scroll_area.setFixedHeight(300) - self.timeline_scroll_area.setWidget(self.timeline_drag_container_widget) - # self.timeline_scroll_area.setWidget(self.timeline_drag_widget) - self.timeline_layout.addWidget(self.timeline_scroll_area) - - self.right_widget = qt.QWidget() - self.right_layout = qt.QVBoxLayout(self.right_widget) - - self.timeline_add_button = AspectRatioButton("+") - self.timeline_add_button.setStyleSheet(self.default_button_style) - self.timeline_add_button.clicked.connect(self.add_timeline_item) - self.right_layout.addWidget(self.timeline_add_button) - self.right_layout.addStretch(1) - - self.timeline_save_button = qt.QPushButton("Save Mission") - self.timeline_save_button.clicked.connect(self.save_timeline) - self.right_layout.addWidget(self.timeline_save_button) - - self.timeline_load_button = qt.QPushButton("Load Mission") - self.timeline_load_button.clicked.connect(self.load_timeline) - self.right_layout.addWidget(self.timeline_load_button) - - self.timeline_clear_button = qt.QPushButton("Clear Mission") - self.timeline_clear_button.clicked.connect(self.clear_timeline) - self.right_layout.addWidget(self.timeline_clear_button) - - self.timeline_layout.addWidget(self.right_widget) - - self.vbox.addWidget(self.timeline_widget) - - self.timer = core.QTimer(self) - self.timer.timeout.connect(self.play) - - def pause_all(self): - commands = BehaviorTreeCommands() - for group in self.button_groups.keys(): - for i in range(len(self.button_groups[group]["buttons"])): - b = self.button_groups[group]["buttons"][i] - command = BehaviorTreeCommand() - command.condition_name = self.button_groups[group]["condition_names"][i] - if b.isChecked(): - b.toggle() - - command.status = Status.FAILURE - if command.condition_name == "Pause Commanded": - command.status = Status.SUCCESS - commands.commands.append(command) - for robot in self.settings["publishers"].keys(): - self.settings["publishers"][robot]["command_pub"].publish(commands) - - def robot_selection_change(self, s): - msg = String() - msg.data = self.robot_combo_box.itemText(s) - self.robot_selection_pub.publish(msg) - - def save_timeline(self): - timeline_widgets = self.get_timeline_widgets() - save_data = [] - for t in timeline_widgets: - s = t.get_save_data() - save_data.append(s) # t.get_save_data()) - filename = qt.QFileDialog.getSaveFileName( - self.widget, "Save Mission", "", "Mission Files (*.mission)" - )[0] - if filename == "": - return - if not filename.endswith(".mission"): - filename += ".mission" - with open(filename, "wb") as handle: - pickle.dump(save_data, handle, protocol=pickle.HIGHEST_PROTOCOL) - - def load_timeline(self): - filename = qt.QFileDialog.getOpenFileName( - self.widget, "Load Mission", "", "Mission Files (*.mission)" - )[0] - if not os.path.isfile(filename): - return - self.clear_timeline() - with open(filename, "rb") as handle: - save_data = pickle.load(handle) - for s in save_data: - item = DragItem(TimelineEventWidget(self.settings, s)) - item.setMinimumSize(260, 260) - item.setMaximumSize(260, 260) - self.timeline_drag_widget.add_item(item) - - def clear_timeline(self): - timeline_widgets = self.get_timeline_widgets() - for t in timeline_widgets: - t.deleteLater() - - def get_timeline_widgets(self): - timeline_widgets = [] - for i in range(self.timeline_drag_widget.blayout.count()): - try: - timeline_widgets.append( - self.timeline_drag_widget.blayout.itemAt(i).widget().widget - ) - except: - pass - - return timeline_widgets - - def update_timeline_widgets(self): - timeline_widgets = self.get_timeline_widgets() - for tw in timeline_widgets: - tw.set_done(tw.event.is_done()) - - def play(self): - timeline_widgets = self.get_timeline_widgets() - if len(timeline_widgets) == 0: - return - self.timeline_play_button.setStyleSheet(self.green_button_style) - self.timeline_pause_button.setStyleSheet(self.default_button_style) - self.timeline_stop_button.setStyleSheet(self.default_button_style) - for tw in timeline_widgets: - if not tw.event.is_done(): - tw.event.play() - break - self.timer.start(100) - self.update_timeline_widgets() - self.timeline_drag_widget.setEnabled(False) - self.timeline_add_button.setEnabled(False) - - def pause(self): - self.timeline_play_button.setStyleSheet(self.default_button_style) - self.timeline_pause_button.setStyleSheet(self.yellow_button_style) - self.timeline_stop_button.setStyleSheet(self.default_button_style) - self.timer.stop() - timeline_widgets = self.get_timeline_widgets() - for tw in timeline_widgets: - tw.event.pause() - self.update_timeline_widgets() - - def stop(self): - self.timeline_play_button.setStyleSheet(self.default_button_style) - self.timeline_pause_button.setStyleSheet(self.default_button_style) - self.timeline_stop_button.setStyleSheet(self.red_button_style) - self.timer.stop() - timeline_widgets = self.get_timeline_widgets() - for tw in timeline_widgets: - tw.event.stop() - self.update_timeline_widgets() - self.timeline_drag_widget.setEnabled(True) - self.timeline_add_button.setEnabled(True) - - def add_timeline_item(self): - item = DragItem(TimelineEventWidget(self.settings)) - item.setMinimumSize(260, 260) - item.setMaximumSize(260, 260) - self.timeline_drag_widget.add_item(item) - - def select_config_file(self): - starting_path = get_package_share_directory("rqt_gcs") + "/config/" - filename = qt.QFileDialog.getOpenFileName( - self.widget, "Open Config File", starting_path, "Config Files (*.yaml)" - )[0] - print(filename) - self.set_command_config(filename) - - def set_command_config(self, filename): - if filename != "": - self.settings["command_config_filename"] = filename - if self.settings["command_config_filename"] != None: - self.init_buttons(filename) - - def set_trajectory_config(self, filename): - if filename != "": - self.settings["trajectory_config_filename"] = filename - - def init_buttons(self, filename): - y = yaml.load(open(filename, "r").read(), Loader=yaml.Loader) - # self.node.get_logger().info(str(y)) - - for i in reversed(range(self.button_layout.count())): - self.button_layout.itemAt(i).widget().setParent(None) - self.button_groups = {} - - def get_click_function(group, button): - def click_function(): - commands = BehaviorTreeCommands() - # self.node.get_logger().info(str(self.button_groups)) - for i in range(len(self.button_groups[group]["buttons"])): - b = self.button_groups[group]["buttons"][i] - command = BehaviorTreeCommand() - command.condition_name = self.button_groups[group][ - "condition_names" - ][i] - - if b != button and b.isChecked(): - b.toggle() - command.status = Status.FAILURE - elif b == button and not b.isChecked(): - command.status = Status.FAILURE - elif b == button and b.isChecked(): - command.status = Status.SUCCESS - commands.commands.append(command) - # self.command_pub.publish(commands) - self.settings["publishers"][self.robot_combo_box.currentText()][ - "command_pub" - ].publish(commands) - # self.node.get_logger().info(str(self.settings['publishers'][self.robot_combo_box.currentText()]['command_pub'].topic_name)) - - return click_function - - self.robot_combo_box.clear() - for robot in y["robots"]: - self.robot_combo_box.addItem(robot) - # self.robot_combo_box.model().item(0).setBackground(gui.QColor('red')) - self.settings["robots"].append(robot) - - # init publishers - for robot in self.settings["robots"]: - self.settings["publishers"][robot] = { - "command_pub": self.node.create_publisher( - BehaviorTreeCommands, - "/" + robot + "/behavior/behavior_tree_commands", - 1, - ), - "trajectory_pub": self.node.create_publisher( - FixedTrajectory, - "/" - + robot - + "/fixed_trajectory_generator/fixed_trajectory_command", - 1, - ), - } - - for group in y["groups"]: - group_name = list(group.keys())[0] - if group_name not in self.button_groups.keys(): - self.button_groups[group_name] = {"buttons": [], "condition_names": []} - self.settings["groups"][group_name] = { - "condition_titles": [], - "condition_names": [], - } - - group_widget = qt.QWidget() - group_layout = qt.QVBoxLayout() - group_widget.setLayout(group_layout) - self.button_layout.addWidget(group_widget) - - group_layout.addWidget(qt.QLabel(group_name)) - - button_widget = qt.QWidget() - button_layout = qt.QHBoxLayout() - button_widget.setLayout(button_layout) - group_layout.addWidget(button_widget) - - for buttons in group[group_name]: - button_name = list(buttons.keys())[0] - condition_name = buttons[button_name]["condition_name"] - - button = qt.QPushButton(button_name) - button.clicked.connect(get_click_function(group_name, button)) - button.setCheckable(True) - button_layout.addWidget(button) - - # print(condition_name, bt.get_condition_topic_name(condition_name)) - self.button_groups[group_name]["buttons"].append(button) - self.button_groups[group_name]["condition_names"].append(condition_name) - - self.settings["groups"][group_name]["condition_titles"].append( - button_name - ) - self.settings["groups"][group_name]["condition_names"].append( - condition_name - ) - - def save_settings(self, plugin_settings, instance_settings): - instance_settings.set_value( - "command_config_filename", self.settings["command_config_filename"] - ) - instance_settings.set_value( - "trajectory_config_filename", self.settings["trajectory_config_filename"] - ) - - def restore_settings(self, plugin_settings, instance_settings): - self.set_command_config(instance_settings.value("command_config_filename")) - self.set_trajectory_config( - instance_settings.value("trajectory_config_filename") - ) - - def trigger_configuration(self): - sd = SettingsDialog(self.settings) - if sd.exec(): - self.set_command_config(sd.command_config_filename) - self.set_trajectory_config(sd.trajectory_config_filename) - else: - pass - - def shutdown_console_widget(self): - pass - - def shutdown_plugin(self): - self.shutdown_console_widget() - - -class SettingsDialog(qt.QDialog): - def __init__(self, settings): - super().__init__(None) - self.setWindowTitle("Settings") - layout = qt.QVBoxLayout() - - self.command_config_filename = settings["command_config_filename"] - self.trajectory_config_filename = settings["trajectory_config_filename"] - - command_widget = qt.QWidget() - command_layout = qt.QHBoxLayout(command_widget) - - command_config_button = qt.QPushButton("Open Config...") - command_config_button.clicked.connect(self.select_command_config_file) - command_layout.addWidget(command_config_button) - - self.command_config_label = qt.QLabel( - "Command Config File:" + os.path.basename(str(self.command_config_filename)) - ) - command_layout.addWidget(self.command_config_label) - - trajectory_widget = qt.QWidget() - trajectory_layout = qt.QHBoxLayout(trajectory_widget) - - trajectory_config_button = qt.QPushButton("Open Config...") - trajectory_layout.addWidget(trajectory_config_button) - trajectory_config_button.clicked.connect(self.select_trajectory_config_file) - - self.trajectory_config_label = qt.QLabel( - "Trajectory Config File:" - + os.path.basename(str(self.trajectory_config_filename)) - ) - trajectory_layout.addWidget(self.trajectory_config_label) - - self.buttons = qt.QDialogButtonBox( - qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel - ) - self.buttons.accepted.connect(self.accept) - self.buttons.rejected.connect(self.reject) - - layout.addWidget(command_widget) - layout.addWidget(trajectory_widget) - layout.addWidget(self.buttons) - self.setLayout(layout) - - def select_command_config_file(self): - starting_path = get_package_share_directory("rqt_gcs") + "/config/" - self.command_config_filename = qt.QFileDialog.getOpenFileName( - self, "Open Config File", starting_path, "Config Files (*.yaml)" - )[0] - self.command_config_label.setText( - "Command Config File: " + os.path.basename(self.command_config_filename) - ) - - def select_trajectory_config_file(self): - starting_path = get_package_share_directory("rqt_gcs") + "/config/" - self.trajectory_config_filename = qt.QFileDialog.getOpenFileName( - self, "Open Config File", starting_path, "Config Files (*.yaml)" - )[0] - self.trajectory_config_label.setText( - "Trajectory Config File: " - + os.path.basename(self.trajectory_config_filename) - ) - - -class TimelineEventWidget(QWidget): - - def __init__(self, global_settings, save_data=None): - super().__init__() - self.global_settings = global_settings - self.event = None - - self.layout = qt.QVBoxLayout() - self.setLayout(self.layout) - - self.top_widget = qt.QWidget() - # self.top_widget.setObjectName('top') - # self.top_widget.setStyleSheet('QWidget#top {background-color: red; border: 1px solid black;}') - self.top_widget.setMaximumHeight(50) - self.top_layout = qt.QHBoxLayout(self.top_widget) - # self.top_layout.setSpacing(0) - # self.top_layout.setContentsMargins(0, 0, 0, 0) - - self.task_combo_box = qt.QComboBox() - self.task_combo_box.addItem("Wait", WaitEvent) - self.task_combo_box.addItem("Command", CommandEvent) - self.task_combo_box.addItem("Trajectory", TrajectoryEvent) - self.top_layout.addWidget(self.task_combo_box) - # self.layout.addWidget(self.task_combo_box) - - self.delete_button = qt.QPushButton("X") - self.delete_button.clicked.connect(self.deleteLater) - self.delete_button.setMaximumWidth(20) - self.top_layout.addWidget(self.delete_button) - self.layout.addWidget(self.top_widget) - # self.layout.addStretch(1) - - self.content_widget = qt.QWidget() - self.content_widget.setObjectName("content") - self.content_layout = qt.QVBoxLayout() - self.content_widget.setLayout(self.content_layout) - self.layout.addWidget(self.content_widget) - - if save_data == None: - self.task_combo_box.currentIndexChanged.connect(self.task_combo_box_change) - self.task_combo_box_change(0) - else: - self.task_combo_box.setCurrentText(save_data["type"]) - self.clear_content() - self.event = self.task_combo_box.itemData( - self.task_combo_box.currentIndex() - )(self.global_settings, save_data["local_settings"]) - self.event.init_widgets(self.content_layout) - - def get_save_data(self): - dct = {"type": self.task_combo_box.currentText(), "local_settings": {}} - if self.event != None: - dct["local_settings"] = self.event.local_settings - return dct - - def set_done(self, b): - if b: - self.content_widget.setStyleSheet( - "QWidget#content {background-color: green; border: 1px solid black;}" - ) - else: - self.content_widget.setStyleSheet( - "QWidget#content {background-color: lightcyan;}" - ) - - def task_combo_box_change(self, s): - self.clear_content() - self.event = self.task_combo_box.itemData(s)(self.global_settings) - self.event.init_widgets(self.content_layout) - - def clear_content(self): - for i in reversed(range(self.content_layout.count())): - self.content_layout.itemAt(i).widget().setParent(None) - - -class TimelineEvent: - def __init__(self, global_settings, local_settings=None): - self.global_settings = global_settings - self.local_settings = local_settings - if self.local_settings == None: - self.local_settings = {} - - self.done = False - - def play(self): - self.done = True - - def pause(self): - pass - - def stop(self): - self.done = False - - def init_widgets(self, parent_layout): - pass - - def is_done(self): - return self.done - - -class RobotEvent(TimelineEvent): - def __init__(self, global_settings, local_settings=None): - super(RobotEvent, self).__init__(global_settings, local_settings) - if "robot" not in self.local_settings.keys(): - self.local_settings["robot"] = self.global_settings["robots"][0] - - def init_widgets(self, parent_layout): - super().init_widgets(parent_layout) - - widget = qt.QWidget() - layout = qt.QHBoxLayout(widget) - - self.robot_label = qt.QLabel("Robot:") - layout.addWidget(self.robot_label) - - self.robots_combo_box = qt.QComboBox() - self.robots_combo_box.addItems(self.global_settings["robots"]) - self.robots_combo_box.currentIndexChanged.connect(self.robots_combo_box_change) - self.robots_combo_box.setCurrentText(self.local_settings["robot"]) - layout.addWidget(self.robots_combo_box) - - parent_layout.addWidget(widget) - - def robots_combo_box_change(self, index): - self.local_settings["robot"] = self.robots_combo_box.itemText(index) - - -class PublisherEvent(TimelineEvent): - def __init__(self, global_settings, local_settings=None): - super(PublisherEvent, self).__init__(global_settings, local_settings) - - def init_widgets(self, parent_layout): - super().init_widgets(parent_layout) - - def get_publisher(self): - return None - - def get_message(self): - return None - - def play(self): - self.done = True - pub = self.get_publisher() - msg = self.get_message() - if pub == None or msg == None: - return - pub.publish(msg) - - -class CommandEvent(RobotEvent, PublisherEvent): - def __init__(self, global_settings, local_settings=None): - super(CommandEvent, self).__init__(global_settings, local_settings) - if "command_title" not in self.local_settings.keys(): - self.local_settings["command_title"] = self.global_settings["groups"][ - "commands" - ]["condition_titles"][0] - if "command_name" not in self.local_settings.keys(): - self.local_settings["command_name"] = self.global_settings["groups"][ - "commands" - ]["condition_names"][0] - - def get_publisher(self): - if self.local_settings["robot"] in self.global_settings["publishers"]: - return self.global_settings["publishers"][self.local_settings["robot"]][ - "command_pub" - ] - return None - - def get_message(self): - commands = BehaviorTreeCommands() - selected = self.local_settings[ - "command_title" - ] # self.commands_combo_box.currentText() - for i in range(self.commands_combo_box.count()): - command = BehaviorTreeCommand() - command.condition_name = self.commands_combo_box.itemData(i) - if selected == self.commands_combo_box.itemText(i): - command.status = Status.SUCCESS - else: - command.status = Status.FAILURE - commands.commands.append(command) - return commands - - def init_widgets(self, parent_layout): - super().init_widgets(parent_layout) - - self.commands_combo_box = qt.QComboBox() - for i in range( - len(self.global_settings["groups"]["commands"]["condition_titles"]) - ): - title = self.global_settings["groups"]["commands"]["condition_titles"][i] - name = self.global_settings["groups"]["commands"]["condition_names"][i] - self.commands_combo_box.addItem(title, name) - self.commands_combo_box.currentIndexChanged.connect( - self.commands_combo_box_change - ) - self.commands_combo_box.setCurrentText(self.local_settings["command_title"]) - # self.commands_combo_box_change(self.global_settings['groups']['commands']['condition_titles'].index(self.local_settings['command_title'])) - parent_layout.addWidget(self.commands_combo_box) - - def commands_combo_box_change(self, index): - self.local_settings["command_title"] = self.commands_combo_box.itemText(index) - self.local_settings["command_name"] = self.commands_combo_box.itemData(index) - - -class TrajectoryEvent(RobotEvent, PublisherEvent): - def __init__(self, global_settings, local_settings=None): - super(TrajectoryEvent, self).__init__(global_settings, local_settings) - if "trajectory_attributes" not in self.local_settings.keys(): - self.local_settings["trajectory_attributes"] = collections.OrderedDict() - - def get_publisher(self): - if self.local_settings["robot"] in self.global_settings["publishers"]: - return self.global_settings["publishers"][self.local_settings["robot"]][ - "trajectory_pub" - ] - return None - - def get_message(self): - trajectory_name = self.label.text().split(":")[-1].strip() - if trajectory_name == "None": - return None - msg = FixedTrajectory() - msg.type = trajectory_name - for attribute, value in iter( - self.local_settings["trajectory_attributes"][trajectory_name].items() - ): - key_value = KeyValue() - key_value.key = attribute - key_value.value = value - msg.attributes.append(key_value) - return msg - - def init_widgets(self, parent_layout): - super().init_widgets(parent_layout) - - widget = qt.QWidget() - layout = qt.QGridLayout(widget) - - traj_name = "None" - if "trajectory_name" in self.local_settings["trajectory_attributes"].keys(): - traj_name = self.local_settings["trajectory_attributes"]["trajectory_name"] - self.label = qt.QLabel("Trajectory: " + traj_name) - layout.addWidget(self.label, 1, 0) - - button = qt.QPushButton("Configure") - - def click(s): - td = TrajectoryDialog( - self.global_settings["trajectory_config_filename"], - copy.deepcopy(self.local_settings["trajectory_attributes"]), - ) - ret = td.exec() - if ret: - self.local_settings["trajectory_attributes"] = copy.deepcopy( - td.attribute_settings - ) - self.label.setText( - "Trajectory: " + td.attribute_settings["trajectory_name"] - ) - - button.clicked.connect(click) - layout.addWidget(button, 2, 0) - - parent_layout.addWidget(widget) - - -class WaitEvent(TimelineEvent): - def __init__(self, global_settings, local_settings=None): - super(WaitEvent, self).__init__(global_settings, local_settings) - if "wait_time" not in self.local_settings.keys(): - self.local_settings["wait_time"] = 5.0 - self.start_time = None - self.paused_elapsed = 0.0 - self.elapsed = 0.0 - - def init_widgets(self, parent_layout): - super().init_widgets(parent_layout) - - widget = qt.QWidget() - layout = qt.QHBoxLayout(widget) - - time_label = qt.QLabel("Time:") - layout.addWidget(time_label) - - self.line_edit = qt.QLineEdit() - self.line_edit.textChanged.connect(self.text_changed) - self.line_edit.setText(str(self.local_settings["wait_time"])) - layout.addWidget(self.line_edit) - - s_label = qt.QLabel("s") - layout.addWidget(s_label) - - self.elapsed_label = qt.QLabel( - "Elapsed: %0.1f / %0.1f s" % (0.0, self.local_settings["wait_time"]) - ) - - parent_layout.addWidget(widget) - parent_layout.addWidget(self.elapsed_label) - - def text_changed(self, text): - try: - self.local_settings["wait_time"] = float(text) - self.elapsed_label.setText( - "Elapsed: %0.1f / %0.1f s" % (0.0, self.local_settings["wait_time"]) - ) - except: - pass - - def play(self): - if self.start_time == None or self.paused_elapsed != 0.0: - self.start_time = time.time() - self.paused_elapsed - self.paused_elapsed = 0.0 - self.elapsed = time.time() - self.start_time - self.elapsed_label.setText( - "Elapsed: %0.1f / %0.1f s" - % (self.elapsed, self.local_settings["wait_time"]) - ) - - def pause(self): - if self.start_time != None: - self.paused_elapsed = time.time() - self.start_time - - def stop(self): - self.start_time = None - self.paused_elapsed = 0.0 - self.elapsed = 0.0 - self.elapsed_label.setText( - "Elapsed: %0.1f / %0.1f s" - % (self.elapsed, self.local_settings["wait_time"]) - ) - - def is_done(self): - return (self.start_time != None) and ( - self.elapsed >= self.local_settings["wait_time"] - ) - - -class AspectRatioButton(qt.QPushButton): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.aspect_ratio = 1.0 - - def resizeEvent(self, event): - size = event.size() - if size.height() == 0 or size.width() / size.height() > self.aspect_ratio: - size.setWidth(int(size.height() * self.aspect_ratio)) - else: - size.setHeight(int(size.width() / self.aspect_ratio)) - self.resize(size) diff --git a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/trajectory_dialog.py b/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/trajectory_dialog.py deleted file mode 100644 index 6820be84d..000000000 --- a/gcs/ros_ws/src/rqt_gcs/src/rqt_gcs/trajectory_dialog.py +++ /dev/null @@ -1,181 +0,0 @@ -from python_qt_binding.QtWidgets import QVBoxLayout, QWidget -from rqt_gui_py.plugin import Plugin -from qt_gui_py_common.simple_settings_dialog import SimpleSettingsDialog -from rqt_py_console.py_console_widget import PyConsoleWidget - -import python_qt_binding.QtWidgets as qt -import python_qt_binding.QtWidgets as QtWidgets -import python_qt_binding.QtGui as gui -import python_qt_binding.QtCore as QtCore - -from ament_index_python.packages import get_package_share_directory -import yaml -import os -import collections - - -class TrajectoryDialog(qt.QDialog): - ''' - def __init__(self, parent=None): - super().__init__(parent) - - self.setWindowTitle("HELLO!") - - QBtn = qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel - - self.buttonBox = qt.QDialogButtonBox(QBtn) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - layout = qt.QVBoxLayout() - message = qt.QLabel("Something happened, is that OK?") - layout.addWidget(message) - layout.addWidget(self.buttonBox) - self.setLayout(layout) - ''' - def __init__(self, trajectory_config_filename, default_attribute_settings): - super().__init__(None) - - self.config_filename = '' - - self.button_dct = {} - self.attribute_settings = default_attribute_settings - - # main layout - self.vbox = qt.QVBoxLayout() - self.setLayout(self.vbox) - - # trajectory widget - self.trajectory_widget = qt.QWidget() - self.trajectory_layout = qt.QVBoxLayout() - self.trajectory_widget.setLayout(self.trajectory_layout) - self.vbox.addWidget(self.trajectory_widget) - - self.tab_widget = qt.QTabWidget() - self.trajectory_layout.addWidget(self.tab_widget) - - # button widget - self.button_widget = qt.QWidget() - self.button_layout = qt.QHBoxLayout() - self.button_widget.setLayout(self.button_layout) - self.vbox.addWidget(self.button_widget) - - self.publish_button = qt.QPushButton('Publish') - self.publish_button.clicked.connect(self.publish_trajectory) - self.button_layout.addWidget(self.publish_button) - - self.trajectory_type_label = qt.QLabel('Type: ') - self.button_layout.addWidget(self.trajectory_type_label) - - self.trajectory_type_combo_box = qt.QComboBox() - self.trajectory_type_combo_box.addItem('Fixed Trajectory') - self.trajectory_type_combo_box.addItem('Global Plan') - self.button_layout.addWidget(self.trajectory_type_combo_box) - - # ok/cancel buttons - self.buttons = qt.QDialogButtonBox(qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel) - self.buttons.accepted.connect(self.accept) - self.buttons.rejected.connect(self.reject) - self.vbox.addWidget(self.buttons) - - self.set_config(trajectory_config_filename) - self.tab_widget.currentChanged.connect(self.on_tab_changed) - - def on_tab_changed(self, index): - self.attribute_settings['tab_index'] = index - self.attribute_settings['trajectory_name'] = self.tab_widget.tabText(index) - - def publish_trajectory(self): - trajectory_type = self.trajectory_type_combo_box.currentText() - trajectory_name = self.tab_widget.tabText(self.tab_widget.currentIndex()) - msg = FixedTrajectory() - msg.type = trajectory_name - for attribute, value in iter(self.attribute_settings[trajectory_name].items()): - key_value = KeyValue() - key_value.key = attribute - key_value.value = value - msg.attributes.append(key_value) - if trajectory_type == 'Fixed Trajectory': - self.fixed_trajectory_pub.publish(msg) - elif trajectory_type == 'Global Plan': - self.global_plan_fixed_trajectory_pub.publish(msg) - - def select_config_file(self): - starting_path = get_package_share_directory('rqt_fixed_trajectory_generator') + '/config/' - print(starting_path) - filename = qt.QFileDialog.getOpenFileName(self.widget, 'Open Config File', starting_path, "Config Files (*.yaml)")[0] - self.set_config(filename) - - def set_config(self, filename): - if filename != '': - self.config_filename = filename - if self.config_filename != None: - self.init_buttons(filename) - if 'trajectory_name' not in self.attribute_settings: - self.on_tab_changed(0) - - def init_buttons(self, filename): - y = yaml.load(open(filename, 'r').read(), Loader=yaml.Loader) - print(y) - - def get_attribute_changed_function(trajectory_name, attribute_name): - def attribute_changed(text): - if trajectory_name not in self.attribute_settings: - self.attribute_settings[trajectory_name] = {} - self.attribute_settings[trajectory_name][attribute_name] = text - return attribute_changed - - def get_publish_function(trajectory_name): - def publish_function(): - msg = FixedTrajectory() - msg.type = trajectory_name - for attribute, value in iter(self.attribute_settings[trajectory_name].items()): - key_value = KeyValue() - key_value.key = attribute - key_value.value = value - msg.attributes.append(key_value) - self.fixed_trajectory_pub.publish(msg) - return publish_function - - - for trajectory in y['trajectories']: - trajectory_name = list(trajectory.keys())[0] - attributes = trajectory[trajectory_name]['attributes'] - - trajectory_tab = qt.QWidget() - trajectory_layout = qt.QVBoxLayout() - trajectory_tab.setLayout(trajectory_layout) - - for attribute in attributes: - attribute_widget = qt.QWidget() - attribute_layout = qt.QHBoxLayout() - attribute_widget.setLayout(attribute_layout) - - attribute_label = qt.QLabel() - attribute_label.setText(attribute) - attribute_layout.addWidget(attribute_label) - - attribute_default = '0' - if attribute == 'frame_id': - attribute_default = 'base_link' - if trajectory_name in self.attribute_settings.keys(): - if attribute in self.attribute_settings[trajectory_name].keys(): - attribute_default = self.attribute_settings[trajectory_name][attribute] - - attribute_edit = qt.QLineEdit() - attribute_edit.textChanged.connect(get_attribute_changed_function(trajectory_name, - attribute)) - attribute_edit.setText(attribute_default) - - attribute_layout.addWidget(attribute_edit) - - trajectory_layout.addWidget(attribute_widget) - - #publish_button = qt.QPushButton('Publish') - #publish_button.clicked.connect(get_publish_function(trajectory_name)) - #trajectory_layout.addWidget(publish_button) - - self.tab_widget.addTab(trajectory_tab, trajectory_name) - if 'tab_index' in self.attribute_settings: - print(self.attribute_settings['tab_index']) - self.tab_widget.setCurrentIndex(self.attribute_settings['tab_index']) diff --git a/git-hooks/README.md b/git-hooks/README.md index 84e69617e..2a453f5d2 100644 --- a/git-hooks/README.md +++ b/git-hooks/README.md @@ -1,33 +1,23 @@ -# Git Hooks +# Git Hooks (deprecated) -This directory contains git hooks used in the AirStack repository. +!!! warning "These hooks are obsolete — do not install them." -## Available Hooks +The docker-versioning pre-commit hook in this directory wrote the current git +commit hash into the `.env` `VERSION` variable. That scheme has been replaced: +`VERSION` must now be **valid semver, strictly greater than the base branch**, +enforced by the `check-version-increment.yml` CI gate on every pull request. +A hook-written commit hash fails that gate. -### Docker Versioning Hook +The supported flow is: -The `update-docker-image-tag.pre-commit` hook automatically updates the `VERSION` in the `.env` file with the current git commit hash whenever Docker-related files (Dockerfile or docker-compose.yaml) are modified. It also adds a comment above the variable indicating that the value is auto-generated from the git commit hash. +1. Bump `VERSION` in `.env` to the next semver value. +2. Record the change in the versioned Release Notes + (`docs/release_notes/index.md`). -This ensures that Docker images are always tagged with the exact commit they were built from, eliminating version conflicts between parallel branches. +See the `bump-version-and-release` skill (`.agents/skills/bump-version-and-release`) +for the full workflow. -### Installation - -To install the hooks: - -1. Copy the hook to your local .git/hooks directory: - ```bash - cp git-hooks/docker-versioning/update-docker-image-tag.pre-commit .git/hooks/pre-commit - ``` - -2. Make sure the hook file is executable: - ```bash - chmod +x .git/hooks/pre-commit - ``` - -## How the Docker Versioning Hook Works - -1. When you commit changes, the hook checks if any Dockerfile or docker-compose.yaml files are being committed -2. If Docker-related files are detected, it updates the VERSION in the .env file with the current git commit hash and adds a comment above the variable -3. The modified .env file is automatically added to the commit - -This approach eliminates version conflicts between parallel branches by ensuring Docker images are tagged with the exact commit they were built from. \ No newline at end of file +> Note: `airstack config git-hooks` (and `airstack config all`) still installs +> the old hook from this directory; until that CLI path is removed, avoid +> running it. The hook script is kept only so existing installs can be +> identified and removed (`rm .git/hooks/pre-commit`). diff --git a/git-hooks/docker-versioning/README.md b/git-hooks/docker-versioning/README.md index 49000933d..6bd9e58b2 100644 --- a/git-hooks/docker-versioning/README.md +++ b/git-hooks/docker-versioning/README.md @@ -1,35 +1,9 @@ -# Docker Versioning Git Hook +# Docker Versioning Hook (deprecated) -This directory contains a git hook that automatically updates the Docker image tag with the current git commit hash. +Do not install this hook. It writes the git commit hash into the `.env` +`VERSION` variable, which conflicts with the current versioning scheme: +`VERSION` must be valid semver and strictly greater than the base branch, +enforced by the `check-version-increment.yml` CI gate. -## Hook: update-docker-image-tag.pre-commit - -This pre-commit hook automatically updates the `VERSION` in the `.env` file with the current git commit hash whenever Docker-related files (Dockerfile or docker-compose.yaml) are modified. - -### Features - -- Automatically updates `VERSION` with the git commit hash -- Adds a comment above the variable indicating it's auto-generated -- Only triggers when Docker-related files are modified -- Automatically stages the modified .env file for commit - -### Installation - -To install the hook: - -1. Copy the hook to your local .git/hooks directory: - ```bash - cp update-docker-image-tag.pre-commit ../../.git/hooks/pre-commit - ``` - -2. Make sure the hook file is executable: - ```bash - chmod +x ../../.git/hooks/pre-commit - ``` - -### Benefits - -- Eliminates version conflicts between parallel branches -- Ensures Docker images are tagged with the exact commit they were built from -- Simplifies tracking which version of the code is running in Docker containers -- Provides a consistent and automated versioning system for Docker images \ No newline at end of file +See [`git-hooks/README.md`](../README.md) for the supported release flow and +how to remove an existing install of this hook. diff --git a/mkdocs.yml b/mkdocs.yml index d56789e58..28eaa2bcd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,12 +5,26 @@ site_name: AirStack site_dir: ../site site_url: "https://docs.theairlab.org/docs/" # Trailing slash is recommended exclude_docs: | + # The repo README is GitHub-facing; the site's home page is docs/index.md, + # and same-dir would otherwise collide on the root URL. + README.md **/ros_ws/build **/docker/Foxglove - **/macvo **/ros_ws/install **/kit-app-template/** **/isaac_sim_data/** + # Fetched module checkouts (RFC #379 §9): the docs deploy workflows clone the + # registry index + each registered module repo into the gitignored modules/ + # dir. Their content is linked from docs/modules/ (generated by + # tools/gen_docs_catalog.py), never embedded as site pages this phase — so + # exclude the checkouts and their overlay symlinks entirely. + modules/** + robot/ros_ws/src/modules/** + simulation/isaac-sim/launch_scripts/modules/** + # Dot-prefixed dirs (stacks/.external, .airstack) are already skipped by + # MkDocs' default dotfile rule; listed here to keep that intent explicit. + .airstack/** + stacks/.external/** extra: version: provider: mike @@ -22,6 +36,10 @@ extra: link: https://x.com/airlabcmu extra_css: - stylesheets/extra.css +# Trim the Release Notes page to the section matching the .env VERSION at +# build time, so each mike-deployed docs version shows only its own notes. +hooks: + - docs/hooks/release_notes_current_version.py markdown_extensions: - admonition - attr_list @@ -50,143 +68,163 @@ markdown_extensions: permalink: true nav: - Home: docs/index.md - - Getting Started: - - docs/getting_started/index.md - - docs/tutorials/airstack_on_osmo.md - - docs/getting_started/tutorials_reference.md - - Development: - - docs/development/index.md - - Beginner Tutorials: - - Key Concepts: - - Overview: docs/development/beginner/key_concepts.md - - CLI Introduction: docs/development/beginner/airstack-cli/info.md - - Docker Workflow: docs/development/beginner/airstack-cli/docker_usage.md - - docs/development/beginner/development_environment.md - - docs/development/beginner/vscode/vscode_debug.md - - docs/development/beginner/fork_your_own_project.md - - Intermediate Tutorials: - - Testing: - - Overview: docs/development/intermediate/testing/index.md - - Unit Testing: docs/development/intermediate/testing/unit_testing.md - - System Tests: tests/README.md - - End-to-End Testing: docs/development/intermediate/testing/end_to_end_testing.md - - CI/CD Pipeline: docs/development/intermediate/testing/ci_cd.md - - CI/CD Orchestrator: tests/ci-cd-orchestrator.md - - Frame Conventions: docs/development/intermediate/frame_conventions.md - - Docker Build Profiles: docs/development/intermediate/docker-build-profiles.md - - Contributing: - - docs/development/intermediate/contributing.md - - docs/development/intermediate/documentation.md - - Feature Notebook: docs/development/intermediate/feature_notebook.md - - Advanced Tutorials: - - AI Agent Guide: docs/development/advanced/ai_agent_guide.md - - AirStack CLI Tool: - - Extending: docs/development/advanced/airstack-cli/extending.md - - Architecture: docs/development/advanced/airstack-cli/architecture.md - - Simulation: - - docs/simulation/index.md - - Isaac Sim: - - docs/simulation/isaac_sim/index.md - - Docker: docs/simulation/isaac_sim/docker.md - - docs/simulation/isaac_sim/pegasus_scene_setup.md + - Beginner Tutorials: + - Get AirStack Flying: docs/getting_started/index.md + - Fly a Mission from the GCS: docs/getting_started/fly_a_mission.md + - Change a Parameter: docs/getting_started/change_a_parameter.md + - Modular AirStack Walkthrough: docs/getting_started/modular_airstack.md + - Write Your First Module: docs/getting_started/first_module.md + - Your First Fleet: docs/getting_started/first_fleet.md + - Build and Fly Your Own Scene: docs/getting_started/build_your_own_scene.md + - Deploy to Hardware: docs/real_world/deploying_to_hardware.md + - What Next: docs/getting_started/tutorials_reference.md + - Concepts: + - Key Concepts: docs/development/beginner/key_concepts.md + - System Architecture: docs/robot/autonomy/system_architecture.md + - Modular AirStack: + - Modules: docs/development/modules.md + - Stacks: docs/development/stacks.md + - Fleets: docs/development/fleets.md + - Robot Identity: docs/robot/docker/robot_identity.md + - Simulation Platforms: + - Overview: docs/simulation/index.md + - Isaac Sim: docs/simulation/isaac_sim/index.md + - Pegasus Extension: docs/simulation/isaac_sim/pegasus_scene_setup.md + - Microsoft AirSim (legacy): docs/simulation/ms-airsim/index.md + - Simple Sim: docs/simulation/simple_sim/index.md + - Ground Control Station: docs/gcs/index.md + - CI/CD Architecture: docs/development/intermediate/testing/ci_cd.md + - CLI Architecture: docs/development/advanced/airstack-cli/architecture.md + - Frame Conventions: docs/development/intermediate/frame_conventions.md + - How-to Guides: + - Overview: docs/development/index.md + - Development Environment: + - Setup: docs/development/beginner/development_environment.md + - VSCode & Debugging: docs/development/beginner/vscode/vscode_debug.md + - Fork Your Own Project: docs/development/beginner/fork_your_own_project.md + - Remote Development on OSMO: docs/tutorials/airstack_on_osmo.md + - Docker & Builds: + - Docker Workflow: docs/development/beginner/airstack-cli/docker_usage.md + - Build Profiles: docs/development/intermediate/docker-build-profiles.md + - Simulation: + - Scenes: docs/simulation/scenes.md + - Isaac Sim Container Workflows: docs/simulation/isaac_sim/container_workflows.md - Spawning Drones: docs/simulation/isaac_sim/spawning_drones.md - Overhead Camera: docs/simulation/isaac_sim/overhead_camera.md - - docs/simulation/isaac_sim/ascent_sitl_extension.md - - docs/simulation/isaac_sim/export_stages_from_unreal.md - - MoCap Emulator: docs/simulation/isaac_sim/natnet_emulator.md - - Microsoft AirSim (legacy): - - docs/simulation/ms-airsim/index.md - - Docker: docs/simulation/ms-airsim/docker.md - - Simple Sim: - - docs/simulation/simple_sim/index.md - - Docker: docs/simulation/simple_sim/docker.md - - Robot: - - docs/robot/index.md + - Gimbal: docs/robot/autonomy/sensors/gimbal.md + - Export Stages from Unreal: docs/simulation/isaac_sim/export_stages_from_unreal.md + - Autonomy: + - Add a State Estimator: docs/robot/autonomy/perception/adding_a_state_estimator.md + - Add a World Model and Planner: docs/robot/autonomy/adding_a_world_model_and_planner.md + - Add a Controller: docs/robot/autonomy/adding_a_controller.md + - Create a Coordination Algorithm: docs/robot/autonomy/coordination/creating_coordination_algorithms.md + - Coordination Payloads: docs/robot/autonomy/coordination/payloads.md + - Integration Checklist: docs/robot/autonomy/integration_checklist.md + - GCS Operation: + - Waypoints & Geofences: docs/gcs/waypoints_and_geofences.md + - Foxglove Visualization: docs/gcs/foxglove.md + - Extending the Visualizer: docs/gcs/extending_foxglove.md + - Operating the GCS: docs/gcs/usage/user_interface.md + - Robot & Field: + - Overview: docs/real_world/index.md + - Install on Hardware: docs/real_world/installation/index.md + - Add a Vehicle or Platform: docs/development/adding_a_vehicle.md + - HITL Testing: docs/real_world/HITL/index.md + - Data Offloading: docs/real_world/data_offloading/index.md + - Data Offloading (DIY patterns): docs/robot/logging/data_offloading.md + - Logging: docs/robot/logging/index.md + - ROS Bags: docs/robot/logging/rosbags.md + - Modules & Stacks: + - Create a Custom Stack: docs/development/creating_a_stack.md + - Module CI: docs/development/module_ci.md + - Testing: + - Overview: docs/development/intermediate/testing/index.md + - Unit Testing: docs/development/intermediate/testing/unit_testing.md + - End-to-End Testing: docs/development/intermediate/testing/end_to_end_testing.md + - Using CI: docs/development/intermediate/testing/using_ci.md + - Contributing: + - Contributing Guide: docs/development/intermediate/contributing.md + - Documentation Guide: docs/development/intermediate/documentation.md + - Feature Notebook: docs/development/intermediate/feature_notebook.md + - Working with Coding Agents: docs/development/working_with_coding_agents.md + - Extending the CLI: docs/development/advanced/airstack-cli/extending.md + - Reference: + - CLI: docs/development/beginner/airstack-cli/index.md - Configuration: - - docs/robot/configuration/index.md - - Docker: - - docs/robot/docker/index.md - - Robot Identity: docs/robot/docker/robot_identity.md - - Autonomy Modules: - - Overview: docs/robot/autonomy/index.md - - System Architecture: docs/robot/autonomy/system_architecture.md + - Robot Configuration: docs/robot/configuration/index.md + - Environment Variables: docs/robot/configuration/environment_variables.md + - Vehicle Config Schema: config/vehicles/README.md + - Local Calibration: config/local/README.md + - Static Transforms: docs/robot/static_transforms/index.md + - Interfaces: + - Interface Conventions Spec: docs/robot/autonomy/interface_conventions.md - Tasks and Task Executors: docs/robot/autonomy/tasks.md - - Interface: - - docs/robot/autonomy/interface/index.md + - DDS Router: docs/robot/autonomy/dds_router.md + - airstack_msgs: common/ros_packages/msgs/airstack_msgs/README.md + - Containers: + - Robot: docs/robot/docker/index.md + - Isaac Sim: docs/simulation/isaac_sim/docker.md + - Microsoft AirSim (legacy): docs/simulation/ms-airsim/docker.md + - Simple Sim: docs/simulation/simple_sim/docker.md + - GCS: docs/gcs/docker/index.md + - Deployment Topologies: docs/robot/autonomy_modes.md + - Platform Matrix: docs/real_world/supported_platforms.md + - Reference Stacks: + - full_default: stacks/full_default/README.md + - full_droan_cpu: stacks/full_droan_cpu/README.md + - full_macvo: stacks/full_macvo/README.md + - full_mighty: stacks/full_mighty/README.md + - lite_default: stacks/lite_default/README.md + - lite_offload_global: stacks/lite_offload_global/README.md + - Modules Catalog: + - Catalog: docs/modules/index.md + - module.yaml Schema: common/module_schema/README.md + - dfm2_disturbances: docs/modules/dfm2_disturbances.md + - macvo: docs/modules/macvo.md + - mighty: docs/modules/mighty.md + - optitrack: docs/modules/optitrack.md + - Autonomy Packages: + - Robot Overview: docs/robot/index.md + - Autonomy Overview: docs/robot/autonomy/index.md + - Interface: docs/robot/autonomy/interface/index.md - Sensors: - - docs/robot/autonomy/sensors/index.md - - Gimbal: docs/robot/autonomy/sensors/gimbal.md + - Overview: docs/robot/autonomy/sensors/index.md + - LiDAR Point Cloud Filter: robot/ros_ws/src/sensors/lidar_point_cloud_filter/README.md - Perception: - - docs/robot/autonomy/perception/index.md - - NatNet (OptiTrack): robot/ros_ws/src/perception/natnet_ros2/README.md - - PX4 External Vision (mocap): docs/robot/px4_external_vision.md + - Overview: docs/robot/autonomy/perception/index.md + - OptiTrack (asm_optitrack module): docs/robot/optitrack.md - Local: - - docs/robot/autonomy/local/index.md - - World Model: - - docs/robot/autonomy/local/world_model/index.md - - DROAN Obstacle Avoidance: - - Disparity Expansion: robot/ros_ws/src/local/world_models/disparity_expansion/README.md - - Disparity Graph: robot/ros_ws/src/local/world_models/disparity_graph/README.md - - Disparity Graph Cost Map: robot/ros_ws/src/local/world_models/disparity_graph_cost_map/README.md - - Planning: - - docs/robot/autonomy/local/planning/index.md - - Trajectory Library: robot/ros_ws/src/local/planners/trajectory_library/README.md - - Takeoff Landing Planner: - - Overview: robot/ros_ws/src/local/planners/takeoff_landing_planner/README.md - - Testing: robot/ros_ws/src/local/planners/takeoff_landing_planner/test/README.md - - DROAN Local Planner: robot/ros_ws/src/local/planners/droan_local_planner/README.md - - DROAN GL: robot/ros_ws/src/local/planners/droan_gl/README.md - - Controls: - - docs/robot/autonomy/local/controls/index.md - - Trajectory Controller: robot/ros_ws/src/local/controls/trajectory_controller/README.md + - Overview: docs/robot/autonomy/local/index.md + - World Model: docs/robot/autonomy/local/world_model/index.md + - Disparity Expansion: robot/ros_ws/src/local/world_models/disparity_expansion/README.md + - Disparity Graph: robot/ros_ws/src/local/world_models/disparity_graph/README.md + - Disparity Graph Cost Map: robot/ros_ws/src/local/world_models/disparity_graph_cost_map/README.md + - Planning: docs/robot/autonomy/local/planning/index.md + - Trajectory Library: robot/ros_ws/src/local/planners/trajectory_library/README.md + - Takeoff Landing Planner: robot/ros_ws/src/local/planners/takeoff_landing_planner/README.md + - Takeoff Landing Testing: robot/ros_ws/src/local/planners/takeoff_landing_planner/test/README.md + - DROAN Local Planner: robot/ros_ws/src/local/planners/droan_local_planner/README.md + - DROAN GL: robot/ros_ws/src/local/planners/droan_gl/README.md + - Controls: docs/robot/autonomy/local/controls/index.md + - Trajectory Controller: robot/ros_ws/src/local/controls/trajectory_controller/README.md - Global: - - docs/robot/autonomy/global/index.md - - World Model: - - docs/robot/autonomy/global/world_model/index.md - - VDB Mapping: robot/ros_ws/src/global/world_models/vdb_mapping_ros2/README.md - - Planning: - - docs/robot/autonomy/global/planning/index.md - - Random Walk: robot/ros_ws/src/global/planners/random_walk/README.md - - Exploration: robot/ros_ws/src/global/planners/exploration/README.md - - Behavior: - - docs/robot/autonomy/behavior/index.md - - Integration Guide: docs/robot/autonomy/integration_checklist.md - - Coordination: - - Overview: docs/robot/autonomy/coordination/index.md - - Payloads & Foxglove: docs/robot/autonomy/coordination/payloads.md - - Autonomy Modes: docs/robot/autonomy_modes.md - - DDS Router: docs/robot/autonomy/dds_router.md - - Static Transforms: - - docs/robot/static_transforms/index.md - - Logging: - - docs/robot/logging/index.md - - ROS Bags: docs/robot/logging/rosbags.md - - Data Offloading: docs/robot/logging/data_offloading.md - - Bag Recorder: common/ros_packages/bag_recorder_pid/README.md - - Ground Control Station: - - docs/gcs/index.md - - Docker: - - docs/gcs/docker/index.md - - Usage: - - User Interface: docs/gcs/usage/user_interface.md - - Foxglove: - - Visualization: docs/gcs/foxglove.md - - Adding Waypoints & Geofences: docs/gcs/waypoints_and_geofences.md - - Command Center: - - docs/gcs/command_center/command_center.md - - Casualty Assessment: - - docs/gcs/casualty_assessment/casualty_assessment.md - - WinTAK Installation: - - docs/gcs/wintak/installation.md - - Real World: - - docs/real_world/index.md - - Deploying to Hardware: docs/real_world/deploying_to_hardware.md - - Installation on Hardware: - - docs/real_world/installation/index.md - - HITL Testing: - - docs/real_world/HITL/index.md - - Data Offloading: - - docs/real_world/data_offloading/index.md + - Overview: docs/robot/autonomy/global/index.md + - World Model: docs/robot/autonomy/global/world_model/index.md + - VDB Mapping: robot/ros_ws/src/global/world_models/vdb_mapping_ros2/README.md + - Planning: docs/robot/autonomy/global/planning/index.md + - Random Walk: robot/ros_ws/src/global/planners/random_walk/README.md + - Exploration: robot/ros_ws/src/global/planners/exploration/README.md + - Behavior: docs/robot/autonomy/behavior/index.md + - Coordination: docs/robot/autonomy/coordination/index.md + - RViz Tasks Panel: common/ros_packages/gui/rviz/rviz_tasks_panel/README.md + - RViz 3D Waypoint Plugin: common/ros_packages/gui/rviz/3d_waypoint_rviz2_plugin/README.md + - Bag Recorder: common/ros_packages/logging/bag_recorder_pid/README.md + - CI & Testing: + - System Test Suite: tests/README.md + - CI/CD Orchestrator Runbook: tests/ci-cd-orchestrator.md + - OSMO Lab Admin Guide: osmo/README.md + - AI Agent Guide: docs/development/advanced/ai_agent_guide.md + - Release Notes: docs/release_notes/index.md - About: docs/about.md plugins: - search @@ -196,6 +234,13 @@ plugins: - redirects: redirect_maps: 'index.md': 'docs/index.md' + # Fossil pages removed in the Diátaxis docs overhaul + 'docs/simulation/isaac_sim/scene_setup.md': 'docs/simulation/isaac_sim/pegasus_scene_setup.md' + 'docs/simulation/isaac_sim/ascent_sitl_extension.md': 'docs/simulation/isaac_sim/pegasus_scene_setup.md' + 'docs/development/intermediate/testing/testing_frameworks.md': 'docs/development/intermediate/testing/index.md' + 'docs/development/development_environment.md': 'docs/development/beginner/development_environment.md' + 'docs/development/airstack-cli/index.md': 'docs/development/beginner/airstack-cli/index.md' + 'docs/tutorials/index.md': 'docs/getting_started/tutorials_reference.md' repo_name: castacks/AirStack repo_url: https://github.com/castacks/AirStack theme: @@ -205,25 +250,35 @@ theme: - navigation.indexes - navigation.path - navigation.tabs + - navigation.tabs.sticky + - navigation.instant + - navigation.instant.progress + - navigation.tracking # - navigation.expand - navigation.footer - navigation.top - navigation.sections - search.highlight - search.suggest + - search.share - toc.integrate - toc.follow - content.code.copy + - content.code.annotate + - content.tooltips + font: + text: Inter + code: JetBrains Mono logo: docs/assets/airstack_white.png name: material palette: - - accent: pink + - accent: custom primary: custom scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode - - accent: pink + - accent: custom primary: custom scheme: slate toggle: diff --git a/osmo/README.md b/osmo/README.md index e3f6041bd..1b7ee4924 100644 --- a/osmo/README.md +++ b/osmo/README.md @@ -69,7 +69,7 @@ store. Every student registers their own three credentials with `osmo credential set` once on their laptop. The full walkthrough — including the exact `osmo credential set ...` commands and how to obtain a Nucleus API token — lives in -[`docs/tutorials/airstack_on_osmo.md` Step 0](../docs/tutorials/airstack_on_osmo.md#step-0--register-your-osmo-credentials-one-time). +[`docs/tutorials/airstack_on_osmo.md` Step 0](../docs/tutorials/airstack_on_osmo.md#step-0-register-your-osmo-credentials-one-time). The three credentials, summarized for quick reference: @@ -79,7 +79,7 @@ The three credentials, summarized for quick reference: | `airlab-docker-login` | `GENERIC` | `entrypoint.sh` calls `docker login airlab-docker.andrew.cmu.edu` on the **inner** dockerd before `airstack up`, so the inner Compose stack can pull AirStack images | Yes — exposed as env vars `AIRLAB_REGISTRY_USER`/`AIRLAB_REGISTRY_PASS`. | | `airlab-nucleus` | `GENERIC` | `entrypoint.sh` materializes `simulation/isaac-sim/docker/omni_pass.env` from it so Compose can env-file it into the Isaac Sim container | Yes — exposed as env vars `OMNI_USER`/`OMNI_PASS`/`OMNI_SERVER`. | -The convenience helper `airstack osmo:setup` in +The convenience helper `airstack osmo setup` in [`.airstack/modules/osmo.sh`](../.airstack/modules/osmo.sh) prompts for the underlying values (Andrew ID, AirLab password, Nucleus API token) and runs all three `osmo credential set` commands. @@ -215,7 +215,7 @@ If any container is missing or restarting, the most common causes (in order): 1. The user's `airlab-docker-login` GENERIC credential is wrong / unset → inner `docker pull` from `airlab-docker.andrew.cmu.edu` failed. - Re-run `airstack osmo:setup` (or the explicit `osmo credential set + Re-run `airstack osmo setup` (or the explicit `osmo credential set airlab-docker-login ...` command in the tutorial Step 0). 2. `nvidia-container-toolkit` is not configured on the node → inner Isaac Sim can't see the GPU. Check `docker info | grep -i runtime` inside the @@ -280,7 +280,7 @@ If you see Isaac Sim's "Login Required" popup at startup: 2. **Regenerate the token** at → right-click the cloud icon → **API Tokens** → create a new one. -3. **Update the OSMO credential** with `airstack osmo:setup` (or the +3. **Update the OSMO credential** with `airstack osmo setup` (or the raw `osmo credential set airlab-nucleus ...` command from the tutorial Step 0) and **resubmit the workflow** so the new token lands in `omni_pass.env` on pod boot. To live-patch a running pod diff --git a/overrides/isaac-optitrack-simulation.env b/overrides/isaac-optitrack-simulation.env deleted file mode 100644 index 32208f00c..000000000 --- a/overrides/isaac-optitrack-simulation.env +++ /dev/null @@ -1,36 +0,0 @@ -# Isaac Sim + OptiTrack NatNet mocap, with PX4 flying on external-vision (EV) fusion. -# -# Usage: -# airstack up --env-file overrides/isaac-optitrack-simulation.env -# -# Data path: -# in-sim NatNet emulator -> natnet_ros2 -> vision_pose_converter -# -> /{robot}/interface/mavros/vision_pose/pose_cov -> PX4 EKF2 -# -# Real-robot counterpart: overrides/l4t-optitrack-realrobot.env - -COMPOSE_PROFILES="desktop,isaac-sim" -AUTOLAUNCH="true" -NUM_ROBOTS="1" - -# --- Isaac scene -------------------------------------------------------------- -ISAAC_SIM_USE_STANDALONE="true" -ISAAC_SIM_SCRIPT_NAME="example_one_px4_pegasus_natnet_launch_script.py" -# For multi-agent, use the following script instead. -# ISAAC_SIM_SCRIPT_NAME="example_multi_px4_pegasus_natnet_launch_script.py" -# Ensure each spawned robot has a corresponding rigid body name and streaming ID as in -# the natnet_config.yaml file. Also, increase NUM_ROBOTS to match. - -PLAY_SIM_ON_START="true" - -# --- OptiTrack / NatNet ------------------------------------------------------- -LAUNCH_NATNET="true" - -# The emulator runs inside the isaac-sim container, which holds this static IP on -# airstack_network. -NATNET_SERVER_IP="172.31.0.200" - -# --- PX4 EKF2 external-vision fusion ----------------------------------------- -# Selects simulation/isaac-sim/docker/px4-params/external-vision.env, which holds the -# EKF2 values. See docs/robot/px4_external_vision.md for what each one does. -PX4_PARAM_SET="external-vision" diff --git a/overrides/l4t-optitrack-realrobot.env b/overrides/l4t-optitrack-realrobot.env deleted file mode 100644 index c88253419..000000000 --- a/overrides/l4t-optitrack-realrobot.env +++ /dev/null @@ -1,35 +0,0 @@ -# Real-robot deployment on an NVIDIA Jetson (aarch64 / l4t) flying on OptiTrack mocap: -# PX4 EKF2 fuses the mocap pose as external vision instead of GPS. Use -# overrides/l4t-px4-realrobot.env instead if the vehicle flies on GPS. -# -# Build: airstack image-build --profile l4t robot-l4t -# Run: airstack up --env-file overrides/l4t-optitrack-realrobot.env robot-l4t -# -# Setup guide (PX4 parameters, frames, troubleshooting): -# docs/robot/px4_external_vision.md - -COMPOSE_PROFILES="l4t" -AUTOLAUNCH="true" -NUM_ROBOTS="1" -AUTONOMY_ROLE="full" - -# --- Robot identity ----------------------------------------------------------- -# Resolved from this device's hostname: name the Jetson robot-1 on the HOST -# hostnamectl set-hostname robot-1 -> robot_1 on domain 1 - -# --- OptiTrack / NatNet ------------------------------------------------------- -LAUNCH_NATNET="true" -# Motive host. No sensible default — set this before the first flight. -NATNET_SERVER_IP="192.168.1.100" - -# --- Flight controller (MAVROS) ---------------------------------------------- -# Jetson UART; some airframes wire the FCU through USB-serial instead -# (e.g. /dev/ttyUSB0:921600). -FCU_URL="/dev/ttyTHS4:115200" - -# --- Robot description -------------------------------------------------------- -URDF_FILE="robot_descriptions/iris/urdf/iris_with_sensors.pegasus.robot.urdf" - -# --- Flight-data recording ---------------------------------------------------- -BAG_STORAGE_PATH="/media/airlab/Storage/airstack_collection" -RECORD_BAGS="false" diff --git a/overrides/l4t-px4-realrobot.env b/overrides/l4t-px4-realrobot.env index 6da88cf8d..3ab0aca06 100644 --- a/overrides/l4t-px4-realrobot.env +++ b/overrides/l4t-px4-realrobot.env @@ -1,10 +1,17 @@ # Real-robot deployment on an NVIDIA Jetson (aarch64 / l4t) with a PX4 flight -# controller (e.g. Cube Orange) over serial. +# controller (e.g. Cube Orange) over serial. # Build (first time / after image changes): # airstack image-build --profile l4t robot-l4t # Run: # airstack up --env-file overrides/l4t-px4-realrobot.env robot-l4t +# +# This file selects HARDWARE (Jetson profile, serial FCU, bag storage) plus a +# default stack. Topology selection layers on the same command: +# --stack override the stack below (docs/development/stacks.md) +# --fleet fleet-file identity/placement resolution; replaces +# the hostname→robot_name mapping and, per robot, +# AIRSTACK_STACK_DIR/URDF_FILE (docs/development/fleets.md) # Only bring up the Jetson stack (robot-l4t + zed-l4t). COMPOSE_PROFILES="l4t" @@ -18,8 +25,10 @@ NUM_ROBOTS="1" # Run the following: ``hostnamectl set-hostname robot-1`` # resulting in robot_1 on domain 1. -# Launches entire robot autonomy stack -AUTONOMY_ROLE="full" +# Launches the entire robot autonomy stack (stacks are the only dispatch — +# the legacy AUTONOMY_ROLE was removed). Container path: stacks/ is +# bind-mounted at /root/AirStack/stacks. +AIRSTACK_STACK_DIR="/root/AirStack/stacks/full_default" # --- Flight controller (MAVROS) ---------------------------------------------- # Default is the Jetson UART (ttyTHS4). diff --git a/overrides/ms-airsim.env b/overrides/ms-airsim.env index 125471c66..decc30909 100644 --- a/overrides/ms-airsim.env +++ b/overrides/ms-airsim.env @@ -1,4 +1,9 @@ # overrides specific to running airsim # run as airstack up --env-file overrides/ms-airsim.env; OR docker compose --env-file .env --env-file overrides/ms-airsim.env up +# +# Equivalent intent flag (derives both values below): airstack up --sim airsim +# This file selects a SIMULATOR, not a topology — stack/fleet selection is +# orthogonal: add --stack (docs/development/stacks.md) or +# --fleet (docs/development/fleets.md) on the same command line. COMPOSE_PROFILES="desktop,ms-airsim" -URDF_FILE="robot_descriptions/iris/urdf/iris_stereo.ms-airsim.urdf" \ No newline at end of file +URDF_FILE="robot_descriptions/iris/urdf/iris_stereo.ms-airsim.urdf" diff --git a/robot/docker/.bashrc b/robot/docker/.bashrc index a62e5f18b..9293009aa 100755 --- a/robot/docker/.bashrc +++ b/robot/docker/.bashrc @@ -66,16 +66,102 @@ function cws(){ fi } +# Build → source → launch, with an unmissable banner when a stage fails. +# Used by the docker-compose AUTOLAUNCH tmux commands: a bare +# `bws && sws && ros2 launch ...` dies silently on a build failure — the tmux +# pane just returns to a prompt and `docker logs` shows nothing — so bringup +# failures went unnoticed. Also fine to use interactively. +function _autolaunch_banner(){ + local line='!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + printf '\n\033[1;97;41m%s\033[0m\n' "$line" " AUTOLAUNCH FAILED on $(hostname) (ROBOT_NAME=${ROBOT_NAME:-unset})" " $1" " Scroll up in this tmux pane (airstack connect) or 'airstack logs'" " for the first error." "$line" + # Plain repeat so the message survives log processors that strip ANSI. + printf '%s\n' "AUTOLAUNCH FAILED: $1" +} +function autolaunch(){ + if ! bws; then + _autolaunch_banner "colcon build (bws) failed — the stack was NOT launched" + return 1 + fi + sws + ros2 launch "$@" + local rc=$? + if [ $rc -ne 0 ]; then + _autolaunch_banner "ros2 launch $* exited with code $rc — the stack is DOWN" + return $rc + fi +} + source /opt/ros/jazzy/setup.bash sws # source the ROS2 workspace by default +# Resolve this container's docker compose container name from inside the +# container: reverse-DNS the container IP (hostname -> IP -> PTR record), +# then strip the network suffix. Docker's embedded DNS serves the PTR record +# with the compose container name (e.g. airstack-robot-desktop-1). +# https://wiki.psuter.ch/doku.php?id=get_docker_container_name_from_within_the_container +# WARNING: this technique ONLY works with docker version 29 and up. +_resolve_container_name() { + host $(host $(hostname) | awk '{print $NF}') | awk '{print $NF}' | awk -F . '{print $1}' +} + +# --- Fleet resolution (RFC #380 §2, OPT-IN) --- +# When FLEET_CONFIG_FILE is set (airstack up --fleet ), resolve this +# container's WHOLE fleet entry — name, domain, stack placement, vehicle, +# calibration overlay — via tools/fleet/resolve_fleet.py. Contract: +# - pre-set ROBOT_NAME skips resolution entirely (same guard as the legacy +# branch below; heterogeneous-fleet services set it explicitly); +# - pre-set non-empty ROS_DOMAIN_ID / AIRSTACK_STACK_DIR(+ENTRY) / URDF_FILE +# win over the fleet's values (leaf-value precedence); +# - resolution failure warns and falls through to the legacy resolver. +# FLEET_CONFIG_FILE unset or empty = byte-identical legacy behavior. +if [ -n "${FLEET_CONFIG_FILE:-}" ] && [ -z "${ROBOT_NAME:-}" ]; then + if [ "$ROBOT_NAME_SOURCE" == "hostname" ]; then + fleet_identity=$(hostname) + else + # container-name resolution (shared helper; needs docker >= 29) + fleet_identity=$(_resolve_container_name) + CONTAINER_NAME=":$fleet_identity" + fi + fleet_resolver="$HOME/AirStack/tools/fleet/resolve_fleet.py" + if [ -f "$fleet_resolver" ]; then + _fleet_prev_domain="${ROS_DOMAIN_ID:-}" + _fleet_prev_stack_dir="${AIRSTACK_STACK_DIR:-}" + _fleet_prev_stack_entry="${AIRSTACK_STACK_ENTRY:-}" + _fleet_prev_urdf="${URDF_FILE:-}" + _fleet_exports=$(python3 "$fleet_resolver" "$FLEET_CONFIG_FILE" --name "$fleet_identity") + if [ $? -eq 0 ] && [ -n "$_fleet_exports" ]; then + eval "$_fleet_exports" + export ROBOT_NAME VEHICLE CALIBRATION_DIR + # pre-set env wins per variable + [ -n "$_fleet_prev_domain" ] && ROS_DOMAIN_ID="$_fleet_prev_domain" + export ROS_DOMAIN_ID + if [ -n "$_fleet_prev_stack_dir" ]; then + AIRSTACK_STACK_DIR="$_fleet_prev_stack_dir" + AIRSTACK_STACK_ENTRY="$_fleet_prev_stack_entry" + fi + export AIRSTACK_STACK_DIR AIRSTACK_STACK_ENTRY + [ -n "$_fleet_prev_urdf" ] && URDF_FILE="$_fleet_prev_urdf" + export URDF_FILE + else + echo "WARNING: fleet resolution failed for '$fleet_identity' via" \ + "$FLEET_CONFIG_FILE (resolver error above) — falling back to the" \ + "legacy robot_name_map resolver." + fi + unset _fleet_prev_domain _fleet_prev_stack_dir _fleet_prev_stack_entry \ + _fleet_prev_urdf _fleet_exports + else + echo "WARNING: FLEET_CONFIG_FILE=$FLEET_CONFIG_FILE is set but $fleet_resolver" \ + "is missing (tools/fleet should be bind-mounted) — falling back to the" \ + "legacy robot_name_map resolver." + fi +fi + # If ROBOT_NAME is pre-set (e.g. via docker compose), keep it. # Otherwise extract robot name and ROS domain ID from the container/hostname mapping. if [ -z "${ROBOT_NAME:-}" ]; then if [ "$ROBOT_NAME_SOURCE" == "container_name" ]; then - # https://wiki.psuter.ch/doku.php?id=get_docker_container_name_from_within_the_container - # WARNING: this technique ONLY works with docker version 29 and up. - name_to_map=$(host $(host $(hostname) | awk '{print $NF}') | awk '{print $NF}' | awk -F . '{print $1}') + # container-name resolution (shared helper; needs docker >= 29) + name_to_map=$(_resolve_container_name) CONTAINER_NAME=":$name_to_map" elif [ "$ROBOT_NAME_SOURCE" == "hostname" ]; then name_to_map=$(hostname) @@ -229,8 +315,8 @@ if [ ! -h $HISTFILE ]; then # remove existing .bash_history file if it exists rm $HISTFILE > /dev/null 2>&1 # initialize .bash_history file if doesn't exist yet - if [ ! -d /.dev/.bash_history ]; then - cp $HOME/.dev/.bash_history_init $HOME/.dev/.bash_history + if [ ! -f "$HOME/.dev/.bash_history" ]; then + cp $HOME/.dev/.bash_history_init $HOME/.dev/.bash_history 2>/dev/null fi # symlink to /.dev/.bash_history, silently on error ln -s $HOME/.dev/.bash_history $HISTFILE > /dev/null 2>&1 diff --git a/robot/docker/.vscode/c_cpp_properties.json b/robot/docker/.vscode/c_cpp_properties.json deleted file mode 100644 index f2687974b..000000000 --- a/robot/docker/.vscode/c_cpp_properties.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "configurations": [ - { - "browse": { - "databaseFilename": "${default}", - "limitSymbolsToIncludedHeaders": false - }, - "includePath": [ - "${workspaceFolder}/install/vdb_mapping_interfaces/include/**", - "${workspaceFolder}/install/trajectory_library/include/**", - "${workspaceFolder}/install/mavros_interface/include/**", - "${workspaceFolder}/install/robot_interface/include/**", - "${workspaceFolder}/install/px4_msgs/include/**", - "${workspaceFolder}/install/mav_system_msgs/include/**", - "${workspaceFolder}/install/mav_state_machine_msgs/include/**", - "${workspaceFolder}/install/mav_planning_msgs/include/**", - "${workspaceFolder}/install/mav_msgs/include/**", - "${workspaceFolder}/install/disparity_graph_cost_map/include/**", - "${workspaceFolder}/install/cost_map_interface/include/**", - "${workspaceFolder}/install/disparity_graph/include/**", - "${workspaceFolder}/install/behavior_tree/include/**", - "${workspaceFolder}/install/behavior_tree_msgs/include/**", - "${workspaceFolder}/install/airstack_common/include/**", - "${workspaceFolder}/install/airstack_msgs/include/**", - "${workspaceFolder}/src/**", - "${workspaceFolder}/install/**", - "/opt/ros/humble/include/**", - "/usr/include/**" - ], - "name": "ROS", - "intelliSenseMode": "gcc-x64", - "compilerPath": "/usr/bin/gcc", - "cStandard": "gnu11", - "cppStandard": "c++17" - } - ], - "version": 4 -} \ No newline at end of file diff --git a/robot/docker/.vscode/extensions.json b/robot/docker/.vscode/extensions.json deleted file mode 100644 index da9aa2fe3..000000000 --- a/robot/docker/.vscode/extensions.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "recommendations": [ - "ms-python.black-formatter", - "ms-iot.vscode-ros", - "ms-vscode-remote.remote-ssh", - "ms-python.python", - "ms-vscode-remote.remote-containers", - "redhat.vscode-xml", - "dotjoshjohnson.xml", - "ms-vscode.cmake-tools", - "cschlosser.doxdocgen" - ] -} \ No newline at end of file diff --git a/robot/docker/.vscode/launch.json b/robot/docker/.vscode/launch.json deleted file mode 100644 index 2b6bd5542..000000000 --- a/robot/docker/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "configurations": [ - { - "name": "ROS2: Launch Robot Bringup", - "type": "ros2", - "request": "launch", - "target": "${workspaceFolder}/src/robot_bringup/launch/robot.launch.xml", - }, - { - "name": "ROS2: Attach", - "type": "ros2", - "request": "attach", - }, - ] -} \ No newline at end of file diff --git a/robot/docker/.vscode/settings.json b/robot/docker/.vscode/settings.json deleted file mode 100644 index 6008119e1..000000000 --- a/robot/docker/.vscode/settings.json +++ /dev/null @@ -1,847 +0,0 @@ -{ - // PYTHON - "python.languageServer": "Pylance", - // This enables python language server. Seems to work slightly better than jedi: - "python.jediEnabled": false, - // We use "black" as a formatter: - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" - }, - // ROS - "ros.distro": "jazzy", - "search.exclude": { - "**/build": true, - "**/install": true, - "**/log": true - }, - "C_Cpp.errorSquiggles": "enabled", - // ISAAC SIM - "python.analysis.extraPaths": [ - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.exporter.urdf", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.exporter.urdf/pip_prebundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.app.selector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.app.setup", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.articulation_inspector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.asset_browser", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.assets_check", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.benchmark.services", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.benchmark_environments", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.benchmarks", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.block_world", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.camera_inspector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.cloner", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.common_includes", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.conveyor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.conveyor.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.core_archive", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.core_archive/pip_prebundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.core_nodes", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.cortex", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.cortex.sample_behaviors", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.cortex_sync", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.debug_draw", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.doctest", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.dofbot", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.dynamic_control", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.examples", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.examples_nodes", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.extension_templates", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.franka", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.gain_tuner", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.gym", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.import_wizard", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.internal_tools", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.jupyter_notebook", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.kit", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.lula", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.lula/pip_prebundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.lula_test_widget", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.manipulators", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.manipulators.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.menu", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.merge_mesh", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.ml_archive", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.ml_archive/pip_prebundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.motion_generation", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.nucleus", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.occupancy_map", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.occupancy_map.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.ocs2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.physics_inspector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.physics_utilities", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.proximity_sensor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.quadruped", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.range_sensor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.range_sensor.examples", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.range_sensor.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.repl", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.robot_assembler", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.robot_benchmark", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.robot_description_editor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.ros2_bridge", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.ros2_bridge.robot_description", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.ros_bridge", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.scene_blox", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.sensor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.surface_gripper", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.surface_gripper.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.synthetic_recorder", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.tf_viewer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.throttling", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.ui_template", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.universal_robots", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.utils", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.version", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.vscode", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.wheeled_robots", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.wheeled_robots.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.isaac.window.about", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.kit.loop-isaac", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.kit.property.isaac", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.pip.cloud", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.pip.cloud/pip_prebundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.pip.compute", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.pip.compute/pip_prebundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.replicator.isaac", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/exts/omni.usd.schema.isaac", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.blockworld", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.convexdecomposition", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.kit.property.physx", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.kvdb", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.localcache", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physics.tensors", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physics.tensors.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.bundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.camera", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.cct", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.commands", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.cooking", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.demos", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.fabric", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.forcefields", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.foundation", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.graph", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.internal", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.pvd", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.stageupdate", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.supportui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.telemetry", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.tensors", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.tests.mini", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.tests.visual", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.vehicle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.vehicle.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.physx.zerogravity", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.usd.schema.forcefield", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.usd.schema.physx", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.usdphysics", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.usdphysics.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extsPhysics/omni.usdphysics.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.asset-106.0.3+106.0.0.lx64.r", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.behavior.schema-106.0.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.curve.bundle-1.2.3+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.curve.core-1.1.13+106.0.0.lx64.r.cp310.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.curve.ui-1.3.16+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.curve_editor-105.17.8+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.graph.bundle-106.0.3+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.graph.core-106.0.6+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.graph.schema-106.0.2+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.graph.ui-106.0.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.navigation.bundle-106.0.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.navigation.core-106.0.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.navigation.schema-106.0.2+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.navigation.ui-106.0.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.people-0.3.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.retarget.bundle-106.0.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.retarget.core-106.0.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.retarget.ui-106.0.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.shared.core-106.0.0+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.skelJoint-106.0.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.timeline-105.0.23+106.0.0.lx64.r.cp310.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.anim.window.timeline-105.13.5+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.asset_validator.core-0.11.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.asset_validator.ui-0.11.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.blast-0.15.2+106.0.0.lx64.r", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.cuopt.examples-1.0.0+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.cuopt.service-1.0.0+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.cuopt.visualization-1.0.0+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.curve.creator-105.0.4+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.curve.manipulator-105.2.6+105.2.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.entity.spawn.bundle-0.2.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.entity.spawn.core-0.2.3+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.entity.spawn.ui-0.4.3+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.extended.materials-105.0.9", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.flowusd-106.0.14+106.0.0.lx64.r.cp310.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.flowusd.bundle-1.0.2+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.flowusd.ui-106.0.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.gdn_asset_publisher-0.9.26", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.genproc.bundle-105.1.0+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.genproc.core-105.1.9+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.genproc.ui-105.1.2+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.action-1.101.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.action_nodes-1.21.3+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.bundle.action-2.0.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.io-1.8.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.nodes-1.141.2+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.scriptnode-1.18.2+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.telemetry-2.12.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.tutorials-1.26.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.ui-1.67.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.ui_nodes-1.24.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.visualization.nodes-2.1.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.window.action-1.25.2+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.window.core-1.107.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.window.generic-1.23.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.graph.window.particle.system-105.1.22+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.importer.mjcf-1.1.0+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.importer.onshape-0.7.1+105.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.importer.urdf-1.14.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.isaac.onshape-0.6.5+105.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.agent.watcher-0.2.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.asset_converter-2.1.10+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.asset-1.3.9", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.asset_provider.actorcore-1.0.6", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.asset_provider.local-1.0.9", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.asset_provider.sketchfab-1.0.10", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.asset_provider.turbosquid-1.0.9", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.asset_store-1.3.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.core-2.3.11", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.deepsearch-1.1.8", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.folder.core-1.9.12", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.material-1.5.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.sample-1.4.7", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.showcase-1.0.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.browser.texture-1.2.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.cad-201.0.2+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.cad_core-201.0.2+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.common-201.0.2+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.dgn_core-201.0.2+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.geojson-0.0.10+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.jt_core-201.0.2+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.lib3mf-1.1.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.ogc-1.1.22+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.stl-0.1.1+105.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.converter.vtk-2.3.1+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.core.collection-0.1.7", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.data2ui.core-1.0.25+106.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.data2ui.usd-1.0.25+106.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.environment.core-1.3.10", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.gfn-106.0.4+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.graph.delegate.default-1.2.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.graph.delegate.modern-1.10.6", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.graph.delegate.neo-1.1.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.graph.editor.core-1.5.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.graph.editor.example-1.0.24", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.graph.usd.commands-1.3.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.graph.widget.variables-2.1.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.livestream.core-3.2.0+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.livestream.core-4.3.3+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.livestream.core-4.3.5+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.livestream.messaging-1.1.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.livestream.native-4.1.0+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.livestream.webrtc-4.1.0+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.livestream.webrtc-4.1.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.manipulator.tool.mesh_snap-1.4.5+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.menu.stage-1.2.5", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.mesh.raycast-105.4.0+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.ngsearch-0.3.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.playlist.core-1.3.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.pointclouds-1.3.4+cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.preferences.animation-1.1.7+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.prim.icon-1.0.13", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.profiler.tracy-1.1.4+106.0.0.lx64", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.profiler.window-2.2.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.property.collection-0.1.17", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.property.environment-1.1.7", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.property.sbsar-107.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.property.visualization-104.0.5", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.scripting-106.0.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.search.files-1.0.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.search.service-0.1.12", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.sequencer.core-103.4.2+105.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.sequencer.usd-103.4.4+105.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.simscale-2.1.0+105.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.stage_column.payload-2.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.stage_column.variant-1.0.13", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.stagerecorder.bundle-105.0.2+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.stagerecorder.core-105.0.5+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.stagerecorder.ui-105.0.6+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.streamsdk.plugins-3.2.1+105.2.lx64.r", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.streamsdk.plugins-4.3.3+106.0.0.lx64.r", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.streamsdk.plugins-4.4.1+106.0.0.lx64.r", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.text3d-1.3.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.thumbnails.mdl-1.0.24", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.thumbnails.usd-1.0.9", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.timeline.minibar-1.2.9", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.tool.asset_exporter-1.3.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.tool.asset_importer-2.5.5", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.tool.measure-105.2.5+105.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.tool.remove_unused.controller-0.1.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.tool.remove_unused.core-0.1.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.tools.mergemesh-0.1.6", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.variant.editor-106.0.0+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.variant.presenter-105.1.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.viewport.menubar.lighting-106.0.2+ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.viewport.menubar.waypoint-104.2.16", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.waypoint.bundle-1.0.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.waypoint.core-1.4.52", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.waypoint.playlist-1.0.8", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.widget.calendar-1.0.8", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.widget.collection-0.1.18", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.widget.extended_searchfield-1.0.27", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.widget.material_preview-1.0.16", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.widget.sliderbar-1.0.10", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.widget.timeline-105.0.1+105.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.widget.zoombar-1.0.5", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.widgets.custom-1.0.8", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.window.collection-0.1.22", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.window.environment-1.7.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.window.material-1.5.7", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.window.material_graph-1.8.15", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.window.movie_capture-2.4.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.window.quicksearch-2.4.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.window.section-107.0.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.window.usddebug-1.0.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.advertise-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.core-106.0.50+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.example.usd_scene_ui-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.profile.ar-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.profile.common-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.profile.tabletar-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.profile.vr-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.scene_view.core-106.0.50+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.scene_view.utils-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.system.cloudxr-106.0.50+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.system.cloudxr41-106.0.50+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.system.openxr-106.0.50+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.system.playback-106.0.50+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.system.steamvr-106.0.50+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.telemetry-106.0.50+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.ui.config.common-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.ui.config.generic-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.ui.config.htcvive-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.ui.config.magicleap-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.ui.config.metaquest-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.ui.stage.common-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.ui.window.profile-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.kit.xr.ui.window.viewport-106.0.50+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.no_code_ui.bundle-1.0.25+106.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.ocean-0.4.8", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.paint.brush.attributes-1.3.1+105.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.paint.brush.scatter-105.1.7+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.paint.brush.scripting-105.0.2+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.paint.system.bundle-105.10.2+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.paint.system.core-105.10.1+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.paint.system.ui-105.1.6+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.particle.system.bundle-105.1.0+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.particle.system.core-105.1.8+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.particle.system.ui-105.1.13+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.product_configurator.panel-1.0.15", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.product_configurator.utils-1.2.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.ramp-105.1.15+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.replicator.agent.camera_calibration-0.2.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.replicator.agent.core-0.2.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.replicator.agent.ui-0.2.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.replicator.core-1.11.8+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.replicator.object-0.2.16", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.replicator.replicator_yaml-2.0.5+lx64", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.scene.optimizer.bundle-106.0.4+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.scene.optimizer.core-106.0.4+106.0.0.lx64.r.cp310.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.scene.optimizer.ui-106.0.4+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.scene.visualization.bundle-105.1.0+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.scene.visualization.core-105.4.13+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.scene.visualization.ui-105.1.2+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.schema.audio.boom-0.5.0+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.sensors.nv.common-1.0.1+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.sensors.nv.ids-1.0.1+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.sensors.nv.lidar-1.0.1+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.sensors.nv.materials-1.0.0+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.sensors.nv.radar-1.0.1+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.sensors.nv.ultrasonic-1.0.2+lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.sensors.nv.wpm-1.0.0+lx64.r", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.sensors.tiled-0.0.3+106.0.0.lx64.r", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.browser.asset-1.3.3+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.carb.event_stream-1.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.client-0.5.3", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.core-1.9.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.facilities.base-1.0.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.facilities.workqueue-1.1.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.pip_archive-0.13.3+lx64", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.starfleet.auth-0.1.5", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.streamclient.webrtc-1.3.8", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.streamclient.websocket-2.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.streaming.manager-0.3.10", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.thumbnails.images-1.3.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.transport.client.base-1.2.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.transport.client.http_async-1.3.6", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.transport.server.base-1.1.1", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.transport.server.http-1.3.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.transport.server.zeroconf-1.0.9", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.services.usd-1.1.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.simready.explorer-1.0.26", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.slangnode-106.0.0+106.0.0.lx64.r.cp310.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.tools.array-105.0.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.tools.distribute-105.0.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.tools.randomizer-105.0.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.fileformat.e57-1.2.1+106.0.0.lx64.r.cp310.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.fileformat.sbsar-107.0.2+lx64.r.cp310.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.metrics.assembler-106.0.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.metrics.assembler.physics-106.0.2+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.metrics.assembler.ui-106.0.2+106.0.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.schema.destruction-0.7.0+106.0.0.lx64.r", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.schema.flow-106.0.8+106.0.0.ub3f", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.schema.metrics.assembler-106.0.1+106.0.0.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.schema.scene.visualization-2.0.2+105.2.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.usd.schema.sequence-2.3.0+105.2.lx64.r.cp310", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.vdb_timesample_editor-0.1.10", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.warehouse_creator-0.3.5", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.warp-1.1.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/omni.warp.core-1.1.0", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/semantics.schema.editor-0.3.4", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/extscache/semantics.schema.property-1.0.2", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/carb.audio", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/carb.imaging.python", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/carb.windowing.plugins", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.activity.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.activity.freeze_monitor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.activity.profiler", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.activity.pump", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.activity.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.activity.usd_resolver", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.app.setup", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.app.workflow.startup", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.appwindow", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.audioplayer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.audiorecorder", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.blobkey", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.command.usd", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.cpumemorytracking", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.cuda.libs", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.datastore", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.debugdraw", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.example.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.fabric.agent", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.fabric.commands", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.fabric.fabric_inspector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.gpu_foundation", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.gpucompute.plugins", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.graph", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.graph.action_core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.graph.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.graph.exec", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.graph.image.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.graph.image.nodes", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.graph.tools", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hsscclient", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.engine.stats", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.index", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.index_remote", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.iray", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.pxr", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.pxr.settings", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.rtx", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.scene_api", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.scene_delegate", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.hydra.usdrt_delegate", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.index", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.index.compute", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.index.kit.rtx_scientific", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.index.libs", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.index.renderer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.index.settings.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.index.usd", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.inspect", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.iray.libs", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.iray.settings.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.actions.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.actions.window", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.app_snippets", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.audio.test.usd", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.audiodeviceenum", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.autocapture", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.capture.viewport", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.clipboard", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.collaboration.channel_manager", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.collaboration.presence_layer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.collaboration.selection_outline", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.collaboration.settings", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.collaboration.stage_columns", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.collaboration.telemetry", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.collaboration.viewport.camera", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.commands", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.compatibility_checker", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.context_menu", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.core.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.debug.python", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.debug.vscode", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.documentation.builder", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.documentation.ui.style", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.example.toolbar_button", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.exec.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.exec.debug", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.exec.example-carb", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.exec.example-omni", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.extpath.git", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.filebrowser_column.acl", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.filebrowser_column.tags", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.helper.file_utils", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.hotkeys.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.hotkeys.window", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.hydra_texture", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.loop-default", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.mainwindow", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.camera", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.prim", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.prim.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.prim.fabric", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.prim.legacy", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.prim.usd", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.selection", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.selector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.tool.snap", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.transform", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.manipulator.viewport", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.material.library", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.menu.aov", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.menu.common", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.menu.create", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.menu.edit", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.menu.file", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.menu.utils", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.multinode", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.notification_manager", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.numpy.common", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.pip_archive", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.pip_archive/pip_prebundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.pipapi", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.primitive.mesh", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.profile_python", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.adapter.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.audio", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.bundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.camera", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.file", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.geometry", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.layer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.light", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.material", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.render", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.skel", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.tagging", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.transform", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.usd", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.property.usd_clipboard_test", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.quicklayout", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.raycast.query", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.renderer.capture", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.renderer.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.renderer.cuda_interop", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.renderer.imgui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.renderer.init", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.scene_view.opengl", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.scene_view.usd", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.search_core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.search_example", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.selection", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.stage.copypaste", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.stage.mdl_converter", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.stage_column.active", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.stage_template.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.stage_templates", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.tagging", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.telemetry", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_app_compat", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_app_full_nonrtx", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_async_rendering", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_helpers_gfx", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_suite.browser", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_suite.helpers", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_suite.layer_window", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_suite.layout", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_suite.menu", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_suite.stage_window", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.test_suite.viewport", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.tool.collect", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.ui.actions", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.ui_test", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.uiapp", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.usd.collect", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.usd.layers", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.usd_undo", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.usda_edit", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.usdz_export", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.actions", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.bundle", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.docs", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.iray", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.legacy_gizmos", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.manipulator.transform", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.menubar.camera", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.menubar.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.menubar.display", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.menubar.render", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.menubar.settings", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.pxr", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.ready", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.registry", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.rtx", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.scene_camera_model", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.utility", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport.window", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.viewport_widgets_manager", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.welcome.about", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.welcome.extensions", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.welcome.learn", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.welcome.open", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.welcome.whats_new", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.welcome.window", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.browser_bar", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.cache_indicator", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.context_menu", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.filebrowser", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.filter", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.graph", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.highlight_label", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.imageview", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.inspector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.layers", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.live", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.live_session_management", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.live_session_management.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.nucleus_connector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.nucleus_info", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.opengl", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.options_button", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.options_menu", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.path_field", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.prompt", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.search_delegate", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.searchable_combobox", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.searchfield", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.settings", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.spinner", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.stage", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.stage_fabric", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.stage_icons", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.text_editor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.toolbar", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.versioning", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.widget.viewport", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.about", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.audio.oscilloscope", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.audioplayer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.audiorecorder", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.commands", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.console", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.content_browser", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.content_browser_registry", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.cursor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.drop_support", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.extensions", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.file", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.file_exporter", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.file_importer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.filepicker", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.images", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.imageviewer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.imguidebug", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.inspector", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.material_swap", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.popup_dialog", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.preferences", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.privacy", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.property", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.provide_feedback", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.reshade_editor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.script_editor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.splash", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.splash_close_example", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.stage", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.stage_fabric", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.stageviewer", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.stats", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.status_bar", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.title", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.toolbar", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.usd_paths", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.viewport", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.kit.window.welcome", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.mdl", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.mdl.distill_and_bake", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.mdl.neuraylib", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.mdl.usd_converter", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.mpi.libs", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.mtlx", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.population", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.resourcemonitor", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.rtx.index_composite", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.rtx.registered_compositing.test", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.rtx.settings.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.rtx.shadercache.vulkan", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.rtx.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.rtx.vmaterials.tests", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.rtx.window.settings", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.spatialindex", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.stats", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.syntheticdata", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.taskagent", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.timeline", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.timeline.live_session", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ucx.libs", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ui", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ui.scene", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ui_query", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.uiaudio", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ujitso.client", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ujitso.default", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ujitso.processor.geometry", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ujitso.processor.test", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ujitso.processor.texture", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ujitso.python", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ujitso.router", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.ujitso.service", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.config", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.libs", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.schema.anim", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.schema.audio", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.schema.geospatial", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.schema.omnigraph", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.schema.omniscripting", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd.schema.semantics", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.usd_resolver", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.videoencoding", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.volume", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/omni.volume_nodes", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/test.omni.graph.core", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/exts/usdrt.scenegraph", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/extscore/omni.assets.plugins", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/extscore/omni.client", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/extscore/omni.kit.async_engine", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/extscore/omni.kit.registry.nucleus", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/kernel/py", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/plugins/bindings-python", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/kit/python/lib/python3.12/site-packages", - "~/.local/share/ov/pkg/isaac-sim-4.0.0/python_packages" - ], - "files.associations": { - "*.launch": "xml", - "*.config": "python", - "functional": "cpp", - "__atomic": "cpp", - "filesystem": "cpp", - "unordered_map": "cpp", - "__node_handle": "cpp", - "iterator": "cpp", - "array": "cpp", - "deque": "cpp", - "list": "cpp", - "string": "cpp", - "vector": "cpp", - "string_view": "cpp", - "initializer_list": "cpp", - "__bit_reference": "cpp", - "__hash_table": "cpp", - "__split_buffer": "cpp", - "__tree": "cpp", - "bitset": "cpp", - "map": "cpp", - "set": "cpp", - "cctype": "cpp", - "clocale": "cpp", - "cmath": "cpp", - "cstdarg": "cpp", - "cstddef": "cpp", - "cstdio": "cpp", - "cstdlib": "cpp", - "cstring": "cpp", - "ctime": "cpp", - "cwchar": "cpp", - "cwctype": "cpp", - "atomic": "cpp", - "*.tcc": "cpp", - "chrono": "cpp", - "complex": "cpp", - "condition_variable": "cpp", - "cstdint": "cpp", - "exception": "cpp", - "algorithm": "cpp", - "memory": "cpp", - "memory_resource": "cpp", - "numeric": "cpp", - "optional": "cpp", - "random": "cpp", - "ratio": "cpp", - "system_error": "cpp", - "tuple": "cpp", - "type_traits": "cpp", - "utility": "cpp", - "fstream": "cpp", - "iomanip": "cpp", - "iosfwd": "cpp", - "iostream": "cpp", - "istream": "cpp", - "limits": "cpp", - "mutex": "cpp", - "new": "cpp", - "ostream": "cpp", - "sstream": "cpp", - "stdexcept": "cpp", - "streambuf": "cpp", - "thread": "cpp", - "cinttypes": "cpp", - "typeinfo": "cpp", - "variant": "cpp", - "bit": "cpp", - "__nullptr": "cpp", - "__locale": "cpp", - "codecvt": "cpp", - "any": "cpp", - "future": "cpp", - "csignal": "cpp", - "strstream": "cpp", - "compare": "cpp", - "concepts": "cpp", - "forward_list": "cpp", - "unordered_set": "cpp", - "regex": "cpp", - "numbers": "cpp", - "ranges": "cpp", - "semaphore": "cpp", - "shared_mutex": "cpp", - "stop_token": "cpp", - "cfenv": "cpp", - "typeindex": "cpp", - "valarray": "cpp", - "*.ipp": "cpp" - }, - "C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 100 }", - "editor.formatOnSave": true, - "python.autoComplete.extraPaths": [ - "/home/robot/ros_ws/install/vdb_mapping_interfaces/local/lib/python3.12/dist-packages", - "/home/robot/ros_ws/build/rqt_py_template/src", - "/home/robot/ros_ws/install/rqt_py_template/lib/python3.12/site-packages", - "/home/robot/ros_ws/build/rqt_fixed_trajectory_generator/src", - "/home/robot/ros_ws/install/rqt_fixed_trajectory_generator/lib/python3.12/site-packages", - "/home/robot/ros_ws/build/rqt_behavior_tree_command/src", - "/home/robot/ros_ws/install/rqt_behavior_tree_command/lib/python3.12/site-packages", - "/home/robot/ros_ws/build/rqt_behavior_tree/src", - "/home/robot/ros_ws/install/rqt_behavior_tree/lib/python3.12/site-packages", - "/home/robot/ros_ws/install/px4_msgs/local/lib/python3.12/dist-packages", - "/home/robot/ros_ws/install/mav_planning_msgs/local/lib/python3.12/dist-packages", - "/home/robot/ros_ws/install/mav_msgs/local/lib/python3.12/dist-packages", - "/home/robot/ros_ws/install/behavior_tree_msgs/local/lib/python3.12/dist-packages", - "/home/robot/ros_ws/install/airstack_msgs/local/lib/python3.12/dist-packages", - "/opt/ros/jazzy/lib/python3.12/site-packages", - "/opt/ros/jazzy/local/lib/python3.12/dist-packages" - ] -} \ No newline at end of file diff --git a/robot/docker/.vscode/tasks.json b/robot/docker/.vscode/tasks.json deleted file mode 100644 index 4555b049a..000000000 --- a/robot/docker/.vscode/tasks.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build ros_ws debug", - "type": "shell", - "options": { - "cwd": "${workspaceFolder}" - }, - "command": [ - "source /.bashrc &&", - "bws --cmake-args '-DCMAKE_BUILD_TYPE=Debug'" - ], - "problemMatcher": [], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "test ros_ws", - "type": "shell", - "options": { - "cwd": "${workspaceFolder}" - }, - "command": [ - "source /opt/ros/humble/setup.bash;", - "source ${workspaceFolder}/install/setup.bash;", - "colcon test && colcon test-result" - ], - } - ] -} \ No newline at end of file diff --git a/robot/docker/Dockerfile.robot b/robot/docker/Dockerfile.robot index 13ab03102..b904ac301 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 @@ -107,26 +105,11 @@ RUN apt update -y && apt install -y --no-install-recommends \ ros-${ROS_DISTRO}-rosbag2-storage-mcap \ ros-${ROS_DISTRO}-xacro \ ros-${ROS_DISTRO}-ament-package \ - ros-${ROS_DISTRO}-foxglove-bridge \ - libcgal-dev \ python3-colcon-common-extensions \ && rm -rf /var/lib/apt/lists/* 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 @@ -134,39 +117,18 @@ RUN pip3 install --break-system-packages --ignore-installed \ "pytest==7.4.*" \ empy==3.3.4 \ future \ - lxml \ + # matplotlib stays: dev-script consumer + published-image behavior + # (its resolver also pulls pillow transitively) 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 \ - pygments \ wheel \ - pymavlink \ pyyaml \ requests \ # setup tools must be <80 for Jazzy https://github.com/ros2/ros2/issues/1702#issuecomment-3007929996 setuptools==79.0.1 \ - 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 + # scipy stays: position_setpoint_pub + scipy # Keep pytest < 8.1. ROS Jazzy launch_testing still implements # pytest_pycollect_makemodule(path=...), which pluggy rejects after pytest 8.1 @@ -174,54 +136,9 @@ 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 -# Diagnostic: Check Python environment before DDS Router build -RUN echo "=== Python version ===" && \ - python3 --version && \ - echo "" && \ - echo "=== PYTHONPATH ===" && \ - echo "$PYTHONPATH" && \ - echo "" && \ - echo "=== sys.path ===" && \ - python3 -c "import sys; print('\n'.join(sys.path))" && \ - echo "" && \ - echo "=== Checking ament_package ===" && \ - python3 -c "import ament_package; print('✓ ament_package found at:', ament_package.__file__)" || echo "✗ ament_package NOT found" && \ - echo "" && \ - echo "=== Checking dpkg for ament packages ===" && \ - dpkg -l | grep -i ament || echo "No ament packages found in dpkg" && \ - echo "" && \ - echo "=== ROS Python packages ===" && \ - ls -la /opt/ros/${ROS_DISTRO}/lib/python*/dist-packages/ 2>/dev/null | head -20 || echo "No ROS python packages found" - # Install eProsima DDS Router # System library dependencies (Asio, TinyXML2, OpenSSL, yaml-cpp) RUN apt update && apt install -y --no-install-recommends \ @@ -252,8 +169,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 @@ -316,7 +231,9 @@ ENV ROS_AUTOMATIC_DISCOVERY_RANGE=SUBNET ENV DEBIAN_FRONTEND= # ======================== -# Install runtime dev tools (no cmake or build-essential) +# Install runtime dev tools. (The compile toolchain — cmake, build-essential — +# still arrives below via ros-dev-tools: the runtime image keeps it because +# `bws` builds the ROS workspace inside this container.) RUN apt update && apt install -y --no-install-recommends \ vim nano tree \ less htop jq \ @@ -334,7 +251,11 @@ RUN python3 -m pip install --no-cache-dir --break-system-packages --ignore-insta "setuptools==79.0.1" \ wheel -# Install runtime ROS2 packages (no libcgal-dev) +# Install runtime ROS2 packages. ros-dev-tools pulls in the compile toolchain +# (cmake, build-essential) — deliberate, since `bws` builds in-container. +# (ros-*-grid-map pulls CGAL in as a transitive dependency where it needs it.) +# foxglove-bridge was dropped from the robot image (the GCS image installs its +# own); per-robot Foxglove returns later as an opt-in stack include. RUN apt update -y && apt install -y --no-install-recommends \ ros-dev-tools \ ros-${ROS_DISTRO}-mavros \ @@ -348,12 +269,9 @@ RUN apt update -y && apt install -y --no-install-recommends \ ros-${ROS_DISTRO}-rosbag2-storage-mcap \ ros-${ROS_DISTRO}-xacro \ ros-${ROS_DISTRO}-ament-package \ - ros-${ROS_DISTRO}-foxglove-bridge \ python3-colcon-common-extensions \ && rm -rf /var/lib/apt/lists/* -# TODO: consider splitting this into a separate "desktop-plus" image, since foxglove-bridge is a large install and not strictly necessary for most robot use cases - # Install emoji font support and refresh font cache RUN apt-get update && apt-get install -y --no-install-recommends \ fonts-noto-color-emoji \ @@ -374,20 +292,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 && \ @@ -399,20 +303,32 @@ RUN if [ "${REAL_ROBOT}" != "true" ] && [ "$(dpkg --print-architecture)" = "amd6 fi && \ rm -rf /var/lib/apt/lists/* -# Add ability to SSH (libglfw3-dev and libglm-dev kept per spec) +# Add ability to SSH RUN apt-get ${UPDATE_FLAGS} update && apt-get ${INSTALL_FLAGS} install -y --no-install-recommends \ - openssh-server libglfw3-dev libglm-dev \ + openssh-server \ && rm -rf /var/lib/apt/lists/* RUN mkdir /var/run/sshd +# droan_gl link deps (assimp/EGL/GL) — declared in its package.xml +# (robot/ros_ws/src/local/planners/droan_gl: rosdep keys assimp, opengl, +# libglfw3-dev, libglm-dev). Installed explicitly so the in-container colcon +# build doesn't rely on ros-desktop transitives. EGL has no rosdep key on +# jazzy/noble, hence libegl-dev appears only here. +RUN apt-get ${UPDATE_FLAGS} update && apt-get ${INSTALL_FLAGS} install -y --no-install-recommends \ + libassimp-dev \ + libgl1-mesa-dev \ + libegl-dev \ + libglfw3-dev \ + libglm-dev \ + && rm -rf /var/lib/apt/lists/* + # Copy build artifacts from the builder stage -# /opt/ros/jazzy is NOT copied — runtime installs the same packages via apt (including foxglove-bridge) +# /opt/ros/jazzy is NOT copied — runtime installs the same packages via apt # /usr/local/lib/python3.12 is NOT copied separately — it is covered by /usr/local/lib below # /usr/local/include is copied to provide OpenVDB (and DDS Router) headers for in-container colcon builds 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/custom_rosdep.yaml b/robot/docker/custom_rosdep.yaml deleted file mode 100644 index 27453029d..000000000 --- a/robot/docker/custom_rosdep.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# ignore openvdb because on ubuntu 22, it builds openvdb8.2.1 which is incompatible with tbb2021.5. we custom build instead -openvdb: {ubuntu: []} diff --git a/robot/docker/docker-compose.yaml b/robot/docker/docker-compose.yaml index cc2bbb6c2..86cd7c573 100644 --- a/robot/docker/docker-compose.yaml +++ b/robot/docker/docker-compose.yaml @@ -30,11 +30,12 @@ services: - AUTOLAUNCH=${AUTOLAUNCH:-true} - NVIDIA_DRIVER_CAPABILITIES=all - LAUNCH_PACKAGE=desktop_bringup # desktop_bringup adds RViz; real robots use autonomy_bringup - - AUTONOMY_ROLE=full - SIM_IP=${SIM_IP:-172.31.0.200} - - LAUNCH_NATNET=${LAUNCH_NATNET:-false} # FCU_URL and TGT_SYSTEM not set, dynamically calculated in interface.launch.py - # 'command' uses variables so that it can be shared across robot-desktop and robot-l4t, with different launch packages and roles. + # 'command' uses variables so that it can be shared across robot-desktop and robot-l4t, with + # different launch packages. The stack to launch comes from AIRSTACK_STACK_DIR / + # AIRSTACK_STACK_ENTRY (robot-base defaults: stacks/full_default) — stacks are the only + # dispatch (the legacy AUTONOMY_ROLE role arg was removed). command: > bash -c " if [ -z \"$$DISPLAY\" ] && command -v Xvfb >/dev/null 2>&1; then @@ -45,7 +46,7 @@ services: service ssh restart; tmux new -d -s bringup; if [ $$AUTOLAUNCH == 'true' ]; then - tmux send-keys -t bringup:0.0 'bws && sws && ros2 launch $$LAUNCH_PACKAGE robot.launch.xml role:=$$AUTONOMY_ROLE' ENTER; + tmux send-keys -t bringup:0.0 'autolaunch $$LAUNCH_PACKAGE robot.launch.xml' ENTER; fi; sleep infinity" # assumes you're connected to work internet, so creates a network to isolate from other developers on your work internet @@ -55,7 +56,6 @@ services: ports: # for ssh, starting from 2223-2243 on the host port all map to 22 in the container. Assumes no more than 21 robots - "2223-2243:22" - - "8767-8787:8765" # for Foxglove remote display (range supports up to 21 robots; GCS uses 8765) # for multiple robots deploy: replicas: ${NUM_ROBOTS:-1} @@ -68,8 +68,9 @@ services: # =================================================================================================================== # desktop_split: simulates onboard computer on desktop for debugging the split configuration. - # Same image as robot-desktop; role=onboard means only lite autonomy modules launch. - # robot-offboard (below) runs concurrently on the same machine with role=offboard. + # Same image as robot-desktop; the lite_default stack launches only the lite autonomy modules + # (interface, sensors, perception, local, behavior — no global/logging). + # robot-offboard (below) runs concurrently on the same machine with the split stack's offboard half. robot-desktop-onboard: profiles: !override - desktop_split @@ -77,7 +78,7 @@ services: file: ./docker-compose.yaml service: robot-desktop environment: - - AUTONOMY_ROLE=onboard + - AIRSTACK_STACK_DIR=/root/AirStack/stacks/lite_default - LAUNCH_PACKAGE=desktop_bringup # for no RViz, change to autonomy_bringup to treat as simulated onboard computer # =================================================================================================================== @@ -105,9 +106,16 @@ services: ports: !reset {} # offboard containers don't need ssh ports environment: - ROBOT_NAME_SOURCE=container_name # robot_name_map resolves replica index → robot_N - - ROS_DOMAIN_ID=0 # all offboard containers share domain 0; domain_bridge connects to per-robot onboard domains + - ROS_DOMAIN_ID=0 # all offboard containers share domain 0; the split stack's DDS router connects to per-robot onboard domains - LAUNCH_PACKAGE=autonomy_bringup # no RViz per-robot; visualization lives in the GCS container - - AUTONOMY_ROLE=offboard # runs global planner only + # Ground half of the split stack: global planning only (RFC #380 §2). + # NOTE: the paired vehicle half (lite_offload_global:onboard) loads the + # DDS-router config GENERATED from the stack's bridge.yaml — run + # `airstack fleet generate ` or + # `python3 tools/gen_dds_router.py stacks/lite_offload_global/bridge.yaml` + # before bringing this profile up. + - AIRSTACK_STACK_DIR=/root/AirStack/stacks/lite_offload_global + - AIRSTACK_STACK_ENTRY=offboard # inherit deploy.replicas from robot-desktop (NUM_ROBOTS) # inherit bridge network from robot-desktop @@ -125,8 +133,6 @@ services: args: BASE_IMAGE: ubuntu:24.04 REAL_ROBOT: true - SKIP_MACVO: true - SKIP_TENSORRT: true TARGET_ARCH: aarch64 ROS_DISTRO: jazzy tags: @@ -135,17 +141,18 @@ services: cache_from: - *voxl_image - *voxl_cache - environment: + environment: - ROBOT_NAME_SOURCE=hostname # see .bashrc - AUTOLAUNCH=${AUTOLAUNCH:-true} - LAUNCH_PACKAGE=autonomy_bringup - - AUTONOMY_ROLE=onboard # VOXL is always lite-only; never runs global planning - - LAUNCH_NATNET=${LAUNCH_NATNET:-false} + # VOXL is compute-constrained: default to the lite stack (no global + # planning/logging). Hardware default — redefine at will via env/--env-file. + - AIRSTACK_STACK_DIR=${AIRSTACK_STACK_DIR:-/root/AirStack/stacks/lite_default} command: > bash -c " tmux new -d -s bringup; if [ $$AUTOLAUNCH == 'true' ]; then - tmux send-keys -t bringup 'bws && sws && ros2 launch $$LAUNCH_PACKAGE robot.launch.xml role:=$$AUTONOMY_ROLE sim:=false' ENTER; + tmux send-keys -t bringup 'autolaunch $$LAUNCH_PACKAGE robot.launch.xml sim:=false' ENTER; fi; sleep infinity" network_mode: host @@ -153,7 +160,7 @@ services: # =================================================================================================================== # Intermediate Jetson stack: dusty Jazzy + ROS keyring / pip / OpenCV shim for Dockerfile.robot (same recipe as Ubuntu). - # `airstack image-build --profile l4t robot-l4t` builds this first automatically; raw `compose build robot-l4t` may parallelize incorrectly. + # `airstack images build --profile l4t robot-l4t` builds this first automatically; raw `compose build robot-l4t` may parallelize incorrectly. robot-l4t-stack-base: profiles: - l4t @@ -187,8 +194,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: @@ -206,7 +211,7 @@ services: service ssh restart; tmux new -d -s bringup; if [ $$AUTOLAUNCH == 'true' ]; then - tmux send-keys -t bringup 'bws && sws && ros2 launch $$LAUNCH_PACKAGE robot.launch.xml role:=$$AUTONOMY_ROLE sim:=false' ENTER; + tmux send-keys -t bringup 'autolaunch $$LAUNCH_PACKAGE robot.launch.xml sim:=false' ENTER; fi; sleep infinity" deploy: @@ -221,8 +226,9 @@ services: - ROBOT_NAME_SOURCE=hostname - AUTOLAUNCH=${AUTOLAUNCH:-true} - LAUNCH_PACKAGE=autonomy_bringup - - AUTONOMY_ROLE=${AUTONOMY_ROLE:-full} - - LAUNCH_NATNET=${LAUNCH_NATNET:-false} + # Autonomous Jetson runs the full stack by default. Hardware default — + # redefine at will via env/--env-file (e.g. a lite stack for weak Jetsons). + - AIRSTACK_STACK_DIR=${AIRSTACK_STACK_DIR:-/root/AirStack/stacks/full_default} # mavros mavlink settings - FCU_URL=${FCU_URL:-/dev/ttyTHS4:115200} - TGT_SYSTEM=1 @@ -241,7 +247,9 @@ services: file: ./docker-compose.yaml service: robot-l4t environment: - - AUTONOMY_ROLE=onboard + # Hardware default — redefine at will (e.g. lite_offload_global:onboard + # when pairing with a ground host running the offboard half). + - AIRSTACK_STACK_DIR=${AIRSTACK_STACK_DIR:-/root/AirStack/stacks/lite_default} # ----------------------- # for running the zed camera driver on an NVIDIA jetson (linux for tegra) device. This ONLY runs the zed driver @@ -265,7 +273,7 @@ services: - *zed_l4t_image - *zed_l4t_cache command: > - bash -c "ssh service restart; + bash -c "service ssh restart; tmux new -d -s zed_driver && tmux send-keys -t zed_driver 'bws && sws && ros2 launch zed_wrapper zed_dual_camera.launch.py pose_cam_serial:='41591402' wire_cam_serial:='44405253' camera_name:=\"robot_1/sensors\" node_name:=\"front_stereo\" ' ENTER @@ -312,8 +320,8 @@ services: bash -il -c " echo 'Building and sourcing workspace...'; bws &> /dev/null && sws &> /dev/null && - echo 'Starting tests for packages: takeoff_landing_planner'; - colcon test --packages-select takeoff_landing_planner --event-handlers=console_direct+; + echo 'Starting colcon tests for the workspace'; + colcon test --event-handlers=console_direct+; TEST_EXIT_CODE=$$?; echo \"Tests completed with exit code: $$TEST_EXIT_CODE\"; exit $$TEST_EXIT_CODE" diff --git a/robot/docker/robot-base-docker-compose.yaml b/robot/docker/robot-base-docker-compose.yaml index 793cf6cab..5c98a74ca 100644 --- a/robot/docker/robot-base-docker-compose.yaml +++ b/robot/docker/robot-base-docker-compose.yaml @@ -14,22 +14,30 @@ services: - RECORD_BAGS=${RECORD_BAGS} - LOG_CONFIG=${LOG_CONFIG:-log.yaml} # docker compose interpolation to env variables - - AUTONOMY_ROLE=${AUTONOMY_ROLE:-full} - - URDF_FILE=${URDF_FILE} + - URDF_FILE=${URDF_FILE} # MAVROS - OFFBOARD_BASE_PORT=${OFFBOARD_BASE_PORT} - ONBOARD_BASE_PORT=${ONBOARD_BASE_PORT} - ROBOT_NAME_MAP_CONFIG_FILE=${ROBOT_NAME_MAP_CONFIG_FILE:-default_robot_name_map.yaml} - DEBUG_RVIZ=${DEBUG_RVIZ:-false} - # OptiTrack / NatNet - - NATNET_SERVER_IP=${NATNET_SERVER_IP:-172.31.0.200} + # Stack dispatch (RFC #379) — the ONLY dispatch (the legacy + # AUTONOMY_ROLE role dispatch was removed). Set by `airstack up + # --stack ` to the CONTAINER path of the stack folder + # (/root/AirStack/stacks/); unset = the trunk reference stack + # full_default. + - AIRSTACK_STACK_DIR=${AIRSTACK_STACK_DIR:-/root/AirStack/stacks/full_default} + - AIRSTACK_STACK_ENTRY=${AIRSTACK_STACK_ENTRY:-stack} + # Fleet dispatch (RFC #380): set by `airstack up --fleet ` to the + # CONTAINER path of the fleet file (/root/AirStack/config/fleets/...). + # Empty = legacy robot_name_map resolution in .bashrc. + - FLEET_CONFIG_FILE=${FLEET_CONFIG_FILE:-} volumes: # display stuff - - $HOME/.Xauthority:/.Xauthority + - $HOME/.Xauthority:/root/.Xauthority - /tmp/.X11-unix:/tmp/.X11-unix # developer stuff - .dev:/root/.dev:rw # developer config - - ../../common/.bash_profile:/root/.bash_profile:rw # commented out - common/ no longer exists + - ../../common/.bash_profile:/root/.bash_profile:rw - .bashrc:/root/.bashrc:rw # bash config - ../../common/inputrc:/etc/inputrc:rw - ../../common/.tmux.conf:/root/.tmux.conf:rw @@ -40,6 +48,15 @@ services: - ../../common/ros_packages:/root/AirStack/robot/ros_ws/src/common:rw - ../../common/fastdds.xml:/root/AirStack/robot/ros_ws/src/fastdds.xml - ../ros_ws:/root/AirStack/robot/ros_ws:rw + # reference stack folders (launch entry points read via AIRSTACK_STACK_DIR) + - ../../stacks:/root/AirStack/stacks:rw + # fleet + vehicle configs and the fleet resolver (.bashrc uses them only + # when FLEET_CONFIG_FILE is set — RFC #380) + - ../../config:/root/AirStack/config:ro + - ../../tools/fleet:/root/AirStack/tools/fleet:ro + # Generated artifacts split-stack entries read at launch (bridge-derived + # DDS-router configs from tools/gen_dds_router.py). + - ../../.airstack/generated:/root/AirStack/.airstack/generated:ro # bags - ../bags:/bags:rw diff --git a/robot/docker/robot_name_map/default_robot_name_map.yaml b/robot/docker/robot_name_map/default_robot_name_map.yaml index 5d638b6ed..8cbd53fff 100644 --- a/robot/docker/robot_name_map/default_robot_name_map.yaml +++ b/robot/docker/robot_name_map/default_robot_name_map.yaml @@ -6,7 +6,7 @@ # More specific rules should be higher up. The last rule is a catch-all for anything that doesn't match above. mappings: # extract the number from any input that contains "robot-" followed by a number, and use that number in the robot name and domain ID - - pattern: '.*robot-.*(\d+)' + - pattern: '.*robot-\D*(\d+)' robot: 'robot_{1}' domain_id: '{1}' diff --git a/robot/docker/wait_for_px4.py b/robot/docker/wait_for_px4.py deleted file mode 100755 index 764f0fd94..000000000 --- a/robot/docker/wait_for_px4.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -""" -wait_for_px4.py - Wait until PX4 MAVLink backend is actually sending heartbeats. -Uses pymavlink to bind to the correct local UDP port and block until heartbeat. -""" - -import sys -import re -import argparse -from pymavlink import mavutil -import time - -def get_robot_ports(robot_name: str) -> tuple[int, int]: - """ - Return (local_port, remote_port) for MAVLink UDP connection. - Example: robot_1 -> local=14540, remote=14580 - """ - match = re.match(r'robot_(\d+)', robot_name) - if match: - robot_num = int(match.group(1)) - local_port = 14540 + (robot_num - 1) # MAVROS local port - remote_port = 14580 + (robot_num - 1) # PX4 remote port - return local_port, remote_port - return 14540, 14580 - -def wait_for_px4(robot_name: str, timeout: int = 120) -> bool: - """Wait for a MAVLink heartbeat from PX4 on the real MAVROS port.""" - local_port, remote_port = get_robot_ports(robot_name) - bind_addr = f"udp:0.0.0.0:{local_port}" # bind to MAVROS port - - print(f"[INFO] Waiting for PX4 heartbeat for {robot_name}") - print(f"[INFO] Listening on local port {local_port}, sending heartbeat to PX4 remote port {remote_port}") - print(f"[INFO] Timeout: {timeout} seconds") - - try: - mav = mavutil.mavlink_connection(bind_addr, dialect='common') - except Exception as e: - print(f"[ERROR] Could not bind to {bind_addr}: {e}") - return False - - # Send a heartbeat from the local port to PX4 to trigger responses - mav.mav.heartbeat_send( - mavutil.mavlink.MAV_TYPE_GCS, - mavutil.mavlink.MAV_AUTOPILOT_INVALID, - 0, 0, 0 - ) - - start_time = time.time() - while True: - elapsed = int(time.time() - start_time) - if elapsed >= timeout: - print(f"[ERROR] Timeout: No heartbeat received from PX4 after {timeout} seconds.") - return False - - try: - hb = mav.recv_match(type='HEARTBEAT', blocking=True, timeout=1) - if hb: - print(f"[OK] Heartbeat received from PX4! " - f"System ID: {hb.get_srcSystem()}, Component ID: {hb.get_srcComponent()}") - mav.close() # Release the port for MAVROS - return True - except Exception: - pass - - print(f"[WAIT] No heartbeat yet... ({elapsed}/{timeout} seconds)") - - - -def main(): - parser = argparse.ArgumentParser(description="Wait for PX4 MAVLink heartbeat") - parser.add_argument("robot_name", nargs="?", default="robot_1", - help="Robot name (e.g., robot_1, robot_2)") - parser.add_argument("timeout", nargs="?", type=int, default=120, - help="Timeout in seconds") - - args = parser.parse_args() - - success = wait_for_px4(args.robot_name, args.timeout) - sys.exit(0 if success else 1) - - -if __name__ == "__main__": - main() diff --git a/robot/ros_ws/src/autonomy_bringup/CMakeLists.txt b/robot/ros_ws/src/autonomy_bringup/CMakeLists.txt index b8335e4a4..455e2251a 100644 --- a/robot/ros_ws/src/autonomy_bringup/CMakeLists.txt +++ b/robot/ros_ws/src/autonomy_bringup/CMakeLists.txt @@ -14,9 +14,10 @@ if(BUILD_TESTING) ament_lint_auto_find_test_dependencies() endif() +# config/ carries the shared DDS-router YAML (moved up from the removed +# AUTONOMY_ROLE role folders onboard_all/ and onboard_local_offboard_global/ +# — stacks select it directly). install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) -install(DIRECTORY onboard_all DESTINATION share/${PROJECT_NAME}) -install(DIRECTORY onboard_local_offboard_global DESTINATION share/${PROJECT_NAME}) ament_package() diff --git a/robot/ros_ws/src/autonomy_bringup/config/dds_router.yaml b/robot/ros_ws/src/autonomy_bringup/config/dds_router.yaml new file mode 100644 index 000000000..1143b8b84 --- /dev/null +++ b/robot/ros_ws/src/autonomy_bringup/config/dds_router.yaml @@ -0,0 +1,68 @@ +# note that all ROS2 topics must be prefixed: +# rt/ (ROS Topic): Prefixed to all standard ROS 2 topics. +# rq/Request (ROS Service Request): Prefixed to the request topic of a ROS 2 service. +# rr/Reply (ROS Service Reply): Prefixed to the reply topic of a ROS 2 service. +# rs/ (ROS Service): Reserved for systems where services are handled as a single entity rather than separate request/reply topics. +# [action_topic]/_action/status: A topic for goal status updates. +# [action_topic]/_action/feedback: A topic for feedback during execution. +# [action_topic]/_action/send_goal: A service to initiate the action. +# [action_topic]/_action/get_result: A service to retrieve the final result. +# [action_topic]/_action/cancel_goal: A service to cancel an active goal. + +# all topics are bidirectional by default. See https://eprosima-dds-router.readthedocs.io/ for more details + +# Shared robot <-> GCS DDS Router allowlist (autonomy_bringup/config/). +# The full_* stacks and lite_default point their interpolate_dds_router +# include here; the lite_offload_global split stack instead GENERATES its +# router config from its bridge.yaml (tools/gen_dds_router.py). +participants: + - name: "robot" + kind: "local" + domain: $(env ROS_DOMAIN_ID) + - name: "gcs" + kind: "local" + domain: $(var gcs_domain) +allowlist: + # lidar and world model + - name: "rt/$(env ROBOT_NAME)/sensors/ouster/point_cloud" + - name: "rt/$(env ROBOT_NAME)/vdb_mapping/vdb_map_visualization" + + # camera streams + - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/left/image_rect" + - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/left/camera_info" + - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/right/image_rect" + - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/right/camera_info" + - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/right/depth_ground_truth" + - name: "rt/$(env ROBOT_NAME)/perception/stereo_image_proc/point_cloud" + + # state information + - name: "rt/$(env ROBOT_NAME)/odometry_conversion/odometry" + - name: "rt/$(env ROBOT_NAME)/interface/mavros/global_position/global" + - name: "rt/$(env ROBOT_NAME)/trajectory_controller/trajectory_vis" + - name: "rt/$(env ROBOT_NAME)/global_plan" + + # # allow all services + # - name: "rq/*" + # type: "*" + # - name: "rr/*" + # type: "*" + + # behavior tree services + - name: "rq/$(env ROBOT_NAME)/interface/robot_commandRequest" + - name: "rr/$(env ROBOT_NAME)/interface/robot_commandReply" + + - name: "rq/$(env ROBOT_NAME)/trajectory_controller/set_trajectory_modeRequest" + - name: "rr/$(env ROBOT_NAME)/trajectory_controller/set_trajectory_modeReply" + + - name: "rq/$(env ROBOT_NAME)/takeoff_landing_planner/set_takeoff_landing_commandRequest" + - name: "rr/$(env ROBOT_NAME)/takeoff_landing_planner/set_takeoff_landing_commandReply" + + - name: "rq/$(env ROBOT_NAME)/behavior/global_plan_toggleRequest" + - name: "rr/$(env ROBOT_NAME)/behavior/global_plan_toggleReply" + + # bag recording status + - name: "rt/$(env ROBOT_NAME)/bag_record/bag_recording_status" + - name: "rt/$(env ROBOT_NAME)/bag_record/set_recording_status" + + # gossip peer profiles are bridged by the dedicated gossip_dds_router (domain 99), + # NOT here — having it in both routers causes message flooding/amplification. diff --git a/robot/ros_ws/src/autonomy_bringup/config/mavros_config.yaml b/robot/ros_ws/src/autonomy_bringup/config/mavros_config.yaml deleted file mode 100644 index 7d7ef2e77..000000000 --- a/robot/ros_ws/src/autonomy_bringup/config/mavros_config.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# MAVROS Configuration for AirStack -# Ensures proper time synchronization and connection settings for simulation - -/**: - ros__parameters: - # Use simulation time for proper synchronization - use_sim_time: true - - # Connection settings optimized for simulation - conn: - timeout: 10.0 # Increased timeout for slower simulation - heartbeat_rate: 2.0 # Reduced heartbeat rate to prevent overwhelming slow simulation - - # System plugin settings - sys: - min_voltage: 10.0 # Lower voltage threshold for simulation - disable_diag: false # Keep diagnostics enabled - - # Time sync settings for simulation - time: - time_ref_source: "fcu" # Use FCU time reference - timesync_rate: 10.0 # Reduced rate for simulation - timesync_avg_alpha: 0.6 # Time sync averaging - - # Setpoint rate limiting for simulation stability - setpoint_rate: - attitude: 20.0 # Reduced from default 50Hz - position: 10.0 # Reduced from default 20Hz - velocity: 10.0 # Reduced from default 20Hz 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..45ef95290 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 @@ -20,7 +20,7 @@ extending file. The 'extends:' key itself is stripped from the final merged config. Example: - extends: "$(find-pkg-share my_pkg)/config/base_router.yaml" + extends: "$(find-pkg-share autonomy_bringup)/config/dds_router.yaml" participants: - name: "extra" kind: "local" @@ -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 + $(find-pkg-share autonomy_bringup)/config/dds_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 deleted file mode 100644 index 96b644498..000000000 --- a/robot/ros_ws/src/autonomy_bringup/launch/interpolate_domain_bridge.launch.py +++ /dev/null @@ -1,286 +0,0 @@ -""" -interpolate_domain_bridge.launch.py -==================================== -Launches a domain_bridge node using a YAML config file that supports variable -interpolation and config inheritance before being passed to the node. - -Supported substitution syntax (mirrors ROS 2 launch XML syntax): - $(env VAR_NAME) – replaced with the value of the environment variable VAR_NAME. - Raises RuntimeError if the variable is not set. - $(var VAR_NAME) – replaced with the value supplied via the 'args' launch argument. - Raises RuntimeError if the variable was not provided. - $(find-pkg-share PKG_NAME) – replaced with the share directory path of the given ROS 2 package. - Raises RuntimeError if the package is not found. - -Config inheritance via 'extends:': - A YAML config file may contain a top-level 'extends:' key whose value is a path to - another YAML config file (typically using $(find-pkg-share) to locate it). The base - file is loaded first (recursively, so chains are supported), and then the keys in the - extending file are deep-merged on top of it. Colliding keys are overwritten by the - extending file. The 'extends:' key itself is stripped from the final merged config. - - Example: - extends: "$(find-pkg-share my_pkg)/config/base_bridge.yaml" - topics: - extra_topic: - type: std_msgs/msg/String - from_domain: 0 - to_domain: 1 - -Merge-control YAML tags: - !override – applied to a list or dict value in the extending file; the tagged - value completely replaces the corresponding base value instead of - being appended/merged. Example: - topics: !override - my_topic: - type: std_msgs/msg/String - from_domain: 0 - to_domain: 1 - !reset – applied to any value; removes the key from the merged result - entirely. Example: - topics: !reset - -Launch arguments: - 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: "") - Space-separated key:=value pairs that resolve $(var key) tokens in the - config file, e.g.: "gcs_domain:=0 robot_domain:=5" - -Example (XML caller): - - - - -""" - -import os -import re -import tempfile - -import yaml -from ament_index_python.packages import get_package_share_directory - -from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, LogInfo, OpaqueFunction -from launch.substitutions import LaunchConfiguration -from launch_ros.actions import Node - - -# ── Merge-control sentinel types ───────────────────────────────────────────── - -class _OverrideValue: - """Wraps a value tagged with !override; replaces the base value outright.""" - __slots__ = ('value',) - - def __init__(self, value): - self.value = value - - -class _ResetValue: - """Sentinel for !reset; causes the key to be removed from the merged result.""" - __slots__ = () - - -def _make_loader(): - """Return a yaml.SafeLoader subclass that recognises !override and !reset tags.""" - class _Loader(yaml.SafeLoader): - pass - - def _override_ctor(loader, node): - if isinstance(node, yaml.SequenceNode): - value = loader.construct_sequence(node, deep=True) - elif isinstance(node, yaml.MappingNode): - value = loader.construct_mapping(node, deep=True) - else: - value = loader.construct_scalar(node) - return _OverrideValue(value) - - def _reset_ctor(loader, node): - return _ResetValue() - - _Loader.add_constructor('!override', _override_ctor) - _Loader.add_constructor('!reset', _reset_ctor) - return _Loader - - -def _normalize(data): - """Unwrap/strip any leftover _OverrideValue/_ResetValue markers from the tree.""" - if isinstance(data, _OverrideValue): - return _normalize(data.value) - if isinstance(data, _ResetValue): - return None - if isinstance(data, dict): - return { - k: _normalize(v) - for k, v in data.items() - if not isinstance(v, _ResetValue) - } - if isinstance(data, list): - return [_normalize(item) for item in data if not isinstance(item, _ResetValue)] - return data - - -# ───────────────────────────────────────────────────────────────────────────── - - -def _resolve_find_pkg_share(content): - """Replace all $(find-pkg-share PKG) tokens with the package's share directory path.""" - def replace_find_pkg(match): - pkg_name = match.group(1) - try: - return get_package_share_directory(pkg_name) - except Exception: - raise RuntimeError( - f"interpolate_domain_bridge: ROS 2 package '{pkg_name}' not found" - ) - return re.sub(r'\$\(find-pkg-share\s+([\w-]+)\)', replace_find_pkg, content) - - -def _deep_merge(base, override): - """Return a new dict that is *override* deep-merged on top of *base*. - - Merge rules: - - Value is _ResetValue → remove the key from the result entirely. - - Value is _OverrideValue → replace the base value outright (no merge). - - Both values are dicts → merge recursively (override wins on collisions). - - Both values are lists → concatenate (base entries first, then override - entries that are not already present in base). - - All other cases → override value wins outright. - """ - result = base.copy() - for key, value in override.items(): - if isinstance(value, _ResetValue): - result.pop(key, None) - elif isinstance(value, _OverrideValue): - result[key] = value.value - elif key in result and isinstance(result[key], dict) and isinstance(value, dict): - result[key] = _deep_merge(result[key], value) - elif key in result and isinstance(result[key], list) and isinstance(value, list): - # Append override entries that don't already exist in the base list - merged = list(result[key]) - for item in value: - if item not in merged: - merged.append(item) - result[key] = merged - else: - result[key] = value - return result - - -def _load_and_merge_config(config_file): - """Load a domain bridge YAML config, recursively resolving 'extends:' chains. - - Processing order for each file: - 1. Read raw content. - 2. Resolve $(find-pkg-share) tokens so that 'extends:' paths can be opened. - 3. Parse YAML. - 4. If an 'extends:' key is present, load the base file recursively and - deep-merge the current file's keys on top of the base (current wins). - 5. Return the merged dict (without the 'extends:' key). - - Note: $(env) and $(var) tokens are intentionally left unresolved here so - that they can be substituted as a final step after all files are merged. - """ - with open(config_file, 'r') as f: - content = f.read() - - # Resolve $(find-pkg-share) so paths in 'extends:' can be followed - content = _resolve_find_pkg_share(content) - - data = yaml.load(content, Loader=_make_loader()) - if data is None: - data = {} - - if 'extends' in data: - base_file = data.pop('extends') - base_data = _load_and_merge_config(base_file) - data = _deep_merge(base_data, data) - - return _normalize(data) - - -def launch_domain_bridge(context, *args, **kwargs): - config_file = LaunchConfiguration('config_file').perform(context) - args_str = LaunchConfiguration('args').perform(context) - - # Parse args string: space-separated "key:=value" pairs, e.g. "gcs_domain:=0 foo:=bar" - variables = {} - if args_str.strip(): - for token in args_str.strip().split(): - if ':=' in token: - key, value = token.split(':=', 1) - variables[key.strip()] = value.strip() - - # Load, inherit (extends:), and deep-merge all config files into one dict - merged_data = _load_and_merge_config(config_file) - - # Serialize back to a YAML string so token substitution can be applied uniformly - content = yaml.dump(merged_data, default_flow_style=False, allow_unicode=True) - - # Interpolate $(env VAR_NAME) - def replace_env(match): - var_name = match.group(1) - value = os.environ.get(var_name) - if value is None: - raise RuntimeError( - f"interpolate_domain_bridge: environment variable '{var_name}' is not set" - ) - return value - - content = re.sub(r'\$\(env\s+([\w]+)\)', replace_env, content) - - # Interpolate $(var VAR_NAME) - def replace_var(match): - var_name = match.group(1) - if var_name not in variables: - raise RuntimeError( - f"interpolate_domain_bridge: variable '{var_name}' was not provided in 'args'" - ) - return variables[var_name] - - content = re.sub(r'\$\(var\s+([\w]+)\)', replace_var, content) - - # Write interpolated content to a temporary file so domain_bridge can read it - tmp = tempfile.NamedTemporaryFile( - mode='w', suffix='.yaml', delete=False, prefix='domain_bridge_' - ) - tmp.write(content) - tmp.close() - - return [ - LogInfo(msg=f"[domain_bridge] Final interpolated config:\n{content}"), - Node( - package='domain_bridge', - executable='domain_bridge', - arguments=[tmp.name], - output='screen', - respawn=True, - respawn_delay=1.0, - ) - ] - - -def generate_launch_description(): - return LaunchDescription([ - DeclareLaunchArgument( - 'config_file', - 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.' - ), - ), - DeclareLaunchArgument( - '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".' - ), - ), - 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 0a30ea3f0..fc250b5a7 100644 --- a/robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml +++ b/robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml @@ -1,17 +1,41 @@ - - - - - + + + + @@ -30,51 +54,7 @@ lite, desktop_split sim) name="world_to_map_broadcaster" args="0 0 0 0 0 0 world map" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - \ No newline at end of file + diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml b/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml deleted file mode 100644 index 8a4e1a379..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router.yaml +++ /dev/null @@ -1,65 +0,0 @@ -# note that all ROS2 topics must be prefixed: -# rt/ (ROS Topic): Prefixed to all standard ROS 2 topics. -# rq/Request (ROS Service Request): Prefixed to the request topic of a ROS 2 service. -# rr/Reply (ROS Service Reply): Prefixed to the reply topic of a ROS 2 service. -# rs/ (ROS Service): Reserved for systems where services are handled as a single entity rather than separate request/reply topics. -# [action_topic]/_action/status: A topic for goal status updates. -# [action_topic]/_action/feedback: A topic for feedback during execution. -# [action_topic]/_action/send_goal: A service to initiate the action. -# [action_topic]/_action/get_result: A service to retrieve the final result. -# [action_topic]/_action/cancel_goal: A service to cancel an active goal. - -# all topics are bidirectional by default. See https://eprosima-dds-router.readthedocs.io/ for more details - -# onboard_all DDS Router -participants: - - name: "robot" - kind: "local" - domain: $(env ROS_DOMAIN_ID) - - name: "gcs" - kind: "local" - domain: $(var gcs_domain) -allowlist: - # lidar and world model - - name: "rt/$(env ROBOT_NAME)/sensors/ouster/point_cloud" - - name: "rt/$(env ROBOT_NAME)/vdb_mapping/vdb_map_visualization" - - # camera streams - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/left/image_rect" - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/left/camera_info" - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/right/image_rect" - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/right/camera_info" - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/right/depth_ground_truth" - - name: "rt/$(env ROBOT_NAME)/perception/stereo_image_proc/point_cloud" - - # state information - - name: "rt/$(env ROBOT_NAME)/odometry_conversion/odometry" - - name: "rt/$(env ROBOT_NAME)/interface/mavros/global_position/global" - - name: "rt/$(env ROBOT_NAME)/trajectory_controller/trajectory_vis" - - name: "rt/$(env ROBOT_NAME)/global_plan" - - # # allow all services - # - name: "rq/*" - # type: "*" - # - name: "rr/*" - # type: "*" - - # behavior tree services - - name: "rq/$(env ROBOT_NAME)/interface/robot_commandRequest" - - name: "rr/$(env ROBOT_NAME)/interface/robot_commandReply" - - - name: "rq/$(env ROBOT_NAME)/trajectory_controller/set_trajectory_modeRequest" - - name: "rr/$(env ROBOT_NAME)/trajectory_controller/set_trajectory_modeReply" - - - name: "rq/$(env ROBOT_NAME)/takeoff_landing_planner/set_takeoff_landing_commandRequest" - - name: "rr/$(env ROBOT_NAME)/takeoff_landing_planner/set_takeoff_landing_commandReply" - - - name: "rq/$(env ROBOT_NAME)/behavior/global_plan_toggleRequest" - - name: "rr/$(env ROBOT_NAME)/behavior/global_plan_toggleReply" - - # bag recording status - - name: "rt/$(env ROBOT_NAME)/bag_record/bag_recording_status" - - name: "rt/$(env ROBOT_NAME)/bag_record/set_recording_status" - - # gossip peer profiles are bridged by the dedicated gossip_dds_router (domain 99), - # NOT here — having it in both routers causes message flooding/amplification. diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router_echo.yaml b/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router_echo.yaml deleted file mode 100644 index 517a6a955..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router_echo.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# this is good for debugging -################################## -# PARTICIPANTS -participants: - -################################## -# SIMPLE PARTICIPANT -# This participant will subscribe to topics in allowlist in specific domain and listen every message published there - - - name: SimpleParticipant # 3 - kind: local # 4 - domain: 1 # 5 - -################################## -# ECHO PARTICIPANT -# This Participant will print in stdout every message received by the other Participants, as well as discovery information - - - name: EchoParticipant # 6 - kind: echo # 7 - discovery: true # 8 - data: true # 9 - verbose: true # 10 - -allowlist: - - name: "*" diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router_vanilla.yaml b/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router_vanilla.yaml deleted file mode 100644 index 37b2613d4..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_all/config/dds_router_vanilla.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# dds_router.yaml -# Define which domains to connect -participants: - - name: "robot" - kind: "local" - domain: 0 - - name: "gcs" - kind: "local" - domain: 1 - -# Define specific services to bridge -allowlist: - # To bridge a specific service named "/add_two_ints" - - name: "rq/add_two_intsRequest" - - name: "rr/add_two_intsReply" - - # You can also use wildcards to bridge ALL services - - name: "rq/*" - - name: "rr/*" \ No newline at end of file diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_all/config/domain_bridge.yaml b/robot/ros_ws/src/autonomy_bringup/onboard_all/config/domain_bridge.yaml deleted file mode 100644 index 34e7e1d26..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_all/config/domain_bridge.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: onboard_all_bridge -topics: - # ============= Outgoing from Robot ================ - # state information - odometry_conversion/odometry: - type: nav_msgs/msg/Odometry - from_domain: $(env ROS_DOMAIN_ID) - to_domain: $(var gcs_domain) - - # ground-truth depth from sim — bridged to GCS for visualization - sensors/front_stereo/right/depth_ground_truth: - type: sensor_msgs/msg/Image - from_domain: $(env ROS_DOMAIN_ID) - to_domain: $(var gcs_domain) - - # camera intrinsics — needed alongside depth for any depth-aware viewer - sensors/front_stereo/right/camera_info: - type: sensor_msgs/msg/CameraInfo - from_domain: $(env ROS_DOMAIN_ID) - to_domain: $(var gcs_domain) - - # local trajectory markers for foxglove_visualizer rendering - trajectory_controller/trajectory_vis: - type: visualization_msgs/msg/MarkerArray - from_domain: $(env ROS_DOMAIN_ID) - to_domain: $(var gcs_domain) - - interface/mavros/global_position/global: - type: sensor_msgs/msg/NavSatFix - from_domain: $(env ROS_DOMAIN_ID) - to_domain: $(var gcs_domain) - - # bag recording status - bag_record/bag_recording_status: - type: std_msgs/msg/Bool - from_domain: $(env ROS_DOMAIN_ID) - to_domain: $(var gcs_domain) - - - # ============= Incoming to Robot ================ - # bag recording status - bag_record/set_recording_status: - type: std_msgs/msg/Bool - from_domain: $(var gcs_domain) - to_domain: $(env ROS_DOMAIN_ID) \ No newline at end of file diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_all/launch/onboard_autonomy_all.launch.xml b/robot/ros_ws/src/autonomy_bringup/onboard_all/launch/onboard_autonomy_all.launch.xml deleted file mode 100644 index f9e9d414d..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_all/launch/onboard_autonomy_all.launch.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_all/launch/static_transforms.launch.xml b/robot/ros_ws/src/autonomy_bringup/onboard_all/launch/static_transforms.launch.xml deleted file mode 100644 index e28cda2ce..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_all/launch/static_transforms.launch.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/config/dds_router.yaml b/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/config/dds_router.yaml deleted file mode 100644 index 221722a85..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/config/dds_router.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# onboard_all DDS Router -extends: "$(find-pkg-share autonomy_bringup)/onboard_all/config/dds_router.yaml" - -allowlist: - # ============= Outgoing Robot --> GCS ================ - - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/left/image_rect" - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/left/camera_info" - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/right/image_rect" - - name: "rt/$(env ROBOT_NAME)/sensors/front_stereo/right/camera_info" - - # ============= Incoming GCS --> Robot ================ - # control commands - - - name: "rt/$(env ROBOT_NAME)/global_plan" \ No newline at end of file diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/config/domain_bridge.yaml b/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/config/domain_bridge.yaml deleted file mode 100644 index 7bf7c15fe..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/config/domain_bridge.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: onboard_offboard_bridge -extends: "$(find-pkg-share autonomy_bringup)/onboard_all/config/domain_bridge.yaml" -topics: - # ============= Additional Outgoing from Robot ================ - - # ============= Additional Incoming to Robot ================ - - # to offboard modules - global_plan: - type: nav_msgs/msg/Path - from_domain: $(var gcs_domain) - to_domain: $(env ROS_DOMAIN_ID) \ No newline at end of file diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/launch/offboard_autonomy_global.launch.xml b/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/launch/offboard_autonomy_global.launch.xml deleted file mode 100644 index 8354b5907..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/launch/offboard_autonomy_global.launch.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/launch/onboard_autonomy_local.launch.xml b/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/launch/onboard_autonomy_local.launch.xml deleted file mode 100644 index 6fd49bd6c..000000000 --- a/robot/ros_ws/src/autonomy_bringup/onboard_local_offboard_global/launch/onboard_autonomy_local.launch.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/robot/ros_ws/src/autonomy_bringup/package.xml b/robot/ros_ws/src/autonomy_bringup/package.xml index c32ee72ba..0d0fbfe81 100644 --- a/robot/ros_ws/src/autonomy_bringup/package.xml +++ b/robot/ros_ws/src/autonomy_bringup/package.xml @@ -3,9 +3,9 @@ autonomy_bringup 0.0.0 - Desktop/simulation platform bringup for AirStack - AirLab - BSD-3-Clause + Top-level robot autonomy bringup whose robot.launch.xml dispatches the stack entry launch file selected by AIRSTACK_STACK_DIR/AIRSTACK_STACK_ENTRY. + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/behavior/behavior_bringup/CMakeLists.txt b/robot/ros_ws/src/behavior/behavior_bringup/CMakeLists.txt deleted file mode 100644 index b62906b63..000000000 --- a/robot/ros_ws/src/behavior/behavior_bringup/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(behavior_bringup) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# find dependencies -find_package(ament_cmake REQUIRED) -# uncomment the following section in order to fill in -# further dependencies manually. -# find_package( REQUIRED) - -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - # the following line skips the linter which checks for copyrights - # comment the line when a copyright and license is added to all source files - set(ament_cmake_copyright_FOUND TRUE) - # the following line skips cpplint (only works in a git repo) - # comment the line when this package is in a git repo and when - # a copyright and license is added to all source files - set(ament_cmake_cpplint_FOUND TRUE) - ament_lint_auto_find_test_dependencies() -endif() - -# Install files. -install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY rviz DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY params DESTINATION share/${PROJECT_NAME}) - -ament_package() diff --git a/robot/ros_ws/src/behavior/behavior_bringup/LICENSE b/robot/ros_ws/src/behavior/behavior_bringup/LICENSE deleted file mode 100644 index d64569567..000000000 --- a/robot/ros_ws/src/behavior/behavior_bringup/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/robot/ros_ws/src/behavior/behavior_bringup/launch/behavior.launch.xml b/robot/ros_ws/src/behavior/behavior_bringup/launch/behavior.launch.xml deleted file mode 100644 index cef093702..000000000 --- a/robot/ros_ws/src/behavior/behavior_bringup/launch/behavior.launch.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/robot/ros_ws/src/behavior/behavior_bringup/package.xml b/robot/ros_ws/src/behavior/behavior_bringup/package.xml deleted file mode 100644 index fbdc2cd6a..000000000 --- a/robot/ros_ws/src/behavior/behavior_bringup/package.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - behavior_bringup - 0.0.0 - TODO: Package description - andrew - Apache-2.0 - - ament_cmake - - ament_lint_auto - ament_lint_common - - - ament_cmake - - 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..b9342d24a --- /dev/null +++ b/robot/ros_ws/src/behavior/drone_safety_monitor/launch/drone_safety_monitor.launch.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/behavior/drone_safety_monitor/package.xml b/robot/ros_ws/src/behavior/drone_safety_monitor/package.xml index 2ac348759..30ea38e6f 100644 --- a/robot/ros_ws/src/behavior/drone_safety_monitor/package.xml +++ b/robot/ros_ws/src/behavior/drone_safety_monitor/package.xml @@ -4,8 +4,8 @@ drone_safety_monitor 0.0.0 Safety monitor: state estimate watchdog and pause/resume/rewind command handling - uav - MIT + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/CHANGELOG.rst b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/CHANGELOG.rst deleted file mode 100644 index 0a789b786..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/CHANGELOG.rst +++ /dev/null @@ -1,149 +0,0 @@ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Changelog for package rqt_py_console -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1.0.2 (2021-08-31) ------------------- -* Fix modern setuptools warning about dashes instead of underscores (`#11 `_) -* Contributors: Chris Lalancette - -1.0.1 (2021-04-27) ------------------- -* Changed the build type to ament_python and fixed package to run with ros2 run (`#8 `_) -* Contributors: Alejandro Hernández Cordero - -1.0.0 (2018-12-11) ------------------- -* spyderlib -> spyder (`#5 `_) -* ros2 port (`#3 `_) -* autopep8 (`#2 `_) -* Contributors: Mike Lautman - -0.4.8 (2017-04-28) ------------------- - -0.4.7 (2017-03-02) ------------------- - -0.4.6 (2017-02-27) ------------------- - -0.4.5 (2017-02-03) ------------------- - -0.4.4 (2017-01-24) ------------------- -* use Python 3 compatible syntax (`#421 `_) - -0.4.3 (2016-11-02) ------------------- - -0.4.2 (2016-09-19) ------------------- - -0.4.1 (2016-05-16) ------------------- - -0.4.0 (2016-04-27) ------------------- -* Support Qt 5 (in Kinetic and higher) as well as Qt 4 (in Jade and earlier) (`#359 `_) - -0.3.13 (2016-03-08) -------------------- - -0.3.12 (2015-07-24) -------------------- - -0.3.11 (2015-04-30) -------------------- - -0.3.10 (2014-10-01) -------------------- -* update plugin scripts to use full name to avoid future naming collisions - -0.3.9 (2014-08-18) ------------------- - -0.3.8 (2014-07-15) ------------------- - -0.3.7 (2014-07-11) ------------------- -* export architecture_independent flag in package.xml (`#254 `_) - -0.3.6 (2014-06-02) ------------------- - -0.3.5 (2014-05-07) ------------------- - -0.3.4 (2014-01-28) ------------------- - -0.3.3 (2014-01-08) ------------------- -* add groups for rqt plugins, renamed some plugins (`#167 `_) - -0.3.2 (2013-10-14) ------------------- - -0.3.1 (2013-10-09) ------------------- - -0.3.0 (2013-08-28) ------------------- - -0.2.17 (2013-07-04) -------------------- - -0.2.16 (2013-04-09 13:33) -------------------------- - -0.2.15 (2013-04-09 00:02) -------------------------- - -0.2.14 (2013-03-14) -------------------- - -0.2.13 (2013-03-11 22:14) -------------------------- - -0.2.12 (2013-03-11 13:56) -------------------------- - -0.2.11 (2013-03-08) -------------------- - -0.2.10 (2013-01-22) -------------------- - -0.2.9 (2013-01-17) ------------------- - -0.2.8 (2013-01-11) ------------------- - -0.2.7 (2012-12-24) ------------------- - -0.2.6 (2012-12-23) ------------------- - -0.2.5 (2012-12-21 19:11) ------------------------- - -0.2.4 (2012-12-21 01:13) ------------------------- - -0.2.3 (2012-12-21 00:24) ------------------------- - -0.2.2 (2012-12-20 18:29) ------------------------- - -0.2.1 (2012-12-20 17:47) ------------------------- - -0.2.0 (2012-12-20 17:39) ------------------------- -* first release of this package into groovy diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/config/gui_config.yaml b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/config/gui_config.yaml deleted file mode 100644 index 8e35fcfe4..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/config/gui_config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -groups: - - Commands: - - Arm and Takeoff: - condition_name: Auto Takeoff Commanded - - Fixed Trajectory: - condition_name: Fixed Trajectory Commanded - - Global Plan: - condition_name: Global Plan Commanded - - Pause: - condition_name: Pause Commanded - - Rewind: - condition_name: Rewind Commanded - - Disarm: - condition_name: Disarm Commanded - - Land: - condition_name: Land Commanded - - Autonomously Explore: - condition_name: Autonomously Explore Commanded \ No newline at end of file diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/package.xml b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/package.xml deleted file mode 100644 index 9b9757655..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/package.xml +++ /dev/null @@ -1,28 +0,0 @@ - - rqt_behavior_tree_command - 1.0.2 - rqt_behavior_tree_command is a Python GUI template. - John Keller - - BSD - - - - - - John Keller - - ament_index_python - python_qt_binding - qt_gui - qt_gui_py_common - rclpy - rqt_gui - rqt_gui_py - - - - - ament_python - - diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/plugin.xml b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/plugin.xml deleted file mode 100644 index 205c204eb..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/plugin.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - A Python GUI plugin providing an interactive Python console. - - - - - folder - Plugins related to miscellaneous tools. - - - applications-python - A Python RQT GUI template. - - - diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/resource/py_console_widget.ui b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/resource/py_console_widget.ui deleted file mode 100644 index 12810f1c5..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/resource/py_console_widget.ui +++ /dev/null @@ -1,53 +0,0 @@ - - - PyConsole - - - - 0 - 0 - 276 - 212 - - - - PyConsole - - - - 0 - - - 0 - - - 0 - - - 3 - - - 0 - - - - - 0 - - - - - - - - - - - PyConsoleTextEdit - QTextEdit -
rqt_py_console.py_console_text_edit
-
-
- - -
diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/resource/rqt_behavior_tree_command b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/resource/rqt_behavior_tree_command deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/setup.cfg b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/setup.cfg deleted file mode 100644 index 08ee1a536..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/rqt_behavior_tree_command -[install] -install_scripts=$base/lib/rqt_behavior_tree_command diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/setup.py b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/setup.py deleted file mode 100644 index 1c95a9b5c..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from setuptools import setup -from glob import glob - -package_name = 'rqt_behavior_tree_command' - -setup( - name=package_name, - version='1.0.2', - packages=[package_name], - package_dir={'': 'src'}, - data_files=[ - ('share/ament_index/resource_index/packages', - ['resource/' + package_name]), - ('share/' + package_name + '/resource', - ['resource/py_console_widget.ui']), - ('share/' + package_name, ['package.xml']), - ('share/' + package_name, ['plugin.xml']), - ('share/' + package_name + '/config/', glob('config/*')), - ], - install_requires=['setuptools'], - zip_safe=True, - author='', - maintainer='', - maintainer_email='', - keywords=['ROS'], - classifiers=[ - '', - '', - '', - '', - ], - description=( - 'rqt_behavior_tree_command' - ), - license='BSD', - entry_points={ - 'console_scripts': [ - 'rqt_behavior_tree_command = ' + package_name + '.main:main', - ], - }, -) diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/__init__.py b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/main.py b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/main.py deleted file mode 100755 index 9a5c9376e..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/main.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys - -from rqt_gui.main import Main - - -def main(): - main = Main() - sys.exit(main.main(sys.argv, standalone='rqt_py_console.py_console.PyConsole')) - - -if __name__ == '__main__': - main() diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/py_console_text_edit.py b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/py_console_text_edit.py deleted file mode 100644 index dc9ce1a0a..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/py_console_text_edit.py +++ /dev/null @@ -1,69 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import sys -from code import InteractiveInterpreter - -from python_qt_binding import QT_BINDING, QT_BINDING_VERSION -from python_qt_binding.QtCore import Qt, Signal - -from qt_gui_py_common.console_text_edit import ConsoleTextEdit - - -class PyConsoleTextEdit(ConsoleTextEdit): - _color_stdin = Qt.darkGreen - _multi_line_char = ':' - _multi_line_indent = ' ' - _prompt = ('>>> ', '... ') # prompt for single and multi line - exit = Signal() - - def __init__(self, parent=None): - super(PyConsoleTextEdit, self).__init__(parent) - - self._interpreter_locals = {} - self._interpreter = InteractiveInterpreter(self._interpreter_locals) - - self._comment_writer.write('Python %s on %s\n' % - (sys.version.replace('\n', ''), sys.platform)) - self._comment_writer.write( - 'Qt bindings: %s version %s\n' % (QT_BINDING, QT_BINDING_VERSION)) - - self._add_prompt() - - def update_interpreter_locals(self, newLocals): - self._interpreter_locals.update(newLocals) - - def _exec_code(self, code): - try: - self._interpreter.runsource(code) - except SystemExit: # catch sys.exit() calls, so they don't close the whole gui - self.exit.emit() diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/py_console_widget.py b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/py_console_widget.py deleted file mode 100644 index e69bde34c..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/py_console_widget.py +++ /dev/null @@ -1,59 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import os -from ament_index_python.resources import get_resource - -from python_qt_binding import loadUi -from python_qt_binding.QtWidgets import QWidget -from rqt_py_console.py_console_text_edit import PyConsoleTextEdit - - -class PyConsoleWidget(QWidget): - - def __init__(self, context=None): - super(PyConsoleWidget, self).__init__() - - _, package_path = get_resource('packages', 'rqt_py_console') - ui_file = os.path.join( - package_path, 'share', 'rqt_py_console', 'resource', 'py_console_widget.ui') - - loadUi(ui_file, self, {'PyConsoleTextEdit': PyConsoleTextEdit}) - self.setObjectName('PyConsoleWidget') - - my_locals = { - 'context': context - } - self.py_console.update_interpreter_locals(my_locals) - self.py_console.print_message( - 'The variable "context" is set to the PluginContext of this plugin.') - self.py_console.exit.connect(context.close_plugin) diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/spyder_console_widget.py b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/spyder_console_widget.py deleted file mode 100644 index 374ef7a5d..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/spyder_console_widget.py +++ /dev/null @@ -1,60 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from python_qt_binding.QtGui import QFont - -from spyder.widgets.internalshell import InternalShell -from spyder.utils.module_completion import moduleCompletion - -class SpyderConsoleWidget(InternalShell): - - def __init__(self, context=None): - my_locals = { - 'context': context - } - super(SpyderConsoleWidget, self).__init__(namespace=my_locals) - self.setObjectName('SpyderConsoleWidget') - self.set_pythonshell_font(QFont('Mono')) - self.interpreter.restore_stds() - - def get_module_completion(self, objtxt): - """Return module completion list associated to object name""" - return moduleCompletion(objtxt) - - def run_command(self, *args): - self.interpreter.redirect_stds() - super(SpyderConsoleWidget, self).run_command(*args) - self.flush() - self.interpreter.restore_stds() - - def shutdown(self): - self.exit_interpreter() diff --git a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/template.py b/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/template.py deleted file mode 100644 index 9e17ecf18..000000000 --- a/robot/ros_ws/src/behavior/rqt_behavior_tree_command/src/rqt_behavior_tree_command/template.py +++ /dev/null @@ -1,182 +0,0 @@ -# Software License Agreement (BSD License) -# -# Copyright (c) 2012, Dorian Scholz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from python_qt_binding.QtWidgets import QVBoxLayout, QWidget -from rqt_gui_py.plugin import Plugin -from qt_gui_py_common.simple_settings_dialog import SimpleSettingsDialog -from rqt_py_console.py_console_widget import PyConsoleWidget - -import python_qt_binding.QtWidgets as qt -import python_qt_binding.QtWidgets as QtWidgets -import python_qt_binding.QtGui as QtGui -import python_qt_binding.QtCore as QtCore - -from ament_index_python.packages import get_package_share_directory -import yaml -import os - -from behavior_tree_msgs.msg import Status, BehaviorTreeCommand, BehaviorTreeCommands - -class BehaviorTreeCommandPlugin(Plugin): - """ - Plugin providing an interactive Python console - """ - - def __init__(self, context): - super(BehaviorTreeCommandPlugin, self).__init__(context) - self.setObjectName('BehaviorTreeCommandPlugin') - - self.config_filename = '' - self.button_groups = {} - - self.context = context - self.command_pub = self.context.node.create_publisher(BehaviorTreeCommands, 'behavior_tree_commands', 10) - - # main layout - self.widget = QWidget() - self.vbox = qt.QVBoxLayout() - self.widget.setLayout(self.vbox) - context.add_widget(self.widget) - - # config widget - self.config_widget = qt.QWidget() - self.config_widget.setStyleSheet('QWidget{margin-left:-1px;}') - self.config_layout = qt.QHBoxLayout() - self.config_widget.setLayout(self.config_layout) - self.config_widget.setFixedHeight(50) - - self.config_button = qt.QPushButton('Open Config...') - self.config_button.clicked.connect(self.select_config_file) - self.config_layout.addWidget(self.config_button) - - self.config_label = qt.QLabel('config filename: ') - self.config_layout.addWidget(self.config_label) - self.vbox.addWidget(self.config_widget) - - # button widget - self.button_widget = qt.QWidget() - self.button_layout = qt.QVBoxLayout() - self.button_widget.setLayout(self.button_layout) - self.vbox.addWidget(self.button_widget) - - def select_config_file(self): - starting_path = get_package_share_directory('rqt_behavior_tree_command') + '/config/' - filename = qt.QFileDialog.getOpenFileName(self.widget, 'Open Config File', starting_path, "Config Files (*.yaml)")[0] - print(filename) - self.set_config(filename) - - def set_config(self, filename): - if filename != '': - self.config_filename = filename - if self.config_filename != None: - self.config_label.setText('config filename: ' + os.path.basename(self.config_filename)) - self.init_buttons(filename) - - def init_buttons(self, filename): - y = yaml.load(open(filename, 'r').read(), Loader=yaml.Loader) - print(y) - - def get_click_function(group, button): - def click_function(): - commands = BehaviorTreeCommands() - for i in range(len(self.button_groups[group]['buttons'])): - b = self.button_groups[group]['buttons'][i] - command = BehaviorTreeCommand() - command.condition_name = self.button_groups[group]['condition_names'][i] - - if b != button and b.isChecked(): - b.toggle() - command.status = Status.FAILURE - elif b == button and not b.isChecked(): - command.status = Status.FAILURE - elif b == button and b.isChecked(): - command.status = Status.SUCCESS - commands.commands.append(command) - self.command_pub.publish(commands) - return click_function - - for group in y['groups']: - group_name = list(group.keys())[0] - if group_name not in self.button_groups.keys(): - self.button_groups[group_name] = {'buttons' : [], 'condition_names': []} - - group_widget = qt.QWidget() - group_layout = qt.QVBoxLayout() - group_widget.setLayout(group_layout) - self.button_layout.addWidget(group_widget) - - group_layout.addWidget(qt.QLabel(group_name)) - - button_widget = qt.QWidget() - button_layout = qt.QHBoxLayout() - button_widget.setLayout(button_layout) - group_layout.addWidget(button_widget) - - for buttons in group[group_name]: - button_name = list(buttons.keys())[0] - condition_name = buttons[button_name]['condition_name'] - - button = qt.QPushButton(button_name) - button.clicked.connect(get_click_function(group_name, button)) - button.setCheckable(True) - button_layout.addWidget(button) - - #print(condition_name, bt.get_condition_topic_name(condition_name)) - self.button_groups[group_name]['buttons'].append(button) - self.button_groups[group_name]['condition_names'].append(condition_name) - - def save_settings(self, plugin_settings, instance_settings): - instance_settings.set_value('config_filename', self.config_filename) - - def restore_settings(self, plugin_settings, instance_settings): - self.set_config(instance_settings.value('config_filename')) - - def trigger_configuration(self): - options = [ - {'title': 'Option 1', - 'description': 'Description of option 1.', - 'enabled': True}, - {'title': 'Option 2', - 'description': 'Description of option 2.'}, - ] - dialog = SimpleSettingsDialog(title='Options') - dialog.add_exclusive_option_group(title='List of options:', options=options, selected_index=0) - selected_index = dialog.get_settings()[0] - if selected_index != None: - selected_index = selected_index['selected_index'] - print('selected_index ', selected_index) - - def shutdown_console_widget(self): - pass - - def shutdown_plugin(self): - self.shutdown_console_widget() diff --git a/robot/ros_ws/src/global/global_bringup/CMakeLists.txt b/robot/ros_ws/src/global/global_bringup/CMakeLists.txt index 77d079414..1c0214f8d 100644 --- a/robot/ros_ws/src/global/global_bringup/CMakeLists.txt +++ b/robot/ros_ws/src/global/global_bringup/CMakeLists.txt @@ -23,10 +23,9 @@ if(BUILD_TESTING) ament_lint_auto_find_test_dependencies() endif() -# Install files. -install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY rviz DESTINATION share/${PROJECT_NAME}) +# Install files. No launch/ anymore: the legacy global.launch.xml went with +# the removed AUTONOMY_ROLE dispatch — stacks include vdb_mapping_ros2.py and +# random_walk_planner.launch.xml directly, selecting the config/ files here. install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY params DESTINATION share/${PROJECT_NAME}) ament_package() diff --git a/robot/ros_ws/src/global/global_bringup/LICENSE b/robot/ros_ws/src/global/global_bringup/LICENSE index d64569567..f2eb4e521 100644 --- a/robot/ros_ws/src/global/global_bringup/LICENSE +++ b/robot/ros_ws/src/global/global_bringup/LICENSE @@ -1,202 +1,32 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +The Clear BSD License + +Copyright (c) 2023-2026 Carnegie Mellon University, AirLab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted (subject to the limitations in the disclaimer +below) provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/robot/ros_ws/src/global/global_bringup/config/vdb_remote_params.yaml b/robot/ros_ws/src/global/global_bringup/config/vdb_remote_params.yaml deleted file mode 100644 index b612747b9..000000000 --- a/robot/ros_ws/src/global/global_bringup/config/vdb_remote_params.yaml +++ /dev/null @@ -1,33 +0,0 @@ -/**: - ros__parameters: - # Basic setup - map_frame: map - robot_frame: base_link - max_range: 10.0 - resolution: 0.07 - prob_hit: 0.8 - prob_miss: 0.1 - thres_min: 0.12 - thres_max: 0.8 - map_save_dir: "" - - # Visualizations - publish_pointcloud: true - publish_vis_marker: true - visualization_rate: 2.0 - - # Sensor input - apply_raw_sensor_data: false - - # Remote mapping - publish_updates: false - publish_overwrites: false - publish_sections: false - - remote_sources: [vdb_mapping] - vdb_mapping: - namespace: vdb_mapping - apply_remote_updates: false - apply_remote_overwrites: true - apply_remote_sections: true - diff --git a/robot/ros_ws/src/global/global_bringup/launch/global.launch.xml b/robot/ros_ws/src/global/global_bringup/launch/global.launch.xml deleted file mode 100644 index c0e7dcf40..000000000 --- a/robot/ros_ws/src/global/global_bringup/launch/global.launch.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/global/global_bringup/launch/global_planner.launch.xml b/robot/ros_ws/src/global/global_bringup/launch/global_planner.launch.xml deleted file mode 100644 index 939903f83..000000000 --- a/robot/ros_ws/src/global/global_bringup/launch/global_planner.launch.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/global/global_bringup/package.xml b/robot/ros_ws/src/global/global_bringup/package.xml index a7588894c..38cd50097 100644 --- a/robot/ros_ws/src/global/global_bringup/package.xml +++ b/robot/ros_ws/src/global/global_bringup/package.xml @@ -3,9 +3,9 @@ global_bringup 0.0.0 - TODO: Package description - andrew - Apache-2.0 + Configuration package for the AirStack global autonomy layer (global planners and world models). + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/global/planners/ensemble_planner/CMakeLists.txt b/robot/ros_ws/src/global/planners/ensemble_planner/CMakeLists.txt deleted file mode 100644 index 9de81c1bd..000000000 --- a/robot/ros_ws/src/global/planners/ensemble_planner/CMakeLists.txt +++ /dev/null @@ -1,75 +0,0 @@ -cmake_minimum_required(VERSION 3.5) -project(ensemble_global_planner) - -# Default to C++14 -if(NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 17) -endif() - -# Find dependencies -find_package(ament_cmake REQUIRED) -find_package(rclcpp REQUIRED) -find_package(rclcpp_action REQUIRED) -find_package(std_msgs REQUIRED) -find_package(std_srvs REQUIRED) -find_package(geometry_msgs REQUIRED) -find_package(nav_msgs REQUIRED) -find_package(visualization_msgs REQUIRED) -find_package(tf2 REQUIRED) -find_package(tf2_ros REQUIRED) - -################################### -## ament specific configuration ## -################################### - -ament_package() - -########### -## Build ## -########### - -# Specify additional locations of header files - -# Declare a C++ executable -add_executable(ensemble_global_planner src/ensemble_global_planner_node.cpp) - -# Link libraries -ament_target_dependencies(ensemble_global_planner - rclcpp - rclcpp_action - std_msgs - std_srvs - geometry_msgs - nav_msgs - visualization_msgs - tf2 - tf2_ros -) - -# Install executable -install(TARGETS - ensemble_global_planner - DESTINATION lib/${PROJECT_NAME} -) -# install(DIRECTORY -# launch -# DESTINATION share/${PROJECT_NAME}) - -install(DIRECTORY - config - DESTINATION share/${PROJECT_NAME}) - -install(DIRECTORY - src - DESTINATION share/${PROJECT_NAME}) - - -############# -## Testing ## -############# - -# Add gtest based cpp test target and link libraries -# ament_add_gtest(${PROJECT_NAME}-test test/test_ensemble_global_planner.cpp) -# if(TARGET ${PROJECT_NAME}-test) -# target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}_node) -# endif() \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/ensemble_planner/config/ensemble_global_planner_config.yaml b/robot/ros_ws/src/global/planners/ensemble_planner/config/ensemble_global_planner_config.yaml deleted file mode 100644 index efc46f87f..000000000 --- a/robot/ros_ws/src/global/planners/ensemble_planner/config/ensemble_global_planner_config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -/**: - ros__parameters: - srv_global_plan_toggle_topic: "~/global_plan_toggle" - way_point_planners: - - name: "random_walk" - config: - frequency: 1.0 #hz - diff --git a/robot/ros_ws/src/global/planners/ensemble_planner/include/ensemble_global_planner_node.hpp b/robot/ros_ws/src/global/planners/ensemble_planner/include/ensemble_global_planner_node.hpp deleted file mode 100644 index f989e86cf..000000000 --- a/robot/ros_ws/src/global/planners/ensemble_planner/include/ensemble_global_planner_node.hpp +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - - -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "rclcpp/rclcpp.hpp" - -class EnsembleGlobalPlannerNode : public rclcpp::Node { - private: - // String constants - std::string srv_global_plan_toggle_topic_; - - void globalPlannnerToggleCallback(const std_srvs::srv::Trigger::Request::SharedPtr request, - std_srvs::srv::Trigger::Response::SharedPtr response); - - // Other functions - void readParameters(); - - bool enable_global_planner = false; - - public: - // explicit RandomWalkNode(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()); - EnsembleGlobalPlannerNode(); - ~EnsembleGlobalPlannerNode() = default; - - // ROS subscribers - rclcpp::Subscription::SharedPtr sub_map; - rclcpp::Subscription::SharedPtr sub_robot_tf; - - // ROS publishers - // rclcpp::Publisher::SharedPtr pub_global_path; - rclcpp::Publisher::SharedPtr pub_goal_point; - rclcpp::Publisher::SharedPtr pub_trajectory_lines; - - // ROS services - rclcpp::Service::SharedPtr srv_global_planner_toggle; - - // ROS timers - rclcpp::TimerBase::SharedPtr timer; -}; diff --git a/robot/ros_ws/src/global/planners/ensemble_planner/package.xml b/robot/ros_ws/src/global/planners/ensemble_planner/package.xml deleted file mode 100644 index fafdacbce..000000000 --- a/robot/ros_ws/src/global/planners/ensemble_planner/package.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - ensemble_global_planner - 0.0.0 - Ensemble planner to coordinate different groups of planners - - - - - todo - - - - - - TODO - - - - - - - - - - - - - - ament_cmake - rclcpp - rclcpp_action - std_msgs - std_srvs - base - geometry_msgs - nav_msgs - visualization_msgs - message_generation - tf2 - tf2_ros - - - ament_cmake - - diff --git a/robot/ros_ws/src/global/planners/ensemble_planner/src/ensemble_global_planner_node.cpp b/robot/ros_ws/src/global/planners/ensemble_planner/src/ensemble_global_planner_node.cpp deleted file mode 100644 index 254bf2498..000000000 --- a/robot/ros_ws/src/global/planners/ensemble_planner/src/ensemble_global_planner_node.cpp +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - - -#include "../include/ensemble_global_planner_node.hpp" - -void EnsembleGlobalPlannerNode::readParameters() { - this->declare_parameter("srv_global_plan_toggle_topic"); - if (!this->get_parameter("srv_global_plan_toggle_topic", this->srv_global_plan_toggle_topic_)) { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: srv_global_plan_toggle_topic"); - } -} - -EnsembleGlobalPlannerNode::EnsembleGlobalPlannerNode() : Node("ensemble_global_planner_node") { - // Initialize the Global Planner planner - EnsembleGlobalPlannerNode::readParameters(); - this->srv_global_planner_toggle = this->create_service( - srv_global_plan_toggle_topic_, - std::bind(&EnsembleGlobalPlannerNode::globalPlannnerToggleCallback, this, std::placeholders::_1, - std::placeholders::_2)); -} - -void EnsembleGlobalPlannerNode::globalPlannnerToggleCallback( - const std_srvs::srv::Trigger::Request::SharedPtr request, - std_srvs::srv::Trigger::Response::SharedPtr response) { - if (this->enable_global_planner == false) { - this->enable_global_planner = true; - response->success = true; - response->message = "Global Planner enabled"; - RCLCPP_INFO(this->get_logger(), "Global Planner enabled"); - } else { - this->enable_global_planner = false; - response->success = true; - response->message = "Global Planer disabled"; - RCLCPP_INFO(this->get_logger(), "Global Planner disabled"); - } -} - -int main(int argc, char *argv[]) { - rclcpp::init(argc, argv); - rclcpp::spin(std::make_shared()); - rclcpp::shutdown(); - return 0; -} diff --git a/robot/ros_ws/src/global/planners/exploration/CMakeLists.txt b/robot/ros_ws/src/global/planners/exploration/CMakeLists.txt index 2dfc80ff7..e60aa9f17 100644 --- a/robot/ros_ws/src/global/planners/exploration/CMakeLists.txt +++ b/robot/ros_ws/src/global/planners/exploration/CMakeLists.txt @@ -29,12 +29,6 @@ find_package(PCL REQUIRED) find_package(pcl_conversions REQUIRED) find_package(message_filters REQUIRED) -################################### -## ament specific configuration ## -################################### - -ament_package() - ########### ## Build ## ########### @@ -143,4 +137,11 @@ install(DIRECTORY # ament_add_gtest(${PROJECT_NAME}-test test/test_exploration_planner.cpp) # if(TARGET ${PROJECT_NAME}-test) # target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}_node) -# endif() \ No newline at end of file +# endif() + +################################### +## ament specific configuration ## +################################### + +# ament_package() must be the last call in the file (ament requirement). +ament_package() \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/README.md b/robot/ros_ws/src/global/planners/exploration/README.md index 402b6d62e..0e609baa8 100644 --- a/robot/ros_ws/src/global/planners/exploration/README.md +++ b/robot/ros_ws/src/global/planners/exploration/README.md @@ -1,14 +1,17 @@ # Exploration Planner -The Exploration Planner is an optional global planner for autonomous flight. It combines the maintaining of an openvdb based voxel occupancy grid map, extract frontier from the map, and generates trajectories that enables the drone to explore previously undiscovered areas. With VDB mapping integrated, you can turn off the vdb_mapping node in the launch xml, and the planner will generate and publish multiple linked straight-line trajectories as a shortened RRT path to a selected viewpoint with collision check. +The Exploration Planner is an optional global planner for autonomous flight. It maintains an OpenVDB-based voxel occupancy grid map, extracts frontiers from the map, and generates trajectories that let the drone explore unvisited areas. Because it maintains its own map, you can turn off the `vdb_mapping` node in the launch XML; the planner will generate and publish multiple linked straight-line trajectories as a shortened, collision-checked RRT path to a selected viewpoint. ## Functionality -You can comment out the world model and random walk planning module in `global.launch.xml` and add the following line: +In your stack's entry launch file (e.g. a copy of +`stacks/full_default/launch/stack.launch.xml`), replace the +`random_walk_planner.launch.xml` include (and optionally the vdb_mapping +include) with: ``: -Then when running the robot stack, after taking off, click the `Global Plan` and the exploration planner will work in the place of previous random walk planner. The working process is: +Then when running the robot stack, after taking off, toggle the global plan from the rviz Tasks Panel or the GCS (the `~/global_plan_toggle` service) and the exploration planner runs in place of the random walk planner. The working process is: 1. Create and maintain a voxel grid map with odometry and laser scan, to visualize, check topic `"~/vdb_viz"`, where `~` is the namespace. 2. Extract frontier and select viewpoints for exploration. @@ -16,9 +19,9 @@ Then when running the robot stack, after taking off, click the `Global Plan` and 4. Continuously monitor the robot's progress along the published path. 5. Once the robot completes the current path, a new exploration path will be generated. -This loop continues, enabling the drone to keep explore in the entire space. +This loop continues, enabling the drone to keep exploring the entire space. -We're still cleaning old params of random walk planner, based on which this exploration is developed. We'll update parameter documentation later. +The planner is derived from the random walk planner and shares several of its parameters; parameter documentation is still being expanded. ## Parameters |
Parameter
| Description @@ -35,7 +38,7 @@ We're still cleaning old params of random walk planner, based on which this expl ## Services |
Parameter
| Type | Description |----------------------------|----------------------------------------|-----------------------| -| `~/global_plan_toggle` | std_srvs/Trigger | A toggle switch to turn on and off the random walk planner.| +| `~/global_plan_toggle` | std_srvs/Trigger | A toggle switch to turn on and off the exploration planner.| ## Subscriptions |
Parameter
| Type | Description @@ -46,7 +49,7 @@ We're still cleaning old params of random walk planner, based on which this expl ## Publications |
Parameter
| Type | Description |----------------------------|----------------------------------------|-----------------------| -| `~/pub_global_plan_topic` | nav_msgs/Path | Outputs the global plan that is generated from the random walk planner.| +| `~/pub_global_plan_topic` | nav_msgs/Path | Outputs the global plan generated by the exploration planner.| diff --git a/robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml index 90fd427ad..24e9ee2ca 100644 --- a/robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml +++ b/robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml @@ -1,3 +1,7 @@ + @@ -8,16 +12,4 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/launch/random_walk_launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/random_walk_launch.xml deleted file mode 100644 index 57c5b1aa3..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/random_walk_launch.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo.xml b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo.xml deleted file mode 100644 index eb8eb3ea1..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gazebo_vis.rviz b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gazebo_vis.rviz deleted file mode 100644 index d4e39c4b2..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gazebo_vis.rviz +++ /dev/null @@ -1,696 +0,0 @@ -Panels: - - Class: rviz_common/Displays - Help Height: 78 - Name: Displays - Property Tree Widget: - Expanded: - - /Global Options1 - - /TF1/Frames1 - - /Sensors1 - - /Perception1 - - /Perception1/MACVO PointCloud1 - - /Local1 - - /Local1/DROAN1/Trimmed Global Plan for DROAN1/Topic1 - - /Global1 - - /Global1/VDB Mapping Marker1 - Splitter Ratio: 0.590062141418457 - Tree Height: 677 - - 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: 100 - Reference Frame: - Value: true - - Class: rviz_default_plugins/TF - Enabled: true - Frame Timeout: 15 - Frames: - All Enabled: false - base_link: - Value: true - base_link_stabilized: - Value: false - look_ahead_point: - Value: true - look_ahead_point_stabilized: - Value: false - map: - Value: true - ouster: - Value: false - rmf_owl: - Value: false - rmf_owl/base_link: - Value: false - rmf_owl/base_link/air_pressure: - Value: false - rmf_owl/base_link/base_link_inertia_collision: - Value: false - rmf_owl/base_link/base_link_inertia_visual: - Value: false - rmf_owl/base_link/camera_front: - Value: false - rmf_owl/base_link/imu_sensor: - Value: false - rmf_owl/base_link/magnetometer: - Value: false - rmf_owl/camera_link: - Value: false - rmf_owl/camera_link/camera_collision: - Value: false - rmf_owl/camera_link/camera_visual: - Value: false - rmf_owl/camera_link/depth_camera_front: - Value: false - rmf_owl/camera_link/segmentation_camera: - Value: false - rmf_owl/laser_link: - Value: false - rmf_owl/laser_link/gpu_lidar: - Value: false - rmf_owl/laser_link/laser_collision: - Value: false - rmf_owl/laser_link/laser_visual: - Value: false - rmf_owl/rotor_0: - Value: false - rmf_owl/rotor_0/rotor_0_collision: - Value: false - rmf_owl/rotor_0/rotor_0_visual: - Value: false - rmf_owl/rotor_1: - Value: false - rmf_owl/rotor_1/rotor_1_collision: - Value: false - rmf_owl/rotor_1/rotor_1_visual: - Value: false - rmf_owl/rotor_2: - Value: false - rmf_owl/rotor_2/rotor_2_collision: - Value: false - rmf_owl/rotor_2/rotor_2_visual: - Value: false - rmf_owl/rotor_3: - Value: false - rmf_owl/rotor_3/rotor_3_collision: - Value: false - rmf_owl/rotor_3/rotor_3_visual: - Value: false - tracking_point: - Value: true - tracking_point_stabilized: - Value: false - world: - Value: false - Marker Scale: 2 - Name: TF - Show Arrows: true - Show Axes: true - Show Names: true - Tree: - world: - map: - base_link_stabilized: - {} - look_ahead_point: - {} - look_ahead_point_stabilized: - {} - tracking_point: - {} - tracking_point_stabilized: - {} - rmf_owl: - rmf_owl/base_link: - base_link: - {} - rmf_owl/base_link/air_pressure: - {} - rmf_owl/base_link/base_link_inertia_collision: - {} - rmf_owl/base_link/base_link_inertia_visual: - {} - rmf_owl/base_link/camera_front: - {} - rmf_owl/base_link/imu_sensor: - {} - rmf_owl/base_link/magnetometer: - {} - rmf_owl/camera_link: - rmf_owl/camera_link/camera_collision: - {} - rmf_owl/camera_link/camera_visual: - {} - rmf_owl/camera_link/depth_camera_front: - {} - rmf_owl/camera_link/segmentation_camera: - {} - rmf_owl/laser_link: - rmf_owl/laser_link/gpu_lidar: - ouster: - {} - rmf_owl/laser_link/laser_collision: - {} - rmf_owl/laser_link/laser_visual: - {} - rmf_owl/rotor_0: - rmf_owl/rotor_0/rotor_0_collision: - {} - rmf_owl/rotor_0/rotor_0_visual: - {} - rmf_owl/rotor_1: - rmf_owl/rotor_1/rotor_1_collision: - {} - rmf_owl/rotor_1/rotor_1_visual: - {} - rmf_owl/rotor_2: - rmf_owl/rotor_2/rotor_2_collision: - {} - rmf_owl/rotor_2/rotor_2_visual: - {} - rmf_owl/rotor_3: - rmf_owl/rotor_3/rotor_3_collision: - {} - rmf_owl/rotor_3/rotor_3_visual: - {} - Update Interval: 0 - Value: true - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/Image - Enabled: false - Max Value: 1 - Median window: 5 - Min Value: 0 - Name: Front Left RGB - Normalize Range: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: sensors/front_stereo/left/image_rect - Value: false - - Class: rviz_default_plugins/Image - Enabled: false - Max Value: 100 - Median window: 5 - Min Value: 0 - Name: Front Left Depth - Normalize Range: false - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: sensors/front_stereo/left/depth - Value: false - - Alpha: 1 - Autocompute Intensity Bounds: true - Autocompute Value Bounds: - Max Value: 6.571824073791504 - Min Value: -0.5682187080383301 - Value: true - Axis: Z - Channel Name: intensity - Class: rviz_default_plugins/PointCloud2 - Color: 170; 170; 255 - Color Transformer: FlatColor - Decay Time: 0 - Enabled: false - Invert Rainbow: false - Max Color: 255; 255; 255 - Max Intensity: 4096 - Min Color: 0; 0; 0 - Min Intensity: 0 - Name: Lidar - Position Transformer: XYZ - Selectable: true - Size (Pixels): 1 - Size (m): 0.009999999776482582 - Style: Points - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: sensors/ouster/point_cloud - Use Fixed Frame: true - Use rainbow: true - Value: false - - Angle Tolerance: 0 - Class: rviz_default_plugins/Odometry - Covariance: - Orientation: - Alpha: 0.5 - Color: 255; 255; 127 - Color Style: Unique - Frame: Local - Offset: 1 - Scale: 1 - Value: true - Position: - Alpha: 0.30000001192092896 - Color: 204; 51; 204 - Scale: 1 - Value: true - Value: true - Enabled: false - Keep: 1 - Name: Odometry - Position Tolerance: 0 - Shape: - Alpha: 1 - Axes Length: 1 - Axes Radius: 0.10000000149011612 - Color: 255; 25; 0 - Head Length: 0.30000001192092896 - Head Radius: 0.10000000149011612 - Shaft Length: 1 - Shaft Radius: 0.05000000074505806 - Value: Axes - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: odometry_conversion/odometry - Value: false - Enabled: true - Name: Sensors - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/Image - Enabled: false - Max Value: 1 - Median window: 5 - Min Value: 0 - Name: MACVO Disparity - Normalize Range: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/macvo/disparity - Value: false - - Alpha: 1 - Autocompute Intensity Bounds: true - Autocompute Value Bounds: - Max Value: 10 - Min Value: -10 - Value: true - Axis: Z - Channel Name: intensity - Class: rviz_default_plugins/PointCloud - Color: 255; 255; 255 - Color Transformer: RGBF32 - Decay Time: 5 - Enabled: true - Invert Rainbow: false - Max Color: 255; 255; 255 - Max Intensity: 4096 - Min Color: 0; 0; 0 - Min Intensity: 0 - Name: MACVO PointCloud - Position Transformer: XYZ - Selectable: true - Size (Pixels): 3 - Size (m): 0.009999999776482582 - Style: Flat Squares - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/macvo/point_cloud - Use Fixed Frame: true - Use rainbow: true - Value: true - - Angle Tolerance: 0.10000000149011612 - Class: rviz_default_plugins/Odometry - Covariance: - Orientation: - Alpha: 0.5 - Color: 255; 255; 127 - Color Style: Unique - Frame: Local - Offset: 1 - Scale: 1 - Value: true - Position: - Alpha: 0.30000001192092896 - Color: 204; 51; 204 - Scale: 1 - Value: true - Value: true - Enabled: true - Keep: 100 - Name: MACVO Odometry - Position Tolerance: 0.10000000149011612 - Shape: - Alpha: 1 - Axes Length: 1 - Axes Radius: 0.10000000149011612 - Color: 255; 25; 0 - Head Length: 0.30000001192092896 - Head Radius: 0.10000000149011612 - Shaft Length: 1 - Shaft Radius: 0.05000000074505806 - Value: Arrow - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/macvo/odometry - Value: true - Enabled: true - Name: Perception - - Class: rviz_common/Group - Displays: - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/Marker - Enabled: false - Name: Disparity Frustum - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/droan/frustum - Value: false - - Class: rviz_default_plugins/MarkerArray - Enabled: false - Name: Disparity Map Collision Checking - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/droan/disparity_map_debug - Value: false - - Class: rviz_default_plugins/MarkerArray - Enabled: false - Name: Disparity Graph Poses - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/droan/disparity_graph - Value: false - - Class: rviz_default_plugins/MarkerArray - Enabled: true - Name: Trimmed Global Plan for DROAN - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/local_planner_global_plan_vis - Value: true - - Class: rviz_default_plugins/MarkerArray - Enabled: false - Name: ExpansionPoly - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/expansion_poly - Value: false - - Alpha: 1 - Autocompute Intensity Bounds: true - Autocompute Value Bounds: - Max Value: 10 - Min Value: -10 - Value: true - Axis: Z - Channel Name: intensity - Class: rviz_default_plugins/PointCloud2 - Color: 255; 255; 255 - Color Transformer: Intensity - Decay Time: 0 - Enabled: true - Invert Rainbow: false - Max Color: 255; 255; 255 - Max Intensity: 220 - Min Color: 0; 0; 0 - Min Intensity: 120 - Name: Expansion Cloud - Position Transformer: XYZ - Selectable: true - Size (Pixels): 3 - Size (m): 0.009999999776482582 - Style: Flat Squares - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/expansion_cloud - Use Fixed Frame: true - Use rainbow: true - Value: true - - Class: rviz_default_plugins/MarkerArray - Enabled: true - Name: Traj Library - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/trajectory_library_vis - Value: true - - Class: rviz_default_plugins/MarkerArray - Enabled: true - Name: Virtual Obstacles - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/virtual_obstacles - Value: true - Enabled: true - Name: DROAN - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/MarkerArray - Enabled: true - Name: Traj Vis - Namespaces: - traj_controller: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: trajectory_controller/trajectory_vis - Value: true - - Class: rviz_default_plugins/MarkerArray - Enabled: false - Name: Traj Debug - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: trajectory_controller/trajectory_controller_debug_markers - Value: false - Enabled: true - Name: Trajectory Controller - Enabled: true - Name: Local - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/Marker - Enabled: true - Name: VDB Mapping Marker - Namespaces: - vdb_grid: true - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/exploration_planner/vdb_viz - Value: true - - Alpha: 1 - Buffer Length: 1 - Class: rviz_default_plugins/Path - Color: 0; 255; 255 - Enabled: true - Head Diameter: 0.30000001192092896 - Head Length: 0.20000000298023224 - Length: 0.30000001192092896 - Line Style: Billboards - Line Width: 0.10000000149011612 - Name: Global Plan - Offset: - X: 0 - Y: 0 - Z: 0 - Pose Color: 255; 85; 255 - Pose Style: None - Radius: 0.029999999329447746 - Shaft Diameter: 0.10000000149011612 - Shaft Length: 0.10000000149011612 - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/global_plan - Value: true - Enabled: true - Name: Global - Enabled: true - Global Options: - Background Color: 48; 48; 48 - Fixed Frame: map - 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: 15.553143501281738 - Enable Stereo Rendering: - Stereo Eye Separation: 0.05999999865889549 - Stereo Focal Distance: 1 - Swap Stereo Eyes: false - Value: false - Focal Point: - X: -0.2159808725118637 - Y: -0.7331764101982117 - Z: -1.5793094635009766 - Focal Shape Fixed Size: false - Focal Shape Size: 0.05000000074505806 - Invert Z Axis: false - Name: Current View - Near Clip Distance: 0.009999999776482582 - Pitch: 0.5253989100456238 - Target Frame: base_link - Value: Orbit (rviz) - Yaw: 2.190396785736084 - Saved: ~ -Window Geometry: - Displays: - collapsed: false - Front Left Depth: - collapsed: false - Front Left RGB: - collapsed: false - Height: 1016 - Hide Left Dock: false - Hide Right Dock: false - MACVO Disparity: - collapsed: false - QMainWindow State: 000000ff00000000fd0000000400000000000001e50000032efc020000000afb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b0000032e000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000a0049006d00610067006500000002eb000000c90000000000000000fb00000028004d004100430056004f00200049006d00610067006500200046006500610074007500720065007300000002ba000000ca000000000000000000000001000001f60000032efc0200000008fb00000016004c006500660074002000430061006d006500720061010000003b000001880000000000000000fb00000014004c006500660074002000440065007000740068010000003b0000016a0000000000000000fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000001c00460072006f006e00740020004c0065006600740020005200470042000000003b000001060000002800fffffffb0000002000460072006f006e00740020004c006500660074002000440065007000740068000000003b000001250000002800fffffffb0000001e004d004100430056004f0020004400690073007000610072006900740079000000003b0000032e0000002800fffffffb0000000a00560069006500770073000000025900000114000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000007380000006efc0100000002fb0000000800540069006d00650100000000000007380000025300fffffffb0000000800540069006d006501000000000000045000000000000000000000054d0000032e00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 - Selection: - collapsed: false - Time: - collapsed: false - Tool Properties: - collapsed: false - Views: - collapsed: false - Width: 1848 - X: 1272 - Y: 621 diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_autonomy_launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_autonomy_launch.xml deleted file mode 100644 index e9a133f38..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_autonomy_launch.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_behavior_launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_behavior_launch.xml deleted file mode 100644 index cef093702..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_behavior_launch.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_domain_bridge.yaml b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_domain_bridge.yaml deleted file mode 100644 index 13ffbb91d..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_domain_bridge.yaml +++ /dev/null @@ -1,77 +0,0 @@ -name: my_domain_bridge - -topics: - # Bridge "/foo/chatter" topic from doman ID 2 to domain ID 3 - # Automatically detect QoS settings and default to 'keep_last' history with depth 10 - - /tf: - type: tf2_msgs/msg/TFMessage - from_domain: 0 - to_domain: 1 - qos: - subscription: # on domain 0 side: match gz_parameter_bridge (RELIABLE) - reliability: reliable - durability: volatile - history: keep_last - depth: 100 - publisher: # on domain 1 side: match RViz TF listener (RELIABLE) - reliability: reliable - durability: volatile - history: keep_last - depth: 100 - - /tf_static: - type: tf2_msgs/msg/TFMessage - from_domain: 0 - to_domain: 1 - qos: - reliability: reliable - durability: transient_local - history: keep_last - depth: 1 - - /clock: - type: rosgraph_msgs/msg/Clock - from_domain: 0 - to_domain: 1 - qos: - reliability: best_effort - durability: volatile - history: keep_last - depth: 1 - - /odom: - from_domain: 0 - to_domain: 1 - type: nav_msgs/msg/Odometry - - /robot_1/sensors/ouster/point_cloud: - from_domain: 0 - to_domain: 1 - type: sensor_msgs/msg/PointCloud2 - - /robot_1/interface/cmd_velocity: - from_domain: 1 - to_domain: 0 - type: geometry_msgs/msg/TwistStamped - - # /robot_1/odometry_conversion/odometry: - # from_domain: 1 - # to_domain: 0 - # type: nav_msgs/msg/Odometry - - # /robot_1/behavior/behavior_tree_graphviz: - # type: std_msgs/msg/String - # from_domain: 1 - # to_domain: 0 - -# GPS related topics ----------- - # /robot_1/interface/mavros/global_position/raw/fix: - # type: sensor_msgs/msg/NavSatFix - # from_domain: 1 - # to_domain: 0 - - # /robot_1/interface/mavros/global_position/global: - # type: sensor_msgs/msg/NavSatFix - # from_domain: 1 - # to_domain: 0 diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_global_launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_global_launch.xml deleted file mode 100644 index ae5fed486..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_global_launch.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_interface_launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_interface_launch.xml deleted file mode 100644 index f42a5d1fd..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_interface_launch.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_local_launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_local_launch.xml deleted file mode 100644 index 93217a883..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_local_launch.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_static_transforms.launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_static_transforms.launch.xml deleted file mode 100644 index b1946584a..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/robot_launch_gazebo/gz_static_transforms.launch.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/global/planners/exploration/package.xml b/robot/ros_ws/src/global/planners/exploration/package.xml index a66c09a37..b313f8ecc 100644 --- a/robot/ros_ws/src/global/planners/exploration/package.xml +++ b/robot/ros_ws/src/global/planners/exploration/package.xml @@ -1,46 +1,33 @@ - + exploration_planner 0.0.0 - The exploration planner package + Frontier-based geometric exploration planner — kept as the intended alternative to random_walk for planner selection from the RViz Tasks Panel (future) - - - - todo + Andrew Jong - - - - - TODO - - - - - - - - - - - - + BSD-3-Clause-Clear ament_cmake + rclcpp rclcpp_action std_msgs std_srvs - base geometry_msgs nav_msgs visualization_msgs - message_generation tf2 tf2_ros + tf2_eigen + tf2_geometry_msgs + pcl_conversions message_filters - + + eigen + libpcl-all-dev + libopenvdb-dev + ament_cmake diff --git a/robot/ros_ws/src/global/planners/random_walk/README.md b/robot/ros_ws/src/global/planners/random_walk/README.md index 91cda56d5..50ab2f986 100644 --- a/robot/ros_ws/src/global/planners/random_walk/README.md +++ b/robot/ros_ws/src/global/planners/random_walk/README.md @@ -7,7 +7,7 @@ The blue line is the global plan generated by the random walk. The yellow line s ## Functionality -Upon activation by the behavior tree, the Random Walk Planner will: +Upon activation by an `ExplorationTask` goal, the Random Walk Planner will: 1. Generate a specified number of straight-line path segments. 2. Continuously monitor the robot's progress along the published path. @@ -40,13 +40,13 @@ This node is a **task executor**: it runs as a ROS 2 action server and is activa Random walk delegates navigation to the local planner via a second action: ``` -behavior_executive → ExplorationTask → random_walk_planner - ↓ - NavigateTask (/{robot_name}/tasks/navigate) - ↓ - droan_gl (local planner) - ↓ - trajectory_controller +GCS operator → ExplorationTask → random_walk_planner + ↓ + NavigateTask (/{robot_name}/tasks/navigate) + ↓ + droan_gl (local planner) + ↓ + trajectory_controller ``` ### Goal parameters 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..53b22fd38 --- /dev/null +++ b/robot/ros_ws/src/global/planners/random_walk/launch/random_walk_planner.launch.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/global/planners/random_walk/package.xml b/robot/ros_ws/src/global/planners/random_walk/package.xml index 7fe44cd2c..a3e101f06 100644 --- a/robot/ros_ws/src/global/planners/random_walk/package.xml +++ b/robot/ros_ws/src/global/planners/random_walk/package.xml @@ -2,18 +2,18 @@ random_walk_planner 0.0.0 - The random walk planner package + Random-walk global planner that generates exploratory collision-checked waypoint paths through free space. - todo + Andrew Jong - TODO + BSD-3-Clause-Clear diff --git a/robot/ros_ws/src/global/waypoint_interface/CMakeLists.txt b/robot/ros_ws/src/global/waypoint_interface/CMakeLists.txt deleted file mode 100644 index 3d389fde1..000000000 --- a/robot/ros_ws/src/global/waypoint_interface/CMakeLists.txt +++ /dev/null @@ -1,41 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(waypoint_interface) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# find dependencies -find_package(ament_cmake REQUIRED) -find_package(rclcpp REQUIRED) -find_package(nav_msgs REQUIRED) -find_package(geometry_msgs REQUIRED) - -add_executable(waypoint_interface_node src/waypoint_interface_node.cpp) - -# target_link_libraries(waypoint_interface_node rclcpp nav_msgs geometry_msgs) - -ament_target_dependencies(waypoint_interface_node - rclcpp - std_msgs - geometry_msgs - nav_msgs -) - -install(TARGETS - waypoint_interface_node - DESTINATION lib/${PROJECT_NAME}) - -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - # the following line skips the linter which checks for copyrights - # comment the line when a copyright and license is added to all source files - set(ament_cmake_copyright_FOUND TRUE) - # the following line skips cpplint (only works in a git repo) - # comment the line when this package is in a git repo and when - # a copyright and license is added to all source files - set(ament_cmake_cpplint_FOUND TRUE) - ament_lint_auto_find_test_dependencies() -endif() - -ament_package() diff --git a/robot/ros_ws/src/global/waypoint_interface/package.xml b/robot/ros_ws/src/global/waypoint_interface/package.xml deleted file mode 100644 index 9ea09c96a..000000000 --- a/robot/ros_ws/src/global/waypoint_interface/package.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - waypoint_interface - 0.0.0 - TODO: Package description - airstation-04 - TODO: License declaration - - ament_cmake - - rclcpp - nav_msgs - geometry_msgs - - ament_lint_auto - ament_lint_common - - - ament_cmake - - diff --git a/robot/ros_ws/src/global/waypoint_interface/src/waypoint_interface_node.cpp b/robot/ros_ws/src/global/waypoint_interface/src/waypoint_interface_node.cpp deleted file mode 100644 index 2a60087a7..000000000 --- a/robot/ros_ws/src/global/waypoint_interface/src/waypoint_interface_node.cpp +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -#include -#include - -#include "geometry_msgs/msg/pose_stamped.hpp" -#include "nav_msgs/msg/path.hpp" -#include "rclcpp/rclcpp.hpp" -#include "std_msgs/msg/int32.hpp" - -using std::placeholders::_1; - -class WaypointInterfaceNode : public rclcpp::Node { - public: - WaypointInterfaceNode() : Node("waypoint_interface") { - this->declare_parameter("lookahead_time", 5.0); - lookahead_time_ = this->get_parameter("lookahead_time").as_double(); - - subscription_ref_ = this->create_subscription( - "global_plan_reference", 10, - std::bind(&WaypointInterfaceNode::global_plan_reference_callback, this, _1)); - subscription_eta_ = this->create_subscription( - "global_plan_eta", 10, - std::bind(&WaypointInterfaceNode::global_plan_eta_callback, this, _1)); - - // Publisher for the current waypoint index - waypoint_index_publisher_ = - this->create_publisher("current_waypoint_index", 10); - - timer_ = this->create_wall_timer(std::chrono::seconds(1), - std::bind(&WaypointInterfaceNode::update_position, this)); - } - - private: - void global_plan_reference_callback(const nav_msgs::msg::Path::SharedPtr msg) { - global_plan_reference_ = msg->poses; - RCLCPP_INFO(this->get_logger(), "Received global plan reference with sparse waypoints."); - } - - void global_plan_eta_callback(const nav_msgs::msg::Path::SharedPtr msg) { - global_plan_eta_ = msg->poses; - RCLCPP_INFO(this->get_logger(), "Received global plan eta with dense waypoints."); - } - - void update_position() { - this->get_parameter("lookahead_time", lookahead_time_); - RCLCPP_INFO(this->get_logger(), "Checking waypoints within lookahead time = %f", - lookahead_time_); - track_waypoints(lookahead_time_); - } - - void track_waypoints(double lookahead_time) { - if (global_plan_reference_.empty() || global_plan_eta_.empty()) { - RCLCPP_WARN(this->get_logger(), "Global plans not yet received."); - return; - } - - rclcpp::Time current_time = this->get_clock()->now(); - rclcpp::Time target_time = current_time + rclcpp::Duration::from_seconds(lookahead_time); - - int last_close_waypoint_idx = -1; - - for (const auto &pose_eta : global_plan_eta_) { - if (rclcpp::Time(pose_eta.header.stamp) > target_time) { - break; - } - - for (size_t i = 0; i < global_plan_reference_.size(); ++i) { - if (is_close(pose_eta.pose.position, global_plan_reference_[i].pose.position)) { - last_close_waypoint_idx = i; - } - } - } - - std_msgs::msg::Int32 waypoint_index_msg; - - if (last_close_waypoint_idx == -1 || - last_close_waypoint_idx >= static_cast(global_plan_reference_.size()) - 1) { - RCLCPP_INFO(this->get_logger(), - "Robot is not within close range of any reference waypoints."); - waypoint_index_msg.data = -1; // No waypoint in range - } else { - RCLCPP_INFO(this->get_logger(), "Robot is traveling between waypoints %d and %d.", - last_close_waypoint_idx, last_close_waypoint_idx + 1); - waypoint_index_msg.data = last_close_waypoint_idx; - } - - // Publish the current waypoint index - waypoint_index_publisher_->publish(waypoint_index_msg); - } - - bool is_close(const geometry_msgs::msg::Point &p1, const geometry_msgs::msg::Point &p2) const { - double dist = distance(p1, p2); - return dist < close_range_threshold_; - } - - double distance(const geometry_msgs::msg::Point &p1, - const geometry_msgs::msg::Point &p2) const { - return std::sqrt(std::pow(p1.x - p2.x, 2) + std::pow(p1.y - p2.y, 2) + - std::pow(p1.z - p2.z, 2)); - } - - rclcpp::Subscription::SharedPtr subscription_ref_; - rclcpp::Subscription::SharedPtr subscription_eta_; - rclcpp::Publisher::SharedPtr waypoint_index_publisher_; - rclcpp::TimerBase::SharedPtr timer_; - - std::vector global_plan_reference_; - std::vector global_plan_eta_; - - double lookahead_time_; - const double close_range_threshold_ = 0.5; // Distance threshold for "close range" in meters -}; - -int main(int argc, char **argv) { - rclcpp::init(argc, argv); - auto node = std::make_shared(); - rclcpp::spin(node); - rclcpp::shutdown(); - return 0; -} diff --git a/robot/ros_ws/src/interface/interface_bringup/CMakeLists.txt b/robot/ros_ws/src/interface/interface_bringup/CMakeLists.txt index d93f3f0d8..c07cba377 100644 --- a/robot/ros_ws/src/interface/interface_bringup/CMakeLists.txt +++ b/robot/ros_ws/src/interface/interface_bringup/CMakeLists.txt @@ -25,8 +25,6 @@ endif() # Install files. install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY rviz DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY params DESTINATION share/${PROJECT_NAME}) +install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) ament_package() diff --git a/robot/ros_ws/src/interface/interface_bringup/LICENSE b/robot/ros_ws/src/interface/interface_bringup/LICENSE index d64569567..f2eb4e521 100644 --- a/robot/ros_ws/src/interface/interface_bringup/LICENSE +++ b/robot/ros_ws/src/interface/interface_bringup/LICENSE @@ -1,202 +1,32 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +The Clear BSD License + +Copyright (c) 2023-2026 Carnegie Mellon University, AirLab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted (subject to the limitations in the disclaimer +below) provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/robot/ros_ws/src/interface/interface_bringup/launch/px4_config.yaml b/robot/ros_ws/src/interface/interface_bringup/config/px4_config.yaml similarity index 100% rename from robot/ros_ws/src/interface/interface_bringup/launch/px4_config.yaml rename to robot/ros_ws/src/interface/interface_bringup/config/px4_config.yaml diff --git a/robot/ros_ws/src/interface/interface_bringup/launch/README.md b/robot/ros_ws/src/interface/interface_bringup/launch/README.md index 37ae58bc6..48d14bd4e 100644 --- a/robot/ros_ws/src/interface/interface_bringup/launch/README.md +++ b/robot/ros_ws/src/interface/interface_bringup/launch/README.md @@ -1,69 +1,27 @@ -# Interface Launch File Documentation - -## Overview - -This directory contains both XML and Python versions of the interface launch file: - -- `interface.launch.xml` - Original XML launch file -- `interface.launch.py` - New Python launch file with dynamic FCU URL calculation - -## Python Launch File Features - -The Python launch file (`interface.launch.py`) implements the same functionality as the XML version but with the following enhancements: - -### Dynamic FCU URL Calculation - -The FCU URL for MAVROS is now calculated programmatically based on the `robot_id` (from `ROS_DOMAIN_ID` environment variable), following the same logic as the `px4_mavlink_backend.py` in the Pegasus simulator: - -**Simulation Mode:** -- Connection format: `tcpin:localhost:PORT` -- Port calculation: `4560 + robot_id` -- Examples: - - robot_id=0 → `tcpin:localhost:4560` - - robot_id=1 → `tcpin:localhost:4561` - - robot_id=2 → `tcpin:localhost:4562` - -**Real Hardware Mode:** -- Connection: `/dev/ttyTHS4:115200` (unchanged from original) - -### Usage - -Launch the interface system: - -```bash -# For simulation (sim=true) -ros2 launch interface_bringup interface.launch.py sim:=true - -# For real hardware (sim=false, default) -ros2 launch interface_bringup interface.launch.py sim:=false - -# With custom odometry topic -ros2 launch interface_bringup interface.launch.py \ - sim:=true \ - interface_odometry_in_topic:=/custom/robot/interface/mavros/local_position/odom +# interface_bringup launch files + +- `interface.launch.py` — the canonical interface bringup, included by the + stack entry files under `stacks/*/launch/`. Launches MAVROS (via + `mavros_px4.launch.xml`, skipped when `SIM_TYPE=simple`), the + `robot_interface_node`, the position setpoint publisher, and the odometry + conversion node. +- `mavros_px4.launch.xml` — wraps the upstream `mavros` `node.launch` with the + AirStack MAVROS config (`../config/px4_config.yaml`) and PX4 plugin list. + +There is no `sim` launch argument. The MAVLink connection is computed from +environment variables (see `interface.launch.py`): + +```text +OFFBOARD_PORT = OFFBOARD_BASE_PORT (default 14540) + ROS_DOMAIN_ID +ONBOARD_PORT = ONBOARD_BASE_PORT (default 14580) + ROS_DOMAIN_ID +FCU_URL = udp://:@: + (unless FCU_URL is set; SIM_IP default 172.31.0.200) +TGT_SYSTEM = 1 + ROS_DOMAIN_ID (unless TGT_SYSTEM is set) ``` -### Environment Variables Required - -- `ROS_DOMAIN_ID`: Used as the robot_id for port calculation -- `ROBOT_NAME`: Used for topic remapping - -### Integration with Pegasus Simulator - -This launch file is designed to work seamlessly with the Pegasus Isaac Sim integration where: - -1. Isaac Sim launches PX4 SITL instances with calculated ports -2. The PX4MavlinkBackend uses the same port calculation logic -3. MAVROS connects to the correct PX4 instance using the calculated FCU URL - -### Node Configuration - -The launch file starts the following nodes: - -1. **MAVROS** - MAVLink communication with PX4 -2. **robot_interface_node** - Robot interface abstraction -3. **position_setpoint_pub.py** - Position setpoint publisher -4. **odometry_conversion** - Converts odometry topics and publishes TF -5. **drone_safety_monitor** - Safety monitoring +One launch argument: `interface_odometry_in_topic` — the odometry topic +remapped into `odometry_conversion` (default +`/$ROBOT_NAME/interface/mavros/local_position/odom`). -All nodes maintain the same configuration and remapping as the original XML launch file. +The drone safety monitor is NOT launched here — the stack entry files launch it +(`drone_safety_monitor.launch.xml`). diff --git a/robot/ros_ws/src/interface/interface_bringup/launch/interface.launch.py b/robot/ros_ws/src/interface/interface_bringup/launch/interface.launch.py index 1b1636424..da0f28416 100644 --- a/robot/ros_ws/src/interface/interface_bringup/launch/interface.launch.py +++ b/robot/ros_ws/src/interface/interface_bringup/launch/interface.launch.py @@ -1,6 +1,11 @@ #!/usr/bin/env python3 """ROS2 Python launch file for interface bringup. +Launches MAVROS (via mavros_px4.launch.xml), the robot_interface node with the +MAVROS plugin, the position setpoint publisher, and odometry conversion. +Included by: stacks/*/launch entry files — the canonical interface bringup +(wrapped by design until the platform-module refactor, RFC #380 Part 2). + Dynamically computes FCU URL and TGT_SYSTEM from environment variables: OFFBOARD_PORT = OFFBOARD_BASE_PORT + ROS_DOMAIN_ID ONBOARD_PORT = ONBOARD_BASE_PORT + ROS_DOMAIN_ID @@ -122,7 +127,8 @@ def launch_setup(context, *args, **kwargs): ) actions.append(odometry_conversion_node) - # NOTE: drone_safety_monitor is now launched from behavior_bringup + # NOTE: drone_safety_monitor is launched by the stack entry files + # (drone_safety_monitor.launch.xml), not here return actions diff --git a/robot/ros_ws/src/interface/interface_bringup/launch/mavros_px4.launch.xml b/robot/ros_ws/src/interface/interface_bringup/launch/mavros_px4.launch.xml index decb1ad11..5117034c2 100644 --- a/robot/ros_ws/src/interface/interface_bringup/launch/mavros_px4.launch.xml +++ b/robot/ros_ws/src/interface/interface_bringup/launch/mavros_px4.launch.xml @@ -1,23 +1,37 @@ + - - - - - - - - + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/robot/ros_ws/src/interface/interface_bringup/package.xml b/robot/ros_ws/src/interface/interface_bringup/package.xml index 415a2be59..16202ef4b 100644 --- a/robot/ros_ws/src/interface/interface_bringup/package.xml +++ b/robot/ros_ws/src/interface/interface_bringup/package.xml @@ -3,9 +3,9 @@ interface_bringup 0.0.0 - TODO: Package description - andrew - Apache-2.0 + Bringup package that launches the AirStack interface layer: the robot interface node and MAVROS for PX4. + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/interface/mavros_interface/CMakeLists.txt b/robot/ros_ws/src/interface/mavros_interface/CMakeLists.txt index c4605217d..f1a33872f 100644 --- a/robot/ros_ws/src/interface/mavros_interface/CMakeLists.txt +++ b/robot/ros_ws/src/interface/mavros_interface/CMakeLists.txt @@ -84,6 +84,4 @@ ament_export_targets( export_${PROJECT_NAME} ) -install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) - ament_package() diff --git a/robot/ros_ws/src/interface/mavros_interface/launch/README.md b/robot/ros_ws/src/interface/mavros_interface/launch/README.md deleted file mode 100644 index 94ed10a02..000000000 --- a/robot/ros_ws/src/interface/mavros_interface/launch/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# MAVROS Connection Polling Launch File - -This directory contains a Python launch file that continuously polls for a mavlink connection and launches `px4.launch` once the connection is established. - -## File Overview - -### `mavros_connection_poll.launch.py` (OpaqueFunction Approach) - -This implementation uses ROS2's `OpaqueFunction` to handle connection polling and dynamic launch actions. - -**Features:** -- Comprehensive URL parsing (TCP, UDP, Serial) -- Configurable polling intervals and timeouts -- Built-in connection checking for different protocols -- Thread-based polling to avoid blocking the launch process -- Automatic MAVROS launch when connection is established - -**Usage:** -```bash -export MAVROS_FCU_URL="tcpin:localhost:4560" -ros2 launch mavros_interface mavros_connection_poll.launch.py - -# With custom max wait time -ros2 launch mavros_interface mavros_connection_poll.launch.py max_wait_time:=120.0 - -# With multiple custom arguments -ros2 launch mavros_interface mavros_connection_poll.launch.py \ - max_wait_time:=90.0 \ - polling_interval:=0.5 -``` - -## Supported FCU URL Formats - -All implementations support the following FCU URL formats: - -### TCP Connections -- `tcpin:localhost:4560` - TCP input connection -- `tcp://localhost:4560` - Standard TCP URL format - -### UDP Connections -- `udp://localhost:14540` - Simple UDP connection -- `udp://:14540@172.31.0.200:14580` - UDP with local and remote ports - -### Serial Connections -- `/dev/ttyTHS4:115200` - Serial device connection -- `/dev/ttyUSB0:57600` - USB serial connection - -## Environment Variables - -### Optional -- `ROS_DOMAIN_ID` - Used for robot identification -- `ROBOT_NAME` - Used for namespacing - -## Launch Arguments - -- `polling_interval` (default: 1.0s) - How often to check connection -- `max_wait_time` (default: 60s) - Maximum time in seconds to wait for mavlink connection -- `MAVROS_FCU_URL` - The FCU connection URL (see formats above) - -## Integration with AirStack - -This launch file is designed to work with the AirStack interface system: - -1. **Set the FCU URL**: Configure `MAVROS_FCU_URL` in your robot's `.bashrc` -2. **Launch Interface**: Use this launch file instead of directly launching MAVROS -3. **Connection Handling**: The system will wait for the mavlink endpoint to be available -4. **Automatic Launch**: MAVROS will be launched automatically when connection is established - -## Example Usage Scenarios - -### Scenario 1: Pegasus Isaac Sim Integration -```bash -# Robot 0 -export ROS_DOMAIN_ID=0 -ros2 launch mavros_interface mavros_connection_poll.launch.py fcu_url:="tcpin:localhost:4560" - -# Robot 1 -export ROS_DOMAIN_ID=1 -export MAVROS_FCU_URL="tcpin:localhost:4561" -ros2 launch mavros_interface mavros_connection_poll.launch.py -``` - -### Scenario 2: Real Hardware -```bash -ros2 launch mavros_interface mavros_connection_poll.launch.py connection_timeout:=30.0 fcu_url:="/dev/ttyTHS4:115200" -``` - -### Scenario 3: SITL Development -```bash -ros2 launch mavros_interface mavros_connection_poll.launch.py fcu_url:="$MAVROS_FCU_URL" -``` - -## Troubleshooting - -### Connection Issues -1. Verify `MAVROS_FCU_URL` is set correctly -2. Check that the target endpoint is reachable -3. For serial connections, verify device permissions -4. Increase `connection_timeout` for slow-starting systems - -### Launch Issues -1. Ensure MAVROS package is installed -2. Check ROS2 workspace is sourced -3. Verify launch file permissions -4. Review ROS2 logs for detailed error messages diff --git a/robot/ros_ws/src/interface/mavros_interface/launch/mavros_connection_poll.launch.py b/robot/ros_ws/src/interface/mavros_interface/launch/mavros_connection_poll.launch.py deleted file mode 100644 index a912efb00..000000000 --- a/robot/ros_ws/src/interface/mavros_interface/launch/mavros_connection_poll.launch.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 - -""" -ROS2 Python Launch file for MAVROS interface with connection polling. -This launch file continuously polls for a mavlink connection using pymavlink -to check for heartbeat messages and launches px4.launch once the -connection is established. -""" - -import time -from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, OpaqueFunction -from launch.substitutions import LaunchConfiguration -from launch_ros.actions import PushRosNamespace -from launch_ros.substitutions import FindPackageShare -from launch.launch_description_sources import AnyLaunchDescriptionSource - -from pymavlink import mavutil - - -def wait_for_connection_and_launch(context): - """Wait for mavlink connection and then launch px4.launch.""" - - # Get the MAVROS FCU URL from launch configuration - fcu_url = LaunchConfiguration('fcu_url').perform(context) - - if not fcu_url: - print("ERROR: fcu_url launch argument is not set!") - return [] - - # Get launch configurations from context - heartbeat_timeout = float(LaunchConfiguration('heartbeat_timeout').perform(context)) - - mavlink_url = fcu_url.split("@")[0].replace("udp://", "udpin:") + fcu_url.split("@")[1] if "@" in fcu_url else "" - - print(f"Starting mavlink heartbeat polling for: {mavlink_url}") - if heartbeat_timeout < 0: - print(f"Heartbeat timeout: infinite (will wait indefinitely)") - else: - print(f"Heartbeat timeout: {heartbeat_timeout} seconds") - - # Poll for heartbeat with single timeout loop - start_time = time.time() - print(f"Polling for mavlink heartbeat from: {mavlink_url}") - - connection = None - tgt_system, tgt_component = None, None - - - while heartbeat_timeout < 0 or (time.time() - start_time) < heartbeat_timeout: - try: - # Create connection once - if connection is None: - connection = mavutil.mavlink_connection(mavlink_url) - - msg = connection.recv_match(type='HEARTBEAT', blocking=False, timeout=1) - - if msg: - # Check the message type to identify a heartbeat - if msg.get_type() == 'HEARTBEAT': - print("Heartbeat message received!") - print(f"MAV_TYPE: {msg.type}, MAV_AUTOPILOT: {msg.autopilot}") - - print("Requesting autopilot version...") - connection.mav.command_long_send( - connection.target_system, # Target system - connection.target_component, # Target component - mavutil.mavlink.MAV_CMD_REQUEST_AUTOPILOT_CAPABILITIES, # Command ID - 0, # Confirmation - 0, # Parameter 1 - 0, # Parameter 2 - 0, # Parameter 3 - 0, # Parameter 4 - 0, # Parameter 5 - 0, # Parameter 6 - 0 # Parameter 7 - ) - - # wait for a response and print it - msg = connection.recv_match(type='AUTOPILOT_VERSION', blocking=True, timeout=10) - if msg: - print("Autopilot version message received!") - print(msg) - else: - print("No autopilot version message received within timeout.") - exit(1) - - tgt_system = connection.target_system - tgt_component = connection.target_component - print(f"Target System: {tgt_system}, Target Component: {tgt_component}") - - connection.close() - break - print(f"Waiting for mavlink heartbeat from: {mavlink_url}") - except Exception as e: - print(f"Waiting for connection {time.time() - start_time:.1f}s") - time.sleep(1) - - print(f"Heartbeat received! Launching MAVROS px4.launch on {fcu_url}") - - # Create the MAVROS launch action - mavros_launch = [ - PushRosNamespace('interface'), - IncludeLaunchDescription( - AnyLaunchDescriptionSource([ - FindPackageShare('mavros'), '/launch/px4.launch' - ]), - launch_arguments={ - 'fcu_url': fcu_url, - # "tgt_system": str(tgt_system), - # "tgt_component": str(tgt_component) - }.items() - ) - ] - - return mavros_launch - - -def generate_launch_description(): - """Generate the launch description.""" - - # Declare launch arguments - fcu_url_arg = DeclareLaunchArgument( - 'fcu_url', - default_value='', - description='FCU connection URL (e.g., udp://localhost:14540, tcp://localhost:5760, /dev/ttyUSB0:57600)' - ) - - heartbeat_timeout_arg = DeclareLaunchArgument( - 'heartbeat_timeout', - default_value='-1.0', - description='Timeout in seconds to wait for mavlink heartbeat (use negative value for infinite waiting)' - ) - - # Use OpaqueFunction to handle the connection polling and launch - connection_and_launch = OpaqueFunction(function=wait_for_connection_and_launch) - - return LaunchDescription([ - # Launch arguments - fcu_url_arg, - heartbeat_timeout_arg, - - # Connection polling and MAVROS launch - connection_and_launch, - ]) diff --git a/robot/ros_ws/src/interface/mavros_interface/package.xml b/robot/ros_ws/src/interface/mavros_interface/package.xml index c7459043f..6c67c5b43 100644 --- a/robot/ros_ws/src/interface/mavros_interface/package.xml +++ b/robot/ros_ws/src/interface/mavros_interface/package.xml @@ -3,9 +3,9 @@ mavros_interface 0.0.0 - TODO: Package description - uav - TODO: License declaration + MAVROS-based implementation of the AirStack robot interface for PX4 flight controllers: arming, mode switching, and command/setpoint forwarding. + Andrew Jong + BSD-3-Clause-Clear ament_cmake_ros diff --git a/robot/ros_ws/src/interface/px4_interface/CMakeLists.txt b/robot/ros_ws/src/interface/px4_interface/CMakeLists.txt deleted file mode 100644 index 4567d39a7..000000000 --- a/robot/ros_ws/src/interface/px4_interface/CMakeLists.txt +++ /dev/null @@ -1,67 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(px4_interface) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# ── Dependencies ──────────────────────────────────────────────────────────── -find_package(ament_cmake REQUIRED) -find_package(ament_cmake_ros REQUIRED) -find_package(robot_interface REQUIRED) -find_package(pluginlib REQUIRED) -find_package(rclcpp REQUIRED) -find_package(px4_msgs REQUIRED) -find_package(nav_msgs REQUIRED) -find_package(geometry_msgs REQUIRED) -find_package(mav_msgs REQUIRED) -find_package(tf2 REQUIRED) - -# ── Library ───────────────────────────────────────────────────────────────── -add_library(px4_interface src/px4_interface.cpp) - -target_compile_features(px4_interface PUBLIC cxx_std_17) - -target_include_directories(px4_interface PUBLIC - $ - $) - -ament_target_dependencies(px4_interface - robot_interface - pluginlib - rclcpp - px4_msgs - nav_msgs - geometry_msgs - mav_msgs - tf2) - -pluginlib_export_plugin_description_file(robot_interface plugins.xml) - -# ── Install ────────────────────────────────────────────────────────────────── -install( - DIRECTORY include/ - DESTINATION include) - -install( - TARGETS px4_interface - EXPORT export_${PROJECT_NAME} - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin) - -install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) - -# ── Export ─────────────────────────────────────────────────────────────────── -ament_export_include_directories(include) -ament_export_libraries(px4_interface) -ament_export_targets(export_${PROJECT_NAME}) - -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - set(ament_cmake_copyright_FOUND TRUE) - set(ament_cmake_cpplint_FOUND TRUE) - ament_lint_auto_find_test_dependencies() -endif() - -ament_package() diff --git a/robot/ros_ws/src/interface/px4_interface/include/px4_interface/px4_interface.hpp b/robot/ros_ws/src/interface/px4_interface/include/px4_interface/px4_interface.hpp deleted file mode 100644 index 19597e8e4..000000000 --- a/robot/ros_ws/src/interface/px4_interface/include/px4_interface/px4_interface.hpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// This file is developed as part of software from the AirLab at the Robotics -// Institute at Carnegie Mellon University (https://theairlab.org). -// -// SPDX-License-Identifier: MIT - -#pragma once - -// The PX4Interface class is fully defined in px4_interface.cpp as a pluginlib -// plugin. This header is intentionally empty — it exists only to satisfy the -// CMake install(DIRECTORY include/ ...) target and to keep the package -// structure consistent with other RobotInterface implementations. diff --git a/robot/ros_ws/src/interface/px4_interface/launch/px4_interface.launch.xml b/robot/ros_ws/src/interface/px4_interface/launch/px4_interface.launch.xml deleted file mode 100644 index a61891088..000000000 --- a/robot/ros_ws/src/interface/px4_interface/launch/px4_interface.launch.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/interface/px4_interface/package.xml b/robot/ros_ws/src/interface/px4_interface/package.xml deleted file mode 100644 index a48a77bb8..000000000 --- a/robot/ros_ws/src/interface/px4_interface/package.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - px4_interface - 0.0.0 - - RobotInterface plugin that interfaces directly with PX4 via uXRCE-DDS - ROS topics, bypassing MAVROS. Handles ENU↔NED and FLU↔FRD frame - conversions for all control and odometry messages. - - uav - MIT - - ament_cmake_ros - - robot_interface - pluginlib - rclcpp - px4_msgs - nav_msgs - geometry_msgs - mav_msgs - tf2 - - ament_lint_auto - ament_lint_common - - - ament_cmake - - diff --git a/robot/ros_ws/src/interface/px4_interface/plugins.xml b/robot/ros_ws/src/interface/px4_interface/plugins.xml deleted file mode 100644 index 269bf362e..000000000 --- a/robot/ros_ws/src/interface/px4_interface/plugins.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - RobotInterface plugin for PX4 via uXRCE-DDS. - Handles ENU/FLU ↔ NED/FRD frame conversions and publishes directly to - /fmu/in/* topics without MAVROS. - - - diff --git a/robot/ros_ws/src/interface/px4_interface/src/px4_interface.cpp b/robot/ros_ws/src/interface/px4_interface/src/px4_interface.cpp deleted file mode 100644 index 1f9301184..000000000 --- a/robot/ros_ws/src/interface/px4_interface/src/px4_interface.cpp +++ /dev/null @@ -1,660 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -/** - * @file px4_interface.cpp - * @author AirLab @ CMU - * @brief RobotInterface implementation that interfaces directly with PX4 via - * uXRCE-DDS ROS topics (no MAVROS). - * - * ## Frame conventions - * - * AirStack / ROS uses: - * - World frame: ENU (x = East, y = North, z = Up) - * - Body frame: FLU (x = Forward, y = Left, z = Up) - * - * PX4 uXRCE-DDS uses: - * - World frame: NED (x = North, y = East, z = Down) - * - Body frame: FRD (x = Forward, y = Right, z = Down) - * - * Position / velocity conversion (ENU <-> NED): - * NED_x = ENU_y, NED_y = ENU_x, NED_z = -ENU_z - * - * Body-rate conversion (FLU <-> FRD): - * FRD_roll = FLU_roll, FRD_pitch = -FLU_pitch, FRD_yaw = -FLU_yaw - * - * Attitude quaternion conversion (q_FLU_ENU <-> q_FRD_NED): - * q_px4 = q_NED_ENU ⊗ q_ros ⊗ q_FLU_FRD - * q_ros = q_ENU_NED ⊗ q_px4 ⊗ q_FRD_FLU - * where - * q_NED_ENU = [w=0, x=1/√2, y=1/√2, z=0] (180° around axis (1,1,0)/√2) - * q_FLU_FRD = [w=0, x=1, y=0, z=0] (180° around body-x) - * q_ENU_NED = conj(q_NED_ENU) — but since w=0 this equals -q_NED_ENU, - * which represents the same rotation; implementation uses the - * product formula with negated x,y directly. - * - * ## Topic mapping (no /fmu/ prefix — push that namespace in the launch file) - * - * Commands → PX4: - * in/offboard_control_mode ← heartbeat, mode selection - * in/trajectory_setpoint ← position / velocity setpoint - * in/vehicle_attitude_setpoint ← attitude setpoint - * in/vehicle_rates_setpoint ← body-rate setpoint - * in/vehicle_command ← arm / disarm / set-mode / takeoff / land - * in/vehicle_visual_odometry ← visual / mocap odometry fusion input - * - * Feedback ← PX4: - * out/vehicle_status → arm state, nav state - * out/vehicle_odometry → odometry (republished as nav_msgs/Odometry in ENU) - */ - -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace px4_interface -{ - -// --------------------------------------------------------------------------- -// Quaternion helpers (all quaternions as [w, x, y, z]) -// --------------------------------------------------------------------------- - -/// Multiply two quaternions: result = a ⊗ b -static inline std::array qmul(double aw, double ax, double ay, double az, - double bw, double bx, double by, double bz) -{ - return {aw * bw - ax * bx - ay * by - az * bz, - aw * bx + ax * bw + ay * bz - az * by, - aw * by - ax * bz + ay * bw + az * bx, - aw * bz + ax * by - ay * bx + az * bw}; -} - -// Fixed frame-conversion quaternions (compile-time constants). -// q_NED_ENU = [w=0, x=1/√2, y=1/√2, z=0] -// q_FLU_FRD = [w=0, x=1, y=0, z=0] (same as q_FRD_FLU, self-inverse) -static constexpr double kSqrt2Inv = 0.70710678118654752; // 1/√2 - -/// Convert an AirStack quaternion (FLU→ENU) to a PX4 quaternion (FRD→NED). -/// q_px4 = q_NED_ENU ⊗ q_ros ⊗ q_FLU_FRD -static inline std::array enu_flu_to_ned_frd(double qw, double qx, - double qy, double qz) -{ - // Step 1: q_NED_ENU ⊗ q_ros (q_NED_ENU = [0, 1/√2, 1/√2, 0]) - auto tmp = qmul(0.0, kSqrt2Inv, kSqrt2Inv, 0.0, qw, qx, qy, qz); - // Step 2: result ⊗ q_FLU_FRD (q_FLU_FRD = [0, 1, 0, 0]) - return qmul(tmp[0], tmp[1], tmp[2], tmp[3], 0.0, 1.0, 0.0, 0.0); -} - -/// Convert a PX4 quaternion (FRD→NED) to an AirStack quaternion (FLU→ENU). -/// q_ros = q_ENU_NED ⊗ q_px4 ⊗ q_FRD_FLU -/// where q_ENU_NED = conj(q_NED_ENU) = [0, -1/√2, -1/√2, 0] -/// and q_FRD_FLU = q_FLU_FRD = [0, 1, 0, 0] (self-inverse rotation) -static inline std::array ned_frd_to_enu_flu(double qw, double qx, - double qy, double qz) -{ - // Step 1: q_ENU_NED ⊗ q_px4 - auto tmp = qmul(0.0, -kSqrt2Inv, -kSqrt2Inv, 0.0, qw, qx, qy, qz); - // Step 2: result ⊗ q_FRD_FLU (= q_FLU_FRD = [0, 1, 0, 0]) - return qmul(tmp[0], tmp[1], tmp[2], tmp[3], 0.0, 1.0, 0.0, 0.0); -} - -// --------------------------------------------------------------------------- -// PX4Interface class -// --------------------------------------------------------------------------- - -class PX4Interface : public robot_interface::RobotInterface -{ -public: - PX4Interface() : RobotInterface("px4_interface") - { - // Use BEST_EFFORT + VOLATILE to match PX4's default uXRCE-DDS QoS. - auto qos = rclcpp::QoS(rclcpp::KeepLast(1)) - .best_effort() - .durability_volatile(); - - // ---- Publishers → PX4 ---- - offboard_mode_pub_ = - this->create_publisher( - "in/offboard_control_mode", qos); - - trajectory_sp_pub_ = - this->create_publisher( - "in/trajectory_setpoint", qos); - - attitude_sp_pub_ = - this->create_publisher( - "in/vehicle_attitude_setpoint", qos); - - rates_sp_pub_ = - this->create_publisher( - "in/vehicle_rates_setpoint", qos); - - vehicle_cmd_pub_ = - this->create_publisher( - "in/vehicle_command", qos); - - visual_odom_pub_ = - this->create_publisher( - "in/vehicle_visual_odometry", qos); - - // ---- Subscribers ← PX4 ---- - vehicle_status_sub_ = - this->create_subscription( - "out/vehicle_status", qos, - std::bind(&PX4Interface::on_vehicle_status, this, - std::placeholders::_1)); - - vehicle_odom_sub_ = - this->create_subscription( - "out/vehicle_odometry", qos, - std::bind(&PX4Interface::on_vehicle_odometry, this, - std::placeholders::_1)); - - // ---- AirStack odometry output ---- - odometry_pub_ = - this->create_publisher("odometry", 10); - - // ---- Optional: visual odometry input from AirStack ---- - visual_odom_in_sub_ = - this->create_subscription( - "visual_odometry_in", 10, - std::bind(&PX4Interface::on_visual_odometry_in, this, - std::placeholders::_1)); - - // ---- Heartbeat timer at 10 Hz ---- - // PX4 requires offboard_control_mode to be published at ≥ 2 Hz while - // in offboard mode. We publish at 10 Hz to guarantee margin. - heartbeat_timer_ = this->create_wall_timer( - std::chrono::milliseconds(100), - std::bind(&PX4Interface::publish_offboard_heartbeat, this)); - - RCLCPP_INFO(this->get_logger(), "PX4Interface initialized (uXRCE-DDS)"); - } - - virtual ~PX4Interface() = default; - - // ----------------------------------------------------------------------- - // RobotInterface: control-command callbacks - // ----------------------------------------------------------------------- - - /** - * @brief Position setpoint (ENU → NED). - * - * Yaw convention: - * yaw_ned = π/2 − yaw_enu - * (ENU yaw 0 = East; NED yaw 0 = North → when ENU yaw = 90° the vehicle - * points North, which is NED yaw 0.) - */ - void pose_callback(const geometry_msgs::msg::PoseStamped::SharedPtr cmd) override - { - set_control_mode(ControlMode::POSITION); - publish_offboard_heartbeat(); - - px4_msgs::msg::TrajectorySetpoint sp{}; - sp.timestamp = now_us(); - - // Position: ENU → NED - sp.position[0] = static_cast(cmd->pose.position.y); // N = ENU-y - sp.position[1] = static_cast(cmd->pose.position.x); // E = ENU-x - sp.position[2] = static_cast(-cmd->pose.position.z); // D = -ENU-z - - // Velocity and acceleration unused → NaN - sp.velocity[0] = sp.velocity[1] = sp.velocity[2] = NAN; - sp.acceleration[0] = sp.acceleration[1] = sp.acceleration[2] = NAN; - - // Yaw: extract ENU yaw then convert to NED yaw - tf2::Quaternion q_enu(cmd->pose.orientation.x, cmd->pose.orientation.y, - cmd->pose.orientation.z, cmd->pose.orientation.w); - double roll{}, pitch{}, yaw_enu{}; - tf2::Matrix3x3(q_enu).getRPY(roll, pitch, yaw_enu); - sp.yaw = static_cast(M_PI_2 - yaw_enu); - sp.yawspeed = NAN; - - trajectory_sp_pub_->publish(sp); - } - - /** - * @brief Velocity setpoint (ENU → NED). - * - * Yaw-rate sign: ENU CCW positive → NED CW positive → negate. - */ - void velocity_callback(const geometry_msgs::msg::TwistStamped::SharedPtr cmd) override - { - set_control_mode(ControlMode::VELOCITY); - publish_offboard_heartbeat(); - - px4_msgs::msg::TrajectorySetpoint sp{}; - sp.timestamp = now_us(); - - // Position unused → NaN - sp.position[0] = sp.position[1] = sp.position[2] = NAN; - - // Velocity: ENU → NED - sp.velocity[0] = static_cast(cmd->twist.linear.y); // vN = vEast (ENU-y) - sp.velocity[1] = static_cast(cmd->twist.linear.x); // vE = vNorth (ENU-x) - sp.velocity[2] = static_cast(-cmd->twist.linear.z); // vD = -vUp - - sp.acceleration[0] = sp.acceleration[1] = sp.acceleration[2] = NAN; - sp.yaw = NAN; - - // Yaw-rate: ENU CCW+ → NED CW+ → negate - sp.yawspeed = static_cast(-cmd->twist.angular.z); - - trajectory_sp_pub_->publish(sp); - } - - /** - * @brief Attitude + thrust setpoint. - * - * Quaternion convention: - * q_px4 = q_NED_ENU ⊗ q_ros ⊗ q_FLU_FRD - * - * Thrust convention: - * mav_msgs thrust.z is normalized [0,1] (positive = up in FLU). - * PX4 thrust_body[2] is negative-FRD (negative = up/thrust direction). - * So thrust_body[2] = −thrust.z. - */ - void attitude_thrust_callback( - const mav_msgs::msg::AttitudeThrust::SharedPtr cmd) override - { - set_control_mode(ControlMode::ATTITUDE); - publish_offboard_heartbeat(); - - px4_msgs::msg::VehicleAttitudeSetpoint sp{}; - sp.timestamp = now_us(); - - auto q = enu_flu_to_ned_frd(cmd->attitude.w, cmd->attitude.x, - cmd->attitude.y, cmd->attitude.z); - sp.q_d[0] = static_cast(q[0]); // w - sp.q_d[1] = static_cast(q[1]); // x - sp.q_d[2] = static_cast(q[2]); // y - sp.q_d[3] = static_cast(q[3]); // z - - sp.thrust_body[0] = 0.0f; - sp.thrust_body[1] = 0.0f; - sp.thrust_body[2] = static_cast(-cmd->thrust.z); - - attitude_sp_pub_->publish(sp); - } - - /** - * @brief Body-rate + thrust setpoint. - * - * PX4 VehicleRatesSetpoint is in body FRD frame. - * AirStack mav_msgs rates are in body FLU frame. - * roll (around x): same direction - * pitch (around y): negated (FLU-y = −FRD-y) - * yaw (around z): negated (FLU-z = −FRD-z) - */ - void rate_thrust_callback( - const mav_msgs::msg::RateThrust::SharedPtr cmd) override - { - set_control_mode(ControlMode::BODY_RATE); - publish_offboard_heartbeat(); - - px4_msgs::msg::VehicleRatesSetpoint sp{}; - sp.timestamp = now_us(); - - sp.roll = static_cast(cmd->angular_rates.x); - sp.pitch = static_cast(-cmd->angular_rates.y); - sp.yaw = static_cast(-cmd->angular_rates.z); - - sp.thrust_body[0] = 0.0f; - sp.thrust_body[1] = 0.0f; - sp.thrust_body[2] = static_cast(-cmd->thrust.z); - - rates_sp_pub_->publish(sp); - } - - /** - * @brief Roll / pitch / yaw-rate + thrust setpoint. - * - * Same body-rate axis convention as rate_thrust_callback. - * Note: cmd->roll and cmd->pitch are angles (rad), not rates — pitch is - * negated for the same reason as pitch rate. - */ - void roll_pitch_yawrate_thrust_callback( - const mav_msgs::msg::RollPitchYawrateThrust::SharedPtr cmd) override - { - set_control_mode(ControlMode::BODY_RATE); - publish_offboard_heartbeat(); - - px4_msgs::msg::VehicleRatesSetpoint sp{}; - sp.timestamp = now_us(); - - sp.roll = static_cast(cmd->roll); - sp.pitch = static_cast(-cmd->pitch); - sp.yaw = static_cast(-cmd->yaw_rate); - - sp.thrust_body[0] = 0.0f; - sp.thrust_body[1] = 0.0f; - sp.thrust_body[2] = static_cast(-cmd->thrust.z); - - rates_sp_pub_->publish(sp); - } - - // ----------------------------------------------------------------------- - // RobotInterface: command functions - // ----------------------------------------------------------------------- - - /** - * @brief Request offboard control mode from PX4. - * - * Sends VEHICLE_CMD_DO_SET_MODE with: - * param1 = 1 (MAV_MODE_FLAG_CUSTOM_MODE_ENABLED) - * param2 = 6 (PX4_CUSTOM_MAIN_MODE_OFFBOARD) - * - * The offboard heartbeat (offboard_control_mode + setpoint) must already - * be flowing before calling this, otherwise PX4 will reject the switch. - */ - bool request_control() override - { - send_vehicle_command( - px4_msgs::msg::VehicleCommand::VEHICLE_CMD_DO_SET_MODE, - 1.0f, // param1: enable custom mode - 6.0f // param2: OFFBOARD - ); - RCLCPP_INFO(this->get_logger(), "Offboard mode requested."); - return true; - } - - /// Arm the vehicle via VEHICLE_CMD_COMPONENT_ARM_DISARM (param1 = 1). - bool arm() override - { - send_vehicle_command( - px4_msgs::msg::VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, - 1.0f); - RCLCPP_INFO(this->get_logger(), "Arm command sent."); - return true; - } - - /// Disarm the vehicle via VEHICLE_CMD_COMPONENT_ARM_DISARM (param1 = 0). - bool disarm() override - { - send_vehicle_command( - px4_msgs::msg::VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, - 0.0f); - RCLCPP_INFO(this->get_logger(), "Disarm command sent."); - return true; - } - - bool is_armed() override - { - return status_received_ && - vehicle_status_.arming_state == - px4_msgs::msg::VehicleStatus::ARMING_STATE_ARMED; - } - - bool has_control() override - { - return status_received_ && - vehicle_status_.nav_state == - px4_msgs::msg::VehicleStatus::NAVIGATION_STATE_OFFBOARD; - } - - /// Send NAV_TAKEOFF command (PX4 will use the configured takeoff altitude). - bool takeoff() override - { - send_vehicle_command( - px4_msgs::msg::VehicleCommand::VEHICLE_CMD_NAV_TAKEOFF); - RCLCPP_INFO(this->get_logger(), "Takeoff command sent."); - return true; - } - - /// Send NAV_LAND command. - bool land() override - { - send_vehicle_command( - px4_msgs::msg::VehicleCommand::VEHICLE_CMD_NAV_LAND); - RCLCPP_INFO(this->get_logger(), "Land command sent."); - return true; - } - -private: - // ----------------------------------------------------------------------- - // Internal types - // ----------------------------------------------------------------------- - - enum class ControlMode : uint8_t - { - NONE = 0, - POSITION = 1, - VELOCITY = 2, - ATTITUDE = 3, - BODY_RATE = 4, - }; - - // ----------------------------------------------------------------------- - // Member variables - // ----------------------------------------------------------------------- - - ControlMode control_mode_{ControlMode::NONE}; - - px4_msgs::msg::VehicleStatus vehicle_status_{}; - bool status_received_{false}; - - // Publishers → PX4 - rclcpp::Publisher::SharedPtr offboard_mode_pub_; - rclcpp::Publisher::SharedPtr trajectory_sp_pub_; - rclcpp::Publisher::SharedPtr attitude_sp_pub_; - rclcpp::Publisher::SharedPtr rates_sp_pub_; - rclcpp::Publisher::SharedPtr vehicle_cmd_pub_; - rclcpp::Publisher::SharedPtr visual_odom_pub_; - - // Subscribers ← PX4 - rclcpp::Subscription::SharedPtr vehicle_status_sub_; - rclcpp::Subscription::SharedPtr vehicle_odom_sub_; - - // AirStack I/O - rclcpp::Publisher::SharedPtr odometry_pub_; - rclcpp::Subscription::SharedPtr visual_odom_in_sub_; - - // Heartbeat timer - rclcpp::TimerBase::SharedPtr heartbeat_timer_; - - // ----------------------------------------------------------------------- - // Helpers - // ----------------------------------------------------------------------- - - /// @return Microseconds since system start used by PX4. - uint64_t now_us() - { - return static_cast( - this->get_clock()->now().nanoseconds() / 1000ULL); - } - - void set_control_mode(ControlMode mode) { control_mode_ = mode; } - - /// Publish the offboard_control_mode heartbeat with the current mode flags. - void publish_offboard_heartbeat() - { - if (control_mode_ == ControlMode::NONE) return; - - px4_msgs::msg::OffboardControlMode msg{}; - msg.timestamp = now_us(); - msg.position = (control_mode_ == ControlMode::POSITION); - msg.velocity = (control_mode_ == ControlMode::VELOCITY); - msg.acceleration = false; - msg.attitude = (control_mode_ == ControlMode::ATTITUDE); - msg.body_rate = (control_mode_ == ControlMode::BODY_RATE); - offboard_mode_pub_->publish(msg); - } - - /** - * @brief Send a VehicleCommand to PX4. - * - * All unused params default to 0. target_system = 1 (the autopilot). - */ - void send_vehicle_command(uint32_t command, - float p1 = 0.f, float p2 = 0.f, - float p3 = 0.f, float p4 = 0.f, - double p5 = 0.0, double p6 = 0.0, - float p7 = 0.f) - { - px4_msgs::msg::VehicleCommand cmd{}; - cmd.timestamp = now_us(); - cmd.command = command; - cmd.param1 = p1; - cmd.param2 = p2; - cmd.param3 = p3; - cmd.param4 = p4; - cmd.param5 = p5; // float64 - cmd.param6 = p6; // float64 - cmd.param7 = p7; - cmd.target_system = 1; - cmd.target_component = 1; - cmd.source_system = 1; - cmd.source_component = 1; // uint16 - cmd.from_external = true; - vehicle_cmd_pub_->publish(cmd); - } - - // ----------------------------------------------------------------------- - // PX4 subscriber callbacks - // ----------------------------------------------------------------------- - - void on_vehicle_status( - const px4_msgs::msg::VehicleStatus::SharedPtr msg) - { - vehicle_status_ = *msg; - status_received_ = true; - } - - /** - * @brief Convert PX4 VehicleOdometry (NED/FRD) to nav_msgs/Odometry (ENU/FLU). - * - * The VehicleOdometry published on out/vehicle_odometry has: - * pose_frame = POSE_FRAME_NED - * velocity_frame = VELOCITY_FRAME_NED (linear vel in NED world frame) - * angular_velocity in body FRD frame - */ - void on_vehicle_odometry( - const px4_msgs::msg::VehicleOdometry::SharedPtr msg) - { - nav_msgs::msg::Odometry odom; - odom.header.stamp = this->get_clock()->now(); - odom.header.frame_id = "map"; - odom.child_frame_id = "base_link"; - - // Position: NED → ENU - odom.pose.pose.position.x = static_cast(msg->position[1]); // E = NED-y - odom.pose.pose.position.y = static_cast(msg->position[0]); // N = NED-x - odom.pose.pose.position.z = static_cast(-msg->position[2]); // U = -NED-z - - // Attitude quaternion: FRD→NED → FLU→ENU - auto q = ned_frd_to_enu_flu( - static_cast(msg->q[0]), // w - static_cast(msg->q[1]), // x - static_cast(msg->q[2]), // y - static_cast(msg->q[3])); // z - odom.pose.pose.orientation.w = q[0]; - odom.pose.pose.orientation.x = q[1]; - odom.pose.pose.orientation.y = q[2]; - odom.pose.pose.orientation.z = q[3]; - - // Linear velocity: NED world → ENU world - odom.twist.twist.linear.x = static_cast(msg->velocity[1]); // E = NED-vy - odom.twist.twist.linear.y = static_cast(msg->velocity[0]); // N = NED-vx - odom.twist.twist.linear.z = static_cast(-msg->velocity[2]); // U = -NED-vz - - // Angular velocity: FRD body → FLU body - odom.twist.twist.angular.x = static_cast(msg->angular_velocity[0]); // roll = same - odom.twist.twist.angular.y = -static_cast(msg->angular_velocity[1]); // pitch negated - odom.twist.twist.angular.z = -static_cast(msg->angular_velocity[2]); // yaw negated - - odometry_pub_->publish(odom); - } - - /** - * @brief Accept a nav_msgs/Odometry in ENU/FLU and forward it to PX4's - * visual-odometry fusion input (in/vehicle_visual_odometry) in NED/FRD. - * - * Enable external vision fusion in PX4 with EKF2_EV_CTRL. - */ - void on_visual_odometry_in( - const nav_msgs::msg::Odometry::SharedPtr msg) - { - px4_msgs::msg::VehicleOdometry vio{}; - vio.timestamp = now_us(); - vio.timestamp_sample = now_us(); - - // Position: ENU → NED - vio.position[0] = static_cast(msg->pose.pose.position.y); // N = ENU-y - vio.position[1] = static_cast(msg->pose.pose.position.x); // E = ENU-x - vio.position[2] = static_cast(-msg->pose.pose.position.z); // D = -ENU-z - - // Attitude quaternion: FLU→ENU → FRD→NED - auto q = enu_flu_to_ned_frd( - msg->pose.pose.orientation.w, - msg->pose.pose.orientation.x, - msg->pose.pose.orientation.y, - msg->pose.pose.orientation.z); - vio.q[0] = static_cast(q[0]); - vio.q[1] = static_cast(q[1]); - vio.q[2] = static_cast(q[2]); - vio.q[3] = static_cast(q[3]); - - // Velocity: ENU → NED (world frame) - vio.velocity[0] = static_cast(msg->twist.twist.linear.y); // vN = vENU-y - vio.velocity[1] = static_cast(msg->twist.twist.linear.x); // vE = vENU-x - vio.velocity[2] = static_cast(-msg->twist.twist.linear.z); // vD = -vENU-z - - vio.pose_frame = px4_msgs::msg::VehicleOdometry::POSE_FRAME_NED; - vio.velocity_frame = px4_msgs::msg::VehicleOdometry::VELOCITY_FRAME_NED; - - // Position variance (diagonal of 3×3 covariance) - vio.position_variance[0] = static_cast(msg->pose.covariance[0]); // xx - vio.position_variance[1] = static_cast(msg->pose.covariance[7]); // yy - vio.position_variance[2] = static_cast(msg->pose.covariance[14]); // zz - - // Orientation variance - vio.orientation_variance[0] = static_cast(msg->pose.covariance[21]); // roll - vio.orientation_variance[1] = static_cast(msg->pose.covariance[28]); // pitch - vio.orientation_variance[2] = static_cast(msg->pose.covariance[35]); // yaw - - visual_odom_pub_->publish(vio); - } -}; - -} // namespace px4_interface - -#include -PLUGINLIB_EXPORT_CLASS(px4_interface::PX4Interface, robot_interface::RobotInterface) diff --git a/robot/ros_ws/src/interface/robot_interface/CMakeLists.txt b/robot/ros_ws/src/interface/robot_interface/CMakeLists.txt index 5920ac965..d2c14d4b6 100644 --- a/robot/ros_ws/src/interface/robot_interface/CMakeLists.txt +++ b/robot/ros_ws/src/interface/robot_interface/CMakeLists.txt @@ -129,8 +129,6 @@ if(BUILD_TESTING) endif() # Install files. -install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) - # install(DIRECTORY rviz DESTINATION share/${PROJECT_NAME}) # install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) # install(DIRECTORY params DESTINATION share/${PROJECT_NAME}) diff --git a/robot/ros_ws/src/interface/robot_interface/launch/odometry_conversion.xml b/robot/ros_ws/src/interface/robot_interface/launch/odometry_conversion.xml deleted file mode 100644 index 384160a47..000000000 --- a/robot/ros_ws/src/interface/robot_interface/launch/odometry_conversion.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/interface/robot_interface/package.xml b/robot/ros_ws/src/interface/robot_interface/package.xml index 419091386..9e0a765aa 100644 --- a/robot/ros_ws/src/interface/robot_interface/package.xml +++ b/robot/ros_ws/src/interface/robot_interface/package.xml @@ -3,9 +3,9 @@ robot_interface 0.0.0 - TODO: Package description - uav - TODO: License declaration + Abstract robot interface defining the contract between the AirStack autonomy stack and flight-controller backends (commands, setpoints, odometry conversion). + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/interface/robot_interface/src/robot_interface_node.cpp b/robot/ros_ws/src/interface/robot_interface/src/robot_interface_node.cpp index 3aefb6497..6b7ba1a6f 100644 --- a/robot/ros_ws/src/interface/robot_interface/src/robot_interface_node.cpp +++ b/robot/ros_ws/src/interface/robot_interface/src/robot_interface_node.cpp @@ -128,7 +128,9 @@ int main(int argc, char** argv) { "robot_interface", "robot_interface::RobotInterface"); try { - ri = loader.createSharedInstance("mavros_interface::MAVROSInterface"); + // Instantiate the plugin selected by the "interface" parameter (read above); + // previously this hardcoded mavros_interface::MAVROSInterface, ignoring the param. + ri = loader.createSharedInstance(interface); // subscribers attitude_thrust_sub = ri->create_subscription( diff --git a/robot/ros_ws/src/local/controls/attitude_controller/CMakeLists.txt b/robot/ros_ws/src/local/controls/attitude_controller/CMakeLists.txt deleted file mode 100644 index 6fa04bb25..000000000 --- a/robot/ros_ws/src/local/controls/attitude_controller/CMakeLists.txt +++ /dev/null @@ -1,42 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(attitude_controller) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# find dependencies -find_package(ament_cmake REQUIRED) -# uncomment the following section in order to fill in -# further dependencies manually. -# find_package( REQUIRED) - -add_executable(attitude_controller src/attitude_controller.cpp) -target_include_directories(attitude_controller PUBLIC - $ - $) -target_compile_features(attitude_controller PUBLIC c_std_99 cxx_std_17) -install(TARGETS attitude_controller - DESTINATION lib/${PROJECT_NAME}) - -add_executable(controller_ekf src/controller_ekf.cpp) -target_include_directories(controller_ekf PUBLIC - $ - $) -target_compile_features(controller_ekf PUBLIC c_std_99 cxx_std_17) -install(TARGETS controller_ekf - DESTINATION lib/${PROJECT_NAME}) - -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - # the following line skips the linter which checks for copyrights - # comment the line when a copyright and license is added to all source files - set(ament_cmake_copyright_FOUND TRUE) - # the following line skips cpplint (only works in a git repo) - # comment the line when this package is in a git repo and when - # a copyright and license is added to all source files - set(ament_cmake_cpplint_FOUND TRUE) - ament_lint_auto_find_test_dependencies() -endif() - -ament_package() diff --git a/robot/ros_ws/src/local/controls/attitude_controller/COLCON_IGNORE b/robot/ros_ws/src/local/controls/attitude_controller/COLCON_IGNORE deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/attitude_controller.hpp b/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/attitude_controller.hpp deleted file mode 100644 index b16f972e4..000000000 --- a/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/attitude_controller.hpp +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef _DRONE_FLIGHT_CONTROL_H_ -#define _DRONE_FLIGHT_CONTROL_H_ - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include - -class DroneFlightControl : public BaseNode { -private: - - // params - std::string target_frame; - double p1, p2, p3, p1_volt, p2_volt; - double kp1, kp2, kp3, kp4, kd1, kd2, kd3, ki1_body, ki2_body, ki3_body, ki1_vel_body, ki2_vel_body, - ki1_ground, ki2_ground, ki3_ground, ki1_vel_ground, ki2_vel_ground, ki3_vel_ground, kf1, kf2, kf3; - - double command_angle_limit; - double i_angle_limit; - double z_i_limit; - double g, m, hover_throttle, hover_throttle_param; - double min_dt; - double low_thrust_duration, ekf_delay; - bool use_ekf_state, use_ekf_dist, thrust_sim; - double yawrate_limit; - double upper_bbox; - bool compensate_control; - - bool disturb_est_body_frame; - bool clip_disturbances, vel_only_stuck, zero_rp_stuck; - double attitude_stuck_limit, control_stuck_threshold, recover_threshold; - - // variables - bool got_odom, /*got_tracking_point,*/ got_closest_point, /*got_vtp,*/ got_accel, got_filtered_odom, got_filtered_state, - got_in_air, got_run_local_planner, got_vtp_jerk, got_battery_volt; - nav_msgs::msg::Odometry odom, prev_odom, /*tracking_point,*/ closest_point, /*vtp, */filtered_odom, prev_filtered_odom, px4_odom; - subt_control_ekf::kfState filtered_state; - std_msgs::msg::Bool run_local_planner; - airstack_msgs::Odometry vtp_jerk; - - double x_i_body, y_i_body, z_i_body, x_i_ground, y_i_ground, z_i_ground, vx_i_ground, vy_i_ground, vz_i_ground; - double vx_i_body, vy_i_body; - double x_e_prev, y_e_prev, z_e_prev; - ros::Time prev_time; - attitude_controller_msgs::AttitudeControllerDebug debug; - ros::Time low_thrust_start_time, start_time_ekf, lqr_start_time; - double boost_factor_angle_p; - std_msgs::Bool in_air; - double roll_px4_odom, pitch_px4_odom; - double battery_volt; - - templib::TimeOutMonitor* control_stuck_monitor; - templib::TimeOutMonitor* recover_monitor; - - // accel variables - tf2::Vector3 setpoint_vel_target_frame_prev; - tf2::Vector3 actual_accel_target_frame, longterm_accel_filter, shortterm_accel_filter, command_filtered_accel; - double accelAlpha; - double accelLongTermAlpha; - double targetDT; - double angle_tilt; - tf2::Vector3 gainAccel,alphaCommandAccel,betaAccel,maxAccel; - - // publishers - ros::Publisher command_pub, debug_pub, control_stuck_pub; - - // subscribers - tf::TransformListener* listener; - ros::Subscriber odometry_sub, /*tracking_point_sub,*/ closest_point_sub, /*vtp_sub,*/ imu_sub, in_air_sub, filtered_odometry_sub; - ros::Subscriber filtered_state_sub, pixhawk_odom_sub, run_local_planner_sub, vtp_jerk_sub, battery_volt_sub; - - // services - ros::ServiceServer reset_integrator_server; - - // callbacks - void odometry_callback(const nav_msgs::msg::Odometry::SharedPtr msg); - //void tracking_point_callback(nav_msgs::Odometry msg); - void closest_point_callback(const nav_msgs::msg::Odometry::SharedPtr msg); - void vtp_jerk_callback(const airstack_msgs::msg::Odometry::SharedPtr msg); - //void vtp_callback(nav_msgs::Odometry msg); - bool reset_integrator_callback(std_srvs::srv::Empty::Request& request, std_srvs::srv::Empty::Response& response); - void reset_integrators(); - void imu_callback(const sensor_msgs::msg::Imu::SharedPtr imu); - void in_air_callback(const std_msgs::msg::Bool::SharedPtr msg); - void filtered_odometry_callback(const nav_msgs::msg::Odometry::SharedPtr msg); - void filtered_state_callback(subt_control_ekf::kfState msg); - void pixhawk_odom_callback(const nav_msgs::msg::Odometry::SharedPtr msg); - void run_local_planner_callback(const std_msgs::msg::Bool::SharedPtr msg); - void battery_volt_callback(const sensor_msgs::msg::BatteryState::SharedPtr msg); - double thrustToThrottle(double thrust_normIn); - -public: - DroneFlightControl(std::string node_name); - - virtual bool initialize(); - virtual bool execute(); - virtual ~DroneFlightControl(); - -}; - - -#endif diff --git a/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/control_ekf.hpp b/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/control_ekf.hpp deleted file mode 100644 index fe9b7d5d1..000000000 --- a/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/control_ekf.hpp +++ /dev/null @@ -1,135 +0,0 @@ -#ifndef _SUBT_CONTROL_EKF_ -#define _SUBT_CONTROL_EKF_ - -#include -// #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "tf2/transform_datatypes.h" -#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" -#include "std_msgs/Bool.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace Eigen; - -typedef Matrix Matrix9d; -typedef Matrix Matrix6d; -typedef Matrix Matrix96d; -typedef Matrix Matrix69d; -typedef Matrix Vector9d; -typedef Matrix Vector6d; - -class EKFControl: public BaseNode{ - -private: - - //params - std::string target_frame; - double gravity, hover_throttle, hover_throttle_param; - double p1, p2, p3, p1_volt, p2_volt; - double init_cov_pos_xy, init_cov_pos_z, init_cov_vel_xy, init_cov_vel_z, init_cov_dis_xy, init_cov_dis_z; - double model_cov_pos_xy, model_cov_pos_z, model_cov_vel_xy, model_cov_vel_z, model_cov_dis_xy, model_cov_dis_z, - meas_cov_pos_xy, meas_cov_pos_z, meas_cov_vel_xy, meas_cov_vel_z; - double alpha_motor; - double reject_threshold, attitude_comp_lim, g_lim; - int sat_rc_limit; - - //variables - ros::Time prev_ekf_time, start_time_command; - double x_meas, y_meas, z_meas, yaw_meas, xdot_meas, ydot_meas, zdot_meas; - double roll_meas, pitch_meas; - - double x_prev, y_prev, z_prev, yaw_prev, xdot_prev, ydot_prev, zdot_prev, roll_comp_prev, pitch_comp_prev, - thrust_comp_prev, thrust_achieved_prev, roll_prev, pitch_prev, thrust_in_prev, thrust_command_prev; - - std::string tf_prefix; - - double x_ap, y_ap, z_ap, xdot_ap, ydot_ap, zdot_ap, roll_comp_ap, pitch_comp_ap, thrust_comp_ap; - subt_control_ekf::EKFControlDebug debug; - bool got_odom, got_command, got_in_air, got_px4_odom, got_battery_volt, got_ekf_active, got_rc_out; - nav_msgs::Odometry odom, prev_odom, px4_odom; - mav_msgs::RollPitchYawrateThrust command, prev_command; - std_msgs::Bool in_air, ekf_active; - mavros_msgs::RCOut rc_out; - - double targetDT, min_dt, min_command_dt; - double dt; - int odomCt, commandCt; - double roll_px4, pitch_px4, roll_px4_odom, pitch_px4_odom; - double battery_volt; - - // double P_x[2][2], P_y[2][2], P_z[2][2]; - // double Q_x[2][2], Q_y[2][2], Q_z[2][2]; - Matrix3d rot_mat; - Matrix3d dis_pos; - Matrix3d dis_vel; - Matrix3d dis_acc; - Matrix9d P; - Matrix9d Q; - Matrix6d R; - - Matrix9d A_model; - Matrix69d H; - - Vector9d state_ap; - Vector6d state_meas; - - bool disturb_est_on; - bool disturb_est_body_frame; - bool print_flag, thrust_sim; - int set_attitude; - tf::TransformBroadcaster* broadcaster; - - //publishers - ros::Publisher stateOut_pub; - ros::Publisher debug_pub; - ros::Publisher kfState_pub; - - //subscribers - tf::TransformListener* listener; - ros::Subscriber odometry_sub, command_sub, in_air_sub, pixhawk_imu_sub, pixhawk_odom_sub; - ros::Subscriber battery_volt_sub, ekf_active_sub, rc_out_sub; - - //services - - //callbacks - void odometry_callback(nav_msgs::Odometry msg); - void command_callback(mav_msgs::RollPitchYawrateThrust msg); - void in_air_callback(std_msgs::Bool msg); - // void pixhawk_imu_callback(sensor_msgs::Imu msg); - void pixhawk_odom_callback(nav_msgs::Odometry msg); - void battery_volt_callback(sensor_msgs::BatteryState msg); - void ekf_active_callback(std_msgs::Bool msg); - void rc_out_callback(mavros_msgs::RCOut msg); - - void calcAP( double dtIn, double thrust_In, double thrust_In_prev, double& x_ap_, - double& y_ap_, double& z_ap_, double& xdot_ap_, double& ydot_ap_, double& zdot_ap_, double roll_comp_ap_, - double pitch_comp_ap_, double thrust_comp_ap_, double roll, double pitch, double yaw); - - double throttleToThrust(double throttle); - -public: - EKFControl(std::string node_name); - - virtual bool initialize(); - virtual bool execute(); - virtual ~EKFControl(); -}; - - -#endif diff --git a/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/controller_ekf.hpp b/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/controller_ekf.hpp deleted file mode 100644 index fe9b7d5d1..000000000 --- a/robot/ros_ws/src/local/controls/attitude_controller/include/attitude_controller/controller_ekf.hpp +++ /dev/null @@ -1,135 +0,0 @@ -#ifndef _SUBT_CONTROL_EKF_ -#define _SUBT_CONTROL_EKF_ - -#include -// #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "tf2/transform_datatypes.h" -#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" -#include "std_msgs/Bool.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace Eigen; - -typedef Matrix Matrix9d; -typedef Matrix Matrix6d; -typedef Matrix Matrix96d; -typedef Matrix Matrix69d; -typedef Matrix Vector9d; -typedef Matrix Vector6d; - -class EKFControl: public BaseNode{ - -private: - - //params - std::string target_frame; - double gravity, hover_throttle, hover_throttle_param; - double p1, p2, p3, p1_volt, p2_volt; - double init_cov_pos_xy, init_cov_pos_z, init_cov_vel_xy, init_cov_vel_z, init_cov_dis_xy, init_cov_dis_z; - double model_cov_pos_xy, model_cov_pos_z, model_cov_vel_xy, model_cov_vel_z, model_cov_dis_xy, model_cov_dis_z, - meas_cov_pos_xy, meas_cov_pos_z, meas_cov_vel_xy, meas_cov_vel_z; - double alpha_motor; - double reject_threshold, attitude_comp_lim, g_lim; - int sat_rc_limit; - - //variables - ros::Time prev_ekf_time, start_time_command; - double x_meas, y_meas, z_meas, yaw_meas, xdot_meas, ydot_meas, zdot_meas; - double roll_meas, pitch_meas; - - double x_prev, y_prev, z_prev, yaw_prev, xdot_prev, ydot_prev, zdot_prev, roll_comp_prev, pitch_comp_prev, - thrust_comp_prev, thrust_achieved_prev, roll_prev, pitch_prev, thrust_in_prev, thrust_command_prev; - - std::string tf_prefix; - - double x_ap, y_ap, z_ap, xdot_ap, ydot_ap, zdot_ap, roll_comp_ap, pitch_comp_ap, thrust_comp_ap; - subt_control_ekf::EKFControlDebug debug; - bool got_odom, got_command, got_in_air, got_px4_odom, got_battery_volt, got_ekf_active, got_rc_out; - nav_msgs::Odometry odom, prev_odom, px4_odom; - mav_msgs::RollPitchYawrateThrust command, prev_command; - std_msgs::Bool in_air, ekf_active; - mavros_msgs::RCOut rc_out; - - double targetDT, min_dt, min_command_dt; - double dt; - int odomCt, commandCt; - double roll_px4, pitch_px4, roll_px4_odom, pitch_px4_odom; - double battery_volt; - - // double P_x[2][2], P_y[2][2], P_z[2][2]; - // double Q_x[2][2], Q_y[2][2], Q_z[2][2]; - Matrix3d rot_mat; - Matrix3d dis_pos; - Matrix3d dis_vel; - Matrix3d dis_acc; - Matrix9d P; - Matrix9d Q; - Matrix6d R; - - Matrix9d A_model; - Matrix69d H; - - Vector9d state_ap; - Vector6d state_meas; - - bool disturb_est_on; - bool disturb_est_body_frame; - bool print_flag, thrust_sim; - int set_attitude; - tf::TransformBroadcaster* broadcaster; - - //publishers - ros::Publisher stateOut_pub; - ros::Publisher debug_pub; - ros::Publisher kfState_pub; - - //subscribers - tf::TransformListener* listener; - ros::Subscriber odometry_sub, command_sub, in_air_sub, pixhawk_imu_sub, pixhawk_odom_sub; - ros::Subscriber battery_volt_sub, ekf_active_sub, rc_out_sub; - - //services - - //callbacks - void odometry_callback(nav_msgs::Odometry msg); - void command_callback(mav_msgs::RollPitchYawrateThrust msg); - void in_air_callback(std_msgs::Bool msg); - // void pixhawk_imu_callback(sensor_msgs::Imu msg); - void pixhawk_odom_callback(nav_msgs::Odometry msg); - void battery_volt_callback(sensor_msgs::BatteryState msg); - void ekf_active_callback(std_msgs::Bool msg); - void rc_out_callback(mavros_msgs::RCOut msg); - - void calcAP( double dtIn, double thrust_In, double thrust_In_prev, double& x_ap_, - double& y_ap_, double& z_ap_, double& xdot_ap_, double& ydot_ap_, double& zdot_ap_, double roll_comp_ap_, - double pitch_comp_ap_, double thrust_comp_ap_, double roll, double pitch, double yaw); - - double throttleToThrust(double throttle); - -public: - EKFControl(std::string node_name); - - virtual bool initialize(); - virtual bool execute(); - virtual ~EKFControl(); -}; - - -#endif diff --git a/robot/ros_ws/src/local/controls/attitude_controller/package.xml b/robot/ros_ws/src/local/controls/attitude_controller/package.xml deleted file mode 100644 index a6b0a244e..000000000 --- a/robot/ros_ws/src/local/controls/attitude_controller/package.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - attitude_controller - 0.0.0 - TODO: Package description - root - TODO: License declaration - - ament_cmake - - ament_lint_auto - ament_lint_common - - - ament_cmake - - diff --git a/robot/ros_ws/src/local/controls/attitude_controller/src/attitude_controller.cpp b/robot/ros_ws/src/local/controls/attitude_controller/src/attitude_controller.cpp deleted file mode 100644 index e8c69f17f..000000000 --- a/robot/ros_ws/src/local/controls/attitude_controller/src/attitude_controller.cpp +++ /dev/null @@ -1,765 +0,0 @@ -#include - -DroneFlightControl::DroneFlightControl(std::string node_name) - : BaseNode(node_name){ -} - -bool DroneFlightControl::initialize(){ - ros::NodeHandle* nh = get_node_handle(); - ros::NodeHandle* pnh = get_private_node_handle(); - - // init params - target_frame = airstack::get_param(this, "target_frame", std::string("map")); - p1 = airstack::get_param(this, "p1", 24.28); - p2 = airstack::get_param(this, "p2", 6.287); - p3 = airstack::get_param(this, "p3", -1.844); - kp1 = airstack::get_param(this, "kp1", 1.0); - kp2 = airstack::get_param(this, "kp2", 1.0); - kp3 = airstack::get_param(this, "kp3", 1.0); - kp4 = airstack::get_param(this, "kp4", 1.0); - kd1 = airstack::get_param(this, "kd1", 1.0); - kd2 = airstack::get_param(this, "kd2", 1.0); - kd3 = airstack::get_param(this, "kd3", 1.0); - ki1_body = airstack::get_param(this, "ki1_body", 1.0); - ki2_body = airstack::get_param(this, "ki2_body", 1.0); - ki1_vel_body = airstack::get_param(this, "ki1_vel_body", 1.0); - ki2_vel_body = airstack::get_param(this, "ki2_vel_body", 1.0); - // ki3_body = airstack::get_param(this, "ki3_body", 1.0); - ki1_ground = airstack::get_param(this, "ki1_ground", 1.0); - ki2_ground = airstack::get_param(this, "ki2_ground", 1.0); - ki3_ground = airstack::get_param(this, "ki3_ground", 1.0); - ki1_vel_ground = airstack::get_param(this, "ki1_vel_ground", 0.0); - ki2_vel_ground = airstack::get_param(this, "ki2_vel_ground", 0.0); - ki3_vel_ground = airstack::get_param(this, "ki3_vel_ground", 0.0); - kf1 = airstack::get_param(this, "kf1", 1.0); - kf2 = airstack::get_param(this, "kf2", 1.0); - kf3 = airstack::get_param(this, "kf3", 0.0); - disturb_est_body_frame = airstack::get_param(this, "disturb_est_body_frame", false); - command_angle_limit = airstack::get_param(this, "command_angle_limit", 20.0)*M_PI/180.; - i_angle_limit = airstack::get_param(this, "i_angle_limit", 2.0)*M_PI/180.; - z_i_limit = airstack::get_param(this, "z_i_limit", 10000.); - g = airstack::get_param(this, "gravity", 9.81); - m = airstack::get_param(this, "mass", 5000.0); - hover_throttle_param = airstack::get_param(this, "hover_throttle", 0.4); - min_dt = airstack::get_param(this, "min_dt", 0.0001); - low_thrust_duration = airstack::get_param(this, "low_thrust_duration", 2.0); - ekf_delay = airstack::get_param(this, "ekf_delay", 2.0); - use_ekf_state = airstack::get_param(this, "use_ekf_state", true); - use_ekf_dist = airstack::get_param(this, "use_ekf_dist", true); - thrust_sim = airstack::get_param(this, "thrust_sim", true); - boost_factor_angle_p = airstack::get_param(this, "boost_factor_angle_p", 1.0); - yawrate_limit = fabs(airstack::get_param(this, "yawrate_limit", 10.0)*M_PI/180.); - upper_bbox = airstack::get_param(this, "upper_bbox", 2.0); - compensate_control = airstack::get_param(this, "compensate_control", false); - p1_volt = airstack::get_param(this, "p1_volt", 1.0); - p2_volt = airstack::get_param(this, "p2_volt", 1.0); - - attitude_stuck_limit = airstack::get_param(this, "attitude_stuck_limit", 15.0)*M_PI/180.; - control_stuck_threshold = airstack::get_param(this, "control_stuck_threshold", 1.0); - recover_threshold = airstack::get_param(this, "recover_threshold", 1.0); - clip_disturbances = airstack::get_param(this, "clip_disturbances", true); - vel_only_stuck = airstack::get_param(this, "vel_only_stuck", true); - zero_rp_stuck = airstack::get_param(this, "zero_rp_stuck", true); - - // accel params - accelAlpha = airstack::get_param(this, "accelAlpha", 0.05); - accelLongTermAlpha = airstack::get_param(this, "accelLongTermAlpha", 0.003); - gainAccel = tf2::Vector3(airstack::get_param(this, "gainAccelX", 0.015), - airstack::get_param(this, "gainAccelY", 0.015), - airstack::get_param(this, "gainAccelZ", 0.008)); - betaAccel = tf2::Vector3(airstack::get_param(this, "betaAccelX", 1.0), - airstack::get_param(this, "betaAccelY", 1.0), - airstack::get_param(this, "betaAccelZ", 1.0)); - maxAccel = tf2::Vector3(airstack::get_param(this, "maxAccelX", 0.5), - airstack::get_param(this, "maxAccelY", 0.5), - airstack::get_param(this, "maxAccelZ", 0.5)); - alphaCommandAccel = tf2::Vector3(airstack::get_param(this, "alphaCommandAccelX", 0.3), - airstack::get_param(this, "alphaCommandAccelY", 0.3), - airstack::get_param(this, "alphaCommandAccelZ", 0.3)); - targetDT = 1.0/airstack::get_param(this, "execute_target", 50); - longterm_accel_filter = tf2::Vector3(0,0,9.81); - shortterm_accel_filter = tf2::Vector3(0,0,9.81); - command_filtered_accel = tf2::Vector3(0,0,0); - setpoint_vel_target_frame_prev = tf2::Vector3(0,0,0); - angle_tilt = 1.0; - low_thrust_start_time = ros::Time(0, 0); // make sure the time is in the past far enough so that it doesn't start out in low thrust mode - start_time_ekf = ros::Time(0, 0); - - // init variables - got_odom = false; /*got_tracking_point = false;*/ got_closest_point = false; got_vtp_jerk = false; - /*got_vtp = false;*/ got_accel = false; got_filtered_odom = false; got_filtered_state = false; - in_air.data = false; got_in_air = false, got_battery_volt=false; - x_i_body = 0; y_i_body = 0; // z_i_body = 0; - x_e_prev = 0; y_e_prev = 0; z_e_prev = 0; x_i_ground = 0; y_i_ground = 0; z_i_ground = 0; - vx_i_ground = 0; vy_i_ground = 0; vz_i_ground = 0; - hover_throttle = hover_throttle_param; - - prev_time = ros::Time::now(); - control_stuck_monitor = new templib::TimeOutMonitor(control_stuck_threshold); - recover_monitor = new templib::TimeOutMonitor(recover_threshold); - control_stuck_monitor->add_time(); - recover_monitor->add_time(); - - // init publishers - command_pub = nh->advertise("roll_pitch_yawrate_thrust_command", 1); - debug_pub = nh->advertise("drone_flight_control_debug", 1); - control_stuck_pub = nh->advertise("control_stuck", 1); - - // init subscribers - listener = new tf::TransformListener(); - //tracking_point_sub = nh->subscribe("tracking_point", 1, &DroneFlightControl::tracking_point_callback, this, ros::TransportHints().tcpNoDelay()); - closest_point_sub = nh->subscribe("closest_point", 1, &DroneFlightControl::closest_point_callback, this, ros::TransportHints().tcpNoDelay()); - //vtp_sub = nh->subscribe("virtual_target_point", 1, &DroneFlightControl::vtp_callback, this, ros::TransportHints().tcpNoDelay()); - odometry_sub = nh->subscribe("odometry", 1, &DroneFlightControl::odometry_callback, this, ros::TransportHints().tcpNoDelay()); - imu_sub = nh->subscribe("/imu/data", 2, &DroneFlightControl::imu_callback, this); - in_air_sub = nh->subscribe("in_air", 1, &DroneFlightControl::in_air_callback, this); - filtered_odometry_sub = nh->subscribe("filtered_odom_control", 1, &DroneFlightControl::filtered_odometry_callback, this, ros::TransportHints().tcpNoDelay()); - filtered_state_sub = nh->subscribe("subt_control_ekf_state", 1, &DroneFlightControl::filtered_state_callback, this, ros::TransportHints().tcpNoDelay()); - pixhawk_odom_sub = nh->subscribe("mavros/local_position/odom", 1, &DroneFlightControl::pixhawk_odom_callback, this, ros::TransportHints().tcpNoDelay()); - run_local_planner_sub = nh->subscribe("run_local_planner", 1, &DroneFlightControl::run_local_planner_callback, this); - vtp_jerk_sub = nh->subscribe("tracking_point", 1, &DroneFlightControl::vtp_jerk_callback, this, ros::TransportHints().tcpNoDelay()); - battery_volt_sub = nh->subscribe("mavros/battery", 1, &DroneFlightControl::battery_volt_callback, this); - - // init services - reset_integrator_server = nh->advertiseService("reset_integrator", &DroneFlightControl::reset_integrator_callback, this); - - return true; -} - -bool DroneFlightControl::execute(){ - - if(got_battery_volt && !thrust_sim) - hover_throttle = p1_volt * battery_volt + p2_volt; - else - hover_throttle = hover_throttle_param; - - if(got_odom && got_vtp_jerk && odom.header.seq != prev_odom.header.seq){// && - // filtered_odom.header.seq != prev_filtered_odom.header.seq){ - - ros::Time curr_time = odom.header.stamp;//ros::Time::now(); - double dt = (curr_time - prev_time).toSec(); - if(dt < min_dt) - return true; - prev_time = curr_time; - prev_odom = odom; - - if((ros::Time::now() - low_thrust_start_time).toSec() < low_thrust_duration){ - mav_msgs::RollPitchYawrateThrust command; - command.header.stamp = ros::Time::now(); - command.roll = 0; - command.pitch = 0; - command.thrust.z = 0.15; - command.yaw_rate = 0; - command_pub.publish(command); - reset_integrators(); - - return true; - } - - // actual values - double x_a, y_a, z_a, vx_a, vy_a, vz_a;//, x_d, y_d, z_d, vx_d, vy_d, vz_d; - double roll_a, pitch_a, yaw_a, roll_c, pitch_c, yaw_c, roll_d, pitch_d, yaw_d, roll_v, pitch_v, yaw_v; - tf::Vector3 velocity_target_frame, position_target_frame; - //tf::Vector3 tracking_position_target, tracking_velocity_target; - tf::Vector3 closest_position_target, closest_velocity_target; - tf::Vector3 vtp_position_target, vtp_velocity_target; - tf::StampedTransform velocity_transform_closest, velocity_transform_vtp;; - try{ - - // transform for filtered odom - if(use_ekf_state && got_filtered_odom && start_time_ekf.toSec()>0.0 && (ros::Time::now() - start_time_ekf).toSec() > ekf_delay){ - // && filtered_odom.header.seq != prev_filtered_odom.header.seq){ - - // tf::Vector3 filtered_velocity_target, filtered_position_target; - tf::StampedTransform filtered_transform; - listener->waitForTransform(target_frame, filtered_odom.header.frame_id, filtered_odom.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, filtered_odom.header.frame_id, filtered_odom.header.stamp, filtered_transform); - tf::StampedTransform filtered_velocity_transform; - listener->waitForTransform(target_frame, filtered_odom.child_frame_id, filtered_odom.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, filtered_odom.child_frame_id, filtered_odom.header.stamp, filtered_velocity_transform); - filtered_velocity_transform.setOrigin(tf::Vector3(0, 0, 0)); - - tf::Quaternion q_filtered_target = filtered_transform*tflib::to_tf(filtered_odom.pose.pose.orientation); - tf::Matrix3x3(q_filtered_target).getRPY(roll_a, pitch_a, yaw_a); - - position_target_frame = filtered_transform*tflib::to_tf(filtered_odom.pose.pose.position); - x_a = position_target_frame.x(); - y_a = position_target_frame.y(); - z_a = position_target_frame.z(); - - velocity_target_frame = filtered_velocity_transform*tflib::to_tf(filtered_odom.twist.twist.linear); - vx_a = velocity_target_frame.x(); - vy_a = velocity_target_frame.y(); - vz_a = velocity_target_frame.z(); - } - else{ // use absolute odom - - // transform for odom - tf::StampedTransform transform; - listener->waitForTransform(target_frame, odom.header.frame_id, odom.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, odom.header.frame_id, odom.header.stamp, transform); - tf::StampedTransform velocity_transform; - listener->waitForTransform(target_frame, odom.child_frame_id, odom.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, odom.child_frame_id, odom.header.stamp, velocity_transform); - velocity_transform.setOrigin(tf::Vector3(0, 0, 0)); - - tf::Quaternion q_target_frame = transform*tflib::to_tf(odom.pose.pose.orientation); - tf::Matrix3x3(q_target_frame).getRPY(roll_a, pitch_a, yaw_a); - - position_target_frame = transform*tflib::to_tf(odom.pose.pose.position); - x_a = position_target_frame.x(); - y_a = position_target_frame.y(); - z_a = position_target_frame.z(); - - velocity_target_frame = velocity_transform*tflib::to_tf(odom.twist.twist.linear); - vx_a = velocity_target_frame.x(); - vy_a = velocity_target_frame.y(); - vz_a = velocity_target_frame.z(); - } - - // transform for tracking point - /* - tf::StampedTransform transform_tracking; - listener->waitForTransform(target_frame, tracking_point.header.frame_id, tracking_point.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, tracking_point.header.frame_id, tracking_point.header.stamp, transform_tracking); - tf::StampedTransform velocity_transform_tracking; - listener->waitForTransform(target_frame, tracking_point.child_frame_id, tracking_point.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, tracking_point.child_frame_id, tracking_point.header.stamp, velocity_transform_tracking); - velocity_transform_tracking.setOrigin(tf::Vector3(0, 0, 0)); - - tracking_position_target = transform_tracking*tflib::to_tf(tracking_point.pose.pose.position); - tf::Quaternion q_tracking_target = transform_tracking*tflib::to_tf(tracking_point.pose.pose.orientation); - x_d = tracking_position_target.x(); - y_d = tracking_position_target.y(); - z_d = tracking_position_target.z(); - tf::Matrix3x3(q_tracking_target).getRPY(roll_d, pitch_d, yaw_d); - - tracking_velocity_target = velocity_transform_tracking*tflib::to_tf(tracking_point.twist.twist.linear); - vx_d = tracking_velocity_target.x(); - vy_d = tracking_velocity_target.y(); - vz_d = tracking_velocity_target.z(); - */ - - // transform for closest point - if( got_closest_point ){ - - tf::StampedTransform transform_closest; - listener->waitForTransform(target_frame, closest_point.header.frame_id, closest_point.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, closest_point.header.frame_id, closest_point.header.stamp, transform_closest); - - listener->waitForTransform(target_frame, closest_point.child_frame_id, closest_point.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, closest_point.child_frame_id, closest_point.header.stamp, velocity_transform_closest); - velocity_transform_closest.setOrigin(tf::Vector3(0, 0, 0)); // TODO: what does this line do? - - closest_position_target = transform_closest*tflib::to_tf(closest_point.pose.pose.position); - tf::Quaternion q_closest_target = transform_closest*tflib::to_tf(closest_point.pose.pose.orientation); - tf::Matrix3x3(q_closest_target).getRPY(roll_c, pitch_c, yaw_c); - - closest_velocity_target = velocity_transform_closest*tflib::to_tf(closest_point.twist.twist.linear); - } - - // transform for VTP - if( got_vtp_jerk ){ - - tf::StampedTransform transform_vtp; - listener->waitForTransform(target_frame, vtp_jerk.header.frame_id, vtp_jerk.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, vtp_jerk.header.frame_id, vtp_jerk.header.stamp, transform_vtp); - - listener->waitForTransform(target_frame, vtp_jerk.child_frame_id, vtp_jerk.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, vtp_jerk.child_frame_id, vtp_jerk.header.stamp, velocity_transform_vtp); - velocity_transform_vtp.setOrigin(tf::Vector3(0, 0, 0)); // TODO: what does this line do? - - vtp_position_target = transform_vtp*tflib::to_tf(vtp_jerk.pose.position); - tf::Quaternion q_vtp_target = transform_vtp*tflib::to_tf(vtp_jerk.pose.orientation); - tf::Matrix3x3(q_vtp_target).getRPY(roll_v, pitch_v, yaw_v); - - vtp_velocity_target = velocity_transform_vtp*tflib::to_tf(vtp_jerk.twist.linear); - } - - // transform for px4 - if(compensate_control){ - - double yaw_temp; - tf::StampedTransform transform_px4; - listener->waitForTransform(target_frame, px4_odom.header.frame_id, px4_odom.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, px4_odom.header.frame_id, px4_odom.header.stamp, transform_px4); - - tf::Quaternion px4_q_target_frame = transform_px4*tflib::to_tf(px4_odom.pose.pose.orientation); - tf::Matrix3x3(px4_q_target_frame).getRPY(roll_px4_odom, pitch_px4_odom, yaw_temp); - } - - } - catch(tf::TransformException& te){ - ROS_ERROR_STREAM("TransformException while transform odometry: " << te.what()); - return true; - } - - - double yaw_tf = yaw_a; - - // declare variables - double x_e, y_e, z_e, yaw_e=0, vx_e, vy_e, vz_e, x_e_d, y_e_d, z_e_d, roll, pitch, yawrate, throttle, thrust_norm; - double x_e_body, y_e_body, vx_e_body, vy_e_body; - double x_ff=0, y_ff=0, z_ff=0; - - ///// Control laws ///////// - - // check stuck condition - bool velOnly=false, zero_rp=false; - if( sqrt( roll_a*roll_a + pitch_a*pitch_a ) < attitude_stuck_limit && - sqrt( roll_px4_odom*roll_px4_odom + pitch_px4_odom*pitch_px4_odom ) < attitude_stuck_limit ){ - control_stuck_monitor->add_time(); - } - - std_msgs::Bool control_stuck_msg; - - debug.recover_mode.data = false; // default - - if(control_stuck_monitor->is_timed_out()){ - control_stuck_msg.data = true; - - if(!recover_monitor->is_timed_out()){ - // do recovery action - debug.recover_mode.data=true; - - // clip disturbances - if(clip_disturbances){ - filtered_state.disOut.x=0.; - filtered_state.disOut.y=0.; - } - - // switch to vel control - if(vel_only_stuck){ - velOnly=true; - } - - // switch to zero_rp - if(zero_rp_stuck){ - zero_rp=true; - } - } - } - else{ - control_stuck_msg.data = false; - recover_monitor->add_time(); - } - control_stuck_pub.publish(control_stuck_msg); - - debug.stuck_mode.data = control_stuck_msg.data; - - // When tracking VTP // - if(got_vtp_jerk){// && got_filtered_odom){// && filtered_odom.header.seq != prev_filtered_odom.header.seq){ - - tf::Vector3 diffPos = vtp_position_target - position_target_frame; - - // TODO: better way to get direction of motion - tf::Vector3 posErr; - if( fabs(vtp_velocity_target.length2()) < 1e-6 || diffPos.length() > upper_bbox){ - - posErr = diffPos; - // ROS_INFO_STREAM("Setpoint velocity went to zero"); - } - else - posErr = diffPos - diffPos.dot(vtp_velocity_target)/( vtp_velocity_target.length2() )*vtp_velocity_target; - - tf::Vector3 velErr = vtp_velocity_target - velocity_target_frame; - - // errors - x_e = posErr.x(); - y_e = posErr.y(); - z_e = vtp_position_target.z() - z_a; - - if(velOnly){ - x_e=0.; y_e=0.; - } - - yaw_e = atan2(sin(yaw_v - yaw_a), cos(yaw_v - yaw_a)); - vx_e = velErr.x(); - vy_e = velErr.y(); - vz_e = velErr.z(); - - if(got_run_local_planner && run_local_planner.data && got_vtp_jerk){ - - // tf::Vector3 vtp_acceleration_target = velocity_transform_vtp*tflib::to_tf(vtp_jerk.acceleration); - // x_ff = vtp_acceleration_target.x(); y_ff = vtp_acceleration_target.y(); z_ff = vtp_acceleration_target.z(); - x_ff = vtp_jerk.acceleration.x; y_ff = vtp_jerk.acceleration.y; z_ff = vtp_jerk.acceleration.z; - } - else if(got_vtp_jerk){ - x_ff = vtp_jerk.acceleration.x; y_ff = vtp_jerk.acceleration.y; z_ff = vtp_jerk.acceleration.z; - } - } - - // for integrators - x_e_body = cos(yaw_tf)*x_e + sin(yaw_tf)*y_e; - vx_e_body = cos(yaw_tf)*vx_e + sin(yaw_tf)*vy_e; - y_e_body = -sin(yaw_tf)*x_e + cos(yaw_tf)*y_e; - vy_e_body = -sin(yaw_tf)*vx_e + cos(yaw_tf)*vy_e; - - double accel_xdes = kp1 * x_e + kd1 * vx_e + ki1_ground * x_i_ground + ki1_vel_ground*vx_i_ground + kf1 * x_ff; - double accel_ydes = kp2 * y_e + kd2 * vy_e + ki2_ground * y_i_ground + ki2_vel_ground*vy_i_ground + kf2 * y_ff; - double accel_zdes = kp3 * z_e + kd3 * vz_e + ki3_ground * z_i_ground + ki3_vel_ground*vz_i_ground + kf3 * z_ff; - - // add ground frame dist terms - if (use_ekf_dist && got_filtered_odom && !disturb_est_body_frame && start_time_ekf.toSec()>0.0 && - (ros::Time::now() - start_time_ekf).toSec() > ekf_delay ){ - accel_xdes += filtered_state.disOut.x; - accel_ydes += filtered_state.disOut.y; - accel_zdes += filtered_state.disOut.z * g; - } - - // generate control inputs - // thrust_norm = (tf::Vector3(accel_xdes, accel_ydes, accel_zdes) + tf::Vector3(0,0,g)).length(); - - roll = ( accel_xdes * sin(yaw_tf) - accel_ydes * cos(yaw_tf) )/g - ki2_body*y_i_body - ki2_vel_body*vy_i_body; - pitch = ( accel_xdes * cos(yaw_tf) + accel_ydes * sin(yaw_tf) )/g + ki1_body*x_i_body + ki1_vel_body*vx_i_body; - // roll = asin( std::min( 1.0, std::max( (accel_xdes*sin(yaw_tf) - accel_ydes*cos(yaw_tf) )/ - // thrust_norm, -1.0 )) ); - // pitch = acos( std::min(1.0, std::max(-1.0, (accel_zdes + g)/(thrust_norm*cos(roll_a)) )) ); - - - if(compensate_control && in_air.data){ - roll += roll_px4_odom - roll_a; - pitch += pitch_px4_odom - pitch_a; - } - - // add body frame dist terms - if(use_ekf_dist && got_filtered_odom && disturb_est_body_frame && start_time_ekf.toSec()>0.0 && - (ros::Time::now() - start_time_ekf).toSec() > ekf_delay ){ - roll += filtered_state.disOut.x; - pitch += filtered_state.disOut.y; - accel_zdes += filtered_state.disOut.z * g; - } - - accel_zdes = std::max(accel_zdes, -g); - thrust_norm = (accel_zdes+g); - // thrust_norm = (accel_zdes + g)/std::max(0.5, std::min(1.0, cos(roll_a)*cos(pitch_a) ) ); - yawrate = std::max(-yawrate_limit, std::min(yawrate_limit, kp4*yaw_e)); - - // get throttle from thrust - if(isnan(thrust_norm)) - throttle = hover_throttle; - else - throttle = std::min( 1.0, thrustToThrottle(thrust_norm) ); - - - // cap control angles - if(roll > command_angle_limit) - roll = command_angle_limit; - else if(roll < -command_angle_limit) - roll = -command_angle_limit; - else if(isnan(roll)) - roll = 0; - - if(pitch > command_angle_limit) - pitch = command_angle_limit; - else if(pitch < -command_angle_limit) - pitch = -command_angle_limit; - else if (isnan(pitch)) - pitch=0; - - if(isnan(yaw_e)) - yawrate=0; - - // while taking off // - if(!in_air.data){ - - roll = 0; pitch=0; throttle = hover_throttle; yawrate=0; - } - - // recovery condition - if(zero_rp){roll=0.; pitch=0.;} - - // publish debug info - debug.header = odom.header; - - if(got_closest_point){ - - debug.position_closest.x = closest_position_target.x(); - debug.position_closest.y = closest_position_target.y(); - debug.position_closest.z = closest_position_target.z(); - debug.yaw_closest = yaw_c; - debug.velocity_closest.x = closest_velocity_target.x(); - debug.velocity_closest.y = closest_velocity_target.y(); - debug.velocity_closest.z = closest_velocity_target.z(); - } - - if(got_vtp_jerk){ - - debug.position_vtp.x = vtp_position_target.x(); - debug.position_vtp.y = vtp_position_target.y(); - debug.position_vtp.z = vtp_position_target.z(); - debug.yaw_vtp = yaw_c; - debug.velocity_vtp.x = vtp_velocity_target.x(); - debug.velocity_vtp.y = vtp_velocity_target.y(); - debug.velocity_vtp.z = vtp_velocity_target.z(); - debug.acceleration_vtp.x = vtp_jerk.acceleration.x; - debug.acceleration_vtp.x = vtp_jerk.acceleration.y; - debug.acceleration_vtp.x = vtp_jerk.acceleration.z; - debug.accel_vtp_net = tf::Vector3(vtp_jerk.acceleration.x, vtp_jerk.acceleration.y, vtp_jerk.acceleration.z + g).length(); - } - - debug.position_actual.x = x_a; - debug.position_actual.y = y_a; - debug.position_actual.z = z_a; - debug.yaw_actual = yaw_a; - debug.velocity_actual.x = vx_a; - debug.velocity_actual.y = vy_a; - debug.velocity_actual.z = vz_a; - - debug.integrator_ground.x = x_i_ground; - debug.integrator_ground.y = y_i_ground; - debug.integrator_ground.z = z_i_ground; - - debug.vel_integrator_ground.x = vx_i_ground; - debug.vel_integrator_ground.y = vy_i_ground; - debug.vel_integrator_ground.z = vz_i_ground; - - debug.integrator_body.x = x_i_body; - debug.integrator_body.y = y_i_body; - - debug.vel_integrator_body.x = vx_i_body; - debug.vel_integrator_body.y = vy_i_body; - - debug.position_error.x = x_e; - debug.position_error.y = y_e; - debug.position_error.z = z_e; - debug.yaw_error = yaw_e; - debug.velocity_error.x = vx_e; - debug.velocity_error.y = vy_e; - debug.velocity_error.z = vz_e; - - debug.derivative.x = x_e_d; - debug.derivative.y = y_e_d; - debug.derivative.z = z_e_d; - - debug.kp.x = kp1; - debug.kp.y = kp2; - debug.kp.z = kp3; - debug.ki_ground.x = ki1_ground; - debug.ki_ground.y = ki2_ground; - debug.ki_ground.z = ki3_ground; - debug.ki_vel_ground.x = ki1_vel_ground; - debug.ki_vel_ground.y = ki2_vel_ground; - debug.ki_vel_ground.z = ki3_vel_ground; - debug.kd.x = kd1; - debug.kd.y = kd2; - debug.kd.z = kd3; - - debug.dt = dt; - - debug.roll = roll * 180./M_PI; - debug.pitch = pitch * 180./M_PI; - debug.yawrate = yawrate; - debug.throttle = throttle; - debug.thrust_norm = thrust_norm; - - debug_pub.publish(debug); - - // accumulate integrators - // if(ki2_body != 0.) - // x_i_body = std::max(-fabs(i_angle_limit*g/ki2_body), std::min(fabs(i_angle_limit*g/ki2_body), x_i_body + x_e_body)); - // if(ki1_body != 0.) - // y_i_body = std::max(-fabs(i_angle_limit*g/ki1_body), std::min(fabs(i_angle_limit*g/ki1_body), y_i_body + y_e_body)); - // if(ki3_body != 0.) - // z_i_body = std::max(-fabs(z_i_limit), std::min(fabs(z_i_limit), z_i_body + z_e)); - double ki_ground = std::max(fabs(ki1_ground), fabs(ki2_ground)); - if(ki_ground != 0.) - x_i_ground = std::max(-fabs(i_angle_limit*g/ki_ground), std::min(fabs(i_angle_limit*g/ki_ground), x_i_ground + x_e)); - if(ki_ground != 0.) - y_i_ground = std::max(-fabs(i_angle_limit*g/ki_ground), std::min(fabs(i_angle_limit*g/ki_ground), y_i_ground + y_e)); - z_i_ground += z_e; - - double ki_vel_ground = std::max(fabs(ki1_vel_ground), fabs(ki2_vel_ground)); - if(ki_vel_ground != 0.) - vx_i_ground = std::max(-fabs(i_angle_limit*g/ki_vel_ground), - std::min(fabs(i_angle_limit*g/ki_vel_ground), vx_i_ground + vx_e)); - - if(ki_vel_ground != 0.) - vy_i_ground = std::max(-fabs(i_angle_limit*g/ki_vel_ground), - std::min(fabs(i_angle_limit*g/ki_vel_ground), vy_i_ground + vy_e)); - - vz_i_ground += vz_e; - - double ki_body = std::max(fabs(ki1_body), fabs(ki2_body)); - if(ki_body != 0.) - x_i_body = std::max(-fabs(i_angle_limit*g/ki_body), - std::min(fabs(i_angle_limit*g/ki_body), x_i_body + x_e_body)); - - if(ki_body != 0.) - y_i_body = std::max(-fabs(i_angle_limit*g/ki_body), - std::min(fabs(i_angle_limit*g/ki_body), y_i_body + y_e_body)); - - double ki_vel_body = std::max(fabs(ki1_vel_body), fabs(ki2_vel_body)); - if(ki_vel_body != 0.) - vx_i_body = std::max(-fabs(i_angle_limit*g/ki_vel_body), - std::min(fabs(i_angle_limit*g/ki_vel_body), vx_i_body + vx_e_body)); - - if(ki_vel_body != 0.) - vy_i_body = std::max(-fabs(i_angle_limit*g/ki_vel_body), - std::min(fabs(i_angle_limit*g/ki_vel_body), vy_i_body + vy_e_body)); - - // x_i_body += x_e_body; - // y_i_body += y_e_body; - // vx_i_body += vx_e_body; - // vy_i_body += vy_e_body; - - // set previous errors - x_e_prev = x_e; - y_e_prev = y_e; - z_e_prev = z_e; - - mav_msgs::RollPitchYawrateThrust command; - command.header.stamp = ros::Time::now(); - command.roll = roll; - command.pitch = pitch; - command.thrust.z = throttle; - command.yaw_rate = yawrate; - command_pub.publish(command); - } - - return true; -} - -void DroneFlightControl::odometry_callback(nav_msgs::Odometry msg){ - got_odom = true; - odom = msg; -} -/* -void DroneFlightControl::tracking_point_callback(nav_msgs::Odometry msg){ - got_tracking_point = true; - tracking_point = msg; -} -*/ -void DroneFlightControl::closest_point_callback(nav_msgs::Odometry msg){ - got_closest_point = true; - closest_point = msg; -} - -void DroneFlightControl::vtp_jerk_callback(core_trajectory_msgs::Odometry msg){ - got_vtp_jerk = true; - vtp_jerk = msg; -} -/* -void DroneFlightControl::vtp_callback(nav_msgs::Odometry msg){ - got_vtp = true; - vtp = msg; -} -*/ -bool DroneFlightControl::reset_integrator_callback(std_srvs::Empty::Request& request, std_srvs::Empty::Response& response){ - reset_integrators(); - low_thrust_start_time = ros::Time::now(); - return true; -} - -void DroneFlightControl::reset_integrators(){ - x_i_body = 0; - y_i_body = 0; - vx_i_body=0; - vy_i_body=0; - // z_i_body = 0; - x_i_ground = 0; - y_i_ground = 0; - z_i_ground = 0; - vx_i_ground = 0; - vy_i_ground = 0; - vz_i_ground = 0; -} - -void DroneFlightControl::imu_callback(sensor_msgs::Imu imu){ - //Assumes IMU is aligned with axis on vehicle and avoid transform - //Rotate to level - tf2::Quaternion quat_tf; - double roll,pitch,yaw; - tf2::convert(imu.orientation,quat_tf); - tf2::Matrix3x3(quat_tf).getRPY(roll,pitch,yaw); - tf2::Matrix3x3 levelMat; - levelMat.setRPY(roll,pitch,0.0); - tf2::Vector3 preRotateAccl,rotatedAccel; - tf2::convert(imu.linear_acceleration,preRotateAccl); - rotatedAccel = levelMat * preRotateAccl;//Undo the rotation by roll/pitch - - angle_tilt = cos(pitch)*cos(roll); - - //Already filter and remove - //This is the long term world acceleration (such as gravity) - longterm_accel_filter = longterm_accel_filter + accelLongTermAlpha * (rotatedAccel - longterm_accel_filter); - - //This is the actual acceleration on the vehicle - shortterm_accel_filter = shortterm_accel_filter + accelAlpha *(rotatedAccel - shortterm_accel_filter); - actual_accel_target_frame = -(shortterm_accel_filter - longterm_accel_filter); - //ROS_INFO_STREAM(roll<<","< -#include -#include -#include - -// using namespace Eigen; - -// #define N_dof 6 - -EKFControl::EKFControl(std::string node_name): BaseNode(node_name){} - -bool EKFControl::initialize(){ - - ros::NodeHandle* nh = get_node_handle(); - ros::NodeHandle* pnh = get_private_node_handle(); - - // init params - tf_prefix = pnh->param("tf_prefix", std::string("")); - target_frame = pnh->param("target_frame", std::string("map")); - min_dt = pnh->param("min_dt", 0.01); - min_command_dt = pnh->param("min_command_dt", 0.01); - gravity =pnh->param("gravity", 9.81); - hover_throttle_param =pnh->param("hover_throttle", 0.66); - p1 = pnh->param("p1", 24.28); p2 = pnh->param("p2", 6.287); p3 = pnh->param("p3", -1.844); - targetDT = 1.0/pnh->param("execute_target", 50); - init_cov_pos_xy = pnh->param("init_cov_pos_xy", 1e-3); - init_cov_pos_z = pnh->param("init_cov_pos_z", 1e-3); - init_cov_vel_xy = pnh->param("init_cov_vel_xy", 1e-2); - init_cov_vel_z = pnh->param("init_cov_vel_z", 1e-2); - init_cov_dis_xy = pnh->param("init_cov_dis_xy", 1e-4); // 2 - init_cov_dis_z = pnh->param("init_cov_dis_z", 1e-4); - model_cov_pos_xy = pnh->param("model_cov_pos_xy", 1e-6); //3 - model_cov_pos_z = pnh->param("model_cov_pos_z", 1e-6); - model_cov_vel_xy = pnh->param("model_cov_vel_xy", 1e-6); // 2 - model_cov_vel_z = pnh->param("model_cov_vel_z", 1e-6); - model_cov_dis_xy = pnh->param("model_cov_dis_xy", 1e-6); // 2 - model_cov_dis_z = pnh->param("model_cov_dis_z", 1e-6); - meas_cov_pos_xy = pnh->param("meas_cov_pos_xy", 1e-3); - meas_cov_pos_z = pnh->param("meas_cov_pos_z", 1e-3); - meas_cov_vel_xy = pnh->param("meas_cov_vel_xy", 1e-2); - meas_cov_vel_z = pnh->param("meas_cov_vel_z", 1e-2); - disturb_est_on = pnh->param("disturb_est_on", true); - disturb_est_body_frame = pnh->param("disturb_est_body_frame", false); - print_flag = pnh->param("print_flag", false); - thrust_sim = pnh->param("thrust_sim", false); - set_attitude = pnh->param("set_attitude", 0); - alpha_motor = pnh->param("alpha_motor", 1.0); - p1_volt = pnh->param("p1_volt", 1.0); - p2_volt = pnh->param("p2_volt", 1.0); - reject_threshold = pnh->param("reject_threshold", 3.0); - attitude_comp_lim = pnh->param("attitude_comp_lim", 10.0); - g_lim = pnh->param("g_lim", 0.5); - sat_rc_limit = pnh->param("sat_rc_limit", 1900); - - // init variables - odomCt=0; commandCt=0; - in_air.data = false; - ekf_active.data = false; - - // roll_prev=0; pitch_prev=0; thrust_prev=0; - got_odom = false; got_command=false; got_in_air = false; got_battery_volt = false; got_ekf_active = false; - // prev_odom_time = odom.header.stamp; - prev_ekf_time = odom.header.stamp; // ros::Time::now(); - // prev_odom = odom; prev_command = command; - prev_command.roll = 0.0; prev_command.pitch = 0.0; prev_command.thrust.z = 0.0; - - H <<1., 0., 0., 0., 0., 0., 0., 0., 0., - 0., 1., 0., 0., 0., 0., 0., 0., 0., - 0., 0., 1., 0., 0., 0., 0., 0., 0., - 0., 0., 0., 1., 0., 0., 0., 0., 0., - 0., 0., 0., 0., 1., 0., 0., 0., 0., - 0., 0., 0., 0., 0., 1., 0., 0., 0.; - - P << init_cov_pos_xy, 0., 0., 0., 0., 0., 0., 0., 0., - 0., init_cov_pos_xy, 0., 0., 0., 0., 0., 0., 0., - 0., 0., init_cov_pos_z, 0., 0., 0., 0., 0., 0., - 0., 0., 0., init_cov_vel_xy, 0., 0., 0., 0., 0., - 0., 0., 0., 0., init_cov_vel_xy, 0., 0., 0., 0., - 0., 0., 0., 0., 0., init_cov_vel_z, 0., 0., 0., - 0., 0., 0., 0., 0., 0., init_cov_dis_xy, 0., 0., - 0., 0., 0., 0., 0., 0., 0., init_cov_dis_xy, 0., - 0., 0., 0., 0., 0., 0., 0., 0., init_cov_dis_z; - - Q << model_cov_pos_xy, 0., 0., 0., 0., 0., 0., 0., 0., - 0., model_cov_pos_xy, 0., 0., 0., 0., 0., 0., 0., - 0., 0., model_cov_pos_z, 0., 0., 0., 0., 0., 0., - 0., 0., 0., model_cov_vel_xy, 0., 0., 0., 0., 0., - 0., 0., 0., 0., model_cov_vel_xy, 0., 0., 0., 0., - 0., 0., 0., 0., 0., model_cov_vel_z, 0., 0., 0., - 0., 0., 0., 0., 0., 0., model_cov_dis_xy, 0., 0., - 0., 0., 0., 0., 0., 0., 0., model_cov_dis_xy, 0., - 0., 0., 0., 0., 0., 0., 0., 0., model_cov_dis_z; - - R << meas_cov_pos_xy, 0., 0., 0., 0., 0., - 0., meas_cov_pos_xy, 0., 0., 0., 0., - 0., 0., meas_cov_pos_z, 0., 0., 0., - 0., 0., 0., meas_cov_vel_xy, 0., 0., - 0., 0., 0., 0., meas_cov_vel_xy, 0., - 0., 0., 0., 0., 0., meas_cov_vel_z; - - // try{ - // tf::StampedTransform transform; - // listener->waitForTransform(target_frame, odom.header.frame_id, odom.header.stamp, ros::Duration(0.1)); - // ROS_INFO_STREAM("wait for velo transform done\n"); - // listener->lookupTransform(target_frame, odom.header.frame_id, odom.header.stamp, transform); - // ROS_INFO_STREAM("look up velo transform done\n"); - // tf::StampedTransform velocity_transform; - // listener->waitForTransform(target_frame, odom.child_frame_id, odom.header.stamp, ros::Duration(0.1)); - // listener->lookupTransform(target_frame, odom.child_frame_id, odom.header.stamp, velocity_transform); - // velocity_transform.setOrigin(tf::Vector3(0, - // tf::Vector3 position_target_frame = transform*tflib::to_tf(odom.pose.pose.position); - // tf::Quaternion q_target_frame = transform*tflib::to_tf(odom.pose.pose.orientation); - // x_prev = position_target_frame.x(); - // y_prev = position_target_frame.y(); - // z_prev = position_target_frame.z(); - // tf::Matrix3x3(q_target_frame).getRPY(roll_prev, pitch_prev, yaw_prev); - - // tf::Vector3 velocity_target_frame = velocity_transform*tflib::to_tf(odom.twist.twist.linear); - // xdot_prev = velocity_target_frame.x(); - // ydot_prev = velocity_target_frame.y(); - // zdot_prev = velocity_target_frame.z(); - // } - // catch(tf::TransformException& te){ - // ROS_ERROR_STREAM("TransformException while transform odometry in initialize: " << te.what()); - // return true; - // } - - // ^ TODO may not work in bagfile, can comment out and init prev to 0 - // see below v - - x_prev = 0.0; y_prev = 0.0; z_prev = 0.0; - xdot_prev = 0.0; ydot_prev = 0.0; zdot_prev = 0.0; - roll_prev=0.0; roll_comp_prev=0.0; - pitch_prev=0.0; pitch_comp_prev=0.0; - thrust_comp_prev = -1.0; - roll_px4_odom=0.0; pitch_px4_odom = 0.0; - hover_throttle = hover_throttle_param; - thrust_achieved_prev = 0.; thrust_command_prev = throttleToThrust(hover_throttle); thrust_in_prev = 0.; - prev_ekf_time = ros::Time::now(); start_time_command = ros::Time::now(); - battery_volt = 16.0; - - // init publishers - stateOut_pub = nh->advertise("filtered_odom_control", 1); - debug_pub = nh->advertise("subt_control_ekf_debug", 1); - kfState_pub = nh->advertise("subt_control_ekf_state", 1); - - // init subscribers - listener = new tf::TransformListener(); - broadcaster = new tf::TransformBroadcaster(); - odometry_sub = nh->subscribe("odometry", 1, &EKFControl::odometry_callback, this, ros::TransportHints().tcpNoDelay()); - command_sub = nh->subscribe("roll_pitch_yawrate_thrust_command", 1, &EKFControl::command_callback, this); - in_air_sub = nh->subscribe("in_air", 1, &EKFControl::in_air_callback, this); - // pixhawk_imu_sub = nh->subscribe("mavros/imu/data", 1, &EKFControl::pixhawk_imu_callback, this); - pixhawk_odom_sub = nh->subscribe("mavros/local_position/odom", 1, &EKFControl::pixhawk_odom_callback, this, ros::TransportHints().tcpNoDelay()); - battery_volt_sub = nh->subscribe("mavros/battery", 1, &EKFControl::battery_volt_callback, this); - ekf_active_sub = nh->subscribe("ekf_active", 1, &EKFControl::ekf_active_callback, this); - rc_out_sub = nh->subscribe("mavros/rc/out", 1, &EKFControl::rc_out_callback, this); - - return true; -} - -bool EKFControl::execute(){ - - ros::Time start_time = ros::Time::now(); - double now = start_time.toSec(); - double prev_time = start_time.toSec(); - - if(got_battery_volt && !thrust_sim) - hover_throttle = p1_volt * battery_volt + p2_volt; - else - hover_throttle = hover_throttle_param; - - if(got_odom && odom.header.seq != prev_odom.header.seq){ - - ros::Time curr_time = ros::Time::now(); - // ROS_INFO_STREAM("Time according to odom is "<waitForTransform(target_frame, px4_odom.header.frame_id, px4_odom.header.stamp, ros::Duration(0.1)); - //listener->lookupTransform(target_frame, px4_odom.header.frame_id, px4_odom.header.stamp, transform_px4); - listener->lookupTransform(target_frame, px4_odom.header.frame_id, ros::Time(0), transform_px4); - //ROS_INFO_STREAM("elapsed: " << monitor.toc("px4tf")/1000000.); - - tf::Quaternion px4_q_target_frame = transform_px4*tflib::to_tf(px4_odom.pose.pose.orientation); - tf::Matrix3x3(px4_q_target_frame).getRPY(roll_px4_odom, pitch_px4_odom, unused); - - //monitor.tic("other4tf"); - auto start1 = std::chrono::steady_clock::now(); - tf::StampedTransform transform; - listener->waitForTransform(target_frame, odom.header.frame_id, odom.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, odom.header.frame_id, odom.header.stamp, transform); - tf::StampedTransform velocity_transform; - listener->waitForTransform(target_frame, odom.child_frame_id, odom.header.stamp, ros::Duration(0.1)); - listener->lookupTransform(target_frame, odom.child_frame_id, odom.header.stamp, velocity_transform); - velocity_transform.setOrigin(tf::Vector3(0, 0, 0)); - //ROS_INFO_STREAM("elapsed: " << monitor.toc("other4tf")/1000000.); - - tf::Vector3 position_target_frame = transform*tflib::to_tf(odom.pose.pose.position); - tf::Quaternion q_target_frame = transform*tflib::to_tf(odom.pose.pose.orientation); - x_meas = position_target_frame.x(); - y_meas = position_target_frame.y(); - z_meas = position_target_frame.z(); - tf::Matrix3x3(q_target_frame).getRPY(roll_meas, pitch_meas, yaw_meas); - - tf::Vector3 velocity_target_frame = velocity_transform*tflib::to_tf(odom.twist.twist.linear); - xdot_meas = velocity_target_frame.x(); - ydot_meas = velocity_target_frame.y(); - zdot_meas = velocity_target_frame.z(); - // if (zdot_meas > 6.0 ) { - // zdot_meas = 6.0; - // } - // else if (zdot_meas < -30.0) { - // zdot_meas = -30.0; - // } - // ROS_INFO_STREAM("transform time: " <= sat_rc_limit || rc_out.channels[1] >= sat_rc_limit || - rc_out.channels[2] >= sat_rc_limit || rc_out.channels[3] >= sat_rc_limit ) - saturated = true; - - if( !saturated && ekf_active.data && (sqMahanolobis >= reject_threshold*reject_threshold) ){ - - state_out = state_ap; - ROS_INFO_STREAM("Outlier measurement rejected. Sq Mahanolobis is "<< sqMahanolobis); - } - else{ - state_out = state_ap + K * innovation; - P = P - K * H * P; - } - - double radian_lim = attitude_comp_lim*3.1415/180.0; - state_out(6) = std::min(std::max( state_out(6), -radian_lim ), radian_lim); - state_out(7) = std::min(std::max( state_out(7), -radian_lim ), radian_lim); - state_out(8) = std::min(std::max( state_out(8), -g_lim ), g_lim); - - double xOut = state_out(0), xdotOut = state_out(3); - double yOut = state_out(1), ydotOut = state_out(4); - double zOut = state_out(2), zdotOut = state_out(5); - double roll_compOut = state_out(6); - double pitch_compOut = state_out(7); - double thrust_compOut = state_out(8); - - // set previous // - - // if(set_attitude==0){ - // pitch_prev = postCommandBuffer.back().pitch; - // roll_prev = postCommandBuffer.back().roll; - // } - if(set_attitude==1){ - pitch_prev = pitch_meas; - roll_prev = roll_meas; - } - else if(set_attitude==2){ - pitch_prev = pitch_px4_odom; - roll_prev = roll_px4_odom; - } - - x_prev = xOut; y_prev = yOut; z_prev = zOut; - xdot_prev = xdotOut; ydot_prev = ydotOut; zdot_prev = zdotOut; - roll_comp_prev=roll_compOut; pitch_comp_prev=pitch_compOut; thrust_comp_prev=thrust_compOut; - - prev_ekf_time = ros::Time::now(); - thrust_in_prev = thrust_in; - prev_odom = odom; - - //publish debug info - debug.header = odom.header; - debug.positionOut.x = xOut; - debug.positionOut.y = yOut; - debug.positionOut.z = zOut; - debug.velOut.x = xdotOut; - debug.velOut.y = ydotOut; - debug.velOut.z = zdotOut; - debug.disOut.x = roll_compOut; - debug.disOut.y = pitch_compOut; - debug.disOut.z = thrust_compOut; - - debug.position_ap.x = x_ap; - debug.position_ap.y = y_ap; - debug.position_ap.z = z_ap; - debug.vel_ap.x = xdot_ap; - debug.vel_ap.y = ydot_ap; - debug.vel_ap.z = zdot_ap; - debug.dis_ap.x = roll_comp_ap; - debug.dis_ap.y = pitch_comp_ap; - debug.dis_ap.z = thrust_comp_ap; - - debug.position_meas.x = x_meas; - debug.position_meas.y = y_meas; - debug.position_meas.z = z_meas; - debug.vel_meas.x = xdot_meas; - debug.vel_meas.y = ydot_meas; - debug.vel_meas.z = zdot_meas; - - debug.roll_meas = roll_meas; - debug.pitch_meas = pitch_meas; - debug.yaw_meas = yaw_meas; - // debug.roll_px4 = roll_px4; - // debug.pitch_px4 = pitch_px4; - debug.roll_px4_odom = roll_px4_odom; - debug.pitch_px4_odom = pitch_px4_odom; - - debug.model_cov_pos_xy = model_cov_pos_xy; - debug.model_cov_pos_z = model_cov_pos_z; - debug.model_cov_vel_xy = model_cov_vel_xy; - debug.model_cov_vel_z = model_cov_vel_z; - - debug.model_cov_dis_xy= model_cov_dis_xy; - debug.model_cov_dis_z= model_cov_dis_z; - - debug.meas_cov_pos_xy = meas_cov_pos_xy; - debug.meas_cov_pos_z = meas_cov_pos_z; - debug.meas_cov_vel_xy = meas_cov_vel_xy; - debug.meas_cov_vel_z = meas_cov_vel_z; - - debug.thrust_in = thrust_in; - - debug_pub.publish(debug); - // ROS_INFO_STREAM("Published Debug"); - - nav_msgs::Odometry stateOut; - stateOut.header.stamp = odom.header.stamp; // ros::Time::now() in actual system; - stateOut.header.frame_id = odom.header.frame_id; - stateOut.pose.pose.position.x = xOut; - stateOut.pose.pose.position.y = yOut; - stateOut.pose.pose.position.z = zOut; - - // stateOut.child_frame_id = odom.child_frame_id; - stateOut.child_frame_id = odom.header.frame_id; - stateOut.twist.twist.linear.x = xdotOut; - stateOut.twist.twist.linear.y = ydotOut; - stateOut.twist.twist.linear.z = zdotOut; - - stateOut.twist.twist.angular = odom.twist.twist.angular; - stateOut.pose.pose.orientation = odom.pose.pose.orientation; - - stateOut_pub.publish(stateOut); - - // publish Kalman Filter state - subt_control_ekf::kfState augmentedState; - augmentedState.header = odom.header; - - augmentedState.disOut.x = roll_compOut; - augmentedState.disOut.y = pitch_compOut; - augmentedState.disOut.z = thrust_compOut; - - kfState_pub.publish(augmentedState); - - // create tf for filtered_state - tf::StampedTransform transform = tflib::to_tf(stateOut, tf_prefix + "/filtered_odom"); - tf::StampedTransform transform_stabilized = tflib::get_stabilized(transform); - transform_stabilized.child_frame_id_ = tf_prefix + "/filtered_odom_stabilized"; - broadcaster->sendTransform(transform); - broadcaster->sendTransform(transform_stabilized); - - } - } - - //monitor.print_time_statistics(); - - return true; -} - - -void EKFControl::odometry_callback(nav_msgs::Odometry msg){ - got_odom = true; - odom = msg; -} - -void EKFControl::command_callback(mav_msgs::RollPitchYawrateThrust msg){ - got_command = true; - // command = msg; - ros::Time time_curr = ros::Time::now(); - double dt_temp_command = (time_curr - start_time_command).toSec(); - thrust_achieved_prev += (thrust_command_prev - thrust_achieved_prev)*(1 - exp(-alpha_motor*dt_temp_command)); - thrust_command_prev = throttleToThrust(msg.thrust.z); - start_time_command = time_curr; -} - -void EKFControl::in_air_callback(std_msgs::Bool msg){ - got_in_air = true; - in_air = msg; -} - -// void EKFControl::pixhawk_imu_callback(sensor_msgs::Imu msg){ - -// tf::Matrix3x3 m(tf::Quaternion(msg.orientation.x, msg.orientation.y, msg.orientation.z, msg.orientation.w)); -// double yaw; -// m.getRPY(roll_px4, pitch_px4, yaw); -// } - -void EKFControl::pixhawk_odom_callback(nav_msgs::Odometry msg){ - - got_odom = true; - px4_odom = msg; -} - -void EKFControl::battery_volt_callback(sensor_msgs::BatteryState msg){ - - got_battery_volt = true; - battery_volt = msg.voltage; -} - -void EKFControl::ekf_active_callback(std_msgs::Bool msg){ - got_ekf_active = true; - ekf_active = msg; -} - -void EKFControl::rc_out_callback(mavros_msgs::RCOut msg){ - got_rc_out = true; - rc_out = msg; -} - -void EKFControl::calcAP( double dtIn, double thrust_In, double thrust_In_prev, double& x_ap_, - double& y_ap_, double& z_ap_, double& xdot_ap_, double& ydot_ap_, double& zdot_ap_, double roll_comp_ap_, - double pitch_comp_ap_, double thrust_comp_ap_, double roll, double pitch, double yaw){ - - // incorporate thrust delay - // double thrust_eff_av = thrust_eff + (thrust_prev_running - thrust_eff) * (1 - pow(e, (-alpha_motor * dtIn)) )/alpha_motor; - // thrust_prev_running += (thrust_eff - thrust_prev_running) * (1 - exp(-alpha_motor*dtIn)); - - double thrust_eff_av = (thrust_In + thrust_In_prev)/2.0;// - thrust_comp_ap_*gravity; - double roll_eff = roll; double pitch_eff = pitch; - if(disturb_est_on && disturb_est_body_frame){ - roll_eff += -roll_comp_ap_; - pitch_eff += -pitch_comp_ap_; - } - - double a_x_comm, a_y_comm, a_z_comm; - a_x_comm = thrust_eff_av*(cos(yaw)*sin(pitch_eff)*cos(roll_eff) + sin(yaw)*sin(roll_eff)); - a_y_comm = thrust_eff_av*(sin(yaw)*sin(pitch_eff)*cos(roll_eff) - cos(yaw)*sin(roll_eff)); - a_z_comm = - gravity + thrust_eff_av*(cos(pitch_eff)*cos(roll_eff)); - - if(disturb_est_on && !disturb_est_body_frame){ - a_x_comm += -roll_comp_ap_; - a_y_comm += -pitch_comp_ap_; - } - if(disturb_est_on) - a_z_comm += -thrust_comp_ap_ * gravity; - - // this is Ax+Bu (pretty sure need to use yaw_meas) - x_ap_ += xdot_ap_*dtIn + 0.5*a_x_comm*dtIn*dtIn; - y_ap_ += ydot_ap_*dtIn + 0.5*a_y_comm*dtIn*dtIn; - z_ap_ += zdot_ap_*dtIn + 0.5*a_z_comm*dtIn*dtIn;// - 0.5*gravity*dtIn*dtIn*thrust_comp_ap_; - - xdot_ap_ += dtIn*( a_x_comm ); - ydot_ap_ += dtIn*( a_y_comm ); - zdot_ap_ += dtIn*( a_z_comm );// - gravity*dtIn*thrust_comp_ap_; - - // if (disturb_est_body_frame) { - // x_ap_ += -0.5*gravity*dtIn*dtIn*sin(yaw_meas)*roll_comp_ap_ - 0.5*gravity*dtIn*dtIn*cos(yaw_meas)*pitch_comp_ap_; - // y_ap_ += 0.5*gravity*dtIn*dtIn*cos(yaw_meas)*roll_comp_ap_ - 0.5*gravity*dtIn*dtIn*sin(yaw_meas)*pitch_comp_ap_; - // } - // else { - // x_ap_ -= 0.5*dtIn*dtIn*roll_comp_ap_; - // y_ap_ -= 0.5*dtIn*dtIn*pitch_comp_ap_; - // } - // if (disturb_est_body_frame) { - // xdot_ap_ += -gravity*dtIn*sin(yaw_meas)*roll_comp_ap_ - gravity*dtIn*cos(yaw_meas)*pitch_comp_ap_; - // ydot_ap_ += gravity*dtIn*cos(yaw_meas)*roll_comp_ap_ - gravity*dtIn*sin(yaw_meas)*pitch_comp_ap_; - // } - // else { - // xdot_ap_ -= dtIn*roll_comp_ap_; - // ydot_ap_ -= dtIn*pitch_comp_ap_; - // } - - // TODO may not need to multiply by gravity above since you multiply by gravity in A but idk, it may matter for the controller - - // roll_comp_ap, pitch_comp_ap, and thrust_comp_ap are just their prev values - - // ROS_INFO_STREAM("exiting calcAP"); -} - -double EKFControl::throttleToThrust(double throttle){ - - if(thrust_sim) - return gravity* (throttle / hover_throttle)*(throttle / hover_throttle); - else - return gravity * ( p1*throttle*throttle + p2*throttle + p3 ) - / ( p1*(hover_throttle)*(hover_throttle) + p2*(hover_throttle) + p3 ) ; -} - - -EKFControl::~EKFControl(){} - - -BaseNode* BaseNode::get(){ - EKFControl* subt_control_ekf = new EKFControl("EKFControl"); - return subt_control_ekf; -} diff --git a/robot/ros_ws/src/local/controls/attitude_controller_msgs/CMakeLists.txt b/robot/ros_ws/src/local/controls/attitude_controller_msgs/CMakeLists.txt deleted file mode 100644 index 19d6dac07..000000000 --- a/robot/ros_ws/src/local/controls/attitude_controller_msgs/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(attitude_controller_msgs) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# find dependencies -find_package(ament_cmake REQUIRED) -# uncomment the following section in order to fill in -# further dependencies manually. -# find_package( REQUIRED) - -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - # the following line skips the linter which checks for copyrights - # comment the line when a copyright and license is added to all source files - set(ament_cmake_copyright_FOUND TRUE) - # the following line skips cpplint (only works in a git repo) - # comment the line when this package is in a git repo and when - # a copyright and license is added to all source files - set(ament_cmake_cpplint_FOUND TRUE) - ament_lint_auto_find_test_dependencies() -endif() - -ament_package() diff --git a/robot/ros_ws/src/local/controls/attitude_controller_msgs/COLCON_IGNORE b/robot/ros_ws/src/local/controls/attitude_controller_msgs/COLCON_IGNORE deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/local/controls/attitude_controller_msgs/package.xml b/robot/ros_ws/src/local/controls/attitude_controller_msgs/package.xml deleted file mode 100644 index 9e0253a68..000000000 --- a/robot/ros_ws/src/local/controls/attitude_controller_msgs/package.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - attitude_controller_msgs - 0.0.0 - TODO: Package description - root - TODO: License declaration - - ament_cmake - - ament_lint_auto - ament_lint_common - - - ament_cmake - - diff --git a/robot/ros_ws/src/local/controls/mav_comm/mav_planning_msgs/package.xml b/robot/ros_ws/src/local/controls/mav_comm/mav_planning_msgs/package.xml index 2c477271a..d65f8d41e 100644 --- a/robot/ros_ws/src/local/controls/mav_comm/mav_planning_msgs/package.xml +++ b/robot/ros_ws/src/local/controls/mav_comm/mav_planning_msgs/package.xml @@ -3,9 +3,7 @@ mav_planning_msgs 3.3.3 - - Messages specific to MAV planning, especially polynomial planning. - + Messages and services specific to MAV planning, especially polynomial trajectory planning. Helen Oleynikova diff --git a/robot/ros_ws/src/local/controls/mav_comm/mav_state_machine_msgs/package.xml b/robot/ros_ws/src/local/controls/mav_comm/mav_state_machine_msgs/package.xml index 099abe786..0f6cb23e5 100644 --- a/robot/ros_ws/src/local/controls/mav_comm/mav_state_machine_msgs/package.xml +++ b/robot/ros_ws/src/local/controls/mav_comm/mav_state_machine_msgs/package.xml @@ -3,9 +3,7 @@ mav_state_machine_msgs 0.0.0 - - Messages specific to MAV state machine. - + Message definitions for starting and stopping tasks in MAV state machines. Christian Lanegger diff --git a/robot/ros_ws/src/local/controls/mav_comm/mav_system_msgs/package.xml b/robot/ros_ws/src/local/controls/mav_comm/mav_system_msgs/package.xml index a5574879a..639247a59 100644 --- a/robot/ros_ws/src/local/controls/mav_comm/mav_system_msgs/package.xml +++ b/robot/ros_ws/src/local/controls/mav_comm/mav_system_msgs/package.xml @@ -3,9 +3,7 @@ mav_system_msgs 0.0.0 - - Messages specific to MAV utils scripts. - + Messages for MAV system status such as CPU and process information. Christian Lanegger diff --git a/robot/ros_ws/src/local/controls/pid_controller/CMakeLists.txt b/robot/ros_ws/src/local/controls/pid_controller/CMakeLists.txt index 9c5d71947..177326c66 100644 --- a/robot/ros_ws/src/local/controls/pid_controller/CMakeLists.txt +++ b/robot/ros_ws/src/local/controls/pid_controller/CMakeLists.txt @@ -34,6 +34,11 @@ install(TARGETS pid_controller DESTINATION lib/${PROJECT_NAME}) +install(DIRECTORY + launch + config + DESTINATION share/${PROJECT_NAME}) + if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) diff --git a/robot/ros_ws/src/local/controls/pid_controller/config/pid_controller.yaml b/robot/ros_ws/src/local/controls/pid_controller/config/pid_controller.yaml new file mode 100644 index 000000000..8f86acfcf --- /dev/null +++ b/robot/ros_ws/src/local/controls/pid_controller/config/pid_controller.yaml @@ -0,0 +1,61 @@ +# pid_controller node parameters (position + velocity PID cascade gains). +# Values moved verbatim from the inline block the legacy +# local_bringup/launch/local.launch.xml carried for pid_controller +# (P5-E2, RFC #379); loaded by launch/pid_controller.launch.xml. +/**: + ros__parameters: + target_frame: base_link_stabilized + + x_p: 1.0 + x_i: 0.0 + x_d: 0.0 + x_ff: 0.0 + x_d_alpha: 0.0 + x_min: -3.0 + x_max: 3.0 + x_constant: 0.0 + + y_p: 1.0 + y_i: 0.0 + y_d: 0.0 + y_ff: 0.0 + y_d_alpha: 0.0 + y_min: -3.0 + y_max: 3.0 + y_constant: 0.0 + + z_p: 1.0 + z_i: 0.0 + z_d: 0.0 + z_ff: 0.0 + z_d_alpha: 0.0 + z_min: -1.0 + z_max: 1.0 + z_constant: 0.0 + + vx_p: 0.2 + vx_i: 0.025 + vx_d: 0.0 + vx_ff: 0.0 + vx_d_alpha: 0.9 + vx_min: -0.34 + vx_max: 0.34 + vx_constant: 0.0 + + vy_p: 0.2 + vy_i: 0.025 + vy_d: 0.0 + vy_ff: 0.0 + vy_d_alpha: 0.9 + vy_min: -0.34 + vy_max: 0.34 + vy_constant: 0.0 + + vz_p: 0.2 + vz_i: 0.1 + vz_d: 0.0 + vz_ff: 0.0 + vz_d_alpha: 0.0 + vz_min: -0.5 + vz_max: 1.0 + vz_constant: 0.5 diff --git a/robot/ros_ws/src/local/controls/pid_controller/launch/pid_controller.launch.xml b/robot/ros_ws/src/local/controls/pid_controller/launch/pid_controller.launch.xml new file mode 100644 index 000000000..cca498e77 --- /dev/null +++ b/robot/ros_ws/src/local/controls/pid_controller/launch/pid_controller.launch.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/local/controls/pid_controller/package.xml b/robot/ros_ws/src/local/controls/pid_controller/package.xml index 75d601b2e..7bef31a22 100644 --- a/robot/ros_ws/src/local/controls/pid_controller/package.xml +++ b/robot/ros_ws/src/local/controls/pid_controller/package.xml @@ -3,9 +3,9 @@ pid_controller 0.0.0 - TODO: Package description - root - TODO: License declaration + Cascaded PID controller that tracks the trajectory controller's tracking point against odometry and publishes roll/pitch/yaw-rate/thrust commands. + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/local/controls/pid_controller_msgs/package.xml b/robot/ros_ws/src/local/controls/pid_controller_msgs/package.xml index ba8f3126e..df190c09d 100644 --- a/robot/ros_ws/src/local/controls/pid_controller_msgs/package.xml +++ b/robot/ros_ws/src/local/controls/pid_controller_msgs/package.xml @@ -3,9 +3,9 @@ pid_controller_msgs 0.0.0 - TODO: Package description - root - TODO: License declaration + ROS 2 message definitions for the AirStack PID controller (PIDInfo controller-state telemetry). + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/local/controls/px4_msgs/.github/dependabot.yml b/robot/ros_ws/src/local/controls/px4_msgs/.github/dependabot.yml deleted file mode 100644 index c7fc735ee..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: "daily" diff --git a/robot/ros_ws/src/local/controls/px4_msgs/.github/workflows/build.yml b/robot/ros_ws/src/local/controls/px4_msgs/.github/workflows/build.yml deleted file mode 100644 index 0a467bba1..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/.github/workflows/build.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Build package - -# CI runs over all branches that do not contain 'ros1' in the name -on: - push: - schedule: - - cron: '0 0 * * *' - -defaults: - run: - shell: bash - -jobs: - focal: - name: "Build on Ubuntu Focal" - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v4 - - uses: ros-tooling/setup-ros@v0.6 - with: - required-ros-distributions: foxy - - uses: ros-tooling/action-ros-ci@v0.3 - with: - package-name: px4_msgs - target-ros2-distro: foxy - jammy: - name: "Build on Ubuntu Jammy" - runs-on: ubuntu-22.04 - strategy: - matrix: - ros2_distro: [humble, rolling] - steps: - - uses: actions/checkout@v4 - - uses: ros-tooling/setup-ros@v0.6 - with: - required-ros-distributions: ${{ matrix.ros2_distro }} - - uses: ros-tooling/action-ros-ci@v0.3 - with: - package-name: px4_msgs - target-ros2-distro: ${{ matrix.ros2_distro }} \ No newline at end of file diff --git a/robot/ros_ws/src/local/controls/px4_msgs/.gitignore b/robot/ros_ws/src/local/controls/px4_msgs/.gitignore deleted file mode 100644 index b89e19841..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -msg/idl.cc diff --git a/robot/ros_ws/src/local/controls/px4_msgs/CMakeLists.txt b/robot/ros_ws/src/local/controls/px4_msgs/CMakeLists.txt deleted file mode 100644 index 0fc6edc22..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -project(px4_msgs) - -list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake") - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra) -endif() - -find_package(ament_cmake REQUIRED) -find_package(builtin_interfaces REQUIRED) -find_package(rosidl_default_generators REQUIRED) - -# ############################################################################## -# Generate ROS messages, ROS2 interfaces and IDL files # -# ############################################################################## - -# get all msg files -set(MSGS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/msg") -file(GLOB PX4_MSGS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${MSGS_DIR}/*.msg") - -# get all srv files -set(SRVS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/srv") -file(GLOB PX4_SRVS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${SRVS_DIR}/*.srv") - -# Generate introspection typesupport for C and C++ and IDL files -rosidl_generate_interfaces(${PROJECT_NAME} - ${PX4_MSGS} - ${PX4_SRVS} - DEPENDENCIES builtin_interfaces - ADD_LINTER_TESTS -) - -ament_export_dependencies(rosidl_default_runtime) - -ament_package() diff --git a/robot/ros_ws/src/local/controls/px4_msgs/CONTRIBUTING.md b/robot/ros_ws/src/local/controls/px4_msgs/CONTRIBUTING.md deleted file mode 100644 index 5df008e53..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# Contributing - -*Do not* commit changes directly to this repository that change the message definitions. All the message definitions are directly generated from the [uORB msg definitions](https://github.com/PX4/Firmware/tree/master/msg) on the [PX4 Firmware repository](https://github.com/PX4/Firmware). Any fixes or improvements one finds suitable to apply to the message definitions should be directly done on the uORB message files. The deployment of these are taken care by a Jenkins CI/CD stage. - -### Contributing to the PX4 Firmware repository (or to this repository, not including message definitions) - -Follow the [`Contributing` guide](https://github.com/PX4/Firmware/blob/master/CONTRIBUTING.md) from the PX4 Firmware repo. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/LICENSE b/robot/ros_ws/src/local/controls/px4_msgs/LICENSE deleted file mode 100644 index 31f2eb48f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2019, PX4 Development Team -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/README.md b/robot/ros_ws/src/local/controls/px4_msgs/README.md deleted file mode 100644 index 5cbfa157b..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# px4_msgs - -[![GitHub license](https://img.shields.io/github/license/PX4/px4_msgs.svg)](https://github.com/PX4/px4_msg/blob/master/LICENSE) [![Build package](https://github.com/PX4/px4_msgs/workflows/Build%20package/badge.svg)](https://github.com/PX4/px4_msgs/actions) - -[![Discord Shield](https://discordapp.com/api/guilds/1022170275984457759/widget.png?style=shield)](https://discord.gg/dronecode) - -ROS 2 message definitions for the [PX4 Autopilot](https://px4.io/) project. - -Building this package generates all the required interfaces to interface ROS 2 nodes with the PX4 internals. - -## Supported versions and compatibility - -Depending on the PX4 and ROS versions you want to use, you need to checkout the appropriate branch of this package: - -| PX4 | ROS 2 | Ubuntu | branch | -|----------------|---------|--------------|-------------------------------------------------------------------| -| [v1.13](https://github.com/PX4/px4_msgs/tree/release/1.13) | Foxy | Ubuntu 20.04 | [release/1.13](https://github.com/PX4/px4_msgs/tree/release/1.13) | -| [v1.14](https://github.com/PX4/px4_msgs/tree/release/1.14) | Foxy | Ubuntu 20.04 | [release/1.14](https://github.com/PX4/px4_msgs/tree/release/1.14) | -| [v1.14](https://github.com/PX4/px4_msgs/tree/release/1.14) | Humble | Ubuntu 22.04 | [release/1.14](https://github.com/PX4/px4_msgs/tree/release/1.14) | -| [v1.14](https://github.com/PX4/px4_msgs/tree/release/1.14) | Rolling | Ubuntu 22.04 | [release/1.14](https://github.com/PX4/px4_msgs/tree/release/1.14) | -| [main](https://github.com/PX4/px4_msgs/tree/main) | Foxy | Ubuntu 22.04 | [main](https://github.com/PX4/px4_msgs) | -| [main](https://github.com/PX4/px4_msgs/tree/main) | Humble | Ubuntu 22.04 | [main](https://github.com/PX4/px4_msgs) | -| [main](https://github.com/PX4/px4_msgs/tree/main) | Rolling | Ubuntu 22.04 | [main](https://github.com/PX4/px4_msgs) | - -### Messages Sync from PX4 - -When PX4 message definitions in the `main` branch of [PX4 Autopilot](https://github.com/PX4/PX4-Autopilot) change, a [CI/CD pipeline](https://github.com/PX4/PX4-Autopilot/blob/main/.github/workflows/metadata.yml#L119) automatically copies and pushes updated ROS message definitions to this repository. This ensures that this repository `main` branch and the PX4-Autopilot `main` branch are always up to date. -However, if you are using a custom PX4 version and you modified existing messages or created new one, then you have to manually synchronize them in this repository: -### Manual Message Sync - -- Checkout the correct branch associated to the PX4 version from which you detached you custom version. -- Delete all `*.msg` files in `msg/` and copy all `*.msg` files from `PX4-Autopilot/msg/` in it. Assuming that this repository and the PX4-Autopilot repository are placed in your home folder, you can run: - ```sh - rm -f ~/px4_msgs/msg/*.msg - cp ~/PX4-Autopilot/msg/*.msg ~/px4_msgs/msg/ - ``` - -## Install, build and usage - -Check [Using colcon to build packages](https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.html#build-a-package) to understand how this can be built inside a workspace. Check the [PX4 ROS 2 User Guide](https://docs.px4.io/main/en/ros/ros2_comm.html) section on the PX4 documentation for further details on how this integrates PX4 and how to exchange messages with the autopilot. - -## Bug tracking and feature requests - -Use the [Issues](https://github.com/PX4/px4_msgs/issues) section to create a new issue. Report your issue or feature request [here](https://github.com/PX4/px4_msgs/issues/new). - -## Questions and troubleshooting - -Reach the PX4 development team on the [PX4 Discord Server](https://discord.gg/dronecode). - diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActionRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ActionRequest.msg deleted file mode 100644 index 888814e0c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActionRequest.msg +++ /dev/null @@ -1,19 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 action # what action is requested -uint8 ACTION_DISARM = 0 -uint8 ACTION_ARM = 1 -uint8 ACTION_TOGGLE_ARMING = 2 -uint8 ACTION_UNKILL = 3 -uint8 ACTION_KILL = 4 -uint8 ACTION_SWITCH_MODE = 5 -uint8 ACTION_VTOL_TRANSITION_TO_MULTICOPTER = 6 -uint8 ACTION_VTOL_TRANSITION_TO_FIXEDWING = 7 - -uint8 source # how the request was triggered -uint8 SOURCE_RC_STICK_GESTURE = 0 -uint8 SOURCE_RC_SWITCH = 1 -uint8 SOURCE_RC_BUTTON = 2 -uint8 SOURCE_RC_MODE_SLOT = 3 - -uint8 mode # for ACTION_SWITCH_MODE what mode is requested according to vehicle_status_s::NAVIGATION_STATE_* diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorArmed.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorArmed.msg deleted file mode 100644 index 6867adf2c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorArmed.msg +++ /dev/null @@ -1,9 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -bool armed # Set to true if system is armed -bool prearmed # Set to true if the actuator safety is disabled but motors are not armed -bool ready_to_arm # Set to true if system is ready to be armed -bool lockdown # Set to true if actuators are forced to being disabled (due to emergency or HIL) -bool manual_lockdown # Set to true if manual throttle kill switch is engaged -bool force_failsafe # Set to true if the actuators are forced to the failsafe position -bool in_esc_calibration_mode # IO/FMU should ignore messages from the actuator controls topics diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorControlsStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorControlsStatus.msg deleted file mode 100644 index c89f669eb..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorControlsStatus.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32[3] control_power - -# TOPICS actuator_controls_status_0 actuator_controls_status_1 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorMotors.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorMotors.msg deleted file mode 100644 index e74566f1f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorMotors.msg +++ /dev/null @@ -1,12 +0,0 @@ -# Motor control message -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp the data this control response is based on was sampled - -uint16 reversible_flags # bitset which motors are configured to be reversible - -uint8 ACTUATOR_FUNCTION_MOTOR1 = 101 - -uint8 NUM_CONTROLS = 12 -float32[12] control # range: [-1, 1], where 1 means maximum positive thrust, - # -1 maximum negative (if not supported by the output, <0 maps to NaN), - # and NaN maps to disarmed (stop the motors) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorOutputs.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorOutputs.msg deleted file mode 100644 index 3209e54e3..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorOutputs.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 NUM_ACTUATOR_OUTPUTS = 16 -uint8 NUM_ACTUATOR_OUTPUT_GROUPS = 4 # for sanity checking -uint32 noutputs # valid outputs -float32[16] output # output data, in natural output units - -# actuator_outputs_sim is used for SITL, HITL & SIH (with an output range of [-1, 1]) -# TOPICS actuator_outputs actuator_outputs_sim actuator_outputs_debug diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorServos.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorServos.msg deleted file mode 100644 index 2c7900e81..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorServos.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Servo control message -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp the data this control response is based on was sampled - -uint8 NUM_CONTROLS = 8 -float32[8] control # range: [-1, 1], where 1 means maximum positive position, - # -1 maximum negative, - # and NaN maps to disarmed diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorServosTrim.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorServosTrim.msg deleted file mode 100644 index 30953e7ae..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorServosTrim.msg +++ /dev/null @@ -1,5 +0,0 @@ -# Servo trims, added as offset to servo outputs -uint64 timestamp # time since system start (microseconds) - -uint8 NUM_CONTROLS = 8 -float32[8] trim # range: [-1, 1] diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorTest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorTest.msg deleted file mode 100644 index 221258f90..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ActuatorTest.msg +++ /dev/null @@ -1,21 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -# Topic to test individual actuator output functions - -uint8 ACTION_RELEASE_CONTROL = 0 # exit test mode for the given function -uint8 ACTION_DO_CONTROL = 1 # enable actuator test mode - -uint8 FUNCTION_MOTOR1 = 101 -uint8 MAX_NUM_MOTORS = 12 -uint8 FUNCTION_SERVO1 = 201 -uint8 MAX_NUM_SERVOS = 8 - -uint8 action # one of ACTION_* -uint16 function # actuator output function -float32 value # range: [-1, 1], where 1 means maximum positive output, - # 0 to center servos or minimum motor thrust, - # -1 maximum negative (if not supported by the motors, <0 maps to NaN), - # and NaN maps to disarmed (stop the motors) -uint32 timeout_ms # timeout in ms after which to exit test mode (if 0, do not time out) - -uint8 ORB_QUEUE_LENGTH = 16 # >= MAX_NUM_MOTORS to support code in esc_calibration diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/AdcReport.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/AdcReport.msg deleted file mode 100644 index 1ae72b6d6..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/AdcReport.msg +++ /dev/null @@ -1,6 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint32 device_id # unique device ID for the sensor that does not change between power cycles -int16[12] channel_id # ADC channel IDs, negative for non-existent, TODO: should be kept same as array index -int32[12] raw_data # ADC channel raw value, accept negative value, valid if channel ID is positive -uint32 resolution # ADC channel resolution -float32 v_ref # ADC channel voltage reference, use to calculate LSB voltage(lsb=scale/resolution) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Airspeed.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Airspeed.msg deleted file mode 100644 index aaed7b72c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Airspeed.msg +++ /dev/null @@ -1,10 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -float32 indicated_airspeed_m_s # indicated airspeed in m/s - -float32 true_airspeed_m_s # true filtered airspeed in m/s - -float32 air_temperature_celsius # air temperature in degrees Celsius, -1000 if unknown - -float32 confidence # confidence value from 0 to 1 for this sensor diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/AirspeedValidated.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/AirspeedValidated.msg deleted file mode 100644 index 06731cc41..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/AirspeedValidated.msg +++ /dev/null @@ -1,12 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 indicated_airspeed_m_s # indicated airspeed in m/s (IAS), set to NAN if invalid -float32 calibrated_airspeed_m_s # calibrated airspeed in m/s (CAS, accounts for instrumentation errors), set to NAN if invalid -float32 true_airspeed_m_s # true filtered airspeed in m/s (TAS), set to NAN if invalid - -float32 calibrated_ground_minus_wind_m_s # CAS calculated from groundspeed - windspeed, where windspeed is estimated based on a zero-sideslip assumption, set to NAN if invalid -float32 true_ground_minus_wind_m_s # TAS calculated from groundspeed - windspeed, where windspeed is estimated based on a zero-sideslip assumption, set to NAN if invalid - -bool airspeed_sensor_measurement_valid # True if data from at least one airspeed sensor is declared valid. - -int8 selected_airspeed_index # 1-3: airspeed sensor index, 0: groundspeed-windspeed, -1: airspeed invalid diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/AirspeedWind.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/AirspeedWind.msg deleted file mode 100644 index 6ca513a35..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/AirspeedWind.msg +++ /dev/null @@ -1,26 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float32 windspeed_north # Wind component in north / X direction (m/sec) -float32 windspeed_east # Wind component in east / Y direction (m/sec) - -float32 variance_north # Wind estimate error variance in north / X direction (m/sec)**2 - set to zero (no uncertainty) if not estimated -float32 variance_east # Wind estimate error variance in east / Y direction (m/sec)**2 - set to zero (no uncertainty) if not estimated - -float32 tas_innov # True airspeed innovation -float32 tas_innov_var # True airspeed innovation variance - -float32 tas_scale_raw # Estimated true airspeed scale factor (not validated) -float32 tas_scale_raw_var # True airspeed scale factor variance - -float32 tas_scale_validated # Estimated true airspeed scale factor after validation - -float32 beta_innov # Sideslip measurement innovation -float32 beta_innov_var # Sideslip measurement innovation variance - -uint8 source # source of wind estimate - -uint8 SOURCE_AS_BETA_ONLY = 0 # wind estimate only based on synthetic sideslip fusion -uint8 SOURCE_AS_SENSOR_1 = 1 # combined synthetic sideslip and airspeed fusion (data from first airspeed sensor) -uint8 SOURCE_AS_SENSOR_2 = 2 # combined synthetic sideslip and airspeed fusion (data from second airspeed sensor) -uint8 SOURCE_AS_SENSOR_3 = 3 # combined synthetic sideslip and airspeed fusion (data from third airspeed sensor) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ArmingCheckReply.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ArmingCheckReply.msg deleted file mode 100644 index 589ad1b1c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ArmingCheckReply.msg +++ /dev/null @@ -1,33 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 request_id -uint8 registration_id - -uint8 HEALTH_COMPONENT_INDEX_NONE = 0 -uint8 HEALTH_COMPONENT_INDEX_AVOIDANCE = 19 - -uint8 health_component_index # HEALTH_COMPONENT_INDEX_* -bool health_component_is_present -bool health_component_warning -bool health_component_error - -bool can_arm_and_run # whether arming is possible, and if it's a navigation mode, if it can run - -uint8 num_events - -Event[5] events - -# Mode requirements -bool mode_req_angular_velocity -bool mode_req_attitude -bool mode_req_local_alt -bool mode_req_local_position -bool mode_req_local_position_relaxed -bool mode_req_global_position -bool mode_req_mission -bool mode_req_home_position -bool mode_req_prevent_arming -bool mode_req_manual_control - - -uint8 ORB_QUEUE_LENGTH = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ArmingCheckRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ArmingCheckRequest.msg deleted file mode 100644 index 69e7e85f3..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ArmingCheckRequest.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -# broadcast message to request all registered arming checks to be reported - -uint8 request_id diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/AutotuneAttitudeControlStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/AutotuneAttitudeControlStatus.msg deleted file mode 100644 index 021a8c345..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/AutotuneAttitudeControlStatus.msg +++ /dev/null @@ -1,35 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32[5] coeff # coefficients of the identified discrete-time model -float32[5] coeff_var # coefficients' variance of the identified discrete-time model -float32 fitness # fitness of the parameter estimate -float32 innov -float32 dt_model - -float32 kc -float32 ki -float32 kd -float32 kff -float32 att_p - -float32[3] rate_sp - -float32 u_filt -float32 y_filt - -uint8 STATE_IDLE = 0 -uint8 STATE_INIT = 1 -uint8 STATE_ROLL = 2 -uint8 STATE_ROLL_PAUSE = 3 -uint8 STATE_PITCH = 4 -uint8 STATE_PITCH_PAUSE = 5 -uint8 STATE_YAW = 6 -uint8 STATE_YAW_PAUSE = 7 -uint8 STATE_VERIFICATION = 8 -uint8 STATE_APPLY = 9 -uint8 STATE_TEST = 10 -uint8 STATE_COMPLETE = 11 -uint8 STATE_FAIL = 12 -uint8 STATE_WAIT_FOR_DISARM = 13 - -uint8 state diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/BatteryStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/BatteryStatus.msg deleted file mode 100644 index 66fcffa0e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/BatteryStatus.msg +++ /dev/null @@ -1,78 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -bool connected # Whether or not a battery is connected, based on a voltage threshold -float32 voltage_v # Battery voltage in volts, 0 if unknown -float32 voltage_filtered_v # Battery voltage in volts, filtered, 0 if unknown -float32 current_a # Battery current in amperes, -1 if unknown -float32 current_filtered_a # Battery current in amperes, filtered, 0 if unknown -float32 current_average_a # Battery current average in amperes (for FW average in level flight), -1 if unknown -float32 discharged_mah # Discharged amount in mAh, -1 if unknown -float32 remaining # From 1 to 0, -1 if unknown -float32 scale # Power scaling factor, >= 1, or -1 if unknown -float32 time_remaining_s # predicted time in seconds remaining until battery is empty under previous averaged load, NAN if unknown -float32 temperature # temperature of the battery. NaN if unknown -uint8 cell_count # Number of cells, 0 if unknown - -uint8 BATTERY_SOURCE_POWER_MODULE = 0 -uint8 BATTERY_SOURCE_EXTERNAL = 1 -uint8 BATTERY_SOURCE_ESCS = 2 -uint8 source # Battery source -uint8 priority # Zero based priority is the connection on the Power Controller V1..Vn AKA BrickN-1 -uint16 capacity # actual capacity of the battery -uint16 cycle_count # number of discharge cycles the battery has experienced -uint16 average_time_to_empty # predicted remaining battery capacity based on the average rate of discharge in min -uint16 serial_number # serial number of the battery pack -uint16 manufacture_date # manufacture date, part of serial number of the battery pack. Formatted as: Day + Month×32 + (Year–1980)×512 -uint16 state_of_health # state of health. FullChargeCapacity/DesignCapacity, 0-100%. -uint16 max_error # max error, expected margin of error in % in the state-of-charge calculation with a range of 1 to 100% -uint8 id # ID number of a battery. Should be unique and consistent for the lifetime of a vehicle. 1-indexed. -uint16 interface_error # interface error counter - -float32[14] voltage_cell_v # Battery individual cell voltages, 0 if unknown -float32 max_cell_voltage_delta # Max difference between individual cell voltages - -bool is_powering_off # Power off event imminent indication, false if unknown -bool is_required # Set if the battery is explicitly required before arming - - -uint8 BATTERY_WARNING_NONE = 0 # no battery low voltage warning active -uint8 BATTERY_WARNING_LOW = 1 # warning of low voltage -uint8 BATTERY_WARNING_CRITICAL = 2 # critical voltage, return / abort immediately -uint8 BATTERY_WARNING_EMERGENCY = 3 # immediate landing required -uint8 BATTERY_WARNING_FAILED = 4 # the battery has failed completely -uint8 BATTERY_STATE_UNHEALTHY = 6 # Battery is diagnosed to be defective or an error occurred, usage is discouraged / prohibited. Possible causes (faults) are listed in faults field. -uint8 BATTERY_STATE_CHARGING = 7 # Battery is charging - -uint8 BATTERY_FAULT_DEEP_DISCHARGE = 0 # Battery has deep discharged -uint8 BATTERY_FAULT_SPIKES = 1 # Voltage spikes -uint8 BATTERY_FAULT_CELL_FAIL= 2 # One or more cells have failed -uint8 BATTERY_FAULT_OVER_CURRENT = 3 # Over-current -uint8 BATTERY_FAULT_OVER_TEMPERATURE = 4 # Over-temperature -uint8 BATTERY_FAULT_UNDER_TEMPERATURE = 5 # Under-temperature fault -uint8 BATTERY_FAULT_INCOMPATIBLE_VOLTAGE = 6 # Vehicle voltage is not compatible with battery one -uint8 BATTERY_FAULT_INCOMPATIBLE_FIRMWARE = 7 # Battery firmware is not compatible with current autopilot firmware -uint8 BATTERY_FAULT_INCOMPATIBLE_MODEL = 8 # Battery model is not supported by the system -uint8 BATTERY_FAULT_HARDWARE_FAILURE = 9 # hardware problem -uint8 BATTERY_WARNING_OVER_TEMPERATURE = 10 # Over-temperature -uint8 BATTERY_FAULT_COUNT = 11 # Counter - keep it as last element! - -uint16 faults # Smart battery supply status/fault flags (bitmask) for health indication. -uint32 custom_faults # Bitmask indicating smart battery internal manufacturer faults, those are not user actionable. -uint8 warning # Current battery warning -uint8 mode # Battery mode. Note, the normal operation mode - -uint8 BATTERY_MODE_UNKNOWN = 0 # Battery does not support a mode, or if it does, is operational -uint8 BATTERY_MODE_AUTO_DISCHARGING = 1 # Battery is auto discharging (towards storage level) -uint8 BATTERY_MODE_HOT_SWAP = 2 # Battery in hot-swap mode -uint8 BATTERY_MODE_COUNT = 3 # Counter - keep it as last element (once we're fully migrated to events interface we can just comment this)! - - -uint8 MAX_INSTANCES = 4 - -float32 average_power # The average power of the current discharge -float32 available_energy # The predicted charge or energy remaining in the battery -float32 full_charge_capacity_wh # The compensated battery capacity -float32 remaining_capacity_wh # The compensated battery capacity remaining -float32 design_capacity # The design capacity of the battery -uint16 average_time_to_full # The predicted remaining time until the battery reaches full charge, in minutes -uint16 over_discharge_count # Number of battery overdischarge -float32 nominal_voltage # Nominal voltage of the battery pack diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Buffer128.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Buffer128.msg deleted file mode 100644 index 342aa83db..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Buffer128.msg +++ /dev/null @@ -1,9 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 len # length of data -uint32 MAX_BUFLEN = 128 - -uint8[128] data # data - -# TOPICS voxl2_io_data - diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ButtonEvent.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ButtonEvent.msg deleted file mode 100644 index bbca356aa..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ButtonEvent.msg +++ /dev/null @@ -1,6 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -bool triggered # Set to true if the event is triggered - -# TOPICS button_event safety_button - -uint8 ORB_QUEUE_LENGTH = 2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraCapture.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraCapture.msg deleted file mode 100644 index 141bb2eb6..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraCapture.msg +++ /dev/null @@ -1,9 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_utc # Capture time in UTC / GPS time -uint32 seq # Image sequence number -float64 lat # Latitude in degrees (WGS84) -float64 lon # Longitude in degrees (WGS84) -float32 alt # Altitude (AMSL) -float32 ground_distance # Altitude above ground (meters) -float32[4] q # Attitude of the camera relative to NED earth-fixed frame when using a gimbal, otherwise vehicle attitude -int8 result # 1 for success, 0 for failure, -1 if camera does not provide feedback diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraStatus.msg deleted file mode 100644 index c83be897d..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraStatus.msg +++ /dev/null @@ -1,4 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 active_sys_id # mavlink system id of the currently active camera -uint8 active_comp_id # mavlink component id of currently active camera diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraTrigger.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraTrigger.msg deleted file mode 100644 index abfdac6dd..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/CameraTrigger.msg +++ /dev/null @@ -1,7 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_utc # UTC timestamp - -uint32 seq # Image sequence number -bool feedback # Trigger feedback from camera - -uint32 ORB_QUEUE_LENGTH = 2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/CanInterfaceStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/CanInterfaceStatus.msg deleted file mode 100644 index 4129c8d56..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/CanInterfaceStatus.msg +++ /dev/null @@ -1,6 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 interface - -uint64 io_errors -uint64 frames_tx -uint64 frames_rx diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/CellularStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/CellularStatus.msg deleted file mode 100644 index 5a1c8bac7..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/CellularStatus.msg +++ /dev/null @@ -1,28 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 CELLULAR_STATUS_FLAG_UNKNOWN=0 # State unknown or not reportable -uint8 CELLULAR_STATUS_FLAG_FAILED=1 # velocity setpoint -uint8 CELLULAR_STATUS_FLAG_INITIALIZING=2 # Modem is being initialized -uint8 CELLULAR_STATUS_FLAG_LOCKED=3 # Modem is locked -uint8 CELLULAR_STATUS_FLAG_DISABLED=4 # Modem is not enabled and is powered down -uint8 CELLULAR_STATUS_FLAG_DISABLING=5 # Modem is currently transitioning to the CELLULAR_STATUS_FLAG_DISABLED state -uint8 CELLULAR_STATUS_FLAG_ENABLING=6 # Modem is currently transitioning to the CELLULAR_STATUS_FLAG_ENABLED state -uint8 CELLULAR_STATUS_FLAG_ENABLED=7 # Modem is enabled and powered on but not registered with a network provider and not available for data connections -uint8 CELLULAR_STATUS_FLAG_SEARCHING=8 # Modem is searching for a network provider to register -uint8 CELLULAR_STATUS_FLAG_REGISTERED=9 # Modem is registered with a network provider, and data connections and messaging may be available for use -uint8 CELLULAR_STATUS_FLAG_DISCONNECTING=10 # Modem is disconnecting and deactivating the last active packet data bearer. This state will not be entered if more than one packet data bearer is active and one of the active bearers is deactivated -uint8 CELLULAR_STATUS_FLAG_CONNECTING=11 # Modem is activating and connecting the first packet data bearer. Subsequent bearer activations when another bearer is already active do not cause this state to be entered -uint8 CELLULAR_STATUS_FLAG_CONNECTED=12 # One or more packet data bearers is active and connected - -uint8 CELLULAR_NETWORK_FAILED_REASON_NONE=0 # No error -uint8 CELLULAR_NETWORK_FAILED_REASON_UNKNOWN=1 # Error state is unknown -uint8 CELLULAR_NETWORK_FAILED_REASON_SIM_MISSING=2 # SIM is required for the modem but missing -uint8 CELLULAR_NETWORK_FAILED_REASON_SIM_ERROR=3 # SIM is available, but not usable for connection - -uint16 status # Status bitmap 1: Roaming is active -uint8 failure_reason #Failure reason when status in in CELLUAR_STATUS_FAILED -uint8 type # Cellular network radio type 0: none 1: gsm 2: cdma 3: wcdma 4: lte -uint8 quality # Cellular network RSSI/RSRP in dBm, absolute value -uint16 mcc # Mobile country code. If unknown, set to: UINT16_MAX -uint16 mnc # Mobile network code. If unknown, set to: UINT16_MAX -uint16 lac # Location area code. If unknown, set to: 0 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/CollisionConstraints.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/CollisionConstraints.msg deleted file mode 100644 index 40f67e29c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/CollisionConstraints.msg +++ /dev/null @@ -1,7 +0,0 @@ -# Local setpoint constraints in NED frame -# setting something to NaN means that no limit is provided - -uint64 timestamp # time since system start (microseconds) - -float32[2] original_setpoint # velocities demanded -float32[2] adapted_setpoint # velocities allowed diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/CollisionReport.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/CollisionReport.msg deleted file mode 100644 index 1ad7ce726..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/CollisionReport.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 src -uint32 id -uint8 action -uint8 threat_level -float32 time_to_minimum_delta -float32 altitude_minimum_delta -float32 horizontal_minimum_delta diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ConfigOverrides.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ConfigOverrides.msg deleted file mode 100644 index 09b87253a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ConfigOverrides.msg +++ /dev/null @@ -1,18 +0,0 @@ -# Configurable overrides by (external) modes or mode executors -uint64 timestamp # time since system start (microseconds) - -bool disable_auto_disarm # Prevent the drone from automatically disarming after landing (if configured) - -bool defer_failsafes # Defer all failsafes that can be deferred (until the flag is cleared) -int16 defer_failsafes_timeout_s # Maximum time a failsafe can be deferred. 0 = system default, -1 = no timeout - - -int8 SOURCE_TYPE_MODE = 0 -int8 SOURCE_TYPE_MODE_EXECUTOR = 1 -int8 source_type - -uint8 source_id # ID depending on source_type - -uint8 ORB_QUEUE_LENGTH = 4 - -# TOPICS config_overrides config_overrides_request diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ControlAllocatorStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ControlAllocatorStatus.msg deleted file mode 100644 index 2d7b08832..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ControlAllocatorStatus.msg +++ /dev/null @@ -1,21 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -bool torque_setpoint_achieved # Boolean indicating whether the 3D torque setpoint was correctly allocated to actuators. 0 if not achieved, 1 if achieved. -float32[3] unallocated_torque # Unallocated torque. Equal to 0 if the setpoint was achieved. - # Computed as: unallocated_torque = torque_setpoint - allocated_torque - -bool thrust_setpoint_achieved # Boolean indicating whether the 3D thrust setpoint was correctly allocated to actuators. 0 if not achieved, 1 if achieved. -float32[3] unallocated_thrust # Unallocated thrust. Equal to 0 if the setpoint was achieved. - # Computed as: unallocated_thrust = thrust_setpoint - allocated_thrust - -int8 ACTUATOR_SATURATION_OK = 0 # The actuator is not saturated -int8 ACTUATOR_SATURATION_UPPER_DYN = 1 # The actuator is saturated (with a value <= the desired value) because it cannot increase its value faster -int8 ACTUATOR_SATURATION_UPPER = 2 # The actuator is saturated (with a value <= the desired value) because it has reached its maximum value -int8 ACTUATOR_SATURATION_LOWER_DYN = -1 # The actuator is saturated (with a value >= the desired value) because it cannot decrease its value faster -int8 ACTUATOR_SATURATION_LOWER = -2 # The actuator is saturated (with a value >= the desired value) because it has reached its minimum value - -int8[16] actuator_saturation # Indicates actuator saturation status. - # Note 1: actuator saturation does not necessarily imply that the thrust setpoint or the torque setpoint were not achieved. - # Note 2: an actuator with limited dynamics can be indicated as upper-saturated even if it as not reached its maximum value. - -uint16 handled_motor_failure_mask # Bitmask of failed motors that were removed from the allocation / effectiveness matrix. Not necessarily identical to the report from FailureDetector diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Cpuload.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Cpuload.msg deleted file mode 100644 index efc2c4df5..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Cpuload.msg +++ /dev/null @@ -1,3 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -float32 load # processor load from 0 to 1 -float32 ram_usage # RAM usage from 0 to 1 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DatamanRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DatamanRequest.msg deleted file mode 100644 index f819771a4..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DatamanRequest.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 client_id -uint8 request_type # id/read/write/clear -uint8 item # dm_item_t -uint32 index -uint8[56] data -uint32 data_length \ No newline at end of file diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DatamanResponse.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DatamanResponse.msg deleted file mode 100644 index ebf752db5..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DatamanResponse.msg +++ /dev/null @@ -1,15 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 client_id -uint8 request_type # id/read/write/clear -uint8 item # dm_item_t -uint32 index -uint8[56] data - -uint8 STATUS_SUCCESS = 0 -uint8 STATUS_FAILURE_ID_ERR = 1 -uint8 STATUS_FAILURE_NO_DATA = 2 -uint8 STATUS_FAILURE_READ_FAILED = 3 -uint8 STATUS_FAILURE_WRITE_FAILED = 4 -uint8 STATUS_FAILURE_CLEAR_FAILED = 5 -uint8 status diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugArray.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugArray.msg deleted file mode 100644 index 4763a0f5a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugArray.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint8 ARRAY_SIZE = 58 -uint64 timestamp # time since system start (microseconds) -uint16 id # unique ID of debug array, used to discriminate between arrays -char[10] name # name of the debug array (max. 10 characters) -float32[58] data # data \ No newline at end of file diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugKeyValue.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugKeyValue.msg deleted file mode 100644 index 6815811ef..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugKeyValue.msg +++ /dev/null @@ -1,3 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -char[10] key # max. 10 characters as key / name -float32 value # the value to send as debug output diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugValue.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugValue.msg deleted file mode 100644 index 8be131247..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugValue.msg +++ /dev/null @@ -1,3 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -int8 ind # index of debug variable -float32 value # the value to send as debug output diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugVect.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugVect.msg deleted file mode 100644 index 9c22e1dac..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DebugVect.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -char[10] name # max. 10 characters as key / name -float32 x # x value -float32 y # y value -float32 z # z value diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DifferentialDriveSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DifferentialDriveSetpoint.msg deleted file mode 100644 index f7e4c5840..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DifferentialDriveSetpoint.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 speed # [m/s] collective roll-off speed in body x-axis -bool closed_loop_speed_control # true if speed is controlled using estimator feedback, false if direct feed-forward -float32 yaw_rate # [rad/s] yaw rate -bool closed_loop_yaw_rate_control # true if yaw rate is controlled using gyroscope feedback, false if direct feed-forward - -# TOPICS differential_drive_setpoint differential_drive_control_output diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DifferentialPressure.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DifferentialPressure.msg deleted file mode 100644 index 0cdf1e4cc..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DifferentialPressure.msg +++ /dev/null @@ -1,10 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 differential_pressure_pa # differential pressure reading in Pascals (may be negative) - -float32 temperature # Temperature provided by sensor in degrees Celsius, NAN if unknown - -uint32 error_count # Number of errors detected by driver diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/DistanceSensor.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/DistanceSensor.msg deleted file mode 100644 index dd08e4b58..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/DistanceSensor.msg +++ /dev/null @@ -1,42 +0,0 @@ -# DISTANCE_SENSOR message data - -uint64 timestamp # time since system start (microseconds) - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 min_distance # Minimum distance the sensor can measure (in m) -float32 max_distance # Maximum distance the sensor can measure (in m) -float32 current_distance # Current distance reading (in m) -float32 variance # Measurement variance (in m^2), 0 for unknown / invalid readings -int8 signal_quality # Signal quality in percent (0...100%), where 0 = invalid signal, 100 = perfect signal, and -1 = unknown signal quality. - -uint8 type # Type from MAV_DISTANCE_SENSOR enum -uint8 MAV_DISTANCE_SENSOR_LASER = 0 -uint8 MAV_DISTANCE_SENSOR_ULTRASOUND = 1 -uint8 MAV_DISTANCE_SENSOR_INFRARED = 2 -uint8 MAV_DISTANCE_SENSOR_RADAR = 3 - -float32 h_fov # Sensor horizontal field of view (rad) -float32 v_fov # Sensor vertical field of view (rad) -float32[4] q # Quaterion sensor orientation with respect to the vehicle body frame to specify the orientation ROTATION_CUSTOM - -uint8 orientation # Direction the sensor faces from MAV_SENSOR_ORIENTATION enum - -uint8 ROTATION_YAW_0 = 0 # MAV_SENSOR_ROTATION_NONE -uint8 ROTATION_YAW_45 = 1 # MAV_SENSOR_ROTATION_YAW_45 -uint8 ROTATION_YAW_90 = 2 # MAV_SENSOR_ROTATION_YAW_90 -uint8 ROTATION_YAW_135 = 3 # MAV_SENSOR_ROTATION_YAW_135 -uint8 ROTATION_YAW_180 = 4 # MAV_SENSOR_ROTATION_YAW_180 -uint8 ROTATION_YAW_225 = 5 # MAV_SENSOR_ROTATION_YAW_225 -uint8 ROTATION_YAW_270 = 6 # MAV_SENSOR_ROTATION_YAW_270 -uint8 ROTATION_YAW_315 = 7 # MAV_SENSOR_ROTATION_YAW_315 - -uint8 ROTATION_FORWARD_FACING = 0 # MAV_SENSOR_ROTATION_NONE -uint8 ROTATION_RIGHT_FACING = 2 # MAV_SENSOR_ROTATION_YAW_90 -uint8 ROTATION_BACKWARD_FACING = 4 # MAV_SENSOR_ROTATION_YAW_180 -uint8 ROTATION_LEFT_FACING = 6 # MAV_SENSOR_ROTATION_YAW_270 - -uint8 ROTATION_UPWARD_FACING = 24 # MAV_SENSOR_ROTATION_PITCH_90 -uint8 ROTATION_DOWNWARD_FACING = 25 # MAV_SENSOR_ROTATION_PITCH_270 - -uint8 ROTATION_CUSTOM = 100 # MAV_SENSOR_ROTATION_CUSTOM diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Ekf2Timestamps.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Ekf2Timestamps.msg deleted file mode 100644 index ae3ac0676..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Ekf2Timestamps.msg +++ /dev/null @@ -1,23 +0,0 @@ -# this message contains the (relative) timestamps of the sensor inputs used by EKF2. -# It can be used for reproducible replay. - -# the timestamp field is the ekf2 reference time and matches the timestamp of -# the sensor_combined topic. - -uint64 timestamp # time since system start (microseconds) - -int16 RELATIVE_TIMESTAMP_INVALID = 32767 # (0x7fff) If one of the relative timestamps - # is set to this value, it means the associated sensor values did not update - -# timestamps are relative to the main timestamp and are in 0.1 ms (timestamp + -# *_timestamp_rel = absolute timestamp). For int16, this allows a maximum -# difference of +-3.2s to the sensor_combined topic. - -int16 airspeed_timestamp_rel -int16 distance_sensor_timestamp_rel -int16 optical_flow_timestamp_rel -int16 vehicle_air_data_timestamp_rel -int16 vehicle_magnetometer_timestamp_rel -int16 visual_odometry_timestamp_rel - -# Note: this is a high-rate logged topic, so it needs to be as small as possible diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EscReport.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EscReport.msg deleted file mode 100644 index 9a75c3d7c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EscReport.msg +++ /dev/null @@ -1,27 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint32 esc_errorcount # Number of reported errors by ESC - if supported -int32 esc_rpm # Motor RPM, negative for reverse rotation [RPM] - if supported -float32 esc_voltage # Voltage measured from current ESC [V] - if supported -float32 esc_current # Current measured from current ESC [A] - if supported -float32 esc_temperature # Temperature measured from current ESC [degC] - if supported -uint8 esc_address # Address of current ESC (in most cases 1-8 / must be set by driver) -uint8 esc_cmdcount # Counter of number of commands - -uint8 esc_state # State of ESC - depend on Vendor - -uint8 actuator_function # actuator output function (one of Motor1...MotorN) - -uint16 failures # Bitmask to indicate the internal ESC faults -int8 esc_power # Applied power 0-100 in % (negative values reserved) - -uint8 FAILURE_OVER_CURRENT = 0 # (1 << 0) -uint8 FAILURE_OVER_VOLTAGE = 1 # (1 << 1) -uint8 FAILURE_MOTOR_OVER_TEMPERATURE = 2 # (1 << 2) -uint8 FAILURE_OVER_RPM = 3 # (1 << 3) -uint8 FAILURE_INCONSISTENT_CMD = 4 # (1 << 4) Set if ESC received an inconsistent command (i.e out of boundaries) -uint8 FAILURE_MOTOR_STUCK = 5 # (1 << 5) -uint8 FAILURE_GENERIC = 6 # (1 << 6) -uint8 FAILURE_MOTOR_WARN_TEMPERATURE = 7 # (1 << 7) -uint8 FAILURE_WARN_ESC_TEMPERATURE = 8 # (1 << 8) -uint8 FAILURE_OVER_ESC_TEMPERATURE = 9 # (1 << 9) -uint8 ESC_FAILURE_COUNT = 10 # Counter - keep it as last element! diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EscStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EscStatus.msg deleted file mode 100644 index e5e220ce0..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EscStatus.msg +++ /dev/null @@ -1,28 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 CONNECTED_ESC_MAX = 8 # The number of ESCs supported. Current (Q2/2013) we support 8 ESCs - -uint8 ESC_CONNECTION_TYPE_PPM = 0 # Traditional PPM ESC -uint8 ESC_CONNECTION_TYPE_SERIAL = 1 # Serial Bus connected ESC -uint8 ESC_CONNECTION_TYPE_ONESHOT = 2 # One Shot PPM -uint8 ESC_CONNECTION_TYPE_I2C = 3 # I2C -uint8 ESC_CONNECTION_TYPE_CAN = 4 # CAN-Bus -uint8 ESC_CONNECTION_TYPE_DSHOT = 5 # DShot - -uint16 counter # incremented by the writing thread everytime new data is stored - -uint8 esc_count # number of connected ESCs -uint8 esc_connectiontype # how ESCs connected to the system - -uint8 esc_online_flags # Bitmask indicating which ESC is online/offline -# esc_online_flags bit 0 : Set to 1 if ESC0 is online -# esc_online_flags bit 1 : Set to 1 if ESC1 is online -# esc_online_flags bit 2 : Set to 1 if ESC2 is online -# esc_online_flags bit 3 : Set to 1 if ESC3 is online -# esc_online_flags bit 4 : Set to 1 if ESC4 is online -# esc_online_flags bit 5 : Set to 1 if ESC5 is online -# esc_online_flags bit 6 : Set to 1 if ESC6 is online -# esc_online_flags bit 7 : Set to 1 if ESC7 is online - -uint8 esc_armed_flags # Bitmask indicating which ESC is armed. For ESC's where the arming state is not known (returned by the ESC), the arming bits should always be set. - -EscReport[8] esc diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource1d.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource1d.msg deleted file mode 100644 index 7bd8ea765..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource1d.msg +++ /dev/null @@ -1,27 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -uint8 estimator_instance - -uint32 device_id - -uint64 time_last_fuse - -float32 observation -float32 observation_variance - -float32 innovation -float32 innovation_filtered - -float32 innovation_variance - -float32 test_ratio # normalized innovation squared -float32 test_ratio_filtered # signed filtered test ratio - -bool innovation_rejected # true if the observation has been rejected -bool fused # true if the sample was successfully fused - -# TOPICS estimator_aid_src_baro_hgt estimator_aid_src_ev_hgt estimator_aid_src_gnss_hgt estimator_aid_src_rng_hgt -# TOPICS estimator_aid_src_airspeed estimator_aid_src_sideslip -# TOPICS estimator_aid_src_fake_hgt -# TOPICS estimator_aid_src_gnss_yaw estimator_aid_src_ev_yaw diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource2d.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource2d.msg deleted file mode 100644 index 14e3ac3f8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource2d.msg +++ /dev/null @@ -1,26 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -uint8 estimator_instance - -uint32 device_id - -uint64 time_last_fuse - -float32[2] observation -float32[2] observation_variance - -float32[2] innovation -float32[2] innovation_filtered - -float32[2] innovation_variance - -float32[2] test_ratio # normalized innovation squared -float32[2] test_ratio_filtered # signed filtered test ratio - -bool innovation_rejected # true if the observation has been rejected -bool fused # true if the sample was successfully fused - -# TOPICS estimator_aid_src_ev_pos estimator_aid_src_fake_pos estimator_aid_src_gnss_pos estimator_aid_src_aux_global_position -# TOPICS estimator_aid_src_aux_vel estimator_aid_src_optical_flow -# TOPICS estimator_aid_src_drag diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource3d.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource3d.msg deleted file mode 100644 index b89add28e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorAidSource3d.msg +++ /dev/null @@ -1,24 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -uint8 estimator_instance - -uint32 device_id - -uint64 time_last_fuse - -float32[3] observation -float32[3] observation_variance - -float32[3] innovation -float32[3] innovation_filtered - -float32[3] innovation_variance - -float32[3] test_ratio # normalized innovation squared -float32[3] test_ratio_filtered # signed filtered test ratio - -bool innovation_rejected # true if the observation has been rejected -bool fused # true if the sample was successfully fused - -# TOPICS estimator_aid_src_ev_vel estimator_aid_src_gnss_vel estimator_aid_src_gravity estimator_aid_src_mag diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorBias.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorBias.msg deleted file mode 100644 index bb65e47bc..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorBias.msg +++ /dev/null @@ -1,12 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -uint32 device_id # unique device ID for the sensor that does not change between power cycles -float32 bias # estimated barometric altitude bias (m) -float32 bias_var # estimated barometric altitude bias variance (m^2) - -float32 innov # innovation of the last measurement fusion (m) -float32 innov_var # innovation variance of the last measurement fusion (m^2) -float32 innov_test_ratio # normalized innovation squared test ratio - -# TOPICS estimator_baro_bias estimator_gnss_hgt_bias diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorBias3d.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorBias3d.msg deleted file mode 100644 index 16b293729..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorBias3d.msg +++ /dev/null @@ -1,14 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32[3] bias # estimated barometric altitude bias (m) -float32[3] bias_var # estimated barometric altitude bias variance (m^2) - -float32[3] innov # innovation of the last measurement fusion (m) -float32[3] innov_var # innovation variance of the last measurement fusion (m^2) -float32[3] innov_test_ratio # normalized innovation squared test ratio - -# TOPICS estimator_bias3d -# TOPICS estimator_ev_pos_bias diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorEventFlags.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorEventFlags.msg deleted file mode 100644 index 1a47e676a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorEventFlags.msg +++ /dev/null @@ -1,37 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -# information events -uint32 information_event_changes # number of information event changes -bool gps_checks_passed # 0 - true when gps quality checks are passing passed -bool reset_vel_to_gps # 1 - true when the velocity states are reset to the gps measurement -bool reset_vel_to_flow # 2 - true when the velocity states are reset using the optical flow measurement -bool reset_vel_to_vision # 3 - true when the velocity states are reset to the vision system measurement -bool reset_vel_to_zero # 4 - true when the velocity states are reset to zero -bool reset_pos_to_last_known # 5 - true when the position states are reset to the last known position -bool reset_pos_to_gps # 6 - true when the position states are reset to the gps measurement -bool reset_pos_to_vision # 7 - true when the position states are reset to the vision system measurement -bool starting_gps_fusion # 8 - true when the filter starts using gps measurements to correct the state estimates -bool starting_vision_pos_fusion # 9 - true when the filter starts using vision system position measurements to correct the state estimates -bool starting_vision_vel_fusion # 10 - true when the filter starts using vision system velocity measurements to correct the state estimates -bool starting_vision_yaw_fusion # 11 - true when the filter starts using vision system yaw measurements to correct the state estimates -bool yaw_aligned_to_imu_gps # 12 - true when the filter resets the yaw to an estimate derived from IMU and GPS data -bool reset_hgt_to_baro # 13 - true when the vertical position state is reset to the baro measurement -bool reset_hgt_to_gps # 14 - true when the vertical position state is reset to the gps measurement -bool reset_hgt_to_rng # 15 - true when the vertical position state is reset to the rng measurement -bool reset_hgt_to_ev # 16 - true when the vertical position state is reset to the ev measurement - -# warning events -uint32 warning_event_changes # number of warning event changes -bool gps_quality_poor # 0 - true when the gps is failing quality checks -bool gps_fusion_timout # 1 - true when the gps data has not been used to correct the state estimates for a significant time period -bool gps_data_stopped # 2 - true when the gps data has stopped for a significant time period -bool gps_data_stopped_using_alternate # 3 - true when the gps data has stopped for a significant time period but the filter is able to use other sources of data to maintain navigation -bool height_sensor_timeout # 4 - true when the height sensor has not been used to correct the state estimates for a significant time period -bool stopping_navigation # 5 - true when the filter has insufficient data to estimate velocity and position and is falling back to an attitude, height and height rate mode of operation -bool invalid_accel_bias_cov_reset # 6 - true when the filter has detected bad acceerometer bias state esitmstes and has reset the corresponding covariance matrix elements -bool bad_yaw_using_gps_course # 7 - true when the filter has detected an invalid yaw estimate and has reset the yaw angle to the GPS ground course -bool stopping_mag_use # 8 - true when the filter has detected bad magnetometer data and is stopping further use of the magnetometer data -bool vision_data_stopped # 9 - true when the vision system data has stopped for a significant time period -bool emergency_yaw_reset_mag_stopped # 10 - true when the filter has detected bad magnetometer data, has reset the yaw to anothter source of data and has stopped further use of the magnetometer data -bool emergency_yaw_reset_gps_yaw_stopped # 11 - true when the filter has detected bad GNSS yaw data, has reset the yaw to anothter source of data and has stopped further use of the GNSS yaw data diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorGpsStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorGpsStatus.msg deleted file mode 100644 index 2d2462ee5..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorGpsStatus.msg +++ /dev/null @@ -1,19 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -bool checks_passed - -bool check_fail_gps_fix # 0 : insufficient fix type (no 3D solution) -bool check_fail_min_sat_count # 1 : minimum required sat count fail -bool check_fail_max_pdop # 2 : maximum allowed PDOP fail -bool check_fail_max_horz_err # 3 : maximum allowed horizontal position error fail -bool check_fail_max_vert_err # 4 : maximum allowed vertical position error fail -bool check_fail_max_spd_err # 5 : maximum allowed speed error fail -bool check_fail_max_horz_drift # 6 : maximum allowed horizontal position drift fail - requires stationary vehicle -bool check_fail_max_vert_drift # 7 : maximum allowed vertical position drift fail - requires stationary vehicle -bool check_fail_max_horz_spd_err # 8 : maximum allowed horizontal speed fail - requires stationary vehicle -bool check_fail_max_vert_spd_err # 9 : maximum allowed vertical velocity discrepancy fail - -float32 position_drift_rate_horizontal_m_s # Horizontal position rate magnitude (m/s) -float32 position_drift_rate_vertical_m_s # Vertical position rate magnitude (m/s) -float32 filtered_horizontal_speed_m_s # Filtered horizontal velocity magnitude (m/s) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorInnovations.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorInnovations.msg deleted file mode 100644 index 11cc6a58a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorInnovations.msg +++ /dev/null @@ -1,39 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -# GPS -float32[2] gps_hvel # horizontal GPS velocity innovation (m/sec) and innovation variance ((m/sec)**2) -float32 gps_vvel # vertical GPS velocity innovation (m/sec) and innovation variance ((m/sec)**2) -float32[2] gps_hpos # horizontal GPS position innovation (m) and innovation variance (m**2) -float32 gps_vpos # vertical GPS position innovation (m) and innovation variance (m**2) - -# External Vision -float32[2] ev_hvel # horizontal external vision velocity innovation (m/sec) and innovation variance ((m/sec)**2) -float32 ev_vvel # vertical external vision velocity innovation (m/sec) and innovation variance ((m/sec)**2) -float32[2] ev_hpos # horizontal external vision position innovation (m) and innovation variance (m**2) -float32 ev_vpos # vertical external vision position innovation (m) and innovation variance (m**2) - -# Height sensors -float32 rng_vpos # range sensor height innovation (m) and innovation variance (m**2) -float32 baro_vpos # barometer height innovation (m) and innovation variance (m**2) - -# Auxiliary velocity -float32[2] aux_hvel # horizontal auxiliary velocity innovation from landing target measurement (m/sec) and innovation variance ((m/sec)**2) - -# Optical flow -float32[2] flow # flow innvoation (rad/sec) and innovation variance ((rad/sec)**2) - -# Various -float32 heading # heading innovation (rad) and innovation variance (rad**2) -float32[3] mag_field # earth magnetic field innovation (Gauss) and innovation variance (Gauss**2) -float32[3] gravity # gravity innovation from accelerometerr vector (m/s**2) -float32[2] drag # drag specific force innovation (m/sec**2) and innovation variance ((m/sec)**2) -float32 airspeed # airspeed innovation (m/sec) and innovation variance ((m/sec)**2) -float32 beta # synthetic sideslip innovation (rad) and innovation variance (rad**2) -float32 hagl # height of ground innovation (m) and innovation variance (m**2) -float32 hagl_rate # height of ground rate innovation (m/s) and innovation variance ((m/s)**2) - -# The innovation test ratios are scalar values. In case the field is a vector, -# the test ratio will be put in the first component of the vector. - -# TOPICS estimator_innovations estimator_innovation_variances estimator_innovation_test_ratios diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorSelectorStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorSelectorStatus.msg deleted file mode 100644 index 52f808590..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorSelectorStatus.msg +++ /dev/null @@ -1,22 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 primary_instance - -uint8 instances_available - -uint32 instance_changed_count -uint64 last_instance_change - -uint32 accel_device_id -uint32 baro_device_id -uint32 gyro_device_id -uint32 mag_device_id - -float32[9] combined_test_ratio -float32[9] relative_test_ratio -bool[9] healthy - -float32[4] accumulated_gyro_error -float32[4] accumulated_accel_error -bool gyro_fault_detected -bool accel_fault_detected diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorSensorBias.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorSensorBias.msg deleted file mode 100644 index f42e1aa87..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorSensorBias.msg +++ /dev/null @@ -1,30 +0,0 @@ -# -# Sensor readings and in-run biases in SI-unit form. Sensor readings are compensated for static offsets, -# scale errors, in-run bias and thermal drift (if thermal compensation is enabled and available). -# - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -# In-run bias estimates (subtract from uncorrected data) - -uint32 gyro_device_id # unique device ID for the sensor that does not change between power cycles -float32[3] gyro_bias # gyroscope in-run bias in body frame (rad/s) -float32 gyro_bias_limit # magnitude of maximum gyroscope in-run bias in body frame (rad/s) -float32[3] gyro_bias_variance -bool gyro_bias_valid -bool gyro_bias_stable # true when the gyro bias estimate is stable enough to use for calibration - -uint32 accel_device_id # unique device ID for the sensor that does not change between power cycles -float32[3] accel_bias # accelerometer in-run bias in body frame (m/s^2) -float32 accel_bias_limit # magnitude of maximum accelerometer in-run bias in body frame (m/s^2) -float32[3] accel_bias_variance -bool accel_bias_valid -bool accel_bias_stable # true when the accel bias estimate is stable enough to use for calibration - -uint32 mag_device_id # unique device ID for the sensor that does not change between power cycles -float32[3] mag_bias # magnetometer in-run bias in body frame (Gauss) -float32 mag_bias_limit # magnitude of maximum magnetometer in-run bias in body frame (Gauss) -float32[3] mag_bias_variance -bool mag_bias_valid -bool mag_bias_stable # true when the mag bias estimate is stable enough to use for calibration diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStates.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStates.msg deleted file mode 100644 index 885246d8a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStates.msg +++ /dev/null @@ -1,7 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float32[25] states # Internal filter states -uint8 n_states # Number of states effectively used - -float32[24] covariances # Diagonal Elements of Covariance Matrix diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStatus.msg deleted file mode 100644 index ac13b59ea..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStatus.msg +++ /dev/null @@ -1,121 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float32[3] output_tracking_error # return a vector containing the output predictor angular, velocity and position tracking error magnitudes (rad), (m/s), (m) - -uint16 gps_check_fail_flags # Bitmask to indicate status of GPS checks - see definition below -# bits are true when corresponding test has failed -uint8 GPS_CHECK_FAIL_GPS_FIX = 0 # 0 : insufficient fix type (no 3D solution) -uint8 GPS_CHECK_FAIL_MIN_SAT_COUNT = 1 # 1 : minimum required sat count fail -uint8 GPS_CHECK_FAIL_MAX_PDOP = 2 # 2 : maximum allowed PDOP fail -uint8 GPS_CHECK_FAIL_MAX_HORZ_ERR = 3 # 3 : maximum allowed horizontal position error fail -uint8 GPS_CHECK_FAIL_MAX_VERT_ERR = 4 # 4 : maximum allowed vertical position error fail -uint8 GPS_CHECK_FAIL_MAX_SPD_ERR = 5 # 5 : maximum allowed speed error fail -uint8 GPS_CHECK_FAIL_MAX_HORZ_DRIFT = 6 # 6 : maximum allowed horizontal position drift fail - requires stationary vehicle -uint8 GPS_CHECK_FAIL_MAX_VERT_DRIFT = 7 # 7 : maximum allowed vertical position drift fail - requires stationary vehicle -uint8 GPS_CHECK_FAIL_MAX_HORZ_SPD_ERR = 8 # 8 : maximum allowed horizontal speed fail - requires stationary vehicle -uint8 GPS_CHECK_FAIL_MAX_VERT_SPD_ERR = 9 # 9 : maximum allowed vertical velocity discrepancy fail - -uint64 control_mode_flags # Bitmask to indicate EKF logic state -uint8 CS_TILT_ALIGN = 0 # 0 - true if the filter tilt alignment is complete -uint8 CS_YAW_ALIGN = 1 # 1 - true if the filter yaw alignment is complete -uint8 CS_GPS = 2 # 2 - true if GPS measurements are being fused -uint8 CS_OPT_FLOW = 3 # 3 - true if optical flow measurements are being fused -uint8 CS_MAG_HDG = 4 # 4 - true if a simple magnetic yaw heading is being fused -uint8 CS_MAG_3D = 5 # 5 - true if 3-axis magnetometer measurement are being fused -uint8 CS_MAG_DEC = 6 # 6 - true if synthetic magnetic declination measurements are being fused -uint8 CS_IN_AIR = 7 # 7 - true when thought to be airborne -uint8 CS_WIND = 8 # 8 - true when wind velocity is being estimated -uint8 CS_BARO_HGT = 9 # 9 - true when baro height is being fused as a primary height reference -uint8 CS_RNG_HGT = 10 # 10 - true when range finder height is being fused as a primary height reference -uint8 CS_GPS_HGT = 11 # 11 - true when GPS height is being fused as a primary height reference -uint8 CS_EV_POS = 12 # 12 - true when local position data from external vision is being fused -uint8 CS_EV_YAW = 13 # 13 - true when yaw data from external vision measurements is being fused -uint8 CS_EV_HGT = 14 # 14 - true when height data from external vision measurements is being fused -uint8 CS_BETA = 15 # 15 - true when synthetic sideslip measurements are being fused -uint8 CS_MAG_FIELD = 16 # 16 - true when only the magnetic field states are updated by the magnetometer -uint8 CS_FIXED_WING = 17 # 17 - true when thought to be operating as a fixed wing vehicle with constrained sideslip -uint8 CS_MAG_FAULT = 18 # 18 - true when the magnetometer has been declared faulty and is no longer being used -uint8 CS_ASPD = 19 # 19 - true when airspeed measurements are being fused -uint8 CS_GND_EFFECT = 20 # 20 - true when when protection from ground effect induced static pressure rise is active -uint8 CS_RNG_STUCK = 21 # 21 - true when a stuck range finder sensor has been detected -uint8 CS_GPS_YAW = 22 # 22 - true when yaw (not ground course) data from a GPS receiver is being fused -uint8 CS_MAG_ALIGNED = 23 # 23 - true when the in-flight mag field alignment has been completed -uint8 CS_EV_VEL = 24 # 24 - true when local frame velocity data fusion from external vision measurements is intended -uint8 CS_SYNTHETIC_MAG_Z = 25 # 25 - true when we are using a synthesized measurement for the magnetometer Z component -uint8 CS_VEHICLE_AT_REST = 26 # 26 - true when the vehicle is at rest -uint8 CS_GPS_YAW_FAULT = 27 # 27 - true when the GNSS heading has been declared faulty and is no longer being used -uint8 CS_RNG_FAULT = 28 # 28 - true when the range finder has been declared faulty and is no longer being used - -uint32 filter_fault_flags # Bitmask to indicate EKF internal faults -# 0 - true if the fusion of the magnetometer X-axis has encountered a numerical error -# 1 - true if the fusion of the magnetometer Y-axis has encountered a numerical error -# 2 - true if the fusion of the magnetometer Z-axis has encountered a numerical error -# 3 - true if the fusion of the magnetic heading has encountered a numerical error -# 4 - true if the fusion of the magnetic declination has encountered a numerical error -# 5 - true if fusion of the airspeed has encountered a numerical error -# 6 - true if fusion of the synthetic sideslip constraint has encountered a numerical error -# 7 - true if fusion of the optical flow X axis has encountered a numerical error -# 8 - true if fusion of the optical flow Y axis has encountered a numerical error -# 9 - true if fusion of the North velocity has encountered a numerical error -# 10 - true if fusion of the East velocity has encountered a numerical error -# 11 - true if fusion of the Down velocity has encountered a numerical error -# 12 - true if fusion of the North position has encountered a numerical error -# 13 - true if fusion of the East position has encountered a numerical error -# 14 - true if fusion of the Down position has encountered a numerical error -# 15 - true if bad delta velocity bias estimates have been detected -# 16 - true if bad vertical accelerometer data has been detected -# 17 - true if delta velocity data contains clipping (asymmetric railing) - -float32 pos_horiz_accuracy # 1-Sigma estimated horizontal position accuracy relative to the estimators origin (m) -float32 pos_vert_accuracy # 1-Sigma estimated vertical position accuracy relative to the estimators origin (m) - -float32 mag_test_ratio # low-pass filtered ratio of the largest magnetometer innovation component to the innovation test limit -float32 vel_test_ratio # low-pass filtered ratio of the largest velocity innovation component to the innovation test limit -float32 pos_test_ratio # low-pass filtered ratio of the largest horizontal position innovation component to the innovation test limit -float32 hgt_test_ratio # low-pass filtered ratio of the vertical position innovation to the innovation test limit -float32 tas_test_ratio # low-pass filtered ratio of the true airspeed innovation to the innovation test limit -float32 hagl_test_ratio # low-pass filtered ratio of the height above ground innovation to the innovation test limit -float32 beta_test_ratio # low-pass filtered ratio of the synthetic sideslip innovation to the innovation test limit - -uint16 solution_status_flags # Bitmask indicating which filter kinematic state outputs are valid for flight control use. -# 0 - True if the attitude estimate is good -# 1 - True if the horizontal velocity estimate is good -# 2 - True if the vertical velocity estimate is good -# 3 - True if the horizontal position (relative) estimate is good -# 4 - True if the horizontal position (absolute) estimate is good -# 5 - True if the vertical position (absolute) estimate is good -# 6 - True if the vertical position (above ground) estimate is good -# 7 - True if the EKF is in a constant position mode and is not using external measurements (eg GPS or optical flow) -# 8 - True if the EKF has sufficient data to enter a mode that will provide a (relative) position estimate -# 9 - True if the EKF has sufficient data to enter a mode that will provide a (absolute) position estimate -# 10 - True if the EKF has detected a GPS glitch -# 11 - True if the EKF has detected bad accelerometer data - -uint8 reset_count_vel_ne # number of horizontal position reset events (allow to wrap if count exceeds 255) -uint8 reset_count_vel_d # number of vertical velocity reset events (allow to wrap if count exceeds 255) -uint8 reset_count_pos_ne # number of horizontal position reset events (allow to wrap if count exceeds 255) -uint8 reset_count_pod_d # number of vertical position reset events (allow to wrap if count exceeds 255) -uint8 reset_count_quat # number of quaternion reset events (allow to wrap if count exceeds 255) - -float32 time_slip # cumulative amount of time in seconds that the EKF inertial calculation has slipped relative to system time - -bool pre_flt_fail_innov_heading -bool pre_flt_fail_innov_vel_horiz -bool pre_flt_fail_innov_vel_vert -bool pre_flt_fail_innov_height -bool pre_flt_fail_mag_field_disturbed - -uint32 accel_device_id -uint32 gyro_device_id -uint32 baro_device_id -uint32 mag_device_id - -# legacy local position estimator (LPE) flags -uint8 health_flags # Bitmask to indicate sensor health states (vel, pos, hgt) -uint8 timeout_flags # Bitmask to indicate timeout flags (vel, pos, hgt) - -float32 mag_inclination_deg -float32 mag_inclination_ref_deg -float32 mag_strength_gs -float32 mag_strength_ref_gs diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStatusFlags.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStatusFlags.msg deleted file mode 100644 index c6e0504f1..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/EstimatorStatusFlags.msg +++ /dev/null @@ -1,74 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - - -# filter control status -uint32 control_status_changes # number of filter control status (cs) changes -bool cs_tilt_align # 0 - true if the filter tilt alignment is complete -bool cs_yaw_align # 1 - true if the filter yaw alignment is complete -bool cs_gps # 2 - true if GPS measurement fusion is intended -bool cs_opt_flow # 3 - true if optical flow measurements fusion is intended -bool cs_mag_hdg # 4 - true if a simple magnetic yaw heading fusion is intended -bool cs_mag_3d # 5 - true if 3-axis magnetometer measurement fusion is intended -bool cs_mag_dec # 6 - true if synthetic magnetic declination measurements fusion is intended -bool cs_in_air # 7 - true when the vehicle is airborne -bool cs_wind # 8 - true when wind velocity is being estimated -bool cs_baro_hgt # 9 - true when baro height is being fused as a primary height reference -bool cs_rng_hgt # 10 - true when range finder height is being fused as a primary height reference -bool cs_gps_hgt # 11 - true when GPS height is being fused as a primary height reference -bool cs_ev_pos # 12 - true when local position data fusion from external vision is intended -bool cs_ev_yaw # 13 - true when yaw data from external vision measurements fusion is intended -bool cs_ev_hgt # 14 - true when height data from external vision measurements is being fused -bool cs_fuse_beta # 15 - true when synthetic sideslip measurements are being fused -bool cs_mag_field_disturbed # 16 - true when the mag field does not match the expected strength -bool cs_fixed_wing # 17 - true when the vehicle is operating as a fixed wing vehicle -bool cs_mag_fault # 18 - true when the magnetometer has been declared faulty and is no longer being used -bool cs_fuse_aspd # 19 - true when airspeed measurements are being fused -bool cs_gnd_effect # 20 - true when protection from ground effect induced static pressure rise is active -bool cs_rng_stuck # 21 - true when rng data wasn't ready for more than 10s and new rng values haven't changed enough -bool cs_gps_yaw # 22 - true when yaw (not ground course) data fusion from a GPS receiver is intended -bool cs_mag_aligned_in_flight # 23 - true when the in-flight mag field alignment has been completed -bool cs_ev_vel # 24 - true when local frame velocity data fusion from external vision measurements is intended -bool cs_synthetic_mag_z # 25 - true when we are using a synthesized measurement for the magnetometer Z component -bool cs_vehicle_at_rest # 26 - true when the vehicle is at rest -bool cs_gps_yaw_fault # 27 - true when the GNSS heading has been declared faulty and is no longer being used -bool cs_rng_fault # 28 - true when the range finder has been declared faulty and is no longer being used -bool cs_inertial_dead_reckoning # 29 - true if we are no longer fusing measurements that constrain horizontal velocity drift -bool cs_wind_dead_reckoning # 30 - true if we are navigationg reliant on wind relative measurements -bool cs_rng_kin_consistent # 31 - true when the range finder kinematic consistency check is passing -bool cs_fake_pos # 32 - true when fake position measurements are being fused -bool cs_fake_hgt # 33 - true when fake height measurements are being fused -bool cs_gravity_vector # 34 - true when gravity vector measurements are being fused -bool cs_mag # 35 - true if 3-axis magnetometer measurement fusion (mag states only) is intended -bool cs_ev_yaw_fault # 36 - true when the EV heading has been declared faulty and is no longer being used -bool cs_mag_heading_consistent # 37 - true when the heading obtained from mag data is declared consistent with the filter -bool cs_aux_gpos # 38 - true if auxiliary global position measurement fusion is intended - -# fault status -uint32 fault_status_changes # number of filter fault status (fs) changes -bool fs_bad_mag_x # 0 - true if the fusion of the magnetometer X-axis has encountered a numerical error -bool fs_bad_mag_y # 1 - true if the fusion of the magnetometer Y-axis has encountered a numerical error -bool fs_bad_mag_z # 2 - true if the fusion of the magnetometer Z-axis has encountered a numerical error -bool fs_bad_hdg # 3 - true if the fusion of the heading angle has encountered a numerical error -bool fs_bad_mag_decl # 4 - true if the fusion of the magnetic declination has encountered a numerical error -bool fs_bad_airspeed # 5 - true if fusion of the airspeed has encountered a numerical error -bool fs_bad_sideslip # 6 - true if fusion of the synthetic sideslip constraint has encountered a numerical error -bool fs_bad_optflow_x # 7 - true if fusion of the optical flow X axis has encountered a numerical error -bool fs_bad_optflow_y # 8 - true if fusion of the optical flow Y axis has encountered a numerical error -bool fs_bad_acc_bias # 9 - true if bad delta velocity bias estimates have been detected -bool fs_bad_acc_vertical # 10 - true if bad vertical accelerometer data has been detected -bool fs_bad_acc_clipping # 11 - true if delta velocity data contains clipping (asymmetric railing) - - -# innovation test failures -uint32 innovation_fault_status_changes # number of innovation fault status (reject) changes -bool reject_hor_vel # 0 - true if horizontal velocity observations have been rejected -bool reject_ver_vel # 1 - true if vertical velocity observations have been rejected -bool reject_hor_pos # 2 - true if horizontal position observations have been rejected -bool reject_ver_pos # 3 - true if vertical position observations have been rejected -bool reject_yaw # 7 - true if the yaw observation has been rejected -bool reject_airspeed # 8 - true if the airspeed observation has been rejected -bool reject_sideslip # 9 - true if the synthetic sideslip observation has been rejected -bool reject_hagl # 10 - true if the height above ground observation has been rejected -bool reject_optflow_x # 11 - true if the X optical flow observation has been rejected -bool reject_optflow_y # 12 - true if the Y optical flow observation has been rejected diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Event.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Event.msg deleted file mode 100644 index df1dd4a97..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Event.msg +++ /dev/null @@ -1,10 +0,0 @@ -# Events interface -uint64 timestamp # time since system start (microseconds) - -uint32 id # Event ID -uint16 event_sequence # Event sequence number -uint8[25] arguments # (optional) arguments, depend on event id - -uint8 log_levels # Log levels: 4 bits MSB: internal, 4 bits LSB: external - -uint8 ORB_QUEUE_LENGTH = 16 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/FailsafeFlags.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/FailsafeFlags.msg deleted file mode 100644 index 44945afae..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/FailsafeFlags.msg +++ /dev/null @@ -1,59 +0,0 @@ -# Input flags for the failsafe state machine set by the arming & health checks. -# -# Flags must be named such that false == no failure (e.g. _invalid, _unhealthy, _lost) -# The flag comments are used as label for the failsafe state machine simulation - -uint64 timestamp # time since system start (microseconds) - -# Per-mode requirements -uint32 mode_req_angular_velocity -uint32 mode_req_attitude -uint32 mode_req_local_alt -uint32 mode_req_local_position -uint32 mode_req_local_position_relaxed -uint32 mode_req_global_position -uint32 mode_req_mission -uint32 mode_req_offboard_signal -uint32 mode_req_home_position -uint32 mode_req_wind_and_flight_time_compliance # if set, mode cannot be entered if wind or flight time limit exceeded -uint32 mode_req_prevent_arming # if set, cannot arm while in this mode -uint32 mode_req_manual_control -uint32 mode_req_other # other requirements, not covered above (for external modes) - - -# Mode requirements -bool angular_velocity_invalid # Angular velocity invalid -bool attitude_invalid # Attitude invalid -bool local_altitude_invalid # Local altitude invalid -bool local_position_invalid # Local position estimate invalid -bool local_position_invalid_relaxed # Local position with reduced accuracy requirements invalid (e.g. flying with optical flow) -bool local_velocity_invalid # Local velocity estimate invalid -bool global_position_invalid # Global position estimate invalid -bool auto_mission_missing # No mission available -bool offboard_control_signal_lost # Offboard signal lost -bool home_position_invalid # No home position available - -# Control links -bool manual_control_signal_lost # Manual control (RC) signal lost -bool gcs_connection_lost # GCS connection lost - -# Battery -uint8 battery_warning # Battery warning level -bool battery_low_remaining_time # Low battery based on remaining flight time -bool battery_unhealthy # Battery unhealthy - -# Other -bool geofence_breached # Geofence breached (one or multiple) -bool mission_failure # Mission failure -bool vtol_fixed_wing_system_failure # vehicle in fixed-wing system failure failsafe mode (after quad-chute) -bool wind_limit_exceeded # Wind limit exceeded -bool flight_time_limit_exceeded # Maximum flight time exceeded -bool local_position_accuracy_low # Local position estimate has dropped below threshold, but is currently still declared valid - -# Failure detector -bool fd_critical_failure # Critical failure (attitude/altitude limit exceeded, or external ATS) -bool fd_esc_arming_failure # ESC failed to arm -bool fd_imbalanced_prop # Imbalanced propeller detected -bool fd_motor_failure # Motor failure - - diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/FailureDetectorStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/FailureDetectorStatus.msg deleted file mode 100644 index 923ceb36d..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/FailureDetectorStatus.msg +++ /dev/null @@ -1,14 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -# FailureDetector status -bool fd_roll -bool fd_pitch -bool fd_alt -bool fd_ext -bool fd_arm_escs -bool fd_battery -bool fd_imbalanced_prop -bool fd_motor - -float32 imbalanced_prop_metric # Metric of the imbalanced propeller check (low-passed) -uint16 motor_failure_mask # Bit-mask with motor indices, indicating critical motor failures diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/FigureEightStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/FigureEightStatus.msg deleted file mode 100644 index e14d8f0d8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/FigureEightStatus.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -float32 major_radius # Major axis radius of the figure eight [m]. Positive values orbit clockwise, negative values orbit counter-clockwise. -float32 minor_radius # Minor axis radius of the figure eight [m]. -float32 orientation # Orientation of the major axis of the figure eight [rad]. -uint8 frame # The coordinate system of the fields: x, y, z. -int32 x # X coordinate of center point. Coordinate system depends on frame field: local = x position in meters * 1e4, global = latitude in degrees * 1e7. -int32 y # Y coordinate of center point. Coordinate system depends on frame field: local = y position in meters * 1e4, global = latitude in degrees * 1e7. -float32 z # Altitude of center point. Coordinate system depends on frame field. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/FlightPhaseEstimation.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/FlightPhaseEstimation.msg deleted file mode 100644 index e05b32912..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/FlightPhaseEstimation.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 flight_phase # Estimate of current flight phase - -uint8 FLIGHT_PHASE_UNKNOWN = 0 # vehicle flight phase is unknown -uint8 FLIGHT_PHASE_LEVEL = 1 # Vehicle is in level flight -uint8 FLIGHT_PHASE_DESCEND = 2 # vehicle is in descend -uint8 FLIGHT_PHASE_CLIMB = 3 # vehicle is climbing diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTarget.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTarget.msg deleted file mode 100644 index e88c2460c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTarget.msg +++ /dev/null @@ -1,11 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float64 lat # target position (deg * 1e7) -float64 lon # target position (deg * 1e7) -float32 alt # target position - -float32 vy # target vel in y -float32 vx # target vel in x -float32 vz # target vel in z - -uint8 est_cap # target reporting capabilities diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTargetEstimator.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTargetEstimator.msg deleted file mode 100644 index 9d3df9f6f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTargetEstimator.msg +++ /dev/null @@ -1,16 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 last_filter_reset_timestamp # time of last filter reset (microseconds) - -bool valid # True if estimator states are okay to be used -bool stale # True if estimator stopped receiving follow_target messages for some time. The estimate can still be valid, though it might be inaccurate. - -float64 lat_est # Estimated target latitude -float64 lon_est # Estimated target longitude -float32 alt_est # Estimated target altitude - -float32[3] pos_est # Estimated target NED position (m) -float32[3] vel_est # Estimated target NED velocity (m/s) -float32[3] acc_est # Estimated target NED acceleration (m^2/s) - -uint64 prediction_count -uint64 fusion_count diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTargetStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTargetStatus.msg deleted file mode 100644 index 713a7dcdb..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/FollowTargetStatus.msg +++ /dev/null @@ -1,12 +0,0 @@ -uint64 timestamp # [microseconds] time since system start - -float32 tracked_target_course # [rad] Tracked target course in NED local frame (North is course zero) -float32 follow_angle # [rad] Current follow angle setting - -float32 orbit_angle_setpoint # [rad] Current orbit angle setpoint from the smooth trajectory generator -float32 angular_rate_setpoint # [rad/s] Angular rate commanded from Jerk-limited Orbit Angle trajectory for Orbit Angle - -float32[3] desired_position_raw # [m] Raw 'idealistic' desired drone position if a drone could teleport from place to places - -bool in_emergency_ascent # [bool] True when doing emergency ascent (when distance to ground is below safety altitude) -float32 gimbal_pitch # [rad] Gimbal pitch commanded to track target in the center of the frame diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/FuelTankStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/FuelTankStatus.msg deleted file mode 100644 index 22d21e4a4..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/FuelTankStatus.msg +++ /dev/null @@ -1,17 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 maximum_fuel_capacity # maximum fuel capacity. Must always be provided, either from the driver or a parameter -float32 consumed_fuel # consumed fuel, NaN if not measured. Should not be inferred from the max fuel capacity -float32 fuel_consumption_rate # fuel consumption rate, NaN if not measured - -uint8 percent_remaining # percentage of remaining fuel, UINT8_MAX if not provided -float32 remaining_fuel # remaining fuel, NaN if not measured. Should not be inferred from the max fuel capacity - -uint8 fuel_tank_id # identifier for the fuel tank. Must match ID of other messages for same fuel system. 0 by default when only a single tank exists - -uint32 fuel_type # type of fuel based on MAV_FUEL_TYPE enum. Set to MAV_FUEL_TYPE_UNKNOWN if unknown or it does not fit the provided types -uint8 MAV_FUEL_TYPE_UNKNOWN = 0 # fuel type not specified. Fuel levels are normalized (i.e., maximum is 1, and other levels are relative to 1). -uint8 MAV_FUEL_TYPE_LIQUID = 1 # represents generic liquid fuels, such as gasoline or diesel. Fuel levels are measured in millilitres (ml), and flow rates in millilitres per second (ml/s). -uint8 MAV_FUEL_TYPE_GAS = 2 # represents a gas fuel, such as hydrogen, methane, or propane. Fuel levels are in kilo-Pascal (kPa), and flow rates are in milliliters per second (ml/s). - -float32 temperature # fuel temperature in Kelvin, NaN if not measured diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GeneratorStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GeneratorStatus.msg deleted file mode 100644 index 7ba9b4022..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GeneratorStatus.msg +++ /dev/null @@ -1,44 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - - -uint64 STATUS_FLAG_OFF = 1 # Generator is off. -uint64 STATUS_FLAG_READY = 2 # Generator is ready to start generating power. -uint64 STATUS_FLAG_GENERATING = 4 # Generator is generating power. -uint64 STATUS_FLAG_CHARGING = 8 # Generator is charging the batteries (generating enough power to charge and provide the load). -uint64 STATUS_FLAG_REDUCED_POWER = 16 # Generator is operating at a reduced maximum power. -uint64 STATUS_FLAG_MAXPOWER = 32 # Generator is providing the maximum output. -uint64 STATUS_FLAG_OVERTEMP_WARNING = 64 # Generator is near the maximum operating temperature, cooling is insufficient. -uint64 STATUS_FLAG_OVERTEMP_FAULT = 128 # Generator hit the maximum operating temperature and shutdown. -uint64 STATUS_FLAG_ELECTRONICS_OVERTEMP_WARNING = 256 # Power electronics are near the maximum operating temperature, cooling is insufficient. -uint64 STATUS_FLAG_ELECTRONICS_OVERTEMP_FAULT = 512 # Power electronics hit the maximum operating temperature and shutdown. -uint64 STATUS_FLAG_ELECTRONICS_FAULT = 1024 # Power electronics experienced a fault and shutdown. -uint64 STATUS_FLAG_POWERSOURCE_FAULT = 2048 # The power source supplying the generator failed e.g. mechanical generator stopped, tether is no longer providing power, solar cell is in shade, hydrogen reaction no longer happening. -uint64 STATUS_FLAG_COMMUNICATION_WARNING = 4096 # Generator controller having communication problems. -uint64 STATUS_FLAG_COOLING_WARNING = 8192 # Power electronic or generator cooling system error. -uint64 STATUS_FLAG_POWER_RAIL_FAULT = 16384 # Generator controller power rail experienced a fault. -uint64 STATUS_FLAG_OVERCURRENT_FAULT = 32768 # Generator controller exceeded the overcurrent threshold and shutdown to prevent damage. -uint64 STATUS_FLAG_BATTERY_OVERCHARGE_CURRENT_FAULT = 65536 # Generator controller detected a high current going into the batteries and shutdown to prevent battery damage. | -uint64 STATUS_FLAG_OVERVOLTAGE_FAULT = 131072 # Generator controller exceeded it's overvoltage threshold and shutdown to prevent it exceeding the voltage rating. -uint64 STATUS_FLAG_BATTERY_UNDERVOLT_FAULT = 262144 # Batteries are under voltage (generator will not start). -uint64 STATUS_FLAG_START_INHIBITED = 524288 # Generator start is inhibited by e.g. a safety switch. -uint64 STATUS_FLAG_MAINTENANCE_REQUIRED = 1048576 # Generator requires maintenance. -uint64 STATUS_FLAG_WARMING_UP = 2097152 # Generator is not ready to generate yet. -uint64 STATUS_FLAG_IDLE = 4194304 # Generator is idle. - -uint64 status # Status flags - - -float32 battery_current # [A] Current into/out of battery. Positive for out. Negative for in. NaN: field not provided. -float32 load_current # [A] Current going to the UAV. If battery current not available this is the DC current from the generator. Positive for out. Negative for in. NaN: field not provided -float32 power_generated # [W] The power being generated. NaN: field not provided -float32 bus_voltage # [V] Voltage of the bus seen at the generator, or battery bus if battery bus is controlled by generator and at a different voltage to main bus. -float32 bat_current_setpoint # [A] The target battery current. Positive for out. Negative for in. NaN: field not provided - -uint32 runtime # [s] Seconds this generator has run since it was rebooted. UINT32_MAX: field not provided. - -int32 time_until_maintenance # [s] Seconds until this generator requires maintenance. A negative value indicates maintenance is past-due. INT32_MAX: field not provided. - -uint16 generator_speed # [rpm] Speed of electrical generator or alternator. UINT16_MAX: field not provided. - -int16 rectifier_temperature # [degC] The temperature of the rectifier or power converter. INT16_MAX: field not provided. -int16 generator_temperature # [degC] The temperature of the mechanical motor, fuel cell core or generator. INT16_MAX: field not provided. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GeofenceResult.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GeofenceResult.msg deleted file mode 100644 index 7782d1d6e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GeofenceResult.msg +++ /dev/null @@ -1,13 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 GF_ACTION_NONE = 0 # no action on geofence violation -uint8 GF_ACTION_WARN = 1 # critical mavlink message -uint8 GF_ACTION_LOITER = 2 # switch to AUTO|LOITER -uint8 GF_ACTION_RTL = 3 # switch to AUTO|RTL -uint8 GF_ACTION_TERMINATE = 4 # flight termination -uint8 GF_ACTION_LAND = 5 # switch to AUTO|LAND - -bool geofence_max_dist_triggered # true the check for max distance from Home is triggered -bool geofence_max_alt_triggered # true the check for max altitude above Home is triggered -bool geofence_custom_fence_triggered # true the check for custom inclusion/exclusion geofence(s) is triggered - -uint8 geofence_action # action to take when the geofence is breached diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GeofenceStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GeofenceStatus.msg deleted file mode 100644 index d32b9010c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GeofenceStatus.msg +++ /dev/null @@ -1,7 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 geofence_id # loaded geofence id -uint8 status # Current geofence status - -uint8 GF_STATUS_LOADING = 0 -uint8 GF_STATUS_READY = 1 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalControls.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalControls.msg deleted file mode 100644 index 3e1c5a9dd..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalControls.msg +++ /dev/null @@ -1,7 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 INDEX_ROLL = 0 -uint8 INDEX_PITCH = 1 -uint8 INDEX_YAW = 2 - -uint64 timestamp_sample # the timestamp the data this control response is based on was sampled -float32[3] control diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceAttitudeStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceAttitudeStatus.msg deleted file mode 100644 index 0be66babe..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceAttitudeStatus.msg +++ /dev/null @@ -1,20 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 target_system -uint8 target_component -uint16 device_flags - -uint16 DEVICE_FLAGS_RETRACT = 1 -uint16 DEVICE_FLAGS_NEUTRAL = 2 -uint16 DEVICE_FLAGS_ROLL_LOCK = 4 -uint16 DEVICE_FLAGS_PITCH_LOCK = 8 -uint16 DEVICE_FLAGS_YAW_LOCK = 16 - -float32[4] q -float32 angular_velocity_x -float32 angular_velocity_y -float32 angular_velocity_z - -uint32 failure_flags - -bool received_from_mavlink diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceInformation.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceInformation.msg deleted file mode 100644 index 8f7a41643..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceInformation.msg +++ /dev/null @@ -1,36 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8[32] vendor_name -uint8[32] model_name -uint8[32] custom_name -uint32 firmware_version -uint32 hardware_version -uint64 uid - -uint16 cap_flags - -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_RETRACT = 1 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_NEUTRAL = 2 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_ROLL_AXIS = 4 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_ROLL_FOLLOW = 8 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_ROLL_LOCK = 16 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_PITCH_AXIS = 32 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_PITCH_FOLLOW = 64 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_PITCH_LOCK = 128 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_YAW_AXIS = 256 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_YAW_FOLLOW = 512 -uint32 GIMBAL_DEVICE_CAP_FLAGS_HAS_YAW_LOCK = 1024 -uint32 GIMBAL_DEVICE_CAP_FLAGS_SUPPORTS_INFINITE_YAW = 2048 - -uint16 custom_cap_flags - -float32 roll_min # [rad] -float32 roll_max # [rad] - -float32 pitch_min # [rad] -float32 pitch_max # [rad] - -float32 yaw_min # [rad] -float32 yaw_max # [rad] - -uint8 gimbal_device_compid diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceSetAttitude.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceSetAttitude.msg deleted file mode 100644 index f224a2344..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalDeviceSetAttitude.msg +++ /dev/null @@ -1,17 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 target_system -uint8 target_component - -uint16 flags -uint32 GIMBAL_DEVICE_FLAGS_RETRACT = 1 -uint32 GIMBAL_DEVICE_FLAGS_NEUTRAL = 2 -uint32 GIMBAL_DEVICE_FLAGS_ROLL_LOCK = 4 -uint32 GIMBAL_DEVICE_FLAGS_PITCH_LOCK = 8 -uint32 GIMBAL_DEVICE_FLAGS_YAW_LOCK = 16 - -float32[4] q - -float32 angular_velocity_x -float32 angular_velocity_y -float32 angular_velocity_z diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerInformation.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerInformation.msg deleted file mode 100644 index 28db68a45..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerInformation.msg +++ /dev/null @@ -1,29 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 cap_flags - -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_RETRACT = 1 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_NEUTRAL = 2 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_ROLL_AXIS = 4 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_ROLL_FOLLOW = 8 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_ROLL_LOCK = 16 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_PITCH_AXIS = 32 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_PITCH_FOLLOW = 64 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_PITCH_LOCK = 128 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_AXIS = 256 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_FOLLOW = 512 -uint32 GIMBAL_MANAGER_CAP_FLAGS_HAS_YAW_LOCK = 1024 -uint32 GIMBAL_MANAGER_CAP_FLAGS_SUPPORTS_INFINITE_YAW = 2048 -uint32 GIMBAL_MANAGER_CAP_FLAGS_CAN_POINT_LOCATION_LOCAL = 65536 -uint32 GIMBAL_MANAGER_CAP_FLAGS_CAN_POINT_LOCATION_GLOBAL = 131072 - -uint8 gimbal_device_id - -float32 roll_min # [rad] -float32 roll_max # [rad] - -float32 pitch_min # [rad] -float32 pitch_max # [rad] - -float32 yaw_min # [rad] -float32 yaw_max # [rad] diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerSetAttitude.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerSetAttitude.msg deleted file mode 100644 index d88acca8b..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerSetAttitude.msg +++ /dev/null @@ -1,22 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 origin_sysid -uint8 origin_compid - -uint8 target_system -uint8 target_component - -uint32 GIMBAL_MANAGER_FLAGS_RETRACT = 1 -uint32 GIMBAL_MANAGER_FLAGS_NEUTRAL = 2 -uint32 GIMBAL_MANAGER_FLAGS_ROLL_LOCK = 4 -uint32 GIMBAL_MANAGER_FLAGS_PITCH_LOCK = 8 -uint32 GIMBAL_MANAGER_FLAGS_YAW_LOCK = 16 - -uint32 flags -uint8 gimbal_device_id - -float32[4] q - -float32 angular_velocity_x -float32 angular_velocity_y -float32 angular_velocity_z diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerSetManualControl.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerSetManualControl.msg deleted file mode 100644 index 4061438f7..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerSetManualControl.msg +++ /dev/null @@ -1,21 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 origin_sysid -uint8 origin_compid - -uint8 target_system -uint8 target_component - -uint32 GIMBAL_MANAGER_FLAGS_RETRACT = 1 -uint32 GIMBAL_MANAGER_FLAGS_NEUTRAL = 2 -uint32 GIMBAL_MANAGER_FLAGS_ROLL_LOCK = 4 -uint32 GIMBAL_MANAGER_FLAGS_PITCH_LOCK = 8 -uint32 GIMBAL_MANAGER_FLAGS_YAW_LOCK = 16 - -uint32 flags -uint8 gimbal_device_id - -float32 pitch # unitless -1..1, can be NAN -float32 yaw # unitless -1..1, can be NAN -float32 pitch_rate # unitless -1..1, can be NAN -float32 yaw_rate # unitless -1..1, can be NAN diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerStatus.msg deleted file mode 100644 index 002e5c90e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GimbalManagerStatus.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 flags -uint8 gimbal_device_id -uint8 primary_control_sysid -uint8 primary_control_compid -uint8 secondary_control_sysid -uint8 secondary_control_compid diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GotoSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GotoSetpoint.msg deleted file mode 100644 index 5fe3ab8a7..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GotoSetpoint.msg +++ /dev/null @@ -1,24 +0,0 @@ -# Position and (optional) heading setpoints with corresponding speed constraints -# Setpoints are intended as inputs to position and heading smoothers, respectively -# Setpoints do not need to be kinematically consistent -# Optional heading setpoints may be specified as controlled by the respective flag -# Unset optional setpoints are not controlled -# Unset optional constraints default to vehicle specifications - -uint64 timestamp # time since system start (microseconds) - -# setpoints -float32[3] position # [m] NED local world frame - -bool flag_control_heading # true if heading is to be controlled -float32 heading # (optional) [rad] [-pi,pi] from North - -# constraints -bool flag_set_max_horizontal_speed # true if setting a non-default horizontal speed limit -float32 max_horizontal_speed # (optional) [m/s] maximum speed (absolute) in the NE-plane - -bool flag_set_max_vertical_speed # true if setting a non-default vertical speed limit -float32 max_vertical_speed # (optional) [m/s] maximum speed (absolute) in the D-axis - -bool flag_set_max_heading_rate # true if setting a non-default heading rate limit -float32 max_heading_rate # (optional) [rad/s] maximum heading rate (absolute) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioConfig.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioConfig.msg deleted file mode 100644 index 0ff393ec8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioConfig.msg +++ /dev/null @@ -1,28 +0,0 @@ -# GPIO configuration - -uint64 timestamp # time since system start (microseconds) -uint32 device_id # Device id - -uint32 mask # Pin mask -uint32 state # Initial pin output state - -# Configuration Mask -# Bit 0-3: Direction: 0=Input, 1=Output -# Bit 4-7: Input Config: 0=Floating, 1=PullUp, 2=PullDown -# Bit 8-12: Output Config: 0=PushPull, 1=OpenDrain -# Bit 13-31: Reserved -uint32 INPUT = 0 # 0x0000 -uint32 OUTPUT = 1 # 0x0001 -uint32 PULLUP = 16 # 0x0010 -uint32 PULLDOWN = 32 # 0x0020 -uint32 OPENDRAIN = 256 # 0x0100 - -uint32 INPUT_FLOATING = 0 # 0x0000 -uint32 INPUT_PULLUP = 16 # 0x0010 -uint32 INPUT_PULLDOWN = 32 # 0x0020 - -uint32 OUTPUT_PUSHPULL = 0 # 0x0000 -uint32 OUTPUT_OPENDRAIN = 256 # 0x0100 -uint32 OUTPUT_OPENDRAIN_PULLUP = 272 # 0x0110 - -uint32 config diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioIn.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioIn.msg deleted file mode 100644 index 0482a2188..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioIn.msg +++ /dev/null @@ -1,6 +0,0 @@ -# GPIO mask and state - -uint64 timestamp # time since system start (microseconds) -uint32 device_id # Device id - -uint32 state # pin state mask diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioOut.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioOut.msg deleted file mode 100644 index 3865bbf2e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioOut.msg +++ /dev/null @@ -1,7 +0,0 @@ -# GPIO mask and state - -uint64 timestamp # time since system start (microseconds) -uint32 device_id # Device id - -uint32 mask # pin mask -uint32 state # pin state mask diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioRequest.msg deleted file mode 100644 index 3328b0014..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpioRequest.msg +++ /dev/null @@ -1,4 +0,0 @@ -# Request GPIO mask to be read - -uint64 timestamp # time since system start (microseconds) -uint32 device_id # Device id diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpsDump.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GpsDump.msg deleted file mode 100644 index 2477bcfa3..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpsDump.msg +++ /dev/null @@ -1,10 +0,0 @@ -# This message is used to dump the raw gps communication to the log. - -uint64 timestamp # time since system start (microseconds) - -uint8 instance # Instance of GNSS receiver -uint8 len # length of data, MSB bit set = message to the gps device, - # clear = message from the device -uint8[79] data # data to write to the log - -uint8 ORB_QUEUE_LENGTH = 8 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpsInjectData.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/GpsInjectData.msg deleted file mode 100644 index 516d5cb5d..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/GpsInjectData.msg +++ /dev/null @@ -1,11 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -uint16 len # length of data -uint8 flags # LSB: 1=fragmented -uint8[300] data # data to write to GPS device (RTCM message) - -uint8 ORB_QUEUE_LENGTH = 8 - -uint8 MAX_INSTANCES = 2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Gripper.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Gripper.msg deleted file mode 100644 index 4f1445cb5..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Gripper.msg +++ /dev/null @@ -1,7 +0,0 @@ -## Used to command an actuation in the gripper, which is mapped to a specific output in the control allocation module - -uint64 timestamp - -int8 command # Commanded state for the gripper -int8 COMMAND_GRAB = 0 -int8 COMMAND_RELEASE = 1 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/HealthReport.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/HealthReport.msg deleted file mode 100644 index 189518052..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/HealthReport.msg +++ /dev/null @@ -1,12 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint64 can_arm_mode_flags # bitfield for each flight mode (NAVIGATION_STATE_*) if arming is possible -uint64 can_run_mode_flags # bitfield for each flight mode if it can run - -uint64 health_is_present_flags # flags for each health_component_t -uint64 health_warning_flags -uint64 health_error_flags -# A component is required but missing, if present==0 and error==1 - -uint64 arming_check_warning_flags -uint64 arming_check_error_flags diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/HeaterStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/HeaterStatus.msg deleted file mode 100644 index 44207a398..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/HeaterStatus.msg +++ /dev/null @@ -1,20 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 device_id - -bool heater_on -bool temperature_target_met - -float32 temperature_sensor -float32 temperature_target - -uint32 controller_period_usec -uint32 controller_time_on_usec - -float32 proportional_value -float32 integrator_value -float32 feed_forward_value - -uint8 MODE_GPIO = 1 -uint8 MODE_PX4IO = 2 -uint8 mode diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/HomePosition.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/HomePosition.msg deleted file mode 100644 index e6a517285..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/HomePosition.msg +++ /dev/null @@ -1,21 +0,0 @@ -# GPS home position in WGS84 coordinates. - -uint64 timestamp # time since system start (microseconds) - -float64 lat # Latitude in degrees -float64 lon # Longitude in degrees -float32 alt # Altitude in meters (AMSL) - -float32 x # X coordinate in meters -float32 y # Y coordinate in meters -float32 z # Z coordinate in meters - -float32 yaw # Yaw angle in radians - -bool valid_alt # true when the altitude has been set -bool valid_hpos # true when the latitude and longitude have been set -bool valid_lpos # true when the local position (xyz) has been set - -bool manual_home # true when home position was set manually - -uint32 update_count # update counter of the home position diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/HoverThrustEstimate.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/HoverThrustEstimate.msg deleted file mode 100644 index a38d90425..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/HoverThrustEstimate.msg +++ /dev/null @@ -1,13 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # time of corresponding sensor data last used for this estimate - -float32 hover_thrust # estimated hover thrust [0.1, 0.9] -float32 hover_thrust_var # estimated hover thrust variance - -float32 accel_innov # innovation of the last acceleration fusion -float32 accel_innov_var # innovation variance of the last acceleration fusion -float32 accel_innov_test_ratio # normalized innovation squared test ratio - -float32 accel_noise_var # vertical acceleration noise variance estimated form innovation residual - -bool valid diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/InputRc.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/InputRc.msg deleted file mode 100644 index db4b3de23..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/InputRc.msg +++ /dev/null @@ -1,40 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 RC_INPUT_SOURCE_UNKNOWN = 0 -uint8 RC_INPUT_SOURCE_PX4FMU_PPM = 1 -uint8 RC_INPUT_SOURCE_PX4IO_PPM = 2 -uint8 RC_INPUT_SOURCE_PX4IO_SPEKTRUM = 3 -uint8 RC_INPUT_SOURCE_PX4IO_SBUS = 4 -uint8 RC_INPUT_SOURCE_PX4IO_ST24 = 5 -uint8 RC_INPUT_SOURCE_MAVLINK = 6 -uint8 RC_INPUT_SOURCE_QURT = 7 -uint8 RC_INPUT_SOURCE_PX4FMU_SPEKTRUM = 8 -uint8 RC_INPUT_SOURCE_PX4FMU_SBUS = 9 -uint8 RC_INPUT_SOURCE_PX4FMU_ST24 = 10 -uint8 RC_INPUT_SOURCE_PX4FMU_SUMD = 11 -uint8 RC_INPUT_SOURCE_PX4FMU_DSM = 12 -uint8 RC_INPUT_SOURCE_PX4IO_SUMD = 13 -uint8 RC_INPUT_SOURCE_PX4FMU_CRSF = 14 -uint8 RC_INPUT_SOURCE_PX4FMU_GHST = 15 - -uint8 RC_INPUT_MAX_CHANNELS = 18 # Maximum number of R/C input channels in the system. S.Bus has up to 18 channels. - -uint64 timestamp_last_signal # last valid reception time - -uint8 channel_count # number of channels actually being seen - -int8 RSSI_MAX = 100 -int32 rssi # receive signal strength indicator (RSSI): < 0: Undefined, 0: no signal, 100: full reception - -bool rc_failsafe # explicit failsafe flag: true on TX failure or TX out of range , false otherwise. Only the true state is reliable, as there are some (PPM) receivers on the market going into failsafe without telling us explicitly. -bool rc_lost # RC receiver connection status: True,if no frame has arrived in the expected time, false otherwise. True usually means that the receiver has been disconnected, but can also indicate a radio link loss on "stupid" systems. Will remain false, if a RX with failsafe option continues to transmit frames after a link loss. - -uint16 rc_lost_frame_count # Number of lost RC frames. Note: intended purpose: observe the radio link quality if RSSI is not available. This value must not be used to trigger any failsafe-alike functionality. -uint16 rc_total_frame_count # Number of total RC frames. Note: intended purpose: observe the radio link quality if RSSI is not available. This value must not be used to trigger any failsafe-alike functionality. -uint16 rc_ppm_frame_length # Length of a single PPM frame. Zero for non-PPM systems - -uint8 input_source # Input source -uint16[18] values # measured pulse widths for each of the supported channels - -int8 link_quality # link quality. Percentage 0-100%. -1 = invalid -float32 rssi_dbm # Actual rssi in units of dBm. NaN = invalid \ No newline at end of file diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/InternalCombustionEngineStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/InternalCombustionEngineStatus.msg deleted file mode 100644 index 301eb92a8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/InternalCombustionEngineStatus.msg +++ /dev/null @@ -1,64 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 STATE_STOPPED = 0 # The engine is not running. This is the default state. -uint8 STATE_STARTING = 1 # The engine is starting. This is a transient state. -uint8 STATE_RUNNING = 2 # The engine is running normally. -uint8 STATE_FAULT = 3 # The engine can no longer function. -uint8 state - -uint32 FLAG_GENERAL_ERROR = 1 # General error. - -uint32 FLAG_CRANKSHAFT_SENSOR_ERROR_SUPPORTED = 2 # Error of the crankshaft sensor. This flag is optional. -uint32 FLAG_CRANKSHAFT_SENSOR_ERROR = 4 - -uint32 FLAG_TEMPERATURE_SUPPORTED = 8 # Temperature levels. These flags are optional -uint32 FLAG_TEMPERATURE_BELOW_NOMINAL = 16 # Under-temperature warning -uint32 FLAG_TEMPERATURE_ABOVE_NOMINAL = 32 # Over-temperature warning -uint32 FLAG_TEMPERATURE_OVERHEATING = 64 # Critical overheating -uint32 FLAG_TEMPERATURE_EGT_ABOVE_NOMINAL = 128 # Exhaust gas over-temperature warning - -uint32 FLAG_FUEL_PRESSURE_SUPPORTED = 256 # Fuel pressure. These flags are optional -uint32 FLAG_FUEL_PRESSURE_BELOW_NOMINAL = 512 # Under-pressure warning -uint32 FLAG_FUEL_PRESSURE_ABOVE_NOMINAL = 1024 # Over-pressure warning - -uint32 FLAG_DETONATION_SUPPORTED = 2048 # Detonation warning. This flag is optional. -uint32 FLAG_DETONATION_OBSERVED = 4096 # Detonation condition observed warning - -uint32 FLAG_MISFIRE_SUPPORTED = 8192 # Misfire warning. This flag is optional. -uint32 FLAG_MISFIRE_OBSERVED = 16384 # Misfire condition observed warning - -uint32 FLAG_OIL_PRESSURE_SUPPORTED = 32768 # Oil pressure. These flags are optional -uint32 FLAG_OIL_PRESSURE_BELOW_NOMINAL = 65536 # Under-pressure warning -uint32 FLAG_OIL_PRESSURE_ABOVE_NOMINAL = 131072 # Over-pressure warning - -uint32 FLAG_DEBRIS_SUPPORTED = 262144 # Debris warning. This flag is optional -uint32 FLAG_DEBRIS_DETECTED = 524288 # Detection of debris warning -uint32 flags - -uint8 engine_load_percent # Engine load estimate, percent, [0, 127] -uint32 engine_speed_rpm # Engine speed, revolutions per minute -float32 spark_dwell_time_ms # Spark dwell time, millisecond -float32 atmospheric_pressure_kpa # Atmospheric (barometric) pressure, kilopascal -float32 intake_manifold_pressure_kpa # Engine intake manifold pressure, kilopascal -float32 intake_manifold_temperature # Engine intake manifold temperature, kelvin -float32 coolant_temperature # Engine coolant temperature, kelvin -float32 oil_pressure # Oil pressure, kilopascal -float32 oil_temperature # Oil temperature, kelvin -float32 fuel_pressure # Fuel pressure, kilopascal -float32 fuel_consumption_rate_cm3pm # Instant fuel consumption estimate, (centimeter^3)/minute -float32 estimated_consumed_fuel_volume_cm3 # Estimate of the consumed fuel since the start of the engine, centimeter^3 -uint8 throttle_position_percent # Throttle position, percent -uint8 ecu_index # The index of the publishing ECU - - -uint8 SPARK_PLUG_SINGLE = 0 -uint8 SPARK_PLUG_FIRST_ACTIVE = 1 -uint8 SPARK_PLUG_SECOND_ACTIVE = 2 -uint8 SPARK_PLUG_BOTH_ACTIVE = 3 -uint8 spark_plug_usage # Spark plug activity report. - -float32 ignition_timing_deg # Cylinder ignition timing, angular degrees of the crankshaft -float32 injection_time_ms # Fuel injection time, millisecond -float32 cylinder_head_temperature # Cylinder head temperature (CHT), kelvin -float32 exhaust_gas_temperature # Exhaust gas temperature (EGT), kelvin -float32 lambda_coefficient # Estimated lambda coefficient, dimensionless ratio diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/IridiumsbdStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/IridiumsbdStatus.msg deleted file mode 100644 index 436654e4f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/IridiumsbdStatus.msg +++ /dev/null @@ -1,15 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 last_at_ok_timestamp # timestamp of the last "OK" received after the "AT" command -uint16 tx_buf_write_index # current size of the tx buffer -uint16 rx_buf_read_index # the rx buffer is parsed up to that index -uint16 rx_buf_end_index # current size of the rx buffer -uint16 failed_sbd_sessions # number of failed sbd sessions -uint16 successful_sbd_sessions # number of successful sbd sessions -uint16 num_tx_buf_reset # number of times the tx buffer was reset -uint8 signal_quality # current signal quality, 0 is no signal, 5 the best -uint8 state # current state of the driver, see the satcom_state of IridiumSBD.h for the definition -bool ring_pending # indicates if a ring call is pending -bool tx_buf_write_pending # indicates if a tx buffer write is pending -bool tx_session_pending # indicates if a tx session is pending -bool rx_read_pending # indicates if a rx read is pending -bool rx_session_pending # indicates if a rx session is pending diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/IrlockReport.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/IrlockReport.msg deleted file mode 100644 index 9f23cbf3c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/IrlockReport.msg +++ /dev/null @@ -1,11 +0,0 @@ -# IRLOCK_REPORT message data - -uint64 timestamp # time since system start (microseconds) - -uint16 signature - -# When looking along the optical axis of the camera, x points right, y points down, and z points along the optical axis. -float32 pos_x # tan(theta), where theta is the angle between the target and the camera center of projection in camera x-axis -float32 pos_y # tan(theta), where theta is the angle between the target and the camera center of projection in camera y-axis -float32 size_x #/** size of target along camera x-axis in units of tan(theta) **/ -float32 size_y #/** size of target along camera y-axis in units of tan(theta) **/ diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingGear.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingGear.msg deleted file mode 100644 index 5ef9ee52f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingGear.msg +++ /dev/null @@ -1,7 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -int8 GEAR_UP = 1 # landing gear up -int8 GEAR_DOWN = -1 # landing gear down -int8 GEAR_KEEP = 0 # keep the current state - -int8 landing_gear diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingGearWheel.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingGearWheel.msg deleted file mode 100644 index 2ff99fcc5..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingGearWheel.msg +++ /dev/null @@ -1,3 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 normalized_wheel_setpoint # negative is turning left, positive turning right [-1, 1] diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingTargetInnovations.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingTargetInnovations.msg deleted file mode 100644 index 5dd892c56..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingTargetInnovations.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -# Innovation of landing target position estimator -float32 innov_x -float32 innov_y - -# Innovation covariance of landing target position estimator -float32 innov_cov_x -float32 innov_cov_y diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingTargetPose.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingTargetPose.msg deleted file mode 100644 index 875920f18..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/LandingTargetPose.msg +++ /dev/null @@ -1,26 +0,0 @@ -# Relative position of precision land target in navigation (body fixed, north aligned, NED) and inertial (world fixed, north aligned, NED) frames - -uint64 timestamp # time since system start (microseconds) - -bool is_static # Flag indicating whether the landing target is static or moving with respect to the ground - -bool rel_pos_valid # Flag showing whether relative position is valid -bool rel_vel_valid # Flag showing whether relative velocity is valid - -float32 x_rel # X/north position of target, relative to vehicle (navigation frame) [meters] -float32 y_rel # Y/east position of target, relative to vehicle (navigation frame) [meters] -float32 z_rel # Z/down position of target, relative to vehicle (navigation frame) [meters] - -float32 vx_rel # X/north velocity of target, relative to vehicle (navigation frame) [meters/second] -float32 vy_rel # Y/east velocity of target, relative to vehicle (navigation frame) [meters/second] - -float32 cov_x_rel # X/north position variance [meters^2] -float32 cov_y_rel # Y/east position variance [meters^2] - -float32 cov_vx_rel # X/north velocity variance [(meters/second)^2] -float32 cov_vy_rel # Y/east velocity variance [(meters/second)^2] - -bool abs_pos_valid # Flag showing whether absolute position is valid -float32 x_abs # X/north position of target, relative to origin (navigation frame) [meters] -float32 y_abs # Y/east position of target, relative to origin (navigation frame) [meters] -float32 z_abs # Z/down position of target, relative to origin (navigation frame) [meters] diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/LaunchDetectionStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/LaunchDetectionStatus.msg deleted file mode 100644 index 6917f4bc2..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/LaunchDetectionStatus.msg +++ /dev/null @@ -1,9 +0,0 @@ -# Status of the launch detection state machine (fixed-wing only) - -uint64 timestamp # time since system start (microseconds) - -uint8 STATE_WAITING_FOR_LAUNCH = 0 # waiting for launch -uint8 STATE_LAUNCH_DETECTED_DISABLED_MOTOR = 1 # launch detected, but keep motor(s) disabled (e.g. because it can't spin freely while on catapult) -uint8 STATE_FLYING = 2 # launch detected, use normal takeoff/flying configuration - -uint8 launch_detection_state diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/LedControl.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/LedControl.msg deleted file mode 100644 index 4be5cc1ce..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/LedControl.msg +++ /dev/null @@ -1,37 +0,0 @@ -# LED control: control a single or multiple LED's. -# These are the externally visible LED's, not the board LED's - -uint64 timestamp # time since system start (microseconds) - -# colors -uint8 COLOR_OFF = 0 # this is only used in the drivers -uint8 COLOR_RED = 1 -uint8 COLOR_GREEN = 2 -uint8 COLOR_BLUE = 3 -uint8 COLOR_YELLOW = 4 -uint8 COLOR_PURPLE = 5 -uint8 COLOR_AMBER = 6 -uint8 COLOR_CYAN = 7 -uint8 COLOR_WHITE = 8 - -# LED modes definitions -uint8 MODE_OFF = 0 # turn LED off -uint8 MODE_ON = 1 # turn LED on -uint8 MODE_DISABLED = 2 # disable this priority (switch to lower priority setting) -uint8 MODE_BLINK_SLOW = 3 -uint8 MODE_BLINK_NORMAL = 4 -uint8 MODE_BLINK_FAST = 5 -uint8 MODE_BREATHE = 6 # continuously increase & decrease brightness (solid color if driver does not support it) -uint8 MODE_FLASH = 7 # two fast blinks (on/off) with timing as in MODE_BLINK_FAST and then off for a while - -uint8 MAX_PRIORITY = 2 # maximum priority (minimum is 0) - - -uint8 led_mask # bitmask which LED(s) to control, set to 0xff for all -uint8 color # see COLOR_* -uint8 mode # see MODE_* -uint8 num_blinks # how many times to blink (number of on-off cycles if mode is one of MODE_BLINK_*) . Set to 0 for infinite - # in MODE_FLASH it is the number of cycles. Max number of blinks: 122 and max number of flash cycles: 20 -uint8 priority # priority: higher priority events will override current lower priority events (see MAX_PRIORITY) - -uint8 ORB_QUEUE_LENGTH = 8 # needs to match BOARD_MAX_LEDS diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/LogMessage.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/LogMessage.msg deleted file mode 100644 index afb690b14..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/LogMessage.msg +++ /dev/null @@ -1,8 +0,0 @@ -# A logging message, output with PX4_WARN, PX4_ERR, PX4_INFO - -uint64 timestamp # time since system start (microseconds) - -uint8 severity # log level (same as in the linux kernel, starting with 0) -char[127] text - -uint8 ORB_QUEUE_LENGTH = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/LoggerStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/LoggerStatus.msg deleted file mode 100644 index c67c88959..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/LoggerStatus.msg +++ /dev/null @@ -1,23 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 LOGGER_TYPE_FULL = 0 # Normal, full size log -uint8 LOGGER_TYPE_MISSION = 1 # reduced mission log (e.g. for geotagging) -uint8 type - -uint8 BACKEND_FILE = 1 -uint8 BACKEND_MAVLINK = 2 -uint8 BACKEND_ALL = 3 -uint8 backend - -bool is_logging - -float32 total_written_kb # total written to log in kiloBytes -float32 write_rate_kb_s # write rate in kiloBytes/s - -uint32 dropouts # number of failed buffer writes due to buffer overflow -uint32 message_gaps # messages misssed - -uint32 buffer_used_bytes # current buffer fill in Bytes -uint32 buffer_size_bytes # total buffer size in Bytes - -uint8 num_messages diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/MagWorkerData.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/MagWorkerData.msg deleted file mode 100644 index 09626e8a8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/MagWorkerData.msg +++ /dev/null @@ -1,13 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint8 MAX_MAGS = 4 - -uint32 done_count -uint32 calibration_points_perside -uint64 calibration_interval_perside_us -uint32[4] calibration_counter_total -bool[4] side_data_collected -float32[4] x -float32[4] y -float32[4] z diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/MagnetometerBiasEstimate.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/MagnetometerBiasEstimate.msg deleted file mode 100644 index 3c0c1136c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/MagnetometerBiasEstimate.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32[4] bias_x # estimated X-bias of all the sensors -float32[4] bias_y # estimated Y-bias of all the sensors -float32[4] bias_z # estimated Z-bias of all the sensors - -bool[4] valid # true if the estimator has converged -bool[4] stable diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ManualControlSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ManualControlSetpoint.msg deleted file mode 100644 index 95fa62228..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ManualControlSetpoint.msg +++ /dev/null @@ -1,46 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -bool valid - -uint8 SOURCE_UNKNOWN = 0 -uint8 SOURCE_RC = 1 # radio control (input_rc) -uint8 SOURCE_MAVLINK_0 = 2 # mavlink instance 0 -uint8 SOURCE_MAVLINK_1 = 3 # mavlink instance 1 -uint8 SOURCE_MAVLINK_2 = 4 # mavlink instance 2 -uint8 SOURCE_MAVLINK_3 = 5 # mavlink instance 3 -uint8 SOURCE_MAVLINK_4 = 6 # mavlink instance 4 -uint8 SOURCE_MAVLINK_5 = 7 # mavlink instance 5 - -uint8 data_source - -# Any of the channels may not be available and be set to NaN -# to indicate that it does not contain valid data. - -# Stick positions [-1,1] -# on a common RC mode 1/2/3/4 remote/joystick the stick deflection: -1 is down/left, 1 is up/right -# Note: QGC sends throttle/z in range [0,1000] - [0,1]. The MAVLink input conversion [0,1] to [-1,1] is at the moment kept backwards compatible. -# Positive values are generally used for: -float32 roll # move right, positive roll rotation, right side down -float32 pitch # move forward, negative pitch rotation, nose down -float32 yaw # positive yaw rotation, clockwise when seen top down -float32 throttle # move up, positive thrust, -1 is minimum available 0% or -100% +1 is 100% thrust - -float32 flaps # position of flaps switch/knob/lever [-1, 1] - -float32 aux1 -float32 aux2 -float32 aux3 -float32 aux4 -float32 aux5 -float32 aux6 - -bool sticks_moving - -uint16 buttons # From uint16 buttons field of Mavlink manual_control message - -# TOPICS manual_control_setpoint manual_control_input -# DEPRECATED: float32 x -# DEPRECATED: float32 y -# DEPRECATED: float32 z -# DEPRECATED: float32 r diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ManualControlSwitches.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ManualControlSwitches.msg deleted file mode 100644 index 4d1cbab23..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ManualControlSwitches.msg +++ /dev/null @@ -1,34 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -uint8 SWITCH_POS_NONE = 0 # switch is not mapped -uint8 SWITCH_POS_ON = 1 # switch activated (value = 1) -uint8 SWITCH_POS_MIDDLE = 2 # middle position (value = 0) -uint8 SWITCH_POS_OFF = 3 # switch not activated (value = -1) - -uint8 MODE_SLOT_NONE = 0 # no mode slot assigned -uint8 MODE_SLOT_1 = 1 # mode slot 1 selected -uint8 MODE_SLOT_2 = 2 # mode slot 2 selected -uint8 MODE_SLOT_3 = 3 # mode slot 3 selected -uint8 MODE_SLOT_4 = 4 # mode slot 4 selected -uint8 MODE_SLOT_5 = 5 # mode slot 5 selected -uint8 MODE_SLOT_6 = 6 # mode slot 6 selected -uint8 MODE_SLOT_NUM = 6 # number of slots - -uint8 mode_slot # the slot a specific model selector is in - -uint8 arm_switch # arm/disarm switch: _DISARMED_, ARMED -uint8 return_switch # return to launch 2 position switch (mandatory): _NORMAL_, RTL -uint8 loiter_switch # loiter 2 position switch (optional): _MISSION_, LOITER -uint8 offboard_switch # offboard 2 position switch (optional): _NORMAL_, OFFBOARD -uint8 kill_switch # throttle kill: _NORMAL_, KILL -uint8 gear_switch # landing gear switch: _DOWN_, UP -uint8 transition_switch # VTOL transition switch: _HOVER, FORWARD_FLIGHT - -uint8 photo_switch # Photo trigger switch -uint8 video_switch # Photo trigger switch - -uint8 engage_main_motor_switch # Engage the main motor (for helicopters) - -uint32 switch_changes # number of switch changes diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/MavlinkLog.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/MavlinkLog.msg deleted file mode 100644 index 8f52ec7db..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/MavlinkLog.msg +++ /dev/null @@ -1,6 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -char[127] text -uint8 severity # log level (same as in the linux kernel, starting with 0) - -uint8 ORB_QUEUE_LENGTH = 8 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/MavlinkTunnel.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/MavlinkTunnel.msg deleted file mode 100644 index 16934a952..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/MavlinkTunnel.msg +++ /dev/null @@ -1,20 +0,0 @@ -# MAV_TUNNEL_PAYLOAD_TYPE enum - -uint8 MAV_TUNNEL_PAYLOAD_TYPE_UNKNOWN = 0 # Encoding of payload unknown -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED0 = 200 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED1 = 201 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED2 = 202 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED3 = 203 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED4 = 204 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED5 = 205 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED6 = 206 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED7 = 207 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED8 = 208 # Registered for STorM32 gimbal controller -uint8 MAV_TUNNEL_PAYLOAD_TYPE_STORM32_RESERVED9 = 209 # Registered for STorM32 gimbal controller - -uint64 timestamp # Time since system start (microseconds) -uint16 payload_type # A code that identifies the content of the payload (0 for unknown, which is the default). If this code is less than 32768, it is a 'registered' payload type and the corresponding code should be added to the MAV_TUNNEL_PAYLOAD_TYPE enum. Software creators can register blocks of types as needed. Codes greater than 32767 are considered local experiments and should not be checked in to any widely distributed codebase. -uint8 target_system # System ID (can be 0 for broadcast, but this is discouraged) -uint8 target_component # Component ID (can be 0 for broadcast, but this is discouraged) -uint8 payload_length # Length of the data transported in payload -uint8[128] payload # Data itself diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/MessageFormatRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/MessageFormatRequest.msg deleted file mode 100644 index 6ceb66d03..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/MessageFormatRequest.msg +++ /dev/null @@ -1,9 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -# Request to PX4 to get the hash of a message, to check for message compatibility - -uint16 LATEST_PROTOCOL_VERSION = 1 # Current version of this protocol. Increase this whenever the MessageFormatRequest or MessageFormatResponse changes. - -uint16 protocol_version # Must be set to LATEST_PROTOCOL_VERSION. Do not change this field, it must be the first field after the timestamp - -char[50] topic_name # E.g. /fmu/in/vehicle_command diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/MessageFormatResponse.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/MessageFormatResponse.msg deleted file mode 100644 index 41ee96274..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/MessageFormatResponse.msg +++ /dev/null @@ -1,11 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -# Response from PX4 with the format of a message - -uint16 protocol_version # Must be set to LATEST_PROTOCOL_VERSION. Do not change this field, it must be the first field after the timestamp - -char[50] topic_name # E.g. /fmu/in/vehicle_command - -bool success -uint32 message_hash # hash over all message fields - diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Mission.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Mission.msg deleted file mode 100644 index a923193da..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Mission.msg +++ /dev/null @@ -1,14 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 mission_dataman_id # default 0, there are two offboard storage places in the dataman: 0 or 1 -uint8 fence_dataman_id # default 0, there are two offboard storage places in the dataman: 0 or 1 -uint8 safepoint_dataman_id # default 0, there are two offboard storage places in the dataman: 0 or 1 - -uint16 count # count of the missions stored in the dataman -int32 current_seq # default -1, start at the one changed latest - -int32 land_start_index # Index of the land start marker, if unavailable index of the land item, -1 otherwise -int32 land_index # Index of the land item, -1 otherwise - -uint32 mission_id # indicates updates to the mission, reload from dataman if changed -uint32 geofence_id # indicates updates to the geofence, reload from dataman if changed -uint32 safe_points_id # indicates updates to the safe points, reload from dataman if changed diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/MissionResult.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/MissionResult.msg deleted file mode 100644 index f70326be3..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/MissionResult.msg +++ /dev/null @@ -1,20 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 mission_id # Id for the mission for which the result was generated -uint32 geofence_id # Id for the corresponding geofence for which the result was generated (used for mission feasibility) -uint32 home_position_counter # Counter of the home position for which the result was generated (used for mission feasibility) - -int32 seq_reached # Sequence of the mission item which has been reached, default -1 -uint16 seq_current # Sequence of the current mission item -uint16 seq_total # Total number of mission items - -bool valid # true if mission is valid -bool warning # true if mission is valid, but has potentially problematic items leading to safety warnings -bool finished # true if mission has been completed -bool failure # true if the mission cannot continue or be completed for some reason - -bool item_do_jump_changed # true if the number of do jumps remaining has changed -uint16 item_changed_index # indicate which item has changed -uint16 item_do_jump_remaining # set to the number of do jumps remaining for that item - -uint8 execution_mode # indicates the mode in which the mission is executed diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ModeCompleted.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ModeCompleted.msg deleted file mode 100644 index bacff4a94..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ModeCompleted.msg +++ /dev/null @@ -1,15 +0,0 @@ -# Mode completion result, published by an active mode. -# The possible values of nav_state are defined in the VehicleStatus msg. -# Note that this is not always published (e.g. when a user switches modes or on -# failsafe activation) -uint64 timestamp # time since system start (microseconds) - - -uint8 RESULT_SUCCESS = 0 -# [1-99]: reserved -uint8 RESULT_FAILURE_OTHER = 100 # Mode failed (generic error) - -uint8 result # One of RESULT_* - -uint8 nav_state # Source mode (values in VehicleStatus) - diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/MountOrientation.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/MountOrientation.msg deleted file mode 100644 index 7ae54a396..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/MountOrientation.msg +++ /dev/null @@ -1,2 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -float32[3] attitude_euler_angle # Attitude/direction of the mount as euler angles in rad diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/NavigatorMissionItem.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/NavigatorMissionItem.msg deleted file mode 100644 index 64af762f7..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/NavigatorMissionItem.msg +++ /dev/null @@ -1,25 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 instance_count # Instance count of this mission. Increments monotonically whenever the mission is modified - -uint16 sequence_current # Sequence of the current mission item - -uint16 nav_cmd - -float32 latitude -float32 longitude - -float32 time_inside # time that the MAV should stay inside the radius before advancing in seconds -float32 acceptance_radius # default radius in which the mission is accepted as reached in meters -float32 loiter_radius # loiter radius in meters, 0 for a VTOL to hover, negative for counter-clockwise -float32 yaw # in radians NED -PI..+PI, NAN means don't change yaw -float32 altitude # altitude in meters (AMSL) - -uint8 frame # mission frame -uint8 origin # mission item origin (onboard or mavlink) - -bool loiter_exit_xtrack # exit xtrack location: 0 for center of loiter wp, 1 for exit location -bool force_heading # heading needs to be reached -bool altitude_is_relative # true if altitude is relative from start point -bool autocontinue # true if next waypoint should follow after this one -bool vtol_back_transition # part of the vtol back transition sequence diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/NormalizedUnsignedSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/NormalizedUnsignedSetpoint.msg deleted file mode 100644 index 10193b8cc..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/NormalizedUnsignedSetpoint.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 normalized_setpoint # [0, 1] - -# TOPICS flaps_setpoint spoilers_setpoint diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/NpfgStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/NpfgStatus.msg deleted file mode 100644 index 132c1f7f3..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/NpfgStatus.msg +++ /dev/null @@ -1,17 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 wind_est_valid # (boolean) true = wind estimate is valid and/or being used by controller (also indicates if wind est usage is disabled despite being valid) -float32 lat_accel # resultant lateral acceleration reference [m/s^2] -float32 lat_accel_ff # lateral acceleration demand only for maintaining curvature [m/s^2] -float32 bearing_feas # bearing feasibility [0,1] -float32 bearing_feas_on_track # on-track bearing feasibility [0,1] -float32 signed_track_error # signed track error [m] -float32 track_error_bound # track error bound [m] -float32 airspeed_ref # (true) airspeed reference [m/s] -float32 bearing # bearing angle [rad] -float32 heading_ref # heading angle reference [rad] -float32 min_ground_speed_ref # minimum forward ground speed reference [m/s] -float32 adapted_period # adapted period (if auto-tuning enabled) [s] -float32 p_gain # controller proportional gain [rad/s] -float32 time_const # controller time constant [s] -float32 can_run_factor # estimate of certainty of the correct functionality of the npfg roll setpoint in [0, 1] diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ObstacleDistance.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ObstacleDistance.msg deleted file mode 100644 index e3c4963ab..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ObstacleDistance.msg +++ /dev/null @@ -1,24 +0,0 @@ -# Obstacle distances in front of the sensor. -uint64 timestamp # time since system start (microseconds) - -uint8 frame #Coordinate frame of reference for the yaw rotation and offset of the sensor data. Defaults to MAV_FRAME_GLOBAL, which is North aligned. For body-mounted sensors use MAV_FRAME_BODY_FRD, which is vehicle front aligned. -uint8 MAV_FRAME_GLOBAL = 0 -uint8 MAV_FRAME_LOCAL_NED = 1 -uint8 MAV_FRAME_BODY_FRD = 12 - -uint8 sensor_type # Type from MAV_DISTANCE_SENSOR enum. -uint8 MAV_DISTANCE_SENSOR_LASER = 0 -uint8 MAV_DISTANCE_SENSOR_ULTRASOUND = 1 -uint8 MAV_DISTANCE_SENSOR_INFRARED = 2 -uint8 MAV_DISTANCE_SENSOR_RADAR = 3 - -uint16[72] distances # Distance of obstacles around the UAV with index 0 corresponding to local North. A value of 0 means that the obstacle is right in front of the sensor. A value of max_distance +1 means no obstacle is present. A value of UINT16_MAX for unknown/not used. In a array element, one unit corresponds to 1cm. - -float32 increment # Angular width in degrees of each array element. - -uint16 min_distance # Minimum distance the sensor can measure in centimeters. -uint16 max_distance # Maximum distance the sensor can measure in centimeters. - -float32 angle_offset # Relative angle offset of the 0-index element in the distances array. Value of 0 corresponds to forward. Positive values are offsets to the right. - -# TOPICS obstacle_distance obstacle_distance_fused diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/OffboardControlMode.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/OffboardControlMode.msg deleted file mode 100644 index 885164a65..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/OffboardControlMode.msg +++ /dev/null @@ -1,11 +0,0 @@ -# Off-board control mode - -uint64 timestamp # time since system start (microseconds) - -bool position -bool velocity -bool acceleration -bool attitude -bool body_rate -bool thrust_and_torque -bool direct_actuator diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/OnboardComputerStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/OnboardComputerStatus.msg deleted file mode 100644 index 736932bca..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/OnboardComputerStatus.msg +++ /dev/null @@ -1,23 +0,0 @@ -# ONBOARD_COMPUTER_STATUS message data -uint64 timestamp # [us] time since system start (microseconds) -uint32 uptime # [ms] time since system boot of the companion (milliseconds) - -uint8 type # type of onboard computer 0: Mission computer primary, 1: Mission computer backup 1, 2: Mission computer backup 2, 3: Compute node, 4-5: Compute spares, 6-9: Payload computers. - -uint8[8] cpu_cores # CPU usage on the component in percent -uint8[10] cpu_combined # Combined CPU usage as the last 10 slices of 100 MS -uint8[4] gpu_cores # GPU usage on the component in percent -uint8[10] gpu_combined # Combined GPU usage as the last 10 slices of 100 MS -int8 temperature_board # [degC] Temperature of the board -int8[8] temperature_core # [degC] Temperature of the CPU core -int16[4] fan_speed # [rpm] Fan speeds -uint32 ram_usage # [MB] Amount of used RAM on the component system -uint32 ram_total # [MB] Total amount of RAM on the component system -uint32[4] storage_type # Storage type: 0: HDD, 1: SSD, 2: EMMC, 3: SD card (non-removable), 4: SD card (removable) -uint32[4] storage_usage # [MB] Amount of used storage space on the component system -uint32[4] storage_total # [MB] Total amount of storage space on the component system -uint32[6] link_type # [Kb/s] Link type: 0-9: UART, 10-19: Wired network, 20-29: Wifi, 30-39: Point-to-point proprietary, 40-49: Mesh proprietary -uint32[6] link_tx_rate # [Kb/s] Network traffic from the component system -uint32[6] link_rx_rate # [Kb/s] Network traffic to the component system -uint32[6] link_tx_max # [Kb/s] Network capacity from the component system -uint32[6] link_rx_max # [Kb/s] Network capacity to the component system diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTest.msg deleted file mode 100644 index bbbca412f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTest.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -int32 val - -# TOPICS orb_test orb_multitest diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTestLarge.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTestLarge.msg deleted file mode 100644 index 48d6a427e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTestLarge.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -int32 val - -uint8[512] junk diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTestMedium.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTestMedium.msg deleted file mode 100644 index 43109d49d..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbTestMedium.msg +++ /dev/null @@ -1,9 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -int32 val - -uint8[64] junk - -uint8 ORB_QUEUE_LENGTH = 16 - -# TOPICS orb_test_medium orb_test_medium_multi orb_test_medium_wrap_around orb_test_medium_queue orb_test_medium_queue_poll diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbitStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbitStatus.msg deleted file mode 100644 index a04265db4..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/OrbitStatus.msg +++ /dev/null @@ -1,14 +0,0 @@ -# ORBIT_YAW_BEHAVIOUR -uint8 ORBIT_YAW_BEHAVIOUR_HOLD_FRONT_TO_CIRCLE_CENTER = 0 -uint8 ORBIT_YAW_BEHAVIOUR_HOLD_INITIAL_HEADING = 1 -uint8 ORBIT_YAW_BEHAVIOUR_UNCONTROLLED = 2 -uint8 ORBIT_YAW_BEHAVIOUR_HOLD_FRONT_TANGENT_TO_CIRCLE = 3 -uint8 ORBIT_YAW_BEHAVIOUR_RC_CONTROLLED = 4 - -uint64 timestamp # time since system start (microseconds) -float32 radius # Radius of the orbit circle. Positive values orbit clockwise, negative values orbit counter-clockwise. [m] -uint8 frame # The coordinate system of the fields: x, y, z. -float64 x # X coordinate of center point. Coordinate system depends on frame field: local = x position in meters * 1e4, global = latitude in degrees * 1e7. -float64 y # Y coordinate of center point. Coordinate system depends on frame field: local = y position in meters * 1e4, global = latitude in degrees * 1e7. -float32 z # Altitude of center point. Coordinate system depends on frame field. -uint8 yaw_behaviour diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterResetRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterResetRequest.msg deleted file mode 100644 index db08edb37..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterResetRequest.msg +++ /dev/null @@ -1,8 +0,0 @@ -# ParameterResetRequest : Used by the primary to reset one or all parameter value(s) on the remote - -uint64 timestamp -uint16 parameter_index - -bool reset_all # If this is true then ignore parameter_index - -uint8 ORB_QUEUE_LENGTH = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetUsedRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetUsedRequest.msg deleted file mode 100644 index ca97f9e9e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetUsedRequest.msg +++ /dev/null @@ -1,6 +0,0 @@ -# ParameterSetUsedRequest : Used by a remote to update the used flag for a parameter on the primary - -uint64 timestamp -uint16 parameter_index - -uint8 ORB_QUEUE_LENGTH = 64 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetValueRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetValueRequest.msg deleted file mode 100644 index eaf650f24..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetValueRequest.msg +++ /dev/null @@ -1,11 +0,0 @@ -# ParameterSetValueRequest : Used by a remote or primary to update the value for a parameter at the other end - -uint64 timestamp -uint16 parameter_index - -int32 int_value # Optional value for an integer parameter -float32 float_value # Optional value for a float parameter - -uint8 ORB_QUEUE_LENGTH = 32 - -# TOPICS parameter_set_value_request parameter_remote_set_value_request parameter_primary_set_value_request diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetValueResponse.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetValueResponse.msg deleted file mode 100644 index 09f8e3084..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterSetValueResponse.msg +++ /dev/null @@ -1,9 +0,0 @@ -# ParameterSetValueResponse : Response to a set value request by either primary or secondary - -uint64 timestamp -uint64 request_timestamp -uint16 parameter_index - -uint8 ORB_QUEUE_LENGTH = 4 - -# TOPICS parameter_set_value_response parameter_remote_set_value_response parameter_primary_set_value_response \ No newline at end of file diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterUpdate.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterUpdate.msg deleted file mode 100644 index bfb499374..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/ParameterUpdate.msg +++ /dev/null @@ -1,14 +0,0 @@ -# This message is used to notify the system about one or more parameter changes - -uint64 timestamp # time since system start (microseconds) - -uint32 instance # Instance count - constantly incrementing - -uint32 get_count -uint32 set_count -uint32 find_count -uint32 export_count - -uint16 active -uint16 changed -uint16 custom_default diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Ping.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Ping.msg deleted file mode 100644 index 498a3c73f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Ping.msg +++ /dev/null @@ -1,7 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 ping_time # Timestamp of the ping packet -uint32 ping_sequence # Sequence number of the ping packet -uint32 dropped_packets # Number of dropped ping packets -float32 rtt_ms # Round trip time (in ms) -uint8 system_id # System ID of the remote system -uint8 component_id # Component ID of the remote system diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionControllerLandingStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionControllerLandingStatus.msg deleted file mode 100644 index 249529b40..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionControllerLandingStatus.msg +++ /dev/null @@ -1,16 +0,0 @@ -uint64 timestamp # [us] time since system start -float32 lateral_touchdown_offset # [m] lateral touchdown position offset manually commanded during landing -bool flaring # true if the aircraft is flaring - -# abort status is: -# 0 if not aborted -# >0 if aborted, with the singular abort criterion which triggered the landing abort enumerated by the following abort reasons -uint8 abort_status - -# abort reasons -# after the manual operator abort, corresponds to individual bits of param FW_LND_ABORT -uint8 NOT_ABORTED = 0 -uint8 ABORTED_BY_OPERATOR = 1 -uint8 TERRAIN_NOT_FOUND = 2 # FW_LND_ABORT (1 << 0) -uint8 TERRAIN_TIMEOUT = 3 # FW_LND_ABORT (1 << 1) -uint8 UNKNOWN_ABORT_CRITERION = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionControllerStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionControllerStatus.msg deleted file mode 100644 index 7237351fd..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionControllerStatus.msg +++ /dev/null @@ -1,12 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 nav_roll # Roll setpoint [rad] -float32 nav_pitch # Pitch setpoint [rad] -float32 nav_bearing # Bearing angle[rad] -float32 target_bearing # Bearing angle from aircraft to current target [rad] -float32 xtrack_error # Signed track error [m] -float32 wp_dist # Distance to active (next) waypoint [m] -float32 acceptance_radius # Current horizontal acceptance radius [m] -float32 yaw_acceptance # Yaw acceptance error[rad] -float32 altitude_acceptance # Current vertical acceptance error [m] -uint8 type # Current (applied) position setpoint type (see PositionSetpoint.msg) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionSetpoint.msg deleted file mode 100644 index 035d35205..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionSetpoint.msg +++ /dev/null @@ -1,37 +0,0 @@ -# this file is only used in the position_setpoint triple as a dependency - -uint64 timestamp # time since system start (microseconds) - -uint8 SETPOINT_TYPE_POSITION=0 # position setpoint -uint8 SETPOINT_TYPE_VELOCITY=1 # velocity setpoint -uint8 SETPOINT_TYPE_LOITER=2 # loiter setpoint -uint8 SETPOINT_TYPE_TAKEOFF=3 # takeoff setpoint -uint8 SETPOINT_TYPE_LAND=4 # land setpoint, altitude must be ignored, descend until landing -uint8 SETPOINT_TYPE_IDLE=5 # do nothing, switch off motors or keep at idle speed (MC) - -uint8 LOITER_TYPE_ORBIT=0 # Circular pattern -uint8 LOITER_TYPE_FIGUREEIGHT=1 # Pattern resembling an 8 - -bool valid # true if setpoint is valid -uint8 type # setpoint type to adjust behavior of position controller - -float32 vx # local velocity setpoint in m/s in NED -float32 vy # local velocity setpoint in m/s in NED -float32 vz # local velocity setpoint in m/s in NED - -float64 lat # latitude, in deg -float64 lon # longitude, in deg -float32 alt # altitude AMSL, in m -float32 yaw # yaw (only in hover), in rad [-PI..PI), NaN = leave to flight task - -float32 loiter_radius # loiter major axis radius in m -float32 loiter_minor_radius # loiter minor axis radius (used for non-circular loiter shapes) in m -bool loiter_direction_counter_clockwise # loiter direction is clockwise by default and can be changed using this field -float32 loiter_orientation # Orientation of the major axis with respect to true north in rad [-pi,pi) -uint8 loiter_pattern # loitern pattern to follow - -float32 acceptance_radius # navigation acceptance_radius if we're doing waypoint navigation - -float32 cruising_speed # the generally desired cruising speed (not a hard constraint) -bool gliding_enabled # commands the vehicle to glide if the capability is available (fixed wing only) -float32 cruising_throttle # the generally desired cruising throttle (not a hard constraint), only has an effect for rover diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionSetpointTriplet.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionSetpointTriplet.msg deleted file mode 100644 index 6f9ac4d2a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/PositionSetpointTriplet.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Global position setpoint triplet in WGS84 coordinates. -# This are the three next waypoints (or just the next two or one). - -uint64 timestamp # time since system start (microseconds) - -PositionSetpoint previous -PositionSetpoint current -PositionSetpoint next diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/PowerButtonState.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/PowerButtonState.msg deleted file mode 100644 index 151806663..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/PowerButtonState.msg +++ /dev/null @@ -1,10 +0,0 @@ -# power button state notification message - -uint64 timestamp # time since system start (microseconds) - -uint8 PWR_BUTTON_STATE_IDEL = 0 # Button went up without meeting shutdown button down time (delete event) -uint8 PWR_BUTTON_STATE_DOWN = 1 # Button went Down -uint8 PWR_BUTTON_STATE_UP = 2 # Button went Up -uint8 PWR_BUTTON_STATE_REQUEST_SHUTDOWN = 3 # Button went Up after meeting shutdown button down time - -uint8 event # one of PWR_BUTTON_STATE_* diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/PowerMonitor.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/PowerMonitor.msg deleted file mode 100644 index a64585fa7..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/PowerMonitor.msg +++ /dev/null @@ -1,15 +0,0 @@ -# power monitor message - -uint64 timestamp # Time since system start (microseconds) - -float32 voltage_v # Voltage in volts, 0 if unknown -float32 current_a # Current in amperes, -1 if unknown -float32 power_w # power in watts, -1 if unknown -int16 rconf -int16 rsv -int16 rbv -int16 rp -int16 rc -int16 rcal -int16 me -int16 al diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/PpsCapture.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/PpsCapture.msg deleted file mode 100644 index c6fa2cb97..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/PpsCapture.msg +++ /dev/null @@ -1,3 +0,0 @@ -uint64 timestamp # time since system start (microseconds) at PPS capture event -uint64 rtc_timestamp # Corrected GPS UTC timestamp at PPS capture event -uint8 pps_rate_exceeded_counter # Increments when PPS dt < 50ms diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/PwmInput.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/PwmInput.msg deleted file mode 100644 index fcc7dbe4a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/PwmInput.msg +++ /dev/null @@ -1,4 +0,0 @@ -uint64 timestamp # Time since system start (microseconds) -uint64 error_count # Timer overcapture error flag (AUX5 or MAIN5) -uint32 pulse_width # Pulse width, timer counts -uint32 period # Period, timer counts diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Px4ioStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Px4ioStatus.msg deleted file mode 100644 index 295c8fba6..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Px4ioStatus.msg +++ /dev/null @@ -1,43 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint16 free_memory_bytes - -float32 voltage_v # Servo rail voltage in volts -float32 rssi_v # RSSI pin voltage in volts - -# PX4IO status flags (PX4IO_P_STATUS_FLAGS) -bool status_arm_sync -bool status_failsafe -bool status_fmu_initialized -bool status_fmu_ok -bool status_init_ok -bool status_outputs_armed -bool status_raw_pwm -bool status_rc_ok -bool status_rc_dsm -bool status_rc_ppm -bool status_rc_sbus -bool status_rc_st24 -bool status_rc_sumd -bool status_safety_button_event # px4io safety button was pressed for longer than 1 second - -# PX4IO alarms (PX4IO_P_STATUS_ALARMS) -bool alarm_pwm_error -bool alarm_rc_lost - -# PX4IO arming (PX4IO_P_SETUP_ARMING) -bool arming_failsafe_custom -bool arming_fmu_armed -bool arming_fmu_prearmed -bool arming_force_failsafe -bool arming_io_arm_ok -bool arming_lockdown -bool arming_termination_failsafe - -uint16[8] pwm -uint16[8] pwm_disarmed -uint16[8] pwm_failsafe - -uint16[8] pwm_rate_hz - -uint16[18] raw_inputs diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/QshellReq.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/QshellReq.msg deleted file mode 100644 index d472e8dd3..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/QshellReq.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -char[100] cmd -uint32 MAX_STRLEN = 100 -uint32 strlen -uint32 request_sequence diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/QshellRetval.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/QshellRetval.msg deleted file mode 100644 index d42a7715f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/QshellRetval.msg +++ /dev/null @@ -1,3 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -int32 return_value -uint32 return_sequence diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RadioStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RadioStatus.msg deleted file mode 100644 index c57c82b5e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RadioStatus.msg +++ /dev/null @@ -1,13 +0,0 @@ - -uint64 timestamp # time since system start (microseconds) - -uint8 rssi # local signal strength -uint8 remote_rssi # remote signal strength - -uint8 txbuf # how full the tx buffer is as a percentage -uint8 noise # background noise level - -uint8 remote_noise # remote background noise level -uint16 rxerrors # receive errors - -uint16 fix # count of error corrected packets diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RateCtrlStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RateCtrlStatus.msg deleted file mode 100644 index 3f5644699..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RateCtrlStatus.msg +++ /dev/null @@ -1,7 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -# rate controller integrator status -float32 rollspeed_integ -float32 pitchspeed_integ -float32 yawspeed_integ -float32 wheel_rate_integ # FW only and optional diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RcChannels.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RcChannels.msg deleted file mode 100644 index 546755f66..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RcChannels.msg +++ /dev/null @@ -1,40 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 FUNCTION_THROTTLE = 0 -uint8 FUNCTION_ROLL = 1 -uint8 FUNCTION_PITCH = 2 -uint8 FUNCTION_YAW = 3 -uint8 FUNCTION_RETURN = 4 -uint8 FUNCTION_LOITER = 5 -uint8 FUNCTION_OFFBOARD = 6 -uint8 FUNCTION_FLAPS = 7 -uint8 FUNCTION_AUX_1 = 8 -uint8 FUNCTION_AUX_2 = 9 -uint8 FUNCTION_AUX_3 = 10 -uint8 FUNCTION_AUX_4 = 11 -uint8 FUNCTION_AUX_5 = 12 -uint8 FUNCTION_AUX_6 = 13 -uint8 FUNCTION_PARAM_1 = 14 -uint8 FUNCTION_PARAM_2 = 15 -uint8 FUNCTION_PARAM_3_5 = 16 -uint8 FUNCTION_KILLSWITCH = 17 -uint8 FUNCTION_TRANSITION = 18 -uint8 FUNCTION_GEAR = 19 -uint8 FUNCTION_ARMSWITCH = 20 -uint8 FUNCTION_FLTBTN_SLOT_1 = 21 -uint8 FUNCTION_FLTBTN_SLOT_2 = 22 -uint8 FUNCTION_FLTBTN_SLOT_3 = 23 -uint8 FUNCTION_FLTBTN_SLOT_4 = 24 -uint8 FUNCTION_FLTBTN_SLOT_5 = 25 -uint8 FUNCTION_FLTBTN_SLOT_6 = 26 -uint8 FUNCTION_ENGAGE_MAIN_MOTOR = 27 - -uint8 FUNCTION_FLTBTN_SLOT_COUNT = 6 - -uint64 timestamp_last_valid # Timestamp of last valid RC signal -float32[18] channels # Scaled to -1..1 (throttle: 0..1) -uint8 channel_count # Number of valid channels -int8[28] function # Functions mapping -uint8 rssi # Receive signal strength index -bool signal_lost # Control signal lost, should be checked together with topic timeout -uint32 frame_drop_count # Number of dropped frames diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RcParameterMap.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RcParameterMap.msg deleted file mode 100644 index 2a5df3e74..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RcParameterMap.msg +++ /dev/null @@ -1,11 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 RC_PARAM_MAP_NCHAN = 3 # This limit is also hardcoded in the enum RC_CHANNELS_FUNCTION in rc_channels.h -uint8 PARAM_ID_LEN = 16 # corresponds to MAVLINK_MSG_PARAM_VALUE_FIELD_PARAM_ID_LEN - -bool[3] valid #true for RC-Param channels which are mapped to a param -int32[3] param_index # corresponding param index, this field is ignored if set to -1, in this case param_id will be used -char[51] param_id # MAP_NCHAN * (ID_LEN + 1) chars, corresponding param id, null terminated -float32[3] scale # scale to map the RC input [-1, 1] to a parameter value -float32[3] value0 # initial value around which the parameter value is changed -float32[3] value_min # minimal parameter value -float32[3] value_max # minimal parameter value diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RegisterExtComponentReply.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RegisterExtComponentReply.msg deleted file mode 100644 index 7cd7eef07..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RegisterExtComponentReply.msg +++ /dev/null @@ -1,13 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint64 request_id # ID from the request -char[25] name # name from the request - -uint16 px4_ros2_api_version - -bool success -int8 arming_check_id # arming check registration ID (-1 if invalid) -int8 mode_id # assigned mode ID (-1 if invalid) -int8 mode_executor_id # assigned mode executor ID (-1 if invalid) - -uint8 ORB_QUEUE_LENGTH = 2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RegisterExtComponentRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RegisterExtComponentRequest.msg deleted file mode 100644 index 46ab0cb0a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RegisterExtComponentRequest.msg +++ /dev/null @@ -1,21 +0,0 @@ -# Request to register an external component -uint64 timestamp # time since system start (microseconds) - -uint64 request_id # ID, set this to a random value -char[25] name # either the requested mode name, or component name - -uint16 LATEST_PX4_ROS2_API_VERSION = 1 # API version compatibility. Increase this on a breaking semantic change. Changes to any message field are detected separately and do not require an API version change. - -uint16 px4_ros2_api_version # Set to LATEST_PX4_ROS2_API_VERSION - -# Components to be registered -bool register_arming_check -bool register_mode # registering a mode also requires arming_check to be set -bool register_mode_executor # registering an executor also requires a mode to be registered (which is the owned mode by the executor) - -bool enable_replace_internal_mode # set to true if an internal mode should be replaced -uint8 replace_internal_mode # vehicle_status::NAVIGATION_STATE_* -bool activate_mode_immediately # switch to the registered mode (can only be set in combination with an executor) - - -uint8 ORB_QUEUE_LENGTH = 2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RoverAckermannGuidanceStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RoverAckermannGuidanceStatus.msg deleted file mode 100644 index 3c34b63c6..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RoverAckermannGuidanceStatus.msg +++ /dev/null @@ -1,10 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 actual_speed # [m/s] Rover ground speed -float32 desired_speed # [m/s] Rover desired ground speed -float32 lookahead_distance # [m] Lookahead distance of pure the pursuit controller -float32 heading_error # [deg] Heading error of the pure pursuit controller -float32 pid_throttle_integral # [-1, 1] Integral of the PID for the normalized throttle to control the rover speed during missions -float32 crosstrack_error # [m] Shortest distance from the vehicle to the path - -# TOPICS rover_ackermann_guidance_status diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Rpm.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Rpm.msg deleted file mode 100644 index baab7c6a6..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Rpm.msg +++ /dev/null @@ -1,4 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 indicated_frequency_rpm # indicated rotor Frequency in Revolution per minute -float32 estimated_accurancy_rpm # estimated accuracy in Revolution per minute diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RtlStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RtlStatus.msg deleted file mode 100644 index f25b22243..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RtlStatus.msg +++ /dev/null @@ -1,15 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 safe_points_id # unique ID of active set of safe_point_items -bool is_evaluation_pending # flag if the RTL point needs reevaluation (e.g. new safe points available, but need loading). - -bool has_vtol_approach # flag if approaches are defined for current RTL_TYPE parameter setting - -uint8 rtl_type # Type of RTL chosen -uint8 safe_point_index # index of the chosen safe point, if in RTL_STATUS_TYPE_DIRECT_SAFE_POINT mode - -uint8 RTL_STATUS_TYPE_NONE=0 # pending if evaluation can't pe performed currently e.g. when it is still loading the safe points -uint8 RTL_STATUS_TYPE_DIRECT_SAFE_POINT=1 # chosen to directly go to a safe point or home position -uint8 RTL_STATUS_TYPE_DIRECT_MISSION_LAND=2 # going straight to the beginning of the mission landing -uint8 RTL_STATUS_TYPE_FOLLOW_MISSION=3 # Following the mission from start index to mission landing. Start index is current WP if in Mission mode, and closest WP otherwise. -uint8 RTL_STATUS_TYPE_FOLLOW_MISSION_REVERSE=4 # Following the mission in reverse from start index to the beginning of the mission. Start index is previous WP if in Mission mode, and closest WP otherwise. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/RtlTimeEstimate.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/RtlTimeEstimate.msg deleted file mode 100644 index ee46888dd..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/RtlTimeEstimate.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -bool valid # Flag indicating whether the time estiamtes are valid -float32 time_estimate # [s] Estimated time for RTL -float32 safe_time_estimate # [s] Same as time_estimate, but with safety factor and safety margin included (factor*t + margin) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SatelliteInfo.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SatelliteInfo.msg deleted file mode 100644 index 2980ffcbd..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SatelliteInfo.msg +++ /dev/null @@ -1,10 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint8 SAT_INFO_MAX_SATELLITES = 20 - -uint8 count # Number of satellites visible to the receiver -uint8[20] svid # Space vehicle ID [1..255], see scheme below -uint8[20] used # 0: Satellite not used, 1: used for navigation -uint8[20] elevation # Elevation (0: right on top of receiver, 90: on the horizon) of satellite -uint8[20] azimuth # Direction of satellite, 0: 0 deg, 255: 360 deg. -uint8[20] snr # dBHz, Signal to noise ratio of satellite C/N0, range 0..99, zero when not tracking this satellite. -uint8[20] prn # Satellite PRN code assignment, (psuedorandom number SBAS, valid codes are 120-144) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAccel.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAccel.msg deleted file mode 100644 index e47d813a7..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAccel.msg +++ /dev/null @@ -1,18 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 x # acceleration in the FRD board frame X-axis in m/s^2 -float32 y # acceleration in the FRD board frame Y-axis in m/s^2 -float32 z # acceleration in the FRD board frame Z-axis in m/s^2 - -float32 temperature # temperature in degrees Celsius - -uint32 error_count - -uint8[3] clip_counter # clip count per axis in the sample period - -uint8 samples # number of raw samples that went into this message - -uint8 ORB_QUEUE_LENGTH = 8 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAccelFifo.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAccelFifo.msg deleted file mode 100644 index 1eae5dd14..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAccelFifo.msg +++ /dev/null @@ -1,13 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 dt # delta time between samples (microseconds) -float32 scale - -uint8 samples # number of valid samples - -int16[32] x # acceleration in the FRD board frame X-axis in m/s^2 -int16[32] y # acceleration in the FRD board frame Y-axis in m/s^2 -int16[32] z # acceleration in the FRD board frame Z-axis in m/s^2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAirflow.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAirflow.msg deleted file mode 100644 index dd55ad0b0..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorAirflow.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint32 device_id # unique device ID for the sensor that does not change between power cycles -float32 speed # the speed being reported by the wind / airflow sensor -float32 direction # the direction bein report by the wind / airflow sensor -uint8 status # Status code from the sensor diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorBaro.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorBaro.msg deleted file mode 100644 index 7c4154e15..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorBaro.msg +++ /dev/null @@ -1,12 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 pressure # static pressure measurement in Pascals - -float32 temperature # temperature in degrees Celsius - -uint32 error_count - -uint8 ORB_QUEUE_LENGTH = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorCombined.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorCombined.msg deleted file mode 100644 index 837c7a1fa..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorCombined.msg +++ /dev/null @@ -1,25 +0,0 @@ -# Sensor readings in SI-unit form. -# These fields are scaled and offset-compensated where possible and do not -# change with board revisions and sensor updates. - -uint64 timestamp # time since system start (microseconds) - -int32 RELATIVE_TIMESTAMP_INVALID = 2147483647 # (0x7fffffff) If one of the relative timestamps is set to this value, it means the associated sensor values are invalid - -# gyro timstamp is equal to the timestamp of the message -float32[3] gyro_rad # average angular rate measured in the FRD body frame XYZ-axis in rad/s over the last gyro sampling period -uint32 gyro_integral_dt # gyro measurement sampling period in microseconds - -int32 accelerometer_timestamp_relative # timestamp + accelerometer_timestamp_relative = Accelerometer timestamp -float32[3] accelerometer_m_s2 # average value acceleration measured in the FRD body frame XYZ-axis in m/s^2 over the last accelerometer sampling period -uint32 accelerometer_integral_dt # accelerometer measurement sampling period in microseconds - -uint8 CLIPPING_X = 1 -uint8 CLIPPING_Y = 2 -uint8 CLIPPING_Z = 4 - -uint8 accelerometer_clipping # bitfield indicating if there was any accelerometer clipping (per axis) during the integration time frame -uint8 gyro_clipping # bitfield indicating if there was any gyro clipping (per axis) during the integration time frame - -uint8 accel_calibration_count # Calibration changed counter. Monotonically increases whenever accelermeter calibration changes. -uint8 gyro_calibration_count # Calibration changed counter. Monotonically increases whenever rate gyro calibration changes. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorCorrection.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorCorrection.msg deleted file mode 100644 index bfbc8e2eb..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorCorrection.msg +++ /dev/null @@ -1,41 +0,0 @@ -# -# Sensor corrections in SI-unit form for the voted sensor -# - -uint64 timestamp # time since system start (microseconds) - -# Corrections for acceleromter acceleration outputs where corrected_accel = raw_accel * accel_scale + accel_offset -# Note the corrections are in the sensor frame and must be applied before the sensor data is rotated into body frame -uint32[4] accel_device_ids -float32[4] accel_temperature -float32[3] accel_offset_0 # accelerometer 0 offsets in the FRD board frame XYZ-axis in m/s^s -float32[3] accel_offset_1 # accelerometer 1 offsets in the FRD board frame XYZ-axis in m/s^s -float32[3] accel_offset_2 # accelerometer 2 offsets in the FRD board frame XYZ-axis in m/s^s -float32[3] accel_offset_3 # accelerometer 3 offsets in the FRD board frame XYZ-axis in m/s^s - -# Corrections for gyro angular rate outputs where corrected_rate = raw_rate * gyro_scale + gyro_offset -# Note the corrections are in the sensor frame and must be applied before the sensor data is rotated into body frame -uint32[4] gyro_device_ids -float32[4] gyro_temperature -float32[3] gyro_offset_0 # gyro 0 XYZ offsets in the sensor frame in rad/s -float32[3] gyro_offset_1 # gyro 1 XYZ offsets in the sensor frame in rad/s -float32[3] gyro_offset_2 # gyro 2 XYZ offsets in the sensor frame in rad/s -float32[3] gyro_offset_3 # gyro 3 XYZ offsets in the sensor frame in rad/s - -# Corrections for magnetometer measurement outputs where corrected_mag = raw_mag * mag_scale + mag_offset -# Note the corrections are in the sensor frame and must be applied before the sensor data is rotated into body frame -uint32[4] mag_device_ids -float32[4] mag_temperature -float32[3] mag_offset_0 # magnetometer 0 offsets in the FRD board frame XYZ-axis in m/s^s -float32[3] mag_offset_1 # magnetometer 1 offsets in the FRD board frame XYZ-axis in m/s^s -float32[3] mag_offset_2 # magnetometer 2 offsets in the FRD board frame XYZ-axis in m/s^s -float32[3] mag_offset_3 # magnetometer 3 offsets in the FRD board frame XYZ-axis in m/s^s - -# Corrections for barometric pressure outputs where corrected_pressure = raw_pressure * pressure_scale + pressure_offset -# Note the corrections are in the sensor frame and must be applied before the sensor data is rotated into body frame -uint32[4] baro_device_ids -float32[4] baro_temperature -float32 baro_offset_0 # barometric pressure 0 offsets in the sensor frame in Pascals -float32 baro_offset_1 # barometric pressure 1 offsets in the sensor frame in Pascals -float32 baro_offset_2 # barometric pressure 2 offsets in the sensor frame in Pascals -float32 baro_offset_3 # barometric pressure 3 offsets in the sensor frame in Pascals diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGnssRelative.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGnssRelative.msg deleted file mode 100644 index 6d87a344c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGnssRelative.msg +++ /dev/null @@ -1,30 +0,0 @@ -# GNSS relative positioning information in NED frame. The NED frame is defined as the local topological system at the reference station. - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # time since system start (microseconds) - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -uint64 time_utc_usec # Timestamp (microseconds, UTC), this is the timestamp which comes from the gps module. It might be unavailable right after cold start, indicated by a value of 0 - -uint16 reference_station_id # Reference Station ID - -float32[3] position # GPS NED relative position vector (m) -float32[3] position_accuracy # Accuracy of relative position (m) - -float32 heading # Heading of the relative position vector (radians) -float32 heading_accuracy # Accuracy of heading of the relative position vector (radians) - -float32 position_length # Length of the position vector (m) -float32 accuracy_length # Accuracy of the position length (m) - -bool gnss_fix_ok # GNSS valid fix (i.e within DOP & accuracy masks) -bool differential_solution # differential corrections were applied -bool relative_position_valid -bool carrier_solution_floating # carrier phase range solution with floating ambiguities -bool carrier_solution_fixed # carrier phase range solution with fixed ambiguities -bool moving_base_mode # if the receiver is operating in moving base mode -bool reference_position_miss # extrapolated reference position was used to compute moving base solution this epoch -bool reference_observations_miss # extrapolated reference observations were used to compute moving base solution this epoch -bool heading_valid -bool relative_position_normalized # the components of the relative position vector (including the high-precision parts) are normalized diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGps.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGps.msg deleted file mode 100644 index ce2bfad4f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGps.msg +++ /dev/null @@ -1,72 +0,0 @@ -# GPS position in WGS84 coordinates. -# the field 'timestamp' is for the position & velocity (microseconds) -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float64 latitude_deg # Latitude in degrees, allows centimeter level RTK precision -float64 longitude_deg # Longitude in degrees, allows centimeter level RTK precision -float64 altitude_msl_m # Altitude above MSL, meters -float64 altitude_ellipsoid_m # Altitude above Ellipsoid, meters - -float32 s_variance_m_s # GPS speed accuracy estimate, (metres/sec) -float32 c_variance_rad # GPS course accuracy estimate, (radians) -uint8 FIX_TYPE_NONE = 1 # Value 0 is also valid to represent no fix. -uint8 FIX_TYPE_2D = 2 -uint8 FIX_TYPE_3D = 3 -uint8 FIX_TYPE_RTCM_CODE_DIFFERENTIAL = 4 -uint8 FIX_TYPE_RTK_FLOAT = 5 -uint8 FIX_TYPE_RTK_FIXED = 6 -uint8 FIX_TYPE_EXTRAPOLATED = 8 -uint8 fix_type # Some applications will not use the value of this field unless it is at least two, so always correctly fill in the fix. - -float32 eph # GPS horizontal position accuracy (metres) -float32 epv # GPS vertical position accuracy (metres) - -float32 hdop # Horizontal dilution of precision -float32 vdop # Vertical dilution of precision - -int32 noise_per_ms # GPS noise per millisecond -uint16 automatic_gain_control # Automatic gain control monitor - -uint8 JAMMING_STATE_UNKNOWN = 0 -uint8 JAMMING_STATE_OK = 1 -uint8 JAMMING_STATE_WARNING = 2 -uint8 JAMMING_STATE_CRITICAL = 3 -uint8 jamming_state # indicates whether jamming has been detected or suspected by the receivers. O: Unknown, 1: OK, 2: Warning, 3: Critical -int32 jamming_indicator # indicates jamming is occurring - -uint8 SPOOFING_STATE_UNKNOWN = 0 -uint8 SPOOFING_STATE_NONE = 1 -uint8 SPOOFING_STATE_INDICATED = 2 -uint8 SPOOFING_STATE_MULTIPLE = 3 -uint8 spoofing_state # indicates whether spoofing has been detected or suspected by the receivers. O: Unknown, 1: OK, 2: Warning, 3: Critical - -float32 vel_m_s # GPS ground speed, (metres/sec) -float32 vel_n_m_s # GPS North velocity, (metres/sec) -float32 vel_e_m_s # GPS East velocity, (metres/sec) -float32 vel_d_m_s # GPS Down velocity, (metres/sec) -float32 cog_rad # Course over ground (NOT heading, but direction of movement), -PI..PI, (radians) -bool vel_ned_valid # True if NED velocity is valid - -int32 timestamp_time_relative # timestamp + timestamp_time_relative = Time of the UTC timestamp since system start, (microseconds) -uint64 time_utc_usec # Timestamp (microseconds, UTC), this is the timestamp which comes from the gps module. It might be unavailable right after cold start, indicated by a value of 0 - -uint8 satellites_used # Number of satellites used - -float32 heading # heading angle of XYZ body frame rel to NED. Set to NaN if not available and updated (used for dual antenna GPS), (rad, [-PI, PI]) -float32 heading_offset # heading offset of dual antenna array in body frame. Set to NaN if not applicable. (rad, [-PI, PI]) -float32 heading_accuracy # heading accuracy (rad, [0, 2PI]) - -float32 rtcm_injection_rate # RTCM message injection rate Hz -uint8 selected_rtcm_instance # uorb instance that is being used for RTCM corrections - -bool rtcm_crc_failed # RTCM message CRC failure detected - -uint8 RTCM_MSG_USED_UNKNOWN = 0 -uint8 RTCM_MSG_USED_NOT_USED = 1 -uint8 RTCM_MSG_USED_USED = 2 -uint8 rtcm_msg_used # Indicates if the RTCM message was used successfully by the receiver - -# TOPICS sensor_gps vehicle_gps_position diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyro.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyro.msg deleted file mode 100644 index b906127b5..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyro.msg +++ /dev/null @@ -1,18 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 x # angular velocity in the FRD board frame X-axis in rad/s -float32 y # angular velocity in the FRD board frame Y-axis in rad/s -float32 z # angular velocity in the FRD board frame Z-axis in rad/s - -float32 temperature # temperature in degrees Celsius - -uint32 error_count - -uint8[3] clip_counter # clip count per axis in the sample period - -uint8 samples # number of raw samples that went into this message - -uint8 ORB_QUEUE_LENGTH = 8 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyroFft.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyroFft.msg deleted file mode 100644 index ed84d0a0d..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyroFft.msg +++ /dev/null @@ -1,15 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 sensor_sample_rate_hz -float32 resolution_hz - -float32[3] peak_frequencies_x # x axis peak frequencies -float32[3] peak_frequencies_y # y axis peak frequencies -float32[3] peak_frequencies_z # z axis peak frequencies - -float32[3] peak_snr_x # x axis peak SNR -float32[3] peak_snr_y # y axis peak SNR -float32[3] peak_snr_z # z axis peak SNR diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyroFifo.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyroFifo.msg deleted file mode 100644 index 2e77ef07e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorGyroFifo.msg +++ /dev/null @@ -1,15 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 dt # delta time between samples (microseconds) -float32 scale - -uint8 samples # number of valid samples - -int16[32] x # angular velocity in the FRD board frame X-axis in rad/s -int16[32] y # angular velocity in the FRD board frame Y-axis in rad/s -int16[32] z # angular velocity in the FRD board frame Z-axis in rad/s - -uint8 ORB_QUEUE_LENGTH = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorHygrometer.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorHygrometer.msg deleted file mode 100644 index 490a7402a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorHygrometer.msg +++ /dev/null @@ -1,8 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 temperature # Temperature provided by sensor (Celsius) - -float32 humidity # Humidity provided by sensor diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorMag.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorMag.msg deleted file mode 100644 index 1b5ba487e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorMag.msg +++ /dev/null @@ -1,14 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32 x # magnetic field in the FRD board frame X-axis in Gauss -float32 y # magnetic field in the FRD board frame Y-axis in Gauss -float32 z # magnetic field in the FRD board frame Z-axis in Gauss - -float32 temperature # temperature in degrees Celsius - -uint32 error_count - -uint8 ORB_QUEUE_LENGTH = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorOpticalFlow.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorOpticalFlow.msg deleted file mode 100644 index ce7e8bf08..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorOpticalFlow.msg +++ /dev/null @@ -1,30 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32[2] pixel_flow # (radians) optical flow in radians where a positive value is produced by a RH rotation about the body axis - -float32[3] delta_angle # (radians) accumulated gyro radians where a positive value is produced by a RH rotation about the body axis. Set to NaN if flow sensor does not have 3-axis gyro data. -bool delta_angle_available - -float32 distance_m # (meters) Distance to the center of the flow field -bool distance_available - -uint32 integration_timespan_us # (microseconds) accumulation timespan in microseconds - -uint8 quality # quality, 0: bad quality, 255: maximum quality - -uint32 error_count - -float32 max_flow_rate # (radians/s) Magnitude of maximum angular which the optical flow sensor can measure reliably - -float32 min_ground_distance # (meters) Minimum distance from ground at which the optical flow sensor operates reliably -float32 max_ground_distance # (meters) Maximum distance from ground at which the optical flow sensor operates reliably - -uint8 MODE_UNKNOWN = 0 -uint8 MODE_BRIGHT = 1 -uint8 MODE_LOWLIGHT = 2 -uint8 MODE_SUPER_LOWLIGHT = 3 - -uint8 mode diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorPreflightMag.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorPreflightMag.msg deleted file mode 100644 index 2b5333c4b..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorPreflightMag.msg +++ /dev/null @@ -1,7 +0,0 @@ -# -# Pre-flight sensor check metrics. -# The topic will not be updated when the vehicle is armed -# -uint64 timestamp # time since system start (microseconds) - -float32 mag_inconsistency_angle # maximum angle between magnetometer instance field vectors in radians. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorSelection.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorSelection.msg deleted file mode 100644 index 799ccf18a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorSelection.msg +++ /dev/null @@ -1,7 +0,0 @@ -# -# Sensor ID's for the voted sensors output on the sensor_combined topic. -# Will be updated on startup of the sensor module and when sensor selection changes -# -uint64 timestamp # time since system start (microseconds) -uint32 accel_device_id # unique device ID for the selected accelerometers -uint32 gyro_device_id # unique device ID for the selected rate gyros diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorUwb.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorUwb.msg deleted file mode 100644 index ae889a8bd..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorUwb.msg +++ /dev/null @@ -1,34 +0,0 @@ -# UWB distance contains the distance information measured by an ultra-wideband positioning system, -# such as Pozyx or NXP Rddrone. - -uint64 timestamp # time since system start (microseconds) - -uint32 sessionid # UWB SessionID -uint32 time_offset # Time between Ranging Rounds in ms -uint32 counter # Number of Ranges since last Start of Ranging -uint16 mac # MAC adress of Initiator (controller) - -uint16 mac_dest # MAC adress of Responder (Controlee) -uint16 status # status feedback # -uint8 nlos # None line of site condition y/n -float32 distance # distance in m to the UWB receiver - - -#Angle of arrival, Angle in Degree -60..+60; FOV in both axis is 120 degrees -float32 aoa_azimuth_dev # Angle of arrival of first incomming RX msg -float32 aoa_elevation_dev # Angle of arrival of first incomming RX msg -float32 aoa_azimuth_resp # Angle of arrival of first incomming RX msg at the responder -float32 aoa_elevation_resp # Angle of arrival of first incomming RX msg at the responder - -# Figure of merit for the angle measurements -uint8 aoa_azimuth_fom # AOA Azimuth FOM -uint8 aoa_elevation_fom # AOA Elevation FOM -uint8 aoa_dest_azimuth_fom # AOA Azimuth FOM -uint8 aoa_dest_elevation_fom # AOA Elevation FOM - -# Initiator physical configuration -uint8 orientation # Direction the sensor faces from MAV_SENSOR_ORIENTATION enum - # Standard configuration is Antennas facing down and azimuth aligened in forward direction -float32 offset_x # UWB initiator offset in X axis (NED drone frame) -float32 offset_y # UWB initiator offset in Y axis (NED drone frame) -float32 offset_z # UWB initiator offset in Z axis (NED drone frame) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorsStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorsStatus.msg deleted file mode 100644 index c16bf1c6a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorsStatus.msg +++ /dev/null @@ -1,15 +0,0 @@ -# -# Sensor check metrics. This will be zero for a sensor that's primary or unpopulated. -# -uint64 timestamp # time since system start (microseconds) - -uint32 device_id_primary # current primary device id for reference - -uint32[4] device_ids -float32[4] inconsistency # magnitude of difference between sensor instance and mean -bool[4] healthy # sensor healthy -uint8[4] priority -bool[4] enabled -bool[4] external - -# TOPICS sensors_status_baro sensors_status_mag diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorsStatusImu.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorsStatusImu.msg deleted file mode 100644 index cfad3419c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SensorsStatusImu.msg +++ /dev/null @@ -1,18 +0,0 @@ -# -# Sensor check metrics. This will be zero for a sensor that's primary or unpopulated. -# -uint64 timestamp # time since system start (microseconds) - -uint32 accel_device_id_primary # current primary accel device id for reference - -uint32[4] accel_device_ids -float32[4] accel_inconsistency_m_s_s # magnitude of acceleration difference between IMU instance and mean in m/s^2. -bool[4] accel_healthy -uint8[4] accel_priority - -uint32 gyro_device_id_primary # current primary gyro device id for reference - -uint32[4] gyro_device_ids -float32[4] gyro_inconsistency_rad_s # magnitude of angular rate difference between IMU instance and mean in (rad/s). -bool[4] gyro_healthy -uint8[4] gyro_priority diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/SystemPower.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/SystemPower.msg deleted file mode 100644 index 21b35ba1d..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/SystemPower.msg +++ /dev/null @@ -1,21 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -float32 voltage5v_v # peripheral 5V rail voltage -float32[4] sensors3v3 # Sensors 3V3 rail voltage -uint8 sensors3v3_valid # Sensors 3V3 rail voltage was read (bitfield). -uint8 usb_connected # USB is connected when 1 -uint8 brick_valid # brick bits power is good when bit 1 -uint8 usb_valid # USB is valid when 1 -uint8 servo_valid # servo power is good when 1 -uint8 periph_5v_oc # peripheral overcurrent when 1 -uint8 hipower_5v_oc # high power peripheral overcurrent when 1 -uint8 comp_5v_valid # 5V to companion valid -uint8 can1_gps1_5v_valid # 5V for CAN1/GPS1 valid - -uint8 BRICK1_VALID_SHIFTS=0 -uint8 BRICK1_VALID_MASK=1 -uint8 BRICK2_VALID_SHIFTS=1 -uint8 BRICK2_VALID_MASK=2 -uint8 BRICK3_VALID_SHIFTS=2 -uint8 BRICK3_VALID_MASK=4 -uint8 BRICK4_VALID_SHIFTS=3 -uint8 BRICK4_VALID_MASK=8 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TakeoffStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TakeoffStatus.msg deleted file mode 100644 index 4cc49d509..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TakeoffStatus.msg +++ /dev/null @@ -1,14 +0,0 @@ -# Status of the takeoff state machine currently just available for multicopters - -uint64 timestamp # time since system start (microseconds) - -uint8 TAKEOFF_STATE_UNINITIALIZED = 0 -uint8 TAKEOFF_STATE_DISARMED = 1 -uint8 TAKEOFF_STATE_SPOOLUP = 2 -uint8 TAKEOFF_STATE_READY_FOR_TAKEOFF = 3 -uint8 TAKEOFF_STATE_RAMPUP = 4 -uint8 TAKEOFF_STATE_FLIGHT = 5 - -uint8 takeoff_state - -float32 tilt_limit # limited tilt feasibility during takeoff, contains maximum tilt otherwise diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TaskStackInfo.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TaskStackInfo.msg deleted file mode 100644 index bb69bf1a8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TaskStackInfo.msg +++ /dev/null @@ -1,8 +0,0 @@ -# stack information for a single running process - -uint64 timestamp # time since system start (microseconds) - -uint16 stack_free -char[24] task_name - -uint8 ORB_QUEUE_LENGTH = 2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TecsStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TecsStatus.msg deleted file mode 100644 index ae6835dc5..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TecsStatus.msg +++ /dev/null @@ -1,29 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 altitude_sp # Altitude setpoint AMSL [m] -float32 altitude_reference # Altitude setpoint reference AMSL [m] -float32 height_rate_reference # Height rate setpoint reference [m/s] -float32 height_rate_direct # Direct height rate setpoint from velocity reference generator [m/s] -float32 height_rate_setpoint # Height rate setpoint [m/s] -float32 height_rate # Height rate [m/s] -float32 equivalent_airspeed_sp # Equivalent airspeed setpoint [m/s] -float32 true_airspeed_sp # True airspeed setpoint [m/s] -float32 true_airspeed_filtered # True airspeed filtered [m/s] -float32 true_airspeed_derivative_sp # True airspeed derivative setpoint [m/s^2] -float32 true_airspeed_derivative # True airspeed derivative [m/s^2] -float32 true_airspeed_derivative_raw # True airspeed derivative raw [m/s^2] - -float32 total_energy_rate_sp # Total energy rate setpoint [m^2/s^3] -float32 total_energy_rate # Total energy rate estimate [m^2/s^3] - -float32 total_energy_balance_rate_sp # Energy balance rate setpoint [m^2/s^3] -float32 total_energy_balance_rate # Energy balance rate estimate [m^2/s^3] - -float32 throttle_integ # Throttle integrator value [-] -float32 pitch_integ # Pitch integrator value [rad] - -float32 throttle_sp # Current throttle setpoint [-] -float32 pitch_sp_rad # Current pitch setpoint [rad] -float32 throttle_trim # estimated throttle value [0,1] required to fly level at equivalent_airspeed_sp in the current atmospheric conditions - -float32 underspeed_ratio # 0: no underspeed, 1: maximal underspeed. Controller takes measures to avoid stall proportional to ratio if >0. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TelemetryStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TelemetryStatus.msg deleted file mode 100644 index 48d85f934..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TelemetryStatus.msg +++ /dev/null @@ -1,63 +0,0 @@ -uint8 LINK_TYPE_GENERIC = 0 -uint8 LINK_TYPE_UBIQUITY_BULLET = 1 -uint8 LINK_TYPE_WIRE = 2 -uint8 LINK_TYPE_USB = 3 -uint8 LINK_TYPE_IRIDIUM = 4 - -uint64 timestamp # time since system start (microseconds) - -uint8 type # type of the radio hardware (LINK_TYPE_*) - -uint8 mode - -bool flow_control -bool forwarding -bool mavlink_v2 -bool ftp - -uint8 streams - -float32 data_rate # configured maximum data rate (Bytes/s) - -float32 rate_multiplier - -float32 tx_rate_avg # transmit rate average (Bytes/s) -float32 tx_error_rate_avg # transmit error rate average (Bytes/s) -uint32 tx_message_count # total message sent count -uint32 tx_buffer_overruns # number of TX buffer overruns - -float32 rx_rate_avg # transmit rate average (Bytes/s) -uint32 rx_message_count # count of total messages received -uint32 rx_message_lost_count -uint32 rx_buffer_overruns # number of RX buffer overruns -uint32 rx_parse_errors # number of parse errors -uint32 rx_packet_drop_count # number of packet drops -float32 rx_message_lost_rate - - -uint64 HEARTBEAT_TIMEOUT_US = 2500000 # Heartbeat timeout (tolerate missing 1 + jitter) - -# Heartbeats per type -bool heartbeat_type_antenna_tracker # MAV_TYPE_ANTENNA_TRACKER -bool heartbeat_type_gcs # MAV_TYPE_GCS -bool heartbeat_type_onboard_controller # MAV_TYPE_ONBOARD_CONTROLLER -bool heartbeat_type_gimbal # MAV_TYPE_GIMBAL -bool heartbeat_type_adsb # MAV_TYPE_ADSB -bool heartbeat_type_camera # MAV_TYPE_CAMERA -bool heartbeat_type_parachute # MAV_TYPE_PARACHUTE -bool heartbeat_type_open_drone_id # MAV_TYPE_ODID - -# Heartbeats per component -bool heartbeat_component_telemetry_radio # MAV_COMP_ID_TELEMETRY_RADIO -bool heartbeat_component_log # MAV_COMP_ID_LOG -bool heartbeat_component_osd # MAV_COMP_ID_OSD -bool heartbeat_component_obstacle_avoidance # MAV_COMP_ID_OBSTACLE_AVOIDANCE -bool heartbeat_component_vio # MAV_COMP_ID_VISUAL_INERTIAL_ODOMETRY -bool heartbeat_component_pairing_manager # MAV_COMP_ID_PAIRING_MANAGER -bool heartbeat_component_udp_bridge # MAV_COMP_ID_UDP_BRIDGE -bool heartbeat_component_uart_bridge # MAV_COMP_ID_UART_BRIDGE - -# Misc component health -bool avoidance_system_healthy -bool open_drone_id_system_healthy -bool parachute_system_healthy diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TiltrotorExtraControls.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TiltrotorExtraControls.msg deleted file mode 100644 index 20af316ce..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TiltrotorExtraControls.msg +++ /dev/null @@ -1,4 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 collective_tilt_normalized_setpoint # Collective tilt angle of motors of tiltrotor, 0: vertical, 1: horizontal [0, 1] -float32 collective_thrust_normalized_setpoint # Collective thrust setpoint [0, 1] diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TimesyncStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TimesyncStatus.msg deleted file mode 100644 index 71e84e85a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TimesyncStatus.msg +++ /dev/null @@ -1,11 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint8 SOURCE_PROTOCOL_UNKNOWN = 0 -uint8 SOURCE_PROTOCOL_MAVLINK = 1 -uint8 SOURCE_PROTOCOL_DDS = 2 -uint8 source_protocol # timesync source - -uint64 remote_timestamp # remote system timestamp (microseconds) -int64 observed_offset # raw time offset directly observed from this timesync packet (microseconds) -int64 estimated_offset # smoothed time offset between companion system and PX4 (microseconds) -uint32 round_trip_time # round trip time of this timesync packet (microseconds) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectoryBezier.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectoryBezier.msg deleted file mode 100644 index e3d9d4e0f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectoryBezier.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Bezier Trajectory description. See also Mavlink TRAJECTORY msg -# The topic trajectory_bezier describe each waypoint defined in vehicle_trajectory_bezier - -uint64 timestamp # time since system start (microseconds) - -float32[3] position # local position x,y,z (metres) -float32 yaw # yaw angle (rad) -float32 delta # time it should take to get to this waypoint, if this is the final waypoint (seconds) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectorySetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectorySetpoint.msg deleted file mode 100644 index 4a88c8676..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectorySetpoint.msg +++ /dev/null @@ -1,15 +0,0 @@ -# Trajectory setpoint in NED frame -# Input to PID position controller. -# Needs to be kinematically consistent and feasible for smooth flight. -# setting a value to NaN means the state should not be controlled - -uint64 timestamp # time since system start (microseconds) - -# NED local world frame -float32[3] position # in meters -float32[3] velocity # in meters/second -float32[3] acceleration # in meters/second^2 -float32[3] jerk # in meters/second^3 (for logging only) - -float32 yaw # euler angle of desired attitude in radians -PI..+PI -float32 yawspeed # angular velocity around NED frame z-axis in radians/second diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectoryWaypoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectoryWaypoint.msg deleted file mode 100644 index 6ea9bae4d..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TrajectoryWaypoint.msg +++ /dev/null @@ -1,13 +0,0 @@ -# Waypoint Trajectory description. See also Mavlink TRAJECTORY msg -# The topic trajectory_waypoint describe each waypoint defined in vehicle_trajectory_waypoint - -uint64 timestamp # time since system start (microseconds) - -float32[3] position -float32[3] velocity -float32[3] acceleration -float32 yaw -float32 yaw_speed - -bool point_valid -uint8 type diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TransponderReport.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TransponderReport.msg deleted file mode 100644 index d5171cf3b..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TransponderReport.msg +++ /dev/null @@ -1,50 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint32 icao_address # ICAO address -float64 lat # Latitude, expressed as degrees -float64 lon # Longitude, expressed as degrees -uint8 altitude_type # Type from ADSB_ALTITUDE_TYPE enum -float32 altitude # Altitude(ASL) in meters -float32 heading # Course over ground in radians, -pi to +pi, 0 is north -float32 hor_velocity # The horizontal velocity in m/s -float32 ver_velocity # The vertical velocity in m/s, positive is up -char[9] callsign # The callsign, 8+null -uint8 emitter_type # Type from ADSB_EMITTER_TYPE enum -uint8 tslc # Time since last communication in seconds -uint16 flags # Flags to indicate various statuses including valid data fields -uint16 squawk # Squawk code -uint8[18] uas_id # Unique UAS ID - -# ADSB flags -uint16 PX4_ADSB_FLAGS_VALID_COORDS = 1 -uint16 PX4_ADSB_FLAGS_VALID_ALTITUDE = 2 -uint16 PX4_ADSB_FLAGS_VALID_HEADING = 4 -uint16 PX4_ADSB_FLAGS_VALID_VELOCITY = 8 -uint16 PX4_ADSB_FLAGS_VALID_CALLSIGN = 16 -uint16 PX4_ADSB_FLAGS_VALID_SQUAWK = 32 -uint16 PX4_ADSB_FLAGS_RETRANSLATE = 256 - -#ADSB Emitter Data: -#from mavlink/v2.0/common/common.h -uint16 ADSB_EMITTER_TYPE_NO_INFO=0 -uint16 ADSB_EMITTER_TYPE_LIGHT=1 -uint16 ADSB_EMITTER_TYPE_SMALL=2 -uint16 ADSB_EMITTER_TYPE_LARGE=3 -uint16 ADSB_EMITTER_TYPE_HIGH_VORTEX_LARGE=4 -uint16 ADSB_EMITTER_TYPE_HEAVY=5 -uint16 ADSB_EMITTER_TYPE_HIGHLY_MANUV=6 -uint16 ADSB_EMITTER_TYPE_ROTOCRAFT=7 -uint16 ADSB_EMITTER_TYPE_UNASSIGNED=8 -uint16 ADSB_EMITTER_TYPE_GLIDER=9 -uint16 ADSB_EMITTER_TYPE_LIGHTER_AIR=10 -uint16 ADSB_EMITTER_TYPE_PARACHUTE=11 -uint16 ADSB_EMITTER_TYPE_ULTRA_LIGHT=12 -uint16 ADSB_EMITTER_TYPE_UNASSIGNED2=13 -uint16 ADSB_EMITTER_TYPE_UAV=14 -uint16 ADSB_EMITTER_TYPE_SPACE=15 -uint16 ADSB_EMITTER_TYPE_UNASSGINED3=16 -uint16 ADSB_EMITTER_TYPE_EMERGENCY_SURFACE=17 -uint16 ADSB_EMITTER_TYPE_SERVICE_SURFACE=18 -uint16 ADSB_EMITTER_TYPE_POINT_OBSTACLE=19 -uint16 ADSB_EMITTER_TYPE_ENUM_END=20 - -uint8 ORB_QUEUE_LENGTH = 16 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/TuneControl.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/TuneControl.msg deleted file mode 100644 index 96d70af15..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/TuneControl.msg +++ /dev/null @@ -1,39 +0,0 @@ -# This message is used to control the tunes, when the tune_id is set to CUSTOM -# then the frequency, duration are used otherwise those values are ignored. - -uint64 timestamp # time since system start (microseconds) - -uint8 TUNE_ID_STOP = 0 -uint8 TUNE_ID_STARTUP = 1 -uint8 TUNE_ID_ERROR = 2 -uint8 TUNE_ID_NOTIFY_POSITIVE = 3 -uint8 TUNE_ID_NOTIFY_NEUTRAL = 4 -uint8 TUNE_ID_NOTIFY_NEGATIVE = 5 -uint8 TUNE_ID_ARMING_WARNING = 6 -uint8 TUNE_ID_BATTERY_WARNING_SLOW = 7 -uint8 TUNE_ID_BATTERY_WARNING_FAST = 8 -uint8 TUNE_ID_GPS_WARNING = 9 -uint8 TUNE_ID_ARMING_FAILURE = 10 -uint8 TUNE_ID_PARACHUTE_RELEASE = 11 -uint8 TUNE_ID_SINGLE_BEEP = 12 -uint8 TUNE_ID_HOME_SET = 13 -uint8 TUNE_ID_SD_INIT = 14 -uint8 TUNE_ID_SD_ERROR = 15 -uint8 TUNE_ID_PROG_PX4IO = 16 -uint8 TUNE_ID_PROG_PX4IO_OK = 17 -uint8 TUNE_ID_PROG_PX4IO_ERR = 18 -uint8 TUNE_ID_POWER_OFF = 19 -uint8 NUMBER_OF_TUNES = 20 - -uint8 tune_id # tune_id corresponding to TuneID::* from the tune_defaults.h in the tunes library -bool tune_override # if true the tune which is playing will be stopped and the new started -uint16 frequency # in Hz -uint32 duration # in us -uint32 silence # in us -uint8 volume # value between 0-100 if supported by backend - -uint8 VOLUME_LEVEL_MIN = 0 -uint8 VOLUME_LEVEL_DEFAULT = 20 -uint8 VOLUME_LEVEL_MAX = 100 - -uint8 ORB_QUEUE_LENGTH = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/UavcanParameterRequest.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/UavcanParameterRequest.msg deleted file mode 100644 index 3cfec15f8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/UavcanParameterRequest.msg +++ /dev/null @@ -1,23 +0,0 @@ -# UAVCAN-MAVLink parameter bridge request type -uint64 timestamp # time since system start (microseconds) - -uint8 MESSAGE_TYPE_PARAM_REQUEST_READ = 20 # MAVLINK_MSG_ID_PARAM_REQUEST_READ -uint8 MESSAGE_TYPE_PARAM_REQUEST_LIST = 21 # MAVLINK_MSG_ID_PARAM_REQUEST_LIST -uint8 MESSAGE_TYPE_PARAM_SET = 23 # MAVLINK_MSG_ID_PARAM_SET -uint8 message_type # MAVLink message type: PARAM_REQUEST_READ, PARAM_REQUEST_LIST, PARAM_SET - -uint8 NODE_ID_ALL = 0 # MAV_COMP_ID_ALL -uint8 node_id # UAVCAN node ID mapped from MAVLink component ID - -char[17] param_id # MAVLink/UAVCAN parameter name -int16 param_index # -1 if the param_id field should be used as identifier - -uint8 PARAM_TYPE_UINT8 = 1 # MAV_PARAM_TYPE_UINT8 -uint8 PARAM_TYPE_INT64 = 8 # MAV_PARAM_TYPE_INT64 -uint8 PARAM_TYPE_REAL32 = 9 # MAV_PARAM_TYPE_REAL32 -uint8 param_type # MAVLink parameter type - -int64 int_value # current value if param_type is int-like -float32 real_value # current value if param_type is float-like - -uint8 ORB_QUEUE_LENGTH = 4 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/UavcanParameterValue.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/UavcanParameterValue.msg deleted file mode 100644 index 8eff663a5..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/UavcanParameterValue.msg +++ /dev/null @@ -1,9 +0,0 @@ -# UAVCAN-MAVLink parameter bridge response type -uint64 timestamp # time since system start (microseconds) -uint8 node_id # UAVCAN node ID mapped from MAVLink component ID -char[17] param_id # MAVLink/UAVCAN parameter name -int16 param_index # parameter index, if known -uint16 param_count # number of parameters exposed by the node -uint8 param_type # MAVLink parameter type -int64 int_value # current value if param_type is int-like -float32 real_value # current value if param_type is float-like diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/UlogStream.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/UlogStream.msg deleted file mode 100644 index d206b4a42..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/UlogStream.msg +++ /dev/null @@ -1,19 +0,0 @@ -# Message to stream ULog data from the logger. Corresponds to the LOGGING_DATA -# mavlink message - -uint64 timestamp # time since system start (microseconds) - -# flags bitmasks -uint8 FLAGS_NEED_ACK = 1 # if set, this message requires to be acked. - # Acked messages are published synchronous: a - # publisher waits for an ack before sending the - # next message - -uint8 length # length of data -uint8 first_message_offset # offset into data where first message starts. This - # can be used for recovery, when a previous message got lost -uint16 msg_sequence # allows determine drops -uint8 flags # see FLAGS_* -uint8[249] data # ulog data - -uint8 ORB_QUEUE_LENGTH = 16 # TODO: we might be able to reduce this if mavlink polled on the topic diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/UlogStreamAck.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/UlogStreamAck.msg deleted file mode 100644 index e3747fff6..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/UlogStreamAck.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Ack a previously sent ulog_stream message that had -# the NEED_ACK flag set - -uint64 timestamp # time since system start (microseconds) -int32 ACK_TIMEOUT = 50 # timeout waiting for an ack until we retry to send the message [ms] -int32 ACK_MAX_TRIES = 50 # maximum amount of tries to (re-)send a message, each time waiting ACK_TIMEOUT ms - -uint16 msg_sequence diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/UnregisterExtComponent.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/UnregisterExtComponent.msg deleted file mode 100644 index 2ad78d4b6..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/UnregisterExtComponent.msg +++ /dev/null @@ -1,7 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -char[25] name # either the mode name, or component name - -int8 arming_check_id # arming check registration ID (-1 if not registered) -int8 mode_id # assigned mode ID (-1 if not registered) -int8 mode_executor_id # assigned mode executor ID (-1 if not registered) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAcceleration.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAcceleration.msg deleted file mode 100644 index 7d555d7f3..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAcceleration.msg +++ /dev/null @@ -1,6 +0,0 @@ - -uint64 timestamp # time since system start (microseconds) - -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float32[3] xyz # Bias corrected acceleration (including gravity) in the FRD body frame XYZ-axis in m/s^2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAirData.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAirData.msg deleted file mode 100644 index 59ca5e5c8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAirData.msg +++ /dev/null @@ -1,15 +0,0 @@ - -uint64 timestamp # time since system start (microseconds) - -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -uint32 baro_device_id # unique device ID for the selected barometer - -float32 baro_alt_meter # Altitude above MSL calculated from temperature compensated baro sensor data using an ISA corrected for sea level pressure SENS_BARO_QNH. -float32 baro_temp_celcius # Temperature in degrees Celsius -float32 baro_pressure_pa # Absolute pressure in Pascals - -float32 rho # air density -float32 eas2tas # equivalent airspeed to true airspeed conversion factor - -uint8 calibration_count # Calibration changed counter. Monotonically increases whenever calibration changes. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAngularAccelerationSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAngularAccelerationSetpoint.msg deleted file mode 100644 index 94da11b8c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAngularAccelerationSetpoint.msg +++ /dev/null @@ -1,5 +0,0 @@ - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # timestamp of the data sample on which this message is based (microseconds) - -float32[3] xyz # angular acceleration about X, Y, Z body axis in rad/s^2 diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAngularVelocity.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAngularVelocity.msg deleted file mode 100644 index db3767c0a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAngularVelocity.msg +++ /dev/null @@ -1,9 +0,0 @@ - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # timestamp of the data sample on which this message is based (microseconds) - -float32[3] xyz # Bias corrected angular velocity about the FRD body frame XYZ-axis in rad/s - -float32[3] xyz_derivative # angular acceleration about the FRD body frame XYZ-axis in rad/s^2 - -# TOPICS vehicle_angular_velocity vehicle_angular_velocity_groundtruth diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAttitude.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAttitude.msg deleted file mode 100644 index 99e6f25c2..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAttitude.msg +++ /dev/null @@ -1,13 +0,0 @@ -# This is similar to the mavlink message ATTITUDE_QUATERNION, but for onboard use -# The quaternion uses the Hamilton convention, and the order is q(w, x, y, z) - -uint64 timestamp # time since system start (microseconds) - -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float32[4] q # Quaternion rotation from the FRD body frame to the NED earth frame -float32[4] delta_q_reset # Amount by which quaternion has changed during last reset -uint8 quat_reset_counter # Quaternion reset counter - -# TOPICS vehicle_attitude vehicle_attitude_groundtruth external_ins_attitude -# TOPICS estimator_attitude diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAttitudeSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAttitudeSetpoint.msg deleted file mode 100644 index f52025d2b..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleAttitudeSetpoint.msg +++ /dev/null @@ -1,20 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -float32 roll_body # body angle in NED frame (can be NaN for FW) -float32 pitch_body # body angle in NED frame (can be NaN for FW) -float32 yaw_body # body angle in NED frame (can be NaN for FW) - -float32 yaw_sp_move_rate # rad/s (commanded by user) - -# For quaternion-based attitude control -float32[4] q_d # Desired quaternion for quaternion control - -# For clarification: For multicopters thrust_body[0] and thrust[1] are usually 0 and thrust[2] is the negative throttle demand. -# For fixed wings thrust_x is the throttle demand and thrust_y, thrust_z will usually be zero. -float32[3] thrust_body # Normalized thrust command in body FRD frame [-1,1] - -bool reset_integral # Reset roll/pitch/yaw integrals (navigation logic change) - -bool fw_control_yaw_wheel # control heading with steering wheel (used for auto takeoff on runway) - -# TOPICS vehicle_attitude_setpoint mc_virtual_attitude_setpoint fw_virtual_attitude_setpoint diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleCommand.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleCommand.msg deleted file mode 100644 index b147bef09..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleCommand.msg +++ /dev/null @@ -1,187 +0,0 @@ -# Vehicle Command uORB message. Used for commanding a mission / action / etc. -# Follows the MAVLink COMMAND_INT / COMMAND_LONG definition - -uint64 timestamp # time since system start (microseconds) - -uint16 VEHICLE_CMD_CUSTOM_0 = 0 # test command -uint16 VEHICLE_CMD_CUSTOM_1 = 1 # test command -uint16 VEHICLE_CMD_CUSTOM_2 = 2 # test command -uint16 VEHICLE_CMD_NAV_WAYPOINT = 16 # Navigate to MISSION. |Hold time in decimal seconds. (ignored by fixed wing, time to stay at MISSION for rotary wing)| Acceptance radius in meters (if the sphere with this radius is hit, the MISSION counts as reached)| 0 to pass through the WP, if > 0 radius in meters to pass by WP. Positive value for clockwise orbit, negative value for counter-clockwise orbit. Allows trajectory control.| Desired yaw angle at MISSION (rotary wing)| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_NAV_LOITER_UNLIM = 17 # Loiter around this MISSION an unlimited amount of time |Empty| Empty| Radius around MISSION, in meters. If positive loiter clockwise, else counter-clockwise| Desired yaw angle.| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_NAV_LOITER_TURNS = 18 # Loiter around this MISSION for X turns |Turns| Empty| Radius around MISSION, in meters. If positive loiter clockwise, else counter-clockwise| Desired yaw angle.| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_NAV_LOITER_TIME = 19 # Loiter around this MISSION for X seconds |Seconds (decimal)| Empty| Radius around MISSION, in meters. If positive loiter clockwise, else counter-clockwise| Desired yaw angle.| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_NAV_RETURN_TO_LAUNCH = 20 # Return to launch location |Empty| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_NAV_LAND = 21 # Land at location |Empty| Empty| Empty| Desired yaw angle.| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_NAV_TAKEOFF = 22 # Takeoff from ground / hand |Minimum pitch (if airspeed sensor present), desired pitch without sensor| Empty| Empty| Yaw angle (if magnetometer present), ignored without magnetometer| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_NAV_PRECLAND = 23 # Attempt a precision landing -uint16 VEHICLE_CMD_DO_ORBIT = 34 # Start orbiting on the circumference of a circle defined by the parameters. |Radius [m] |Velocity [m/s] |Yaw behaviour |Empty |Latitude/X |Longitude/Y |Altitude/Z | -uint16 VEHICLE_CMD_DO_FIGUREEIGHT = 35 # Start flying on the outline of a figure eight defined by the parameters. |Major Radius [m] |Minor Radius [m] |Velocity [m/s] |Orientation |Latitude/X |Longitude/Y |Altitude/Z | -uint16 VEHICLE_CMD_NAV_ROI = 80 # Sets the region of interest (ROI) for a sensor set or the vehicle itself. This can then be used by the vehicles control system to control the vehicle attitude and the attitude of various sensors such as cameras. |Region of interest mode. (see MAV_ROI enum)| MISSION index/ target ID. (see MAV_ROI enum)| ROI index (allows a vehicle to manage multiple ROI's)| Empty| x the location of the fixed ROI (see MAV_FRAME)| y| z| -uint16 VEHICLE_CMD_NAV_PATHPLANNING = 81 # Control autonomous path planning on the MAV. |0: Disable local obstacle avoidance / local path planning (without resetting map), 1: Enable local path planning, 2: Enable and reset local path planning| 0: Disable full path planning (without resetting map), 1: Enable, 2: Enable and reset map/occupancy grid, 3: Enable and reset planned route, but not occupancy grid| Empty| Yaw angle at goal, in compass degrees, [0..360]| Latitude/X of goal| Longitude/Y of goal| Altitude/Z of goal| -uint16 VEHICLE_CMD_NAV_VTOL_TAKEOFF = 84 # Takeoff from ground / hand and transition to fixed wing |Minimum pitch (if airspeed sensor present), desired pitch without sensor| Empty| Empty| Yaw angle (if magnetometer present), ignored without magnetometer| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_NAV_VTOL_LAND = 85 # Transition to MC and land at location |Empty| Empty| Empty| Desired yaw angle.| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_NAV_GUIDED_LIMITS = 90 # set limits for external control |timeout - maximum time (in seconds) that external controller will be allowed to control vehicle. 0 means no timeout| absolute altitude min (in meters, AMSL) - if vehicle moves below this alt, the command will be aborted and the mission will continue. 0 means no lower altitude limit| absolute altitude max (in meters)- if vehicle moves above this alt, the command will be aborted and the mission will continue. 0 means no upper altitude limit| horizontal move limit (in meters, AMSL) - if vehicle moves more than this distance from it's location at the moment the command was executed, the command will be aborted and the mission will continue. 0 means no horizontal altitude limit| Empty| Empty| Empty| -uint16 VEHICLE_CMD_NAV_GUIDED_MASTER = 91 # set id of master controller |System ID| Component ID| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_NAV_DELAY = 93 # Delay the next navigation command a number of seconds or until a specified time |Delay in seconds (decimal, -1 to enable time-of-day fields)| hour (24h format, UTC, -1 to ignore)| minute (24h format, UTC, -1 to ignore)| second (24h format, UTC)| Empty| Empty| Empty| -uint16 VEHICLE_CMD_NAV_LAST = 95 # NOP - This command is only used to mark the upper limit of the NAV/ACTION commands in the enumeration |Empty| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_CONDITION_DELAY = 112 # Delay mission state machine. |Delay in seconds (decimal)| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_CONDITION_CHANGE_ALT = 113 # Ascend/descend at rate. Delay mission state machine until desired altitude reached. |Descent / Ascend rate (m/s)| Empty| Empty| Empty| Empty| Empty| Finish Altitude| -uint16 VEHICLE_CMD_CONDITION_DISTANCE = 114 # Delay mission state machine until within desired distance of next NAV point. |Distance (meters)| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_CONDITION_YAW = 115 # Reach a certain target angle. |target angle: [0-360], 0 is north| speed during yaw change:[deg per second]| direction: negative: counter clockwise, positive: clockwise [-1,1]| relative offset or absolute angle: [ 1,0]| Empty| Empty| Empty| -uint16 VEHICLE_CMD_CONDITION_LAST = 159 # NOP - This command is only used to mark the upper limit of the CONDITION commands in the enumeration |Empty| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_CONDITION_GATE = 4501 # Wait until passing a threshold |2D coord mode: 0: Orthogonal to planned route | Altitude mode: 0: Ignore altitude| Empty| Empty| Lat| Lon| Alt| -uint16 VEHICLE_CMD_DO_SET_MODE = 176 # Set system mode. |Mode, as defined by ENUM MAV_MODE| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_JUMP = 177 # Jump to the desired command in the mission list. Repeat this action only the specified number of times |Sequence number| Repeat count| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_CHANGE_SPEED = 178 # Change speed and/or throttle set points. |Speed type (0=Airspeed, 1=Ground Speed)| Speed (m/s, -1 indicates no change)| Throttle ( Percent, -1 indicates no change)| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_SET_HOME = 179 # Changes the home location either to the current location or a specified location. |Use current (1=use current location, 0=use specified location)| Empty| Empty| Empty| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_DO_SET_PARAMETER = 180 # Set a system parameter. Caution! Use of this command requires knowledge of the numeric enumeration value of the parameter. |Parameter number| Parameter value| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_SET_RELAY = 181 # Set a relay to a condition. |Relay number| Setting (1=on, 0=off, others possible depending on system hardware)| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_REPEAT_RELAY = 182 # Cycle a relay on and off for a desired number of cycles with a desired period. |Relay number| Cycle count| Cycle time (seconds, decimal)| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_REPEAT_SERVO = 184 # Cycle a between its nominal setting and a desired PWM for a desired number of cycles with a desired period. |Servo number| PWM (microseconds, 1000 to 2000 typical)| Cycle count| Cycle time (seconds)| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_FLIGHTTERMINATION = 185 # Terminate flight immediately |Flight termination activated if > 0.5| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_CHANGE_ALTITUDE = 186 # Set the vehicle to Loiter mode and change the altitude to specified value |Altitude| Frame of new altitude | Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_SET_ACTUATOR = 187 # Sets actuators (e.g. servos) to a desired value. |Actuator 1| Actuator 2| Actuator 3| Actuator 4| Actuator 5| Actuator 6| Index| -uint16 VEHICLE_CMD_DO_LAND_START = 189 # Mission command to perform a landing. This is used as a marker in a mission to tell the autopilot where a sequence of mission items that represents a landing starts. It may also be sent via a COMMAND_LONG to trigger a landing, in which case the nearest (geographically) landing sequence in the mission will be used. The Latitude/Longitude is optional, and may be set to 0/0 if not needed. If specified then it will be used to help find the closest landing sequence. |Empty| Empty| Empty| Empty| Latitude| Longitude| Empty| -uint16 VEHICLE_CMD_DO_GO_AROUND = 191 # Mission command to safely abort an autonomous landing. |Altitude (meters)| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_REPOSITION = 192 # Reposition to specific WGS84 GPS position. |Ground speed [m/s] |Bitmask |Loiter radius [m] for planes |Yaw [deg] |Latitude |Longitude |Altitude | -uint16 VEHICLE_CMD_DO_PAUSE_CONTINUE = 193 -uint16 VEHICLE_CMD_DO_SET_ROI_LOCATION = 195 # Sets the region of interest (ROI) to a location. This can then be used by the vehicles control system to control the vehicle attitude and the attitude of various sensors such as cameras. |Empty| Empty| Empty| Empty| Latitude| Longitude| Altitude| -uint16 VEHICLE_CMD_DO_SET_ROI_WPNEXT_OFFSET = 196 # Sets the region of interest (ROI) to be toward next waypoint, with optional pitch/roll/yaw offset. This can then be used by the vehicles control system to control the vehicle attitude and the attitude of various sensors such as cameras. |Empty| Empty| Empty| Empty| pitch offset from next waypoint| roll offset from next waypoint| yaw offset from next waypoint| -uint16 VEHICLE_CMD_DO_SET_ROI_NONE = 197 # Cancels any previous ROI command returning the vehicle/sensors to default flight characteristics. This can then be used by the vehicles control system to control the vehicle attitude and the attitude of various sensors such as cameras. |Empty| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_CONTROL_VIDEO = 200 # Control onboard camera system. |Camera ID (-1 for all)| Transmission: 0: disabled, 1: enabled compressed, 2: enabled raw| Transmission mode: 0: video stream, >0: single images every n seconds (decimal)| Recording: 0: disabled, 1: enabled compressed, 2: enabled raw| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_SET_ROI = 201 # Sets the region of interest (ROI) for a sensor set or the vehicle itself. This can then be used by the vehicles control system to control the vehicle attitude and the attitude of various sensors such as cameras. |Region of interest mode. (see MAV_ROI enum)| MISSION index/ target ID. (see MAV_ROI enum)| ROI index (allows a vehicle to manage multiple ROI's)| Empty| x the location of the fixed ROI (see MAV_FRAME)| y| z| -uint16 VEHICLE_CMD_DO_DIGICAM_CONTROL=203 -uint16 VEHICLE_CMD_DO_MOUNT_CONFIGURE=204 # Mission command to configure a camera or antenna mount |Mount operation mode (see MAV_MOUNT_MODE enum)| stabilize roll? (1 = yes, 0 = no)| stabilize pitch? (1 = yes, 0 = no)| stabilize yaw? (1 = yes, 0 = no)| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_MOUNT_CONTROL=205 # Mission command to control a camera or antenna mount |pitch or lat in degrees, depending on mount mode.| roll or lon in degrees depending on mount mode| yaw or alt (in meters) depending on mount mode| reserved| reserved| reserved| MAV_MOUNT_MODE enum value| -uint16 VEHICLE_CMD_DO_SET_CAM_TRIGG_DIST=206 # Mission command to set TRIG_DIST for this flight |Camera trigger distance (meters)| Shutter integration time (ms)| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_FENCE_ENABLE=207 # Mission command to enable the geofence |enable? (0=disable, 1=enable)| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_PARACHUTE=208 # Mission command to trigger a parachute |action (0=disable, 1=enable, 2=release, for some systems see PARACHUTE_ACTION enum, not in general message set.)| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_MOTOR_TEST=209 # motor test command |Instance (1, ...)| throttle type| throttle| timeout [s]| Motor count | Test order| Empty| -uint16 VEHICLE_CMD_DO_INVERTED_FLIGHT=210 # Change to/from inverted flight |inverted (0=normal, 1=inverted)| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_GRIPPER = 211 # Command to operate a gripper -uint16 VEHICLE_CMD_DO_SET_CAM_TRIGG_INTERVAL=214 # Mission command to set TRIG_INTERVAL for this flight |Camera trigger distance (meters)| Shutter integration time (ms)| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_MOUNT_CONTROL_QUAT=220 # Mission command to control a camera or antenna mount, using a quaternion as reference. |q1 - quaternion param #1, w (1 in null-rotation)| q2 - quaternion param #2, x (0 in null-rotation)| q3 - quaternion param #3, y (0 in null-rotation)| q4 - quaternion param #4, z (0 in null-rotation)| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_GUIDED_MASTER=221 # set id of master controller |System ID| Component ID| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_GUIDED_LIMITS=222 # set limits for external control |timeout - maximum time (in seconds) that external controller will be allowed to control vehicle. 0 means no timeout| absolute altitude min (in meters, AMSL) - if vehicle moves below this alt, the command will be aborted and the mission will continue. 0 means no lower altitude limit| absolute altitude max (in meters)- if vehicle moves above this alt, the command will be aborted and the mission will continue. 0 means no upper altitude limit| horizontal move limit (in meters, AMSL) - if vehicle moves more than this distance from it's location at the moment the command was executed, the command will be aborted and the mission will continue. 0 means no horizontal altitude limit| Empty| Empty| Empty| -uint16 VEHICLE_CMD_DO_LAST = 240 # NOP - This command is only used to mark the upper limit of the DO commands in the enumeration |Empty| Empty| Empty| Empty| Empty| Empty| Empty| -uint16 VEHICLE_CMD_PREFLIGHT_CALIBRATION = 241 # Trigger calibration. This command will be only accepted if in pre-flight mode. See mavlink spec MAV_CMD_PREFLIGHT_CALIBRATION -uint16 PREFLIGHT_CALIBRATION_TEMPERATURE_CALIBRATION = 3# param value for VEHICLE_CMD_PREFLIGHT_CALIBRATION to start temperature calibration -uint16 VEHICLE_CMD_PREFLIGHT_SET_SENSOR_OFFSETS = 242 # Set sensor offsets. This command will be only accepted if in pre-flight mode. |Sensor to adjust the offsets for: 0: gyros, 1: accelerometer, 2: magnetometer, 3: barometer, 4: optical flow| X axis offset (or generic dimension 1), in the sensor's raw units| Y axis offset (or generic dimension 2), in the sensor's raw units| Z axis offset (or generic dimension 3), in the sensor's raw units| Generic dimension 4, in the sensor's raw units| Generic dimension 5, in the sensor's raw units| Generic dimension 6, in the sensor's raw units| -uint16 VEHICLE_CMD_PREFLIGHT_UAVCAN = 243 # UAVCAN configuration. If param 1 == 1 actuator mapping and direction assignment should be started -uint16 VEHICLE_CMD_PREFLIGHT_STORAGE = 245 # Request storage of different parameter values and logs. This command will be only accepted if in pre-flight mode. |Parameter storage: 0: READ FROM FLASH/EEPROM, 1: WRITE CURRENT TO FLASH/EEPROM| Mission storage: 0: READ FROM FLASH/EEPROM, 1: WRITE CURRENT TO FLASH/EEPROM| Reserved| Reserved| Empty| Empty| Empty| -uint16 VEHICLE_CMD_PREFLIGHT_REBOOT_SHUTDOWN = 246 # Request the reboot or shutdown of system components. |0: Do nothing for autopilot, 1: Reboot autopilot, 2: Shutdown autopilot.| 0: Do nothing for onboard computer, 1: Reboot onboard computer, 2: Shutdown onboard computer.| Reserved| Reserved| Empty| Empty| Empty| -uint16 VEHICLE_CMD_OBLIQUE_SURVEY=260 # Mission command to set a Camera Auto Mount Pivoting Oblique Survey for this flight|Camera trigger distance (meters)| Shutter integration time (ms)| Camera minimum trigger interval| Number of positions| Roll| Pitch| Empty| -uint16 VEHICLE_CMD_DO_SET_STANDARD_MODE=262 # Enable the specified standard MAVLink mode |MAV_STANDARD_MODE| -uint16 VEHICLE_CMD_GIMBAL_DEVICE_INFORMATION = 283 # Command to ask information about a low level gimbal - -uint16 VEHICLE_CMD_MISSION_START = 300 # start running a mission |first_item: the first mission item to run| last_item: the last mission item to run (after this item is run, the mission ends)| -uint16 VEHICLE_CMD_ACTUATOR_TEST = 310 # Actuator testing command|value [-1,1]|timeout [s]|Empty|Empty|output function| -uint16 VEHICLE_CMD_CONFIGURE_ACTUATOR = 311 # Actuator configuration command|configuration|Empty|Empty|Empty|output function| -uint16 VEHICLE_CMD_COMPONENT_ARM_DISARM = 400 # Arms / Disarms a component |1 to arm, 0 to disarm -uint16 VEHICLE_CMD_RUN_PREARM_CHECKS = 401 # Instructs a target system to run pre-arm checks. -uint16 VEHICLE_CMD_INJECT_FAILURE = 420 # Inject artificial failure for testing purposes -uint16 VEHICLE_CMD_START_RX_PAIR = 500 # Starts receiver pairing |0:Spektrum| 0:Spektrum DSM2, 1:Spektrum DSMX| -uint16 VEHICLE_CMD_REQUEST_MESSAGE = 512 # Request to send a single instance of the specified message -uint16 VEHICLE_CMD_SET_CAMERA_MODE = 530 # Set camera capture mode (photo, video, etc.) -uint16 VEHICLE_CMD_SET_CAMERA_ZOOM = 531 # Set camera zoom -uint16 VEHICLE_CMD_SET_CAMERA_FOCUS = 532 -uint16 VEHICLE_CMD_DO_GIMBAL_MANAGER_PITCHYAW = 1000 # Setpoint to be sent to a gimbal manager to set a gimbal pitch and yaw -uint16 VEHICLE_CMD_DO_GIMBAL_MANAGER_CONFIGURE = 1001 # Gimbal configuration to set which sysid/compid is in primary and secondary control -uint16 VEHICLE_CMD_IMAGE_START_CAPTURE = 2000 # Start image capture sequence. -uint16 VEHICLE_CMD_DO_TRIGGER_CONTROL = 2003 # Enable or disable on-board camera triggering system -uint16 VEHICLE_CMD_VIDEO_START_CAPTURE = 2500 # Start a video capture. -uint16 VEHICLE_CMD_VIDEO_STOP_CAPTURE = 2501 # Stop the current video capture. -uint16 VEHICLE_CMD_LOGGING_START = 2510 # start streaming ULog data -uint16 VEHICLE_CMD_LOGGING_STOP = 2511 # stop streaming ULog data -uint16 VEHICLE_CMD_CONTROL_HIGH_LATENCY = 2600 # control starting/stopping transmitting data over the high latency link -uint16 VEHICLE_CMD_DO_VTOL_TRANSITION = 3000 # Command VTOL transition -uint16 VEHICLE_CMD_ARM_AUTHORIZATION_REQUEST = 3001 # Request arm authorization -uint16 VEHICLE_CMD_PAYLOAD_PREPARE_DEPLOY = 30001 # Prepare a payload deployment in the flight plan -uint16 VEHICLE_CMD_PAYLOAD_CONTROL_DEPLOY = 30002 # Control a pre-programmed payload deployment -uint16 VEHICLE_CMD_FIXED_MAG_CAL_YAW = 42006 # Magnetometer calibration based on provided known yaw. This allows for fast calibration using WMM field tables in the vehicle, given only the known yaw of the vehicle. If Latitude and longitude are both zero then use the current vehicle location. -uint16 VEHICLE_CMD_DO_WINCH = 42600 # Command to operate winch. - -uint16 VEHICLE_CMD_EXTERNAL_POSITION_ESTIMATE = 43003 # external reset of estimator global position when deadreckoning - -# PX4 vehicle commands (beyond 16 bit mavlink commands) -uint32 VEHICLE_CMD_PX4_INTERNAL_START = 65537 # start of PX4 internal only vehicle commands (> UINT16_MAX) -uint32 VEHICLE_CMD_SET_GPS_GLOBAL_ORIGIN = 100000 # Sets the GPS coordinates of the vehicle local origin (0,0,0) position. |Empty|Empty|Empty|Empty|Latitude|Longitude|Altitude| -uint32 VEHICLE_CMD_SET_NAV_STATE = 100001 # Change mode by specifying nav_state directly. |nav_state|Empty|Empty|Empty|Empty|Empty|Empty| - -uint8 VEHICLE_MOUNT_MODE_RETRACT = 0 # Load and keep safe position (Roll,Pitch,Yaw) from permanent memory and stop stabilization | -uint8 VEHICLE_MOUNT_MODE_NEUTRAL = 1 # Load and keep neutral position (Roll,Pitch,Yaw) from permanent memory. | -uint8 VEHICLE_MOUNT_MODE_MAVLINK_TARGETING = 2 # Load neutral position and start MAVLink Roll,Pitch,Yaw control with stabilization | -uint8 VEHICLE_MOUNT_MODE_RC_TARGETING = 3 # Load neutral position and start RC Roll,Pitch,Yaw control with stabilization | -uint8 VEHICLE_MOUNT_MODE_GPS_POINT = 4 # Load neutral position and start to point to Lat,Lon,Alt | -uint8 VEHICLE_MOUNT_MODE_ENUM_END = 5 # - -uint8 VEHICLE_ROI_NONE = 0 # No region of interest | -uint8 VEHICLE_ROI_WPNEXT = 1 # Point toward next MISSION | -uint8 VEHICLE_ROI_WPINDEX = 2 # Point toward given MISSION | -uint8 VEHICLE_ROI_LOCATION = 3 # Point toward fixed location | -uint8 VEHICLE_ROI_TARGET = 4 # Point toward target -uint8 VEHICLE_ROI_ENUM_END = 5 - -uint8 PARACHUTE_ACTION_DISABLE = 0 -uint8 PARACHUTE_ACTION_ENABLE = 1 -uint8 PARACHUTE_ACTION_RELEASE = 2 - -uint8 FAILURE_UNIT_SENSOR_GYRO = 0 -uint8 FAILURE_UNIT_SENSOR_ACCEL = 1 -uint8 FAILURE_UNIT_SENSOR_MAG = 2 -uint8 FAILURE_UNIT_SENSOR_BARO = 3 -uint8 FAILURE_UNIT_SENSOR_GPS = 4 -uint8 FAILURE_UNIT_SENSOR_OPTICAL_FLOW = 5 -uint8 FAILURE_UNIT_SENSOR_VIO = 6 -uint8 FAILURE_UNIT_SENSOR_DISTANCE_SENSOR = 7 -uint8 FAILURE_UNIT_SENSOR_AIRSPEED = 8 -uint8 FAILURE_UNIT_SYSTEM_BATTERY = 100 -uint8 FAILURE_UNIT_SYSTEM_MOTOR = 101 -uint8 FAILURE_UNIT_SYSTEM_SERVO = 102 -uint8 FAILURE_UNIT_SYSTEM_AVOIDANCE = 103 -uint8 FAILURE_UNIT_SYSTEM_RC_SIGNAL = 104 -uint8 FAILURE_UNIT_SYSTEM_MAVLINK_SIGNAL = 105 - -uint8 FAILURE_TYPE_OK = 0 -uint8 FAILURE_TYPE_OFF = 1 -uint8 FAILURE_TYPE_STUCK = 2 -uint8 FAILURE_TYPE_GARBAGE = 3 -uint8 FAILURE_TYPE_WRONG = 4 -uint8 FAILURE_TYPE_SLOW = 5 -uint8 FAILURE_TYPE_DELAYED = 6 -uint8 FAILURE_TYPE_INTERMITTENT = 7 - -# used as param1 in DO_CHANGE_SPEED command -uint8 SPEED_TYPE_AIRSPEED = 0 -uint8 SPEED_TYPE_GROUNDSPEED = 1 -uint8 SPEED_TYPE_CLIMB_SPEED = 2 -uint8 SPEED_TYPE_DESCEND_SPEED = 3 - -# used as param1 in ARM_DISARM command -int8 ARMING_ACTION_DISARM = 0 -int8 ARMING_ACTION_ARM = 1 - -# param2 in VEHICLE_CMD_DO_GRIPPER -uint8 GRIPPER_ACTION_RELEASE = 0 -uint8 GRIPPER_ACTION_GRAB = 1 - -uint8 ORB_QUEUE_LENGTH = 8 - -float32 param1 # Parameter 1, as defined by MAVLink uint16 VEHICLE_CMD enum. -float32 param2 # Parameter 2, as defined by MAVLink uint16 VEHICLE_CMD enum. -float32 param3 # Parameter 3, as defined by MAVLink uint16 VEHICLE_CMD enum. -float32 param4 # Parameter 4, as defined by MAVLink uint16 VEHICLE_CMD enum. -float64 param5 # Parameter 5, as defined by MAVLink uint16 VEHICLE_CMD enum. -float64 param6 # Parameter 6, as defined by MAVLink uint16 VEHICLE_CMD enum. -float32 param7 # Parameter 7, as defined by MAVLink uint16 VEHICLE_CMD enum. -uint32 command # Command ID -uint8 target_system # System which should execute the command -uint8 target_component # Component which should execute the command, 0 for all components -uint8 source_system # System sending the command -uint16 source_component # Component / mode executor sending the command -uint8 confirmation # 0: First transmission of this command. 1-255: Confirmation transmissions (e.g. for kill command) -bool from_external - -uint16 COMPONENT_MODE_EXECUTOR_START = 1000 - -# TOPICS vehicle_command gimbal_v1_command vehicle_command_mode_executor diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleCommandAck.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleCommandAck.msg deleted file mode 100644 index 6f54fa463..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleCommandAck.msg +++ /dev/null @@ -1,33 +0,0 @@ -# Vehicle Command Ackonwledgement uORB message. -# Used for acknowledging the vehicle command being received. -# Follows the MAVLink COMMAND_ACK message definition - -uint64 timestamp # time since system start (microseconds) - -# Result cases. This follows the MAVLink MAV_RESULT enum definition -uint8 VEHICLE_CMD_RESULT_ACCEPTED = 0 # Command ACCEPTED and EXECUTED | -uint8 VEHICLE_CMD_RESULT_TEMPORARILY_REJECTED = 1 # Command TEMPORARY REJECTED/DENIED | -uint8 VEHICLE_CMD_RESULT_DENIED = 2 # Command PERMANENTLY DENIED | -uint8 VEHICLE_CMD_RESULT_UNSUPPORTED = 3 # Command UNKNOWN/UNSUPPORTED | -uint8 VEHICLE_CMD_RESULT_FAILED = 4 # Command executed, but failed | -uint8 VEHICLE_CMD_RESULT_IN_PROGRESS = 5 # Command being executed | -uint8 VEHICLE_CMD_RESULT_CANCELLED = 6 # Command Canceled - -# Arming denied specific cases -uint16 ARM_AUTH_DENIED_REASON_GENERIC = 0 -uint16 ARM_AUTH_DENIED_REASON_NONE = 1 -uint16 ARM_AUTH_DENIED_REASON_INVALID_WAYPOINT = 2 -uint16 ARM_AUTH_DENIED_REASON_TIMEOUT = 3 -uint16 ARM_AUTH_DENIED_REASON_AIRSPACE_IN_USE = 4 -uint16 ARM_AUTH_DENIED_REASON_BAD_WEATHER = 5 - -uint8 ORB_QUEUE_LENGTH = 4 - -uint32 command # Command that is being acknowledged -uint8 result # Command result -uint8 result_param1 # Also used as progress[%], it can be set with the reason why the command was denied, or the progress percentage when result is MAV_RESULT_IN_PROGRESS -int32 result_param2 # Additional parameter of the result, example: which parameter of MAV_CMD_NAV_WAYPOINT caused it to be denied. -uint8 target_system -uint16 target_component # Target component / mode executor - -bool from_external # Indicates if the command came from an external source diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleConstraints.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleConstraints.msg deleted file mode 100644 index aa3a491b1..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleConstraints.msg +++ /dev/null @@ -1,9 +0,0 @@ -# Local setpoint constraints in NED frame -# setting something to NaN means that no limit is provided - -uint64 timestamp # time since system start (microseconds) - -float32 speed_up # in meters/sec -float32 speed_down # in meters/sec - -bool want_takeoff # tell the controller to initiate takeoff when idling (ignored during flight) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleControlMode.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleControlMode.msg deleted file mode 100644 index 9b33f9b8c..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleControlMode.msg +++ /dev/null @@ -1,22 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -bool flag_armed # synonym for actuator_armed.armed - -bool flag_multicopter_position_control_enabled - -bool flag_control_manual_enabled # true if manual input is mixed in -bool flag_control_auto_enabled # true if onboard autopilot should act -bool flag_control_offboard_enabled # true if offboard control should be used -bool flag_control_position_enabled # true if position is controlled -bool flag_control_velocity_enabled # true if horizontal velocity (implies direction) is controlled -bool flag_control_altitude_enabled # true if altitude is controlled -bool flag_control_climb_rate_enabled # true if climb rate is controlled -bool flag_control_acceleration_enabled # true if acceleration is controlled -bool flag_control_attitude_enabled # true if attitude stabilization is mixed in -bool flag_control_rates_enabled # true if rates are stabilized -bool flag_control_allocation_enabled # true if control allocation is enabled -bool flag_control_termination_enabled # true if flighttermination is enabled - -# TODO: use dedicated topic for external requests -uint8 source_id # Mode ID (nav_state) - -# TOPICS vehicle_control_mode config_control_setpoints diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleGlobalPosition.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleGlobalPosition.msg deleted file mode 100644 index c7d9ee781..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleGlobalPosition.msg +++ /dev/null @@ -1,30 +0,0 @@ -# Fused global position in WGS84. -# This struct contains global position estimation. It is not the raw GPS -# measurement (@see vehicle_gps_position). This topic is usually published by the position -# estimator, which will take more sources of information into account than just GPS, -# e.g. control inputs of the vehicle in a Kalman-filter implementation. -# - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float64 lat # Latitude, (degrees) -float64 lon # Longitude, (degrees) -float32 alt # Altitude AMSL, (meters) -float32 alt_ellipsoid # Altitude above ellipsoid, (meters) - -float32 delta_alt # Reset delta for altitude -uint8 lat_lon_reset_counter # Counter for reset events on horizontal position coordinates -uint8 alt_reset_counter # Counter for reset events on altitude - -float32 eph # Standard deviation of horizontal position error, (metres) -float32 epv # Standard deviation of vertical position error, (metres) - -float32 terrain_alt # Terrain altitude WGS84, (metres) -bool terrain_alt_valid # Terrain altitude estimate is valid - -bool dead_reckoning # True if this position is estimated through dead-reckoning - -# TOPICS vehicle_global_position vehicle_global_position_groundtruth external_ins_global_position -# TOPICS estimator_global_position -# TOPICS aux_global_position diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleImu.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleImu.msg deleted file mode 100644 index a71bb7a01..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleImu.msg +++ /dev/null @@ -1,23 +0,0 @@ -# IMU readings in SI-unit form. - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 accel_device_id # Accelerometer unique device ID for the sensor that does not change between power cycles -uint32 gyro_device_id # Gyroscope unique device ID for the sensor that does not change between power cycles - -float32[3] delta_angle # delta angle about the FRD body frame XYZ-axis in rad over the integration time frame (delta_angle_dt) -float32[3] delta_velocity # delta velocity in the FRD body frame XYZ-axis in m/s over the integration time frame (delta_velocity_dt) - -uint16 delta_angle_dt # integration period in microseconds -uint16 delta_velocity_dt # integration period in microseconds - -uint8 CLIPPING_X = 1 -uint8 CLIPPING_Y = 2 -uint8 CLIPPING_Z = 4 - -uint8 delta_angle_clipping # bitfield indicating if there was any gyro clipping (per axis) during the integration time frame -uint8 delta_velocity_clipping # bitfield indicating if there was any accelerometer clipping (per axis) during the integration time frame - -uint8 accel_calibration_count # Calibration changed counter. Monotonically increases whenever accelermeter calibration changes. -uint8 gyro_calibration_count # Calibration changed counter. Monotonically increases whenever rate gyro calibration changes. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleImuStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleImuStatus.msg deleted file mode 100644 index 78fb44703..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleImuStatus.msg +++ /dev/null @@ -1,28 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -uint32 accel_device_id # unique device ID for the sensor that does not change between power cycles -uint32 gyro_device_id # unique device ID for the sensor that does not change between power cycles - -uint32[3] accel_clipping # total clipping per axis -uint32[3] gyro_clipping # total clipping per axis - -uint32 accel_error_count -uint32 gyro_error_count - -float32 accel_rate_hz -float32 gyro_rate_hz - -float32 accel_raw_rate_hz # full raw sensor sample rate (Hz) -float32 gyro_raw_rate_hz # full raw sensor sample rate (Hz) - -float32 accel_vibration_metric # high frequency vibration level in the accelerometer data (m/s/s) -float32 gyro_vibration_metric # high frequency vibration level in the gyro data (rad/s) -float32 delta_angle_coning_metric # average IMU delta angle coning correction (rad^2) - -float32[3] mean_accel # average accelerometer readings since last publication -float32[3] mean_gyro # average gyroscope readings since last publication -float32[3] var_accel # accelerometer variance since last publication -float32[3] var_gyro # gyroscope variance since last publication - -float32 temperature_accel -float32 temperature_gyro diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLandDetected.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLandDetected.msg deleted file mode 100644 index fc0ca4a6d..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLandDetected.msg +++ /dev/null @@ -1,19 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -bool freefall # true if vehicle is currently in free-fall -bool ground_contact # true if vehicle has ground contact but is not landed (1. stage) -bool maybe_landed # true if the vehicle might have landed (2. stage) -bool landed # true if vehicle is currently landed on the ground (3. stage) - -bool in_ground_effect # indicates if from the perspective of the landing detector the vehicle might be in ground effect (baro). This flag will become true if the vehicle is not moving horizontally and is descending (crude assumption that user is landing). -bool in_descend - -bool has_low_throttle - -bool vertical_movement -bool horizontal_movement -bool rotational_movement - -bool close_to_ground_or_skipped_check - -bool at_rest diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLocalPosition.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLocalPosition.msg deleted file mode 100644 index c1a0dffe1..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLocalPosition.msg +++ /dev/null @@ -1,79 +0,0 @@ -# Fused local position in NED. -# The coordinate system origin is the vehicle position at the time when the EKF2-module was started. - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -bool xy_valid # true if x and y are valid -bool z_valid # true if z is valid -bool v_xy_valid # true if vx and vy are valid -bool v_z_valid # true if vz is valid - -# Position in local NED frame -float32 x # North position in NED earth-fixed frame, (metres) -float32 y # East position in NED earth-fixed frame, (metres) -float32 z # Down position (negative altitude) in NED earth-fixed frame, (metres) - -# Position reset delta -float32[2] delta_xy # Amount of lateral shift of position estimate in latest reset (in x and y) [m] -uint8 xy_reset_counter # Index of latest lateral position estimate reset -float32 delta_z # Amount of vertical shift of position estimate in latest reset [m] -uint8 z_reset_counter # Index of latest vertical position estimate reset - -# Velocity in NED frame -float32 vx # North velocity in NED earth-fixed frame, (metres/sec) -float32 vy # East velocity in NED earth-fixed frame, (metres/sec) -float32 vz # Down velocity in NED earth-fixed frame, (metres/sec) -float32 z_deriv # Down position time derivative in NED earth-fixed frame, (metres/sec) - -# Velocity reset delta -float32[2] delta_vxy # Amount of lateral shift of velocity estimate in latest reset (in x and y) [m/s] -uint8 vxy_reset_counter # Index of latest vertical velocity estimate reset -float32 delta_vz # Amount of vertical shift of velocity estimate in latest reset [m/s] -uint8 vz_reset_counter # Index of latest vertical velocity estimate reset - -# Acceleration in NED frame -float32 ax # North velocity derivative in NED earth-fixed frame, (metres/sec^2) -float32 ay # East velocity derivative in NED earth-fixed frame, (metres/sec^2) -float32 az # Down velocity derivative in NED earth-fixed frame, (metres/sec^2) - -float32 heading # Euler yaw angle transforming the tangent plane relative to NED earth-fixed frame, -PI..+PI, (radians) -float32 heading_var -float32 unaided_heading # Same as heading but generated by integrating corrected gyro data only -float32 delta_heading # Heading delta caused by latest heading reset [rad] -uint8 heading_reset_counter # Index of latest heading reset -bool heading_good_for_control - -float32 tilt_var - -# Position of reference point (local NED frame origin) in global (GPS / WGS84) frame -bool xy_global # true if position (x, y) has a valid global reference (ref_lat, ref_lon) -bool z_global # true if z has a valid global reference (ref_alt) -uint64 ref_timestamp # Time when reference position was set since system start, (microseconds) -float64 ref_lat # Reference point latitude, (degrees) -float64 ref_lon # Reference point longitude, (degrees) -float32 ref_alt # Reference altitude AMSL, (metres) - -# Distance to surface -float32 dist_bottom # Distance from from bottom surface to ground, (metres) -bool dist_bottom_valid # true if distance to bottom surface is valid -uint8 dist_bottom_sensor_bitfield # bitfield indicating what type of sensor is used to estimate dist_bottom -uint8 DIST_BOTTOM_SENSOR_NONE = 0 -uint8 DIST_BOTTOM_SENSOR_RANGE = 1 # (1 << 0) a range sensor is used to estimate dist_bottom field -uint8 DIST_BOTTOM_SENSOR_FLOW = 2 # (1 << 1) a flow sensor is used to estimate dist_bottom field (mostly fixed-wing use case) - -float32 eph # Standard deviation of horizontal position error, (metres) -float32 epv # Standard deviation of vertical position error, (metres) -float32 evh # Standard deviation of horizontal velocity error, (metres/sec) -float32 evv # Standard deviation of vertical velocity error, (metres/sec) - -bool dead_reckoning # True if this position is estimated through dead-reckoning - -# estimator specified vehicle limits -float32 vxy_max # maximum horizontal speed - set to 0 when limiting not required (meters/sec) -float32 vz_max # maximum vertical speed - set to 0 when limiting not required (meters/sec) -float32 hagl_min # minimum height above ground level - set to 0 when limiting not required (meters) -float32 hagl_max # maximum height above ground level - set to 0 when limiting not required (meters) - -# TOPICS vehicle_local_position vehicle_local_position_groundtruth external_ins_local_position -# TOPICS estimator_local_position diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLocalPositionSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLocalPositionSetpoint.msg deleted file mode 100644 index 0093d52d7..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleLocalPositionSetpoint.msg +++ /dev/null @@ -1,19 +0,0 @@ -# Local position setpoint in NED frame -# Telemetry of PID position controller to monitor tracking. -# NaN means the state was not controlled - -uint64 timestamp # time since system start (microseconds) - -float32 x # in meters NED -float32 y # in meters NED -float32 z # in meters NED - -float32 vx # in meters/sec -float32 vy # in meters/sec -float32 vz # in meters/sec - -float32[3] acceleration # in meters/sec^2 -float32[3] thrust # normalized thrust vector in NED - -float32 yaw # in radians NED -PI..+PI -float32 yawspeed # in radians/sec diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleMagnetometer.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleMagnetometer.msg deleted file mode 100644 index f2249c784..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleMagnetometer.msg +++ /dev/null @@ -1,10 +0,0 @@ - -uint64 timestamp # time since system start (microseconds) - -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -uint32 device_id # unique device ID for the selected magnetometer - -float32[3] magnetometer_ga # Magnetic field in the FRD body frame XYZ-axis in Gauss - -uint8 calibration_count # Calibration changed counter. Monotonically increases whenever calibration changes. diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOdometry.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOdometry.msg deleted file mode 100644 index fbdd1920e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOdometry.msg +++ /dev/null @@ -1,31 +0,0 @@ -# Vehicle odometry data. Fits ROS REP 147 for aerial vehicles -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint8 POSE_FRAME_UNKNOWN = 0 -uint8 POSE_FRAME_NED = 1 # NED earth-fixed frame -uint8 POSE_FRAME_FRD = 2 # FRD world-fixed frame, arbitrary heading reference -uint8 pose_frame # Position and orientation frame of reference - -float32[3] position # Position in meters. Frame of reference defined by local_frame. NaN if invalid/unknown -float32[4] q # Quaternion rotation from FRD body frame to reference frame. First value NaN if invalid/unknown - -uint8 VELOCITY_FRAME_UNKNOWN = 0 -uint8 VELOCITY_FRAME_NED = 1 # NED earth-fixed frame -uint8 VELOCITY_FRAME_FRD = 2 # FRD world-fixed frame, arbitrary heading reference -uint8 VELOCITY_FRAME_BODY_FRD = 3 # FRD body-fixed frame -uint8 velocity_frame # Reference frame of the velocity data - -float32[3] velocity # Velocity in meters/sec. Frame of reference defined by velocity_frame variable. NaN if invalid/unknown - -float32[3] angular_velocity # Angular velocity in body-fixed frame (rad/s). NaN if invalid/unknown - -float32[3] position_variance -float32[3] orientation_variance -float32[3] velocity_variance - -uint8 reset_counter -int8 quality - -# TOPICS vehicle_odometry vehicle_mocap_odometry vehicle_visual_odometry -# TOPICS estimator_odometry diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOpticalFlow.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOpticalFlow.msg deleted file mode 100644 index 13bdb57bb..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOpticalFlow.msg +++ /dev/null @@ -1,21 +0,0 @@ -# Optical flow in XYZ body frame in SI units. - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample - -uint32 device_id # unique device ID for the sensor that does not change between power cycles - -float32[2] pixel_flow # (radians) accumulated optical flow in radians where a positive value is produced by a RH rotation about the body axis - -float32[3] delta_angle # (radians) accumulated gyro radians where a positive value is produced by a RH rotation about the body axis. (NAN if unavailable) - -float32 distance_m # (meters) Distance to the center of the flow field (NAN if unavailable) - -uint32 integration_timespan_us # (microseconds) accumulation timespan in microseconds - -uint8 quality # Average of quality of accumulated frames, 0: bad quality, 255: maximum quality - -float32 max_flow_rate # (radians/s) Magnitude of maximum angular which the optical flow sensor can measure reliably - -float32 min_ground_distance # (meters) Minimum distance from ground at which the optical flow sensor operates reliably -float32 max_ground_distance # (meters) Maximum distance from ground at which the optical flow sensor operates reliably diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOpticalFlowVel.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOpticalFlowVel.msg deleted file mode 100644 index 947131da4..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleOpticalFlowVel.msg +++ /dev/null @@ -1,15 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float32[2] vel_body # velocity obtained from gyro-compensated and distance-scaled optical flow raw measurements in body frame(m/s) -float32[2] vel_ne # same as vel_body but in local frame (m/s) - -float32[2] flow_rate_uncompensated # integrated optical flow measurement (rad/s) -float32[2] flow_rate_compensated # integrated optical flow measurement compensated for angular motion (rad/s) - -float32[3] gyro_rate # gyro measurement synchronized with flow measurements (rad/s) - -float32[3] gyro_bias -float32[3] ref_gyro - -# TOPICS estimator_optical_flow_vel vehicle_optical_flow_vel diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleRatesSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleRatesSetpoint.msg deleted file mode 100644 index 35a06c35a..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleRatesSetpoint.msg +++ /dev/null @@ -1,12 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -# body angular rates in FRD frame -float32 roll # [rad/s] roll rate setpoint -float32 pitch # [rad/s] pitch rate setpoint -float32 yaw # [rad/s] yaw rate setpoint - -# For clarification: For multicopters thrust_body[0] and thrust[1] are usually 0 and thrust[2] is the negative throttle demand. -# For fixed wings thrust_x is the throttle demand and thrust_y, thrust_z will usually be zero. -float32[3] thrust_body # Normalized thrust command in body NED frame [-1,1] - -bool reset_integral # Reset roll/pitch/yaw integrals (navigation logic change) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleRoi.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleRoi.msg deleted file mode 100644 index 2948f157f..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleRoi.msg +++ /dev/null @@ -1,21 +0,0 @@ -# Vehicle Region Of Interest (ROI) - -uint64 timestamp # time since system start (microseconds) - -uint8 ROI_NONE = 0 # No region of interest -uint8 ROI_WPNEXT = 1 # Point toward next MISSION with optional offset -uint8 ROI_WPINDEX = 2 # Point toward given MISSION -uint8 ROI_LOCATION = 3 # Point toward fixed location -uint8 ROI_TARGET = 4 # Point toward target -uint8 ROI_ENUM_END = 5 - -uint8 mode # ROI mode (see above) - -float64 lat # Latitude to point to -float64 lon # Longitude to point to -float32 alt # Altitude to point to - -# additional angle offsets to next waypoint (only used with ROI_WPNEXT) -float32 roll_offset # angle offset in rad -float32 pitch_offset # angle offset in rad -float32 yaw_offset # angle offset in rad diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleStatus.msg deleted file mode 100644 index 4c711b976..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleStatus.msg +++ /dev/null @@ -1,138 +0,0 @@ -# Encodes the system state of the vehicle published by commander - -uint64 timestamp # time since system start (microseconds) - -uint64 armed_time # Arming timestamp (microseconds) -uint64 takeoff_time # Takeoff timestamp (microseconds) - -uint8 arming_state -uint8 ARMING_STATE_DISARMED = 1 -uint8 ARMING_STATE_ARMED = 2 - -uint8 latest_arming_reason -uint8 latest_disarming_reason -uint8 ARM_DISARM_REASON_TRANSITION_TO_STANDBY = 0 -uint8 ARM_DISARM_REASON_RC_STICK = 1 -uint8 ARM_DISARM_REASON_RC_SWITCH = 2 -uint8 ARM_DISARM_REASON_COMMAND_INTERNAL = 3 -uint8 ARM_DISARM_REASON_COMMAND_EXTERNAL = 4 -uint8 ARM_DISARM_REASON_MISSION_START = 5 -uint8 ARM_DISARM_REASON_SAFETY_BUTTON = 6 -uint8 ARM_DISARM_REASON_AUTO_DISARM_LAND = 7 -uint8 ARM_DISARM_REASON_AUTO_DISARM_PREFLIGHT = 8 -uint8 ARM_DISARM_REASON_KILL_SWITCH = 9 -uint8 ARM_DISARM_REASON_LOCKDOWN = 10 -uint8 ARM_DISARM_REASON_FAILURE_DETECTOR = 11 -uint8 ARM_DISARM_REASON_SHUTDOWN = 12 -uint8 ARM_DISARM_REASON_UNIT_TEST = 13 - -uint64 nav_state_timestamp # time when current nav_state activated - -uint8 nav_state_user_intention # Mode that the user selected (might be different from nav_state in a failsafe situation) - -uint8 nav_state # Currently active mode -uint8 NAVIGATION_STATE_MANUAL = 0 # Manual mode -uint8 NAVIGATION_STATE_ALTCTL = 1 # Altitude control mode -uint8 NAVIGATION_STATE_POSCTL = 2 # Position control mode -uint8 NAVIGATION_STATE_AUTO_MISSION = 3 # Auto mission mode -uint8 NAVIGATION_STATE_AUTO_LOITER = 4 # Auto loiter mode -uint8 NAVIGATION_STATE_AUTO_RTL = 5 # Auto return to launch mode -uint8 NAVIGATION_STATE_POSITION_SLOW = 6 -uint8 NAVIGATION_STATE_FREE5 = 7 -uint8 NAVIGATION_STATE_FREE4 = 8 -uint8 NAVIGATION_STATE_FREE3 = 9 -uint8 NAVIGATION_STATE_ACRO = 10 # Acro mode -uint8 NAVIGATION_STATE_FREE2 = 11 -uint8 NAVIGATION_STATE_DESCEND = 12 # Descend mode (no position control) -uint8 NAVIGATION_STATE_TERMINATION = 13 # Termination mode -uint8 NAVIGATION_STATE_OFFBOARD = 14 -uint8 NAVIGATION_STATE_STAB = 15 # Stabilized mode -uint8 NAVIGATION_STATE_FREE1 = 16 -uint8 NAVIGATION_STATE_AUTO_TAKEOFF = 17 # Takeoff -uint8 NAVIGATION_STATE_AUTO_LAND = 18 # Land -uint8 NAVIGATION_STATE_AUTO_FOLLOW_TARGET = 19 # Auto Follow -uint8 NAVIGATION_STATE_AUTO_PRECLAND = 20 # Precision land with landing target -uint8 NAVIGATION_STATE_ORBIT = 21 # Orbit in a circle -uint8 NAVIGATION_STATE_AUTO_VTOL_TAKEOFF = 22 # Takeoff, transition, establish loiter -uint8 NAVIGATION_STATE_EXTERNAL1 = 23 -uint8 NAVIGATION_STATE_EXTERNAL2 = 24 -uint8 NAVIGATION_STATE_EXTERNAL3 = 25 -uint8 NAVIGATION_STATE_EXTERNAL4 = 26 -uint8 NAVIGATION_STATE_EXTERNAL5 = 27 -uint8 NAVIGATION_STATE_EXTERNAL6 = 28 -uint8 NAVIGATION_STATE_EXTERNAL7 = 29 -uint8 NAVIGATION_STATE_EXTERNAL8 = 30 -uint8 NAVIGATION_STATE_MAX = 31 - -uint8 executor_in_charge # Current mode executor in charge (0=Autopilot) - -uint32 valid_nav_states_mask # Bitmask for all valid nav_state values -uint32 can_set_nav_states_mask # Bitmask for all modes that a user can select - -# Bitmask of detected failures -uint16 failure_detector_status -uint16 FAILURE_NONE = 0 -uint16 FAILURE_ROLL = 1 # (1 << 0) -uint16 FAILURE_PITCH = 2 # (1 << 1) -uint16 FAILURE_ALT = 4 # (1 << 2) -uint16 FAILURE_EXT = 8 # (1 << 3) -uint16 FAILURE_ARM_ESC = 16 # (1 << 4) -uint16 FAILURE_BATTERY = 32 # (1 << 5) -uint16 FAILURE_IMBALANCED_PROP = 64 # (1 << 6) -uint16 FAILURE_MOTOR = 128 # (1 << 7) - -uint8 hil_state -uint8 HIL_STATE_OFF = 0 -uint8 HIL_STATE_ON = 1 - -# If it's a VTOL, then the value will be VEHICLE_TYPE_ROTARY_WING while flying as a multicopter, and VEHICLE_TYPE_FIXED_WING when flying as a fixed-wing -uint8 vehicle_type -uint8 VEHICLE_TYPE_UNKNOWN = 0 -uint8 VEHICLE_TYPE_ROTARY_WING = 1 -uint8 VEHICLE_TYPE_FIXED_WING = 2 -uint8 VEHICLE_TYPE_ROVER = 3 -uint8 VEHICLE_TYPE_AIRSHIP = 4 - -uint8 FAILSAFE_DEFER_STATE_DISABLED = 0 -uint8 FAILSAFE_DEFER_STATE_ENABLED = 1 -uint8 FAILSAFE_DEFER_STATE_WOULD_FAILSAFE = 2 # Failsafes deferred, but would trigger a failsafe - -bool failsafe # true if system is in failsafe state (e.g.:RTL, Hover, Terminate, ...) -bool failsafe_and_user_took_over # true if system is in failsafe state but the user took over control -uint8 failsafe_defer_state # one of FAILSAFE_DEFER_STATE_* - -# Link loss -bool gcs_connection_lost # datalink to GCS lost -uint8 gcs_connection_lost_counter # counts unique GCS connection lost events -bool high_latency_data_link_lost # Set to true if the high latency data link (eg. RockBlock Iridium 9603 telemetry module) is lost - -# VTOL flags -bool is_vtol # True if the system is VTOL capable -bool is_vtol_tailsitter # True if the system performs a 90° pitch down rotation during transition from MC to FW -bool in_transition_mode # True if VTOL is doing a transition -bool in_transition_to_fw # True if VTOL is doing a transition from MC to FW - -# MAVLink identification -uint8 system_type # system type, contains mavlink MAV_TYPE -uint8 system_id # system id, contains MAVLink's system ID field -uint8 component_id # subsystem / component id, contains MAVLink's component ID field - -bool safety_button_available # Set to true if a safety button is connected -bool safety_off # Set to true if safety is off - -bool power_input_valid # set if input power is valid -bool usb_connected # set to true (never cleared) once telemetry received from usb link - -bool open_drone_id_system_present -bool open_drone_id_system_healthy - -bool parachute_system_present -bool parachute_system_healthy - -bool avoidance_system_required # Set to true if avoidance system is enabled via COM_OBS_AVOID parameter -bool avoidance_system_valid # Status of the obstacle avoidance system - -bool rc_calibration_in_progress -bool calibration_enabled - -bool pre_flight_checks_pass # true if all checks necessary to arm pass diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleThrustSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleThrustSetpoint.msg deleted file mode 100644 index 444ee5003..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleThrustSetpoint.msg +++ /dev/null @@ -1,8 +0,0 @@ - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # timestamp of the data sample on which this message is based (microseconds) - -float32[3] xyz # thrust setpoint along X, Y, Z body axis [-1, 1] - -# TOPICS vehicle_thrust_setpoint -# TOPICS vehicle_thrust_setpoint_virtual_fw vehicle_thrust_setpoint_virtual_mc diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTorqueSetpoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTorqueSetpoint.msg deleted file mode 100644 index c20519b16..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTorqueSetpoint.msg +++ /dev/null @@ -1,8 +0,0 @@ - -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # timestamp of the data sample on which this message is based (microseconds) - -float32[3] xyz # torque setpoint about X, Y, Z body axis (normalized) - -# TOPICS vehicle_torque_setpoint -# TOPICS vehicle_torque_setpoint_virtual_fw vehicle_torque_setpoint_virtual_mc diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTrajectoryBezier.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTrajectoryBezier.msg deleted file mode 100644 index d4bf99b46..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTrajectoryBezier.msg +++ /dev/null @@ -1,18 +0,0 @@ -# Vehicle Waypoints Trajectory description. See also MAVLink MAV_TRAJECTORY_REPRESENTATION msg -# The topic vehicle_trajectory_bezier is used to send a smooth flight path from the -# companion computer / avoidance module to the position controller. - -uint64 timestamp # time since system start (microseconds) - -uint8 POINT_0 = 0 -uint8 POINT_1 = 1 -uint8 POINT_2 = 2 -uint8 POINT_3 = 3 -uint8 POINT_4 = 4 - -uint8 NUMBER_POINTS = 5 - -TrajectoryBezier[5] control_points -uint8 bezier_order - -# TOPICS vehicle_trajectory_bezier diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTrajectoryWaypoint.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTrajectoryWaypoint.msg deleted file mode 100644 index 6bff1cec8..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VehicleTrajectoryWaypoint.msg +++ /dev/null @@ -1,21 +0,0 @@ -# Vehicle Waypoints Trajectory description. See also MAVLink MAV_TRAJECTORY_REPRESENTATION msg -# The topic vehicle_trajectory_waypoint_desired is used to send the user desired waypoints from the position controller to the companion computer / avoidance module. -# The topic vehicle_trajectory_waypoint is used to send the adjusted waypoints from the companion computer / avoidance module to the position controller. - -uint64 timestamp # time since system start (microseconds) - -uint8 MAV_TRAJECTORY_REPRESENTATION_WAYPOINTS = 0 - -uint8 type # Type from MAV_TRAJECTORY_REPRESENTATION enum. - -uint8 POINT_0 = 0 -uint8 POINT_1 = 1 -uint8 POINT_2 = 2 -uint8 POINT_3 = 3 -uint8 POINT_4 = 4 - -uint8 NUMBER_POINTS = 5 - -TrajectoryWaypoint[5] waypoints - -# TOPICS vehicle_trajectory_waypoint vehicle_trajectory_waypoint_desired diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VelocityLimits.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VelocityLimits.msg deleted file mode 100644 index 9ab5115ab..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VelocityLimits.msg +++ /dev/null @@ -1,8 +0,0 @@ -# Velocity and yaw rate limits for a multicopter position slow mode only - -uint64 timestamp # time since system start (microseconds) - -# absolute speeds, NAN means use default limit -float32 horizontal_velocity # [m/s] -float32 vertical_velocity # [m/s] -float32 yaw_rate # [rad/s] diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/VtolVehicleStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/VtolVehicleStatus.msg deleted file mode 100644 index 61a824679..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/VtolVehicleStatus.msg +++ /dev/null @@ -1,12 +0,0 @@ -# VEHICLE_VTOL_STATE, should match 1:1 MAVLinks's MAV_VTOL_STATE -uint8 VEHICLE_VTOL_STATE_UNDEFINED = 0 -uint8 VEHICLE_VTOL_STATE_TRANSITION_TO_FW = 1 -uint8 VEHICLE_VTOL_STATE_TRANSITION_TO_MC = 2 -uint8 VEHICLE_VTOL_STATE_MC = 3 -uint8 VEHICLE_VTOL_STATE_FW = 4 - -uint64 timestamp # time since system start (microseconds) - -uint8 vehicle_vtol_state # current state of the vtol, see VEHICLE_VTOL_STATE - -bool fixed_wing_system_failure # vehicle in fixed-wing system failure failsafe mode (after quad-chute) diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/WheelEncoders.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/WheelEncoders.msg deleted file mode 100644 index a4f3955dc..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/WheelEncoders.msg +++ /dev/null @@ -1,5 +0,0 @@ -uint64 timestamp # time since system start (microseconds) - -# Two wheels: 0 right, 1 left -float32[2] wheel_speed # [rad/s] -float32[2] wheel_angle # [rad] diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/Wind.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/Wind.msg deleted file mode 100644 index ff8b6f453..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/Wind.msg +++ /dev/null @@ -1,16 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float32 windspeed_north # Wind component in north / X direction (m/sec) -float32 windspeed_east # Wind component in east / Y direction (m/sec) - -float32 variance_north # Wind estimate error variance in north / X direction (m/sec)**2 - set to zero (no uncertainty) if not estimated -float32 variance_east # Wind estimate error variance in east / Y direction (m/sec)**2 - set to zero (no uncertainty) if not estimated - -float32 tas_innov # True airspeed innovation -float32 tas_innov_var # True airspeed innovation variance - -float32 beta_innov # Sideslip measurement innovation -float32 beta_innov_var # Sideslip measurement innovation variance - -# TOPICS wind estimator_wind diff --git a/robot/ros_ws/src/local/controls/px4_msgs/msg/YawEstimatorStatus.msg b/robot/ros_ws/src/local/controls/px4_msgs/msg/YawEstimatorStatus.msg deleted file mode 100644 index 36091e26e..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/msg/YawEstimatorStatus.msg +++ /dev/null @@ -1,11 +0,0 @@ -uint64 timestamp # time since system start (microseconds) -uint64 timestamp_sample # the timestamp of the raw data (microseconds) - -float32 yaw_composite # composite yaw from GSF (rad) -float32 yaw_variance # composite yaw variance from GSF (rad^2) -bool yaw_composite_valid - -float32[5] yaw # yaw estimate for each model in the filter bank (rad) -float32[5] innov_vn # North velocity innovation for each model in the filter bank (m/s) -float32[5] innov_ve # East velocity innovation for each model in the filter bank (m/s) -float32[5] weight # weighting for each model in the filter bank diff --git a/robot/ros_ws/src/local/controls/px4_msgs/package.xml b/robot/ros_ws/src/local/controls/px4_msgs/package.xml deleted file mode 100644 index 65291fb62..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/package.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - px4_msgs - 2.0.1 - Package with the ROS-equivalent of PX4 uORB msgs - Nuno Marques - Nuno Marques - BSD 3-Clause - - ament_cmake - rosidl_default_generators - - builtin_interfaces - ros_environment - - rosidl_default_runtime - - ament_lint_common - - rosidl_interface_packages - - - ament_cmake - - diff --git a/robot/ros_ws/src/local/controls/px4_msgs/srv/VehicleCommand.srv b/robot/ros_ws/src/local/controls/px4_msgs/srv/VehicleCommand.srv deleted file mode 100644 index 134e2a810..000000000 --- a/robot/ros_ws/src/local/controls/px4_msgs/srv/VehicleCommand.srv +++ /dev/null @@ -1,3 +0,0 @@ -VehicleCommand request ---- -VehicleCommandAck reply diff --git a/robot/ros_ws/src/local/controls/trajectory_controller/CMakeLists.txt b/robot/ros_ws/src/local/controls/trajectory_controller/CMakeLists.txt index f19e0bd6d..ac396b472 100644 --- a/robot/ros_ws/src/local/controls/trajectory_controller/CMakeLists.txt +++ b/robot/ros_ws/src/local/controls/trajectory_controller/CMakeLists.txt @@ -54,6 +54,7 @@ install(TARGETS trajectory_controller fixed_trajectory_task install(DIRECTORY launch + config DESTINATION share/${PROJECT_NAME}/ ) diff --git a/robot/ros_ws/src/local/controls/trajectory_controller/README.md b/robot/ros_ws/src/local/controls/trajectory_controller/README.md index 21125650b..fa981e7ec 100644 --- a/robot/ros_ws/src/local/controls/trajectory_controller/README.md +++ b/robot/ros_ws/src/local/controls/trajectory_controller/README.md @@ -48,10 +48,6 @@ The `pid_controller` subscribes to `~/tracking_point` and runs a **cascaded (nes Each PID includes an **exponential moving-average filter** on the derivative term (controlled by `_d_alpha`) and **integral windup clamping** against the configured output limits. -### Attitude Controller (`attitude_controller` node) - -An alternative higher-fidelity controller is available in `attitude_controller`. It uses a PD position/velocity loop augmented with an **Extended Kalman Filter (EKF)** for disturbance estimation and compensation. The EKF estimates external forces (e.g. wind) acting on the drone; these estimates are fed forward to cancel the disturbance before the PD feedback acts on the residual error. Gains are grouped as `kp*` (proportional), `kd*` (derivative), `ki*` (integral, in both body and ground frames), and `kf*` (feed-forward scaling). - ## Algorithm ### Core Concepts diff --git a/robot/ros_ws/src/local/controls/trajectory_controller/config/trajectory_controller.yaml b/robot/ros_ws/src/local/controls/trajectory_controller/config/trajectory_controller.yaml new file mode 100644 index 000000000..18c5bfd62 --- /dev/null +++ b/robot/ros_ws/src/local/controls/trajectory_controller/config/trajectory_controller.yaml @@ -0,0 +1,22 @@ +# trajectory_control_node parameters. +# Values moved verbatim from the inline block the legacy +# local_bringup/launch/local.launch.xml carried for trajectory_controller +# (P5-E2, RFC #379); loaded by launch/trajectory_controller.launch.xml. +/**: + ros__parameters: + tf_prefix: "" + target_frame: map + tracking_point_distance_limit: 1000.5 + velocity_look_ahead_time: 0.9 + # look ahead time controls the speed, greater is faster + look_ahead_time: 1.0 + virtual_tracking_ahead_time: 0.5 + min_virtual_tracking_velocity: 0.5 + sphere_radius: 1.0 + ff_min_velocity: 0.0 + search_ahead_factor: 1.5 + transition_velocity_scale: 1.0 + traj_vis_thickness: 0.03 + rewind_skip_max_velocity: 0.1 + rewind_skip_max_distance: 0.1 + velocity_sphere_radius_multiplier: 1.0 diff --git a/robot/ros_ws/src/local/controls/trajectory_controller/launch/fixed_trajectory_task.launch.xml b/robot/ros_ws/src/local/controls/trajectory_controller/launch/fixed_trajectory_task.launch.xml new file mode 100644 index 000000000..4ea4b5d76 --- /dev/null +++ b/robot/ros_ws/src/local/controls/trajectory_controller/launch/fixed_trajectory_task.launch.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/local/controls/trajectory_controller/launch/trajectory_controller.launch.xml b/robot/ros_ws/src/local/controls/trajectory_controller/launch/trajectory_controller.launch.xml index 099ee5b8d..0a4100b33 100644 --- a/robot/ros_ws/src/local/controls/trajectory_controller/launch/trajectory_controller.launch.xml +++ b/robot/ros_ws/src/local/controls/trajectory_controller/launch/trajectory_controller.launch.xml @@ -1,24 +1,33 @@ - - - + + + - - + - + + + - \ No newline at end of file + + + + + diff --git a/robot/ros_ws/src/local/controls/trajectory_controller/launch/trajectory_controller_bag.launch b/robot/ros_ws/src/local/controls/trajectory_controller/launch/trajectory_controller_bag.launch deleted file mode 100644 index d7493916f..000000000 --- a/robot/ros_ws/src/local/controls/trajectory_controller/launch/trajectory_controller_bag.launch +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/robot/ros_ws/src/local/controls/trajectory_controller/package.xml b/robot/ros_ws/src/local/controls/trajectory_controller/package.xml index 6d51bccf9..73d47963e 100644 --- a/robot/ros_ws/src/local/controls/trajectory_controller/package.xml +++ b/robot/ros_ws/src/local/controls/trajectory_controller/package.xml @@ -3,9 +3,9 @@ trajectory_controller 0.0.0 - TODO: Package description - uav - TODO: License declaration + Trajectory tracking controller that manages fixed and stitched-segment trajectories, publishing a tracking point for low-level control and a look-ahead point for local planning. + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/local/local_bringup/CMakeLists.txt b/robot/ros_ws/src/local/local_bringup/CMakeLists.txt deleted file mode 100644 index 19c86fbba..000000000 --- a/robot/ros_ws/src/local/local_bringup/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(local_bringup) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# find dependencies -find_package(ament_cmake REQUIRED) -# uncomment the following section in order to fill in -# further dependencies manually. -# find_package( REQUIRED) - -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - # the following line skips the linter which checks for copyrights - # comment the line when a copyright and license is added to all source files - set(ament_cmake_copyright_FOUND TRUE) - # the following line skips cpplint (only works in a git repo) - # comment the line when this package is in a git repo and when - # a copyright and license is added to all source files - set(ament_cmake_cpplint_FOUND TRUE) - ament_lint_auto_find_test_dependencies() -endif() - -# Install files. -install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) -install(DIRECTORY rviz DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY params DESTINATION share/${PROJECT_NAME}) - -ament_package() diff --git a/robot/ros_ws/src/local/local_bringup/LICENSE b/robot/ros_ws/src/local/local_bringup/LICENSE deleted file mode 100644 index d64569567..000000000 --- a/robot/ros_ws/src/local/local_bringup/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/robot/ros_ws/src/local/local_bringup/launch/local.launch.xml b/robot/ros_ws/src/local/local_bringup/launch/local.launch.xml deleted file mode 100644 index fd46b9282..000000000 --- a/robot/ros_ws/src/local/local_bringup/launch/local.launch.xml +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ?> - - - - - - - - - - - - - ?> - - - \ No newline at end of file diff --git a/robot/ros_ws/src/local/local_bringup/launch/local_droan_cpu.launch.xml b/robot/ros_ws/src/local/local_bringup/launch/local_droan_cpu.launch.xml deleted file mode 100644 index 045f6a08e..000000000 --- a/robot/ros_ws/src/local/local_bringup/launch/local_droan_cpu.launch.xml +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ?> - - - - - - - - - - - ?> - - - \ No newline at end of file diff --git a/robot/ros_ws/src/local/local_bringup/launch/local_macvo_obstacle_avoidance.launch.xml b/robot/ros_ws/src/local/local_bringup/launch/local_macvo_obstacle_avoidance.launch.xml deleted file mode 100644 index 65a8bef88..000000000 --- a/robot/ros_ws/src/local/local_bringup/launch/local_macvo_obstacle_avoidance.launch.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/robot/ros_ws/src/local/local_bringup/package.xml b/robot/ros_ws/src/local/local_bringup/package.xml deleted file mode 100644 index 4075599fa..000000000 --- a/robot/ros_ws/src/local/local_bringup/package.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - local_bringup - 0.0.0 - TODO: Package description - andrew - Apache-2.0 - - ament_cmake - - ament_lint_auto - ament_lint_common - - - ament_cmake - - diff --git a/robot/ros_ws/src/local/local_bringup/rviz/droan.rviz b/robot/ros_ws/src/local/local_bringup/rviz/droan.rviz deleted file mode 100644 index e8754fbc0..000000000 --- a/robot/ros_ws/src/local/local_bringup/rviz/droan.rviz +++ /dev/null @@ -1,675 +0,0 @@ -Panels: - - Class: rviz_common/Displays - Help Height: 78 - Name: Displays - Property Tree Widget: - Expanded: - - /TF1/Frames1 - - /Sensors1 - - /Local1 - - /Local1/DROAN1 - - /Local1/Trajectory Controller1 - - /Global1 - Splitter Ratio: 0.590062141418457 - Tree Height: 1085 - - 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: Expansion Cloud -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: 100 - Reference Frame: - Value: true - - Class: rviz_default_plugins/TF - Enabled: true - Frame Timeout: 15 - Frames: - All Enabled: false - American_Beech: - Value: true - Plane: - Value: true - base_link: - Value: true - base_link_frd: - Value: false - base_link_stabilized: - Value: false - front_stereo: - Value: false - left_camera: - Value: true - look_ahead_point: - Value: false - look_ahead_point_stabilized: - Value: false - map: - Value: true - map_FLU: - Value: false - map_ned: - Value: false - odom: - Value: false - odom_ned: - Value: false - ouster: - Value: false - right_camera: - Value: true - tracking_point: - Value: true - tracking_point_stabilized: - Value: false - world: - Value: false - Marker Scale: 1 - Name: TF - Show Arrows: true - Show Axes: true - Show Names: true - Tree: - world: - American_Beech: - {} - Plane: - {} - map_FLU: - map: - base_link: - base_link_frd: - {} - front_stereo: - left_camera: - {} - right_camera: - {} - ouster: - {} - base_link_stabilized: - {} - look_ahead_point: - {} - look_ahead_point_stabilized: - {} - map_ned: - {} - tracking_point: - {} - tracking_point_stabilized: - {} - Update Interval: 0 - Value: true - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/Image - Enabled: true - Max Value: 1 - Median window: 5 - Min Value: 0 - Name: Front Left RGB - Normalize Range: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: sensors/front_stereo/left/image_rect - Value: true - - Class: rviz_default_plugins/Image - Enabled: true - Max Value: 100 - Median window: 5 - Min Value: 0 - Name: Front Left Depth - Normalize Range: false - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: sensors/front_stereo/left/depth - Value: true - - Alpha: 1 - Autocompute Intensity Bounds: true - Autocompute Value Bounds: - Max Value: 6.571824073791504 - Min Value: -0.5682187080383301 - Value: true - Axis: Z - Channel Name: intensity - Class: rviz_default_plugins/PointCloud2 - Color: 170; 170; 255 - Color Transformer: FlatColor - Decay Time: 0 - Enabled: false - Invert Rainbow: false - Max Color: 255; 255; 255 - Max Intensity: 4096 - Min Color: 0; 0; 0 - Min Intensity: 0 - Name: Lidar - Position Transformer: XYZ - Selectable: true - Size (Pixels): 1 - Size (m): 0.009999999776482582 - Style: Points - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: sensors/ouster/point_cloud - Use Fixed Frame: true - Use rainbow: true - Value: false - - Angle Tolerance: 0 - Class: rviz_default_plugins/Odometry - Covariance: - Orientation: - Alpha: 0.5 - Color: 255; 255; 127 - Color Style: Unique - Frame: Local - Offset: 1 - Scale: 1 - Value: true - Position: - Alpha: 0.30000001192092896 - Color: 204; 51; 204 - Scale: 1 - Value: true - Value: true - Enabled: false - Keep: 1 - Name: Odometry - Position Tolerance: 0 - Shape: - Alpha: 1 - Axes Length: 1 - Axes Radius: 0.10000000149011612 - Color: 255; 25; 0 - Head Length: 0.30000001192092896 - Head Radius: 0.10000000149011612 - Shaft Length: 1 - Shaft Radius: 0.05000000074505806 - Value: Axes - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: odometry_conversion/odometry - Value: false - Enabled: true - Name: Sensors - - Class: rviz_common/Group - Displays: - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/Marker - Enabled: true - Name: Disparity Frustum - Namespaces: - frustum: true - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/droan/frustum - Value: true - - Class: rviz_default_plugins/MarkerArray - Enabled: false - Name: Disparity Map Collision Checking - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/droan/disparity_map_debug - Value: false - - Class: rviz_default_plugins/MarkerArray - Enabled: false - Name: Disparity Graph Poses - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/droan/disparity_graph - Value: false - - Class: rviz_default_plugins/MarkerArray - Enabled: true - Name: Trimmed Global Plan for DROAN - Namespaces: - global_plan: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/local_planner_global_plan_vis - Value: true - - Class: rviz_default_plugins/MarkerArray - Enabled: false - Name: ExpansionPoly - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/expansion_poly - Value: false - - Alpha: 1 - Autocompute Intensity Bounds: true - Autocompute Value Bounds: - Max Value: 10 - Min Value: -10 - Value: true - Axis: Z - Channel Name: intensity - Class: rviz_default_plugins/PointCloud2 - Color: 255; 255; 255 - Color Transformer: Intensity - Decay Time: 0 - Enabled: true - Invert Rainbow: false - Max Color: 255; 255; 255 - Max Intensity: 220 - Min Color: 0; 0; 0 - Min Intensity: 120 - Name: Expansion Cloud - Position Transformer: XYZ - Selectable: true - Size (Pixels): 3 - Size (m): 0.009999999776482582 - Style: Flat Squares - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/expansion_cloud - Use Fixed Frame: true - Use rainbow: true - Value: true - - Class: rviz_default_plugins/MarkerArray - Enabled: true - Name: Traj Library - Namespaces: - trajectory_0: true - trajectory_1: true - trajectory_10: true - trajectory_100: true - trajectory_101: true - trajectory_102: true - trajectory_103: true - trajectory_104: true - trajectory_105: true - trajectory_106: true - trajectory_107: true - trajectory_108: true - trajectory_109: true - trajectory_11: true - trajectory_110: true - trajectory_111: true - trajectory_112: true - trajectory_113: true - trajectory_114: true - trajectory_115: true - trajectory_116: true - trajectory_117: true - trajectory_118: true - trajectory_119: true - trajectory_12: true - trajectory_120: true - trajectory_121: true - trajectory_122: true - trajectory_123: true - trajectory_124: true - trajectory_125: true - trajectory_126: true - trajectory_127: true - trajectory_128: true - trajectory_129: true - trajectory_13: true - trajectory_130: true - trajectory_131: true - trajectory_132: true - trajectory_133: true - trajectory_134: true - trajectory_135: true - trajectory_136: true - trajectory_137: true - trajectory_138: true - trajectory_139: true - trajectory_14: true - trajectory_140: true - trajectory_141: true - trajectory_142: true - trajectory_143: true - trajectory_144: true - trajectory_145: true - trajectory_146: true - trajectory_147: true - trajectory_148: true - trajectory_149: true - trajectory_15: true - trajectory_150: true - trajectory_151: true - trajectory_152: true - trajectory_153: true - trajectory_154: true - trajectory_155: true - trajectory_156: true - trajectory_157: true - trajectory_158: true - trajectory_159: true - trajectory_16: true - trajectory_160: true - trajectory_161: true - trajectory_17: true - trajectory_18: true - trajectory_19: true - trajectory_2: true - trajectory_20: true - trajectory_21: true - trajectory_22: true - trajectory_23: true - trajectory_24: true - trajectory_25: true - trajectory_26: true - trajectory_27: true - trajectory_28: true - trajectory_29: true - trajectory_3: true - trajectory_30: true - trajectory_31: true - trajectory_32: true - trajectory_33: true - trajectory_34: true - trajectory_35: true - trajectory_36: true - trajectory_37: true - trajectory_38: true - trajectory_39: true - trajectory_4: true - trajectory_40: true - trajectory_41: true - trajectory_42: true - trajectory_43: true - trajectory_44: true - trajectory_45: true - trajectory_46: true - trajectory_47: true - trajectory_48: true - trajectory_49: true - trajectory_5: true - trajectory_50: true - trajectory_51: true - trajectory_52: true - trajectory_53: true - trajectory_54: true - trajectory_55: true - trajectory_56: true - trajectory_57: true - trajectory_58: true - trajectory_59: true - trajectory_6: true - trajectory_60: true - trajectory_61: true - trajectory_62: true - trajectory_63: true - trajectory_64: true - trajectory_65: true - trajectory_66: true - trajectory_67: true - trajectory_68: true - trajectory_69: true - trajectory_7: true - trajectory_70: true - trajectory_71: true - trajectory_72: true - trajectory_73: true - trajectory_74: true - trajectory_75: true - trajectory_76: true - trajectory_77: true - trajectory_78: true - trajectory_79: true - trajectory_8: true - trajectory_80: true - trajectory_81: true - trajectory_82: true - trajectory_83: true - trajectory_84: true - trajectory_85: true - trajectory_86: true - trajectory_87: true - trajectory_88: true - trajectory_89: true - trajectory_9: true - trajectory_90: true - trajectory_91: true - trajectory_92: true - trajectory_93: true - trajectory_94: true - trajectory_95: true - trajectory_96: true - trajectory_97: true - trajectory_98: true - trajectory_99: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: droan/trajectory_library_vis - Value: true - Enabled: true - Name: DROAN - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/MarkerArray - Enabled: true - Name: Traj Vis - Namespaces: - traj_controller: true - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: trajectory_controller/trajectory_vis - Value: true - - Class: rviz_default_plugins/MarkerArray - Enabled: false - Name: Traj Debug - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - History Policy: Keep Last - Reliability Policy: Reliable - Value: trajectory_controller/trajectory_controller_debug_markers - Value: false - Enabled: true - Name: Trajectory Controller - Enabled: true - Name: Local - - Class: rviz_common/Group - Displays: - - Class: rviz_default_plugins/Marker - Enabled: false - Name: VDB Mapping Marker - Namespaces: - {} - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: vdb_mapping/vdb_map_visualization - Value: false - - Alpha: 1 - Buffer Length: 1 - Class: rviz_default_plugins/Path - Color: 0; 255; 255 - Enabled: true - Head Diameter: 0.30000001192092896 - Head Length: 0.20000000298023224 - Length: 0.30000001192092896 - Line Style: Billboards - Line Width: 0.10000000149011612 - Name: Global Plan - Offset: - X: 0 - Y: 0 - Z: 0 - Pose Color: 255; 85; 255 - Pose Style: None - Radius: 0.029999999329447746 - Shaft Diameter: 0.10000000149011612 - Shaft Length: 0.10000000149011612 - Topic: - Depth: 5 - Durability Policy: Volatile - Filter size: 10 - History Policy: Keep Last - Reliability Policy: Reliable - Value: /robot_1/global_plan - Value: true - Enabled: true - Name: Global - Enabled: true - Global Options: - Background Color: 48; 48; 48 - Fixed Frame: world - 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: 8.18502426147461 - Enable Stereo Rendering: - Stereo Eye Separation: 0.05999999865889549 - Stereo Focal Distance: 1 - Swap Stereo Eyes: false - Value: false - Focal Point: - X: 3.3486995697021484 - Y: -0.9512473344802856 - Z: 1.4642823934555054 - Focal Shape Fixed Size: false - Focal Shape Size: 0.05000000074505806 - Invert Z Axis: false - Name: Current View - Near Clip Distance: 0.009999999776482582 - Pitch: 0.560396134853363 - Target Frame: - Value: Orbit (rviz) - Yaw: 2.143571615219116 - Saved: ~ -Window Geometry: - Displays: - collapsed: false - Front Left Depth: - collapsed: false - Front Left RGB: - collapsed: false - Height: 1376 - Hide Left Dock: false - Hide Right Dock: false - QMainWindow State: 000000ff00000000fd0000000400000000000001e5000004c6fc0200000009fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b000004c6000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000a0049006d00610067006500000002eb000000c9000000000000000000000001000001f6000004c6fc0200000007fb00000016004c006500660074002000430061006d006500720061010000003b000001880000000000000000fb00000014004c006500660074002000440065007000740068010000003b0000016a0000000000000000fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000001c00460072006f006e00740020004c0065006600740020005200470042010000003b0000020e0000002800fffffffb0000002000460072006f006e00740020004c006500660074002000440065007000740068010000024f000002b20000002800fffffffb0000000a0056006900650077007300000000fd000001a8000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000009ba0000003efc0100000002fb0000000800540069006d00650100000000000009ba0000025300fffffffb0000000800540069006d00650100000000000004500000000000000000000005d3000004c600000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 - Selection: - collapsed: false - Time: - collapsed: false - Tool Properties: - collapsed: false - Views: - collapsed: false - Width: 2490 - X: 1990 - Y: 27 diff --git a/robot/ros_ws/src/local/planners/droan_gl/CMakeLists.txt b/robot/ros_ws/src/local/planners/droan_gl/CMakeLists.txt index 61a470544..4d09ded82 100644 --- a/robot/ros_ws/src/local/planners/droan_gl/CMakeLists.txt +++ b/robot/ros_ws/src/local/planners/droan_gl/CMakeLists.txt @@ -36,7 +36,7 @@ target_link_libraries(droan_gl_node glfw dl assimp EGL GL) # install(TARGETS droan DESTINATION lib/${PROJECT_NAME}) install(TARGETS droan_gl_node DESTINATION lib/${PROJECT_NAME}) -# install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) +install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) install(DIRECTORY shaders DESTINATION share/${PROJECT_NAME}) diff --git a/robot/ros_ws/src/local/planners/droan_gl/config/droan_gl.yaml b/robot/ros_ws/src/local/planners/droan_gl/config/droan_gl.yaml new file mode 100644 index 000000000..32ac1a70d --- /dev/null +++ b/robot/ros_ws/src/local/planners/droan_gl/config/droan_gl.yaml @@ -0,0 +1,28 @@ +# droan_gl node parameters. +# Values moved verbatim from the inline block the legacy +# local_bringup/launch/local.launch.xml carried for droan_gl_node (P5-E2, +# RFC #379); loaded by launch/droan_gl.launch.xml with allow_substs. +/**: + ros__parameters: + target_frame: map + look_ahead_frame: look_ahead_point_stabilized + rewind_info_frame: base_link_stabilized + visualize: true + + # graph / expansion (see gl_interface.cpp) + graph_nodes: 10 + expansion_radius: 2.0 + seen_radius: 1.0 + dt: 0.2 + ht: 7.0 + downsample_scale: 2 + graph_distance_threshold: 1.0 + graph_angle_threshold: 30.0 # degrees + + # rewind monitor (see rewind_monitor.cpp) + all_in_collision_duration_threshold: 2.0 + all_in_collision_rewind_duration: 6.0 + stationary_distance_threshold: 0.5 + stationary_history_duration: 10.0 + stationary_rewind_distance: 10.0 + stationary_rewind_duration: 20.0 diff --git a/robot/ros_ws/src/local/planners/droan_gl/launch/droan_gl.launch.xml b/robot/ros_ws/src/local/planners/droan_gl/launch/droan_gl.launch.xml new file mode 100644 index 000000000..e37ad5dab --- /dev/null +++ b/robot/ros_ws/src/local/planners/droan_gl/launch/droan_gl.launch.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/local/planners/droan_gl/package.xml b/robot/ros_ws/src/local/planners/droan_gl/package.xml index 3e0a29f04..6ac11201b 100644 --- a/robot/ros_ws/src/local/planners/droan_gl/package.xml +++ b/robot/ros_ws/src/local/planners/droan_gl/package.xml @@ -3,9 +3,9 @@ droan_gl 0.0.0 - TODO: Package description - root - TODO: License declaration + GPU-accelerated DROAN local planner that performs true-sphere disparity-space obstacle expansion and collision checking with OpenGL shaders. + Andrew Jong + BSD-3-Clause-Clear ament_cmake @@ -26,6 +26,21 @@ airstack_msgs trajectory_library + + assimp + opengl + libglfw3-dev + libglm-dev + ament_lint_auto ament_lint_common diff --git a/robot/ros_ws/src/local/planners/droan_local_planner/README.md b/robot/ros_ws/src/local/planners/droan_local_planner/README.md index 16a8e68fd..15842f7fe 100644 --- a/robot/ros_ws/src/local/planners/droan_local_planner/README.md +++ b/robot/ros_ws/src/local/planners/droan_local_planner/README.md @@ -16,7 +16,7 @@ The cost map is built by three supporting packages: 2. **disparity_graph** — maintains a rolling graph of (pose, obstacle cloud) observations, discarding old entries as the drone moves 3. **disparity_graph_cost_map** — queries the disparity graph to assign a collision cost to any 3D point in space -The cost map plugin is configurable via the `cost_map` parameter (default: `disparity_graph_cost_map::DisparityGraphCostMap`). +The cost map plugin is configurable via the `cost_map` parameter; the shipped `config/droan.yaml` sets `disparity_graph_cost_map::DisparityGraphCostMap` (the code's declared default is `PointCloudMapRepresentation`). ### 2. Trajectory Library diff --git a/robot/ros_ws/src/local/planners/droan_local_planner/launch/droan_local_planner.launch.xml b/robot/ros_ws/src/local/planners/droan_local_planner/launch/droan_local_planner.launch.xml index 96efd0611..50905f56a 100644 --- a/robot/ros_ws/src/local/planners/droan_local_planner/launch/droan_local_planner.launch.xml +++ b/robot/ros_ws/src/local/planners/droan_local_planner/launch/droan_local_planner.launch.xml @@ -1,5 +1,55 @@ + - - + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + + diff --git a/robot/ros_ws/src/local/planners/droan_local_planner/launch/droan_local_planner_launch.yaml b/robot/ros_ws/src/local/planners/droan_local_planner/launch/droan_local_planner_launch.yaml deleted file mode 100644 index b1a89a75d..000000000 --- a/robot/ros_ws/src/local/planners/droan_local_planner/launch/droan_local_planner_launch.yaml +++ /dev/null @@ -1,16 +0,0 @@ -launch: -# - arg: -# name: "robot_name" -# default: "robot1" -# - arg: -# name: "tf_prefix" -# default: "$(var robot_name)" -- node: - pkg: "droan_local_planner" - exec: "droan_local_planner" - name: "droan_local_planner" - namespace: "droan_local_planner" - param: - - - from: $(find-pkg-share droan_local_planner)/config/droan.yaml - # allow_substs: true diff --git a/robot/ros_ws/src/local/planners/droan_local_planner/package.xml b/robot/ros_ws/src/local/planners/droan_local_planner/package.xml index fa56673cc..7a3fac672 100644 --- a/robot/ros_ws/src/local/planners/droan_local_planner/package.xml +++ b/robot/ros_ws/src/local/planners/droan_local_planner/package.xml @@ -3,9 +3,9 @@ droan_local_planner 0.0.0 - TODO: Package description - andrew - TODO: License declaration + DROAN disparity-space local planner (CPU implementation) that selects the best collision-free trajectory from a trajectory library using a disparity-based cost map. + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/local/planners/takeoff_landing_planner/launch/takeoff_landing_planner.launch.xml b/robot/ros_ws/src/local/planners/takeoff_landing_planner/launch/takeoff_landing_planner.launch.xml index 7b4a46a66..1a5e08fdd 100644 --- a/robot/ros_ws/src/local/planners/takeoff_landing_planner/launch/takeoff_landing_planner.launch.xml +++ b/robot/ros_ws/src/local/planners/takeoff_landing_planner/launch/takeoff_landing_planner.launch.xml @@ -1,18 +1,69 @@ + + - - - - - - - - - + + + + + + + + + + + - - - + + + + + + + + + + + + + - - \ No newline at end of file + + + + + diff --git a/robot/ros_ws/src/local/planners/takeoff_landing_planner/package.xml b/robot/ros_ws/src/local/planners/takeoff_landing_planner/package.xml index a718f1309..b7efbe884 100644 --- a/robot/ros_ws/src/local/planners/takeoff_landing_planner/package.xml +++ b/robot/ros_ws/src/local/planners/takeoff_landing_planner/package.xml @@ -3,9 +3,9 @@ takeoff_landing_planner 0.0.0 - TODO: Package description - uav - TODO: License declaration + ROS 2 action servers for takeoff and landing that generate trajectory overrides to reach target altitude or ground and track completion via position and time thresholds. + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/local/planners/trajectory_library/README.md b/robot/ros_ws/src/local/planners/trajectory_library/README.md index dad899195..db8c65c90 100644 --- a/robot/ros_ws/src/local/planners/trajectory_library/README.md +++ b/robot/ros_ws/src/local/planners/trajectory_library/README.md @@ -2,6 +2,125 @@ Contact: John Keller -Defines some basic trajectory classes and functions for generating and manipulating trajectories. +`trajectory_library` is a C++ library (no node of its own) for generating and manipulating candidate trajectories for obstacle-avoidance planners. It provides the `Trajectory`/`Waypoint` classes used throughout the local layer to interpolate, transform, trim, merge, and visualize waypoint paths, plus a `TrajectoryLibrary` class that loads a *library* of candidate trajectory generators from a YAML config file — the file the DROAN local planner points its `trajectory_library_config` parameter at. Every trajectory converts to/from [`airstack_msgs/msg/TrajectoryXYZVYaw`](../../../../../../common/ros_packages/msgs/airstack_msgs/README.md), the trajectory-controller command type (see the [Interface Conventions Specification §5](../../../../../../docs/robot/autonomy/interface_conventions.md)). -Docs TODO. Help appreciated. \ No newline at end of file +## Core classes + +Defined in [`include/trajectory_library/trajectory_library.hpp`](include/trajectory_library/trajectory_library.hpp), implemented in [`src/trajectory_library.cpp`](src/trajectory_library.cpp): + +| Class | What it is | Key operations | +|---|---|---| +| `Waypoint` | One sample: position, yaw, velocity, acceleration, jerk, time | `interpolate()`, `as_odometry_msg()` (→ `airstack_msgs/Odometry`) | +| `Trajectory` | A timed waypoint sequence in a TF frame; constructible from `TrajectoryXYZVYaw` or `nav_msgs/Path` (positions only, velocity 0) | closest-point queries, `get_waypoint(time)`, `get_odom(time)`, `to_frame()`, `merge()`, `trim()`, `get_trimmed_trajectory_between_distances()`, `get_reversed_trajectory()`, `get_markers()` (RViz), `get_TrajectoryXYZVYaw_msg()` | +| `TrajectoryLibrary` | Loads a set of candidate-trajectory generators from a YAML file | `get_static_trajectories()`, `get_dynamic_trajectories(odom)`, `get_markers()` | + +Waypoint times are generated lazily from positions and speeds (`generate_waypoint_times()`): the time to each waypoint is segment distance divided by the average of the two endpoint speeds (floored at 0.01 m/s). When a `TrajectoryXYZVYaw` is ingested, each waypoint's scalar `velocity` is turned into a velocity *vector* along the local segment direction. + +## Trajectory generator classes + +The library distinguishes **static** generators (fixed shape, computed once, `get_trajectory()`) from **dynamic** generators (recomputed from the robot's current odometry, `get_trajectory(odom)`): + +| Class | Kind | What it generates | Parameters (constructor) | +|---|---|---|---| +| `CurveTrajectory` | static | A constant-speed arc in the x-y plane of `frame`, integrating heading at a fixed turn rate for `time` seconds in `dt` steps; yaw either follows the heading or is fixed | `linear_velocity` (m/s), `angular_velocity` (rad/s), `frame`, `time` (s), `dt` (s), `use_heading`, `yaw` (rad) | +| `AccelerationTrajectory` | dynamic | Forward-integrates the robot's current position/velocity (transformed into `frame`) under a constant acceleration for horizon `ht` in `dt` steps; per-waypoint speed capped at `max_velocity` | `frame`, `ax, ay, az` (m/s²), `dt` (s), `ht` (s), `max_velocity` (m/s) | +| `TakeoffTrajectory` | dynamic | A 3-waypoint vertical (optionally tilted) climb of `height` meters from the current pose, ending with a near-zero-velocity waypoint | `height` (m), `velocity` (m/s), `path_roll`, `path_pitch` (rad), `relative_to_orientation` | + +Only `curve` and `acceleration` can be created from the YAML config; `TakeoffTrajectory` is constructed programmatically (the `takeoff_landing_planner` builds its takeoff/landing trajectories with it). Entries with any other `type` are silently skipped by the parser (`src/trajectory_library.cpp:1284-1344`). + +## Config file format (`trajectory_library_config`) + +`TrajectoryLibrary(config_filename, node_ptr)` loads a YAML file with a single top-level key, `trajectories:`, a list of generator definitions. Each entry's keys depend on its `type`: + +**`type: curve`** → one static `CurveTrajectory`: + +| Key | Unit | Meaning | +|---|---|---| +| `linear_velocity` | m/s | Constant speed along the arc | +| `angular_velocity` | **deg/s** | Turn rate (converted to rad/s at load) | +| `frame` | TF frame | Frame the arc starts at the origin of (e.g. `tracking_point_stabilized`) | +| `time` | s | Duration of the arc | +| `dt` | s | Waypoint spacing in time | +| `yaw` | `heading` or **deg** | The literal string `heading` makes yaw follow the direction of travel; a number fixes yaw to that value | + +**`type: acceleration`** → one dynamic `AccelerationTrajectory`. The acceleration vector is given either componentwise (`x`/`y`/`z`) **or** polar (`magnitude`/`magnitude_yaw`/`magnitude_pitch`); if neither complete set is present the entry is rejected with a console message: + +| Key | Unit | Meaning | +|---|---|---| +| `frame` | TF frame | Frame the integration happens in (e.g. `look_ahead_point_stabilized`) | +| `x`, `y`, `z` | m/s² | Acceleration vector components | +| `magnitude` | m/s² | Alternative: acceleration magnitude… | +| `magnitude_yaw`, `magnitude_pitch` | **deg** | …rotated by this yaw/pitch from the +x axis | +| `dt` | s | Integration/waypoint time step | +| `ht` | s | Horizon time (how long to integrate) | +| `max_velocity` | m/s | Speed cap applied to each generated waypoint | + +**`$(param )` substitution:** any scalar value may be the string `$(param name)`, which is replaced at load time with the value of the ROS parameter `name` on the node that constructed the `TrajectoryLibrary` (parser: `include/trajectory_library/trajectory_library.hpp`, `parse()`). This is how one config file serves different speed profiles: DROAN's `droan.yaml` sets `dt`, `ht`, `ht_long`, `max_velocity`, and `magnitude` as node parameters and the trajectory YAML references them. `TrajectoryLibrary`'s constructor declares these five parameter names (with placeholder defaults) so the substitution always resolves. + +Real excerpt from [`config/long.yaml`](config/long.yaml) — the default library for `droan_local_planner` (an `acceleration` fan: one entry per `magnitude_yaw` heading, 22.5° apart, plus climbing/descending variants): + +```yaml +--- +trajectories: + - dt: $(param dt) + frame: look_ahead_point_stabilized + ht: $(param ht_long) + magnitude: $(param magnitude) + magnitude_pitch: 0 + magnitude_yaw: 0 + max_velocity: $(param max_velocity) + type: acceleration + - dt: $(param dt) + frame: look_ahead_point_stabilized + ht: $(param ht_long) + magnitude: $(param magnitude) + magnitude_pitch: 0 + magnitude_yaw: 22.5 + max_velocity: $(param max_velocity) + type: acceleration + # ... more headings ... +``` + +And a `curve` example from [`config/backup.yaml`](config/backup.yaml): + +```yaml +trajectories: + - type: curve + linear_velocity: 1 + angular_velocity: -45 + frame: tracking_point_stabilized + dt: 0.2 + time: 3 + yaw: heading +``` + +### Shipped config files + +Installed to `share/trajectory_library/config/`; reference them with `$(find-pkg-share trajectory_library)/config/.yaml`: + +| File | Contents | +|---|---| +| `long.yaml` | Acceleration fan over `ht_long` horizon with level/climb/descend pitches — **DROAN's default** | +| `flat.yaml` | Acceleration fan, level flight only (`magnitude_pitch: 0`) | +| `acceleration_magnitudes.yaml` | Large acceleration fan parameterized by `$(param magnitude)` | +| `acceleration_trajectories.yaml`, `acceleration_trajectories_fast.yaml` | Fixed-value (no `$(param)`) acceleration sets at low/high accelerations | +| `demo_trajectory_definitions.yaml` | Slow `curve` set (0.2 m/s) for demos | +| `backup.yaml` | `curve` set in `tracking_point_stabilized` plus a straight acceleration entry | +| `fixed_trajectories.yaml` | **Different schema** — a catalog listing which `attributes` each `airstack_msgs/FixedTrajectory` type (Figure8, Racetrack, Circle, Line, Point) takes; not loadable by `TrajectoryLibrary`, and no trunk code reads it | + +[`src/trajectory_library_generator.py`](src/trajectory_library_generator.py) is a standalone developer script (not installed) that plots `curve`/`arc` config entries with matplotlib for eyeballing a library before flying it. + +## Consumers + +| Package | How it uses this library | +|---|---| +| [`droan_local_planner`](../droan_local_planner/README.md) | Constructs `TrajectoryLibrary` from its `trajectory_library_config` parameter (default `$(find-pkg-share trajectory_library)/config/long.yaml`, set in `config/droan.yaml`); each planning cycle calls `get_dynamic_trajectories(look_ahead_odom)` to get the candidate set it collision-checks and scores | +| [`droan_gl`](../droan_gl/README.md) | Links the library for the `Trajectory`/`Waypoint` utility classes (e.g. wrapping the incoming `nav_msgs/Path` global plan); it does **not** load a YAML library — its candidates come from its own graph expansion | +| [`takeoff_landing_planner`](../takeoff_landing_planner/README.md) | Constructs `TakeoffTrajectory` generators programmatically for takeoff and landing | + +## See also + +- [DROAN Local Planner README](../droan_local_planner/README.md) — the primary consumer of the YAML library +- [DROAN GL README](../droan_gl/README.md) — GPU DROAN variant +- [Trajectory Controller README](../../controls/trajectory_controller/README.md) — where the generated `TrajectoryXYZVYaw` trajectories are sent +- [Interface Conventions Specification](../../../../../../docs/robot/autonomy/interface_conventions.md) — canonical topics/types for the trajectory group diff --git a/robot/ros_ws/src/local/planners/trajectory_library/config/test.yaml b/robot/ros_ws/src/local/planners/trajectory_library/config/test.yaml deleted file mode 100644 index b63102ac2..000000000 --- a/robot/ros_ws/src/local/planners/trajectory_library/config/test.yaml +++ /dev/null @@ -1,1298 +0,0 @@ ---- -trajectories: - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 0 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 15 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -15 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 30 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 0 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: 180 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -22.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -45 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -67.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -90 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -112.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -135 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -30 - magnitude_yaw: -157.5 - max_velocity: $(param max_velocity) - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: 90 - magnitude_yaw: 0 - max_velocity: 0.3 - type: acceleration - - dt: $(param dt) - frame: look_ahead_point_stabilized - ht: $(param ht_long) - magnitude: $(param magnitude) - magnitude_pitch: -90 - magnitude_yaw: 0 - max_velocity: 0.3 - type: acceleration \ No newline at end of file diff --git a/robot/ros_ws/src/local/planners/trajectory_library/package.xml b/robot/ros_ws/src/local/planners/trajectory_library/package.xml index fe4637413..c5044f91e 100644 --- a/robot/ros_ws/src/local/planners/trajectory_library/package.xml +++ b/robot/ros_ws/src/local/planners/trajectory_library/package.xml @@ -3,9 +3,9 @@ trajectory_library 0.0.0 - TODO: Package description - uav - TODO: License declaration + Trajectory classes and generators for creating, transforming, and sampling candidate trajectories used by AirStack planners and controllers. + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/local/planners/trajectory_library/plugin.xml b/robot/ros_ws/src/local/planners/trajectory_library/plugin.xml deleted file mode 100644 index ba4923252..000000000 --- a/robot/ros_ws/src/local/planners/trajectory_library/plugin.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - An interface for choosing fixed trajectories. - - - - - - - system-help - Fixed Trajectory Selector. - - - diff --git a/robot/ros_ws/src/local/planners/trajectory_library/scripts/rqt_fixed_trajectory_selector b/robot/ros_ws/src/local/planners/trajectory_library/scripts/rqt_fixed_trajectory_selector deleted file mode 100644 index 4d4951103..000000000 --- a/robot/ros_ws/src/local/planners/trajectory_library/scripts/rqt_fixed_trajectory_selector +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python - -import sys - -from rqt_fixed_trajectory_selector.rqt_fixed_trajectory_selector import FixedTrajectorySelectorPlugin -from rqt_gui.main import Main - -plugin = 'rqt_fixed_trajectory_selector' -main = Main(filename=plugin) -sys.exit(main.main(standalone=plugin)) diff --git a/robot/ros_ws/src/local/planners/trajectory_library/setup.py b/robot/ros_ws/src/local/planners/trajectory_library/setup.py deleted file mode 100644 index d8e789de9..000000000 --- a/robot/ros_ws/src/local/planners/trajectory_library/setup.py +++ /dev/null @@ -1,9 +0,0 @@ -from distutils.core import setup -from catkin_pkg.python_setup import generate_distutils_setup - -d = generate_distutils_setup( - packages=['rqt_fixed_trajectory_selector'], - package_dir={'': 'src'}, -) - -setup(**d) diff --git a/robot/ros_ws/src/local/planners/trajectory_library/src/rqt_fixed_trajectory_selector/__init__.py b/robot/ros_ws/src/local/planners/trajectory_library/src/rqt_fixed_trajectory_selector/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/local/planners/trajectory_library/src/rqt_fixed_trajectory_selector/rqt_fixed_trajectory_selector.py b/robot/ros_ws/src/local/planners/trajectory_library/src/rqt_fixed_trajectory_selector/rqt_fixed_trajectory_selector.py deleted file mode 100644 index a8fd1a8f5..000000000 --- a/robot/ros_ws/src/local/planners/trajectory_library/src/rqt_fixed_trajectory_selector/rqt_fixed_trajectory_selector.py +++ /dev/null @@ -1,204 +0,0 @@ -import os -import time -import rospy -import rospkg -from std_msgs.msg import String, Bool -from behavior_tree_msgs.msg import Status -import numpy as np -import yaml -import collections - -from qt_gui.plugin import Plugin -import python_qt_binding.QtWidgets as qt -import python_qt_binding.QtCore as core -import python_qt_binding.QtGui as gui - -from python_qt_binding import QT_BINDING, QT_BINDING_VERSION - -from python_qt_binding.QtCore import Slot, Qt, qVersion, qWarning, Signal -from python_qt_binding.QtGui import QColor -from python_qt_binding.QtWidgets import QWidget, QVBoxLayout, QSizePolicy - -from airstack_msgs.msg import FixedTrajectory -from diagnostic_msgs.msg import KeyValue - -class FixedTrajectorySelectorPlugin(Plugin): - def __init__(self, context): - super(FixedTrajectorySelectorPlugin, self).__init__(context) - self.setObjectName('FixedTrajectorySelectorPlugin') - - self.config_filename = '' - - self.button_dct = {} - self.attribute_settings = collections.OrderedDict() - - self.timer = rospy.Timer(rospy.Duration(1./10.), self.timer_callback) - - self.fixed_trajectory_pub = rospy.Publisher('fixed_trajectory_command', FixedTrajectory, queue_size=1) - self.global_plan_fixed_trajectory_pub = rospy.Publisher('global_plan_fixed_trajectory', FixedTrajectory, queue_size=1) - - # main layout - self.widget = QWidget() - self.vbox = qt.QVBoxLayout() - self.widget.setLayout(self.vbox) - context.add_widget(self.widget) - - # config widget - self.config_widget = qt.QWidget() - self.config_widget.setStyleSheet('QWidget{margin-left:-1px;}') - self.config_layout = qt.QHBoxLayout() - self.config_widget.setLayout(self.config_layout) - self.config_widget.setFixedHeight(50) - - self.config_button = qt.QPushButton('Open Config...') - self.config_button.clicked.connect(self.select_config_file) - self.config_layout.addWidget(self.config_button) - - self.config_label = qt.QLabel('config filename: ') - self.config_layout.addWidget(self.config_label) - self.vbox.addWidget(self.config_widget) - - # trajectory widget - self.trajectory_widget = qt.QWidget() - self.trajectory_layout = qt.QVBoxLayout() - self.trajectory_widget.setLayout(self.trajectory_layout) - self.vbox.addWidget(self.trajectory_widget) - - self.tab_widget = qt.QTabWidget() - self.trajectory_layout.addWidget(self.tab_widget) - - # button widget - self.button_widget = qt.QWidget() - self.button_layout = qt.QHBoxLayout() - self.button_widget.setLayout(self.button_layout) - self.vbox.addWidget(self.button_widget) - - self.publish_button = qt.QPushButton('Publish') - self.publish_button.clicked.connect(self.publish_trajectory) - self.button_layout.addWidget(self.publish_button) - - self.trajectory_type_label = qt.QLabel('Type: ') - self.button_layout.addWidget(self.trajectory_type_label) - - self.trajectory_type_combo_box = qt.QComboBox() - self.trajectory_type_combo_box.addItem('Fixed Trajectory') - self.trajectory_type_combo_box.addItem('Global Plan') - self.button_layout.addWidget(self.trajectory_type_combo_box) - - def publish_trajectory(self): - trajectory_type = self.trajectory_type_combo_box.currentText() - trajectory_name = self.tab_widget.tabText(self.tab_widget.currentIndex()) - msg = FixedTrajectory() - msg.type = trajectory_name - for attribute, value in self.attribute_settings[trajectory_name].iteritems(): - key_value = KeyValue() - key_value.key = attribute - key_value.value = value - msg.attributes.append(key_value) - if trajectory_type == 'Fixed Trajectory': - self.fixed_trajectory_pub.publish(msg) - elif trajectory_type == 'Global Plan': - self.global_plan_fixed_trajectory_pub.publish(msg) - - - def select_config_file(self): - starting_path = os.path.join(rospkg.RosPack().get_path('trajectory_library'), 'config') - filename = qt.QFileDialog.getOpenFileName(self.widget, 'Open Config File', starting_path, "Config Files (*.yaml)")[0] - self.set_config(filename) - - def set_config(self, filename): - if filename != '': - self.config_filename = filename - if self.config_filename != None: - self.config_label.setText('config filename: ' + self.config_filename) - self.init_buttons(filename) - - def init_buttons(self, filename): - y = yaml.load(open(filename, 'r').read()) - print(y) - - def get_attribute_changed_function(trajectory_name, attribute_name): - def attribute_changed(text): - if trajectory_name not in self.attribute_settings: - self.attribute_settings[trajectory_name] = {} - self.attribute_settings[trajectory_name][attribute_name] = text - return attribute_changed - - def get_publish_function(trajectory_name): - def publish_function(): - msg = FixedTrajectory() - msg.type = trajectory_name - for attribute, value in self.attribute_settings[trajectory_name].iteritems(): - key_value = KeyValue() - key_value.key = attribute - key_value.value = value - msg.attributes.append(key_value) - self.fixed_trajectory_pub.publish(msg) - return publish_function - - - for trajectory in y['trajectories']: - trajectory_name = trajectory.keys()[0] - attributes = trajectory[trajectory_name]['attributes'] - - trajectory_tab = qt.QWidget() - trajectory_layout = qt.QVBoxLayout() - trajectory_tab.setLayout(trajectory_layout) - - for attribute in attributes: - attribute_widget = qt.QWidget() - attribute_layout = qt.QHBoxLayout() - attribute_widget.setLayout(attribute_layout) - - attribute_label = qt.QLabel() - attribute_label.setText(attribute) - attribute_layout.addWidget(attribute_label) - - attribute_default = '0' - if attribute == 'frame_id': - attribute_default = 'world' - if trajectory_name in self.attribute_settings.keys(): - if attribute in self.attribute_settings[trajectory_name].keys(): - attribute_default = self.attribute_settings[trajectory_name][attribute] - - attribute_edit = qt.QLineEdit() - attribute_edit.textChanged.connect(get_attribute_changed_function(trajectory_name, - attribute)) - attribute_edit.setText(attribute_default) - - attribute_layout.addWidget(attribute_edit) - - trajectory_layout.addWidget(attribute_widget) - - #publish_button = qt.QPushButton('Publish') - #publish_button.clicked.connect(get_publish_function(trajectory_name)) - #trajectory_layout.addWidget(publish_button) - - self.tab_widget.addTab(trajectory_tab, trajectory_name) - - - - def timer_callback(self, msg): - bool_msg = Bool() - for key in self.button_dct.keys(): - bool_msg.data = self.button_dct[key]['data'] - self.button_dct[key]['publisher'].publish(bool_msg) - - def shutdown_plugin(self): - pass - - def save_settings(self, plugin_settings, instance_settings): - instance_settings.set_value('config_filename', self.config_filename) - instance_settings.set_value('attribute_settings', self.attribute_settings) - - def restore_settings(self, plugin_settings, instance_settings): - attribute_settings = instance_settings.value('attribute_settings') - if attribute_settings != None: - self.attribute_settings = attribute_settings - self.set_config(instance_settings.value('config_filename')) - - #def trigger_configuration(self): - # Comment in to signal that the plugin has a way to configure - # This will enable a setting button (gear icon) in each dock widget title bar - # Usually used to open a modal configuration dialog - diff --git a/robot/ros_ws/src/local/world_models/cost_map_interface/package.xml b/robot/ros_ws/src/local/world_models/cost_map_interface/package.xml index 1c29d00a3..3465296a5 100644 --- a/robot/ros_ws/src/local/world_models/cost_map_interface/package.xml +++ b/robot/ros_ws/src/local/world_models/cost_map_interface/package.xml @@ -3,10 +3,10 @@ cost_map_interface 0.0.0 - The cost_map_interface package + Pluginlib base interface that local planners use to query collision costs for candidate trajectories from a cost map implementation. - john - TODO + Andrew Jong + BSD-3-Clause-Clear ament_cmake diff --git a/robot/ros_ws/src/local/world_models/disparity_expansion/README.md b/robot/ros_ws/src/local/world_models/disparity_expansion/README.md index 8ddb1380a..1d58f9dd1 100644 --- a/robot/ros_ws/src/local/world_models/disparity_expansion/README.md +++ b/robot/ros_ws/src/local/world_models/disparity_expansion/README.md @@ -1,4 +1,4 @@ -# README # +# Disparity Expansion This package generates a world representation using disparity images. This enables planning in image space by applying C-space expansion in 2.5D disparity images. diff --git a/robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_expansion.launch.xml b/robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_expansion.launch.xml index 79200d9b3..21682c51c 100644 --- a/robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_expansion.launch.xml +++ b/robot/ros_ws/src/local/world_models/disparity_expansion/launch/disparity_expansion.launch.xml @@ -1,10 +1,43 @@ - + - - - - - - + + + + + + + + + + + + + + + - \ No newline at end of file + + 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/local/world_models/disparity_expansion/package.xml b/robot/ros_ws/src/local/world_models/disparity_expansion/package.xml index 2da6f7f18..0f1aabcfc 100644 --- a/robot/ros_ws/src/local/world_models/disparity_expansion/package.xml +++ b/robot/ros_ws/src/local/world_models/disparity_expansion/package.xml @@ -3,18 +3,18 @@ disparity_expansion 0.0.0 - The disparity_expansion package + C-space expansion of stereo disparity images that inflates obstacles by the robot radius for disparity-space collision checking (DROAN). - aeroscout + Andrew Jong - TODO + BSD-3-Clause-Clear diff --git a/robot/ros_ws/src/local/world_models/disparity_graph/README.md b/robot/ros_ws/src/local/world_models/disparity_graph/README.md index fd2784da1..93b1529ae 100644 --- a/robot/ros_ws/src/local/world_models/disparity_graph/README.md +++ b/robot/ros_ws/src/local/world_models/disparity_graph/README.md @@ -1,6 +1,9 @@ # Disparity Graph +Maintains a sliding graph of keyframe disparity images and their camera poses for occupancy queries by disparity-based cost maps. A single disparity frame only covers the current camera view; keeping a rolling window of past (pose, expanded-disparity) keyframes gives the local planner spatial memory of obstacles the camera is no longer looking at. + +Used by [`disparity_graph_cost_map`](../disparity_graph_cost_map/README.md), which serves collision costs to the [DROAN local planner](../../planners/droan_local_planner/README.md). Expanded disparity inputs come from [`disparity_expansion`](../disparity_expansion/README.md). Contact: Andrew Jong -Docs TODO. Help appreciated. \ No newline at end of file +Docs TODO. Help appreciated. diff --git a/robot/ros_ws/src/local/world_models/disparity_graph/package.xml b/robot/ros_ws/src/local/world_models/disparity_graph/package.xml index 481d5a5cb..885dadb0f 100644 --- a/robot/ros_ws/src/local/world_models/disparity_graph/package.xml +++ b/robot/ros_ws/src/local/world_models/disparity_graph/package.xml @@ -3,9 +3,9 @@ disparity_graph 0.0.0 - TODO: Package description - andrew - TODO: License declaration + Maintains a sliding graph of keyframe disparity images and poses for occupancy queries by disparity-based cost maps. + Andrew Jong + BSD-3-Clause-Clear ament_cmake_ros diff --git a/robot/ros_ws/src/local/world_models/disparity_graph_cost_map/README.md b/robot/ros_ws/src/local/world_models/disparity_graph_cost_map/README.md index dd477e3b4..e6a2df986 100644 --- a/robot/ros_ws/src/local/world_models/disparity_graph_cost_map/README.md +++ b/robot/ros_ws/src/local/world_models/disparity_graph_cost_map/README.md @@ -1,6 +1,7 @@ # Disparity Graph Cost Map +A cost map plugin backed by a [disparity graph](../disparity_graph/README.md): it answers "what is the collision cost of this 3D point?" by projecting the query point into the expanded disparity keyframes stored in the graph. The [DROAN local planner](../../planners/droan_local_planner/README.md) loads it via its `cost_map` parameter (`disparity_graph_cost_map::DisparityGraphCostMap`) to score candidate trajectories. Contact: Andrew Jong -Docs TODO. Help appreciated. \ No newline at end of file +Docs TODO. Help appreciated. diff --git a/robot/ros_ws/src/local/world_models/disparity_graph_cost_map/package.xml b/robot/ros_ws/src/local/world_models/disparity_graph_cost_map/package.xml index e762a3229..4cbb781b0 100644 --- a/robot/ros_ws/src/local/world_models/disparity_graph_cost_map/package.xml +++ b/robot/ros_ws/src/local/world_models/disparity_graph_cost_map/package.xml @@ -3,10 +3,10 @@ disparity_graph_cost_map 0.0.0 - The disparity_graph_cost_map package + Cost map plugin backed by a disparity graph, providing trajectory collision costs to the DROAN local planner. - john - TODO + Andrew Jong + BSD-3-Clause-Clear ament_cmake 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/natnet_ros2/.gitignore b/robot/ros_ws/src/perception/natnet_ros2/.gitignore deleted file mode 100644 index f4a20d2b9..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# Ignore proprietary NatNetSDK (downloaded at setup time) -lib/libNatNet.so -include/natnet/ - -# Python runtime and cache -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -env/ -venv/ -ENV/ -*.egg-info/ -dist/ -build/ - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# ROS build artifacts -devel/ -install/ diff --git a/robot/ros_ws/src/perception/natnet_ros2/CMakeLists.txt b/robot/ros_ws/src/perception/natnet_ros2/CMakeLists.txt deleted file mode 100644 index 7d762f688..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/CMakeLists.txt +++ /dev/null @@ -1,100 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(natnet_ros2) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# ament / ROS 2 dependencies -find_package(ament_cmake REQUIRED) -find_package(rclcpp REQUIRED) -find_package(geometry_msgs REQUIRED) -find_package(nav_msgs REQUIRED) - -# --------------------------------------------------------------------------- -# NatNet SDK — pre-built shared library downloaded via `airstack setup`. -# Headers: include/natnet/ Library: lib/libNatNet.so -# -# The SDK is proprietary and is NOT committed to the repository. -# Run `airstack setup` (or `airstack setup --natnet`) to download and place -# the SDK files before building this package. -# -# If the SDK is absent the C++ node is skipped with a warning; the Python -# vision_pose_converter_node.py and all launch/config files are still installed -# so the rest of the autonomy stack builds cleanly. -# --------------------------------------------------------------------------- -set(_NATNET_LIB "${CMAKE_CURRENT_SOURCE_DIR}/lib/libNatNet.so") -set(_NATNET_INC "${CMAKE_CURRENT_SOURCE_DIR}/include/natnet") - -if(EXISTS "${_NATNET_LIB}" AND EXISTS "${_NATNET_INC}") - add_library(NatNet SHARED IMPORTED) - set_target_properties(NatNet PROPERTIES - IMPORTED_LOCATION "${_NATNET_LIB}" - INTERFACE_INCLUDE_DIRECTORIES "${_NATNET_INC}" - ) - - # C++ NatNet ROS 2 node - add_executable(natnet_ros2_node - src/natnet_ros2_node.cpp - src/natnet_client_adapter.cpp) - target_include_directories(natnet_ros2_node PUBLIC - $ - $) - ament_target_dependencies(natnet_ros2_node rclcpp geometry_msgs nav_msgs) - target_link_libraries(natnet_ros2_node NatNet) - - install(TARGETS natnet_ros2_node - DESTINATION lib/${PROJECT_NAME}) - - # Install libNatNet.so alongside the node and register an environment hook so - # that sourcing the workspace adds lib/natnet_ros2/ to LD_LIBRARY_PATH. - # Use PROGRAMS (not FILES) to preserve the execute bit - install(PROGRAMS "${_NATNET_LIB}" - DESTINATION lib/${PROJECT_NAME}) - - ament_environment_hooks( - "${CMAKE_CURRENT_SOURCE_DIR}/env-hooks/natnet_library_path.dsv.in" - ) -else() - message(WARNING - "[natnet_ros2] NatNet SDK not found — skipping natnet_ros2_node build.\n" - " Expected: ${_NATNET_LIB}\n" - " ${_NATNET_INC}/\n" - " Run 'airstack setup' to download the OptiTrack NatNet SDK.") -endif() - -# --------------------------------------------------------------------------- -# Python nodes -# --------------------------------------------------------------------------- -install(PROGRAMS - src/vision_pose_converter_node.py - src/mavros_gp_origin_node.py - src/px4_param_setter_node.py - DESTINATION lib/${PROJECT_NAME}) - -# --------------------------------------------------------------------------- -# Launch and config files -# --------------------------------------------------------------------------- -install(DIRECTORY launch/ - DESTINATION share/${PROJECT_NAME}/launch) - -install(DIRECTORY config/ - DESTINATION share/${PROJECT_NAME}/config) - -install(DIRECTORY include/ - DESTINATION include) - -# --------------------------------------------------------------------------- -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - ament_lint_auto_find_test_dependencies() - - # --- gtest: pure-logic unit tests (no NatNet SDK, no rclcpp) --- - find_package(ament_cmake_gtest REQUIRED) - ament_add_gtest(test_natnet_logic test/test_natnet_logic.cpp) - target_include_directories(test_natnet_logic PRIVATE - $ - $) -endif() - -ament_package() diff --git a/robot/ros_ws/src/perception/natnet_ros2/README.md b/robot/ros_ws/src/perception/natnet_ros2/README.md deleted file mode 100644 index 8a3970b4f..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/README.md +++ /dev/null @@ -1,269 +0,0 @@ -# NatNet ROS 2 Wrapper - -OptiTrack NatNet ROS 2 wrapper for motion capture integration in AirStack (optional). Receives rigid body pose data from an external Motive PC via NatNet UDP protocol and publishes into the AirStack perception layer. - -**Note:** This module is only required if you intend to use OptiTrack Motive motion capture systems. If you do not plan to use OptiTrack, you can skip the NatNet SDK setup with `airstack setup --no-natnet`. - -### OptiTrack room calibration - -If rigid bodies are jumping around or not tracking well, consider re-calibrating the capture volume in Motive. See the [OptiTrack Motive calibration guide](https://docs.optitrack.com/motive/calibration). - -## Overview - -This module provides a bridge between OptiTrack Motive motion capture systems and the AirStack autonomy stack. It: - -- Receives **NatNet UDP packets** from an external Motive PC (configurable IP/port) -- **Decodes motion capture frames** containing rigid body positions and orientations -- **Publishes pose data** to the AirStack perception layer in standard ROS 2 formats -- **Tracks multiple rigid bodies per robot** (e.g. a drone for state estimation plus a separate target), each mapped to its own topic -- **Supports multi-robot** via per-robot profiles selected by `ROBOT_NAME` -- **Optionally bridges** to MAVROS for PX4 external pose feedback (per-robot) -- **Respects OptiTrack licensing** by keeping the NatNet SDK external (host-side download with explicit consent) - -## Architecture - -``` -Motive (External PC) - ↓ NatNet UDP (port 1511) - ↓ -NatNet ROS 2 Node (loads the ROBOT_NAME profile from natnet_config.yaml) - │ per configured body (one or more): - ├→ /{ROBOT_NAME}/{topic} (PoseStamped, when pose: true) - ├→ /{ROBOT_NAME}/{topic}/pose_cov (PoseWithCovarianceStamped, when pose_cov: true) - └→ (Optional, vision_pose.enabled: true) - mavros_gp_origin_node - └→ /{ROBOT_NAME}/interface/mavros/global_position/set_gp_origin - px4_param_setter_node - └→ /{ROBOT_NAME}/interface/mavros/param/set (external-vision PX4 params) - vision_pose_converter_node (reads input/output topics from the profile) - ├→ /{ROBOT_NAME}/interface/mavros/vision_pose/pose - └→ /{ROBOT_NAME}/interface/mavros/vision_pose/pose_cov -``` - -## Interfaces - -### Inputs - -- **Network**: NatNet UDP stream from Motive PC (external network) -- **Configuration**: `natnet_config.yaml` — generic `server` settings plus a `robots` map of per-robot profiles (body list + optional MAVROS `vision_pose` block). The launch file selects the profile matching `ROBOT_NAME`. - -### Outputs - -For each rigid body in the robot's profile, `topic` is a **relative** leaf namespaced -under `/{ROBOT_NAME}/` (it defaults to `perception/optitrack/{rigid_body_name}` when -omitted): - -#### Direct OptiTrack pose - -- **Topic**: `/{ROBOT_NAME}/{topic}` -- **Type**: `geometry_msgs/PoseStamped` -- **Description**: Position and orientation only (no covariance) -- **Enabled by**: `pose: true` on that body (per body) - -#### Pose with covariance - -- **Topic**: `/{ROBOT_NAME}/{topic}/pose_cov` -- **Type**: `geometry_msgs/PoseWithCovarianceStamped` -- **Description**: Same pose plus a 6×6 covariance matrix from that body's `position_covariance` / `orientation_covariance`. -- **Enabled by**: `pose_cov: true` on that body (per body) - -#### MAVROS vision pose bridge (optional, per robot) - -When the robot's `vision_pose.enabled: true`, `vision_pose_converter_node` subscribes to the configured `input_topic` (a body's `pose_cov`) and republishes for PX4 on the configured outputs: - -- **Topic** (`output_pose_topic`): `/{ROBOT_NAME}/interface/mavros/vision_pose/pose` — `geometry_msgs/PoseStamped` (pose extracted from the covariance message) -- **Topic** (`output_pose_cov_topic`): `/{ROBOT_NAME}/interface/mavros/vision_pose/pose_cov` — `geometry_msgs/PoseWithCovarianceStamped` (full message, quaternion optionally canonicalized) -- **Enabled by**: `vision_pose.enabled: true` in the robot's profile -- **Retargetable**: change `input_topic` / `output_pose_topic` / `output_pose_cov_topic` (relative, namespaced) to bridge to other middleware -- **PX4 side**: set `SITL_PARAM_PROFILE=px4-vision` in `.env` so Isaac SITL loads EKF2 external-vision params from `simulation/isaac-sim/docker/sitl-files/px4-vision.env` - -##### Synthetic GPS origin (mocap / no-GNSS arming) - -With GNSS disabled (`EKF2_GPS_CTRL=0`), PX4 fused EKF has **no global position**. This fails preflight checks and refuse to arm. When `vision_pose.enabled: true`, -`mavros_gp_origin_node` publishes a synthetic origin once at startup: - -- **Topic**: `/{ROBOT_NAME}/interface/mavros/global_position/set_gp_origin` — `geographic_msgs/GeoPointStamped` -- **Guarded**: waits for `mavros/state.connected`, then publishes only if no - origin already exists (it watches `…/global_position/gp_origin`), so a - GNSS-equipped vehicle is left untouched. -- **Params** (`config/mavros_gp_origin.yaml`): `enabled` (default `true`), - `latitude/longitude/altitude` (default Lisbon — the AirStack shared world - datum; **must match** the GCS origin in `gcs_visualizer/gcs_utils.py` and the - sim's `gps_utils.py` so Foxglove waypoints transform 1:1), `settle_sec`. - Set `enabled: false` to rely on real GNSS. - -##### PX4 parameter enforcement (external-vision EKF2 setup) - -When `vision_pose.enabled: true`, `px4_param_setter_node` pushes the PX4 -parameter set for OptiTrack-only flight through the MAVROS param plugin at -startup, so the FCU doesn't need manual QGroundControl configuration: - -- **Services used**: `/{ROBOT_NAME}/interface/mavros/param/get_parameters` - (read current), `…/param/set` (`mavros_msgs/ParamSetV2`, set + verify readback) -- **Idempotent**: waits for `mavros/state.connected` + `settle_sec` (initial - param-table pull), reads each param first, and skips ones already correct — - PX4 persists parameters, so subsequent boots are a verify-only pass. -- **Reboot warning**: if any parameter actually changed, it logs a warning to - reboot the FCU before flight so EKF2 restarts with a clean fusion config. -- **Params** (`config/px4_params.yaml`): `enabled`, `settle_sec`, - `retry_period_sec`, `max_attempts`, and the `params.*` map of desired FCU - values — external-vision fusion (`EKF2_EV_CTRL: 11`, `EKF2_HGT_REF: 3`), - GPS/mag/baro disabled (`EKF2_GPS_CTRL: 0`, `EKF2_MAG_TYPE: 5`, - `EKF2_BARO_CTRL: 0`), measured vision delay (`EKF2_EV_DELAY: 6.0` ms), and - EV noise floors (`EKF2_EV_NOISE_MD: 1`, `EKF2_EVP_NOISE`, `EKF2_EVA_NOISE`). - YAML type selects the MAVLink param type: write floats with a decimal point - (`6.0`), integers bare. Values assume PX4 ≥ 1.14; for older firmware use - `EKF2_AID_MASK: 24` / `EKF2_HGT_MODE: 3` instead. - -## Configuration - -`config/natnet_config.yaml` uses a custom `natnet:` schema (not a flat ROS 2 param -file): generic `server` settings shared by every agent, then a `robots` map of -per-robot profiles. The launch file parses it, selects the profile matching the -container's `ROBOT_NAME`, flattens the body list into node parameters, and brings up -the MAVROS bridge only when that robot's `vision_pose.enabled` is true. - -```yaml -natnet: - server: # generic across all agents - server_ip: "$(env NATNET_SERVER_IP 172.31.0.200)" - client_ip: "0.0.0.0" - command_port: 1510 - data_port: 1511 - connection_type: "unicast" # or "multicast" - multicast_address: "239.255.42.99" - frame_id: "world" - debug: false - robots: - robot_1: - vision_pose: # per-robot MAVROS bridge (omit/false to skip) - enabled: true - input_topic: "perception/optitrack/drone/pose_cov" - output_pose_topic: "interface/mavros/vision_pose/pose" - output_pose_cov_topic: "interface/mavros/vision_pose/pose_cov" - bodies: # one or more tracked rigid bodies - - rigid_body_name: "Drone" # Motive name (case-sensitive) - id: 1 # Motive streaming id - topic: "perception/optitrack/drone" # relative → /{ROBOT_NAME}/ - pose: true # publish PoseStamped - pose_cov: true # publish PoseWithCovarianceStamped - position_covariance: [1.0e-6, 0, 0, 0, 1.0e-6, 0, 0, 0, 1.0e-6] - orientation_covariance: [3.0e-6, 0, 0, 0, 3.0e-6, 0, 0, 0, 3.0e-6] -``` - -To track an additional body (e.g. a target) for a robot, add another entry under that -robot's `bodies`. To add a robot, add a new key under `robots`. The shipped file -includes commented scaffolding for a 3-drone fleet where `robot_1` and `robot_2` also -track a shared `Target` body and `robot_3` tracks only its drone. - -## Launch - -### Basic launch - -Parameters come from `config/natnet_config.yaml` (server + the `ROBOT_NAME` profile). Optional overrides: - -```bash -ros2 launch natnet_ros2 natnet_ros2.launch.py \ - config_file:=/path/to/custom_natnet.yaml \ - vision_pose_config_file:=/path/to/custom_vision_pose.yaml \ - use_sim_time:=true -``` - -### MAVROS bridge - -Set `vision_pose.enabled: true` in the robot's profile. The launch file includes `vision_pose_converter.launch.xml` (plus `mavros_gp_origin.launch.xml` and `px4_param_setter.launch.xml`) and forwards the profile's `input_topic` / `output_pose_topic` / `output_pose_cov_topic`. - -### From perception bringup - -With `LAUNCH_NATNET=true` in `.env`, `perception.launch.xml` includes `natnet_ros2.launch.py`. - -## Dependencies - -### Runtime -- `rclpy` — ROS 2 Python client -- `geometry_msgs` — Standard pose message types -- `tf_transformations` — Quaternion and rotation utilities -- `mavros_msgs` — Optional, for MAVROS bridge - -### Required -- **OptiTrack NatNet SDK** (Linux SDK) — **REQUIRED**, downloaded via `airstack setup` - -### Installation -To install the NatNet SDK and accept the license: -```bash -airstack setup -``` -The SDK will be installed into `robot/ros_ws/src/perception/natnet_ros2/lib/` and `robot/ros_ws/src/perception/natnet_ros2/include/natnet/` after accepting the OptiTrack License Agreement. - -## Implementation Details - -### Protocol Support -- **NatNet Version**: 4.4+ (SDK handles protocol negotiation) -- **Packet Type**: Frame of Data with rigid bodies and markers -- **Transport**: UDP (configurable port, default 1511) -- **SDK**: OptiTrack NatNet SDK handles all protocol parsing - -### Multi-Robot Support -Each container instance gets its own `ROBOT_NAME` and `ROS_DOMAIN_ID`: -- The node loads the `robots[$ROBOT_NAME]` profile, so each robot tracks only the bodies (and runs the MAVROS bridge) configured for it. -- Topics are namespaced under `/{ROBOT_NAME}/` from each body's relative `topic`. -- Set `NUM_ROBOTS=N`; each replica resolves its own `ROBOT_NAME` (via `resolve_robot_name.py`) and auto-selects its profile — no per-robot env overrides. - -### Error Handling -- Invalid/malformed packets are skipped with debug logging -- Lost connectivity logs warnings; gracefully recovers when stream resumes -- Covariance in config allows tuning uncertainty per deployment -- **Connect retry:** the initial handshake is retried every 2 s until it - succeeds, so the node tolerates the NatNet server starting *after* the robot - (e.g. a Motive PC powered on later, or the Isaac Sim NatNet emulator which only - binds ~100 s into sim boot). The retry timer cancels itself on first success. - -## Testing - -### With Real Motive -1. Ensure Motive PC and robot are on same network -2. Configure server IP in `natnet_config.yaml` -3. Launch the node: - ```bash - ros2 launch natnet_ros2 natnet_ros2.launch.py - ``` -4. Verify topics (default profile maps the `Drone` body to `perception/optitrack/drone`): - ```bash - ros2 topic echo /robot_1/perception/optitrack/drone/pose_cov - ``` - -### Without Real Hardware (Mock) -TODO: Implement Motive simulator in Isaac Sim to generate fake NatNet packets - -## Known Limitations - -- The node publishes only bodies listed in the robot's profile (matched by `id`); bodies streamed by Motive but absent from the profile are ignored. -- MAVROS bridge applies frame_id override and quaternion canonicalization; full PX4 frame alignment may still need tuning per airframe -- No support for skeleton tracking or labeled markers yet (future enhancement) - -## References - -- [OptiTrack NatNet Protocol Documentation](https://docs.optitrack.com/developer-tools/natnet-sdk/natnet-4.0) -- [NatNet SDK Download](https://optitrack.com/software/natnet-sdk/) -- [MAVROS Vision Pose Plugin](https://docs.ros.org/en/melodic/api/mavros_extras/html/classmavros_1_1extra__plugins_1_1VisionPoseEstimatePlugin.html) - -## Troubleshooting - -### No data being received -- Check Motive PC IP address in config -- Verify UDP port is not blocked by firewall -- Use `ros2 topic hz` to check if data is arriving - -### Topics not published -- Check `ros2 node list` — should see `natnet_ros2_node` -- Check `ros2 topic list | grep optitrack` — should see published topics -- Look at logs: `ros2 node info natnet_ros2_node` - -### Low frame rate or dropped frames -- Reduce other network traffic -- Check NatNet streaming rate in Motive (default 120 Hz) -- Monitor CPU usage: `docker stats` - -## License - -**Note on NatNet SDK Licensing**: The OptiTrack NatNet SDK is proprietary software governed by the OptiTrack Software License Agreement. Users download and install the SDK locally under their own license compliance. AirStack does not redistribute the SDK and remains fully open-source. diff --git a/robot/ros_ws/src/perception/natnet_ros2/config/mavros_gp_origin.yaml b/robot/ros_ws/src/perception/natnet_ros2/config/mavros_gp_origin.yaml deleted file mode 100644 index 0035f017b..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/config/mavros_gp_origin.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Synthetic GPS origin for mocap / no-GNSS flight via MAVROS. -# Loaded by mavros_gp_origin.launch.xml when publish_to_mavros is enabled. -# See docs/robot/px4_external_vision.md for the height-datum explanation. - -/**: - ros__parameters: - # Skipped if an origin already exists (e.g. a GNSS-equipped vehicle). - enabled: true - # MUST match the GCS world origin (gcs_visualizer/gcs_utils.py) and the sim - # datum (launch_scripts/gps_utils.py), or the relay computes a huge ENU offset. - latitude: 38.736832 - longitude: -9.137977 - # Shared world datum; used directly when use_geoid_altitude is false or in sim. - altitude: 90.0 - # Real hardware: derive origin altitude from the geoid so local_position z - # equals OptiTrack height. Auto-skipped in sim. - use_geoid_altitude: true - # AMSL of the mocap floor. 36.0 = the shared world datum (90 m ellipsoidal) in AMSL, - # so the robot's global altitude agrees with sim and the GCS. - desired_floor_amsl: 36.0 - geoid_model: "egm96-5" - settle_sec: 5.0 diff --git a/robot/ros_ws/src/perception/natnet_ros2/config/natnet_config.yaml b/robot/ros_ws/src/perception/natnet_ros2/config/natnet_config.yaml deleted file mode 100644 index 6ad16e074..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/config/natnet_config.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# NatNet ROS 2 configuration — parsed by natnet_ros2.launch.py, which selects the -# profile matching the container's ROBOT_NAME. -# See docs/robot/px4_external_vision.md for the schema and setup guide. - -natnet: - - # --- Connection settings (generic across all agents) ----------------------- - server: - # Motive host; defaults to the in-sim emulator. Set NATNET_SERVER_IP per deployment. - server_ip: "$(env NATNET_SERVER_IP 172.31.0.200)" - # Bind explicitly when the client has multiple NICs. - client_ip: "0.0.0.0" - - command_port: 1510 - data_port: 1511 - - # "unicast" (default) or "multicast"; multicast_address applies to the latter. - connection_type: "unicast" - multicast_address: "239.255.42.99" - - frame_id: "world" - debug: false - - # Per-message latency reporting. - latency_sampling_warmup_s: 5.0 - latency_sampling_window_s: 20.0 - cube_orange_latency_ms: 5.0 - - # --- Per-robot profiles (selected by ROBOT_NAME) --------------------------- - robots: - - robot_1: - # MAVROS vision_pose bridge; enabled=false skips the converter. - vision_pose: - enabled: true - input_topic: "perception/optitrack/drone/pose_cov" - output_pose_topic: "interface/mavros/vision_pose/pose" - output_pose_cov_topic: "interface/mavros/vision_pose/pose_cov" - - # Rigid bodies this robot tracks. name and id must match Motive exactly; the - # client filters frames by numeric id. Defaults match the in-sim emulator. - bodies: - - rigid_body_name: "Drone" - id: 1 - topic: "perception/optitrack/drone" - pose: true - pose_cov: true - position_covariance: - [1.0e-6, 0.0, 0.0, - 0.0, 1.0e-6, 0.0, - 0.0, 0.0, 1.0e-6] - orientation_covariance: - [3.0e-6, 0.0, 0.0, - 0.0, 3.0e-6, 0.0, - 0.0, 0.0, 3.0e-6] - robot_2: - vision_pose: - enabled: true - input_topic: "perception/optitrack/drone/pose_cov" - output_pose_topic: "interface/mavros/vision_pose/pose" - output_pose_cov_topic: "interface/mavros/vision_pose/pose_cov" - bodies: - - rigid_body_name: "Drone2" - id: 2 - topic: "perception/optitrack/drone" - pose: true - pose_cov: true - - rigid_body_name: "Target" # shared target — also tracked by robot_1 - id: 100 - topic: "perception/optitrack/target" - pose: true - pose_cov: false - position_covariance: - [1.0e-6, 0.0, 0.0, - 0.0, 1.0e-6, 0.0, - 0.0, 0.0, 1.0e-6] - orientation_covariance: - [3.0e-6, 0.0, 0.0, - 0.0, 3.0e-6, 0.0, - 0.0, 0.0, 3.0e-6] - - robot_3: - vision_pose: - enabled: true - input_topic: "perception/optitrack/drone/pose_cov" - output_pose_topic: "interface/mavros/vision_pose/pose" - output_pose_cov_topic: "interface/mavros/vision_pose/pose_cov" - bodies: - - rigid_body_name: "Drone3" - id: 3 - topic: "perception/optitrack/drone" - pose: true - pose_cov: true - position_covariance: - [1.0e-6, 0.0, 0.0, - 0.0, 1.0e-6, 0.0, - 0.0, 0.0, 1.0e-6] - orientation_covariance: - [3.0e-6, 0.0, 0.0, - 0.0, 3.0e-6, 0.0, - 0.0, 0.0, 3.0e-6] diff --git a/robot/ros_ws/src/perception/natnet_ros2/config/px4_params.yaml b/robot/ros_ws/src/perception/natnet_ros2/config/px4_params.yaml deleted file mode 100644 index c2bd3b7be..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/config/px4_params.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# PX4 parameters for OptiTrack-only (external vision) flight, checked at startup by -# px4_param_setter. See docs/robot/px4_external_vision.md for what each one does, the -# tuning rationale, and how to set them in QGroundControl. -# -# TYPE MATTERS: integers bare (11), floats with a decimal point (7.0), so the MAVLink -# param type matches the FCU's declaration. -# -# Values assume PX4 >= 1.14. For older firmware use EKF2_AID_MASK: 24 and -# EKF2_HGT_MODE: 3 instead. - -/**: - ros__parameters: - enabled: true - # Check-only by default: read the FCU's params and flag differences, never write. - auto_set: false - # On mismatch with auto_set:false — 'warn' (log diffs) or 'halt' (exit non-zero). - on_mismatch: "warn" - # Initial full param pull over serial (115200) is slow; give it time. - settle_sec: 10.0 - retry_period_sec: 2.0 - max_attempts: 30 - - params: - # Fuse vision horizontal position (1) + vertical position (2) + yaw (8). - EKF2_EV_CTRL: 11 - # Vision is the height reference. - EKF2_HGT_REF: 3 - EKF2_GPS_CTRL: 0 - # Magnetometer off; yaw comes from vision. - EKF2_MAG_TYPE: 5 - EKF2_BARO_CTRL: 0 - # Remove the baro at system level, not just from fusion, so the height datum is - # deterministic on every boot. WARNING: no baro backup — indoor mocap only. - SYS_HAS_BARO: 0 - EKF2_RNG_CTRL: 0 - # Do NOT raise to chase apparent lag; higher is measurably worse (see docs). - EKF2_EV_DELAY: 7.0 - # Use the NOISE floors below rather than the message covariance. - EKF2_EV_NOISE_MD: 1 - # Also sets the innovation gate (EKF2_EVP_GATE sigma wide): 0.05 -> ~25 cm. - EKF2_EVP_NOISE: 0.05 - EKF2_EVA_NOISE: 0.05 - COM_ARM_WO_GPS: 1 diff --git a/robot/ros_ws/src/perception/natnet_ros2/config/vision_pose_converter.yaml b/robot/ros_ws/src/perception/natnet_ros2/config/vision_pose_converter.yaml deleted file mode 100644 index f14b4d2d7..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/config/vision_pose_converter.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Vision pose converter → MAVROS bridge parameters. -# Loaded by vision_pose_converter.launch.xml via . - -/**: - ros__parameters: - frame_id: "world" - child_frame_id: "$(env ROBOT_NAME robot_1)/base_link" - # Normalise quaternion to canonical form (qw >= 0) before publishing. - canonical_quaternion: true - # Cap the rate forwarded to MAVROS (0 = passthrough); EKF2 needs only 30-50 Hz. - max_rate_hz: 50.0 - # Which vision_pose topic(s) to forward: 'pose', 'pose_cov', or 'both'. - publish_mode: "pose_cov" diff --git a/robot/ros_ws/src/perception/natnet_ros2/env-hooks/natnet_library_path.dsv.in b/robot/ros_ws/src/perception/natnet_ros2/env-hooks/natnet_library_path.dsv.in deleted file mode 100644 index 8046498e2..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/env-hooks/natnet_library_path.dsv.in +++ /dev/null @@ -1 +0,0 @@ -prepend-non-duplicate;LD_LIBRARY_PATH;lib/natnet_ros2 diff --git a/robot/ros_ws/src/perception/natnet_ros2/include/natnet_ros2/natnet_client_adapter.hpp b/robot/ros_ws/src/perception/natnet_ros2/include/natnet_ros2/natnet_client_adapter.hpp deleted file mode 100644 index b64b254df..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/include/natnet_ros2/natnet_client_adapter.hpp +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// natnet_client_adapter.hpp — declaration of NatNetClientAdapter. -// -// NatNetClientAdapter wraps the NatNet SDK's NatNetClient and implements -// INatNetClient. It is the only place in the codebase that includes NatNet -// SDK headers; unit tests use FakeNatNetClient instead. -// -// Implementation: src/natnet_client_adapter.cpp - -#pragma once - -#include "natnet_ros2/natnet_logic.hpp" - -#include -#include -#include - -// Forward-declare the SDK type so this header stays SDK-header-free. -class NatNetClient; - -namespace natnet_ros2 -{ - -class NatNetClientAdapter : public INatNetClient -{ -public: - NatNetClientAdapter(); - ~NatNetClientAdapter() override; - - NatNetResult connect(const ConnectConfig & cfg) override; - bool get_server_info(ServerInfo & out) override; - std::vector get_body_descriptors() override; - void set_frame_callback(std::function cb) override; - void disconnect() override; - - // Context handed to the SDK's C frame callback. Bundles the client (needed to - // convert TransmitTimestamp → latency via SecondsSinceHostTimestamp) with the - // user callback, since the SDK passes only a single void* through. Public so the - // file-scope trampoline in the .cpp can reinterpret the void* ctx. - struct FrameCallbackCtx - { - NatNetClient * client = nullptr; - std::function * cb = nullptr; - }; - -private: - std::unique_ptr client_; - std::function user_cb_; - FrameCallbackCtx cb_ctx_{}; -}; - -} // namespace natnet_ros2 diff --git a/robot/ros_ws/src/perception/natnet_ros2/include/natnet_ros2/natnet_logic.hpp b/robot/ros_ws/src/perception/natnet_ros2/include/natnet_ros2/natnet_logic.hpp deleted file mode 100644 index f2c93b838..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/include/natnet_ros2/natnet_logic.hpp +++ /dev/null @@ -1,390 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -// natnet_logic.hpp — pure C++ helpers for natnet_ros2 (no ROS, no NatNet SDK), so -// test_natnet_logic.cpp compiles with only gtest. SDK types stay in -// natnet_ros2_node.cpp / natnet_client_adapter.cpp. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -namespace natnet_ros2 -{ - -// =========================================================================== -// 1. Covariance -// =========================================================================== - -/// Build a row-major 36-element 6×6 covariance from two flat 3×3 blocks. -/// -/// pos_cov (up to 9 elements) fills the top-left 3×3 block (rows/cols 0-2). -/// ori_cov (up to 9 elements) fills the bottom-right 3×3 block (rows/cols 3-5). -/// All other entries are zero. -inline std::array build_covariance_6x6( - const std::vector & pos_cov, - const std::vector & ori_cov) -{ - std::array cov{}; - cov.fill(0.0); - const int np = static_cast(pos_cov.size()); - const int no = static_cast(ori_cov.size()); - for (int r = 0; r < 3; ++r) { - for (int c = 0; c < 3; ++c) { - const int idx = r * 3 + c; - if (idx < np) { cov[r * 6 + c] = pos_cov[idx]; } - if (idx < no) { cov[(r + 3) * 6 + (c + 3)] = ori_cov[idx]; } - } - } - return cov; -} - - -// =========================================================================== -// 2. Topic names -// =========================================================================== - -/// Base topic for a rigid body: /{robot_name}/perception/optitrack/{body_name} -inline std::string optitrack_topic_base( - const std::string & robot_name, - const std::string & body_name) -{ - return "/" + robot_name + "/perception/optitrack/" + body_name; -} - -/// PoseWithCovarianceStamped topic: …/{body_name}/pose_cov -inline std::string optitrack_pose_cov_topic( - const std::string & robot_name, - const std::string & body_name) -{ - return optitrack_topic_base(robot_name, body_name) + "/pose_cov"; -} - -/// Namespace a relative topic leaf under /{robot_name}/. -/// -/// Leading slashes in \p relative are stripped so the result always has exactly -/// one. Used for the per-body ``topic`` overrides in natnet_config.yaml, which are -/// relative and namespaced by the node at runtime. -inline std::string namespaced_topic( - const std::string & robot_name, - const std::string & relative) -{ - const std::size_t start = relative.find_first_not_of('/'); - const std::string leaf = - (start == std::string::npos) ? std::string{} : relative.substr(start); - return "/" + robot_name + "/" + leaf; -} - -/// Topic base for one configured body: the per-body relative override when set, -/// otherwise the default /{robot_name}/perception/optitrack/{body_name}. -inline std::string body_topic_base( - const std::string & robot_name, - const std::string & body_name, - const std::string & relative_override) -{ - if (relative_override.empty()) { - return optitrack_topic_base(robot_name, body_name); - } - return namespaced_topic(robot_name, relative_override); -} - - -// =========================================================================== -// 3. Connection-configuration helpers -// =========================================================================== - -/// Return ct if it is "unicast" or "multicast"; otherwise throw. -/// -/// Deliberately strict: silently falling back to "unicast" turns a typo into a -/// vehicle that connects to the wrong transport and never receives frames. -inline std::string validate_connection_type(const std::string & ct) -{ - if (ct == "unicast" || ct == "multicast") { return ct; } - throw std::invalid_argument( - "connection_type must be \"unicast\" or \"multicast\", got \"" + ct + "\""); -} - -/// SDK-independent connection configuration aggregate. -/// -/// natnet_ros2_node.cpp converts this into sNatNetClientConnectParams; tests -/// exercise the pure logic without linking the NatNet SDK. -struct ConnectConfig -{ - std::string server_ip = "192.168.1.1"; - std::string client_ip = "0.0.0.0"; - uint16_t command_port = 1510u; - uint16_t data_port = 1511u; - std::string connection_type = "unicast"; ///< "unicast" or "multicast" - std::string multicast_address = "239.255.42.99"; -}; - -/// Build a validated ConnectConfig from raw user-supplied strings. -/// Throws std::invalid_argument when connection_type is not "unicast"/"multicast". -inline ConnectConfig make_connect_config( - const std::string & server_ip, - const std::string & client_ip, - uint16_t command_port, - uint16_t data_port, - const std::string & connection_type, - const std::string & multicast_address = "239.255.42.99") -{ - return ConnectConfig{ - server_ip, - client_ip, - command_port, - data_port, - validate_connection_type(connection_type), - multicast_address - }; -} - -/// Returns true when the config requests a multicast connection. -inline bool is_multicast(const ConnectConfig & cfg) -{ - return cfg.connection_type == "multicast"; -} - -/// Returns true when the multicast address should be used. -/// When false, the multicast_address field is irrelevant and should be nullptr -/// when passed to sNatNetClientConnectParams. -inline bool needs_multicast_address(const ConnectConfig & cfg) -{ - return is_multicast(cfg); -} - - -// =========================================================================== -// 4. Rigid-body frame helpers (SDK-independent) -// =========================================================================== - -/// Lightweight, SDK-free representation of a single rigid-body sample. -/// natnet_ros2_node.cpp converts sRigidBodyData → RigidBodySample. -struct RigidBodySample -{ - int32_t id = 0; - float x = 0.f; - float y = 0.f; - float z = 0.f; - float qx = 0.f; - float qy = 0.f; - float qz = 0.f; - float qw = 1.f; - int16_t params = 0; ///< NatNet rb.params bitmask -}; - -/// Lightweight, SDK-free representation of one frame of mocap data. -struct FrameSample -{ - int32_t frame_num = 0; - float timestamp = 0.f; - int16_t params = 0; ///< NatNet frame.params bitmask - /// transit + client-processing latency the drone observes per message. - double transit_latency_s = 0.0; - bool has_latency = false; - std::vector bodies; -}; - -/// Returns true when bit 0 of rb.params is set (NatNet: tracking valid). -inline bool is_tracking_valid(int16_t rb_params) -{ - return (rb_params & 0x01) != 0; -} - -/// Returns true when bit 1 of frame.params is set (NatNet: model list changed). -inline bool model_list_changed(int16_t frame_params) -{ - return (frame_params & 0x02) != 0; -} - -/// Returns true when the rigid body should be published. -/// filter_id < 0 means "publish all bodies"; otherwise only the matching ID. -inline bool should_publish_body(int32_t filter_id, int32_t rb_id) -{ - return filter_id < 0 || rb_id == filter_id; -} - -/// Returns true when rb_id is one of the configured body ids. -/// -/// The node publishes only a fixed set of ids based on natnet_config.yaml. -inline bool body_is_configured(const std::vector & configured_ids, int32_t rb_id) -{ - return std::find(configured_ids.begin(), configured_ids.end(), rb_id) - != configured_ids.end(); -} - -/// Double-precision pose extracted from a RigidBodySample. -struct PoseData -{ - double x = 0.0; - double y = 0.0; - double z = 0.0; - double qx = 0.0; - double qy = 0.0; - double qz = 0.0; - double qw = 1.0; -}; - -/// Convert a RigidBodySample to a double-precision PoseData. -inline PoseData rb_to_pose(const RigidBodySample & rb) -{ - return PoseData{ - static_cast(rb.x), - static_cast(rb.y), - static_cast(rb.z), - static_cast(rb.qx), - static_cast(rb.qy), - static_cast(rb.qz), - static_cast(rb.qw) - }; -} - -/// Fill a 36-element covariance array into a pre-allocated ROS-style covariance -/// field from a pre-built std::array. -/// Returns a copy of the array (ROS msg.covariance = cov6x6_to_array(...)). -inline std::array cov6x6_to_array(const std::array & src) -{ - return src; -} - -// =========================================================================== -// 5. Abstraction seam: INatNetClient + negotiation logic -// =========================================================================== - -/// SDK-independent result codes for connection attempts. -enum class NatNetResult -{ - OK, - NetworkError, - InvalidAddress, - Timeout, - InternalError, -}; - -inline const char * natnet_result_str(NatNetResult r) -{ - switch (r) { - case NatNetResult::OK: return "OK"; - case NatNetResult::NetworkError: return "NetworkError"; - case NatNetResult::InvalidAddress: return "InvalidAddress"; - case NatNetResult::Timeout: return "Timeout"; - case NatNetResult::InternalError: return "InternalError"; - } - return "Unknown"; -} - -/// Server identity returned after a successful connection. -struct ServerInfo -{ - bool host_present = false; - std::string host_app_name; - int host_app_version[4] = {}; ///< major.minor.build.revision - int natnet_version[4] = {}; ///< major.minor.build.revision -}; - -/// SDK-independent description of one rigid-body asset. -struct BodyDescriptor -{ - int32_t id = 0; - std::string name; - int32_t parent_id = -1; ///< >= 0 → skeleton bone; skip for top-level publishing -}; - -/// Result of the connect + GetServerDescription handshake. -struct NegotiationResult -{ - bool ok = false; - ServerInfo server_info; - std::string log_message; ///< human-readable outcome for the ROS logger -}; - -/// Pure-virtual client interface — implemented by NatNetClientAdapter (production) -/// and FakeNatNetClient (unit tests). -/// -/// Depends only on natnet_logic.hpp types; never includes NatNet SDK headers. -class INatNetClient -{ -public: - virtual ~INatNetClient() = default; - - /// Attempt to connect to a Motive server. - virtual NatNetResult connect(const ConnectConfig & cfg) = 0; - - /// Populate \p out with server identity. Returns false when host info is - /// unavailable (HostPresent == false in the SDK's sServerDescription). - virtual bool get_server_info(ServerInfo & out) = 0; - - /// Return descriptions of all rigid-body assets currently known to Motive. - /// Returns empty on failure; callers should retry on model-list-changed. - virtual std::vector get_body_descriptors() = 0; - - /// Register a callback invoked on every incoming frame. - /// The callback is called from the SDK receive thread. - virtual void set_frame_callback(std::function cb) = 0; - - /// Disconnect from the server and release SDK resources. - virtual void disconnect() = 0; -}; - -/// Execute the connect + GetServerDescription handshake and return a structured -/// result. Pure logic: no ROS calls, no SDK types — fully testable with a fake. -inline NegotiationResult negotiate(INatNetClient & client, const ConnectConfig & cfg) -{ - NegotiationResult result; - - const NatNetResult err = client.connect(cfg); - if (err != NatNetResult::OK) { - result.ok = false; - result.log_message = std::string("NatNetClient::Connect failed (") - + natnet_result_str(err) - + ") — server=" + cfg.server_ip - + " port=" + std::to_string(cfg.command_port) - + " type=" + cfg.connection_type; - return result; - } - - result.ok = true; - - const bool host_ok = client.get_server_info(result.server_info); - if (!host_ok || !result.server_info.host_present) { - result.log_message = "Connected to " + cfg.server_ip - + " but GetServerDescription returned no host info."; - } else { - result.log_message = "Connected to Motive '" - + result.server_info.host_app_name - + "' v" - + std::to_string(result.server_info.host_app_version[0]) - + "." - + std::to_string(result.server_info.host_app_version[1]) - + " (NatNet " - + std::to_string(result.server_info.natnet_version[0]) - + "." - + std::to_string(result.server_info.natnet_version[1]) - + ") at " + cfg.server_ip; - } - return result; -} - -} // namespace natnet_ros2 diff --git a/robot/ros_ws/src/perception/natnet_ros2/launch/mavros_gp_origin.launch.xml b/robot/ros_ws/src/perception/natnet_ros2/launch/mavros_gp_origin.launch.xml deleted file mode 100644 index 4cb1f785b..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/launch/mavros_gp_origin.launch.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/perception/natnet_ros2/launch/natnet_ros2.launch.py b/robot/ros_ws/src/perception/natnet_ros2/launch/natnet_ros2.launch.py deleted file mode 100644 index 440eaf8d6..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/launch/natnet_ros2.launch.py +++ /dev/null @@ -1,272 +0,0 @@ -#!/usr/bin/env python3 -"""Bring up the NatNet node from natnet_config.yaml; optionally the MAVROS bridge. - -The config uses a custom ``natnet:`` schema (server settings + per-robot profiles), -so this launch file parses it, selects the profile matching ``ROBOT_NAME``, flattens -the body list into node parameters, and — when the robot's ``vision_pose`` block is -enabled — includes the MAVROS GP-origin + vision_pose_converter bridges. - -natnet_ros2_node is a C++ executable that requires the OptiTrack NatNet SDK. -If the SDK was not installed (``airstack setup`` not run) and the workspace -has not been rebuilt, launching this file will raise a RuntimeError with -instructions. Set LAUNCH_NATNET=false in .env to disable OptiTrack entirely. -""" - -from __future__ import annotations - -import os -import re -from pathlib import Path -from typing import Any, cast - -import yaml -from ament_index_python.packages import get_package_share_directory -from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, OpaqueFunction -from launch.launch_description_sources import FrontendLaunchDescriptionSource -from launch.substitutions import LaunchConfiguration -from launch_ros.actions import Node - -# Per-body covariance fallback when a body omits its own (sub-0.1 mm / sub-0.1 deg). -_DEFAULT_POSITION_COVARIANCE = [1.0e-6, 0.0, 0.0, 0.0, 1.0e-6, 0.0, 0.0, 0.0, 1.0e-6] -_DEFAULT_ORIENTATION_COVARIANCE = [3.0e-6, 0.0, 0.0, 0.0, 3.0e-6, 0.0, 0.0, 0.0, 3.0e-6] - -_ENV_SUBST = re.compile(r"\$\(env\s+(\w+)(?:\s+([^)]*))?\)") - - -def _expand_env(value: Any) -> Any: - """Expand ``$(env VAR default)`` tokens in a string using os.environ.""" - if not isinstance(value, str): - return value - - def _replace(match: re.Match) -> str: - var, default = match.group(1), match.group(2) - return os.environ.get(var, default if default is not None else "") - - return _ENV_SUBST.sub(_replace, value) - - -def _load_natnet_config(config_path: str) -> dict: - """Parse the ``natnet:`` block from the config YAML.""" - path = Path(config_path) - if not path.is_file(): - return {} - with path.open(encoding='utf-8') as f: - data = yaml.safe_load(f) - if not isinstance(data, dict): - return {} - natnet = data.get('natnet', {}) - return cast(dict, natnet) if isinstance(natnet, dict) else {} - - -def _flatten_covariance(values: Any, fallback: list[float]) -> list[float]: - """Coerce a 9-element covariance block to floats, falling back when absent.""" - if not isinstance(values, (list, tuple)) or len(values) == 0: - return list(fallback) - return [float(v) for v in values] - - -def _build_node_params(server: dict, profile: dict) -> dict: - """Flatten the server block + a robot's body list into node parameters.""" - bodies = profile.get('bodies', []) or [] - - params: dict[str, Any] = { - 'server_ip': str(_expand_env(server.get('server_ip', '172.31.0.200'))), - 'client_ip': str(_expand_env(server.get('client_ip', '0.0.0.0'))), - 'command_port': int(server.get('command_port', 1510)), - 'data_port': int(server.get('data_port', 1511)), - 'connection_type': str(server.get('connection_type', 'unicast')), - 'multicast_address': str(server.get('multicast_address', '239.255.42.99')), - 'frame_id': str(server.get('frame_id', 'world')), - 'debug': bool(server.get('debug', False)), - 'latency_sampling_warmup_s': float(server.get('latency_sampling_warmup_s', 5.0)), - 'latency_sampling_window_s': float(server.get('latency_sampling_window_s', 20.0)), - 'cube_orange_latency_ms': float(server.get('cube_orange_latency_ms', 5.0)), - } - - body_names: list[str] = [] - body_ids: list[int] = [] - body_topics: list[str] = [] - body_pose: list[bool] = [] - body_pose_cov: list[bool] = [] - body_position_covariance: list[float] = [] - body_orientation_covariance: list[float] = [] - - for body in bodies: - body_names.append(str(body.get('rigid_body_name', ''))) - body_ids.append(int(body.get('id', -1))) - body_topics.append(str(body.get('topic', ''))) - body_pose.append(bool(body.get('pose', True))) - body_pose_cov.append(bool(body.get('pose_cov', True))) - body_position_covariance.extend( - _flatten_covariance(body.get('position_covariance'), _DEFAULT_POSITION_COVARIANCE) - ) - body_orientation_covariance.extend( - _flatten_covariance(body.get('orientation_covariance'), _DEFAULT_ORIENTATION_COVARIANCE) - ) - - params.update( - { - 'body_names': body_names, - 'body_ids': body_ids, - 'body_topics': body_topics, - 'body_pose': body_pose, - 'body_pose_cov': body_pose_cov, - 'body_position_covariance': body_position_covariance, - 'body_orientation_covariance': body_orientation_covariance, - } - ) - return params - - -def _namespaced(robot_name: str, relative: str) -> str: - """Namespace a relative topic under /{robot_name}/.""" - return '/' + robot_name + '/' + relative.lstrip('/') - - -def generate_launch_description() -> LaunchDescription: - pkg_share = get_package_share_directory('natnet_ros2') - default_natnet_yaml = os.path.join(pkg_share, 'config', 'natnet_config.yaml') - default_vp_yaml = os.path.join(pkg_share, 'config', 'vision_pose_converter.yaml') - default_gp_origin_yaml = os.path.join(pkg_share, 'config', 'mavros_gp_origin.yaml') - default_px4_params_yaml = os.path.join(pkg_share, 'config', 'px4_params.yaml') - - config_file = LaunchConfiguration('config_file') - vision_pose_config_file = LaunchConfiguration('vision_pose_config_file') - gp_origin_config_file = LaunchConfiguration('gp_origin_config_file') - px4_params_config_file = LaunchConfiguration('px4_params_config_file') - use_sim_time = LaunchConfiguration('use_sim_time') - - def launch_setup(context, *_args, **_kwargs): - cfg_path = config_file.perform(context) - vp_path = vision_pose_config_file.perform(context) - gp_path = gp_origin_config_file.perform(context) - px4_path = px4_params_config_file.perform(context) - ust = use_sim_time.perform(context) - - robot_name = os.environ.get('ROBOT_NAME', 'robot_1') - natnet = _load_natnet_config(cfg_path) - server = natnet.get('server', {}) if isinstance(natnet, dict) else {} - robots = natnet.get('robots', {}) if isinstance(natnet, dict) else {} - profile = robots.get(robot_name, {}) if isinstance(robots, dict) else {} - - if not profile: - print( - f"[natnet_ros2.launch] WARNING: no profile for ROBOT_NAME='{robot_name}' " - f"in {cfg_path}; node will start with no tracked bodies." - ) - - node_params = _build_node_params(server, profile) - # launch_ros / rclpy cannot infer the type of an empty-list parameter, so drop - # any empty arrays; the node declares matching empty defaults and tracks nothing. - node_params = { - k: v for k, v in node_params.items() if not (isinstance(v, list) and len(v) == 0) - } - - # pkg_share = /share/natnet_ros2 → go up two levels to reach , - # then down into lib/natnet_ros2/ where colcon installs executables. - node_path = Path(pkg_share).parent.parent / 'lib' / 'natnet_ros2' / 'natnet_ros2_node' - if not node_path.exists(): - raise RuntimeError( - 'natnet_ros2_node executable not found — NatNet SDK is not installed.\n' - "Run 'airstack setup' to download and install the OptiTrack NatNet SDK,\n" - 'then rebuild the workspace: bws --packages-select natnet_ros2\n' - 'Or set LAUNCH_NATNET=false in .env to disable OptiTrack.' - ) - - actions = [ - Node( - package='natnet_ros2', - executable='natnet_ros2_node', - name='natnet_ros2_node', - output='screen', - parameters=[node_params], - # The closed-source NatNet SDK can assert (SIGABRT) on connect - # in odd network states; restart rather than losing mocap. - respawn=True, - respawn_delay=2.0, - ), - ] - - vision_pose = profile.get('vision_pose', {}) if isinstance(profile, dict) else {} - if vision_pose.get('enabled', False): - input_topic = _namespaced( - robot_name, str(vision_pose.get('input_topic', 'perception/optitrack/drone/pose_cov')) - ) - output_pose_topic = _namespaced( - robot_name, str(vision_pose.get('output_pose_topic', 'interface/mavros/vision_pose/pose')) - ) - output_pose_cov_topic = _namespaced( - robot_name, - str(vision_pose.get('output_pose_cov_topic', 'interface/mavros/vision_pose/pose_cov')), - ) - - actions.append( - IncludeLaunchDescription( - FrontendLaunchDescriptionSource( - os.path.join(pkg_share, 'launch', 'mavros_gp_origin.launch.xml'), - ), - launch_arguments=[ - ('config_file', gp_path), - ('use_sim_time', ust), - ], - ), - ) - actions.append( - IncludeLaunchDescription( - FrontendLaunchDescriptionSource( - os.path.join(pkg_share, 'launch', 'px4_param_setter.launch.xml'), - ), - launch_arguments=[ - ('config_file', px4_path), - ('use_sim_time', ust), - ], - ), - ) - actions.append( - IncludeLaunchDescription( - FrontendLaunchDescriptionSource( - os.path.join(pkg_share, 'launch', 'vision_pose_converter.launch.xml'), - ), - launch_arguments=[ - ('config_file', vp_path), - ('input_topic', input_topic), - ('output_pose_topic', output_pose_topic), - ('output_pose_cov_topic', output_pose_cov_topic), - ('use_sim_time', ust), - ], - ), - ) - return actions - - return LaunchDescription( - [ - DeclareLaunchArgument( - 'config_file', - default_value=default_natnet_yaml, - description='NatNet config YAML (natnet: server + per-robot profiles). ' - 'The robot profile selected by ROBOT_NAME drives bodies + MAVROS include.', - ), - DeclareLaunchArgument( - 'vision_pose_config_file', - default_value=default_vp_yaml, - description='vision_pose_converter parameter YAML (frame_id, canonical_quaternion).', - ), - DeclareLaunchArgument( - 'gp_origin_config_file', - default_value=default_gp_origin_yaml, - description='mavros_gp_origin parameter YAML.', - ), - DeclareLaunchArgument( - 'px4_params_config_file', - default_value=default_px4_params_yaml, - description='px4_param_setter parameter YAML (params.* = desired FCU parameters).', - ), - DeclareLaunchArgument( - 'use_sim_time', - default_value='false', - description='Forwarded to MAVROS bridge launch files.', - ), - OpaqueFunction(function=launch_setup), - ], - ) diff --git a/robot/ros_ws/src/perception/natnet_ros2/launch/px4_param_setter.launch.xml b/robot/ros_ws/src/perception/natnet_ros2/launch/px4_param_setter.launch.xml deleted file mode 100644 index a3852b0c8..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/launch/px4_param_setter.launch.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/perception/natnet_ros2/launch/vision_pose_converter.launch.xml b/robot/ros_ws/src/perception/natnet_ros2/launch/vision_pose_converter.launch.xml deleted file mode 100644 index 803cbdad7..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/launch/vision_pose_converter.launch.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/robot/ros_ws/src/perception/natnet_ros2/package.xml b/robot/ros_ws/src/perception/natnet_ros2/package.xml deleted file mode 100644 index 1f9e04f4d..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/package.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - natnet_ros2 - 0.1.0 - - NatNet ROS 2 wrapper for OptiTrack Motive motion capture integration. - Receives NatNet data from external Motive PC via the official NatNet SDK - and publishes pose data into the AirStack perception layer. - - - AirLab CMU - MIT - - ament_cmake - - - rclcpp - geometry_msgs - nav_msgs - - - rclpy - tf_transformations - airstack_msgs - - - mavros_msgs - geographic_msgs - rcl_interfaces - - ament_index_python - launch - launch_ros - python3-yaml - - - ament_lint_auto - ament_copyright - ament_flake8 - ament_pep257 - python3-pytest - ament_cmake_gtest - - - ament_cmake - - diff --git a/robot/ros_ws/src/perception/natnet_ros2/scripts/download-natnet-sdk.sh b/robot/ros_ws/src/perception/natnet_ros2/scripts/download-natnet-sdk.sh deleted file mode 100644 index e91360197..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/scripts/download-natnet-sdk.sh +++ /dev/null @@ -1,167 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -module_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -sdk_lib_dir="${module_root}/lib" -sdk_include_dir="${module_root}/include/natnet" - -sdk_archive_name="" -sdk_download_url="" -temp_dir="" - -info() { - printf '[INFO] %s\n' "$1" -} - -warn() { - printf '[WARN] %s\n' "$1" >&2 -} - -fail() { - printf '[ERROR] %s\n' "$1" >&2 - exit 1 -} - -have() { - command -v "$1" >/dev/null 2>&1 -} - -cleanup() { - if [ -n "$temp_dir" ] && [ -d "$temp_dir" ]; then - rm -rf "$temp_dir" - fi -} - -trap cleanup EXIT - -sdk_is_installed() { - [ -f "${sdk_lib_dir}/libNatNet.so" ] || return 1 - [ -n "$(find "${sdk_include_dir}" -maxdepth 1 -type f -name 'NatNet*' -print -quit 2>/dev/null)" ] -} - -choose_archive() { - case "$(uname -m)" in - x86_64) - sdk_archive_name="NatNet_SDK_4.4_ubuntu.tar" - sdk_download_url="https://d2mzlempwep3hb.cloudfront.net/NatNetSDKLinux/ubuntu/${sdk_archive_name}" - info "Using the x86_64 NatNet SDK package" - ;; - aarch64|arm*) - sdk_archive_name="NatNet_SDK_4.4_ubuntu_ARM.tar" - sdk_download_url="https://d2mzlempwep3hb.cloudfront.net/NatNetSDKLinux/ubuntu_arm/${sdk_archive_name}" - info "Using the ARM NatNet SDK package" - ;; - *) - fail "Unsupported architecture: $(uname -m)" - ;; - esac -} - -show_license_notice() { - cat <<'EOF' -=============================================================================== -OptiTrack NatNet SDK License -=============================================================================== - -The NatNet SDK is used for OptiTrack Motive motion capture integration. -It is only required if you intend to use OptiTrack with AirStack. - -The SDK is proprietary and AirStack does not redistribute it. -Please review the OptiTrack terms before continuing: -https://optitrack.com/about/legal/eula - -This installer will download the SDK into the natnet_ros2 module tree: - - lib/libNatNet.so - - include/natnet/NatNet* - -If you do not plan to use OptiTrack, you can skip this step by pressing 'N' or by running: - airstack setup --no-natnet - -=============================================================================== -EOF -} - -download_archive() { - info "Downloading ${sdk_archive_name}" - - if have wget; then - wget -O "${temp_dir}/${sdk_archive_name}" "${sdk_download_url}" - return 0 - fi - - if have curl; then - curl -fsSL -o "${temp_dir}/${sdk_archive_name}" "${sdk_download_url}" - return 0 - fi - - fail "Neither wget nor curl is available. Install one of them and try again." -} - -extract_archive() { - info "Extracting archive" - mkdir -p "${temp_dir}/extract" - tar -xf "${temp_dir}/${sdk_archive_name}" -C "${temp_dir}/extract" -} - -install_files() { - local source_lib - local source_include - - source_lib="$(find "${temp_dir}/extract" -type f -name 'libNatNet.so' -print -quit)" - source_include="$(find "${temp_dir}/extract" -type d -name include -print -quit)" - - [ -n "${source_lib}" ] || fail "libNatNet.so was not found inside the SDK archive" - [ -n "${source_include}" ] || fail "include/ was not found inside the SDK archive" - - mkdir -p "${sdk_lib_dir}" "${sdk_include_dir}" - - info "Installing library and headers into the module tree" - cp "${source_lib}" "${sdk_lib_dir}/" - find "${source_include}" -maxdepth 1 -type f -name 'NatNet*' -exec cp -f {} "${sdk_include_dir}/" \; - - [ -f "${sdk_lib_dir}/libNatNet.so" ] || fail "NatNet library copy did not complete" - [ -n "$(find "${sdk_include_dir}" -maxdepth 1 -type f -name 'NatNet*' -print -quit)" ] || fail "NatNet headers copy did not complete" -} - -main() { - # Non-interactive / CI mode: set NATNET_ACCEPT_LICENSE=1 or pass --accept-license. - # By using this flag you confirm that you have read and accept the OptiTrack - # Software License Agreement (https://optitrack.com/about/legal/eula). - local auto_accept=false - for arg in "$@"; do - [[ "$arg" == "--accept-license" ]] && auto_accept=true - done - [[ "${NATNET_ACCEPT_LICENSE:-0}" == "1" ]] && auto_accept=true - - if sdk_is_installed; then - info "NatNet SDK already installed" - info "Library: ${sdk_lib_dir}/libNatNet.so" - info "Headers: ${sdk_include_dir}" - exit 0 - fi - - choose_archive - show_license_notice - - if [[ "$auto_accept" == "true" ]]; then - info "NATNET_ACCEPT_LICENSE=1 / --accept-license set — accepting license non-interactively" - else - read -r -p "Accept the OptiTrack NatNet SDK license and download the SDK now? [Y/n] " reply - reply="${reply:-y}" - if ! [[ "${reply}" =~ ^[Yy]$ ]]; then - warn "NatNet SDK installation skipped" - exit 1 - fi - fi - - temp_dir="$(mktemp -d "/tmp/natnet-sdk.XXXXXX")" - download_archive - extract_archive - install_files - - info "NatNet SDK installation completed" - info "Rebuild with: colcon build --packages-select natnet_ros2" -} - -main "$@" diff --git a/robot/ros_ws/src/perception/natnet_ros2/src/mavros_gp_origin_node.py b/robot/ros_ws/src/perception/natnet_ros2/src/mavros_gp_origin_node.py deleted file mode 100755 index b4151d835..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/src/mavros_gp_origin_node.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python3 - -""" -MAVROS GPS Origin Node - -Publishes a synthetic GPS origin to MAVROS once at startup for mocap / no-GNSS -flight. With GNSS disabled, PX4 fuses vision into a valid local position but -has no global position, so modes that require one (e.g. AUTO.LOITER) refuse to -arm. Setting an origin lets PX4 derive global position from the fused estimate. - -The publish is guarded: it waits for MAVROS to connect, watches for an existing -origin, and only publishes if none is present — GNSS-equipped vehicles are left -untouched. -""" - -import shutil -import subprocess - -import rclpy -from rclpy.node import Node -from geographic_msgs.msg import GeoPointStamped -from mavros_msgs.msg import State - - -class MavrosGpOriginNode(Node): - """One-shot synthetic GPS origin publisher for MAVROS / PX4.""" - - def __init__(self): - super().__init__('mavros_gp_origin') - - self.declare_parameter('enabled', True) - # Defaults match the AirStack shared world datum (Lisbon) used by the GCS - # (gcs_utils.py) and sim (gps_utils.py). Normally overridden by - # config/mavros_gp_origin.yaml; kept in sync to avoid a stale fallback. - self.declare_parameter('latitude', 38.736832) - self.declare_parameter('longitude', -9.137977) - self.declare_parameter('altitude', 90.0) - # Real-hardware geoid handling. mavros/PX4 treat the origin altitude as a - # WGS-84 ELLIPSOIDAL height and internally apply the egm96-5 geoid model - # (mavros_uas::egm96_5) to convert to/from AMSL. With no GNSS/baro the - # vehicle height comes purely from vision (mocap floor ~ 0 AMSL), so to - # make local_position z equal the OptiTrack height the origin's ellipsoidal - # altitude must be: N(lat,lon) + desired_floor_amsl, where N is the geoid - # undulation. Because the SAME egm96-5 model computes N here and inside - # mavros, the undulation cancels EXACTLY (accuracy is independent of the - # model's absolute error). Skipped when use_sim_time=true: sim's synthetic - # GPS carries no geoid separation and uses the literal altitude. - self.declare_parameter('use_geoid_altitude', False) - # AMSL (m) assigned to the mocap floor / vision z = 0. Local z equals OptiTrack z - # for any value; this only sets what global altitude the floor reports. - self.declare_parameter('desired_floor_amsl', 36.0) - # Geoid model — MUST match mavros (egm96-5) for exact cancellation. - self.declare_parameter('geoid_model', 'egm96-5') - # Seconds to wait after MAVROS connects (listening for an existing - # origin) before publishing our synthetic one. - self.declare_parameter('settle_sec', 5.0) - - self._enabled = self.get_parameter('enabled').value - if not self._enabled: - self.get_logger().info('Synthetic GPS origin disabled (enabled=false).') - return - - self._lat = self.get_parameter('latitude').value - self._lon = self.get_parameter('longitude').value - self._settle_sec = self.get_parameter('settle_sec').value - self._alt = self._resolve_altitude() - - self._done = False - self._origin_exists = False - self._connected_since = None - self._publish_count = 0 - - self._set_origin_pub = self.create_publisher( - GeoPointStamped, 'set_gps_origin', 10 - ) - self._origin_sub = self.create_subscription( - GeoPointStamped, 'current_gps_origin', self._on_existing_origin, 10 - ) - self._state_sub = self.create_subscription( - State, 'mavros_state', self._on_mavros_state, 10 - ) - self._timer = self.create_timer(1.0, self._tick) - - self.get_logger().info( - f'MAVROS GPS origin node started ' - f'(lat={self._lat}, lon={self._lon}, alt={self._alt}, ' - f'settle_sec={self._settle_sec})' - ) - - def _geoid_undulation(self, lat, lon, model): - """ - Geoid undulation N (metres, height of the geoid above the WGS-84 - ellipsoid) at (lat, lon) via GeographicLib's GeoidEval — the same - egm96-5 dataset mavros loads (mavros_uas::egm96_5), so N cancels exactly - against mavros' internal ellipsoid<->AMSL conversion. Raises on failure. - """ - exe = shutil.which('GeoidEval') - if exe is None: - raise RuntimeError('GeoidEval not found on PATH (install GeographicLib tools)') - proc = subprocess.run( - [exe, '-n', model], - input=f'{lat:.9f} {lon:.9f}\n', - capture_output=True, text=True, timeout=10.0, - ) - if proc.returncode != 0: - raise RuntimeError( - f'GeoidEval rc={proc.returncode}: {proc.stderr.strip() or proc.stdout.strip()}' - ) - return float(proc.stdout.strip().split()[0]) - - def _resolve_altitude(self): - """ - Origin altitude to publish: the literal `altitude` param, unless - use_geoid_altitude is set on real hardware, in which case it is the - egm96-5 geoid undulation at (lat, lon) plus desired_floor_amsl. - """ - if not self.get_parameter('use_geoid_altitude').value: - return self.get_parameter('altitude').value - if self.get_parameter('use_sim_time').value: - self.get_logger().info( - 'use_sim_time=true: using literal altitude (sim datum), not geoid.' - ) - return self.get_parameter('altitude').value - floor = self.get_parameter('desired_floor_amsl').value - model = self.get_parameter('geoid_model').value - try: - n = self._geoid_undulation(self._lat, self._lon, model) - except Exception as e: - literal = self.get_parameter('altitude').value - self.get_logger().error( - f'use_geoid_altitude=true but geoid lookup failed ({e}); falling ' - f'back to literal altitude {literal} m. LOCAL Z WILL BE OFFSET BY ' - f'THE GEOID (tens of m) — fix GeographicLib/GeoidEval before flight.' - ) - return literal - alt = n + floor - self.get_logger().info( - f'Geoid origin altitude: N({model})={n:.4f} + floor_amsl={floor:.4f} ' - f'=> {alt:.4f} m ellipsoidal (local z will equal OptiTrack z).' - ) - return alt - - def _on_existing_origin(self, _msg: GeoPointStamped): - """An origin already exists (e.g. from GNSS) — never override it.""" - if not self._origin_exists and not self._done: - self.get_logger().info( - 'Existing GPS origin detected; skipping synthetic origin.' - ) - self._origin_exists = True - - def _on_mavros_state(self, msg: State): - if msg.connected and self._connected_since is None: - self._connected_since = self.get_clock().now() - - def _tick(self): - if self._done: - return - if self._origin_exists: - self._done = True - self._timer.cancel() - return - if self._connected_since is None: - return - elapsed = (self.get_clock().now() - self._connected_since).nanoseconds * 1e-9 - if elapsed < self._settle_sec: - return - - msg = GeoPointStamped() - msg.header.stamp = self.get_clock().now().to_msg() - msg.position.latitude = self._lat - msg.position.longitude = self._lon - msg.position.altitude = self._alt - self._set_origin_pub.publish(msg) - self._publish_count += 1 - self.get_logger().info( - f'Published synthetic GPS origin ' - f'(lat={self._lat}, lon={self._lon}, alt={self._alt}) ' - f'[{self._publish_count}/3]' - ) - # Publish a few times in case MAVROS subscribed late, then stop. - if self._publish_count >= 3: - self._done = True - self._timer.cancel() - - -def main(args=None): - rclpy.init(args=args) - try: - node = MavrosGpOriginNode() - rclpy.spin(node) - except KeyboardInterrupt: - pass - finally: - rclpy.shutdown() - - -if __name__ == '__main__': - main() diff --git a/robot/ros_ws/src/perception/natnet_ros2/src/natnet_client_adapter.cpp b/robot/ros_ws/src/perception/natnet_ros2/src/natnet_client_adapter.cpp deleted file mode 100644 index 78caa940b..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/src/natnet_client_adapter.cpp +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// natnet_client_adapter.cpp — NatNetClientAdapter implementation. -// -// This is the ONLY translation unit that includes NatNet SDK headers. -// All other code (including tests) depends only on INatNetClient. - -#include "natnet_ros2/natnet_client_adapter.hpp" - -// NatNet SDK (bundled: include/natnet/, lib/libNatNet.so) -#include "NatNetClient.h" -#include "NatNetCAPI.h" -#include "NatNetTypes.h" - -#include - -namespace natnet_ros2 -{ - -// --------------------------------------------------------------------------- -// SDK frame callback trampoline — file-scope so it has C linkage compatible -// with the NATNET_CALLCONV calling convention. -// --------------------------------------------------------------------------- -namespace -{ - -void NATNET_CALLCONV sdk_frame_callback(sFrameOfMocapData * data, void * ctx) -{ - auto * cb_ctx = static_cast(ctx); - if (!data || !cb_ctx || !cb_ctx->cb || !*cb_ctx->cb) { return; } - - FrameSample fs; - fs.frame_num = data->iFrame; - fs.timestamp = data->fTimestamp; - fs.params = static_cast(data->params); - - // TransmitTimestamp is 0 on servers/streams that don't populate frame timing; - // only compute latency when it's present so downstream sampling can skip it. - if (data->TransmitTimestamp != 0 && cb_ctx->client) { - fs.transit_latency_s = - cb_ctx->client->SecondsSinceHostTimestamp(data->TransmitTimestamp); - fs.has_latency = true; - } - - fs.bodies.reserve(static_cast(data->nRigidBodies)); - for (int i = 0; i < data->nRigidBodies; ++i) { - const sRigidBodyData & rb = data->RigidBodies[i]; - RigidBodySample s; - s.id = rb.ID; - s.x = rb.x; s.y = rb.y; s.z = rb.z; - s.qx = rb.qx; s.qy = rb.qy; s.qz = rb.qz; s.qw = rb.qw; - s.params = static_cast(rb.params); - fs.bodies.push_back(s); - } - - (*cb_ctx->cb)(fs); -} - -/// Map NatNet SDK ErrorCode to our NatNetResult. -/// Note: ErrorCode_Timeout was added in NatNet SDK >= 4.5 and is absent in 4.4. -/// If upgrading the SDK, add: case ErrorCode_Timeout: return NatNetResult::Timeout; -NatNetResult from_sdk_error(ErrorCode ec) -{ - switch (ec) { - case ErrorCode_OK: return NatNetResult::OK; - case ErrorCode_Network: return NatNetResult::NetworkError; - case ErrorCode_InvalidArgument: return NatNetResult::InvalidAddress; - default: return NatNetResult::InternalError; - } -} - -} // anonymous namespace - - -// --------------------------------------------------------------------------- -NatNetClientAdapter::NatNetClientAdapter() -: client_(std::make_unique()) -{} - -NatNetClientAdapter::~NatNetClientAdapter() -{ - disconnect(); -} - -// --------------------------------------------------------------------------- -NatNetResult NatNetClientAdapter::connect(const ConnectConfig & cfg) -{ - sNatNetClientConnectParams params; - params.serverAddress = cfg.server_ip.c_str(); - params.localAddress = cfg.client_ip.c_str(); - params.serverCommandPort = cfg.command_port; - params.serverDataPort = cfg.data_port; - - if (is_multicast(cfg)) { - params.connectionType = ConnectionType_Multicast; - params.multicastAddress = cfg.multicast_address.c_str(); - } else { - params.connectionType = ConnectionType_Unicast; - params.multicastAddress = nullptr; - } - - return from_sdk_error(client_->Connect(params)); -} - -// --------------------------------------------------------------------------- -bool NatNetClientAdapter::get_server_info(ServerInfo & out) -{ - sServerDescription desc; - std::memset(&desc, 0, sizeof(desc)); - const ErrorCode ec = client_->GetServerDescription(&desc); - if (ec != ErrorCode_OK) { return false; } - - out.host_present = desc.HostPresent; - out.host_app_name = desc.szHostApp; - for (int i = 0; i < 4; ++i) { - out.host_app_version[i] = static_cast(desc.HostAppVersion[i]); - out.natnet_version[i] = static_cast(desc.NatNetVersion[i]); - } - return true; -} - -// --------------------------------------------------------------------------- -std::vector NatNetClientAdapter::get_body_descriptors() -{ - std::vector result; - - sDataDescriptions * desc_list = nullptr; - if (client_->GetDataDescriptionList(&desc_list) != ErrorCode_OK || !desc_list) { - return result; - } - - for (int i = 0; i < desc_list->nDataDescriptions; ++i) { - const sDataDescription & dd = desc_list->arrDataDescriptions[i]; - if (dd.type != Descriptor_RigidBody || !dd.Data.RigidBodyDescription) { continue; } - const sRigidBodyDescription & rb = *dd.Data.RigidBodyDescription; - - BodyDescriptor bd; - bd.id = rb.ID; - bd.name = rb.szName; - bd.parent_id = rb.parentID; - result.push_back(bd); - } - - NatNet_FreeDescriptions(desc_list); - return result; -} - -// --------------------------------------------------------------------------- -void NatNetClientAdapter::set_frame_callback( - std::function cb) -{ - user_cb_ = std::move(cb); - cb_ctx_.client = client_.get(); - cb_ctx_.cb = &user_cb_; - client_->SetFrameReceivedCallback(sdk_frame_callback, &cb_ctx_); -} - -// --------------------------------------------------------------------------- -void NatNetClientAdapter::disconnect() -{ - if (client_) { - client_->SetFrameReceivedCallback(sdk_frame_callback, nullptr); - client_->Disconnect(); - } - user_cb_ = nullptr; - cb_ctx_.client = nullptr; - cb_ctx_.cb = nullptr; -} - -} // namespace natnet_ros2 diff --git a/robot/ros_ws/src/perception/natnet_ros2/src/natnet_ros2_node.cpp b/robot/ros_ws/src/perception/natnet_ros2/src/natnet_ros2_node.cpp deleted file mode 100644 index 58b7c2fda..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/src/natnet_ros2_node.cpp +++ /dev/null @@ -1,478 +0,0 @@ -// natnet_ros2_node.cpp — ROS 2 NatNet SDK node for OptiTrack Motive. -// Parameters are flattened from config/natnet_config.yaml by natnet_ros2.launch.py. -// See docs/robot/px4_external_vision.md. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// POSIX sockets for the pre-connect reachability probe -#include -#include -#include - -// ROS 2 -#include "rclcpp/rclcpp.hpp" -#include "geometry_msgs/msg/pose_stamped.hpp" -#include "geometry_msgs/msg/pose_with_covariance_stamped.hpp" - -// Pure logic + interface (no SDK, testable with FakeNatNetClient) -#include "natnet_ros2/natnet_logic.hpp" -#include "natnet_ros2/natnet_client_adapter.hpp" - - -// Ping the Motive command port before handing the server to the SDK: Connect() can -// assert deep in ClientCore::ValidateHostConnection (SIGABRT) rather than returning -// NetworkError when the host is unreachable. Do not remove this pre-check. -static bool natnet_server_reachable( - const std::string & server_ip, int command_port, int timeout_ms) -{ - const int fd = ::socket(AF_INET, SOCK_DGRAM, 0); - if (fd < 0) { return false; } - - timeval tv{}; - tv.tv_sec = timeout_ms / 1000; - tv.tv_usec = (timeout_ms % 1000) * 1000; - ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(static_cast(command_port)); - if (::inet_pton(AF_INET, server_ip.c_str(), &addr.sin_addr) != 1) { - ::close(fd); - return false; - } - - // Official connect packet: header (msg_id=NAT_CONNECT(0), size=271), - // 270-byte payload starting with "Ping" + NatNet version at offset 265, - // then a trailing NUL (matches the SDK PythonClient's send_request). - std::array pkt{}; - pkt[2] = 271 & 0xFF; - pkt[3] = 271 >> 8; - pkt[4] = 'P'; pkt[5] = 'i'; pkt[6] = 'n'; pkt[7] = 'g'; - pkt[4 + 265] = 4; // requested NatNet version 4.2.0.0 - pkt[4 + 266] = 2; - - bool reachable = false; - if (::sendto(fd, pkt.data(), pkt.size(), 0, - reinterpret_cast(&addr), sizeof(addr)) == - static_cast(pkt.size())) - { - uint8_t reply[512]; - reachable = ::recv(fd, reply, sizeof(reply), 0) > 0; - } - ::close(fd); - return reachable; -} - - -// --------------------------------------------------------------------------- -// NatNetROS2Node -// --------------------------------------------------------------------------- -class NatNetROS2Node : public rclcpp::Node -{ -public: - using PoseStamped = geometry_msgs::msg::PoseStamped; - using PoseWithCovarianceStamped = geometry_msgs::msg::PoseWithCovarianceStamped; - - struct BodyConfig - { - int32_t id = -1; - std::string rigid_body_name; - std::string topic_base; - bool publish_pose = true; - bool publish_pose_cov = true; - std::array covariance{}; - rclcpp::Publisher::SharedPtr pose_pub; - rclcpp::Publisher::SharedPtr pose_cov_pub; - }; - - // ----------------------------------------------------------------------- - explicit NatNetROS2Node() - : Node("natnet_ros2_node") - { - // ----- Parameters -------------------------------------------------- - this->declare_parameter("server_ip", "192.168.1.1"); - this->declare_parameter("client_ip", "0.0.0.0"); - this->declare_parameter("command_port", 1510); - this->declare_parameter("data_port", 1511); - this->declare_parameter("connection_type", std::string("unicast")); - this->declare_parameter("multicast_address", std::string("239.255.42.99")); - this->declare_parameter("frame_id", "world"); - this->declare_parameter("debug", false); - - // Latency sampling: warmup + window for mean/stdev, then log a summary of measured latency. - this->declare_parameter("latency_sampling_warmup_s", 5.0); - this->declare_parameter("latency_sampling_window_s", 20.0); - // Suggested latency for the Cube Orange (PX4) from the OptiTrack Motive model. - // This is added to the measured transport latency to estimate total latency to PX4. - // NOTE: an estimate, not a measurement — see docs/robot/px4_external_vision.md. - // Diagnostic only; it is logged, never fused. - this->declare_parameter("cube_orange_latency_ms", 5.0); - - // Parallel per-body arrays (flattened from natnet_config.yaml by the launch file). - this->declare_parameter("body_names", std::vector{}); - this->declare_parameter("body_ids", std::vector{}); - this->declare_parameter("body_topics", std::vector{}); - this->declare_parameter("body_pose", std::vector{}); - this->declare_parameter("body_pose_cov", std::vector{}); - this->declare_parameter("body_position_covariance", std::vector{}); - this->declare_parameter("body_orientation_covariance", std::vector{}); - - // ----- Read parameters --------------------------------------------- - // Fatally fail if the config is invalid (e.g. unknown connection_type). - natnet_ros2::ConnectConfig connect_cfg; - try { - connect_cfg = natnet_ros2::make_connect_config( - this->get_parameter("server_ip").as_string(), - this->get_parameter("client_ip").as_string(), - static_cast(this->get_parameter("command_port").as_int()), - static_cast(this->get_parameter("data_port").as_int()), - this->get_parameter("connection_type").as_string(), - this->get_parameter("multicast_address").as_string()); - } catch (const std::invalid_argument & e) { - RCLCPP_FATAL(get_logger(), "Invalid natnet configuration: %s", e.what()); - throw; - } - - frame_id_ = this->get_parameter("frame_id").as_string(); - debug_ = this->get_parameter("debug").as_bool(); - - latency_warmup_s_ = this->get_parameter("latency_sampling_warmup_s").as_double(); - latency_window_s_ = this->get_parameter("latency_sampling_window_s").as_double(); - cube_orange_latency_ms_ = this->get_parameter("cube_orange_latency_ms").as_double(); - - const char * rn = std::getenv("ROBOT_NAME"); - robot_name_ = rn ? rn : "robot_1"; - - build_body_configs(); - - RCLCPP_INFO(get_logger(), "========================================="); - RCLCPP_INFO(get_logger(), "NatNet ROS 2 Node"); - RCLCPP_INFO(get_logger(), " robot_name: %s", robot_name_.c_str()); - RCLCPP_INFO(get_logger(), " server_ip: %s", connect_cfg.server_ip.c_str()); - RCLCPP_INFO(get_logger(), " command_port: %d", static_cast(connect_cfg.command_port)); - RCLCPP_INFO(get_logger(), " connection_type: %s", connect_cfg.connection_type.c_str()); - if (natnet_ros2::is_multicast(connect_cfg)) { - RCLCPP_INFO(get_logger(), " multicast_addr: %s", connect_cfg.multicast_address.c_str()); - } - RCLCPP_INFO(get_logger(), " tracked bodies: %zu", bodies_.size()); - RCLCPP_INFO(get_logger(), "========================================="); - - // Production client — NatNetClientAdapter wraps the SDK - client_ = std::make_unique(); - connect_cfg_ = connect_cfg; - - // Try to connect now; keep retrying. - if (!connect_and_setup(connect_cfg_)) { - connect_timer_ = this->create_wall_timer( - std::chrono::seconds(2), - std::bind(&NatNetROS2Node::retry_connect, this)); - } - } - - // ----------------------------------------------------------------------- - ~NatNetROS2Node() - { - if (client_) { client_->disconnect(); } - } - - // ----------------------------------------------------------------------- - // Called from the NatNetClientAdapter's frame trampoline. - // publish() and Clock::now() are thread-safe; bodies_ is immutable after init. - // ----------------------------------------------------------------------- - void on_frame(const natnet_ros2::FrameSample & frame) - { - if (debug_) { - RCLCPP_DEBUG(get_logger(), "Frame %d: %zu rigid bodies, ts=%.4f s", - frame.frame_num, frame.bodies.size(), static_cast(frame.timestamp)); - } - - const rclcpp::Time stamp = this->get_clock()->now(); - - maybe_sample_latency(frame, stamp); - - for (const auto & rb : frame.bodies) { - if (!natnet_ros2::is_tracking_valid(rb.params)) { - if (debug_) { - RCLCPP_DEBUG(get_logger(), " RB id=%d: tracking invalid, skipping", rb.id); - } - continue; - } - - const auto it = bodies_.find(rb.id); - if (it == bodies_.end()) { continue; } // not configured for this robot - - const natnet_ros2::PoseData pose = natnet_ros2::rb_to_pose(rb); - const BodyConfig & body = it->second; - - if (body.publish_pose && body.pose_pub) { - PoseStamped msg; - msg.header.frame_id = frame_id_; - msg.header.stamp = stamp; - msg.pose.position.x = pose.x; - msg.pose.position.y = pose.y; - msg.pose.position.z = pose.z; - msg.pose.orientation.x = pose.qx; - msg.pose.orientation.y = pose.qy; - msg.pose.orientation.z = pose.qz; - msg.pose.orientation.w = pose.qw; - body.pose_pub->publish(msg); - } - - if (body.publish_pose_cov && body.pose_cov_pub) { - PoseWithCovarianceStamped cov_msg; - cov_msg.header.frame_id = frame_id_; - cov_msg.header.stamp = stamp; - cov_msg.pose.pose.position.x = pose.x; - cov_msg.pose.pose.position.y = pose.y; - cov_msg.pose.pose.position.z = pose.z; - cov_msg.pose.pose.orientation.x = pose.qx; - cov_msg.pose.pose.orientation.y = pose.qy; - cov_msg.pose.pose.orientation.z = pose.qz; - cov_msg.pose.pose.orientation.w = pose.qw; - cov_msg.pose.covariance = body.covariance; - body.pose_cov_pub->publish(cov_msg); - } - } - } - -private: - // ----------------------------------------------------------------------- - // Returns true once the handshake succeeds. - bool connect_and_setup(const natnet_ros2::ConnectConfig & cfg) - { - // Wire-level probe first - if (!natnet_server_reachable(cfg.server_ip, cfg.command_port, 500)) { - RCLCPP_WARN_THROTTLE(get_logger(), *get_clock(), 10000, - "Motive at %s:%d not answering NatNet ping — waiting to connect.", - cfg.server_ip.c_str(), cfg.command_port); - return false; - } - - const natnet_ros2::NegotiationResult neg = - natnet_ros2::negotiate(*client_, cfg); - - if (!neg.ok) { - RCLCPP_WARN(get_logger(), "%s", neg.log_message.c_str()); - return false; - } - - if (neg.server_info.host_present) { - RCLCPP_INFO(get_logger(), "%s", neg.log_message.c_str()); - } else { - RCLCPP_WARN(get_logger(), "%s", neg.log_message.c_str()); - } - - client_->set_frame_callback( - [this](const natnet_ros2::FrameSample & f) { on_frame(f); }); - RCLCPP_INFO(get_logger(), "Frame callback registered — receiving mocap data."); - connected_ = true; - return true; - } - - // ----------------------------------------------------------------------- - // Timer-driven reconnect. - void retry_connect() - { - if (connected_) { - if (connect_timer_) { connect_timer_->cancel(); } - return; - } - RCLCPP_INFO_THROTTLE(get_logger(), *get_clock(), 10000, - "NatNet not connected — retrying handshake to %s ...", - connect_cfg_.server_ip.c_str()); - if (connect_and_setup(connect_cfg_) && connect_timer_) { - connect_timer_->cancel(); - } - } - - // ----------------------------------------------------------------------- - // Build the per-body config map + publishers from the parallel param arrays. - // Publishers are created up front (config-driven), so streaming begins as soon - // as frames arrive — no dependency on Motive's data-description handshake. - // ----------------------------------------------------------------------- - void build_body_configs() - { - const auto names = this->get_parameter("body_names").as_string_array(); - const auto ids = this->get_parameter("body_ids").as_integer_array(); - const auto topics = this->get_parameter("body_topics").as_string_array(); - const auto pose = this->get_parameter("body_pose").as_bool_array(); - const auto pose_cov = this->get_parameter("body_pose_cov").as_bool_array(); - const auto pos_cov = this->get_parameter("body_position_covariance").as_double_array(); - const auto ori_cov = this->get_parameter("body_orientation_covariance").as_double_array(); - - const std::size_t n = std::min(names.size(), ids.size()); - if (names.size() != ids.size()) { - RCLCPP_WARN(get_logger(), - "body_names (%zu) and body_ids (%zu) length mismatch — using %zu.", - names.size(), ids.size(), n); - } - - for (std::size_t i = 0; i < n; ++i) { - BodyConfig body; - body.id = static_cast(ids[i]); - body.rigid_body_name = names[i]; - body.publish_pose = (i < pose.size()) ? pose[i] : true; - body.publish_pose_cov = (i < pose_cov.size()) ? pose_cov[i] : true; - - const std::string relative = (i < topics.size()) ? topics[i] : std::string{}; - body.topic_base = - natnet_ros2::body_topic_base(robot_name_, body.rigid_body_name, relative); - - body.covariance = natnet_ros2::build_covariance_6x6( - cov_slice(pos_cov, i, _DEFAULT_POSITION_COVARIANCE), - cov_slice(ori_cov, i, _DEFAULT_ORIENTATION_COVARIANCE)); - - if (body.publish_pose) { - body.pose_pub = this->create_publisher(body.topic_base, 10); - } - if (body.publish_pose_cov) { - body.pose_cov_pub = this->create_publisher( - body.topic_base + "/pose_cov", 10); - } - - RCLCPP_INFO(get_logger(), - "Tracking body id=%d name='%s' → %s (pose=%d pose_cov=%d)", - static_cast(body.id), body.rigid_body_name.c_str(), - body.topic_base.c_str(), - static_cast(body.publish_pose), - static_cast(body.publish_pose_cov)); - - bodies_.emplace(body.id, std::move(body)); - } - } - - // ----------------------------------------------------------------------- - // Return the i-th 9-element covariance block from a flattened array, or the - // built-in default when the slice is missing. - static std::vector cov_slice( - const std::vector & flat, std::size_t i, const std::vector & fallback) - { - const std::size_t start = i * 9; - if (flat.size() < start + 9) { return fallback; } - return std::vector(flat.begin() + start, flat.begin() + start + 9); - } - - // ----------------------------------------------------------------------- - // Accumulate per-message transit latency over a fixed window and log a - // one-shot mean/stdev summary. Called once per frame from on_frame() (the SDK - // receive thread); all sampling state is touched only here, so no locking. - void maybe_sample_latency(const natnet_ros2::FrameSample & frame, - const rclcpp::Time & now) - { - if (latency_reported_ || !frame.has_latency) { return; } - - if (!latency_first_seen_) { - latency_first_seen_ = true; - latency_first_time_ = now; - RCLCPP_INFO(get_logger(), - "Latency sampling armed: %.1fs warm-up, then %.1fs sampling window.", - latency_warmup_s_, latency_window_s_); - return; - } - - const double elapsed = (now - latency_first_time_).seconds(); - if (elapsed < latency_warmup_s_) { return; } // still warming up - if (elapsed > latency_warmup_s_ + latency_window_s_) { // window closed - report_latency(); - return; - } - - const double lat = frame.transit_latency_s; - latency_count_ += 1; - latency_sum_s_ += lat; - latency_sum_sq_s_ += lat * lat; - } - - // ----------------------------------------------------------------------- - // Compute and log the latency summary once, then latch so it never repeats. - void report_latency() - { - latency_reported_ = true; - - if (latency_count_ == 0) { - RCLCPP_WARN(get_logger(), - "Latency window elapsed but no timestamped frames were sampled " - "(server may not populate TransmitTimestamp)."); - return; - } - - const double n = static_cast(latency_count_); - const double mean_s = latency_sum_s_ / n; - double var_s2 = 0.0; - if (latency_count_ > 1) { - // Sample variance (Bessel-corrected); clamp tiny negatives from round-off. - var_s2 = (latency_sum_sq_s_ - n * mean_s * mean_s) / (n - 1.0); - if (var_s2 < 0.0) { var_s2 = 0.0; } - } - - const double mean_ms = mean_s * 1.0e3; - const double stdev_ms = std::sqrt(var_s2) * 1.0e3; - const double total_ms = mean_ms + cube_orange_latency_ms_; - - RCLCPP_INFO(get_logger(), - "\n" - "========= OptiTrack -> drone message latency =========\n" - " sampling window : %.1f s (%llu frames)\n" - " transport mean : %.3f ms\n" - " transport std dev : %.3f ms\n" - " Cube Orange (model) : %.3f ms\n" - " estimated total : %.3f ms (to PX4 / EKF2 fusion)\n" - "======================================================", - latency_window_s_, - static_cast(latency_count_), - mean_ms, stdev_ms, cube_orange_latency_ms_, total_ms); - } - - // ----------------------------------------------------------------------- - // Parameters / state - std::string frame_id_; - bool debug_ = false; - std::string robot_name_; - - // Latency sampling parameters + running accumulators. - double latency_warmup_s_ = 5.0; - double latency_window_s_ = 20.0; - double cube_orange_latency_ms_ = 5.0; - bool latency_first_seen_ = false; - bool latency_reported_ = false; - rclcpp::Time latency_first_time_{0, 0, RCL_ROS_TIME}; - uint64_t latency_count_ = 0; - double latency_sum_s_ = 0.0; - double latency_sum_sq_s_ = 0.0; - - std::unique_ptr client_; - natnet_ros2::ConnectConfig connect_cfg_; - bool connected_ = false; - - std::unordered_map bodies_; - - rclcpp::TimerBase::SharedPtr connect_timer_; - - static const std::vector _DEFAULT_POSITION_COVARIANCE; - static const std::vector _DEFAULT_ORIENTATION_COVARIANCE; -}; - -const std::vector NatNetROS2Node::_DEFAULT_POSITION_COVARIANCE = - {1.0e-6, 0., 0., 0., 1.0e-6, 0., 0., 0., 1.0e-6}; -const std::vector NatNetROS2Node::_DEFAULT_ORIENTATION_COVARIANCE = - {3.0e-6, 0., 0., 0., 3.0e-6, 0., 0., 0., 3.0e-6}; - - -// --------------------------------------------------------------------------- -int main(int argc, char ** argv) -{ - rclcpp::init(argc, argv); - auto node = std::make_shared(); - rclcpp::spin(node); - rclcpp::shutdown(); - return 0; -} diff --git a/robot/ros_ws/src/perception/natnet_ros2/src/px4_param_setter_node.py b/robot/ros_ws/src/perception/natnet_ros2/src/px4_param_setter_node.py deleted file mode 100755 index 1a08bef30..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/src/px4_param_setter_node.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 - -""" -PX4 Parameter Checker Node - -Compares the FCU's PX4 parameters against a configured set (see -config/px4_params.yaml) for mocap-only flight (OptiTrack external vision, no GNSS, -no magnetometer). **By default it only checks and flags** — it does not write to the -FCU. The desired values are meant to be set once by a human in QGroundControl (see -docs/robot/…/px4_external_vision.md); this node is a safety net that catches a -mis-configured FCU before flight. - -Two safety flags control behaviour: - -- ``auto_set`` (default ``false``): when ``true``, the node also *writes* any - mismatched param via ``param/set`` (ParamSetV2) and verifies the readback — the - legacy enforce behaviour. When ``false`` (default) the node never writes. -- ``on_mismatch`` (``warn`` | ``halt``, default ``warn``): with ``auto_set=false``, - what to do when a param disagrees. ``warn`` logs the diffs and lets the stack come - up; ``halt`` logs fatal and exits non-zero so a ``required`` launch node tears the - stack down. - -For each entry under the ``params.`` prefix the node waits for an FCU connection + -settle, reads the current value via ``get_parameters``, and compares. Type mapping -follows the YAML literal: integers → MAVLink int params, floats → float params — -write ``6.0`` (not ``6``) for float params like EKF2_EV_DELAY so the type matches. -""" - -import math -import sys - -import rclpy -from rclpy.node import Node -from rclpy.parameter import Parameter -from rcl_interfaces.msg import ParameterValue, ParameterType -from rcl_interfaces.srv import GetParameters -from mavros_msgs.msg import State -from mavros_msgs.srv import ParamSetV2 - - -class Px4ParamSetterNode(Node): - """PX4 parameter checker (optionally setter) via the MAVROS param plugin.""" - - def __init__(self): - super().__init__( - 'px4_param_setter', - automatically_declare_parameters_from_overrides=True, - ) - - self._enabled = self._param_or('enabled', True) - if not self._enabled: - self.get_logger().info('PX4 param checker disabled (enabled=false).') - return - - # Safety flags: check-only by default; opt in to writing with auto_set. - self._auto_set = bool(self._param_or('auto_set', False)) - self._on_mismatch = str(self._param_or('on_mismatch', 'warn')).lower() - if self._on_mismatch not in ('warn', 'halt'): - self.get_logger().warn( - f"Invalid on_mismatch {self._on_mismatch!r}; falling back to 'warn'." - ) - self._on_mismatch = 'warn' - - # Seconds after MAVROS connects before the first attempt (initial - # param-table pull over serial takes a while at 115200 baud). - self._settle_sec = float(self._param_or('settle_sec', 10.0)) - self._retry_period_sec = float(self._param_or('retry_period_sec', 2.0)) - self._max_attempts = int(self._param_or('max_attempts', 30)) - - # Desired FCU params from the params.* prefix; YAML int → PX4 int32, - # YAML float → PX4 float. - self._desired = { - name: p.value - for name, p in self.get_parameters_by_prefix('params').items() - } - self._pending = dict(self._desired) - self._changed: list[str] = [] - self._skipped: list[str] = [] - # (param_id, current, desired) for params that disagree and were NOT set - # (auto_set=false). Drives the on_mismatch policy in _finish(). - self._mismatched: list[tuple] = [] - self._attempts = 0 - self._connected_since = None - self._done = False - self._inflight = False - - if not self._pending: - self.get_logger().warn('No params.* entries configured; nothing to do.') - self._done = True - return - - self._get_cli = self.create_client(GetParameters, 'param_get_parameters') - self._set_cli = self.create_client(ParamSetV2, 'param_set') - self._state_sub = self.create_subscription( - State, 'mavros_state', self._on_mavros_state, 10 - ) - self._timer = self.create_timer(self._retry_period_sec, self._tick) - - mode = 'auto-set' if self._auto_set else f'check-only (on_mismatch={self._on_mismatch})' - self.get_logger().info( - f'PX4 param checker started [{mode}]: {len(self._pending)} params ' - f'({", ".join(sorted(self._pending))}), settle_sec={self._settle_sec}' - ) - - def _param_or(self, name, default): - """Return a declared-from-overrides parameter value, or the default.""" - if self.has_parameter(name): - value = self.get_parameter(name).value - if value is not None: - return value - return default - - # --- MAVROS state ------------------------------------------------------ - - def _on_mavros_state(self, msg: State): - if msg.connected and self._connected_since is None: - self._connected_since = self.get_clock().now() - self.get_logger().info('FCU connected; waiting for param table to settle.') - - # --- Main retry loop --------------------------------------------------- - - def _tick(self): - if self._done or self._inflight: - return - if self._connected_since is None: - return - elapsed = (self.get_clock().now() - self._connected_since).nanoseconds * 1e-9 - if elapsed < self._settle_sec: - return - if not self._pending: - self._finish() - return - if self._attempts >= self._max_attempts: - self.get_logger().error( - f'Giving up after {self._attempts} attempts; ' - f'unset params: {", ".join(sorted(self._pending))}' - ) - self._finish() - return - - self._attempts += 1 - param_id = sorted(self._pending)[0] - if not self._get_cli.service_is_ready() or not self._set_cli.service_is_ready(): - self.get_logger().info('MAVROS param services not ready yet; retrying.') - return - - self._inflight = True - req = GetParameters.Request(names=[param_id]) - future = self._get_cli.call_async(req) - future.add_done_callback( - lambda f, pid=param_id: self._on_get_done(pid, f) - ) - - # --- Get → compare → set → verify chain -------------------------------- - - def _on_get_done(self, param_id: str, future): - try: - resp = future.result() - except Exception as e: # noqa: BLE001 — retry on any transport error - self.get_logger().warn(f'{param_id}: get_parameters failed ({e}); will retry.') - self._inflight = False - return - - current = resp.values[0] if resp.values else None - if current is not None and self._matches(current, self._desired[param_id]): - self.get_logger().info(f'{param_id}: already {self._desired[param_id]} — skipping.') - self._skipped.append(param_id) - del self._pending[param_id] - self._inflight = False - return - if current is None or current.type == ParameterType.PARAMETER_NOT_SET: - # Param table likely not pulled yet — retry rather than flag/force-set. - self.get_logger().info(f'{param_id}: not in MAVROS param table yet; will retry.') - self._inflight = False - return - - # Mismatch. Check-only mode (default): record and flag, never write. - if not self._auto_set: - self._mismatched.append( - (param_id, self._value_of(current), self._desired[param_id]) - ) - del self._pending[param_id] - self._inflight = False - return - - req = ParamSetV2.Request() - req.force_set = False - req.param_id = param_id - req.value = self._to_parameter_value(self._desired[param_id]) - set_future = self._set_cli.call_async(req) - set_future.add_done_callback( - lambda f, pid=param_id, old=self._value_of(current): self._on_set_done(pid, old, f) - ) - - def _on_set_done(self, param_id: str, old_value, future): - self._inflight = False - try: - resp = future.result() - except Exception as e: # noqa: BLE001 — retry on any transport error - self.get_logger().warn(f'{param_id}: set failed ({e}); will retry.') - return - - desired = self._desired[param_id] - if not resp.success or not self._matches(resp.value, desired): - self.get_logger().warn( - f'{param_id}: set rejected or readback mismatch ' - f'(wanted {desired}, got {self._value_of(resp.value)}); will retry.' - ) - return - - self.get_logger().info(f'{param_id}: {old_value} -> {desired}') - self._changed.append(param_id) - del self._pending[param_id] - - def _finish(self): - self._done = True - self._timer.cancel() - self.get_logger().info( - f'PX4 param check finished: {len(self._skipped)} already correct, ' - f'{len(self._changed)} set, {len(self._mismatched)} mismatched, ' - f'{len(self._pending)} unread.' - ) - if self._changed: - self.get_logger().warn( - f'FCU parameters changed ({", ".join(sorted(self._changed))}). ' - 'Reboot the flight controller before flying so EKF2 starts clean.' - ) - - # Check-only mismatches: report each, then apply the on_mismatch policy. - if self._mismatched: - for pid, current, desired in sorted(self._mismatched): - self.get_logger().warn( - f'{pid}: FCU has {current}, expected {desired} ' - '(not set — auto_set=false). Fix in QGroundControl.' - ) - names = ", ".join(sorted(p for p, _, _ in self._mismatched)) - if self._on_mismatch == 'halt': - self.get_logger().fatal( - f'{len(self._mismatched)} PX4 param(s) wrong for external-vision ' - f'flight ({names}); halting (on_mismatch=halt). Set them in ' - 'QGroundControl or enable auto_set.' - ) - # SystemExit propagates out of spin(); main()'s finally shuts down - # rclpy. Non-zero code lets a `required` launch node tear the stack down. - sys.exit(1) - self.get_logger().warn( - f'{len(self._mismatched)} PX4 param(s) differ from the external-vision ' - f'set ({names}); continuing (on_mismatch=warn).' - ) - - # --- Value helpers ------------------------------------------------------ - - @staticmethod - def _to_parameter_value(value) -> ParameterValue: - pv = ParameterValue() - if isinstance(value, bool) or isinstance(value, int): - pv.type = ParameterType.PARAMETER_INTEGER - pv.integer_value = int(value) - elif isinstance(value, float): - pv.type = ParameterType.PARAMETER_DOUBLE - pv.double_value = value - else: - raise TypeError(f'Unsupported PX4 param value type: {type(value)}') - return pv - - @staticmethod - def _value_of(pv: ParameterValue): - if pv.type == ParameterType.PARAMETER_INTEGER: - return pv.integer_value - if pv.type == ParameterType.PARAMETER_DOUBLE: - return pv.double_value - return None - - @classmethod - def _matches(cls, pv: ParameterValue, desired) -> bool: - current = cls._value_of(pv) - if current is None: - return False - # FCU floats are float32 — compare with a tolerance that absorbs the - # float64 → float32 round trip. - return math.isclose(float(current), float(desired), rel_tol=1e-5, abs_tol=1e-6) - - -def main(args=None): - rclpy.init(args=args) - try: - node = Px4ParamSetterNode() - rclpy.spin(node) - except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException): - pass - finally: - rclpy.try_shutdown() - - -if __name__ == '__main__': - main() diff --git a/robot/ros_ws/src/perception/natnet_ros2/src/vision_pose_converter_node.py b/robot/ros_ws/src/perception/natnet_ros2/src/vision_pose_converter_node.py deleted file mode 100755 index 88caede52..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/src/vision_pose_converter_node.py +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env python3 - -""" -Vision Pose Converter Node - -Optional converter that bridges NatNet pose data to MAVROS vision_pose format -for PX4 external pose estimation and state fusion. - -Converts from NatNet coordinate frame to a frame suitable for MAVROS. - -Topics are configurable so the bridge can be retargeted to other middleware. -``input_topic`` / ``output_pose_topic`` / ``output_pose_cov_topic`` default to the -relative names ``input_pose`` / ``output_pose`` / ``output_pose_cov`` (remappable), -but natnet_ros2.launch.py overrides them with the absolute, ROBOT_NAME-namespaced -topics from the robot's ``vision_pose`` block in natnet_config.yaml. -""" - -import rclpy -from rclpy.node import Node -from geometry_msgs.msg import PoseStamped, PoseWithCovarianceStamped - - -class VisionPoseConverterNode(Node): - """ - Converts NatNet pose to MAVROS vision_pose format. - - Listens to NatNet pose data and publishes to MAVROS for external - pose feedback to PX4 autopilot. - """ - - def __init__(self): - super().__init__('vision_pose_converter') - - self.declare_parameter('frame_id', 'world') - self.declare_parameter('child_frame_id', 'base_link') - self.declare_parameter('canonical_quaternion', True) - # Max output rate to MAVROS (0 = passthrough). Each pose becomes a - # ~116-byte VISION_POSITION_ESTIMATE on the FCU serial link; at - # 115200 baud (~11.5 kB/s) a full-rate 100+ Hz mocap stream alone - # overflows the MAVROS TX queue. EKF2 only needs 30-50 Hz. - self.declare_parameter('max_rate_hz', 30.0) - # Which MAVROS vision_pose topic(s) to publish: 'pose', 'pose_cov', or - # 'both'. MAVROS turns EACH of vision_pose/pose and vision_pose/pose_cov - # into its own VISION_POSITION_ESTIMATE on the FCU link, so 'both' sends - # msg 102 at 2x the rate. Use a single topic to halve serial TX load. - self.declare_parameter('publish_mode', 'both') - # Topic names — overridden by the launch file from the per-robot - # vision_pose block; defaults are the historical remappable relative names. - self.declare_parameter('input_topic', 'input_pose') - self.declare_parameter('output_pose_topic', 'output_pose') - self.declare_parameter('output_pose_cov_topic', 'output_pose_cov') - - self.frame_id = self.get_parameter('frame_id').value - self.child_frame_id = self.get_parameter('child_frame_id').value - self.canonical_quaternion = self.get_parameter('canonical_quaternion').value - max_rate_hz = self.get_parameter('max_rate_hz').value - publish_mode = str(self.get_parameter('publish_mode').value).lower() - if publish_mode not in ('pose', 'pose_cov', 'both'): - self.get_logger().warn( - f"Invalid publish_mode {publish_mode!r}; falling back to 'both'" - ) - publish_mode = 'both' - self._publish_pose = publish_mode in ('pose', 'both') - self._publish_pose_cov = publish_mode in ('pose_cov', 'both') - # 0.95 factor so an input stream at exactly max_rate_hz doesn't beat - # against the period check and alias down to half rate. - self._min_period_ns = 0 if max_rate_hz <= 0.0 else int(0.95e9 / max_rate_hz) - self._last_pub_ns = 0 - input_topic = self.get_parameter('input_topic').value - output_pose_topic = self.get_parameter('output_pose_topic').value - output_pose_cov_topic = self.get_parameter('output_pose_cov_topic').value - - # Subscribers - self.pose_sub = self.create_subscription( - PoseWithCovarianceStamped, - input_topic, - self._on_pose, - 10 - ) - - # Publishers - self.pose_pub = self.create_publisher( - PoseStamped, - output_pose_topic, - 10 - ) - self.pose_cov_pub = self.create_publisher( - PoseWithCovarianceStamped, - output_pose_cov_topic, - 10 - ) - - self.get_logger().info( - f'Vision pose converter started ' - f'(frame_id={self.frame_id!r}, child_frame_id={self.child_frame_id!r}, ' - f'canonical_quaternion={self.canonical_quaternion}, ' - f'max_rate_hz={max_rate_hz}, publish_mode={publish_mode!r}, ' - f'input_topic={input_topic!r}, output_pose_topic={output_pose_topic!r}, ' - f'output_pose_cov_topic={output_pose_cov_topic!r})' - ) - - @staticmethod - def _canonical_quaternion(o): - """ - Return the quaternion in canonical form (qw >= 0) by negating all - four components when qw < 0. - - q and -q represent the same 3-D rotation, but some EKF implementations - (ArduPilot EKF3 in particular) are sensitive to sign flips between - consecutive frames. Keeping qw >= 0 guarantees a consistent - representation across the full orientation space. - """ - if o.w < 0.0: - o.x, o.y, o.z, o.w = -o.x, -o.y, -o.z, -o.w - return o - - def _on_pose(self, msg: PoseWithCovarianceStamped): - """ - Callback for incoming NatNet pose. - - Converts and republishes for MAVROS consumption. - Normalises the quaternion to canonical form (qw >= 0) before publishing - so that EKF consumers never see a sign-flip discontinuity. - """ - try: - if self._min_period_ns: - now_ns = self.get_clock().now().nanoseconds - if now_ns - self._last_pub_ns < self._min_period_ns: - return - self._last_pub_ns = now_ns - - msg.header.frame_id = self.frame_id - if self.canonical_quaternion: - msg.pose.pose.orientation = self._canonical_quaternion( - msg.pose.pose.orientation - ) - - if self._publish_pose_cov: - self.pose_cov_pub.publish(msg) - - if self._publish_pose: - pose_msg = PoseStamped() - pose_msg.header = msg.header - pose_msg.pose = msg.pose.pose - self.pose_pub.publish(pose_msg) - - except Exception as e: - self.get_logger().error(f"Error converting pose: {e}") - - -def main(args=None): - """Main entry point""" - rclpy.init(args=args) - try: - node = VisionPoseConverterNode() - rclpy.spin(node) - except KeyboardInterrupt: - pass - finally: - rclpy.shutdown() - - -if __name__ == '__main__': - main() diff --git a/robot/ros_ws/src/perception/natnet_ros2/test/fake_natnet_client.hpp b/robot/ros_ws/src/perception/natnet_ros2/test/fake_natnet_client.hpp deleted file mode 100644 index 3ee0dd8b2..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/test/fake_natnet_client.hpp +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// fake_natnet_client.hpp — in-process test double for INatNetClient. -// -// Used exclusively in unit tests (test_natnet_logic.cpp). -// Never included in production binaries. -// -// Usage: -// FakeNatNetClient fake; -// fake.connect_result = NatNetResult::OK; -// fake.server_info = { .host_present = true, .host_app_name = "Motive" }; -// fake.body_descriptors = {{ .id=1, .name="Drone", .parent_id=-1 }}; -// -// auto result = natnet_ros2::negotiate(fake, cfg); -// EXPECT_TRUE(result.ok); - -#pragma once - -#include "natnet_ros2/natnet_logic.hpp" - -#include -#include -#include - -namespace natnet_ros2 -{ - -class FakeNatNetClient : public INatNetClient -{ -public: - // ----------------------------------------------------------------------- - // Configurable behaviour — set before calling negotiate() / testing - // ----------------------------------------------------------------------- - - /// What connect() should return. - NatNetResult connect_result = NatNetResult::OK; - - /// What get_server_info() should populate and return. - /// Set host_present = true to simulate a fully-identified server. - ServerInfo server_info; - - /// get_server_info() return value (independent of server_info.host_present, - /// so tests can simulate "SDK call failed" vs. "host not present"). - bool server_info_call_succeeds = true; - - /// What get_body_descriptors() should return. - std::vector body_descriptors; - - // ----------------------------------------------------------------------- - // Call-record state — inspect after exercising the fake - // ----------------------------------------------------------------------- - - bool connect_was_called = false; - ConnectConfig last_connect_config; - - bool server_info_was_called = false; - bool descriptors_was_called = false; - bool set_callback_was_called = false; - bool disconnect_was_called = false; - - /// Frames that were injected via inject_frame(). - int frames_injected = 0; - - // ----------------------------------------------------------------------- - // INatNetClient overrides - // ----------------------------------------------------------------------- - - NatNetResult connect(const ConnectConfig & cfg) override - { - connect_was_called = true; - last_connect_config = cfg; - return connect_result; - } - - bool get_server_info(ServerInfo & out) override - { - server_info_was_called = true; - out = server_info; - return server_info_call_succeeds; - } - - std::vector get_body_descriptors() override - { - descriptors_was_called = true; - return body_descriptors; - } - - void set_frame_callback(std::function cb) override - { - set_callback_was_called = true; - frame_cb_ = cb; - } - - void disconnect() override - { - disconnect_was_called = true; - } - - // ----------------------------------------------------------------------- - // Test helper: push a synthetic frame into the registered callback. - // ----------------------------------------------------------------------- - void inject_frame(const FrameSample & frame) - { - if (frame_cb_) { - ++frames_injected; - frame_cb_(frame); - } - } - - /// Convenience: build and inject a single-body tracking frame. - void inject_body(int32_t id, float x, float y, float z, - float qx = 0.f, float qy = 0.f, - float qz = 0.f, float qw = 1.f, - int16_t rb_params = 0x01 /* tracking valid */, - int16_t frame_params = 0x00) - { - FrameSample f; - f.params = frame_params; - RigidBodySample rb; - rb.id = id; rb.x = x; rb.y = y; rb.z = z; - rb.qx = qx; rb.qy = qy; rb.qz = qz; rb.qw = qw; - rb.params = rb_params; - f.bodies.push_back(rb); - inject_frame(f); - } - - // ----------------------------------------------------------------------- - // Reset all recorded state (keep configuration). - // ----------------------------------------------------------------------- - void reset_records() - { - connect_was_called = false; - server_info_was_called = false; - descriptors_was_called = false; - set_callback_was_called = false; - disconnect_was_called = false; - frames_injected = 0; - last_connect_config = {}; - } - -private: - std::function frame_cb_; -}; - -} // namespace natnet_ros2 diff --git a/robot/ros_ws/src/perception/natnet_ros2/test/test_natnet_logic.cpp b/robot/ros_ws/src/perception/natnet_ros2/test/test_natnet_logic.cpp deleted file mode 100644 index f469c2a29..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/test/test_natnet_logic.cpp +++ /dev/null @@ -1,807 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Unit tests for natnet_ros2/natnet_logic.hpp. -// -// NO dependency on the NatNet SDK or rclcpp — compiles with gtest only. -// Run via: -// colcon test --packages-select natnet_ros2 --event-handlers console_direct+ -// colcon test-result --test-result-base build/natnet_ros2 --verbose - -#include -#include "natnet_ros2/natnet_logic.hpp" -#include "fake_natnet_client.hpp" - -using namespace natnet_ros2; - - -// =========================================================================== -// Covariance -// =========================================================================== - -TEST(BuildCovariance6x6, DiagonalBlocksLandInCorrectSlots) -{ - const std::vector pos = {0.1, 0.0, 0.0, - 0.0, 0.1, 0.0, - 0.0, 0.0, 0.1}; - const std::vector ori = {0.01, 0.0, 0.0, - 0.0, 0.01, 0.0, - 0.0, 0.0, 0.01}; - - auto cov = build_covariance_6x6(pos, ori); - - ASSERT_EQ(cov.size(), 36u); - EXPECT_DOUBLE_EQ(cov[0 * 6 + 0], 0.1); - EXPECT_DOUBLE_EQ(cov[1 * 6 + 1], 0.1); - EXPECT_DOUBLE_EQ(cov[2 * 6 + 2], 0.1); - EXPECT_DOUBLE_EQ(cov[3 * 6 + 3], 0.01); - EXPECT_DOUBLE_EQ(cov[4 * 6 + 4], 0.01); - EXPECT_DOUBLE_EQ(cov[5 * 6 + 5], 0.01); -} - -TEST(BuildCovariance6x6, CrossBlockEntriesAreZero) -{ - const std::vector ones(9, 1.0); - auto cov = build_covariance_6x6(ones, ones); - - for (int r = 0; r < 6; ++r) { - for (int c = 0; c < 6; ++c) { - const bool in_pos = (r < 3 && c < 3); - const bool in_ori = (r >= 3 && c >= 3); - if (!in_pos && !in_ori) { - EXPECT_DOUBLE_EQ(cov[r * 6 + c], 0.0) - << "Expected 0 at [" << r << "][" << c << "]"; - } - } - } -} - -TEST(BuildCovariance6x6, OffDiagonalEntriesPreserved) -{ - const std::vector pos = {1, 2, 3, 4, 5, 6, 7, 8, 9}; - const std::vector ori = {0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9}; - - auto cov = build_covariance_6x6(pos, ori); - - EXPECT_DOUBLE_EQ(cov[1 * 6 + 2], 6.0); // pos[1][2] - EXPECT_DOUBLE_EQ(cov[2 * 6 + 0], 7.0); // pos[2][0] - EXPECT_DOUBLE_EQ(cov[3 * 6 + 4], 0.2); // ori[0][1] - EXPECT_DOUBLE_EQ(cov[5 * 6 + 4], 0.8); // ori[2][1] -} - -TEST(BuildCovariance6x6, ShortInputFillsRemainingWithZero) -{ - auto cov = build_covariance_6x6({0.5}, {0.05}); - - EXPECT_DOUBLE_EQ(cov[0 * 6 + 0], 0.5); - EXPECT_DOUBLE_EQ(cov[3 * 6 + 3], 0.05); - EXPECT_DOUBLE_EQ(cov[0 * 6 + 1], 0.0); - EXPECT_DOUBLE_EQ(cov[4 * 6 + 4], 0.0); -} - -TEST(BuildCovariance6x6, EmptyInputsProduceAllZeros) -{ - auto cov = build_covariance_6x6({}, {}); - for (double v : cov) { EXPECT_DOUBLE_EQ(v, 0.0); } -} - -TEST(BuildCovariance6x6, OutputIsExactly36Elements) -{ - EXPECT_EQ(static_cast(build_covariance_6x6({}, {}).size()), 36); -} - - -// =========================================================================== -// Topic names -// =========================================================================== - -TEST(TopicNames, BaseTopicFormat) -{ - EXPECT_EQ(optitrack_topic_base("robot_1", "Drone"), - "/robot_1/perception/optitrack/Drone"); -} - -TEST(TopicNames, PoseCovTopicAppendsSuffix) -{ - const std::string base = optitrack_topic_base("robot_1", "Drone"); - const std::string cov = optitrack_pose_cov_topic("robot_1", "Drone"); - EXPECT_EQ(cov, base + "/pose_cov"); -} - -TEST(TopicNames, DifferentRobotsGetDifferentNamespaces) -{ - EXPECT_NE(optitrack_topic_base("robot_1", "Body"), - optitrack_topic_base("robot_2", "Body")); -} - -TEST(TopicNames, LeadingSlashPresent) -{ - EXPECT_EQ(optitrack_topic_base("robot_1", "Body")[0], '/'); -} - -TEST(TopicNames, NamespacedTopicStripsLeadingSlashes) -{ - EXPECT_EQ(namespaced_topic("robot_1", "perception/optitrack/drone"), - "/robot_1/perception/optitrack/drone"); - EXPECT_EQ(namespaced_topic("robot_1", "/perception/optitrack/drone"), - "/robot_1/perception/optitrack/drone"); - EXPECT_EQ(namespaced_topic("robot_2", "///a/b"), "/robot_2/a/b"); -} - -TEST(TopicNames, BodyTopicBaseUsesOverrideWhenSet) -{ - // Empty override → default perception/optitrack/{name} - EXPECT_EQ(body_topic_base("robot_1", "Drone", ""), - "/robot_1/perception/optitrack/Drone"); - // Non-empty override → namespaced relative leaf (decoupled from body name) - EXPECT_EQ(body_topic_base("robot_1", "Drone", "perception/optitrack/drone"), - "/robot_1/perception/optitrack/drone"); - EXPECT_EQ(body_topic_base("robot_3", "Target", "perception/optitrack/target"), - "/robot_3/perception/optitrack/target"); -} - -// =========================================================================== -// Multi-body filtering — body_is_configured -// =========================================================================== - -TEST(BodyIsConfigured, MatchesConfiguredIds) -{ - const std::vector ids = {1, 100}; - EXPECT_TRUE(body_is_configured(ids, 1)); - EXPECT_TRUE(body_is_configured(ids, 100)); - EXPECT_FALSE(body_is_configured(ids, 2)); -} - -TEST(BodyIsConfigured, EmptySetMatchesNothing) -{ - const std::vector ids = {}; - EXPECT_FALSE(body_is_configured(ids, 0)); - EXPECT_FALSE(body_is_configured(ids, 1)); -} - - -// =========================================================================== -// Server negotiation — validate_connection_type -// =========================================================================== - -TEST(ValidateConnectionType, UnicastPassesThrough) -{ - EXPECT_EQ(validate_connection_type("unicast"), "unicast"); -} - -TEST(ValidateConnectionType, MulticastPassesThrough) -{ - EXPECT_EQ(validate_connection_type("multicast"), "multicast"); -} - -TEST(ValidateConnectionType, UnknownThrows) -{ - EXPECT_THROW(validate_connection_type("broadcast"), std::invalid_argument); - EXPECT_THROW(validate_connection_type(""), std::invalid_argument); - EXPECT_THROW(validate_connection_type("UDP"), std::invalid_argument); -} - -TEST(ValidateConnectionType, CaseSensitiveThrows) -{ - // Accepting "Unicast" would mean the config silently disagrees with itself. - EXPECT_THROW(validate_connection_type("Unicast"), std::invalid_argument); - EXPECT_THROW(validate_connection_type("MULTICAST"), std::invalid_argument); -} - -TEST(ValidateConnectionType, MessageNamesTheOffendingValue) -{ - try { - validate_connection_type("broadcst"); - FAIL() << "expected std::invalid_argument"; - } catch (const std::invalid_argument & e) { - EXPECT_NE(std::string(e.what()).find("broadcst"), std::string::npos); - } -} - - -// =========================================================================== -// Server negotiation — ConnectConfig + make_connect_config -// =========================================================================== - -TEST(ConnectConfig, DefaultsAreUnicast) -{ - const ConnectConfig cfg{}; - EXPECT_EQ(cfg.connection_type, "unicast"); - EXPECT_FALSE(is_multicast(cfg)); -} - -TEST(ConnectConfig, UnicastConfigNotMulticast) -{ - const auto cfg = make_connect_config( - "10.0.0.1", "0.0.0.0", 1510, 1511, "unicast"); - EXPECT_FALSE(is_multicast(cfg)); - EXPECT_FALSE(needs_multicast_address(cfg)); -} - -TEST(ConnectConfig, MulticastConfigIsMulticast) -{ - const auto cfg = make_connect_config( - "10.0.0.1", "0.0.0.0", 1510, 1511, "multicast", "239.255.42.99"); - EXPECT_TRUE(is_multicast(cfg)); - EXPECT_TRUE(needs_multicast_address(cfg)); - EXPECT_EQ(cfg.multicast_address, "239.255.42.99"); -} - -TEST(ConnectConfig, InvalidConnectionTypeThrows) -{ - EXPECT_THROW( - make_connect_config("10.0.0.1", "0.0.0.0", 1510, 1511, "broadcast"), - std::invalid_argument); -} - -TEST(ConnectConfig, PortsArePreserved) -{ - const auto cfg = make_connect_config( - "192.168.0.100", "192.168.0.200", 9000u, 9001u, "unicast"); - EXPECT_EQ(cfg.server_ip, "192.168.0.100"); - EXPECT_EQ(cfg.client_ip, "192.168.0.200"); - EXPECT_EQ(cfg.command_port, 9000u); - EXPECT_EQ(cfg.data_port, 9001u); -} - -TEST(ConnectConfig, CustomMulticastAddress) -{ - const auto cfg = make_connect_config( - "10.0.0.1", "0.0.0.0", 1510, 1511, "multicast", "239.0.0.1"); - EXPECT_EQ(cfg.multicast_address, "239.0.0.1"); -} - -TEST(ConnectConfig, UnicastAddressFieldIgnored) -{ - // multicast_address is still stored but should not be passed to the SDK - const auto cfg = make_connect_config( - "10.0.0.1", "0.0.0.0", 1510, 1511, "unicast", "239.255.42.99"); - EXPECT_FALSE(needs_multicast_address(cfg)); -} - - -// =========================================================================== -// Data streaming — is_tracking_valid -// =========================================================================== - -TEST(IsTrackingValid, Bit0SetMeansValid) -{ - EXPECT_TRUE(is_tracking_valid(0x01)); - EXPECT_TRUE(is_tracking_valid(0x03)); // bits 0 and 1 - EXPECT_TRUE(is_tracking_valid(0xFF)); -} - -TEST(IsTrackingValid, Bit0ClearMeansInvalid) -{ - EXPECT_FALSE(is_tracking_valid(0x00)); - EXPECT_FALSE(is_tracking_valid(0x02)); // only bit 1 set - EXPECT_FALSE(is_tracking_valid(0xFE)); // all bits except 0 -} - - -// =========================================================================== -// Data streaming — model_list_changed -// =========================================================================== - -TEST(ModelListChanged, Bit1SetMeansChanged) -{ - EXPECT_TRUE(model_list_changed(0x02)); - EXPECT_TRUE(model_list_changed(0x03)); - EXPECT_TRUE(model_list_changed(0xFF)); -} - -TEST(ModelListChanged, Bit1ClearMeansNotChanged) -{ - EXPECT_FALSE(model_list_changed(0x00)); - EXPECT_FALSE(model_list_changed(0x01)); // only bit 0 - EXPECT_FALSE(model_list_changed(0xFD)); // all bits except 1 -} - - -// =========================================================================== -// Data streaming — should_publish_body -// =========================================================================== - -TEST(ShouldPublishBody, NegativeFilterMeansPublishAll) -{ - EXPECT_TRUE(should_publish_body(-1, 0)); - EXPECT_TRUE(should_publish_body(-1, 1)); - EXPECT_TRUE(should_publish_body(-1, 999)); -} - -TEST(ShouldPublishBody, ZeroFilterAllowsOnlyId0) -{ - EXPECT_TRUE(should_publish_body(0, 0)); - EXPECT_FALSE(should_publish_body(0, 1)); - EXPECT_FALSE(should_publish_body(0, 999)); -} - -TEST(ShouldPublishBody, PositiveFilterMatchesExact) -{ - EXPECT_TRUE(should_publish_body(5, 5)); - EXPECT_FALSE(should_publish_body(5, 4)); - EXPECT_FALSE(should_publish_body(5, 6)); -} - - -// =========================================================================== -// Data streaming — rb_to_pose (sample data conversion) -// =========================================================================== - -TEST(RbToPose, PositionComponentsConvertedToDouble) -{ - RigidBodySample rb; - rb.x = 1.5f; rb.y = -2.25f; rb.z = 0.5f; - rb.qx = 0.f; rb.qy = 0.f; rb.qz = 0.f; rb.qw = 1.f; - - const PoseData p = rb_to_pose(rb); - - EXPECT_DOUBLE_EQ(p.x, static_cast(1.5f)); - EXPECT_DOUBLE_EQ(p.y, static_cast(-2.25f)); - EXPECT_DOUBLE_EQ(p.z, static_cast(0.5f)); -} - -TEST(RbToPose, OrientationComponentsConvertedToDouble) -{ - RigidBodySample rb; - rb.x = 0.f; rb.y = 0.f; rb.z = 0.f; - // 90-degree rotation about Z: qw = cos(45°), qz = sin(45°) - rb.qx = 0.f; - rb.qy = 0.f; - rb.qz = 0.7071068f; - rb.qw = 0.7071068f; - - const PoseData p = rb_to_pose(rb); - - EXPECT_NEAR(p.qz, 0.7071068, 1e-6); - EXPECT_NEAR(p.qw, 0.7071068, 1e-6); - EXPECT_DOUBLE_EQ(p.qx, 0.0); - EXPECT_DOUBLE_EQ(p.qy, 0.0); -} - -TEST(RbToPose, IdentityOrientationPreserved) -{ - RigidBodySample rb; // default: x=y=z=0, qw=1 - const PoseData p = rb_to_pose(rb); - - EXPECT_DOUBLE_EQ(p.x, 0.0); - EXPECT_DOUBLE_EQ(p.y, 0.0); - EXPECT_DOUBLE_EQ(p.z, 0.0); - EXPECT_DOUBLE_EQ(p.qx, 0.0); - EXPECT_DOUBLE_EQ(p.qy, 0.0); - EXPECT_DOUBLE_EQ(p.qz, 0.0); - EXPECT_DOUBLE_EQ(p.qw, 1.0); -} - -TEST(RbToPose, NegativeCoordinates) -{ - RigidBodySample rb; - rb.x = -10.f; rb.y = -20.f; rb.z = -30.f; - rb.qw = 1.f; - - const PoseData p = rb_to_pose(rb); - - EXPECT_DOUBLE_EQ(p.x, static_cast(-10.f)); - EXPECT_DOUBLE_EQ(p.y, static_cast(-20.f)); - EXPECT_DOUBLE_EQ(p.z, static_cast(-30.f)); -} - - -// =========================================================================== -// Data streaming — FrameSample helpers (integration-style scenarios) -// =========================================================================== - -// Simulate a frame where one body is tracking and one is not. -TEST(FrameSample, TrackingFilterApplied) -{ - FrameSample frame; - frame.frame_num = 42; - frame.timestamp = 1.234f; - frame.params = 0x00; - - RigidBodySample tracking, lost; - tracking.id = 1; - tracking.params = 0x01; // valid - lost.id = 2; - lost.params = 0x00; // invalid - - frame.bodies = {tracking, lost}; - - int published = 0; - for (const auto & rb : frame.bodies) { - if (is_tracking_valid(rb.params)) { - ++published; - } - } - EXPECT_EQ(published, 1); -} - -// Simulate a frame that signals model-list changed while also carrying data. -TEST(FrameSample, ModelListChangedFlagDetected) -{ - FrameSample frame; - frame.params = 0x02; // bit 1 set - - EXPECT_TRUE(model_list_changed(frame.params)); -} - -// Simulate single-body tracking filter: only body id=3 should be published. -TEST(FrameSample, SingleBodyFilterSelectsCorrectBody) -{ - FrameSample frame; - frame.params = 0x00; - - for (int id : {1, 2, 3, 4, 5}) { - RigidBodySample rb; - rb.id = id; - rb.params = 0x01; // all tracking valid - frame.bodies.push_back(rb); - } - - constexpr int32_t filter = 3; - std::vector published; - - for (const auto & rb : frame.bodies) { - if (is_tracking_valid(rb.params) && should_publish_body(filter, rb.id)) { - published.push_back(rb.id); - } - } - - ASSERT_EQ(published.size(), 1u); - EXPECT_EQ(published[0], 3); -} - -// Simulate all-body mode: every valid body gets a PoseData. -TEST(FrameSample, AllBodyModePublishesAllTrackedBodies) -{ - FrameSample frame; - for (int id = 1; id <= 4; ++id) { - RigidBodySample rb; - rb.id = id; - rb.x = static_cast(id); - rb.params = (id % 2 == 0) ? int16_t(0x01) : int16_t(0x00); // even = valid - frame.bodies.push_back(rb); - } - - std::vector out; - for (const auto & rb : frame.bodies) { - if (is_tracking_valid(rb.params) && should_publish_body(-1, rb.id)) { - out.push_back(rb_to_pose(rb)); - } - } - - // Bodies 2 and 4 are valid - ASSERT_EQ(out.size(), 2u); - EXPECT_DOUBLE_EQ(out[0].x, static_cast(2.f)); - EXPECT_DOUBLE_EQ(out[1].x, static_cast(4.f)); -} - -// Verify covariance is stamped into the output as expected. -TEST(FrameSample, CovarianceStampedIntoMessage) -{ - const std::vector pos_cov(9, 0.1); - const std::vector ori_cov(9, 0.01); - const auto cov = build_covariance_6x6(pos_cov, ori_cov); - - // Simulate what natnet_ros2_node.cpp does when building PoseWithCovarianceStamped - std::array msg_covariance = cov6x6_to_array(cov); - - // Position diagonal - EXPECT_DOUBLE_EQ(msg_covariance[0 * 6 + 0], 0.1); - EXPECT_DOUBLE_EQ(msg_covariance[1 * 6 + 1], 0.1); - EXPECT_DOUBLE_EQ(msg_covariance[2 * 6 + 2], 0.1); - // Orientation diagonal - EXPECT_DOUBLE_EQ(msg_covariance[3 * 6 + 3], 0.01); - EXPECT_DOUBLE_EQ(msg_covariance[4 * 6 + 4], 0.01); - EXPECT_DOUBLE_EQ(msg_covariance[5 * 6 + 5], 0.01); - // Cross-block zeros - EXPECT_DOUBLE_EQ(msg_covariance[0 * 6 + 3], 0.0); - EXPECT_DOUBLE_EQ(msg_covariance[3 * 6 + 0], 0.0); -} - - -// =========================================================================== -// Server negotiation — negotiate() + FakeNatNetClient -// =========================================================================== - -// --------------- helpers --------------------------------------------------- - -static ConnectConfig make_test_cfg(const std::string & ct = "unicast") -{ - return make_connect_config("192.168.1.100", "0.0.0.0", 1510u, 1511u, ct); -} - -static ServerInfo make_server_info(bool present = true, - const std::string & app = "Motive", - int vmaj = 3, int vmin = 1, - int nnmaj = 4, int nnmin = 1) -{ - ServerInfo si; - si.host_present = present; - si.host_app_name = app; - si.host_app_version[0] = vmaj; - si.host_app_version[1] = vmin; - si.natnet_version[0] = nnmaj; - si.natnet_version[1] = nnmin; - return si; -} - -// ----------- negotiate() success paths ------------------------------------ - -TEST(Negotiate, SuccessWithHostPresent) -{ - FakeNatNetClient fake; - fake.connect_result = NatNetResult::OK; - fake.server_info = make_server_info(true, "Motive", 3, 1, 4, 1); - - const auto result = negotiate(fake, make_test_cfg()); - - EXPECT_TRUE(result.ok); - EXPECT_TRUE(result.server_info.host_present); - EXPECT_EQ(result.server_info.host_app_name, "Motive"); - EXPECT_EQ(result.server_info.host_app_version[0], 3); - EXPECT_EQ(result.server_info.natnet_version[0], 4); - - EXPECT_TRUE(fake.connect_was_called); - EXPECT_TRUE(fake.server_info_was_called); - // log message should mention the server IP - EXPECT_NE(result.log_message.find("192.168.1.100"), std::string::npos); -} - -TEST(Negotiate, SuccessButHostNotPresent) -{ - FakeNatNetClient fake; - fake.connect_result = NatNetResult::OK; - fake.server_info = make_server_info(false); - - const auto result = negotiate(fake, make_test_cfg()); - - EXPECT_TRUE(result.ok); // connection itself succeeded - EXPECT_FALSE(result.server_info.host_present); - // log message should flag the missing host info - EXPECT_NE(result.log_message.find("no host info"), std::string::npos); -} - -TEST(Negotiate, SuccessServerInfoCallFails) -{ - // SDK's GetServerDescription returns an error (simulated via call_succeeds=false) - FakeNatNetClient fake; - fake.connect_result = NatNetResult::OK; - fake.server_info_call_succeeds = false; - - const auto result = negotiate(fake, make_test_cfg()); - - EXPECT_TRUE(result.ok); - EXPECT_FALSE(result.server_info.host_present); - EXPECT_NE(result.log_message.find("no host info"), std::string::npos); -} - -// ----------- negotiate() failure paths ------------------------------------ - -TEST(Negotiate, NetworkErrorReturnsFalse) -{ - FakeNatNetClient fake; - fake.connect_result = NatNetResult::NetworkError; - - const auto result = negotiate(fake, make_test_cfg()); - - EXPECT_FALSE(result.ok); - EXPECT_FALSE(fake.server_info_was_called); // should not reach GetServerDescription - EXPECT_NE(result.log_message.find("NetworkError"), std::string::npos); - EXPECT_NE(result.log_message.find("192.168.1.100"), std::string::npos); -} - -TEST(Negotiate, InvalidAddressReturnsFalse) -{ - FakeNatNetClient fake; - fake.connect_result = NatNetResult::InvalidAddress; - - const auto result = negotiate(fake, make_test_cfg()); - - EXPECT_FALSE(result.ok); - EXPECT_NE(result.log_message.find("InvalidAddress"), std::string::npos); -} - -TEST(Negotiate, TimeoutReturnsFalse) -{ - FakeNatNetClient fake; - fake.connect_result = NatNetResult::Timeout; - - const auto result = negotiate(fake, make_test_cfg()); - - EXPECT_FALSE(result.ok); - EXPECT_NE(result.log_message.find("Timeout"), std::string::npos); -} - -// ----------- negotiate() passes ConnectConfig correctly ------------------- - -TEST(Negotiate, UnicastConfigPassedToClient) -{ - FakeNatNetClient fake; - const auto cfg = make_test_cfg("unicast"); - negotiate(fake, cfg); - - EXPECT_EQ(fake.last_connect_config.connection_type, "unicast"); - EXPECT_EQ(fake.last_connect_config.server_ip, "192.168.1.100"); - EXPECT_EQ(fake.last_connect_config.command_port, 1510u); -} - -TEST(Negotiate, MulticastConfigPassedToClient) -{ - FakeNatNetClient fake; - const auto cfg = make_connect_config( - "10.0.0.1", "0.0.0.0", 1510u, 1511u, "multicast", "239.0.0.1"); - negotiate(fake, cfg); - - EXPECT_EQ(fake.last_connect_config.connection_type, "multicast"); - EXPECT_EQ(fake.last_connect_config.multicast_address, "239.0.0.1"); -} - -// ----------- log message content ------------------------------------------ - -TEST(Negotiate, SuccessLogContainsAppAndVersion) -{ - FakeNatNetClient fake; - fake.connect_result = NatNetResult::OK; - fake.server_info = make_server_info(true, "MotiveBody", 2, 5, 4, 0); - - const auto result = negotiate(fake, make_test_cfg()); - - EXPECT_NE(result.log_message.find("MotiveBody"), std::string::npos); - EXPECT_NE(result.log_message.find("2.5"), std::string::npos); // v2.5 - EXPECT_NE(result.log_message.find("4.0"), std::string::npos); // NatNet 4.0 -} - -TEST(Negotiate, FailureLogContainsPortAndType) -{ - FakeNatNetClient fake; - fake.connect_result = NatNetResult::Timeout; - const auto cfg = make_connect_config( - "10.1.2.3", "0.0.0.0", 9000u, 9001u, "multicast"); - - const auto result = negotiate(fake, cfg); - - EXPECT_NE(result.log_message.find("9000"), std::string::npos); - EXPECT_NE(result.log_message.find("multicast"), std::string::npos); -} - - -// =========================================================================== -// FakeNatNetClient — frame injection -// =========================================================================== - -TEST(FakeNatNetClient, CallbackNotCalledBeforeRegistration) -{ - FakeNatNetClient fake; - // No set_frame_callback called — inject_frame should be a no-op - fake.inject_body(1, 1.f, 2.f, 3.f); - EXPECT_EQ(fake.frames_injected, 0); -} - -TEST(FakeNatNetClient, CallbackInvokedAfterRegistration) -{ - FakeNatNetClient fake; - - std::vector received; - fake.set_frame_callback([&](const FrameSample & f) { received.push_back(f); }); - - fake.inject_body(1, 1.f, 2.f, 3.f); - fake.inject_body(2, 4.f, 5.f, 6.f); - - EXPECT_EQ(fake.frames_injected, 2); - ASSERT_EQ(received.size(), 2u); -} - -TEST(FakeNatNetClient, InjectedBodyDataIsPreserved) -{ - FakeNatNetClient fake; - - FrameSample captured; - fake.set_frame_callback([&](const FrameSample & f) { captured = f; }); - - fake.inject_body(42, 1.5f, -2.5f, 0.75f, - 0.f, 0.f, 0.7071068f, 0.7071068f, - 0x01 /* tracking valid */); - - ASSERT_EQ(captured.bodies.size(), 1u); - const auto & rb = captured.bodies[0]; - EXPECT_EQ(rb.id, 42); - EXPECT_FLOAT_EQ(rb.x, 1.5f); - EXPECT_FLOAT_EQ(rb.y, -2.5f); - EXPECT_FLOAT_EQ(rb.z, 0.75f); - EXPECT_NEAR(rb.qz, 0.7071068f, 1e-6f); - EXPECT_TRUE(is_tracking_valid(rb.params)); -} - -TEST(FakeNatNetClient, ModelListChangedFlagDeliveredInFrame) -{ - FakeNatNetClient fake; - - bool model_changed = false; - fake.set_frame_callback([&](const FrameSample & f) { - model_changed = model_list_changed(f.params); - }); - - FrameSample f; - f.params = 0x02; // bit 1 = model list changed - fake.inject_frame(f); - - EXPECT_TRUE(model_changed); -} - -TEST(FakeNatNetClient, ResetRecordsClearsState) -{ - FakeNatNetClient fake; - fake.set_frame_callback([](const FrameSample &) {}); - fake.inject_body(1, 0.f, 0.f, 0.f); - - fake.reset_records(); - - EXPECT_FALSE(fake.connect_was_called); - EXPECT_FALSE(fake.set_callback_was_called); - EXPECT_EQ(fake.frames_injected, 0); -} - - -// =========================================================================== -// Body descriptors + filtering -// =========================================================================== - -TEST(BodyDescriptor, SkeletonBoneHasPositiveParentId) -{ - BodyDescriptor bone; - bone.id = 10; - bone.name = "Hip"; - bone.parent_id = 5; // part of skeleton with id=5 - - EXPECT_GE(bone.parent_id, 0); // should be skipped in publisher creation -} - -TEST(BodyDescriptor, TopLevelBodyHasNegativeParentId) -{ - BodyDescriptor body; - body.id = 1; - body.name = "Drone"; - body.parent_id = -1; - - EXPECT_LT(body.parent_id, 0); // should be published -} - -TEST(FakeNatNetClient, GetBodyDescriptorsReturnsConfigured) -{ - FakeNatNetClient fake; - fake.body_descriptors = { - {1, "Drone1", -1}, - {2, "Drone2", -1}, - {3, "Hip", 2}, // skeleton bone - }; - - const auto descs = fake.get_body_descriptors(); - - ASSERT_EQ(descs.size(), 3u); - EXPECT_TRUE(fake.descriptors_was_called); - - // Only top-level bodies should be published (parent_id < 0) - int top_level = 0; - for (const auto & d : descs) { - if (d.parent_id < 0) { ++top_level; } - } - EXPECT_EQ(top_level, 2); -} diff --git a/robot/ros_ws/src/perception/natnet_ros2/test/test_natnet_ros2.py b/robot/ros_ws/src/perception/natnet_ros2/test/test_natnet_ros2.py deleted file mode 100644 index 37526cd18..000000000 --- a/robot/ros_ws/src/perception/natnet_ros2/test/test_natnet_ros2.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Unit tests for natnet_ros2 Python helpers (no ROS install required). - -Stubs rclpy/launch at import time. Covers ``VisionPoseConverterNode`` quaternion -canonicalisation, configurable-topic wiring, and ``natnet_ros2.launch.py`` -profile-flattening helpers (server + per-body arrays, env expansion, namespacing). - -C++ logic (``natnet_logic.hpp``) is tested in ``test_natnet_logic.cpp`` via colcon. -""" - -import importlib.util -import sys -from pathlib import Path -from types import SimpleNamespace -from unittest.mock import MagicMock - -# --------------------------------------------------------------------------- -# Stub ROS before importing the source. -# -# The key subtlety: VisionPoseConverterNode inherits from rclpy.node.Node. -# If Node is a plain MagicMock() the class body is never executed (Python's -# metaclass machinery returns a Mock for attribute access instead of running -# __init_subclass__ / defining methods). We supply a real dummy base class -# so the actual class body — including _canonical_quaternion — is defined. -# -# The fake also records declared params and created sub/pub topics so the -# configurable-topic wiring can be asserted without a ROS install. -# --------------------------------------------------------------------------- - -class _FakeNode: - # Per-test parameter overrides keyed by name; consulted by declare_parameter so - # values survive the node's super().__init__ (which resets per-instance state). - _overrides: dict = {} - - def __init__(self, name: str): - self._params: dict = {} - self.created_subscriptions: list = [] - self.created_publishers: list = [] - def get_logger(self): - return MagicMock() - def declare_parameter(self, name, default=None): - self._params[name] = self._overrides.get(name, default) - def get_parameter(self, name): - return SimpleNamespace(value=self._params.get(name)) - def create_subscription(self, msg_type, topic, callback, qos): - self.created_subscriptions.append(topic) - return MagicMock() - def create_publisher(self, msg_type, topic, qos): - self.created_publishers.append(topic) - return MagicMock() - - -_rclpy_node_mod = MagicMock() -_rclpy_node_mod.Node = _FakeNode -sys.modules.setdefault("rclpy", MagicMock()) -sys.modules["rclpy.node"] = _rclpy_node_mod -sys.modules.setdefault("geometry_msgs", MagicMock()) -sys.modules.setdefault("geometry_msgs.msg", MagicMock()) - -# Add the package's src/ directory (co-located: test/ → package root → src/). -_natnet_src = Path(__file__).resolve().parent.parent / "src" -if str(_natnet_src) not in sys.path: - sys.path.insert(0, str(_natnet_src)) - -from vision_pose_converter_node import VisionPoseConverterNode # noqa: E402 - - -# --------------------------------------------------------------------------- -# Load natnet_ros2.launch.py with its heavy launch/ROS deps stubbed, so the -# pure flattening helpers can be unit-tested without a ROS install. -# --------------------------------------------------------------------------- - -for _mod in ( - "ament_index_python", - "ament_index_python.packages", - "launch", - "launch.actions", - "launch.launch_description_sources", - "launch.substitutions", - "launch_ros", - "launch_ros.actions", -): - sys.modules.setdefault(_mod, MagicMock()) - -# yaml is only needed by _load_natnet_config (not the flattening helpers); stub it -# if PyYAML is absent so the launch module still imports in a minimal unit env. -try: - import yaml # noqa: F401 -except ImportError: - sys.modules.setdefault("yaml", MagicMock()) - -_launch_path = Path(__file__).resolve().parent.parent / "launch" / "natnet_ros2.launch.py" -_spec = importlib.util.spec_from_file_location("natnet_ros2_launch_under_test", _launch_path) -natnet_launch = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(natnet_launch) - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -def _quat(x: float, y: float, z: float, w: float) -> SimpleNamespace: - """Minimal quaternion-like object matching the expected interface.""" - return SimpleNamespace(x=x, y=y, z=z, w=w) - - -# --------------------------------------------------------------------------- -# VisionPoseConverterNode._canonical_quaternion -# --------------------------------------------------------------------------- - -import pytest - - -@pytest.mark.unit -def test_canonical_quaternion_positive_w_unchanged(): - """Quaternion with w > 0 must not be altered.""" - q = _quat(0.1, 0.2, 0.3, 0.9) - out = VisionPoseConverterNode._canonical_quaternion(q) - assert out.w == pytest.approx(0.9) - assert out.x == pytest.approx(0.1) - assert out.y == pytest.approx(0.2) - assert out.z == pytest.approx(0.3) - - -@pytest.mark.unit -def test_canonical_quaternion_negative_w_flipped(): - """Quaternion with w < 0 must have all four components negated.""" - q = _quat(0.1, 0.2, 0.3, -0.9) - out = VisionPoseConverterNode._canonical_quaternion(q) - assert out.w == pytest.approx(0.9) - assert out.x == pytest.approx(-0.1) - assert out.y == pytest.approx(-0.2) - assert out.z == pytest.approx(-0.3) - - -@pytest.mark.unit -def test_canonical_quaternion_zero_w_unchanged(): - """w == 0 satisfies w >= 0 so no flip should occur.""" - q = _quat(1.0, 0.0, 0.0, 0.0) - out = VisionPoseConverterNode._canonical_quaternion(q) - assert out.w == pytest.approx(0.0) - assert out.x == pytest.approx(1.0) - - -@pytest.mark.unit -def test_canonical_quaternion_identity(): - q = _quat(0.0, 0.0, 0.0, 1.0) - out = VisionPoseConverterNode._canonical_quaternion(q) - assert out.w == pytest.approx(1.0) - assert out.x == pytest.approx(0.0) - - -@pytest.mark.unit -def test_canonical_quaternion_returns_same_object(): - """The method mutates and returns the same object (not a copy).""" - q = _quat(0.0, 0.0, 0.0, 1.0) - out = VisionPoseConverterNode._canonical_quaternion(q) - assert out is q - - -@pytest.mark.unit -def test_canonical_quaternion_w_stays_non_negative(): - """After canonicalisation w must always be >= 0.""" - cases = [ - _quat(0.0, 0.0, 0.7071, 0.7071), - _quat(0.0, 0.0, -0.7071, -0.7071), - _quat(0.5, -0.5, 0.5, -0.5), - _quat(0.0, 0.0, 1.0, 0.0), - ] - for q in cases: - out = VisionPoseConverterNode._canonical_quaternion(q) - assert out.w >= 0.0, f"w={out.w} after canonicalisation of {q}" - - -@pytest.mark.unit -def test_canonical_quaternion_dual_sign_produces_same_result(): - """q and -q must both canonicalise to the same output.""" - q_pos = _quat(0.1, 0.2, 0.3, 0.9) - q_neg = _quat(-0.1, -0.2, -0.3, -0.9) - out_pos = VisionPoseConverterNode._canonical_quaternion(q_pos) - out_neg = VisionPoseConverterNode._canonical_quaternion(q_neg) - assert out_pos.w == pytest.approx(out_neg.w) - assert out_pos.x == pytest.approx(out_neg.x) - assert out_pos.y == pytest.approx(out_neg.y) - assert out_pos.z == pytest.approx(out_neg.z) - - -# --------------------------------------------------------------------------- -# VisionPoseConverterNode — configurable input/output topics -# --------------------------------------------------------------------------- - -@pytest.mark.unit -def test_vision_pose_converter_default_topics(): - """Defaults reproduce the historical relative (remappable) topic names.""" - node = VisionPoseConverterNode() - assert node.created_subscriptions == ["input_pose"] - assert node.created_publishers == ["output_pose", "output_pose_cov"] - - -@pytest.mark.unit -def test_vision_pose_converter_topic_overrides_applied(): - """When the topic params are set, sub/pub use those exact names.""" - _FakeNode._overrides = { - "input_topic": "/robot_2/perception/optitrack/drone/pose_cov", - "output_pose_topic": "/robot_2/custom/vision/pose", - "output_pose_cov_topic": "/robot_2/custom/vision/pose_cov", - } - try: - node = VisionPoseConverterNode() - finally: - _FakeNode._overrides = {} - assert node.created_subscriptions == ["/robot_2/perception/optitrack/drone/pose_cov"] - assert node.created_publishers == [ - "/robot_2/custom/vision/pose", - "/robot_2/custom/vision/pose_cov", - ] - - -# --------------------------------------------------------------------------- -# natnet_ros2.launch.py — pure config-flattening helpers -# --------------------------------------------------------------------------- - -@pytest.mark.unit -def test_expand_env_uses_default_when_unset(monkeypatch): - monkeypatch.delenv("NATNET_SERVER_IP", raising=False) - assert natnet_launch._expand_env("$(env NATNET_SERVER_IP 172.31.0.200)") == "172.31.0.200" - - -@pytest.mark.unit -def test_expand_env_uses_environment_value(monkeypatch): - monkeypatch.setenv("NATNET_SERVER_IP", "10.0.0.5") - assert natnet_launch._expand_env("$(env NATNET_SERVER_IP 172.31.0.200)") == "10.0.0.5" - - -@pytest.mark.unit -def test_namespaced_strips_and_prefixes(): - assert natnet_launch._namespaced("robot_1", "perception/optitrack/drone") == \ - "/robot_1/perception/optitrack/drone" - assert natnet_launch._namespaced("robot_2", "/already/abs") == "/robot_2/already/abs" - - -@pytest.mark.unit -def test_build_node_params_flattens_bodies(): - server = {"server_ip": "1.2.3.4", "command_port": 1510, "connection_type": "unicast"} - profile = { - "bodies": [ - { - "rigid_body_name": "Drone", - "id": 1, - "topic": "perception/optitrack/drone", - "pose": True, - "pose_cov": True, - "position_covariance": [9.0] * 9, - "orientation_covariance": [8.0] * 9, - }, - { - "rigid_body_name": "Target", - "id": 100, - "topic": "perception/optitrack/target", - "pose": True, - "pose_cov": False, - }, - ] - } - params = natnet_launch._build_node_params(server, profile) - - assert params["server_ip"] == "1.2.3.4" - assert params["body_names"] == ["Drone", "Target"] - assert params["body_ids"] == [1, 100] - assert params["body_topics"] == ["perception/optitrack/drone", "perception/optitrack/target"] - assert params["body_pose"] == [True, True] - assert params["body_pose_cov"] == [True, False] - # 9 floats per body, flattened in body order. - assert len(params["body_position_covariance"]) == 18 - assert params["body_position_covariance"][:9] == [9.0] * 9 - # Target omitted its covariance → built-in default fills its slice. - assert params["body_position_covariance"][9:] == natnet_launch._DEFAULT_POSITION_COVARIANCE - - -@pytest.mark.unit -def test_build_node_params_empty_profile(): - """A robot with no profile yields empty body arrays (node tracks nothing).""" - params = natnet_launch._build_node_params({}, {}) - assert params["body_names"] == [] - assert params["body_ids"] == [] - assert params["body_position_covariance"] == [] diff --git a/robot/ros_ws/src/perception/perception_bringup/LICENSE b/robot/ros_ws/src/perception/perception_bringup/LICENSE index d64569567..f2eb4e521 100644 --- a/robot/ros_ws/src/perception/perception_bringup/LICENSE +++ b/robot/ros_ws/src/perception/perception_bringup/LICENSE @@ -1,202 +1,32 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +The Clear BSD License + +Copyright (c) 2023-2026 Carnegie Mellon University, AirLab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted (subject to the limitations in the disclaimer +below) provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. 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 deleted file mode 100644 index 1e9f8662e..000000000 --- a/robot/ros_ws/src/perception/perception_bringup/launch/perception.launch.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file 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..49f686782 --- /dev/null +++ b/robot/ros_ws/src/perception/perception_bringup/launch/stereo_image_proc.launch.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..7e1e0ac0b 100644 --- a/robot/ros_ws/src/perception/perception_bringup/package.xml +++ b/robot/ros_ws/src/perception/perception_bringup/package.xml @@ -3,13 +3,14 @@ perception_bringup 0.0.0 - TODO: Package description - andrew - Apache-2.0 + Bringup package for the AirStack perception layer: stereo image processing and topic keepalive utilities. + Andrew Jong + BSD-3-Clause-Clear ament_cmake rclpy + stereo_image_proc sensor_msgs visualization_msgs nav_msgs 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/robot/ros_ws/src/sensors/camera_param_server/README.md b/robot/ros_ws/src/sensors/camera_param_server/README.md deleted file mode 100644 index 1f17a4c74..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/README.md +++ /dev/null @@ -1,51 +0,0 @@ - -# Camera Parameter Server - -## Summary - -The camera parameter server was designed to eliminate the need for multiple nodes to subscribe to each camera individually, reducing unnecessary subscribers and callbacks. Its sole purpose is to listen to the camera info topics, store relevant information about the cameras, and provide it on demand for other nodes to query. - -## Configuration - -The camera parameter server is currently configurable through a non-ROS configuration file. This file allows users to define a list of cameras, specifying their types and topic names. At the top level of the configuration, a base link name is provided to indicate the `tf` name for the robot's center. Additionally, a parameter called `camera_list` contains a list of dictionaries, with each dictionary representing an individual camera. Currently, two camera types are supported: monocular and stereo. - -## Parameters - -Below are the parameters needed for the meta level camera parameter server configuration, as well as the camera fields needed to specify individual camera types. - -### Meta Level Parameters - -|
Parameter
| Description -|----------------------------|--------------------------------------------------------------- -| `base_link_frame_id` | The frame name of the base link, or center frame of the robot| -| `camera_list` | A list of dictionaries that define each camera of the system| - -### Monocular Camera Parameters - -|
Parameter
| Description -|----------------------------|--------------------------------------------------------------- -| `camera_name` | The name of the camera| -| `camera_type` | The type of camera, for monocular being `mono` | -| `camera_info_sub_topic` | The info topic name for the camera, normally `camera_info`| -| `camera_frame_id` | The frame name of the camera to find its tf | - -### Stereo Camera Parameters - -|
Parameter
| Description -|----------------------------|--------------------------------------------------------------- -| `camera_name` | The name of the camera| -| `camera_type` | The type of camera, for stereo being `stereo` | -| `camera_info_sub_topic` | The info topic name for the camera, normally `camera_info`| -| `left_camera_frame_id` | The frame name of the left camera for find its tf | -| `right_camera_frame_id` | The frame name of the right camera to find its tf | - -## Services -|
Parameter
| Type | Description -|----------------------------|----------------------------------------|-----------------------| -| `~/get_camera_params` | sensor_interfaces/GetCameraParams | The service to get info about the desired camera. This provides camera intrinsics, transform frame ids, and baseline if the camera type is a stereo| - -## Subscriptions -|
Parameter
| Type | Description -|----------------------------|----------------------------------------|-----------------------| -| `tf/` | tfMessage | Listens for the tf for the specified cameras | -| `~/camera_info` | sensor_msgs/CameraInfo | Listens for the info for specified cameras| \ No newline at end of file diff --git a/robot/ros_ws/src/sensors/camera_param_server/camera_param_server/__init__.py b/robot/ros_ws/src/sensors/camera_param_server/camera_param_server/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/sensors/camera_param_server/camera_param_server/camera_param_server.py b/robot/ros_ws/src/sensors/camera_param_server/camera_param_server/camera_param_server.py deleted file mode 100644 index 90f701e4a..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/camera_param_server/camera_param_server.py +++ /dev/null @@ -1,207 +0,0 @@ -import rclpy -from rclpy.node import Node -import tf2_ros - -from sensor_msgs.msg import CameraInfo -from sensor_interfaces.srv import GetCameraParams - -import yaml -import os -import numpy as np - -class StereoCameraInfo: - def __init__(self, node, camera_name, left_camera_info_topic, right_camera_info_topic, left_camera_frame_id, right_camera_frame_id): - self.left_camera_info = CameraInfo() - self.right_camera_info = CameraInfo() - self.camera_type = 'stereo' - - self.tf_initialized = False - self.left_info_initialized = False - self.right_info_initialized = False - self.info_initialized = False - - self.left_camera_info_sub = node.create_subscription(CameraInfo, left_camera_info_topic, self.left_camera_info_callback, 10) - self.right_camera_info_sub = node.create_subscription(CameraInfo, right_camera_info_topic, self.right_camera_info_callback, 10) - - self.camera_name = camera_name - self.left_camera_frame_id = left_camera_frame_id - self.right_camera_frame_id = right_camera_frame_id - self.base_link_frame_id = node.base_link_frame_id - self.left_camera_transform_to_baselink = None - self.right_camera_transform_to_baselink = None - self.baseline = None - - def left_camera_info_callback(self, msg): - self.left_camera_info = msg - self.left_info_initialized = True - if self.left_info_initialized and self.right_info_initialized: - self.info_initialized = True - - def right_camera_info_callback(self, msg): - self.right_camera_info = msg - self.right_info_initialized = True - if self.left_info_initialized and self.right_info_initialized: - self.info_initialized = True - -class MonoCameraInfo: - def __init__(self, node, camera_name, camera_info_topic, camera_frame_id): - self.camera_info = CameraInfo() - self.camera_type = 'mono' - - self.info_initialized = False - - info_topic_merged = camera_name + '/' + camera_info_topic - self.camera_info_sub = node.create_subscription(CameraInfo, info_topic_merged, self.camera_info_callback, 10) - - self.camera_name = camera_name - self.camera_frame_id = camera_frame_id - self.base_link_frame_id = node.base_link_frame_id - self.camera_transform_to_baselink = None - - def camera_info_callback(self, msg): - self.camera_info = msg - self.info_initialized = True - -class CameraParamServer(Node): - def __init__(self): - super().__init__('cam_param_server') - - self.camera_dict = {} - self.base_link_frame_id = None - - self.tfs_initialized = False - self.info_initialized = False - self.server_initialized = False - - self.declare_parameter('camera_config', rclpy.parameter.Parameter.Type.STRING) - camera_config_file = self.get_parameter('camera_config').value - if not os.path.exists(camera_config_file): - self.get_logger().error('Camera configuration file not found: %s' % camera_config_file) - return - - self.parse_camera_config(camera_config_file) - - self.tf_buffer = tf2_ros.Buffer() - self.tf_listener = tf2_ros.TransformListener(self.tf_buffer, self) - self.tf_checking_timer = self.create_timer(0.1, self.check_tf) - - def parse_camera_config(self, config_file_path): - try: - config_file = self.load_config(config_file_path) - camera_list = config_file.get('camera_list') - self.base_link_frame_id = config_file.get('base_link_frame_id') - except Exception as e: - self.get_logger().error(f"Error parsing camera config file: {e}") - - for camera in camera_list: - camera_name = camera.get('camera_name') - if camera.get('camera_type') == 'stereo': - self.camera_dict[camera_name] = StereoCameraInfo(self, camera_name, - camera.get('left_camera_info_sub_topic'), - camera.get('right_camera_info_sub_topic'), - camera.get('left_camera_frame_id'), - camera.get('right_camera_frame_id')) - elif camera.get('camera_type') == 'mono': - self.camera_dict[camera_name] = MonoCameraInfo(self, camera_name, camera.get('camera_info_sub_topic'), camera.get('camera_frame_id')) - else: - self.get_logger().error('Invalid camera type: %s' % camera.get('type')) - - def load_config(self, file): - try: - with open(file, 'r') as file: - config = yaml.safe_load(file) - return config - except FileNotFoundError: - self.get_logger().error(f"Error: The file '{file}' does not exist.") - raise - except yaml.YAMLError as e: - self.get_logger().error(f"Error parsing YAML file '{file}': {e}") - raise - - def check_tf(self): - if not self.tfs_initialized: - for camera in self.camera_dict.values(): - if isinstance(camera, StereoCameraInfo): - try: - camera.left_camera_transform_to_baselink = self.tf_buffer.lookup_transform(camera.left_camera_frame_id, camera.base_link_frame_id, rclpy.time.Time()) - camera.right_camera_transform_to_baselink = self.tf_buffer.lookup_transform(camera.right_camera_frame_id, camera.base_link_frame_id, rclpy.time.Time()) - left_cam_location = np.array([camera.left_camera_transform_to_baselink.transform.translation.x, camera.left_camera_transform_to_baselink.transform.translation.y, camera.left_camera_transform_to_baselink.transform.translation.z]) - right_cam_location = np.array([camera.right_camera_transform_to_baselink.transform.translation.x, camera.right_camera_transform_to_baselink.transform.translation.y, camera.right_camera_transform_to_baselink.transform.translation.z]) - camera.baseline = np.linalg.norm(left_cam_location - right_cam_location) - camera.tf_initialized = True - except Exception as e: - self.get_logger().error(f"Error looking up transform: {e}") - elif isinstance(camera, MonoCameraInfo): - try: - camera.camera_transform_to_baselink = self.tf_buffer.lookup_transform(camera.camera_frame_id, camera.base_link_frame_id, rclpy.time.Time()) - except Exception as e: - self.get_logger().error(f"Error looking up transform: {e}") - - if all(camera.tf_initialized for camera in self.camera_dict.values()): - self.tfs_initialized = True - self.get_logger().info('Camera transforms initialized') - - if not self.info_initialized: - if all(camera.info_initialized for camera in self.camera_dict.values()): - self.info_initialized = True - self.get_logger().info('Camera parameters initialized') - - if self.tfs_initialized and self.info_initialized and not self.server_initialized: - self.camera_params_srv = self.create_service(GetCameraParams, 'get_camera_params', self.get_camera_params) - self.server_initialized = True - self.get_logger().info('Camera parameter server initialized') - - - def get_camera_params(self, request, response): - # Check if camera parameters are initialized - if self.tfs_initialized and self.info_initialized: - camera_name_list = request.camera_names - camera_type_list = request.camera_types - if len(camera_name_list) != len(camera_type_list): - self.get_logger().error('Camera name and type list length mismatch') - response.success = False - return response - for i, camera_name in enumerate(camera_name_list): - camera_type = camera_type_list[i] - response = self.get_camera_params_single(response, camera_name, camera_type) - return response - else: - self.get_logger().error('Camera parameters not initialized') - response.success = False - return response - - def get_camera_params_single(self, response, incoming_camera_name, incoming_camera_type): - if incoming_camera_name in self.camera_dict: - camera = self.camera_dict[incoming_camera_name] - if camera.camera_type == incoming_camera_type: - if isinstance(camera, StereoCameraInfo): - response.camera_frame_ids.append(camera.left_camera_frame_id) - response.camera_infos.append(camera.left_camera_info) - response.camera_frame_ids.append(camera.right_camera_frame_id) - response.camera_infos.append(camera.right_camera_info) - response.baselines.append(camera.baseline) - response.success = True - elif isinstance(camera, MonoCameraInfo): - response.camera_info = camera.camera_info - response.camera_transform_to_baselink = camera.camera_transform_to_baselink - response.success = True - else: - self.get_logger().error('Camera type mismatch: %s' % incoming_camera_name) - response.success = False - return response - else: - self.get_logger().error('Camera not found: %s' % incoming_camera_name) - return response - - - -def main(args=None): - rclpy.init(args=args) - - camera_param_server = CameraParamServer() - - rclpy.spin(camera_param_server) - - camera_param_server.destroy_node() - rclpy.shutdown() - diff --git a/robot/ros_ws/src/sensors/camera_param_server/config/camera_config.yaml b/robot/ros_ws/src/sensors/camera_param_server/config/camera_config.yaml deleted file mode 100644 index ef624b88d..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/config/camera_config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -base_link_frame_id: "base_link" -camera_list: - - camera_name: "front_stereo" - camera_type: "stereo" - camera_info_sub_topic: "camera_info" - left_camera_frame_id: "front_stereo_left_camera_optical_frame" - right_camera_frame_id: "front_stereo_right_camera_optical_frame" diff --git a/robot/ros_ws/src/sensors/camera_param_server/launch/camera_param_server.launch.xml b/robot/ros_ws/src/sensors/camera_param_server/launch/camera_param_server.launch.xml deleted file mode 100644 index 1347318d9..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/launch/camera_param_server.launch.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/sensors/camera_param_server/package.xml b/robot/ros_ws/src/sensors/camera_param_server/package.xml deleted file mode 100644 index f46cc6053..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/package.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - camera_param_server - 0.0.0 - TODO: Package description - root - TODO: License declaration - - sensor_interfaces - - ament_copyright - ament_flake8 - ament_pep257 - python3-pytest - - - ament_python - - diff --git a/robot/ros_ws/src/sensors/camera_param_server/resource/camera_param_server b/robot/ros_ws/src/sensors/camera_param_server/resource/camera_param_server deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/sensors/camera_param_server/setup.cfg b/robot/ros_ws/src/sensors/camera_param_server/setup.cfg deleted file mode 100644 index 2e62fcd72..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/camera_param_server -[install] -install_scripts=$base/lib/camera_param_server diff --git a/robot/ros_ws/src/sensors/camera_param_server/setup.py b/robot/ros_ws/src/sensors/camera_param_server/setup.py deleted file mode 100644 index be9a2e7b4..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -from setuptools import find_packages, setup - -package_name = 'camera_param_server' - -setup( - name=package_name, - version='0.0.0', - packages=find_packages(exclude=['test']), - data_files=[ - ('share/ament_index/resource_index/packages', - ['resource/' + package_name]), - ('share/' + package_name, ['package.xml']), - ('share/' + package_name + '/launch', ['launch/camera_param_server.launch.xml']), - ], - install_requires=['setuptools'], - zip_safe=True, - maintainer='root', - maintainer_email='root@todo.todo', - description='TODO: Package description', - license='TODO: License declaration', - tests_require=['pytest'], - entry_points={ - 'console_scripts': [ - 'camera_param_server = camera_param_server.camera_param_server:main', - ], - }, -) diff --git a/robot/ros_ws/src/sensors/camera_param_server/test/test_copyright.py b/robot/ros_ws/src/sensors/camera_param_server/test/test_copyright.py deleted file mode 100644 index 97a39196e..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/test/test_copyright.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_copyright.main import main -import pytest - - -# Remove the `skip` decorator once the source file(s) have a copyright header -@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') -@pytest.mark.copyright -@pytest.mark.linter -def test_copyright(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found errors' diff --git a/robot/ros_ws/src/sensors/camera_param_server/test/test_flake8.py b/robot/ros_ws/src/sensors/camera_param_server/test/test_flake8.py deleted file mode 100644 index 27ee1078f..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/test/test_flake8.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2017 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_flake8.main import main_with_errors -import pytest - - -@pytest.mark.flake8 -@pytest.mark.linter -def test_flake8(): - rc, errors = main_with_errors(argv=[]) - assert rc == 0, \ - 'Found %d code style errors / warnings:\n' % len(errors) + \ - '\n'.join(errors) diff --git a/robot/ros_ws/src/sensors/camera_param_server/test/test_pep257.py b/robot/ros_ws/src/sensors/camera_param_server/test/test_pep257.py deleted file mode 100644 index b234a3840..000000000 --- a/robot/ros_ws/src/sensors/camera_param_server/test/test_pep257.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_pep257.main import main -import pytest - - -@pytest.mark.linter -@pytest.mark.pep257 -def test_pep257(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found code style errors / warnings' diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/gimbal_stabilizer/__init__.py b/robot/ros_ws/src/sensors/gimbal_stabilizer/gimbal_stabilizer/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/gimbal_stabilizer/gimbal_stabilizer_node.py b/robot/ros_ws/src/sensors/gimbal_stabilizer/gimbal_stabilizer/gimbal_stabilizer_node.py deleted file mode 100644 index c923f44e6..000000000 --- a/robot/ros_ws/src/sensors/gimbal_stabilizer/gimbal_stabilizer/gimbal_stabilizer_node.py +++ /dev/null @@ -1,94 +0,0 @@ -import threading -import math -import rclpy -from rclpy.node import Node -from nav_msgs.msg import Odometry -from sensor_msgs.msg import JointState -# from transforms3d.euler import quat2euler -from std_msgs.msg import Float64 # Assuming the desired yaw is published as a Float64 - -class GimbalStabilizerNode(Node): - def __init__(self): - super().__init__('gimbal_stabilizer') - - # Publisher to send joint commands - self.joint_pub = self.create_publisher(JointState, 'gimbal/joint_command', 10) - - # Subscriber to receive drone odometry - self.create_subscription(Odometry, 'odometry_conversion/odometry', self.odometry_callback, 10) - self.create_subscription(JointState, 'gimbal/joint_states', self.joint_callback, 10) - self.create_subscription(Float64, 'gimbal/desired_gimbal_yaw', self.yaw_callback, 10) - self.create_subscription(Float64, 'gimbal/desired_gimbal_pitch', self.pitch_callback, 10) - - # Initialize joint state message - self.joint_command = JointState() - self.joint_command.name = ["yaw_joint","roll_joint", "pitch_joint"] - self.joint_command.position = [0.0, 0.0, 0.0] - self.desired_yaw = 0.0 - self.desired_pitch = 0.0 - - def yaw_callback(self, msg): - self.desired_yaw = msg.data - # self.get_logger().info(f"Received desired yaw angle: {self.desired_yaw}") - - def pitch_callback(self, msg): - self.desired_pitch = msg.data - - def joint_callback(self, msg): - self.got_joint_states = True - # Inverse the drone angles to stabilize the gimbal - # self.joint_command.position[0] = -roll # roll joint - # self.joint_command.position[1] = -pitch # pitch joint - # self.joint_command.position[2] = -yaw # yaw joint - - # self.joint_command.effort = [100000000.0, 100000000.0, 100000000.0] - - # self.joint_command.position[0] = -20.0/180*3.14 # yaw joint - # self.joint_command.position[1] = 10.0/180*3.14 # roll joint - # self.joint_command.position[2] = 20.0/180*3.14 # pitch joint - # self.joint_command.velocity = [float('nan'), float('nan'), float('nan')] - # self.joint_command.velocity = [-1.0, -1.0, -1.0] - - # Publish the joint command - # self.joint_pub.publish(self.joint_command) - - def odometry_callback(self, msg): - # Extract quaternion from odometry message - orientation_q = msg.pose.pose.orientation - quaternion = [ - orientation_q.w, - orientation_q.x, - orientation_q.y, - orientation_q.z - ] - - # Convert quaternion to Euler angles (roll, pitch, yaw) - # roll, pitch, yaw = quat2euler(quaternion, axes='sxyz') - - # Inverse the drone angles to stabilize the gimbal - # self.joint_command.position[0] = -roll # roll joint - # self.joint_command.position[1] = -pitch # pitch joint - # self.joint_command.position[2] = -yaw # yaw joint - - self.joint_command.position[0] = -self.desired_yaw/180*3.14 # yaw joint - self.joint_command.position[1] = -0.0/180*3.14 # roll joint - self.joint_command.position[2] = self.desired_pitch/180*3.14 # pitch joint - self.joint_command.velocity = [float('nan'), float('nan'), float('nan')] - # self.joint_command.velocity = [-1.0, -1.0, -1.0] - - # Publish the joint command - self.joint_pub.publish(self.joint_command) - -def main(): - rclpy.init() - node = GimbalStabilizerNode() - - try: - rclpy.spin(node) # Run the node in a single thread - except KeyboardInterrupt: - pass - finally: - rclpy.shutdown() - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/package.xml b/robot/ros_ws/src/sensors/gimbal_stabilizer/package.xml deleted file mode 100644 index 59d2ddd6c..000000000 --- a/robot/ros_ws/src/sensors/gimbal_stabilizer/package.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - gimbal_stabilizer - 0.0.0 - TODO: Package description - airstation-04 - TODO: License declaration - - rclpy - nav_msgs - sensor_msgs - tf_transformations - - ament_copyright - ament_flake8 - ament_pep257 - python3-pytest - - - ament_python - - diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/resource/gimbal_stabilizer b/robot/ros_ws/src/sensors/gimbal_stabilizer/resource/gimbal_stabilizer deleted file mode 100644 index e69de29bb..000000000 diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/setup.cfg b/robot/ros_ws/src/sensors/gimbal_stabilizer/setup.cfg deleted file mode 100644 index 1c6018e30..000000000 --- a/robot/ros_ws/src/sensors/gimbal_stabilizer/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/gimbal_stabilizer -[install] -install_scripts=$base/lib/gimbal_stabilizer diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/setup.py b/robot/ros_ws/src/sensors/gimbal_stabilizer/setup.py deleted file mode 100644 index d339493c9..000000000 --- a/robot/ros_ws/src/sensors/gimbal_stabilizer/setup.py +++ /dev/null @@ -1,25 +0,0 @@ -from setuptools import find_packages, setup - -package_name = 'gimbal_stabilizer' - -setup( - name=package_name, - version='0.1.0', - packages=[package_name], - data_files=[ - ('share/ament_index/resource_index/packages', ['resource/' + package_name]), - ('share/' + package_name, ['package.xml']), - ], - install_requires=['setuptools'], - zip_safe=True, - maintainer='Your Name', - maintainer_email='you@example.com', - description='Package for gimbal stabilization using roll, pitch, and yaw from odometry data', - license='Apache License 2.0', - tests_require=['pytest'], - entry_points={ - 'console_scripts': [ - 'gimbal_stabilizer_node = gimbal_stabilizer.gimbal_stabilizer_node:main' - ], - }, -) \ No newline at end of file diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_copyright.py b/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_copyright.py deleted file mode 100644 index 97a39196e..000000000 --- a/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_copyright.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_copyright.main import main -import pytest - - -# Remove the `skip` decorator once the source file(s) have a copyright header -@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') -@pytest.mark.copyright -@pytest.mark.linter -def test_copyright(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found errors' diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_flake8.py b/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_flake8.py deleted file mode 100644 index 27ee1078f..000000000 --- a/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_flake8.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2017 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_flake8.main import main_with_errors -import pytest - - -@pytest.mark.flake8 -@pytest.mark.linter -def test_flake8(): - rc, errors = main_with_errors(argv=[]) - assert rc == 0, \ - 'Found %d code style errors / warnings:\n' % len(errors) + \ - '\n'.join(errors) diff --git a/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_pep257.py b/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_pep257.py deleted file mode 100644 index b234a3840..000000000 --- a/robot/ros_ws/src/sensors/gimbal_stabilizer/test/test_pep257.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_pep257.main import main -import pytest - - -@pytest.mark.linter -@pytest.mark.pep257 -def test_pep257(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found code style errors / warnings' diff --git a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/README.md b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/README.md index 44449f50f..1521b55fa 100644 --- a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/README.md +++ b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/README.md @@ -33,7 +33,7 @@ Defaults are in `config/lidar_point_cloud_filter.yaml`. `$(env ROBOT_NAME)` is e ros2 launch lidar_point_cloud_filter lidar_point_cloud_filter.launch.xml ``` -Included from `sensors_bringup` under the robot and `sensors` namespaces. Defaults use **`sensors/ouster/point_cloud_raw` → `sensors/ouster/point_cloud`** to match Pegasus / Isaac and `vdb_params`. For RTX-only topic names, override `input_topic` and `output_topic` (for example under `sensors/lidar/...`). +Included from the stack entry files (stacks/*/launch) under the robot and `sensors` namespaces. Defaults use **`sensors/ouster/point_cloud_raw` → `sensors/ouster/point_cloud`** to match Pegasus / Isaac and `vdb_params`. For RTX-only topic names, override `input_topic` and `output_topic` (for example under `sensors/lidar/...`). ## System tests (`sensors` mark) 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/robot/ros_ws/src/sensors/lidar_point_cloud_filter/lidar_point_cloud_filter/validation_core.py b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/lidar_point_cloud_filter/validation_core.py index 5d3f22cfd..4dd449f3a 100644 --- a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/lidar_point_cloud_filter/validation_core.py +++ b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/lidar_point_cloud_filter/validation_core.py @@ -1,5 +1,5 @@ # Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. +# SPDX-License-Identifier: BSD-3-Clause-Clear """Pure-numeric LiDAR filter validation helpers (no ROS imports). Shared by: diff --git a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/package.xml b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/package.xml index 5f3786cd5..916980a8c 100644 --- a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/package.xml +++ b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/package.xml @@ -4,8 +4,8 @@ lidar_point_cloud_filter 0.1.0 Near-range lidar noise filter: subscribes raw PointCloud2, drops points inside a sensor-frame sphere, publishes xyz float32 cloud. - AirLab CMU - Apache-2.0 + Andrew Jong + BSD-3-Clause-Clear rclpy sensor_msgs diff --git a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/scripts/validate_lidar_filter_clouds.py b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/scripts/validate_lidar_filter_clouds.py index f53460a56..f89d6fc0a 100644 --- a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/scripts/validate_lidar_filter_clouds.py +++ b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/scripts/validate_lidar_filter_clouds.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. +# SPDX-License-Identifier: BSD-3-Clause-Clear """One-shot ROS 2 check for liveliness: filtered LiDAR cloud vs raw (Isaac / Pegasus). Run inside the robot container with workspace sourced and ROS_DOMAIN_ID set:: diff --git a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/test/test_validation_core.py b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/test/test_validation_core.py index 04526c478..b5dd81bc9 100644 --- a/robot/ros_ws/src/sensors/lidar_point_cloud_filter/test/test_validation_core.py +++ b/robot/ros_ws/src/sensors/lidar_point_cloud_filter/test/test_validation_core.py @@ -1,5 +1,5 @@ # Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. +# SPDX-License-Identifier: BSD-3-Clause-Clear """Unit tests for ``validation_core`` (numpy-only).""" import sys diff --git a/robot/ros_ws/src/sensors/sensor_interfaces/CMakeLists.txt b/robot/ros_ws/src/sensors/sensor_interfaces/CMakeLists.txt deleted file mode 100644 index ea4cbdf62..000000000 --- a/robot/ros_ws/src/sensors/sensor_interfaces/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -cmake_minimum_required(VERSION 3.5) -project(sensor_interfaces) -# Default to C++14 -if(NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 14) -endif() -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() -find_package(ament_cmake REQUIRED) -find_package(rosidl_default_generators REQUIRED) -find_package(sensor_msgs REQUIRED) - -rosidl_generate_interfaces(${PROJECT_NAME} - "srv/GetCameraParams.srv" - DEPENDENCIES sensor_msgs - ) - -ament_export_dependencies(rosidl_default_runtime) -ament_package() \ No newline at end of file diff --git a/robot/ros_ws/src/sensors/sensor_interfaces/package.xml b/robot/ros_ws/src/sensors/sensor_interfaces/package.xml deleted file mode 100644 index 0fd76afe0..000000000 --- a/robot/ros_ws/src/sensors/sensor_interfaces/package.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - sensor_interfaces - 0.0.0 - TODO: Package description - user - TODO: License declaration - - ament_cmake - rosidl_default_generators - - rosidl_default_runtime - - sensor_msgs - - ament_lint_auto - ament_lint_common - - rosidl_interface_packages - - - ament_cmake - - diff --git a/robot/ros_ws/src/sensors/sensor_interfaces/srv/GetCameraParams.srv b/robot/ros_ws/src/sensors/sensor_interfaces/srv/GetCameraParams.srv deleted file mode 100644 index 2972d8d83..000000000 --- a/robot/ros_ws/src/sensors/sensor_interfaces/srv/GetCameraParams.srv +++ /dev/null @@ -1,7 +0,0 @@ -string[] camera_names -string[] camera_types ---- -bool success -string[] camera_frame_ids -sensor_msgs/CameraInfo[] camera_infos -float64[] baselines diff --git a/robot/ros_ws/src/sensors/sensors_bringup/CMakeLists.txt b/robot/ros_ws/src/sensors/sensors_bringup/CMakeLists.txt deleted file mode 100644 index 785bb6402..000000000 --- a/robot/ros_ws/src/sensors/sensors_bringup/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(sensors_bringup) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# find dependencies -find_package(ament_cmake REQUIRED) -# uncomment the following section in order to fill in -# further dependencies manually. -# find_package( REQUIRED) - -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - # the following line skips the linter which checks for copyrights - # comment the line when a copyright and license is added to all source files - set(ament_cmake_copyright_FOUND TRUE) - # the following line skips cpplint (only works in a git repo) - # comment the line when this package is in a git repo and when - # a copyright and license is added to all source files - set(ament_cmake_cpplint_FOUND TRUE) - ament_lint_auto_find_test_dependencies() -endif() - -# Install files. -install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY rviz DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) -# install(DIRECTORY params DESTINATION share/${PROJECT_NAME}) - -ament_package() diff --git a/robot/ros_ws/src/sensors/sensors_bringup/LICENSE b/robot/ros_ws/src/sensors/sensors_bringup/LICENSE deleted file mode 100644 index d64569567..000000000 --- a/robot/ros_ws/src/sensors/sensors_bringup/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/robot/ros_ws/src/sensors/sensors_bringup/launch/gst2ros.launch.xml b/robot/ros_ws/src/sensors/sensors_bringup/launch/gst2ros.launch.xml deleted file mode 100644 index 02f8c72bd..000000000 --- a/robot/ros_ws/src/sensors/sensors_bringup/launch/gst2ros.launch.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/robot/ros_ws/src/sensors/sensors_bringup/launch/sensors.launch.xml b/robot/ros_ws/src/sensors/sensors_bringup/launch/sensors.launch.xml deleted file mode 100644 index 95acdc865..000000000 --- a/robot/ros_ws/src/sensors/sensors_bringup/launch/sensors.launch.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/sensors/sensors_bringup/package.xml b/robot/ros_ws/src/sensors/sensors_bringup/package.xml deleted file mode 100644 index 82d396f55..000000000 --- a/robot/ros_ws/src/sensors/sensors_bringup/package.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - sensors_bringup - 0.0.0 - TODO: Package description - andrew - Apache-2.0 - - ament_cmake - - lidar_point_cloud_filter - - ament_lint_auto - ament_lint_common - - - ament_cmake - - diff --git a/simulation/isaac-sim/.gitignore b/simulation/isaac-sim/.gitignore deleted file mode 100644 index dec1f7e46..000000000 --- a/simulation/isaac-sim/.gitignore +++ /dev/null @@ -1 +0,0 @@ -AscentAeroSystemsSITLPackage/ \ No newline at end of file diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/.collect.mapping.json b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/.collect.mapping.json new file mode 100644 index 000000000..da2b4c742 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/.collect.mapping.json @@ -0,0 +1,1103 @@ +{ + "version": "1.0", + "file_records": [ + { + "source_url": "/tmp/prepared_scene.usd", + "source_hash": "a8bb6b7b6f8b145c41a6d015940415997c80869e", + "target_url": "./prepared_scene.usd", + "target_hash": "1a67c7ea11331132d514bab19244dfb6f6ec6b45" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/0001.png", + "source_hash": "dc0217e4fe9eb86c937a873ba2678992c0407b71", + "target_url": "./SubUSDs/textures/0001.png", + "target_hash": "c4cb4a04631c33de410795ecfb130f0a1ce80b79" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/0009.png", + "source_hash": "8b6aca1914b8c3263413d5030966ee85266656b7", + "target_url": "./SubUSDs/textures/0009.png", + "target_hash": "5388cf2c529a1a2ec65db7548eb48c990f13c486" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/0011.png", + "source_hash": "7fea36ab6b97a55a3c2a08975b5c1cd2151036a2", + "target_url": "./SubUSDs/textures/0011.png", + "target_hash": "af009625a1ed81701c1a4c29a29b741115724537" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/0012.png", + "source_hash": "f7fe32b8b73eef41cd77eb5672b1d800a8091b57", + "target_url": "./SubUSDs/textures/0012.png", + "target_hash": "ca6b7ad37c06720f2b9f62b67df8dad21eccc1a6" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/0013.png", + "source_hash": "0c367d5fe3a17b911fb23f93c214e09aef5e3475", + "target_url": "./SubUSDs/textures/0013.png", + "target_hash": "6cb385306d28de7aba8e28da363996fd85b6ab26" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/0014.png", + "source_hash": "007f42457471c7ce4412743356f82f1fb7a8c2dc", + "target_url": "./SubUSDs/textures/0014.png", + "target_hash": "de2986e2c225b761d008efded5a20bd4fb47f2aa" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/0015.png", + "source_hash": "e691185a75e987bf99bd5be7f234bfe41c0841a8", + "target_url": "./SubUSDs/textures/0015.png", + "target_hash": "56c1632a9b8a7a93d54ea0d464249007193effea" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/0020.png", + "source_hash": "31b24099fb4a5b39022fe4babb5e2ab785602dc0", + "target_url": "./SubUSDs/textures/0020.png", + "target_hash": "c1b68513dcb3f515857ef3a6b9c138ee961166d9" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/AisleSign_Text_01.png", + "source_hash": "66a2a2bd4cbff2771752c050f6e88edc33ed1e98", + "target_url": "./SubUSDs/textures/AisleSign_Text_01.png", + "target_hash": "71291573b46358f919abef7ae8154d0ab0f7f0d9" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/AisleSign_Text_02.png", + "source_hash": "0252138752907207b2311ab66ec99ec5af5da020", + "target_url": "./SubUSDs/textures/AisleSign_Text_02.png", + "target_hash": "75f3c2ae8daa38e788eccea23827ba3f787a7c3b" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/AisleSign_Text_03.png", + "source_hash": "fe8f57408258e95c8c562d0898375ff7058ced11", + "target_url": "./SubUSDs/textures/AisleSign_Text_03.png", + "target_hash": "5c444eaf10164bfb932489d349a50bf8f991ba5e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/AisleSign_Text_04.png", + "source_hash": "844b697c60e807ccba313e263957b92a334c1e94", + "target_url": "./SubUSDs/textures/AisleSign_Text_04.png", + "target_hash": "ae5ef384e49328ff0a99664d99d8ac038e1bb029" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/AisleSign_Text_05.png", + "source_hash": "001a920cf1aba1115e3ffbfcf64fde4034bcbd61", + "target_url": "./SubUSDs/textures/AisleSign_Text_05.png", + "target_hash": "637231d4cc9cf6796763934ccab19772a9ea50f2" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/AisleSign_Text_06.png", + "source_hash": "8e0e2ef626f8eadf444a742296283737a5c027ee", + "target_url": "./SubUSDs/textures/AisleSign_Text_06.png", + "target_hash": "f9af30a82fca7e6e038632239f4a005ff6a0672a" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticA_D.png", + "source_hash": "8fed9654f96b924cc691856287f3289e7329ca58", + "target_url": "./SubUSDs/textures/T_BarelPlasticA_D.png", + "target_hash": "923d96018da808b9279e8400b68cb785bd505dab" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticA_M.png", + "source_hash": "9d41fac38468298ee54d26a66d9132e4289e8204", + "target_url": "./SubUSDs/textures/T_BarelPlasticA_M.png", + "target_hash": "fda1292b53445b1e48f85ef32ede2a5bc3b7b456" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticA_N.png", + "source_hash": "1560a8ac77c7392f7a420a0b29c4204bcf1de928", + "target_url": "./SubUSDs/textures/T_BarelPlasticA_N.png", + "target_hash": "84ea05b2ff6e2183df1c9ab5e8be04e993424707" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticA_ORM.png", + "source_hash": "0367fcc009c1cbb98c3e7a90e5cf9572d551a0a5", + "target_url": "./SubUSDs/textures/T_BarelPlasticA_ORM.png", + "target_hash": "6c6af13a135de721e95bf1cc99608886d689d130" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticB_D.png", + "source_hash": "5df4fa1cbb9dcf60c7ffeea2ded8df924862b000", + "target_url": "./SubUSDs/textures/T_BarelPlasticB_D.png", + "target_hash": "9345796dd3b04eedaa84ddc9396dc9bf9737b5bc" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticB_M.png", + "source_hash": "389e135fb199e44881f4291336e5220a9fe5da1f", + "target_url": "./SubUSDs/textures/T_BarelPlasticB_M.png", + "target_hash": "ee4c035d53094a0d7d8425aaae0109e69a96923a" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticB_N.png", + "source_hash": "2356b51bb950b9f94adb772c3dccc31a639a1378", + "target_url": "./SubUSDs/textures/T_BarelPlasticB_N.png", + "target_hash": "2bd2d1f4d1f97a7c3ffcbee15c032b97108109df" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticB_ORM.png", + "source_hash": "71e5d66fc79e622d99294c1939b0e19a97eadf2c", + "target_url": "./SubUSDs/textures/T_BarelPlasticB_ORM.png", + "target_hash": "3cf9ee09dc6700103078c583cbd7fd925a616513" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticC_D.png", + "source_hash": "521714de81fe028688eaf5751eeb6f1199ad1d51", + "target_url": "./SubUSDs/textures/T_BarelPlasticC_D.png", + "target_hash": "2ba76d58824a3903a81969ada4b7ac3970867783" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticC_M.png", + "source_hash": "26c40ef1bdc392d7ca516d5365d171790b3a8eeb", + "target_url": "./SubUSDs/textures/T_BarelPlasticC_M.png", + "target_hash": "c3038031afdf9c6c25cf0b584217b0448c5e16ce" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticC_N.png", + "source_hash": "929673e64de1677df3825681e2f40342862fb011", + "target_url": "./SubUSDs/textures/T_BarelPlasticC_N.png", + "target_hash": "6e9b2ff9b8d83f60faf7d410c3503f07b2eaa216" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticC_ORM.png", + "source_hash": "6f71e2a2505ec7b5eeae5d0d33cb8c09bd62fa8c", + "target_url": "./SubUSDs/textures/T_BarelPlasticC_ORM.png", + "target_hash": "d1a97d6d3cffe997fd5b4d67804700315f0630e9" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticD_D.png", + "source_hash": "adf9e5748eeb30a22864812f2ddca97ec5797fb8", + "target_url": "./SubUSDs/textures/T_BarelPlasticD_D.png", + "target_hash": "7d8f9a9eba1ef44d259c0179b564d117d3e2d84f" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticD_M.png", + "source_hash": "9d2cf0feefecd8544d79dfb91cf7391f52a7d488", + "target_url": "./SubUSDs/textures/T_BarelPlasticD_M.png", + "target_hash": "30326e9a29b915fd05b314753a1de69a47441c4f" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticD_N.png", + "source_hash": "146e21526a617c7213a4923036cf6600275f729f", + "target_url": "./SubUSDs/textures/T_BarelPlasticD_N.png", + "target_hash": "d7b27ffa63643b4e2e9318683ae38084e6b702e1" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BarelPlasticD_ORM.png", + "source_hash": "e6573a75da46d23a7908b0e3f8d35797a44350c4", + "target_url": "./SubUSDs/textures/T_BarelPlasticD_ORM.png", + "target_hash": "324bf32df433976960cc4255708027bb852b59d5" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BeamsA_D.png", + "source_hash": "44c31cdd25ffe857d007967eb29911359b07855f", + "target_url": "./SubUSDs/textures/T_BeamsA_D.png", + "target_hash": "d34931ebd4a55f7d321cbe516c292d307589cb0c" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BeamsA_M.png", + "source_hash": "f484c8f299e060072419299e99382d408bd4d40b", + "target_url": "./SubUSDs/textures/T_BeamsA_M.png", + "target_hash": "8880dec17d07d406cf30ed6f0b46636b0e7cf1f3" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BeamsA_N.png", + "source_hash": "e1494e29559d7d30d7ffac81a74243f8f8739236", + "target_url": "./SubUSDs/textures/T_BeamsA_N.png", + "target_hash": "645ee7c4ebdebf99d0596243b3d54762ddf96ac5" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BeamsA_ORM.png", + "source_hash": "a4a41a6ec63d2d7b4a3926eae5e173e1c8fb2b0e", + "target_url": "./SubUSDs/textures/T_BeamsA_ORM.png", + "target_hash": "98a8556cd0acb7a417eca5c2e5238ba3b708c637" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BlankMask_M.png", + "source_hash": "a56e2c85766bf337cb647b8449380236c1045365", + "target_url": "./SubUSDs/textures/T_BlankMask_M.png", + "target_hash": "49a64574824b8b113241a2211601526ea44b4bab" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BottlesPlastic_D.png", + "source_hash": "b5a9dda3bc26d0599e3d48ec8e3c7e981b4ba899", + "target_url": "./SubUSDs/textures/T_BottlesPlastic_D.png", + "target_hash": "9297dd9b8763448629ed4ca5efb257f60df85972" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BottlesPlastic_N.png", + "source_hash": "4638cea68200ed16d862baebf112a96fd261c230", + "target_url": "./SubUSDs/textures/T_BottlesPlastic_N.png", + "target_hash": "1b916dbe783b15bcab4758dfb2145421818d59f8" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BottlesPlastic_ORM.png", + "source_hash": "224fe32269464c914684cb00647c3ccf95d6d564", + "target_url": "./SubUSDs/textures/T_BottlesPlastic_ORM.png", + "target_hash": "075322dc57c08a2071e2614ddd3ee5abff6309f6" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BucketPlasticB_D.png", + "source_hash": "c132975dab407727f44439da8e51bb4934e32ea0", + "target_url": "./SubUSDs/textures/T_BucketPlasticB_D.png", + "target_hash": "dcccb3617c8c336f7a95ed6b7e36ddf8a715a0c3" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BucketPlasticB_M.png", + "source_hash": "8a69034b0e589db6fa45a25259fa4c8c98d065c0", + "target_url": "./SubUSDs/textures/T_BucketPlasticB_M.png", + "target_hash": "fe8b4dc01b6631e2209572dd6840baede897a2eb" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BucketPlasticB_N.png", + "source_hash": "bbb391b3bcb5fe4956c5dd970ce996973f15a89d", + "target_url": "./SubUSDs/textures/T_BucketPlasticB_N.png", + "target_hash": "b67fc3742ca8cab90dd70e03b68b054a7d738a18" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BucketPlasticB_ORM.png", + "source_hash": "0ca876f88e9ece37a167e0ac1f516be103c1ed80", + "target_url": "./SubUSDs/textures/T_BucketPlasticB_ORM.png", + "target_hash": "664f17d4e1bf927900208060e7bc7c279410adb7" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BucketPlasticD_D.png", + "source_hash": "b1bb2c01b14f1c42e67a4e6dd02d1ae7648237bc", + "target_url": "./SubUSDs/textures/T_BucketPlasticD_D.png", + "target_hash": "9a5120364334ff13271347ef14b76f5110ebc6c8" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BucketPlasticD_M.png", + "source_hash": "99e90b960e816cbae7eaa210c4bdcb0cc31befbc", + "target_url": "./SubUSDs/textures/T_BucketPlasticD_M.png", + "target_hash": "07adc4a3ae341710187bfcf34595c5eddf5f4290" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BucketPlasticD_N.png", + "source_hash": "d1bd20ad6b03a8447bf1a64059264b9f7277f22a", + "target_url": "./SubUSDs/textures/T_BucketPlasticD_N.png", + "target_hash": "843c9354db24f49dc370690029359644dc649ee9" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_BucketPlasticD_ORM.png", + "source_hash": "869edb7f570ee35793f11f2c531d797ae65f291d", + "target_url": "./SubUSDs/textures/T_BucketPlasticD_ORM.png", + "target_hash": "113209dd802e592521f02547c0c732140100579d" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxA_D.png", + "source_hash": "6560c336153924747856fae137a84431f4f066d2", + "target_url": "./SubUSDs/textures/T_CardBoxA_D.png", + "target_hash": "9e8d1fe514b3ba6b1edc1912df4394835968da4c" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxA_N.png", + "source_hash": "46bacf3848d0e934e9c1c0741aa442634fbef01c", + "target_url": "./SubUSDs/textures/T_CardBoxA_N.png", + "target_hash": "8cf7e8cd5d39e82d5a5f2048a5650d2cef31dc24" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxA_ORM.png", + "source_hash": "fb05eacd9a5e41a33f34ee75621f7ea79d38cac6", + "target_url": "./SubUSDs/textures/T_CardBoxA_ORM.png", + "target_hash": "c13b495b9968bf7cdab63622cce4b03f3e75a8f3" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxB_D.png", + "source_hash": "228e05e61626bc4c47909f6a9652f98d30bf912c", + "target_url": "./SubUSDs/textures/T_CardBoxB_D.png", + "target_hash": "c9b9bc0bd20d0c226b5da79dbe5cce4d4d013de6" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxB_N.png", + "source_hash": "67d9764765604d58a7862c509845b73a28a8d037", + "target_url": "./SubUSDs/textures/T_CardBoxB_N.png", + "target_hash": "8e94d02e5c485642a1ec930c8a4c4333bfc4a5b1" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxB_ORM.png", + "source_hash": "7bdab2e1d085d13150f653eae29f6fd197dba367", + "target_url": "./SubUSDs/textures/T_CardBoxB_ORM.png", + "target_hash": "bba69736557afdb6e8a4f83c32f19d96fb195279" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxC_D.png", + "source_hash": "c111d071bb1019c51fa43a5e3d8d92b40a8a1c39", + "target_url": "./SubUSDs/textures/T_CardBoxC_D.png", + "target_hash": "5df187deffe061cb70e98e2e35f8b0a993285cab" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxC_N.png", + "source_hash": "9751dd13a3509c53a27d8eeb8f7630ea73f77f42", + "target_url": "./SubUSDs/textures/T_CardBoxC_N.png", + "target_hash": "fba412e676505461cfaa93ee7fc9ab90e0be1fcc" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxC_ORM.png", + "source_hash": "101f2a052ecae45f6ca1737bbe61f7b20e39d838", + "target_url": "./SubUSDs/textures/T_CardBoxC_ORM.png", + "target_hash": "91626f3690a85d6bcee35d83a03b359e01f323c0" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxD_D.png", + "source_hash": "86945e58a5b56add885704a315b2d76a42ff13db", + "target_url": "./SubUSDs/textures/T_CardBoxD_D.png", + "target_hash": "d67f26892c289c2e63a9d7ecdeaca54c13ae861b" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxD_N.png", + "source_hash": "b9233a4581e882cb364285df1949dd41e647f55f", + "target_url": "./SubUSDs/textures/T_CardBoxD_N.png", + "target_hash": "f6db2c5c2466796293591ce5c6f316aaf6652984" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CardBoxD_ORM.png", + "source_hash": "4317004c912ff5176c90d4c0143489ce968afe3f", + "target_url": "./SubUSDs/textures/T_CardBoxD_ORM.png", + "target_hash": "fdceade4b13faa28f60bf25b124980affeeff661" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CeilingA_D.png", + "source_hash": "b56a44d6e0be94d46611a950c974070e1ff254f8", + "target_url": "./SubUSDs/textures/T_CeilingA_D.png", + "target_hash": "1886747c5601a264308a803c6988f7f79143d8da" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CeilingA_M.png", + "source_hash": "2a95d80d950c5a1ada408ba09c23385db93e040d", + "target_url": "./SubUSDs/textures/T_CeilingA_M.png", + "target_hash": "ae38384c583db8f7df0a023dddebd2542cae24f2" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CeilingA_N.png", + "source_hash": "d32bd6d511ac4d9655408686be1c8070a0e01761", + "target_url": "./SubUSDs/textures/T_CeilingA_N.png", + "target_hash": "508588d4b319e32cb8994213238950dc7d1cce38" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CeilingA_ORM.png", + "source_hash": "8410026ca4ed4a9b98047ad68ae9604fc54ff491", + "target_url": "./SubUSDs/textures/T_CeilingA_ORM.png", + "target_hash": "c2679c37d64e9bcd9d963414e3026496b8cbd0c7" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_A_D.png", + "source_hash": "de5c880eb03e6c4a16d949667f1d04e4b8d23457", + "target_url": "./SubUSDs/textures/T_CratePlastic_A_D.png", + "target_hash": "6f59ced862a81a452c316d8d7451f98267526173" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_A_M.png", + "source_hash": "04b2b081e8fdafd840bafa7aa7760c3d618a1ebf", + "target_url": "./SubUSDs/textures/T_CratePlastic_A_M.png", + "target_hash": "daaf5a1d84fb0143ffa143cca56c19996fc56858" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_A_N.png", + "source_hash": "701ff07d878a1f1d010c7d256d3e50ee4c10257e", + "target_url": "./SubUSDs/textures/T_CratePlastic_A_N.png", + "target_hash": "0167795d143d72f051f75dfa2302c18c479f2f9b" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_A_ORM.png", + "source_hash": "5fa118a2ac0ae6c072ab300ba48ebb5d79d6907d", + "target_url": "./SubUSDs/textures/T_CratePlastic_A_ORM.png", + "target_hash": "865af741795d10fbe99b5204e99ecd6bce013c17" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_B_D.png", + "source_hash": "5045b98f0e58396a1be066b8f0126931c3dac1a3", + "target_url": "./SubUSDs/textures/T_CratePlastic_B_D.png", + "target_hash": "0fc608bcdcfaaf6c27879848f9f8501b0fefff53" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_B_M.png", + "source_hash": "de1b6985408d285985314631a2a590ad325d31f5", + "target_url": "./SubUSDs/textures/T_CratePlastic_B_M.png", + "target_hash": "78d1fa9737e1bc7d0654506b60b79895830368b0" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_B_N.png", + "source_hash": "e78b173224eef05c90739c33ff082b3f7c54b9ce", + "target_url": "./SubUSDs/textures/T_CratePlastic_B_N.png", + "target_hash": "807920979bf48a15bf3ef263ac8329b0bcd03a18" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_B_ORM.png", + "source_hash": "85cef844641e4bc109b878dd8b2bd3d91e33ee39", + "target_url": "./SubUSDs/textures/T_CratePlastic_B_ORM.png", + "target_hash": "376d79172cb915c7235984b2acf115f721f708ed" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_C_D.png", + "source_hash": "919c5bc88161765b0e9183e951ada8968dec33a1", + "target_url": "./SubUSDs/textures/T_CratePlastic_C_D.png", + "target_hash": "6f3a550129de90fd788aedca181f86dcefabc75a" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_C_M.png", + "source_hash": "2594f6d29aa08bae1228a4b23dc47b69c12d192d", + "target_url": "./SubUSDs/textures/T_CratePlastic_C_M.png", + "target_hash": "773c05fdc0317f4495bb73037b2e308018c1804c" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_C_N.png", + "source_hash": "bfbee4cfa54808e5346dd1f930085b625a9c75fa", + "target_url": "./SubUSDs/textures/T_CratePlastic_C_N.png", + "target_hash": "3a101e808ba1abe5238622c365e412fa5d3ac09a" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_C_ORM.png", + "source_hash": "e61cad738da5d39d88c4990367bfaa7348ceaab7", + "target_url": "./SubUSDs/textures/T_CratePlastic_C_ORM.png", + "target_hash": "7673b22852bd275536e9ef46b703d716350154f5" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_E_D.png", + "source_hash": "f78c39720d826fcacb3c6b61c91ee18407818a82", + "target_url": "./SubUSDs/textures/T_CratePlastic_E_D.png", + "target_hash": "a0df9352c695015e79e0d58e0e3bad40b8bdb8f8" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_E_M.png", + "source_hash": "7ea7a4f0c7c4057ef770253e9fe459c13a13b799", + "target_url": "./SubUSDs/textures/T_CratePlastic_E_M.png", + "target_hash": "2dad50bf7ae2a672938259e0e9329d2aa05ce3ee" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_E_N.png", + "source_hash": "96822ebc22f5058a332a7dd931c22099e347fa97", + "target_url": "./SubUSDs/textures/T_CratePlastic_E_N.png", + "target_hash": "e71e4f25c8b419e3f300863760827272f4b139b8" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_CratePlastic_E_ORM.png", + "source_hash": "910e7a54573ba010aa225f0d681893a5a4fd5c13", + "target_url": "./SubUSDs/textures/T_CratePlastic_E_ORM.png", + "target_hash": "b5e50d36ace37483918f5189ce46d84b95f3cdf1" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FireExtinguisher_D.png", + "source_hash": "951b7418c994663e8d2d657053e680228dd6c0b5", + "target_url": "./SubUSDs/textures/T_FireExtinguisher_D.png", + "target_hash": "5eb23eb869bb6ee278d2abbb8917206bf32776d2" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FireExtinguisher_N.png", + "source_hash": "bcb8d644480054742fef800df2f14c1323882d81", + "target_url": "./SubUSDs/textures/T_FireExtinguisher_N.png", + "target_hash": "d3393ab6a616595bd1c99240f8f4403bb56e5a63" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FireExtinguisher_ORM.png", + "source_hash": "f88cc79d786b90d2e09922990afd834b12d95e27", + "target_url": "./SubUSDs/textures/T_FireExtinguisher_ORM.png", + "target_hash": "46999f66293a6ed30e1da80329419c959b3cb5db" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FirstAid_D.png", + "source_hash": "0f6e776383507701547e185ca40ca60f4662a20c", + "target_url": "./SubUSDs/textures/T_FirstAid_D.png", + "target_hash": "c172af28ed84bd6ad5a5b0297820bd591c7c5159" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FirstAid_N.png", + "source_hash": "d7f614e96702cbd715215c0136af592b9f979aa9", + "target_url": "./SubUSDs/textures/T_FirstAid_N.png", + "target_hash": "4be4c97a51de78ca0052179c34dcba5b51c83c46" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FirstAid_ORM.png", + "source_hash": "6f8a71722e6a893df02c126fa4fa9e856f5533fa", + "target_url": "./SubUSDs/textures/T_FirstAid_ORM.png", + "target_hash": "1b835e9bfe37437f552e932cb1f29cf6541d65e2" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FloorStripes_D.png", + "source_hash": "8716a80b17080c401d70873ec7d3ae4c98a1a655", + "target_url": "./SubUSDs/textures/T_FloorStripes_D.png", + "target_hash": "731d2ceb04318a1c71548a484592130441d0f193" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FloorStripes_M.png", + "source_hash": "78b318006f041d64ee1b39b8d9a07442705c92f2", + "target_url": "./SubUSDs/textures/T_FloorStripes_M.png", + "target_hash": "56f9d1ba697be9193b4e59c70f3b5204c2777531" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FloorStripes_N.png", + "source_hash": "de8f9108cbfe26b30b270b1b1e408c726c497cff", + "target_url": "./SubUSDs/textures/T_FloorStripes_N.png", + "target_hash": "6768772557e4b7b7647115046fef4aafcf7bc574" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FloorStripes_ORM.png", + "source_hash": "fd76367102547cddda231f8555b8fb0f2dcf407f", + "target_url": "./SubUSDs/textures/T_FloorStripes_ORM.png", + "target_hash": "3a94e3fd93608383f19319ac5257279badcded78" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_Floor_01_D.png", + "source_hash": "f05f28604df9477c2667a81bd52b1fd857361178", + "target_url": "./SubUSDs/textures/T_Floor_01_D.png", + "target_hash": "4025c3a752f3251b6778c562856ce5f2e3e33ebf" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_Floor_01_M.png", + "source_hash": "50bb33d5220c6cbc71edb775e6ab852a76220095", + "target_url": "./SubUSDs/textures/T_Floor_01_M.png", + "target_hash": "e5954b5e28968e4f9ed01fbe6f9f3be8df45f843" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_Floor_01_N.png", + "source_hash": "e3e3dddcce3ca7484bae9c672aa5c89b35c95ece", + "target_url": "./SubUSDs/textures/T_Floor_01_N.png", + "target_hash": "75276b80f766382d49bd80da8aaaca6802547d3f" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_Floor_01_ORM.png", + "source_hash": "39434398e65ccec437a7977ebe0560daa863199f", + "target_url": "./SubUSDs/textures/T_Floor_01_ORM.png", + "target_hash": "f3d445cc5b9bdd4bf10cfeed9250e26091259628" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FrameA_D.png", + "source_hash": "125a89be8df7df11cbfd8c1eaf9d46f2c4edc255", + "target_url": "./SubUSDs/textures/T_FrameA_D.png", + "target_hash": "20cb6a749fc85ed86c0ae192924e2fca80894fcf" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FrameA_M.png", + "source_hash": "91a6a1346df4c870611eaa11791934f2ab50e437", + "target_url": "./SubUSDs/textures/T_FrameA_M.png", + "target_hash": "95e973edde9291e1780a2882ea7a670ed357a84f" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FrameA_N.png", + "source_hash": "e2581dbe782a8a4183ca8881cbe9837dc3dfc56b", + "target_url": "./SubUSDs/textures/T_FrameA_N.png", + "target_hash": "708d8c05a861df3b82522dd5dce139c01c87699e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_FrameA_ORM.png", + "source_hash": "d400dcc6de9ecf7250fef89fdd6f02c89f9e6fa2", + "target_url": "./SubUSDs/textures/T_FrameA_ORM.png", + "target_hash": "7f87fdd9f8cc10a65b7f3baa0a5f56544f28556d" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_LampCeilingA_D.png", + "source_hash": "8d387be80df07372deef69d5c86291ce254952be", + "target_url": "./SubUSDs/textures/T_LampCeilingA_D.png", + "target_hash": "71f9460f749adcbfaa6b23b7c9baf4435e8e89c6" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_LampCeilingA_N.png", + "source_hash": "9b946e63a39fafd9f1de87f229a51e24336f30a8", + "target_url": "./SubUSDs/textures/T_LampCeilingA_N.png", + "target_hash": "e0f0d1869e31165f2bfd1a5cef022659fd41fb7b" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_LampCeilingA_ORM.png", + "source_hash": "beebc4fff90e4cc949e717b57543dd1be712bf08", + "target_url": "./SubUSDs/textures/T_LampCeilingA_ORM.png", + "target_hash": "04cdc9d7d3dbb421bddd582aa0beca2889c38501" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PaletteA_01_D.png", + "source_hash": "e89e8ce8164faff0ae56de35f2f90dc62e81ea82", + "target_url": "./SubUSDs/textures/T_PaletteA_01_D.png", + "target_hash": "5f9312320497728f59bd1ab2abe0909253361d62" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PaletteA_01_M.png", + "source_hash": "3dd41e58686ae492c12a9a03ce79aa278deb05cf", + "target_url": "./SubUSDs/textures/T_PaletteA_01_M.png", + "target_hash": "dd5b86e383d235620818b0e55b9bef85d55541d2" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PaletteA_01_N.png", + "source_hash": "f1369d80f9ed0a9b3d9cc06d02dc18dab49c1a3a", + "target_url": "./SubUSDs/textures/T_PaletteA_01_N.png", + "target_hash": "02ea523d6e2f2e9ae63bd83bfba72737667f1954" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PaletteA_01_ORM.png", + "source_hash": "c84d2348d5e0ebfbf569e4dff232d8fa041ce3fb", + "target_url": "./SubUSDs/textures/T_PaletteA_01_ORM.png", + "target_hash": "8196718f6865d1c8996c55e2530493454789f588" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PlasticWrap_D.png", + "source_hash": "0e42122e4417673f8546a1ac07b79bc577aae766", + "target_url": "./SubUSDs/textures/T_PlasticWrap_D.png", + "target_hash": "19564cd163e4e83973a17d7972351e04e4db8fa4" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PlasticWrap_ORM.png", + "source_hash": "ae1b9469edf611156be821eb98e6d76ef08813d0", + "target_url": "./SubUSDs/textures/T_PlasticWrap_ORM.png", + "target_hash": "87e8cb6dd0fdfac5121e3d55d21a19fe859c465c" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PushcartA_D.png", + "source_hash": "4ed8fbbdcd472e45df98988045e8c3b0b3c463e3", + "target_url": "./SubUSDs/textures/T_PushcartA_D.png", + "target_hash": "8b8ad0b7e5c03999b2ffba61643ed18291f9f0d5" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PushcartA_M.png", + "source_hash": "4bc04e5d7d8076ff5481076a0f6351272dc43aa0", + "target_url": "./SubUSDs/textures/T_PushcartA_M.png", + "target_hash": "9be959ace6b0256b03aeb385702fa65f6e8b0059" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PushcartA_N.png", + "source_hash": "33cee5afdad9a14351dfb0e2d0866680cbbff14f", + "target_url": "./SubUSDs/textures/T_PushcartA_N.png", + "target_hash": "043edb0bf8e58d1fb3bd3ac6e923174e4038e33e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PushcartA_ORM.png", + "source_hash": "4e5b0a82ba28c3afa55f334a7ca08675bb9081d0", + "target_url": "./SubUSDs/textures/T_PushcartA_ORM.png", + "target_hash": "511e0cc336c1ddb59552413dcc9ebc18df44cda8" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_01_D.png", + "source_hash": "c5582a1cc18ae9b171622a046755c9fca5c31810", + "target_url": "./SubUSDs/textures/T_RackSetA_01_D.png", + "target_hash": "06ee35caf8ba0180d1024693e10b6aa963bc7d12" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_01_M.png", + "source_hash": "9197751f164e98e9bf3e5a0af524aedadbd5a214", + "target_url": "./SubUSDs/textures/T_RackSetA_01_M.png", + "target_hash": "bbb236407e64abdceae4ed924af9b9b3906e325f" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_01_N.png", + "source_hash": "082b13b40e1570f723d021826e4a81d43ebc5675", + "target_url": "./SubUSDs/textures/T_RackSetA_01_N.png", + "target_hash": "c1acba766a6ce9b474d9abb2268fe21fc77e56f4" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_01_ORM.png", + "source_hash": "e29673585f18884d9a937852129ca30aecd4716a", + "target_url": "./SubUSDs/textures/T_RackSetA_01_ORM.png", + "target_hash": "59d6cc34536b28189e61297c6183c3900cda5e08" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_02_D.png", + "source_hash": "3e2272c74bc132c1cab712329c47d85b4f41a041", + "target_url": "./SubUSDs/textures/T_RackSetA_02_D.png", + "target_hash": "34353795bf40bfcc4861ad6f424d4fb1f8d27c73" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_02_M.png", + "source_hash": "efcfef2043d45c0a79a2a73a9944bbb6b0dc4264", + "target_url": "./SubUSDs/textures/T_RackSetA_02_M.png", + "target_hash": "6f64b24479731416a1cb00375afe42f37ce75f96" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_02_N.png", + "source_hash": "1a3bef3225f89680f811dd40dcf751c693326ebd", + "target_url": "./SubUSDs/textures/T_RackSetA_02_N.png", + "target_hash": "68eeee4123e1b35bb1de868104325c7b458ad80e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_02_ORM.png", + "source_hash": "3641ff408224b3055257dd3d45588277c0eb758b", + "target_url": "./SubUSDs/textures/T_RackSetA_02_ORM.png", + "target_hash": "61812481e81cc51c273111d491c3b2e22c48c8b9" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_03_D.png", + "source_hash": "9488c72c78f32e870d812590645e9f271d376e17", + "target_url": "./SubUSDs/textures/T_RackSetA_03_D.png", + "target_hash": "63a7a9f2ec447cd704d0fafdcf350af90595ca16" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_03_M.png", + "source_hash": "db7c2960a8e90126fd5f844f6f353730c48b172a", + "target_url": "./SubUSDs/textures/T_RackSetA_03_M.png", + "target_hash": "e3b837139346d49e8bb4c8a9c75f00ebd78d7e1a" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_03_N.png", + "source_hash": "cd5f1726efcf93bd798aecd9d6c86d241b336e47", + "target_url": "./SubUSDs/textures/T_RackSetA_03_N.png", + "target_hash": "5b5e7310df5ab7eec22563f434e38e95b83ab602" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_03_ORM.png", + "source_hash": "925d1fea1e36d7cd493c4ed0f7d3312d69aad728", + "target_url": "./SubUSDs/textures/T_RackSetA_03_ORM.png", + "target_hash": "4d41e00c46b6f3ce3e6ec025efd9d63c5b51264b" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_04_D.png", + "source_hash": "344e68f74c3abec67bc270f63fc147c46377fdb9", + "target_url": "./SubUSDs/textures/T_RackSetA_04_D.png", + "target_hash": "fff4fd640c23d7f1ba74d3e97f0b88cdeecfdd2d" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_04_M.png", + "source_hash": "d9d30f825a2cc89d7c88dcfd3f364d6f00639e2c", + "target_url": "./SubUSDs/textures/T_RackSetA_04_M.png", + "target_hash": "ce804aebc3f387acf314cbbc2992a6c257d0a5e7" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_04_N.png", + "source_hash": "922a6ef9e8f14a2a4b51b7e3e0b5fc7f21ffead0", + "target_url": "./SubUSDs/textures/T_RackSetA_04_N.png", + "target_hash": "8af0794bc04e9809be63c61349e1ec16b605087f" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackSetA_04_ORM.png", + "source_hash": "785b466e5dad3f28a3813a4c5d2b48b4a3b0193f", + "target_url": "./SubUSDs/textures/T_RackSetA_04_ORM.png", + "target_hash": "59f3fb03ef855d32ec7d1dde7717efbdb92f4ee6" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackShield_D.png", + "source_hash": "39d168ae1dba39f073c7ebbb32252f4867c239a2", + "target_url": "./SubUSDs/textures/T_RackShield_D.png", + "target_hash": "2b3b886cd3f073505b2624c18cd558b458384d1b" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackShield_N.png", + "source_hash": "faf45d5b7c0ec5005b569f5d7b006f69a1fe75cc", + "target_url": "./SubUSDs/textures/T_RackShield_N.png", + "target_hash": "f00b3566bf06a1928ac4ee428ae7f4f4d43e28c6" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_RackShield_ORM.png", + "source_hash": "6799def29e3b95dc95ab91c9092143515073ff21", + "target_url": "./SubUSDs/textures/T_RackShield_ORM.png", + "target_hash": "7efc45eea53b74427e591268b5f50e47567a0255" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_SignsB_D.png", + "source_hash": "c9217ea67be25b7ce30cf4eef1de51261b23e890", + "target_url": "./SubUSDs/textures/T_SignsB_D.png", + "target_hash": "3e145cf7b64c8435baa77c1601dd940b86544fb6" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_SignsC_D.png", + "source_hash": "cf0807340025ea0ac4ca276d894ffbbf041381c3", + "target_url": "./SubUSDs/textures/T_SignsC_D.png", + "target_hash": "2c3c6070c90343926fef2ebfb2c94f3b2117b984" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_SignsC_ORM.png", + "source_hash": "a26f0b7dbbb42b1d88acf45de75bbdb43a4067e8", + "target_url": "./SubUSDs/textures/T_SignsC_ORM.png", + "target_hash": "7a1abfd0969c36990777eeddf1b456e606a8f8e3" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallA_01_D.png", + "source_hash": "48f4a47dd4e9e92e4b3d68ad7aa77a14b6207773", + "target_url": "./SubUSDs/textures/T_WallA_01_D.png", + "target_hash": "387ea0a943437ec2a68e1580ed186b01e5cd517c" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallA_01_M.png", + "source_hash": "79daea91f5f2108902f60a23dba4d5d2b1118ee2", + "target_url": "./SubUSDs/textures/T_WallA_01_M.png", + "target_hash": "96af76f668e9edd29713d2d8eb83e9f3330c7a5e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallA_01_N.png", + "source_hash": "ab0ec62c3eebd07f6208e5cbceb5a569871339f4", + "target_url": "./SubUSDs/textures/T_WallA_01_N.png", + "target_hash": "dc8124d95f50ce1f1f49e8a6319695f6aa0ee5e5" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallA_01_ORM.png", + "source_hash": "136e139910e3fe0b4bb52de83b5951a88d2072b6", + "target_url": "./SubUSDs/textures/T_WallA_01_ORM.png", + "target_hash": "b5e0aaa63f01ea1ff7e1d84918f83bf2f14e3f17" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallA_02_D.png", + "source_hash": "d5d3f933c736bca4afa34c61fac99d4b74c1acdf", + "target_url": "./SubUSDs/textures/T_WallA_02_D.png", + "target_hash": "ac950e5013e3c98cb7d484c8aaf42f5cb84a64f8" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallA_02_N.png", + "source_hash": "b44ce179773490ef6f0cb404a1327c0a04412f1b", + "target_url": "./SubUSDs/textures/T_WallA_02_N.png", + "target_hash": "36cfc5f2bb1ded9731235522e76d1173326726b0" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallA_02_ORM.png", + "source_hash": "66b4c8745b04221bbb746f0d6b5849bd3081794b", + "target_url": "./SubUSDs/textures/T_WallA_02_ORM.png", + "target_hash": "274eb5d4479aeb91b8350d1cb415ce96b73c5e4f" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallDetails_D.png", + "source_hash": "d8d38c783e8c611ecb24e6338d7fdb01c676e0b3", + "target_url": "./SubUSDs/textures/T_WallDetails_D.png", + "target_hash": "056058856abb14a13cc81c8fc6cb70fe7474f63d" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallDetails_N.png", + "source_hash": "b43829bc848fd754eea16dc27225588054d98dbd", + "target_url": "./SubUSDs/textures/T_WallDetails_N.png", + "target_hash": "ddc1ed9e1fed4f28ed740cf0c3ab032825dbffd4" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallDetails_ORM.png", + "source_hash": "f7f4a333c741fe942e50dc3dddc327e78cfc56e9", + "target_url": "./SubUSDs/textures/T_WallDetails_ORM.png", + "target_hash": "d2cfee5159cf3b4608889f7ea5d73dda3f18eb95" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Props/Forklift/Materials/Textures/T_Forklift_D.png", + "source_hash": "d7329d892137215a97b9813835b59fa9a55e1c22", + "target_url": "./SubUSDs/textures/T_Forklift_D.png", + "target_hash": "82411ced7ac3a9e61ea52349cbb698518d3f5547" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Props/Forklift/Materials/Textures/T_Forklift_N.png", + "source_hash": "45c72bf8b867d88fbcad21be3cbc03c519a0089a", + "target_url": "./SubUSDs/textures/T_Forklift_N.png", + "target_hash": "e224feef8f64dc0817b75230503cb4082183a290" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Props/Forklift/Materials/Textures/T_Forklift_ORM.png", + "source_hash": "833295ee727f1641216fca14f81fbd95001c3c48", + "target_url": "./SubUSDs/textures/T_Forklift_ORM.png", + "target_hash": "0fe7926e866c63241b6402611538ed0f48ca8d11" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_Barcode_0001.mdl", + "source_hash": "e2d8feef9ed39d2e07e5e1622848a6e0334c18d0", + "target_url": "./SubUSDs/materials/MI_Barcode_0001.mdl", + "target_hash": "5a7b4cb2f130da175fb34dcc578727afe8cf4c22" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_CeilingA_06b.mdl", + "source_hash": "5c6ab40a3344b3cfc6ca00dc677b674a3e4636dd", + "target_url": "./SubUSDs/materials/MI_CeilingA_06b.mdl", + "target_hash": "cf7619d1fcddda748fe66914479c78067df63d72" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_CratePlasticE_01.mdl", + "source_hash": "9000b670198da7be0a05905d38112b2760882bda", + "target_url": "./SubUSDs/materials/MI_CratePlasticE_01.mdl", + "target_hash": "dc4d5f64229465c672d5d0f8e362b3d753a1ad71" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_Floor_01.mdl", + "source_hash": "16e8835ab86c6476d24dae1700eda03c200ee60f", + "target_url": "./SubUSDs/materials/MI_Floor_01.mdl", + "target_hash": "ea67299b5559ac313d49229dbebf8af9935f4972" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_FrameA_01.mdl", + "source_hash": "3fcf281e175bea2cff57cb06accd5394cbceb47c", + "target_url": "./SubUSDs/materials/MI_FrameA_01.mdl", + "target_hash": "67ce2a80e03dbb288185ac75eb869889eaf21671" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_LampCeilingA.mdl", + "source_hash": "2dcf8853388db28bd642022b67d9cd08180cc927", + "target_url": "./SubUSDs/materials/MI_LampCeilingA.mdl", + "target_hash": "0c8e3238a4fa1c83d7225ed945a56a69285b5b1e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_PaperNotes_01.mdl", + "source_hash": "0af822ddf6a857c281191c5be7828fd667a29f70", + "target_url": "./SubUSDs/materials/MI_PaperNotes_01.mdl", + "target_hash": "d63c7f4076ab34ac145f574bc98c23d850b9cdee" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_PushcartA_01.mdl", + "source_hash": "1138033547b8ad6aff7aead15f49bf27bf34e26b", + "target_url": "./SubUSDs/materials/MI_PushcartA_01.mdl", + "target_hash": "6b3c1ca78d8bde3f8ccf905415156fc4f0b55492" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_RackShield_01.mdl", + "source_hash": "1073c43ce0419b708746772e5d4b339d4869ff50", + "target_url": "./SubUSDs/materials/MI_RackShield_01.mdl", + "target_hash": "72ef9839323ffae48944c7bf916c7afb47e4894e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_SignB.mdl", + "source_hash": "dc5ede2225b29a2a82d3c53b37efa4e5634acab3", + "target_url": "./SubUSDs/materials/MI_SignB.mdl", + "target_hash": "02350981c937b13a5dda637309de951226678662" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MI_WallB_01.mdl", + "source_hash": "00e989e99b41c61bb9347f3f3401ece2ff97b312", + "target_url": "./SubUSDs/materials/MI_WallB_01.mdl", + "target_hash": "833a37ca8ad1c728bb6ceb9ac2f5771aee72d616" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/M_AisleSign.mdl", + "source_hash": "4916972f64f3f3a266d56ba042235dce7f5f8f21", + "target_url": "./SubUSDs/materials/M_AisleSign.mdl", + "target_hash": "dc7563232644e537e1a91681b9751e38ed0648e8" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/M_Glow.mdl", + "source_hash": "33ea79116c799ea597402ab4b744527a462c0d2f", + "target_url": "./SubUSDs/materials/M_Glow.mdl", + "target_hash": "41ddb0e8e49fe34c282d62e0dd680968d5c1ffc9" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/M_TrafficCone.mdl", + "source_hash": "e1844f44b39b9b5a05420ec98a652255f066c703", + "target_url": "./SubUSDs/materials/M_TrafficCone.mdl", + "target_hash": "ef2e126dbabab22383376b80762a44e67769c2a1" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/M_WallBoard_01.mdl", + "source_hash": "d92881b2b0b739aa0cc728f0d3d67c08ab96b557", + "target_url": "./SubUSDs/materials/M_WallBoard_01.mdl", + "target_hash": "c247d2e78b1c7a52c86822b7014d901db033234b" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/M_WetFloorSign.mdl", + "source_hash": "6a7cd9f154d867d41c6f21862c5425763aeb8832", + "target_url": "./SubUSDs/materials/M_WetFloorSign.mdl", + "target_hash": "bc1c8bcefaba8888d53e484731827d9c2d58f595" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/MaterialInstanceDynamic_1220.mdl", + "source_hash": "7929bb3e8e4bf728573f4d3d7105c892360da5a1", + "target_url": "./SubUSDs/materials/MaterialInstanceDynamic_1220.mdl", + "target_hash": "389c9bd3456c87af220e0cdad37ec50a09f463d4" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/OmniUe4Function.mdl", + "source_hash": "c1ec0401aebbf6908480c070cc43dfcc40a9bd51", + "target_url": "./SubUSDs/materials/OmniUe4Function.mdl", + "target_hash": "75dba72a4f438978e54c017fffd2628b6e773c41" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/OmniUe4Base.mdl", + "source_hash": "331ab4a83c96157c0f172dcad1a20aacd7be77ca", + "target_url": "./SubUSDs/materials/OmniUe4Base.mdl", + "target_hash": "a45ec7cca6a6fc9b7f5b21c835d968d116a9b2b1" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PaperNotes_D.png", + "source_hash": "2f1e58e843e3ee9c7954202b0de75ae5999f16ca", + "target_url": "./SubUSDs/textures/MI_PaperNotes_01/T_PaperNotes_D.png", + "target_hash": "88635ac492f803bd510cf730d25cf5b55c41c11d" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PaperNotes_M.png", + "source_hash": "4a72feeac3a077814c6b5afafd27259a91892e19", + "target_url": "./SubUSDs/textures/MI_PaperNotes_01/T_PaperNotes_M.png", + "target_hash": "8ca235e2b720dbd96ff7662c57a1de7fd58ac0a3" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_SignsA_D.png", + "source_hash": "a8e665d32219ffab4457369647d773b83973b231", + "target_url": "./SubUSDs/textures/MI_SignB/T_SignsA_D.png", + "target_hash": "c79c644d575dce3cdf5065243105529fb18d91df" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WetFloorSign_N.png", + "source_hash": "7cedfde5eff57fdef630442a1424887326a3fae6", + "target_url": "./SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_N.png", + "target_hash": "ca57abf7023f3754e4550ed4df03a0fc510ef30e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WetFloorSign_D.png", + "source_hash": "97cd4d7b0b96200fe5d81034f0f365f21d5a6c74", + "target_url": "./SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_D.png", + "target_hash": "700c9226a0a3d0b08dd0a34344c9e5cf37f34163" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WetFloorSign_ORM.png", + "source_hash": "7ed60175793d66c75418966e49930d2f38bbb194", + "target_url": "./SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_ORM.png", + "target_hash": "9792b412289a8dbeb33c32d24f87cf996012cc2a" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_TrafficCone_N.png", + "source_hash": "b0345f127ebfc134ee9014c1e59f9d4800eb461a", + "target_url": "./SubUSDs/textures/M_TrafficCone/T_TrafficCone_N.png", + "target_hash": "b9c8bf8376a591252c77addfb51a88467d688a00" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_TrafficCone_D.png", + "source_hash": "0c7cc2e937f2917f89359d629c1d7c74d2a91f0e", + "target_url": "./SubUSDs/textures/M_TrafficCone/T_TrafficCone_D.png", + "target_hash": "a1c946cc33fdbb0d21e3df863db8a4e179981ce7" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_TrafficCone_Stripes.png", + "source_hash": "4f31a7499c655b7160e52878830cd901a1d965e8", + "target_url": "./SubUSDs/textures/M_TrafficCone/T_TrafficCone_Stripes.png", + "target_hash": "25e356ee0f2a4598cc5b7aeec7283395e8870439" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_TrafficCone_ORM.png", + "source_hash": "5b70bcb5f458ab77b05f2f8ed0c65a87f48a75af", + "target_url": "./SubUSDs/textures/M_TrafficCone/T_TrafficCone_ORM.png", + "target_hash": "89b0d05e7a172b98572e11c29b383abddcd8dd9b" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/Alum_Anodized_roughness.png", + "source_hash": "4c556a400cb21ff2704ec582c7bdad3848bb1cfe", + "target_url": "./SubUSDs/textures/MI_RackShield_01/Alum_Anodized_roughness.png", + "target_hash": "e0147eaf7c818baf703ede16aa531c117d9cca07" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_AisleSign_N.png", + "source_hash": "22e3b819e0f0aaa363581d2f707dab369ce5107e", + "target_url": "./SubUSDs/textures/M_AisleSign/T_AisleSign_N.png", + "target_hash": "dc499020439c7e02a2de52f872bd73f15e873a2e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_AisleSign_D.png", + "source_hash": "cd3641d6cf30b036f1c166feefa98289e98bd3cb", + "target_url": "./SubUSDs/textures/M_AisleSign/T_AisleSign_D.png", + "target_hash": "417259aa9f324b12f91b71c9a1d00a32af6a6d38" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_AisleSign_ORM.png", + "source_hash": "3df6dddce4a35908fe4e944f449e6469dea7299d", + "target_url": "./SubUSDs/textures/M_AisleSign/T_AisleSign_ORM.png", + "target_hash": "5f5bb0a42dd01f501e3d1247e49137e65be10f8e" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallBoard_01_N.png", + "source_hash": "548216d0b5c6b5b9fcd23da2b63e6fdb6bc98f28", + "target_url": "./SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_N.png", + "target_hash": "af17d060fad97d474bb4e413a88e5e9517472682" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallBoard_01_D.png", + "source_hash": "0595541aba20495568e01caf9443f4397b4d791d", + "target_url": "./SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_D.png", + "target_hash": "6da727bab39607d9d998cfa5ef3f885676681114" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallBoard_01_ORM.png", + "source_hash": "aedc21a9781d3a74896e9ee5d4c4dcfdfc6381a5", + "target_url": "./SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_ORM.png", + "target_hash": "ae4db609809c4f87f3f0487739eabd0ead32b513" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_WallBoard_01_M.png", + "source_hash": "9109fae514ec802d056da19a351a73e670884e92", + "target_url": "./SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_M.png", + "target_hash": "9337209413d8525ce144d6ceb1f77cef87c70047" + }, + { + "source_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/Materials/Textures/T_PlasticWrap_N.png", + "source_hash": "4cf7fe28165136ac3847aa4bab544e8e882a59e9", + "target_url": "./SubUSDs/textures/MaterialInstanceDynamic_1220/T_PlasticWrap_N.png", + "target_hash": "cf39204691c044531ea6bd1fad1284fa464cf636" + } + ] +} \ No newline at end of file diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_Barcode_0001.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_Barcode_0001.mdl new file mode 100644 index 000000000..3b199bcbe --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_Barcode_0001.mdl @@ -0,0 +1,56 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_Barcode_0001( + uniform texture_2d BaseColor_Texture = texture_2d("../textures/0001.png",::tex::gamma_srgb) + [[sampler_color()]], + float4 BaseColor_Tint = float4(1.0,1.0,1.0,1.0), + float Metallic = 0.05, + float Roughness = 0.3) +[[ + dither_masked_off() +]] + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + + float3 Normal_mdl = float3(0.0,0.0,1.0); + + float4 Local0 = tex::lookup_float4(BaseColor_Texture,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local1 = (float3(Local0.x,Local0.y,Local0.z) * float3(BaseColor_Tint.x,BaseColor_Tint.y,BaseColor_Tint.z)); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = (Local0.w - 0.3333) < 0.0f ? 0.0f : 1.0f; + float3 BaseColor_mdl = Local1; + float Metallic_mdl = Metallic; + float Specular_mdl = 0.5; + float Roughness_mdl = Roughness; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_CeilingA_06b.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_CeilingA_06b.mdl new file mode 100644 index 000000000..785bc3518 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_CeilingA_06b.mdl @@ -0,0 +1,65 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_CeilingA_06b( + float4 MainTiling = float4(1.0,1.0,0.0,1.0), + uniform texture_2d MainNormalInput = texture_2d("../textures/T_BeamsA_N.png",::tex::gamma_linear) + [[sampler_normal()]], + float4 ColorAlbedo = float4(0.145,0.145,0.145,0.0), + uniform texture_2d AlbedoTexture = texture_2d("../textures/T_Floor_01_D.png",::tex::gamma_srgb) + [[sampler_color()]], + uniform texture_2d MaskSelection = texture_2d("../textures/T_BeamsA_M.png",::tex::gamma_linear) + [[sampler_masks()]], + uniform texture_2d MergeMapInput = texture_2d("../textures/T_BeamsA_ORM.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float2 Local0 = (float2(float3(MainTiling.x,MainTiling.y,MainTiling.z).x,float3(MainTiling.x,MainTiling.y,MainTiling.z).y) * CustomizedUV0_mdl); + float4 Local1 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local1.x,Local1.y,Local1.z); + + float4 Local2 = tex::lookup_float4(AlbedoTexture,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float4 Local3 = tex::lookup_float4(MaskSelection,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local4 = math::lerp(float3(ColorAlbedo.x,ColorAlbedo.y,ColorAlbedo.z),float3(Local2.x,Local2.y,Local2.z),float3(Local3.x,Local3.y,Local3.z)); + float4 Local5 = tex::lookup_float4(MergeMapInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float Local6 = math::lerp(RoughnessMin,RoughnessMax,float3(Local5.x,Local5.y,Local5.z).y); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local4; + float Metallic_mdl = float3(Local5.x,Local5.y,Local5.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local6; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_CratePlasticE_01.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_CratePlasticE_01.mdl new file mode 100644 index 000000000..cdc015d69 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_CratePlasticE_01.mdl @@ -0,0 +1,68 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_CratePlasticE_01( + uniform texture_2d MainNormalInput = texture_2d("../textures/T_BarelPlasticA_N.png",::tex::gamma_linear) + [[sampler_normal()]], + float4 Body = float4(0.128,0.128,0.128,1.0), + uniform texture_2d MaskSelection = texture_2d("../textures/T_BarelPlasticA_M.png",::tex::gamma_linear) + [[sampler_color()]], + float4 Handle = float4(0.128,0.128,0.128,1.0), + float4 Cap = float4(0.128,0.128,0.128,1.0), + uniform texture_2d AlbedoTexture = texture_2d("../textures/T_BarelPlasticA_D.png",::tex::gamma_srgb) + [[sampler_color()]], + uniform texture_2d MergeMapInput = texture_2d("../textures/T_BarelPlasticA_ORM.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float4 Local0 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local0.x,Local0.y,Local0.z); + + float4 Local1 = tex::lookup_float4(MaskSelection,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local2 = math::lerp(float3(0.0,0.0,0.0),float3(Body.x,Body.y,Body.z),Local1.x); + float3 Local3 = math::lerp(Local2,float3(Handle.x,Handle.y,Handle.z),Local1.y); + float3 Local4 = math::lerp(Local3,float3(Cap.x,Cap.y,Cap.z),Local1.z); + float4 Local5 = tex::lookup_float4(AlbedoTexture,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local6 = math::lerp(Local4,float3(Local5.x,Local5.y,Local5.z),Local1.w); + float4 Local7 = tex::lookup_float4(MergeMapInput,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float Local8 = math::lerp(RoughnessMin,RoughnessMax,float3(Local7.x,Local7.y,Local7.z).y); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local6; + float Metallic_mdl = float3(Local7.x,Local7.y,Local7.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local8; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_Floor_01.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_Floor_01.mdl new file mode 100644 index 000000000..2f23d7952 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_Floor_01.mdl @@ -0,0 +1,67 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_Floor_01( + float4 MainTiling = float4(1.0,1.0,0.0,1.0), + uniform texture_2d MainNormalInput = texture_2d("../textures/T_Floor_01_N.png",::tex::gamma_linear) + [[sampler_normal()]], + float4 MainNormalStrenght = float4(1.0,1.0,0.9,1.0), + float4 ColorAlbedo = float4(0.145,0.145,0.145,0.0), + uniform texture_2d AlbedoTexture = texture_2d("../textures/T_Floor_01_D.png",::tex::gamma_srgb) + [[sampler_color()]], + uniform texture_2d MaskSelection = texture_2d("../textures/T_Floor_01_M.png",::tex::gamma_linear) + [[sampler_masks()]], + uniform texture_2d MergeMapInput = texture_2d("../textures/T_Floor_01_ORM.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float2 Local0 = (float2(float3(MainTiling.x,MainTiling.y,MainTiling.z).x,float3(MainTiling.x,MainTiling.y,MainTiling.z).y) * CustomizedUV0_mdl); + float4 Local1 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat)); + float3 Local2 = (float3(Local1.x,Local1.y,Local1.z) * float3(MainNormalStrenght.x,MainNormalStrenght.y,MainNormalStrenght.z)); + + float3 Normal_mdl = Local2; + + float4 Local3 = tex::lookup_float4(AlbedoTexture,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float4 Local4 = tex::lookup_float4(MaskSelection,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local5 = math::lerp(float3(ColorAlbedo.x,ColorAlbedo.y,ColorAlbedo.z),float3(Local3.x,Local3.y,Local3.z),float3(Local4.x,Local4.y,Local4.z)); + float4 Local6 = tex::lookup_float4(MergeMapInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float Local7 = math::lerp(RoughnessMin,RoughnessMax,float3(Local6.x,Local6.y,Local6.z).y); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local5; + float Metallic_mdl = float3(Local6.x,Local6.y,Local6.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local7; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_FrameA_01.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_FrameA_01.mdl new file mode 100644 index 000000000..38c411481 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_FrameA_01.mdl @@ -0,0 +1,65 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_FrameA_01( + float4 MainTiling = float4(1.0,1.0,0.0,1.0), + uniform texture_2d MainNormalInput = texture_2d("../textures/T_BeamsA_N.png",::tex::gamma_linear) + [[sampler_normal()]], + float4 ColorAlbedo = float4(0.145,0.145,0.145,0.0), + uniform texture_2d AlbedoTexture = texture_2d("../textures/T_Floor_01_D.png",::tex::gamma_srgb) + [[sampler_color()]], + uniform texture_2d MaskSelection = texture_2d("../textures/T_BeamsA_M.png",::tex::gamma_linear) + [[sampler_masks()]], + uniform texture_2d MergeMapInput = texture_2d("../textures/T_BeamsA_ORM.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float2 Local0 = (float2(float3(MainTiling.x,MainTiling.y,MainTiling.z).x,float3(MainTiling.x,MainTiling.y,MainTiling.z).y) * CustomizedUV0_mdl); + float4 Local1 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local1.x,Local1.y,Local1.z); + + float4 Local2 = tex::lookup_float4(AlbedoTexture,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float4 Local3 = tex::lookup_float4(MaskSelection,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local4 = math::lerp(float3(ColorAlbedo.x,ColorAlbedo.y,ColorAlbedo.z),float3(Local2.x,Local2.y,Local2.z),float3(Local3.x,Local3.y,Local3.z)); + float4 Local5 = tex::lookup_float4(MergeMapInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float Local6 = math::lerp(RoughnessMin,RoughnessMax,float3(Local5.x,Local5.y,Local5.z).y); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local4; + float Metallic_mdl = float3(Local5.x,Local5.y,Local5.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local6; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_LampCeilingA.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_LampCeilingA.mdl new file mode 100644 index 000000000..ee7185270 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_LampCeilingA.mdl @@ -0,0 +1,66 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_LampCeilingA( + float U_Tiling = 1.0, + float V_Tiling = 1.0, + uniform texture_2d MainNormalInput = texture_2d("../textures/T_Floor_01_N.png",::tex::gamma_linear) + [[sampler_normal()]], + uniform texture_2d AlbedoTexture = texture_2d("../textures/T_Floor_01_D.png",::tex::gamma_srgb) + [[sampler_color()]], + float Desaturation = 0.0, + float4 BaseColor_Tint = float4(1.0,1.0,1.0,1.0), + uniform texture_2d MergeMapInput = texture_2d("../textures/MI_RackShield_01/Alum_Anodized_roughness.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float2 Local0 = (CustomizedUV0_mdl * float2(U_Tiling,V_Tiling)); + float4 Local1 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local1.x,Local1.y,Local1.z); + + float4 Local2 = tex::lookup_float4(AlbedoTexture,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float Local3 = math::dot(float3(Local2.x,Local2.y,Local2.z), float3(0.3,0.59,0.11)); + float3 Local4 = math::lerp(float3(Local2.x,Local2.y,Local2.z),float3(Local3,Local3,Local3),Desaturation); + float3 Local5 = (Local4 * float3(BaseColor_Tint.x,BaseColor_Tint.y,BaseColor_Tint.z)); + float4 Local6 = tex::lookup_float4(MergeMapInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float Local7 = math::lerp(RoughnessMin,RoughnessMax,float3(Local6.x,Local6.y,Local6.z).y); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local5; + float Metallic_mdl = float3(Local6.x,Local6.y,Local6.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local7; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_PaperNotes_01.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_PaperNotes_01.mdl new file mode 100644 index 000000000..a5dbeb836 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_PaperNotes_01.mdl @@ -0,0 +1,51 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_PaperNotes_01( + float4 ColorAlbedo = float4(1.0,0.92926,0.86,1.0)) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + + float3 Normal_mdl = float3(0.0,0.0,1.0); + + float4 Local0 = tex::lookup_float4(texture_2d("../textures/MI_PaperNotes_01/T_PaperNotes_D.png",::tex::gamma_srgb),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local1 = (float3(ColorAlbedo.x,ColorAlbedo.y,ColorAlbedo.z) * float3(Local0.x,Local0.y,Local0.z)); + float4 Local2 = tex::lookup_float4(texture_2d("../textures/MI_PaperNotes_01/T_PaperNotes_M.png",::tex::gamma_linear),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local3 = math::lerp(Local1,float3(Local0.x,Local0.y,Local0.z),float3(Local2.x,Local2.y,Local2.z)); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local3; + float Metallic_mdl = 0.0; + float Specular_mdl = 0.5; + float Roughness_mdl = 0.7; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: true); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_PushcartA_01.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_PushcartA_01.mdl new file mode 100644 index 000000000..48af8232b --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_PushcartA_01.mdl @@ -0,0 +1,68 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_PushcartA_01( + uniform texture_2d MainNormalInput = texture_2d("../textures/T_BarelPlasticA_N.png",::tex::gamma_linear) + [[sampler_normal()]], + float4 Body = float4(0.128,0.128,0.128,1.0), + uniform texture_2d MaskSelection = texture_2d("../textures/T_BarelPlasticA_M.png",::tex::gamma_linear) + [[sampler_color()]], + float4 Handle = float4(0.128,0.128,0.128,1.0), + float4 Cap = float4(0.128,0.128,0.128,1.0), + uniform texture_2d AlbedoTexture = texture_2d("../textures/T_BarelPlasticA_D.png",::tex::gamma_srgb) + [[sampler_color()]], + uniform texture_2d MergeMapInput = texture_2d("../textures/T_BarelPlasticA_ORM.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float4 Local0 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local0.x,Local0.y,Local0.z); + + float4 Local1 = tex::lookup_float4(MaskSelection,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local2 = math::lerp(float3(0.0,0.0,0.0),float3(Body.x,Body.y,Body.z),Local1.x); + float3 Local3 = math::lerp(Local2,float3(Handle.x,Handle.y,Handle.z),Local1.y); + float3 Local4 = math::lerp(Local3,float3(Cap.x,Cap.y,Cap.z),Local1.z); + float4 Local5 = tex::lookup_float4(AlbedoTexture,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local6 = math::lerp(Local4,float3(Local5.x,Local5.y,Local5.z),Local1.w); + float4 Local7 = tex::lookup_float4(MergeMapInput,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float Local8 = math::lerp(RoughnessMin,RoughnessMax,float3(Local7.x,Local7.y,Local7.z).y); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local6; + float Metallic_mdl = float3(Local7.x,Local7.y,Local7.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local8; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_RackShield_01.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_RackShield_01.mdl new file mode 100644 index 000000000..aead5a7b5 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_RackShield_01.mdl @@ -0,0 +1,66 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_RackShield_01( + float U_Tiling = 1.0, + float V_Tiling = 1.0, + uniform texture_2d MainNormalInput = texture_2d("../textures/T_Floor_01_N.png",::tex::gamma_linear) + [[sampler_normal()]], + uniform texture_2d AlbedoTexture = texture_2d("../textures/T_Floor_01_D.png",::tex::gamma_srgb) + [[sampler_color()]], + float Desaturation = 0.0, + float4 BaseColor_Tint = float4(1.0,1.0,1.0,1.0), + uniform texture_2d MergeMapInput = texture_2d("../textures/MI_RackShield_01/Alum_Anodized_roughness.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float2 Local0 = (CustomizedUV0_mdl * float2(U_Tiling,V_Tiling)); + float4 Local1 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local1.x,Local1.y,Local1.z); + + float4 Local2 = tex::lookup_float4(AlbedoTexture,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float Local3 = math::dot(float3(Local2.x,Local2.y,Local2.z), float3(0.3,0.59,0.11)); + float3 Local4 = math::lerp(float3(Local2.x,Local2.y,Local2.z),float3(Local3,Local3,Local3),Desaturation); + float3 Local5 = (Local4 * float3(BaseColor_Tint.x,BaseColor_Tint.y,BaseColor_Tint.z)); + float4 Local6 = tex::lookup_float4(MergeMapInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float Local7 = math::lerp(RoughnessMin,RoughnessMax,float3(Local6.x,Local6.y,Local6.z).y); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local5; + float Metallic_mdl = float3(Local6.x,Local6.y,Local6.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local7; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_SignB.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_SignB.mdl new file mode 100644 index 000000000..36a839b2a --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_SignB.mdl @@ -0,0 +1,49 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_SignB( + uniform texture_2d TextureSelection = texture_2d("../textures/MI_SignB/T_SignsA_D.png",::tex::gamma_srgb) + [[sampler_color()]]) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + + float3 Normal_mdl = float3(0.0,0.0,1.0); + + float4 Local0 = tex::lookup_float4(TextureSelection,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = float3(Local0.x,Local0.y,Local0.z); + float Metallic_mdl = 0.0; + float Specular_mdl = 0.2; + float Roughness_mdl = 0.125; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_WallB_01.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_WallB_01.mdl new file mode 100644 index 000000000..23d4d6b11 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MI_WallB_01.mdl @@ -0,0 +1,67 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MI_WallB_01( + float4 MainTiling = float4(1.0,1.0,0.0,1.0), + uniform texture_2d MainNormalInput = texture_2d("../textures/T_Floor_01_N.png",::tex::gamma_linear) + [[sampler_normal()]], + float4 MainNormalStrenght = float4(1.0,1.0,0.9,1.0), + float4 ColorAlbedo = float4(0.145,0.145,0.145,0.0), + uniform texture_2d AlbedoTexture = texture_2d("../textures/T_Floor_01_D.png",::tex::gamma_srgb) + [[sampler_color()]], + uniform texture_2d MaskSelection = texture_2d("../textures/T_Floor_01_M.png",::tex::gamma_linear) + [[sampler_masks()]], + uniform texture_2d MergeMapInput = texture_2d("../textures/T_Floor_01_ORM.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float2 Local0 = (float2(float3(MainTiling.x,MainTiling.y,MainTiling.z).x,float3(MainTiling.x,MainTiling.y,MainTiling.z).y) * CustomizedUV0_mdl); + float4 Local1 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat)); + float3 Local2 = (float3(Local1.x,Local1.y,Local1.z) * float3(MainNormalStrenght.x,MainNormalStrenght.y,MainNormalStrenght.z)); + + float3 Normal_mdl = Local2; + + float4 Local3 = tex::lookup_float4(AlbedoTexture,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float4 Local4 = tex::lookup_float4(MaskSelection,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local5 = math::lerp(float3(ColorAlbedo.x,ColorAlbedo.y,ColorAlbedo.z),float3(Local3.x,Local3.y,Local3.z),float3(Local4.x,Local4.y,Local4.z)); + float4 Local6 = tex::lookup_float4(MergeMapInput,float2(Local0.x,1.0-Local0.y),tex::wrap_repeat,tex::wrap_repeat); + float Local7 = math::lerp(RoughnessMin,RoughnessMax,float3(Local6.x,Local6.y,Local6.z).y); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local5; + float Metallic_mdl = float3(Local6.x,Local6.y,Local6.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local7; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_AisleSign.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_AisleSign.mdl new file mode 100644 index 000000000..e3a787cd8 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_AisleSign.mdl @@ -0,0 +1,62 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material M_AisleSign( + uniform texture_2d Text = texture_2d("../textures/AisleSign_Text_01.png",::tex::gamma_linear) + [[sampler_color()]]) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float4 Local0 = ::unpack_normal_map(tex::lookup_float4(texture_2d("../textures/M_AisleSign/T_AisleSign_N.png",::tex::gamma_linear),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local0.x,Local0.y,Local0.z); + + float4 Local1 = tex::lookup_float4(Text,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local2 = (1.0 - float3(Local1.x,Local1.y,Local1.z)); + float3 Local3 = (Local2 * 2.0); + float4 Local4 = tex::lookup_float4(texture_2d("../textures/M_AisleSign/T_AisleSign_D.png",::tex::gamma_srgb),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local5 = (1.0 - float3(Local4.x,Local4.y,Local4.z)); + float3 Local6 = (Local3 * Local5); + float3 Local7 = (1.0 - Local6); + float3 Local8 = (float3(Local1.x,Local1.y,Local1.z) * 2.0); + float3 Local9 = (Local8 * float3(Local4.x,Local4.y,Local4.z)); + float Local10 = ((float3(Local1.x,Local1.y,Local1.z).x >= 0.5) ? Local7.x : Local9.x); + float Local11 = ((float3(Local1.x,Local1.y,Local1.z).y >= 0.5) ? Local7.y : Local9.y); + float Local12 = ((float3(Local1.x,Local1.y,Local1.z).z >= 0.5) ? Local7.z : Local9.z); + float4 Local13 = tex::lookup_float4(texture_2d("../textures/M_AisleSign/T_AisleSign_ORM.png",::tex::gamma_linear),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = float3(float2(Local10,Local11).x,float2(Local10,Local11).y,Local12); + float Metallic_mdl = Local13.z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local13.y; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_Glow.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_Glow.mdl new file mode 100644 index 000000000..ca5e7b0cb --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_Glow.mdl @@ -0,0 +1,48 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material M_Glow( + float4 EmissiveColor = float4(0.28835,0.365,0.365,1.0), + float EmissiveStrength = 10.0) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + + + float3 Normal_mdl = float3(0.0,0.0,1.0); + + float3 Local0 = (float3(EmissiveColor.x,EmissiveColor.y,EmissiveColor.z) * EmissiveStrength); + + float3 EmissiveColor_mdl = Local0; + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = float3(0.0,0.0,0.0); + float Metallic_mdl = 0.0; + float Specular_mdl = 0.5; + float Roughness_mdl = 0.5; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_TrafficCone.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_TrafficCone.mdl new file mode 100644 index 000000000..bff31b691 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_TrafficCone.mdl @@ -0,0 +1,57 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material M_TrafficCone( + float Param = 5.0) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float4 Local0 = ::unpack_normal_map(tex::lookup_float4(texture_2d("../textures/M_TrafficCone/T_TrafficCone_N.png",::tex::gamma_linear),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local0.x,Local0.y,Local0.z); + + float4 Local1 = tex::lookup_float4(texture_2d("../textures/M_TrafficCone/T_TrafficCone_D.png",::tex::gamma_srgb),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float4 Local2 = tex::lookup_float4(texture_2d("../textures/M_TrafficCone/T_TrafficCone_Stripes.png",::tex::gamma_linear),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local3 = (float3(Local1.x,Local1.y,Local1.z) * float3(Local2.x,Local2.y,Local2.z)); + float3 Local4 = (Local3 * Param); + float3 Local5 = ::transform_vector_from_tangent_to_world(float3(float3(Local0.x,Local0.y,Local0.z).x,float3(Local0.x,Local0.y,Local0.z).y,float3(Local0.x,Local0.y,Local0.z).z)); + float Local6 = ::fresnel(0.05, 0.1, Local5); + float Local7 = (1.0 - Local6); + float3 Local8 = (Local4 * Local7); + float4 Local9 = tex::lookup_float4(texture_2d("../textures/M_TrafficCone/T_TrafficCone_ORM.png",::tex::gamma_linear),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + + float3 EmissiveColor_mdl = Local8; + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = float3(Local1.x,Local1.y,Local1.z); + float Metallic_mdl = Local9.z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local9.y; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_WallBoard_01.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_WallBoard_01.mdl new file mode 100644 index 000000000..65a3f9749 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_WallBoard_01.mdl @@ -0,0 +1,64 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material M_WallBoard_01( + uniform texture_2d MainNormalInput = texture_2d("../textures/M_WallBoard_01/T_WallBoard_01_N.png",::tex::gamma_linear) + [[sampler_normal()]], + uniform texture_2d AlbedoTexture = texture_2d("../textures/M_WallBoard_01/T_WallBoard_01_D.png",::tex::gamma_srgb) + [[sampler_color()]], + uniform texture_2d MergeMapInput = texture_2d("../textures/M_WallBoard_01/T_WallBoard_01_ORM.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.1, + float RoughnessMax = 0.9, + uniform texture_2d AlphaSelection = texture_2d("../textures/M_WallBoard_01/T_WallBoard_01_M.png",::tex::gamma_linear) + [[sampler_alpha()]]) +[[ + dither_masked_off() +]] + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float4 Local0 = ::unpack_normal_map(tex::lookup_float4(MainNormalInput,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local0.x,Local0.y,Local0.z); + + float4 Local1 = tex::lookup_float4(AlbedoTexture,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float4 Local2 = tex::lookup_float4(MergeMapInput,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float Local3 = math::lerp(RoughnessMin,RoughnessMax,float3(Local2.x,Local2.y,Local2.z).y); + float4 Local4 = ::greyscale_texture_lookup(tex::lookup_float4(AlphaSelection,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = (float3(Local4.x,Local4.y,Local4.z).x - 0.3333) < 0.0f ? 0.0f : 1.0f; + float3 BaseColor_mdl = float3(Local1.x,Local1.y,Local1.z); + float Metallic_mdl = float3(Local2.x,Local2.y,Local2.z).z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local3; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: true); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_WetFloorSign.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_WetFloorSign.mdl new file mode 100644 index 000000000..7b3b0f68e --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/M_WetFloorSign.mdl @@ -0,0 +1,50 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material M_WetFloorSign( +) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float4 Local0 = ::unpack_normal_map(tex::lookup_float4(texture_2d("../textures/M_WetFloorSign/T_WetFloorSign_N.png",::tex::gamma_linear),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat)); + + float3 Normal_mdl = float3(Local0.x,Local0.y,Local0.z); + + float4 Local1 = tex::lookup_float4(texture_2d("../textures/M_WetFloorSign/T_WetFloorSign_D.png",::tex::gamma_srgb),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float4 Local2 = tex::lookup_float4(texture_2d("../textures/M_WetFloorSign/T_WetFloorSign_ORM.png",::tex::gamma_linear),float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = float3(Local1.x,Local1.y,Local1.z); + float Metallic_mdl = Local2.z; + float Specular_mdl = 0.5; + float Roughness_mdl = Local2.y; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MaterialInstanceDynamic_1220.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MaterialInstanceDynamic_1220.mdl new file mode 100644 index 000000000..135c5865e --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/MaterialInstanceDynamic_1220.mdl @@ -0,0 +1,80 @@ +mdl 1.3; + +import ::math::*; +import ::state::*; +import ::tex::*; +import ::anno::*; +using OmniUe4Function import *; +using OmniUe4Base import *; + +export annotation sampler_color(); +export annotation sampler_normal(); +export annotation sampler_grayscale(); +export annotation sampler_alpha(); +export annotation sampler_masks(); +export annotation sampler_distancefield(); +export annotation dither_masked_off(); +export annotation world_space_normal(); + +export material MaterialInstanceDynamic_1220( + uniform texture_2d NormalMap_Box = texture_2d("../textures/T_CardBoxA_N.png",::tex::gamma_linear) + [[sampler_normal()]], + float U_Tiling = 4.0, + float V_Tiling = 4.0, + float PlasticNormalAlpha = 0.75, + uniform texture_2d BaseColor_Box = texture_2d("../textures/T_CardBoxA_D.png",::tex::gamma_srgb) + [[sampler_color()]], + float4 BaseColorBox_Tint = float4(1.0,1.0,1.0,1.0), + uniform texture_2d BaseColor_Plastic = texture_2d("../textures/T_PlasticWrap_D.png",::tex::gamma_srgb) + [[sampler_color()]], + float4 BaseColorPlastic_Tint = float4(0.16,0.19,0.2,1.0), + float PlasticOpacity = 0.45, + uniform texture_2d MultiMap_Plastic = texture_2d("../textures/T_PlasticWrap_ORM.png",::tex::gamma_linear) + [[sampler_color()]], + float RoughnessMin = 0.0, + float RoughnessMax = 0.05, + uniform texture_2d MultiMap_Box = texture_2d("../textures/T_CardBoxA_ORM.png",::tex::gamma_linear) + [[sampler_color()]]) + = + let { + float3 WorldPositionOffset_mdl = float3(0.0,0.0,0.0); + float2 CustomizedUV0_mdl = float2(state::texture_coordinate(0).x,1.0-state::texture_coordinate(0).y); + + float4 Local0 = ::unpack_normal_map(tex::lookup_float4(NormalMap_Box,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat)); + float2 Local1 = (CustomizedUV0_mdl * float2(U_Tiling,V_Tiling)); + float4 Local2 = ::unpack_normal_map(tex::lookup_float4(texture_2d("../textures/MaterialInstanceDynamic_1220/T_PlasticWrap_N.png",::tex::gamma_linear),float2(Local1.x,1.0-Local1.y),tex::wrap_repeat,tex::wrap_repeat)); + float3 Local3 = math::lerp(float3(Local0.x,Local0.y,Local0.z),float3(Local2.x,Local2.y,Local2.z),PlasticNormalAlpha); + + float3 Normal_mdl = Local3; + + float4 Local4 = tex::lookup_float4(BaseColor_Box,float2(CustomizedUV0_mdl.x,1.0-CustomizedUV0_mdl.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local5 = (float3(Local4.x,Local4.y,Local4.z) * float3(BaseColorBox_Tint.x,BaseColorBox_Tint.y,BaseColorBox_Tint.z)); + float4 Local6 = tex::lookup_float4(BaseColor_Plastic,float2(Local1.x,1.0-Local1.y),tex::wrap_repeat,tex::wrap_repeat); + float3 Local7 = (float3(Local6.x,Local6.y,Local6.z) * float3(BaseColorPlastic_Tint.x,BaseColorPlastic_Tint.y,BaseColorPlastic_Tint.z)); + float Local8 = (1.0 - Local2.z); + float Local9 = (PlasticOpacity + Local8); + float Local10 = math::min(math::max(Local9,0.0),1.0); + float3 Local11 = math::lerp(Local5,Local7,Local10); + float4 Local12 = tex::lookup_float4(MultiMap_Plastic,float2(Local1.x,1.0-Local1.y),tex::wrap_repeat,tex::wrap_repeat); + float Local13 = math::min(math::max(Local12.y,RoughnessMin),RoughnessMax); + float4 Local14 = tex::lookup_float4(MultiMap_Box,float2(Local1.x,1.0-Local1.y),tex::wrap_repeat,tex::wrap_repeat); + + + float3 EmissiveColor_mdl = float3(0.0,0.0,0.0); + float OpacityMask_mdl = 1.0; + float3 BaseColor_mdl = Local11; + float Metallic_mdl = Local12.z + Local14.z * 0; + float Specular_mdl = 0.5; + float Roughness_mdl = Local13; + + } in + ::OmniUe4Base( + base_color: BaseColor_mdl, + metallic: Metallic_mdl, + roughness: Roughness_mdl, + specular: Specular_mdl, + normal: Normal_mdl, + opacity: OpacityMask_mdl, + emissive_color: EmissiveColor_mdl, + displacement: WorldPositionOffset_mdl, + two_sided: false); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/OmniUe4Base.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/OmniUe4Base.mdl new file mode 100644 index 000000000..fa6a0729f --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/OmniUe4Base.mdl @@ -0,0 +1,195 @@ +/*************************************************************************************************** + * Copyright 2020 NVIDIA Corporation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of NVIDIA CORPORATION nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************************************/ + +//* 1.0.0 - first version +//* 1.0.1 - merge unlit template +//* 1.0.2 - Fix EDF in the back side: the EDF contained in surface is only used for the front side and not for the back side +//* 1.0.3 - UE4 normal mapping: Geometry normal shouldn't be changed +//* 1.0.4 - using absolute import paths when importing standard modules + +mdl 1.3; + +import ::df::*; +import ::state::*; +import ::math::*; +import ::tex::*; +import ::anno::*; + +float emissive_multiplier() +[[ + anno::description("the multiplier to convert UE4 emissive to raw data"), + anno::noinline() +]] +{ + return 20.0f * 128.0f; +} + +float3 tangent_space_normal( + float3 normal = float3(0.0,0.0,1.0), + float3 tangent_u = state::texture_tangent_u(0), + float3 tangent_v = state::texture_tangent_v(0) +) +[[ + anno::description("Interprets the vector in tangent space"), + anno::noinline() +]] +{ + return math::normalize( + tangent_u * normal.x - /* flip_tangent_v */ + tangent_v * normal.y + + state::normal() * (normal.z)); +} + +export material OmniUe4Base( + float3 base_color = float3(0.0, 0.0, 0.0), + float metallic = 0.0, + float roughness = 0.5, + float specular = 0.5, + float3 normal = float3(0.0,0.0,1.0), + float clearcoat_weight = 0.0, + float clearcoat_roughness = 0.0, + float3 clearcoat_normal = float3(0.0,0.0,1.0), + uniform bool enable_opacity = true, + float opacity = 1.0, + float3 emissive_color = float3(0.0, 0.0, 0.0), + float3 displacement = float3(0.0), + uniform bool is_tangent_space_normal = true, + uniform bool two_sided = false, + uniform bool is_unlit = false +) +[[ + anno::display_name("Omni UE4 Base"), + anno::description("Omni UE4 Base, supports UE4 default lit and clearcoat shading model"), + anno::version( 1, 0, 0), + anno::author("NVIDIA CORPORATION"), + anno::key_words(string[]("omni", "UE4", "omniverse", "lit", "clearcoat", "generic")) +]] + = let { + color final_base_color = math::saturate(base_color); + float final_metallic = math::saturate(metallic); + float final_roughness = math::saturate(roughness); + float final_specular = math::saturate(specular); + color final_emissive_color = math::max(emissive_color, 0.0f) * emissive_multiplier(); /*factor for converting ue4 emissive to raw value*/ + float final_clearcoat_weight = math::saturate(clearcoat_weight); + float final_clearcoat_roughness = math::saturate(clearcoat_roughness); + float3 final_normal = math::normalize(normal); + float3 final_clearcoat_normal = math::normalize(clearcoat_normal); + + // - compute final roughness by squaring the "roughness" parameter + float alpha = final_roughness * final_roughness; + // reduce the reflectivity at grazing angles to avoid "dark edges" for high roughness due to the layering + float grazing_refl = math::max((1.0 - final_roughness), 0.0); + + float3 the_normal = is_unlit ? state::normal() : + (is_tangent_space_normal ? tangent_space_normal( + normal: final_normal, + tangent_u: state::texture_tangent_u(0), + tangent_v: state::texture_tangent_v(0) + ) : final_normal); + + // for the dielectric component we layer the glossy component on top of the diffuse one, + // the glossy layer has no color tint + + bsdf dielectric_component = df::custom_curve_layer( + weight: final_specular, + normal_reflectivity: 0.08, + grazing_reflectivity: grazing_refl, + layer: df::microfacet_ggx_smith_bsdf(roughness_u: alpha), + base: df::diffuse_reflection_bsdf(tint: final_base_color), + normal: the_normal); + + // the metallic component doesn't have a diffuse component, it's only glossy + // base_color is applied to tint it + bsdf metallic_component = df::microfacet_ggx_smith_bsdf(tint: final_base_color, roughness_u: alpha); + + // final BSDF is a linear blend between dielectric and metallic component + bsdf dielectric_metal_mix = + df::normalized_mix( + components: + df::bsdf_component[]( + df::bsdf_component( + component: metallic_component, + weight: final_metallic), + df::bsdf_component( + component: dielectric_component, + weight: 1.0-final_metallic) + ) + ); + + // clearcoat layer + float clearcoat_grazing_refl = math::max((1.0 - final_clearcoat_roughness), 0.0); + float clearcoat_alpha = final_clearcoat_roughness * final_clearcoat_roughness; + + float3 the_clearcoat_normal = is_tangent_space_normal ? tangent_space_normal( + normal: final_clearcoat_normal, + tangent_u: state::texture_tangent_u(0), + tangent_v: state::texture_tangent_v(0) + ) : final_clearcoat_normal; + + + bsdf clearcoat = + df::custom_curve_layer( + base: df::weighted_layer( + layer: dielectric_metal_mix, + weight: 1.0, + normal: final_clearcoat_weight == 0.0 ? state::normal() : the_normal + ), + layer: df::microfacet_ggx_smith_bsdf( + roughness_u: clearcoat_alpha, + tint: color(1.0) + ), + normal_reflectivity: 0.04, + grazing_reflectivity: clearcoat_grazing_refl, + normal: the_clearcoat_normal, + weight: final_clearcoat_weight + ); + bsdf surface = is_unlit ? bsdf() : clearcoat; +} +in material( + thin_walled: two_sided, // Graphene? + surface: material_surface( + scattering: surface, + emission: + material_emission ( + emission: df::diffuse_edf (), + intensity: final_emissive_color + ) + ), + backface: material_surface( + emission: + material_emission ( + emission: df::diffuse_edf (), + intensity: final_emissive_color + ) + ), + geometry: material_geometry( + displacement: displacement, + normal: final_clearcoat_weight == 0.0 ? the_normal : state::normal(), + cutout_opacity: enable_opacity ? opacity : 1.0 + ) +); diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/OmniUe4Function.mdl b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/OmniUe4Function.mdl new file mode 100644 index 000000000..dd9f564d2 --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/materials/OmniUe4Function.mdl @@ -0,0 +1,1163 @@ +/*************************************************************************************************** + * Copyright 2020 NVIDIA Corporation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of NVIDIA CORPORATION nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************************************/ + +//* 1.0.1 - using absolute import paths when importing standard modules + +mdl 1.3; + +import ::df::*; +import ::state::*; +import ::math::*; +import ::tex::*; +import ::anno::*; + + +export float3x3 matrix_inverse(float3x3 matrix) +[[ + anno::description("Inverse the 3x3 matrix"), + anno::noinline() +]] +{ + float determinant = (matrix[0][0] * matrix[1][1] * matrix[2][2] + matrix[1][0] * matrix[2][1] * matrix[0][2] + matrix[2][0] * matrix[0][1] * matrix[1][2]) - (matrix[0][2] * matrix[1][1] * matrix[2][0] + matrix[1][2] * matrix[2][1] * matrix[0][0] + matrix[2][2] * matrix[0][1] * matrix[1][0]); + float rdet = 1.0f / determinant; + + float3x3 result; + + result[0][0] = rdet * (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1]); + result[0][1] = -rdet * (matrix[0][1] * matrix[2][2] - matrix[0][2] * matrix[2][1]); + result[0][2] = rdet * (matrix[0][1] * matrix[1][2] - matrix[0][2] * matrix[1][1]); + + result[1][0] = -rdet * (matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0]); + result[1][1] = rdet * (matrix[0][0] * matrix[2][2] - matrix[0][2] * matrix[2][0]); + result[1][2] = -rdet * (matrix[0][0] * matrix[1][2] - matrix[0][2] * matrix[1][0]); + + result[2][0] = rdet * (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]); + result[2][1] = -rdet * (matrix[0][0] * matrix[2][1] - matrix[0][1] * matrix[2][0]); + result[2][2] = rdet * (matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]); + + return result; +} + +export float3 transform_vector_from_tangent_to_world(float3 vector) +[[ + anno::description("Transform vector from tangent space to world space"), + anno::noinline() +]] +{ + /* flip_tangent_v */ + float3x3 tangent_to_world = float3x3(state::texture_tangent_u(0), -state::texture_tangent_v(0), state::normal()); + return tangent_to_world * vector; +} + +export float3 transform_vector_from_world_to_tangent(float3 vector) +[[ + anno::description("Transform vector from world space to tangent space"), + anno::noinline() +]] +{ + /* flip_tangent_v */ + float3x3 tangent_to_world = float3x3(state::texture_tangent_u(0), -state::texture_tangent_v(0), state::normal()); + + // inverse tangent to world matrix + float3x3 world_to_tangent = matrix_inverse(tangent_to_world); + + return world_to_tangent * vector; +} + +export float4 unpack_normal_map( + float4 texture_sample = float4(0.0, 0.0, 1.0, 1.0) + ) +[[ + anno::description("Unpack a normal stored in a normal map"), + anno::noinline() +]] +{ + float2 normal_xy = float2(texture_sample.x, texture_sample.y); + + normal_xy = normal_xy * float2(2.0,2.0) - float2(1.0,1.0); + float normal_z = math::sqrt( math::saturate( 1.0 - math::dot( normal_xy, normal_xy ) ) ); + return float4( normal_xy.x, normal_xy.y, normal_z, 1.0 ); +} + +// for get color value from normal. +export float4 pack_normal_map( + float4 texture_sample = float4(0.0, 0.0, 1.0, 1.0) + ) +[[ + anno::description("Pack to color from a normal") +]] +{ + float2 return_xy = float2(texture_sample.x, texture_sample.y); + + return_xy = (return_xy + float2(1.0,1.0)) / float2(2.0,2.0); + + return float4( return_xy.x, return_xy.y, 0.0, 1.0 ); +} + +export float4 greyscale_texture_lookup( + float4 texture_sample = float4(0.0, 0.0, 0.0, 1.0) + ) +[[ + anno::description("Sampling a greyscale texture"), + anno::noinline() +]] +{ + return float4(texture_sample.x, texture_sample.x, texture_sample.x, texture_sample.x); +} + +export float3 pixel_normal_world_space() +[[ + anno::description("Pixel normal in world space"), + anno::noinline() +]] +{ + return state::transform_normal(state::coordinate_internal,state::coordinate_world,state::normal()); +} + +export float3 vertex_normal_world_space() +[[ + anno::description("Vertex normal in world space"), + anno::noinline() +]] +{ + return state::transform_normal(state::coordinate_internal,state::coordinate_world,state::normal()); +} + +export float3 landscape_normal_world_space() +[[ + anno::description("Landscape normal in world space") +]] +{ + float3 normalFromNormalmap = math::floor((::vertex_normal_world_space() * 0.5 + 0.5) * 255.0) / 255.0 * 2.0 - 1.0; + + float2 normalXY = float2(normalFromNormalmap.x, normalFromNormalmap.y); + return float3(normalXY.x, normalXY.y, math::sqrt(math::saturate(1.0 - math::dot(normalXY, normalXY)))); +} + +// Different implementation specific between mdl and hlsl for smoothstep +export float smoothstep(float a, float b, float l) +{ + if (a < b) + { + return math::smoothstep(a, b, l); + } + else if (a > b) + { + return 1.0 - math::smoothstep(b, a, l); + } + else + { + return l <= a ? 0.0 : 1.0; + } +} + +export float2 smoothstep(float2 a, float2 b, float2 l) +{ + return float2(smoothstep(a.x, b.x, l.x), smoothstep(a.y, b.y, l.y)); +} + +export float3 smoothstep(float3 a, float3 b, float3 l) +{ + return float3(smoothstep(a.x, b.x, l.x), smoothstep(a.y, b.y, l.y), smoothstep(a.z, b.z, l.z)); +} + +export float4 smoothstep(float4 a, float4 b, float4 l) +{ + return float4(smoothstep(a.x, b.x, l.x), smoothstep(a.y, b.y, l.y), smoothstep(a.z, b.z, l.z), smoothstep(a.w, b.w, l.w)); +} + +export float2 smoothstep(float2 a, float2 b, float l) +{ + return float2(smoothstep(a.x, b.x, l), smoothstep(a.y, b.y, l)); +} + +export float3 smoothstep(float3 a, float3 b, float l) +{ + return float3(smoothstep(a.x, b.x, l), smoothstep(a.y, b.y, l), smoothstep(a.z, b.z, l)); +} + +export float4 smoothstep(float4 a, float4 b, float l) +{ + return float4(smoothstep(a.x, b.x, l), smoothstep(a.y, b.y, l), smoothstep(a.z, b.z, l), smoothstep(a.w, b.w, l)); +} + +//------------------ Random from UE4 ----------------------- +float length2(float3 v) +{ + return math::dot(v, v); +} + +float3 GetPerlinNoiseGradientTextureAt(uniform texture_2d PerlinNoiseGradientTexture, float3 v) +{ + const float2 ZShear = float2(17.0f, 89.0f); + + float2 OffsetA = v.z * ZShear; + float2 TexA = (float2(v.x, v.y) + OffsetA + 0.5f) / 128.0f; + float4 PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexA.x,1.0-TexA.y),tex::wrap_repeat,tex::wrap_repeat); + return float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z) * 2.0 - 1.0; +} + +float3 SkewSimplex(float3 In) +{ + return In + math::dot(In, float3(1.0 / 3.0f) ); +} +float3 UnSkewSimplex(float3 In) +{ + return In - math::dot(In, float3(1.0 / 6.0f) ); +} + +// 3D random number generator inspired by PCGs (permuted congruential generator) +// Using a **simple** Feistel cipher in place of the usual xor shift permutation step +// @param v = 3D integer coordinate +// @return three elements w/ 16 random bits each (0-0xffff). +// ~8 ALU operations for result.x (7 mad, 1 >>) +// ~10 ALU operations for result.xy (8 mad, 2 >>) +// ~12 ALU operations for result.xyz (9 mad, 3 >>) + +//TODO: uint3 +int3 Rand3DPCG16(int3 p) +{ + // taking a signed int then reinterpreting as unsigned gives good behavior for negatives + //TODO: uint3 + int3 v = int3(p); + + // Linear congruential step. These LCG constants are from Numerical Recipies + // For additional #'s, PCG would do multiple LCG steps and scramble each on output + // So v here is the RNG state + v = v * 1664525 + 1013904223; + + // PCG uses xorshift for the final shuffle, but it is expensive (and cheap + // versions of xorshift have visible artifacts). Instead, use simple MAD Feistel steps + // + // Feistel ciphers divide the state into separate parts (usually by bits) + // then apply a series of permutation steps one part at a time. The permutations + // use a reversible operation (usually ^) to part being updated with the result of + // a permutation function on the other parts and the key. + // + // In this case, I'm using v.x, v.y and v.z as the parts, using + instead of ^ for + // the combination function, and just multiplying the other two parts (no key) for + // the permutation function. + // + // That gives a simple mad per round. + v.x += v.y*v.z; + v.y += v.z*v.x; + v.z += v.x*v.y; + v.x += v.y*v.z; + v.y += v.z*v.x; + v.z += v.x*v.y; + + // only top 16 bits are well shuffled + return v >> 16; +} + +// Wraps noise for tiling texture creation +// @param v = unwrapped texture parameter +// @param bTiling = true to tile, false to not tile +// @param RepeatSize = number of units before repeating +// @return either original or wrapped coord +float3 NoiseTileWrap(float3 v, bool bTiling, float RepeatSize) +{ + return bTiling ? (math::frac(v / RepeatSize) * RepeatSize) : v; +} + +// Evaluate polynomial to get smooth transitions for Perlin noise +// only needed by Perlin functions in this file +// scalar(per component): 2 add, 5 mul +float4 PerlinRamp(float4 t) +{ + return t * t * t * (t * (t * 6 - 15) + 10); +} + +// Blum-Blum-Shub-inspired pseudo random number generator +// http://www.umbc.edu/~olano/papers/mNoise.pdf +// real BBS uses ((s*s) mod M) with bignums and M as the product of two huge Blum primes +// instead, we use a single prime M just small enough not to overflow +// note that the above paper used 61, which fits in a half, but is unusably bad +// @param Integer valued floating point seed +// @return random number in range [0,1) +// ~8 ALU operations (5 *, 3 frac) +float RandBBSfloat(float seed) +{ + float BBS_PRIME24 = 4093.0; + float s = math::frac(seed / BBS_PRIME24); + s = math::frac(s * s * BBS_PRIME24); + s = math::frac(s * s * BBS_PRIME24); + return s; +} + +// Modified noise gradient term +// @param seed - random seed for integer lattice position +// @param offset - [-1,1] offset of evaluation point from lattice point +// @return gradient direction (xyz) and contribution (w) from this lattice point +float4 MGradient(int seed, float3 offset) +{ + //TODO uint + int rand = Rand3DPCG16(int3(seed,0,0)).x; + int3 MGradientMask = int3(0x8000, 0x4000, 0x2000); + float3 MGradientScale = float3(1.0 / 0x4000, 1.0 / 0x2000, 1.0 / 0x1000); + float3 direction = float3(int3(rand, rand, rand) & MGradientMask) * MGradientScale - 1; + return float4(direction.x, direction.y, direction.z, math::dot(direction, offset)); +} + +// compute Perlin and related noise corner seed values +// @param v = 3D noise argument, use float3(x,y,0) for 2D or float3(x,0,0) for 1D +// @param bTiling = true to return seed values for a repeating noise pattern +// @param RepeatSize = integer units before tiling in each dimension +// @param seed000-seed111 = hash function seeds for the eight corners +// @return fractional part of v +struct SeedValue +{ + float3 fv = float3(0); + float seed000 = 0; + float seed001 = 0; + float seed010 = 0; + float seed011 = 0; + float seed100 = 0; + float seed101 = 0; + float seed110 = 0; + float seed111 = 0; +}; + +SeedValue NoiseSeeds(float3 v, bool bTiling, float RepeatSize) +{ + SeedValue seeds; + seeds.fv = math::frac(v); + float3 iv = math::floor(v); + + const float3 primes = float3(19, 47, 101); + + if (bTiling) + { // can't algebraically combine with primes + seeds.seed000 = math::dot(primes, NoiseTileWrap(iv, true, RepeatSize)); + seeds.seed100 = math::dot(primes, NoiseTileWrap(iv + float3(1, 0, 0), true, RepeatSize)); + seeds.seed010 = math::dot(primes, NoiseTileWrap(iv + float3(0, 1, 0), true, RepeatSize)); + seeds.seed110 = math::dot(primes, NoiseTileWrap(iv + float3(1, 1, 0), true, RepeatSize)); + seeds.seed001 = math::dot(primes, NoiseTileWrap(iv + float3(0, 0, 1), true, RepeatSize)); + seeds.seed101 = math::dot(primes, NoiseTileWrap(iv + float3(1, 0, 1), true, RepeatSize)); + seeds.seed011 = math::dot(primes, NoiseTileWrap(iv + float3(0, 1, 1), true, RepeatSize)); + seeds.seed111 = math::dot(primes, NoiseTileWrap(iv + float3(1, 1, 1), true, RepeatSize)); + } + else + { // get to combine offsets with multiplication by primes in this case + seeds.seed000 = math::dot(iv, primes); + seeds.seed100 = seeds.seed000 + primes.x; + seeds.seed010 = seeds.seed000 + primes.y; + seeds.seed110 = seeds.seed100 + primes.y; + seeds.seed001 = seeds.seed000 + primes.z; + seeds.seed101 = seeds.seed100 + primes.z; + seeds.seed011 = seeds.seed010 + primes.z; + seeds.seed111 = seeds.seed110 + primes.z; + } + + return seeds; +} + +struct SimplexWeights +{ + float4 Result = float4(0); + float3 PosA = float3(0); + float3 PosB = float3(0); + float3 PosC = float3(0); + float3 PosD = float3(0); +}; + +// Computed weights and sample positions for simplex interpolation +// @return float4(a,b,c, d) Barycentric coordinate defined as Filtered = Tex(PosA) * a + Tex(PosB) * b + Tex(PosC) * c + Tex(PosD) * d +SimplexWeights ComputeSimplexWeights3D(float3 OrthogonalPos) +{ + SimplexWeights weights; + float3 OrthogonalPosFloor = math::floor(OrthogonalPos); + + weights.PosA = OrthogonalPosFloor; + weights.PosB = weights.PosA + float3(1, 1, 1); + + OrthogonalPos -= OrthogonalPosFloor; + + float Largest = math::max(OrthogonalPos.x, math::max(OrthogonalPos.y, OrthogonalPos.z)); + float Smallest = math::min(OrthogonalPos.x, math::min(OrthogonalPos.y, OrthogonalPos.z)); + + weights.PosC = weights.PosA + float3(Largest == OrthogonalPos.x, Largest == OrthogonalPos.y, Largest == OrthogonalPos.z); + weights.PosD = weights.PosA + float3(Smallest != OrthogonalPos.x, Smallest != OrthogonalPos.y, Smallest != OrthogonalPos.z); + + float RG = OrthogonalPos.x - OrthogonalPos.y; + float RB = OrthogonalPos.x - OrthogonalPos.z; + float GB = OrthogonalPos.y - OrthogonalPos.z; + + weights.Result.z = + math::min(math::max(0, RG), math::max(0, RB)) // X + + math::min(math::max(0, -RG), math::max(0, GB)) // Y + + math::min(math::max(0, -RB), math::max(0, -GB)); // Z + + weights.Result.w = + math::min(math::max(0, -RG), math::max(0, -RB)) // X + + math::min(math::max(0, RG), math::max(0, -GB)) // Y + + math::min(math::max(0, RB), math::max(0, GB)); // Z + + weights.Result.y = Smallest; + weights.Result.x = 1.0f - weights.Result.y - weights.Result.z - weights.Result.w; + + return weights; +} + +// filtered 3D gradient simple noise (few texture lookups, high quality) +// @param v >0 +// @return random number in the range -1 .. 1 +float SimplexNoise3D_TEX(uniform texture_2d PerlinNoiseGradientTexture, float3 EvalPos) +{ + float3 OrthogonalPos = SkewSimplex(EvalPos); + + SimplexWeights Weights = ComputeSimplexWeights3D(OrthogonalPos); + + // can be optimized to 1 or 2 texture lookups (4 or 8 channel encoded in 32 bit) + float3 A = GetPerlinNoiseGradientTextureAt(PerlinNoiseGradientTexture, Weights.PosA); + float3 B = GetPerlinNoiseGradientTextureAt(PerlinNoiseGradientTexture, Weights.PosB); + float3 C = GetPerlinNoiseGradientTextureAt(PerlinNoiseGradientTexture, Weights.PosC); + float3 D = GetPerlinNoiseGradientTextureAt(PerlinNoiseGradientTexture, Weights.PosD); + + Weights.PosA = UnSkewSimplex(Weights.PosA); + Weights.PosB = UnSkewSimplex(Weights.PosB); + Weights.PosC = UnSkewSimplex(Weights.PosC); + Weights.PosD = UnSkewSimplex(Weights.PosD); + + float DistanceWeight; + + DistanceWeight = math::saturate(0.6f - length2(EvalPos - Weights.PosA)); DistanceWeight *= DistanceWeight; DistanceWeight *= DistanceWeight; + float a = math::dot(A, EvalPos - Weights.PosA) * DistanceWeight; + DistanceWeight = math::saturate(0.6f - length2(EvalPos - Weights.PosB)); DistanceWeight *= DistanceWeight; DistanceWeight *= DistanceWeight; + float b = math::dot(B, EvalPos - Weights.PosB) * DistanceWeight; + DistanceWeight = math::saturate(0.6f - length2(EvalPos - Weights.PosC)); DistanceWeight *= DistanceWeight; DistanceWeight *= DistanceWeight; + float c = math::dot(C, EvalPos - Weights.PosC) * DistanceWeight; + DistanceWeight = math::saturate(0.6f - length2(EvalPos - Weights.PosD)); DistanceWeight *= DistanceWeight; DistanceWeight *= DistanceWeight; + float d = math::dot(D, EvalPos - Weights.PosD) * DistanceWeight; + + return 32 * (a + b + c + d); +} + +// filtered 3D noise, can be optimized +// @param v = 3D noise argument, use float3(x,y,0) for 2D or float3(x,0,0) for 1D +// @param bTiling = repeat noise pattern +// @param RepeatSize = integer units before tiling in each dimension +// @return random number in the range -1 .. 1 +float GradientNoise3D_TEX(uniform texture_2d PerlinNoiseGradientTexture, float3 v, bool bTiling, float RepeatSize) +{ + bTiling = true; + float3 fv = math::frac(v); + float3 iv0 = NoiseTileWrap(math::floor(v), bTiling, RepeatSize); + float3 iv1 = NoiseTileWrap(iv0 + 1, bTiling, RepeatSize); + + const int2 ZShear = int2(17, 89); + + float2 OffsetA = iv0.z * ZShear; + float2 OffsetB = OffsetA + ZShear; // non-tiling, use relative offset + if (bTiling) // tiling, have to compute from wrapped coordinates + { + OffsetB = iv1.z * ZShear; + } + + // Texture size scale factor + float ts = 1 / 128.0f; + + // texture coordinates for iv0.xy, as offset for both z slices + float2 TexA0 = (float2(iv0.x, iv0.y) + OffsetA + 0.5f) * ts; + float2 TexB0 = (float2(iv0.x, iv0.y) + OffsetB + 0.5f) * ts; + + // texture coordinates for iv1.xy, as offset for both z slices + float2 TexA1 = TexA0 + ts; // for non-tiling, can compute relative to existing coordinates + float2 TexB1 = TexB0 + ts; + if (bTiling) // for tiling, need to compute from wrapped coordinates + { + TexA1 = (float2(iv1.x, iv1.y) + OffsetA + 0.5f) * ts; + TexB1 = (float2(iv1.x, iv1.y) + OffsetB + 0.5f) * ts; + } + + + // can be optimized to 1 or 2 texture lookups (4 or 8 channel encoded in 8, 16 or 32 bit) + float4 PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexA0.x,1.0-TexA0.y),tex::wrap_repeat,tex::wrap_repeat); + float3 PerlinNoiseColor = float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z); + float3 A = PerlinNoiseColor * 2 - 1; + PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexA1.x,1.0-TexA0.y),tex::wrap_repeat,tex::wrap_repeat); + PerlinNoiseColor = float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z); + float3 B = PerlinNoiseColor * 2 - 1; + PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexA0.x,1.0-TexA1.y),tex::wrap_repeat,tex::wrap_repeat); + PerlinNoiseColor = float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z); + float3 C = PerlinNoiseColor * 2 - 1; + PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexA1.x,1.0-TexA1.y),tex::wrap_repeat,tex::wrap_repeat); + PerlinNoiseColor = float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z); + float3 D = PerlinNoiseColor * 2 - 1; + PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexB0.x,1.0-TexB0.y),tex::wrap_repeat,tex::wrap_repeat); + PerlinNoiseColor = float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z); + float3 E = PerlinNoiseColor * 2 - 1; + PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexB1.x,1.0-TexB0.y),tex::wrap_repeat,tex::wrap_repeat); + PerlinNoiseColor = float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z); + float3 F = PerlinNoiseColor * 2 - 1; + PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexB0.x,1.0-TexB1.y),tex::wrap_repeat,tex::wrap_repeat); + PerlinNoiseColor = float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z); + float3 G = PerlinNoiseColor * 2 - 1; + PerlinNoise = tex::lookup_float4(PerlinNoiseGradientTexture,float2(TexB1.x,1.0-TexB1.y),tex::wrap_repeat,tex::wrap_repeat); + PerlinNoiseColor = float3(PerlinNoise.x, PerlinNoise.y, PerlinNoise.z); + float3 H = PerlinNoiseColor * 2 - 1; + + float a = math::dot(A, fv - float3(0, 0, 0)); + float b = math::dot(B, fv - float3(1, 0, 0)); + float c = math::dot(C, fv - float3(0, 1, 0)); + float d = math::dot(D, fv - float3(1, 1, 0)); + float e = math::dot(E, fv - float3(0, 0, 1)); + float f = math::dot(F, fv - float3(1, 0, 1)); + float g = math::dot(G, fv - float3(0, 1, 1)); + float h = math::dot(H, fv - float3(1, 1, 1)); + + float4 Weights = PerlinRamp(math::frac(float4(fv.x, fv.y, fv.z, 0))); + + float i = math::lerp(math::lerp(a, b, Weights.x), math::lerp(c, d, Weights.x), Weights.y); + float j = math::lerp(math::lerp(e, f, Weights.x), math::lerp(g, h, Weights.x), Weights.y); + + return math::lerp(i, j, Weights.z); +} + +// @return random number in the range -1 .. 1 +// scalar: 6 frac, 31 mul/mad, 15 add, +float FastGradientPerlinNoise3D_TEX(uniform texture_3d PerlinNoise3DTexture, float3 xyz) +{ + // needs to be the same value when creating the PerlinNoise3D texture + float Extent = 16; + + // last texel replicated and needed for filtering + // scalar: 3 frac, 6 mul + xyz = math::frac(xyz / (Extent - 1)) * (Extent - 1); + + // scalar: 3 frac + float3 uvw = math::frac(xyz); + // = floor(xyz); + // scalar: 3 add + float3 p0 = xyz - uvw; +// float3 f = math::pow(uvw, 2) * 3.0f - math::pow(uvw, 3) * 2.0f; // original perlin hermite (ok when used without bump mapping) + // scalar: 2*3 add 5*3 mul + float4 pr = PerlinRamp(float4(uvw.x, uvw.y, uvw.z, 0)); + float3 f = float3(pr.x, pr.y, pr.z); // new, better with continues second derivative for bump mapping + // scalar: 3 add + float3 p = p0 + f; + // scalar: 3 mad + // TODO: need reverse??? + float4 NoiseSample = tex::lookup_float4(PerlinNoise3DTexture, p / Extent + 0.5f / Extent); // +0.5f to get rid of bilinear offset + + // reconstruct from 8bit (using mad with 2 constants and dot4 was same instruction count) + // scalar: 4 mad, 3 mul, 3 add + float3 n = float3(NoiseSample.x, NoiseSample.y, NoiseSample.z) * 255.0f / 127.0f - 1.0f; + float d = NoiseSample.w * 255.f - 127; + return math::dot(xyz, n) - d; +} + +// Perlin-style "Modified Noise" +// http://www.umbc.edu/~olano/papers/index.html#mNoise +// @param v = 3D noise argument, use float3(x,y,0) for 2D or float3(x,0,0) for 1D +// @param bTiling = repeat noise pattern +// @param RepeatSize = integer units before tiling in each dimension +// @return random number in the range -1 .. 1 +float GradientNoise3D_ALU(float3 v, bool bTiling, float RepeatSize) +{ + SeedValue seeds = NoiseSeeds(v, bTiling, RepeatSize); + + float rand000 = MGradient(int(seeds.seed000), seeds.fv - float3(0, 0, 0)).w; + float rand100 = MGradient(int(seeds.seed100), seeds.fv - float3(1, 0, 0)).w; + float rand010 = MGradient(int(seeds.seed010), seeds.fv - float3(0, 1, 0)).w; + float rand110 = MGradient(int(seeds.seed110), seeds.fv - float3(1, 1, 0)).w; + float rand001 = MGradient(int(seeds.seed001), seeds.fv - float3(0, 0, 1)).w; + float rand101 = MGradient(int(seeds.seed101), seeds.fv - float3(1, 0, 1)).w; + float rand011 = MGradient(int(seeds.seed011), seeds.fv - float3(0, 1, 1)).w; + float rand111 = MGradient(int(seeds.seed111), seeds.fv - float3(1, 1, 1)).w; + + float4 Weights = PerlinRamp(float4(seeds.fv.x, seeds.fv.y, seeds.fv.z, 0)); + + float i = math::lerp(math::lerp(rand000, rand100, Weights.x), math::lerp(rand010, rand110, Weights.x), Weights.y); + float j = math::lerp(math::lerp(rand001, rand101, Weights.x), math::lerp(rand011, rand111, Weights.x), Weights.y); + return math::lerp(i, j, Weights.z); +} + +// 3D value noise - used to be incorrectly called Perlin noise +// @param v = 3D noise argument, use float3(x,y,0) for 2D or float3(x,0,0) for 1D +// @param bTiling = repeat noise pattern +// @param RepeatSize = integer units before tiling in each dimension +// @return random number in the range -1 .. 1 +float ValueNoise3D_ALU(float3 v, bool bTiling, float RepeatSize) +{ + SeedValue seeds = NoiseSeeds(v, bTiling, RepeatSize); + + float rand000 = RandBBSfloat(seeds.seed000) * 2 - 1; + float rand100 = RandBBSfloat(seeds.seed100) * 2 - 1; + float rand010 = RandBBSfloat(seeds.seed010) * 2 - 1; + float rand110 = RandBBSfloat(seeds.seed110) * 2 - 1; + float rand001 = RandBBSfloat(seeds.seed001) * 2 - 1; + float rand101 = RandBBSfloat(seeds.seed101) * 2 - 1; + float rand011 = RandBBSfloat(seeds.seed011) * 2 - 1; + float rand111 = RandBBSfloat(seeds.seed111) * 2 - 1; + + float4 Weights = PerlinRamp(float4(seeds.fv.x, seeds.fv.y, seeds.fv.z, 0)); + + float i = math::lerp(math::lerp(rand000, rand100, Weights.x), math::lerp(rand010, rand110, Weights.x), Weights.y); + float j = math::lerp(math::lerp(rand001, rand101, Weights.x), math::lerp(rand011, rand111, Weights.x), Weights.y); + return math::lerp(i, j, Weights.z); +} + +// 3D jitter offset within a voronoi noise cell +// @param pos - integer lattice corner +// @return random offsets vector +float3 VoronoiCornerSample(float3 pos, int Quality) +{ + // random values in [-0.5, 0.5] + float3 noise = float3(Rand3DPCG16(int3(pos))) / 0xffff - 0.5; + + // quality level 1 or 2: searches a 2x2x2 neighborhood with points distributed on a sphere + // scale factor to guarantee jittered points will be found within a 2x2x2 search + if (Quality <= 2) + { + return math::normalize(noise) * 0.2588; + } + + // quality level 3: searches a 3x3x3 neighborhood with points distributed on a sphere + // scale factor to guarantee jittered points will be found within a 3x3x3 search + if (Quality == 3) + { + return math::normalize(noise) * 0.3090; + } + + // quality level 4: jitter to anywhere in the cell, needs 4x4x4 search + return noise; +} + +// compare previous best with a new candidate +// not producing point locations makes it easier for compiler to eliminate calculations when they're not needed +// @param minval = location and distance of best candidate seed point before the new one +// @param candidate = candidate seed point +// @param offset = 3D offset to new candidate seed point +// @param bDistanceOnly = if true, only set maxval.w with distance, otherwise maxval.w is distance and maxval.xyz is position +// @return position (if bDistanceOnly is false) and distance to closest seed point so far +float4 VoronoiCompare(float4 minval, float3 candidate, float3 offset, bool bDistanceOnly) +{ + if (bDistanceOnly) + { + return float4(0, 0, 0, math::min(minval.w, math::dot(offset, offset))); + } + else + { + float newdist = math::dot(offset, offset); + return newdist > minval.w ? minval : float4(candidate.x, candidate.y, candidate.z, newdist); + } +} + +// 220 instruction Worley noise +float4 VoronoiNoise3D_ALU(float3 v, int Quality, bool bTiling, float RepeatSize, bool bDistanceOnly) +{ + float3 fv = math::frac(v), fv2 = math::frac(v + 0.5); + float3 iv = math::floor(v), iv2 = math::floor(v + 0.5); + + // with initial minimum distance = infinity (or at least bigger than 4), first min is optimized away + float4 mindist = float4(0,0,0,100); + float3 p, offset; + + // quality level 3: do a 3x3x3 search + if (Quality == 3) + { + int offset_x; + int offset_y; + int offset_z; + for (offset_x = -1; offset_x <= 1; ++offset_x) + { + for (offset_y = -1; offset_y <= 1; ++offset_y) + { + for (offset_z = -1; offset_z <= 1; ++offset_z) + { + offset = float3(offset_x, offset_y, offset_z); + p = offset + VoronoiCornerSample(NoiseTileWrap(iv2 + offset, bTiling, RepeatSize), Quality); + mindist = VoronoiCompare(mindist, iv2 + p, fv2 - p, bDistanceOnly); + } + } + } + } + + // everybody else searches a base 2x2x2 neighborhood + else + { + int offset_x; + int offset_y; + int offset_z; + for (offset_x = 0; offset_x <= 1; ++offset_x) + { + for (offset_y = 0; offset_y <= 1; ++offset_y) + { + for (offset_z = 0; offset_z <= 1; ++offset_z) + { + offset = float3(offset_x, offset_y, offset_z); + p = offset + VoronoiCornerSample(NoiseTileWrap(iv + offset, bTiling, RepeatSize), Quality); + mindist = VoronoiCompare(mindist, iv + p, fv - p, bDistanceOnly); + + // quality level 2, do extra set of points, offset by half a cell + if (Quality == 2) + { + // 467 is just an offset to a different area in the random number field to avoid similar neighbor artifacts + p = offset + VoronoiCornerSample(NoiseTileWrap(iv2 + offset, bTiling, RepeatSize) + 467, Quality); + mindist = VoronoiCompare(mindist, iv2 + p, fv2 - p, bDistanceOnly); + } + } + } + } + } + + // quality level 4: add extra sets of four cells in each direction + if (Quality >= 4) + { + int offset_x; + int offset_y; + int offset_z; + for (offset_x = -1; offset_x <= 2; offset_x += 3) + { + for (offset_y = 0; offset_y <= 1; ++offset_y) + { + for (offset_z = 0; offset_z <= 1; ++offset_z) + { + offset = float3(offset_x, offset_y, offset_z); + // along x axis + p = offset + VoronoiCornerSample(NoiseTileWrap(iv + offset, bTiling, RepeatSize), Quality); + mindist = VoronoiCompare(mindist, iv + p, fv - p, bDistanceOnly); + + // along y axis + p = float3(offset.y, offset.z, offset.x) + VoronoiCornerSample(NoiseTileWrap(iv + float3(offset.y, offset.z, offset.x), bTiling, RepeatSize), Quality); + mindist = VoronoiCompare(mindist, iv + p, fv - p, bDistanceOnly); + + // along z axis + p = float3(offset.z, offset.x, offset.y) + VoronoiCornerSample(NoiseTileWrap(iv + float3(offset.z, offset.x, offset.y), bTiling, RepeatSize), Quality); + mindist = VoronoiCompare(mindist, iv + p, fv - p, bDistanceOnly); + } + } + } + } + + // transform squared distance to real distance + return float4(mindist.x, mindist.y, mindist.z, math::sqrt(mindist.w)); +} + +// Coordinates for corners of a Simplex tetrahedron +// Based on McEwan et al., Efficient computation of noise in GLSL, JGT 2011 +// @param v = 3D noise argument +// @return 4 corner locations +float4x3 SimplexCorners(float3 v) +{ + // find base corner by skewing to tetrahedral space and back + float3 tet = math::floor(v + v.x/3 + v.y/3 + v.z/3); + float3 base = tet - tet.x/6 - tet.y/6 - tet.z/6; + float3 f = v - base; + + // Find offsets to other corners (McEwan did this in tetrahedral space, + // but since skew is along x=y=z axis, this works in Euclidean space too.) + float3 g = math::step(float3(f.y,f.z,f.x), float3(f.x,f.y,f.z)), h = 1 - float3(g.z, g.x, g.y); + float3 a1 = math::min(g, h) - 1.0 / 6.0, a2 = math::max(g, h) - 1.0 / 3.0; + + // four corners + return float4x3(base, base + a1, base + a2, base + 0.5); +} + +// Improved smoothing function for simplex noise +// @param f = fractional distance to four tetrahedral corners +// @return weight for each corner +float4 SimplexSmooth(float4x3 f) +{ + const float scale = 1024. / 375.; // scale factor to make noise -1..1 + float4 d = float4(math::dot(f[0], f[0]), math::dot(f[1], f[1]), math::dot(f[2], f[2]), math::dot(f[3], f[3])); + float4 s = math::saturate(2 * d); + return (1 * scale + s*(-3 * scale + s*(3 * scale - s*scale))); +} + +// Derivative of simplex noise smoothing function +// @param f = fractional distanc eto four tetrahedral corners +// @return derivative of smoothing function for each corner by x, y and z +float3x4 SimplexDSmooth(float4x3 f) +{ + const float scale = 1024. / 375.; // scale factor to make noise -1..1 + float4 d = float4(math::dot(f[0], f[0]), math::dot(f[1], f[1]), math::dot(f[2], f[2]), math::dot(f[3], f[3])); + float4 s = math::saturate(2 * d); + s = -12 * scale + s*(24 * scale - s * 12 * scale); + + return float3x4( + s * float4(f[0][0], f[1][0], f[2][0], f[3][0]), + s * float4(f[0][1], f[1][1], f[2][1], f[3][1]), + s * float4(f[0][2], f[1][2], f[2][2], f[3][2])); +} + +// Simplex noise and its Jacobian derivative +// @param v = 3D noise argument +// @param bTiling = whether to repeat noise pattern +// @param RepeatSize = integer units before tiling in each dimension, must be a multiple of 3 +// @return float3x3 Jacobian in J[*].xyz, vector noise in J[*].w +// J[0].w, J[1].w, J[2].w is a Perlin-style simplex noise with vector output, e.g. (Nx, Ny, Nz) +// J[i].x is X derivative of the i'th component of the noise so J[2].x is dNz/dx +// You can use this to compute the noise, gradient, curl, or divergence: +// float3x4 J = JacobianSimplex_ALU(...); +// float3 VNoise = float3(J[0].w, J[1].w, J[2].w); // 3D noise +// float3 Grad = J[0].xyz; // gradient of J[0].w +// float3 Curl = float3(J[1][2]-J[2][1], J[2][0]-J[0][2], J[0][1]-J[1][2]); +// float Div = J[0][0]+J[1][1]+J[2][2]; +// All of these are confirmed to compile out all unneeded terms. +// So Grad of X doesn't compute Y or Z components, and VNoise doesn't do any of the derivative computation. +float3x4 JacobianSimplex_ALU(float3 v, bool bTiling, float RepeatSize) +{ + int3 MGradientMask = int3(0x8000, 0x4000, 0x2000); + float3 MGradientScale = float3(1. / 0x4000, 1. / 0x2000, 1. / 0x1000); + + // corners of tetrahedron + float4x3 T = SimplexCorners(v); + // TODO: uint3 + int3 rand = int3(0); + float4x3 gvec0 = float4x3(1.0); + float4x3 gvec1 = float4x3(1.0); + float4x3 gvec2 = float4x3(1.0); + float4x3 fv = float4x3(1.0); + float3x4 grad = float3x4(1.0); + + // processing of tetrahedral vertices, unrolled + // to compute gradient at each corner + fv[0] = v - T[0]; + rand = Rand3DPCG16(int3(math::floor(NoiseTileWrap(6 * T[0] + 0.5, bTiling, RepeatSize)))); + gvec0[0] = float3(int3(rand.x,rand.x,rand.x) & MGradientMask) * MGradientScale - 1; + gvec1[0] = float3(int3(rand.y,rand.y,rand.y) & MGradientMask) * MGradientScale - 1; + gvec2[0] = float3(int3(rand.z,rand.z,rand.z) & MGradientMask) * MGradientScale - 1; + grad[0][0] = math::dot(gvec0[0], fv[0]); + grad[1][0] = math::dot(gvec1[0], fv[0]); + grad[2][0] = math::dot(gvec2[0], fv[0]); + + fv[1] = v - T[1]; + rand = Rand3DPCG16(int3(math::floor(NoiseTileWrap(6 * T[1] + 0.5, bTiling, RepeatSize)))); + gvec0[1] = float3(int3(rand.x,rand.x,rand.x) & MGradientMask) * MGradientScale - 1; + gvec1[1] = float3(int3(rand.y,rand.y,rand.y) & MGradientMask) * MGradientScale - 1; + gvec1[1] = float3(int3(rand.z,rand.z,rand.z) & MGradientMask) * MGradientScale - 1; + grad[0][1] = math::dot(gvec0[1], fv[1]); + grad[1][1] = math::dot(gvec1[1], fv[1]); + grad[2][1] = math::dot(gvec2[1], fv[1]); + + fv[2] = v - T[2]; + rand = Rand3DPCG16(int3(math::floor(NoiseTileWrap(6 * T[2] + 0.5, bTiling, RepeatSize)))); + gvec0[2] = float3(int3(rand.x,rand.x,rand.x) & MGradientMask) * MGradientScale - 1; + gvec1[2] = float3(int3(rand.y,rand.y,rand.y) & MGradientMask) * MGradientScale - 1; + gvec2[2] = float3(int3(rand.z,rand.z,rand.z) & MGradientMask) * MGradientScale - 1; + grad[0][2] = math::dot(gvec0[2], fv[2]); + grad[1][2] = math::dot(gvec1[2], fv[2]); + grad[2][2] = math::dot(gvec2[2], fv[2]); + + fv[3] = v - T[3]; + rand = Rand3DPCG16(int3(math::floor(NoiseTileWrap(6 * T[3] + 0.5, bTiling, RepeatSize)))); + gvec0[3] = float3(int3(rand.x,rand.x,rand.x) & MGradientMask) * MGradientScale - 1; + gvec1[3] = float3(int3(rand.y,rand.y,rand.y) & MGradientMask) * MGradientScale - 1; + gvec2[3] = float3(int3(rand.z,rand.z,rand.z) & MGradientMask) * MGradientScale - 1; + grad[0][3] = math::dot(gvec0[3], fv[3]); + grad[1][3] = math::dot(gvec1[3], fv[3]); + grad[2][3] = math::dot(gvec2[3], fv[3]); + + // blend gradients + float4 sv = SimplexSmooth(fv); + float3x4 ds = SimplexDSmooth(fv); + + float3x4 jacobian = float3x4(1.0); + float3 vec0 = gvec0*sv + grad[0]*ds; // NOTE: mdl is column major, convert from UE4 (row major) + jacobian[0] = float4(vec0.x, vec0.y, vec0.z, math::dot(sv, grad[0])); + float3 vec1 = gvec1*sv + grad[1]*ds; + jacobian[1] = float4(vec1.x, vec1.y, vec1.z, math::dot(sv, grad[1])); + float3 vec2 = gvec2*sv + grad[2]*ds; + jacobian[2] = float4(vec2.x, vec2.y, vec2.z, math::dot(sv, grad[2])); + + return jacobian; +} + +// While RepeatSize is a float here, the expectation is that it would be largely integer values coming in from the UI. The downstream logic assumes +// floats for all called functions (NoiseTileWrap) and this prevents any float-to-int conversion errors from automatic type conversion. +float Noise3D_Multiplexer(uniform texture_2d PerlinNoiseGradientTexture, uniform texture_3d PerlinNoise3DTexture, int Function, float3 Position, int Quality, bool bTiling, float RepeatSize) +{ + // verified, HLSL compiled out the switch if Function is a constant + switch(Function) + { + case 0: + return SimplexNoise3D_TEX(PerlinNoiseGradientTexture, Position); + case 1: + return GradientNoise3D_TEX(PerlinNoiseGradientTexture, Position, bTiling, RepeatSize); + case 2: + return FastGradientPerlinNoise3D_TEX(PerlinNoise3DTexture, Position); + case 3: + return GradientNoise3D_ALU(Position, bTiling, RepeatSize); + case 4: + return ValueNoise3D_ALU(Position, bTiling, RepeatSize); + case 5: + return VoronoiNoise3D_ALU(Position, Quality, bTiling, RepeatSize, true).w * 2.0 - 1.0; + } + return 0; +} +//---------------------------------------------------------- + +export float noise(uniform texture_2d PerlinNoiseGradientTexture, uniform texture_3d PerlinNoise3DTexture, float3 Position, float Scale, float Quality, float Function, float Turbulence, float Levels, float OutputMin, float OutputMax, float LevelScale, float FilterWidth, float Tiling, float RepeatSize) +[[ + anno::description("Noise"), + anno::noinline() +]] +{ + Position *= Scale; + FilterWidth *= Scale; + + float Out = 0.0f; + float OutScale = 1.0f; + float InvLevelScale = 1.0f / LevelScale; + + int iFunction(Function); + int iQuality(Quality); + int iLevels(Levels); + bool bTurbulence(Turbulence); + bool bTiling(Tiling); + + for(int i = 0; i < iLevels; ++i) + { + // fade out noise level that are too high frequent (not done through dynamic branching as it usually requires gradient instructions) + OutScale *= math::saturate(1.0 - FilterWidth); + + if(bTurbulence) + { + Out += math::abs(Noise3D_Multiplexer(PerlinNoiseGradientTexture, PerlinNoise3DTexture, iFunction, Position, iQuality, bTiling, RepeatSize)) * OutScale; + } + else + { + Out += Noise3D_Multiplexer(PerlinNoiseGradientTexture, PerlinNoise3DTexture, iFunction, Position, iQuality, bTiling, RepeatSize) * OutScale; + } + + Position *= LevelScale; + RepeatSize *= LevelScale; + OutScale *= InvLevelScale; + FilterWidth *= LevelScale; + } + + if(!bTurbulence) + { + // bring -1..1 to 0..1 range + Out = Out * 0.5f + 0.5f; + } + + // Out is in 0..1 range + return math::lerp(OutputMin, OutputMax, Out); +} + +// Material node for noise functions returning a vector value +// @param LevelScale usually 2 but higher values allow efficient use of few levels +// @return in user defined range (OutputMin..OutputMax) +export float4 vector4_noise(float3 Position, float Quality, float Function, float Tiling, float TileSize) +[[ + anno::description("Vector Noise"), + anno::noinline() +]] +{ + float4 result = float4(0,0,0,1); + float3 ret = float3(0); + int iQuality = int(Quality); + int iFunction = int(Function); + bool bTiling = Tiling > 0.0; + + float3x4 Jacobian = JacobianSimplex_ALU(Position, bTiling, TileSize); // compiled out if not used + + // verified, HLSL compiled out the switch if Function is a constant + switch (iFunction) + { + case 0: // Cellnoise + ret = float3(Rand3DPCG16(int3(math::floor(NoiseTileWrap(Position, bTiling, TileSize))))) / 0xffff; + result = float4(ret.x, ret.y, ret.z, 1); + break; + case 1: // Color noise + ret = float3(Jacobian[0].w, Jacobian[1].w, Jacobian[2].w); + result = float4(ret.x, ret.y, ret.z, 1); + break; + case 2: // Gradient + result = Jacobian[0]; + break; + case 3: // Curl + ret = float3(Jacobian[2][1] - Jacobian[1][2], Jacobian[0][2] - Jacobian[2][0], Jacobian[1][0] - Jacobian[0][1]); + result = float4(ret.x, ret.y, ret.z, 1); + break; + case 4: // Voronoi + result = VoronoiNoise3D_ALU(Position, iQuality, bTiling, TileSize, false); + break; + } + return result; +} + +export float3 vector3_noise(float3 Position, float Quality, float Function, float Tiling, float TileSize) +[[ + anno::description("Vector Noise float3 version"), + anno::noinline() +]] +{ + float4 noise = vector4_noise(Position, Quality, Function, Tiling, TileSize); + return float3(noise.x, noise.y, noise.z); +} + + +// workaround for ue4 fresnel (without supporting for camera vector) : replacing it with 0.0, means facing to the view +export float fresnel(float exponent [[anno::unused()]], float base_reflect_fraction [[anno::unused()]], float3 normal [[anno::unused()]]) +[[ + anno::description("Fresnel"), + anno::noinline() +]] +{ + return 0.0; +} + +export float fresnel_function(float3 normal_vector [[anno::unused()]], float3 camera_vector [[anno::unused()]], + bool invert_fresnel [[anno::unused()]], float power [[anno::unused()]], + bool use_cheap_contrast [[anno::unused()]], float cheap_contrast_dark [[anno::unused()]], float cheap_contrast_bright [[anno::unused()]], + bool clamp_fresnel_dot_product [[anno::unused()]]) +[[ + anno::description("Fresnel Function"), + anno::noinline() +]] +{ + return 0.0; +} + +export float3 camera_vector() +[[ + anno::description("Camera Vector"), + anno::noinline() +]] +{ + // assume camera postion is 0,0,0 + return math::normalize(float3(0) - state::transform_point(state::coordinate_internal,state::coordinate_world,state::position())); +} + +export float pixel_depth() +[[ + anno::description("Pixel Depth"), + anno::noinline() +]] +{ + return 256.0f; +} + +export float scene_depth() +[[ + anno::description("Scene Depth") +]] +{ + return 65500.0f; +} + +export float3 scene_color() +[[ + anno::description("Scene Color") +]] +{ + return float3(1.0f); +} + +export float4 vertex_color() +[[ + anno::description("Vertex Color"), + anno::noinline() +]] +{ + return float4(1.0f); +} + +export float4 vertex_color_from_coordinate(int VertexColorCoordinateIndex) +[[ + anno::description("Vertex Color for float2 PrimVar"), + anno::noinline() +]] +{ + // Kit only supports 4 uv sets, 2 uvs are available to vertex color. if vertex color index is invalid, output the constant WHITE color intead + return (VertexColorCoordinateIndex > 2) ? float4(1.0f) : float4(state::texture_coordinate(VertexColorCoordinateIndex).x, state::texture_coordinate(VertexColorCoordinateIndex).y, state::texture_coordinate(VertexColorCoordinateIndex+1).x, state::texture_coordinate(VertexColorCoordinateIndex+1).y); +} + +export float3 camera_position() +[[ + anno::description("Camera Position"), + anno::noinline() +]] +{ + return float3(1000.0f, 0, 0); +} + +export float3 rotate_about_axis(float4 NormalizedRotationAxisAndAngle, float3 PositionOnAxis, float3 Position) +[[ + anno::description("Rotates Position about the given axis by the given angle") +]] +{ + // Project Position onto the rotation axis and find the closest point on the axis to Position + float3 NormalizedRotationAxis = float3(NormalizedRotationAxisAndAngle.x,NormalizedRotationAxisAndAngle.y,NormalizedRotationAxisAndAngle.z); + float3 ClosestPointOnAxis = PositionOnAxis + NormalizedRotationAxis * math::dot(NormalizedRotationAxis, Position - PositionOnAxis); + // Construct orthogonal axes in the plane of the rotation + float3 UAxis = Position - ClosestPointOnAxis; + float3 VAxis = math::cross(NormalizedRotationAxis, UAxis); + float[2] SinCosAngle = math::sincos(NormalizedRotationAxisAndAngle.w); + // Rotate using the orthogonal axes + float3 R = UAxis * SinCosAngle[1] + VAxis * SinCosAngle[0]; + // Reconstruct the rotated world space position + float3 RotatedPosition = ClosestPointOnAxis + R; + // Convert from position to a position offset + return RotatedPosition - Position; +} + +export float2 rotate_scale_offset_texcoords(float2 InTexCoords, float4 InRotationScale, float2 InOffset) +[[ + anno::description("Returns a float2 texture coordinate after 2x2 transform and offset applied") +]] +{ + return float2(math::dot(InTexCoords, float2(InRotationScale.x, InRotationScale.y)), math::dot(InTexCoords, float2(InRotationScale.z, InRotationScale.w))) + InOffset; +} + +export float3 reflection_custom_world_normal(float3 WorldNormal, bool bNormalizeInputNormal) +[[ + anno::description("Reflection vector about the specified world space normal") +]] +{ + if (bNormalizeInputNormal) + { + WorldNormal = math::normalize(WorldNormal); + } + + return -camera_vector() + WorldNormal * math::dot(WorldNormal, camera_vector()) * 2.0; +} + +export float3 reflection_vector() +[[ + anno::description("Reflection Vector"), + anno::noinline() +]] +{ + float3 normal = state::transform_normal(state::coordinate_internal,state::coordinate_world,state::normal()); + return reflection_custom_world_normal(normal, false); +} + +export float dither_temporalAA(float AlphaThreshold = 0.5f, float Random = 1.0f [[anno::unused()]]) +[[ + anno::description("Dither TemporalAA"), + anno::noinline() +]] +{ + return AlphaThreshold; +} + diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0001.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0001.png new file mode 100644 index 000000000..d87c093f5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0001.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0009.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0009.png new file mode 100644 index 000000000..813331045 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0009.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0011.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0011.png new file mode 100644 index 000000000..e8b29459f Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0011.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0012.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0012.png new file mode 100644 index 000000000..f82a1319c Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0012.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0013.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0013.png new file mode 100644 index 000000000..bff71e836 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0013.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0014.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0014.png new file mode 100644 index 000000000..8cfff8644 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0014.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0015.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0015.png new file mode 100644 index 000000000..43a156d8f Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0015.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0020.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0020.png new file mode 100644 index 000000000..1d18d82c7 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/0020.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_01.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_01.png new file mode 100644 index 000000000..4db48c6c7 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_01.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_02.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_02.png new file mode 100644 index 000000000..c8700fd95 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_02.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_03.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_03.png new file mode 100644 index 000000000..e0f3114ac Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_03.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_04.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_04.png new file mode 100644 index 000000000..bdf5ed349 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_04.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_05.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_05.png new file mode 100644 index 000000000..f4bed62fe Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_05.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_06.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_06.png new file mode 100644 index 000000000..0b5d91211 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/AisleSign_Text_06.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_PaperNotes_01/T_PaperNotes_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_PaperNotes_01/T_PaperNotes_D.png new file mode 100644 index 000000000..99c522a45 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_PaperNotes_01/T_PaperNotes_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_PaperNotes_01/T_PaperNotes_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_PaperNotes_01/T_PaperNotes_M.png new file mode 100644 index 000000000..696ab04da Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_PaperNotes_01/T_PaperNotes_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_RackShield_01/Alum_Anodized_roughness.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_RackShield_01/Alum_Anodized_roughness.png new file mode 100644 index 000000000..88a18ba59 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_RackShield_01/Alum_Anodized_roughness.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_SignB/T_SignsA_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_SignB/T_SignsA_D.png new file mode 100644 index 000000000..1271b91aa Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MI_SignB/T_SignsA_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_D.png new file mode 100644 index 000000000..c85109040 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_N.png new file mode 100644 index 000000000..c037b07c1 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_ORM.png new file mode 100644 index 000000000..401168010 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_AisleSign/T_AisleSign_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_D.png new file mode 100644 index 000000000..1b99b2caa Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_N.png new file mode 100644 index 000000000..1d17b93f5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_ORM.png new file mode 100644 index 000000000..caf01b76e Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_Stripes.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_Stripes.png new file mode 100644 index 000000000..dd8e4a2cb Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_TrafficCone/T_TrafficCone_Stripes.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_D.png new file mode 100644 index 000000000..4d8a44d86 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_M.png new file mode 100644 index 000000000..e3a7f9870 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_N.png new file mode 100644 index 000000000..d4bd1f002 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_ORM.png new file mode 100644 index 000000000..300876dd5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WallBoard_01/T_WallBoard_01_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_D.png new file mode 100644 index 000000000..68a98bb89 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_N.png new file mode 100644 index 000000000..cf3bbf4af Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_ORM.png new file mode 100644 index 000000000..5348f014b Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/M_WetFloorSign/T_WetFloorSign_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MaterialInstanceDynamic_1220/T_PlasticWrap_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MaterialInstanceDynamic_1220/T_PlasticWrap_N.png new file mode 100644 index 000000000..f69284559 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/MaterialInstanceDynamic_1220/T_PlasticWrap_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_D.png new file mode 100644 index 000000000..c5ff2ebcc Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_M.png new file mode 100644 index 000000000..28d4c89ec Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_N.png new file mode 100644 index 000000000..1e3eb57ef Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_ORM.png new file mode 100644 index 000000000..4eeaed9ad Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticA_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_D.png new file mode 100644 index 000000000..fae6473ce Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_M.png new file mode 100644 index 000000000..3f962cfda Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_N.png new file mode 100644 index 000000000..af414408d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_ORM.png new file mode 100644 index 000000000..f65545d69 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticB_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_D.png new file mode 100644 index 000000000..4ccbb7076 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_M.png new file mode 100644 index 000000000..5602cbfa5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_N.png new file mode 100644 index 000000000..21f17afe5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_ORM.png new file mode 100644 index 000000000..3f49f546a Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticC_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_D.png new file mode 100644 index 000000000..336b4f62f Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_M.png new file mode 100644 index 000000000..da612ca0f Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_N.png new file mode 100644 index 000000000..0f3dcb9af Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_ORM.png new file mode 100644 index 000000000..0aedbcfc4 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BarelPlasticD_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_D.png new file mode 100644 index 000000000..e4c7748af Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_M.png new file mode 100644 index 000000000..986dcb472 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_N.png new file mode 100644 index 000000000..18c466887 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_ORM.png new file mode 100644 index 000000000..e238379a8 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BeamsA_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BlankMask_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BlankMask_M.png new file mode 100644 index 000000000..b8ad4ef30 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BlankMask_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_D.png new file mode 100644 index 000000000..714eabfcf Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_N.png new file mode 100644 index 000000000..9e6666170 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_ORM.png new file mode 100644 index 000000000..39ea9b213 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BottlesPlastic_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_D.png new file mode 100644 index 000000000..255c4ef81 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_M.png new file mode 100644 index 000000000..d7bc64177 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_N.png new file mode 100644 index 000000000..e28290724 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_ORM.png new file mode 100644 index 000000000..302a0abd2 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticB_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_D.png new file mode 100644 index 000000000..dd69d342b Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_M.png new file mode 100644 index 000000000..eb3e31321 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_N.png new file mode 100644 index 000000000..bcb5ccf99 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_ORM.png new file mode 100644 index 000000000..e10bfde79 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_BucketPlasticD_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_D.png new file mode 100644 index 000000000..d4c8f5ba5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_N.png new file mode 100644 index 000000000..4669f5f6e Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_ORM.png new file mode 100644 index 000000000..cfe31afcc Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxA_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_D.png new file mode 100644 index 000000000..8cafa2217 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_N.png new file mode 100644 index 000000000..feec64dae Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_ORM.png new file mode 100644 index 000000000..46269465c Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxB_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_D.png new file mode 100644 index 000000000..5cfe391b0 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_N.png new file mode 100644 index 000000000..eef7fc8a8 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_ORM.png new file mode 100644 index 000000000..5093974c7 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxC_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_D.png new file mode 100644 index 000000000..1cf9d8e99 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_N.png new file mode 100644 index 000000000..76a5fc1ed Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_ORM.png new file mode 100644 index 000000000..b927097fa Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CardBoxD_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_D.png new file mode 100644 index 000000000..d14e94c2a Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_M.png new file mode 100644 index 000000000..05ac605b5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_N.png new file mode 100644 index 000000000..f6e01fb9d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_ORM.png new file mode 100644 index 000000000..1ffc1423a Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CeilingA_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_D.png new file mode 100644 index 000000000..f7d6eec1d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_M.png new file mode 100644 index 000000000..df15954af Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_N.png new file mode 100644 index 000000000..d3c724d9d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_ORM.png new file mode 100644 index 000000000..8a5c8e36d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_A_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_D.png new file mode 100644 index 000000000..240b00263 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_M.png new file mode 100644 index 000000000..bdf3b9c5d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_N.png new file mode 100644 index 000000000..d91219987 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_ORM.png new file mode 100644 index 000000000..df81800dd Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_B_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_D.png new file mode 100644 index 000000000..cc5c5d25b Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_M.png new file mode 100644 index 000000000..3447985b4 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_N.png new file mode 100644 index 000000000..29368a5ae Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_ORM.png new file mode 100644 index 000000000..f86fd0572 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_C_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_D.png new file mode 100644 index 000000000..31a58b59e Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_M.png new file mode 100644 index 000000000..d786f8a61 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_N.png new file mode 100644 index 000000000..821caffe9 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_ORM.png new file mode 100644 index 000000000..d12d5dda5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_CratePlastic_E_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_D.png new file mode 100644 index 000000000..4e7a3b8d8 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_N.png new file mode 100644 index 000000000..d49d14c09 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_ORM.png new file mode 100644 index 000000000..554f89e08 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FireExtinguisher_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_D.png new file mode 100644 index 000000000..27507eafe Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_N.png new file mode 100644 index 000000000..c91025134 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_ORM.png new file mode 100644 index 000000000..d30927579 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FirstAid_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_D.png new file mode 100644 index 000000000..95876eacd Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_M.png new file mode 100644 index 000000000..cc4126f0d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_N.png new file mode 100644 index 000000000..54c56861f Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_ORM.png new file mode 100644 index 000000000..400479cf2 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FloorStripes_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_D.png new file mode 100644 index 000000000..b640b395e Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_M.png new file mode 100644 index 000000000..8ef0b8812 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_N.png new file mode 100644 index 000000000..bd6666515 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_ORM.png new file mode 100644 index 000000000..dddc1deea Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Floor_01_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_D.png new file mode 100644 index 000000000..0d0cd300c Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_N.png new file mode 100644 index 000000000..09e01b243 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_ORM.png new file mode 100644 index 000000000..76f2f6162 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_Forklift_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_D.png new file mode 100644 index 000000000..ac3069fdc Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_M.png new file mode 100644 index 000000000..f3b2dc1c4 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_N.png new file mode 100644 index 000000000..700b99fc8 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_ORM.png new file mode 100644 index 000000000..27094d121 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_FrameA_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_D.png new file mode 100644 index 000000000..2ddbc635c Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_N.png new file mode 100644 index 000000000..4aee56565 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_ORM.png new file mode 100644 index 000000000..bdb700656 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_LampCeilingA_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_D.png new file mode 100644 index 000000000..3de562575 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_M.png new file mode 100644 index 000000000..8bf00adfc Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_N.png new file mode 100644 index 000000000..20b9dfde0 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_ORM.png new file mode 100644 index 000000000..a08dad314 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PaletteA_01_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PlasticWrap_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PlasticWrap_D.png new file mode 100644 index 000000000..ac1bc5b93 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PlasticWrap_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PlasticWrap_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PlasticWrap_ORM.png new file mode 100644 index 000000000..1a289d3e5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PlasticWrap_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_D.png new file mode 100644 index 000000000..64f265e27 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_M.png new file mode 100644 index 000000000..ba462960f Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_N.png new file mode 100644 index 000000000..63fec8972 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_ORM.png new file mode 100644 index 000000000..d5de84641 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_PushcartA_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_D.png new file mode 100644 index 000000000..8c0abb728 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_M.png new file mode 100644 index 000000000..934c8e5e9 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_N.png new file mode 100644 index 000000000..cc562a7da Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_ORM.png new file mode 100644 index 000000000..7e2e39b08 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_01_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_D.png new file mode 100644 index 000000000..0911fcee5 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_M.png new file mode 100644 index 000000000..75058336d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_N.png new file mode 100644 index 000000000..3c09e75c3 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_ORM.png new file mode 100644 index 000000000..ab4bb7ef2 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_02_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_D.png new file mode 100644 index 000000000..ef947d772 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_M.png new file mode 100644 index 000000000..ceb6de59f Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_N.png new file mode 100644 index 000000000..d44a9bf38 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_ORM.png new file mode 100644 index 000000000..5096c696d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_03_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_D.png new file mode 100644 index 000000000..97da43342 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_M.png new file mode 100644 index 000000000..d9609e068 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_N.png new file mode 100644 index 000000000..ab059edd9 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_ORM.png new file mode 100644 index 000000000..d89afed90 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackSetA_04_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_D.png new file mode 100644 index 000000000..4aa8e1750 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_N.png new file mode 100644 index 000000000..3db9e2eee Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_ORM.png new file mode 100644 index 000000000..5053fad07 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_RackShield_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsB_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsB_D.png new file mode 100644 index 000000000..11d15d797 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsB_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsC_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsC_D.png new file mode 100644 index 000000000..a079104c1 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsC_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsC_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsC_ORM.png new file mode 100644 index 000000000..cf9be8ddd Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_SignsC_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_D.png new file mode 100644 index 000000000..ca082dc8c Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_M.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_M.png new file mode 100644 index 000000000..567e57157 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_M.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_N.png new file mode 100644 index 000000000..2f97a4f5e Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_ORM.png new file mode 100644 index 000000000..dbb217e61 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_01_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_D.png new file mode 100644 index 000000000..198debb59 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_N.png new file mode 100644 index 000000000..8d488cf8d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_ORM.png new file mode 100644 index 000000000..f1f97e094 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallA_02_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_D.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_D.png new file mode 100644 index 000000000..1ffa3025f Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_D.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_N.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_N.png new file mode 100644 index 000000000..004b9996d Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_N.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_ORM.png b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_ORM.png new file mode 100644 index 000000000..4a4152915 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/SubUSDs/textures/T_WallDetails_ORM.png differ diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/bake.json b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/bake.json new file mode 100644 index 000000000..cafb3e4eb --- /dev/null +++ b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/bake.json @@ -0,0 +1,7 @@ +{ + "env_url": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1/Isaac/Environments/Simple_Warehouse/full_warehouse.usd", + "env_hash": "1bfe5688f17c", + "baked_at": "2026-05-22T16:28:39.018847+00:00", + "mesh_count": 3473, + "stage_scale": 1.0 +} \ No newline at end of file diff --git a/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/prepared_scene.usd b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/prepared_scene.usd new file mode 100644 index 000000000..abee9e194 Binary files /dev/null and b/simulation/isaac-sim/assets/scenes/local/1bfe5688f17c/prepared_scene.usd differ diff --git a/simulation/isaac-sim/config/sim_to_robot_bridge.yaml b/simulation/isaac-sim/config/sim_to_robot_bridge.yaml deleted file mode 100644 index 40ec50f85..000000000 --- a/simulation/isaac-sim/config/sim_to_robot_bridge.yaml +++ /dev/null @@ -1,111 +0,0 @@ -# ROS 2 Domain Bridge Configuration for AirStack Isaac Sim Integration -# This bridges topics from simulation domain (100) to robot domain (1) -# -# Simulation topics (domain 100) - namespaced for Isaac Sim: -# /robot_1/sensors/gps -# /robot_1/sensors/gps_twist -# /robot_1/sensors/imu -# /robot_1/sensors/mag -# /robot_1/state/accel -# /robot_1/state/pose -# /robot_1/state/twist -# /robot_1/state/twist_inertial -# /clock -# /tf -# /tf_static -# -# Robot domain topics (domain 1): -# Same topic names - robot services expect namespaced topics -# -# TF frames remain non-namespaced to match static_transforms.launch.xml -# -# Usage: -# domain_bridge /config/sim_to_robot_bridge.yaml - -name: airstack_sim_domain_bridge -from_domain: 100 # Simulation domain -to_domain: 1 # Robot domain - -topics: - # GPS data - /robot_1/sensors/gps: - type: sensor_msgs/msg/NavSatFix - from_domain: 100 - to_domain: 1 - - # GPS twist/velocity - /robot_1/sensors/gps_twist: - type: geometry_msgs/msg/TwistStamped - from_domain: 100 - to_domain: 1 - - # IMU data - /robot_1/sensors/imu: - type: sensor_msgs/msg/Imu - from_domain: 100 - to_domain: 1 - - # Magnetometer data - /robot_1/sensors/mag: - type: sensor_msgs/msg/MagneticField - from_domain: 100 - to_domain: 1 - - # Robot state - acceleration - /robot_1/state/accel: - type: geometry_msgs/msg/AccelStamped - from_domain: 100 - to_domain: 1 - - # Robot state - pose - /robot_1/state/pose: - type: geometry_msgs/msg/PoseStamped - from_domain: 100 - to_domain: 1 - - # Robot state - twist (body frame) - /robot_1/state/twist: - type: geometry_msgs/msg/TwistStamped - from_domain: 100 - to_domain: 1 - - # Robot state - twist inertial frame - /robot_1/state/twist_inertial: - type: geometry_msgs/msg/TwistStamped - from_domain: 100 - to_domain: 1 - - # TF transforms (important for camera frames) - /tf: - type: tf2_msgs/msg/TFMessage - from_domain: 100 - to_domain: 1 - - /tf_static: - type: tf2_msgs/msg/TFMessage - from_domain: 100 - to_domain: 1 - - # Clock for simulation time synchronization - /clock: - type: rosgraph_msgs/msg/Clock - from_domain: 100 - to_domain: 1 - -# Services (if needed) -# services: -# /robot_1/some_service: -# type: some_msgs/srv/SomeService -# from_domain: 100 -# to_domain: 1 - -# Notes: -# - Start simulation with ROS_DOMAIN_ID=100 (set in docker-compose.yaml) -# - Start domain bridge with: domain_bridge /config/sim_to_robot_bridge.yaml -# - Monitor topics in robot domain: export ROS_DOMAIN_ID=1 && ros2 topic list -# - Monitor topics in simulation domain: export ROS_DOMAIN_ID=100 && ros2 topic list -# -# For testing without domain bridge: -# - Set ROS_DOMAIN_ID=1 in docker-compose.yaml (comment out ROS_DOMAIN_ID=100) -# - Don't run the domain bridge -# - Both simulation and robot will communicate directly on domain 1 diff --git a/simulation/isaac-sim/docker/.bashrc b/simulation/isaac-sim/docker/.bashrc index 307b84d70..a214fbee1 100644 --- a/simulation/isaac-sim/docker/.bashrc +++ b/simulation/isaac-sim/docker/.bashrc @@ -110,8 +110,10 @@ tmux source ~/.tmux.conf if [ ! -h ~/.bash_history ]; then # File is not a symlink rm ~/.bash_history || echo "No existing .bash_history to remove" - # initialize .bash_history file if doesn't exist yet - if [ ! -d ~/.dev/.bash_history ]; then + # Seed .bash_history only when the file doesn't exist yet (-f, not -d: + # the old directory test was always false for a file, so every shell + # start clobbered the persisted history with the init seed). + if [ ! -f ~/.dev/.bash_history ]; then cp ~/.dev/.bash_history_init ~/.dev/.bash_history fi # symlink to ~/.dev/.bash_history @@ -134,7 +136,9 @@ export ROS_AUTOMATIC_DISCOVERY_RANGE=SUBNET export ISAAC_SIM_PYTHONPATH=$(echo "${PYTHONPATH:-}" | tr ':' '\n' | grep -v 'lib/python3.12/site-packages' | paste -sd ':' -):/isaac-sim/exts/isaacsim.ros2.bridge/jazzy/rclpy # --- Isaac Setup --- -alias runapp="/isaac-sim/runapp.sh --path omniverse://airlab-nucleus.andrew.cmu.edu/Library/Assets/Ascent_Aerosystems/Spirit_UAV/spirit_uav_red_yellow.prop.usd" +# Opens the plain Isaac Sim GUI (no scene). AirStack scenes are launched via +# the launch_scripts/ standalone path instead — see the compose command. +alias runapp="/isaac-sim/runapp.sh" alias runheadless.native=/isaac-sim/runheadless.native.sh alias runheadless.webrtc=/isaac-sim/runheadless.webrtc.sh diff --git a/simulation/isaac-sim/docker/.dev/.bash_history_init b/simulation/isaac-sim/docker/.dev/.bash_history_init index 929c68700..b69607686 100644 --- a/simulation/isaac-sim/docker/.dev/.bash_history_init +++ b/simulation/isaac-sim/docker/.dev/.bash_history_init @@ -1,8 +1,5 @@ -tmux a +tmux attach -t isaac tmux ls -cd ~/ros_ws -ros2 launch gcs_bringup gcs.launch.xml -cws -bws -sws -bws --packages-select gcs_bringup \ No newline at end of file +PYTHONPATH="$ISAAC_SIM_PYTHONPATH" /isaac-sim/python.sh /isaac-sim/AirStack/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py --ext-folder ~/.local/share/ov/data/documents/Kit/shared/exts +tail -f /isaac-sim/.nvidia-omniverse/logs/Kit/Isaac-Sim*/*/kit_*.log +ros2 topic list diff --git a/simulation/isaac-sim/docker/Dockerfile.isaac-ros b/simulation/isaac-sim/docker/Dockerfile.isaac-ros index b03816015..69bbd8117 100644 --- a/simulation/isaac-sim/docker/Dockerfile.isaac-ros +++ b/simulation/isaac-sim/docker/Dockerfile.isaac-ros @@ -145,11 +145,6 @@ ENV ACCEPT_EULA="Y" # ENV ISAACSIM_PYTHON=/isaac-sim/python.sh RUN /isaac-sim/python.sh -m pip install --no-cache-dir -e /isaac-sim/.local/share/ov/data/documents/Kit/shared/exts/pegasus.simulator -# Installing OptiTrack NatNet emulator extension into Kit for plugin discovery. -COPY extensions/optitrack.natnet.emulator \ - /isaac-sim/.local/share/ov/data/documents/Kit/shared/exts/optitrack.natnet.emulator -RUN /isaac-sim/python.sh -m pip install --no-cache-dir -e /isaac-sim/.local/share/ov/data/documents/Kit/shared/exts/optitrack.natnet.emulator - # Install PX4 things RUN git clone --branch ${PX4_VERSION} --recursive https://github.com/PX4/PX4-Autopilot.git diff --git a/simulation/isaac-sim/docker/docker-compose.yaml b/simulation/isaac-sim/docker/docker-compose.yaml index a78382ddc..03d14ed67 100644 --- a/simulation/isaac-sim/docker/docker-compose.yaml +++ b/simulation/isaac-sim/docker/docker-compose.yaml @@ -35,7 +35,10 @@ services: privileged: true networks: airstack_network: - ipv4_address: 172.31.0.200 # required to not conflict with other default docker networks on the host machine + # INVARIANT: every sim service (isaac-sim, ms-airsim, simple-sim) + # binds this same fixed address — robot containers reach the sim at + # SIM_IP (default 172.31.0.200), so only one sim can run at a time. + ipv4_address: 172.31.0.200 env_file: - ./omni_pass.env # PX4 SITL parameter set, applied by rcS at boot. @@ -48,7 +51,29 @@ services: - PLAY_SIM_ON_START=${PLAY_SIM_ON_START} - NUM_ROBOTS=${NUM_ROBOTS:-1} - ENABLE_LIDAR=${ENABLE_LIDAR:-false} + # Fleet spawner input: fleet_spawn.py maps the robot-container + # path onto this container's /isaac-sim/AirStack checkout mount. + - FLEET_CONFIG_FILE=${FLEET_CONFIG_FILE:-} - ISAAC_SIM_HEADLESS=${ISAAC_SIM_HEADLESS:-false} + # Scene selection (`airstack up --scene ` → simulation/scenes.yaml): + # a Pegasus SIMULATION_ENVIRONMENTS key or a USD URL, resolved by the + # launch scripts (pegasus_app.resolve_scene_from_env). Empty = script default. + - ISAAC_SIM_SCENE=${ISAAC_SIM_SCENE:-} + - ISAAC_SIM_STAGE_SCALE=${ISAAC_SIM_STAGE_SCALE:-} + # Viewport follow-camera (pegasus_app.py): domain id of the drone to + # chase (default 1; off/none/0 disables) and its world-frame x,y,z + # offset in meters. + - ISAAC_SIM_FOLLOW_CAM=${ISAAC_SIM_FOLLOW_CAM:-} + - ISAAC_SIM_FOLLOW_CAM_OFFSET=${ISAAC_SIM_FOLLOW_CAM_OFFSET:-} + # Spawn-row center "x,y" in meters (default origin) — for scenes whose + # origin is cluttered; see launch_scripts/pegasus_app.py. + - ISAAC_SIM_SPAWN_XY=${ISAAC_SIM_SPAWN_XY:-} + # Dome light override "intensity[,exposure]" for dim indoor scenes. + - ISAAC_SIM_DOME_LIGHT=${ISAAC_SIM_DOME_LIGHT:-} + # Multiply the scene's own lights (office/hospital ceiling lights are + # authored dim); follow-cam headlight intensity for unlit interiors. + - ISAAC_SIM_LIGHT_BOOST=${ISAAC_SIM_LIGHT_BOOST:-} + - ISAAC_SIM_FOLLOW_CAM_LIGHT=${ISAAC_SIM_FOLLOW_CAM_LIGHT:-} # Pegasus physics tuning — read by pegasus/simulator/params.py - PX4_PHYSICS_HZ=${PX4_PHYSICS_HZ:-100} - ARDUPILOT_PHYSICS_HZ=${ARDUPILOT_PHYSICS_HZ:-800} @@ -75,8 +100,6 @@ services: - $HOME/docker/isaac-sim/pkg:/isaac-sim/.local/share/ov/pkg:rw \ # pegasus integration - ../extensions/PegasusSimulator/extensions/pegasus.simulator:/isaac-sim/.local/share/ov/data/documents/Kit/shared/exts/pegasus.simulator/:rw - # natnet emulator integration - - ../extensions/optitrack.natnet.emulator:/isaac-sim/.local/share/ov/data/documents/Kit/shared/exts/optitrack.natnet.emulator/:rw # omniverse - ./omniverse.toml:/isaac-sim/.nvidia-omniverse/config/omniverse.toml:rw - ./user.config.json:/isaac-sim/.local/share/ov/data/Kit/Isaac-Sim Full/5.1/user.config.json:rw # enables pegasus extension; IMPORTANT: set the version number without the trailing .0 @@ -91,6 +114,11 @@ services: - ../../../.devcontainer/isaac-sim/tasks.json:/isaac-sim/AirStack/.vscode/tasks.json:rw # =================================================================================================================== + # Full Isaac Sim GUI editor for USD/scene editing on any asset (runapp.sh, + # no Pegasus/ROS launch script). Deliberately NOT on airstack_network — no + # DDS reaches the robots, so it is for scene authoring, not for flying. + # Run with: airstack up --profile isaac-sim-gui isaac-sim-gui + # See docs/simulation/isaac_sim/docker.md ("GUI-Only Mode"). isaac-sim-gui: extends: service: isaac-sim @@ -152,7 +180,6 @@ services: - $HOME/docker/isaac-sim/data:/isaac-sim/.local/share/ov/data:rw - $HOME/docker/isaac-sim/pkg:/isaac-sim/.local/share/ov/pkg:rw - ../extensions/PegasusSimulator/extensions/pegasus.simulator:/isaac-sim/.local/share/ov/data/documents/Kit/shared/exts/pegasus.simulator/:rw - - ../extensions/optitrack.natnet.emulator:/isaac-sim/.local/share/ov/data/documents/Kit/shared/exts/optitrack.natnet.emulator/:rw - ./omniverse.toml:/isaac-sim/.nvidia-omniverse/config/omniverse.toml:rw - ./user.config.json:/isaac-sim/.local/share/ov/data/Kit/Isaac-Sim Full/5.1/user.config.json:rw - .dev:/isaac-sim/.dev:rw diff --git a/simulation/isaac-sim/docker/px4-params/external-vision.env b/simulation/isaac-sim/docker/px4-params/external-vision.env index bfd95a2fd..99b2a2f40 100644 --- a/simulation/isaac-sim/docker/px4-params/external-vision.env +++ b/simulation/isaac-sim/docker/px4-params/external-vision.env @@ -1,9 +1,11 @@ # PX4 SITL on external vision (OptiTrack mocap) instead of GPS. # # Select with PX4_PARAM_SET=external-vision. Mirrors the deployment-validated set in -# robot/ros_ws/src/perception/natnet_ros2/config/px4_params.yaml, which is the source of -# truth — px4_param_setter checks the FCU against it on the real robot. -# See docs/robot/px4_external_vision.md for what each parameter does. +# the asm_optitrack module's natnet_ros2/config/px4_params.yaml, which is the source of +# truth — the module's px4_param_setter checks the FCU against it on the real robot. +# See the asm_optitrack module docs (https://github.com/castacks/asm_optitrack) for +# what each parameter does. This file stays in trunk: it is generic PX4 +# external-vision fusion, usable by any mocap/EV source. PX4_PARAM_EKF2_EV_CTRL=11 # fuse vision horizontal position + vertical position + yaw PX4_PARAM_EKF2_HGT_REF=3 # vision is the height reference diff --git a/simulation/isaac-sim/docker/user_TEMPLATE.config.json b/simulation/isaac-sim/docker/user_TEMPLATE.config.json index 0ce7c11f0..ad9531910 100755 --- a/simulation/isaac-sim/docker/user_TEMPLATE.config.json +++ b/simulation/isaac-sim/docker/user_TEMPLATE.config.json @@ -7,7 +7,7 @@ }, "isaac": { "asset_root": { - "default": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/4.5", + "default": "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1", "timeout": 5.0 } }, diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/.gitignore b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/.gitignore deleted file mode 100644 index adef4d964..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# OptiTrack SDK archives and build artifacts (reference tree may exist locally) -**/*.obj -**/*.pdb -**/*.exe -**/*.iobj -**/*.ipdb -**/*.tlog/ -**/__pycache__/ -**/*.pyc -# Generated by the editable install (pip install -e) the Dockerfile and tests use. -**/*.egg-info/ diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/README.md b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/README.md deleted file mode 100644 index cd822ac5f..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# OptiTrack NatNet Emulator (Isaac Sim Extension) - -Python NatNet **server** emulator for AirStack simulation and integration testing with [`natnet_ros2`](../../../../robot/ros_ws/src/perception/natnet_ros2/). - -The extension has two layers: - -1. **Transport + protocol** (`optitrack.natnet.emulator.server`) — UDP NatNet server, ctypes wire types, MODELDEF cache, frame streaming. Importable outside Isaac Sim (unit tests, host-side integration). -2. **Isaac integration** (`optitrack.natnet.emulator.isaac`) — stage-driven `/World/NatNetInterface` config prim, pose sampling on physics steps, Kit UI editor, and Pegasus launch-script helpers. - -## Layout - -``` -optitrack.natnet.emulator/ -├── config/extension.toml # Kit manifest (server module + UI entry point) -├── schema/schema.usda # Typed NatNet interface attribute definitions -├── setup.py -├── docs/ # (legacy design notes — see docs/simulation/isaac_sim/natnet_emulator.md) -├── test/ # Co-located unit tests (listed in colcon_unit_test_packages.yaml) -└── optitrack/natnet/emulator/ - ├── defaults.py # Reference Drone → prim bindings for tests - ├── server/ # NatNet UDP server (transport + protocol) - │ ├── natnet_server.py # Base server, queue, MODELDEF cache - │ ├── natnet_unicast_server.py - │ ├── natnet_data_types.py - │ ├── natnet_model_types.py - │ └── natnet_server_types.py - └── isaac/ # Isaac Sim wrapper (Kit + USD) - ├── config.py # Pure-Python NatNetInterfaceConfig model - ├── usd_bindings.py # Author/read interface prims on a stage - ├── catalog.py # Config → sDataDescriptions (MODELDEF) - ├── frames.py # Prim poses → sFrameOfMocapData - ├── manager.py # NatNetServerManager (lifecycle + sampling) - ├── scene_setup.py # Pegasus launch helpers (author_drone_natnet_interface) - └── ui_extension.py # Docked editor panel (NatNetEmulatorExtension) -``` - -## Responsibilities - -| Layer | Role | -|-------|------| -| **Server** | UDP transport; `NAT_CONNECT` / `NAT_SERVERINFO`; `NAT_REQUEST_MODELDEF`; `NAT_KEEPALIVE`; `NAT_ECHOREQUEST` / `NAT_ECHORESPONSE`; `NAT_FRAMEOFDATA` on the **data port** (1511). MODELDEF stored as packed bytes via `set_model_def_payload()`. Frames enqueued with `enqueue_mocap_data()`. | -| **Isaac wrapper** | Authors and reads the NatNet interface config prim; builds MODELDEF from scene config; samples tracked prim world poses each physics step; calls `flush_mocap_data()` synchronously (background timer disabled — see below). | -| **`defaults.py`** | Hardcoded `Drone` → `/World/base_link` binding for legacy tests; production paths use the stage prim via `scene_setup.build_drone_config()`. | - -The server does **not** own prim-path bindings. The Isaac layer calls `set_model_def_payload(catalog.pack())` after building `sDataDescriptions` from the interface config. - -## Stage-driven config prim - -Configuration lives on a USD prim (conventionally `/World/NatNetInterface`) with `natnet:*` attributes: - -- Server: IP, unicast/multicast mode, command/data ports, publish rate, NatNet version, up-axis, optional pose noise. -- Bodies: multi-apply `natnet:body::*` fields mapping rigid-body name / streaming ID → target prim path. - -`NatNetServerManager` scans the stage, resyncs the catalog when the prim changes, and streams one rigid body per configured target. Missing prims emit **lost** bodies (NaN position, tracking-invalid bit clear) until the target appears — important for Pegasus drones spawned on first Play. - -**Up axis:** default `Z` passes Isaac/USD world poses through unchanged (matches `natnet_ros2`). Set `Y` to emulate a Y-up Motive room. - -## Streaming model (Isaac) - -Inside Kit, the server's background `_data_update_loop` is **disabled** (`auto_stream = False`) because the GIL-starved daemon thread does not reliably transmit frames. Instead, each physics step: - -1. `NatNetServerManager.sample_once()` reads prim poses and `enqueue_mocap_data(frame)`. -2. `NatNetUnicastServer.flush_mocap_data()` sends immediately on the physics-step thread. - -Outside Isaac (host unit tests), `auto_stream=True` uses the timer-driven loop. - -Default Docker sim IP: **`172.31.0.200`** (Isaac container on the AirStack bridge network). - -## Enabling in AirStack - -**Robot:** `LAUNCH_NATNET=true` in `.env` → `natnet_ros2` in perception bringup. Configure Motive/emulator IP in [`natnet_config.yaml`](../../../../robot/ros_ws/src/perception/natnet_ros2/config/natnet_config.yaml). - -**Isaac Sim:** set `ISAAC_SIM_SCRIPT_NAME` to a NatNet launch script (NatNet always starts — no `LAUNCH_NATNET` gate in the script): - -| Script | Use | -|--------|-----| -| `example_one_px4_pegasus_natnet_launch_script.py` | Single drone + static `Target` | -| `example_multi_px4_pegasus_natnet_launch_script.py` | `NUM_ROBOTS` drones + shared `Target` (system tests with NatNet use this even for `NUM_ROBOTS=1`) | - -Baseline Pegasus scripts (`example_one_px4_pegasus_launch_script.py`, `example_multi_px4_pegasus_launch_script.py`) have **no** NatNet integration. - -Convenience bundle for NatNet + external-vision PX4 SITL: - -```bash -airstack up --env-file overrides/isaac-optitrack-simulation.env -``` - -See [optitrack-development skill](../../../../.agents/skills/optitrack-development/SKILL.md) for wire-protocol details, libNatNet 4.4 unicast quirks, and debugging. - -## Usage - -### Server only (no Kit) - -```python -from optitrack.natnet.emulator import NatNetUnicastServer, make_default_drone_catalog -from optitrack.natnet.emulator.isaac.frames import BodySample, build_frame - -server = NatNetUnicastServer(local_interface="172.31.0.200") -server.set_model_def_payload(make_default_drone_catalog().pack()) -server.start() - -frame = build_frame(0, [BodySample(1, (0, 0, 1), (0, 0, 0, 1))]) -server.enqueue_mocap_data(frame) -server.flush_mocap_data() -``` - -### Isaac launch script - -```python -from isaacsim.core.utils.extensions import enable_extension - -# Register the extension with Kit before importing from it. -enable_extension("optitrack.natnet.emulator") - -from optitrack.natnet.emulator.isaac import author_drone_natnet_interface - -# Author the interface prim; the extension starts the server on Play. -author_drone_natnet_interface( - stage, - drones=[("Drone", 1, "/World/drone1/base_link/body")], - server_ip="172.31.0.200", -) -``` - -### Kit UI - -The extension registers **Window → NatNet Emulator** — a docked panel to create/edit the interface prim and view live body readouts. The extension owns the `NatNetServerManager`, building the server from the prim on Play and shutting it down on Stop. - -## Protocol notes (unicast, libNatNet 4.4) - -| Port | Traffic | -|------|---------| -| **1510** | Command: `NAT_CONNECT`, `NAT_REQUEST_MODELDEF`, keepalives, echo | -| **1511** | Data: `NAT_FRAMEOFDATA` — **must** be sent from a socket bound to the data port | - -Frames sent from the command socket are silently dropped by libNatNet. Every frame payload must include the 4-byte end-of-data tag expected by the C SDK unpacker. - -Full handshake layouts and sniffing workflow: [optitrack-development skill](../../../../.agents/skills/optitrack-development/SKILL.md). - -## Tests - -| Tier | Mark | What | -|------|------|------| -| Unit | `unit` | Serializers, protocol, config, USD authoring, catalog, pose sampling, server lifecycle, scene setup | -| Integration | `integration` | Host emulator → robot `natnet_ros2` pose Hz | - -Co-located tests live in `test/`. The root harness collects them via the `sim:` key in [`colcon_unit_test_packages.yaml`](../../../../tests/colcon_unit_test_packages.yaml). - -```bash -# Unit (no Docker / no SDK) -airstack test -m unit -v - -# Integration (robot container + NatNet SDK) -pytest tests/integration/natnet/ -m integration -v -``` - -Representative unit modules: `test_unicast_protocol.py`, `test_pose_streaming.py`, `test_interface_authoring.py`, `test_server_lifecycle.py`, `test_scene_setup.py`. - -## Reference material - -- User guide: [`docs/simulation/isaac_sim/natnet_emulator.md`](../../../../docs/simulation/isaac_sim/natnet_emulator.md) -- Robot client: [`natnet_ros2/README.md`](../../../../robot/ros_ws/src/perception/natnet_ros2/README.md) -- Integration tier: [`tests/integration/natnet/README.md`](../../../../tests/integration/natnet/README.md) - -OptiTrack SDK sample headers may exist locally under `NatNetClientSDK/` for wire-format reference; they are **not** redistributed by AirStack (proprietary license). diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/config/extension.toml b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/config/extension.toml deleted file mode 100644 index a5df394da..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/config/extension.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -version = "0.1.0" -title = "OptiTrack NatNet Emulator" -description = "NatNet UDP server emulator for Isaac Sim integration with natnet_ros2" -category = "Simulation" -keywords = ["optitrack", "natnet", "mocap", "simulation"] - -[dependencies] -"omni.isaac.core" = {} -"omni.usd" = {} -"omni.ui" = {} -"omni.kit.menu.utils" = {} - -# Pure transport/types package (no Kit UI; safe to import anywhere). -[[python.module]] -name = "optitrack.natnet.emulator" - -# Kit UI entry point: NatNetEmulatorExtension (menu + config-prim authoring window). -[[python.module]] -name = "optitrack.natnet.emulator.isaac.ui_extension" - -[python.build-system] -requires = ["setuptools"] diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/__init__.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/__init__.py deleted file mode 100644 index 39ed38144..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""OptiTrack NatNet packages for AirStack Isaac Sim integration.""" diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/__init__.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/__init__.py deleted file mode 100644 index b19da2cbc..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""NatNet simulation components.""" diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/__init__.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/__init__.py deleted file mode 100644 index e819d8b1b..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -"""OptiTrack Motive NatNet emulator for Isaac Sim.""" - -from .defaults import ( - DEFAULT_DRONE_BINDING, - DEFAULT_TRACKED_BODY_BINDINGS, - TrackedBodyBinding, -) -from .server import Client, NatNetServer, NatNetUnicastServer, TransmissionType -from .server.natnet_model_types import make_default_drone_catalog - -__all__ = [ - "Client", - "DEFAULT_DRONE_BINDING", - "DEFAULT_TRACKED_BODY_BINDINGS", - "NatNetServer", - "NatNetUnicastServer", - "TrackedBodyBinding", - "TransmissionType", - "make_default_drone_catalog", -] diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/defaults.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/defaults.py deleted file mode 100644 index e888a6eff..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/defaults.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Reference tracked-body defaults for tests and Isaac Sim wrapper.""" - -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass(frozen=True) -class TrackedBodyBinding: - """Maps a NatNet rigid body to a USD prim path (not sent on the NatNet wire).""" - - name: str - id: int - prim_path: str - parent_id: int = -1 - - -# Single-drone NatNet Pegasus scenes (example_one_px4_pegasus_natnet_launch_script.py). -DEFAULT_DRONE_BINDING = TrackedBodyBinding( - name="Drone", - id=1, - prim_path="/World/base_link", -) - -DEFAULT_TRACKED_BODY_BINDINGS: tuple[TrackedBodyBinding, ...] = (DEFAULT_DRONE_BINDING,) diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/__init__.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/__init__.py deleted file mode 100644 index e28f9e412..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Isaac Sim integration for the NatNet emulator (stage-driven config prim). - -``config`` is pure Python. ``usd_bindings`` imports ``pxr`` lazily, so -importing this package is safe in non-Isaac environments. -""" - -from .config import ( - BodyBinding, - NatNetInterfaceConfig, - body_attr_name, - make_instance_key, -) -from .catalog import build_catalog, find_duplicate_targets -from .frames import BodySample, build_frame, make_rigid_body_data -from .manager import NatNetServerManager, default_server_factory, format_interface -from .scene_setup import ( - DEFAULT_INTERFACE_PATH, - DEFAULT_TARGET_PATH, - DEFAULT_TARGET_POSITION, - DEFAULT_TARGET_STREAMING_ID, - author_drone_natnet_interface, - author_static_target, - build_drone_config, -) -from .usd_bindings import ( - author_interface, - find_interfaces, - is_interface, - read_interface, - read_world_pose, - resolve_targets, -) - -__all__ = [ - "DEFAULT_INTERFACE_PATH", - "DEFAULT_TARGET_PATH", - "DEFAULT_TARGET_POSITION", - "DEFAULT_TARGET_STREAMING_ID", - "BodyBinding", - "BodySample", - "NatNetInterfaceConfig", - "NatNetServerManager", - "author_interface", - "author_drone_natnet_interface", - "author_static_target", - "body_attr_name", - "build_catalog", - "build_drone_config", - "build_frame", - "default_server_factory", - "find_duplicate_targets", - "find_interfaces", - "format_interface", - "is_interface", - "make_instance_key", - "make_rigid_body_data", - "read_interface", - "read_world_pose", - "resolve_targets", -] diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/catalog.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/catalog.py deleted file mode 100644 index 9c3bfaa35..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/catalog.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -""" -Turn a :class:`NatNetInterfaceConfig` into the server's MODELDEF catalog -(``sDataDescriptions`` of rigid bodies). Pure Python + ctypes (the ``server`` -package is stdlib-only), so this is hermetically unit-testable — no USD, no Kit. -""" - -from __future__ import annotations - -from ..server.natnet_common import ModelLimits -from ..server.natnet_model_types import DataDescriptors, sDataDescriptions -from .config import NatNetInterfaceConfig - -# szName is null-terminated on the wire; reserve one byte for the terminator. -_MAX_NAME_BYTES = int(ModelLimits.MAX_NAMELENGTH) - 1 -_MAX_MODELS = int(ModelLimits.MAX_MODELS) - - -def build_catalog(config: NatNetInterfaceConfig) -> sDataDescriptions: - """Build an ``sDataDescriptions`` rigid-body catalog from the config bodies. - - No bodies -> an empty catalog (``nDataDescriptions == 0``). Names longer than - the NatNet name field are truncated. Raises ``ValueError`` if there are more - bodies than the protocol allows. - """ - bodies = config.bodies - if len(bodies) > _MAX_MODELS: - raise ValueError( - f"Too many bodies for one catalog: {len(bodies)} > {_MAX_MODELS} (MAX_MODELS)" - ) - - descriptions = sDataDescriptions() - descriptions.nDataDescriptions = len(bodies) - for i, body in enumerate(bodies): - desc = descriptions.arrDataDescriptions[i] - desc.type = int(DataDescriptors.Descriptor_RigidBody) - rb = desc.RigidBodyDescription - rb.szName = body.rigid_body_name.encode("utf-8")[:_MAX_NAME_BYTES] - rb.ID = int(body.streaming_id) - rb.parentID = int(body.parent_id) - rb.offsetqw = 1.0 # identity quaternion offset - rb.nMarkers = 0 - return descriptions - - -def find_duplicate_targets(config: NatNetInterfaceConfig) -> list[str]: - """Return target prim paths referenced by more than one body (empties ignored).""" - counts: dict[str, int] = {} - for body in config.bodies: - if body.target_prim: - counts[body.target_prim] = counts.get(body.target_prim, 0) + 1 - return [path for path, count in counts.items() if count > 1] diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/config.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/config.py deleted file mode 100644 index 6b6943ded..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/config.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Pure-Python config model for the stage-driven NatNet interface. - -The USD binding layer (author/read against a ``Usd.Stage``) -lives in ``usd_bindings.py`` and depends on this model. - -Attribute names follow the multi-apply schema convention -(``natnet:body::``). -The custom-attribute backing is for a future typed applied schema. -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any, Iterable, Mapping - -# --- attribute name constants (USD property names) ----------------------------- - -ATTR_NAMESPACE = "natnet" -MARKER_ATTR = "natnet:isInterface" - -ATTR_SERVER_ENABLED = "natnet:serverEnabled" -ATTR_SERVER_IP = "natnet:serverIp" -ATTR_MODE = "natnet:mode" -ATTR_MULTICAST_ADDR = "natnet:multicastAddr" -ATTR_COMMAND_PORT = "natnet:commandPort" -ATTR_DATA_PORT = "natnet:dataPort" -ATTR_PUBLISH_RATE = "natnet:publishRate" -ATTR_NATNET_VERSION = "natnet:natnetVersion" -ATTR_UP_AXIS = "natnet:upAxis" - -ATTR_POSE_NOISE_ENABLED = "natnet:poseNoiseEnabled" -ATTR_POSE_NOISE_STD_METERS = "natnet:poseNoiseStdMeters" -ATTR_POSE_NOISE_ROTATION_DEG = "natnet:poseNoiseRotationDeg" - -BODY_PREFIX = "natnet:body:" -BODY_FIELD_RIGID_BODY_NAME = "rigidBodyName" -BODY_FIELD_STREAMING_ID = "streamingId" -BODY_FIELD_PARENT_ID = "parentId" -BODY_FIELD_TARGET = "target" - -VALID_MODES = ("unicast", "multicast") - -# Streamed up-axis. "Z" (default) passes the USD pose through; "Y" emulates a Y-up -# Motive by rotating the streamed pose -90deg about X. -VALID_UP_AXES = ("Y", "Z") - -# defaults shared with NatNetUnicastServer -DEFAULT_SERVER_IP = "172.31.0.200" -DEFAULT_MULTICAST_ADDR = "239.255.42.99" -DEFAULT_COMMAND_PORT = 1510 -DEFAULT_DATA_PORT = 1511 -DEFAULT_PUBLISH_RATE = 100.0 -DEFAULT_NATNET_VERSION = "4.4.0.0" -DEFAULT_UP_AXIS = "Z" -DEFAULT_POSE_NOISE_ENABLED = True -DEFAULT_POSE_NOISE_STD_METERS = 0.0005 -DEFAULT_POSE_NOISE_ROTATION_DEG = 0.05 - - -def body_attr_name(key: str, field_name: str) -> str: - """USD property name for a body-binding field on the given instance key.""" - return f"{BODY_PREFIX}{key}:{field_name}" - - -def make_instance_key(name: str, used: set[str]) -> str: - """Derive a valid, unique multi-apply instance token from a rigid body name. - - USD property/instance tokens must be identifier-like; sanitize non-alnum chars - to underscores and disambiguate collisions with a numeric suffix. - """ - sanitized = "".join(c if c.isalnum() else "_" for c in name).strip("_") - if not sanitized: - sanitized = "body" - if sanitized[0].isdigit(): - sanitized = f"b_{sanitized}" - key = sanitized - i = 1 - while key in used: - key = f"{sanitized}_{i}" - i += 1 - used.add(key) - return key - - -@dataclass -class BodyBinding: - """One tracked rigid body: a Motive name/ID mapped to a USD prim path.""" - - rigid_body_name: str - target_prim: str - streaming_id: int = 1 - parent_id: int = -1 - - @classmethod - def from_dict(cls, data: Mapping[str, Any], *, target_prim: str | None = None) -> "BodyBinding": - d = dict(data) - resolved_target = target_prim if target_prim is not None else d.get("target_prim") - if not resolved_target: - raise ValueError("BodyBinding requires a target_prim (USD path of the tracked prim)") - if "rigid_body_name" not in d: - raise ValueError("BodyBinding requires a rigid_body_name") - return cls( - rigid_body_name=str(d["rigid_body_name"]), - target_prim=str(resolved_target), - streaming_id=int(d.get("streaming_id", 1)), - parent_id=int(d.get("parent_id", -1)), - ) - - def to_dict(self) -> dict[str, Any]: - return { - "rigid_body_name": self.rigid_body_name, - "target_prim": self.target_prim, - "streaming_id": self.streaming_id, - "parent_id": self.parent_id, - } - - -def _normalize_bodies(bodies: Any) -> list[BodyBinding]: - """Accept a list of dicts/BodyBindings, or a ``{prim_path: {...}}`` mapping.""" - if bodies is None: - return [] - out: list[BodyBinding] = [] - if isinstance(bodies, Mapping): - for prim_path, body in bodies.items(): - out.append(BodyBinding.from_dict(body, target_prim=prim_path)) - return out - if isinstance(bodies, Iterable): - for body in bodies: - if isinstance(body, BodyBinding): - out.append(body) - else: - out.append(BodyBinding.from_dict(body)) - return out - raise ValueError(f"`bodies` must be a list or a mapping, got {type(bodies).__name__}") - - -@dataclass -class NatNetInterfaceConfig: - """Server-level config plus the body catalog for one NatNet interface prim.""" - - server_enabled: bool = True - server_ip: str = DEFAULT_SERVER_IP - mode: str = "unicast" - multicast_addr: str = DEFAULT_MULTICAST_ADDR - command_port: int = DEFAULT_COMMAND_PORT - data_port: int = DEFAULT_DATA_PORT - publish_rate: float = DEFAULT_PUBLISH_RATE - natnet_version: str = DEFAULT_NATNET_VERSION - up_axis: str = DEFAULT_UP_AXIS - pose_noise_enabled: bool = DEFAULT_POSE_NOISE_ENABLED - pose_noise_std_meters: float = DEFAULT_POSE_NOISE_STD_METERS - pose_noise_rotation_deg: float = DEFAULT_POSE_NOISE_ROTATION_DEG - bodies: list[BodyBinding] = field(default_factory=list) - - @classmethod - def from_dict(cls, data: Mapping[str, Any]) -> "NatNetInterfaceConfig": - d = dict(data) - return cls( - server_enabled=bool(d.get("server_enabled", True)), - server_ip=str(d.get("server_ip", DEFAULT_SERVER_IP)), - mode=str(d.get("mode", "unicast")), - multicast_addr=str(d.get("multicast_addr", DEFAULT_MULTICAST_ADDR)), - command_port=int(d.get("command_port", DEFAULT_COMMAND_PORT)), - data_port=int(d.get("data_port", DEFAULT_DATA_PORT)), - publish_rate=float(d.get("publish_rate", DEFAULT_PUBLISH_RATE)), - natnet_version=str(d.get("natnet_version", DEFAULT_NATNET_VERSION)), - up_axis=str(d.get("up_axis", DEFAULT_UP_AXIS)).upper(), - pose_noise_enabled=bool(d.get("pose_noise_enabled", DEFAULT_POSE_NOISE_ENABLED)), - pose_noise_std_meters=float(d.get("pose_noise_std_meters", DEFAULT_POSE_NOISE_STD_METERS)), - pose_noise_rotation_deg=float(d.get("pose_noise_rotation_deg", DEFAULT_POSE_NOISE_ROTATION_DEG)), - bodies=_normalize_bodies(d.get("bodies")), - ) - - def to_dict(self) -> dict[str, Any]: - return { - "server_enabled": self.server_enabled, - "server_ip": self.server_ip, - "mode": self.mode, - "multicast_addr": self.multicast_addr, - "command_port": self.command_port, - "data_port": self.data_port, - "publish_rate": self.publish_rate, - "natnet_version": self.natnet_version, - "up_axis": self.up_axis, - "pose_noise_enabled": self.pose_noise_enabled, - "pose_noise_std_meters": self.pose_noise_std_meters, - "pose_noise_rotation_deg": self.pose_noise_rotation_deg, - "bodies": [b.to_dict() for b in self.bodies], - } - - def validate(self) -> "NatNetInterfaceConfig": - """Raise ``ValueError`` (aggregating all problems) if the config is invalid.""" - errors: list[str] = [] - if self.mode not in VALID_MODES: - errors.append(f"mode must be one of {VALID_MODES}, got {self.mode!r}") - if str(self.up_axis).upper() not in VALID_UP_AXES: - errors.append(f"up_axis must be one of {VALID_UP_AXES}, got {self.up_axis!r}") - for port_name, port in (("command_port", self.command_port), ("data_port", self.data_port)): - if not (0 < port < 65536): - errors.append(f"{port_name} must be in 1..65535, got {port}") - if self.command_port == self.data_port: - errors.append("command_port and data_port must differ") - if self.publish_rate <= 0: - errors.append(f"publish_rate must be > 0, got {self.publish_rate}") - if self.pose_noise_std_meters < 0: - errors.append( - f"pose_noise_std_meters must be >= 0, got {self.pose_noise_std_meters}" - ) - if self.pose_noise_rotation_deg < 0: - errors.append( - f"pose_noise_rotation_deg must be >= 0, got {self.pose_noise_rotation_deg}" - ) - for i, body in enumerate(self.bodies): - if not body.rigid_body_name: - errors.append(f"body[{i}] rigid_body_name must be non-empty") - names = [b.rigid_body_name for b in self.bodies] - if len(set(names)) != len(names): - errors.append("rigid_body_name values must be unique across bodies") - ids = [b.streaming_id for b in self.bodies] - if len(set(ids)) != len(ids): - errors.append("streaming_id values must be unique across bodies") - if errors: - raise ValueError("Invalid NatNetInterfaceConfig: " + "; ".join(errors)) - return self - - def assign_instance_keys(self) -> list[tuple[str, BodyBinding]]: - """Pair each body with a deterministic, unique multi-apply instance key.""" - used: set[str] = set() - return [(make_instance_key(b.rigid_body_name, used), b) for b in self.bodies] diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/frames.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/frames.py deleted file mode 100644 index 2c92a99ce..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/frames.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Pose -> NatNet frame conversion (the data-enqueue path). - -Pure Python + ctypes. Sampled prim world poses become an -``sFrameOfMocapData`` of rigid bodies that the server -streams to the client. - -**Frame convention:** Motive exposes an "Up Axis" setting. AirStack's ``natnet_ros2`` -requires it set to **Z** and copy the rigid-body pose straight through -(``rb_to_pose`` is an identity copy). Isaac Sim coordinates are Z-up. -The default ``up_axis="Z"`` emits the prim's USD world pose as-is. -``up_axis="Y"`` emulates a default (Y-up) Motive by rotating the pose -90 deg about X. - -**params bits** (must match the client's ``is_tracking_valid`` / ``model_list_changed``): -- ``0x01`` on a rigid body marks tracking valid — the client *skips* bodies without it. -- ``0x02`` on the frame signals the model list changed so the client re-requests MODELDEF(set the frame after the catalog changes, e.g. a body added live). -""" - -from __future__ import annotations - -import math -import numpy as np -from dataclasses import dataclass -from scipy.spatial.transform import Rotation -from ..server.natnet_data_types import sFrameOfMocapData, sRigidBodyData - -TRACKING_VALID = 0x01 -MODEL_LIST_CHANGED = 0x02 - - -@dataclass -class BodySample: - """One sampled rigid body: streaming ID + world pose, or an invalid (lost) body.""" - - streaming_id: int - position: tuple[float, float, float] = (0.0, 0.0, 0.0) - orientation: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0) # qx,qy,qz,qw - valid: bool = True - - @classmethod - def lost(cls, streaming_id: int) -> "BodySample": - """An untracked body (missing prim): NaN position, tracking-invalid bit clear.""" - nan = float("nan") - return cls(streaming_id, (nan, nan, nan), (0.0, 0.0, 0.0, 1.0), valid=False) - - -def to_motive_pose(position: tuple[float, float, float], orientation: tuple[float, float, float, float], up_axis: str = "Z"): - """Re-express an Isaac (Z-up) world pose in Motive's streamed up-axis frame. - - Returns ``(position, orientation)`` re-axed for the given ``up_axis``: - - - ``"Z"`` (default) — identity pass-through. Isaac/USD is Z-up and the - reference Motive setup streams Z-up, so the pose flows through unchanged and - matches ``natnet_ros2`` (which does no axis conversion). - - ``"Y"`` — emulate a default Y-up Motive by rotating the pose -90 deg about X - (Isaac ``+Z`` -> Motive ``+Y``): ``(x, y, z) -> (x, z, -y)``. This is a - proper right-handed -> right-handed change of basis (det = +1), so the - quaternion's vector part takes the same swap and the scalar part is - unchanged: ``(qx, qy, qz, qw) -> (qx, qz, -qy, qw)``. - - Non-finite components (a lost body's NaN position) pass through unchanged. - """ - if str(up_axis).upper() != "Y": - return position, orientation - x, y, z = position - qx, qy, qz, qw = orientation - return (x, z, -y), (qx, qz, -qy, qw) - - -def make_rigid_body_data(sample: BodySample) -> sRigidBodyData: - """Build one ``sRigidBodyData`` from a sample (sets the tracking-valid bit).""" - rb = sRigidBodyData() - rb.ID = int(sample.streaming_id) - x, y, z = sample.position - qx, qy, qz, qw = sample.orientation - rb.x, rb.y, rb.z = float(x), float(y), float(z) - rb.qx, rb.qy, rb.qz, rb.qw = float(qx), float(qy), float(qz), float(qw) - rb.MeanError = 0.0 - rb.params = TRACKING_VALID if sample.valid else 0 - return rb - - -def build_frame( - frame_number: int, - samples, - *, - timestamp: float = 0.0, - model_list_changed: bool = False, -) -> sFrameOfMocapData: - """Assemble an ``sFrameOfMocapData`` of rigid bodies from samples.""" - frame = sFrameOfMocapData() - frame.iFrame = int(frame_number) - samples = list(samples) - frame.nRigidBodies = len(samples) - for i, sample in enumerate(samples): - frame.RigidBodies[i] = make_rigid_body_data(sample) - frame.fTimestamp = float(timestamp) - frame.params = MODEL_LIST_CHANGED if model_list_changed else 0 - return frame - - -def is_finite_pose(sample: BodySample) -> bool: - """True if all position/orientation components are finite (no NaN/inf).""" - return all(math.isfinite(v) for v in (*sample.position, *sample.orientation)) - - -def apply_pose_noise( - position: tuple[float, float, float], - orientation: tuple[float, float, float, float], - pose_noise_std_meters: float, - pose_noise_rotation_deg: float, -) -> tuple[tuple[float, float, float], tuple[float, float, float, float]]: - """Add independent Gaussian noise to position (m) and orientation (deg, XYZ euler).""" - - x, y, z = position - if pose_noise_std_meters > 0.0: - x += np.random.normal(0, pose_noise_std_meters) - y += np.random.normal(0, pose_noise_std_meters) - z += np.random.normal(0, pose_noise_std_meters) - - roll, pitch, yaw = Rotation.from_quat(orientation).as_euler("xyz", degrees=True) - if pose_noise_rotation_deg > 0.0: - roll += np.random.normal(0, pose_noise_rotation_deg) - pitch += np.random.normal(0, pose_noise_rotation_deg) - yaw += np.random.normal(0, pose_noise_rotation_deg) - - qx, qy, qz, qw = Rotation.from_euler("xyz", (roll, pitch, yaw), degrees=True).as_quat() - return (x, y, z), (float(qx), float(qy), float(qz), float(qw)) diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/manager.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/manager.py deleted file mode 100644 index db64895a4..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/manager.py +++ /dev/null @@ -1,444 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -""" -``NatNetServerManager`` detects interface prims, samples poses from the stage, -and owns a **single** server instance it can start and stop. -On each enable it builds a MODELDEF catalog from the config and constructs a -fresh server via an injectable factory. -""" - -from __future__ import annotations - -from .catalog import build_catalog, find_duplicate_targets -from .config import DEFAULT_UP_AXIS, NatNetInterfaceConfig -from .frames import BodySample, apply_pose_noise, build_frame, to_motive_pose -from .usd_bindings import find_interfaces, read_interface, read_world_pose, resolve_targets - - -def _catalog_signature(config: NatNetInterfaceConfig): - """Identity of the catalog (body id/name set) — changes trigger a MODELDEF refresh.""" - return tuple((b.streaming_id, b.rigid_body_name) for b in config.bodies) - - -def _parse_version(version_str: str) -> tuple[int, int, int, int]: - try: - parts = tuple(int(x) for x in str(version_str).split(".")) - except ValueError: - parts = () - return (parts + (0, 0, 0, 0))[:4] - - -def default_server_factory(config: NatNetInterfaceConfig): - """Construct (but do not start) a ``NatNetUnicastServer`` from a config.""" - from ..server import NatNetUnicastServer, TransmissionType - - if config.mode != "unicast": - raise NotImplementedError( - f"mode {config.mode!r} is not supported yet (unicast only)" - ) - server = NatNetUnicastServer( - local_interface=config.server_ip, - transmission_type=TransmissionType.UNICAST, - multicast_address=None, - command_port=config.command_port, - data_port=config.data_port, - ) - server.publish_rate = config.publish_rate - server.natnet_version = _parse_version(config.natnet_version) - return server - - -def format_interface(prim_path: str, cfg: NatNetInterfaceConfig) -> str: - """Render a human-readable multi-line summary of one interface config.""" - lines = [f"[natnet] Interface @ {prim_path}"] - lines.append(f" serverEnabled : {cfg.server_enabled}") - lines.append(f" serverIp : {cfg.server_ip}") - lines.append(f" mode : {cfg.mode}") - if cfg.mode == "multicast": - lines.append(f" multicastAddr : {cfg.multicast_addr}") - lines.append(f" commandPort : {cfg.command_port}") - lines.append(f" dataPort : {cfg.data_port}") - lines.append(f" publishRate : {cfg.publish_rate}") - lines.append(f" natnetVersion : {cfg.natnet_version}") - lines.append(f" upAxis : {cfg.up_axis}") - lines.append(f" poseNoise : enabled={cfg.pose_noise_enabled}") - lines.append( - f" std={cfg.pose_noise_std_meters} m, rot={cfg.pose_noise_rotation_deg} deg" - ) - if cfg.bodies: - lines.append(f" bodies ({len(cfg.bodies)}):") - for b in cfg.bodies: - target = b.target_prim or "" - lines.append( - f" - {b.rigid_body_name} (id={b.streaming_id}, parent={b.parent_id}) -> {target}" - ) - else: - lines.append(" bodies : (none)") - return "\n".join(lines) - - -class NatNetServerManager: - """Detects interface prims, prints config, and owns one server instance.""" - - def __init__(self, server_factory=None): - self._stage_event_sub = None - self._usd_listener = None - self._scan_tick_sub = None - self._scan_pending = False - self._timeline_sub = None - self._server = None - self._server_factory = server_factory or default_server_factory - # Sampling state. A NatNet prim edit sets ``_needs_resync``; the next physics - # sample re-reads the catalog/targets and clears it. - self._needs_resync = False - self._sample_cache: list = [] - self._frame_counter = 0 - self._catalog_signature = None - self._physx_sub = None - # Streamed up-axis, re-read on every resync. See frames.to_motive_pose. - self._up_axis = DEFAULT_UP_AXIS - # Pose noise. - self._pose_noise_enabled = False - self._pose_noise_std_meters = 0.0 - self._pose_noise_rotation_deg = 0.0 - - # --- lifecycle ------------------------------------------------------------- - - def on_startup(self): - import omni.usd - - usd_context = omni.usd.get_context() - self._stage_event_sub = usd_context.get_stage_event_stream().create_subscription_to_pop( - self._on_stage_event, name="natnet_manager_stage_events" - ) - self._register_usd_listener() - self._subscribe_physics() - self._subscribe_timeline() - print("[natnet] NatNetServerManager initialized") - self.scan_and_print() - - def on_shutdown(self): - self.stop_server() - self._physx_sub = None - self._timeline_sub = None - self._stage_event_sub = None - self._scan_tick_sub = None - self._scan_pending = False - self._revoke_usd_listener() - - def _subscribe_physics(self): - # Sample + enqueue poses on every physics step (only fires while playing). - try: - import omni.physx - - self._physx_sub = omni.physx.get_physx_interface().subscribe_physics_step_events( - self._on_physics_step - ) - except Exception as exc: # Kit/physx only - print(f"[natnet] Physics step subscription unavailable: {exc}") - self._physx_sub = None - - def _on_physics_step(self, _dt): - if self._server is not None: - self.sample_once() - - # --- timeline-driven lifecycle --------------------------------------------- - - def _subscribe_timeline(self): - """Bind the server's lifetime to the sim: Play starts it, Stop shuts it down. - - Play builds the server from the prim, so ``serverIp``/ports/``mode`` — bound - into the socket at construction — pick up whatever is authored at that point. - Body and noise edits need no rebuild; ``_resync`` re-reads them while running. - """ - try: - import omni.timeline - - self._timeline_sub = ( - omni.timeline.get_timeline_interface() - .get_timeline_event_stream() - .create_subscription_to_pop(self._on_timeline_event) - ) - except Exception as exc: # Kit only - print(f"[natnet] Timeline subscription unavailable: {exc}") - self._timeline_sub = None - - def _on_timeline_event(self, event): - import omni.timeline - - if event.type == int(omni.timeline.TimelineEventType.PLAY): - self._start_for_play() - elif event.type == int(omni.timeline.TimelineEventType.STOP): - self.stop_server() - - def _start_for_play(self): - """Start from the stage on Play, honouring the prim's ``serverEnabled``.""" - if self.is_running: - return - stage = self._get_stage() - if stage is None: - return - interfaces = find_interfaces(stage) - if not interfaces: - return - config = read_interface(interfaces[0]) - if not config.server_enabled: - print("[natnet] Play: serverEnabled is false — not starting.") - return - self.log_target_diagnostics(config) - self.start_server(config) - - # --- scanning -------------------------------------------------------------- - - def scan_and_print(self, *_): - """Find every interface prim and print its parsed config.""" - stage = self._get_stage() - if stage is None: - return - interfaces = find_interfaces(stage) - if not interfaces: - print("[natnet] Scan: no NatNetInterface prims on stage.") - return - print(f"[natnet] Scan: {len(interfaces)} interface(s) detected.") - for prim in interfaces: - cfg = read_interface(prim) - print(format_interface(prim.GetPath().pathString, cfg)) - - # --- server lifecycle (single instance; USD-free, factory-injectable) ------ - - @property - def is_running(self) -> bool: - return self._server is not None - - @property - def server(self): - return self._server - - def start_server(self, config: NatNetInterfaceConfig) -> bool: - """Build the catalog, construct a fresh server, and start it — once. - - Idempotent: if a server is already running this is a no-op returning False. - Returns True when a new server was created and started. - """ - if self._server is not None: - print("[natnet] start_server ignored: a server is already running.") - return False - catalog = build_catalog(config) - server = self._server_factory(config) - server.set_model_def_payload(catalog.pack()) - # Pump frames from the physics-step thread; the server's own background timer - # is starved by Kit's render/physics main loop. - if hasattr(server, "auto_stream"): - server.auto_stream = False - server.start() - self._server = server - # Build the prim->pose cache from the live stage on the first sampled frame. - self._needs_resync = True - self._frame_counter = 0 - # None so the first resync reports "changed" and the first frame flags - # model_list_changed, prompting the client to read MODELDEF. - self._catalog_signature = None - print( - f"[natnet] Server started on {config.server_ip} " - f"(cmd {config.command_port} / data {config.data_port}) " - f"with {len(config.bodies)} body(ies)." - ) - return True - - def stop_server(self) -> bool: - """Shut down the running server (fresh instance is built on next start). - - Idempotent: returns False if nothing was running. - """ - if self._server is None: - return False - try: - self._server.shutdown() - finally: - self._server = None - self._sample_cache = [] - self._needs_resync = False - print("[natnet] Server stopped.") - return True - - def toggle_server(self, config: NatNetInterfaceConfig) -> bool: - """Start if stopped, stop if running. Returns the resulting running state.""" - if self.is_running: - self.stop_server() - else: - self.start_server(config) - return self.is_running - - def apply_enabled(self, config: NatNetInterfaceConfig) -> None: - """Reconcile running state to ``config.server_enabled`` (start/stop).""" - if config.server_enabled and not self.is_running: - self.start_server(config) - elif not config.server_enabled and self.is_running: - self.stop_server() - - def log_target_diagnostics(self, config: NatNetInterfaceConfig) -> None: - """Warn about missing target prims and duplicate targets (best-effort).""" - stage = self._get_stage() - if stage is not None: - _existing, missing = resolve_targets(stage, config) - for body in missing: - print( - f"[natnet] WARNING: body '{body.rigid_body_name}' target prim " - f"missing or empty: {body.target_prim or ''}" - ) - for path in find_duplicate_targets(config): - print(f"[natnet] WARNING: multiple bodies target the same prim: {path}") - - # --- scripting entry point ------------------------------------------------- - - def start_from_stage(self) -> bool: - """Find the interface prim on the current stage, read it, and start. - - Convenience for scripts/Pegasus launchers: author the prim (see - ``author_interface``) then call this. Returns False if nothing to start. - """ - stage = self._get_stage() - if stage is None: - print("[natnet] start_from_stage: no active stage.") - return False - interfaces = find_interfaces(stage) - if not interfaces: - print("[natnet] start_from_stage: no NatNetInterface prim found.") - return False - config = read_interface(interfaces[0]) - self.log_target_diagnostics(config) - return self.start_server(config) - - # --- pose sampling + dynamic catalog (the data-enqueue path) --------------- - - def mark_dirty(self) -> None: - """Flag that the on-stage config changed; next sample re-reads the catalog.""" - self._needs_resync = True - - def _resync(self, stage) -> bool: - """Re-read the interface config, rebuild the catalog, and re-resolve targets. - - Returns True if the catalog (body id/name set) actually changed, so the next - frame can flag ``model_list_changed`` and the client re-requests MODELDEF. - """ - interfaces = find_interfaces(stage) - if not interfaces: - self._sample_cache = [] - return False - config = read_interface(interfaces[0]) - self._up_axis = config.up_axis - self._pose_noise_enabled = config.pose_noise_enabled - self._pose_noise_std_meters = config.pose_noise_std_meters - self._pose_noise_rotation_deg = config.pose_noise_rotation_deg - if self._server is not None: - self._server.set_model_def_payload(build_catalog(config).pack()) - # Cache target paths, not prim handles, so bodies whose target prim appears - # after the server starts begin streaming as soon as it exists. - self._sample_cache = [ - (body.streaming_id, body.rigid_body_name, body.target_prim) - for body in config.bodies - ] - signature = _catalog_signature(config) - changed = signature != self._catalog_signature - self._catalog_signature = signature - return changed - - def sample_once(self, stage=None): - """Sample every body's USD world pose and enqueue one frame to the server. - - Resyncs the catalog first if the config is dirty (so bodies added/removed - live are picked up). Returns the enqueued frame (or None if nothing to do). - """ - if self._server is None: - return None - if stage is None: - stage = self._get_stage() - if stage is None: - return None - - model_changed = False - if self._needs_resync: - model_changed = self._resync(stage) - self._needs_resync = False - - samples = [] - for streaming_id, _name, target_path in self._sample_cache: - prim = stage.GetPrimAtPath(target_path) if target_path else None - pose = read_world_pose(prim) if prim is not None else None - if pose is None: - samples.append(BodySample.lost(streaming_id)) - else: - position, orientation = to_motive_pose(*pose, up_axis=self._up_axis) - if self._pose_noise_enabled: - position, orientation = apply_pose_noise(position, orientation, self._pose_noise_std_meters, self._pose_noise_rotation_deg) - samples.append(BodySample(streaming_id, position, orientation, valid=True)) - - frame = build_frame( - self._frame_counter, samples, model_list_changed=model_changed - ) - self._frame_counter += 1 - self._server.enqueue_mocap_data(frame) - # Send synchronously from this (physics-step) thread. - flush_mocap_data = getattr(self._server, "flush_mocap_data", None) - if callable(flush_mocap_data): - flush_mocap_data() - return frame - - # --- stage / USD notifications -------------------------------------------- - - def _get_stage(self): - import omni.usd - - return omni.usd.get_context().get_stage() - - def _on_stage_event(self, event): - import omni.usd - - if event.type == int(omni.usd.StageEventType.OPENED): - self._register_usd_listener() - self.scan_and_print() - - def _register_usd_listener(self): - from pxr import Tf, Usd - - stage = self._get_stage() - if stage is None: - return - self._revoke_usd_listener() - self._usd_listener = Tf.Notice.Register( - Usd.Notice.ObjectsChanged, self._on_objects_changed, stage - ) - - def _revoke_usd_listener(self): - if self._usd_listener is not None: - self._usd_listener.Revoke() - self._usd_listener = None - - def _on_objects_changed(self, notice, sender): - # Only re-scan when something NatNet-related changed - try: - paths = list(notice.GetResyncedPaths()) + list(notice.GetChangedInfoOnlyPaths()) - except Exception: - paths = [] - if any(("NatNetInterface" in str(p)) or ("natnet:" in str(p)) for p in paths): - # A NatNet prim changed: mark the sampler dirty so the next physics step re-reads the catalog. - self._needs_resync = True - # Debounce author_interface() calls into one scan on the next update tick. - self._request_scan() - - def _request_scan(self): - if self._scan_pending: - return - self._scan_pending = True - import omni.kit.app - - self._scan_tick_sub = ( - omni.kit.app.get_app() - .get_update_event_stream() - .create_subscription_to_pop(self._on_scan_tick, name="natnet_manager_scan_tick") - ) - - def _on_scan_tick(self, _event): - self._scan_pending = False - self._scan_tick_sub = None - self.scan_and_print() diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/scene_setup.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/scene_setup.py deleted file mode 100644 index 8c41053c3..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/scene_setup.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Standalone-launch helpers: stand up a drone NatNet interface on scene load. - -Used by the Pegasus example launch scripts so a Motive-compatible NatNet server -comes up automatically with one rigid body per drone ``base_link`` — no UI clicks. - -Two layers, mirroring the rest of the package: - -- ``build_drone_config`` is **pure** (no USD / Kit), so it unit-tests hermetically. -- ``author_drone_natnet_interface`` writes the interface prim the extension builds - its server from. It imports ``pxr`` lazily (only when called). -""" - -from __future__ import annotations - -from typing import Iterable, Sequence, Tuple - -from .config import ( - DEFAULT_COMMAND_PORT, - DEFAULT_DATA_PORT, - DEFAULT_POSE_NOISE_ENABLED, - DEFAULT_POSE_NOISE_ROTATION_DEG, - DEFAULT_POSE_NOISE_STD_METERS, - DEFAULT_PUBLISH_RATE, - DEFAULT_SERVER_IP, - DEFAULT_UP_AXIS, - BodyBinding, - NatNetInterfaceConfig, -) - -# Where the example scripts author the single interface prim. -DEFAULT_INTERFACE_PATH = "/World/NatNetInterface" - -# Default world prim + position for the demo "target" body (a static placeholder -# the example scripts stream alongside the drones so a tracked target is available). -DEFAULT_TARGET_PATH = "/World/target" -DEFAULT_TARGET_POSITION = (2.0, 0.0, 1.0) -DEFAULT_TARGET_STREAMING_ID = 100 - -# (rigid_body_name, streaming_id, target_prim_path) -DroneSpec = Tuple[str, int, str] - - -def author_static_target( - stage, - prim_path: str = DEFAULT_TARGET_PATH, - position: Sequence[float] = DEFAULT_TARGET_POSITION, -): - """Author a static ``Xform`` prim to act as a NatNet-tracked target. - - Creates ``prim_path`` (a plain transform with a single translate op) at - ``position`` so the emulator can sample it like any other tracked body. The - prim is static — no physics, no animation — representing a fixed point of - interest that drones can be commanded toward. Imports ``pxr`` lazily so this - module stays importable outside Isaac. Returns ``prim_path``. - """ - from pxr import Gf, UsdGeom - - xform = UsdGeom.Xform.Define(stage, prim_path) - xform.AddTranslateOp().Set(Gf.Vec3d(float(position[0]), float(position[1]), float(position[2]))) - return prim_path - - -def build_drone_config( - drones: Iterable[DroneSpec], - *, - server_ip: str = DEFAULT_SERVER_IP, - mode: str = "unicast", - command_port: int = DEFAULT_COMMAND_PORT, - data_port: int = DEFAULT_DATA_PORT, - publish_rate: float = DEFAULT_PUBLISH_RATE, - server_enabled: bool = True, - up_axis: str = DEFAULT_UP_AXIS, - pose_noise_enabled: bool = DEFAULT_POSE_NOISE_ENABLED, - pose_noise_std_meters: float = DEFAULT_POSE_NOISE_STD_METERS, - pose_noise_rotation_deg: float = DEFAULT_POSE_NOISE_ROTATION_DEG, -) -> NatNetInterfaceConfig: - """Build a validated config with one rigid body per drone. - - ``drones`` is an iterable of ``(rigid_body_name, streaming_id, target_prim)`` - tuples — typically one per spawned drone, with ``target_prim`` pointing at the - drone's ``base_link``. Raises ``ValueError`` (via ``validate``) on duplicate - names/ids or bad ports. - """ - bodies = [ - BodyBinding( - rigid_body_name=str(name), - target_prim=str(target), - streaming_id=int(streaming_id), - ) - for name, streaming_id, target in drones - ] - cfg = NatNetInterfaceConfig( - server_enabled=server_enabled, - server_ip=server_ip, - mode=mode, - command_port=command_port, - data_port=data_port, - publish_rate=publish_rate, - up_axis=up_axis, - pose_noise_enabled=pose_noise_enabled, - pose_noise_std_meters=pose_noise_std_meters, - pose_noise_rotation_deg=pose_noise_rotation_deg, - bodies=bodies, - ) - cfg.validate() - return cfg - - -def author_drone_natnet_interface( - stage, - drones: Sequence[DroneSpec], - *, - prim_path: str = DEFAULT_INTERFACE_PATH, - **config_kwargs, -) -> NatNetInterfaceConfig: - """Author the NatNet interface prim from ``drones``. - - Writes ``prim_path`` (overwriting any existing interface) with one rigid body per - drone. Call this before starting the timeline: the extension builds the server - from this prim on Play. Returns the authored config. - """ - from .usd_bindings import author_interface - - cfg = build_drone_config(drones, **config_kwargs) - author_interface(stage, prim_path, cfg) - return cfg diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/ui_extension.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/ui_extension.py deleted file mode 100644 index 16a852d1f..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/ui_extension.py +++ /dev/null @@ -1,435 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Kit extension entry: docked editor for the NatNet interface config prim. - -Create/manage the ``/World/NatNetInterface`` prim. The window docks to the -bottom-right (alongside the Property panel, like Pegasus) so it's easy to find. - -Sync model is explicit and user-driven via the top button row. -""" - -from __future__ import annotations - -import omni.ext - -from .config import VALID_MODES, VALID_UP_AXES, BodyBinding, NatNetInterfaceConfig -from .manager import NatNetServerManager -from .usd_bindings import author_interface, find_interfaces, read_interface, read_world_pose - -_DEFAULT_PRIM_PATH = "/World/NatNetInterface" -_LABEL_WIDTH = 140 -_POS_REFRESH_PERIOD = 1.0 / 6.0 # seconds between live USD position reads - -_COLOR_LIVE = 0xFF33CC33 # green: prim resolves and server is streaming -_COLOR_IDLE = 0xFFAAAAAA # grey: prim resolves but server not running -_COLOR_LOST = 0xFF3333FF # red: no prim / NaN - - -class NatNetEmulatorExtension(omni.ext.IExt): - """Registers the Window menu entry + the docked editor panel.""" - - def on_startup(self, ext_id): # noqa: D401 - Kit lifecycle hook - self._window = None - self._bodies_frame = None - self._cfg = NatNetInterfaceConfig() - self._row_readouts = {} - self._pos_refresh_sub = None - self._last_pos_refresh = 0.0 - self._manager = NatNetServerManager() - self._manager.on_startup() - self._add_menu() - self._subscribe_position_refresh() - - def on_shutdown(self): - self._remove_menu() - self._pos_refresh_sub = None - self._row_readouts = {} - if self._manager is not None: - self._manager.on_shutdown() - self._manager = None - if self._window is not None: - self._window.destroy() - self._window = None - - # --- live position readout ------------------------------------------------- - - def _subscribe_position_refresh(self): - try: - import omni.kit.app - except Exception: # pragma: no cover - Kit only - return - self._pos_refresh_sub = ( - omni.kit.app.get_app() - .get_update_event_stream() - .create_subscription_to_pop(self._on_pos_refresh, name="natnet_ui_pos_refresh") - ) - - def _on_pos_refresh(self, _event): - import time - - if self._window is None or not self._window.visible or not self._row_readouts: - return - now = time.monotonic() - if now - self._last_pos_refresh < _POS_REFRESH_PERIOD: - return - self._last_pos_refresh = now - stage = self._get_stage() - running = self._manager is not None and self._manager.is_running - for idx, (status_label, pos_label) in list(self._row_readouts.items()): - if not (0 <= idx < len(self._cfg.bodies)): - continue - target = self._cfg.bodies[idx].target_prim - symbol, color, text = self._row_readout(stage, target, running) - status_label.text = symbol - status_label.style = {"color": color} - pos_label.text = text - pos_label.style = {"color": color} - - def _row_readout(self, stage, target, running): - if not target: - return "\u25cb", _COLOR_IDLE, "no target prim" - prim = stage.GetPrimAtPath(target) if stage is not None else None - pose = read_world_pose(prim) if prim is not None else None - if pose is None: - return "\u2717", _COLOR_LOST, "NaN (prim missing)" - (x, y, z), _quat = pose - text = f"{x:+.3f}, {y:+.3f}, {z:+.3f}" - if running: - return "\u25cf", _COLOR_LIVE, text - return "\u25cf", _COLOR_IDLE, text - - # --- menu ------------------------------------------------------------------ - - def _add_menu(self): - try: - import omni.kit.menu.utils as menu_utils - from omni.kit.menu.utils import MenuItemDescription - except Exception: # pragma: no cover - Kit only - return - self._menu_entries = [ - MenuItemDescription(name="NatNet Interface", onclick_fn=self._toggle_window) - ] - menu_utils.add_menu_items(self._menu_entries, "Window") - - def _remove_menu(self): - try: - import omni.kit.menu.utils as menu_utils - except Exception: # pragma: no cover - Kit only - return - if getattr(self, "_menu_entries", None): - menu_utils.remove_menu_items(self._menu_entries, "Window") - self._menu_entries = None - - # --- window ---------------------------------------------------------------- - - def _toggle_window(self, *_): - import omni.ui as ui - - if self._window is None: - # Open on the interface authored on the stage, so Save writes back what is - # there — author_interface replaces the whole body set. - self._load_from_stage() - self._window = ui.Window("NatNet Interface", width=400, height=600) - self._window.frame.set_build_fn(self._build_window) - # Dock bottom-right next to the Property panel, like Pegasus. - self._window.deferred_dock_in("Property", ui.DockPolicy.CURRENT_WINDOW_IS_ACTIVE) - self._window.visible = True - return - self._window.visible = not self._window.visible - - def _refresh(self, *_): - if self._window is not None: - self._window.frame.rebuild() - - def _build_window(self): - import omni.ui as ui - - with ui.ScrollingFrame(): - with ui.VStack(spacing=6, height=0): - ui.Label("NatNet interface", height=0, style={"font_size": 16}) - - with ui.HStack(height=28, spacing=6): - ui.Button("Create Interface", clicked_fn=self._create_server) - ui.Button("Save", clicked_fn=self._save) - ui.Button("Load from Stage", clicked_fn=self._load_from_stage) - ui.Button("Print config", clicked_fn=self._print_config) - - running = self._manager is not None and self._manager.is_running - # Read-only: the server's lifetime follows the sim, so there is no - # control here. Play starts it from the prim, Stop shuts it down. - with ui.HStack(height=28, spacing=6): - ui.Label( - f"Server: {'RUNNING' if running else 'stopped (press Play)'}", - width=0, - style={"color": 0xFF33CC33 if running else 0xFF888888}, - ) - - ui.Label( - "\u26a0 Remember to save after each edit", - height=0, - word_wrap=True, - style={"color": 0xFF33CCFF, "font_size": 14}, - ) - - ui.Label(self._status_text(), height=0, word_wrap=True) - - ui.Separator(height=6) - self._bool_row(ui, "Server enabled", "server_enabled", self._cfg.server_enabled) - self._bool_row(ui, "Pose noise enabled", "pose_noise_enabled", self._cfg.pose_noise_enabled) - self._float_row(ui, "Pose noise std meters", "pose_noise_std_meters", self._cfg.pose_noise_std_meters) - self._float_row(ui, "Pose noise rotation deg", "pose_noise_rotation_deg", self._cfg.pose_noise_rotation_deg) - self._str_row(ui, "Server IP", "server_ip", self._cfg.server_ip) - self._combo_row(ui, "Mode", "mode", self._cfg.mode, VALID_MODES) - self._int_row(ui, "Command port", "command_port", self._cfg.command_port) - self._int_row(ui, "Data port", "data_port", self._cfg.data_port) - self._float_row(ui, "Publish rate (Hz)", "publish_rate", self._cfg.publish_rate) - self._combo_row(ui, "Up axis", "up_axis", self._cfg.up_axis, VALID_UP_AXES) - - ui.Separator(height=6) - ui.Label("Tracked bodies", height=0, style={"font_size": 14}) - self._bodies_frame = ui.Frame(height=0) - self._bodies_frame.set_build_fn(self._build_bodies) - with ui.HStack(height=0, spacing=6): - ui.Button("Add body (from selection)", clicked_fn=self._add_body) - - def _status_text(self): - prim = self._find_interface() - if prim is None: - return "No prim on stage yet — Save or Create Server to author one." - return f"Prim on stage: {prim.GetPath().pathString} (Save to push edits, Load to pull)" - - # --- server field rows (edit the working copy only) ------------------------ - - def _bool_row(self, ui, label, key, value): - with ui.HStack(height=0): - ui.Label(label, width=_LABEL_WIDTH) - cb = ui.CheckBox() - cb.model.set_value(bool(value)) - cb.model.add_value_changed_fn( - lambda m, k=key: self._set_cfg_field(k, m.get_value_as_bool()) - ) - - def _str_row(self, ui, label, key, value): - with ui.HStack(height=0): - ui.Label(label, width=_LABEL_WIDTH) - model = ui.StringField().model - model.set_value(str(value)) - model.add_value_changed_fn( - lambda m, k=key: self._set_cfg_field(k, m.get_value_as_string()) - ) - - def _int_row(self, ui, label, key, value): - with ui.HStack(height=0): - ui.Label(label, width=_LABEL_WIDTH) - model = ui.IntField().model - model.set_value(int(value)) - model.add_value_changed_fn( - lambda m, k=key: self._set_cfg_field(k, m.get_value_as_int()) - ) - - def _float_row(self, ui, label, key, value): - with ui.HStack(height=0): - ui.Label(label, width=_LABEL_WIDTH) - model = ui.FloatField().model - model.set_value(float(value)) - model.add_value_changed_fn( - lambda m, k=key: self._set_cfg_field(k, m.get_value_as_float()) - ) - - def _combo_row(self, ui, label, key, value, choices): - with ui.HStack(height=0): - ui.Label(label, width=_LABEL_WIDTH) - index = choices.index(value) if value in choices else 0 - combo = ui.ComboBox(index, *choices) - combo.model.get_item_value_model().add_value_changed_fn( - lambda m, k=key, c=choices: self._set_cfg_field(k, c[m.get_value_as_int()]) - ) - - def _set_cfg_field(self, attr, value): - setattr(self._cfg, attr, value) - - # --- bodies ---------------------------------------------------------------- - - def _rebuild_bodies(self, *_): - if self._bodies_frame is not None: - self._bodies_frame.rebuild() - - def _build_bodies(self): - import omni.ui as ui - - self._row_readouts = {} - with ui.VStack(spacing=6, height=0): - if not self._cfg.bodies: - ui.Label(" (no bodies — select a prim and click Add body)", height=0) - return - with ui.HStack(height=0, spacing=4): - ui.Label("Rigid body name", width=ui.Fraction(1)) - ui.Label("ID", width=40) - ui.Label("Parent", width=50) - ui.Label("Target prim", width=ui.Fraction(2)) - ui.Spacer(width=98) - for idx, body in enumerate(self._cfg.bodies): - self._build_body_row(ui, idx, body) - - def _build_body_row(self, ui, idx, body): - with ui.VStack(height=0, spacing=2): - with ui.HStack(height=0, spacing=4): - name = ui.StringField(width=ui.Fraction(1)).model - name.set_value(body.rigid_body_name) - name.add_value_changed_fn( - lambda m, i=idx: self._set_body_field(i, "rigid_body_name", m.get_value_as_string()) - ) - - sid = ui.IntField(width=40).model - sid.set_value(body.streaming_id) - sid.add_value_changed_fn( - lambda m, i=idx: self._set_body_field(i, "streaming_id", m.get_value_as_int()) - ) - - parent = ui.IntField(width=50).model - parent.set_value(body.parent_id) - parent.add_value_changed_fn( - lambda m, i=idx: self._set_body_field(i, "parent_id", m.get_value_as_int()) - ) - - target = ui.StringField(width=ui.Fraction(2), tooltip="USD path of the tracked prim").model - target.set_value(body.target_prim) - target.add_value_changed_fn( - lambda m, i=idx: self._set_body_field(i, "target_prim", m.get_value_as_string()) - ) - - ui.Button("set target", width=70, clicked_fn=lambda i=idx: self._retarget_body(i)) - ui.Button("x", width=24, clicked_fn=lambda i=idx: self._remove_body_at(i)) - - # Live readout: status dot + world position pulled from the USD stage. - stage = self._get_stage() - running = self._manager is not None and self._manager.is_running - symbol, color, text = self._row_readout(stage, body.target_prim, running) - with ui.HStack(height=0, spacing=6): - ui.Spacer(width=4) - status_label = ui.Label(symbol, width=14, style={"color": color}) - ui.Label("pos:", width=30, style={"color": _COLOR_IDLE}) - pos_label = ui.Label(text, width=ui.Fraction(1), style={"color": color}) - self._row_readouts[idx] = (status_label, pos_label) - - def _set_body_field(self, index, attr, value): - if 0 <= index < len(self._cfg.bodies): - setattr(self._cfg.bodies[index], attr, value) - - def _add_body(self): - next_id = max((b.streaming_id for b in self._cfg.bodies), default=0) + 1 - target = self._selected_target_path(self._find_interface()) - name = target.rsplit("/", 1)[-1] if target else f"Body{next_id}" - existing = {b.rigid_body_name for b in self._cfg.bodies} - while name in existing: - name = f"{name}_{next_id}" - self._cfg.bodies.append(BodyBinding(rigid_body_name=name, target_prim=target, streaming_id=next_id)) - self._rebuild_bodies() - - def _remove_body_at(self, index): - if 0 <= index < len(self._cfg.bodies): - self._cfg.bodies.pop(index) - self._rebuild_bodies() - - def _retarget_body(self, index): - import carb - - path = self._selected_target_path(self._find_interface()) - if not path: - carb.log_warn("[natnet] Select a prim in the viewport to retarget this body.") - return - if 0 <= index < len(self._cfg.bodies): - self._cfg.bodies[index].target_prim = path - self._rebuild_bodies() - - # --- stage helpers --------------------------------------------------------- - - def _get_stage(self): - import omni.usd - - return omni.usd.get_context().get_stage() - - def _find_interface(self): - stage = self._get_stage() - if stage is None: - return None - interfaces = find_interfaces(stage) - return interfaces[0] if interfaces else None - - def _interface_path(self): - prim = self._find_interface() - return prim.GetPath().pathString if prim is not None else _DEFAULT_PRIM_PATH - - def _select(self, prim_path): - import omni.usd - - omni.usd.get_context().get_selection().set_selected_prim_paths([prim_path], True) - - def _selected_target_path(self, interface_prim): - import omni.usd - - sel = omni.usd.get_context().get_selection().get_selected_prim_paths() - iface_path = interface_prim.GetPath().pathString if interface_prim else None - for path in sel: - if path != iface_path: - return path - return "" - - # --- explicit sync actions ------------------------------------------------- - - def _save(self): - import carb - - stage = self._get_stage() - if stage is None: - carb.log_error("[natnet] No active stage.") - return - try: - self._cfg.validate() - except ValueError as exc: - carb.log_error(f"[natnet] Not saved: {exc}") - return - path = self._interface_path() - author_interface(stage, path, self._cfg) - carb.log_info(f"[natnet] Saved interface to {path} ({len(self._cfg.bodies)} bodies).") - self._refresh() - - def _load_from_stage(self): - import carb - - prim = self._find_interface() - if prim is None: - self._cfg = NatNetInterfaceConfig() - carb.log_warn("[natnet] No interface on stage — reset to defaults.") - else: - self._cfg = read_interface(prim) - carb.log_info(f"[natnet] Loaded interface from {prim.GetPath().pathString}.") - self._refresh() - - def _print_config(self): - # Print whatever is authored on the stage (the source of truth). - if self._manager is not None: - self._manager.scan_and_print() - - def _create_server(self): - import carb - - stage = self._get_stage() - if stage is None: - carb.log_error("[natnet] No active stage.") - return - prim = self._find_interface() - if prim is None: - try: - self._cfg.validate() - except ValueError as exc: - carb.log_error(f"[natnet] Cannot create: {exc}") - return - author_interface(stage, _DEFAULT_PRIM_PATH, self._cfg) - path = _DEFAULT_PRIM_PATH - carb.log_info(f"[natnet] Created interface prim at {path}. (Server start: later commit.)") - else: - path = prim.GetPath().pathString - carb.log_info(f"[natnet] Interface already exists at {path}. (Server start: later commit.)") - self._select(path) - self._refresh() diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/usd_bindings.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/usd_bindings.py deleted file mode 100644 index 262216ee2..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/isaac/usd_bindings.py +++ /dev/null @@ -1,216 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""USD binding layer: author / read / find NatNet interface prims on a stage. - -``pxr`` is imported lazily inside each function so importing this module doesn't -require USD. - -Backing today is plain namespaced custom attributes + relationships. Property names -follow the multi-apply schema convention (``natnet:body::``). -""" - -from __future__ import annotations - -from typing import Any - -from .config import ( - ATTR_COMMAND_PORT, - ATTR_DATA_PORT, - ATTR_MODE, - ATTR_MULTICAST_ADDR, - ATTR_NATNET_VERSION, - ATTR_POSE_NOISE_ENABLED, - ATTR_POSE_NOISE_ROTATION_DEG, - ATTR_POSE_NOISE_STD_METERS, - ATTR_PUBLISH_RATE, - ATTR_SERVER_ENABLED, - ATTR_SERVER_IP, - ATTR_UP_AXIS, - BODY_FIELD_PARENT_ID, - BODY_FIELD_RIGID_BODY_NAME, - BODY_FIELD_STREAMING_ID, - BODY_FIELD_TARGET, - BODY_PREFIX, - DEFAULT_COMMAND_PORT, - DEFAULT_DATA_PORT, - DEFAULT_MULTICAST_ADDR, - DEFAULT_NATNET_VERSION, - DEFAULT_POSE_NOISE_ENABLED, - DEFAULT_POSE_NOISE_ROTATION_DEG, - DEFAULT_POSE_NOISE_STD_METERS, - DEFAULT_PUBLISH_RATE, - DEFAULT_SERVER_IP, - DEFAULT_UP_AXIS, - MARKER_ATTR, - BodyBinding, - NatNetInterfaceConfig, - body_attr_name, -) - - -def author_interface(stage, prim_path: str, config: Any) -> Any: - """Create/overwrite a NatNet interface prim at ``prim_path`` from ``config``. - - ``config`` may be a :class:`NatNetInterfaceConfig` or a plain ``dict`` (passed - through ``from_dict``). Returns the ``Usd.Prim``. - """ - from pxr import Sdf - - cfg = config if isinstance(config, NatNetInterfaceConfig) else NatNetInterfaceConfig.from_dict(config) - cfg.validate() - - prim = stage.DefinePrim(prim_path, "Scope") - - # Overwrite semantics: drop any previously-authored body properties so removed - # bodies don't linger across re-authoring. - _clear_body_properties(prim) - - _set(prim, MARKER_ATTR, Sdf.ValueTypeNames.Bool, True) - _set(prim, ATTR_SERVER_ENABLED, Sdf.ValueTypeNames.Bool, cfg.server_enabled) - _set(prim, ATTR_SERVER_IP, Sdf.ValueTypeNames.String, cfg.server_ip) - _set(prim, ATTR_MODE, Sdf.ValueTypeNames.Token, cfg.mode) - _set(prim, ATTR_MULTICAST_ADDR, Sdf.ValueTypeNames.String, cfg.multicast_addr) - _set(prim, ATTR_COMMAND_PORT, Sdf.ValueTypeNames.Int, cfg.command_port) - _set(prim, ATTR_DATA_PORT, Sdf.ValueTypeNames.Int, cfg.data_port) - _set(prim, ATTR_PUBLISH_RATE, Sdf.ValueTypeNames.Float, cfg.publish_rate) - _set(prim, ATTR_NATNET_VERSION, Sdf.ValueTypeNames.String, cfg.natnet_version) - _set(prim, ATTR_UP_AXIS, Sdf.ValueTypeNames.Token, cfg.up_axis) - _set(prim, ATTR_POSE_NOISE_ENABLED, Sdf.ValueTypeNames.Bool, cfg.pose_noise_enabled) - _set(prim, ATTR_POSE_NOISE_STD_METERS, Sdf.ValueTypeNames.Float, cfg.pose_noise_std_meters) - _set(prim, ATTR_POSE_NOISE_ROTATION_DEG, Sdf.ValueTypeNames.Float, cfg.pose_noise_rotation_deg) - - for key, body in cfg.assign_instance_keys(): - _set(prim, body_attr_name(key, BODY_FIELD_RIGID_BODY_NAME), Sdf.ValueTypeNames.String, body.rigid_body_name) - _set(prim, body_attr_name(key, BODY_FIELD_STREAMING_ID), Sdf.ValueTypeNames.Int, body.streaming_id) - _set(prim, body_attr_name(key, BODY_FIELD_PARENT_ID), Sdf.ValueTypeNames.Int, body.parent_id) - rel = prim.CreateRelationship(body_attr_name(key, BODY_FIELD_TARGET), False) - rel.SetTargets([Sdf.Path(body.target_prim)] if body.target_prim else []) - - return prim - - -def read_interface(prim) -> NatNetInterfaceConfig: - """Reconstruct a :class:`NatNetInterfaceConfig` from an authored interface prim.""" - return NatNetInterfaceConfig( - server_enabled=bool(_get(prim, ATTR_SERVER_ENABLED, True)), - server_ip=str(_get(prim, ATTR_SERVER_IP, DEFAULT_SERVER_IP)), - mode=str(_get(prim, ATTR_MODE, "unicast")), - multicast_addr=str(_get(prim, ATTR_MULTICAST_ADDR, DEFAULT_MULTICAST_ADDR)), - command_port=int(_get(prim, ATTR_COMMAND_PORT, DEFAULT_COMMAND_PORT)), - data_port=int(_get(prim, ATTR_DATA_PORT, DEFAULT_DATA_PORT)), - publish_rate=float(_get(prim, ATTR_PUBLISH_RATE, DEFAULT_PUBLISH_RATE)), - natnet_version=str(_get(prim, ATTR_NATNET_VERSION, DEFAULT_NATNET_VERSION)), - up_axis=str(_get(prim, ATTR_UP_AXIS, DEFAULT_UP_AXIS)), - pose_noise_enabled=bool(_get(prim, ATTR_POSE_NOISE_ENABLED, DEFAULT_POSE_NOISE_ENABLED)), - pose_noise_std_meters=float(_get(prim, ATTR_POSE_NOISE_STD_METERS, DEFAULT_POSE_NOISE_STD_METERS)), - pose_noise_rotation_deg=float(_get(prim, ATTR_POSE_NOISE_ROTATION_DEG, DEFAULT_POSE_NOISE_ROTATION_DEG)), - bodies=_read_bodies(prim), - ) - - -def find_interfaces(stage) -> list: - """Return every prim on the stage marked as a NatNet interface.""" - interfaces = [] - for prim in stage.Traverse(): - attr = prim.GetAttribute(MARKER_ATTR) - if attr and attr.HasAuthoredValue() and bool(attr.Get()): - interfaces.append(prim) - return interfaces - - -def is_interface(prim) -> bool: - attr = prim.GetAttribute(MARKER_ATTR) - return bool(attr and attr.HasAuthoredValue() and bool(attr.Get())) - - -def read_world_pose(prim): - """Return ``((x, y, z), (qx, qy, qz, qw))`` from a prim's USD world transform. - - Reads the position/orientation **stored in the USD stage** (the local-to-world - transform), which is what the physics step writes back each frame. Returns - ``None`` for an invalid/non-xformable prim so callers can mark the body lost. - """ - from pxr import Usd, UsdGeom - - if prim is None or not prim.IsValid(): - return None - xformable = UsdGeom.Xformable(prim) - if not xformable: - return None - matrix = xformable.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) - translation = matrix.ExtractTranslation() - quat = matrix.ExtractRotationQuat() # Gf.Quatd, normalized - imaginary = quat.GetImaginary() - position = (float(translation[0]), float(translation[1]), float(translation[2])) - orientation = ( - float(imaginary[0]), - float(imaginary[1]), - float(imaginary[2]), - float(quat.GetReal()), - ) - return position, orientation - - -def resolve_targets(stage, config): - """Split a config's bodies into (existing, missing) by target prim presence. - - A body whose ``target_prim`` is empty or points at a non-existent prim lands in - ``missing``. Returns two lists of :class:`BodyBinding`. - """ - existing = [] - missing = [] - for body in config.bodies: - prim = stage.GetPrimAtPath(body.target_prim) if body.target_prim else None - if prim is not None and prim.IsValid(): - existing.append(body) - else: - missing.append(body) - return existing, missing - - -# --- internal helpers ---------------------------------------------------------- - - -def _set(prim, name, type_name, value): - attr = prim.CreateAttribute(name, type_name) - attr.Set(value) - return attr - - -def _get(prim, name, default): - attr = prim.GetAttribute(name) - if attr and attr.HasAuthoredValue(): - return attr.Get() - return default - - -def _clear_body_properties(prim) -> None: - for name in list(prim.GetPropertyNames()): - if name.startswith(BODY_PREFIX): - prim.RemoveProperty(name) - - -def _read_bodies(prim) -> list[BodyBinding]: - suffix = f":{BODY_FIELD_RIGID_BODY_NAME}" - keys = [ - name[len(BODY_PREFIX): -len(suffix)] - for name in prim.GetPropertyNames() - if name.startswith(BODY_PREFIX) and name.endswith(suffix) - ] - - bodies: list[BodyBinding] = [] - for key in keys: - rel = prim.GetRelationship(body_attr_name(key, BODY_FIELD_TARGET)) - targets = rel.GetTargets() if rel else [] - bodies.append( - BodyBinding( - rigid_body_name=str(_get(prim, body_attr_name(key, BODY_FIELD_RIGID_BODY_NAME), "")), - target_prim=str(targets[0]) if targets else "", - streaming_id=int(_get(prim, body_attr_name(key, BODY_FIELD_STREAMING_ID), 1)), - parent_id=int(_get(prim, body_attr_name(key, BODY_FIELD_PARENT_ID), -1)), - ) - ) - - # Stable, deterministic order (independent of USD property iteration order). - bodies.sort(key=lambda b: (b.streaming_id, b.rigid_body_name)) - return bodies diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/__init__.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/__init__.py deleted file mode 100644 index c84c1fe4b..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""NatNet UDP server implementation (unicast; multicast planned).""" - -from .natnet_server import Client, NatNetServer, TransmissionType -from .natnet_unicast_server import NatNetUnicastServer - -__all__ = [ - "Client", - "NatNetServer", - "NatNetUnicastServer", - "TransmissionType", -] diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_common.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_common.py deleted file mode 100644 index 1eb32177c..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_common.py +++ /dev/null @@ -1,27 +0,0 @@ -from enum import IntEnum -import ctypes - -class ModelLimits(IntEnum): - MAX_MODELS = 2000 # maximum number of total models (data descriptions) - MAX_MARKERSETS = 1000 # maximum number of MarkerSets - MAX_RIGIDBODIES = 1000 # maximum number of RigidBodies - MAX_ASSETS = 1000 # Maximum number of Assets - MAX_NAMELENGTH = 256 # maximum length for strings - MAX_MARKERS = 200 # maximum number of markers per MarkerSet - MAX_RBMARKERS = 20 # maximum number of markers per RigidBody - MAX_SKELETONS = 100 # maximum number of skeletons - MAX_SKELRIGIDBODIES = 200 # maximum number of RididBodies per Skeleton - MAX_LABELED_MARKERS = 1000 # maximum number of labeled markers per frame - MAX_UNLABELED_MARKERS = 1000 # maximum number of unlabeled (other) markers per frame - - MAX_FORCEPLATES = 100 # maximum number of force plate 'bundles' - MAX_DEVICES = 100 # maximum number of peripheral device 'bundles' - MAX_ANALOG_CHANNELS = 32 # maximum number of data channels (signals) per analog/force plate device - MAX_ANALOG_SUBFRAMES = 30 # maximum number of analog/force plate frames per mocap frame - - MAX_PACKETSIZE = 65503 # max size of packet in bytes (actual packet size is dynamic) - # (65535 byte IP limit - 20 byte IP header - 8 byte UDP header - 4 byte sPacket header = 65503 bytes) - - - -MarkerData = ctypes.c_float * 3 diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_data_types.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_data_types.py deleted file mode 100644 index 1128cc351..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_data_types.py +++ /dev/null @@ -1,224 +0,0 @@ -import ctypes -import struct -from .natnet_common import ModelLimits, MarkerData - -class sMarker(ctypes.Structure): - _pack_ = 1 - _fields_ = [ - ("ID", ctypes.c_int32), - ("x", ctypes.c_float), - ("y", ctypes.c_float), - ("z", ctypes.c_float), - ("size", ctypes.c_float), - ("params", ctypes.c_int16), - ("residual", ctypes.c_float) - ] - - def pack(self) -> bytes: - return struct.pack(' bytes: - # szName is null-terminated on the wire. - name_bytes = self.szName.rstrip(b'\x00') + b'\x00' - payload = bytearray(name_bytes) - payload += struct.pack(' bytes: - return struct.pack(' bytes: - payload = bytearray(struct.pack(' bytes: - payload = bytearray(struct.pack(' bytes: - payload = bytearray(struct.pack(' bytes: - payload = bytearray(struct.pack(' bytes: - payload = bytearray(struct.pack(' bytes: - """NatNet 4.1+ prefixes each collection with a 4-byte byte count.""" - payload = bytearray(struct.pack(' 0) or natnet_major > 4: - payload += struct.pack(' bytes: - def pack_section(count: int, items, pack_item=lambda item: item.pack()) -> bytes: - """Count-prefixed section holding the first `count` entries of `items`.""" - data = bytearray() - for i in range(count): - data += pack_item(items[i]) - return self._pack_counted_section( - count, bytes(data), natnet_major=natnet_major, natnet_minor=natnet_minor - ) - - payload = bytearray() - - payload += struct.pack(' bytes: - # szName is null-terminated on the wire, not fixed MAX_NAMELENGTH. - name_bytes = self.szName.rstrip(b"\x00") + b"\x00" - payload = bytearray(name_bytes) - payload += struct.pack( - " bytes: - if self.type == int(DataDescriptors.Descriptor_RigidBody): - body = self.RigidBodyDescription.pack() - else: - raise ValueError(f"Unsupported data description type: {self.type}") - payload = bytearray(struct.pack(" bytes: - payload = bytearray(struct.pack(" sDataDescriptions: - """Build the default single-body catalog (Drone id=1) for natnet_ros2.""" - descriptions = sDataDescriptions() - descriptions.nDataDescriptions = 1 - desc = descriptions.arrDataDescriptions[0] - desc.type = int(DataDescriptors.Descriptor_RigidBody) - rb = desc.RigidBodyDescription - rb.szName = b"Drone" - rb.ID = 1 - rb.parentID = -1 - rb.offsetqw = 1.0 - rb.nMarkers = 0 - return descriptions diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_server.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_server.py deleted file mode 100644 index 5ef19009e..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_server.py +++ /dev/null @@ -1,353 +0,0 @@ -from . import natnet_data_types as DataMessages -from . import natnet_server_types as ServerMessages -from . import natnet_model_types as ModelTypes -from enum import Enum -import socket -import threading -import queue -import signal -import ctypes -import time -import typing - - -class TransmissionType(str, Enum): - UNICAST = "unicast" - MULTICAST = "multicast" - -class Client: - def __init__(self, ip: str, port: int, version: typing.Tuple[int, int, int, int] = (4, 4, 0, 0)): - self.ip = ip - self.port = port - self.version = version - self.subscribed_assets = set() - self.socket_lock = threading.Lock() - - def __hash__(self): - # Uniquely identify a client session by their IP and their unique command port. - return hash((self.ip, self.port)) - - def __eq__(self, other): - return (isinstance(other, Client) and - self.ip == other.ip and - self.port == other.port) - - -class NatNetServer: - def __init__(self, - local_interface : str = "172.31.0.200", - transmission_type: TransmissionType = TransmissionType.MULTICAST, - multicast_address : str = "239.255.42.99", - command_port: int = 1510, - data_port : int = 1511, - motive_app_version : typing.Tuple[int, int, int, int]=(3, 1, 0, 0), - natnet_version : typing.Tuple[int, int, int, int]=(4, 4, 0, 0), - high_res_clock_freq : int = 1_000_000_000, - publish_rate : int = 100 # Hz (default 100Hz) - ): - - self.local_interface = local_interface - self.transmission_type = transmission_type - self.multicast_address = multicast_address - self.command_port = command_port - self.data_port = data_port - self.motive_app_version = motive_app_version - self.natnet_version = natnet_version - self.high_res_clock_freq = high_res_clock_freq - self.publish_rate = publish_rate - - self._validate_init_params() - - self.server_description = self._build_server_description() - # Initialize synchronously safe data structures for server state and mocap data - - # Thread-safe queue for Mocp frames - self.mocap_data_queue = queue.Queue(maxsize=100) - self._last_mocap_frame: DataMessages.sFrameOfMocapData | None = None - self._last_mocap_lock = threading.Lock() - - # Thread list and shutdown event - self.threads = [] - self.shutdown_event = threading.Event() - - # Connected clients for unicast mode - self.connected_clients : typing.Set[Client] = set() - self.clients_lock : threading.Lock = threading.Lock() - - # MODELDEF wire cache (Isaac wrapper updates via set_model_def_payload) - self._model_def_lock = threading.Lock() - self._model_def_payload: bytes = ModelTypes.make_default_drone_catalog().pack() - - # Sockets - self.command_socket : socket.socket | None = None - self.data_socket : socket.socket | None = None - - self.running = False - - # When True (default), the background data loop streams frames on its own timer. - # Set False when an external driver (the Isaac wrapper's physics-step callback) - # sends frames synchronously via ``flush_mocap_data``. - self.auto_stream = True - - # start() launches two daemon threads: a command listener (handshake / MODELDEF / keepalive) - # and a data loop that streams mocap frames. The transmission-specific behavior lives in the unicast/multicast subclass. - - def _signal_handler(self, signum, frame): - print(f"\n[NatNetServer] Received interrupt signal {signum}. Initiating shutdown...") - self.shutdown() - - def enqueue_mocap_data(self, new_data: DataMessages.sFrameOfMocapData): - # Thread-safe method to push new physics frames (called by Isaac-Sim extension) - if self.mocap_data_queue.full(): - try: - # Drop oldest frame if falling behind - self.mocap_data_queue.get_nowait() - except queue.Empty: - pass - self.mocap_data_queue.put(new_data) - with self._last_mocap_lock: - self._last_mocap_frame = new_data - - def _get_last_known_mocap_frame(self) -> DataMessages.sFrameOfMocapData | None: - with self._last_mocap_lock: - return self._last_mocap_frame - - def set_model_def_payload(self, payload: bytes) -> None: - """Replace MODELDEF body served on NAT_REQUEST_MODELDEF (Isaac wrapper calls this).""" - with self._model_def_lock: - self._model_def_payload = payload - - def set_model_def_from_descriptions( - self, descriptions: ModelTypes.sDataDescriptions - ) -> None: - """Pack descriptions once and store as the MODELDEF wire cache.""" - self.set_model_def_payload(descriptions.pack()) - - def _get_model_def_payload(self) -> bytes: - """Return cached MODELDEF bytes (command thread only).""" - with self._model_def_lock: - return self._model_def_payload - - def start(self): - # Bind sockets and launch worker threads automatically on init - - # Register signal handlers for graceful shutdown (Catches Ctrl+C and kill) - try: - signal.signal(signal.SIGINT, self._signal_handler) - signal.signal(signal.SIGTERM, self._signal_handler) - except ValueError: - pass # Safe fallback if not called from the main thread - - # 1. Setup Command Socket (Receives connection/discovery requests) - self.command_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - self.command_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.command_socket.bind(('', self.command_port)) - - # 2. Setup Data Socket (Sends outward Mocap frames). - # Bind to the data port so frames leave with source port == data_port. - # libNatNet routes unicast NAT_FRAMEOFDATA by the server's data port - self.data_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - self.data_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.data_socket.bind(('', self.data_port)) - if self.transmission_type == TransmissionType.MULTICAST: - self.data_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.local_interface)) - - # 3. Launch Threads - cmd_thread = threading.Thread(target=self._command_listener_loop, daemon=True) - data_thread = threading.Thread(target=self._data_update_loop, daemon=True) - - self.threads.extend([cmd_thread, data_thread]) - - for t in self.threads: - t.start() - - self.running = True - - def shutdown(self): - # Cleanly shutdown threads and close sockets - self.running = False - self.shutdown_event.set() - - if self.command_socket: - self.command_socket.close() - - if self.data_socket: - self.data_socket.close() - - for t in self.threads: - if t.is_alive(): - t.join(timeout=1.0) - - def _validate_init_params(self): - - # Validate the local_interface is a valid IP address - if not self.local_interface or not isinstance(self.local_interface, str) or self.local_interface.count('.') != 3: - raise ValueError(f"Invalid local interface IP address: {self.local_interface}") - - # Validate between transmission types and address requirements - if self.transmission_type not in TransmissionType: - raise ValueError(f"Invalid transmission type: {self.transmission_type}. Must be 'unicast' or 'multicast'.") - - if self.transmission_type == TransmissionType.MULTICAST and not self.multicast_address: - raise ValueError("Multicast address must be provided for multicast transmission type.") - - if self.transmission_type == TransmissionType.UNICAST and self.multicast_address: - raise ValueError("Multicast address should not be provided for unicast transmission type.") - - if not (0 < self.command_port < 65536): - raise ValueError(f"Invalid command port: {self.command_port}. Must be between 1 and 65535.") - - if not (0 < self.data_port < 65536): - raise ValueError(f"Invalid data port: {self.data_port}. Must be between 1 and 65535.") - - if self.command_port == self.data_port: - raise ValueError("Command port and data port must be different.") - - if self.motive_app_version and (not isinstance(self.motive_app_version, tuple) or len(self.motive_app_version) != 4): - raise ValueError(f"Invalid Motive app version: {self.motive_app_version}. Must be a tuple of 4 integers (major, minor, build, revision).") - - if self.natnet_version and (not isinstance(self.natnet_version, tuple) or len(self.natnet_version) != 4): - raise ValueError(f"Invalid NatNet version: {self.natnet_version}. Must be a tuple of 4 integers (major, minor, build, revision).") - - if self.motive_app_version and not self.motive_app_version[0] == 3: - raise ValueError(f"Unsupported Motive app version: {self.motive_app_version}. Minimum supported version is 3.0.0.0. Recommended to use 3.1.0.0") - - if not self.natnet_version[0] == 4: - raise ValueError(f"Unsupported NatNet version: {self.natnet_version}. Minimum supported version is 4.0.0.0. Recommended to use 4.4.0.0") - - if self.high_res_clock_freq <= 0: - raise ValueError( - f"Invalid high resolution clock frequency: {self.high_res_clock_freq}. Must be a positive integer representing the frequency in Hz." - ) - - if self.publish_rate <= 0: - raise ValueError( - f"Invalid publish rate: {self.publish_rate}. Must be a positive number representing Hz." - ) - def _get_latest_mocap_packet(self) -> DataMessages.sFrameOfMocapData | None: - # Thread-safe method to retrieve the latest mocap data to be sent - try: - return self.mocap_data_queue.get_nowait() - except queue.Empty: - return None - - @staticmethod - def _pad_fixed_string(value: bytes) -> bytes: - """Null-pad a byte string to MAX_NAMELENGTH for fixed-size NatNet name fields.""" - truncated = value[: ServerMessages.MAX_NAMELENGTH - 1] - return truncated + b"\x00" * (ServerMessages.MAX_NAMELENGTH - len(truncated)) - - @staticmethod - def _assign_version_bytes(field: ctypes.Array, version: typing.Tuple[int, int, int, int]) -> None: - for index, component in enumerate(version): - field[index] = component - - @staticmethod - def _assign_ipv4_bytes(field: ctypes.Array, address: str | bytes) -> None: - octets = socket.inet_aton(address) if isinstance(address, str) else address - for index, octet in enumerate(octets): - field[index] = octet - - def _build_server_description(self) -> ServerMessages.sServerDescription: - # Helper to build the server description struct with current server info (e.g. on startup or in response to command request) - description = ServerMessages.sServerDescription() - description.HostPresent = True - description.szHostComputerName = self._pad_fixed_string( - socket.gethostname().encode("utf-8") - ) - self._assign_ipv4_bytes(description.HostComputerAddress, self.local_interface) - description.szHostApp = self._pad_fixed_string(b"Motive") - self._assign_version_bytes(description.HostAppVersion, self.motive_app_version) - self._assign_version_bytes(description.NatNetVersion, self.natnet_version) - description.HighResClockFrequency = self.high_res_clock_freq - description.bConnectionInfoValid = True - description.ConnectionDataPort = self.data_port - description.ConnectionMulticast = self.transmission_type == TransmissionType.MULTICAST - - if self.transmission_type == TransmissionType.MULTICAST: - self._assign_ipv4_bytes(description.ConnectionMulticastAddress, self.multicast_address) - else: - self._assign_ipv4_bytes(description.ConnectionMulticastAddress, b"\x00\x00\x00\x00") - - return description - - def _build_connect_response_payload(self) -> bytes: - """NAT_CONNECT reply: libNatNet parses NAT_SERVERINFO payload as sSender_Server.""" - sender = ServerMessages.sSender_Server() - sender.Common.szName = self._pad_fixed_string(b"Motive") - self._assign_version_bytes(sender.Common.Version, self.motive_app_version) - self._assign_version_bytes(sender.Common.NatNetVersion, self.natnet_version) - sender.HighResClockFrequency = self.high_res_clock_freq - sender.DataPort = self.data_port - sender.IsMulticast = self.transmission_type == TransmissionType.MULTICAST - if self.transmission_type == TransmissionType.MULTICAST: - self._assign_ipv4_bytes(sender.MulticastGroupAddress, self.multicast_address) - else: - self._assign_ipv4_bytes(sender.MulticastGroupAddress, b"\x00\x00\x00\x00") - return sender.pack() - - def _send_packet_to_client( - self, - client: Client, - message_id: ServerMessages.MessageId | int, - payload: bytes, - sock: socket.socket | None = None, - ) -> None: - """Send a NatNet packet to a unicast client (libNatNet 4.4). - - Command replies go out the command socket; mocap frames go out the data socket. - """ - if self.shutdown_event.is_set(): - return - sock = sock or self.command_socket - if not sock: - raise ValueError("[NatNetServer] Socket not initialized. Cannot send packet.") - - header = ServerMessages.sPacketHeader( - iMessage=int(message_id), - nDataBytes=len(payload), - ) - packet = header.pack() + payload - try: - with client.socket_lock: - sock.sendto(packet, (client.ip, client.port)) - except OSError as e: - raise ValueError( - f"[NatNetServer] Error sending message {int(message_id)} to " - f"client {client.ip}:{client.port}: {e}" - ) from e - - def _data_update_loop(self): # Stub: Different betweeen multicast and unicast server implementations, as they will need to handle client connections differently (multicast will just send to the multicast group address) - # Loop to update mocap data and send packets at regular intervals. - pass - - def _send_data_packet(self, client: Client, data_message: DataMessages.sFrameOfMocapData): - # Serialize frame payload and send via the data socket. - # - # Stamp the transmit time in the server's high-resolution clock domain so - # the client can recover per-message transit latency via - # NatNetClient::SecondsSinceHostTimestamp(TransmitTimestamp). This must match - # the clock used in the NAT_ECHORESPONSE handshake (time.time() nanoseconds) - # and the advertised HighResClockFrequency (defaults to 1e9 ticks/s), so the - # SDK's server-clock estimate and this timestamp share one timeline. - data_message.TransmitTimestamp = int(time.time() * 1_000_000_000) - try: - packet_bytes = data_message.pack() - except Exception as e: - raise ValueError(f"[NatNetServer] Error serializing data message: {e}") from e - - self._send_packet_to_client( - client, - ServerMessages.MessageId.NAT_FRAMEOFDATA, - packet_bytes, - sock=self.data_socket, - ) - - def _command_listener_loop(self): # Stub: Different betweeen multicast and unicast server implementations, as they will need to handle client connections differently (multicast will just send to the multicast group address) - # Loop to listen for and handle incoming command requests (e.g. from client apps) - pass - - def _handle_command_request(self, request_data: bytes): # Stub: Different betweeen multicast and unicast server implementations, as they will need to handle client connections differently (multicast will just send to the multicast group address) - # Parse incoming command request, perform requested action, and send response if needed - pass - diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_server_types.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_server_types.py deleted file mode 100644 index 3083f1a6e..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_server_types.py +++ /dev/null @@ -1,156 +0,0 @@ -import ctypes -import struct -from enum import IntEnum - -MAX_NAMELENGTH = 256 -MAX_PACKETSIZE = 65503 - -# NatNet SDK sServerDescription uses default struct alignment (#pragma pack(pop)), not pack(1). -SERVER_DESCRIPTION_WIRE_SIZE = 552 -# NAT_CONNECT / NAT_SERVERINFO reply uses packed sSender_Server (#pragma pack(1) in NatNetTypes.h). -SENDER_SERVER_WIRE_SIZE = 256 + 4 + 4 + 8 + 2 + 1 + 4 # 279 - -# Client/server message ids -class MessageId(IntEnum): - NAT_CONNECT = 0 - NAT_SERVERINFO = 1 - NAT_REQUEST = 2 - NAT_RESPONSE = 3 - NAT_REQUEST_MODELDEF = 4 - NAT_MODELDEF = 5 - NAT_REQUEST_FRAMEOFDATA = 6 - NAT_FRAMEOFDATA = 7 - NAT_MESSAGESTRING = 8 - NAT_DISCONNECT = 9 - NAT_KEEPALIVE = 10 - NAT_DISCONNECTBYTIMEOUT = 11 - NAT_ECHOREQUEST = 12 - NAT_ECHORESPONSE = 13 - NAT_DISCOVERY = 14 - NAT_UNRECOGNIZED_REQUEST = 100 - -# Server/Sender configuration and info -def _fixed_name(field: ctypes.Array) -> bytes: - raw = bytes(field).split(b"\x00", 1)[0] + b"\x00" - if len(raw) > MAX_NAMELENGTH: - raw = raw[: MAX_NAMELENGTH - 1] + b"\x00" - return raw + b"\x00" * (MAX_NAMELENGTH - len(raw)) - - -class sSender(ctypes.Structure): - _pack_ = 1 - _fields_ = [ - ("szName", ctypes.c_char * MAX_NAMELENGTH), # host app's name - ("Version", ctypes.c_uint8 * 4), # host app's version [major.minor.build.revision] - ("NatNetVersion", ctypes.c_uint8 * 4) # host app's NatNet version - ] - - def pack(self) -> bytes: - payload = bytearray() - payload += _fixed_name(self.szName) - payload += bytes(self.Version) - payload += bytes(self.NatNetVersion) - return bytes(payload) - -class sSender_Server(ctypes.Structure): - _pack_ = 1 - _fields_ = [ - ("Common", sSender), - ("HighResClockFrequency", ctypes.c_uint64), - ("DataPort", ctypes.c_uint16), - ("IsMulticast", ctypes.c_bool), - ("MulticastGroupAddress", ctypes.c_uint8 * 4) - ] - - def pack(self) -> bytes: - payload = bytearray(self.Common.pack()) - payload += struct.pack(" bytes: - # Wire layout matches NatNet SDK on x86-64 (3 pad bytes before HighResClockFrequency). - payload = bytearray() - payload.append(1 if self.HostPresent else 0) - payload += _fixed_name(self.szHostComputerName) - payload += bytes(self.HostComputerAddress) - payload += _fixed_name(self.szHostApp) - payload += bytes(self.HostAppVersion) - payload += bytes(self.NatNetVersion) - while len(payload) % 8: - payload.append(0) - payload += struct.pack(" bytes: - return bytes(self) - -# Connection types enum matching NatNet SDK rules -class ConnectionType(IntEnum): - ConnectionType_Multicast = 0 - ConnectionType_Unicast = 1 - -class sNatNetClientConnectParams(ctypes.Structure): - """ - Python ctypes translation of the C++ sNatNetClientConnectParams struct. - Enforces a packed structure byte alignment matching the NatNet binary network protocol. - """ - _pack_ = 1 - _fields_ = [ - ("connectionType", ctypes.c_int32), # 4 bytes (mapping to standard ConnectionType enum) - ("serverCommandPort", ctypes.c_uint16), # 2 bytes - ("serverDataPort", ctypes.c_uint16), # 2 bytes - - # NOTE: Represented as void pointers (c_void_p) to safely match the host system's native bit size (e.g., 8 bytes on 64-bit) without string data unpacking overhead. - ("serverAddress", ctypes.c_void_p), - ("localAddress", ctypes.c_void_p), - ("multicastAddress", ctypes.c_void_p), - - ("subscribedDataOnly", ctypes.c_bool), # 1 byte - ("BitstreamVersion", ctypes.c_uint8 * 4) # 4 bytes: [Major, Minor, Build, Revision] - ] - - def pack(self) -> bytes: - return bytes(self) \ No newline at end of file diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_unicast_server.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_unicast_server.py deleted file mode 100644 index d9c0a1e22..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/optitrack/natnet/emulator/server/natnet_unicast_server.py +++ /dev/null @@ -1,172 +0,0 @@ -import ctypes -import time - -from . import natnet_server_types as ServerTypes -from .natnet_server import TransmissionType, Client, NatNetServer - - -class NatNetUnicastServer(NatNetServer): - def __init__(self, - local_interface="172.31.0.200", - transmission_type: TransmissionType = TransmissionType.UNICAST, - multicast_address=None, - command_port=1510, - data_port=1511 - ): - - if not transmission_type == TransmissionType.UNICAST: - raise ValueError("Transmission type 'MULTICAST' is not supported in NatNetUnicastServer. Please use NatNetMulticastServer instead.") - - super().__init__(local_interface, transmission_type, multicast_address, command_port, data_port) - - def _data_update_loop(self): - # Loop to update mocap data and send packets at regular intervals. - # When auto_stream is False the frames are pumped externally (Isaac physics step), - # so this thread only idles — but stays alive for clean shutdown. - while not self.shutdown_event.is_set(): - time.sleep(1 / self.publish_rate) - if not self.auto_stream: - continue - self.flush_mocap_data() - - def flush_mocap_data(self): - """Send the latest (or last) mocap frame to every connected client, once.""" - with self.clients_lock: - clients = list(self.connected_clients) - if not clients: - return - - data_messages = self._get_latest_mocap_packet() - - if data_messages is None: # If the server stops producing frames, use the last known frame. - data_messages = self._get_last_known_mocap_frame() - if data_messages is None: - return - - for client in clients: - try: - self._send_data_packet(client, data_messages) - except ValueError as e: - print(str(e)) - continue - - def _command_listener_loop(self): - # Listens on UDP command socket for incoming command requests from clients. - # Handles incoming client handshakes and teardown. - - print(f"[Command Listener] Command listener thread started. Listening for incoming client command requests on UDP address:port {self.local_interface}:{self.command_port}...") - - while not self.shutdown_event.is_set(): - try: - data, addr = self.command_socket.recvfrom(1024) # Buffer size of 1024 bytes should be sufficient for command requests - if not data: - continue - self._handle_command_request(data, addr) - except Exception as e: - if self.shutdown_event.is_set(): - break - print(f"[Command Listener] Error receiving command request: {e}") - time.sleep(0.1) # Sleep briefly to avoid tight loop on errors - - def _handle_command_request(self, request_data: bytes, client_address: tuple): - """ - Processes standard binary headers and registers unicast endpoints. - """ - header_size = ctypes.sizeof(ServerTypes.sPacketHeader) - if len(request_data) < header_size: - return - - # Parse the header via ctypes - header = ServerTypes.sPacketHeader.from_buffer_copy(request_data[:header_size]) - - # Handle Connection Handshake - if header.iMessage == int(ServerTypes.MessageId.NAT_CONNECT): - client_requested_version = self.natnet_version # Fallback to server's version. Version handshaking not supported in this extension. - - client_ip, client_port = client_address - - # Create and store a new client object - new_client = Client(client_ip, client_port, version=client_requested_version) - try: - with self.clients_lock: - self.connected_clients.discard(new_client) # Remove any existing client with the same IP and port - self.connected_clients.add(new_client) # Add the new client to the connected clients list - print(f"[Command Handler] Added client {new_client.ip}:{new_client.port} to connected clients list.") - except Exception as e: - print(f"[Command Handler] Error adding client {new_client.ip}:{new_client.port} to connected clients list: {e}") - return - - try: - self._send_packet_to_client( - new_client, - ServerTypes.MessageId.NAT_SERVERINFO, - self._build_connect_response_payload(), - ) - except ValueError as e: - raise ValueError( - f"[Command Handler] Error sending server description to client {client_address}: {e}" - ) from e - print( - f"[Command Handler] Sent server description to client address " - f"through its port {client_address}." - ) - return - - # Non-handshake commands require a prior NAT_CONNECT from this endpoint. - client_ip, client_port = client_address - client = self._find_client(client_ip, client_port) - if client is None: - print( - f"[Command Handler] Ignoring message {header.iMessage} from " - f"unregistered client {client_address}." - ) - return - - if header.iMessage == int(ServerTypes.MessageId.NAT_REQUEST_MODELDEF): - try: - self._send_packet_to_client( - client, - ServerTypes.MessageId.NAT_MODELDEF, - self._get_model_def_payload(), - ) - except ValueError as e: - print( - f"[Command Handler] Error sending MODELDEF to client " - f"{client_address}: {e}" - ) - return - - if header.iMessage == int(ServerTypes.MessageId.NAT_KEEPALIVE): - # Receiving a keepalive refreshes the client's liveness; nothing to send back. - return - - if header.iMessage == int(ServerTypes.MessageId.NAT_ECHOREQUEST): - echo_payload = request_data[header_size : header_size + header.nDataBytes] - # libNatNet expects clientRequestTimestamp + hostReceivedTimestamp (8 + 8 bytes). - host_ts = int(time.time() * 1_000_000_000).to_bytes(8, "little", signed=False) - response_payload = echo_payload[:8].ljust(8, b"\x00") + host_ts - try: - self._send_packet_to_client( - client, - ServerTypes.MessageId.NAT_ECHORESPONSE, - response_payload, - ) - except ValueError as e: - print( - f"[Command Handler] Error sending ECHORESPONSE to client " - f"{client_address}: {e}" - ) - return - - print( - f"[Command Handler] Unhandled message id {header.iMessage} from " - f"registered client {client_address}." - ) - - def _find_client(self, ip: str, port: int) -> Client | None: - target = Client(ip, port) - with self.clients_lock: - for client in self.connected_clients: - if client == target: - return client - return None diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/schema/schema.usda b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/schema/schema.usda deleted file mode 100644 index 55defc731..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/schema/schema.usda +++ /dev/null @@ -1,100 +0,0 @@ -#usda 1.0 -( - """ - NatNet emulator applied API schemas (CODELESS). - - These schemas give the - interface prim typed, Property-panel-friendly attributes WITHOUT compiled - classes. They are codeless — `skipCodeGeneration = true` below — but still need - USD's plugin system to discover the generated registry. - - To produce the registry files (run once, in an env with USD tooling): - - usdGenSchema schema/schema.usda schema/ - - That emits `schema/generatedSchema.usda` and `schema/plugInfo.json`. The Kit - extension then registers the plugin dir on startup (Plug.Registry().RegisterPlugins). - - Until that registration is verified inside Kit, `optitrack.natnet.emulator.isaac` - authors the SAME attribute names as plain namespaced custom attributes (the - registration-free fallback), so nothing here is required for the facade to work. - """ - subLayers = [ - @usd/schema.usda@, - @usdGeom/schema.usda@ - ] -) -{ -} - -over "GLOBAL" ( - customData = { - bool skipCodeGeneration = true - string libraryName = "optitrackNatNet" - string libraryPath = "." - string libraryPrefix = "OptiTrackNatNet" - } -) -{ -} - -class "NatNetInterfaceAPI" ( - inherits = - customData = { - token apiSchemaType = "singleApply" - } - doc = "Marks a prim as a NatNet emulator interface and holds server-level config." -) -{ - bool natnet:isInterface = true ( - doc = "Discovery marker — find_interfaces() scans for prims with this set true." - ) - bool natnet:serverEnabled = true ( - doc = "When true the manager keeps a server running; toggling restarts it." - ) - string natnet:serverIp = "172.31.0.200" ( - doc = "Server interface IP (NatNetUnicastServer.local_interface)." - ) - token natnet:mode = "unicast" ( - allowedTokens = ["unicast", "multicast"] - doc = "Transmission mode." - ) - string natnet:multicastAddr = "239.255.42.99" ( - doc = "Multicast group (only used when mode = multicast)." - ) - int natnet:commandPort = 1510 ( - doc = "NatNet command port." - ) - int natnet:dataPort = 1511 ( - doc = "NatNet data port (frames stream from this source port)." - ) - float natnet:publishRate = 100 ( - doc = "Frame publish rate in Hz." - ) - string natnet:natnetVersion = "4.4.0.0" ( - doc = "Advertised NatNet protocol version." - ) -} - -class "NatNetBodyBindingAPI" ( - inherits = - customData = { - token apiSchemaType = "multipleApply" - token propertyNamespacePrefix = "natnet:body" - } - doc = "One tracked rigid body entry on a NatNet interface prim (apply once per body)." -) -{ - string natnet:body:__INSTANCE_NAME__:rigidBodyName = "" ( - doc = "Motive rigid body name (sRigidBodyDescription.szName)." - ) - int natnet:body:__INSTANCE_NAME__:streamingId = 1 ( - doc = "Streaming ID (sRigidBodyDescription.ID)." - ) - int natnet:body:__INSTANCE_NAME__:parentId = -1 ( - doc = "Parent rigid body ID (-1 if none)." - ) - rel natnet:body:__INSTANCE_NAME__:target ( - doc = "Tracked prim whose world pose is streamed for this body." - ) -} diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/setup.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/setup.py deleted file mode 100644 index 1a1c153fc..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Isaac Sim extension install metadata for the OptiTrack NatNet emulator.""" - -import os - -from setuptools import find_packages, setup - -EXTENSION_PATH = os.path.dirname(os.path.realpath(__file__)) - -setup( - name="optitrack-natnet-emulator", - version="0.1.0", - description="NatNet UDP server emulator for Isaac Sim and natnet_ros2 integration", - license="MIT", - include_package_data=True, - python_requires=">=3.10", - install_requires=[ - "numpy", - "scipy", - ], - packages=find_packages(where="."), - package_dir={"": "."}, - zip_safe=False, -) diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/natnet_test_helpers.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/natnet_test_helpers.py deleted file mode 100644 index 07d6c8a5e..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/natnet_test_helpers.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Shared helpers for optitrack.natnet.emulator unit tests.""" - -from __future__ import annotations - -import socket -import struct -import time -from contextlib import contextmanager - -from optitrack.natnet.emulator import NatNetUnicastServer, TransmissionType -from optitrack.natnet.emulator.server import natnet_server_types as st - - -def ephemeral_udp_port(host: str = "127.0.0.1") -> int: - """Return a free UDP port on *host* by binding and releasing a probe socket.""" - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe: - probe.bind((host, 0)) - return probe.getsockname()[1] - - -class NatNetTestClient: - """Minimal UDP client for NatNet command-port protocol tests.""" - - def __init__(self, host: str = "127.0.0.1", timeout: float = 2.0) -> None: - self._host = host - self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self._sock.bind((host, 0)) - self._sock.settimeout(timeout) - - @property - def local_port(self) -> int: - return self._sock.getsockname()[1] - - def send_message( - self, - server_port: int, - message_id: st.MessageId | int, - payload: bytes = b"", - server_host: str | None = None, - ) -> None: - header = st.sPacketHeader( - iMessage=int(message_id), - nDataBytes=len(payload), - ) - self.send_raw(header.pack() + payload, server_port, server_host) - - def send_raw( - self, - data: bytes, - server_port: int, - server_host: str | None = None, - ) -> None: - """Send a raw UDP datagram (for malformed / malicious packet tests).""" - self._sock.sendto(data, (server_host or self._host, server_port)) - - def send_header_only( - self, - server_port: int, - message_id: st.MessageId | int, - declared_payload_len: int, - server_host: str | None = None, - ) -> None: - """Send a header whose nDataBytes does not match any trailing payload.""" - header = struct.pack(" tuple[int, bytes, tuple[str, int]]: - data, addr = self._sock.recvfrom(65535) - message_id, payload_len = struct.unpack(" None: - self._sock.close() - - -@contextmanager -def running_unicast_server( - command_port: int | None = None, - local_interface: str = "127.0.0.1", - publish_rate: int = 100, -): - """Start NatNetUnicastServer on ephemeral (or fixed) command + data ports. - - Both ports are ephemeral by default so concurrent/sequential tests never - collide on the well-known 1510/1511 pair. - """ - port = command_port if command_port is not None else ephemeral_udp_port(local_interface) - data_port = ephemeral_udp_port(local_interface) - while data_port == port: - data_port = ephemeral_udp_port(local_interface) - server = NatNetUnicastServer( - local_interface=local_interface, - transmission_type=TransmissionType.UNICAST, - multicast_address=None, - command_port=port, - data_port=data_port, - ) - server.publish_rate = publish_rate - server.start() - time.sleep(0.05) - try: - yield server, port - finally: - server.shutdown() diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_catalog.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_catalog.py deleted file mode 100644 index a3cc8cd34..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_catalog.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Catalog builder: body counts, wire fidelity, truncation, MAX_MODELS, duplicate targets.""" - -from __future__ import annotations - -import struct - -import pytest - -from optitrack.natnet.emulator.isaac.catalog import build_catalog, find_duplicate_targets -from optitrack.natnet.emulator.isaac.config import BodyBinding, NatNetInterfaceConfig -from optitrack.natnet.emulator.server import natnet_model_types as mt -from optitrack.natnet.emulator.server.natnet_common import ModelLimits - -pytestmark = pytest.mark.unit - - -def _unpack_bodies(payload: bytes): - """Decode a packed sDataDescriptions into [(name, id, parentID), ...].""" - (n,) = struct.unpack_from("frame builder tests (no USD, no Kit).""" - -from __future__ import annotations - -import math -import struct - -import pytest - -from optitrack.natnet.emulator.isaac.frames import ( - MODEL_LIST_CHANGED, - TRACKING_VALID, - BodySample, - apply_pose_noise, - build_frame, - make_rigid_body_data, - to_motive_pose, -) - -pytestmark = pytest.mark.unit - - -def test_to_motive_pose_z_is_identity(): - pos = (1.0, 2.0, 3.0) - quat = (0.1, 0.2, 0.3, 0.9) - assert to_motive_pose(pos, quat, up_axis="Z") == (pos, quat) - # Case-insensitive and default to Z. - assert to_motive_pose(pos, quat, up_axis="z") == (pos, quat) - assert to_motive_pose(pos, quat) == (pos, quat) - - -def test_to_motive_pose_y_swaps_axes_and_quat(): - # (x, y, z) -> (x, z, -y); quat vector part takes the same swap, scalar kept. - pos, quat = to_motive_pose((1.0, 2.0, 3.0), (0.1, 0.2, 0.3, 0.9), up_axis="Y") - assert pos == (1.0, 3.0, -2.0) - assert quat == (0.1, 0.3, -0.2, 0.9) - - -def test_to_motive_pose_y_maps_isaac_up_to_motive_up(): - # Isaac +Z (up) must become Motive +Y (up) under the Y-up emulation. - pos, _ = to_motive_pose((0.0, 0.0, 1.0), (0.0, 0.0, 0.0, 1.0), up_axis="y") - assert pos == (0.0, 1.0, 0.0) - - -def test_make_rigid_body_data_copies_pose_and_sets_valid_bit(): - rb = make_rigid_body_data( - BodySample(7, (1.0, 2.0, 3.0), (0.0, 0.0, 0.7071068, 0.7071068), valid=True) - ) - assert rb.ID == 7 - assert (rb.x, rb.y, rb.z) == (1.0, 2.0, 3.0) - assert rb.qw == pytest.approx(0.7071068) - assert rb.params & TRACKING_VALID # client requires this bit or it skips the body - - -def test_lost_sample_clears_valid_bit_and_is_nan(): - rb = make_rigid_body_data(BodySample.lost(3)) - assert rb.ID == 3 - assert rb.params & TRACKING_VALID == 0 - assert math.isnan(rb.x) and math.isnan(rb.y) and math.isnan(rb.z) - - -def test_build_frame_no_bodies(): - frame = build_frame(0, []) - assert frame.iFrame == 0 - assert frame.nRigidBodies == 0 - assert frame.params == 0 - - -def test_apply_pose_noise_zero_std_is_identity(): - position = (1.0, 2.0, 3.0) - orientation = (0.0, 0.0, 0.0, 1.0) - pos_out, quat_out = apply_pose_noise(position, orientation, 0.0, 0.0) - assert pos_out == position - assert quat_out == pytest.approx(orientation) - - -def test_apply_pose_noise_preserves_y_position(): - np = pytest.importorskip("numpy") - np.random.seed(0) - position = (0.0, 1.5, 0.0) - orientation = (0.0, 0.0, 0.0, 1.0) - pos_out, _ = apply_pose_noise(position, orientation, 0.001, 0.0) - # Before the euler-yaw shadowing bug, y collapsed to ~0 instead of staying near 1.5. - assert pos_out[1] == pytest.approx(1.5, abs=0.01) - - -def test_apply_pose_noise_adds_position_jitter(): - np = pytest.importorskip("numpy") - np.random.seed(1) - position = (0.0, 0.0, 0.0) - orientation = (0.0, 0.0, 0.0, 1.0) - pos_out, _ = apply_pose_noise(position, orientation, 0.001, 0.0) - assert pos_out != position - - -def test_build_frame_multiple_bodies_preserve_order(): - samples = [ - BodySample(1, (1.0, 0.0, 0.0)), - BodySample(2, (0.0, 2.0, 0.0)), - BodySample(5, (0.0, 0.0, 3.0)), - ] - frame = build_frame(42, samples) - assert frame.iFrame == 42 - assert frame.nRigidBodies == 3 - assert frame.RigidBodies[0].ID == 1 and frame.RigidBodies[0].x == 1.0 - assert frame.RigidBodies[1].ID == 2 and frame.RigidBodies[1].y == 2.0 - assert frame.RigidBodies[2].ID == 5 and frame.RigidBodies[2].z == 3.0 - - -def test_model_list_changed_sets_frame_param_bit(): - assert build_frame(0, [], model_list_changed=True).params & MODEL_LIST_CHANGED - assert build_frame(0, [], model_list_changed=False).params & MODEL_LIST_CHANGED == 0 - - -def test_frame_packs_and_rigid_body_section_decodes(): - frame = build_frame(9, [BodySample(4, (1.5, -2.5, 3.5), (0.0, 0.0, 0.0, 1.0))]) - payload = frame.pack(natnet_major=4, natnet_minor=4) - - # iFrame, then 4.4 counted sections (count+size each) for markersets & other markers. - (iframe,) = struct.unpack_from(" author -> read is stable - author_interface(stage, "/World/NatNetInterface", cfg) - assert read_interface(find_interfaces(stage)[0]) == cfg - - -def test_reauthoring_removes_stale_bodies(): - stage = _new_stage() - author_interface(stage, "/World/NatNetInterface", _CONFIG) - - single = NatNetInterfaceConfig.from_dict( - {"bodies": [{"rigid_body_name": "Drone", "target_prim": "/World/base_link", "streaming_id": 1}]} - ) - author_interface(stage, "/World/NatNetInterface", single) - - cfg = read_interface(find_interfaces(stage)[0]) - assert [b.rigid_body_name for b in cfg.bodies] == ["Drone"] - - -def test_up_axis_authors_and_reads_back(): - stage = _new_stage() - # Default (absent) -> Z. - author_interface(stage, "/World/NatNetInterface", _CONFIG) - assert read_interface(find_interfaces(stage)[0]).up_axis == "Z" - - # Explicit Y survives the USD round trip. - cfg = NatNetInterfaceConfig.from_dict({**_CONFIG, "up_axis": "Y"}) - author_interface(stage, "/World/NatNetInterface", cfg) - assert read_interface(find_interfaces(stage)[0]).up_axis == "Y" - - -def test_pose_noise_authors_and_reads_back(): - stage = _new_stage() - cfg = NatNetInterfaceConfig.from_dict( - { - **_CONFIG, - "pose_noise_enabled": False, - "pose_noise_std_meters": 0.001, - "pose_noise_rotation_deg": 0.1, - } - ) - author_interface(stage, "/World/NatNetInterface", cfg) - read = read_interface(find_interfaces(stage)[0]) - assert read.pose_noise_enabled is False - assert read.pose_noise_std_meters == pytest.approx(0.001) - assert read.pose_noise_rotation_deg == pytest.approx(0.1) - - -def test_empty_target_round_trips(): - # The UI's "Add body" can create a body with no target yet (set later in the - # Property panel); it must author and read back cleanly with an empty target. - stage = _new_stage() - cfg = NatNetInterfaceConfig(bodies=[BodyBinding("Drone", "", 1)]) - author_interface(stage, "/World/NatNetInterface", cfg) - read = read_interface(find_interfaces(stage)[0]) - assert read.bodies[0].rigid_body_name == "Drone" - assert read.bodies[0].target_prim == "" - - -def test_invalid_config_raises_before_authoring(): - stage = _new_stage() - with pytest.raises(ValueError): - author_interface(stage, "/World/NatNetInterface", {"mode": "bogus"}) - assert find_interfaces(stage) == [] diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_interface_config.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_interface_config.py deleted file mode 100644 index 9247c5f12..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_interface_config.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Hermetic unit tests for the pure-Python NatNet interface config model. - -No USD / Isaac imports — exercises dataclasses, dict normalization, the attribute -name builder, instance-key generation, and validation. -""" - -from __future__ import annotations - -import pytest - -from optitrack.natnet.emulator.isaac.config import ( - DEFAULT_POSE_NOISE_ENABLED, - DEFAULT_POSE_NOISE_ROTATION_DEG, - DEFAULT_POSE_NOISE_STD_METERS, - BodyBinding, - NatNetInterfaceConfig, - body_attr_name, - make_instance_key, -) - -pytestmark = pytest.mark.unit - - -def test_defaults_match_server_expectations(): - cfg = NatNetInterfaceConfig() - assert cfg.server_enabled is True - assert cfg.server_ip == "172.31.0.200" - assert cfg.mode == "unicast" - assert cfg.command_port == 1510 - assert cfg.data_port == 1511 - assert cfg.up_axis == "Z" # Isaac/USD native; matches the reference Motive setup - assert cfg.pose_noise_enabled is DEFAULT_POSE_NOISE_ENABLED - assert cfg.pose_noise_std_meters == DEFAULT_POSE_NOISE_STD_METERS - assert cfg.pose_noise_rotation_deg == DEFAULT_POSE_NOISE_ROTATION_DEG - assert cfg.bodies == [] - - -def test_up_axis_from_dict_normalizes_case(): - assert NatNetInterfaceConfig.from_dict({"up_axis": "y"}).up_axis == "Y" - assert NatNetInterfaceConfig.from_dict({"up_axis": "z"}).up_axis == "Z" - # Absent -> default Z. - assert NatNetInterfaceConfig.from_dict({}).up_axis == "Z" - - -def test_up_axis_survives_round_trip(): - cfg = NatNetInterfaceConfig.from_dict({"up_axis": "Y"}) - assert NatNetInterfaceConfig.from_dict(cfg.to_dict()).up_axis == "Y" - - -def test_pose_noise_survives_round_trip(): - cfg = NatNetInterfaceConfig.from_dict( - { - "pose_noise_enabled": False, - "pose_noise_std_meters": 0.001, - "pose_noise_rotation_deg": 0.1, - } - ) - restored = NatNetInterfaceConfig.from_dict(cfg.to_dict()) - assert restored.pose_noise_enabled is False - assert restored.pose_noise_std_meters == 0.001 - assert restored.pose_noise_rotation_deg == 0.1 - - -def test_from_dict_with_bodies_as_list(): - cfg = NatNetInterfaceConfig.from_dict( - { - "server_ip": "10.0.0.5", - "bodies": [ - {"rigid_body_name": "Drone", "target_prim": "/World/base_link", "streaming_id": 1}, - ], - } - ) - assert cfg.server_ip == "10.0.0.5" - assert len(cfg.bodies) == 1 - assert cfg.bodies[0] == BodyBinding("Drone", "/World/base_link", 1, -1) - - -def test_from_dict_with_bodies_as_prim_mapping(): - # The "dictionary of prims -> rigid body names and stuff" form. - cfg = NatNetInterfaceConfig.from_dict( - { - "bodies": { - "/World/base_link": {"rigid_body_name": "Drone", "streaming_id": 1}, - "/World/target": {"rigid_body_name": "Target", "streaming_id": 2}, - } - } - ) - by_name = {b.rigid_body_name: b for b in cfg.bodies} - assert by_name["Drone"].target_prim == "/World/base_link" - assert by_name["Target"].target_prim == "/World/target" - assert by_name["Target"].streaming_id == 2 - - -def test_to_dict_round_trip(): - cfg = NatNetInterfaceConfig.from_dict( - { - "mode": "multicast", - "publish_rate": 120, - "bodies": [{"rigid_body_name": "Drone", "target_prim": "/World/base_link"}], - } - ) - restored = NatNetInterfaceConfig.from_dict(cfg.to_dict()) - assert restored == cfg - - -def test_body_from_dict_requires_target_and_name(): - with pytest.raises(ValueError): - BodyBinding.from_dict({"rigid_body_name": "Drone"}) # no target_prim - with pytest.raises(ValueError): - BodyBinding.from_dict({"target_prim": "/World/base_link"}) # no name - - -def test_body_attr_name_builder(): - assert body_attr_name("Drone", "streamingId") == "natnet:body:Drone:streamingId" - - -def test_make_instance_key_sanitizes_and_dedupes(): - used: set[str] = set() - assert make_instance_key("Drone 1", used) == "Drone_1" - # collision after sanitization -> numeric suffix - assert make_instance_key("Drone-1", used) == "Drone_1_1" - # leading digit gets a safe prefix - assert make_instance_key("3PO", used).startswith("b_") - - -def test_assign_instance_keys_are_unique(): - cfg = NatNetInterfaceConfig( - bodies=[ - BodyBinding("Drone", "/World/a", 1), - BodyBinding("Drone", "/World/b", 2), # duplicate display name - ] - ) - keys = [k for k, _ in cfg.assign_instance_keys()] - assert len(set(keys)) == 2 - - -@pytest.mark.parametrize( - "overrides", - [ - {"mode": "bogus"}, - {"command_port": 0}, - {"data_port": 70000}, - {"command_port": 1510, "data_port": 1510}, - {"publish_rate": 0}, - {"up_axis": "X"}, - {"up_axis": "bogus"}, - {"pose_noise_std_meters": -0.001}, - {"pose_noise_rotation_deg": -0.1}, - ], -) -def test_validate_rejects_bad_server_config(overrides): - cfg = NatNetInterfaceConfig(**overrides) - with pytest.raises(ValueError): - cfg.validate() - - -def test_validate_rejects_duplicate_streaming_ids(): - cfg = NatNetInterfaceConfig( - bodies=[ - BodyBinding("A", "/World/a", 1), - BodyBinding("B", "/World/b", 1), - ] - ) - with pytest.raises(ValueError): - cfg.validate() - - -def test_validate_rejects_blank_rigid_body_name(): - cfg = NatNetInterfaceConfig(bodies=[BodyBinding("", "/World/a", 1)]) - with pytest.raises(ValueError): - cfg.validate() - - -def test_validate_allows_empty_target(): - # An empty target is valid: a freshly added body to be pointed in the UI/Property panel. - cfg = NatNetInterfaceConfig(bodies=[BodyBinding("Drone", "", 1)]) - assert cfg.validate() is cfg - - -def test_validate_accepts_good_config(): - cfg = NatNetInterfaceConfig( - bodies=[BodyBinding("Drone", "/World/base_link", 1)] - ) - assert cfg.validate() is cfg diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_pose_sampling.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_pose_sampling.py deleted file mode 100644 index 25db5ad96..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_pose_sampling.py +++ /dev/null @@ -1,238 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Pose sampling and catalog resync against an in-memory USD stage (fake server, no sockets).""" - -from __future__ import annotations - -import math - -import pytest - -pytest.importorskip("pxr") - -from pxr import Gf, Usd, UsdGeom # noqa: E402 - -from optitrack.natnet.emulator.isaac.config import BodyBinding, NatNetInterfaceConfig # noqa: E402 -from optitrack.natnet.emulator.isaac.frames import MODEL_LIST_CHANGED, TRACKING_VALID # noqa: E402 -from optitrack.natnet.emulator.isaac.manager import NatNetServerManager # noqa: E402 -from optitrack.natnet.emulator.isaac.usd_bindings import author_interface, read_world_pose # noqa: E402 - -pytestmark = pytest.mark.unit - - -class FakeServer: - def __init__(self): - self.frames = [] - self.payloads = [] - - def set_model_def_payload(self, payload): - self.payloads.append(payload) - - def start(self): - pass - - def shutdown(self): - pass - - def enqueue_mocap_data(self, frame): - self.frames.append(frame) - - -def _xform(stage, path, translate=(0.0, 0.0, 0.0)): - xform = UsdGeom.Xform.Define(stage, path) - xform.AddTranslateOp().Set(Gf.Vec3d(*translate)) - return xform - - -def _manager_with_fake(): - fake = FakeServer() - mgr = NatNetServerManager(server_factory=lambda cfg: fake) - return mgr, fake - - -# --- read_world_pose ------------------------------------------------------------- - - -def test_read_world_pose_returns_translation(): - stage = Usd.Stage.CreateInMemory() - _xform(stage, "/World/base_link", translate=(1.0, 2.0, 3.0)) - pose = read_world_pose(stage.GetPrimAtPath("/World/base_link")) - assert pose is not None - (x, y, z), (qx, qy, qz, qw) = pose - assert (round(x, 3), round(y, 3), round(z, 3)) == (1.0, 2.0, 3.0) - assert qw == pytest.approx(1.0) - - -def test_read_world_pose_invalid_prim_is_none(): - stage = Usd.Stage.CreateInMemory() - assert read_world_pose(stage.GetPrimAtPath("/World/nope")) is None - - -# --- sample_once ----------------------------------------------------------------- - - -def test_sample_once_no_bodies(): - stage = Usd.Stage.CreateInMemory() - author_interface(stage, "/World/NatNetInterface", NatNetInterfaceConfig()) - mgr, fake = _manager_with_fake() - mgr.start_server(NatNetInterfaceConfig(server_ip="127.0.0.1")) - frame = mgr.sample_once(stage) - assert frame is not None and frame.nRigidBodies == 0 - - -def test_sample_once_streams_world_pose(): - stage = Usd.Stage.CreateInMemory() - _xform(stage, "/World/base_link", translate=(4.0, 5.0, 6.0)) - cfg = NatNetInterfaceConfig( - server_ip="127.0.0.1", - pose_noise_enabled=False, - bodies=[BodyBinding("Drone", "/World/base_link", 1)], - ) - author_interface(stage, "/World/NatNetInterface", cfg) - mgr, fake = _manager_with_fake() - mgr.start_server(cfg) - - frame = mgr.sample_once(stage) - assert frame.nRigidBodies == 1 - rb = frame.RigidBodies[0] - assert rb.ID == 1 - assert (round(rb.x, 3), round(rb.y, 3), round(rb.z, 3)) == (4.0, 5.0, 6.0) - assert rb.params & TRACKING_VALID - # First frame after start resyncs -> client should be told the model list changed. - assert frame.params & MODEL_LIST_CHANGED - - -def test_sample_once_missing_prim_is_lost(): - stage = Usd.Stage.CreateInMemory() - cfg = NatNetInterfaceConfig( - server_ip="127.0.0.1", bodies=[BodyBinding("Ghost", "/World/missing", 9)] - ) - author_interface(stage, "/World/NatNetInterface", cfg) - mgr, fake = _manager_with_fake() - mgr.start_server(cfg) - - frame = mgr.sample_once(stage) - rb = frame.RigidBodies[0] - assert rb.ID == 9 - assert rb.params & TRACKING_VALID == 0 - assert math.isnan(rb.x) - - -def test_moving_prim_updates_streamed_position(): - stage = Usd.Stage.CreateInMemory() - xform = _xform(stage, "/World/base_link", translate=(0.0, 0.0, 0.0)) - cfg = NatNetInterfaceConfig( - server_ip="127.0.0.1", - pose_noise_enabled=False, - bodies=[BodyBinding("Drone", "/World/base_link", 1)], - ) - author_interface(stage, "/World/NatNetInterface", cfg) - mgr, fake = _manager_with_fake() - mgr.start_server(cfg) - - mgr.sample_once(stage) - xform.GetOrderedXformOps()[0].Set(Gf.Vec3d(10.0, 0.0, 0.0)) - frame = mgr.sample_once(stage) - assert round(frame.RigidBodies[0].x, 3) == 10.0 - - -def test_up_axis_z_streams_isaac_pose_as_is(): - stage = Usd.Stage.CreateInMemory() - _xform(stage, "/World/base_link", translate=(1.0, 2.0, 3.0)) - cfg = NatNetInterfaceConfig( - server_ip="127.0.0.1", - up_axis="Z", - pose_noise_enabled=False, - bodies=[BodyBinding("Drone", "/World/base_link", 1)], - ) - author_interface(stage, "/World/NatNetInterface", cfg) - mgr, _fake = _manager_with_fake() - mgr.start_server(cfg) - - rb = mgr.sample_once(stage).RigidBodies[0] - assert (round(rb.x, 3), round(rb.y, 3), round(rb.z, 3)) == (1.0, 2.0, 3.0) - - -def test_up_axis_y_reaxes_streamed_pose(): - # Y-up Motive emulation: Isaac (x, y, z) streams as (x, z, -y). - stage = Usd.Stage.CreateInMemory() - _xform(stage, "/World/base_link", translate=(1.0, 2.0, 3.0)) - cfg = NatNetInterfaceConfig( - server_ip="127.0.0.1", - up_axis="Y", - pose_noise_enabled=False, - bodies=[BodyBinding("Drone", "/World/base_link", 1)], - ) - author_interface(stage, "/World/NatNetInterface", cfg) - mgr, _fake = _manager_with_fake() - mgr.start_server(cfg) - - rb = mgr.sample_once(stage).RigidBodies[0] - assert (round(rb.x, 3), round(rb.y, 3), round(rb.z, 3)) == (1.0, 3.0, -2.0) - - -def test_body_added_while_live_is_picked_up_on_resync(): - stage = Usd.Stage.CreateInMemory() - _xform(stage, "/World/a", translate=(1.0, 0.0, 0.0)) - _xform(stage, "/World/b", translate=(0.0, 2.0, 0.0)) - cfg1 = NatNetInterfaceConfig( - server_ip="127.0.0.1", bodies=[BodyBinding("A", "/World/a", 1)] - ) - author_interface(stage, "/World/NatNetInterface", cfg1) - mgr, fake = _manager_with_fake() - mgr.start_server(cfg1) - - first = mgr.sample_once(stage) - assert first.nRigidBodies == 1 - - # Add a second body live: re-author the prim, then mark dirty (the UI/USD-notice - # path calls mark_dirty for us in Kit). - cfg2 = NatNetInterfaceConfig( - server_ip="127.0.0.1", - bodies=[BodyBinding("A", "/World/a", 1), BodyBinding("B", "/World/b", 2)], - ) - author_interface(stage, "/World/NatNetInterface", cfg2) - mgr.mark_dirty() - - second = mgr.sample_once(stage) - assert second.nRigidBodies == 2 - assert second.params & MODEL_LIST_CHANGED # catalog grew -> tell the client - ids = {second.RigidBodies[i].ID for i in range(second.nRigidBodies)} - assert ids == {1, 2} - # MODELDEF payload was refreshed on the server for the new catalog. - assert len(fake.payloads) >= 2 - - -def test_target_prim_created_after_start_becomes_valid(): - """A body whose target prim is spawned *after* the server starts (e.g. a Pegasus - drone base_link created on the first Play tick) must start streaming a valid pose - as soon as the prim appears — no mark_dirty/resync required, because the target - path is re-resolved every sample.""" - stage = Usd.Stage.CreateInMemory() - cfg = NatNetInterfaceConfig( - server_ip="127.0.0.1", - pose_noise_enabled=False, - bodies=[BodyBinding("Drone", "/World/drone1/base_link", 1)], - ) - author_interface(stage, "/World/NatNetInterface", cfg) - mgr, _fake = _manager_with_fake() - mgr.start_server(cfg) - - # Prim does not exist yet -> lost. - first = mgr.sample_once(stage) - assert first.RigidBodies[0].params & TRACKING_VALID == 0 - - # Spawn the target prim later (simulating the Play-tick drone creation). - _xform(stage, "/World/drone1/base_link", translate=(7.0, 8.0, 9.0)) - - # Next sample re-resolves the path -> valid pose, with no mark_dirty(). - second = mgr.sample_once(stage) - rb = second.RigidBodies[0] - assert rb.params & TRACKING_VALID - assert (round(rb.x, 3), round(rb.y, 3), round(rb.z, 3)) == (7.0, 8.0, 9.0) - - -def test_sample_once_noop_without_server(): - stage = Usd.Stage.CreateInMemory() - mgr, _fake = _manager_with_fake() - assert mgr.sample_once(stage) is None diff --git a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_pose_streaming.py b/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_pose_streaming.py deleted file mode 100644 index bc526c5b5..000000000 --- a/simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_pose_streaming.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""Loopback: sample_once on a USD prim → NAT_FRAMEOFDATA with sampled position. - -Real server + UDP sockets + in-memory stage. Requires pxr. -""" - -from __future__ import annotations - -import socket -import struct -import time - -import pytest - -pytest.importorskip("pxr") - -from pxr import Gf, Usd, UsdGeom # noqa: E402 - -from natnet_test_helpers import NatNetTestClient, ephemeral_udp_port # noqa: E402 - -from optitrack.natnet.emulator.isaac.config import BodyBinding, NatNetInterfaceConfig # noqa: E402 -from optitrack.natnet.emulator.isaac.manager import NatNetServerManager # noqa: E402 -from optitrack.natnet.emulator.isaac.usd_bindings import author_interface # noqa: E402 -from optitrack.natnet.emulator.server import natnet_server_types as st # noqa: E402 - -pytestmark = pytest.mark.unit - - -def _decode_first_rigid_body(payload: bytes): - # iFrame(4) + markersets(count4+size4) + othermarkers(count4+size4) = 20, then - # rigid bodies: count(4) + size(4) at 20, first body at 28: id + xyz. - rb_count, _rb_size = struct.unpack_from(" server only; real Motive sends no reply. An echo - # reply makes libNatNet log "Received unrecognized message Message=10". - with running_unicast_server() as (server, command_port): - client = NatNetTestClient(timeout=0.5) - try: - client.send_message(command_port, st.MessageId.NAT_CONNECT) - client.recv_message() - - client.send_message(command_port, st.MessageId.NAT_KEEPALIVE) - with pytest.raises(socket.timeout): - client.recv_message() - - # Client stays registered and keeps receiving frames. - assert len(server.connected_clients) == 1 - finally: - client.close() - - -# ============================================================================= -# Malformed datagrams — registered client & recovery -# ============================================================================= - - -def test_unknown_message_from_registered_client_gets_no_reply(): - with running_unicast_server() as (server, command_port): - client = NatNetTestClient(timeout=0.5) - try: - client.send_message(command_port, st.MessageId.NAT_CONNECT) - client.recv_message() - - client.send_message(command_port, 999) - with pytest.raises(socket.timeout): - client.recv_message() - - assert len(server.connected_clients) == 1 - finally: - client.close() - - -def test_server_survives_malformed_burst_then_valid_connect(): - with running_unicast_server() as (server, command_port): - client = NatNetTestClient(timeout=2.0) - try: - client.send_raw(b"", command_port) - client.send_raw(b"\xff", command_port) - client.send_header_only(command_port, 999, declared_payload_len=50000) - client.send_message(command_port, st.MessageId.NAT_REQUEST_MODELDEF) - - client.send_message(command_port, st.MessageId.NAT_CONNECT) - message_id, _payload, _addr = client.recv_message() - finally: - client.close() - - assert message_id == int(st.MessageId.NAT_SERVERINFO) - assert len(server.connected_clients) == 1 diff --git a/simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_launch_script.py b/simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_launch_script.py index fbc8040d0..d193fe1eb 100644 --- a/simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_launch_script.py +++ b/simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_launch_script.py @@ -9,6 +9,8 @@ single-drone example script always enables LiDAR; AirStack pytest ``isaacsim`` liveliness sets ``ENABLE_LIDAR=true`` so behavior matches. - PLAY_SIM_ON_START (default true): autoplay timeline + - ISAAC_SIM_SCENE / ISAAC_SIM_STAGE_SCALE (set by `airstack up --scene`): + scene to load — a Pegasus catalog key or USD URL (default: Default Environment) - ISAAC_SIM_HEADLESS / ISAAC_SIM_LIVESTREAM: see pegasus_app.py """ @@ -22,7 +24,12 @@ simulation_app = create_simulation_app() from pegasus.simulator.params import SIMULATION_ENVIRONMENTS # noqa: E402 -from pegasus_app import PegasusApp, row_spawn_configs # noqa: E402 +from pegasus_app import ( # noqa: E402 + PegasusApp, + resolve_scene_from_env, + resolve_spawn_center_from_env, + row_spawn_configs, +) NUM_ROBOTS = int(os.environ.get("NUM_ROBOTS", "1")) ENABLE_LIDAR = os.environ.get("ENABLE_LIDAR", "false").lower() == "true" @@ -30,11 +37,15 @@ def main(): print(f"[example_multi] Spawning {NUM_ROBOTS} drone(s), lidar={'on' if ENABLE_LIDAR else 'off'}") + env_url, stage_scale = resolve_scene_from_env(SIMULATION_ENVIRONMENTS) + print(f"[example_multi] Scene: {env_url} (stage_scale={stage_scale})") PegasusApp( - env_url=SIMULATION_ENVIRONMENTS["Default Environment"], - stage_scale=1.0, - # Spread drones along X: -2, 0, 2, 4, ... centered near origin - drone_configs=row_spawn_configs(NUM_ROBOTS), + env_url=env_url, + stage_scale=stage_scale, + # Spread drones along X, centered on ISAAC_SIM_SPAWN_XY (default origin) + drone_configs=row_spawn_configs( + NUM_ROBOTS, center_xy=resolve_spawn_center_from_env() + ), enable_lidar=ENABLE_LIDAR, ).run() diff --git a/simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_natnet_launch_script.py b/simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_natnet_launch_script.py deleted file mode 100644 index 85b28f00d..000000000 --- a/simulation/isaac-sim/launch_scripts/example_multi_px4_pegasus_natnet_launch_script.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python -""" -Multi-drone PX4 Pegasus launcher with OptiTrack NatNet mocap streaming. - -Same scene prep and sensor stack as ``example_multi_px4_pegasus_launch_script.py``, -plus a Motive-compatible NatNet server that always streams one rigid body per drone -and a shared static ``Target`` (id 100) at ``/World/target``. - -Body naming: - - ``NUM_ROBOTS=1``: drone body ``Drone`` (id 1) - - ``NUM_ROBOTS>1``: ``Drone1``, ``Drone2``, … (ids 1..N) - -Intended multi-robot profile pairing (see ``natnet_config.yaml`` commented scaffolding): - - ``robot_1``: tracks its drone + the shared ``Target`` - - ``robot_2``: tracks its drone + the shared ``Target`` - - ``robot_3``: tracks its drone only (no Target in profile) - -Set ``NUM_ROBOTS=3`` on both sim and robot stacks; each container picks its profile -via ``ROBOT_NAME``. - -Env: - - ``NUM_ROBOTS`` (default 1) - - ``ENABLE_LIDAR`` (default false) - - ``PLAY_SIM_ON_START`` (default true) - - ``ISAAC_SIM_HEADLESS`` / ``ISAAC_SIM_LIVESTREAM``: see pegasus_app.py -""" - -import os -import sys - -import carb - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from pegasus_app import create_simulation_app - -# Must be created before any omni/pegasus imports. -simulation_app = create_simulation_app() - -from pegasus.simulator.params import SIMULATION_ENVIRONMENTS # noqa: E402 -from pegasus_app import PegasusApp, row_spawn_configs # noqa: E402 - -# Register the emulator extension with Kit before importing from it. -# See docs/simulation/isaac_sim/natnet_emulator.md. -from isaacsim.core.utils.extensions import enable_extension # noqa: E402 - -enable_extension("optitrack.natnet.emulator") - -from optitrack.natnet.emulator.isaac import ( # noqa: E402 - DEFAULT_TARGET_PATH, - DEFAULT_TARGET_POSITION, - DEFAULT_TARGET_STREAMING_ID, - author_static_target, - author_drone_natnet_interface, -) - -# --------------------- CONFIGURATION --------------------- -NUM_ROBOTS = int(os.environ.get("NUM_ROBOTS", "1")) -ENABLE_LIDAR = os.environ.get("ENABLE_LIDAR", "false").lower() == "true" -# Base name for the streamed drone bodies; drone i is streamed with id i. Must match -# the body entries in the per-robot profiles in natnet_config.yaml. -# See docs/simulation/isaac_sim/natnet_emulator.md. -NATNET_BODY_NAME = "Drone" -NATNET_TARGET_NAME = "Target" - -_NATNET_SERVER_KWARGS = { - "pose_noise_enabled": True, - "pose_noise_std_meters": 0.0005, - "pose_noise_rotation_deg": 0.05, -} -# --------------------------------------------------------- - - -def _drone_body_name(index: int) -> str: - """Single agent uses bare ``Drone``; multi uses ``Drone1``, ``Drone2``, …""" - return NATNET_BODY_NAME if NUM_ROBOTS == 1 else f"{NATNET_BODY_NAME}{index}" - - -class NatNetPegasusApp(PegasusApp): - - def post_spawn(self, stage): - """Author NatNet bodies: one per drone plus one shared static target. - - Runs before the timeline starts; the emulator extension builds the server - from this prim on Play. - """ - try: - author_static_target(stage, DEFAULT_TARGET_PATH, DEFAULT_TARGET_POSITION) - bodies = [ - (_drone_body_name(i), i, f"/World/drone{i}/base_link/body") - for i in range(1, NUM_ROBOTS + 1) - ] - bodies.append((NATNET_TARGET_NAME, DEFAULT_TARGET_STREAMING_ID, DEFAULT_TARGET_PATH)) - - author_drone_natnet_interface(stage, bodies, **_NATNET_SERVER_KWARGS) - carb.log_warn( - f"[natnet] Interface authored with {NUM_ROBOTS} drone body(ies) " - f"and shared target '{NATNET_TARGET_NAME}' (robot_1/robot_2 subscribe via " - f"natnet_config; robot_3 omits Target)." - ) - except Exception as exc: # noqa: BLE001 - never let NatNet kill the sim - carb.log_error(f"[natnet] Failed to author interface: {exc}") - - -def main(): - print(f"[example_multi_natnet] Spawning {NUM_ROBOTS} drone(s), lidar={'on' if ENABLE_LIDAR else 'off'}") - NatNetPegasusApp( - env_url=SIMULATION_ENVIRONMENTS["Default Environment"], - stage_scale=1.0, - drone_configs=row_spawn_configs(NUM_ROBOTS), - enable_lidar=ENABLE_LIDAR, - ).run() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py b/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py index 8e3ad9c5c..c59fcbbe8 100755 --- a/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py +++ b/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py @@ -11,7 +11,9 @@ (pass ``save_scene_to=`` below) Env (see pegasus_app.py): ISAAC_SIM_LIVESTREAM, ISAAC_SIM_HEADLESS, -PLAY_SIM_ON_START. +PLAY_SIM_ON_START, and ISAAC_SIM_SCENE / ISAAC_SIM_STAGE_SCALE (set by +`airstack up --scene ` — a Pegasus catalog key or USD URL; +default: Default Environment). """ import os @@ -24,15 +26,17 @@ simulation_app = create_simulation_app() from pegasus.simulator.params import SIMULATION_ENVIRONMENTS # noqa: E402 -from pegasus_app import PegasusApp # noqa: E402 +from pegasus_app import PegasusApp, resolve_scene_from_env # noqa: E402 def main(): + # Scene from `airstack up --scene` (or set ISAAC_SIM_SCENE to any + # catalog key / USD URL); stage_scale converts cm-authored stages to m. + env_url, stage_scale = resolve_scene_from_env(SIMULATION_ENVIRONMENTS) + print(f"[example_one] Scene: {env_url} (stage_scale={stage_scale})") PegasusApp( - # Environment to load. Swap this URL/key for any other scene. - env_url=SIMULATION_ENVIRONMENTS["Default Environment"], - # 0.01 converts cm→m for Nucleus assets; 1.0 if already in meters. - stage_scale=1.0, + env_url=env_url, + stage_scale=stage_scale, drone_configs=[ { "domain_id": 1, # MAVLink port = 14540 + vehicle_id (= domain_id) diff --git a/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_natnet_launch_script.py b/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_natnet_launch_script.py deleted file mode 100644 index 3e0608cb4..000000000 --- a/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_natnet_launch_script.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python -""" -Single-drone PX4 Pegasus launcher with OptiTrack NatNet mocap streaming. - -Same scene prep and sensor stack as ``example_one_px4_pegasus_launch_script.py``, plus -a Motive-compatible NatNet server that always streams: - - - ``Drone`` (id 1) from the Pegasus ``body`` prim under ``/World/base_link`` - - ``Target`` (id 100) from a static ``/World/target`` prim - -Pair with robot-side ``LAUNCH_NATNET=true`` and a matching ``natnet_config.yaml`` -profile. To consume the target on the robot, add a Target body to the profile -(see the commented scaffolding in ``natnet_config.yaml``). - -Override rigid-body names with ``NATNET_BODY_NAME`` / ``NATNET_TARGET_NAME``. -""" - -import os -import sys - -import carb - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from pegasus_app import create_simulation_app - -# Must be created before any omni/pegasus imports. -simulation_app = create_simulation_app() - -from pegasus.simulator.params import SIMULATION_ENVIRONMENTS # noqa: E402 -from pegasus_app import PegasusApp # noqa: E402 -from gps_utils import DEFAULT_WORLD_ORIGIN # noqa: E402 - -# Register the emulator extension with Kit before importing from it. -# See docs/simulation/isaac_sim/natnet_emulator.md. -from isaacsim.core.utils.extensions import enable_extension # noqa: E402 - -enable_extension("optitrack.natnet.emulator") - -from optitrack.natnet.emulator.isaac import ( # noqa: E402 - DEFAULT_TARGET_PATH, - DEFAULT_TARGET_POSITION, - DEFAULT_TARGET_STREAMING_ID, - author_static_target, - author_drone_natnet_interface, -) - -# --------------------- CONFIGURATION --------------------- -# What world (0, 0, 0) maps to in GPS coordinates. Must match the GCS origin and -# the robot's natnet_ros2 mavros_gp_origin.yaml. -WORLD_GPS_ORIGIN = DEFAULT_WORLD_ORIGIN - -# Rigid body this scene streams. Must match a body entry in the robot's profile in -# natnet_ros2/config/natnet_config.yaml — a mismatch fails silently. -# See docs/simulation/isaac_sim/natnet_emulator.md. -NATNET_BODY_NAME = os.environ.get("NATNET_BODY_NAME", "Drone") -NATNET_BODY_ID = 1 -NATNET_TARGET_NAME = os.environ.get("NATNET_TARGET_NAME", "Target") - -_NATNET_SERVER_KWARGS = { - "pose_noise_enabled": True, - "pose_noise_std_meters": 0.0005, - "pose_noise_rotation_deg": 0.05, -} -# --------------------------------------------------------- - - -class NatNetPegasusApp(PegasusApp): - - def post_spawn(self, stage): - """Author the NatNet interface prim (drone + static target). - - Runs before the timeline starts; the emulator extension builds the server - from this prim on Play. - """ - try: - author_static_target(stage, DEFAULT_TARGET_PATH, DEFAULT_TARGET_POSITION) - bodies = [ - (NATNET_BODY_NAME, NATNET_BODY_ID, "/World/base_link/body"), - (NATNET_TARGET_NAME, DEFAULT_TARGET_STREAMING_ID, DEFAULT_TARGET_PATH), - ] - author_drone_natnet_interface(stage, bodies, **_NATNET_SERVER_KWARGS) - carb.log_warn( - f"[natnet] Interface authored: '{NATNET_BODY_NAME}' (-> /World/base_link/body), " - f"'{NATNET_TARGET_NAME}' (-> {DEFAULT_TARGET_PATH})." - ) - except Exception as exc: # noqa: BLE001 - never let NatNet kill the sim - carb.log_error(f"[natnet] Failed to author interface: {exc}") - - -def main(): - NatNetPegasusApp( - env_url=SIMULATION_ENVIRONMENTS["Default Environment"], - stage_scale=1.0, - drone_configs=[ - { - "domain_id": 1, - "x_m": 0.0, "y_m": 0.0, "z_m": 0.07, - "prim": "/World/base_link", - "node_name": "PX4Multirotor", - } - ], - enable_lidar=True, - # Written before the PX4 SITL subprocess starts. - world_gps_origin=WORLD_GPS_ORIGIN, - ).run() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/launch_scripts/fleet_spawn.py b/simulation/isaac-sim/launch_scripts/fleet_spawn.py new file mode 100755 index 000000000..826e603c0 --- /dev/null +++ b/simulation/isaac-sim/launch_scripts/fleet_spawn.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +"""Generic Isaac Sim fleet spawner (RFC #380 §2). + +Replaces the hardcoded one-/multi-drone example launch scripts when a fleet is +selected: spawn positions, per-robot vehicle (pass-through → the Pegasus Iris +asset today), sensor toggles, and the scene all come from the fleet file named +by ``FLEET_CONFIG_FILE`` (``airstack up --fleet `` exports it and +switches ``ISAAC_SIM_SCRIPT_NAME`` to this script when the default was +untouched). + +``FLEET_CONFIG_FILE`` carries the ROBOT-container path +(``/root/AirStack/config/fleets/.yaml``); this container mounts the +checkout at ``/isaac-sim/AirStack``, so the path is remapped onto that mount. + +Fleet fields consumed: + - ``robots..spawn`` — [x, y, z] in meters (default [0, 0, 0.07]) + - ``robots.`` — order defines domain_id / vehicle_id (robot N → + domain N, ``network.domain_policy: auto``) + - vehicle manifests (``config/vehicles//vehicle.yaml``) — any ``lidar*`` + sensor entry enables the RTX lidar subgraph for that robot (the + ``ENABLE_LIDAR`` env-var equivalent, but per vehicle); any ``stereo_cam`` + entry enables the ZED camera subgraph. + - ``sim.scene`` — ``default`` (or unset) = the Pegasus "Default Environment" + (matching the example scripts); any other value: a + ``SIMULATION_ENVIRONMENTS`` key, or a ``.usd`` path/URL used verbatim. + +Env (see pegasus_app.py): ISAAC_SIM_LIVESTREAM, ISAAC_SIM_HEADLESS, +PLAY_SIM_ON_START. + +Import-order contract: everything Isaac/Pegasus (including pegasus_app, which +imports ``carb`` at module scope) is imported inside ``main()`` — the +fleet-parsing helpers below are stdlib+PyYAML only, so this module parses and +imports without an Isaac install (unit tests exercise the mapping directly). +""" + +import os +import sys + +import yaml + +# Robot-container checkout root → this container's checkout mount. +ROBOT_CONTAINER_ROOT = "/root/AirStack" +ISAAC_CONTAINER_ROOT = "/isaac-sim/AirStack" + +DEFAULT_SPAWN_Z = 0.07 + + +def remap_fleet_path(fleet_config_file, isaac_root=ISAAC_CONTAINER_ROOT): + """Map the robot-container FLEET_CONFIG_FILE path onto this container.""" + if fleet_config_file.startswith(ROBOT_CONTAINER_ROOT + "/"): + return isaac_root + fleet_config_file[len(ROBOT_CONTAINER_ROOT):] + return fleet_config_file + + +def load_yaml(path): + with open(path, encoding="utf-8") as f: + return yaml.safe_load(f) or {} + + +def vehicle_sensor_flags(project_root, vehicle_name): + """(has_stereo_cam, has_lidar) from the vehicle manifest's sensor list. + + A missing/invalid manifest keeps the permissive defaults (camera on, + lidar on) so a mis-mounted config degrades loudly-visibly, not silently + sensor-less. + """ + manifest = os.path.join(project_root, "config", "vehicles", vehicle_name, "vehicle.yaml") + if not os.path.isfile(manifest): + print(f"[fleet_spawn] WARNING: no vehicle manifest at {manifest} — " + f"defaulting to camera+lidar on") + return True, True + sensors = load_yaml(manifest).get("sensors") or [] + has_cam = any("cam" in str(s.get("type", "")) for s in sensors if isinstance(s, dict)) + has_lidar = any("lidar" in str(s.get("type", "")) for s in sensors if isinstance(s, dict)) + return has_cam, has_lidar + + +def fleet_to_drone_configs(fleet, project_root): + """Fleet dict → PegasusApp drone_configs (pure function; unit-tested). + + Robot N (1-based file order) gets domain_id N — the ``domain_policy: auto`` + rule, matching the legacy resolver and ``row_spawn_configs``. + """ + robots = fleet.get("robots") or {} + if not robots: + raise ValueError("fleet has no robots: — nothing to spawn") + defaults = fleet.get("defaults") or {} + configs = [] + for i, (name, entry) in enumerate(robots.items(), start=1): + entry = entry or {} + spawn = entry.get("spawn", [0.0, 0.0, DEFAULT_SPAWN_Z]) + vehicle = entry.get("vehicle", defaults.get("vehicle", "")) + has_cam, has_lidar = vehicle_sensor_flags(project_root, vehicle) + configs.append({ + "domain_id": i, # MAVLink port = 14540 + vehicle_id (= domain_id) + "robot_name": name, + "x_m": float(spawn[0]), + "y_m": float(spawn[1]), + "z_m": float(spawn[2]), + "camera": has_cam, + "lidar": has_lidar, + }) + if len(configs) == 1: + # Single-robot fleets must be byte-equivalent to the validated + # example_one script, including the historical single-drone prim and + # node names (multi-style names are for multi-drone scenes). + configs[0]["prim"] = "/World/base_link" + configs[0]["node_name"] = "PX4Multirotor" + return configs + + +def fleet_env_url(fleet, simulation_environments): + """Resolve ``sim.scene`` against Pegasus SIMULATION_ENVIRONMENTS. + + An exported ``ISAAC_SIM_SCENE`` (from ``airstack up --scene``) wins over + the fleet file's ``sim.scene``. + """ + scene = (fleet.get("sim") or {}).get("scene", "default") + env_scene = os.environ.get("ISAAC_SIM_SCENE", "").strip() + if env_scene: + if scene not in (None, "", "default"): + print(f"[fleet_spawn] --scene override: ISAAC_SIM_SCENE='{env_scene}' " + f"replaces the fleet's sim.scene '{scene}'") + scene = env_scene + if scene in (None, "", "default"): + return simulation_environments["Default Environment"] + if scene in simulation_environments: + return simulation_environments[scene] + if "://" in str(scene) or str(scene).endswith((".usd", ".usda", ".usdc", ".usdz")): + return scene + raise ValueError( + f"sim.scene '{scene}' is neither a SIMULATION_ENVIRONMENTS key nor a USD reference " + f"(keys: {', '.join(sorted(simulation_environments))})" + ) + + +def main(): + fleet_config_file = os.environ.get("FLEET_CONFIG_FILE", "") + if not fleet_config_file: + print("[fleet_spawn] ERROR: FLEET_CONFIG_FILE is not set — this script is " + "selected by `airstack up --fleet `; use the example launch " + "scripts for fleetless runs.", file=sys.stderr) + return 1 + fleet_path = remap_fleet_path(fleet_config_file) + if not os.path.isfile(fleet_path): + print(f"[fleet_spawn] ERROR: fleet file not found: {fleet_path} " + f"(from FLEET_CONFIG_FILE={fleet_config_file})", file=sys.stderr) + return 1 + # /config/fleets/.yaml → + project_root = os.path.dirname(os.path.dirname(os.path.dirname(fleet_path))) + + fleet = load_yaml(fleet_path) + drone_configs = fleet_to_drone_configs(fleet, project_root) + print(f"[fleet_spawn] {os.path.basename(fleet_path)}: spawning " + f"{len(drone_configs)} drone(s): " + + ", ".join( + f"{c['robot_name']}@({c['x_m']:g},{c['y_m']:g},{c['z_m']:g})" + f"{' +cam' if c['camera'] else ''}{' +lidar' if c['lidar'] else ''}" + for c in drone_configs)) + + # ── Isaac/Pegasus imports — deferred (see module docstring) ────────────── + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from pegasus_app import create_simulation_app + + # Must be created before any omni/pegasus imports. + create_simulation_app() + + from pegasus.simulator.params import SIMULATION_ENVIRONMENTS # noqa: E402 + from pegasus_app import PegasusApp # noqa: E402 + + PegasusApp( + env_url=fleet_env_url(fleet, SIMULATION_ENVIRONMENTS), + # `airstack up --scene` exports the matching stage scale; default 1.0. + stage_scale=float(os.environ.get("ISAAC_SIM_STAGE_SCALE") or 1.0), + drone_configs=drone_configs, + # Per-robot "camera"/"lidar" keys above override the app-level + # defaults (enable_camera defaults True in PegasusApp). + enable_lidar=False, + ).run() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/simulation/isaac-sim/launch_scripts/gps_utils.py b/simulation/isaac-sim/launch_scripts/gps_utils.py index 801b3b87d..1668fa919 100644 --- a/simulation/isaac-sim/launch_scripts/gps_utils.py +++ b/simulation/isaac-sim/launch_scripts/gps_utils.py @@ -8,7 +8,14 @@ import math import os -# Lisbon — matches the Pegasus configs.yaml default. +# ── Global ENU world origin — Lisbon (the Pegasus configs.yaml default) ────── +# KEEP IN SYNC: the same anchor is replicated in three other files that can't +# import each other across container/mount boundaries (this file runs inside +# the isaac container, where the checkout is mounted at /isaac-sim/AirStack): +# - common/ros_packages/coordination/coordination_bringup/coordination_bringup/frame_utils.py +# - gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/gcs_utils.py +# - gcs/ros_ws/src/action_relay/action_relay/relay_node.py +# If you change the anchor here, change all four together. DEFAULT_WORLD_ORIGIN = (38.736832, -9.137977, 90.0) diff --git a/simulation/isaac-sim/launch_scripts/pegasus_app.py b/simulation/isaac-sim/launch_scripts/pegasus_app.py index b746a5666..62373e8ce 100644 --- a/simulation/isaac-sim/launch_scripts/pegasus_app.py +++ b/simulation/isaac-sim/launch_scripts/pegasus_app.py @@ -27,6 +27,14 @@ Env vars honored by ``PegasusApp``: - ``PLAY_SIM_ON_START`` (default true): autoplay the timeline after setup. + - ``ISAAC_SIM_FOLLOW_CAM`` (default ``1``): domain id of the drone the + viewport follow-camera tracks; ``off``/``none``/``0`` disables it. The + follow-cam (``/World/follow_cam``) chases the drone with smoothing and + keeps it centered — the stock perspective camera often starts inside + geometry on cm-authored stages (black viewport), so the viewport is + switched to the follow-cam at startup. + - ``ISAAC_SIM_FOLLOW_CAM_OFFSET`` (default ``-5,-5,2.5``): world-frame + ``x,y,z`` offset (meters) from the tracked drone to the camera. """ import os @@ -80,7 +88,7 @@ def create_simulation_app(launch_config=None): derived configuration entirely. When ``ISAAC_SIM_LIVESTREAM=true``, mirrors the NVIDIA reference config from - simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/livestream.py + /isaac-sim/standalone_examples/api/isaacsim.simulation_app/livestream.py (in-image) so the Kit GUI (menu bar, toolbar, viewport, status bar) actually gets rendered into the WebRTC stream instead of just the bare 3D viewport. Key field: ``hide_ui: False`` — SimulationApp's default when ``headless=True`` @@ -176,15 +184,86 @@ def wait_for_stage(stage, timeout_s: float = 10.0): return False -def row_spawn_configs(num_robots, spacing_m=2.0, z_m=0.07): - """Drone configs in a row along X, centered near the origin: -2, 0, 2, …""" +def resolve_spawn_center_from_env(default=(0.0, 0.0)): + """Parse ISAAC_SIM_SPAWN_XY ("x,y" in meters) — where the spawn row is + centered. Useful for scenes whose origin is cluttered or unlit.""" + raw = os.environ.get("ISAAC_SIM_SPAWN_XY", "").strip() + if not raw: + return default + try: + x, y = (float(v) for v in raw.split(",")) + return (x, y) + except ValueError: + carb.log_warn(f"ISAAC_SIM_SPAWN_XY='{raw}' is not 'x,y' — using {default}.") + return default + + +def row_spawn_configs(num_robots, spacing_m=2.0, z_m=0.07, center_xy=(0.0, 0.0)): + """Drone configs in a row along X, centered on ``center_xy``.""" + cx, cy = center_xy configs = [] for i in range(1, num_robots + 1): - init_x = spacing_m * (i - 1) - spacing_m * (num_robots - 1) / 2.0 - configs.append({"domain_id": i, "x_m": init_x, "y_m": 0.0, "z_m": z_m}) + init_x = cx + spacing_m * (i - 1) - spacing_m * (num_robots - 1) / 2.0 + configs.append({"domain_id": i, "x_m": init_x, "y_m": cy, "z_m": z_m}) return configs +def resolve_scene_from_env(simulation_environments, + default_key="Default Environment"): + """Resolve the ISAAC_SIM_SCENE / ISAAC_SIM_STAGE_SCALE env vars set by + `airstack up --scene ` (simulation/scenes.yaml). + + Returns ``(env_url, stage_scale)``. ISAAC_SIM_SCENE may be a Pegasus + ``SIMULATION_ENVIRONMENTS`` key, or a USD reference (``omniverse://`` / + ``https://`` URL or ``*.usd*`` path) used verbatim. Unset → ``default_key``. + """ + scene = os.environ.get("ISAAC_SIM_SCENE", "").strip() + scale = float(os.environ.get("ISAAC_SIM_STAGE_SCALE") or 1.0) + if not scene: + return simulation_environments[default_key], scale + if scene in simulation_environments: + return simulation_environments[scene], scale + if "://" in scene or scene.endswith((".usd", ".usda", ".usdc", ".usdz")): + return scene, scale + raise ValueError( + f"ISAAC_SIM_SCENE '{scene}' is neither a SIMULATION_ENVIRONMENTS key " + f"nor a USD reference (keys: {', '.join(sorted(simulation_environments))})" + ) + + +FOLLOW_CAM_PATH = "/World/follow_cam" + + +def resolve_follow_cam_from_env(): + """Parse ISAAC_SIM_FOLLOW_CAM / ISAAC_SIM_FOLLOW_CAM_OFFSET. + + Returns ``(domain_id, offset_xyz)``; ``domain_id`` is ``None`` when the + follow-cam is disabled. + """ + raw = os.environ.get("ISAAC_SIM_FOLLOW_CAM", "").strip().lower() + if raw in ("off", "false", "none", "0"): + return None, None + try: + target = int(raw) if raw else 1 + except ValueError: + carb.log_warn( + f"ISAAC_SIM_FOLLOW_CAM='{raw}' is not a domain id — following drone 1." + ) + target = 1 + offset = (-5.0, -5.0, 2.5) + off_raw = os.environ.get("ISAAC_SIM_FOLLOW_CAM_OFFSET", "").strip() + if off_raw: + try: + x, y, z = (float(v) for v in off_raw.split(",")) + offset = (x, y, z) + except ValueError: + carb.log_warn( + f"ISAAC_SIM_FOLLOW_CAM_OFFSET='{off_raw}' is not 'x,y,z' — " + f"using default {offset}." + ) + return target, offset + + class PegasusApp: """Base Pegasus launch app: world + environment + scene prep + drones. @@ -195,8 +274,8 @@ class PegasusApp: scale/colliders (e.g. referencing extra root prims). - ``post_scene_prep(stage)`` — after scene prep, before drones spawn (e.g. authoring an overhead map camera). - - ``post_spawn(stage)`` — after all drones spawn (e.g. authoring a - NatNet mocap interface). + - ``post_spawn(stage)`` — after all drones spawn (e.g. authoring + extra scene-level prims such as a mocap interface). Each entry in ``drone_configs`` is a dict: ``domain_id`` (required) — ROS 2 domain; also the default vehicle_id @@ -205,6 +284,7 @@ class PegasusApp: ``orient`` — quaternion [x, y, z, w] (default identity). ``prim`` — drone root prim (default ``/World/drone{i}/base_link``). ``node_name`` — Pegasus OmniGraph node name (default ``PX4Multirotor_{i}``). + ``camera`` — per-drone camera override (default: app-level ``enable_camera``). ``lidar`` — per-drone lidar override (default: app-level ``enable_lidar``). ``lidar_min_range`` — per-drone min range (default: app-level value). """ @@ -256,6 +336,20 @@ def __init__( # GPS origins must be written before the PX4 SITL subprocesses start # (robot containers read them during their own bring-up). + # + # Multi-drone default (audit H4): without per-drone PX4_HOME_* values, + # every PX4 SITL boots with the same GPS home, so the GCS map renders + # the whole fleet stacked at one coordinate. When the caller didn't + # pick a world origin, multi-drone spawns are anchored at + # gps_utils.DEFAULT_WORLD_ORIGIN (Lisbon — the same anchor the Pegasus + # configs.yaml default uses, so the map doesn't move cities). + # Single-drone spawns (len == 1) are deliberately left untouched: their + # GPS home comes from the PX4/Pegasus defaults and that behavior is + # machine-validated — only opt in via an explicit world_gps_origin. + if world_gps_origin is None and len(self.drone_configs) > 1: + from gps_utils import DEFAULT_WORLD_ORIGIN + + world_gps_origin = DEFAULT_WORLD_ORIGIN if world_gps_origin is not None: from gps_utils import set_gps_origins @@ -302,9 +396,34 @@ def __init__( else: carb.log_warn("/World/stage not found — skipping scale and collision.") + # ISAAC_SIM_LIGHT_BOOST=: multiply the scene's own lights + # (e.g. office/hospital ceiling lights, which are authored very dim). + boost_raw = os.environ.get("ISAAC_SIM_LIGHT_BOOST", "").strip() + if boost_raw: + from scene_prep import boost_scene_lights + + try: + n = boost_scene_lights(stage, float(boost_raw)) + carb.log_warn(f"[light_boost] boosted {n} scene lights x{boost_raw}") + except ValueError: + carb.log_warn(f"ISAAC_SIM_LIGHT_BOOST='{boost_raw}' is not a number — ignored.") + # Dome light for uniform illumination: True → defaults, dict → kwargs. + # ISAAC_SIM_DOME_LIGHT="intensity[,exposure]" overrides either source — + # useful for dim indoor scenes (office/hospital corners). if self.dome_light: kwargs = self.dome_light if isinstance(self.dome_light, dict) else {} + raw = os.environ.get("ISAAC_SIM_DOME_LIGHT", "").strip() + if raw: + try: + parts = [float(v) for v in raw.split(",")] + kwargs["intensity"] = parts[0] + if len(parts) > 1: + kwargs["exposure"] = parts[1] + except ValueError: + carb.log_warn( + f"ISAAC_SIM_DOME_LIGHT='{raw}' is not 'intensity[,exposure]' — ignored." + ) add_dome_light(stage, **kwargs) self._maybe_export_scene() @@ -327,6 +446,8 @@ def __init__( self.post_spawn(stage) + self._setup_follow_cam(stage) + self.play_on_start = os.environ.get("PLAY_SIM_ON_START", "true").lower() == "true" # --- Hooks (default no-ops) --- @@ -391,7 +512,7 @@ def spawn_drone(self, cfg): init_orient=init_orient, ) - if self.enable_camera: + if cfg.get("camera", self.enable_camera): add_zed_stereo_camera_subgraph( parent_graph_handle=graph_handle, drone_prim=drone_prim, @@ -415,6 +536,148 @@ def spawn_drone(self, cfg): return graph_handle + # --- Follow camera ------------------------------------------------- + + def _setup_follow_cam(self, stage): + """Author the follow-camera and switch the viewport to it. + + The stock perspective camera often starts inside geometry on + cm-authored stages (black viewport), so a chase camera that tracks a + drone is both the fix and a nicer default view. The tracked drone + prim only materializes on the first Play tick, so the per-frame + update tolerates a missing target until then. + """ + self._follow_target_path = None + self._follow_cam_pos = None + self._follow_look = None + self._follow_vehicle_logged = False + + target, offset = resolve_follow_cam_from_env() + if target is None or not self.drone_configs: + return + + cfg = next( + (c for c in self.drone_configs if c["domain_id"] == target), + self.drone_configs[0], + ) + if cfg["domain_id"] != target: + carb.log_warn( + f"ISAAC_SIM_FOLLOW_CAM={target} has no matching drone — " + f"following drone {cfg['domain_id']}." + ) + i = cfg["domain_id"] + self._follow_target_path = cfg.get("prim", f"/World/drone{i}/base_link") + + from pxr import Gf, UsdGeom + + self._follow_offset = Gf.Vec3d(*offset) + cam = UsdGeom.Camera.Define(stage, FOLLOW_CAM_PATH) + cam.GetFocalLengthAttr().Set(16.0) + # Generous range: cm-scaled stages have geometry both very near and + # (pre-scale) very far from the camera. + cam.GetClippingRangeAttr().Set(Gf.Vec2f(0.1, 1.0e6)) + + # ISAAC_SIM_FOLLOW_CAM_LIGHT=: headlight riding the camera, + # for scenes too dark to film (interiors the dome light can't reach). + light_raw = os.environ.get("ISAAC_SIM_FOLLOW_CAM_LIGHT", "").strip() + if light_raw: + from pxr import UsdLux + + try: + headlight = UsdLux.SphereLight.Define( + stage, FOLLOW_CAM_PATH + "/headlight" + ) + headlight.GetIntensityAttr().Set(float(light_raw)) + headlight.GetRadiusAttr().Set(0.05) + headlight.CreateNormalizeAttr().Set(True) + except ValueError: + carb.log_warn( + f"ISAAC_SIM_FOLLOW_CAM_LIGHT='{light_raw}' is not a number — ignored." + ) + + # Frame the spawn point immediately so the viewport is never black + # while the sim is still paused. + s = self._position_scale + spawn = Gf.Vec3d( + cfg.get("x_m", 0.0) * s, cfg.get("y_m", 0.0) * s, cfg.get("z_m", 0.07) * s + ) + self._follow_cam_pos = spawn + self._follow_offset + self._follow_look = spawn + self._author_follow_cam() + + try: + from omni.kit.viewport.utility import get_active_viewport + + viewport = get_active_viewport() + if viewport is not None: + viewport.camera_path = FOLLOW_CAM_PATH + except Exception as exc: # headless variants may have no viewport + carb.log_warn(f"Could not switch viewport to follow cam: {exc}") + + def _author_follow_cam(self): + from isaacsim.core.utils.viewports import set_camera_view + + set_camera_view( + eye=self._follow_cam_pos, + target=self._follow_look, + camera_prim_path=FOLLOW_CAM_PATH, + ) + + def _follow_target_position(self): + """Live world position of the tracked drone, or ``None`` pre-spawn. + + PhysX (fabric) does not write simulated poses back to USD, so the + authoritative source is the Pegasus vehicle state; the USD transform + only covers the pre-play window before the vehicle registers. + """ + from pxr import Gf + + # OGN-spawned vehicles register under the full drone prim path + # (e.g. "/World/drone2/base_link"); UI-spawned ones may use the parent. + target_path = self._follow_target_path.rstrip("/") + parent = target_path.rsplit("/", 1)[0] + try: + from pegasus.simulator.logic.vehicle_manager import VehicleManager + + for stage_prefix, vehicle in VehicleManager.get_vehicle_manager().vehicles.items(): + sp = stage_prefix.rstrip("/") + if sp in (target_path, parent) or sp.startswith(parent + "/"): + if not self._follow_vehicle_logged: + self._follow_vehicle_logged = True + carb.log_warn(f"[follow_cam] tracking Pegasus vehicle '{sp}'") + p = vehicle.state.position + return Gf.Vec3d(float(p[0]), float(p[1]), float(p[2])) + except Exception: + pass + + import omni.usd + + stage = omni.usd.get_context().get_stage() + if stage is None: + return None + prim = stage.GetPrimAtPath(self._follow_target_path) + if not prim.IsValid(): + return None # drone spawns on the first Play tick + try: + return omni.usd.get_world_transform_matrix(prim).ExtractTranslation() + except Exception: + return None + + def _update_follow_cam(self): + if not self._follow_target_path: + return + from pxr import Gf + + target = self._follow_target_position() + if target is None: + return + desired = target + self._follow_offset + # Exponential smoothing keeps the chase fluid through sharp maneuvers. + alpha = 0.08 + self._follow_cam_pos += (desired - self._follow_cam_pos) * alpha + self._follow_look += (Gf.Vec3d(target) - self._follow_look) * alpha + self._author_follow_cam() + def run(self): """Play (unless PLAY_SIM_ON_START=false) and step until closed.""" import omni.kit.app @@ -427,6 +690,7 @@ def run(self): app = omni.kit.app.get_app() while SIMULATION_APP.is_running() and not self.stop_sim: + self._update_follow_cam() # File → Save re-opens the stage, which invalidates the World. # Fall back to app.update() until the extension re-creates it. world = World.instance() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.asset.importer.urdf/urdf_import.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.asset.importer.urdf/urdf_import.py deleted file mode 100644 index 01b054561..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.asset.importer.urdf/urdf_import.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -# URDF import, configuration and simulation sample -kit = SimulationApp({"renderer": "RaytracedLighting", "headless": False}) -import omni.kit.commands -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.extensions import get_extension_path_from_name -from pxr import Gf, PhysxSchema, Sdf, UsdLux, UsdPhysics - -# Setting up import configuration: -status, import_config = omni.kit.commands.execute("URDFCreateImportConfig") -import_config.merge_fixed_joints = False -import_config.convex_decomp = False -import_config.import_inertia_tensor = True -import_config.fix_base = False -import_config.distance_scale = 1.0 - -# Get path to extension data: -extension_path = get_extension_path_from_name("isaacsim.asset.importer.urdf") -# Import URDF, prim_path contains the path the path to the usd prim in the stage. -status, prim_path = omni.kit.commands.execute( - "URDFParseAndImportFile", - urdf_path=extension_path + "/data/urdf/robots/carter/urdf/carter.urdf", - import_config=import_config, - get_articulation_root=True, -) -# Get stage handle -stage = omni.usd.get_context().get_stage() - -# Enable physics -scene = UsdPhysics.Scene.Define(stage, Sdf.Path("/physicsScene")) -# Set gravity -scene.CreateGravityDirectionAttr().Set(Gf.Vec3f(0.0, 0.0, -1.0)) -scene.CreateGravityMagnitudeAttr().Set(9.81) -# Set solver settings -PhysxSchema.PhysxSceneAPI.Apply(stage.GetPrimAtPath("/physicsScene")) -physxSceneAPI = PhysxSchema.PhysxSceneAPI.Get(stage, "/physicsScene") -physxSceneAPI.CreateEnableCCDAttr(True) -physxSceneAPI.CreateEnableStabilizationAttr(True) -physxSceneAPI.CreateEnableGPUDynamicsAttr(False) -physxSceneAPI.CreateBroadphaseTypeAttr("MBP") -physxSceneAPI.CreateSolverTypeAttr("TGS") - -# Add ground plane -omni.kit.commands.execute( - "AddGroundPlaneCommand", - stage=stage, - planePath="/groundPlane", - axis="Z", - size=1500.0, - position=Gf.Vec3f(0, 0, -0.50), - color=Gf.Vec3f(0.5), -) - -# Add lighting -distantLight = UsdLux.DistantLight.Define(stage, Sdf.Path("/DistantLight")) -distantLight.CreateIntensityAttr(500) - -# Get handle to the Drive API for both wheels -left_wheel_drive = UsdPhysics.DriveAPI.Get(stage.GetPrimAtPath("/carter/joints/left_wheel"), "angular") -right_wheel_drive = UsdPhysics.DriveAPI.Get(stage.GetPrimAtPath("/carter/joints/right_wheel"), "angular") - -# Set the velocity drive target in degrees/second -left_wheel_drive.GetTargetVelocityAttr().Set(150) -right_wheel_drive.GetTargetVelocityAttr().Set(150) - -# Set the drive damping, which controls the strength of the velocity drive -left_wheel_drive.GetDampingAttr().Set(15000) -right_wheel_drive.GetDampingAttr().Set(15000) - -# Set the drive stiffness, which controls the strength of the position drive -# In this case because we want to do velocity control this should be set to zero -left_wheel_drive.GetStiffnessAttr().Set(0) -right_wheel_drive.GetStiffnessAttr().Set(0) - -# Start simulation -omni.timeline.get_timeline_interface().play() -# perform one simulation step so physics is loaded and dynamic control works. -kit.update() -art = Articulation(prim_path) -art.initialize() - -if not art.is_physics_handle_valid(): - print(f"{prim_path} is not an articulation") -else: - print(f"Got articulation ({prim_path})") - -# perform simulation -for frame in range(1000): - kit.update() - -# Shutdown and exit -omni.timeline.get_timeline_interface().stop() -kit.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/add_cubes.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/add_cubes.py deleted file mode 100644 index be144c675..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/add_cubes.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid, VisualCuboid - -my_world = World(stage_units_in_meters=1.0) - -cube_1 = my_world.scene.add( - VisualCuboid( - prim_path="/new_cube_1", - name="visual_cube", - position=np.array([0, 0, 0.5]), - size=0.3, - color=np.array([255, 255, 255]), - ) -) - -cube_2 = my_world.scene.add( - DynamicCuboid( - prim_path="/new_cube_2", - name="cube_1", - position=np.array([0, 0, 1.0]), - scale=np.array([0.6, 0.5, 0.2]), - size=1.0, - color=np.array([255, 0, 0]), - ) -) - -cube_3 = my_world.scene.add( - DynamicCuboid( - prim_path="/new_cube_3", - name="cube_2", - position=np.array([0, 0, 3.0]), - scale=np.array([0.1, 0.1, 0.1]), - size=1.0, - color=np.array([0, 0, 255]), - linear_velocity=np.array([0, 0, 0.4]), - ) -) - -my_world.scene.add_default_ground_plane() - -for i in range(5): - my_world.reset() - for i in range(500): - my_world.step(render=True) - print(cube_2.get_angular_velocity()) - print(cube_2.get_world_pose()) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/add_frankas.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/add_frankas.py deleted file mode 100644 index cb3e45f3b..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/add_frankas.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.robots import Robot -from isaacsim.core.utils.stage import add_reference_to_stage, get_stage_units -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() - -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka_1") -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka_2") -articulated_system_1 = my_world.scene.add(Robot(prim_path="/World/Franka_1", name="my_franka_1")) -articulated_system_2 = my_world.scene.add(Robot(prim_path="/World/Franka_2", name="my_franka_2")) - -for i in range(5): - print("resetting...") - my_world.reset() - articulated_system_1.set_world_pose(position=np.array([0.0, 2.0, 0.0]) / get_stage_units()) - articulated_system_2.set_world_pose(position=np.array([0.0, -2.0, 0.0]) / get_stage_units()) - articulated_system_1.set_joint_positions(np.array([1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5])) - for j in range(500): - my_world.step(render=True) - if j == 100: - articulated_system_2.get_articulation_controller().apply_action( - ArticulationAction(joint_positions=np.array([1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5])) - ) - if j == 400: - print("Franka 1's joint positions are: ", articulated_system_1.get_joint_positions()) - print("Franka 2's joint positions are: ", articulated_system_2.get_joint_positions()) - if args.test is True: - break -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/cloth.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/cloth.py deleted file mode 100644 index d6c5d160e..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/cloth.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) -import argparse -import sys - -import carb -import numpy as np -import torch -from isaacsim.core.api import World -from isaacsim.core.api.materials.particle_material import ParticleMaterial -from isaacsim.core.prims import ClothPrim, SingleClothPrim, SingleParticleSystem -from isaacsim.storage.native import get_assets_root_path -from omni.physx.scripts import deformableUtils, physicsUtils -from pxr import Gf, UsdGeom - -# The example shows how to create and manipulate environments with particle cloth through the ClothPrim -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - - -class ParticleClothExample: - def __init__(self): - self._array_container = torch.Tensor - self.my_world = World(stage_units_in_meters=1.0, backend="torch", device="cuda") - self.stage = simulation_app.context.get_stage() - self.num_envs = 10 - self.dimx = 5 - self.dimy = 5 - self.my_world.scene.add_default_ground_plane() - self.initial_positions = None - self.makeEnvs() - - def makeEnvs(self): - for i in range(self.num_envs): - env_path = "/World/Env" + str(i) - env = UsdGeom.Xform.Define(self.stage, env_path) - # set up the geometry - cloth_path = env.GetPrim().GetPath().AppendChild("cloth") - plane_mesh = UsdGeom.Mesh.Define(self.stage, cloth_path) - tri_points, tri_indices = deformableUtils.create_triangle_mesh_square(dimx=5, dimy=5, scale=1.0) - if self.initial_positions is None: - self.initial_positions = torch.zeros((self.num_envs, len(tri_points), 3)) - plane_mesh.GetPointsAttr().Set(tri_points) - plane_mesh.GetFaceVertexIndicesAttr().Set(tri_indices) - plane_mesh.GetFaceVertexCountsAttr().Set([3] * (len(tri_indices) // 3)) - init_loc = Gf.Vec3f(i * 2, 0.0, 2.0) - physicsUtils.setup_transform_as_scale_orient_translate(plane_mesh) - physicsUtils.set_or_add_translate_op(plane_mesh, init_loc) - physicsUtils.set_or_add_orient_op(plane_mesh, Gf.Rotation(Gf.Vec3d([1, 0, 0]), 15 * i).GetQuat()) - self.initial_positions[i] = torch.tensor(init_loc) + torch.tensor(plane_mesh.GetPointsAttr().Get()) - particle_system_path = env.GetPrim().GetPath().AppendChild("particleSystem") - particle_material_path = env.GetPrim().GetPath().AppendChild("particleMaterial") - - self.particle_material = ParticleMaterial( - prim_path=str(particle_material_path), drag=0.1, lift=0.3, friction=0.6 - ) - radius = 0.5 * (0.6 / 5.0) - restOffset = radius - contactOffset = restOffset * 1.5 - self.particle_system = SingleParticleSystem( - prim_path=str(particle_system_path), - simulation_owner=self.my_world.get_physics_context().prim_path, - rest_offset=restOffset, - contact_offset=contactOffset, - solid_rest_offset=restOffset, - fluid_rest_offset=restOffset, - particle_contact_offset=contactOffset, - ) - # note that no particle material is applied to the particle system at this point. - # this can be done manually via self.particle_system.apply_particle_material(self.particle_material) - # or to pass the material to the clothPrim which binds it internally to the particle system - self.cloth = SingleClothPrim( - name="clothPrim" + str(i), - prim_path=str(cloth_path), - particle_system=self.particle_system, - particle_material=self.particle_material, - ) - self.my_world.scene.add(self.cloth) - - # create a view to deal with all the cloths - self.clothView = ClothPrim(prim_paths_expr="/World/Env*/cloth", name="clothView1") - self.my_world.scene.add(self.clothView) - self.my_world.reset(soft=False) - - def play(self): - reset_needed = False - while simulation_app.is_running(): - if self.my_world.is_stopped() and not reset_needed: - reset_needed = True - if self.my_world.is_playing(): - # deal with sim re-initialization after restarting sim - if reset_needed: - # initialize simulation views - self.my_world.reset(soft=False) - reset_needed = False - - self.my_world.step(render=True) - - if self.my_world.current_time_step_index % 50 == 49: - for i in range(self.num_envs): - print( - "cloth {} average height = {:.2f}".format( - i, self.clothView.get_world_positions()[i, :, 2].mean() - ) - ) - if args.test is True: - break - - # reset some random environments - if self.my_world.current_time_step_index % 200 == 1: - indices = torch.tensor( - np.random.choice(range(self.num_envs), self.num_envs // 2, replace=False), dtype=torch.long - ) - new_positions = self.initial_positions[indices] + torch.tensor([0, 0, 5]) - self.clothView.set_world_positions(new_positions, indices) - updated_positions = self.clothView.get_world_positions() - for i in indices: - print("reset index {} average height = {:.2f}".format(i, updated_positions[i, :, 2].mean())) - - simulation_app.close() - - -ParticleClothExample().play() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/control_robot.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/control_robot.py deleted file mode 100644 index fc1438233..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/control_robot.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -from isaacsim.core.api import SimulationContext -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" - -simulation_context = SimulationContext() -add_reference_to_stage(asset_path, "/Franka") - -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() -art = Articulation("/Franka") -art.initialize() -dof_ptr = art.get_dof_index("panda_joint2") - -simulation_context.play() -# NOTE: before interacting with dc directly you need to step physics for one step at least -# simulation_context.step(render=True) which happens inside .play() -for i in range(1000): - art.set_joint_positions([[-1.5]], joint_indices=[dof_ptr]) - simulation_context.step(render=True) - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/data_logging.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/data_logging.py deleted file mode 100644 index 89b35baeb..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/data_logging.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import sys - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -from isaacsim.core.api import World -from isaacsim.core.api.robots import Robot -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() - -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka_1") -articulated_system_1 = my_world.scene.add(Robot(prim_path="/World/Franka_1", name="my_franka_1")) - - -my_world.reset() -data_logger = my_world.get_data_logger() - - -def frame_logging_func(tasks, scene): - return { - "joint_positions": scene.get_object("my_franka_1").get_joint_positions().tolist(), - "applied_joint_positions": scene.get_object("my_franka_1").get_applied_action().joint_positions.tolist(), - } - - -data_logger.add_data_frame_logging_func(frame_logging_func) -data_logger.start() -for j in range(100): - my_world.step(render=True) - -data_logger.save(log_path="./isaac_sim_data.json") -data_logger.reset() - -data_logger.load(log_path="./isaac_sim_data.json") -print(data_logger.get_data_frame(data_frame_index=2)) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/deformable.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/deformable.py deleted file mode 100644 index 5b5fdbc63..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/deformable.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) -import argparse -import sys - -import carb -import isaacsim.core.utils.deformable_mesh_utils as deformableMeshUtils -import numpy as np -import torch -from isaacsim.core.api import World -from isaacsim.core.api.materials.deformable_material import DeformableMaterial -from isaacsim.core.prims import DeformablePrim, SingleDeformablePrim -from isaacsim.storage.native import get_assets_root_path -from omni.physx.scripts import deformableUtils, physicsUtils -from pxr import Gf, UsdGeom, UsdLux - -# The example shows how to create and manipulate environments with deformable prim through the DeformablePrim -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - - -class DeformableExample: - def __init__(self): - self._array_container = torch.Tensor - self.my_world = World(stage_units_in_meters=1.0, backend="torch", device="cuda") - self.stage = simulation_app.context.get_stage() - self.num_envs = 10 - self.dimx = 5 - self.dimy = 5 - self.my_world.scene.add_default_ground_plane() - self.initial_positions = None - self.makeEnvs() - - def makeEnvs(self): - for i in range(self.num_envs): - init_loc = Gf.Vec3f(i * 2 - self.num_envs, 0.0, 0.0) - env_scope = UsdGeom.Scope.Define(self.stage, "/World/Envs") - env_path = "/World/Envs/Env" + str(i) - env = UsdGeom.Xform.Define(self.stage, env_path) - physicsUtils.set_or_add_translate_op(UsdGeom.Xformable(env), init_loc) - - mesh_path = env.GetPrim().GetPath().AppendChild("deformable") - skin_mesh = UsdGeom.Mesh.Define(self.stage, mesh_path) - tri_points, tri_indices = deformableMeshUtils.createTriangleMeshCube(8) - skin_mesh.GetPointsAttr().Set(tri_points) - skin_mesh.GetFaceVertexIndicesAttr().Set(tri_indices) - skin_mesh.GetFaceVertexCountsAttr().Set([3] * (len(tri_indices) // 3)) - physicsUtils.setup_transform_as_scale_orient_translate(skin_mesh) - physicsUtils.set_or_add_translate_op(skin_mesh, (0.0, 0.0, 2.0)) - physicsUtils.set_or_add_orient_op(skin_mesh, Gf.Rotation(Gf.Vec3d([1, 0, 0]), 15 * i).GetQuat()) - deformable_material_path = env.GetPrim().GetPath().AppendChild("deformableMaterial").pathString - self.deformable_material = DeformableMaterial( - prim_path=deformable_material_path, - dynamic_friction=0.5, - youngs_modulus=5e4, - poissons_ratio=0.4, - damping_scale=0.1, - elasticity_damping=0.1, - ) - - self.deformable = SingleDeformablePrim( - name="deformablePrim" + str(i), - prim_path=str(mesh_path), - deformable_material=self.deformable_material, - vertex_velocity_damping=0.0, - sleep_damping=1.0, - sleep_threshold=0.05, - settling_threshold=0.1, - self_collision=True, - self_collision_filter_distance=0.05, - solver_position_iteration_count=20, - kinematic_enabled=False, - simulation_hexahedral_resolution=2, - collision_simplification=True, - ) - self.my_world.scene.add(self.deformable) - - # create a view to deal with all the deformables - self.deformableView = DeformablePrim(prim_paths_expr="/World/Envs/Env*/deformable", name="deformableView1") - self.my_world.scene.add(self.deformableView) - self.my_world.reset(soft=False) - # mesh data is available only after cooking - # rest_points are represented with respect to the env positions, but simulation_mesh_nodal_positions can be either global or local positions - # However, because we don't currently consider subspace root path with World/SimulationContext initialization, the environment xforms are not identified - # below and the following call will be positions w.r.t to a global frame. - self.initial_positions = self.deformableView.get_simulation_mesh_nodal_positions().cpu() - self.initial_velocities = self.deformableView.get_simulation_mesh_nodal_velocities().cpu() - # print(self.initial_positions) - # self.initial_positions = self.deformableView.get_simulation_mesh_rest_points().cpu() - # for i in range(self.num_envs): - # self.initial_positions[i] += torch.tensor([i * 2, 0.0, 2.0]) - # print(self.initial_positions[i]) - - def play(self): - while simulation_app.is_running(): - if self.my_world.is_playing(): - # deal with sim re-initialization after restarting sim - if self.my_world.current_time_step_index == 1: - # initialize simulation views - self.my_world.reset(soft=False) - - self.my_world.step(render=True) - - if self.my_world.current_time_step_index == 200: - for i in range(self.num_envs): - print( - "deformable {} average height = {:.2f}".format( - i, self.deformableView.get_simulation_mesh_nodal_positions()[i, :, 2].mean() - ) - ) - print( - "deformable {} average vertical speed = {:.2f}".format( - i, self.deformableView.get_simulation_mesh_nodal_velocities()[i, :, 2].mean() - ) - ) - - # reset some random environments - if self.my_world.current_time_step_index % 500 == 1: - indices = torch.tensor( - np.random.choice(range(self.num_envs), self.num_envs // 2, replace=False), dtype=torch.long - ) - new_positions = self.initial_positions[indices] + torch.tensor([0, 0, 5]) - new_velocities = self.initial_velocities[indices] + torch.tensor([0, 0, 3]) - self.deformableView.set_simulation_mesh_nodal_positions(new_positions, indices) - self.deformableView.set_simulation_mesh_nodal_velocities(new_velocities, indices) - updated_positions = self.deformableView.get_simulation_mesh_nodal_positions() - updated_velocities = self.deformableView.get_simulation_mesh_nodal_velocities() - for i in indices: - print("reset index {} average height = {:.2f}".format(i, updated_positions[i, :, 2].mean())) - print( - "reset index {} average vertical speed = {:.2f}".format(i, updated_velocities[i, :, 2].mean()) - ) - - simulation_app.close() - - -DeformableExample().play() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/detailed_contact_data.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/detailed_contact_data.py deleted file mode 100644 index c1908d729..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/detailed_contact_data.py +++ /dev/null @@ -1,181 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.materials.physics_material import PhysicsMaterial -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.prims import RigidPrim - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - - -class RigidViewExample: - def __init__(self): - self.my_world = World(stage_units_in_meters=1.0, backend="numpy") - self.stage = simulation_app.context.get_stage() - self.g = 10 - self.count = 3 - - def makeEnv(self): - self.cube_height = 1.0 - self.top_cube_height = self.cube_height + 3.0 - self.cube_dx = 5.0 - self.cube_y = 2.0 - self.top_cube_y = self.cube_y + 0.0 - - self.my_world._physics_context.set_gravity(-10) - self.my_world.scene.add_default_ground_plane() - material = PhysicsMaterial( - prim_path="/World/PhysicsMaterials", - static_friction=0.5, - dynamic_friction=0.5, - ) - for i in range(self.count): - DynamicCuboid( - prim_path=f"/World/Box_{i+1}", - name=f"box_{i}", - size=1.0, - color=np.array([0.5, 0, 0]), - mass=1.0, - ).apply_physics_material(material) - - # add top box as filters to the view to receive contacts between the bottom boxes and top boxes - self._box_view = RigidPrim( - prim_paths_expr="/World/Box_*", - name="box_view", - positions=np.array( - [ - [0, self.cube_y, self.cube_height], - [-self.cube_dx, self.cube_y, self.cube_height], - [self.cube_dx, self.cube_y, self.cube_height], - ] - ), - contact_filter_prim_paths_expr=[ - "/World/defaultGroundPlane/GroundPlane/CollisionPlane", - ], - max_contact_count=3 * 10, - ) - - self.my_world.scene.add(self._box_view) - self.my_world.reset(soft=False) - - def play(self): - self.makeEnv() - reset_needed = False - while simulation_app.is_running(): - if self.my_world.is_stopped() and not reset_needed: - reset_needed = True - if self.my_world.is_playing(): - # deal with sim re-initialization after restarting sim - if reset_needed: - # initialize simulation views - self.my_world.reset(soft=False) - reset_needed = False - - forces = np.array([[self.g, 0, 0], [self.g, 0, 0], [self.g, 0, 0]]) - self._box_view.apply_forces(forces) - self.my_world.step(render=True) - if self.my_world.current_time_step_index % 100 == 99: - # tangential forces - ( - friction_forces, - friction_points, - friction_pair_contacts_count, - friction_pair_contacts_start_indices, - ) = self._box_view.get_friction_data(dt=1 / 60) - - # normal forces - ( - forces, # only normal impulses - points, - normals, - distances, - pair_contacts_count, - pair_contacts_start_indices, - ) = self._box_view.get_contact_force_data(dt=1 / 60) - - # pair_contacts_count, pair_contacts_start_indices, friction_pair_contacts_count, friction_pair_contacts_start_indices are tensors of size count x num_filters = (3x1) - force_aggregate = np.zeros( - ( - self._box_view._contact_view.num_shapes, - self._box_view._contact_view.num_filters, - 3, - ) - ) # shape is count x num_filters x 3 = 3 x 1 x 1 - friction_force_aggregate = np.zeros( - ( - self._box_view._contact_view.num_shapes, - self._box_view._contact_view.num_filters, - 3, - ) - ) # shape is count x num_filters x 3 = 3 x 1 x 1 - effective_position = np.zeros( - ( - self._box_view._contact_view.num_shapes, - self._box_view._contact_view.num_filters, - 3, - ) - ) - friction_effective_position = np.zeros( - ( - self._box_view._contact_view.num_shapes, - self._box_view._contact_view.num_filters, - 3, - ) - ) - # process contacts for each pair i, j - for i in range(pair_contacts_count.shape[0]): - for j in range(pair_contacts_count.shape[1]): - start_idx = pair_contacts_start_indices[i, j] - friction_start_idx = friction_pair_contacts_start_indices[i, j] - count = pair_contacts_count[i, j] - friction_count = friction_pair_contacts_count[i, j] - # sum/average across all the contact pairs - pair_forces = forces[start_idx : start_idx + count] # all the pair forces, shape [count, 3] - pair_normals = normals[start_idx : start_idx + count] # all the pair forces, shape [count, 3] - - force_aggregate[i, j] = np.sum(pair_forces * pair_normals, axis=0) - effective_position[i, j] = np.sum(points[start_idx : start_idx + count], axis=0) / count - - # sum/average across all the friction pairs - pair_forces = friction_forces[ - friction_start_idx : friction_start_idx + friction_count - ] # all the pair forces, shape [count, 3] - friction_force_aggregate[i, j] = np.sum(pair_forces, axis=0) - friction_effective_position[i, j] = ( - np.sum( - friction_points[friction_start_idx : friction_start_idx + friction_count], - axis=0, - ) - / friction_count - ) - - print("==================================================") - print("friction forces: \n", friction_force_aggregate) - # applied tangential forces (m*g) is larger than the maximum dynamic friction force of mu * mg so the friction forces will be capped to that - print("contact forces: \n", force_aggregate) - # boxes will start sliding so the effective position of the friction/contact points will change over time - print("friction point: \n", friction_effective_position) - print("contact point: \n", effective_position) - if args.test is True: - break - - simulation_app.close() - - -RigidViewExample().play() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/omnigraph_triggers.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/omnigraph_triggers.py deleted file mode 100644 index 3620940f0..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/omnigraph_triggers.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - - -import time - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"renderer": "RaytracedLighting", "headless": True}) - -import omni.graph.core as og -from isaacsim.core.api import SimulationContext - -""" -This script demonstrates how Push and Action graphs differ, and how to trigger graphs manually. - -""" - - -## build the Push graph with a printout that says "Push Graph Running" -try: - keys = og.Controller.Keys - (push_graph, _, _, _) = og.Controller.edit( - { - "graph_path": "/Push_Graph", - "evaluator_name": "push", - }, - { - keys.CREATE_NODES: [ - ("string", "omni.graph.nodes.ConstantString"), - ("print", "omni.graph.ui_nodes.PrintText"), - ], - keys.SET_VALUES: [ - ("string.inputs:value", "Push Graph Running"), - ("print.inputs:logLevel", "Warning"), - ], - keys.CONNECT: [ - ("string.inputs:value", "print.inputs:text"), - ], - }, - ) -except Exception as e: - print(e) - simulation_app.close() - exit() - -## build an Action graph with a printout that says "Action Graph Running" -try: - keys = og.Controller.Keys - (action_graph, _, _, _) = og.Controller.edit( - { - "graph_path": "/Action_Graph", - "evaluator_name": "execution", - }, - { - keys.CREATE_NODES: [ - ("tick", "omni.graph.action.OnTick"), # action graph needs a trigger - ("string", "omni.graph.nodes.ConstantString"), - ("print", "omni.graph.ui_nodes.PrintText"), - ], - keys.SET_VALUES: [ - ("string.inputs:value", "Action Graph Running"), - ("print.inputs:logLevel", "Warning"), - ], - keys.CONNECT: [ - ("string.inputs:value", "print.inputs:text"), - ("tick.outputs:tick", "print.inputs:execIn"), - ], - }, - ) -except Exception as e: - print(e) - simulation_app.close() - exit() - - -# let the application run but not simulating (i.e. no physics running). Equivalent to open the app but not pressing "play" -# expected output: only Push Graph ran -print("Starting just the app. Expected output: only Push Graph ran") -for frame in range(10): - simulation_app.update() - -print("ADDING SIMULATION, expected output: both Push Graph and Action Graph ran") -# initiate the simulation (pressed "play") -# expected output: both Push Graph and Action Graph ran -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) -simulation_context.initialize_physics() -simulation_context.play() -for frame in range(10): - simulation_app.update() - - -# make both Push and Action Graph OnDemand Only so we can trigger them manually -# default pipeline stage is og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_SIMULATION -push_graph.change_pipeline_stage(og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND) -action_graph.change_pipeline_stage(og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND) - -# do the same as before -# expected output: neither graph runs because neither are called explicitly -print("SWITCHED pipelinestage, expected output: neither graph runs because neither are called explicitly") -for frame in range(10): - simulation_app.update() - - -print("Manually trigger graphs, expected output: push graph print 2x in 20 frames, action graph printed 4x") -# explicitly calls the push graph every 10 frames -# expected output: push graph print twice in 20 frames, action graph printed 4x -for frame in range(20): - simulation_app.update() # still updates every frame doing whatever is needed - if frame % 10 == 0: - og.Controller.evaluate_sync(push_graph) - if frame % 5 == 0: - og.Controller.evaluate_sync(action_graph) - -# add the evaluation of an action graph as part of the physics callback, -# expected output: action graph prints all 10 frames, -print("Trigger a Graph in physics callback, expected output: action graph prints all 10 frames") -simulation_context.add_physics_callback("physics callback", lambda x: og.Controller.evaluate_sync(action_graph)) -for frame in range(10): - simulation_app.update() - -# add the evaluation of a push graph as part of the rendering callback (after already having the action graph in physics callback) -# expected output: push and action graph both print all 10 frames -print("trigger push graph in rendering callback,expected output: push and action graph both print all 10 frames ") -simulation_context.add_render_callback("render callback", lambda x: og.Controller.evaluate_sync(push_graph)) -for frame in range(10): - simulation_app.update() - - -# separately step rendering and physics -# expected output: push graph prints 10 times (every 2 frames), action graph prints 2 times (every 10 frames) -print( - "separate rendering and physics stepping. expected output: push graph prints 10 times (every 2 frames), action graph prints 2 times (every 10 frames)" -) -for frame in range(20): - if frame % 2 == 0: - simulation_context.render() # only render, no physics - - if frame % 10 == 0: - simulation_context.step(render=False) # only physics, no render - - -# shutdown -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/rigid_contact_view.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/rigid_contact_view.py deleted file mode 100644 index 866bc98ce..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/rigid_contact_view.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse - -import numpy as np -import torch -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.prims import GeometryPrim, RigidPrim - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - - -class RigidViewExample: - def __init__(self): - self._array_container = torch.Tensor - self.my_world = World(stage_units_in_meters=1.0, backend="torch") - self.stage = simulation_app.context.get_stage() - - def makeEnv(self): - self.cube_height = 1.0 - self.top_cube_height = self.cube_height + 3.0 - self.cube_dx = 5.0 - self.cube_y = 2.0 - self.top_cube_y = self.cube_y + 0.0 - - self.my_world._physics_context.set_gravity(-10) - self.my_world.scene.add_default_ground_plane() - - for i in range(3): - DynamicCuboid( - prim_path=f"/World/Box_{i+1}", name=f"box_{i}", size=1.0, color=np.array([0.5, 0, 0]), mass=1.0 - ) - DynamicCuboid( - prim_path=f"/World/TopBox_{i+1}", - name=f"top_box_{i}", - size=1.0, - color=np.array([0.0, 0.0, 0.5]), - mass=1.0, - ) - - # add top box as filters to the view to receive contacts between the bottom boxes and top boxes - self._box_view = RigidPrim( - prim_paths_expr="/World/Box_*", - name="box_view", - positions=self._array_container( - [ - [0, self.cube_y, self.cube_height], - [-self.cube_dx, self.cube_y, self.cube_height], - [self.cube_dx, self.cube_y, self.cube_height], - ] - ), - contact_filter_prim_paths_expr=["/World/TopBox_*"], - ) - # a view just to manipulate the top boxes - self._top_box_view = RigidPrim( - prim_paths_expr="/World/TopBox_*", - name="top_box_view", - positions=self._array_container( - [ - [0.0, self.top_cube_y, self.top_cube_height], - [-self.cube_dx, self.top_cube_y, self.top_cube_height], - [self.cube_dx, self.top_cube_y, self.top_cube_height], - ] - ), - track_contact_forces=True, - ) - - # can get contact forces with non-rigid body prims such as geometry prims - self._geom_view = GeometryPrim( - prim_paths_expr="/World/defaultGroundPlane*", - name="groundPlaneView", - collisions=self._array_container([True]), - track_contact_forces=True, - prepare_contact_sensors=True, - contact_filter_prim_paths_expr=["/World/Box_1", "/World/Box_2", "/World/Box_3"], - ) - - self.my_world.scene.add(self._box_view) - self.my_world.scene.add(self._top_box_view) - self.my_world.scene.add(self._geom_view) - self.my_world.reset(soft=False) - - def play(self): - self.makeEnv() - reset_needed = False - while simulation_app.is_running(): - if self.my_world.is_stopped() and not reset_needed: - reset_needed = True - if self.my_world.is_playing(): - # deal with sim re-initialization after restarting sim - if reset_needed: - # initialize simulation views - self.my_world.reset(soft=False) - reset_needed = False - - self.my_world.step(render=True) - - if self.my_world.current_time_step_index % 100 == 99: - states = self._box_view.get_current_dynamic_state() - top_states = self._top_box_view.get_current_dynamic_state() - net_forces = self._box_view.get_net_contact_forces(None, dt=1 / 60) - forces_matrix = self._box_view.get_contact_force_matrix(None, dt=1 / 60) - top_net_forces = self._top_box_view.get_net_contact_forces(None, dt=1 / 60) - print("==================================================================") - print("Bottom box net forces: \n", net_forces) - print("Top box net forces: \n", top_net_forces) - print("Bottom box forces from top ones: \n", forces_matrix) - print("Bottom box positions: \n", states.positions) - print("Top box positions: \n", top_states.positions) - print("Bottom box velocities: \n", states.linear_velocities) - print("Top box velocities: \n", top_states.linear_velocities) - - print("ground net force from GeometryPrim : \n", self._geom_view.get_net_contact_forces(dt=1 / 60)) - print("ground force matrix from GeometryPrim: \n", self._geom_view.get_contact_force_matrix(dt=1 / 60)) - if args.test is True: - break - simulation_app.close() - - -RigidViewExample().play() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/simulate_robot.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/simulate_robot.py deleted file mode 100644 index c766c3523..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/simulate_robot.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.prims import create_prim -from isaacsim.core.utils.stage import add_reference_to_stage, is_stage_loading -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -simulation_context = SimulationContext() -add_reference_to_stage(asset_path, "/Franka") -create_prim("/DistantLight", "DistantLight") -# wait for things to load -simulation_app.update() -while is_stage_loading(): - simulation_app.update() - -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() -simulation_context.play() - -for i in range(1000): - simulation_context.step(render=True) - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/simulation_callbacks.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/simulation_callbacks.py deleted file mode 100644 index 289c8d080..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/simulation_callbacks.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -from isaacsim.core.api import SimulationContext -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" - -simulation_context = SimulationContext() -add_reference_to_stage(asset_path, "/Franka") - -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() -art = Articulation("/Franka") -art.initialize() -dof_ptr = art.get_dof_index("panda_joint2") - -simulation_context.play() - - -def step_callback_1(step_size): - art.set_joint_positions([[-1.5]], joint_indices=[dof_ptr]) - - -def step_callback_2(step_size): - print( - "Current joint 2 position @ step " - + str(simulation_context.current_time_step_index) - + " : " - + str(art.get_joint_positions(joint_indices=[dof_ptr]).item()) - ) - print("TIME: ", simulation_context.current_time) - - -def render_callback(event): - print("Render Frame") - - -simulation_context.add_physics_callback("physics_callback_1", step_callback_1) -simulation_context.add_physics_callback("physics_callback_2", step_callback_2) -simulation_context.add_render_callback("render_callback", render_callback) -# Simulate 60 timesteps -for i in range(60): - print("step", i) - simulation_context.step(render=False) -# Render one frame -simulation_context.render() - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/time_stepping.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/time_stepping.py deleted file mode 100644 index 4ea963631..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/time_stepping.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" - -simulation_context = SimulationContext(stage_units_in_meters=1.0) -add_reference_to_stage(asset_path, "/Franka") -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - - -def step_callback(step_size): - print("simulate with step: ", step_size) - return - - -def render_callback(event): - print("update app with step: ", event.payload["dt"]) - - -simulation_context.add_physics_callback("physics_callback", step_callback) -simulation_context.add_render_callback("render_callback", render_callback) -simulation_context.stop() -simulation_context.play() - -print("step physics once with a step size of 1/60 second, these are the default settings") -simulation_context.step(render=False) - -print("step physics & rendering once with a step size of 1/60 second, these are the default settings") -simulation_context.step(render=True) - -print("step physics & rendering once with a step size of 1/60 second") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0) -simulation_context.step(render=True) - -print("step physics 10 steps at a 1/600s per step and rendering at 1.0/60s") -simulation_context.set_simulation_dt(physics_dt=1.0 / 600.0, rendering_dt=1.0 / 60.0) -simulation_context.step(render=True) - -print("step physics once at 600Hz without rendering") -simulation_context.set_simulation_dt(physics_dt=1.0 / 600.0, rendering_dt=1.0 / 60.0) -simulation_context.step(render=False) - -print("step physics 10 steps at a 1/600s per step and rendering at 1.0/60s") -simulation_context.set_simulation_dt(physics_dt=1.0 / 600.0, rendering_dt=1.0 / 60.0) -for step in range(10): - simulation_context.step(render=False) -simulation_context.render() - -print("render a frame, moving editor timeline forward by 1.0/60s, physics does not simulate") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0) -simulation_context.render() - -print("render a frame, moving editor timeline forward by 1.0/60s, physics does not simulate") -simulation_context.set_simulation_dt(physics_dt=0.0, rendering_dt=1.0 / 60) -simulation_context.step(render=True) - -print("step physics once 1/60s per step and rendering 10 times at 1.0/600s") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 600.0) -for step in range(10): - simulation_context.step(render=True) - -print("step physics once 1/60s per step and rendering once at 1.0/600s by explicitly calling step and render") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 600.0) -simulation_context.step(render=False) -simulation_context.render() - -print("step physics once 1/60s per step, rendering a frame does not move editor timeline forward") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=0.0) -simulation_context.step(render=False) -simulation_context.render() - -print("step physics once 1/60s per step, rendering a frame does not move editor timeline forward") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=0.0) -simulation_context.step(render=True) - -print("render a new frame with simulation stopped, editor timeline does not move forward") -simulation_context.stop() -simulation_context.render() - -print("cleanup and exit") -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/visual_materials.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/visual_materials.py deleted file mode 100644 index 14fe82cba..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.api/visual_materials.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import random -import sys - -import carb -from isaacsim.core.api import World -from isaacsim.core.api.materials.omni_glass import OmniGlass -from isaacsim.core.api.materials.omni_pbr import OmniPBR -from isaacsim.core.api.objects import VisualCuboid -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() -asset_path = assets_root_path + "/Isaac/Materials/Textures/Synthetic/bubbles_2.png" - -my_world = World(stage_units_in_meters=1.0) - -textured_material = OmniPBR( - prim_path="/World/visual_cube_material", - name="omni_pbr", - color=np.array([1, 0, 0]), - texture_path=asset_path, - texture_scale=[1.0, 1.0], - texture_translate=[0.5, 0], -) - -glass = OmniGlass( - prim_path=f"/World/visual_cube_material_2", - ior=1.25, - depth=0.001, - thin_walled=False, - color=np.array([random.random(), random.random(), random.random()]), -) - -cube_1 = my_world.scene.add( - VisualCuboid( - prim_path="/new_cube_1", - name="visual_cube", - position=np.array([0, 0, 0.5]), - size=1.0, - color=np.array([255, 255, 255]), - visual_material=textured_material, - ) -) - -cube_2 = my_world.scene.add( - VisualCuboid( - prim_path="/new_cube_2", - name="visual_cube_2", - position=np.array([2, 0.39, 0.5]), - size=1.0, - color=np.array([255, 255, 255]), - visual_material=glass, - ) -) - -visual_material = cube_2.get_applied_visual_material() -visual_material.set_color(np.array([1.0, 0.5, 0.0])) - -my_world.scene.add_default_ground_plane() - -my_world.reset() -for i in range(10000): - my_world.step(render=True) - if args.test is True: - break - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.cloner/clone_ants.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.core.cloner/clone_ants.py deleted file mode 100644 index b94631387..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.core.cloner/clone_ants.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.cloner import GridCloner -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.stage import add_reference_to_stage, get_stage_units -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() - -# create initial robot -asset_path = assets_root_path + "/Isaac/Robots/Ant/ant.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Ants/Ant_0") - -# create GridCloner instance -cloner = GridCloner(spacing=2) - -# generate paths for clones -target_paths = cloner.generate_paths("/World/Ants/Ant", 4) - -# clone -position_offsets = np.array([[0, 0, 1]] * 4) -cloner.clone( - source_prim_path="/World/Ants/Ant_0", - prim_paths=target_paths, - position_offsets=position_offsets, - replicate_physics=True, - base_env_path="/World/Ants", -) - -# create Articulation -ants = Articulation("/World/Ants/.*/torso", name="ants_view") -my_world.scene.add(ants) - -my_world.reset() -for i in range(1000): - print(ants.get_world_poses()) - my_world.step() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/behaviors/franka/franka_behaviors.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/behaviors/franka/franka_behaviors.py deleted file mode 100644 index 7b1ae2722..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/behaviors/franka/franka_behaviors.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import sys - -if __name__ == "__main__": - print( - "This file is not meant to be executed. Please check the files at the root of `isaacsim.cortex.framework` for the main entry points." - ) - sys.exit(0) - -from isaacsim.cortex.behaviors.franka import ( - block_stacking_behavior, - peck_decider_network, - peck_game, - peck_state_machine, -) -from isaacsim.cortex.behaviors.franka.simple import simple_decider_network, simple_state_machine -from isaacsim.cortex.framework.dfb import DfDiagnosticsMonitor - -behaviors = { - "block_stacking_behavior": block_stacking_behavior, - "peck_decider_network": peck_decider_network, - "peck_game": peck_game, - "peck_state_machine": peck_state_machine, - "simple_decider_network": simple_decider_network, - "simple_state_machine": simple_state_machine, -} - - -class ContextStateMonitor(DfDiagnosticsMonitor): - """ - State monitor to read the context and pass it to the UI. - For these behaviors, the context has a `diagnostic_message` that contains the text to be displayed, and each - behavior implements its own monitor to update that. - - """ - - def __init__(self, print_dt, diagnostic_fn=None): - super().__init__(print_dt=print_dt) - - def print_diagnostics(self, context): - if hasattr(context, "diagnostics_message"): - print("====================================") - print(context.diagnostics_message) diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/demo_ur10_conveyor_main.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/demo_ur10_conveyor_main.py deleted file mode 100644 index dbe6af050..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/demo_ur10_conveyor_main.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import random - -import isaacsim.cortex.behaviors.ur10.bin_stacking_behavior as behavior -import isaacsim.cortex.framework.math_util as math_util -import numpy as np -from isaacsim.core.api.objects import VisualCapsule, VisualSphere -from isaacsim.core.api.tasks import BaseTask -from isaacsim.core.prims import XFormPrim -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.cortex.framework.cortex_rigid_prim import CortexRigidPrim -from isaacsim.cortex.framework.cortex_utils import get_assets_root_path_or_die -from isaacsim.cortex.framework.cortex_world import CortexWorld -from isaacsim.cortex.framework.robot import CortexUr10 - - -class Ur10Assets: - def __init__(self): - self.assets_root_path = get_assets_root_path_or_die() - - self.ur10_table_usd = ( - self.assets_root_path + "/Isaac/Samples/Leonardo/Stage/ur10_bin_stacking_short_suction.usd" - ) - self.small_klt_usd = self.assets_root_path + "/Isaac/Props/KLT_Bin/small_KLT.usd" - self.background_usd = self.assets_root_path + "/Isaac/Environments/Simple_Warehouse/warehouse.usd" - self.rubiks_cube_usd = self.assets_root_path + "/Isaac/Props/Rubiks_Cube/rubiks_cube.usd" - - -def print_diagnostics(diagnostic): - print("=========== logical state ==========") - if diagnostic.bin_name: - print("active bin info:") - print("- bin_obj.name: {}".format(diagnostic.bin_name)) - print("- bin_base: {}".format(diagnostic.bin_base)) - print("- grasp_T:\n{}".format(diagnostic.grasp)) - print("- is_grasp_reached: {}".format(diagnostic.grasp_reached)) - print("- is_attached: {}".format(diagnostic.attached)) - print("- needs_flip: {}".format(diagnostic.needs_flip)) - else: - print("") - - print("------------------------------------") - - -def random_bin_spawn_transform(): - x = random.uniform(-0.15, 0.15) - y = 1.5 - z = -0.15 - position = np.array([x, y, z]) - - z = random.random() * 0.02 - 0.01 - w = random.random() * 0.02 - 0.01 - norm = np.sqrt(z**2 + w**2) - quat = math_util.Quaternion([w / norm, 0, 0, z / norm]) - if random.random() > 0.5: - print("") - # flip the bin so it's upside down - quat = quat * math_util.Quaternion([0, 0, 1, 0]) - else: - print("") - - return position, quat.vals - - -class BinStackingTask(BaseTask): - def __init__(self, env_path, assets): - super().__init__("bin_stacking") - self.assets = assets - - self.env_path = "/World/Ur10Table" - self.bins = [] - self.stashed_bins = [] - self.on_conveyor = None - - def _spawn_bin(self, rigid_bin): - x, q = random_bin_spawn_transform() - rigid_bin.set_world_pose(position=x, orientation=q) - rigid_bin.set_linear_velocity(np.array([0, -0.30, 0])) - rigid_bin.set_visibility(True) - - def post_reset(self) -> None: - if len(self.bins) > 0: - for rigid_bin in self.bins: - self.scene.remove_object(rigid_bin.name) - self.bins.clear() - - self.on_conveyor = None - - def pre_step(self, time_step_index, simulation_time) -> None: - """Spawn a new randomly oriented bin if the previous bin has been placed.""" - spawn_new = False - if self.on_conveyor is None: - spawn_new = True - else: - (x, y, z), _ = self.on_conveyor.get_world_pose() - is_on_conveyor = y > 0.0 and -0.4 < x and x < 0.4 - if not is_on_conveyor: - spawn_new = True - - if spawn_new: - name = "bin_{}".format(len(self.bins)) - prim_path = self.env_path + "/bins/{}".format(name) - add_reference_to_stage(usd_path=self.assets.small_klt_usd, prim_path=prim_path) - self.on_conveyor = self.scene.add(CortexRigidPrim(name=name, prim_path=prim_path)) - - self._spawn_bin(self.on_conveyor) - self.bins.append(self.on_conveyor) - - -def main(): - world = CortexWorld() - - env_path = "/World/Ur10Table" - ur10_assets = Ur10Assets() - add_reference_to_stage(usd_path=ur10_assets.ur10_table_usd, prim_path=env_path) - add_reference_to_stage(usd_path=ur10_assets.background_usd, prim_path="/World/Background") - background_prim = XFormPrim( - "/World/Background", - positions=np.array([[10.00, 2.00, -1.18180]]), - orientations=np.array([[0.7071, 0, 0, 0.7071]]), - ) - robot = world.add_robot(CortexUr10(name="robot", prim_path="{}/ur10".format(env_path))) - - obs = world.scene.add( - VisualSphere( - "/World/Ur10Table/Obstacles/FlipStationSphere", - name="flip_station_sphere", - position=np.array([0.73, 0.76, -0.13]), - radius=0.2, - visible=False, - ) - ) - robot.register_obstacle(obs) - obs = world.scene.add( - VisualSphere( - "/World/Ur10Table/Obstacles/NavigationDome", - name="navigation_dome_obs", - position=[-0.031, -0.018, -1.086], - radius=1.1, - visible=False, - ) - ) - robot.register_obstacle(obs) - - az = np.array([1.0, 0.0, -0.3]) - ax = np.array([0.0, 1.0, 0.0]) - ay = np.cross(az, ax) - R = math_util.pack_R(ax, ay, az) - quat = math_util.matrix_to_quat(R) - obs = world.scene.add( - VisualCapsule( - "/World/Ur10Table/Obstacles/NavigationBarrier", - name="navigation_barrier_obs", - position=[0.471, 0.276, -0.463 - 0.1], - orientation=quat, - radius=0.5, - height=0.9, - visible=False, - ) - ) - robot.register_obstacle(obs) - - obs = world.scene.add( - VisualCapsule( - "/World/Ur10Table/Obstacles/NavigationFlipStation", - name="navigation_flip_station_obs", - position=np.array([0.766, 0.755, -0.5]), - radius=0.5, - height=0.5, - visible=False, - ) - ) - robot.register_obstacle(obs) - - world.add_task(BinStackingTask(env_path, ur10_assets)) - world.add_decider_network(behavior.make_decider_network(robot, print_diagnostics)) - - world.run(simulation_app) - simulation_app.close() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/example_command_api_main.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/example_command_api_main.py deleted file mode 100644 index e8df17f46..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/example_command_api_main.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import time - -import numpy as np -import omni -from isaacsim.core.api.objects import DynamicCuboid, VisualCuboid -from isaacsim.cortex.framework.cortex_world import CortexWorld -from isaacsim.cortex.framework.df import DfNetwork, DfState, DfStateMachineDecider, DfStateSequence -from isaacsim.cortex.framework.dfb import DfBasicContext -from isaacsim.cortex.framework.robot import add_franka_to_stage - - -class NullspaceShiftState(DfState): - def __init__(self): - super().__init__() - self.config_mean = np.array([0.00, -1.3, 0.00, -2.87, 0.00, 2.00, 0.75]) - self.target_p = np.array([0.7, 0.0, 0.5]) - self.construction_time = time.time() - - def enter(self): - # Change the posture configuration while maintaining a consistent target. - posture_config = self.config_mean + np.random.randn(7) - self.context.robot.arm.send_end_effector(target_position=self.target_p, posture_config=posture_config) - - self.entry_time = time.time() - - # Close the gripper if open and open the gripper if closed. It closes more quickly than it - # opens. - gripper = self.context.robot.gripper - if gripper.get_width() > 0.05: - gripper.close(speed=0.5) - else: - gripper.open(speed=0.1) - - print("[%f] sampling posture config" % (self.entry_time - self.construction_time)) - - def step(self): - if time.time() - self.entry_time < 2.0: - return self - return None - - -def main(): - world = CortexWorld() - robot = world.add_robot(add_franka_to_stage(name="franka", prim_path="/World/franka")) - world.scene.add_default_ground_plane() - - decider_network = DfNetwork( - DfStateMachineDecider(DfStateSequence([NullspaceShiftState()], loop=True)), context=DfBasicContext(robot) - ) - world.add_decider_network(decider_network) - - world.run(simulation_app) - simulation_app.close() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/follow_example_main.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/follow_example_main.py deleted file mode 100644 index 92deedddb..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/follow_example_main.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api.objects import VisualSphere -from isaacsim.cortex.framework.cortex_world import CortexWorld -from isaacsim.cortex.framework.df import DfNetwork, DfState, DfStateMachineDecider -from isaacsim.cortex.framework.dfb import DfBasicContext -from isaacsim.cortex.framework.robot import add_franka_to_stage - - -class FollowState(DfState): - """The context object is available as self.context. We have access to everything in the context - object, which in this case is everything in the robot object (the command API and the follow - sphere). - """ - - @property - def robot(self): - return self.context.robot - - @property - def follow_sphere(self): - return self.context.robot.follow_sphere - - def enter(self): - self.robot.gripper.close() - self.follow_sphere.set_world_pose(*self.robot.arm.get_fk_pq().as_tuple()) - - def step(self): - target_position, _ = self.follow_sphere.get_world_pose() - self.robot.arm.send_end_effector(target_position=target_position) - return self # Always transition back to this state. - - -def main(): - world = CortexWorld() - robot = world.add_robot(add_franka_to_stage(name="franka", prim_path="/World/Franka")) - - # Add a sphere to the scene to follow, and store it off in a new member as part of the robot. - robot.follow_sphere = world.scene.add( - VisualSphere( - name="follow_sphere", prim_path="/World/FollowSphere", radius=0.02, color=np.array([0.7, 0.0, 0.7]) - ) - ) - world.scene.add_default_ground_plane() - - # Add a simple state machine decider network with the single state defined above. This state - # will be persistently stepped because it always returns itself. - world.add_decider_network(DfNetwork(DfStateMachineDecider(FollowState()), context=DfBasicContext(robot))) - - world.run(simulation_app) - simulation_app.close() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/follow_example_modified_main.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/follow_example_modified_main.py deleted file mode 100644 index 0f292f2a1..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/follow_example_modified_main.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api.objects import VisualSphere -from isaacsim.cortex.framework.cortex_world import CortexWorld -from isaacsim.cortex.framework.df import DfNetwork, DfState, DfStateMachineDecider -from isaacsim.cortex.framework.dfb import DfRobotApiContext -from isaacsim.cortex.framework.robot import add_franka_to_stage - - -class FollowState(DfState): - """The context object is available as self.context. We have access to everything in the context - object, which in this case is everything in the robot object (the command API and the follow - sphere). - """ - - @property - def robot(self): - return self.context.robot - - @property - def follow_sphere(self): - return self.context.robot.follow_sphere - - def enter(self): - self.follow_sphere.set_world_pose(*self.robot.arm.get_fk_pq().as_tuple()) - - def step(self): - target_position, _ = self.follow_sphere.get_world_pose() - target_position[2] = max(target_position[2], 0.02) - self.robot.arm.send_end_effector(target_position=target_position) - return self # Always transition back to this state. - - -class FollowContext(DfRobotApiContext): - def __init__(self, robot): - super().__init__(robot) - self.reset() - - self.add_monitors( - [FollowContext.monitor_end_effector, FollowContext.monitor_gripper, FollowContext.monitor_diagnostics] - ) - - def reset(self): - self.is_target_reached = False - - def monitor_end_effector(self): - eff_p = self.robot.arm.get_fk_p() - target_p, _ = self.robot.follow_sphere.get_world_pose() - self.is_target_reached = np.linalg.norm(target_p - eff_p) < 0.01 - - def monitor_gripper(self): - if self.is_target_reached: - self.robot.gripper.close() - else: - self.robot.gripper.open() - - def monitor_diagnostics(self): - print("is_target_reached: {}".format(self.is_target_reached)) - - -def main(): - world = CortexWorld() - robot = world.add_robot(add_franka_to_stage(name="franka", prim_path="/World/Franka")) - - # Add a sphere to the scene to follow, and store it off in a new member as part of the robot. - robot.follow_sphere = world.scene.add( - VisualSphere( - name="follow_sphere", prim_path="/World/FollowSphere", radius=0.02, color=np.array([0.7, 0.0, 0.7]) - ) - ) - world.scene.add_default_ground_plane() - - # Add a simple state machine decider network with the single state defined above. This state - # will be persistently stepped because it always returns itself. - world.add_decider_network(DfNetwork(DfStateMachineDecider(FollowState()), context=FollowContext(robot))) - - world.run(simulation_app) - simulation_app.close() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/franka_examples_main.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/franka_examples_main.py deleted file mode 100644 index 16cffaa90..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.cortex.framework/franka_examples_main.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -from isaacsim import SimulationApp - -parser = argparse.ArgumentParser("franka_examples") -parser.add_argument( - "--behavior", - type=str, - default="block_stacking_behavior", - help="Which behavior to run. See behavior/franka for available behavior files.", -) -args, _ = parser.parse_known_args() - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from behaviors.franka.franka_behaviors import ContextStateMonitor, behaviors -from isaacsim.core.api.objects import DynamicCuboid, VisualCuboid -from isaacsim.cortex.framework.cortex_utils import load_behavior_module -from isaacsim.cortex.framework.cortex_world import Behavior, CortexWorld, LogicalStateMonitor -from isaacsim.cortex.framework.robot import add_franka_to_stage -from isaacsim.cortex.framework.tools import SteadyRate - - -class CubeSpec: - def __init__(self, name, color): - self.name = name - self.color = np.array(color) - - -def main(): - world = CortexWorld() - context_monitor = ContextStateMonitor(print_dt=0.25) - robot = world.add_robot(add_franka_to_stage(name="franka", prim_path="/World/Franka")) - - obs_specs = [ - CubeSpec("RedCube", [0.7, 0.0, 0.0]), - CubeSpec("BlueCube", [0.0, 0.0, 0.7]), - CubeSpec("YellowCube", [0.7, 0.7, 0.0]), - CubeSpec("GreenCube", [0.0, 0.7, 0.0]), - ] - width = 0.0515 - for i, (x, spec) in enumerate(zip(np.linspace(0.3, 0.7, len(obs_specs)), obs_specs)): - obj = world.scene.add( - DynamicCuboid( - prim_path="/World/Obs/{}".format(spec.name), - name=spec.name, - size=width, - color=spec.color, - position=np.array([x, -0.4, width / 2]), - ) - ) - robot.register_obstacle(obj) - world.scene.add_default_ground_plane() - - print() - print("loading behavior: {}".format(args.behavior)) - print() - if args.behavior in behaviors: - decider_network = behaviors[args.behavior].make_decider_network(robot) - else: - decider_network = load_behavior_module(args.behavior).make_decider_network(robot) - decider_network.context.add_monitor(context_monitor.monitor) - world.add_decider_network(decider_network) - - world.run(simulation_app) - simulation_app.close() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.behavior/behaviors.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.behavior/behaviors.py deleted file mode 100644 index d23f2c90c..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.behavior/behaviors.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import inspect -import os - -import carb.settings -import omni.kit.app -import omni.kit.commands -import omni.timeline -import omni.usd -from isaacsim.core.utils.extensions import get_extension_path_from_name -from isaacsim.replicator.behavior.behaviors import ( - LightRandomizer, - LocationRandomizer, - LookAtBehavior, - RotationRandomizer, - TextureRandomizer, -) -from isaacsim.replicator.behavior.global_variables import EXPOSED_ATTR_NS -from isaacsim.replicator.behavior.utils.behavior_utils import add_behavior_script -from pxr import Sdf - -SCRIPTS_ATTR = "omni:scripting:scripts" - -# Get the path to the behavior scripts python files -EXTENSION_PATH = get_extension_path_from_name("isaacsim.replicator.behavior") -SCRIPTS_PATH = os.path.join(EXTENSION_PATH, "isaacsim/replicator/behavior/behaviors") - -# Enable scripting extension in standalone mode -ext_manager = omni.kit.app.get_app().get_extension_manager() -ext_manager.set_extension_enabled_immediate("omni.kit.scripting", True) -simulation_app.update() - -# Enable behavior scripts (running python scripts attached to USD assets) -carb.settings.get_settings().set_bool("/app/scripting/ignoreWarningDialog", True) -simulation_app.update() - - -# Setup a new stage with a dome light -def setup_stage(): - omni.usd.get_context().new_stage() - stage = omni.usd.get_context().get_stage() - dome_light = stage.DefinePrim("/World/DomeLight", "DomeLight") - dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(500.0) - - -# Add scripting to the root prim with a behavior script and the custom exposed variables values -def add_behavior_script_with_parameters(prim_path, behavior_class, exposed_variables={}): - stage = omni.usd.get_context().get_stage() - prim = stage.GetPrimAtPath(prim_path) - if not prim: - raise RuntimeError(f"No prim found at path: {prim_path}") - - # Get the script path from the behavior class - script_path = inspect.getfile(behavior_class) - - # Add the behavior script to the prim - add_behavior_script(prim, script_path) - - # NOTE: 2-3 updates are needed to ensure the script is loaded and the exposed variables are set - for _ in range(3): - simulation_app.update() - - # Append the exposed variables with the corresponding namespace and set them as properties on the prim - variable_ns = f"{EXPOSED_ATTR_NS}:{behavior_class.BEHAVIOR_NS}" - for var_name, var_value in exposed_variables.items(): - full_var_name = f"{variable_ns}:{var_name}" - exposed_var_attr = prim.GetAttribute(full_var_name) - if not exposed_var_attr: - raise RuntimeError(f"No exposed variable attribute {full_var_name} found on prim: {prim_path}") - exposed_var_attr.Set(var_value) - - -# Remove the scripts from the prim paths list -def remove_all_scripts(prim_paths): - stage = omni.usd.get_context().get_stage() - for prim_path in prim_paths: - prim = stage.GetPrimAtPath(prim_path) - if not prim: - raise RuntimeError(f"No prim found at path: {prim_path}") - scripts_attr = prim.GetAttribute(SCRIPTS_ATTR) - if not scripts_attr: - raise RuntimeError(f"No '{SCRIPTS_ATTR}' attribute found on prim: {prim_path}") - scripts_attr.Set(Sdf.AssetPathArray()) - - -# Create prims for single randomization (single prim at root) -def create_prims_single(prim_path, prim_type): - stage = omni.usd.get_context().get_stage() - prim = stage.DefinePrim(prim_path, prim_type) - if not prim.IsValid(): - raise RuntimeError(f"Failed to create prim of type {prim_type} at {prim_path}") - - -# Create prims for multi randomization (children under a root) -def create_prims_multi(root_path, num_prims=1, prim_type="SphereLight", prim_name="light"): - stage = omni.usd.get_context().get_stage() - - for i in range(num_prims): - prim_path = f"{root_path}/{prim_name}_{i}" if num_prims > 1 else root_path - prim = stage.DefinePrim(prim_path, prim_type) - - if not prim.IsValid(): - raise RuntimeError(f"Failed to create prim of type {prim_type} at {prim_path}") - - -# Create a new stage -setup_stage() - - -# Create a light with behavior scripts -light_path = "/Single/Light" -create_prims_single(light_path, "SphereLight") -add_behavior_script_with_parameters(light_path, LightRandomizer) -add_behavior_script_with_parameters(light_path, LocationRandomizer) -add_behavior_script_with_parameters(light_path, RotationRandomizer) - -# Create a cube with behavior scripts -cube_path = "/Single/Cube" -create_prims_single(cube_path, "Cube") -add_behavior_script_with_parameters(cube_path, LocationRandomizer) -add_behavior_script_with_parameters(cube_path, RotationRandomizer) -add_behavior_script_with_parameters(cube_path, TextureRandomizer) - -# Create a camera with behavior scripts -camera_path = "/Single/Camera" -create_prims_single(camera_path, "Camera") -add_behavior_script_with_parameters(camera_path, LookAtBehavior) -add_behavior_script_with_parameters(camera_path, LocationRandomizer) - - -# Create lights nested under a root prim and add behavior scripts to the root prim with includeChildren flag -nested_lights_path = "/Nested/Lights" -create_prims_multi(root_path=nested_lights_path, num_prims=3, prim_type="SphereLight", prim_name="light") -add_behavior_script_with_parameters(nested_lights_path, LightRandomizer, exposed_variables={f"includeChildren": True}) -add_behavior_script_with_parameters( - nested_lights_path, LocationRandomizer, exposed_variables={f"includeChildren": True} -) -add_behavior_script_with_parameters( - nested_lights_path, RotationRandomizer, exposed_variables={f"includeChildren": True} -) - -# Create cubes nested under a root prim and add behavior scripts to the root prim with includeChildren flag -nested_cubes_path = "/Nested/Cubes" -create_prims_multi(root_path=nested_cubes_path, num_prims=3, prim_type="Cube", prim_name="cube") -add_behavior_script_with_parameters(nested_cubes_path, LocationRandomizer, exposed_variables={f"includeChildren": True}) -add_behavior_script_with_parameters(nested_cubes_path, RotationRandomizer, exposed_variables={f"includeChildren": True}) -add_behavior_script_with_parameters(nested_cubes_path, TextureRandomizer, exposed_variables={f"includeChildren": True}) - -# Create cameras nested under a root prim and add behavior scripts to the root prim with includeChildren flag -nested_cameras_path = "/Nested/Cameras" -create_prims_multi(root_path=nested_cameras_path, num_prims=3, prim_type="Camera", prim_name="camera") -add_behavior_script_with_parameters(nested_cameras_path, LookAtBehavior, exposed_variables={f"includeChildren": True}) -add_behavior_script_with_parameters( - nested_cameras_path, LocationRandomizer, exposed_variables={f"includeChildren": True} -) - - -# Run the randomization scripts through the timeline -timeline = omni.timeline.get_timeline_interface() -timeline.play() -for _ in range(100): - simulation_app.update() -timeline.stop() -for _ in range(3): - simulation_app.update() - - -# Remove the scripts from the prims -prim_paths = [light_path, cube_path, camera_path, nested_lights_path, nested_cubes_path, nested_cameras_path] -remove_all_scripts(prim_paths=prim_paths) -for _ in range(3): - simulation_app.update() - -# Close the simulation app -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.domain_randomization/randomization_demo.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.domain_randomization/randomization_demo.py deleted file mode 100644 index e0fb172e4..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.domain_randomization/randomization_demo.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicSphere -from isaacsim.core.cloner import GridCloner -from isaacsim.core.prims import Articulation, RigidPrim -from isaacsim.core.utils.prims import define_prim, get_prim_at_path -from isaacsim.core.utils.stage import add_reference_to_stage, get_current_stage -from isaacsim.storage.native import get_assets_root_path - -# create the world -world = World(stage_units_in_meters=1.0, physics_prim_path="/physicsScene", backend="numpy") - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder, closing app..") - simulation_app.close() -usd_path = assets_root_path + "/Isaac/Environments/Grid/default_environment.usd" -add_reference_to_stage(usd_path=usd_path, prim_path="/World/defaultGroundPlane") - -# set up grid cloner -cloner = GridCloner(spacing=1.5) -cloner.define_base_env("/World/envs") -define_prim("/World/envs/env_0") - -# set up the first environment -DynamicSphere(prim_path="/World/envs/env_0/object", radius=0.1, position=np.array([0.75, 0.0, 0.2])) -add_reference_to_stage( - usd_path=assets_root_path + "/Isaac/Robots/Franka/franka.usd", prim_path="/World/envs/env_0/franka" -) - -# clone environments -num_envs = 4 -prim_paths = cloner.generate_paths("/World/envs/env", num_envs) -env_pos = cloner.clone(source_prim_path="/World/envs/env_0", prim_paths=prim_paths) - -# creates the views and set up world -object_view = RigidPrim(prim_paths_expr="/World/envs/*/object", name="object_view") -franka_view = Articulation("/World/envs/*/franka", name="franka_view") -world.scene.add(object_view) -world.scene.add(franka_view) -world.reset() - -num_dof = franka_view.num_dof - -# set up randomization with isaacsim.replicator, imported as dr -import isaacsim.replicator.domain_randomization as dr -import omni.replicator.core as rep - -dr.physics_view.register_simulation_context(world) -dr.physics_view.register_rigid_prim_view(object_view) -dr.physics_view.register_articulation_view(franka_view) - -with dr.trigger.on_rl_frame(num_envs=num_envs): - with dr.gate.on_interval(interval=20): - dr.physics_view.randomize_simulation_context( - operation="scaling", gravity=rep.distribution.uniform((1, 1, 0.0), (1, 1, 2.0)) - ) - with dr.gate.on_interval(interval=50): - dr.physics_view.randomize_rigid_prim_view( - view_name=object_view.name, operation="direct", force=rep.distribution.uniform((0, 0, 2.5), (0, 0, 5.0)) - ) - with dr.gate.on_interval(interval=10): - dr.physics_view.randomize_articulation_view( - view_name=franka_view.name, - operation="direct", - joint_velocities=rep.distribution.uniform(tuple([-2] * num_dof), tuple([2] * num_dof)), - ) - with dr.gate.on_env_reset(): - dr.physics_view.randomize_rigid_prim_view( - view_name=object_view.name, - operation="additive", - position=rep.distribution.normal((0.0, 0.0, 0.0), (0.2, 0.2, 0.0)), - velocity=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - ) - dr.physics_view.randomize_articulation_view( - view_name=franka_view.name, - operation="additive", - joint_positions=rep.distribution.uniform(tuple([-0.5] * num_dof), tuple([0.5] * num_dof)), - position=rep.distribution.normal((0.0, 0.0, 0.0), (0.2, 0.2, 0.0)), - ) - - -frame_idx = 0 -while simulation_app.is_running(): - if world.is_playing(): - reset_inds = list() - if frame_idx % 200 == 0: - # triggers reset every 200 steps - reset_inds = np.arange(num_envs) - dr.physics_view.step_randomization(reset_inds) - world.step(render=True) - frame_idx += 1 - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/custom_event_and_write.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/custom_event_and_write.py deleted file mode 100644 index 1b5ef9e90..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/custom_event_and_write.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import os - -import omni.replicator.core as rep -import omni.usd - -omni.usd.get_context().new_stage() -distance_light = rep.create.light(rotation=(315, 0, 0), intensity=4000, light_type="distant") - -large_cube = rep.create.cube(scale=1.25, position=(1, 1, 0)) -small_cube = rep.create.cube(scale=0.75, position=(-1, -1, 0)) -large_cube_prim = large_cube.get_output_prims()["prims"][0] -small_cube_prim = small_cube.get_output_prims()["prims"][0] - -rp = rep.create.render_product("/OmniverseKit_Persp", (512, 512)) -writer = rep.WriterRegistry.get("BasicWriter") -out_dir = os.getcwd() + "/_out_custom_event" -print(f"Writing data to {out_dir}") -writer.initialize(output_dir=out_dir, rgb=True) -writer.attach(rp) - -with rep.trigger.on_custom_event(event_name="randomize_large_cube"): - with large_cube: - rep.randomizer.rotation() - -with rep.trigger.on_custom_event(event_name="randomize_small_cube"): - with small_cube: - rep.randomizer.rotation() - - -def run_example(): - print(f"Randomizing small cube") - rep.utils.send_og_event(event_name="randomize_small_cube") - print("Capturing frame") - rep.orchestrator.step(rt_subframes=8) - - print("Moving small cube") - small_cube_prim.GetAttribute("xformOp:translate").Set((-2, -2, 0)) - print("Capturing frame") - rep.orchestrator.step(rt_subframes=8) - - print(f"Randomizing large cube") - rep.utils.send_og_event(event_name="randomize_large_cube") - print("Capturing frame") - rep.orchestrator.step(rt_subframes=8) - - print("Moving large cube") - large_cube_prim.GetAttribute("xformOp:translate").Set((2, 2, 0)) - print("Capturing frame") - rep.orchestrator.step(rt_subframes=8) - - # Wait until all the data is saved to disk - rep.orchestrator.wait_until_complete() - - -run_example() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/custom_fps_writer_annotator.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/custom_fps_writer_annotator.py deleted file mode 100644 index 135b3d68c..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/custom_fps_writer_annotator.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import os - -import carb.settings -import omni.kit.app -import omni.replicator.core as rep -import omni.timeline -import omni.usd - -# NOTE: To avoid FPS delta misses make sure the sensor framerate is divisible by the timeline framerate -STAGE_FPS = 100.0 -SENSOR_FPS = 10.0 -SENSOR_DT = 1.0 / SENSOR_FPS - - -def run_custom_fps_example(num_frames=10): - # Create a new stage - omni.usd.get_context().new_stage() - - # Disable capture on play (data will only be accessed at custom times) - carb.settings.get_settings().set("/omni/replicator/captureOnPlay", False) - - # Make sure fixed time stepping is set (the timeline will be advanced with the same delta time) - carb.settings.get_settings().set("/app/player/useFixedTimeStepping", True) - - # Set the timeline parameters - timeline = omni.timeline.get_timeline_interface() - timeline.set_looping(False) - timeline.set_current_time(0.0) - timeline.set_end_time(10) - timeline.set_time_codes_per_second(STAGE_FPS) - timeline.play() - timeline.commit() - - # Create a light and a semantically annoated cube - rep.create.light() - rep.create.cube(semantics=[("class", "cube")]) - - # Create a render product and disable it (it will re-enabled when data is needed) - rp = rep.create.render_product("/OmniverseKit_Persp", (512, 512), name="rp") - rp.hydra_texture.set_updates_enabled(False) - - # Create a writer and an annotator as different ways to access the data - out_dir_rgb = os.getcwd() + "/_out_writer_fps_rgb" - print(f"Writer data will be written to: {out_dir_rgb}") - writer_rgb = rep.WriterRegistry.get("BasicWriter") - writer_rgb.initialize(output_dir=out_dir_rgb, rgb=True) - # NOTE: 'trigger=None' is needed to make sure the writer is only triggered at the custom schedule times - writer_rgb.attach(rp, trigger=None) - annot_depth = rep.AnnotatorRegistry.get_annotator("distance_to_camera") - annot_depth.attach(rp) - - # Run the simulation for the given number of frames and access the data at the desired framerates - written_frames = 0 - previous_time = timeline.get_current_time() - elapsed_time = 0.0 - for i in range(num_frames): - current_time = timeline.get_current_time() - delta_time = current_time - previous_time - elapsed_time += delta_time - print( - f"[{i}] current_time={current_time:.4f}; delta_time={delta_time:.4f}; elapsed_time={elapsed_time:.4f}/{SENSOR_DT:.4f};" - ) - - # Check if enough time has passed to trigger the sensor - if elapsed_time >= SENSOR_DT: - # Reset the elapsed time with the difference to the optimal trigger time (when the timeline fps is not divisible by the sensor framerate) - elapsed_time = elapsed_time - SENSOR_DT - - # Enable render products for data access - rp.hydra_texture.set_updates_enabled(True) - - # Write will be scheduled at the next step call - writer_rgb.schedule_write() - - # Step needs to be called after scheduling the write - rep.orchestrator.step(delta_time=0.0) - - # After step, the annotator data is available and in sync with the stage - annot_data = annot_depth.get_data() - - # Count the number of frames written - print(f"\t Writing frame {written_frames}; annotator data shape={annot_data.shape};") - written_frames += 1 - - # Disable render products to avoid unnecessary rendering - rp.hydra_texture.set_updates_enabled(False) - - # Restart the timeline if it has been paused by the replicator step function - if not timeline.is_playing(): - timeline.play() - - previous_time = current_time - # Advance the app (timeline) by one frame - simulation_app.update() - - # Make sure the writer finishes writing the data to disk - rep.orchestrator.wait_until_complete() - - -# Run the example for a given number of frames ( -# NOTE: the expected number of frames written will be (num_frames - 1) * SENSOR_FPS / STAGE_FPS -run_custom_fps_example(num_frames=61) - -# Close the application -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/motion_blur.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/motion_blur.py deleted file mode 100644 index af14b67cd..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/motion_blur.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import os - -import carb.settings -import omni.kit.app -import omni.replicator.core as rep -import omni.timeline -import omni.usd -from isaacsim.storage.native import get_assets_root_path -from pxr import PhysxSchema, Sdf, UsdGeom, UsdPhysics - -# Paths to the animated and physics-ready assets -PHYSICS_ASSET_URL = "/Isaac/Props/YCB/Axis_Aligned_Physics/003_cracker_box.usd" -ANIM_ASSET_URL = "/Isaac/Props/YCB/Axis_Aligned/003_cracker_box.usd" - -# -z velocities and start locations of the animated (left side) and physics (right side) assets (stage units/s) -ASSET_VELOCITIES = [0, 5, 10] -ASSET_X_MIRRORED_LOCATIONS = [(0.5, 0, 0.3), (0.3, 0, 0.3), (0.1, 0, 0.3)] - -# Used to calculate how many frames to animate the assets to maintain the same velocity as the physics assets -ANIMATION_DURATION = 10 - -# Create a new stage with animated and physics-enabled assets with synchronized motion -def setup_stage(): - # Create new stage - omni.usd.get_context().new_stage() - stage = omni.usd.get_context().get_stage() - timeline = omni.timeline.get_timeline_interface() - timeline.set_end_time(ANIMATION_DURATION) - - # Create lights - dome_light = stage.DefinePrim("/World/DomeLight", "DomeLight") - dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(100.0) - distant_light = stage.DefinePrim("/World/DistantLight", "DistantLight") - if not distant_light.GetAttribute("xformOp:rotateXYZ"): - UsdGeom.Xformable(distant_light).AddRotateXYZOp() - distant_light.GetAttribute("xformOp:rotateXYZ").Set((-75, 0, 0)) - distant_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(2500) - - # Setup the physics assets with gravity disabled and the requested velocity - assets_root_path = get_assets_root_path() - physics_asset_url = assets_root_path + PHYSICS_ASSET_URL - for loc, vel in zip(ASSET_X_MIRRORED_LOCATIONS, ASSET_VELOCITIES): - prim = stage.DefinePrim(f"/World/physics_asset_{int(abs(vel))}", "Xform") - prim.GetReferences().AddReference(physics_asset_url) - if not prim.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(prim).AddTranslateOp() - prim.GetAttribute("xformOp:translate").Set(loc) - prim.GetAttribute("physxRigidBody:disableGravity").Set(True) - prim.GetAttribute("physxRigidBody:angularDamping").Set(0.0) - prim.GetAttribute("physxRigidBody:linearDamping").Set(0.0) - prim.GetAttribute("physics:velocity").Set((0, 0, -vel)) - - # Setup animated assets maintaining the same velocity as the physics asssets - anim_asset_url = assets_root_path + ANIM_ASSET_URL - for loc, vel in zip(ASSET_X_MIRRORED_LOCATIONS, ASSET_VELOCITIES): - start_loc = (-loc[0], loc[1], loc[2]) - prim = stage.DefinePrim(f"/World/anim_asset_{int(abs(vel))}", "Xform") - prim.GetReferences().AddReference(anim_asset_url) - if not prim.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(prim).AddTranslateOp() - anim_distance = vel * ANIMATION_DURATION - end_loc = (start_loc[0], start_loc[1], start_loc[2] - anim_distance) - end_keyframe = timeline.get_time_codes_per_seconds() * ANIMATION_DURATION - # Timesampled keyframe (animated) translation - prim.GetAttribute("xformOp:translate").Set(start_loc, time=0) - prim.GetAttribute("xformOp:translate").Set(end_loc, time=end_keyframe) - - -# Capture motion blur frames with the given delta time step and render mode -def run_motion_blur_example(num_frames=3, custom_delta_time=None, use_path_tracing=True, pt_subsamples=8, pt_spp=64): - # Create a new stage with the assets - setup_stage() - stage = omni.usd.get_context().get_stage() - - # Set replicator settings (capture only on request and enable motion blur) - carb.settings.get_settings().set("/omni/replicator/captureOnPlay", False) - carb.settings.get_settings().set("/omni/replicator/captureMotionBlur", True) - - # Set motion blur settings based on the render mode - if use_path_tracing: - print(f"[MotionBlur] Setting PathTracing render mode motion blur settings") - carb.settings.get_settings().set("/rtx/rendermode", "PathTracing") - # (int): Total number of samples for each rendered pixel, per frame. - carb.settings.get_settings().set("/rtx/pathtracing/spp", pt_spp) - # (int): Maximum number of samples to accumulate per pixel. When this count is reached the rendering stops until a scene or setting change is detected, restarting the rendering process. Set to 0 to remove this limit. - carb.settings.get_settings().set("/rtx/pathtracing/totalSpp", pt_spp) - carb.settings.get_settings().set("/rtx/pathtracing/optixDenoiser/enabled", 0) - # Number of sub samples to render if in PathTracing render mode and motion blur is enabled. - carb.settings.get_settings().set("/omni/replicator/pathTracedMotionBlurSubSamples", pt_subsamples) - else: - print(f"[MotionBlur] Setting RaytracedLighting render mode motion blur settings") - carb.settings.get_settings().set("/rtx/rendermode", "RaytracedLighting") - # 0: Disabled, 1: TAA, 2: FXAA, 3: DLSS, 4:RTXAA - carb.settings.get_settings().set("/rtx/post/aa/op", 2) - # (float): The fraction of the largest screen dimension to use as the maximum motion blur diameter. - carb.settings.get_settings().set("/rtx/post/motionblur/maxBlurDiameterFraction", 0.02) - # (float): Exposure time fraction in frames (1.0 = one frame duration) to sample. - carb.settings.get_settings().set("/rtx/post/motionblur/exposureFraction", 1.0) - # (int): Number of samples to use in the filter. A higher number improves quality at the cost of performance. - carb.settings.get_settings().set("/rtx/post/motionblur/numSamples", 8) - - # Setup camera and writer - camera = rep.create.camera(position=(0, 1.5, 0), look_at=(0, 0, 0), name="MotionBlurCam") - render_product = rep.create.render_product(camera, (1280, 720)) - basic_writer = rep.WriterRegistry.get("BasicWriter") - delta_time_str = "None" if custom_delta_time is None else f"{custom_delta_time:.4f}" - render_mode_str = f"pt_subsamples_{pt_subsamples}_spp_{pt_spp}" if use_path_tracing else "rt" - output_directory = os.getcwd() + f"/_out_motion_blur_dt_{delta_time_str}_{render_mode_str}" - print(f"[MotionBlur] Output directory: {output_directory}") - basic_writer.initialize(output_dir=output_directory, rgb=True) - basic_writer.attach(render_product) - - # Run a few updates to make sure all materials are fully loaded for capture - for _ in range(50): - simulation_app.update() - - # Use the physics scene to modify the physics FPS (if needed) to guarantee motion samples at any custom delta time - physx_scene = None - for prim in stage.Traverse(): - if prim.IsA(UsdPhysics.Scene): - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(prim) - break - if physx_scene is None: - print(f"[MotionBlur] Creating a new PhysicsScene") - physics_scene = UsdPhysics.Scene.Define(stage, "/PhysicsScene") - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(stage.GetPrimAtPath("/PhysicsScene")) - - # Check the target physics depending on the custom delta time and the render mode - target_physics_fps = stage.GetTimeCodesPerSecond() if custom_delta_time is None else 1 / custom_delta_time - if use_path_tracing: - target_physics_fps *= pt_subsamples - - # Check if the physics FPS needs to be increased to match the custom delta time - orig_physics_fps = physx_scene.GetTimeStepsPerSecondAttr().Get() - if target_physics_fps > orig_physics_fps: - print(f"[MotionBlur] Changing physics FPS from {orig_physics_fps} to {target_physics_fps}") - physx_scene.GetTimeStepsPerSecondAttr().Set(target_physics_fps) - - # Start the timeline for physics updates in the step function - timeline = omni.timeline.get_timeline_interface() - timeline.play() - - # Capture frames - for i in range(num_frames): - print(f"[MotionBlur] \tCapturing frame {i}") - rep.orchestrator.step(delta_time=custom_delta_time) - - # Restore the original physics FPS - if target_physics_fps > orig_physics_fps: - print(f"[MotionBlur] Restoring physics FPS from {target_physics_fps} to {orig_physics_fps}") - physx_scene.GetTimeStepsPerSecondAttr().Set(orig_physics_fps) - - # Switch back to the raytracing render mode - if use_path_tracing: - print(f"[MotionBlur] Restoring render mode to RaytracedLighting") - carb.settings.get_settings().set("/rtx/rendermode", "RaytracedLighting") - - # Wait until the data is fully written - rep.orchestrator.wait_until_complete() - - -def run_motion_blur_examples(): - motion_blur_step_duration = [None, 1 / 30, 1 / 60, 1 / 240] - for custom_delta_time in motion_blur_step_duration: - # RayTracing examples - run_motion_blur_example(custom_delta_time=custom_delta_time, use_path_tracing=False) - # PathTracing examples - spps = [32, 128] - motion_blur_sub_samples = [4, 16] - for motion_blur_sub_sample in motion_blur_sub_samples: - for spp in spps: - run_motion_blur_example( - custom_delta_time=custom_delta_time, - use_path_tracing=True, - pt_subsamples=motion_blur_sub_sample, - pt_spp=spp, - ) - - -run_motion_blur_examples() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/multi_camera.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/multi_camera.py deleted file mode 100644 index a7fea8b92..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/multi_camera.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import os - -import omni.kit -import omni.replicator.core as rep -import omni.usd -from omni.replicator.core import AnnotatorRegistry, Writer -from PIL import Image -from pxr import Sdf, UsdGeom - -NUM_FRAMES = 5 - -# Save rgb image to file -def save_rgb(rgb_data, file_name): - rgb_img = Image.fromarray(rgb_data, "RGBA") - rgb_img.save(file_name + ".png") - - -# Randomize cube color every frame using a replicator randomizer -def cube_color_randomizer(): - cube_prims = rep.get.prims(path_pattern="Cube") - with cube_prims: - rep.randomizer.color(colors=rep.distribution.uniform((0, 0, 0), (1, 1, 1))) - return cube_prims.node - - -# Access data through a custom replicator writer -class MyWriter(Writer): - def __init__(self, rgb: bool = True): - self._frame_id = 0 - if rgb: - self.annotators.append(AnnotatorRegistry.get_annotator("rgb")) - # Create writer output directory - self.file_path = os.path.join(os.getcwd(), "_out_mc_writer", "") - print(f"Writing writer data to {self.file_path}") - dir = os.path.dirname(self.file_path) - os.makedirs(dir, exist_ok=True) - - def write(self, data): - for annotator in data.keys(): - annotator_split = annotator.split("-") - if len(annotator_split) > 1: - render_product_name = annotator_split[-1] - if annotator.startswith("rgb"): - save_rgb(data[annotator], f"{self.file_path}/{render_product_name}_frame_{self._frame_id}") - self._frame_id += 1 - - -rep.WriterRegistry.register(MyWriter) - -# Create a new stage with a dome light -omni.usd.get_context().new_stage() -stage = omni.usd.get_context().get_stage() -dome_light = stage.DefinePrim("/World/DomeLight", "DomeLight") -dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(900.0) - -# Create cube -cube_prim = stage.DefinePrim("/World/Cube", "Cube") -UsdGeom.Xformable(cube_prim).AddTranslateOp().Set((0.0, 5.0, 1.0)) - -# Register cube color randomizer to trigger on every frame -rep.randomizer.register(cube_color_randomizer) -with rep.trigger.on_frame(): - rep.randomizer.cube_color_randomizer() - -# Create cameras -camera_prim1 = stage.DefinePrim("/World/Camera1", "Camera") -UsdGeom.Xformable(camera_prim1).AddTranslateOp().Set((0.0, 10.0, 20.0)) -UsdGeom.Xformable(camera_prim1).AddRotateXYZOp().Set((-15.0, 0.0, 0.0)) - -camera_prim2 = stage.DefinePrim("/World/Camera2", "Camera") -UsdGeom.Xformable(camera_prim2).AddTranslateOp().Set((-10.0, 15.0, 15.0)) -UsdGeom.Xformable(camera_prim2).AddRotateXYZOp().Set((-45.0, 0.0, 45.0)) - -# Create render products -rp1 = rep.create.render_product(str(camera_prim1.GetPrimPath()), resolution=(320, 320)) -rp2 = rep.create.render_product(str(camera_prim2.GetPrimPath()), resolution=(640, 640)) -rp3 = rep.create.render_product("/OmniverseKit_Persp", (1024, 1024)) - -# Acess the data through a custom writer -writer = rep.WriterRegistry.get("MyWriter") -writer.initialize(rgb=True) -writer.attach([rp1, rp2, rp3]) - -# Acess the data through annotators -rgb_annotators = [] -for rp in [rp1, rp2, rp3]: - rgb = rep.AnnotatorRegistry.get_annotator("rgb") - rgb.attach(rp) - rgb_annotators.append(rgb) - -# Create annotator output directory -file_path = os.path.join(os.getcwd(), "_out_mc_annot", "") -print(f"Writing annotator data to {file_path}") -dir = os.path.dirname(file_path) -os.makedirs(dir, exist_ok=True) - -# Data will be captured manually using step -rep.orchestrator.set_capture_on_play(False) - -for i in range(NUM_FRAMES): - # The step function provides new data to the annotators, triggers the randomizers and the writer - rep.orchestrator.step(rt_subframes=4) - for j, rgb_annot in enumerate(rgb_annotators): - save_rgb(rgb_annot.get_data(), f"{dir}/rp{j}_step_{i}") - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_01.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_01.py deleted file mode 100644 index e5a904eb3..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_01.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import os - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import omni.replicator.core as rep -import omni.usd -from isaacsim.core.utils.semantics import add_update_semantics -from pxr import Sdf - - -def run_example(): - # Create a new stage and disable capture on play - omni.usd.get_context().new_stage() - rep.orchestrator.set_capture_on_play(False) - - # Setup the stage with a dome light and a cube - stage = omni.usd.get_context().get_stage() - dome_light = stage.DefinePrim("/World/DomeLight", "DomeLight") - dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(500.0) - cube = stage.DefinePrim("/World/Cube", "Cube") - add_update_semantics(cube, "MyCube") - - # Create a render product using the viewport perspective camera - rp = rep.create.render_product("/OmniverseKit_Persp", (512, 512)) - - # Write data using the basic writer with the rgb and bounding box annotators - writer = rep.writers.get("BasicWriter") - out_dir = os.getcwd() + "/_out_basic_writer" - print(f"Output directory: {out_dir}") - writer.initialize(output_dir=out_dir, rgb=True, bounding_box_2d_tight=True) - writer.attach(rp) - - # Trigger a data capture request (data will be written to disk by the writer) - for i in range(3): - print(f"Step {i}") - rep.orchestrator.step() - - # Destroy the render product to release resources by detaching it from the writer first - writer.detach() - rp.destroy() - - # Wait for the data to be written to disk - rep.orchestrator.wait_until_complete() - - -# Run the example -run_example() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_02.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_02.py deleted file mode 100644 index db3d3470b..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_02.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import os - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import omni.replicator.core as rep -import omni.usd -from isaacsim.core.utils.semantics import add_update_semantics -from omni.replicator.core import Writer -from pxr import Sdf, UsdGeom - - -# Create a custom writer to access the annotator data -class MyWriter(Writer): - def __init__(self, camera_params: bool = True, bounding_box_3d: bool = True): - # Organize data from render product perspective (legacy, annotator, renderProduct) - self.data_structure = "renderProduct" - if camera_params: - self.annotators.append(rep.annotators.get("camera_params")) - if bounding_box_3d: - self.annotators.append(rep.annotators.get("bounding_box_3d")) - self._frame_id = 0 - - def write(self, data): - print(f"[MyWriter][{self._frame_id}] data:{data}") - self._frame_id += 1 - - -# Register the writer for use -rep.writers.register_writer(MyWriter) - - -def run_example(): - # Create a new stage and disable capture on play - omni.usd.get_context().new_stage() - rep.orchestrator.set_capture_on_play(False) - - # Setup stage - stage = omni.usd.get_context().get_stage() - dome_light = stage.DefinePrim("/World/DomeLight", "DomeLight") - dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(500.0) - cube = stage.DefinePrim("/World/Cube", "Cube") - add_update_semantics(cube, "MyCube") - - # Capture from two perspectives, a custom camera and the viewport perspective camera - camera = stage.DefinePrim("/World/Camera", "Camera") - UsdGeom.Xformable(camera).AddTranslateOp().Set((0, 0, 20)) - - # Create the render products - rp_cam = rep.create.render_product(camera.GetPath(), (400, 400), name="camera_view") - rp_persp = rep.create.render_product("/OmniverseKit_Persp", (512, 512), name="perspective_view") - - # Use the annotators to access the data directly, each annotator is attached to a render product - rgb_annotator_cam = rep.annotators.get("rgb") - rgb_annotator_cam.attach(rp_cam) - rgb_annotator_persp = rep.annotators.get("rgb") - rgb_annotator_persp.attach(rp_persp) - - # Use the custom writer to access the annotator data - custom_writer = rep.writers.get("MyWriter") - custom_writer.initialize(camera_params=True, bounding_box_3d=True) - custom_writer.attach([rp_cam, rp_persp]) - - # Use the pose writer to write the data to disk - pose_writer = rep.WriterRegistry.get("PoseWriter") - out_dir = os.getcwd() + "/_out_pose_writer" - print(f"Output directory: {out_dir}") - pose_writer.initialize(output_dir=out_dir, write_debug_images=True) - pose_writer.attach([rp_cam, rp_persp]) - - # Trigger a data capture request (data will be written to disk by the writer) - for i in range(3): - print(f"Step {i}") - rep.orchestrator.step() - - # Get the data from the annotators - rgb_data_cam = rgb_annotator_cam.get_data() - rgb_data_persp = rgb_annotator_persp.get_data() - print(f"[Annotator][Cam][{i}] rgb_data_cam shape: {rgb_data_cam.shape}") - print(f"[Annotator][Persp][{i}] rgb_data_persp shape: {rgb_data_persp.shape}") - - # Detach the render products from the annotators and writers and clear them to release resources - pose_writer.detach() - custom_writer.detach() - rgb_annotator_cam.detach() - rgb_annotator_persp.detach() - rp_cam.destroy() - rp_persp.destroy() - - # Wait for the data to be written to disk - rep.orchestrator.wait_until_complete() - - -run_example() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_03.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_03.py deleted file mode 100644 index 02b775c56..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_03.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import os -import random - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import omni.replicator.core as rep -import omni.usd -from isaacsim.core.utils.semantics import add_update_semantics -from pxr import UsdGeom - - -# Custom randomizer function using USD API -def randomize_location(prim): - if not prim.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(prim).AddTranslateOp() - translate = prim.GetAttribute("xformOp:translate") - translate.Set((random.uniform(-1, 1), random.uniform(-1, 1), random.uniform(-1, 1))) - - -def run_example(): - # Create a new stage and disable capture on play - omni.usd.get_context().new_stage() - rep.orchestrator.set_capture_on_play(False) - random.seed(42) - rep.set_global_seed(42) - - # Setup stage - stage = omni.usd.get_context().get_stage() - cube = stage.DefinePrim("/World/Cube", "Cube") - add_update_semantics(cube, "MyCube") - - # Create a replicator randomizer with custom event trigger - with rep.trigger.on_custom_event(event_name="randomize_dome_light_color"): - rep.create.light(light_type="Dome", color=rep.distribution.uniform((0, 0, 0), (1, 1, 1))) - - # Create a render product using the viewport perspective camera - rp = rep.create.render_product("/OmniverseKit_Persp", (512, 512)) - - # Write data using the basic writer with the rgb and bounding box annotators - writer = rep.writers.get("BasicWriter") - out_dir = os.getcwd() + "/_out_basic_writer_rand" - print(f"Output directory: {out_dir}") - writer.initialize(output_dir=out_dir, rgb=True, semantic_segmentation=True, colorize_semantic_segmentation=True) - writer.attach(rp) - - # Trigger a data capture request (data will be written to disk by the writer) - for i in range(3): - print(f"Step {i}") - # Trigger the custom event randomizer every other step - if i % 2 == 1: - rep.utils.send_og_event(event_name="randomize_dome_light_color") - - # Run the custom USD API location randomizer on the prims - randomize_location(cube) - - # Since the replicator randomizer is set to trigger on custom events, step will only trigger the writer - rep.orchestrator.step() - - # Destroy the render product to release resources by detaching it from the writer first - writer.detach() - rp.destroy() - - # Wait for the data to be written to disk - rep.orchestrator.wait_until_complete() - - -# Run the example -run_example() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_04.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_04.py deleted file mode 100644 index e7dd5a2b7..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/sdg_getting_started_04.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import os - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import omni.replicator.core as rep -import omni.timeline -import omni.usd -from isaacsim.core.utils.semantics import add_update_semantics -from pxr import Sdf, UsdGeom, UsdPhysics - - -def add_colliders_and_rigid_body_dynamics(prim): - # Add colliders - if not prim.HasAPI(UsdPhysics.CollisionAPI): - collision_api = UsdPhysics.CollisionAPI.Apply(prim) - else: - collision_api = UsdPhysics.CollisionAPI(prim) - collision_api.CreateCollisionEnabledAttr(True) - # Add rigid body dynamics - if not prim.HasAPI(UsdPhysics.RigidBodyAPI): - rigid_body_api = UsdPhysics.RigidBodyAPI.Apply(prim) - else: - rigid_body_api = UsdPhysics.RigidBodyAPI(prim) - rigid_body_api.CreateRigidBodyEnabledAttr(True) - - -def run_example(): - # Create a new stage and disable capture on play - omni.usd.get_context().new_stage() - rep.orchestrator.set_capture_on_play(False) - - # Add a light - stage = omni.usd.get_context().get_stage() - dome_light = stage.DefinePrim("/World/DomeLight", "DomeLight") - dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(500.0) - - # Create a cube with colliders and rigid body dynamics at a specific location - cube = stage.DefinePrim("/World/Cube", "Cube") - add_colliders_and_rigid_body_dynamics(cube) - if not cube.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(cube).AddTranslateOp() - cube.GetAttribute("xformOp:translate").Set((0, 0, 2)) - add_update_semantics(cube, "MyCube") - - # Createa a sphere with colliders and rigid body dynamics next to the cube - sphere = stage.DefinePrim("/World/Sphere", "Sphere") - add_colliders_and_rigid_body_dynamics(sphere) - if not sphere.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(sphere).AddTranslateOp() - sphere.GetAttribute("xformOp:translate").Set((-1, -1, 2)) - add_update_semantics(sphere, "MySphere") - - # Create a render product using the viewport perspective camera - rp = rep.create.render_product("/OmniverseKit_Persp", (512, 512)) - - # Write data using the basic writer with the rgb and bounding box annotators - writer = rep.writers.get("BasicWriter") - out_dir = os.getcwd() + "/_out_basic_writer_sim" - print(f"Output directory: {out_dir}") - writer.initialize(output_dir=out_dir, rgb=True, semantic_segmentation=True, colorize_semantic_segmentation=True) - writer.attach(rp) - - # Start the timeline (will only advance with app update) - timeline = omni.timeline.get_timeline_interface() - timeline.play() - - # Update the app and implicitly advance the simulation - drop_delta = 0.5 - last_capture_height = cube.GetAttribute("xformOp:translate").Get()[2] - for i in range(100): - # Get the current height of the cube and the distance it dropped since the last capture - simulation_app.update() - current_height = cube.GetAttribute("xformOp:translate").Get()[2] - drop_since_last_capture = last_capture_height - current_height - print(f"Step {i}; cube height: {current_height:.3f}; drop since last capture: {drop_since_last_capture:.3f}") - - # Stop the simulation if the cube falls below the ground - if current_height < 0: - print(f"\t Cube fell below the ground at height {current_height:.3f}, stopping simulation..") - timeline.pause() - break - - # Capture every time the cube drops by the threshold distance - if drop_since_last_capture >= drop_delta: - print(f"\t Capturing at height {current_height:.3f}") - last_capture_height = current_height - # Pause the timeline to capture multiple frames of the same simulation state - timeline.pause() - - # Setting delta_time to 0.0 will make sure the step function will not advance the simulation during capture - rep.orchestrator.step(delta_time=0.0) - - # Capture again with the cube hidden - UsdGeom.Imageable(cube).MakeInvisible() - rep.orchestrator.step(delta_time=0.0) - UsdGeom.Imageable(cube).MakeVisible() - - # Resume the timeline to continue the simulation - timeline.play() - - # Destroy the render product to release resources by detaching it from the writer first - writer.detach() - rp.destroy() - - # Wait for the data to be written to disk - rep.orchestrator.wait_until_complete() - - -# Run the example -run_example() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/simulation_get_data.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/simulation_get_data.py deleted file mode 100644 index 3a171ab6d..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/simulation_get_data.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"renderer": "RaytracedLighting", "headless": False}) - -import json -import os - -import carb.settings -import numpy as np -import omni -import omni.replicator.core as rep -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.utils.semantics import add_update_semantics -from PIL import Image - - -# Util function to save rgb annotator data -def write_rgb_data(rgb_data, file_path): - rgb_img = Image.fromarray(rgb_data, "RGBA") - rgb_img.save(file_path + ".png") - - -# Util function to save semantic segmentation annotator data -def write_sem_data(sem_data, file_path): - id_to_labels = sem_data["info"]["idToLabels"] - with open(file_path + ".json", "w") as f: - json.dump(id_to_labels, f) - sem_image_data = np.frombuffer(sem_data["data"], dtype=np.uint8).reshape(*sem_data["data"].shape, -1) - sem_img = Image.fromarray(sem_image_data, "RGBA") - sem_img.save(file_path + ".png") - - -# Create a new stage with the default ground plane -omni.usd.get_context().new_stage() - -# Setup the simulation world -world = World() -world.scene.add_default_ground_plane() -world.reset() - -# Setting capture on play to False will prevent the replicator from capturing data each frame -carb.settings.get_settings().set("/omni/replicator/captureOnPlay", False) - -# Create a camera and render product to collect the data from -cam = rep.create.camera(position=(5, 5, 5), look_at=(0, 0, 0)) -rp = rep.create.render_product(cam, (512, 512)) - -# Set the output directory for the data -out_dir = os.getcwd() + "/_out_sim_event" -os.makedirs(out_dir, exist_ok=True) -print(f"Outputting data to {out_dir}..") - -# Example of using a writer to save the data -writer = rep.WriterRegistry.get("BasicWriter") -writer.initialize( - output_dir=f"{out_dir}/writer", rgb=True, semantic_segmentation=True, colorize_semantic_segmentation=True -) -writer.attach(rp) - -# Run a preview to ensure the replicator graph is initialized -rep.orchestrator.preview() - -# Example of accesing the data directly from annotators -rgb_annot = rep.AnnotatorRegistry.get_annotator("rgb") -rgb_annot.attach(rp) -sem_annot = rep.AnnotatorRegistry.get_annotator("semantic_segmentation", init_params={"colorize": True}) -sem_annot.attach(rp) - -# Spawn and drop a few cubes, capture data when they stop moving -for i in range(5): - cuboid = world.scene.add(DynamicCuboid(prim_path=f"/World/Cuboid_{i}", name=f"Cuboid_{i}", position=(0, 0, 10 + i))) - add_update_semantics(cuboid.prim, "Cuboid") - - for s in range(500): - world.step(render=False) - vel = np.linalg.norm(cuboid.get_linear_velocity()) - if vel < 0.1: - print(f"Cube_{i} stopped moving after {s} simulation steps, writing data..") - # Tigger the writer and update the annotators with new data - rep.orchestrator.step(rt_subframes=4, delta_time=0.0, pause_timeline=False) - write_rgb_data(rgb_annot.get_data(), f"{out_dir}/Cube_{i}_step_{s}_rgb") - write_sem_data(sem_annot.get_data(), f"{out_dir}/Cube_{i}_step_{s}_sem") - break - -rep.orchestrator.wait_until_complete() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/subscribers_and_events.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/subscribers_and_events.py deleted file mode 100644 index dd86b9125..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.replicator.examples/subscribers_and_events.py +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import asyncio -import time - -import carb.events -import carb.settings -import omni.kit.app -import omni.physx -import omni.timeline -import omni.usd -from pxr import PhysxSchema, UsdPhysics - -# TIMELINE / STAGE -USE_CUSTOM_TIMELINE_SETTINGS = False -USE_FIXED_TIME_STEPPING = False -PLAY_EVERY_FRAME = True -PLAY_DELAY_COMPENSATION = 0.0 -SUBSAMPLE_RATE = 1 -STAGE_FPS = 30.0 - -# PHYSX -USE_CUSTOM_PHYSX_FPS = False -PHYSX_FPS = 60.0 -MIN_SIM_FPS = 30 - -# Simulations can also be enabled/disabled at runtime -DISABLE_SIMULATIONS = False - -# APP / RENDER -LIMIT_APP_FPS = False -APP_FPS = 120 - -# Duration after which to clear subscribers and print the cached events -MAX_DURATION = 3.0 -PRINT_EVENTS = False - - -def on_timeline_event(event: omni.timeline.TimelineEventType): - global timeline_sub - global timeline_events - global wall_start_time - elapsed_wall_time = time.time() - wall_start_time - - # Cache only time advance events - if event.type == omni.timeline.TimelineEventType.CURRENT_TIME_TICKED.value: - event_name = omni.timeline.TimelineEventType(event.type).name - event_payload = event.payload - timeline_events.append((elapsed_wall_time, event_name, event_payload)) - - # Clear subscriber and print cached events - if elapsed_wall_time > MAX_DURATION: - if timeline_sub is not None: - timeline_sub.unsubscribe() - timeline_sub = None - num_events = len(timeline_events) - fps = num_events / MAX_DURATION - print(f"[timeline] captured {num_events} events with aprox {fps} FPS") - if PRINT_EVENTS: - for i, (wall_time, event_name, payload) in enumerate(timeline_events): - print(f"\t[timeline][{i}]\ttime={wall_time:.4f};\tevent={event_name};\tpayload={payload}") - - -def on_physics_step(dt: float): - global physx_events - global wall_start_time - elapsed_wall_time = time.time() - wall_start_time - - # Cache physics events - physx_events.append((elapsed_wall_time, dt)) - - # Clear subscriber and print cached events - if elapsed_wall_time > MAX_DURATION: - # Physics unsubscription needs to be defered from the callback function - # see: '[Error] [omni.physx.plugin] Subscription cannot be changed during the event call' - async def clear_physx_sub_async(): - global physx_sub - if physx_sub is not None: - physx_sub.unsubscribe() - physx_sub = None - - asyncio.ensure_future(clear_physx_sub_async()) - num_events = len(physx_events) - fps = num_events / MAX_DURATION - print(f"[physics] captured {num_events} events with aprox {fps} FPS") - if PRINT_EVENTS: - for i, (wall_time, dt) in enumerate(physx_events): - print(f"\t[physics][{i}]\ttime={wall_time:.4f};\tdt={dt};") - - -def on_stage_render_event(event: omni.usd.StageRenderingEventType): - global stage_render_sub - global stage_render_events - global wall_start_time - elapsed_wall_time = time.time() - wall_start_time - - event_name = omni.usd.StageRenderingEventType(event.type).name - event_payload = event.payload - stage_render_events.append((elapsed_wall_time, event_name, event_payload)) - - if elapsed_wall_time > MAX_DURATION: - if stage_render_sub is not None: - stage_render_sub.unsubscribe() - stage_render_sub = None - num_events = len(stage_render_events) - fps = num_events / MAX_DURATION - print(f"[stage render] captured {num_events} events with aprox {fps} FPS") - if PRINT_EVENTS: - for i, (wall_time, event_name, payload) in enumerate(stage_render_events): - print(f"\t[stage render][{i}]\ttime={wall_time:.4f};\tevent={event_name};\tpayload={payload}") - - -def on_app_update(event: carb.events.IEvent): - global app_sub - global app_update_events - global wall_start_time - elapsed_wall_time = time.time() - wall_start_time - - event_type = event.type - event_payload = event.payload - app_update_events.append((elapsed_wall_time, event_type, event_payload)) - - if elapsed_wall_time > MAX_DURATION: - if app_sub is not None: - app_sub.unsubscribe() - app_sub = None - num_events = len(app_update_events) - fps = num_events / MAX_DURATION - print(f"[app] captured {num_events} events with aprox {fps} FPS") - if PRINT_EVENTS: - for i, (wall_time, event_type, payload) in enumerate(app_update_events): - print(f"\t[app][{i}]\ttime={wall_time:.4f};\tevent={event_type};\tpayload={payload}") - - -stage = omni.usd.get_context().get_stage() -timeline = omni.timeline.get_timeline_interface() - - -if USE_CUSTOM_TIMELINE_SETTINGS: - # Ideal to make simulation and animation synchronized. - # Default: True in editor, False in standalone. - # NOTE: - # - It may limit the frame rate (see 'timeline.set_play_every_frame') such that the elapsed wall clock time matches the frame's delta time. - # - If the app runs slower than this, animation playback may slow down (see 'CompensatePlayDelayInSecs'). - # - For performance benchmarks, turn this off or set a very high target in `timeline.set_target_framerate` - carb.settings.get_settings().set("/app/player/useFixedTimeStepping", USE_FIXED_TIME_STEPPING) - - # This compensates for frames that require more computation time than the frame's fixed delta time, by temporarily speeding up playback. - # The parameter represents the length of these "faster" playback periods, which means that it must be larger than the fixed frame time to take effect. - # Default: 0.0 - # NOTE: - # - only effective if `useFixedTimeStepping` is set to True - # - setting a large value results in long fast playback after a huge lag spike - carb.settings.get_settings().set("/app/player/CompensatePlayDelayInSecs", PLAY_DELAY_COMPENSATION) - - # If set to True, no frames are skipped and in every frame time advances by `1 / TimeCodesPerSecond`. - # Default: False - # NOTE: - # - only effective if `useFixedTimeStepping` is set to True - # - simulation is usually faster than real-time and processing is only limited by the frame rate of the runloop - # - useful for recording - # - same as `carb.settings.get_settings().set("/app/player/useFastMode", PLAY_EVERY_FRAME)` - timeline.set_play_every_frame(PLAY_EVERY_FRAME) - - # Timeline sub-stepping, i.e. how many times updates are called (update events are dispatched) each frame. - # Default: 1 - # NOTE: same as `carb.settings.get_settings().set("/app/player/timelineSubsampleRate", SUBSAMPLE_RATE)` - timeline.set_ticks_per_frame(SUBSAMPLE_RATE) - - # Time codes per second for the stage - # NOTE: same as `stage.SetTimeCodesPerSecond(STAGE_FPS)` and `carb.settings.get_settings().set("/app/stage/timeCodesPerSecond", STAGE_FPS)` - timeline.set_time_codes_per_second(STAGE_FPS) - - -# Create a PhysX scene to set the physics time step -if USE_CUSTOM_PHYSX_FPS: - physx_scene = None - for prim in stage.Traverse(): - if prim.IsA(UsdPhysics.Scene): - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(prim) - break - if physx_scene is None: - physics_scene = UsdPhysics.Scene.Define(stage, "/PhysicsScene") - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(stage.GetPrimAtPath("/PhysicsScene")) - - # Time step for the physics simulation - # Default: 60.0 - physx_scene.GetTimeStepsPerSecondAttr().Set(PHYSX_FPS) - - # Minimum simulation frequency to prevent clamping; if the frame rate drops below this, - # physics steps are discarded to avoid app slowdown if the overall frame rate is too low. - # Default: 30.0 - # NOTE: Matching `minFrameRate` with `TimeStepsPerSecond` ensures a single physics step per update. - carb.settings.get_settings().set("/persistent/simulation/minFrameRate", MIN_SIM_FPS) - - -# Throttle Render/UI/Main thread update rate -if LIMIT_APP_FPS: - # Enable rate limiting of the main run loop (UI, rendering, etc.) - # Default: False - carb.settings.get_settings().set("/app/runLoops/main/rateLimitEnabled", LIMIT_APP_FPS) - - # FPS limit of the main run loop (UI, rendering, etc.) - # Default: 120 - # NOTE: disabled if `/app/player/useFixedTimeStepping` is False - carb.settings.get_settings().set("/app/runLoops/main/rateLimitFrequency", int(APP_FPS)) - - -# Simulations can be selectively disabled (or toggled at specific times) -if DISABLE_SIMULATIONS: - carb.settings.get_settings().set("/app/player/playSimulations", False) - - -# Start the timeline -timeline.set_current_time(0) -timeline.set_end_time(MAX_DURATION + 1) -timeline.set_looping(False) -timeline.play() -timeline.commit() -wall_start_time = time.time() - -# Subscribe and cache various events for a limited duration -timeline_events = [] -timeline_sub = timeline.get_timeline_event_stream().create_subscription_to_pop(on_timeline_event) -physx_events = [] -physx_sub = omni.physx.get_physx_interface().subscribe_physics_step_events(on_physics_step) -stage_render_events = [] -stage_render_sub = omni.usd.get_context().get_rendering_event_stream().create_subscription_to_pop(on_stage_render_event) -app_update_events = [] -app_sub = omni.kit.app.get_app().get_update_event_stream().create_subscription_to_pop(on_app_update) - -# Keep the simulation running until the duration is passed -while simulation_app.is_running(): - if time.time() - wall_start_time > MAX_DURATION + 0.1: - break - simulation_app.update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/controllers/pick_place.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/controllers/pick_place.py deleted file mode 100644 index e8786430e..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/controllers/pick_place.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import isaacsim.robot.manipulators.controllers as manipulators_controllers -from isaacsim.core.prims import SingleArticulation -from isaacsim.robot.manipulators.grippers import ParallelGripper - -from .rmpflow import RMPFlowController - - -class PickPlaceController(manipulators_controllers.PickPlaceController): - def __init__( - self, name: str, gripper: ParallelGripper, robot_articulation: SingleArticulation, events_dt=None - ) -> None: - if events_dt is None: - events_dt = [0.005, 0.002, 1, 0.05, 0.0008, 0.005, 0.0008, 0.1, 0.0008, 0.008] - manipulators_controllers.PickPlaceController.__init__( - self, - name=name, - cspace_controller=RMPFlowController( - name=name + "_cspace_controller", robot_articulation=robot_articulation - ), - gripper=gripper, - events_dt=events_dt, - end_effector_initial_height=0.6, - ) - return diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/controllers/rmpflow.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/controllers/rmpflow.py deleted file mode 100644 index b9efd8565..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/controllers/rmpflow.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import os - -import isaacsim.robot_motion.motion_generation as mg -from isaacsim.core.prims import SingleArticulation -from isaacsim.core.utils.extensions import get_extension_path_from_name - - -class RMPFlowController(mg.MotionPolicyController): - def __init__(self, name: str, robot_articulation: SingleArticulation, physics_dt: float = 1.0 / 60.0) -> None: - self.rmpflow = mg.lula.motion_policies.RmpFlow( - robot_description_path=os.path.join(os.path.dirname(__file__), "../rmpflow/robot_descriptor.yaml"), - rmpflow_config_path=os.path.join(os.path.dirname(__file__), "../rmpflow/denso_rmpflow_common.yaml"), - urdf_path=os.path.join(os.path.dirname(__file__), "../rmpflow/cobotta_pro_900.urdf"), - end_effector_frame_name="gripper_center", - maximum_substep_size=0.00334, - ) - - self.articulation_rmp = mg.ArticulationMotionPolicy(robot_articulation, self.rmpflow, physics_dt) - - mg.MotionPolicyController.__init__(self, name=name, articulation_motion_policy=self.articulation_rmp) - ( - self._default_position, - self._default_orientation, - ) = self._articulation_motion_policy._robot_articulation.get_world_pose() - self._motion_policy.set_robot_base_pose( - robot_position=self._default_position, robot_orientation=self._default_orientation - ) - return - - def reset(self): - mg.MotionPolicyController.reset(self) - self._motion_policy.set_robot_base_pose( - robot_position=self._default_position, robot_orientation=self._default_orientation - ) diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/follow_target_example.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/follow_target_example.py deleted file mode 100644 index 9dc7ad733..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/follow_target_example.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse - -import numpy as np -from controllers.rmpflow import RMPFlowController -from isaacsim.core.api import World -from tasks.follow_target import FollowTarget - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -my_world = World(stage_units_in_meters=1.0) -# Initialize the Follow Target task with a target location for the cube to be followed by the end effector -my_task = FollowTarget(name="denso_follow_target", target_position=np.array([0.5, 0, 0.5])) -my_world.add_task(my_task) -my_world.reset() -task_params = my_world.get_task("denso_follow_target").get_params() -target_name = task_params["target_name"]["value"] -denso_name = task_params["robot_name"]["value"] -my_denso = my_world.scene.get_object(denso_name) - -# initialize the controller -my_controller = RMPFlowController(name="target_follower_controller", robot_articulation=my_denso) - -# make RmpFlow aware of the ground plane -ground_plane = my_world.scene.get_object(name="default_ground_plane") -my_controller.add_obstacle(ground_plane) - -articulation_controller = my_denso.get_articulation_controller() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - target_end_effector_position=observations[target_name]["position"], - target_end_effector_orientation=observations[target_name]["orientation"], - ) - articulation_controller.apply_action(actions) - if args.test is True: - break -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/gripper_control.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/gripper_control.py deleted file mode 100644 index 64975c9ce..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/gripper_control.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse - -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.robot.manipulators import SingleManipulator -from isaacsim.robot.manipulators.grippers import ParallelGripper -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -my_world = World(stage_units_in_meters=1.0) -assets_root_path = get_assets_root_path() -if assets_root_path is None: - raise Exception("Could not find Isaac Sim assets folder") -asset_path = assets_root_path + "/Isaac/Robots/Denso/cobotta_pro_900.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/cobotta") -# define the gripper -gripper = ParallelGripper( - # We chose the following values while inspecting the articulation - end_effector_prim_path="/World/cobotta/onrobot_rg6_base_link", - joint_prim_names=["finger_joint", "right_outer_knuckle_joint"], - joint_opened_positions=np.array([0, 0]), - joint_closed_positions=np.array([0.628, -0.628]), - action_deltas=np.array([-0.628, 0.628]), -) -# define the manipulator -my_denso = my_world.scene.add( - SingleManipulator( - prim_path="/World/cobotta", - name="cobotta_robot", - end_effector_prim_name="onrobot_rg6_base_link", - gripper=gripper, - ) -) -# set the default positions of the other gripper joints to be opened so -# that its out of the way of the joints we want to control when gripping an object for instance. -joints_default_positions = np.zeros(12) -joints_default_positions[7] = 0.628 -joints_default_positions[8] = 0.628 -my_denso.set_joints_default_state(positions=joints_default_positions) -my_world.scene.add_default_ground_plane() -my_world.reset() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - reset_needed = False - i += 1 - gripper_positions = my_denso.gripper.get_joint_positions() - if i < 500: - # close the gripper slowly - my_denso.gripper.apply_action( - ArticulationAction(joint_positions=[gripper_positions[0] + 0.1, gripper_positions[1] - 0.1]) - ) - if i > 500: - # open the gripper slowly - my_denso.gripper.apply_action( - ArticulationAction(joint_positions=[gripper_positions[0] - 0.1, gripper_positions[1] + 0.1]) - ) - if i == 1000: - i = 0 - if args.test is True: - break - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/pick_up_example.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/pick_up_example.py deleted file mode 100644 index f4b2d9443..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/pick_up_example.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse - -import numpy as np -from controllers.pick_place import PickPlaceController -from isaacsim.core.api import World -from tasks.pick_place import PickPlace - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -my_world = World(stage_units_in_meters=1.0) - - -target_position = np.array([-0.3, 0.6, 0]) -target_position[2] = 0.0515 / 2.0 -my_task = PickPlace(name="denso_pick_place", target_position=target_position) - -my_world.add_task(my_task) -my_world.reset() -my_denso = my_world.scene.get_object("cobotta_robot") -# initialize the controller -my_controller = PickPlaceController(name="controller", robot_articulation=my_denso, gripper=my_denso.gripper) -task_params = my_world.get_task("denso_pick_place").get_params() -articulation_controller = my_denso.get_articulation_controller() -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - # forward the observation values to the controller to get the actions - actions = my_controller.forward( - picking_position=observations[task_params["cube_name"]["value"]]["position"], - placing_position=observations[task_params["cube_name"]["value"]]["target_position"], - current_joint_positions=observations[task_params["robot_name"]["value"]]["joint_positions"], - end_effector_offset=np.array([0, 0, 0]), - ) - if my_controller.is_done(): - print("done picking and placing") - articulation_controller.apply_action(actions) - if args.test is True: - break -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/cobotta_pro_900.urdf b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/cobotta_pro_900.urdf deleted file mode 100644 index 267a6eda3..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/cobotta_pro_900.urdf +++ /dev/null @@ -1,475 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - transmission_interface/SimpleTransmission - - PositionJointInterface - - - 1 - - - - - - - - - - - - - - - diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/denso_rmpflow_common.yaml b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/denso_rmpflow_common.yaml deleted file mode 100644 index f26059b50..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/denso_rmpflow_common.yaml +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. - -# Artificially limit the robot joints. For example: -# A joint with range +-pi would be limited to +-(pi-.01) -joint_limit_buffers: [.01, .01, .01, .01, .01, .01] - -# RMPflow has many modifiable parameters, but these serve as a great start. -# Most parameters will not need to be modified -rmp_params: - cspace_target_rmp: - metric_scalar: 50. - position_gain: 100. - damping_gain: 50. - robust_position_term_thresh: .5 - inertia: 1. - cspace_trajectory_rmp: - p_gain: 100. - d_gain: 10. - ff_gain: .25 - weight: 50. - cspace_affine_rmp: - final_handover_time_std_dev: .25 - weight: 2000. - joint_limit_rmp: - metric_scalar: 1000. - metric_length_scale: .01 - metric_exploder_eps: 1e-3 - metric_velocity_gate_length_scale: .01 - accel_damper_gain: 200. - accel_potential_gain: 1. - accel_potential_exploder_length_scale: .1 - accel_potential_exploder_eps: 1e-2 - joint_velocity_cap_rmp: - max_velocity: 1. - velocity_damping_region: .3 - damping_gain: 1000.0 - metric_weight: 100. - target_rmp: - accel_p_gain: 30. - accel_d_gain: 85. - accel_norm_eps: .075 - metric_alpha_length_scale: .05 - min_metric_alpha: .01 - max_metric_scalar: 10000 - min_metric_scalar: 2500 - proximity_metric_boost_scalar: 20. - proximity_metric_boost_length_scale: .02 - xi_estimator_gate_std_dev: 20000. - accept_user_weights: false - axis_target_rmp: - accel_p_gain: 210. - accel_d_gain: 60. - metric_scalar: 10 - proximity_metric_boost_scalar: 3000. - proximity_metric_boost_length_scale: .08 - xi_estimator_gate_std_dev: 20000. - accept_user_weights: false - collision_rmp: - damping_gain: 50. - damping_std_dev: .04 - damping_robustness_eps: 1e-2 - damping_velocity_gate_length_scale: .01 - repulsion_gain: 800. - repulsion_std_dev: .01 - metric_modulation_radius: .5 - metric_scalar: 10000. - metric_exploder_std_dev: .02 - metric_exploder_eps: .001 - damping_rmp: - accel_d_gain: 30. - metric_scalar: 50. - inertia: 100. - -canonical_resolve: - max_acceleration_norm: 50. - projection_tolerance: .01 - verbose: false - - -# body_cylinders are used to promote self-collision avoidance between the robot and its base -# The example below defines the robot base to be a capsule defined by the absolute coordinates pt1 and pt2. -# The semantic name provided for each body_cylinder does not need to be present in the robot URDF. -body_cylinders: - - name: base - pt1: [0,0,.12] - pt2: [0,0,0.] - radius: .08 - - name: second_link - pt1: [0,0,.12] - pt2: [0,0,.12] - radius: .16 - - -# body_collision_controllers defines spheres located at specified frames in the robot URDF -# These spheres will not be allowed to collide with the capsules enumerated under body_cylinders -# By design, most frames in industrial robots are kinematically unable to collide with the robot base. -# It is often only necessary to define body_collision_controllers near the end effector -body_collision_controllers: - - name: J5 - radius: .05 - - name: J6 - radius: .05 - - name: right_inner_finger - radius: .02 - - name: left_inner_finger - radius: .02 - - name: right_inner_knuckle - radius: .02 - - name: left_inner_knuckle - radius: .02 diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/robot_descriptor.yaml b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/robot_descriptor.yaml deleted file mode 100644 index 597662b76..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/rmpflow/robot_descriptor.yaml +++ /dev/null @@ -1,141 +0,0 @@ -# The robot description defines the generalized coordinates and how to map those -# to the underlying URDF dofs. - -api_version: 1.0 - -# Defines the generalized coordinates. Each generalized coordinate is assumed -# to have an entry in the URDF. -# Lula will only use these joints to control the robot position. -cspace: - - joint_1 - - joint_2 - - joint_3 - - joint_4 - - joint_5 - - joint_6 -default_q: [ - 0.0,0.3,1.2,0.0,0.0,0.0 -] - -# Most dimensions of the cspace have a direct corresponding element -# in the URDF. This list of rules defines how unspecified coordinates -# should be extracted or how values in the URDF should be overwritten. - -cspace_to_urdf_rules: - - {name: finger_joint, rule: fixed, value: 0.0} - - {name: left_inner_knuckle_joint, rule: fixed, value: 0.0} - - {name: right_inner_knuckle_joint, rule: fixed, value: 0.0} - - {name: right_outer_knuckle_joint, rule: fixed, value: 0.0} - - {name: left_inner_finger_joint, rule: fixed, value: 0.0} - - {name: right_inner_finger_joint, rule: fixed, value: 0.0} - -# Lula uses collision spheres to define the robot geometry in order to avoid -# collisions with external obstacles. If no spheres are specified, Lula will -# not be able to avoid obstacles. - -collision_spheres: - - J1: - - "center": [0.0, 0.0, 0.1] - "radius": 0.08 - - "center": [0.0, 0.0, 0.15] - "radius": 0.08 - - "center": [0.0, 0.0, 0.2] - "radius": 0.08 - - J2: - - "center": [0.0, 0.08, 0.0] - "radius": 0.08 - - "center": [0.0, 0.174, 0.0] - "radius": 0.08 - - "center": [-0.0, 0.186, 0.05] - "radius": 0.065 - - "center": [0.0, 0.175, 0.1] - "radius": 0.065 - - "center": [-0.0, 0.18, 0.15] - "radius": 0.065 - - "center": [0.0, 0.175, 0.2] - "radius": 0.065 - - "center": [0.0, 0.175, 0.25] - "radius": 0.065 - - "center": [0.0, 0.175, 0.3] - "radius": 0.065 - - "center": [0.0, 0.175, 0.35] - "radius": 0.065 - - "center": [0.0, 0.175, 0.4] - "radius": 0.065 - - "center": [0.0, 0.175, 0.45] - "radius": 0.065 - - "center": [0.0, 0.175, 0.5] - "radius": 0.065 - - "center": [-0.002, 0.1, 0.507] - "radius": 0.07 - - J3: - - "center": [0.0, 0.025, 0.0] - "radius": 0.065 - - "center": [0.0, -0.025, 0.0] - "radius": 0.065 - - "center": [0.0, -0.025, 0.05] - "radius": 0.065 - - "center": [0.0, -0.025, 0.1] - "radius": 0.065 - - "center": [0.0, -0.025, 0.15] - "radius": 0.06 - - "center": [0.0, -0.025, 0.2] - "radius": 0.06 - - "center": [0.0, -0.025, 0.25] - "radius": 0.06 - - "center": [0.0, -0.025, 0.3] - "radius": 0.06 - - "center": [0.0, -0.025, 0.35] - "radius": 0.055 - - "center": [0.0, -0.025, 0.4] - "radius": 0.055 - - J5: - - "center": [0.0, 0.05, 0.0] - "radius": 0.055 - - "center": [0.0, 0.1, 0.0] - "radius": 0.055 - - J6: - - "center": [0.0, 0.0, -0.05] - "radius": 0.05 - - "center": [0.0, 0.0, -0.1] - "radius": 0.05 - - "center": [0.0, 0.0, -0.15] - "radius": 0.05 - - "center": [0.0, 0.0, 0.04] - "radius": 0.035 - - "center": [0.0, 0.0, 0.08] - "radius": 0.035 - - "center": [0.0, 0.0, 0.12] - "radius": 0.035 - - right_inner_knuckle: - - "center": [0.0, 0.0, 0.0] - "radius": 0.02 - - "center": [0.0, -0.03, 0.025] - "radius": 0.02 - - "center": [0.0, -0.05, 0.05] - "radius": 0.02 - - right_inner_finger: - - "center": [0.0, 0.02, 0.0] - "radius": 0.015 - - "center": [0.0, 0.02, 0.015] - "radius": 0.015 - - "center": [0.0, 0.02, 0.03] - "radius": 0.015 - - "center": [0.0, 0.025, 0.04] - "radius": 0.01 - - left_inner_knuckle: - - "center": [0.0, 0.0, 0.0] - "radius": 0.02 - - "center": [0.0, -0.03, 0.025] - "radius": 0.02 - - "center": [0.0, -0.05, 0.05] - "radius": 0.02 - - left_inner_finger: - - "center": [0.0, 0.02, 0.0] - "radius": 0.015 - - "center": [0.0, 0.02, 0.015] - "radius": 0.015 - - "center": [0.0, 0.02, 0.03] - "radius": 0.015 - - "center": [0.0, 0.025, 0.04] - "radius": 0.01 diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/tasks/follow_target.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/tasks/follow_target.py deleted file mode 100644 index 86384aaf0..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/tasks/follow_target.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import os -from typing import Optional - -import isaacsim.core.api.tasks as tasks -import numpy as np -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.robot.manipulators import SingleManipulator -from isaacsim.robot.manipulators.grippers import ParallelGripper -from isaacsim.storage.native import get_assets_root_path - - -# Inheriting from the base class Follow Target -class FollowTarget(tasks.FollowTarget): - def __init__( - self, - name: str = "denso_follow_target", - target_prim_path: Optional[str] = None, - target_name: Optional[str] = None, - target_position: Optional[np.ndarray] = None, - target_orientation: Optional[np.ndarray] = None, - offset: Optional[np.ndarray] = None, - ) -> None: - tasks.FollowTarget.__init__( - self, - name=name, - target_prim_path=target_prim_path, - target_name=target_name, - target_position=target_position, - target_orientation=target_orientation, - offset=offset, - ) - return - - def set_robot(self) -> SingleManipulator: - assets_root_path = get_assets_root_path() - if assets_root_path is None: - raise Exception("Could not find Isaac Sim assets folder") - asset_path = assets_root_path + "/Isaac/Robots/Denso/cobotta_pro_900.usd" - add_reference_to_stage(usd_path=asset_path, prim_path="/World/cobotta") - gripper = ParallelGripper( - end_effector_prim_path="/World/cobotta/onrobot_rg6_base_link", - joint_prim_names=["finger_joint", "right_outer_knuckle_joint"], - joint_opened_positions=np.array([0, 0]), - joint_closed_positions=np.array([0.628, -0.628]), - action_deltas=np.array([-0.628, 0.628]), - ) - manipulator = SingleManipulator( - prim_path="/World/cobotta", - name="cobotta_robot", - end_effector_prim_name="onrobot_rg6_base_link", - gripper=gripper, - ) - joints_default_positions = np.zeros(12) - joints_default_positions[7] = 0.628 - joints_default_positions[8] = 0.628 - manipulator.set_joints_default_state(positions=joints_default_positions) - return manipulator diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/tasks/pick_place.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/tasks/pick_place.py deleted file mode 100644 index d41465150..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/cobotta_900/tasks/pick_place.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import os -from typing import Optional - -import isaacsim.core.api.tasks as tasks -import numpy as np -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.robot.manipulators import SingleManipulator -from isaacsim.robot.manipulators.grippers import ParallelGripper -from isaacsim.storage.native import get_assets_root_path - - -class PickPlace(tasks.PickPlace): - def __init__( - self, - name: str = "denso_pick_place", - cube_initial_position: Optional[np.ndarray] = None, - cube_initial_orientation: Optional[np.ndarray] = None, - target_position: Optional[np.ndarray] = None, - offset: Optional[np.ndarray] = None, - ) -> None: - tasks.PickPlace.__init__( - self, - name=name, - cube_initial_position=cube_initial_position, - cube_initial_orientation=cube_initial_orientation, - target_position=target_position, - cube_size=np.array([0.0515, 0.0515, 0.0515]), - offset=offset, - ) - return - - def set_robot(self) -> SingleManipulator: - assets_root_path = get_assets_root_path() - if assets_root_path is None: - raise Exception("Could not find Isaac Sim assets folder") - asset_path = assets_root_path + "/Isaac/Robots/Denso/cobotta_pro_900.usd" - add_reference_to_stage(usd_path=asset_path, prim_path="/World/cobotta") - gripper = ParallelGripper( - end_effector_prim_path="/World/cobotta/onrobot_rg6_base_link", - joint_prim_names=["finger_joint", "right_outer_knuckle_joint"], - joint_opened_positions=np.array([0, 0]), - joint_closed_positions=np.array([0.628, -0.628]), - action_deltas=np.array([-0.3, 0.3]), - ) - manipulator = SingleManipulator( - prim_path="/World/cobotta", - name="cobotta_robot", - end_effector_prim_name="onrobot_rg6_base_link", - gripper=gripper, - ) - joints_default_positions = np.zeros(12) - joints_default_positions[7] = 0.628 - joints_default_positions[8] = 0.628 - manipulator.set_joints_default_state(positions=joints_default_positions) - return manipulator diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/follow_target_with_ik.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/follow_target_with_ik.py deleted file mode 100644 index eca1d48e8..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/follow_target_with_ik.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.franka import KinematicsSolver -from isaacsim.robot.manipulators.examples.franka.controllers.rmpflow_controller import RMPFlowController -from isaacsim.robot.manipulators.examples.franka.tasks import FollowTarget - -my_world = World(stage_units_in_meters=1.0) -my_task = FollowTarget(name="follow_target_task") -my_world.add_task(my_task) -my_world.reset() -task_params = my_world.get_task("follow_target_task").get_params() -franka_name = task_params["robot_name"]["value"] -target_name = task_params["target_name"]["value"] -my_franka = my_world.scene.get_object(franka_name) -my_controller = KinematicsSolver(my_franka) -articulation_controller = my_franka.get_articulation_controller() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - reset_needed = False - observations = my_world.get_observations() - actions, succ = my_controller.compute_inverse_kinematics( - target_position=observations[target_name]["position"], - target_orientation=observations[target_name]["orientation"], - ) - if succ: - articulation_controller.apply_action(actions) - else: - carb.log_warn("IK did not converge to a solution. No action is being taken.") - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/follow_target_with_rmpflow.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/follow_target_with_rmpflow.py deleted file mode 100644 index e520ea8b1..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/follow_target_with_rmpflow.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.franka.controllers.rmpflow_controller import RMPFlowController -from isaacsim.robot.manipulators.examples.franka.tasks import FollowTarget - -my_world = World(stage_units_in_meters=1.0) -my_task = FollowTarget(name="follow_target_task") -my_world.add_task(my_task) -my_world.reset() -task_params = my_world.get_task("follow_target_task").get_params() -franka_name = task_params["robot_name"]["value"] -target_name = task_params["target_name"]["value"] -my_franka = my_world.scene.get_object(franka_name) -my_controller = RMPFlowController(name="target_follower_controller", robot_articulation=my_franka) -articulation_controller = my_franka.get_articulation_controller() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - target_end_effector_position=observations[target_name]["position"], - target_end_effector_orientation=observations[target_name]["orientation"], - ) - articulation_controller.apply_action(actions) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/franka_gripper.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/franka_gripper.py deleted file mode 100644 index 7b2876e33..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/franka_gripper.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse - -from isaacsim.core.api import World -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.robot.manipulators.examples.franka import Franka - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -my_world = World(stage_units_in_meters=1.0) -my_franka = my_world.scene.add(Franka(prim_path="/World/Franka", name="my_franka")) -my_world.scene.add_default_ground_plane() -my_world.reset() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - reset_needed = False - i += 1 - gripper_positions = my_franka.gripper.get_joint_positions() - if i < 500: - my_franka.gripper.apply_action( - ArticulationAction(joint_positions=[gripper_positions[0] - (0.005), gripper_positions[1] - (0.005)]) - ) - if i > 500: - my_franka.gripper.apply_action( - ArticulationAction(joint_positions=[gripper_positions[0] + (0.005), gripper_positions[1] + (0.005)]) - ) - if i == 1000: - i = 0 - if args.test is True: - break - - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/multiple_tasks.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/multiple_tasks.py deleted file mode 100644 index 9eb9e80aa..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/multiple_tasks.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.franka.controllers.pick_place_controller import PickPlaceController -from isaacsim.robot.manipulators.examples.franka.tasks import PickPlace - -my_world = World(stage_units_in_meters=1.0) -tasks = [] -num_of_tasks = 2 -for i in range(num_of_tasks): - tasks.append(PickPlace(name="task" + str(i), offset=np.array([0, (i * 2) - 3, 0]))) - my_world.add_task(tasks[-1]) -my_world.reset() -frankas = [] -cube_names = [] -for i in range(num_of_tasks): - task_params = tasks[i].get_params() - frankas.append(my_world.scene.get_object(task_params["robot_name"]["value"])) - cube_names.append(task_params["cube_name"]["value"]) - -controllers = [] -for i in range(num_of_tasks): - controllers.append( - PickPlaceController(name="pick_place_controller", gripper=frankas[i].gripper, robot_articulation=frankas[i]) - ) - controllers[-1].reset() - -articulation_controllers = [] -for i in range(num_of_tasks): - articulation_controllers.append(frankas[i].get_articulation_controller()) - -my_world.pause() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - for i in range(num_of_tasks): - controllers[i].reset() - reset_needed = False - observations = my_world.get_observations() - for i in range(num_of_tasks): - articulation_controllers.append(frankas[i].get_articulation_controller()) - actions = controllers[i].forward( - picking_position=observations[cube_names[i]]["position"], - placing_position=observations[cube_names[i]]["target_position"], - current_joint_positions=observations[frankas[i].name]["joint_positions"], - end_effector_offset=np.array([0, 0, 0]), - ) - articulation_controllers[i].apply_action(actions) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/pick_place.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/pick_place.py deleted file mode 100644 index 38836c2f5..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/pick_place.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.franka.controllers.pick_place_controller import PickPlaceController -from isaacsim.robot.manipulators.examples.franka.tasks import PickPlace - -my_world = World(stage_units_in_meters=1.0) -my_task = PickPlace() -my_world.add_task(my_task) -my_world.reset() -task_params = my_task.get_params() -my_franka = my_world.scene.get_object(task_params["robot_name"]["value"]) -my_controller = PickPlaceController( - name="pick_place_controller", gripper=my_franka.gripper, robot_articulation=my_franka -) -articulation_controller = my_franka.get_articulation_controller() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - picking_position=observations[task_params["cube_name"]["value"]]["position"], - placing_position=observations[task_params["cube_name"]["value"]]["target_position"], - current_joint_positions=observations[task_params["robot_name"]["value"]]["joint_positions"], - end_effector_offset=np.array([0, 0.005, 0]), - ) - if my_controller.is_done(): - print("done picking and placing") - articulation_controller.apply_action(actions) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/stacking.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/stacking.py deleted file mode 100644 index 9f14d3d56..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka/stacking.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.franka.controllers.stacking_controller import StackingController -from isaacsim.robot.manipulators.examples.franka.tasks import Stacking - -my_world = World(stage_units_in_meters=1.0) -my_task = Stacking() -my_world.add_task(my_task) -my_world.reset() -robot_name = my_task.get_params()["robot_name"]["value"] -my_franka = my_world.scene.get_object(robot_name) -my_controller = StackingController( - name="stacking_controller", - gripper=my_franka.gripper, - robot_articulation=my_franka, - picking_order_cube_names=my_task.get_cube_names(), - robot_observation_name=robot_name, -) -articulation_controller = my_franka.get_articulation_controller() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward(observations=observations) - articulation_controller.apply_action(actions) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka_pick_up.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka_pick_up.py deleted file mode 100644 index 87f5f7d4a..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/franka_pick_up.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) 2022-2023, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.robot.manipulators import SingleManipulator -from isaacsim.robot.manipulators.examples.franka.controllers.pick_place_controller import PickPlaceController -from isaacsim.robot.manipulators.grippers import ParallelGripper -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() - -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka") -gripper = ParallelGripper( - end_effector_prim_path="/World/Franka/panda_rightfinger", - joint_prim_names=["panda_finger_joint1", "panda_finger_joint2"], - joint_opened_positions=np.array([0.05, 0.05]), - joint_closed_positions=np.array([0.02, 0.02]), - action_deltas=np.array([0.01, 0.01]), -) -my_franka = my_world.scene.add( - SingleManipulator( - prim_path="/World/Franka", name="my_franka", end_effector_prim_name="panda_rightfinger", gripper=gripper - ) -) -cube = my_world.scene.add( - DynamicCuboid( - name="cube", - position=np.array([0.3, 0.3, 0.3]), - prim_path="/World/Cube", - scale=np.array([0.0515, 0.0515, 0.0515]), - size=1.0, - color=np.array([0, 0, 1]), - ) -) -my_world.scene.add_default_ground_plane() -my_franka.gripper.set_default_state(my_franka.gripper.joint_opened_positions) -my_world.reset() - -my_controller = PickPlaceController( - name="pick_place_controller", gripper=my_franka.gripper, robot_articulation=my_franka -) -articulation_controller = my_franka.get_articulation_controller() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - picking_position=cube.get_local_pose()[0], - placing_position=np.array([-0.3, -0.3, 0.0515 / 2.0]), - current_joint_positions=my_franka.get_joint_positions(), - end_effector_offset=np.array([0, 0.005, 0]), - ) - if my_controller.is_done(): - print("done picking and placing") - articulation_controller.apply_action(actions) - if args.test is True: - break - - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/rmpflow_supported_robots/README.md b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/rmpflow_supported_robots/README.md deleted file mode 100644 index ffd0b331d..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/rmpflow_supported_robots/README.md +++ /dev/null @@ -1,51 +0,0 @@ -This standalone example provides a generic script for running a follow-target example on any supported robot that uses RMPflow to reach a target while avoiding obstacles. The purpose of this script is to show only how to use RMPflow, and for the sake of simplicity, it does not use the task/controller paradigm that is typical in other Isaac Sim examples. - -The ./supported_robot_follow_target_example.py script takes in runtime with the path to the robot USD asset (which is assumed to be stored on the Nucleus Server) and the name of the robot. By running the script with the default command line arguments, the list of supported robot names will be printed in the terminal. - -###### Command Line Arguments - -The supported command-line arguments are as follows: - - -v,--verbose: If True, prints out useful runtime information such as the list of supported robot names that map to RMPflow config files. Defaults to 'True' - - --robot-name: Name of robot that maps to the stored RMPflow config. Defaults to "Cobotta_Pro_900" - - --usd-path: Path to robot USD asset on the Nucleus Server. Defaults to "/Isaac/Robots/Denso/cobotta_pro_900.usd". The typical location of a specific robot is under - "/Isaac/Robots/{manufacturer_name}/{robot_name}/{robot_name}.usd" - - --add-orientation-target: Add the orientation of the target cube to the RMPflow target. Defaults to False. - - -###### Choosing Correct Robot Name Argument - -With the default arguments, the above script will run using the Cobotta Pro 900 robot and will produce the following output: - - Names of supported robots with provided RMPflow config - ['Franka', 'UR3', 'UR3e', 'UR5', 'UR5e', 'UR10', 'UR10e', 'UR16e', 'Rizon4', 'Cobotta_Pro_900', 'Cobotta_Pro_1300', 'RS007L', 'RS007N', 'RS013N', 'RS025N', 'RS080N', 'FestoCobot'] - - Successfully referenced RMPflow config for Cobotta_Pro_900. Using the following parameters to initialize RmpFlow class: - { - 'end_effector_frame_name': 'gripper_center', - 'ignore_robot_state_updates': False, - 'maximum_substep_size': 0.00334, - 'rmpflow_config_path': '/path/to/omni_isaac_sim/_build/linux-x86_64/release/exts/isaacsim.robot_motion.motion_generation/motion_policy_configs/./Denso/cobotta_pro_900/rmpflow/cobotta_rmpflow_common.yaml', - 'robot_description_path': '/path/to/omni_isaac_sim/_build/linux-x86_64/release/exts/isaacsim.robot_motion.motion_generation/motion_policy_configs/./Denso/cobotta_pro_900/rmpflow/robot_descriptor.yaml', - 'urdf_path': '/path/to/omni_isaac_sim/_build/linux-x86_64/release/exts/isaacsim.robot_motion.motion_generation/motion_policy_configs/./Denso/cobotta_pro_900/rmpflow/../cobotta_pro_900_gripper_frame.urdf' - } - -The names of supported robots are suitable for the `--robot-name` argument, and each must correctly correspond to the robot USD path. In a future release, configuration data for supported robots will be centralized such that only a single argument will be required. The specific method of accessing supported robot RMPflow configs provided here will then be deprecated. - -The remaining output shows the RMPflow configuration information that is found under the name "Cobotta_Pro_900". This configuration is used to initialize the `RmpFlow` class. - - -###### Examples of loading other robots - -Multiple valid combinations of command line arguments are shown for different supported robots: - - python.sh supported_robot_follow_target_example.py --robot-name RS080N --usd-path "/Isaac/Robots/Kawasaki/RS080N/rs080n_onrobot_rg2.usd" - - python.sh supported_robot_follow_target_example.py --robot-name UR16e --usd-path "/Isaac/Robots/UniversalRobots/ur16e/ur16e.usd" - - python.sh supported_robot_follow_target_example.py --robot-name FestoCobot --usd-path "/Isaac/Robots/Festo/FestoCobot/festo_cobot.usd" - - diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/rmpflow_supported_robots/supported_robot_follow_target_example.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/rmpflow_supported_robots/supported_robot_follow_target_example.py deleted file mode 100644 index 1ba2478ab..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/rmpflow_supported_robots/supported_robot_follow_target_example.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -from pprint import pprint - -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import cuboid -from isaacsim.core.api.robots import Robot -from isaacsim.core.utils.prims import create_prim -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.robot_motion.motion_generation.articulation_motion_policy import ArticulationMotionPolicy -from isaacsim.robot_motion.motion_generation.interface_config_loader import ( - get_supported_robot_policy_pairs, - load_supported_motion_policy_config, -) -from isaacsim.robot_motion.motion_generation.lula import RmpFlow -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument( - "-v", - "--verbose", - action="store_true", - default=True, - help="Print useful runtime information such as the list of supported robots", -) -parser.add_argument( - "--robot-name", - type=str, - default="Cobotta_Pro_900", - help="Key to use to access RMPflow config files for a specific robot.", -) -parser.add_argument( - "--usd-path", - type=str, - default="/Isaac/Robots/Denso/cobotta_pro_900.usd", - help="Path to supported robot on Nucleus Server", -) -parser.add_argument("--add-orientation-target", action="store_true", default=False, help="Add orientation target") -args, unknown = parser.parse_known_args() - -robot_name = args.robot_name -usd_path = get_assets_root_path() + args.usd_path -prim_path = "/my_robot" - -add_reference_to_stage(usd_path=usd_path, prim_path=prim_path) - -light_prim = create_prim("/DistantLight", "DistantLight") -light_prim.GetAttribute("inputs:intensity").Set(5000) - -my_world = World(stage_units_in_meters=1.0) - -robot = my_world.scene.add(Robot(prim_path=prim_path, name=robot_name)) - -if args.verbose: - print("Names of supported robots with provided RMPflow config") - print("\t", list(get_supported_robot_policy_pairs().keys())) - print() - -# The load_supported_motion_policy_config() function is currently the simplest way to load supported robots. -# In the future, Isaac Sim will provide a centralized registry of robots with Lula robot description files -# and RMP configuration files stored alongside the robot USD. -rmp_config = load_supported_motion_policy_config(robot_name, "RMPflow") - -if args.verbose: - print( - f"Successfully referenced RMPflow config for {robot_name}. Using the following parameters to initialize RmpFlow class:" - ) - pprint(rmp_config) - print() - -# Initialize an RmpFlow object -rmpflow = RmpFlow(**rmp_config) - -physics_dt = 1 / 60.0 -articulation_rmpflow = ArticulationMotionPolicy(robot, rmpflow, physics_dt) - -articulation_controller = robot.get_articulation_controller() - -# Make a target to follow -target_cube = cuboid.VisualCuboid( - "/World/target", position=np.array([0.5, 0, 0.5]), color=np.array([1.0, 0, 0]), size=0.1 -) - -# Make an obstacle to avoid -obstacle = cuboid.VisualCuboid( - "/World/obstacle", position=np.array([0.8, 0, 0.5]), color=np.array([0, 1.0, 0]), size=0.1 -) -rmpflow.add_obstacle(obstacle) - -my_world.reset() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - reset_needed = False - # Set rmpflow target to be the current position of the target cube. - if args.add_orientation_target: - target_orientation = target_cube.get_world_pose()[1] - else: - target_orientation = None - - rmpflow.set_end_effector_target( - target_position=target_cube.get_world_pose()[0], target_orientation=target_orientation - ) - - # Query the current obstacle position - rmpflow.update_world() - - actions = articulation_rmpflow.get_next_articulation_action() - articulation_controller.apply_action(actions) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/bin_filling.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/bin_filling.py deleted file mode 100644 index 6bee7d638..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/bin_filling.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.utils.rotations import euler_angles_to_quat -from isaacsim.robot.manipulators.examples.universal_robots.controllers.pick_place_controller import PickPlaceController -from isaacsim.robot.manipulators.examples.universal_robots.tasks import BinFilling - -my_world = World(stage_units_in_meters=1.0) -my_task = BinFilling() -my_world.add_task(my_task) -my_world.reset() -task_params = my_task.get_params() -my_ur10 = my_world.scene.get_object(task_params["robot_name"]["value"]) -my_controller = PickPlaceController(name="pick_place_controller", gripper=my_ur10.gripper, robot_articulation=my_ur10) -articulation_controller = my_ur10.get_articulation_controller() - -i = 0 -added_screws = False -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - added_screws = False - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - picking_position=observations[task_params["bin_name"]["value"]]["position"], - placing_position=observations[task_params["bin_name"]["value"]]["target_position"], - current_joint_positions=observations[task_params["robot_name"]["value"]]["joint_positions"], - end_effector_offset=np.array([0, -0.098, 0.03]), - end_effector_orientation=euler_angles_to_quat(np.array([np.pi, 0, np.pi / 2.0])), - ) - if not added_screws and my_controller.get_current_event() == 6 and not my_controller.is_paused(): - my_controller.pause() - my_task.add_screws(screws_number=20) - added_screws = True - if my_controller.is_done(): - print("done picking and placing") - articulation_controller.apply_action(actions) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/follow_target_with_ik.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/follow_target_with_ik.py deleted file mode 100644 index c3a06be07..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/follow_target_with_ik.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.universal_robots import KinematicsSolver -from isaacsim.robot.manipulators.examples.universal_robots.tasks import FollowTarget - -my_world = World(stage_units_in_meters=1.0) -my_task = FollowTarget(name="follow_target_task", attach_gripper=True, target_position=[0.2, 0.4, 0.4]) -my_world.add_task(my_task) -my_world.reset() -task_params = my_world.get_task("follow_target_task").get_params() -ur10_name = task_params["robot_name"]["value"] -target_name = task_params["target_name"]["value"] -my_ur10 = my_world.scene.get_object(ur10_name) -my_controller = KinematicsSolver(my_ur10, attach_gripper=True) -articulation_controller = my_ur10.get_articulation_controller() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - reset_needed = False - observations = my_world.get_observations() - actions, succ = my_controller.compute_inverse_kinematics( - target_position=observations[target_name]["position"], - target_orientation=observations[target_name]["orientation"], - ) - if succ: - articulation_controller.apply_action(actions) - else: - carb.log_warn("IK did not converge to a solution. No action is being taken.") - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/follow_target_with_rmpflow.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/follow_target_with_rmpflow.py deleted file mode 100644 index 0548c49c6..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/follow_target_with_rmpflow.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.universal_robots.controllers.rmpflow_controller import RMPFlowController -from isaacsim.robot.manipulators.examples.universal_robots.tasks import FollowTarget - -my_world = World(stage_units_in_meters=1.0) -my_task = FollowTarget(name="follow_target_task", attach_gripper=True) -my_world.add_task(my_task) -my_world.reset() -task_params = my_world.get_task("follow_target_task").get_params() -ur10_name = task_params["robot_name"]["value"] -target_name = task_params["target_name"]["value"] -my_ur10 = my_world.scene.get_object(ur10_name) -my_controller = RMPFlowController(name="target_follower_controller", robot_articulation=my_ur10, attach_gripper=True) -articulation_controller = my_ur10.get_articulation_controller() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - target_end_effector_position=observations[target_name]["position"], - target_end_effector_orientation=observations[target_name]["orientation"], - ) - articulation_controller.apply_action(actions) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/multiple_tasks.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/multiple_tasks.py deleted file mode 100644 index f02733921..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/multiple_tasks.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.franka.controllers.stacking_controller import ( - StackingController as FrankaStackingController, -) -from isaacsim.robot.manipulators.examples.franka.tasks import Stacking as FrankaStacking -from isaacsim.robot.manipulators.examples.universal_robots.controllers import ( - StackingController as UR10StackingController, -) -from isaacsim.robot.manipulators.examples.universal_robots.tasks import Stacking as UR10Stacking -from isaacsim.robot.wheeled_robots.controllers.differential_controller import DifferentialController -from isaacsim.robot.wheeled_robots.controllers.holonomic_controller import HolonomicController -from isaacsim.robot.wheeled_robots.robots import WheeledRobot -from isaacsim.robot.wheeled_robots.robots.holonomic_robot_usd_setup import HolonomicRobotUsdSetup -from isaacsim.storage.native import get_assets_root_path - -my_world = World(stage_units_in_meters=1.0) -tasks = [] -num_of_tasks = 2 - -tasks.append(FrankaStacking(name="task_0", offset=np.array([0, -2, 0]))) -my_world.add_task(tasks[-1]) -tasks.append(UR10Stacking(name="task_1", offset=np.array([0.5, 0.5, 0]))) -my_world.add_task(tasks[-1]) -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") -kaya_asset_path = assets_root_path + "/Isaac/Robots/Kaya/kaya.usd" -my_kaya = my_world.scene.add( - WheeledRobot( - prim_path="/World/Kaya", - name="my_kaya", - wheel_dof_names=["axle_0_joint", "axle_1_joint", "axle_2_joint"], - create_robot=True, - usd_path=kaya_asset_path, - position=np.array([-1, 0, 0]), - ) -) -jetbot_asset_path = assets_root_path + "/Isaac/Robots/Jetbot/jetbot.usd" -my_jetbot = my_world.scene.add( - WheeledRobot( - prim_path="/World/Jetbot", - name="my_jetbot", - wheel_dof_names=["left_wheel_joint", "right_wheel_joint"], - create_robot=True, - usd_path=jetbot_asset_path, - position=np.array([-1.5, -1.5, 0]), - ) -) - -my_world.reset() -robots = [] -for i in range(num_of_tasks): - task_params = tasks[i].get_params() - robots.append(my_world.scene.get_object(task_params["robot_name"]["value"])) - -controllers = [] -controllers.append( - FrankaStackingController( - name="pick_place_controller", - gripper=robots[0].gripper, - robot_articulation=robots[0], - picking_order_cube_names=tasks[0].get_cube_names(), - robot_observation_name=robots[0].name, - ) -) -controllers[-1].reset() -controllers.append( - UR10StackingController( - name="pick_place_controller", - gripper=robots[1].gripper, - robot_articulation=robots[1], - picking_order_cube_names=tasks[1].get_cube_names(), - robot_observation_name=robots[1].name, - ) -) -controllers[-1].reset() - -kaya_setup = HolonomicRobotUsdSetup( - robot_prim_path=my_kaya.prim_path, com_prim_path="/World/Kaya/base_link/control_offset" -) -( - wheel_radius, - wheel_positions, - wheel_orientations, - mecanum_angles, - wheel_axis, - up_axis, -) = kaya_setup.get_holonomic_controller_params() -kaya_controller = HolonomicController( - name="holonomic_controller", - wheel_radius=wheel_radius, - wheel_positions=wheel_positions, - wheel_orientations=wheel_orientations, - mecanum_angles=mecanum_angles, - wheel_axis=wheel_axis, - up_axis=up_axis, -) - -jetbot_controller = DifferentialController(name="simple_control", wheel_radius=0.03, wheel_base=0.1125) - -articulation_controllers = [] -for i in range(num_of_tasks): - articulation_controllers.append(robots[i].get_articulation_controller()) - -i = 0 -my_world.pause() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - controllers[0].reset() - controllers[1].reset() - kaya_controller.reset() - jetbot_controller.reset() - i = 0 - reset_needed = False - observations = my_world.get_observations() - actions = controllers[0].forward(observations=observations, end_effector_offset=np.array([0, 0, 0])) - articulation_controllers[0].apply_action(actions) - actions = controllers[1].forward(observations=observations, end_effector_offset=np.array([0, 0, 0.02])) - articulation_controllers[1].apply_action(actions) - if i >= 0 and i < 500: - my_kaya.apply_wheel_actions(kaya_controller.forward(command=[0.2, 0.0, 0.0])) - my_jetbot.apply_wheel_actions(jetbot_controller.forward(command=[0.1, 0])) - elif i >= 500 and i < 1000: - my_kaya.apply_wheel_actions(kaya_controller.forward(command=[0, 0.2, 0.0])) - my_jetbot.apply_wheel_actions(jetbot_controller.forward(command=[0.0, np.pi / 10])) - elif i >= 1000 and i < 1500: - my_kaya.apply_wheel_actions(kaya_controller.forward(command=[0, 0.0, 0.6])) - my_jetbot.apply_wheel_actions(jetbot_controller.forward(command=[0.1, 0])) - i += 1 - - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/pick_place.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/pick_place.py deleted file mode 100644 index 3180e6757..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/pick_place.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.universal_robots.controllers.pick_place_controller import PickPlaceController -from isaacsim.robot.manipulators.examples.universal_robots.tasks import PickPlace - -my_world = World(stage_units_in_meters=1.0) -my_task = PickPlace() -my_world.add_task(my_task) -my_world.reset() -task_params = my_task.get_params() -my_ur10 = my_world.scene.get_object(task_params["robot_name"]["value"]) -my_controller = PickPlaceController(name="pick_place_controller", gripper=my_ur10.gripper, robot_articulation=my_ur10) -articulation_controller = my_ur10.get_articulation_controller() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - picking_position=observations[task_params["cube_name"]["value"]]["position"], - placing_position=observations[task_params["cube_name"]["value"]]["target_position"], - current_joint_positions=observations[task_params["robot_name"]["value"]]["joint_positions"], - end_effector_offset=np.array([0, 0, 0.02]), - ) - if my_controller.is_done(): - print("done picking and placing") - articulation_controller.apply_action(actions) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/pick_place2.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/pick_place2.py deleted file mode 100644 index 71b03d966..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/pick_place2.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.utils.collisions import ray_cast -from isaacsim.core.utils.rotations import euler_angles_to_quat -from isaacsim.robot.manipulators.examples.universal_robots.controllers.pick_place_controller import PickPlaceController -from isaacsim.robot.manipulators.examples.universal_robots.tasks import BinFilling - -my_world = World(stage_units_in_meters=1.0) -my_task = BinFilling() -my_world.add_task(my_task) -my_world.reset() -task_params = my_task.get_params() -my_ur10 = my_world.scene.get_object(task_params["robot_name"]["value"]) -my_controller = PickPlaceController(name="pick_place_controller", gripper=my_ur10.gripper, robot_articulation=my_ur10) -articulation_controller = my_ur10.get_articulation_controller() - -i = 0 -reset_needed = False - -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - picking_position=observations[task_params["bin_name"]["value"]]["position"], - placing_position=observations[task_params["bin_name"]["value"]]["target_position"], - current_joint_positions=observations[task_params["robot_name"]["value"]]["joint_positions"], - # end_effector_offset=np.array([0, 0, -0.075]) - end_effector_offset=np.array([0, -0.098, 0.03]), - end_effector_orientation=euler_angles_to_quat(np.array([np.pi, 0, np.pi / 2.0])), - ) - if my_controller.get_current_event() > 2 and my_controller.get_current_event() < 6: - print( - ray_cast( - position=observations[task_params["robot_name"]["value"]]["end_effector_position"], - orientation=observations[task_params["robot_name"]["value"]]["end_effector_orientation"], - offset=np.array([0.162, 0, 0]), - ) - ) - if my_controller.is_done(): - print("done picking and placing") - articulation_controller.apply_action(actions) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/stacking.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/stacking.py deleted file mode 100644 index 50a22f8a6..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/universal_robots/stacking.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import numpy as np -from isaacsim.core.api import World -from isaacsim.robot.manipulators.examples.universal_robots.controllers import StackingController -from isaacsim.robot.manipulators.examples.universal_robots.tasks import Stacking - -my_world = World(stage_units_in_meters=1.0) -my_task = Stacking() -my_world.add_task(my_task) -my_world.reset() -robot_name = my_task.get_params()["robot_name"]["value"] -my_ur10 = my_world.scene.get_object(robot_name) -my_controller = StackingController( - name="stacking_controller", - gripper=my_ur10.gripper, - robot_articulation=my_ur10, - picking_order_cube_names=my_task.get_cube_names(), - robot_observation_name=robot_name, -) -articulation_controller = my_ur10.get_articulation_controller() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward(observations=observations, end_effector_offset=np.array([0.0, 0.0, 0.02])) - articulation_controller.apply_action(actions) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/ur10_pick_up.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/ur10_pick_up.py deleted file mode 100644 index cbc58402e..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.manipulators/ur10_pick_up.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.robot.manipulators import SingleManipulator -from isaacsim.robot.manipulators.examples.universal_robots.controllers.pick_place_controller import PickPlaceController -from isaacsim.robot.manipulators.grippers import SurfaceGripper -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() -asset_path = assets_root_path + "/Isaac/Robots/UniversalRobots/ur10/ur10.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/UR10") -gripper_usd = assets_root_path + "/Isaac/Robots/UR10/Props/short_gripper.usd" -add_reference_to_stage(usd_path=gripper_usd, prim_path="/World/UR10/ee_link") -gripper = SurfaceGripper(end_effector_prim_path="/World/UR10/ee_link", translate=0.1611, direction="x") -ur10 = my_world.scene.add( - SingleManipulator( - prim_path="/World/UR10", name="my_ur10", end_effector_prim_path="/World/UR10/ee_link", gripper=gripper - ) -) -ur10.set_joints_default_state(positions=np.array([-np.pi / 2, -np.pi / 2, -np.pi / 2, -np.pi / 2, np.pi / 2, 0])) -cube = my_world.scene.add( - DynamicCuboid( - name="cube", - position=np.array([0.3, 0.3, 0.3]), - prim_path="/World/Cube", - scale=np.array([0.0515, 0.0515, 0.0515]), - size=1.0, - color=np.array([0, 0, 1]), - ) -) -my_world.scene.add_default_ground_plane() -ur10.gripper.set_default_state(opened=True) -my_world.reset() - -my_controller = PickPlaceController(name="pick_place_controller", gripper=ur10.gripper, robot_articulation=ur10) -articulation_controller = ur10.get_articulation_controller() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - actions = my_controller.forward( - picking_position=cube.get_local_pose()[0], - placing_position=np.array([0.7, 0.7, 0.0515 / 2.0]), - current_joint_positions=ur10.get_joint_positions(), - end_effector_offset=np.array([0, 0, 0.02]), - ) - if my_controller.is_done(): - print("done picking and placing") - articulation_controller.apply_action(actions) - if args.test is True: - break - - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/anymal_standalone.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/anymal_standalone.py deleted file mode 100755 index cbbff9857..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/anymal_standalone.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -import numpy as np -import omni.appwindow # Contains handle to keyboard -from isaacsim.core.api import World -from isaacsim.core.utils.prims import define_prim, get_prim_at_path -from isaacsim.robot.policy.examples.robots import AnymalFlatTerrainPolicy -from isaacsim.storage.native import get_assets_root_path - - -class Anymal_runner(object): - def __init__(self, physics_dt, render_dt) -> None: - """ - creates the simulation world with preset physics_dt and render_dt and creates an anymal robot inside the warehouse - - Argument: - physics_dt {float} -- Physics downtime of the scene. - render_dt {float} -- Render downtime of the scene. - - """ - self._world = World(stage_units_in_meters=1.0, physics_dt=physics_dt, rendering_dt=render_dt) - - assets_root_path = get_assets_root_path() - if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - - # spawn warehouse scene - prim = define_prim("/World/Warehouse", "Xform") - asset_path = assets_root_path + "/Isaac/Environments/Simple_Warehouse/warehouse.usd" - prim.GetReferences().AddReference(asset_path) - - self._anymal = AnymalFlatTerrainPolicy( - prim_path="/World/Anymal", - name="Anymal", - usd_path=assets_root_path + "/Isaac/Robots/ANYbotics/anymal_c.usd", - position=np.array([0, 0, 0.7]), - ) - - self._base_command = np.zeros(3) - - # bindings for keyboard to command - self._input_keyboard_mapping = { - # forward command - "NUMPAD_8": [1.0, 0.0, 0.0], - "UP": [1.0, 0.0, 0.0], - # back command - "NUMPAD_2": [-1.0, 0.0, 0.0], - "DOWN": [-1.0, 0.0, 0.0], - # left command - "NUMPAD_6": [0.0, -1.0, 0.0], - "RIGHT": [0.0, -1.0, 0.0], - # right command - "NUMPAD_4": [0.0, 1.0, 0.0], - "LEFT": [0.0, 1.0, 0.0], - # yaw command (positive) - "NUMPAD_7": [0.0, 0.0, 1.0], - "N": [0.0, 0.0, 1.0], - # yaw command (negative) - "NUMPAD_9": [0.0, 0.0, -1.0], - "M": [0.0, 0.0, -1.0], - } - self.needs_reset = False - self.first_step = True - - def setup(self) -> None: - """ - Set up keyboard listener and add physics callback - - """ - self._appwindow = omni.appwindow.get_default_app_window() - self._input = carb.input.acquire_input_interface() - self._keyboard = self._appwindow.get_keyboard() - self._sub_keyboard = self._input.subscribe_to_keyboard_events(self._keyboard, self._sub_keyboard_event) - self._world.add_physics_callback("anymal_forward", callback_fn=self.on_physics_step) - - def on_physics_step(self, step_size) -> None: - """ - Physics call back, initialize robot (first frame) and call controller forward function to compute and apply joint torque - - """ - if self.first_step: - self._anymal.initialize() - self.first_step = False - elif self.needs_reset: - self._world.reset(True) - self.needs_reset = False - self.first_step = True - else: - self._anymal.forward(step_size, self._base_command) - - def run(self) -> None: - """ - Step simulation based on rendering downtime - - """ - # change to sim running - while simulation_app.is_running(): - self._world.step(render=True) - if self._world.is_stopped(): - self.needs_reset = True - return - - def _sub_keyboard_event(self, event, *args, **kwargs) -> bool: - """ - Keyboard subscriber callback to when kit is updated. - - """ - - # when a key is pressed for released the command is adjusted w.r.t the key-mapping - if event.type == carb.input.KeyboardEventType.KEY_PRESS: - # on pressing, the command is incremented - if event.input.name in self._input_keyboard_mapping: - self._base_command += np.array(self._input_keyboard_mapping[event.input.name]) - - elif event.type == carb.input.KeyboardEventType.KEY_RELEASE: - # on release, the command is decremented - if event.input.name in self._input_keyboard_mapping: - self._base_command -= np.array(self._input_keyboard_mapping[event.input.name]) - return True - - -def main(): - """ - Parse arguments and instantiate the ANYmal runner - - """ - physics_dt = 1 / 200.0 - render_dt = 1 / 60.0 - - runner = Anymal_runner(physics_dt=physics_dt, render_dt=render_dt) - simulation_app.update() - runner._world.reset() - simulation_app.update() - runner.setup() - simulation_app.update() - runner.run() - simulation_app.close() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/h1_standalone.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/h1_standalone.py deleted file mode 100644 index 4c9491bfd..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/h1_standalone.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse - -import carb -import numpy as np -import omni.appwindow # Contains handle to keyboard -from isaacsim.core.api import World -from isaacsim.core.utils.prims import define_prim, get_prim_at_path -from isaacsim.robot.policy.examples.robots import H1FlatTerrainPolicy -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser(description="Define the number of robots.") -parser.add_argument("--num-robots", type=int, default=1, help="Number of robots (default: 1)") -parser.add_argument( - "--env-url", - default="/Isaac/Environments/Grid/default_environment.usd", - required=False, - help="Path to the environment url", -) -args = parser.parse_args() -print(f"Number of robots: {args.num_robots}") - -first_step = True -reset_needed = False -robots = [] - -# initialize robot on first step, run robot advance -def on_physics_step(step_size) -> None: - global first_step - global reset_needed - if first_step: - for robot in robots: - robot.initialize() - first_step = False - elif reset_needed: - my_world.reset(True) - reset_needed = False - first_step = True - else: - for robot in robots: - robot.forward(step_size, base_command) - - -# spawn world -my_world = World(stage_units_in_meters=1.0, physics_dt=1 / 200, rendering_dt=8 / 200) -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - -# spawn warehouse scene -prim = define_prim("/World/Ground", "Xform") -asset_path = assets_root_path + args.env_url -prim.GetReferences().AddReference(asset_path) - -# spawn robot -for i in range(0, args.num_robots): - h1 = H1FlatTerrainPolicy( - prim_path="/World/H1_" + str(i), - name="H1_" + str(i), - usd_path=assets_root_path + "/Isaac/Robots/Unitree/H1/h1.usd", - position=np.array([0, i, 1.05]), - ) - - robots.append(h1) - -my_world.reset() -my_world.add_physics_callback("physics_step", callback_fn=on_physics_step) - -# robot command -base_command = np.zeros(3) - -i = 0 -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped(): - reset_needed = True - if my_world.is_playing(): - if i >= 0 and i < 80: - # forward - base_command = np.array([0.5, 0, 0]) - elif i >= 80 and i < 130: - # rotate - base_command = np.array([0.5, 0, 0.5]) - elif i >= 130 and i < 200: - # side ways - base_command = np.array([0, 0, 0.5]) - elif i == 200: - i = 0 - i += 1 - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/spot_standalone.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/spot_standalone.py deleted file mode 100644 index 6439f8d43..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.policy.examples/spot_standalone.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -import numpy as np -import omni.appwindow # Contains handle to keyboard -from isaacsim.core.api import World -from isaacsim.core.utils.prims import define_prim, get_prim_at_path -from isaacsim.robot.policy.examples.robots import SpotFlatTerrainPolicy -from isaacsim.storage.native import get_assets_root_path - -first_step = True -reset_needed = False - -# initialize robot on first step, run robot advance -def on_physics_step(step_size) -> None: - global first_step - global reset_needed - if first_step: - spot.initialize() - first_step = False - elif reset_needed: - my_world.reset(True) - reset_needed = False - first_step = True - else: - spot.forward(step_size, base_command) - - -# spawn world -my_world = World(stage_units_in_meters=1.0, physics_dt=1 / 500, rendering_dt=1 / 50) -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - -# spawn warehouse scene -prim = define_prim("/World/Ground", "Xform") -asset_path = assets_root_path + "/Isaac/Environments/Grid/default_environment.usd" -prim.GetReferences().AddReference(asset_path) - -# spawn robot -spot = SpotFlatTerrainPolicy( - prim_path="/World/Spot", - name="Spot", - position=np.array([0, 0, 0.8]), -) -my_world.reset() -my_world.add_physics_callback("physics_step", callback_fn=on_physics_step) - -# robot command -base_command = np.zeros(3) - -i = 0 -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped(): - reset_needed = True - if my_world.is_playing(): - if i >= 0 and i < 80: - # forward - base_command = np.array([2, 0, 0]) - elif i >= 80 and i < 130: - # rotate - base_command = np.array([1, 0, 2]) - elif i >= 130 and i < 200: - # side ways - base_command = np.array([0, 1, 0]) - elif i == 200: - i = 0 - i += 1 - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.wheeled_robots.examples/jetbot_differential_move.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.wheeled_robots.examples/jetbot_differential_move.py deleted file mode 100644 index d168bc135..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.wheeled_robots.examples/jetbot_differential_move.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import argparse - -from isaacsim import SimulationApp - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - - -simulation_app = SimulationApp({"headless": False}) - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.robot.wheeled_robots.controllers.differential_controller import DifferentialController -from isaacsim.robot.wheeled_robots.robots import WheeledRobot -from isaacsim.storage.native import get_assets_root_path - -my_world = World(stage_units_in_meters=1.0) -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") -jetbot_asset_path = assets_root_path + "/Isaac/Robots/Jetbot/jetbot.usd" -my_jetbot = my_world.scene.add( - WheeledRobot( - prim_path="/World/Jetbot", - name="my_jetbot", - wheel_dof_names=["left_wheel_joint", "right_wheel_joint"], - create_robot=True, - usd_path=jetbot_asset_path, - position=np.array([0, 0.0, 2.0]), - ) -) -my_world.scene.add_default_ground_plane() -my_controller = DifferentialController(name="simple_control", wheel_radius=0.03, wheel_base=0.1125) -my_world.reset() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - if i >= 0 and i < 1000: - # forward - my_jetbot.apply_wheel_actions(my_controller.forward(command=[0.05, 0])) - print(my_jetbot.get_linear_velocity()) - elif i >= 1000 and i < 1300: - # rotate - my_jetbot.apply_wheel_actions(my_controller.forward(command=[0.0, np.pi / 12])) - print(my_jetbot.get_angular_velocity()) - elif i >= 1300 and i < 2000: - # forward - my_jetbot.apply_wheel_actions(my_controller.forward(command=[0.05, 0])) - elif i == 2000: - i = 0 - i += 1 - if args.test is True: - break - -my_world.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.wheeled_robots.examples/kaya_holonomic_move.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.wheeled_robots.examples/kaya_holonomic_move.py deleted file mode 100644 index e0d873ee0..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.robot.wheeled_robots.examples/kaya_holonomic_move.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.robot.wheeled_robots.controllers.holonomic_controller import HolonomicController -from isaacsim.robot.wheeled_robots.robots import WheeledRobot -from isaacsim.robot.wheeled_robots.robots.holonomic_robot_usd_setup import HolonomicRobotUsdSetup -from isaacsim.storage.native import get_assets_root_path - -my_world = World(stage_units_in_meters=1.0) - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") -kaya_asset_path = assets_root_path + "/Isaac/Robots/Kaya/kaya.usd" -my_kaya = my_world.scene.add( - WheeledRobot( - prim_path="/World/Kaya", - name="my_kaya", - wheel_dof_names=["axle_0_joint", "axle_1_joint", "axle_2_joint"], - create_robot=True, - usd_path=kaya_asset_path, - position=np.array([0, 0.0, 0.02]), - orientation=np.array([1.0, 0.0, 0.0, 0.0]), - ) -) -my_world.scene.add_default_ground_plane() - -kaya_setup = HolonomicRobotUsdSetup( - robot_prim_path=my_kaya.prim_path, com_prim_path="/World/Kaya/base_link/control_offset" -) -( - wheel_radius, - wheel_positions, - wheel_orientations, - mecanum_angles, - wheel_axis, - up_axis, -) = kaya_setup.get_holonomic_controller_params() -my_controller = HolonomicController( - name="holonomic_controller", - wheel_radius=wheel_radius, - wheel_positions=wheel_positions, - wheel_orientations=wheel_orientations, - mecanum_angles=mecanum_angles, - wheel_axis=wheel_axis, - up_axis=up_axis, -) - -my_world.reset() - -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - if i >= 0 and i < 500: - my_kaya.apply_wheel_actions(my_controller.forward(command=[0.4, 0.0, 0.0])) - elif i >= 500 and i < 1000: - my_kaya.apply_wheel_actions(my_controller.forward(command=[0.0, 0.4, 0.0])) - elif i >= 1000 and i < 1200: - my_kaya.apply_wheel_actions(my_controller.forward(command=[0.0, 0.0, 0.05])) - elif i == 1200: - i = 0 - i += 1 - - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_manual.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_manual.py deleted file mode 100644 index 753c786f5..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_manual.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import sys - -from isaacsim import SimulationApp - -CAMERA_STAGE_PATH = "/Camera" -ROS_CAMERA_GRAPH_PATH = "/ROS_Camera" -BACKGROUND_STAGE_PATH = "/background" -BACKGROUND_USD_PATH = "/Isaac/Environments/Simple_Warehouse/warehouse_with_forklifts.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -# Example ROS bridge sample demonstrating the manual loading of stages and manual publishing of images -simulation_app = SimulationApp(CONFIG) -import carb -import omni -import omni.graph.core as og -import omni.replicator.core as rep -import usdrt.Sdf -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import extensions, stage -from isaacsim.storage.native import get_assets_root_path -from omni.kit.viewport.utility import get_active_viewport -from pxr import Gf, Usd, UsdGeom - -# enable ROS bridge extension -extensions.enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Loading the simple_room environment -stage.add_reference_to_stage(assets_root_path + BACKGROUND_USD_PATH, BACKGROUND_STAGE_PATH) - -# Creating a Camera prim -camera_prim = UsdGeom.Camera(omni.usd.get_context().get_stage().DefinePrim(CAMERA_STAGE_PATH, "Camera")) -xform_api = UsdGeom.XformCommonAPI(camera_prim) -xform_api.SetTranslate(Gf.Vec3d(-1, 5, 1)) -xform_api.SetRotate((90, 0, 0), UsdGeom.XformCommonAPI.RotationOrderXYZ) -camera_prim.GetHorizontalApertureAttr().Set(21) -camera_prim.GetVerticalApertureAttr().Set(16) -camera_prim.GetProjectionAttr().Set("perspective") -camera_prim.GetFocalLengthAttr().Set(24) -camera_prim.GetFocusDistanceAttr().Set(400) - -simulation_app.update() - -# Creating an on-demand push graph with cameraHelper nodes to generate ROS image publishers - -keys = og.Controller.Keys -(ros_camera_graph, _, _, _) = og.Controller.edit( - { - "graph_path": ROS_CAMERA_GRAPH_PATH, - "evaluator_name": "push", - "pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND, - }, - { - keys.CREATE_NODES: [ - ("OnTick", "omni.graph.action.OnTick"), - ("createViewport", "isaacsim.core.nodes.IsaacCreateViewport"), - ("getRenderProduct", "isaacsim.core.nodes.IsaacGetViewportRenderProduct"), - ("setCamera", "isaacsim.core.nodes.IsaacSetCameraOnRenderProduct"), - ("cameraHelperRgb", "isaacsim.ros1.bridge.ROS1CameraHelper"), - ("cameraHelperInfo", "isaacsim.ros1.bridge.ROS1CameraHelper"), - ("cameraHelperDepth", "isaacsim.ros1.bridge.ROS1CameraHelper"), - ], - keys.CONNECT: [ - ("OnTick.outputs:tick", "createViewport.inputs:execIn"), - ("createViewport.outputs:execOut", "getRenderProduct.inputs:execIn"), - ("createViewport.outputs:viewport", "getRenderProduct.inputs:viewport"), - ("getRenderProduct.outputs:execOut", "setCamera.inputs:execIn"), - ("getRenderProduct.outputs:renderProductPath", "setCamera.inputs:renderProductPath"), - ("setCamera.outputs:execOut", "cameraHelperRgb.inputs:execIn"), - ("setCamera.outputs:execOut", "cameraHelperInfo.inputs:execIn"), - ("setCamera.outputs:execOut", "cameraHelperDepth.inputs:execIn"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperRgb.inputs:renderProductPath"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperInfo.inputs:renderProductPath"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperDepth.inputs:renderProductPath"), - ], - keys.SET_VALUES: [ - ("createViewport.inputs:viewportId", 0), - ("cameraHelperRgb.inputs:frameId", "sim_camera"), - ("cameraHelperRgb.inputs:topicName", "rgb"), - ("cameraHelperRgb.inputs:type", "rgb"), - ("cameraHelperInfo.inputs:frameId", "sim_camera"), - ("cameraHelperInfo.inputs:topicName", "camera_info"), - ("cameraHelperInfo.inputs:type", "camera_info"), - ("cameraHelperDepth.inputs:frameId", "sim_camera"), - ("cameraHelperDepth.inputs:topicName", "depth"), - ("cameraHelperDepth.inputs:type", "depth"), - ("setCamera.inputs:cameraPrim", [usdrt.Sdf.Path(CAMERA_STAGE_PATH)]), - ], - }, -) - -# Run the ROS Camera graph once to generate ROS image publishers in SDGPipeline -og.Controller.evaluate_sync(ros_camera_graph) - -simulation_app.update() - -# Use the IsaacSimulationGate step value to block execution on specific frames -SD_GRAPH_PATH = "/Render/PostProcess/SDGPipeline" - -viewport_api = get_active_viewport() - -if viewport_api is not None: - import omni.syntheticdata._syntheticdata as sd - - curr_stage = omni.usd.get_context().get_stage() - - # Required for editing the SDGPipeline graph which exists in the Session Layer - with Usd.EditContext(curr_stage, curr_stage.GetSessionLayer()): - - # Get name of rendervar for RGB sensor type - rv_rgb = omni.syntheticdata.SyntheticData.convert_sensor_type_to_rendervar(sd.SensorType.Rgb.name) - - # Get path to IsaacSimulationGate node in RGB pipeline - rgb_camera_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - rv_rgb + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - rv_depth = omni.syntheticdata.SyntheticData.convert_sensor_type_to_rendervar( - sd.SensorType.DistanceToImagePlane.name - ) - # Get path to IsaacSimulationGate node in Depth pipeline - depth_camera_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - rv_depth + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - # Get path to IsaacSimulationGate node in CameraInfo pipeline - camera_info_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - "PostProcessDispatch" + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - -# Need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - -simulation_context.play() - -frame = 0 - -while simulation_app.is_running() and simulation_context.is_playing(): - # Run with a fixed step size - simulation_context.step(render=True) - - if simulation_context.is_playing(): - # Rotate camera by 0.5 degree every frame - xform_api.SetRotate((90, 0, frame / 4.0), UsdGeom.XformCommonAPI.RotationOrderXYZ) - - # Set the step value for the simulation gates to zero to stop execution - og.Controller.attribute(rgb_camera_gate_path + ".inputs:step").set(0) - og.Controller.attribute(depth_camera_gate_path + ".inputs:step").set(0) - og.Controller.attribute(camera_info_gate_path + ".inputs:step").set(0) - - # Publish the ROS rgb image message every 5 frames - if frame % 5 == 0: - # Enable rgb Branch node to start publishing rgb image - og.Controller.attribute(rgb_camera_gate_path + ".inputs:step").set(1) - - # Publish the ROS Depth image message every 60 frames - if frame % 60 == 0: - # Enable depth Branch node to start publishing depth image - og.Controller.attribute(depth_camera_gate_path + ".inputs:step").set(1) - - # Publish the ROS Camera Info message every frame - og.Controller.attribute(camera_info_gate_path + ".inputs:step").set(1) - - frame = frame + 1 - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_noise.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_noise.py deleted file mode 100644 index 801923367..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_noise.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import sys - -from isaacsim import SimulationApp - -CAMERA_STAGE_PATH = "/Camera" -ROS_CAMERA_GRAPH_PATH = "/ROS_Camera" -BACKGROUND_STAGE_PATH = "/background" -BACKGROUND_USD_PATH = "/Isaac/Environments/Simple_Warehouse/warehouse_with_forklifts.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -simulation_app = SimulationApp(CONFIG) -import carb -import numpy as np -import omni -import omni.graph.core as og -import omni.replicator.core as rep -import omni.syntheticdata._syntheticdata as sd -import warp as wp -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import extensions, stage -from isaacsim.core.utils.render_product import set_camera_prim_path -from isaacsim.storage.native import get_assets_root_path -from omni.kit.viewport.utility import get_active_viewport -from pxr import Gf, Usd, UsdGeom - -# enable ROS bridge extension -extensions.enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Loading the simple_room environment -stage.add_reference_to_stage(assets_root_path + BACKGROUND_USD_PATH, BACKGROUND_STAGE_PATH) - -# Creating a Camera prim -camera_prim = UsdGeom.Camera(omni.usd.get_context().get_stage().DefinePrim(CAMERA_STAGE_PATH, "Camera")) -xform_api = UsdGeom.XformCommonAPI(camera_prim) -xform_api.SetTranslate(Gf.Vec3d(-1, 5, 1)) -xform_api.SetRotate((90, 0, 0), UsdGeom.XformCommonAPI.RotationOrderXYZ) -camera_prim.GetHorizontalApertureAttr().Set(21) -camera_prim.GetVerticalApertureAttr().Set(16) -camera_prim.GetProjectionAttr().Set("perspective") -camera_prim.GetFocalLengthAttr().Set(24) -camera_prim.GetFocusDistanceAttr().Set(400) - -simulation_app.update() - -# grab our render product and directly set the camera prim -render_product_path = get_active_viewport().get_render_product_path() -set_camera_prim_path(render_product_path, CAMERA_STAGE_PATH) - -# GPU Noise Kernel for illustrative purposes, input is rgba, outputs rgb -@wp.kernel -def image_gaussian_noise_warp( - data_in: wp.array3d(dtype=wp.uint8), data_out: wp.array3d(dtype=wp.uint8), seed: int, sigma: float = 0.5 -): - i, j = wp.tid() - dim_i = data_out.shape[0] - dim_j = data_out.shape[1] - pixel_id = i * dim_i + j - state_r = wp.rand_init(seed, pixel_id + (dim_i * dim_j * 0)) - state_g = wp.rand_init(seed, pixel_id + (dim_i * dim_j * 1)) - state_b = wp.rand_init(seed, pixel_id + (dim_i * dim_j * 2)) - - data_out[i, j, 0] = wp.uint8(float(data_in[i, j, 0]) + (255.0 * sigma * wp.randn(state_r))) - data_out[i, j, 1] = wp.uint8(float(data_in[i, j, 1]) + (255.0 * sigma * wp.randn(state_g))) - data_out[i, j, 2] = wp.uint8(float(data_in[i, j, 2]) + (255.0 * sigma * wp.randn(state_b))) - - -# register new augmented annotator that adds noise to rgba and then outputs to rgb to the ROS publisher can publish -rep.annotators.register( - name="rgb_gaussian_noise", - annotator=rep.annotators.augment_compose( - source_annotator=rep.annotators.get("rgb", device="cuda"), - augmentations=[ - rep.annotators.Augmentation.from_function( - image_gaussian_noise_warp, sigma=0.1, seed=1234, data_out_shape=(-1, -1, 3) - ), - ], - ), -) - -# Create a new writer with the augmented image -rep.writers.register_node_writer( - name=f"CustomROS1PublishImage", - node_type_id="isaacsim.ros1.bridge.ROS1PublishImage", - annotators=[ - "rgb_gaussian_noise", - omni.syntheticdata.SyntheticData.NodeConnectionTemplate( - "IsaacReadSimulationTime", attributes_mapping={"outputs:simulationTime": "inputs:timeStamp"} - ), - ], - category="custom", -) -# Register writer for Replicator telemetry tracking -rep.WriterRegistry._default_writers.append( - "CustomROS1PublishImage" -) if "CustomROS1PublishImage" not in rep.WriterRegistry._default_writers else None - -# Create the new writer and attach to our render product -writer = rep.writers.get(f"CustomROS1PublishImage") -writer.initialize(topicName="rgb_augmented", frameId="sim_camera") -writer.attach([render_product_path]) - -simulation_app.update() -# Need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() -simulation_context.play() - -frame = 0 - -while simulation_app.is_running(): - # Run with a fixed step size - simulation_context.step(render=True) - - if simulation_context.is_playing(): - # Rotate camera by 0.5 degree every frame - xform_api.SetRotate((90, 0, frame / 4.0), UsdGeom.XformCommonAPI.RotationOrderXYZ) - - frame = frame + 1 - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_periodic.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_periodic.py deleted file mode 100644 index b192d444b..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/camera_periodic.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import sys - -from isaacsim import SimulationApp - -CAMERA_STAGE_PATH = "/Camera" -ROS_CAMERA_GRAPH_PATH = "/ROS_Camera" -BACKGROUND_STAGE_PATH = "/background" -BACKGROUND_USD_PATH = "/Isaac/Environments/Simple_Warehouse/warehouse_with_forklifts.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -simulation_app = SimulationApp(CONFIG) -import carb -import omni -import omni.graph.core as og -import usdrt.Sdf -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import extensions, stage -from isaacsim.storage.native import get_assets_root_path -from omni.kit.viewport.utility import get_active_viewport -from pxr import Gf, Usd, UsdGeom - -# enable ROS bridge extension -extensions.enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Loading the simple_room environment -stage.add_reference_to_stage(assets_root_path + BACKGROUND_USD_PATH, BACKGROUND_STAGE_PATH) - -# Creating a Camera prim -camera_prim = UsdGeom.Camera(omni.usd.get_context().get_stage().DefinePrim(CAMERA_STAGE_PATH, "Camera")) -xform_api = UsdGeom.XformCommonAPI(camera_prim) -xform_api.SetTranslate(Gf.Vec3d(-1, 5, 1)) -xform_api.SetRotate((90, 0, 0), UsdGeom.XformCommonAPI.RotationOrderXYZ) -camera_prim.GetHorizontalApertureAttr().Set(21) -camera_prim.GetVerticalApertureAttr().Set(16) -camera_prim.GetProjectionAttr().Set("perspective") -camera_prim.GetFocalLengthAttr().Set(24) -camera_prim.GetFocusDistanceAttr().Set(400) - -simulation_app.update() - -# Creating an on-demand push graph with cameraHelper nodes to generate ROS image publishers -keys = og.Controller.Keys -(ros_camera_graph, _, _, _) = og.Controller.edit( - { - "graph_path": ROS_CAMERA_GRAPH_PATH, - "evaluator_name": "push", - "pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND, - }, - { - keys.CREATE_NODES: [ - ("OnTick", "omni.graph.action.OnTick"), - ("createViewport", "isaacsim.core.nodes.IsaacCreateViewport"), - ("getRenderProduct", "isaacsim.core.nodes.IsaacGetViewportRenderProduct"), - ("setCamera", "isaacsim.core.nodes.IsaacSetCameraOnRenderProduct"), - ("cameraHelperRgb", "isaacsim.ros1.bridge.ROS1CameraHelper"), - ("cameraHelperInfo", "isaacsim.ros1.bridge.ROS1CameraHelper"), - ("cameraHelperDepth", "isaacsim.ros1.bridge.ROS1CameraHelper"), - ], - keys.CONNECT: [ - ("OnTick.outputs:tick", "createViewport.inputs:execIn"), - ("createViewport.outputs:execOut", "getRenderProduct.inputs:execIn"), - ("createViewport.outputs:viewport", "getRenderProduct.inputs:viewport"), - ("getRenderProduct.outputs:execOut", "setCamera.inputs:execIn"), - ("getRenderProduct.outputs:renderProductPath", "setCamera.inputs:renderProductPath"), - ("setCamera.outputs:execOut", "cameraHelperRgb.inputs:execIn"), - ("setCamera.outputs:execOut", "cameraHelperInfo.inputs:execIn"), - ("setCamera.outputs:execOut", "cameraHelperDepth.inputs:execIn"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperRgb.inputs:renderProductPath"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperInfo.inputs:renderProductPath"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperDepth.inputs:renderProductPath"), - ], - keys.SET_VALUES: [ - ("createViewport.inputs:viewportId", 0), - ("cameraHelperRgb.inputs:frameId", "sim_camera"), - ("cameraHelperRgb.inputs:topicName", "rgb"), - ("cameraHelperRgb.inputs:type", "rgb"), - ("cameraHelperInfo.inputs:frameId", "sim_camera"), - ("cameraHelperInfo.inputs:topicName", "camera_info"), - ("cameraHelperInfo.inputs:type", "camera_info"), - ("cameraHelperDepth.inputs:frameId", "sim_camera"), - ("cameraHelperDepth.inputs:topicName", "depth"), - ("cameraHelperDepth.inputs:type", "depth"), - ("setCamera.inputs:cameraPrim", [usdrt.Sdf.Path(CAMERA_STAGE_PATH)]), - ], - }, -) - -# Run the ROS Camera graph once to generate ROS image publishers in SDGPipeline -og.Controller.evaluate_sync(ros_camera_graph) - -simulation_app.update() - -# Inside the SDGPipeline graph, Isaac Simulation Gate nodes are added to control the execution rate of each of the ROS image and camera info publishers. -# By default the step input of each Isaac Simulation Gate node is set to a value of 1 to execute every frame. -# We can change this value to N for each Isaac Simulation Gate node individually to publish every N number of frames. -viewport_api = get_active_viewport() - -if viewport_api is not None: - import omni.syntheticdata._syntheticdata as sd - - # Get name of rendervar for RGB sensor type - rv_rgb = omni.syntheticdata.SyntheticData.convert_sensor_type_to_rendervar(sd.SensorType.Rgb.name) - - # Get path to IsaacSimulationGate node in RGB pipeline - rgb_camera_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - rv_rgb + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - # Get name of rendervar for DistanceToImagePlane sensor type - rv_depth = omni.syntheticdata.SyntheticData.convert_sensor_type_to_rendervar( - sd.SensorType.DistanceToImagePlane.name - ) - - # Get path to IsaacSimulationGate node in Depth pipeline - depth_camera_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - rv_depth + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - # Get path to IsaacSimulationGate node in CameraInfo pipeline - camera_info_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - "PostProcessDispatch" + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - # Set Rgb execution step to 5 frames - rgb_step_size = 5 - - # Set Depth execution step to 60 frames - depth_step_size = 60 - - # Set Camera info execution step to every frame - info_step_size = 1 - - # Set step input of the Isaac Simulation Gate nodes upstream of ROS publishers to control their execution rate - og.Controller.attribute(rgb_camera_gate_path + ".inputs:step").set(rgb_step_size) - og.Controller.attribute(depth_camera_gate_path + ".inputs:step").set(depth_step_size) - og.Controller.attribute(camera_info_gate_path + ".inputs:step").set(info_step_size) - -# Need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - -simulation_context.play() - -frame = 0 - -while simulation_app.is_running(): - # Run with a fixed step size - simulation_context.step(render=True) - - if simulation_context.is_playing(): - # Rotate camera by 0.5 degree every frame - xform_api.SetRotate((90, 0, frame / 4.0), UsdGeom.XformCommonAPI.RotationOrderXYZ) - - frame = frame + 1 - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/carter_multiple_robot_navigation.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/carter_multiple_robot_navigation.py deleted file mode 100644 index 59fb6bd69..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/carter_multiple_robot_navigation.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import argparse -import sys - -parser = argparse.ArgumentParser() -parser.add_argument( - "--environment", - type=str, - choices=["hospital", "office"], - default="hospital", - help="Choice of navigation environment.", -) -args, _ = parser.parse_known_args() - -HOSPITAL_USD_PATH = "/Isaac/Samples/ROS/Scenario/multiple_robot_carter_hospital_navigation.usd" -OFFICE_USD_PATH = "/Isaac/Samples/ROS/Scenario/multiple_robot_carter_office_navigation.usd" - -if args.environment == "hospital": - ENV_USD_PATH = HOSPITAL_USD_PATH -elif args.environment == "office": - ENV_USD_PATH = OFFICE_USD_PATH - -import carb -from isaacsim import SimulationApp - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -# Example ROS bridge sample demonstrating the manual loading of Multiple Robot Navigation scenario -simulation_app = SimulationApp(CONFIG) -import omni -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path - -# enable ROS bridge extension -enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -# Locate assets root folder to load sample -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -usd_path = assets_root_path + ENV_USD_PATH -omni.usd.get_context().open_stage(usd_path, None) - -# Wait two frames so that stage starts loading -simulation_app.update() -simulation_app.update() - -print("Loading stage...") -from isaacsim.core.utils.stage import is_stage_loading - -while is_stage_loading(): - simulation_app.update() -print("Loading Complete") - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -simulation_app.update() - -simulation_context.play() - -simulation_app.update() - -while simulation_app.is_running(): - - # runs with a realtime clock - simulation_app.update() - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/carter_stereo.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/carter_stereo.py deleted file mode 100644 index cc30305c6..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/carter_stereo.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -from isaacsim import SimulationApp - -parser = argparse.ArgumentParser(description="Carter Stereo Example") -parser.add_argument("--test", action="store_true") -args, unknown = parser.parse_known_args() - -# Example ROS bridge sample showing manual control over messages -simulation_app = SimulationApp({"renderer": "RaytracedLighting", "headless": False}) -import carb -import omni -import omni.graph.core as og -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path -from pxr import Sdf - -# enable ROS bridge extension -enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -# Locate assets root folder to load sample -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - exit() - -usd_path = assets_root_path + "/Isaac/Samples/ROS/Scenario/carter_warehouse_navigation.usd" -omni.usd.get_context().open_stage(usd_path, None) - -# Wait two frames so that stage starts loading -simulation_app.update() -simulation_app.update() - -print("Loading stage...") -from isaacsim.core.utils.stage import is_stage_loading - -while is_stage_loading(): - simulation_app.update() -print("Loading Complete") - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -ros_cameras_graph_path = "/World/Carter_ROS/ROS_Cameras" - -# Enabling rgb and depth image publishers for left camera. Cameras will automatically publish images each frame -og.Controller.set( - og.Controller.attribute(ros_cameras_graph_path + "/isaac_create_render_product_left.inputs:enabled"), True -) - -# Enabling rgb and depth image publishers for right camera. Cameras will automatically publish images each frame -og.Controller.set( - og.Controller.attribute(ros_cameras_graph_path + "/isaac_create_render_product_right.inputs:enabled"), True -) - - -simulation_context.play() -simulation_context.step() - -# Simulate for one second to warm up sim and let everything settle -for frame in range(60): - simulation_context.step() - -# Dock the second camera window -left_viewport = omni.ui.Workspace.get_window("Viewport") -right_viewport = omni.ui.Workspace.get_window("Viewport 2") -if right_viewport is not None and left_viewport is not None: - right_viewport.dock_in(left_viewport, omni.ui.DockPosition.RIGHT) -right_viewport = None -left_viewport = None - -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -import rospy - -# Create a rostopic to publish message to spin robot in place -# Note that this is not the system level rospy, but one compiled for omniverse -from geometry_msgs.msg import Twist - -rospy.init_node("carter_stereo", anonymous=True, disable_signals=True, log_level=rospy.ERROR) -pub = rospy.Publisher("cmd_vel", Twist, queue_size=10) - -frame = 0 -while simulation_app.is_running(): - # Run with a fixed step size - simulation_context.step(render=True) - - # Publish the ROS Twist message every 2 frames - if frame % 2 == 0: - message = Twist() - message.angular.z = 0.5 # spin in place - pub.publish(message) - - if args.test and frame > 120: - break - frame = frame + 1 -pub.unregister() -rospy.signal_shutdown("carter_stereo complete") -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/clock.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/clock.py deleted file mode 100644 index 0b0391de6..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/clock.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import time - -from isaacsim import SimulationApp - -parser = argparse.ArgumentParser(description="ROS Clock Example") -parser.add_argument("--test", action="store_true") -args, unknown = parser.parse_known_args() - - -# Example ROS bridge sample showing rospy and rosclock interaction -simulation_app = SimulationApp({"renderer": "RaytracedLighting", "headless": True}) -import carb -import omni -import omni.graph.core as og -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.extensions import enable_extension - -if args.test: - from isaacsim.ros1.bridge.scripts.roscore import Roscore - from isaacsim.ros1.bridge.tests.common import wait_for_rosmaster - - roscore = Roscore() - wait_for_rosmaster() - -# enable ROS bridge extension -enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() -import rospy - -# Note that this is not the system level rospy, but one compiled for omniverse -from rosgraph_msgs.msg import Clock - -clock_topic = "sim_time" -manual_clock_topic = "manual_time" - -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) - -# Creating a action graph with ROS component nodes -try: - og.Controller.edit( - {"graph_path": "/ActionGraph", "evaluator_name": "execution"}, - { - og.Controller.Keys.CREATE_NODES: [ - ("ReadSimTime", "isaacsim.core.nodes.IsaacReadSimulationTime"), - ("OnPlaybackTick", "omni.graph.action.OnPlaybackTick"), - ("PublishClock", "isaacsim.ros1.bridge.ROS1PublishClock"), - ("OnImpulseEvent", "omni.graph.action.OnImpulseEvent"), - ("PublishManualClock", "isaacsim.ros1.bridge.ROS1PublishClock"), - ], - og.Controller.Keys.CONNECT: [ - # Connecting execution of OnPlaybackTick node to PublishClock to automatically publish each frame - ("OnPlaybackTick.outputs:tick", "PublishClock.inputs:execIn"), - # Connecting execution of OnImpulseEvent node to PublishManualClock so it will only publish when an impulse event is triggered - ("OnImpulseEvent.outputs:execOut", "PublishManualClock.inputs:execIn"), - # Connecting simulationTime data of ReadSimTime to the clock publisher nodes - ("ReadSimTime.outputs:simulationTime", "PublishClock.inputs:timeStamp"), - ("ReadSimTime.outputs:simulationTime", "PublishManualClock.inputs:timeStamp"), - ], - og.Controller.Keys.SET_VALUES: [ - # Assigning topic names to clock publishers - ("PublishClock.inputs:topicName", clock_topic), - ("PublishManualClock.inputs:topicName", manual_clock_topic), - ], - }, - ) -except Exception as e: - print(e) - - -simulation_app.update() -simulation_app.update() - - -# Define ROS callbacks -def sim_clock_callback(data): - print("sim time:", data.clock.to_sec()) - - -def manual_clock_callback(data): - print("manual stepped sim time:", data.clock.to_sec()) - - -# Create rospy ndoe -rospy.init_node("isaac_sim_clock", anonymous=True, disable_signals=True, log_level=rospy.ERROR) -# create subscribers -sim_clock_sub = rospy.Subscriber(clock_topic, Clock, sim_clock_callback) -manual_clock_sub = rospy.Subscriber(manual_clock_topic, Clock, manual_clock_callback) -time.sleep(1.0) -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - -simulation_context.play() - -# perform a fixed number of steps with fixed step size -for frame in range(20): - - # publish manual clock every 10 frames - if frame % 10 == 0: - og.Controller.set(og.Controller.attribute("/ActionGraph/OnImpulseEvent.state:enableImpulse"), True) - simulation_context.render() # This updates rendering/app loop which calls the sim clock - - simulation_context.step(render=False) # runs with a non-realtime clock - # This sleep is to make this sample run a bit more deterministically for the subscriber callback - # In general this sleep is not needed - time.sleep(0.1) - -# perform a fixed number of steps with realtime clock -for frame in range(20): - - # publish manual clock every 10 frames - if frame % 10 == 0: - og.Controller.set(og.Controller.attribute("/ActionGraph/OnImpulseEvent.state:enableImpulse"), True) - - simulation_app.update() # runs with a realtime clock - # This sleep is to make this sample run a bit more deterministically for the subscriber callback - # In general this sleep is not needed - time.sleep(0.1) - -# cleanup and shutdown -sim_clock_sub.unregister() -manual_clock_sub.unregister() -simulation_context.stop() - -if args.test: - roscore = None - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/contact.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/contact.py deleted file mode 100644 index 88110d8b8..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/contact.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"renderer": "RaytracedLighting", "headless": True}) - -import carb -import omni -import omni.kit.commands -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.sensors.physics import _sensor -from pxr import Gf - -# enable ROS bridge extension -enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -# Note that this is not the system level rospy, but one compiled for omniverse -import numpy as np -import rospy - -try: - from isaac_tutorials.msg import ContactSensor -except ModuleNotFoundError: - carb.log_error("isaac_tutorials message definition was not found, please source the ros workspace") - simulation_app.close() - exit() - -rospy.init_node("contact_sample", anonymous=True, disable_signals=True, log_level=rospy.ERROR) - -timeline = omni.timeline.get_timeline_interface() -contact_pub = rospy.Publisher("/contact_report", ContactSensor, queue_size=0) -cs = _sensor.acquire_contact_sensor_interface() - -meters_per_unit = 1.0 -ros_world = World(stage_units_in_meters=1.0) - -# add a cube in the world -cube_path = "/cube" -cube_1 = ros_world.scene.add( - DynamicCuboid(prim_path=cube_path, name="cube_1", position=np.array([0, 0, 1.5]), size=1.0) -) - -simulation_app.update() - -# Add a plane for cube to collide with -ros_world.scene.add_default_ground_plane() - -simulation_app.update() - - -# putting contact sensor in the ContactSensor Message format -def format_contact(c_out, contact): - c_out.time = float(contact.time) - c_out.value = float(contact.value * meters_per_unit) - c_out.in_contact = bool(contact.inContact) - return c_out - - -# Setup contact sensor on cube -result, sensor = omni.kit.commands.execute( - "IsaacSensorCreateContactSensor", - path="/Contact_Sensor", - parent=cube_path, - min_threshold=0, - max_threshold=100000000, - color=Gf.Vec4f(1, 1, 1, 1), - radius=-1, - sensor_period=1.0 / 60.0, - translation=Gf.Vec3d(0, 0, 0), -) -simulation_app.update() - -# initiate the message handle -c_out = ContactSensor() - -# start simulation -timeline.play() - - -for frame in range(10000): - ros_world.step(render=True) - - # Get processed contact data - reading = cs.get_sensor_reading(cube_path + "/Contact_Sensor") - if reading.is_valid: - print(f"Valid contact : inContact = {reading.inContact}, value = {reading.value}, time = {reading.time}") - # pack the raw data into ContactSensor format and publish it - c = format_contact(c_out, reading) - contact_pub.publish(c) - - -# Cleanup -timeline.stop() -contact_pub.unregister() -rospy.signal_shutdown("contact_sample complete") -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/moveit.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/moveit.py deleted file mode 100644 index d9f047419..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/moveit.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import sys - -import numpy as np -from isaacsim import SimulationApp - -FRANKA_STAGE_PATH = "/Franka" -FRANKA_USD_PATH = "/Isaac/Robots/Franka/franka_alt_fingers.usd" -BACKGROUND_STAGE_PATH = "/background" -BACKGROUND_USD_PATH = "/Isaac/Environments/Simple_Room/simple_room.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -# Example ROS bridge sample demonstrating the manual loading of stages -# and creation of ROS components -simulation_app = SimulationApp(CONFIG) -import carb -import omni.graph.core as og -import usdrt.Sdf -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import extensions, prims, rotations, stage, viewports -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf - -# enable ROS bridge extension -extensions.enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Preparing stage -viewports.set_camera_view(eye=np.array([1.2, 1.2, 0.8]), target=np.array([0, 0, 0.5])) - -# Loading the simple_room environment -stage.add_reference_to_stage(assets_root_path + BACKGROUND_USD_PATH, BACKGROUND_STAGE_PATH) - -# Loading the franka robot USD -prims.create_prim( - FRANKA_STAGE_PATH, - "Xform", - position=np.array([0, -0.64, 0]), - orientation=rotations.gf_rotation_to_np_array(Gf.Rotation(Gf.Vec3d(0, 0, 1), 90)), - usd_path=assets_root_path + FRANKA_USD_PATH, -) - -simulation_app.update() - -# Creating a action graph with ROS component nodes -try: - og.Controller.edit( - {"graph_path": "/ActionGraph", "evaluator_name": "execution"}, - { - og.Controller.Keys.CREATE_NODES: [ - ("OnImpulseEvent", "omni.graph.action.OnImpulseEvent"), - ("ReadSimTime", "isaacsim.core.nodes.IsaacReadSimulationTime"), - ("PublishJointState", "isaacsim.ros1.bridge.ROS1PublishJointState"), - ("SubscribeJointState", "isaacsim.ros1.bridge.ROS1SubscribeJointState"), - ("ArticulationController", "isaacsim.core.nodes.IsaacArticulationController"), - ("PublishTF", "isaacsim.ros1.bridge.ROS1PublishTransformTree"), - ("PublishClock", "isaacsim.ros1.bridge.ROS1PublishClock"), - ], - og.Controller.Keys.CONNECT: [ - ("OnImpulseEvent.outputs:execOut", "PublishJointState.inputs:execIn"), - ("OnImpulseEvent.outputs:execOut", "SubscribeJointState.inputs:execIn"), - ("OnImpulseEvent.outputs:execOut", "PublishTF.inputs:execIn"), - ("OnImpulseEvent.outputs:execOut", "PublishClock.inputs:execIn"), - ("OnImpulseEvent.outputs:execOut", "ArticulationController.inputs:execIn"), - ("ReadSimTime.outputs:simulationTime", "PublishJointState.inputs:timeStamp"), - ("ReadSimTime.outputs:simulationTime", "PublishClock.inputs:timeStamp"), - ("ReadSimTime.outputs:simulationTime", "PublishTF.inputs:timeStamp"), - ("SubscribeJointState.outputs:jointNames", "ArticulationController.inputs:jointNames"), - ("SubscribeJointState.outputs:positionCommand", "ArticulationController.inputs:positionCommand"), - ("SubscribeJointState.outputs:velocityCommand", "ArticulationController.inputs:velocityCommand"), - ("SubscribeJointState.outputs:effortCommand", "ArticulationController.inputs:effortCommand"), - ], - og.Controller.Keys.SET_VALUES: [ - # Setting the /Franka target prim to Articulation Controller node - ("ArticulationController.inputs:robotPath", FRANKA_STAGE_PATH), - ("PublishJointState.inputs:targetPrim", [usdrt.Sdf.Path(FRANKA_STAGE_PATH)]), - ("PublishTF.inputs:targetPrims", [usdrt.Sdf.Path(FRANKA_STAGE_PATH)]), - ], - }, - ) -except Exception as e: - print(e) - - -simulation_app.update() - -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - -simulation_context.play() - -while simulation_app.is_running(): - - # Run with a fixed step size - simulation_context.step(render=True) - - # Tick the Publish/Subscribe JointState, Publish TF and Publish Clock nodes each frame - og.Controller.set(og.Controller.attribute("/ActionGraph/OnImpulseEvent.state:enableImpulse"), True) - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/rtx_lidar.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/rtx_lidar.py deleted file mode 100644 index babf2eb2a..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/rtx_lidar.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import sys - -from isaacsim import SimulationApp - -# Example for creating a RTX lidar sensor and publishing PCL data -simulation_app = SimulationApp({"headless": False}) -import carb -import omni -import omni.kit.viewport.utility -import omni.replicator.core as rep -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import stage -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf - -# enable ROS bridge extension -enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -simulation_app.update() -# Loading the simple_room environment -stage.add_reference_to_stage( - assets_root_path + "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd", "/background" -) -simulation_app.update() - -# Create the lidar sensor that generates data into "RtxSensorCpu" -# Sensor needs to be rotated 90 degrees about X so that its Z up - -# Possible options are Example_Rotary and Example_Solid_State -# drive sim applies 0.5,-0.5,-0.5,w(-0.5), we have to apply the reverse -_, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxLidar", - path="/sensor", - parent=None, - config="Example_Rotary", - translation=(0, 0, 1.0), - orientation=Gf.Quatd(1.0, 0.0, 0.0, 0.0), # Gf.Quatd is w,i,j,k -) - -hydra_texture = rep.create.render_product(sensor.GetPath(), [1, 1], name="Isaac") - -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) -simulation_app.update() - -# Create Point cloud publisher pipeline in the post process graph -writer = rep.writers.get("RtxLidar" + "ROS1PublishPointCloud") -writer.initialize(topicName="point_cloud", frameId="sim_lidar") -writer.attach([hydra_texture]) - -# Create the debug draw pipeline in the post process graph -writer = rep.writers.get("RtxLidar" + "DebugDrawPointCloud") -writer.attach([hydra_texture]) - -# Create LaserScan publisher pipeline in the post process graph -writer = rep.writers.get("RtxLidar" + "ROS1PublishLaserScan") -writer.initialize(topicName="laser_scan", frameId="sim_lidar") -writer.attach([hydra_texture]) -simulation_app.update() - -simulation_context.play() - -while simulation_app.is_running(): - simulation_app.update() - -# cleanup and shutdown -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/subscriber.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/subscriber.py deleted file mode 100644 index a3b7fce48..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros1.bridge/subscriber.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"renderer": "RaytracedLighting", "headless": False}) - -import carb -import omni -from isaacsim.core.api import World -from isaacsim.core.api.objects import VisualCuboid -from isaacsim.core.utils.extensions import enable_extension - -# enable ROS bridge extension -enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -# check if rosmaster node is running -# this is to prevent this sample from waiting indefinetly if roscore is not running -# can be removed in regular usage -import rosgraph - -if not rosgraph.is_master_online(): - carb.log_error("Please run roscore before executing this script") - simulation_app.close() - exit() - -import time - -# Note that this is not the system level rospy, but one compiled for omniverse -import numpy as np -import rospy -from std_msgs.msg import Empty - - -class Subscriber: - def __init__(self): - # setting up the world with a cube - self.timeline = omni.timeline.get_timeline_interface() - self.ros_world = World(stage_units_in_meters=1.0) - self.ros_world.scene.add_default_ground_plane() - # add a cube in the world - cube_path = "/cube" - self.ros_world.scene.add( - VisualCuboid(prim_path=cube_path, name="cube_1", position=np.array([0, 0, 10]), size=0.2) - ) - self._cube_position = np.array([0, 0, 0]) - - # setup the ros subscriber here - self.ros_sub = rospy.Subscriber("/move_cube", Empty, self.move_cube_callback, queue_size=10) - - self.ros_world.reset() - - def move_cube_callback(self, data): - # callback function to set the cube position to a new one upon receiving a (empty) ros message - if self.ros_world.is_playing(): - self._cube_position = np.array([np.random.rand() * 0.40, np.random.rand() * 0.40, 0.10]) - - def run_simulation(self): - self.timeline.play() - reset_needed = False - while simulation_app.is_running(): - self.ros_world.step(render=True) - if self.ros_world.is_stopped() and not reset_needed: - reset_needed = True - if self.ros_world.is_playing(): - if reset_needed: - self.ros_world.reset() - reset_needed = False - # the actual setting the cube pose is done here - self.ros_world.scene.get_object("cube_1").set_world_pose(self._cube_position) - - # Cleanup - self.ros_sub.unregister() - rospy.signal_shutdown("subscriber example complete") - self.timeline.stop() - simulation_app.close() - - -if __name__ == "__main__": - rospy.init_node("tutorial_subscriber", anonymous=True, disable_signals=True, log_level=rospy.ERROR) - subscriber = Subscriber() - subscriber.run_simulation() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/camera_manual.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/camera_manual.py deleted file mode 100644 index b941dc0f9..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/camera_manual.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import sys - -from isaacsim import SimulationApp - -CAMERA_STAGE_PATH = "/Camera" -ROS_CAMERA_GRAPH_PATH = "/ROS_Camera" -BACKGROUND_STAGE_PATH = "/background" -BACKGROUND_USD_PATH = "/Isaac/Environments/Simple_Warehouse/warehouse_with_forklifts.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -# Example ROS2 bridge sample demonstrating the manual loading of stages and manual publishing of images -simulation_app = SimulationApp(CONFIG) -import carb -import omni -import omni.graph.core as og -import usdrt.Sdf -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import extensions, stage -from isaacsim.storage.native import get_assets_root_path -from omni.kit.viewport.utility import get_active_viewport -from pxr import Gf, Usd, UsdGeom - -# enable ROS2 bridge extension -extensions.enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Loading the simple_room environment -stage.add_reference_to_stage(assets_root_path + BACKGROUND_USD_PATH, BACKGROUND_STAGE_PATH) - -# Creating a Camera prim -camera_prim = UsdGeom.Camera(omni.usd.get_context().get_stage().DefinePrim(CAMERA_STAGE_PATH, "Camera")) -xform_api = UsdGeom.XformCommonAPI(camera_prim) -xform_api.SetTranslate(Gf.Vec3d(-1, 5, 1)) -xform_api.SetRotate((90, 0, 0), UsdGeom.XformCommonAPI.RotationOrderXYZ) -camera_prim.GetHorizontalApertureAttr().Set(21) -camera_prim.GetVerticalApertureAttr().Set(16) -camera_prim.GetProjectionAttr().Set("perspective") -camera_prim.GetFocalLengthAttr().Set(24) -camera_prim.GetFocusDistanceAttr().Set(400) - -simulation_app.update() - -# Creating an on-demand push graph with cameraHelper nodes to generate ROS image publishers - -keys = og.Controller.Keys -(ros_camera_graph, _, _, _) = og.Controller.edit( - { - "graph_path": ROS_CAMERA_GRAPH_PATH, - "evaluator_name": "push", - "pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND, - }, - { - keys.CREATE_NODES: [ - ("OnTick", "omni.graph.action.OnTick"), - ("createViewport", "isaacsim.core.nodes.IsaacCreateViewport"), - ("getRenderProduct", "isaacsim.core.nodes.IsaacGetViewportRenderProduct"), - ("setCamera", "isaacsim.core.nodes.IsaacSetCameraOnRenderProduct"), - ("cameraHelperRgb", "isaacsim.ros2.bridge.ROS2CameraHelper"), - ("cameraHelperInfo", "isaacsim.ros2.bridge.ROS2CameraInfoHelper"), - ("cameraHelperDepth", "isaacsim.ros2.bridge.ROS2CameraHelper"), - ], - keys.CONNECT: [ - ("OnTick.outputs:tick", "createViewport.inputs:execIn"), - ("createViewport.outputs:execOut", "getRenderProduct.inputs:execIn"), - ("createViewport.outputs:viewport", "getRenderProduct.inputs:viewport"), - ("getRenderProduct.outputs:execOut", "setCamera.inputs:execIn"), - ("getRenderProduct.outputs:renderProductPath", "setCamera.inputs:renderProductPath"), - ("setCamera.outputs:execOut", "cameraHelperRgb.inputs:execIn"), - ("setCamera.outputs:execOut", "cameraHelperInfo.inputs:execIn"), - ("setCamera.outputs:execOut", "cameraHelperDepth.inputs:execIn"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperRgb.inputs:renderProductPath"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperInfo.inputs:renderProductPath"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperDepth.inputs:renderProductPath"), - ], - keys.SET_VALUES: [ - ("createViewport.inputs:viewportId", 0), - ("cameraHelperRgb.inputs:frameId", "sim_camera"), - ("cameraHelperRgb.inputs:topicName", "rgb"), - ("cameraHelperRgb.inputs:type", "rgb"), - ("cameraHelperInfo.inputs:frameId", "sim_camera"), - ("cameraHelperInfo.inputs:topicName", "camera_info"), - ("cameraHelperDepth.inputs:frameId", "sim_camera"), - ("cameraHelperDepth.inputs:topicName", "depth"), - ("cameraHelperDepth.inputs:type", "depth"), - ("setCamera.inputs:cameraPrim", [usdrt.Sdf.Path(CAMERA_STAGE_PATH)]), - ], - }, -) - -# Run the ROS Camera graph once to generate ROS image publishers in SDGPipeline -og.Controller.evaluate_sync(ros_camera_graph) - -simulation_app.update() - -# Use the IsaacSimulationGate step value to block execution on specific frames -SD_GRAPH_PATH = "/Render/PostProcess/SDGPipeline" - -viewport_api = get_active_viewport() - -if viewport_api is not None: - import omni.syntheticdata._syntheticdata as sd - - curr_stage = omni.usd.get_context().get_stage() - - # Required for editing the SDGPipeline graph which exists in the Session Layer - with Usd.EditContext(curr_stage, curr_stage.GetSessionLayer()): - - # Get name of rendervar for RGB sensor type - rv_rgb = omni.syntheticdata.SyntheticData.convert_sensor_type_to_rendervar(sd.SensorType.Rgb.name) - - # Get path to IsaacSimulationGate node in RGB pipeline - rgb_camera_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - rv_rgb + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - rv_depth = omni.syntheticdata.SyntheticData.convert_sensor_type_to_rendervar( - sd.SensorType.DistanceToImagePlane.name - ) - # Get path to IsaacSimulationGate node in Depth pipeline - depth_camera_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - rv_depth + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - # Get path to IsaacSimulationGate node in CameraInfo pipeline - camera_info_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - "PostProcessDispatch" + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - -# Need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - -simulation_context.play() - -frame = 0 - -while simulation_app.is_running() and simulation_context.is_playing(): - # Run with a fixed step size - simulation_context.step(render=True) - - if simulation_context.is_playing(): - # Rotate camera by 0.5 degree every frame - xform_api.SetRotate((90, 0, frame / 4.0), UsdGeom.XformCommonAPI.RotationOrderXYZ) - - # Set the step value for the simulation gates to zero to stop execution - og.Controller.attribute(rgb_camera_gate_path + ".inputs:step").set(0) - og.Controller.attribute(depth_camera_gate_path + ".inputs:step").set(0) - og.Controller.attribute(camera_info_gate_path + ".inputs:step").set(0) - - # Publish the ROS rgb image message every 5 frames - if frame % 5 == 0: - # Enable rgb Branch node to start publishing rgb image - og.Controller.attribute(rgb_camera_gate_path + ".inputs:step").set(1) - - # Publish the ROS Depth image message every 60 frames - if frame % 60 == 0: - # Enable depth Branch node to start publishing depth image - og.Controller.attribute(depth_camera_gate_path + ".inputs:step").set(1) - - # Publish the ROS Camera Info message every frame - og.Controller.attribute(camera_info_gate_path + ".inputs:step").set(1) - - frame = frame + 1 - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/camera_periodic.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/camera_periodic.py deleted file mode 100644 index 0180437e4..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/camera_periodic.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import sys - -from isaacsim import SimulationApp - -CAMERA_STAGE_PATH = "/Camera" -ROS_CAMERA_GRAPH_PATH = "/ROS_Camera" -BACKGROUND_STAGE_PATH = "/background" -BACKGROUND_USD_PATH = "/Isaac/Environments/Simple_Warehouse/warehouse_with_forklifts.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -simulation_app = SimulationApp(CONFIG) -import carb -import omni -import omni.graph.core as og -import usdrt.Sdf -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import extensions, stage -from isaacsim.storage.native import get_assets_root_path -from omni.kit.viewport.utility import get_active_viewport -from pxr import Gf, Usd, UsdGeom - -# enable ROS bridge extension -extensions.enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Loading the simple_room environment -stage.add_reference_to_stage(assets_root_path + BACKGROUND_USD_PATH, BACKGROUND_STAGE_PATH) - -# Creating a Camera prim -camera_prim = UsdGeom.Camera(omni.usd.get_context().get_stage().DefinePrim(CAMERA_STAGE_PATH, "Camera")) -xform_api = UsdGeom.XformCommonAPI(camera_prim) -xform_api.SetTranslate(Gf.Vec3d(-1, 5, 1)) -xform_api.SetRotate((90, 0, 0), UsdGeom.XformCommonAPI.RotationOrderXYZ) -camera_prim.GetHorizontalApertureAttr().Set(21) -camera_prim.GetVerticalApertureAttr().Set(16) -camera_prim.GetProjectionAttr().Set("perspective") -camera_prim.GetFocalLengthAttr().Set(24) -camera_prim.GetFocusDistanceAttr().Set(400) - -simulation_app.update() - -# Creating an on-demand push graph with cameraHelper nodes to generate ROS image publishers -keys = og.Controller.Keys -(ros_camera_graph, _, _, _) = og.Controller.edit( - { - "graph_path": ROS_CAMERA_GRAPH_PATH, - "evaluator_name": "push", - "pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND, - }, - { - keys.CREATE_NODES: [ - ("OnTick", "omni.graph.action.OnTick"), - ("createViewport", "isaacsim.core.nodes.IsaacCreateViewport"), - ("getRenderProduct", "isaacsim.core.nodes.IsaacGetViewportRenderProduct"), - ("setCamera", "isaacsim.core.nodes.IsaacSetCameraOnRenderProduct"), - ("cameraHelperRgb", "isaacsim.ros2.bridge.ROS2CameraHelper"), - ("cameraHelperInfo", "isaacsim.ros2.bridge.ROS2CameraInfoHelper"), - ("cameraHelperDepth", "isaacsim.ros2.bridge.ROS2CameraHelper"), - ], - keys.CONNECT: [ - ("OnTick.outputs:tick", "createViewport.inputs:execIn"), - ("createViewport.outputs:execOut", "getRenderProduct.inputs:execIn"), - ("createViewport.outputs:viewport", "getRenderProduct.inputs:viewport"), - ("getRenderProduct.outputs:execOut", "setCamera.inputs:execIn"), - ("getRenderProduct.outputs:renderProductPath", "setCamera.inputs:renderProductPath"), - ("setCamera.outputs:execOut", "cameraHelperRgb.inputs:execIn"), - ("setCamera.outputs:execOut", "cameraHelperInfo.inputs:execIn"), - ("setCamera.outputs:execOut", "cameraHelperDepth.inputs:execIn"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperRgb.inputs:renderProductPath"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperInfo.inputs:renderProductPath"), - ("getRenderProduct.outputs:renderProductPath", "cameraHelperDepth.inputs:renderProductPath"), - ], - keys.SET_VALUES: [ - ("createViewport.inputs:viewportId", 0), - ("cameraHelperRgb.inputs:frameId", "sim_camera"), - ("cameraHelperRgb.inputs:topicName", "rgb"), - ("cameraHelperRgb.inputs:type", "rgb"), - ("cameraHelperInfo.inputs:frameId", "sim_camera"), - ("cameraHelperInfo.inputs:topicName", "camera_info"), - ("cameraHelperDepth.inputs:frameId", "sim_camera"), - ("cameraHelperDepth.inputs:topicName", "depth"), - ("cameraHelperDepth.inputs:type", "depth"), - ("setCamera.inputs:cameraPrim", [usdrt.Sdf.Path(CAMERA_STAGE_PATH)]), - ], - }, -) - -# Run the ROS Camera graph once to generate ROS image publishers in SDGPipeline -og.Controller.evaluate_sync(ros_camera_graph) - -simulation_app.update() - -# Inside the SDGPipeline graph, Isaac Simulation Gate nodes are added to control the execution rate of each of the ROS image and camera info publishers. -# By default the step input of each Isaac Simulation Gate node is set to a value of 1 to execute every frame. -# We can change this value to N for each Isaac Simulation Gate node individually to publish every N number of frames. -viewport_api = get_active_viewport() - -if viewport_api is not None: - import omni.syntheticdata._syntheticdata as sd - - # Get name of rendervar for RGB sensor type - rv_rgb = omni.syntheticdata.SyntheticData.convert_sensor_type_to_rendervar(sd.SensorType.Rgb.name) - - # Get path to IsaacSimulationGate node in RGB pipeline - rgb_camera_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - rv_rgb + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - # Get name of rendervar for DistanceToImagePlane sensor type - rv_depth = omni.syntheticdata.SyntheticData.convert_sensor_type_to_rendervar( - sd.SensorType.DistanceToImagePlane.name - ) - - # Get path to IsaacSimulationGate node in Depth pipeline - depth_camera_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - rv_depth + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - # Get path to IsaacSimulationGate node in CameraInfo pipeline - camera_info_gate_path = omni.syntheticdata.SyntheticData._get_node_path( - "PostProcessDispatch" + "IsaacSimulationGate", viewport_api.get_render_product_path() - ) - - # Set Rgb execution step to 5 frames - rgb_step_size = 5 - - # Set Depth execution step to 60 frames - depth_step_size = 60 - - # Set Camera info execution step to every frame - info_step_size = 1 - - # Set step input of the Isaac Simulation Gate nodes upstream of ROS publishers to control their execution rate - og.Controller.attribute(rgb_camera_gate_path + ".inputs:step").set(rgb_step_size) - og.Controller.attribute(depth_camera_gate_path + ".inputs:step").set(depth_step_size) - og.Controller.attribute(camera_info_gate_path + ".inputs:step").set(info_step_size) - -# Need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - -simulation_context.play() - -frame = 0 - -while simulation_app.is_running(): - # Run with a fixed step size - simulation_context.step(render=True) - - if simulation_context.is_playing(): - # Rotate camera by 0.5 degree every frame - xform_api.SetRotate((90, 0, frame / 4.0), UsdGeom.XformCommonAPI.RotationOrderXYZ) - - frame = frame + 1 - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/carter_multiple_robot_navigation.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/carter_multiple_robot_navigation.py deleted file mode 100644 index 275d86dc3..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/carter_multiple_robot_navigation.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import argparse -import sys - -parser = argparse.ArgumentParser() -parser.add_argument( - "--environment", - type=str, - choices=["hospital", "office"], - default="hospital", - help="Choice of navigation environment.", -) -args, _ = parser.parse_known_args() - -HOSPITAL_USD_PATH = "/Isaac/Samples/ROS2/Scenario/multiple_robot_carter_hospital_navigation.usd" -OFFICE_USD_PATH = "/Isaac/Samples/ROS2/Scenario/multiple_robot_carter_office_navigation.usd" - -if args.environment == "hospital": - ENV_USD_PATH = HOSPITAL_USD_PATH -elif args.environment == "office": - ENV_USD_PATH = OFFICE_USD_PATH - -import carb -from isaacsim import SimulationApp - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -# Example ROS2 bridge sample demonstrating the manual loading of Multiple Robot Navigation scenario -simulation_app = SimulationApp(CONFIG) -import omni -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path - -# enable ROS2 bridge extension -enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - -# Locate assets root folder to load sample -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -usd_path = assets_root_path + ENV_USD_PATH -omni.usd.get_context().open_stage(usd_path, None) - -# Wait two frames so that stage starts loading -simulation_app.update() -simulation_app.update() - -print("Loading stage...") -from isaacsim.core.utils.stage import is_stage_loading - -while is_stage_loading(): - simulation_app.update() -print("Loading Complete") - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -simulation_app.update() - -simulation_context.play() - -simulation_app.update() - -while simulation_app.is_running(): - - # runs with a realtime clock - simulation_app.update() - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/carter_stereo.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/carter_stereo.py deleted file mode 100644 index 2cdf42b66..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/carter_stereo.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -from isaacsim import SimulationApp - -parser = argparse.ArgumentParser(description="Carter Stereo Example") -parser.add_argument("--test", action="store_true") -args, unknown = parser.parse_known_args() - -# Example ROS2 bridge sample showing manual control over messages -simulation_app = SimulationApp({"renderer": "RaytracedLighting", "headless": False}) -import carb -import omni -import omni.graph.core as og -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path -from pxr import Sdf - -# enable ROS2 bridge extension -enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - -# Locate assets root folder to load sample -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - exit() - -usd_path = assets_root_path + "/Isaac/Samples/ROS2/Scenario/carter_warehouse_navigation.usd" -omni.usd.get_context().open_stage(usd_path, None) - -# Wait two frames so that stage starts loading -simulation_app.update() -simulation_app.update() - -print("Loading stage...") -from isaacsim.core.utils.stage import is_stage_loading - -while is_stage_loading(): - simulation_app.update() -print("Loading Complete") - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -ros_cameras_graph_path = "/World/Nova_Carter_ROS/front_hawk" - -# Enabling rgb image publishers for left camera. Cameras will automatically publish images each frame -og.Controller.set(og.Controller.attribute(ros_cameras_graph_path + "/left_camera_render_product.inputs:enabled"), True) - -# Enabling rgb image publishers for right camera. Cameras will automatically publish images each frame -og.Controller.set(og.Controller.attribute(ros_cameras_graph_path + "/right_camera_render_product.inputs:enabled"), True) - -simulation_context.play() -simulation_context.step() - - -# Simulate for one second to warm up sim and let everything settle -for frame in range(60): - simulation_context.step() - - -# Create a ROS publisher to publish message to spin robot in place - -# If system level rclpy is sourced in bashrc or terminal, it is imported otherwise backup rclpy libraries shipped with Isaac sim is used -import rclpy - -rclpy.init() - -from geometry_msgs.msg import Twist - -node = rclpy.create_node("carter_stereo") -publisher = node.create_publisher(Twist, "cmd_vel", 10) - -frame = 0 -while simulation_app.is_running(): - # Run with a fixed step size - simulation_context.step(render=True) - - # Publish the ROS Twist message every 2 frames - if frame % 2 == 0: - message = Twist() - message.angular.z = 0.5 # spin in place - publisher.publish(message) - - if args.test and frame > 120: - break - frame = frame + 1 -node.destroy_node() -rclpy.shutdown() -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/clock.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/clock.py deleted file mode 100644 index de722245c..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/clock.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import time - -from isaacsim import SimulationApp - -# Example ROS2 bridge sample showing rclpy and rosclock interaction -simulation_app = SimulationApp({"renderer": "RaytracedLighting", "headless": True}) -import carb -import omni -import omni.graph.core as og -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.extensions import enable_extension - -# enable ROS2 bridge extension -enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() -# Note that this is not the system level rclpy, but one compiled for omniverse -import rclpy -from rosgraph_msgs.msg import Clock - -rclpy.init() -clock_topic = "sim_time" -manual_clock_topic = "manual_time" - -# Creating a action graph with ROS component nodes -try: - og.Controller.edit( - {"graph_path": "/ActionGraph", "evaluator_name": "execution"}, - { - og.Controller.Keys.CREATE_NODES: [ - ("ReadSimTime", "isaacsim.core.nodes.IsaacReadSimulationTime"), - ("OnPlaybackTick", "omni.graph.action.OnPlaybackTick"), - ("PublishClock", "isaacsim.ros2.bridge.ROS2PublishClock"), - ("OnImpulseEvent", "omni.graph.action.OnImpulseEvent"), - ("PublishManualClock", "isaacsim.ros2.bridge.ROS2PublishClock"), - ], - og.Controller.Keys.CONNECT: [ - # Connecting execution of OnPlaybackTick node to PublishClock to automatically publish each frame - ("OnPlaybackTick.outputs:tick", "PublishClock.inputs:execIn"), - # Connecting execution of OnImpulseEvent node to PublishManualClock so it will only publish when an impulse event is triggered - ("OnImpulseEvent.outputs:execOut", "PublishManualClock.inputs:execIn"), - # Connecting simulationTime data of ReadSimTime to the clock publisher nodes - ("ReadSimTime.outputs:simulationTime", "PublishClock.inputs:timeStamp"), - ("ReadSimTime.outputs:simulationTime", "PublishManualClock.inputs:timeStamp"), - ], - og.Controller.Keys.SET_VALUES: [ - # Assigning topic names to clock publishers - ("PublishClock.inputs:topicName", clock_topic), - ("PublishManualClock.inputs:topicName", manual_clock_topic), - ], - }, - ) -except Exception as e: - print(e) - - -simulation_app.update() -simulation_app.update() - - -# Define ROS2 callbacks -def sim_clock_callback(data): - print("sim time:", data.clock) - - -def manual_clock_callback(data): - print("manual stepped sim time:", data.clock) - - -# Create rclpy ndoe -node = rclpy.create_node("isaac_sim_clock") - -# create subscribers -sim_clock_sub = node.create_subscription(Clock, clock_topic, sim_clock_callback, 1) -manual_clock_sub = node.create_subscription(Clock, manual_clock_topic, manual_clock_callback, 1) - -time.sleep(1.0) -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - -simulation_context.play() - -# perform a fixed number of steps with fixed step size -for frame in range(20): - - # publish manual clock every 10 frames - if frame % 10 == 0: - og.Controller.set(og.Controller.attribute("/ActionGraph/OnImpulseEvent.state:enableImpulse"), True) - simulation_context.render() # This updates rendering/app loop which calls the sim clock - - simulation_context.step(render=False) # runs with a non-realtime clock - rclpy.spin_once(node, timeout_sec=0.0) # Spin node once - # This sleep is to make this sample run a bit more deterministically for the subscriber callback - # In general this sleep is not needed - time.sleep(0.1) - -# perform a fixed number of steps with realtime clock -for frame in range(20): - - # publish manual clock every 10 frames - if frame % 10 == 0: - og.Controller.set(og.Controller.attribute("/ActionGraph/OnImpulseEvent.state:enableImpulse"), True) - - simulation_app.update() # runs with a realtime clock - rclpy.spin_once(node, timeout_sec=0.0) # Spin node once - # This sleep is to make this sample run a bit more deterministically for the subscriber callback - # In general this sleep is not needed - time.sleep(0.1) - -# shutdown -rclpy.shutdown() -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/moveit.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/moveit.py deleted file mode 100644 index 7436c4851..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/moveit.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import sys - -import numpy as np -from isaacsim import SimulationApp - -FRANKA_STAGE_PATH = "/Franka" -FRANKA_USD_PATH = "/Isaac/Robots/Franka/franka_alt_fingers.usd" -BACKGROUND_STAGE_PATH = "/background" -BACKGROUND_USD_PATH = "/Isaac/Environments/Simple_Room/simple_room.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -# Example ROS2 bridge sample demonstrating the manual loading of stages -# and creation of ROS components -simulation_app = SimulationApp(CONFIG) -import carb -import omni.graph.core as og -import usdrt.Sdf -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import extensions, prims, rotations, stage, viewports -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf - -# enable ROS2 bridge extension -extensions.enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Preparing stage -viewports.set_camera_view(eye=np.array([1.2, 1.2, 0.8]), target=np.array([0, 0, 0.5])) - -# Loading the simple_room environment -stage.add_reference_to_stage(assets_root_path + BACKGROUND_USD_PATH, BACKGROUND_STAGE_PATH) - -# Loading the franka robot USD -prims.create_prim( - FRANKA_STAGE_PATH, - "Xform", - position=np.array([0, -0.64, 0]), - orientation=rotations.gf_rotation_to_np_array(Gf.Rotation(Gf.Vec3d(0, 0, 1), 90)), - usd_path=assets_root_path + FRANKA_USD_PATH, -) - -simulation_app.update() - -# Creating a action graph with ROS component nodes -try: - og.Controller.edit( - {"graph_path": "/ActionGraph", "evaluator_name": "execution"}, - { - og.Controller.Keys.CREATE_NODES: [ - ("OnImpulseEvent", "omni.graph.action.OnImpulseEvent"), - ("ReadSimTime", "isaacsim.core.nodes.IsaacReadSimulationTime"), - ("Context", "isaacsim.ros2.bridge.ROS2Context"), - ("PublishJointState", "isaacsim.ros2.bridge.ROS2PublishJointState"), - ("SubscribeJointState", "isaacsim.ros2.bridge.ROS2SubscribeJointState"), - ("ArticulationController", "isaacsim.core.nodes.IsaacArticulationController"), - ("PublishClock", "isaacsim.ros2.bridge.ROS2PublishClock"), - ], - og.Controller.Keys.CONNECT: [ - ("OnImpulseEvent.outputs:execOut", "PublishJointState.inputs:execIn"), - ("OnImpulseEvent.outputs:execOut", "SubscribeJointState.inputs:execIn"), - ("OnImpulseEvent.outputs:execOut", "PublishClock.inputs:execIn"), - ("OnImpulseEvent.outputs:execOut", "ArticulationController.inputs:execIn"), - ("Context.outputs:context", "PublishJointState.inputs:context"), - ("Context.outputs:context", "SubscribeJointState.inputs:context"), - ("Context.outputs:context", "PublishClock.inputs:context"), - ("ReadSimTime.outputs:simulationTime", "PublishJointState.inputs:timeStamp"), - ("ReadSimTime.outputs:simulationTime", "PublishClock.inputs:timeStamp"), - ("SubscribeJointState.outputs:jointNames", "ArticulationController.inputs:jointNames"), - ( - "SubscribeJointState.outputs:positionCommand", - "ArticulationController.inputs:positionCommand", - ), - ( - "SubscribeJointState.outputs:velocityCommand", - "ArticulationController.inputs:velocityCommand", - ), - ("SubscribeJointState.outputs:effortCommand", "ArticulationController.inputs:effortCommand"), - ], - og.Controller.Keys.SET_VALUES: [ - # Setting the /Franka target prim to Articulation Controller node - ("ArticulationController.inputs:robotPath", FRANKA_STAGE_PATH), - ("PublishJointState.inputs:topicName", "isaac_joint_states"), - ("SubscribeJointState.inputs:topicName", "isaac_joint_commands"), - ("PublishJointState.inputs:targetPrim", [usdrt.Sdf.Path(FRANKA_STAGE_PATH)]), - ("PublishTF.inputs:targetPrims", [usdrt.Sdf.Path(FRANKA_STAGE_PATH)]), - ], - }, - ) -except Exception as e: - print(e) - -simulation_app.update() - -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - -simulation_context.play() - -while simulation_app.is_running(): - - # Run with a fixed step size - simulation_context.step(render=True) - - # Tick the Publish/Subscribe JointState and Publish Clock nodes each frame - og.Controller.set(og.Controller.attribute("/ActionGraph/OnImpulseEvent.state:enableImpulse"), True) - -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/rtx_lidar.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/rtx_lidar.py deleted file mode 100644 index d032e90f6..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/rtx_lidar.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import sys - -from isaacsim import SimulationApp - -# Example for creating a RTX lidar sensor and publishing PointCloud2 data -simulation_app = SimulationApp({"headless": False}) -import carb -import omni -import omni.kit.viewport.utility -import omni.replicator.core as rep -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import stage -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf - -# enable ROS2 bridge extension -enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -simulation_app.update() -# Loading the simple_room environment -stage.add_reference_to_stage( - assets_root_path + "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd", "/background" -) -simulation_app.update() - -# Create the lidar sensor that generates data into "RtxSensorCpu" -# Sensor needs to be rotated 90 degrees about X so that its Z up - -# Possible options are Example_Rotary and Example_Solid_State -# drive sim applies 0.5,-0.5,-0.5,w(-0.5), we have to apply the reverse -_, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxLidar", - path="/sensor", - parent=None, - config="Example_Rotary", - translation=(0, 0, 1.0), - orientation=Gf.Quatd(1.0, 0.0, 0.0, 0.0), # Gf.Quatd is w,i,j,k -) - -# RTX sensors are cameras and must be assigned to their own render product -hydra_texture = rep.create.render_product(sensor.GetPath(), [1, 1], name="Isaac") - -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) -simulation_app.update() - -# Create Point cloud publisher pipeline in the post process graph -writer = rep.writers.get("RtxLidar" + "ROS2PublishPointCloud") -writer.initialize(topicName="point_cloud", frameId="base_scan") -writer.attach([hydra_texture]) - -# Create the debug draw pipeline in the post process graph -writer = rep.writers.get("RtxLidar" + "DebugDrawPointCloud") -writer.attach([hydra_texture]) - - -# Create LaserScan publisher pipeline in the post process graph -writer = rep.writers.get("RtxLidar" + "ROS2PublishLaserScan") -writer.initialize(topicName="scan", frameId="base_scan") -writer.attach([hydra_texture]) - -simulation_app.update() - -simulation_context.play() - -while simulation_app.is_running(): - simulation_app.update() - -# cleanup and shutdown -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/subscriber.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/subscriber.py deleted file mode 100644 index 8c51d224c..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.ros2.bridge/subscriber.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"renderer": "RaytracedLighting", "headless": False}) - -import omni -from isaacsim.core.api import World -from isaacsim.core.api.objects import VisualCuboid -from isaacsim.core.utils.extensions import enable_extension - -# enable ROS2 bridge extension -enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - -import time - -# Note that this is not the system level rclpy, but one compiled for omniverse -import numpy as np -import rclpy -from rclpy.node import Node -from std_msgs.msg import Empty - - -class Subscriber(Node): - def __init__(self): - super().__init__("tutorial_subscriber") - - # setting up the world with a cube - self.timeline = omni.timeline.get_timeline_interface() - self.ros_world = World(stage_units_in_meters=1.0) - self.ros_world.scene.add_default_ground_plane() - # add a cube in the world - cube_path = "/cube" - self.ros_world.scene.add( - VisualCuboid(prim_path=cube_path, name="cube_1", position=np.array([0, 0, 10]), size=0.2) - ) - self._cube_position = np.array([0, 0, 0]) - - # setup the ROS2 subscriber here - self.ros_sub = self.create_subscription(Empty, "move_cube", self.move_cube_callback, 10) - self.ros_world.reset() - - def move_cube_callback(self, data): - # callback function to set the cube position to a new one upon receiving a (empty) ROS2 message - if self.ros_world.is_playing(): - self._cube_position = np.array([np.random.rand() * 0.40, np.random.rand() * 0.40, 0.10]) - - def run_simulation(self): - self.timeline.play() - reset_needed = False - while simulation_app.is_running(): - self.ros_world.step(render=True) - rclpy.spin_once(self, timeout_sec=0.0) - if self.ros_world.is_stopped() and not reset_needed: - reset_needed = True - if self.ros_world.is_playing(): - if reset_needed: - self.ros_world.reset() - reset_needed = False - # the actual setting the cube pose is done here - self.ros_world.scene.get_object("cube_1").set_world_pose(self._cube_position) - - # Cleanup - self.timeline.stop() - self.destroy_node() - simulation_app.close() - - -if __name__ == "__main__": - rclpy.init() - subscriber = Subscriber() - subscriber.run_simulation() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera.py deleted file mode 100644 index dd7760f25..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import isaacsim.core.utils.numpy.rotations as rot_utils -import matplotlib -import matplotlib.pyplot as plt -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.sensors.camera import Camera - -my_world = World(stage_units_in_meters=1.0) - -cube_2 = my_world.scene.add( - DynamicCuboid( - prim_path="/new_cube_2", - name="cube_1", - position=np.array([5.0, 3, 1.0]), - scale=np.array([0.6, 0.5, 0.2]), - size=1.0, - color=np.array([255, 0, 0]), - ) -) - -cube_3 = my_world.scene.add( - DynamicCuboid( - prim_path="/new_cube_3", - name="cube_2", - position=np.array([-5, 1, 3.0]), - scale=np.array([0.1, 0.1, 0.1]), - size=1.0, - color=np.array([0, 0, 255]), - linear_velocity=np.array([0, 0, 0.4]), - ) -) - -camera = Camera( - prim_path="/World/camera", - position=np.array([0.0, 0.0, 25.0]), - frequency=20, - resolution=(256, 256), - orientation=rot_utils.euler_angles_to_quats(np.array([0, 90, 0]), degrees=True), -) - -my_world.scene.add_default_ground_plane() -my_world.reset() -camera.initialize() - -i = 0 -camera.add_motion_vectors_to_frame() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - print(camera.get_current_frame()) - if i == 100: - points_2d = camera.get_image_coords_from_world_points( - np.array([cube_3.get_world_pose()[0], cube_2.get_world_pose()[0]]) - ) - points_3d = camera.get_world_points_from_image_coords(points_2d, np.array([24.94, 24.9])) - print(points_2d) - print(points_3d) - imgplot = plt.imshow(camera.get_rgba()[:, :, :3]) - if matplotlib.get_backend() in ["TkAgg", "nbAgg"]: - plt.draw() - plt.pause(0.01) - print(camera.get_current_frame()["motion_vectors"]) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - reset_needed = False - i += 1 - - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_opencv.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_opencv.py deleted file mode 100644 index b88ad33d7..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_opencv.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import isaacsim.core.utils.numpy.rotations as rot_utils -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.sensors.camera import Camera -from PIL import Image, ImageDraw - -# Given the OpenCV camera matrix and distortion coefficients (Rational Polynomial model), -# creates a camera and a sample scene, renders an image and saves it to -# camera_opencv_fisheye.png file. The asset is also saved to camera_opencv_fisheye.usd file. -width, height = 1920, 1200 -camera_matrix = [[958.8, 0.0, 957.8], [0.0, 956.7, 589.5], [0.0, 0.0, 1.0]] -distortion_coefficients = [0.14, -0.03, -0.0002, -0.00003, 0.009, 0.5, -0.07, 0.017] - -# Camera sensor size and optical path parameters. These parameters are not the part of the -# OpenCV camera model, but they are nessesary to simulate the depth of field effect. -# -# To disable the depth of field effect, set the f_stop to 0.0. This is useful for debugging. -pixel_size = 3 # in microns, 3 microns is common -f_stop = 1.8 # f-number, the ratio of the lens focal length to the diameter of the entrance pupil -focus_distance = 0.6 # in meters, the distance from the camera to the object plane -diagonal_fov = 140 # in degrees, the diagonal field of view to be rendered - - -# Create a world, add a 1x1x1 meter cube, a ground plane, and a camera -world = World(stage_units_in_meters=1.0) -world.scene.add_default_ground_plane() - -cube_1 = world.scene.add( - DynamicCuboid( - prim_path="/new_cube_1", - name="cube_1", - position=np.array([0, 0, 0.5]), - scale=np.array([1.0, 1.0, 1.0]), - size=1.0, - color=np.array([255, 0, 0]), - ) -) - -camera = Camera( - prim_path="/World/camera", - position=np.array([0.0, 0.0, 2.0]), # 1 meter away from the side of the cube - frequency=30, - resolution=(width, height), - orientation=rot_utils.euler_angles_to_quats(np.array([0, 90, 0]), degrees=True), -) - -# Setup the scene and render a frame -world.reset() -camera.initialize() - -# Calculate the focal length and aperture size from the camera matrix -((fx, _, cx), (_, fy, cy), (_, _, _)) = camera_matrix -horizontal_aperture = pixel_size * 1e-3 * width -vertical_aperture = pixel_size * 1e-3 * height -focal_length_x = fx * pixel_size * 1e-3 -focal_length_y = fy * pixel_size * 1e-3 -focal_length = (focal_length_x + focal_length_y) / 2 # in mm - -# Set the camera parameters, note the unit conversion between Isaac Sim sensor and Kit -camera.set_focal_length(focal_length / 10.0) -camera.set_focus_distance(focus_distance) -camera.set_lens_aperture(f_stop * 100.0) -camera.set_horizontal_aperture(horizontal_aperture / 10.0) -camera.set_vertical_aperture(vertical_aperture / 10.0) - -camera.set_clipping_range(0.05, 1.0e5) - -# Set the distortion coefficients -camera.set_projection_type("fisheyePolynomial") -camera.set_rational_polynomial_properties(width, height, cx, cy, diagonal_fov, distortion_coefficients) - -# Get the rendered frame and save it to a file -for i in range(100): - world.step(render=True) -camera.get_current_frame() -img = Image.fromarray(camera.get_rgba()[:, :, :3]) - - -# Optional step, draw the 3D points to the image plane using the OpenCV fisheye model -def draw_points_opencv(points3d): - import cv2 - - rvecs, tvecs = np.array([0.0, 0.0, 0.0]), np.array([0.0, 0.0, 0.0]) - points, jac = cv2.projectPoints( - np.expand_dims(points3d, 1), rvecs, tvecs, np.array(camera_matrix), np.array(distortion_coefficients) - ) - draw = ImageDraw.Draw(img) - for pt in points: - x, y = pt[0] - print("Drawing point at: ", x, y) - draw.ellipse((x - 4, y - 4, x + 4, y + 4), fill="orange", outline="orange") - - -# Draw the 3D points to the image plane -draw_points_opencv(points3d=np.array([[0.5, 0.5, 1.0], [-0.5, 0.5, 1.0], [0.5, -0.5, 1.0], [-0.5, -0.5, 1.0]])) - -print("Saving the rendered image to: camera_opencv.png") -img.save("camera_opencv.png") - -print("Saving the asset to camera_opencv.usd") -world.scene.stage.Export("camera_opencv.usd") - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_opencv_fisheye.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_opencv_fisheye.py deleted file mode 100644 index 552e12f01..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_opencv_fisheye.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) # Option: "renderer": "PathTracing" - -import isaacsim.core.utils.numpy.rotations as rot_utils -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.sensors.camera import Camera -from PIL import Image, ImageDraw - -# Given the OpenCV camera matrix and distortion coefficients (Fisheye, Kannala-Brandt model), -# creates a camera and a sample scene, renders an image and saves it to -# camera_opencv_fisheye.png file. The asset is also saved to camera_opencv_fisheye.usd file. - -# Currently only supports square images (there is an issue in the rendering pipeline). -# To produce non-square images, the region of the image that is not used should be cropped -width, height = 1920, 1200 -camera_matrix = [[455.8, 0.0, 943.8], [0.0, 454.7, 602.3], [0.0, 0.0, 1.0]] -distortion_coefficients = [0.05, 0.01, -0.003, -0.0005] - -# Camera sensor size and optical path parameters. These parameters are not the part of the -# OpenCV camera model, but they are nessesary to simulate the depth of field effect. -# -# To disable the depth of field effect, set the f_stop to 0.0. This is useful for debugging. -pixel_size = 3 # in microns, 3 microns is common -f_stop = 1.8 # f-number, the ratio of the lens focal length to the diameter of the entrance pupil -focus_distance = 0.6 # in meters, the distance from the camera to the object plane -diagonal_fov = 235 # in degrees, the diagonal field of view to be rendered - - -# Create a world, add a 1x1x1 meter cube, a ground plane, and a camera -world = World(stage_units_in_meters=1.0) -world.scene.add_default_ground_plane() - -cube_1 = world.scene.add( - DynamicCuboid( - prim_path="/new_cube_1", - name="cube_1", - position=np.array([0, 0, 0.5]), - scale=np.array([1.0, 1.0, 1.0]), - size=1.0, - color=np.array([255, 0, 0]), - ) -) - -cube_2 = world.scene.add( - DynamicCuboid( - prim_path="/new_cube_2", - name="cube_2", - position=np.array([2, 0, 0.5]), - scale=np.array([1.0, 1.0, 1.0]), - size=1.0, - color=np.array([0, 255, 0]), - ) -) - -cube_3 = world.scene.add( - DynamicCuboid( - prim_path="/new_cube_3", - name="cube_3", - position=np.array([0, 4, 1]), - scale=np.array([2.0, 2.0, 2.0]), - size=1.0, - color=np.array([0, 0, 255]), - ) -) - -camera = Camera( - prim_path="/World/camera", - position=np.array([0.0, 0.0, 2.0]), # 1 meter away from the side of the cube - frequency=30, - resolution=(width, height), - orientation=rot_utils.euler_angles_to_quats(np.array([0, 90, 0]), degrees=True), -) - -# Setup the scene and render a frame -world.reset() -camera.initialize() - -# Calculate the focal length and aperture size from the camera matrix -((fx, _, cx), (_, fy, cy), (_, _, _)) = camera_matrix -horizontal_aperture = pixel_size * 1e-3 * width -vertical_aperture = pixel_size * 1e-3 * height -focal_length_x = fx * pixel_size * 1e-3 -focal_length_y = fy * pixel_size * 1e-3 -focal_length = (focal_length_x + focal_length_y) / 2 # in mm - -# Set the camera parameters, note the unit conversion between Isaac Sim sensor and Kit -camera.set_focal_length(focal_length / 10.0) -camera.set_focus_distance(focus_distance) -camera.set_lens_aperture(f_stop * 100.0) -camera.set_horizontal_aperture(horizontal_aperture / 10.0) -camera.set_vertical_aperture(vertical_aperture / 10.0) - -camera.set_clipping_range(0.05, 1.0e5) - -# Set the distortion coefficients -camera.set_projection_type("fisheyePolynomial") -camera.set_kannala_brandt_properties(width, height, cx, cy, diagonal_fov, distortion_coefficients) - -# Get the rendered frame and save it to a file -for i in range(100): - world.step(render=True) -camera.get_current_frame() -img = Image.fromarray(camera.get_rgba()[:, :, :3]) - -# Optional step, draw the 3D points to the image plane using the OpenCV fisheye model -def draw_points_opencv_fisheye(points3d): - import cv2 - - rvecs, tvecs = np.array([0.0, 0.0, 0.0]), np.array([0.0, 0.0, 0.0]) - points, jac = cv2.fisheye.projectPoints( - np.expand_dims(points3d, 1), rvecs, tvecs, np.array(camera_matrix), np.array(distortion_coefficients) - ) - draw = ImageDraw.Draw(img) - for pt in points: - x, y = pt[0] - print("Drawing point at: ", x, y) - draw.ellipse((x - 4, y - 4, x + 4, y + 4), fill="yellow", outline="yellow") - - -# Draw a few 3D points at the image plane (camera is pointing down to the ground plane). -# OpenCV doen't support projecting points behind the camera, so we avoid that. -draw_points_opencv_fisheye( - points3d=np.array( - [ - [0.5, 0.5, 1.0], - [-0.5, 0.5, 1.0], - [0.5, -0.5, 1.0], - [-0.5, -0.5, 1.0], - [-3.0, -1.0, 0.0], - [-3.0, 1.0, 0.0], - [-0.5, -1.5, 1.0], - [0.5, -1.5, 1.0], - ] - ) -) - - -print("Saving the rendered image to: camera_opencv_fisheye.png") -img.save("camera_opencv_fisheye.png") - -print("Saving the asset to camera_opencv_fisheye.usd") -world.scene.stage.Export("camera_opencv_fisheye.usd") - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_ros.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_ros.py deleted file mode 100644 index 06eb251c7..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_ros.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -# Given a printout of ROS topic, containing the intrinsic and extrinsic parameters of the camera, -# creates a camera and a sample scene, renders an image and saves it to camera_ros.png file. -# The asset is also saved to camera_ros.usd file. The camera model is based on Intel RealSense D435i. - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import math - -import isaacsim.core.utils.numpy.rotations as rot_utils -import numpy as np -import yaml -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.sensors.camera import Camera -from PIL import Image, ImageDraw - -# To create a model of a given ROS camera, print the camera_info topic with: -# rostopicecho /camera/color/camera_info -# And copy the output into the yaml_data variable below. Populate additional parameters using the sensor manual. -# -# Note: only rational_polynomial model is supported in this example. For plump_bob or pinhole -# models set the distortion_model to "rational_polynomial" and compliment array D with 0.0 to 8 elements -# The camera_info topic in the Isaac Sim ROS bridge will be in the rational_polynomial format. -# -# Note: when fx is not equal to fy (pixels are not square), the average of fx and fy is used as the focal length. -# and the intrinsic matrix is adjusted to have square pixels. This updated matrix is used for rendering and -# it is also populated into the camera_info topic in the Isaac Sim ROS bridge. - -yaml_data = """ -# rostopic echo /camera/color/camera_info -header: - seq: 211 - stamp: - secs: 1694379352 - nsecs: 176209771 - frame_id: "camera_color_optical_frame" -height: 480 -width: 640 -distortion_model: "rational_polynomial" -D: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] -K: [612.4178466796875, 0.0, 309.72296142578125, 0.0, 612.362060546875, 245.35870361328125, 0.0, 0.0, 1.0] -R: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] -P: [612.4178466796875, 0.0, 309.72296142578125, 0.0, 0.0, 612.362060546875, 245.35870361328125, 0.0, 0.0, 0.0, 1.0, 0.0] -""" - -# Camera sensor size and optical path parameters. These parameters are not the part of the -# OpenCV camera model, but they are nessesary to simulate the depth of field effect. -# -# To disable the depth of field effect, set the f_stop to 0.0. This is useful for debugging. -pixel_size = 1.4 # Pixel size in microns, 3 microns is common -f_stop = 2.0 # F-number, the ratio of the lens focal length to the diameter of the entrance pupil -focus_distance = 0.5 # Focus distance in meters, the distance from the camera to the object plane - - -# Parsing the YAML data -data = yaml.safe_load(yaml_data) -print("Header Frame ID:", data["header"]["frame_id"]) -width, height, K, D = data["width"], data["height"], data["K"], data["D"] - -# Create a world, add a 1x1x1 meter cube, a ground plane, and a camera -world = World(stage_units_in_meters=1.0) -world.scene.add_default_ground_plane() -world.reset() - -cube_1 = world.scene.add( - DynamicCuboid( - prim_path="/new_cube_1", - name="cube_1", - position=np.array([0, 0, 0.5]), - scale=np.array([1.0, 1.0, 1.0]), - size=1.0, - color=np.array([255, 0, 0]), - ) -) - -camera = Camera( - prim_path="/World/camera", - position=np.array([0.0, 0.0, 3.0]), # 2 meter away from the side of the cube - frequency=30, - resolution=(width, height), - orientation=rot_utils.euler_angles_to_quats(np.array([0, 90, 0]), degrees=True), -) -camera.initialize() - -# Calculate the focal length and aperture size from the camera matrix -(fx, _, cx, _, fy, cy, _, _, _) = K -horizontal_aperture = pixel_size * 1e-3 * width -vertical_aperture = pixel_size * 1e-3 * height -focal_length_x = fx * pixel_size * 1e-3 -focal_length_y = fy * pixel_size * 1e-3 -focal_length = (focal_length_x + focal_length_y) / 2 # in mm - -# Set the camera parameters, note the unit conversion between Isaac Sim sensor and Kit -camera.set_focal_length(focal_length / 10.0) -camera.set_focus_distance(focus_distance) -camera.set_lens_aperture(f_stop * 100.0) -camera.set_horizontal_aperture(horizontal_aperture / 10.0) -camera.set_vertical_aperture(vertical_aperture / 10.0) -camera.set_clipping_range(0.05, 1.0e5) - -# Set the distortion coefficients, this is nessesary, when cx, cy are not in the center of the image -diagonal = 2 * math.sqrt(max(cx, width - cx) ** 2 + max(cy, height - cy) ** 2) -diagonal_fov = 2 * math.atan2(diagonal, fx + fy) * 180 / math.pi -camera.set_projection_type("fisheyePolynomial") -camera.set_rational_polynomial_properties(width, height, cx, cy, diagonal_fov, D) - -# Get the rendered frame and save it to a file -for i in range(100): - world.step(render=True) -camera.get_current_frame() -img = Image.fromarray(camera.get_rgba()[:, :, :3]) - - -# Optional step, draw the 3D points to the image plane using the OpenCV fisheye model -def draw_points_opencv(points3d): - try: - # To install, run python.sh -m pip install opencv-python - import cv2 - - rvecs, tvecs = np.array([0.0, 0.0, 0.0]), np.array([0.0, 0.0, 0.0]) - points, jac = cv2.projectPoints( - np.expand_dims(points3d, 1), rvecs, tvecs, np.array(K).reshape(3, 3), np.array(D) - ) - draw = ImageDraw.Draw(img) - for pt in points: - x, y = pt[0] - print("Drawing point at: ", x, y) - draw.ellipse((x - 4, y - 4, x + 4, y + 4), fill="orange", outline="orange") - except: - print("OpenCV is not installed, skipping OpenCV overlay") - print("To install OpenCV, run: python.sh -m pip install opencv-python") - - -# Draw the 3D points to the image plane -draw_points_opencv(points3d=np.array([[0.5, 0.5, 4.0], [-0.5, 0.5, 4.0], [0.5, -0.5, 4.0], [-0.5, -0.5, 4.0]])) - -print("Saving the rendered image to: camera_ros.png") -img.save("camera_ros.png") - -print("Saving the asset to camera_ros.usd") -world.scene.stage.Export("camera_ros.usd") - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_view.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_view.py deleted file mode 100644 index e9b675fdd..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.camera/camera_view.py +++ /dev/null @@ -1,263 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -NUM_CAPTURES = 2 -RESOLUTION = (256, 256) - -import os - -import numpy as np -import omni.replicator.core as rep -import torch -from isaacsim.core.api import World -from isaacsim.core.api.objects import VisualCuboid -from isaacsim.sensors.camera import CameraView -from PIL import Image - -# Create the world and add some visual cubes -my_world = World(stage_units_in_meters=1.0) -for i in range(2): - my_world.scene.add( - VisualCuboid( - prim_path=f"/new_cube_{i}", - name=f"cube_{i}", - position=np.array([0, i * 0.5, 0.2]), - scale=np.array([0.1, 0.1, 0.1]), - size=1.0, - color=np.array([0, 0, 255]), - ) - ) - -# Create the cameras -camera_01 = rep.create.camera(position=(0, 0, 2), look_at=(0, 0, 0)) -camera_02 = rep.create.camera(position=(0, 1, 2), look_at=(0, 0, 0)) -camera_03 = rep.create.camera(position=(1, 0, 2), look_at=(0, 0, 0)) -camera_04 = rep.create.camera(position=(1, 1, 2), look_at=(0, 0, 0)) - -# Create the camera view from the camera prims -camera_view = CameraView( - name="camera_prim_view", - camera_resolution=RESOLUTION, - prim_paths_expr="/Replicator/Camera_Xform*/Camera", - output_annotators=["rgb", "depth"], -) - -# Add default ground plane environment and wait a few frames to fully load -my_world.scene.add_default_ground_plane() -my_world.reset() -for i in range(20): - simulation_app.update() - -# Create output directory for the test data as images -out_dir = os.path.join(os.getcwd(), "_out_camera_view") -print(f"out_dir: {out_dir}") -os.makedirs(out_dir, exist_ok=True) -os.makedirs(f"{out_dir}/tiled", exist_ok=True) -os.makedirs(f"{out_dir}/batched", exist_ok=True) - -# Use pre-allocated arrays as output for the camera view (RGB and depth, numpy and torch, tiled and batched) -rgb_np_tiled_out = np.zeros((*camera_view.tiled_resolution, 3), dtype=np.uint8) -rgb_tiled_torch_out = torch.zeros((*camera_view.tiled_resolution, 3), device="cuda", dtype=torch.uint8) -batched_rgb_shape = (len(camera_view.prims), *camera_view.camera_resolution, 3) -rgb_batched_out = torch.zeros(batched_rgb_shape, device="cuda", dtype=torch.uint8) - -depth_np_tiled_out = np.zeros((*camera_view.tiled_resolution, 1), dtype=np.float32) -depth_tiled_torch_out = torch.zeros(camera_view.tiled_resolution, device="cuda", dtype=torch.float32) -depth_batched_shape = (len(camera_view.prims), *camera_view.camera_resolution, 1) -depth_batched_out = torch.zeros(depth_batched_shape, device="cuda", dtype=torch.float32) - -# Capture the data for the required number of frames -for i in range(NUM_CAPTURES): - print(f" ** Step {i} ** ") - my_world.step(render=True) - - #### RGB - print(f" ** Running RGB data tests:") - - ## Numpy - print(f" ** Numpy:") - rgb_tiled_np = camera_view.get_rgb_tiled(device="cpu") - print(f"rgb_tiled_np.shape: {rgb_tiled_np.shape}, type: {type(rgb_tiled_np)}, dtype: {rgb_tiled_np.dtype}") - rgb_tiled_np_uint8 = (rgb_tiled_np * 255).astype(np.uint8) - print( - f"rgb_tiled_uint8.shape: {rgb_tiled_np_uint8.shape}, type: {type(rgb_tiled_np_uint8)}, dtype: {rgb_tiled_np_uint8.dtype}" - ) - rgb_tiled_img = Image.fromarray(rgb_tiled_np_uint8) - rgb_tiled_img.save(f"{out_dir}/tiled/{str(i).zfill(3)}_rgb_tiled_np.png") - - # Using pre-allocated memory for out argument - camera_view.get_rgb_tiled(out=rgb_np_tiled_out, device="cpu") - print( - f"rgb_np_tiled_out.shape: {rgb_np_tiled_out.shape}, type: {type(rgb_np_tiled_out)}, dtype: {rgb_np_tiled_out.dtype}" - ) - rgb_np_tiled_out_uint8 = (rgb_np_tiled_out * 255).astype(np.uint8) - print( - f"rgb_np_tiled_out_uint8.shape: {rgb_np_tiled_out_uint8.shape}, type: {type(rgb_np_tiled_out_uint8)}, dtype: {rgb_np_tiled_out_uint8.dtype}" - ) - rgb_np_tiled_out_img = Image.fromarray(rgb_np_tiled_out_uint8) - rgb_np_tiled_out_img.save(f"{out_dir}/tiled/{str(i).zfill(3)}_rgb_tiled_np_out.png") - - ## Torch - print(f" ** Torch:") - rgb_tiled_torch = camera_view.get_rgb_tiled(device="cuda") - print( - f"rgb_tiled_torch.shape: {rgb_tiled_torch.shape}, type: {type(rgb_tiled_torch)}, dtype: {rgb_tiled_torch.dtype}" - ) - rgb_tiled_torch_uint8 = (rgb_tiled_torch * 255).to(dtype=torch.uint8) - print( - f"rgb_tiled_torch_uint8.shape: {rgb_tiled_torch_uint8.shape}, type: {type(rgb_tiled_torch_uint8)}, dtype: {rgb_tiled_torch_uint8.dtype}" - ) - rgb_tiled_torch_img = Image.fromarray(rgb_tiled_torch_uint8.cpu().numpy()) - rgb_tiled_torch_img.save(f"{out_dir}/tiled/{str(i).zfill(3)}_rgb_tiled_torch.png") - - # Using pre-allocated memory for out argument - camera_view.get_rgb_tiled(out=rgb_tiled_torch_out, device="cuda") - print( - f"rgb_tiled_torch_out.shape: {rgb_tiled_torch_out.shape}, type: {type(rgb_tiled_torch_out)}, dtype: {rgb_tiled_torch_out.dtype}" - ) - rgb_tiled_torch_out_uint8 = (rgb_tiled_torch_out * 255).to(dtype=torch.uint8) - print( - f"rgb_tiled_torch_out_uint8.shape: {rgb_tiled_torch_out_uint8.shape}, type: {type(rgb_tiled_torch_out_uint8)}, dtype: {rgb_tiled_torch_out_uint8.dtype}" - ) - rgb_tiled_torch_out_img = Image.fromarray(rgb_tiled_torch_out_uint8.cpu().numpy()) - rgb_tiled_torch_out_img.save(f"{out_dir}/tiled/{str(i).zfill(3)}_rgb_tiled_torch_out.png") - - ## Batched - print(f" ** Batched:") - rgb_batched = camera_view.get_rgb() - print(f"rgb_batched.shape: {rgb_batched.shape}, type: {type(rgb_batched)}, dtype: {rgb_batched.dtype}") - for camera_id in range(rgb_batched.shape[0]): - rgb_batched_uint8 = (rgb_batched[camera_id] * 255).to(dtype=torch.uint8) - print( - f"camera_id={camera_id}: rgb_batched_uint8.shape: {rgb_batched_uint8.shape}, type: {type(rgb_batched_uint8)}, dtype: {rgb_batched_uint8.dtype}" - ) - rgb_batched_img = Image.fromarray(rgb_batched_uint8.cpu().numpy()) - rgb_batched_img.save(f"{out_dir}/batched/{str(i).zfill(3)}_rgb_batched_{camera_id}.png") - - # Using pre-allocated memory for out argument - camera_view.get_rgb(out=rgb_batched_out) - print( - f"rgb_batched_out.shape: {rgb_batched_out.shape}, type: {type(rgb_batched_out)}, dtype: {rgb_batched_out.dtype}" - ) - for camera_id in range(rgb_batched_out.shape[0]): - rgb_batched_out_uint8 = (rgb_batched_out[camera_id] * 255).to(dtype=torch.uint8) - print( - f"camera_id={camera_id}: rgb_batched_out_uint8.shape: {rgb_batched_out_uint8.shape}, type: {type(rgb_batched_out_uint8)}, dtype: {rgb_batched_out_uint8.dtype}" - ) - rgb_batched_out_img = Image.fromarray(rgb_batched_out_uint8.cpu().numpy()) - rgb_batched_out_img.save(f"{out_dir}/batched/{str(i).zfill(3)}_rgb_batched_out_{camera_id}.png") - - #### Depth - print(f" ** Running depth data tests:") - - ## Numpy - print(f" ** Numpy:") - depth_tiled_np = camera_view.get_depth_tiled(device="cpu") - print(f"depth_tiled_np.shape: {depth_tiled_np.shape}, type: {type(depth_tiled_np)}, dtype: {depth_tiled_np.dtype}") - # Change inf to 0.0 and clip to range [0.0, 1.0] - depth_tiled_np[np.isinf(depth_tiled_np)] = 0.0 - depth_tiled_np = np.clip(depth_tiled_np, 0.0, 1.0) - depth_tiled_np_uint8 = (depth_tiled_np * 255).squeeze().astype(np.uint8) - depth_tiled_img = Image.fromarray(depth_tiled_np_uint8, mode="L") - depth_tiled_img.save(f"{out_dir}/tiled/{str(i).zfill(3)}_depth_tiled_np.png") - - # Using pre-allocated memory for out argument - camera_view.get_depth_tiled(out=depth_np_tiled_out, device="cpu") - print( - f"depth_np_tiled_out.shape: {depth_np_tiled_out.shape}, type: {type(depth_np_tiled_out)}, dtype: {depth_np_tiled_out.dtype}" - ) - # Change inf to 0.0 and clip to range [0.0, 1.0] - depth_np_tiled_out[np.isinf(depth_np_tiled_out)] = 0.0 - depth_np_tiled_out = np.clip(depth_np_tiled_out, 0.0, 1.0) - depth_np_tiled_out_uint8 = (depth_np_tiled_out * 255).squeeze().astype(np.uint8) - depth_np_tiled_out_img = Image.fromarray(depth_np_tiled_out_uint8, mode="L") - depth_np_tiled_out_img.save(f"{out_dir}/tiled/{str(i).zfill(3)}_depth_tiled_np_out.png") - - ## Torch - print(f" ** Torch:") - depth_tiled_torch = camera_view.get_depth_tiled(device="cuda") - print( - f"depth_tiled_torch.shape: {depth_tiled_torch.shape}, type: {type(depth_tiled_torch)}, dtype: {depth_tiled_torch.dtype}" - ) - # Change inf to 0.0 and clip to range [0.0, 1.0] - depth_tiled_torch[torch.isinf(depth_tiled_torch)] = 0.0 - depth_tiled_torch = torch.clip(depth_tiled_torch, 0.0, 1.0) - depth_tiled_torch_uint8 = (depth_tiled_torch * 255).squeeze().to(dtype=torch.uint8) - depth_tiled_torch_img = Image.fromarray(depth_tiled_torch_uint8.cpu().numpy(), mode="L") - depth_tiled_torch_img.save(f"{out_dir}/tiled/{str(i).zfill(3)}_depth_tiled_torch.png") - - # Using pre-allocated memory for out argument - camera_view.get_depth_tiled(out=depth_tiled_torch_out, device="cuda") - print( - f"depth_tiled_torch_out.shape: {depth_tiled_torch_out.shape}, type: {type(depth_tiled_torch_out)}, dtype: {depth_tiled_torch_out.dtype}" - ) - # Change inf to 0.0 and clip to range [0.0, 1.0] - depth_tiled_torch_out[torch.isinf(depth_tiled_torch_out)] = 0.0 - depth_tiled_torch_out = torch.clip(depth_tiled_torch_out, 0.0, 1.0) - depth_tiled_torch_out_uint8 = (depth_tiled_torch_out * 255).squeeze().to(dtype=torch.uint8) - depth_tiled_torch_out_img = Image.fromarray(depth_tiled_torch_out_uint8.cpu().numpy(), mode="L") - depth_tiled_torch_out_img.save(f"{out_dir}/tiled/{str(i).zfill(3)}_depth_tiled_torch_out.png") - - ## Batched - print(f" ** Batched:") - depth_batched = camera_view.get_depth() - print(f"depth_batched.shape: {depth_batched.shape}, type: {type(depth_batched)}, dtype: {depth_batched.dtype}") - # Change inf to 0.0 and clip to range [0.0, 1.0] - depth_batched[torch.isinf(depth_batched)] = 0.0 - depth_batched = torch.clip(depth_batched, 0.0, 1.0) - # Split the batched data and save each image - for camera_id in range(depth_batched.shape[0]): - depth_batched_uint8 = (depth_batched[camera_id] * 255).squeeze().to(dtype=torch.uint8) - depth_batched_img = Image.fromarray(depth_batched_uint8.cpu().numpy(), mode="L") - depth_batched_img.save(f"{out_dir}/batched/{str(i).zfill(3)}_depth_batched_{camera_id}.png") - - # Using pre-allocated memory for out argument - camera_view.get_depth(out=depth_batched_out) - print( - f"depth_batched_out.shape: {depth_batched_out.shape}, type: {type(depth_batched_out)}, dtype: {depth_batched_out.dtype}" - ) - # Change inf to 0.0 and clip to range [0.0, 1.0] - depth_batched_out[torch.isinf(depth_batched_out)] = 0.0 - depth_batched_out = torch.clip(depth_batched_out, 0.0, 1.0) - # Split the batched data and save each image - for camera_id in range(depth_batched_out.shape[0]): - depth_batched_out_uint8 = (depth_batched_out[camera_id] * 255).squeeze().to(dtype=torch.uint8) - depth_batched_out_img = Image.fromarray(depth_batched_out_uint8.cpu().numpy(), mode="L") - depth_batched_out_img.save(f"{out_dir}/batched/{str(i).zfill(3)}_depth_batched_out_{camera_id}.png") - - # API - print(f" ** Running API calls:") - print(f"camera_view.get_local_poses(camera_axes='ros'): {camera_view.get_local_poses(camera_axes='ros')}") - print(f"camera_view.get_local_poses(camera_axes='usd'): {camera_view.get_local_poses(camera_axes='usd')}") - print(f"camera_view.get_local_poses(camera_axes='world'): {camera_view.get_local_poses(camera_axes='world')}") - print(f"camera_view.get_world_poses(): {camera_view.get_world_poses()}") - - print(f"camera_view.get_focal_lengths(): {camera_view.get_focal_lengths()}") - print(f"camera_view.get_focus_distances(): {camera_view.get_focus_distances()}") - print(f"camera_view.get_lens_apertures(): {camera_view.get_lens_apertures()}") - print(f"camera_view.get_horizontal_apertures(): {camera_view.get_horizontal_apertures()}") - print(f"camera_view.get_vertical_apertures(): {camera_view.get_vertical_apertures()}") - print(f"camera_view.get_projection_types(): {camera_view.get_projection_types()}") - print(f"camera_view.get_projection_modes(): {camera_view.get_projection_modes()}") - print(f"camera_view.get_stereo_roles(): {camera_view.get_stereo_roles()}") - print(f"camera_view.get_shutter_properties(): {camera_view.get_shutter_properties()}") - - print( - f"camera_view.set_shutter_properties(): {camera_view.set_shutter_properties(camera_view.get_shutter_properties())}" - ) - - print(f"camera_view.get_focus_distances(): {camera_view.get_focus_distances()}") - - simulation_app.update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/contact_sensor.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/contact_sensor.py deleted file mode 100644 index f40a78142..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/contact_sensor.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.sensors.physics import ContactSensor -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() -asset_path = assets_root_path + "/Isaac/Robots/Ant/ant.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Ant") - -ant = my_world.scene.add(Articulation("/World/Ant/torso", name="ant", translations=np.array([[0, 0, 1.5]]))) - -ant_foot_prim_names = ["right_back_foot", "left_back_foot", "front_right_foot", "front_left_foot"] - -translations = np.array( - [[0.38202, -0.40354, -0.0887], [-0.4, -0.40354, -0.0887], [-0.4, 0.4, -0.0887], [0.4, 0.4, -0.0887]] -) - -ant_sensors = [] -for i in range(4): - ant_sensors.append( - my_world.scene.add( - ContactSensor( - prim_path="/World/Ant/" + ant_foot_prim_names[i] + "/contact_sensor", - name="ant_contact_sensor_{}".format(i), - min_threshold=0, - max_threshold=10000000, - radius=0.1, - translation=translations[i], - ) - ) - ) - -ant_sensors[0].add_raw_contact_data_to_frame() -my_world.reset() -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - print(ant_sensors[0].get_current_frame()) - if reset_needed: - my_world.reset() - reset_needed = False - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/effort_sensor.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/effort_sensor.py deleted file mode 100644 index 686059929..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/effort_sensor.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -# In this example, please drag the cube along the arm and see how the effort measurement from the effort sensor changes - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.utils.prims import get_prim_at_path -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.sensors.physics.scripts.effort_sensor import EffortSensor -from isaacsim.storage.native import get_assets_root_path -from pxr import UsdPhysics - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0, physics_dt=1.0 / 60, rendering_dt=1.0 / 60) -my_world.scene.add_default_ground_plane(z_position=-1) - -asset_path = assets_root_path + "/Isaac/Robots/Simple/simple_articulation.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/Articulation") -arm_joint = "/Articulation/Arm/RevoluteJoint" -prim = get_prim_at_path(arm_joint) -joint = UsdPhysics.RevoluteJoint(prim) -joint.CreateAxisAttr("Y") - -DynamicCuboid( - prim_path="/World/Cube", - name="cube_1", - position=np.array([1.5, 0, 0.2]), - color=np.array([255, 0, 0]), - size=0.1, - mass=1, -) - -my_world.reset() -effort_sensor = EffortSensor(prim_path=arm_joint) -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - reading = effort_sensor.get_sensor_reading() - print(f"Sensor Time: {reading.time} Value: {reading.value} Validity: {reading.is_valid}") - - if reset_needed: - my_world.reset() - reset_needed = False - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/imu_sensor.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/imu_sensor.py deleted file mode 100644 index 1bfcf06fa..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physics/imu_sensor.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.core.utils.types import ArticulationActions -from isaacsim.robot.wheeled_robots.controllers.differential_controller import DifferentialController -from isaacsim.sensors.physics import IMUSensor -from isaacsim.storage.native import get_assets_root_path - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -asset_path = assets_root_path + "/Isaac/Robots/NVIDIA/Carter/nova_carter/nova_carter.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Carter") - -my_carter = my_world.scene.add(Articulation("/World/Carter", name="my_carter", positions=np.array([[0, 0.0, 0.5]]))) -wheel_dof_names = ["joint_wheel_left", "joint_wheel_right"] - -my_controller = DifferentialController(name="simple_control", wheel_radius=0.04295, wheel_base=0.4132) - - -imu_sensor = my_world.scene.add( - IMUSensor( - prim_path="/World/Carter/caster_wheel_left/imu_sensor", - name="imu", - frequency=60, - translation=np.array([0, 0, 0]), - ) -) -my_world.reset() -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - wheel_dof_indices = [my_carter.get_dof_index(wheel_dof_names[i]) for i in range(len(wheel_dof_names))] - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - print(imu_sensor.get_current_frame()) - actions = ArticulationActions() - if i >= 0 and i < 1000: - # forward - # convert from ArticulationAction to ArticulationActions - actions.joint_velocities = np.expand_dims(my_controller.forward(command=[0.05, 0]).joint_velocities, axis=0) - - elif i >= 1000 and i < 1265: - # rotate - # convert from ArticulationAction to ArticulationActions - actions.joint_velocities = np.expand_dims( - my_controller.forward(command=[0.0, np.pi / 12]).joint_velocities, axis=0 - ) - elif i >= 1265 and i < 2000: - # forward - # convert from ArticulationAction to ArticulationActions - actions.joint_velocities = np.expand_dims(my_controller.forward(command=[0.05, 0]).joint_velocities, axis=0) - elif i == 2000: - i = 0 - i += 1 - joint_actions = ArticulationActions() - joint_actions.joint_velocities = np.zeros([1, my_carter.num_dof]) - if actions.joint_velocities is not None: - for j in range(len(wheel_dof_indices)): - joint_actions.joint_velocities[0, wheel_dof_indices[j]] = actions.joint_velocities[0, j] - - my_carter.apply_action(joint_actions) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physx/rotating_lidar_physX.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physx/rotating_lidar_physX.py deleted file mode 100644 index b9de2e1e2..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.physx/rotating_lidar_physX.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.robot.wheeled_robots.controllers.differential_controller import DifferentialController -from isaacsim.robot.wheeled_robots.robots import WheeledRobot -from isaacsim.sensors.physx import RotatingLidarPhysX -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() -asset_path = assets_root_path + "/Isaac/Robots/Carter/carter_v1_physx_lidar.usd" -my_carter = my_world.scene.add( - WheeledRobot( - prim_path="/World/Carter", - name="my_carter", - wheel_dof_names=["left_wheel", "right_wheel"], - create_robot=True, - usd_path=asset_path, - position=np.array([0, 0.0, 0.5]), - ) -) - -my_lidar = my_world.scene.add( - RotatingLidarPhysX( - prim_path="/World/Carter/chassis_link/lidar", name="lidar", translation=np.array([-0.06, 0, 0.38]) - ) -) - -cube_1 = my_world.scene.add( - DynamicCuboid(prim_path="/World/cube", name="cube_1", position=np.array([2, 2, 2.5]), scale=np.array([20, 0.2, 5])) -) - -cube_2 = my_world.scene.add( - DynamicCuboid( - prim_path="/World/cube_2", name="cube_2", position=np.array([2, -2, 2.5]), scale=np.array([20, 0.2, 5]) - ) -) - -my_controller = DifferentialController(name="simple_control", wheel_radius=0.24, wheel_base=0.56) - -my_world.reset() -my_lidar.add_depth_data_to_frame() -my_lidar.add_point_cloud_data_to_frame() -my_lidar.enable_visualization() -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - # print(imu_sensor.get_current_frame()) - if i >= 0 and i < 1000: - # print(my_lidar.get_current_frame()) - # forward - my_carter.apply_wheel_actions(my_controller.forward(command=[0.05, 0])) - elif i >= 1000 and i < 1265: - # rotate - my_carter.apply_wheel_actions(my_controller.forward(command=[0.0, np.pi / 12])) - elif i >= 1265 and i < 2000: - # forward - my_carter.apply_wheel_actions(my_controller.forward(command=[0.05, 0])) - elif i == 2000: - i = 0 - i += 1 - if args.test is True: - break -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.rtx/rotating_lidar_rtx.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.rtx/rotating_lidar_rtx.py deleted file mode 100644 index b9e06f749..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.sensors.rtx/rotating_lidar_rtx.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - - -import argparse -import sys - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.robot.wheeled_robots.controllers.differential_controller import DifferentialController -from isaacsim.robot.wheeled_robots.robots import WheeledRobot -from isaacsim.sensors.rtx import LidarRtx -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() -asset_path = assets_root_path + "/Isaac/Robots/NVIDIA/Carter/nova_carter/nova_carter.usd" -my_carter = my_world.scene.add( - WheeledRobot( - prim_path="/World/Carter", - name="my_carter", - wheel_dof_names=["joint_wheel_left", "joint_wheel_right"], - create_robot=True, - usd_path=asset_path, - position=np.array([0, 0.0, 0.5]), - ) -) - -# config_file_name="Example_Rotary" -my_lidar = my_world.scene.add( - LidarRtx(prim_path="/World/Carter/chassis_link/front_hawk/right/lidar_rig/lidar", name="lidar") -) - -cube_1 = my_world.scene.add( - DynamicCuboid(prim_path="/World/cube", name="cube_1", position=np.array([2, 2, 2.5]), scale=np.array([20, 0.2, 5])) -) - -cube_2 = my_world.scene.add( - DynamicCuboid( - prim_path="/World/cube_2", name="cube_2", position=np.array([2, -2, 2.5]), scale=np.array([20, 0.2, 5]) - ) -) - -my_controller = DifferentialController(name="simple_control", wheel_radius=0.04295, wheel_base=0.4132) - -my_world.reset() -my_lidar.add_range_data_to_frame() -my_lidar.add_point_cloud_data_to_frame() -my_lidar.enable_visualization() -i = 0 -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - if i >= 0 and i < 1000: - print(my_lidar.get_current_frame()) - # forward - my_carter.apply_wheel_actions(my_controller.forward(command=[0.05, 0])) - elif i >= 1000 and i < 1265: - # rotate - my_carter.apply_wheel_actions(my_controller.forward(command=[0.0, np.pi / 12])) - elif i >= 1265 and i < 2000: - # forward - my_carter.apply_wheel_actions(my_controller.forward(command=[0.05, 0])) - elif i == 2000: - i = 0 - i += 1 - if args.test is True and i > 100: - break -my_world.stop() -simulation_app.update() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/change_resolution.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/change_resolution.py deleted file mode 100644 index c1e692b3c..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/change_resolution.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import random - -from isaacsim import SimulationApp - -# Simple example showing how to change resolution -kit = SimulationApp({"headless": True}) -kit.update() -for i in range(100): - width = random.randint(128, 1980) - height = random.randint(128, 1980) - kit.set_setting("/app/renderer/resolution/width", width) - kit.set_setting("/app/renderer/resolution/height", height) - kit.update() - print(f"resolution set to: {width}, {height}") - -# cleanup -kit.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/constant_fps.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/constant_fps.py deleted file mode 100644 index 75270d639..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/constant_fps.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import time - -from isaacsim import SimulationApp - -# Simple example showing how to fix frame rate to (roughly) a constant value (with respect to wall-clock) -# Note frame rate cannot be set artificially higher than what the sim will run at on the current hardware, -# but it can be kept artificially lower for (eg.) synchronization with an external service. - -DESIRED_FRAME_RATE = 10.0 # frames per second -frame_period_s = 1.0 / DESIRED_FRAME_RATE - -simulation_app = SimulationApp({"headless": True}) - -import carb -import omni - -# Callback to measure app update time as precisely as possible -last_frametime_timestamp_ns = 0.0 -app_update_time_s = 0.0 - - -def update_event_callback(event: carb.events.IEvent): - timestamp_ns = time.perf_counter_ns() - app_update_time_s = round((timestamp_ns - last_frametime_timestamp_ns) / 1e9, 9) - last_frametime_timestamp_ns = timestamp_ns - - -omni.kit.app.get_app().get_update_event_stream().create_subscription_to_pop(update_event_callback) - -while simulation_app.is_running(): - # Measure duration of single app update - simulation_app.update() - # Sleep for the duration of the fixed frame - sleep_duration_s = frame_period_s - app_update_time_s - if sleep_duration_s <= 0.0: - carb.log_warn(f"simulation_app.update() took {app_update_time_s} s >= fixed period {frame_period_s} s.") - else: - time.sleep(sleep_duration_s) - instantaneous_fps = 1.0 / max(frame_period_s, app_update_time_s) - carb.log_warn(f"FPS is {instantaneous_fps}") - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/hello_world.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/hello_world.py deleted file mode 100644 index d084a47a4..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/hello_world.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -# The most basic usage for creating a simulation app -kit = SimulationApp() - -import omni - -for i in range(100): - kit.update() - -omni.kit.app.get_app().print_and_log("Hello World!") - -kit.close() # Cleanup application diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/livestream.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/livestream.py deleted file mode 100644 index af956aaad..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/livestream.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -# This sample enables a livestream server to connect to when running headless -CONFIG = { - "width": 1280, - "height": 720, - "window_width": 1920, - "window_height": 1080, - "headless": True, - "hide_ui": False, # Show the GUI - "renderer": "RaytracedLighting", - "display_options": 3286, # Set display options to show default grid -} - - -# Start the omniverse application -kit = SimulationApp(launch_config=CONFIG) - -from isaacsim.core.utils.extensions import enable_extension - -# Default Livestream settings -kit.set_setting("/app/window/drawMouse", True) - -# Enable Livestream extension -enable_extension("omni.kit.livestream.webrtc") - -# Run until closed -while kit._app.is_running() and not kit.is_exiting(): - # Run in realtime mode, we don't specify the step size - kit.update() - -kit.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/load_stage.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/load_stage.py deleted file mode 100644 index 61a7660c3..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.simulation_app/load_stage.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import sys - -from isaacsim import SimulationApp - -# This sample loads a usd stage and starts simulation -CONFIG = {"width": 1280, "height": 720, "sync_loads": True, "headless": False, "renderer": "RaytracedLighting"} - - -# Set up command line arguments -parser = argparse.ArgumentParser("Usd Load sample") -parser.add_argument( - "--usd_path", type=str, help="Path to usd file, should be relative to your default assets folder", required=True -) -parser.add_argument("--headless", default=False, action="store_true", help="Run stage headless") -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") - -args, unknown = parser.parse_known_args() -# Start the omniverse application -CONFIG["headless"] = args.headless -kit = SimulationApp(launch_config=CONFIG) - -import carb -import omni - -# Locate Isaac Sim assets folder to load sample -from isaacsim.storage.native import get_assets_root_path, is_file - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - kit.close() - sys.exit() -usd_path = assets_root_path + args.usd_path - -# make sure the file exists before we try to open it -try: - result = is_file(usd_path) -except: - result = False - -if result: - omni.usd.get_context().open_stage(usd_path) -else: - carb.log_error( - f"the usd path {usd_path} could not be opened, please make sure that {args.usd_path} is a valid usd file in {assets_root_path}" - ) - kit.close() - sys.exit() -# Wait two frames so that stage starts loading -kit.update() -kit.update() - -print("Loading stage...") -from isaacsim.core.utils.stage import is_stage_loading - -while is_stage_loading(): - kit.update() -print("Loading Complete") -omni.timeline.get_timeline_interface().play() -# Run in test mode, exit after a fixed number of steps -if args.test is True: - for i in range(10): - # Run in realtime mode, we don't specify the step size - kit.update() -else: - while kit.is_running(): - # Run in realtime mode, we don't specify the step size - kit.update() -omni.timeline.get_timeline_interface().stop() -kit.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.util.clash_detection/carter_clash_detection.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.util.clash_detection/carter_clash_detection.py deleted file mode 100644 index 3b0fa9365..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.util.clash_detection/carter_clash_detection.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -"""Standalone application script demonstrating a use case for the isaacsim.util.clash_detection API. -Randomly places Carter assets in a warehouse scene and informs the user if a carter mesh is clashing -with any other mesh in the scene. Supports exporting detailed information of any clashes to JSON -files for further analysis. -""" - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import argparse -import random - -import numpy as np -from omni.isaac.core.utils.extensions import enable_extension - -# enable isaac sim clash detection extension -enable_extension("isaacsim.util.clash_detection") -simulation_app.update() - -from isaacsim.storage.native import get_assets_root_path -from isaacsim.util.clash_detection import ClashDetector -from omni.isaac.core.prims import XFormPrim, XFormPrimView -from omni.isaac.core.utils.prims import get_prim_at_path -from omni.isaac.core.utils.stage import add_reference_to_stage, get_current_stage, open_stage - -parser = argparse.ArgumentParser() -parser.add_argument("--export_json", default=False, action="store_true", help="Export clash detection results to JSON") -parser.add_argument( - "--export_folder", action="store", type=str, help="Path to folder storing JSON files if exporting clash results" -) -args, unknown = parser.parse_known_args() - -ENV_PATH = "/Isaac/Environments/Simple_Warehouse/warehouse.usd" -CARTER_PATH = "/Isaac/Robots/Carter/carter_v1.usd" -EXPORT = args.export_json -EXPORT_PATH = args.export_folder - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Setup environment -open_stage(assets_root_path + ENV_PATH) -stage = get_current_stage() - -simulation_app.update() - -# Initialize clash detection engine -clash_detector = ClashDetector(stage, logging=False, clash_data_layer=False) - -# Place Carter assets -carter_usd_path = assets_root_path + CARTER_PATH - -for idx in range(5): - carter_prim_path = f"/World/Carter_{idx}" - add_reference_to_stage(usd_path=carter_usd_path, prim_path=carter_prim_path) - - XFormPrim(carter_prim_path).set_local_pose( - translation=np.array([random.uniform(-2, 2), random.uniform(-2, 2), random.uniform(0, 0.4)]) - ) - - simulation_app.update() - - query_name = f"carter_{idx}_query" - if clash_detector.is_prim_clashing(get_prim_at_path(carter_prim_path), query_name=query_name): - print(f"Clash detected for {carter_prim_path}") - if EXPORT: - query_id = clash_detector.get_current_query_id() - clash_detector.export_to_json(EXPORT_PATH + f"/Carter_{idx}_clash_data.json") - -while simulation_app.is_running(): - simulation_app.update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.util.debug_draw/rtx_lidar.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.util.debug_draw/rtx_lidar.py deleted file mode 100644 index dca351b6f..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.util.debug_draw/rtx_lidar.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import sys - -parser = argparse.ArgumentParser() -parser.add_argument("-c", "--config", type=str, default="Example_Rotary", help="Name of lidar config.") -args, _ = parser.parse_known_args() - -from isaacsim import SimulationApp - -# Example for creating a RTX lidar sensor and publishing PCL data -simulation_app = SimulationApp({"headless": False}) -import carb -import omni -import omni.kit.viewport.utility -import omni.replicator.core as rep -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import stage -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf - -# enable ROS bridge extension -enable_extension("isaacsim.util.debug_draw") - -simulation_app.update() - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -simulation_app.update() -# Loading the simple_room environment -stage.add_reference_to_stage( - assets_root_path + "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd", "/background" -) -simulation_app.update() - -lidar_config = args.config - -# Create the lidar sensor that generates data into "RtxSensorCpu" -# Possible config options are Example_Rotary and Example_Solid_State -_, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxLidar", - path="/sensor", - parent=None, - config=lidar_config, - translation=(0, 0, 1.0), - orientation=Gf.Quatd(1.0, 0.0, 0.0, 0.0), # Gf.Quatd is w,i,j,k -) -hydra_texture = rep.create.render_product(sensor.GetPath(), [1, 1], name="Isaac") - -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) -simulation_app.update() - -# Create the debug draw pipeline in the post process graph -writer = rep.writers.get("RtxLidar" + "DebugDrawPointCloud" + "Buffer") -writer.attach([hydra_texture]) - -simulation_app.update() - -simulation_context.play() - -while simulation_app.is_running(): - simulation_app.update() - -# cleanup and shutdown -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.util.debug_draw/rtx_radar.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.util.debug_draw/rtx_radar.py deleted file mode 100644 index 389174737..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.util.debug_draw/rtx_radar.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import sys - -parser = argparse.ArgumentParser() -parser.add_argument("-c", "--config", type=str, default="Example", help="Name of radar config.") -args, _ = parser.parse_known_args() - -from isaacsim import SimulationApp - -# Example for creating a RTX lidar sensor and publishing PCL data -simulation_app = SimulationApp({"headless": False}) -import carb -import omni -import omni.kit.viewport.utility -import omni.replicator.core as rep -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import stage -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf - -# enable ROS bridge extension -enable_extension("isaacsim.util.debug_draw") - -simulation_app.update() - -# Locate Isaac Sim assets folder to load environment and robot stages -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -simulation_app.update() -# Loading the simple_room environment -stage.add_reference_to_stage( - assets_root_path + "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd", "/background" -) -simulation_app.update() - -radar_config = args.config - -# Create the radar sensor that generates data into "RtxSensorCpu" -# Sensor needs to be rotated 90 degrees about +Z so it faces warehouse shelves. -# Possible config options are Example. -_, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxRadar", - path="/sensor", - parent=None, - config=radar_config, - translation=(0, 0, 1.0), - orientation=Gf.Quatd(0.70711, 0.0, 0.0, 0.70711), -) -hydra_texture = rep.create.render_product(sensor.GetPath(), [1, 1], name="Isaac") - -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) -simulation_app.update() - -# Create the debug draw pipeline in the post process graph -writer = rep.writers.get("RtxRadar" + "DebugDrawPointCloud") -writer.attach([hydra_texture]) - -simulation_app.update() - -simulation_context.play() - -while simulation_app.is_running(): - simulation_app.update() - -# cleanup and shutdown -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/isaacsim.xr.openxr/hand_tracking/hand_tracking_sample.py b/simulation/isaac-sim/standalone_examples/api/isaacsim.xr.openxr/hand_tracking/hand_tracking_sample.py deleted file mode 100644 index 9639ebe5a..000000000 --- a/simulation/isaac-sim/standalone_examples/api/isaacsim.xr.openxr/hand_tracking/hand_tracking_sample.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import os - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp( - {"headless": False}, experience=f'{os.environ["EXP_PATH"]}/isaacsim.exp.base.xr.openxr.kit' -) - -# Handle start/stop teleop commands from XR device -import carb -from omni.kit.xr.core import XRCore - -TELEOP_COMMAND_EVENT_TYPE = "teleop_command" - -tracking_enabled = False - - -def on_message(event: carb.events.IEvent): - """Processes the received message using key word.""" - message_in = event.payload["message"] - - global tracking_enabled - if "start" in message_in: - tracking_enabled = True - elif "stop" in message_in: - tracking_enabled = False - elif "reset" in message_in: - carb.log_info("Reset recieved") - else: - carb.log_warn(f"Unexpected message recieved {message_in}") - - -message_bus = XRCore.get_singleton().get_message_bus() -incoming_message_event = carb.events.type_from_string(TELEOP_COMMAND_EVENT_TYPE) -subscription = message_bus.create_subscription_to_pop_by_type(incoming_message_event, on_message) - - -import omni.usd -from isaacsim.core.api import World -from isaacsim.core.api.materials.omni_pbr import OmniPBR -from isaacsim.core.api.objects import VisualCuboid -from isaacsim.core.utils.prims import create_prim, set_prim_visibility -from isaacsim.xr.openxr import OpenXR, OpenXRSpec -from omni.isaac.core.prims import XFormPrim -from pxr import Gf, Sdf, Usd, UsdGeom, UsdLux - -openxr = OpenXR() -my_world = World(stage_units_in_meters=1.0) - -# Add Light Source -stage = omni.usd.get_context().get_stage() -distantLight = UsdLux.DistantLight.Define(stage, Sdf.Path("/DistantLight")) -distantLight.CreateIntensityAttr(300) - -hidden_prim = create_prim("/Hidden/Prototypes", "Scope") -base_cube_path = "/Hidden/Prototypes/BaseCube" -VisualCuboid( - prim_path=base_cube_path, - size=0.01, - color=np.array([255, 0, 0]), -) -set_prim_visibility(hidden_prim, False) - -instancer_path = "/World/CubeInstancer" -point_instancer = UsdGeom.PointInstancer.Define(my_world.stage, instancer_path) -point_instancer.CreatePrototypesRel().SetTargets([Sdf.Path(base_cube_path)]) - -hand_joint_count = int(OpenXRSpec.HandJointEXT.XR_HAND_JOINT_LITTLE_TIP_EXT) + 1 -joint_count = hand_joint_count * 2 - -# Initially hide all cubes until hands are tracked -point_instancer.CreateProtoIndicesAttr().Set([1 for _ in range(joint_count)]) - -positions = [Gf.Vec3f(0.0, 0.0, 0.0) for i in range(joint_count)] -point_instancer.CreatePositionsAttr().Set(positions) - -orientations = [Gf.Quath(1.0, 0.0, 0.0, 0.0) for _ in range(joint_count)] -point_instancer.CreateOrientationsAttr().Set(orientations) - -instancer_prim = XFormPrim(prim_path=instancer_path) -my_world.scene.add(instancer_prim) - -my_world.reset() -reset_needed = False - -positions_attr = point_instancer.GetPositionsAttr() -orientations_attr = point_instancer.GetOrientationsAttr() -proto_idx_attr = point_instancer.GetProtoIndicesAttr() - -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - reset_needed = False - - current_positions = positions_attr.Get() - current_orientations = orientations_attr.Get() - proto_indices = proto_idx_attr.Get() - - left_joints = openxr.locate_hand_joints(OpenXRSpec.XrHandEXT.XR_HAND_LEFT_EXT) or [None] * hand_joint_count - right_joints = openxr.locate_hand_joints(OpenXRSpec.XrHandEXT.XR_HAND_RIGHT_EXT) or [None] * hand_joint_count - joints = left_joints + right_joints - - for joint_idx in range(joint_count): - if tracking_enabled and joints[joint_idx] is not None: - location_flags = joints[joint_idx].locationFlags - - if ( - location_flags & OpenXRSpec.XR_SPACE_LOCATION_POSITION_VALID_BIT - and location_flags & OpenXRSpec.XR_SPACE_LOCATION_ORIENTATION_VALID_BIT - ): - joint_pos = joints[joint_idx].pose.position - joint_quat = joints[joint_idx].pose.orientation - current_positions[joint_idx] = Gf.Vec3f(joint_pos.x, joint_pos.y, joint_pos.z) - current_orientations[joint_idx] = Gf.Quath(joint_quat.w, joint_quat.x, joint_quat.y, joint_quat.z) - proto_indices[joint_idx] = 0 - else: - proto_indices[joint_idx] = 1 - else: - proto_indices[joint_idx] = 1 - - positions_attr.Set(current_positions) - orientations_attr.Set(current_orientations) - proto_idx_attr.Set(proto_indices) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/omni.isaac.dynamic_control/franka_articulation.py b/simulation/isaac-sim/standalone_examples/api/omni.isaac.dynamic_control/franka_articulation.py deleted file mode 100644 index 8a6242728..000000000 --- a/simulation/isaac-sim/standalone_examples/api/omni.isaac.dynamic_control/franka_articulation.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import sys - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -# This sample loads an articulation and prints its information -import carb -import omni -from isaacsim.storage.native import get_assets_root_path -from omni.isaac.dynamic_control import _dynamic_control - -stage = simulation_app.context.get_stage() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -omni.usd.get_context().open_stage(asset_path) -# start simulation -omni.timeline.get_timeline_interface().play() - -# perform timestep -simulation_app.update() - -dc = _dynamic_control.acquire_dynamic_control_interface() -# Get handle to articulation -art = dc.get_articulation("/panda") -if art == _dynamic_control.INVALID_HANDLE: - print("*** '%s' is not an articulation" % "/panda") -else: - # Print information about articulation - root = dc.get_articulation_root_body(art) - print(str("Got articulation handle %d \n" % art) + str("--- Hierarchy\n")) - - body_states = dc.get_articulation_body_states(art, _dynamic_control.STATE_ALL) - print(str("--- Body states:\n") + str(body_states) + "\n") - - dof_states = dc.get_articulation_dof_states(art, _dynamic_control.STATE_ALL) - print(str("--- DOF states:\n") + str(dof_states) + "\n") - - dof_props = dc.get_articulation_dof_properties(art) - print(str("--- DOF properties:\n") + str(dof_props) + "\n") - -# Simulate robot coming to a rest configuration -for i in range(100): - simulation_app.update() - -# Simulate robot for a fixed number of frames and specify a joint position target -for i in range(100): - dof_ptr = dc.find_articulation_dof(art, "panda_joint2") - # This should be called each frame of simulation if state on the articulation is being changed. - dc.wake_up_articulation(art) - # Set joint position target - dc.set_dof_position_target(dof_ptr, -1.5) - simulation_app.update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/omni.kit.app/app_framework.py b/simulation/isaac-sim/standalone_examples/api/omni.kit.app/app_framework.py deleted file mode 100644 index 3be79ac9d..000000000 --- a/simulation/isaac-sim/standalone_examples/api/omni.kit.app/app_framework.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import asyncio -import os - -from isaacsim import AppFramework - -argv = [ - "--empty", - "--ext-folder", - f'{os.path.abspath(os.environ["ISAAC_PATH"])}/exts', - "--no-window", - "--/app/asyncRendering=False", - "--/app/fastShutdown=True", - "--enable", - "omni.usd", - "--enable", - "omni.kit.uiapp", -] -# startup -app = AppFramework("test_app", argv) - -import omni.usd - -stage_task = asyncio.ensure_future(omni.usd.get_context().new_stage_async()) - -while not stage_task.done(): - app.update() - -print("exiting") -app.close() diff --git a/simulation/isaac-sim/standalone_examples/api/omni.kit.asset_converter/asset_usd_converter.py b/simulation/isaac-sim/standalone_examples/api/omni.kit.asset_converter/asset_usd_converter.py deleted file mode 100644 index 8c2b39cc1..000000000 --- a/simulation/isaac-sim/standalone_examples/api/omni.kit.asset_converter/asset_usd_converter.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import asyncio -import os - -from isaacsim import SimulationApp - - -async def convert(in_file, out_file, load_materials=False): - # This import causes conflicts when global - import omni.kit.asset_converter - - def progress_callback(progress, total_steps): - pass - - converter_context = omni.kit.asset_converter.AssetConverterContext() - # setup converter and flags - converter_context.ignore_materials = not load_materials - # converter_context.ignore_animation = False - # converter_context.ignore_cameras = True - # converter_context.single_mesh = True - # converter_context.smooth_normals = True - # converter_context.preview_surface = False - # converter_context.support_point_instancer = False - # converter_context.embed_mdl_in_usd = False - # converter_context.use_meter_as_world_unit = True - # converter_context.create_world_as_default_root_prim = False - instance = omni.kit.asset_converter.get_instance() - task = instance.create_converter_task(in_file, out_file, progress_callback, converter_context) - success = True - while True: - success = await task.wait_until_finished() - if not success: - await asyncio.sleep(0.1) - else: - break - return success - - -def asset_convert(args): - supported_file_formats = ["stl", "obj", "fbx"] - for folder in args.folders: - local_asset_output = folder + "_converted" - result = omni.client.create_folder(f"{local_asset_output}") - - for folder in args.folders: - print(f"\nConverting folder {folder}...") - - (result, models) = omni.client.list(folder) - for i, entry in enumerate(models): - if i >= args.max_models: - print(f"max models ({args.max_models}) reached, exiting conversion") - break - - model = str(entry.relative_path) - model_name = os.path.splitext(model)[0] - model_format = (os.path.splitext(model)[1])[1:] - # Supported input file formats - if model_format in supported_file_formats: - input_model_path = folder + "/" + model - converted_model_path = folder + "_converted/" + model_name + "_" + model_format + ".usd" - if not os.path.exists(converted_model_path): - status = asyncio.get_event_loop().run_until_complete( - convert(input_model_path, converted_model_path, True) - ) - if not status: - print(f"ERROR Status is {status}") - print(f"---Added {converted_model_path}") - - -if __name__ == "__main__": - kit = SimulationApp() - - import omni - from isaacsim.core.utils.extensions import enable_extension - - enable_extension("omni.kit.asset_converter") - - parser = argparse.ArgumentParser("Convert OBJ/STL assets to USD") - parser.add_argument( - "--folders", type=str, nargs="+", default=None, help="List of folders to convert (space seperated)." - ) - parser.add_argument( - "--max-models", type=int, default=50, help="If specified, convert up to `max-models` per folder." - ) - parser.add_argument( - "--load-materials", action="store_true", help="If specified, materials will be loaded from meshes" - ) - args, unknown_args = parser.parse_known_args() - - if args.folders is not None: - # Ensure Omniverse Kit is launched via SimulationApp before asset_convert() is called - asset_convert(args) - else: - print(f"No folders specified via --folders argument, exiting") - - # cleanup - kit.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_camera.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_camera.py deleted file mode 100644 index 589148ac5..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_camera.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-cameras", type=int, default=1, help="Number of cameras") -parser.add_argument( - "--resolution", nargs=2, type=int, default=[1280, 720], help="Camera resolution as [width, height] px" -) -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_camera = args.num_cameras -resolution = args.resolution -n_gpu = args.num_gpus -n_frames = args.num_frames - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True, "max_gpu_count": n_gpu}) - -TEST_NUM_APP_UPDATES = 60 * 10 - -import carb -import omni -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.rotations import euler_angles_to_quat -from isaacsim.core.utils.stage import is_stage_loading -from isaacsim.sensors.camera import Camera -from omni.kit.viewport.utility import get_active_viewport - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_camera", - workflow_metadata={ - "metadata": [ - {"name": "num_cameras", "data": n_camera}, - {"name": "width", "data": resolution[0]}, - {"name": "height", "data": resolution[1]}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - ] - }, - backend_type=args.backend_type, -) -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - - -scene_path = "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" -benchmark.fully_load_stage(benchmark.assets_root_path + scene_path) - -timeline = omni.timeline.get_timeline_interface() -timeline.play() -cameras = [] - -for i in range(n_camera): - render_product_path = None - if i == 0: - viewport_api = get_active_viewport() - render_product_path = viewport_api.get_render_product_path() - cameras.append( - Camera( - prim_path="/Cameras/Camera_" + str(i), - position=np.array([-8, 13, 2.0]), - resolution=resolution, - orientation=euler_angles_to_quat([90, 0, 90 + i * 360 / n_camera], degrees=True), - render_product_path=render_product_path, - ) - ) - - omni.kit.app.get_app().update() - cameras[i].initialize() - -# make sure scene is loaded in all viewports -while is_stage_loading(): - print("asset still loading, waiting to finish") - omni.kit.app.get_app().update() -omni.kit.app.get_app().update() - -benchmark.store_measurements() -# perform benchmark -benchmark.set_phase("benchmark") - -for _ in range(1, n_frames): - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.stop() - -timeline.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_core_world.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_core_world.py deleted file mode 100644 index ed3e967d6..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_core_world.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-envs", type=int, default=1, help="Number of environments to clone.") -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_envs = args.num_envs -n_gpu = args.num_gpus - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -from isaacsim.core.utils.extensions import enable_extension - -enable_extension("isaacsim.benchmark.services") - -import sys - -import carb -import numpy as np -from isaacsim.benchmark.services import BaseIsaacBenchmark -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid, VisualCuboid -from isaacsim.core.cloner import GridCloner -from isaacsim.core.prims import Articulation, GeometryPrim, RigidPrim, XFormPrim -from isaacsim.core.utils.prims import define_prim -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path - - -def define_environment(): - define_prim(prim_path="/World/env_0", prim_type="Xform") - XFormPrim("/World/env_0", positions=np.array([[0.0, 0.0, 0.0]])) - cube_1 = VisualCuboid( - prim_path="/World/env_0/new_cube_1", - name="visual_cube", - position=np.array([0, 0, 0.5]), - size=0.3, - color=np.array([255, 255, 255]), - ) - - cube_2 = DynamicCuboid( - prim_path="/World/env_0/new_cube_2", - name="cube_1", - position=np.array([0, 0, 1.0]), - scale=np.array([0.6, 0.5, 0.2]), - size=1.0, - color=np.array([255, 0, 0]), - ) - - cube_3 = DynamicCuboid( - prim_path="/World/env_0/new_cube_3", - name="cube_2", - position=np.array([0, 0, 3.0]), - scale=np.array([0.1, 0.1, 0.1]), - size=1.0, - color=np.array([0, 0, 255]), - linear_velocity=np.array([0, 0, 0.4]), - ) - - assets_root_path = get_assets_root_path() - if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" - add_reference_to_stage(usd_path=asset_path, prim_path="/World/env_0/Franka_1") - add_reference_to_stage(usd_path=asset_path, prim_path="/World/env_0/Franka_2") - XFormPrim("/World/env_0/Franka_1", name="my_franka_1", positions=np.array([[0.0, 2.0, 0.0]])) - XFormPrim("/World/env_0/Franka_2", name="my_franka_2", positions=np.array([[0.0, -2.0, 0.0]])) - - -def clone_environments(): - cloner = GridCloner(spacing=1) - cloner.define_base_env("/World") - prim_paths = cloner.generate_paths("/World/env", n_envs) - cloner.clone(source_prim_path="/World/env_0", prim_paths=prim_paths, replicate_physics=True, copy_from_source=False) - - -def create_cube_views(): - my_world.scene.add( - GeometryPrim( - prim_paths_expr="/World/env_*/new_cube_1", - name="visual_cube_view", - ) - ) - my_world.scene.add(RigidPrim(prim_paths_expr="/World/env_*/new_cube_2", name="rigid_cube_view_1")) - my_world.scene.add(RigidPrim(prim_paths_expr="/World/env_*/new_cube_3", name="rigid_cube_view_2")) - - -def create_articulation_views(): - my_world.scene.add( - Articulation( - prim_paths_expr="/World/env_*/Franka_1", - name="articulation_view_1", - ) - ) - my_world.scene.add( - Articulation( - prim_paths_expr="/World/env_*/Franka_2", - name="articulation_view_2", - ) - ) - - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_world", - workflow_metadata={"metadata": []}, - backend_type=args.backend_type, -) - -benchmark.set_phase("world_creation", start_recording_frametime=False, start_recording_runtime=True) -my_world = World(stage_units_in_meters=1.0) -benchmark.store_measurements() - -my_world.scene.add_default_ground_plane() - - -benchmark.set_phase("env_creation", start_recording_frametime=False, start_recording_runtime=True) -define_environment() -benchmark.store_measurements() - -benchmark.set_phase("env_cloning", start_recording_frametime=False, start_recording_runtime=True) -clone_environments() -benchmark.store_measurements() - -benchmark.set_phase("cube_views_creation", start_recording_frametime=False, start_recording_runtime=True) -create_cube_views() -benchmark.store_measurements() - -benchmark.set_phase("articulation_views_creation", start_recording_frametime=False, start_recording_runtime=True) -create_articulation_views() -benchmark.store_measurements() - -benchmark.set_phase("get_world_pose_articulation_no_sim", start_recording_frametime=False, start_recording_runtime=True) -articulation_view_1 = my_world.scene.get_object("articulation_view_1") -articulation_view_1.get_world_poses() -benchmark.store_measurements() - -benchmark.set_phase("world_resetting", start_recording_frametime=False, start_recording_runtime=True) -my_world.reset() -benchmark.store_measurements() - -benchmark.set_phase("world_step_w_render", start_recording_frametime=True, start_recording_runtime=False) -for i in range(100): - articulation_view_1.set_joint_position_targets(positions=np.random.randn(n_envs, 9)) - my_world.step(render=True) -benchmark.store_measurements() - -benchmark.set_phase("world_step_no_render", start_recording_frametime=True, start_recording_runtime=False) -for i in range(100): - articulation_view_1.set_joint_position_targets(positions=np.random.randn(n_envs, 9)) - my_world.step(render=False) -benchmark.store_measurements() - -benchmark.set_phase("get_world_pose_articulation_w_sim", start_recording_frametime=False, start_recording_runtime=True) -articulation_view_1.get_world_poses() -benchmark.store_measurements() - - -benchmark.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_nucleus_kpis.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_nucleus_kpis.py deleted file mode 100644 index e21199956..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_nucleus_kpis.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import asyncio - -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path, recursive_list_folder - -enable_extension("isaacsim.benchmark.services") - -from isaacsim.benchmark.services import BaseIsaacBenchmark -from isaacsim.benchmark.services.datarecorders import interface -from isaacsim.benchmark.services.metrics import measurements - - -class IsaacSimNucleusKPIRecorder(interface.MeasurementDataRecorder): - def __init__(self): - self.assets_root_path = get_assets_root_path() - self._loop = asyncio.get_event_loop() - - def _get_num_usds_in_path(self, path: str): - files = self._loop.run_until_complete(recursive_list_folder(path)) - return len([f for f in files if f.endswith(".usd")]) - - def get_data(self): - - measurements_out = [] - - # of objects & scenes for SDG in USD - nucleus_env_paths = self.assets_root_path + "/Isaac/Environments/" - num_environments = self._get_num_usds_in_path(nucleus_env_paths) - - nucleus_prop_paths = self.assets_root_path + "/Isaac/Props/" - num_props = self._get_num_usds_in_path(nucleus_prop_paths) - measurements_out.append( - measurements.SingleMeasurement( - name=f"Number of Objects & Scenes for SDG", - value=num_environments + num_props, - unit="", - ) - ) - - # of robots from partners - nucleus_robot_paths = self.assets_root_path + "/Isaac/Robots/" - num_robots_from_partners = self._get_num_usds_in_path(nucleus_robot_paths) - measurements_out.append( - measurements.SingleMeasurement( - name=f"Number of Robots from Partners", - value=num_robots_from_partners, - unit="", - ) - ) - - # of sensors from partners - nucleus_sensor_paths = self.assets_root_path + "/Isaac/Sensors/" - num_sensors_from_partners = self._get_num_usds_in_path(nucleus_sensor_paths) - measurements_out.append( - measurements.SingleMeasurement( - name=f"Number of Sensors from Partners", - value=num_sensors_from_partners, - unit="", - ) - ) - - return interface.MeasurementData(measurements=measurements_out) - - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_nucleus_kpis", - backend_type=args.backend_type, -) -benchmark.set_phase("benchmark", start_recording_frametime=False, start_recording_runtime=False) -benchmark.recorders.append(IsaacSimNucleusKPIRecorder()) -benchmark.store_measurements() - -benchmark.stop() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_physx_lidar.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_physx_lidar.py deleted file mode 100644 index 55dddda81..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_physx_lidar.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-sensors", type=int, default=1, help="Number of sensors") -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_sensor = args.num_sensors -n_frames = args.num_frames - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import carb -import omni.kit.test -from isaacsim.core.api import PhysicsContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.rotations import euler_angles_to_quat -from pxr import Gf, UsdGeom - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - - -# Create PhysX Lidar from params -def add_physx_lidar(prim_path, translation=Gf.Vec3f(0, 0, 0), orientation=Gf.Vec4f(0, 0, 0, 0)): - _, lidar = omni.kit.commands.execute( - "RangeSensorCreateLidar", - path=prim_path, - parent=None, - min_range=0.4, - max_range=100.0, - draw_points=True, - draw_lines=True, - horizontal_fov=360.0, - vertical_fov=30.0, - horizontal_resolution=0.4, - vertical_resolution=4.0, - rotation_rate=0.0, - high_lod=False, - yaw_offset=0.0, - ) - lidar_prim = lidar.GetPrim() - - if "xformOp:translate" not in lidar_prim.GetPropertyNames(): - UsdGeom.Xformable(lidar_prim).AddTranslateOp() - if "xformOp:orient" not in lidar_prim.GetPropertyNames(): - UsdGeom.Xformable(lidar_prim).AddOrientOp() - - lidar_prim.GetAttribute("xformOp:translate").Set(translation) - lidar_prim.GetAttribute("xformOp:orient").Set(orientation) - - -# ---------------------------------------------------------------------- -# Create benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_physx_lidar", - workflow_metadata={"metadata": [{"name": "num_lidars", "data": n_sensor}]}, - backend_type=args.backend_type, -) -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - -scene_path = "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" -benchmark.fully_load_stage(benchmark.assets_root_path + scene_path) -PhysicsContext(physics_dt=1.0 / 60.0) - -for i in range(n_sensor): - lidar_path = f"/World/PhysxLidar_{i}" - sensor_translation = Gf.Vec3f([-8, 13, 2.0]) # Positions set for full_warehouse.usd - q = euler_angles_to_quat([90, 0, 90 + i * 360 / n_sensor], degrees=True) - sensor_orientation = Gf.Quatf(q[0], q[1], q[2], q[3]) - add_physx_lidar(prim_path=lidar_path, translation=sensor_translation, orientation=sensor_orientation) - - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.set_phase("benchmark") - -timeline = omni.timeline.get_timeline_interface() -timeline.play() - -for _ in range(0, n_frames): - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.stop() - -timeline.stop() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_evobot.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_evobot.py deleted file mode 100644 index 097ec800d..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_evobot.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-robots", nargs="+", type=int, default=[1, 10, 20], help="Number of robots per phase") -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_robot = args.num_robots -n_gpu = args.num_gpus -n_frames = args.num_frames - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True, "max_gpu_count": n_gpu}) - -import carb -import isaacsim.core.utils.prims as prims_utils -import isaacsim.core.utils.stage as stage_utils -import omni -import omni.kit.test -from isaacsim.core.api import PhysicsContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.robot.wheeled_robots.robots import WheeledRobot - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_robots_evobot", - workflow_metadata={ - "metadata": [ - {"name": "num_robots", "data": n_robot}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - ] - }, - backend_type=args.backend_type, -) -robot_path = "/Isaac/Robots/Evobot/evobot.usd" -scene_path = "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" -benchmark.fully_load_stage(benchmark.assets_root_path + scene_path) -stage = omni.usd.get_context().get_stage() -PhysicsContext(physics_dt=1.0 / 60.0) -timeline = omni.timeline.get_timeline_interface() - -for num_robot in n_robot: - benchmark.set_phase(f"loading_{num_robot}_robots", start_recording_frametime=False, start_recording_runtime=True) - - robots = [] - for i in range(int(num_robot)): - robot_prim_path = "/Robots/Robot_" + str(i) - robot_usd_path = benchmark.assets_root_path + robot_path - # position the robot - MAX_IN_LINE = 10 - robot_position = np.array([-2 * (i % MAX_IN_LINE), -2 * np.floor(i / MAX_IN_LINE), 0]) - current_robot = WheeledRobot( - prim_path=robot_prim_path, - wheel_dof_names=["left_wheel_joint", "right_wheel_joint"], - create_robot=True, - usd_path=robot_usd_path, - position=robot_position, - ) - - omni.kit.app.get_app().update() - omni.kit.app.get_app().update() - - robots.append(current_robot) - - timeline.play() - omni.kit.app.get_app().update() - - for robot in robots: - robot.initialize() - # start the robot rotating in place - robot.apply_wheel_actions( - ArticulationAction(joint_positions=None, joint_efforts=None, joint_velocities=5 * np.array([1, -1])) - ) - - omni.kit.app.get_app().update() - omni.kit.app.get_app().update() - - benchmark.store_measurements() - # Perform benchmark - benchmark.set_phase(f"benchmark_{num_robot}_robots") - - for _ in range(1, n_frames): - omni.kit.app.get_app().update() - - benchmark.store_measurements() - timeline.stop() - predicate = lambda path: prims_utils.get_prim_type_name(path) == "Robots" - stage_utils.clear_stage(predicate) - -benchmark.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_nova_carter.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_nova_carter.py deleted file mode 100644 index 8eb62ceb3..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_nova_carter.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-robots", type=int, default=1, help="Number of robots") -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_robot = args.num_robots -n_gpu = args.num_gpus -n_frames = args.num_frames - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True, "max_gpu_count": n_gpu}) - -import carb -import omni -import omni.kit.test -from isaacsim.core.api import PhysicsContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.core.utils.viewports import set_camera_view -from isaacsim.robot.wheeled_robots.robots import WheeledRobot - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_robots_nova_carter", - workflow_metadata={ - "metadata": [ - {"name": "num_robots", "data": n_robot}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - ] - }, - backend_type=args.backend_type, -) -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - -robot_path = "/Isaac/Robots/NVIDIA/Carter/nova_carter/nova_carter.usd" -scene_path = "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" -benchmark.fully_load_stage(benchmark.assets_root_path + scene_path) -stage = omni.usd.get_context().get_stage() -PhysicsContext(physics_dt=1.0 / 60.0) -set_camera_view(eye=[-6, -15.5, 6.5], target=[-6, 10.5, -1], camera_prim_path="/OmniverseKit_Persp") - -robots = [] -for i in range(n_robot): - robot_prim_path = "/Robots/Robot_" + str(i) - robot_usd_path = benchmark.assets_root_path + robot_path - # position the robot - MAX_IN_LINE = 10 - robot_position = np.array([-2 * (i % MAX_IN_LINE), -2 * np.floor(i / MAX_IN_LINE), 0]) - current_robot = WheeledRobot( - prim_path=robot_prim_path, - wheel_dof_names=["joint_wheel_left", "joint_wheel_right"], - create_robot=True, - usd_path=robot_usd_path, - position=robot_position, - ) - - omni.kit.app.get_app().update() - omni.kit.app.get_app().update() - - robots.append(current_robot) - -timeline = omni.timeline.get_timeline_interface() -timeline.play() -omni.kit.app.get_app().update() - -for robot in robots: - robot.initialize() - # start the robot rotating in place so not to run into each - robot.apply_wheel_actions( - ArticulationAction(joint_positions=None, joint_efforts=None, joint_velocities=5 * np.array([0, 1])) - ) - -omni.kit.app.get_app().update() -omni.kit.app.get_app().update() - -benchmark.store_measurements() -# perform benchmark -benchmark.set_phase("benchmark") - -for _ in range(1, n_frames): - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.stop() - -timeline.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_nova_carter_ros2.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_nova_carter_ros2.py deleted file mode 100644 index de59cfd45..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_nova_carter_ros2.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-robots", type=int, default=1, help="Number of robots") -parser.add_argument( - "--enable-3d-lidar", type=int, default=0, choices=range(0, 1 + 1), help="Number of 3D lidars to enable, per robot." -) -parser.add_argument( - "--enable-2d-lidar", type=int, default=0, choices=range(0, 2 + 1), help="Number of 2D lidars to enable, per robot." -) -parser.add_argument( - "--enable-hawks", - type=int, - default=0, - choices=range(0, 4 + 1), - help="Number of Hawk camera stereo pairs to enable, per robot.", -) -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_robot = args.num_robots -enable_3d_lidar = args.enable_3d_lidar -enable_2d_lidar = args.enable_2d_lidar -enable_hawks = args.enable_hawks -n_gpu = args.num_gpus -n_frames = args.num_frames - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True, "max_gpu_count": n_gpu}) - -import carb -import omni -import omni.graph.core as og -import omni.kit.test -from isaacsim.core.api import PhysicsContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.stage import get_current_stage -from isaacsim.core.utils.viewports import set_camera_view -from isaacsim.robot.wheeled_robots.robots import WheeledRobot -from pxr import Usd - -enable_extension("isaacsim.benchmark.services") - -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_robots_nova_carter_ros2", - workflow_metadata={ - "metadata": [ - {"name": "num_hawks", "data": enable_hawks}, - {"name": "num_2d_lidars", "data": enable_2d_lidar}, - {"name": "num_3d_lidars", "data": enable_3d_lidar}, - {"name": "num_robots", "data": n_robot}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - ] - }, - backend_type=args.backend_type, -) - -# Generate Twist message -def move_cmd_msg(x, y, z, ax, ay, az): - msg = Twist() - msg.linear.x = x - msg.linear.y = y - msg.linear.z = z - msg.angular.x = ax - msg.angular.y = ay - msg.angular.z = az - return msg - - -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - -enable_extension("isaacsim.ros2.bridge") -import rclpy -from geometry_msgs.msg import Twist - -omni.kit.app.get_app().update() - -# Create publisher for move commands -rclpy.init() -node = rclpy.create_node("cmd_vel_publisher") -cmd_vel_pub = node.create_publisher(Twist, "cmd_vel", 1) - -robot_path = "/Isaac/Samples/ROS2/Robots/Nova_Carter_ROS.usd" -scene_path = "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" - -benchmark.fully_load_stage(benchmark.assets_root_path + scene_path) - -# NOTE: Modify endtimecode to prevent step skipping errors -with Usd.EditContext(get_current_stage(), get_current_stage().GetRootLayer()): - get_current_stage().SetEndTimeCode(1000000.0) - -stage = omni.usd.get_context().get_stage() -PhysicsContext(physics_dt=1.0 / 60.0) -set_camera_view(eye=[-6, -15.5, 6.5], target=[-6, 10.5, -1], camera_prim_path="/OmniverseKit_Persp") - -lidars_2d = ["/front_2d_lidar_render_product", "/back_2d_lidar_render_product"] -hawk_actiongraphs = ["/front_hawk", "/left_hawk", "/right_hawk", "/back_hawk"] - -robots = [] -for i in range(n_robot): - robot_prim_path = "/Robots/Robot_" + str(i) - robot_usd_path = benchmark.assets_root_path + robot_path - # position the robot robot - MAX_IN_LINE = 10 - robot_position = np.array([-2 * (i % MAX_IN_LINE), -2 * np.floor(i / MAX_IN_LINE), 0]) - current_robot = WheeledRobot( - prim_path=robot_prim_path, - wheel_dof_names=["joint_wheel_left", "joint_wheel_right"], - create_robot=True, - usd_path=robot_usd_path, - position=robot_position, - ) - - omni.kit.app.get_app().update() - omni.kit.app.get_app().update() - - for i in range(len(lidars_2d)): - if i < enable_2d_lidar: - og.Controller.attribute(robot_prim_path + "/ros_lidars" + lidars_2d[i] + ".inputs:enabled").set(True) - else: - og.Controller.attribute(robot_prim_path + "/ros_lidars" + lidars_2d[i] + ".inputs:enabled").set(False) - - if enable_3d_lidar > 0: - og.Controller.attribute(robot_prim_path + "/ros_lidars/front_3d_lidar_render_product.inputs:enabled").set(True) - else: - og.Controller.attribute(robot_prim_path + "/ros_lidars/front_3d_lidar_render_product.inputs:enabled").set(False) - - for i in range(len(hawk_actiongraphs)): - if i < enable_hawks: - og.Controller.attribute( - robot_prim_path + hawk_actiongraphs[i] + "/left_camera_render_product" + ".inputs:enabled" - ).set(True) - og.Controller.attribute( - robot_prim_path + hawk_actiongraphs[i] + "/right_camera_render_product" + ".inputs:enabled" - ).set(True) - else: - og.Controller.attribute( - robot_prim_path + hawk_actiongraphs[i] + "/left_camera_render_product" + ".inputs:enabled" - ).set(False) - og.Controller.attribute( - robot_prim_path + hawk_actiongraphs[i] + "/right_camera_render_product" + ".inputs:enabled" - ).set(False) - - robots.append(current_robot) - -# Set this to true so that we always publish regardless of subscribers -carb.settings.get_settings().set_bool("/exts/isaacsim.ros2.bridge/publish_without_verification", True) - -timeline = omni.timeline.get_timeline_interface() -timeline.play() -omni.kit.app.get_app().update() - -for robot in robots: - robot.initialize() - # start the robot rotating in place so not to run into each - move_cmd = move_cmd_msg(0.0, 0.0, 0.0, 0.0, 0.0, 1.0) - cmd_vel_pub.publish(move_cmd) - -omni.kit.app.get_app().update() -omni.kit.app.get_app().update() - -benchmark.store_measurements() -# perform benchmark -benchmark.set_phase("benchmark") - -for _ in range(1, n_frames): - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.stop() - -node.destroy_node() -rclpy.shutdown() - -timeline.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_o3dyn.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_o3dyn.py deleted file mode 100644 index f7721b1b6..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_o3dyn.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-robots", type=int, default=1, help="Number of robots") -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument("--max-in-line", type=int, default=10, help="Max number of robots in line") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_robots = args.num_robots -n_frames = args.num_frames -n_gpus = args.num_gpus -max_line = args.max_in_line - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True, "max_gpu_count": n_gpus}) - -import carb -import numpy as np -import omni.kit.test -from isaacsim.core.api import PhysicsContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.core.utils.viewports import set_camera_view -from isaacsim.robot.wheeled_robots.robots import WheeledRobot -from omni.kit.viewport.utility import get_active_viewport - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_o3dyn_robot", - workflow_metadata={ - "metadata": [ - {"name": "num_robots", "data": n_robots}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - ] - }, - backend_type=args.backend_type, -) -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - -robot_path = "/Isaac/Robots/O3dyn/o3dyn.usd" -scene_path = "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" -benchmark.fully_load_stage(benchmark.assets_root_path + scene_path) -PhysicsContext(physics_dt=1.0 / 60.0) -set_camera_view(eye=[-6, -15.5, 6.5], target=[-6, 10.5, -1], camera_prim_path="/OmniverseKit_Persp") - -# Configure robots -robots = [] -for i in range(n_robots): - robot_prim_path = f"/Robots/Robot_{i}" - robot_usd_path = benchmark.assets_root_path + robot_path - - # Arrange robot positions - robot_pos = np.array([-3 * (i % max_line) + 3, -3 * np.floor(i / max_line), 0.1]) - current_robot = WheeledRobot( - prim_path=robot_prim_path, - wheel_dof_names=["wheel_fl_joint", "wheel_fr_joint", "wheel_rl_joint", "wheel_rr_joint"], - create_robot=True, - usd_path=robot_usd_path, - position=robot_pos, - ) - - omni.kit.app.get_app().update() - robots.append(current_robot) - -viewport = get_active_viewport() -viewport.set_texture_resolution([1280, 720]) -omni.kit.app.get_app().update() -omni.kit.app.get_app().update() - -timeline = omni.timeline.get_timeline_interface() -timeline.play() -# NOTE: PhysX Simulation Context error if this update() is removed -omni.kit.app.get_app().update() - -# Start robot movement - rotation in place -for robot in robots: - robot.initialize() - robot.apply_wheel_actions( - ArticulationAction(joint_positions=None, joint_efforts=None, joint_velocities=5 * np.array([1, -1, 1, -1])) - ) -omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.set_phase("benchmark") - -for _ in range(0, n_frames): - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.stop() - -timeline.stop() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_ur10.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_ur10.py deleted file mode 100644 index defd67a34..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_robots_ur10.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-robots", type=int, default=1, help="Number of robots") -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument("--device", type=str, default="cpu", help="simulation device, cpu or cuda") -parser.add_argument("--visual", type=bool, default=False, help="Render for debugging purposes") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_robot = args.num_robots -n_gpu = args.num_gpus -n_frames = args.num_frames -device = args.device -visual = args.visual - -import numpy as np -import torch -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": not visual, "max_gpu_count": n_gpu}) - -import asyncio -from functools import partial - -import carb -import isaacsim.core.utils.stage as stage_utils -import omni.physx as _physx -import omni.timeline -from isaacsim.core.api import PhysicsContext, World -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.stage import open_stage_async, update_stage_async -from isaacsim.core.utils.types import ArticulationActions -from omni.kit.viewport.utility import get_active_viewport - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_robots_ur10", - workflow_metadata={ - "metadata": [ - {"name": "num_robots", "data": n_robot}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - {"name": "device", "data": device}, - ] - }, - backend_type=args.backend_type, -) - -# Something about this being in an array makes it work as a global variable inside the physics sub -timestep = [0] - -observed_positions, observed_velocities = [], [] -commanded_positions, commanded_velocities = [], [] - -# v_max is the maximum velocity that each joint will hit in its range of motion -v_max = torch.tensor([2.09, 2.09, 3.14, 3.14, 3.14, 3.14]) - -# T is the period of each sinusoid -T = torch.tensor([9.43, 9.43, 6.28, 6.28, 6.28, 6.28]) - -joint_indices = torch.arange(6) - -robot_path = "/ur10" - - -def get_clipped_joint_ranges(articulation_view): - - limits = articulation_view.get_dof_limits() - lower_limit = limits[..., 0] - upper_limit = limits[..., 1] - - l = lower_limit.clone() - u = upper_limit.clone() - d = upper_limit - lower_limit - mask = d > 2 * torch.pi - - if torch.any(mask): - l[mask] = (upper_limit[mask] - lower_limit[mask]) / 2 + lower_limit[mask] - torch.pi - u[mask] = (upper_limit[mask] - lower_limit[mask]) / 2 + lower_limit[mask] + torch.pi - - return l, u - - -def get_joint_commands(articulation_view, v_max, T, joint_indices): - lower_joint_limits, upper_joint_limits = get_clipped_joint_ranges(articulation_view) - - lower_joint_limits = lower_joint_limits[:, joint_indices] - upper_joint_limits = upper_joint_limits[:, joint_indices] - - p_0 = lower_joint_limits + (upper_joint_limits - lower_joint_limits) / 2 - - position = lambda t: p_0 - v_max * T / torch.pi * torch.cos(torch.pi * t / T) - velocity = lambda t: v_max * torch.sin(torch.pi * t / T) - - return position, velocity - - -def on_physics_step(articulation_view, position_commands, velocity_commands, step): - if position_commands is None: - return - timestep[0] += step - if timestep[0] > 5: - return - - observed_positions.append(articulation_view.get_joint_positions(joint_indices=joint_indices)) - observed_velocities.append(articulation_view.get_joint_velocities(joint_indices=joint_indices)) - - position_command = position_commands(timestep[0]) - velocity_command = velocity_commands(timestep[0]) - - commanded_positions.append(position_command) - commanded_velocities.append(velocity_command) - - action = ArticulationActions(position_command, velocity_command, joint_indices=joint_indices) - articulation_view.apply_action(action) - - -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - -get_active_viewport().updates_enabled = visual - -robot_usd_path = "omniverse://ov-isaac-dev.nvidia.com/Isaac/Robots/UR10/ur10.usd" - -my_world = World(backend="torch", device=device) -PhysicsContext(physics_dt=1.0 / 60.0) -MAX_IN_LINE = 10 -positions = torch.zeros((n_robot, 3)) -for i in range(n_robot): - robot_prim_path = "/Robots/Robot_" + str(i) - # position the robot - robot_position = torch.tensor([-2 * (i % MAX_IN_LINE), -2 * np.floor(i / MAX_IN_LINE), 0]) - positions[i, :] = robot_position - stage_utils.add_reference_to_stage(robot_usd_path, robot_prim_path) - - -omni.kit.app.get_app().update() -my_world.scene.add_default_ground_plane(z_position=-1) - -robot_view = Articulation("/Robots/Robot_*", positions=positions) - -timeline = omni.timeline.get_timeline_interface() -timeline.play() -omni.kit.app.get_app().update() - -robot_view.initialize() -omni.kit.app.get_app().update() - -position_commands, velocity_commands = get_joint_commands(robot_view, v_max, T, joint_indices) -_physxIFace = _physx.acquire_physx_interface() -physx_subscription = _physxIFace.subscribe_physics_step_events( - partial(on_physics_step, robot_view, position_commands, velocity_commands) -) - -position_command = position_commands(0) -velocity_command = velocity_commands(0) - -robot_view.set_joint_positions(position_command, joint_indices=joint_indices) - -commanded_positions.append(position_command) -commanded_velocities.append(velocity_command) - -omni.kit.app.get_app().update() -omni.kit.app.get_app().update() - -benchmark.store_measurements() -# perform benchmark -benchmark.set_phase("benchmark") - -for _ in range(0, n_frames): - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.stop() - -physics_subscription = None - -timeline.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_rtx_lidar.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_rtx_lidar.py deleted file mode 100644 index 50cf50428..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_rtx_lidar.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-sensors", type=int, default=1, help="Number of sensors") -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument( - "--lidar-type", type=str, default="Rotary", choices=["Rotary", "Solid_State"], help="Type of lidar to create" -) - -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - - -args, unknown = parser.parse_known_args() - -n_sensor = args.num_sensors -n_gpu = args.num_gpus -n_frames = args.num_frames - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True, "max_gpu_count": n_gpu}) - -import carb -import omni -import omni.replicator.core as rep -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.prims import delete_prim -from pxr import Gf - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_rtx_lidar", - workflow_metadata={ - "metadata": [ - {"name": "num_3d_lidars", "data": n_sensor}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - ] - }, - backend_type=args.backend_type, -) -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - -scene_path = "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" -benchmark.fully_load_stage(benchmark.assets_root_path + scene_path) -timeline = omni.timeline.get_timeline_interface() -hydra_textures = [] -writers = [] -sensors = [] -lidar_type = args.lidar_type -for i in range(n_sensor): - lidar_path = "/World/Rtx" + lidar_type + "Lidar_" + str(i) - sensor_translation = Gf.Vec3f([-8, 13 + i * 2.0, 2.0]) # these positions are used for full_warehouse.usd - # make sure to test rotary and solid state together. - lidar_config = "Example_" + lidar_type - print("Lidar Config:", lidar_config) - - _, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxLidar", - path=lidar_path, - parent=None, - config=lidar_config, - translation=sensor_translation, - orientation=Gf.Quatd(1.0, 0.0, 0.0, 0.0), # Gf.Quatd is w,i,j,k - ) - sensors.append(sensor) - hydra_texture = rep.create.render_product(sensor.GetPath(), [1, 1], name="Isaac") - hydra_textures.append(hydra_texture) - # Create the post process graph that publishes the render var - writer = rep.writers.get("RtxLidarDebugDrawPointCloudBuffer") - writer.initialize() - writer.attach([hydra_texture]) - writers.append(writer) - - omni.kit.app.get_app().update() - -benchmark.store_measurements() - -benchmark.set_phase("benchmark") -timeline.play() - -for _ in range(1, n_frames): - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.stop() - -timeline.stop() - -for writer in writers: - writer.detach() -omni.kit.app.get_app().update() - -for sensor in sensors: - delete_prim(sensor.GetPath()) -omni.kit.app.get_app().update() - -for texture in hydra_textures: - omni.kit.app.get_app().update() - texture.destroy() - texture = None -omni.kit.app.get_app().update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_rtx_radar.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_rtx_radar.py deleted file mode 100644 index 0d2e3e89d..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_rtx_radar.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--num-sensors", type=int, default=1, help="Number of sensors") -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_sensor = args.num_sensors -n_frames = args.num_frames -n_gpus = args.num_gpus - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True, "max_gpu_count": n_gpus}) - -import carb -import omni.kit.test -import omni.replicator.core as rep -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.prims import delete_prim -from pxr import Gf - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - - -# Create RTX Radar from params -def add_rtx_radar(prim_path, sensor_translation, sensor_orientation): - _, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxRadar", - path=prim_path, - parent=None, - config="Example", - translation=sensor_translation, - orientation=sensor_orientation, - ) - return sensor - - -# ---------------------------------------------------------------------- -# Create benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_rtx_radar", - workflow_metadata={ - "metadata": [ - {"name": "num_radars", "data": n_sensor}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - ] - }, - backend_type=args.backend_type, -) -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - -scene_path = "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" -benchmark.fully_load_stage(benchmark.assets_root_path + scene_path) -timeline = omni.timeline.get_timeline_interface -sensors = [] -writers = [] -hydra_textures = [] - -for i in range(n_sensor): - radar_path = f"/World/rtx_radar_{i}" - sensor_translation = Gf.Vec3f([-0.937, -2.0 + i * 2.0, 0.8940]) # defined for full_warehouse.usd - sensor_orientation = Gf.Quatd(0.70711, 0.70711, 0, 0) - sensor = add_rtx_radar(radar_path, sensor_translation, sensor_orientation) - sensors.append(sensor) - - hydra = rep.create.render_product(sensor.GetPath(), [1, 1], name="Isaac") - hydra_textures.append(hydra) - - # Post-process graph to publish the render var - writer = rep.writers.get("Writer" + "IsaacPrintRTXSensorInfo") - writer.initialize() - writer.attach([hydra]) - writers.append(writer) - - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.set_phase("benchmark") - -timeline = omni.timeline.get_timeline_interface() -timeline.play() - -# NOTE: Need extra updates to process full num of frames -omni.kit.app.get_app().update() -omni.kit.app.get_app().update() - -for _ in range(0, n_frames): - omni.kit.app.get_app().update() - - -benchmark.store_measurements() -benchmark.stop() - -timeline.stop() - -# Destroy sensor components -for writer in writers: - writer.detach() -omni.kit.app.get_app().update() - -for sensor in sensors: - delete_prim(sensor.GetPath()) -omni.kit.app.get_app().update() - -for texture in hydra_textures: - omni.kit.app.get_app().update() - texture.destroy() - texture = None -omni.kit.app.get_app().update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_scene_loading.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_scene_loading.py deleted file mode 100644 index e84766af2..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_scene_loading.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import argparse -import time - -parser = argparse.ArgumentParser() -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to run benchmark for") -parser.add_argument( - "--duration", type=int, default=None, help="Optional - duration in minutes (wall-clock time), overrides frame count" -) -parser.add_argument("--env-url", default=None, required=True, help="Path to the environment url - required") -parser.add_argument( - "--camera-position", type=float, nargs=3, default=None, help="Set perspective position - optional" -) -parser.add_argument( - "--camera-target", type=float, nargs=3, default=None, help="Set perspective target - optional" -) -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -args, unknown = parser.parse_known_args() - -n_frames = args.num_frames -duration = args.duration -env_url = args.env_url -cam_pos = args.camera_position -cam_target = args.camera_target - -# Both cam_pos and cam_target should be specified if used -if (cam_pos and not cam_target) or (cam_target and not cam_pos): - parser.error("Both --camera-position and --camera-target must be specified together.") - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import carb -import omni -import omni.kit.test -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.viewports import set_camera_view - -enable_extension("isaacsim.ros2.bridge") -omni.kit.app.get_app().update() - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_scene_loading", - workflow_metadata={"metadata": [{"name": "env_url", "data": env_url}, {"name": "duration", "data": duration}]}, - backend_type=args.backend_type, -) - -# Track scene loading time -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) -benchmark.fully_load_stage(benchmark.assets_root_path + env_url) -benchmark.store_measurements() - -timeline = omni.timeline.get_timeline_interface() -timeline.play() - -benchmark.set_phase("benchmark") - -if cam_pos is not None: - set_camera_view(eye=cam_pos, target=cam_target, camera_prim_path="/OmniverseKit_Persp") - -if duration is not None: - start_time = time.time() - while time.time() - start_time < (duration * 60): - omni.kit.app.get_app().update() - -else: - for _ in range(1, n_frames): - omni.kit.app.get_app().update() - -benchmark.store_measurements() -benchmark.stop() - -timeline.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_sdg.py b/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_sdg.py deleted file mode 100644 index 81dfcc688..000000000 --- a/simulation/isaac-sim/standalone_examples/benchmarks/benchmark_sdg.py +++ /dev/null @@ -1,220 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse - -VALID_ANNOTATORS = { - "rgb", - "bounding_box_2d_tight", - "bounding_box_2d_loose", - "semantic_segmentation", - "instance_id_segmentation", - "instance_segmentation", - "distance_to_camera", - "distance_to_image_plane", - "bounding_box_3d", - "occlusion", - "normals", - "motion_vectors", - "camera_params", - "pointcloud", - "skeleton_data", -} - -parser = argparse.ArgumentParser() -parser.add_argument("--num-frames", type=int, default=600, help="Number of frames to capture") -parser.add_argument("--num-cameras", type=int, default=1, help="Number of cameras") -parser.add_argument("--num-gpus", type=int, default=None, help="Number of GPUs on machine.") -parser.add_argument("--resolution", nargs=2, type=int, default=[1280, 720], help="Camera resolution") -parser.add_argument( - "--asset-count", type=int, default=10, help="Number of assets of each type (cube, cone, cylinder, sphere, torus)" -) -parser.add_argument( - "--annotators", - nargs="+", - default=["rgb"], - choices=list(VALID_ANNOTATORS) + ["all"], - help="List of annotators to enable, separated by space. Use 'all' to select all available.", -) -parser.add_argument("--disable-viewport-rendering", action="store_true", help="Disable viewport rendering") -parser.add_argument("--delete-data-when-done", action="store_true", help="Delete local data after benchmarking") -parser.add_argument("--print-results", action="store_true", help="Print results in terminal") -parser.add_argument("--headless", action="store_true", help="Run in headless mode") -parser.add_argument( - "--backend-type", - default="OmniPerfKPIFile", - choices=["LocalLogMetrics", "JSONFileMetrics", "OsmoKPIFile", "OmniPerfKPIFile"], - help="Benchmarking backend, defaults", -) - -parser.add_argument("--skip-write", action="store_true", help="Skip writing annotator data to disk") -parser.add_argument("--env-url", default=None, help="Path to the environment url, default None") - -args, unknown = parser.parse_known_args() - -num_frames = args.num_frames -num_cameras = args.num_cameras -width, height = args.resolution[0], args.resolution[1] -asset_count = args.asset_count -annotators_str = ", ".join(args.annotators) -disable_viewport_rendering = args.disable_viewport_rendering -delete_data_when_done = args.delete_data_when_done -print_results = args.print_results -headless = args.headless -n_gpu = args.num_gpus -skip_write = args.skip_write -env_url = args.env_url - -if "all" in args.annotators: - annotators_kwargs = {annotator: True for annotator in VALID_ANNOTATORS} -else: - annotators_kwargs = {annotator: True for annotator in args.annotators if annotator in VALID_ANNOTATORS} - -print(f"[SDG Benchmark] Running SDG Benchmark with:") -print(f"\tnum_frames: {num_frames}") -print(f"\tnum_cameras: {num_cameras}") -print(f"\tresolution: {width}x{height}") -print(f"\tasset_count: {asset_count}") -print(f"\tannotators: {annotators_kwargs.keys()}") -print(f"\tdisable_viewport_rendering: {disable_viewport_rendering}") -print(f"\tdelete_data_when_done: {delete_data_when_done}") -print(f"\tprint_results: {print_results}") -print(f"\theadless: {headless}") -print(f"\tskip_write: {skip_write}") -print(f"\tenv_url: {env_url}") - -import os -import shutil -import time - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": headless, "max_gpu_count": n_gpu}) - -REPLICATOR_GLOBAL_SEED = 11 - -import carb -import omni.kit.app -import omni.replicator.core as rep -import omni.usd -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path -from omni.kit.viewport.utility import get_active_viewport - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# Create the benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_sdg", - workflow_metadata={ - "metadata": [ - {"name": "num_frames", "data": num_frames}, - {"name": "num_cameras", "data": num_cameras}, - {"name": "width", "data": width}, - {"name": "height", "data": height}, - {"name": "asset_count", "data": asset_count}, - {"name": "annotators", "data": annotators_str}, - {"name": "num_gpus", "data": carb.settings.get_settings().get("/renderer/multiGpu/currentGpuCount")}, - ] - }, - backend_type=args.backend_type, -) - -benchmark.set_phase("loading", start_recording_frametime=False, start_recording_runtime=True) - -if env_url is not None: - env_path = env_url if env_url.startswith("omniverse://") else get_assets_root_path() + env_url - print(f"[SDG Benchmark] Loading stage from path: {env_path}") - omni.usd.get_context().open_stage(env_path) -else: - print(f"[SDG Benchmark] Loading a new empty stage..") - omni.usd.get_context().new_stage() - -if disable_viewport_rendering: - print(f"[SDG Benchmark] Disabling viewport rendering..") - get_active_viewport().updates_enabled = False - -rep.set_global_seed(REPLICATOR_GLOBAL_SEED) -rep.create.light(rotation=(315, 0, 0), intensity=2000, light_type="distant") -rep.create.light(intensity=400, light_type="dome") -cubes = rep.create.cube(count=asset_count, semantics=[("class", "cube")]) -cones = rep.create.cone(count=asset_count, semantics=[("class", "cone")]) -cylinders = rep.create.cylinder(count=asset_count, semantics=[("class", "cylinder")]) -spheres = rep.create.sphere(count=asset_count, semantics=[("class", "sphere")]) -tori = rep.create.torus(count=asset_count, semantics=[("class", "torus")]) - -cameras = [] -for i in range(num_cameras): - cameras.append(rep.create.camera(name=f"cam_{i}")) -render_products = [] -for i, cam in enumerate(cameras): - render_products.append(rep.create.render_product(cam, (width, height), name=f"rp_{i}")) -if skip_write: - print("[SDG Benchmark] Skipping writing to disk, attaching annotators to render products..") - for annot_type, enabled in annotators_kwargs.items(): - if enabled: - annot = rep.AnnotatorRegistry.get_annotator(annot_type) - for rp in render_products: - annot.attach(rp) -else: - writer = rep.writers.get("BasicWriter") - output_directory = ( - os.getcwd() - + f"/_out_sdg_benchmark_{num_frames}_frames_{num_cameras}_cameras_{asset_count}_asset_count_{len(annotators_kwargs)}_annotators" - ) - print(f"[SDG Benchmark] Output directory: {output_directory}") - writer.initialize(output_dir=output_directory, **annotators_kwargs) - writer.attach(render_products) -assets = rep.create.group([cubes, cones, cylinders, spheres, tori]) -cameras = rep.create.group(cameras) - -with rep.trigger.on_frame(): - with assets: - rep.modify.pose( - position=rep.distribution.uniform((-3, -3, -3), (3, 3, 3)), - rotation=rep.distribution.uniform((0, 0, 0), (360, 360, 360)), - scale=rep.distribution.uniform(0.1, 1), - ) - rep.randomizer.color(rep.distribution.uniform((0, 0, 0), (1, 1, 1))) - with cameras: - rep.modify.pose( - position=rep.distribution.uniform((5, 5, 5), (10, 10, 10)), - look_at=(0, 0, 0), - ) - -rep.orchestrator.preview() -# Run for a few frames to ensure everything is loaded -for _ in range(10): - omni.kit.app.get_app().update() -benchmark.store_measurements() - -print("[SDG Benchmark] Starting SDG..") -benchmark.set_phase("benchmark") -start_time = time.time() -rep.orchestrator.run_until_complete(num_frames=num_frames) -end_time = time.time() -benchmark.store_measurements() -omni.kit.app.get_app().update() - -duration = end_time - start_time -avg_frametime = duration / num_frames -if delete_data_when_done and not skip_write: - print(f"[SDG Benchmark] Deleting data: {output_directory}") - shutil.rmtree(output_directory) -if print_results: - print(f"[SDG Benchmark] duration: {duration} seconds") - print(f"[SDG Benchmark] avg frametime: {avg_frametime:.4f} seconds") - print(f"[SDG Benchmark] avg FPS: {1 / avg_frametime:.2f}") - results_csv = f"{num_frames}, {num_cameras}, {width}, {height}, {asset_count}, {duration:.4f}, {avg_frametime:.4f}, {1 / avg_frametime:.2f}" - print(f"num_frames, num_cameras, width, height, asset_count, duration, avg_frametime, avg_fps\n{results_csv}\n") - -benchmark.stop() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/data/cube/cube.fbx b/simulation/isaac-sim/standalone_examples/data/cube/cube.fbx deleted file mode 100644 index d1d770c02..000000000 Binary files a/simulation/isaac-sim/standalone_examples/data/cube/cube.fbx and /dev/null differ diff --git a/simulation/isaac-sim/standalone_examples/data/torus/torus.mtl b/simulation/isaac-sim/standalone_examples/data/torus/torus.mtl deleted file mode 100644 index 3fb783de8..000000000 --- a/simulation/isaac-sim/standalone_examples/data/torus/torus.mtl +++ /dev/null @@ -1,12 +0,0 @@ -# Blender MTL File: 'None' -# Material Count: 1 - -newmtl Material -Ns 323.999994 -Ka 1.000000 1.000000 1.000000 -Kd 0.001347 0.000000 0.800000 -Ks 0.500000 0.500000 0.500000 -Ke 0.000000 0.000000 0.000000 -Ni 1.000000 -d 1.000000 -illum 2 diff --git a/simulation/isaac-sim/standalone_examples/data/torus/torus.obj b/simulation/isaac-sim/standalone_examples/data/torus/torus.obj deleted file mode 100644 index f2116edc1..000000000 --- a/simulation/isaac-sim/standalone_examples/data/torus/torus.obj +++ /dev/null @@ -1,2084 +0,0 @@ -# Blender v2.91.2 OBJ File: '' -# www.blender.org -mtllib torus.mtl -o Torus -v 1.250000 0.000000 0.000000 -v 1.216506 0.125000 0.000000 -v 1.125000 0.216506 0.000000 -v 1.000000 0.250000 0.000000 -v 0.875000 0.216506 0.000000 -v 0.783494 0.125000 0.000000 -v 0.750000 0.000000 0.000000 -v 0.783494 -0.125000 0.000000 -v 0.875000 -0.216506 0.000000 -v 1.000000 -0.250000 0.000000 -v 1.125000 -0.216506 0.000000 -v 1.216506 -0.125000 0.000000 -v 1.239306 0.000000 -0.163158 -v 1.206099 0.125000 -0.158786 -v 1.115376 0.216506 -0.146842 -v 0.991445 0.250000 -0.130526 -v 0.867514 0.216506 -0.114210 -v 0.776791 0.125000 -0.102266 -v 0.743584 0.000000 -0.097895 -v 0.776791 -0.125000 -0.102266 -v 0.867514 -0.216506 -0.114210 -v 0.991445 -0.250000 -0.130526 -v 1.115376 -0.216506 -0.146842 -v 1.206099 -0.125000 -0.158786 -v 1.207407 0.000000 -0.323524 -v 1.175055 0.125000 -0.314855 -v 1.086667 0.216506 -0.291171 -v 0.965926 0.250000 -0.258819 -v 0.845185 0.216506 -0.226467 -v 0.756797 0.125000 -0.202783 -v 0.724444 0.000000 -0.194114 -v 0.756797 -0.125000 -0.202783 -v 0.845185 -0.216506 -0.226467 -v 0.965926 -0.250000 -0.258819 -v 1.086667 -0.216506 -0.291171 -v 1.175055 -0.125000 -0.314855 -v 1.154849 0.000000 -0.478354 -v 1.123905 0.125000 -0.465537 -v 1.039364 0.216506 -0.430519 -v 0.923880 0.250000 -0.382683 -v 0.808395 0.216506 -0.334848 -v 0.723854 0.125000 -0.299830 -v 0.692910 0.000000 -0.287013 -v 0.723854 -0.125000 -0.299830 -v 0.808395 -0.216506 -0.334848 -v 0.923880 -0.250000 -0.382683 -v 1.039364 -0.216506 -0.430519 -v 1.123905 -0.125000 -0.465537 -v 1.082532 0.000000 -0.625000 -v 1.053525 0.125000 -0.608253 -v 0.974279 0.216506 -0.562500 -v 0.866025 0.250000 -0.500000 -v 0.757772 0.216506 -0.437500 -v 0.678525 0.125000 -0.391747 -v 0.649519 0.000000 -0.375000 -v 0.678525 -0.125000 -0.391747 -v 0.757772 -0.216506 -0.437500 -v 0.866025 -0.250000 -0.500000 -v 0.974279 -0.216506 -0.562500 -v 1.053525 -0.125000 -0.608253 -v 0.991692 0.000000 -0.760952 -v 0.965119 0.125000 -0.740562 -v 0.892523 0.216506 -0.684856 -v 0.793353 0.250000 -0.608761 -v 0.694184 0.216506 -0.532666 -v 0.621587 0.125000 -0.476961 -v 0.595015 0.000000 -0.456571 -v 0.621587 -0.125000 -0.476961 -v 0.694184 -0.216506 -0.532666 -v 0.793353 -0.250000 -0.608761 -v 0.892523 -0.216506 -0.684856 -v 0.965119 -0.125000 -0.740562 -v 0.883883 0.000000 -0.883884 -v 0.860200 0.125000 -0.860200 -v 0.795495 0.216506 -0.795495 -v 0.707107 0.250000 -0.707107 -v 0.618718 0.216506 -0.618719 -v 0.554014 0.125000 -0.554014 -v 0.530330 0.000000 -0.530330 -v 0.554014 -0.125000 -0.554014 -v 0.618718 -0.216506 -0.618719 -v 0.707107 -0.250000 -0.707107 -v 0.795495 -0.216506 -0.795495 -v 0.860200 -0.125000 -0.860200 -v 0.760952 0.000000 -0.991691 -v 0.740562 0.125000 -0.965119 -v 0.684857 0.216506 -0.892522 -v 0.608762 0.250000 -0.793353 -v 0.532666 0.216506 -0.694184 -v 0.476961 0.125000 -0.621587 -v 0.456571 0.000000 -0.595015 -v 0.476961 -0.125000 -0.621587 -v 0.532666 -0.216506 -0.694184 -v 0.608762 -0.250000 -0.793353 -v 0.684857 -0.216506 -0.892522 -v 0.740562 -0.125000 -0.965119 -v 0.625000 0.000000 -1.082532 -v 0.608253 0.125000 -1.053525 -v 0.562500 0.216506 -0.974279 -v 0.500000 0.250000 -0.866025 -v 0.437500 0.216506 -0.757772 -v 0.391747 0.125000 -0.678525 -v 0.375000 0.000000 -0.649519 -v 0.391747 -0.125000 -0.678525 -v 0.437500 -0.216506 -0.757772 -v 0.500000 -0.250000 -0.866025 -v 0.562500 -0.216506 -0.974279 -v 0.608253 -0.125000 -1.053525 -v 0.478355 0.000000 -1.154849 -v 0.465537 0.125000 -1.123905 -v 0.430519 0.216506 -1.039364 -v 0.382684 0.250000 -0.923879 -v 0.334848 0.216506 -0.808394 -v 0.299830 0.125000 -0.723854 -v 0.287013 0.000000 -0.692910 -v 0.299830 -0.125000 -0.723854 -v 0.334848 -0.216506 -0.808394 -v 0.382684 -0.250000 -0.923879 -v 0.430519 -0.216506 -1.039364 -v 0.465537 -0.125000 -1.123905 -v 0.323524 0.000000 -1.207407 -v 0.314855 0.125000 -1.175055 -v 0.291171 0.216506 -1.086667 -v 0.258819 0.250000 -0.965926 -v 0.226467 0.216506 -0.845185 -v 0.202783 0.125000 -0.756797 -v 0.194114 0.000000 -0.724444 -v 0.202783 -0.125000 -0.756797 -v 0.226467 -0.216506 -0.845185 -v 0.258819 -0.250000 -0.965926 -v 0.291171 -0.216506 -1.086667 -v 0.314855 -0.125000 -1.175055 -v 0.163158 0.000000 -1.239306 -v 0.158786 0.125000 -1.206099 -v 0.146842 0.216506 -1.115376 -v 0.130526 0.250000 -0.991445 -v 0.114210 0.216506 -0.867514 -v 0.102266 0.125000 -0.776791 -v 0.097895 0.000000 -0.743584 -v 0.102266 -0.125000 -0.776791 -v 0.114210 -0.216506 -0.867514 -v 0.130526 -0.250000 -0.991445 -v 0.146842 -0.216506 -1.115376 -v 0.158786 -0.125000 -1.206099 -v 0.000000 0.000000 -1.250000 -v 0.000000 0.125000 -1.216506 -v 0.000000 0.216506 -1.125000 -v 0.000000 0.250000 -1.000000 -v 0.000000 0.216506 -0.875000 -v 0.000000 0.125000 -0.783494 -v 0.000000 0.000000 -0.750000 -v 0.000000 -0.125000 -0.783494 -v 0.000000 -0.216506 -0.875000 -v 0.000000 -0.250000 -1.000000 -v 0.000000 -0.216506 -1.125000 -v 0.000000 -0.125000 -1.216506 -v -0.163158 0.000000 -1.239306 -v -0.158786 0.125000 -1.206099 -v -0.146842 0.216506 -1.115375 -v -0.130526 0.250000 -0.991445 -v -0.114211 0.216506 -0.867514 -v -0.102267 0.125000 -0.776791 -v -0.097895 0.000000 -0.743584 -v -0.102267 -0.125000 -0.776791 -v -0.114211 -0.216506 -0.867514 -v -0.130526 -0.250000 -0.991445 -v -0.146842 -0.216506 -1.115375 -v -0.158786 -0.125000 -1.206099 -v -0.323524 0.000000 -1.207407 -v -0.314855 0.125000 -1.175055 -v -0.291171 0.216506 -1.086667 -v -0.258819 0.250000 -0.965926 -v -0.226467 0.216506 -0.845185 -v -0.202783 0.125000 -0.756797 -v -0.194114 0.000000 -0.724444 -v -0.202783 -0.125000 -0.756797 -v -0.226467 -0.216506 -0.845185 -v -0.258819 -0.250000 -0.965926 -v -0.291171 -0.216506 -1.086667 -v -0.314855 -0.125000 -1.175055 -v -0.478354 0.000000 -1.154849 -v -0.465537 0.125000 -1.123905 -v -0.430519 0.216506 -1.039364 -v -0.382684 0.250000 -0.923880 -v -0.334848 0.216506 -0.808395 -v -0.299830 0.125000 -0.723854 -v -0.287013 0.000000 -0.692910 -v -0.299830 -0.125000 -0.723854 -v -0.334848 -0.216506 -0.808395 -v -0.382684 -0.250000 -0.923880 -v -0.430519 -0.216506 -1.039364 -v -0.465537 -0.125000 -1.123905 -v -0.625000 0.000000 -1.082532 -v -0.608253 0.125000 -1.053526 -v -0.562500 0.216506 -0.974279 -v -0.500000 0.250000 -0.866026 -v -0.437500 0.216506 -0.757772 -v -0.391747 0.125000 -0.678525 -v -0.375000 0.000000 -0.649519 -v -0.391747 -0.125000 -0.678525 -v -0.437500 -0.216506 -0.757772 -v -0.500000 -0.250000 -0.866026 -v -0.562500 -0.216506 -0.974279 -v -0.608253 -0.125000 -1.053526 -v -0.760952 0.000000 -0.991692 -v -0.740562 0.125000 -0.965119 -v -0.684857 0.216506 -0.892522 -v -0.608761 0.250000 -0.793353 -v -0.532666 0.216506 -0.694184 -v -0.476961 0.125000 -0.621587 -v -0.456571 0.000000 -0.595015 -v -0.476961 -0.125000 -0.621587 -v -0.532666 -0.216506 -0.694184 -v -0.608761 -0.250000 -0.793353 -v -0.684857 -0.216506 -0.892522 -v -0.740562 -0.125000 -0.965119 -v -0.883884 0.000000 -0.883883 -v -0.860200 0.125000 -0.860200 -v -0.795495 0.216506 -0.795495 -v -0.707107 0.250000 -0.707107 -v -0.618719 0.216506 -0.618718 -v -0.554014 0.125000 -0.554013 -v -0.530330 0.000000 -0.530330 -v -0.554014 -0.125000 -0.554013 -v -0.618719 -0.216506 -0.618718 -v -0.707107 -0.250000 -0.707107 -v -0.795495 -0.216506 -0.795495 -v -0.860200 -0.125000 -0.860200 -v -0.991692 0.000000 -0.760952 -v -0.965119 0.125000 -0.740562 -v -0.892522 0.216506 -0.684857 -v -0.793353 0.250000 -0.608761 -v -0.694184 0.216506 -0.532666 -v -0.621587 0.125000 -0.476961 -v -0.595015 0.000000 -0.456571 -v -0.621587 -0.125000 -0.476961 -v -0.694184 -0.216506 -0.532666 -v -0.793353 -0.250000 -0.608761 -v -0.892522 -0.216506 -0.684857 -v -0.965119 -0.125000 -0.740562 -v -1.082532 0.000000 -0.625000 -v -1.053525 0.125000 -0.608253 -v -0.974278 0.216506 -0.562500 -v -0.866025 0.250000 -0.500000 -v -0.757772 0.216506 -0.437500 -v -0.678525 0.125000 -0.391747 -v -0.649519 0.000000 -0.375000 -v -0.678525 -0.125000 -0.391747 -v -0.757772 -0.216506 -0.437500 -v -0.866025 -0.250000 -0.500000 -v -0.974278 -0.216506 -0.562500 -v -1.053525 -0.125000 -0.608253 -v -1.154849 0.000000 -0.478354 -v -1.123905 0.125000 -0.465537 -v -1.039364 0.216506 -0.430519 -v -0.923880 0.250000 -0.382683 -v -0.808395 0.216506 -0.334848 -v -0.723854 0.125000 -0.299830 -v -0.692910 0.000000 -0.287013 -v -0.723854 -0.125000 -0.299830 -v -0.808395 -0.216506 -0.334848 -v -0.923880 -0.250000 -0.382683 -v -1.039364 -0.216506 -0.430519 -v -1.123905 -0.125000 -0.465537 -v -1.207407 0.000000 -0.323524 -v -1.175055 0.125000 -0.314855 -v -1.086667 0.216506 -0.291171 -v -0.965926 0.250000 -0.258819 -v -0.845185 0.216506 -0.226467 -v -0.756797 0.125000 -0.202783 -v -0.724444 0.000000 -0.194114 -v -0.756797 -0.125000 -0.202783 -v -0.845185 -0.216506 -0.226467 -v -0.965926 -0.250000 -0.258819 -v -1.086667 -0.216506 -0.291171 -v -1.175055 -0.125000 -0.314855 -v -1.239306 0.000000 -0.163158 -v -1.206099 0.125000 -0.158786 -v -1.115375 0.216506 -0.146842 -v -0.991445 0.250000 -0.130526 -v -0.867514 0.216506 -0.114211 -v -0.776791 0.125000 -0.102267 -v -0.743584 0.000000 -0.097895 -v -0.776791 -0.125000 -0.102267 -v -0.867514 -0.216506 -0.114211 -v -0.991445 -0.250000 -0.130526 -v -1.115375 -0.216506 -0.146842 -v -1.206099 -0.125000 -0.158786 -v -1.250000 0.000000 -0.000000 -v -1.216506 0.125000 -0.000000 -v -1.125000 0.216506 -0.000000 -v -1.000000 0.250000 -0.000000 -v -0.875000 0.216506 -0.000000 -v -0.783494 0.125000 -0.000000 -v -0.750000 0.000000 -0.000000 -v -0.783494 -0.125000 -0.000000 -v -0.875000 -0.216506 -0.000000 -v -1.000000 -0.250000 -0.000000 -v -1.125000 -0.216506 -0.000000 -v -1.216506 -0.125000 -0.000000 -v -1.239306 0.000000 0.163158 -v -1.206099 0.125000 0.158786 -v -1.115375 0.216506 0.146842 -v -0.991445 0.250000 0.130526 -v -0.867514 0.216506 0.114211 -v -0.776791 0.125000 0.102267 -v -0.743584 0.000000 0.097895 -v -0.776791 -0.125000 0.102267 -v -0.867514 -0.216506 0.114211 -v -0.991445 -0.250000 0.130526 -v -1.115375 -0.216506 0.146842 -v -1.206099 -0.125000 0.158786 -v -1.207407 0.000000 0.323524 -v -1.175055 0.125000 0.314855 -v -1.086667 0.216506 0.291171 -v -0.965926 0.250000 0.258819 -v -0.845185 0.216506 0.226467 -v -0.756797 0.125000 0.202783 -v -0.724444 0.000000 0.194114 -v -0.756797 -0.125000 0.202783 -v -0.845185 -0.216506 0.226467 -v -0.965926 -0.250000 0.258819 -v -1.086667 -0.216506 0.291171 -v -1.175055 -0.125000 0.314855 -v -1.154850 0.000000 0.478354 -v -1.123906 0.125000 0.465536 -v -1.039365 0.216506 0.430518 -v -0.923880 0.250000 0.382683 -v -0.808395 0.216506 0.334848 -v -0.723854 0.125000 0.299830 -v -0.692910 0.000000 0.287012 -v -0.723854 -0.125000 0.299830 -v -0.808395 -0.216506 0.334848 -v -0.923880 -0.250000 0.382683 -v -1.039365 -0.216506 0.430518 -v -1.123906 -0.125000 0.465536 -v -1.082532 0.000000 0.625000 -v -1.053526 0.125000 0.608253 -v -0.974279 0.216506 0.562500 -v -0.866026 0.250000 0.500000 -v -0.757772 0.216506 0.437500 -v -0.678525 0.125000 0.391747 -v -0.649519 0.000000 0.375000 -v -0.678525 -0.125000 0.391747 -v -0.757772 -0.216506 0.437500 -v -0.866026 -0.250000 0.500000 -v -0.974279 -0.216506 0.562500 -v -1.053526 -0.125000 0.608253 -v -0.991692 0.000000 0.760952 -v -0.965119 0.125000 0.740562 -v -0.892522 0.216506 0.684857 -v -0.793353 0.250000 0.608761 -v -0.694184 0.216506 0.532666 -v -0.621587 0.125000 0.476961 -v -0.595015 0.000000 0.456571 -v -0.621587 -0.125000 0.476961 -v -0.694184 -0.216506 0.532666 -v -0.793353 -0.250000 0.608761 -v -0.892522 -0.216506 0.684857 -v -0.965119 -0.125000 0.740562 -v -0.883884 0.000000 0.883883 -v -0.860200 0.125000 0.860200 -v -0.795495 0.216506 0.795495 -v -0.707107 0.250000 0.707107 -v -0.618719 0.216506 0.618718 -v -0.554014 0.125000 0.554013 -v -0.530330 0.000000 0.530330 -v -0.554014 -0.125000 0.554013 -v -0.618719 -0.216506 0.618718 -v -0.707107 -0.250000 0.707107 -v -0.795495 -0.216506 0.795495 -v -0.860200 -0.125000 0.860200 -v -0.760952 0.000000 0.991691 -v -0.740563 0.125000 0.965119 -v -0.684857 0.216506 0.892522 -v -0.608762 0.250000 0.793353 -v -0.532667 0.216506 0.694184 -v -0.476961 0.125000 0.621587 -v -0.456571 0.000000 0.595015 -v -0.476961 -0.125000 0.621587 -v -0.532667 -0.216506 0.694184 -v -0.608762 -0.250000 0.793353 -v -0.684857 -0.216506 0.892522 -v -0.740563 -0.125000 0.965119 -v -0.625000 0.000000 1.082532 -v -0.608253 0.125000 1.053526 -v -0.562500 0.216506 0.974279 -v -0.500000 0.250000 0.866026 -v -0.437500 0.216506 0.757772 -v -0.391747 0.125000 0.678525 -v -0.375000 0.000000 0.649519 -v -0.391747 -0.125000 0.678525 -v -0.437500 -0.216506 0.757772 -v -0.500000 -0.250000 0.866026 -v -0.562500 -0.216506 0.974279 -v -0.608253 -0.125000 1.053526 -v -0.478354 0.000000 1.154849 -v -0.465537 0.125000 1.123905 -v -0.430519 0.216506 1.039364 -v -0.382684 0.250000 0.923880 -v -0.334848 0.216506 0.808395 -v -0.299830 0.125000 0.723854 -v -0.287013 0.000000 0.692910 -v -0.299830 -0.125000 0.723854 -v -0.334848 -0.216506 0.808395 -v -0.382684 -0.250000 0.923880 -v -0.430519 -0.216506 1.039364 -v -0.465537 -0.125000 1.123905 -v -0.323524 0.000000 1.207407 -v -0.314855 0.125000 1.175055 -v -0.291172 0.216506 1.086666 -v -0.258819 0.250000 0.965926 -v -0.226467 0.216506 0.845185 -v -0.202783 0.125000 0.756797 -v -0.194115 0.000000 0.724444 -v -0.202783 -0.125000 0.756797 -v -0.226467 -0.216506 0.845185 -v -0.258819 -0.250000 0.965926 -v -0.291172 -0.216506 1.086666 -v -0.314855 -0.125000 1.175055 -v -0.163158 0.000000 1.239306 -v -0.158787 0.125000 1.206099 -v -0.146843 0.216506 1.115375 -v -0.130527 0.250000 0.991445 -v -0.114211 0.216506 0.867514 -v -0.102267 0.125000 0.776791 -v -0.097895 0.000000 0.743584 -v -0.102267 -0.125000 0.776791 -v -0.114211 -0.216506 0.867514 -v -0.130527 -0.250000 0.991445 -v -0.146843 -0.216506 1.115375 -v -0.158787 -0.125000 1.206099 -v 0.000000 0.000000 1.250000 -v 0.000000 0.125000 1.216506 -v 0.000000 0.216506 1.125000 -v 0.000000 0.250000 1.000000 -v 0.000000 0.216506 0.875000 -v 0.000000 0.125000 0.783494 -v 0.000000 0.000000 0.750000 -v 0.000000 -0.125000 0.783494 -v 0.000000 -0.216506 0.875000 -v 0.000000 -0.250000 1.000000 -v 0.000000 -0.216506 1.125000 -v 0.000000 -0.125000 1.216506 -v 0.163158 0.000000 1.239306 -v 0.158786 0.125000 1.206099 -v 0.146842 0.216506 1.115376 -v 0.130526 0.250000 0.991445 -v 0.114210 0.216506 0.867514 -v 0.102266 0.125000 0.776791 -v 0.097895 0.000000 0.743584 -v 0.102266 -0.125000 0.776791 -v 0.114210 -0.216506 0.867514 -v 0.130526 -0.250000 0.991445 -v 0.146842 -0.216506 1.115376 -v 0.158786 -0.125000 1.206099 -v 0.323523 0.000000 1.207407 -v 0.314854 0.125000 1.175055 -v 0.291171 0.216506 1.086667 -v 0.258819 0.250000 0.965926 -v 0.226466 0.216506 0.845185 -v 0.202783 0.125000 0.756797 -v 0.194114 0.000000 0.724444 -v 0.202783 -0.125000 0.756797 -v 0.226466 -0.216506 0.845185 -v 0.258819 -0.250000 0.965926 -v 0.291171 -0.216506 1.086667 -v 0.314854 -0.125000 1.175055 -v 0.478355 0.000000 1.154849 -v 0.465537 0.125000 1.123905 -v 0.430519 0.216506 1.039364 -v 0.382684 0.250000 0.923879 -v 0.334848 0.216506 0.808394 -v 0.299830 0.125000 0.723854 -v 0.287013 0.000000 0.692910 -v 0.299830 -0.125000 0.723854 -v 0.334848 -0.216506 0.808394 -v 0.382684 -0.250000 0.923879 -v 0.430519 -0.216506 1.039364 -v 0.465537 -0.125000 1.123905 -v 0.625000 0.000000 1.082532 -v 0.608253 0.125000 1.053525 -v 0.562500 0.216506 0.974279 -v 0.500000 0.250000 0.866025 -v 0.437500 0.216506 0.757772 -v 0.391747 0.125000 0.678525 -v 0.375000 0.000000 0.649519 -v 0.391747 -0.125000 0.678525 -v 0.437500 -0.216506 0.757772 -v 0.500000 -0.250000 0.866025 -v 0.562500 -0.216506 0.974279 -v 0.608253 -0.125000 1.053525 -v 0.760952 0.000000 0.991692 -v 0.740562 0.125000 0.965120 -v 0.684856 0.216506 0.892523 -v 0.608761 0.250000 0.793353 -v 0.532666 0.216506 0.694184 -v 0.476961 0.125000 0.621587 -v 0.456571 0.000000 0.595015 -v 0.476961 -0.125000 0.621587 -v 0.532666 -0.216506 0.694184 -v 0.608761 -0.250000 0.793353 -v 0.684856 -0.216506 0.892523 -v 0.740562 -0.125000 0.965120 -v 0.883883 0.000000 0.883884 -v 0.860199 0.125000 0.860200 -v 0.795495 0.216506 0.795496 -v 0.707106 0.250000 0.707107 -v 0.618718 0.216506 0.618719 -v 0.554013 0.125000 0.554014 -v 0.530330 0.000000 0.530330 -v 0.554013 -0.125000 0.554014 -v 0.618718 -0.216506 0.618719 -v 0.707106 -0.250000 0.707107 -v 0.795495 -0.216506 0.795496 -v 0.860199 -0.125000 0.860200 -v 0.991692 0.000000 0.760952 -v 0.965119 0.125000 0.740562 -v 0.892523 0.216506 0.684856 -v 0.793353 0.250000 0.608761 -v 0.694184 0.216506 0.532666 -v 0.621587 0.125000 0.476961 -v 0.595015 0.000000 0.456571 -v 0.621587 -0.125000 0.476961 -v 0.694184 -0.216506 0.532666 -v 0.793353 -0.250000 0.608761 -v 0.892523 -0.216506 0.684856 -v 0.965119 -0.125000 0.740562 -v 1.082532 0.000000 0.625000 -v 1.053525 0.125000 0.608253 -v 0.974279 0.216506 0.562500 -v 0.866025 0.250000 0.500000 -v 0.757772 0.216506 0.437500 -v 0.678525 0.125000 0.391747 -v 0.649519 0.000000 0.375000 -v 0.678525 -0.125000 0.391747 -v 0.757772 -0.216506 0.437500 -v 0.866025 -0.250000 0.500000 -v 0.974279 -0.216506 0.562500 -v 1.053525 -0.125000 0.608253 -v 1.154849 0.000000 0.478355 -v 1.123905 0.125000 0.465537 -v 1.039364 0.216506 0.430519 -v 0.923879 0.250000 0.382684 -v 0.808394 0.216506 0.334848 -v 0.723854 0.125000 0.299830 -v 0.692910 0.000000 0.287013 -v 0.723854 -0.125000 0.299830 -v 0.808394 -0.216506 0.334848 -v 0.923879 -0.250000 0.382684 -v 1.039364 -0.216506 0.430519 -v 1.123905 -0.125000 0.465537 -v 1.207407 0.000000 0.323523 -v 1.175055 0.125000 0.314855 -v 1.086667 0.216506 0.291171 -v 0.965926 0.250000 0.258819 -v 0.845185 0.216506 0.226466 -v 0.756797 0.125000 0.202783 -v 0.724444 0.000000 0.194114 -v 0.756797 -0.125000 0.202783 -v 0.845185 -0.216506 0.226466 -v 0.965926 -0.250000 0.258819 -v 1.086667 -0.216506 0.291171 -v 1.175055 -0.125000 0.314855 -v 1.239306 0.000000 0.163158 -v 1.206099 0.125000 0.158786 -v 1.115376 0.216506 0.146842 -v 0.991445 0.250000 0.130526 -v 0.867514 0.216506 0.114210 -v 0.776791 0.125000 0.102266 -v 0.743584 0.000000 0.097895 -v 0.776791 -0.125000 0.102266 -v 0.867514 -0.216506 0.114210 -v 0.991445 -0.250000 0.130526 -v 1.115376 -0.216506 0.146842 -v 1.206099 -0.125000 0.158786 -vt 0.500000 0.500000 -vt 0.520833 0.500000 -vt 0.520833 0.583333 -vt 0.500000 0.583333 -vt 0.520833 0.666667 -vt 0.500000 0.666667 -vt 0.520833 0.750000 -vt 0.500000 0.750000 -vt 0.520833 0.833333 -vt 0.500000 0.833333 -vt 0.520833 0.916667 -vt 0.500000 0.916667 -vt 0.520833 1.000000 -vt 0.500000 1.000000 -vt 0.500000 0.000000 -vt 0.520833 0.000000 -vt 0.520833 0.083333 -vt 0.500000 0.083333 -vt 0.520833 0.166667 -vt 0.500000 0.166667 -vt 0.520833 0.250000 -vt 0.500000 0.250000 -vt 0.520833 0.333333 -vt 0.500000 0.333333 -vt 0.520833 0.416667 -vt 0.500000 0.416667 -vt 0.541667 0.500000 -vt 0.541667 0.583333 -vt 0.541667 0.666667 -vt 0.541667 0.750000 -vt 0.541667 0.833333 -vt 0.541667 0.916667 -vt 0.541667 1.000000 -vt 0.541667 0.000000 -vt 0.541667 0.083333 -vt 0.541667 0.166667 -vt 0.541667 0.250000 -vt 0.541667 0.333333 -vt 0.541667 0.416667 -vt 0.562500 0.500000 -vt 0.562500 0.583333 -vt 0.562500 0.666667 -vt 0.562500 0.750000 -vt 0.562500 0.833333 -vt 0.562500 0.916667 -vt 0.562500 1.000000 -vt 0.562500 0.000000 -vt 0.562500 0.083333 -vt 0.562500 0.166667 -vt 0.562500 0.250000 -vt 0.562500 0.333333 -vt 0.562500 0.416667 -vt 0.583333 0.500000 -vt 0.583333 0.583333 -vt 0.583333 0.666667 -vt 0.583333 0.750000 -vt 0.583333 0.833333 -vt 0.583333 0.916667 -vt 0.583333 1.000000 -vt 0.583333 0.000000 -vt 0.583333 0.083333 -vt 0.583333 0.166667 -vt 0.583333 0.250000 -vt 0.583333 0.333333 -vt 0.583333 0.416667 -vt 0.604167 0.500000 -vt 0.604167 0.583333 -vt 0.604167 0.666667 -vt 0.604167 0.750000 -vt 0.604167 0.833333 -vt 0.604167 0.916667 -vt 0.604167 1.000000 -vt 0.604167 0.000000 -vt 0.604167 0.083333 -vt 0.604167 0.166667 -vt 0.604167 0.250000 -vt 0.604167 0.333333 -vt 0.604167 0.416667 -vt 0.625000 0.500000 -vt 0.625000 0.583333 -vt 0.625000 0.666667 -vt 0.625000 0.750000 -vt 0.625000 0.833333 -vt 0.625000 0.916667 -vt 0.625000 1.000000 -vt 0.625000 0.000000 -vt 0.625000 0.083333 -vt 0.625000 0.166667 -vt 0.625000 0.250000 -vt 0.625000 0.333333 -vt 0.625000 0.416667 -vt 0.645833 0.500000 -vt 0.645833 0.583333 -vt 0.645833 0.666667 -vt 0.645833 0.750000 -vt 0.645833 0.833333 -vt 0.645833 0.916667 -vt 0.645833 1.000000 -vt 0.645833 0.000000 -vt 0.645833 0.083333 -vt 0.645833 0.166667 -vt 0.645833 0.250000 -vt 0.645833 0.333333 -vt 0.645833 0.416667 -vt 0.666667 0.500000 -vt 0.666667 0.583333 -vt 0.666667 0.666667 -vt 0.666667 0.750000 -vt 0.666667 0.833333 -vt 0.666667 0.916667 -vt 0.666667 1.000000 -vt 0.666667 0.000000 -vt 0.666667 0.083333 -vt 0.666667 0.166667 -vt 0.666667 0.250000 -vt 0.666667 0.333333 -vt 0.666667 0.416667 -vt 0.687500 0.500000 -vt 0.687500 0.583333 -vt 0.687500 0.666667 -vt 0.687500 0.750000 -vt 0.687500 0.833333 -vt 0.687500 0.916667 -vt 0.687500 1.000000 -vt 0.687500 0.000000 -vt 0.687500 0.083333 -vt 0.687500 0.166667 -vt 0.687500 0.250000 -vt 0.687500 0.333333 -vt 0.687500 0.416667 -vt 0.708333 0.500000 -vt 0.708333 0.583333 -vt 0.708333 0.666667 -vt 0.708333 0.750000 -vt 0.708333 0.833333 -vt 0.708333 0.916667 -vt 0.708333 1.000000 -vt 0.708333 0.000000 -vt 0.708333 0.083333 -vt 0.708333 0.166667 -vt 0.708333 0.250000 -vt 0.708333 0.333333 -vt 0.708333 0.416667 -vt 0.729167 0.500000 -vt 0.729167 0.583333 -vt 0.729167 0.666667 -vt 0.729167 0.750000 -vt 0.729167 0.833333 -vt 0.729167 0.916667 -vt 0.729167 1.000000 -vt 0.729167 0.000000 -vt 0.729167 0.083333 -vt 0.729167 0.166667 -vt 0.729167 0.250000 -vt 0.729167 0.333333 -vt 0.729167 0.416667 -vt 0.750000 0.500000 -vt 0.750000 0.583333 -vt 0.750000 0.666667 -vt 0.750000 0.750000 -vt 0.750000 0.833333 -vt 0.750000 0.916667 -vt 0.750000 1.000000 -vt 0.750000 0.000000 -vt 0.750000 0.083333 -vt 0.750000 0.166667 -vt 0.750000 0.250000 -vt 0.750000 0.333333 -vt 0.750000 0.416667 -vt 0.770833 0.500000 -vt 0.770833 0.583333 -vt 0.770833 0.666667 -vt 0.770833 0.750000 -vt 0.770833 0.833333 -vt 0.770833 0.916667 -vt 0.770833 1.000000 -vt 0.770833 0.000000 -vt 0.770833 0.083333 -vt 0.770833 0.166667 -vt 0.770833 0.250000 -vt 0.770833 0.333333 -vt 0.770833 0.416667 -vt 0.791667 0.500000 -vt 0.791667 0.583333 -vt 0.791667 0.666667 -vt 0.791667 0.750000 -vt 0.791667 0.833333 -vt 0.791667 0.916667 -vt 0.791667 1.000000 -vt 0.791667 0.000000 -vt 0.791667 0.083333 -vt 0.791667 0.166667 -vt 0.791667 0.250000 -vt 0.791667 0.333333 -vt 0.791667 0.416667 -vt 0.812500 0.500000 -vt 0.812500 0.583333 -vt 0.812500 0.666667 -vt 0.812500 0.750000 -vt 0.812500 0.833333 -vt 0.812500 0.916667 -vt 0.812500 1.000000 -vt 0.812500 0.000000 -vt 0.812500 0.083333 -vt 0.812500 0.166667 -vt 0.812500 0.250000 -vt 0.812500 0.333333 -vt 0.812500 0.416667 -vt 0.833333 0.500000 -vt 0.833333 0.583333 -vt 0.833333 0.666667 -vt 0.833333 0.750000 -vt 0.833333 0.833333 -vt 0.833333 0.916667 -vt 0.833333 1.000000 -vt 0.833333 0.000000 -vt 0.833333 0.083333 -vt 0.833333 0.166667 -vt 0.833333 0.250000 -vt 0.833333 0.333333 -vt 0.833333 0.416667 -vt 0.854167 0.500000 -vt 0.854167 0.583333 -vt 0.854167 0.666667 -vt 0.854167 0.750000 -vt 0.854167 0.833333 -vt 0.854167 0.916667 -vt 0.854167 1.000000 -vt 0.854167 0.000000 -vt 0.854167 0.083333 -vt 0.854167 0.166667 -vt 0.854167 0.250000 -vt 0.854167 0.333333 -vt 0.854167 0.416667 -vt 0.875000 0.500000 -vt 0.875000 0.583333 -vt 0.875000 0.666667 -vt 0.875000 0.750000 -vt 0.875000 0.833333 -vt 0.875000 0.916667 -vt 0.875000 1.000000 -vt 0.875000 0.000000 -vt 0.875000 0.083333 -vt 0.875000 0.166667 -vt 0.875000 0.250000 -vt 0.875000 0.333333 -vt 0.875000 0.416667 -vt 0.895833 0.500000 -vt 0.895833 0.583333 -vt 0.895833 0.666667 -vt 0.895833 0.750000 -vt 0.895833 0.833333 -vt 0.895833 0.916667 -vt 0.895833 1.000000 -vt 0.895833 0.000000 -vt 0.895833 0.083333 -vt 0.895833 0.166667 -vt 0.895833 0.250000 -vt 0.895833 0.333333 -vt 0.895833 0.416667 -vt 0.916667 0.500000 -vt 0.916667 0.583333 -vt 0.916667 0.666667 -vt 0.916667 0.750000 -vt 0.916667 0.833333 -vt 0.916667 0.916667 -vt 0.916667 1.000000 -vt 0.916667 0.000000 -vt 0.916667 0.083333 -vt 0.916667 0.166667 -vt 0.916667 0.250000 -vt 0.916667 0.333333 -vt 0.916667 0.416667 -vt 0.937500 0.500000 -vt 0.937500 0.583333 -vt 0.937500 0.666667 -vt 0.937500 0.750000 -vt 0.937500 0.833333 -vt 0.937500 0.916667 -vt 0.937500 1.000000 -vt 0.937500 0.000000 -vt 0.937500 0.083333 -vt 0.937500 0.166667 -vt 0.937500 0.250000 -vt 0.937500 0.333333 -vt 0.937500 0.416667 -vt 0.958333 0.500000 -vt 0.958333 0.583333 -vt 0.958333 0.666667 -vt 0.958333 0.750000 -vt 0.958333 0.833333 -vt 0.958333 0.916667 -vt 0.958333 1.000000 -vt 0.958333 0.000000 -vt 0.958333 0.083333 -vt 0.958333 0.166667 -vt 0.958333 0.250000 -vt 0.958333 0.333333 -vt 0.958333 0.416667 -vt 0.979167 0.500000 -vt 0.979167 0.583333 -vt 0.979167 0.666667 -vt 0.979167 0.750000 -vt 0.979167 0.833333 -vt 0.979167 0.916667 -vt 0.979167 1.000000 -vt 0.979167 0.000000 -vt 0.979167 0.083333 -vt 0.979167 0.166667 -vt 0.979167 0.250000 -vt 0.979167 0.333333 -vt 0.979167 0.416667 -vt 1.000000 0.500000 -vt 1.000000 0.583333 -vt 1.000000 0.666667 -vt 1.000000 0.750000 -vt 1.000000 0.833333 -vt 1.000000 0.916667 -vt 1.000000 1.000000 -vt 1.000000 0.000000 -vt 1.000000 0.083333 -vt 1.000000 0.166667 -vt 1.000000 0.250000 -vt 1.000000 0.333333 -vt 1.000000 0.416667 -vt 0.000000 0.500000 -vt 0.020833 0.500000 -vt 0.020833 0.583333 -vt 0.000000 0.583333 -vt 0.020833 0.666667 -vt 0.000000 0.666667 -vt 0.020833 0.750000 -vt 0.000000 0.750000 -vt 0.020833 0.833333 -vt 0.000000 0.833333 -vt 0.020833 0.916667 -vt 0.000000 0.916667 -vt 0.020833 1.000000 -vt 0.000000 1.000000 -vt 0.000000 0.000000 -vt 0.020833 0.000000 -vt 0.020833 0.083333 -vt 0.000000 0.083333 -vt 0.020833 0.166667 -vt 0.000000 0.166667 -vt 0.020833 0.250000 -vt 0.000000 0.250000 -vt 0.020833 0.333333 -vt 0.000000 0.333333 -vt 0.020833 0.416667 -vt 0.000000 0.416667 -vt 0.041667 0.500000 -vt 0.041667 0.583333 -vt 0.041667 0.666667 -vt 0.041667 0.750000 -vt 0.041667 0.833333 -vt 0.041667 0.916667 -vt 0.041667 1.000000 -vt 0.041667 0.000000 -vt 0.041667 0.083333 -vt 0.041667 0.166667 -vt 0.041667 0.250000 -vt 0.041667 0.333333 -vt 0.041667 0.416667 -vt 0.062500 0.500000 -vt 0.062500 0.583333 -vt 0.062500 0.666667 -vt 0.062500 0.750000 -vt 0.062500 0.833333 -vt 0.062500 0.916667 -vt 0.062500 1.000000 -vt 0.062500 0.000000 -vt 0.062500 0.083333 -vt 0.062500 0.166667 -vt 0.062500 0.250000 -vt 0.062500 0.333333 -vt 0.062500 0.416667 -vt 0.083333 0.500000 -vt 0.083333 0.583333 -vt 0.083333 0.666667 -vt 0.083333 0.750000 -vt 0.083333 0.833333 -vt 0.083333 0.916667 -vt 0.083333 1.000000 -vt 0.083333 0.000000 -vt 0.083333 0.083333 -vt 0.083333 0.166667 -vt 0.083333 0.250000 -vt 0.083333 0.333333 -vt 0.083333 0.416667 -vt 0.104167 0.500000 -vt 0.104167 0.583333 -vt 0.104167 0.666667 -vt 0.104167 0.750000 -vt 0.104167 0.833333 -vt 0.104167 0.916667 -vt 0.104167 1.000000 -vt 0.104167 0.000000 -vt 0.104167 0.083333 -vt 0.104167 0.166667 -vt 0.104167 0.250000 -vt 0.104167 0.333333 -vt 0.104167 0.416667 -vt 0.125000 0.500000 -vt 0.125000 0.583333 -vt 0.125000 0.666667 -vt 0.125000 0.750000 -vt 0.125000 0.833333 -vt 0.125000 0.916667 -vt 0.125000 1.000000 -vt 0.125000 0.000000 -vt 0.125000 0.083333 -vt 0.125000 0.166667 -vt 0.125000 0.250000 -vt 0.125000 0.333333 -vt 0.125000 0.416667 -vt 0.145833 0.500000 -vt 0.145833 0.583333 -vt 0.145833 0.666667 -vt 0.145833 0.750000 -vt 0.145833 0.833333 -vt 0.145833 0.916667 -vt 0.145833 1.000000 -vt 0.145833 0.000000 -vt 0.145833 0.083333 -vt 0.145833 0.166667 -vt 0.145833 0.250000 -vt 0.145833 0.333333 -vt 0.145833 0.416667 -vt 0.166667 0.500000 -vt 0.166667 0.583333 -vt 0.166667 0.666667 -vt 0.166667 0.750000 -vt 0.166667 0.833333 -vt 0.166667 0.916667 -vt 0.166667 1.000000 -vt 0.166667 0.000000 -vt 0.166667 0.083333 -vt 0.166667 0.166667 -vt 0.166667 0.250000 -vt 0.166667 0.333333 -vt 0.166667 0.416667 -vt 0.187500 0.500000 -vt 0.187500 0.583333 -vt 0.187500 0.666667 -vt 0.187500 0.750000 -vt 0.187500 0.833333 -vt 0.187500 0.916667 -vt 0.187500 1.000000 -vt 0.187500 0.000000 -vt 0.187500 0.083333 -vt 0.187500 0.166667 -vt 0.187500 0.250000 -vt 0.187500 0.333333 -vt 0.187500 0.416667 -vt 0.208333 0.500000 -vt 0.208333 0.583333 -vt 0.208333 0.666667 -vt 0.208333 0.750000 -vt 0.208333 0.833333 -vt 0.208333 0.916667 -vt 0.208333 1.000000 -vt 0.208333 0.000000 -vt 0.208333 0.083333 -vt 0.208333 0.166667 -vt 0.208333 0.250000 -vt 0.208333 0.333333 -vt 0.208333 0.416667 -vt 0.229167 0.500000 -vt 0.229167 0.583333 -vt 0.229167 0.666667 -vt 0.229167 0.750000 -vt 0.229167 0.833333 -vt 0.229167 0.916667 -vt 0.229167 1.000000 -vt 0.229167 0.000000 -vt 0.229167 0.083333 -vt 0.229167 0.166667 -vt 0.229167 0.250000 -vt 0.229167 0.333333 -vt 0.229167 0.416667 -vt 0.250000 0.500000 -vt 0.250000 0.583333 -vt 0.250000 0.666667 -vt 0.250000 0.750000 -vt 0.250000 0.833333 -vt 0.250000 0.916667 -vt 0.250000 1.000000 -vt 0.250000 0.000000 -vt 0.250000 0.083333 -vt 0.250000 0.166667 -vt 0.250000 0.250000 -vt 0.250000 0.333333 -vt 0.250000 0.416667 -vt 0.270833 0.500000 -vt 0.270833 0.583333 -vt 0.270833 0.666667 -vt 0.270833 0.750000 -vt 0.270833 0.833333 -vt 0.270833 0.916667 -vt 0.270833 1.000000 -vt 0.270833 0.000000 -vt 0.270833 0.083333 -vt 0.270833 0.166667 -vt 0.270833 0.250000 -vt 0.270833 0.333333 -vt 0.270833 0.416667 -vt 0.291667 0.500000 -vt 0.291667 0.583333 -vt 0.291667 0.666667 -vt 0.291667 0.750000 -vt 0.291667 0.833333 -vt 0.291667 0.916667 -vt 0.291667 1.000000 -vt 0.291667 0.000000 -vt 0.291667 0.083333 -vt 0.291667 0.166667 -vt 0.291667 0.250000 -vt 0.291667 0.333333 -vt 0.291667 0.416667 -vt 0.312500 0.500000 -vt 0.312500 0.583333 -vt 0.312500 0.666667 -vt 0.312500 0.750000 -vt 0.312500 0.833333 -vt 0.312500 0.916667 -vt 0.312500 1.000000 -vt 0.312500 0.000000 -vt 0.312500 0.083333 -vt 0.312500 0.166667 -vt 0.312500 0.250000 -vt 0.312500 0.333333 -vt 0.312500 0.416667 -vt 0.333333 0.500000 -vt 0.333333 0.583333 -vt 0.333333 0.666667 -vt 0.333333 0.750000 -vt 0.333333 0.833333 -vt 0.333333 0.916667 -vt 0.333333 1.000000 -vt 0.333333 0.000000 -vt 0.333333 0.083333 -vt 0.333333 0.166667 -vt 0.333333 0.250000 -vt 0.333333 0.333333 -vt 0.333333 0.416667 -vt 0.354167 0.500000 -vt 0.354167 0.583333 -vt 0.354167 0.666667 -vt 0.354167 0.750000 -vt 0.354167 0.833333 -vt 0.354167 0.916667 -vt 0.354167 1.000000 -vt 0.354167 0.000000 -vt 0.354167 0.083333 -vt 0.354167 0.166667 -vt 0.354167 0.250000 -vt 0.354167 0.333333 -vt 0.354167 0.416667 -vt 0.375000 0.500000 -vt 0.375000 0.583333 -vt 0.375000 0.666667 -vt 0.375000 0.750000 -vt 0.375000 0.833333 -vt 0.375000 0.916667 -vt 0.375000 1.000000 -vt 0.375000 0.000000 -vt 0.375000 0.083333 -vt 0.375000 0.166667 -vt 0.375000 0.250000 -vt 0.375000 0.333333 -vt 0.375000 0.416667 -vt 0.395833 0.500000 -vt 0.395833 0.583333 -vt 0.395833 0.666667 -vt 0.395833 0.750000 -vt 0.395833 0.833333 -vt 0.395833 0.916667 -vt 0.395833 1.000000 -vt 0.395833 0.000000 -vt 0.395833 0.083333 -vt 0.395833 0.166667 -vt 0.395833 0.250000 -vt 0.395833 0.333333 -vt 0.395833 0.416667 -vt 0.416667 0.500000 -vt 0.416667 0.583333 -vt 0.416667 0.666667 -vt 0.416667 0.750000 -vt 0.416667 0.833333 -vt 0.416667 0.916667 -vt 0.416667 1.000000 -vt 0.416667 0.000000 -vt 0.416667 0.083333 -vt 0.416667 0.166667 -vt 0.416667 0.250000 -vt 0.416667 0.333333 -vt 0.416667 0.416667 -vt 0.437500 0.500000 -vt 0.437500 0.583333 -vt 0.437500 0.666667 -vt 0.437500 0.750000 -vt 0.437500 0.833333 -vt 0.437500 0.916667 -vt 0.437500 1.000000 -vt 0.437500 0.000000 -vt 0.437500 0.083333 -vt 0.437500 0.166667 -vt 0.437500 0.250000 -vt 0.437500 0.333333 -vt 0.437500 0.416667 -vt 0.458333 0.500000 -vt 0.458333 0.583333 -vt 0.458333 0.666667 -vt 0.458333 0.750000 -vt 0.458333 0.833333 -vt 0.458333 0.916667 -vt 0.458333 1.000000 -vt 0.458333 0.000000 -vt 0.458333 0.083333 -vt 0.458333 0.166667 -vt 0.458333 0.250000 -vt 0.458333 0.333333 -vt 0.458333 0.416667 -vt 0.479167 0.500000 -vt 0.479167 0.583333 -vt 0.479167 0.666667 -vt 0.479167 0.750000 -vt 0.479167 0.833333 -vt 0.479167 0.916667 -vt 0.479167 1.000000 -vt 0.479167 0.000000 -vt 0.479167 0.083333 -vt 0.479167 0.166667 -vt 0.479167 0.250000 -vt 0.479167 0.333333 -vt 0.479167 0.416667 -vn 0.9640 0.2583 -0.0632 -vn 0.7063 0.7063 -0.0463 -vn 0.2588 0.9658 -0.0170 -vn -0.2588 0.9658 0.0170 -vn -0.7063 0.7063 0.0463 -vn -0.9640 0.2583 0.0632 -vn -0.9640 -0.2583 0.0632 -vn -0.7063 -0.7063 0.0463 -vn -0.2588 -0.9658 0.0170 -vn 0.2588 -0.9658 -0.0170 -vn 0.7063 -0.7063 -0.0463 -vn 0.9640 -0.2583 -0.0632 -vn 0.9475 0.2583 -0.1885 -vn 0.6943 0.7063 -0.1381 -vn 0.2544 0.9658 -0.0506 -vn -0.2544 0.9658 0.0506 -vn -0.6943 0.7063 0.1381 -vn -0.9475 0.2583 0.1885 -vn -0.9475 -0.2583 0.1885 -vn -0.6943 -0.7063 0.1381 -vn -0.2544 -0.9658 0.0506 -vn 0.2544 -0.9658 -0.0506 -vn 0.6943 -0.7063 -0.1381 -vn 0.9475 -0.2583 -0.1885 -vn 0.9148 0.2583 -0.3105 -vn 0.6703 0.7063 -0.2275 -vn 0.2456 0.9658 -0.0834 -vn -0.2456 0.9658 0.0834 -vn -0.6703 0.7063 0.2275 -vn -0.9148 0.2583 0.3105 -vn -0.9148 -0.2583 0.3105 -vn -0.6703 -0.7063 0.2275 -vn -0.2456 -0.9658 0.0834 -vn 0.2456 -0.9658 -0.0834 -vn 0.6703 -0.7063 -0.2275 -vn 0.9148 -0.2583 -0.3105 -vn 0.8664 0.2583 -0.4273 -vn 0.6349 0.7063 -0.3131 -vn 0.2326 0.9658 -0.1147 -vn -0.2326 0.9658 0.1147 -vn -0.6349 0.7063 0.3131 -vn -0.8664 0.2583 0.4273 -vn -0.8664 -0.2583 0.4273 -vn -0.6349 -0.7063 0.3131 -vn -0.2326 -0.9658 0.1147 -vn 0.2326 -0.9658 -0.1147 -vn 0.6349 -0.7063 -0.3131 -vn 0.8664 -0.2583 -0.4273 -vn 0.8033 0.2583 -0.5367 -vn 0.5886 0.7063 -0.3933 -vn 0.2156 0.9658 -0.1441 -vn -0.2156 0.9658 0.1441 -vn -0.5886 0.7063 0.3933 -vn -0.8033 0.2583 0.5367 -vn -0.8033 -0.2583 0.5367 -vn -0.5886 -0.7063 0.3933 -vn -0.2156 -0.9658 0.1441 -vn 0.2156 -0.9658 -0.1441 -vn 0.5886 -0.7063 -0.3933 -vn 0.8033 -0.2583 -0.5367 -vn 0.7263 0.2583 -0.6370 -vn 0.5322 0.7063 -0.4667 -vn 0.1950 0.9658 -0.1710 -vn -0.1950 0.9658 0.1710 -vn -0.5322 0.7063 0.4667 -vn -0.7263 0.2583 0.6370 -vn -0.7263 -0.2583 0.6370 -vn -0.5322 -0.7063 0.4667 -vn -0.1950 -0.9658 0.1710 -vn 0.1950 -0.9658 -0.1710 -vn 0.5322 -0.7063 -0.4667 -vn 0.7263 -0.2583 -0.6370 -vn 0.6370 0.2583 -0.7263 -vn 0.4667 0.7063 -0.5322 -vn 0.1710 0.9658 -0.1950 -vn -0.1710 0.9658 0.1950 -vn -0.4667 0.7063 0.5322 -vn -0.6370 0.2583 0.7263 -vn -0.6370 -0.2583 0.7263 -vn -0.4667 -0.7063 0.5322 -vn -0.1710 -0.9658 0.1950 -vn 0.1710 -0.9658 -0.1950 -vn 0.4667 -0.7063 -0.5322 -vn 0.6370 -0.2583 -0.7263 -vn 0.5367 0.2583 -0.8033 -vn 0.3933 0.7063 -0.5886 -vn 0.1441 0.9658 -0.2156 -vn -0.1441 0.9658 0.2156 -vn -0.3933 0.7063 0.5886 -vn -0.5367 0.2583 0.8033 -vn -0.5367 -0.2583 0.8033 -vn -0.3933 -0.7063 0.5886 -vn -0.1441 -0.9658 0.2156 -vn 0.1441 -0.9658 -0.2156 -vn 0.3933 -0.7063 -0.5886 -vn 0.5367 -0.2583 -0.8033 -vn 0.4273 0.2583 -0.8664 -vn 0.3131 0.7063 -0.6349 -vn 0.1147 0.9658 -0.2326 -vn -0.1147 0.9658 0.2326 -vn -0.3131 0.7063 0.6349 -vn -0.4273 0.2583 0.8664 -vn -0.4273 -0.2583 0.8664 -vn -0.3131 -0.7063 0.6349 -vn -0.1147 -0.9658 0.2326 -vn 0.1147 -0.9658 -0.2326 -vn 0.3131 -0.7063 -0.6349 -vn 0.4273 -0.2583 -0.8664 -vn 0.3105 0.2583 -0.9148 -vn 0.2275 0.7063 -0.6703 -vn 0.0834 0.9658 -0.2456 -vn -0.0834 0.9658 0.2456 -vn -0.2275 0.7063 0.6703 -vn -0.3105 0.2583 0.9148 -vn -0.3105 -0.2583 0.9148 -vn -0.2275 -0.7063 0.6703 -vn -0.0834 -0.9658 0.2456 -vn 0.0834 -0.9658 -0.2456 -vn 0.2275 -0.7063 -0.6703 -vn 0.3105 -0.2583 -0.9148 -vn 0.1885 0.2583 -0.9475 -vn 0.1381 0.7063 -0.6943 -vn 0.0506 0.9658 -0.2544 -vn -0.0506 0.9658 0.2544 -vn -0.1381 0.7063 0.6943 -vn -0.1885 0.2583 0.9475 -vn -0.1885 -0.2583 0.9475 -vn -0.1381 -0.7063 0.6943 -vn -0.0506 -0.9658 0.2544 -vn 0.0506 -0.9658 -0.2544 -vn 0.1381 -0.7063 -0.6943 -vn 0.1885 -0.2583 -0.9475 -vn 0.0632 0.2583 -0.9640 -vn 0.0463 0.7063 -0.7063 -vn 0.0170 0.9658 -0.2588 -vn -0.0170 0.9658 0.2588 -vn -0.0463 0.7063 0.7063 -vn -0.0632 0.2583 0.9640 -vn -0.0632 -0.2583 0.9640 -vn -0.0463 -0.7063 0.7063 -vn -0.0170 -0.9658 0.2588 -vn 0.0170 -0.9658 -0.2588 -vn 0.0463 -0.7063 -0.7063 -vn 0.0632 -0.2583 -0.9640 -vn -0.0632 0.2583 -0.9640 -vn -0.0463 0.7063 -0.7063 -vn -0.0170 0.9658 -0.2588 -vn 0.0170 0.9658 0.2588 -vn 0.0463 0.7063 0.7063 -vn 0.0632 0.2583 0.9640 -vn 0.0632 -0.2583 0.9640 -vn 0.0463 -0.7063 0.7063 -vn 0.0170 -0.9658 0.2588 -vn -0.0170 -0.9658 -0.2588 -vn -0.0463 -0.7063 -0.7063 -vn -0.0632 -0.2583 -0.9640 -vn -0.1885 0.2583 -0.9475 -vn -0.1381 0.7063 -0.6943 -vn -0.0506 0.9658 -0.2544 -vn 0.0506 0.9658 0.2544 -vn 0.1381 0.7063 0.6943 -vn 0.1885 0.2583 0.9475 -vn 0.1885 -0.2583 0.9475 -vn 0.1381 -0.7063 0.6943 -vn 0.0506 -0.9658 0.2544 -vn -0.0506 -0.9658 -0.2544 -vn -0.1381 -0.7063 -0.6943 -vn -0.1885 -0.2583 -0.9475 -vn -0.3105 0.2583 -0.9148 -vn -0.2275 0.7063 -0.6703 -vn -0.0834 0.9658 -0.2456 -vn 0.0834 0.9658 0.2456 -vn 0.2275 0.7063 0.6703 -vn 0.3105 0.2583 0.9148 -vn 0.3105 -0.2583 0.9148 -vn 0.2275 -0.7063 0.6703 -vn 0.0834 -0.9658 0.2456 -vn -0.0834 -0.9658 -0.2456 -vn -0.2275 -0.7063 -0.6703 -vn -0.3105 -0.2583 -0.9148 -vn -0.4273 0.2583 -0.8664 -vn -0.3131 0.7063 -0.6349 -vn -0.1147 0.9658 -0.2326 -vn 0.1147 0.9658 0.2326 -vn 0.3131 0.7063 0.6349 -vn 0.4273 0.2583 0.8664 -vn 0.4273 -0.2583 0.8664 -vn 0.3131 -0.7063 0.6349 -vn 0.1147 -0.9658 0.2326 -vn -0.1147 -0.9658 -0.2326 -vn -0.3131 -0.7063 -0.6349 -vn -0.4273 -0.2583 -0.8664 -vn -0.5367 0.2583 -0.8033 -vn -0.3933 0.7063 -0.5886 -vn -0.1441 0.9658 -0.2156 -vn 0.1441 0.9658 0.2156 -vn 0.3933 0.7063 0.5886 -vn 0.5367 0.2583 0.8033 -vn 0.5367 -0.2583 0.8033 -vn 0.3933 -0.7063 0.5886 -vn 0.1441 -0.9658 0.2156 -vn -0.1441 -0.9658 -0.2156 -vn -0.3933 -0.7063 -0.5886 -vn -0.5367 -0.2583 -0.8033 -vn -0.6370 0.2583 -0.7263 -vn -0.4667 0.7063 -0.5322 -vn -0.1710 0.9658 -0.1950 -vn 0.1710 0.9658 0.1950 -vn 0.4667 0.7063 0.5322 -vn 0.6370 0.2583 0.7263 -vn 0.6370 -0.2583 0.7263 -vn 0.4667 -0.7063 0.5322 -vn 0.1710 -0.9658 0.1950 -vn -0.1710 -0.9658 -0.1950 -vn -0.4667 -0.7063 -0.5322 -vn -0.6370 -0.2583 -0.7263 -vn -0.7263 0.2583 -0.6370 -vn -0.5322 0.7063 -0.4667 -vn -0.1950 0.9658 -0.1710 -vn 0.1950 0.9658 0.1710 -vn 0.5322 0.7063 0.4667 -vn 0.7263 0.2583 0.6370 -vn 0.7263 -0.2583 0.6370 -vn 0.5322 -0.7063 0.4667 -vn 0.1950 -0.9658 0.1710 -vn -0.1950 -0.9658 -0.1710 -vn -0.5322 -0.7063 -0.4667 -vn -0.7263 -0.2583 -0.6370 -vn -0.8033 0.2583 -0.5367 -vn -0.5886 0.7063 -0.3933 -vn -0.2156 0.9658 -0.1441 -vn 0.2156 0.9658 0.1441 -vn 0.5886 0.7063 0.3933 -vn 0.8033 0.2583 0.5367 -vn 0.8033 -0.2583 0.5367 -vn 0.5886 -0.7063 0.3933 -vn 0.2156 -0.9658 0.1441 -vn -0.2156 -0.9658 -0.1441 -vn -0.5886 -0.7063 -0.3933 -vn -0.8033 -0.2583 -0.5367 -vn -0.8664 0.2583 -0.4273 -vn -0.6349 0.7063 -0.3131 -vn -0.2326 0.9658 -0.1147 -vn 0.2326 0.9658 0.1147 -vn 0.6349 0.7063 0.3131 -vn 0.8664 0.2583 0.4273 -vn 0.8664 -0.2583 0.4273 -vn 0.6349 -0.7063 0.3131 -vn 0.2326 -0.9658 0.1147 -vn -0.2326 -0.9658 -0.1147 -vn -0.6349 -0.7063 -0.3131 -vn -0.8664 -0.2583 -0.4273 -vn -0.9148 0.2583 -0.3105 -vn -0.6703 0.7063 -0.2275 -vn -0.2456 0.9658 -0.0834 -vn 0.2456 0.9658 0.0834 -vn 0.6703 0.7063 0.2275 -vn 0.9148 0.2583 0.3105 -vn 0.9148 -0.2583 0.3105 -vn 0.6703 -0.7063 0.2275 -vn 0.2456 -0.9658 0.0834 -vn -0.2456 -0.9658 -0.0834 -vn -0.6703 -0.7063 -0.2275 -vn -0.9148 -0.2583 -0.3105 -vn -0.9475 0.2583 -0.1885 -vn -0.6943 0.7063 -0.1381 -vn -0.2544 0.9658 -0.0506 -vn 0.2544 0.9658 0.0506 -vn 0.6943 0.7063 0.1381 -vn 0.9475 0.2583 0.1885 -vn 0.9475 -0.2583 0.1885 -vn 0.6943 -0.7063 0.1381 -vn 0.2544 -0.9658 0.0506 -vn -0.2544 -0.9658 -0.0506 -vn -0.6943 -0.7063 -0.1381 -vn -0.9475 -0.2583 -0.1885 -vn -0.9640 0.2583 -0.0632 -vn -0.7063 0.7063 -0.0463 -vn -0.2588 0.9658 -0.0170 -vn 0.2588 0.9658 0.0170 -vn 0.7063 0.7063 0.0463 -vn 0.9640 0.2583 0.0632 -vn 0.9640 -0.2583 0.0632 -vn 0.7063 -0.7063 0.0463 -vn 0.2588 -0.9658 0.0170 -vn -0.2588 -0.9658 -0.0170 -vn -0.7063 -0.7063 -0.0463 -vn -0.9640 -0.2583 -0.0632 -g Torus_Torus_Material -usemtl Material -s off -f 1/1/1 13/2/1 14/3/1 2/4/1 -f 2/4/2 14/3/2 15/5/2 3/6/2 -f 3/6/3 15/5/3 16/7/3 4/8/3 -f 4/8/4 16/7/4 17/9/4 5/10/4 -f 5/10/5 17/9/5 18/11/5 6/12/5 -f 6/12/6 18/11/6 19/13/6 7/14/6 -f 7/15/7 19/16/7 20/17/7 8/18/7 -f 8/18/8 20/17/8 21/19/8 9/20/8 -f 9/20/9 21/19/9 22/21/9 10/22/9 -f 10/22/10 22/21/10 23/23/10 11/24/10 -f 11/24/11 23/23/11 24/25/11 12/26/11 -f 12/26/12 24/25/12 13/2/12 1/1/12 -f 13/2/13 25/27/13 26/28/13 14/3/13 -f 14/3/14 26/28/14 27/29/14 15/5/14 -f 15/5/15 27/29/15 28/30/15 16/7/15 -f 16/7/16 28/30/16 29/31/16 17/9/16 -f 17/9/17 29/31/17 30/32/17 18/11/17 -f 18/11/18 30/32/18 31/33/18 19/13/18 -f 19/16/19 31/34/19 32/35/19 20/17/19 -f 20/17/20 32/35/20 33/36/20 21/19/20 -f 21/19/21 33/36/21 34/37/21 22/21/21 -f 22/21/22 34/37/22 35/38/22 23/23/22 -f 23/23/23 35/38/23 36/39/23 24/25/23 -f 24/25/24 36/39/24 25/27/24 13/2/24 -f 25/27/25 37/40/25 38/41/25 26/28/25 -f 26/28/26 38/41/26 39/42/26 27/29/26 -f 27/29/27 39/42/27 40/43/27 28/30/27 -f 28/30/28 40/43/28 41/44/28 29/31/28 -f 29/31/29 41/44/29 42/45/29 30/32/29 -f 30/32/30 42/45/30 43/46/30 31/33/30 -f 31/34/31 43/47/31 44/48/31 32/35/31 -f 32/35/32 44/48/32 45/49/32 33/36/32 -f 33/36/33 45/49/33 46/50/33 34/37/33 -f 34/37/34 46/50/34 47/51/34 35/38/34 -f 35/38/35 47/51/35 48/52/35 36/39/35 -f 36/39/36 48/52/36 37/40/36 25/27/36 -f 37/40/37 49/53/37 50/54/37 38/41/37 -f 38/41/38 50/54/38 51/55/38 39/42/38 -f 39/42/39 51/55/39 52/56/39 40/43/39 -f 40/43/40 52/56/40 53/57/40 41/44/40 -f 41/44/41 53/57/41 54/58/41 42/45/41 -f 42/45/42 54/58/42 55/59/42 43/46/42 -f 43/47/43 55/60/43 56/61/43 44/48/43 -f 44/48/44 56/61/44 57/62/44 45/49/44 -f 45/49/45 57/62/45 58/63/45 46/50/45 -f 46/50/46 58/63/46 59/64/46 47/51/46 -f 47/51/47 59/64/47 60/65/47 48/52/47 -f 48/52/48 60/65/48 49/53/48 37/40/48 -f 49/53/49 61/66/49 62/67/49 50/54/49 -f 50/54/50 62/67/50 63/68/50 51/55/50 -f 51/55/51 63/68/51 64/69/51 52/56/51 -f 52/56/52 64/69/52 65/70/52 53/57/52 -f 53/57/53 65/70/53 66/71/53 54/58/53 -f 54/58/54 66/71/54 67/72/54 55/59/54 -f 55/60/55 67/73/55 68/74/55 56/61/55 -f 56/61/56 68/74/56 69/75/56 57/62/56 -f 57/62/57 69/75/57 70/76/57 58/63/57 -f 58/63/58 70/76/58 71/77/58 59/64/58 -f 59/64/59 71/77/59 72/78/59 60/65/59 -f 60/65/60 72/78/60 61/66/60 49/53/60 -f 61/66/61 73/79/61 74/80/61 62/67/61 -f 62/67/62 74/80/62 75/81/62 63/68/62 -f 63/68/63 75/81/63 76/82/63 64/69/63 -f 64/69/64 76/82/64 77/83/64 65/70/64 -f 65/70/65 77/83/65 78/84/65 66/71/65 -f 66/71/66 78/84/66 79/85/66 67/72/66 -f 67/73/67 79/86/67 80/87/67 68/74/67 -f 68/74/68 80/87/68 81/88/68 69/75/68 -f 69/75/69 81/88/69 82/89/69 70/76/69 -f 70/76/70 82/89/70 83/90/70 71/77/70 -f 71/77/71 83/90/71 84/91/71 72/78/71 -f 72/78/72 84/91/72 73/79/72 61/66/72 -f 73/79/73 85/92/73 86/93/73 74/80/73 -f 74/80/74 86/93/74 87/94/74 75/81/74 -f 75/81/75 87/94/75 88/95/75 76/82/75 -f 76/82/76 88/95/76 89/96/76 77/83/76 -f 77/83/77 89/96/77 90/97/77 78/84/77 -f 78/84/78 90/97/78 91/98/78 79/85/78 -f 79/86/79 91/99/79 92/100/79 80/87/79 -f 80/87/80 92/100/80 93/101/80 81/88/80 -f 81/88/81 93/101/81 94/102/81 82/89/81 -f 82/89/82 94/102/82 95/103/82 83/90/82 -f 83/90/83 95/103/83 96/104/83 84/91/83 -f 84/91/84 96/104/84 85/92/84 73/79/84 -f 85/92/85 97/105/85 98/106/85 86/93/85 -f 86/93/86 98/106/86 99/107/86 87/94/86 -f 87/94/87 99/107/87 100/108/87 88/95/87 -f 88/95/88 100/108/88 101/109/88 89/96/88 -f 89/96/89 101/109/89 102/110/89 90/97/89 -f 90/97/90 102/110/90 103/111/90 91/98/90 -f 91/99/91 103/112/91 104/113/91 92/100/91 -f 92/100/92 104/113/92 105/114/92 93/101/92 -f 93/101/93 105/114/93 106/115/93 94/102/93 -f 94/102/94 106/115/94 107/116/94 95/103/94 -f 95/103/95 107/116/95 108/117/95 96/104/95 -f 96/104/96 108/117/96 97/105/96 85/92/96 -f 97/105/97 109/118/97 110/119/97 98/106/97 -f 98/106/98 110/119/98 111/120/98 99/107/98 -f 99/107/99 111/120/99 112/121/99 100/108/99 -f 100/108/100 112/121/100 113/122/100 101/109/100 -f 101/109/101 113/122/101 114/123/101 102/110/101 -f 102/110/102 114/123/102 115/124/102 103/111/102 -f 103/112/103 115/125/103 116/126/103 104/113/103 -f 104/113/104 116/126/104 117/127/104 105/114/104 -f 105/114/105 117/127/105 118/128/105 106/115/105 -f 106/115/106 118/128/106 119/129/106 107/116/106 -f 107/116/107 119/129/107 120/130/107 108/117/107 -f 108/117/108 120/130/108 109/118/108 97/105/108 -f 109/118/109 121/131/109 122/132/109 110/119/109 -f 110/119/110 122/132/110 123/133/110 111/120/110 -f 111/120/111 123/133/111 124/134/111 112/121/111 -f 112/121/112 124/134/112 125/135/112 113/122/112 -f 113/122/113 125/135/113 126/136/113 114/123/113 -f 114/123/114 126/136/114 127/137/114 115/124/114 -f 115/125/115 127/138/115 128/139/115 116/126/115 -f 116/126/116 128/139/116 129/140/116 117/127/116 -f 117/127/117 129/140/117 130/141/117 118/128/117 -f 118/128/118 130/141/118 131/142/118 119/129/118 -f 119/129/119 131/142/119 132/143/119 120/130/119 -f 120/130/120 132/143/120 121/131/120 109/118/120 -f 121/131/121 133/144/121 134/145/121 122/132/121 -f 122/132/122 134/145/122 135/146/122 123/133/122 -f 123/133/123 135/146/123 136/147/123 124/134/123 -f 124/134/124 136/147/124 137/148/124 125/135/124 -f 125/135/125 137/148/125 138/149/125 126/136/125 -f 126/136/126 138/149/126 139/150/126 127/137/126 -f 127/138/127 139/151/127 140/152/127 128/139/127 -f 128/139/128 140/152/128 141/153/128 129/140/128 -f 129/140/129 141/153/129 142/154/129 130/141/129 -f 130/141/130 142/154/130 143/155/130 131/142/130 -f 131/142/131 143/155/131 144/156/131 132/143/131 -f 132/143/132 144/156/132 133/144/132 121/131/132 -f 133/144/133 145/157/133 146/158/133 134/145/133 -f 134/145/134 146/158/134 147/159/134 135/146/134 -f 135/146/135 147/159/135 148/160/135 136/147/135 -f 136/147/136 148/160/136 149/161/136 137/148/136 -f 137/148/137 149/161/137 150/162/137 138/149/137 -f 138/149/138 150/162/138 151/163/138 139/150/138 -f 139/151/139 151/164/139 152/165/139 140/152/139 -f 140/152/140 152/165/140 153/166/140 141/153/140 -f 141/153/141 153/166/141 154/167/141 142/154/141 -f 142/154/142 154/167/142 155/168/142 143/155/142 -f 143/155/143 155/168/143 156/169/143 144/156/143 -f 144/156/144 156/169/144 145/157/144 133/144/144 -f 145/157/145 157/170/145 158/171/145 146/158/145 -f 146/158/146 158/171/146 159/172/146 147/159/146 -f 147/159/147 159/172/147 160/173/147 148/160/147 -f 148/160/148 160/173/148 161/174/148 149/161/148 -f 149/161/149 161/174/149 162/175/149 150/162/149 -f 150/162/150 162/175/150 163/176/150 151/163/150 -f 151/164/151 163/177/151 164/178/151 152/165/151 -f 152/165/152 164/178/152 165/179/152 153/166/152 -f 153/166/153 165/179/153 166/180/153 154/167/153 -f 154/167/154 166/180/154 167/181/154 155/168/154 -f 155/168/155 167/181/155 168/182/155 156/169/155 -f 156/169/156 168/182/156 157/170/156 145/157/156 -f 157/170/157 169/183/157 170/184/157 158/171/157 -f 158/171/158 170/184/158 171/185/158 159/172/158 -f 159/172/159 171/185/159 172/186/159 160/173/159 -f 160/173/160 172/186/160 173/187/160 161/174/160 -f 161/174/161 173/187/161 174/188/161 162/175/161 -f 162/175/162 174/188/162 175/189/162 163/176/162 -f 163/177/163 175/190/163 176/191/163 164/178/163 -f 164/178/164 176/191/164 177/192/164 165/179/164 -f 165/179/165 177/192/165 178/193/165 166/180/165 -f 166/180/166 178/193/166 179/194/166 167/181/166 -f 167/181/167 179/194/167 180/195/167 168/182/167 -f 168/182/168 180/195/168 169/183/168 157/170/168 -f 169/183/169 181/196/169 182/197/169 170/184/169 -f 170/184/170 182/197/170 183/198/170 171/185/170 -f 171/185/171 183/198/171 184/199/171 172/186/171 -f 172/186/172 184/199/172 185/200/172 173/187/172 -f 173/187/173 185/200/173 186/201/173 174/188/173 -f 174/188/174 186/201/174 187/202/174 175/189/174 -f 175/190/175 187/203/175 188/204/175 176/191/175 -f 176/191/176 188/204/176 189/205/176 177/192/176 -f 177/192/177 189/205/177 190/206/177 178/193/177 -f 178/193/178 190/206/178 191/207/178 179/194/178 -f 179/194/179 191/207/179 192/208/179 180/195/179 -f 180/195/180 192/208/180 181/196/180 169/183/180 -f 181/196/181 193/209/181 194/210/181 182/197/181 -f 182/197/182 194/210/182 195/211/182 183/198/182 -f 183/198/183 195/211/183 196/212/183 184/199/183 -f 184/199/184 196/212/184 197/213/184 185/200/184 -f 185/200/185 197/213/185 198/214/185 186/201/185 -f 186/201/186 198/214/186 199/215/186 187/202/186 -f 187/203/187 199/216/187 200/217/187 188/204/187 -f 188/204/188 200/217/188 201/218/188 189/205/188 -f 189/205/189 201/218/189 202/219/189 190/206/189 -f 190/206/190 202/219/190 203/220/190 191/207/190 -f 191/207/191 203/220/191 204/221/191 192/208/191 -f 192/208/192 204/221/192 193/209/192 181/196/192 -f 193/209/193 205/222/193 206/223/193 194/210/193 -f 194/210/194 206/223/194 207/224/194 195/211/194 -f 195/211/195 207/224/195 208/225/195 196/212/195 -f 196/212/196 208/225/196 209/226/196 197/213/196 -f 197/213/197 209/226/197 210/227/197 198/214/197 -f 198/214/198 210/227/198 211/228/198 199/215/198 -f 199/216/199 211/229/199 212/230/199 200/217/199 -f 200/217/200 212/230/200 213/231/200 201/218/200 -f 201/218/201 213/231/201 214/232/201 202/219/201 -f 202/219/202 214/232/202 215/233/202 203/220/202 -f 203/220/203 215/233/203 216/234/203 204/221/203 -f 204/221/204 216/234/204 205/222/204 193/209/204 -f 205/222/205 217/235/205 218/236/205 206/223/205 -f 206/223/206 218/236/206 219/237/206 207/224/206 -f 207/224/207 219/237/207 220/238/207 208/225/207 -f 208/225/208 220/238/208 221/239/208 209/226/208 -f 209/226/209 221/239/209 222/240/209 210/227/209 -f 210/227/210 222/240/210 223/241/210 211/228/210 -f 211/229/211 223/242/211 224/243/211 212/230/211 -f 212/230/212 224/243/212 225/244/212 213/231/212 -f 213/231/213 225/244/213 226/245/213 214/232/213 -f 214/232/214 226/245/214 227/246/214 215/233/214 -f 215/233/215 227/246/215 228/247/215 216/234/215 -f 216/234/216 228/247/216 217/235/216 205/222/216 -f 217/235/217 229/248/217 230/249/217 218/236/217 -f 218/236/218 230/249/218 231/250/218 219/237/218 -f 219/237/219 231/250/219 232/251/219 220/238/219 -f 220/238/220 232/251/220 233/252/220 221/239/220 -f 221/239/221 233/252/221 234/253/221 222/240/221 -f 222/240/222 234/253/222 235/254/222 223/241/222 -f 223/242/223 235/255/223 236/256/223 224/243/223 -f 224/243/224 236/256/224 237/257/224 225/244/224 -f 225/244/225 237/257/225 238/258/225 226/245/225 -f 226/245/226 238/258/226 239/259/226 227/246/226 -f 227/246/227 239/259/227 240/260/227 228/247/227 -f 228/247/228 240/260/228 229/248/228 217/235/228 -f 229/248/229 241/261/229 242/262/229 230/249/229 -f 230/249/230 242/262/230 243/263/230 231/250/230 -f 231/250/231 243/263/231 244/264/231 232/251/231 -f 232/251/232 244/264/232 245/265/232 233/252/232 -f 233/252/233 245/265/233 246/266/233 234/253/233 -f 234/253/234 246/266/234 247/267/234 235/254/234 -f 235/255/235 247/268/235 248/269/235 236/256/235 -f 236/256/236 248/269/236 249/270/236 237/257/236 -f 237/257/237 249/270/237 250/271/237 238/258/237 -f 238/258/238 250/271/238 251/272/238 239/259/238 -f 239/259/239 251/272/239 252/273/239 240/260/239 -f 240/260/240 252/273/240 241/261/240 229/248/240 -f 241/261/241 253/274/241 254/275/241 242/262/241 -f 242/262/242 254/275/242 255/276/242 243/263/242 -f 243/263/243 255/276/243 256/277/243 244/264/243 -f 244/264/244 256/277/244 257/278/244 245/265/244 -f 245/265/245 257/278/245 258/279/245 246/266/245 -f 246/266/246 258/279/246 259/280/246 247/267/246 -f 247/268/247 259/281/247 260/282/247 248/269/247 -f 248/269/248 260/282/248 261/283/248 249/270/248 -f 249/270/249 261/283/249 262/284/249 250/271/249 -f 250/271/250 262/284/250 263/285/250 251/272/250 -f 251/272/251 263/285/251 264/286/251 252/273/251 -f 252/273/252 264/286/252 253/274/252 241/261/252 -f 253/274/253 265/287/253 266/288/253 254/275/253 -f 254/275/254 266/288/254 267/289/254 255/276/254 -f 255/276/255 267/289/255 268/290/255 256/277/255 -f 256/277/256 268/290/256 269/291/256 257/278/256 -f 257/278/257 269/291/257 270/292/257 258/279/257 -f 258/279/258 270/292/258 271/293/258 259/280/258 -f 259/281/259 271/294/259 272/295/259 260/282/259 -f 260/282/260 272/295/260 273/296/260 261/283/260 -f 261/283/261 273/296/261 274/297/261 262/284/261 -f 262/284/262 274/297/262 275/298/262 263/285/262 -f 263/285/263 275/298/263 276/299/263 264/286/263 -f 264/286/264 276/299/264 265/287/264 253/274/264 -f 265/287/265 277/300/265 278/301/265 266/288/265 -f 266/288/266 278/301/266 279/302/266 267/289/266 -f 267/289/267 279/302/267 280/303/267 268/290/267 -f 268/290/268 280/303/268 281/304/268 269/291/268 -f 269/291/269 281/304/269 282/305/269 270/292/269 -f 270/292/270 282/305/270 283/306/270 271/293/270 -f 271/294/271 283/307/271 284/308/271 272/295/271 -f 272/295/272 284/308/272 285/309/272 273/296/272 -f 273/296/273 285/309/273 286/310/273 274/297/273 -f 274/297/274 286/310/274 287/311/274 275/298/274 -f 275/298/275 287/311/275 288/312/275 276/299/275 -f 276/299/276 288/312/276 277/300/276 265/287/276 -f 277/300/277 289/313/277 290/314/277 278/301/277 -f 278/301/278 290/314/278 291/315/278 279/302/278 -f 279/302/279 291/315/279 292/316/279 280/303/279 -f 280/303/280 292/316/280 293/317/280 281/304/280 -f 281/304/281 293/317/281 294/318/281 282/305/281 -f 282/305/282 294/318/282 295/319/282 283/306/282 -f 283/307/283 295/320/283 296/321/283 284/308/283 -f 284/308/284 296/321/284 297/322/284 285/309/284 -f 285/309/285 297/322/285 298/323/285 286/310/285 -f 286/310/286 298/323/286 299/324/286 287/311/286 -f 287/311/287 299/324/287 300/325/287 288/312/287 -f 288/312/288 300/325/288 289/313/288 277/300/288 -f 289/326/6 301/327/6 302/328/6 290/329/6 -f 290/329/5 302/328/5 303/330/5 291/331/5 -f 291/331/4 303/330/4 304/332/4 292/333/4 -f 292/333/3 304/332/3 305/334/3 293/335/3 -f 293/335/2 305/334/2 306/336/2 294/337/2 -f 294/337/1 306/336/1 307/338/1 295/339/1 -f 295/340/12 307/341/12 308/342/12 296/343/12 -f 296/343/11 308/342/11 309/344/11 297/345/11 -f 297/345/10 309/344/10 310/346/10 298/347/10 -f 298/347/9 310/346/9 311/348/9 299/349/9 -f 299/349/8 311/348/8 312/350/8 300/351/8 -f 300/351/7 312/350/7 301/327/7 289/326/7 -f 301/327/18 313/352/18 314/353/18 302/328/18 -f 302/328/17 314/353/17 315/354/17 303/330/17 -f 303/330/16 315/354/16 316/355/16 304/332/16 -f 304/332/15 316/355/15 317/356/15 305/334/15 -f 305/334/14 317/356/14 318/357/14 306/336/14 -f 306/336/13 318/357/13 319/358/13 307/338/13 -f 307/341/24 319/359/24 320/360/24 308/342/24 -f 308/342/23 320/360/23 321/361/23 309/344/23 -f 309/344/22 321/361/22 322/362/22 310/346/22 -f 310/346/21 322/362/21 323/363/21 311/348/21 -f 311/348/20 323/363/20 324/364/20 312/350/20 -f 312/350/19 324/364/19 313/352/19 301/327/19 -f 313/352/30 325/365/30 326/366/30 314/353/30 -f 314/353/29 326/366/29 327/367/29 315/354/29 -f 315/354/28 327/367/28 328/368/28 316/355/28 -f 316/355/27 328/368/27 329/369/27 317/356/27 -f 317/356/26 329/369/26 330/370/26 318/357/26 -f 318/357/25 330/370/25 331/371/25 319/358/25 -f 319/359/36 331/372/36 332/373/36 320/360/36 -f 320/360/35 332/373/35 333/374/35 321/361/35 -f 321/361/34 333/374/34 334/375/34 322/362/34 -f 322/362/33 334/375/33 335/376/33 323/363/33 -f 323/363/32 335/376/32 336/377/32 324/364/32 -f 324/364/31 336/377/31 325/365/31 313/352/31 -f 325/365/42 337/378/42 338/379/42 326/366/42 -f 326/366/41 338/379/41 339/380/41 327/367/41 -f 327/367/40 339/380/40 340/381/40 328/368/40 -f 328/368/39 340/381/39 341/382/39 329/369/39 -f 329/369/38 341/382/38 342/383/38 330/370/38 -f 330/370/37 342/383/37 343/384/37 331/371/37 -f 331/372/48 343/385/48 344/386/48 332/373/48 -f 332/373/47 344/386/47 345/387/47 333/374/47 -f 333/374/46 345/387/46 346/388/46 334/375/46 -f 334/375/45 346/388/45 347/389/45 335/376/45 -f 335/376/44 347/389/44 348/390/44 336/377/44 -f 336/377/43 348/390/43 337/378/43 325/365/43 -f 337/378/54 349/391/54 350/392/54 338/379/54 -f 338/379/53 350/392/53 351/393/53 339/380/53 -f 339/380/52 351/393/52 352/394/52 340/381/52 -f 340/381/51 352/394/51 353/395/51 341/382/51 -f 341/382/50 353/395/50 354/396/50 342/383/50 -f 342/383/49 354/396/49 355/397/49 343/384/49 -f 343/385/60 355/398/60 356/399/60 344/386/60 -f 344/386/59 356/399/59 357/400/59 345/387/59 -f 345/387/58 357/400/58 358/401/58 346/388/58 -f 346/388/57 358/401/57 359/402/57 347/389/57 -f 347/389/56 359/402/56 360/403/56 348/390/56 -f 348/390/55 360/403/55 349/391/55 337/378/55 -f 349/391/66 361/404/66 362/405/66 350/392/66 -f 350/392/65 362/405/65 363/406/65 351/393/65 -f 351/393/64 363/406/64 364/407/64 352/394/64 -f 352/394/63 364/407/63 365/408/63 353/395/63 -f 353/395/62 365/408/62 366/409/62 354/396/62 -f 354/396/61 366/409/61 367/410/61 355/397/61 -f 355/398/72 367/411/72 368/412/72 356/399/72 -f 356/399/71 368/412/71 369/413/71 357/400/71 -f 357/400/70 369/413/70 370/414/70 358/401/70 -f 358/401/69 370/414/69 371/415/69 359/402/69 -f 359/402/68 371/415/68 372/416/68 360/403/68 -f 360/403/67 372/416/67 361/404/67 349/391/67 -f 361/404/78 373/417/78 374/418/78 362/405/78 -f 362/405/77 374/418/77 375/419/77 363/406/77 -f 363/406/76 375/419/76 376/420/76 364/407/76 -f 364/407/75 376/420/75 377/421/75 365/408/75 -f 365/408/74 377/421/74 378/422/74 366/409/74 -f 366/409/73 378/422/73 379/423/73 367/410/73 -f 367/411/84 379/424/84 380/425/84 368/412/84 -f 368/412/83 380/425/83 381/426/83 369/413/83 -f 369/413/82 381/426/82 382/427/82 370/414/82 -f 370/414/81 382/427/81 383/428/81 371/415/81 -f 371/415/80 383/428/80 384/429/80 372/416/80 -f 372/416/79 384/429/79 373/417/79 361/404/79 -f 373/417/90 385/430/90 386/431/90 374/418/90 -f 374/418/89 386/431/89 387/432/89 375/419/89 -f 375/419/88 387/432/88 388/433/88 376/420/88 -f 376/420/87 388/433/87 389/434/87 377/421/87 -f 377/421/86 389/434/86 390/435/86 378/422/86 -f 378/422/85 390/435/85 391/436/85 379/423/85 -f 379/424/96 391/437/96 392/438/96 380/425/96 -f 380/425/95 392/438/95 393/439/95 381/426/95 -f 381/426/94 393/439/94 394/440/94 382/427/94 -f 382/427/93 394/440/93 395/441/93 383/428/93 -f 383/428/92 395/441/92 396/442/92 384/429/92 -f 384/429/91 396/442/91 385/430/91 373/417/91 -f 385/430/102 397/443/102 398/444/102 386/431/102 -f 386/431/101 398/444/101 399/445/101 387/432/101 -f 387/432/100 399/445/100 400/446/100 388/433/100 -f 388/433/99 400/446/99 401/447/99 389/434/99 -f 389/434/98 401/447/98 402/448/98 390/435/98 -f 390/435/97 402/448/97 403/449/97 391/436/97 -f 391/437/108 403/450/108 404/451/108 392/438/108 -f 392/438/107 404/451/107 405/452/107 393/439/107 -f 393/439/106 405/452/106 406/453/106 394/440/106 -f 394/440/105 406/453/105 407/454/105 395/441/105 -f 395/441/104 407/454/104 408/455/104 396/442/104 -f 396/442/103 408/455/103 397/443/103 385/430/103 -f 397/443/114 409/456/114 410/457/114 398/444/114 -f 398/444/113 410/457/113 411/458/113 399/445/113 -f 399/445/112 411/458/112 412/459/112 400/446/112 -f 400/446/111 412/459/111 413/460/111 401/447/111 -f 401/447/110 413/460/110 414/461/110 402/448/110 -f 402/448/109 414/461/109 415/462/109 403/449/109 -f 403/450/120 415/463/120 416/464/120 404/451/120 -f 404/451/119 416/464/119 417/465/119 405/452/119 -f 405/452/118 417/465/118 418/466/118 406/453/118 -f 406/453/117 418/466/117 419/467/117 407/454/117 -f 407/454/116 419/467/116 420/468/116 408/455/116 -f 408/455/115 420/468/115 409/456/115 397/443/115 -f 409/456/126 421/469/126 422/470/126 410/457/126 -f 410/457/125 422/470/125 423/471/125 411/458/125 -f 411/458/124 423/471/124 424/472/124 412/459/124 -f 412/459/123 424/472/123 425/473/123 413/460/123 -f 413/460/122 425/473/122 426/474/122 414/461/122 -f 414/461/121 426/474/121 427/475/121 415/462/121 -f 415/463/132 427/476/132 428/477/132 416/464/132 -f 416/464/131 428/477/131 429/478/131 417/465/131 -f 417/465/130 429/478/130 430/479/130 418/466/130 -f 418/466/129 430/479/129 431/480/129 419/467/129 -f 419/467/128 431/480/128 432/481/128 420/468/128 -f 420/468/127 432/481/127 421/469/127 409/456/127 -f 421/469/138 433/482/138 434/483/138 422/470/138 -f 422/470/137 434/483/137 435/484/137 423/471/137 -f 423/471/136 435/484/136 436/485/136 424/472/136 -f 424/472/135 436/485/135 437/486/135 425/473/135 -f 425/473/134 437/486/134 438/487/134 426/474/134 -f 426/474/133 438/487/133 439/488/133 427/475/133 -f 427/476/144 439/489/144 440/490/144 428/477/144 -f 428/477/143 440/490/143 441/491/143 429/478/143 -f 429/478/142 441/491/142 442/492/142 430/479/142 -f 430/479/141 442/492/141 443/493/141 431/480/141 -f 431/480/140 443/493/140 444/494/140 432/481/140 -f 432/481/139 444/494/139 433/482/139 421/469/139 -f 433/482/150 445/495/150 446/496/150 434/483/150 -f 434/483/149 446/496/149 447/497/149 435/484/149 -f 435/484/148 447/497/148 448/498/148 436/485/148 -f 436/485/147 448/498/147 449/499/147 437/486/147 -f 437/486/146 449/499/146 450/500/146 438/487/146 -f 438/487/145 450/500/145 451/501/145 439/488/145 -f 439/489/156 451/502/156 452/503/156 440/490/156 -f 440/490/155 452/503/155 453/504/155 441/491/155 -f 441/491/154 453/504/154 454/505/154 442/492/154 -f 442/492/153 454/505/153 455/506/153 443/493/153 -f 443/493/152 455/506/152 456/507/152 444/494/152 -f 444/494/151 456/507/151 445/495/151 433/482/151 -f 445/495/162 457/508/162 458/509/162 446/496/162 -f 446/496/161 458/509/161 459/510/161 447/497/161 -f 447/497/160 459/510/160 460/511/160 448/498/160 -f 448/498/159 460/511/159 461/512/159 449/499/159 -f 449/499/158 461/512/158 462/513/158 450/500/158 -f 450/500/157 462/513/157 463/514/157 451/501/157 -f 451/502/168 463/515/168 464/516/168 452/503/168 -f 452/503/167 464/516/167 465/517/167 453/504/167 -f 453/504/166 465/517/166 466/518/166 454/505/166 -f 454/505/165 466/518/165 467/519/165 455/506/165 -f 455/506/164 467/519/164 468/520/164 456/507/164 -f 456/507/163 468/520/163 457/508/163 445/495/163 -f 457/508/174 469/521/174 470/522/174 458/509/174 -f 458/509/173 470/522/173 471/523/173 459/510/173 -f 459/510/172 471/523/172 472/524/172 460/511/172 -f 460/511/171 472/524/171 473/525/171 461/512/171 -f 461/512/170 473/525/170 474/526/170 462/513/170 -f 462/513/169 474/526/169 475/527/169 463/514/169 -f 463/515/180 475/528/180 476/529/180 464/516/180 -f 464/516/179 476/529/179 477/530/179 465/517/179 -f 465/517/178 477/530/178 478/531/178 466/518/178 -f 466/518/177 478/531/177 479/532/177 467/519/177 -f 467/519/176 479/532/176 480/533/176 468/520/176 -f 468/520/175 480/533/175 469/521/175 457/508/175 -f 469/521/186 481/534/186 482/535/186 470/522/186 -f 470/522/185 482/535/185 483/536/185 471/523/185 -f 471/523/184 483/536/184 484/537/184 472/524/184 -f 472/524/183 484/537/183 485/538/183 473/525/183 -f 473/525/182 485/538/182 486/539/182 474/526/182 -f 474/526/181 486/539/181 487/540/181 475/527/181 -f 475/528/192 487/541/192 488/542/192 476/529/192 -f 476/529/191 488/542/191 489/543/191 477/530/191 -f 477/530/190 489/543/190 490/544/190 478/531/190 -f 478/531/189 490/544/189 491/545/189 479/532/189 -f 479/532/188 491/545/188 492/546/188 480/533/188 -f 480/533/187 492/546/187 481/534/187 469/521/187 -f 481/534/198 493/547/198 494/548/198 482/535/198 -f 482/535/197 494/548/197 495/549/197 483/536/197 -f 483/536/196 495/549/196 496/550/196 484/537/196 -f 484/537/195 496/550/195 497/551/195 485/538/195 -f 485/538/194 497/551/194 498/552/194 486/539/194 -f 486/539/193 498/552/193 499/553/193 487/540/193 -f 487/541/204 499/554/204 500/555/204 488/542/204 -f 488/542/203 500/555/203 501/556/203 489/543/203 -f 489/543/202 501/556/202 502/557/202 490/544/202 -f 490/544/201 502/557/201 503/558/201 491/545/201 -f 491/545/200 503/558/200 504/559/200 492/546/200 -f 492/546/199 504/559/199 493/547/199 481/534/199 -f 493/547/210 505/560/210 506/561/210 494/548/210 -f 494/548/209 506/561/209 507/562/209 495/549/209 -f 495/549/208 507/562/208 508/563/208 496/550/208 -f 496/550/207 508/563/207 509/564/207 497/551/207 -f 497/551/206 509/564/206 510/565/206 498/552/206 -f 498/552/205 510/565/205 511/566/205 499/553/205 -f 499/554/216 511/567/216 512/568/216 500/555/216 -f 500/555/215 512/568/215 513/569/215 501/556/215 -f 501/556/214 513/569/214 514/570/214 502/557/214 -f 502/557/213 514/570/213 515/571/213 503/558/213 -f 503/558/212 515/571/212 516/572/212 504/559/212 -f 504/559/211 516/572/211 505/560/211 493/547/211 -f 505/560/222 517/573/222 518/574/222 506/561/222 -f 506/561/221 518/574/221 519/575/221 507/562/221 -f 507/562/220 519/575/220 520/576/220 508/563/220 -f 508/563/219 520/576/219 521/577/219 509/564/219 -f 509/564/218 521/577/218 522/578/218 510/565/218 -f 510/565/217 522/578/217 523/579/217 511/566/217 -f 511/567/228 523/580/228 524/581/228 512/568/228 -f 512/568/227 524/581/227 525/582/227 513/569/227 -f 513/569/226 525/582/226 526/583/226 514/570/226 -f 514/570/225 526/583/225 527/584/225 515/571/225 -f 515/571/224 527/584/224 528/585/224 516/572/224 -f 516/572/223 528/585/223 517/573/223 505/560/223 -f 517/573/234 529/586/234 530/587/234 518/574/234 -f 518/574/233 530/587/233 531/588/233 519/575/233 -f 519/575/232 531/588/232 532/589/232 520/576/232 -f 520/576/231 532/589/231 533/590/231 521/577/231 -f 521/577/230 533/590/230 534/591/230 522/578/230 -f 522/578/229 534/591/229 535/592/229 523/579/229 -f 523/580/240 535/593/240 536/594/240 524/581/240 -f 524/581/239 536/594/239 537/595/239 525/582/239 -f 525/582/238 537/595/238 538/596/238 526/583/238 -f 526/583/237 538/596/237 539/597/237 527/584/237 -f 527/584/236 539/597/236 540/598/236 528/585/236 -f 528/585/235 540/598/235 529/586/235 517/573/235 -f 529/586/246 541/599/246 542/600/246 530/587/246 -f 530/587/245 542/600/245 543/601/245 531/588/245 -f 531/588/244 543/601/244 544/602/244 532/589/244 -f 532/589/243 544/602/243 545/603/243 533/590/243 -f 533/590/242 545/603/242 546/604/242 534/591/242 -f 534/591/241 546/604/241 547/605/241 535/592/241 -f 535/593/252 547/606/252 548/607/252 536/594/252 -f 536/594/251 548/607/251 549/608/251 537/595/251 -f 537/595/250 549/608/250 550/609/250 538/596/250 -f 538/596/249 550/609/249 551/610/249 539/597/249 -f 539/597/248 551/610/248 552/611/248 540/598/248 -f 540/598/247 552/611/247 541/599/247 529/586/247 -f 541/599/258 553/612/258 554/613/258 542/600/258 -f 542/600/257 554/613/257 555/614/257 543/601/257 -f 543/601/256 555/614/256 556/615/256 544/602/256 -f 544/602/255 556/615/255 557/616/255 545/603/255 -f 545/603/254 557/616/254 558/617/254 546/604/254 -f 546/604/253 558/617/253 559/618/253 547/605/253 -f 547/606/264 559/619/264 560/620/264 548/607/264 -f 548/607/263 560/620/263 561/621/263 549/608/263 -f 549/608/262 561/621/262 562/622/262 550/609/262 -f 550/609/261 562/622/261 563/623/261 551/610/261 -f 551/610/260 563/623/260 564/624/260 552/611/260 -f 552/611/259 564/624/259 553/612/259 541/599/259 -f 553/612/270 565/625/270 566/626/270 554/613/270 -f 554/613/269 566/626/269 567/627/269 555/614/269 -f 555/614/268 567/627/268 568/628/268 556/615/268 -f 556/615/267 568/628/267 569/629/267 557/616/267 -f 557/616/266 569/629/266 570/630/266 558/617/266 -f 558/617/265 570/630/265 571/631/265 559/618/265 -f 559/619/276 571/632/276 572/633/276 560/620/276 -f 560/620/275 572/633/275 573/634/275 561/621/275 -f 561/621/274 573/634/274 574/635/274 562/622/274 -f 562/622/273 574/635/273 575/636/273 563/623/273 -f 563/623/272 575/636/272 576/637/272 564/624/272 -f 564/624/271 576/637/271 565/625/271 553/612/271 -f 565/625/282 1/1/282 2/4/282 566/626/282 -f 566/626/281 2/4/281 3/6/281 567/627/281 -f 567/627/280 3/6/280 4/8/280 568/628/280 -f 568/628/279 4/8/279 5/10/279 569/629/279 -f 569/629/278 5/10/278 6/12/278 570/630/278 -f 570/630/277 6/12/277 7/14/277 571/631/277 -f 571/632/288 7/15/288 8/18/288 572/633/288 -f 572/633/287 8/18/287 9/20/287 573/634/287 -f 573/634/286 9/20/286 10/22/286 574/635/286 -f 574/635/285 10/22/285 11/24/285 575/636/285 -f 575/636/284 11/24/284 12/26/284 576/637/284 -f 576/637/283 12/26/283 1/1/283 565/625/283 diff --git a/simulation/isaac-sim/standalone_examples/data/torus/torus.stl b/simulation/isaac-sim/standalone_examples/data/torus/torus.stl deleted file mode 100644 index 148588b1b..000000000 Binary files a/simulation/isaac-sim/standalone_examples/data/torus/torus.stl and /dev/null differ diff --git a/simulation/isaac-sim/standalone_examples/notebooks/hello_world.ipynb b/simulation/isaac-sim/standalone_examples/notebooks/hello_world.ipynb deleted file mode 100644 index 86b15cb1e..000000000 --- a/simulation/isaac-sim/standalone_examples/notebooks/hello_world.ipynb +++ /dev/null @@ -1,123 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "f639c7a9", - "metadata": {}, - "outputs": [], - "source": [ - "# Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved.\n", - "#\n", - "# NVIDIA CORPORATION and its licensors retain all intellectual property\n", - "# and proprietary rights in and to this software, related documentation\n", - "# and any modifications thereto. Any use, reproduction, disclosure or\n", - "# distribution of this software and related documentation without an express\n", - "# license agreement from NVIDIA CORPORATION is strictly prohibited.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "420e8940", - "metadata": {}, - "outputs": [], - "source": [ - "from isaacsim import SimulationApp\n", - "\n", - "# Set the path below to your desired nucleus server\n", - "# Make sure you installed a local nucleus server before this\n", - "simulation_app = SimulationApp()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2955b2e2", - "metadata": {}, - "outputs": [], - "source": [ - "from isaacsim.core.api import World\n", - "from isaacsim.core.api.objects import DynamicCuboid\n", - "import numpy as np\n", - "\n", - "world = World(stage_units_in_meters=1.0)\n", - "world.scene.add_default_ground_plane()\n", - "# A render/ step or an update call is needed to reflect the changes to the opened USD in Isaac Sim GUI\n", - "# Note: avoid pressing play/ pause or stop in the GUI in this workflow.\n", - "world.render()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "702a40af", - "metadata": {}, - "outputs": [], - "source": [ - "fancy_cube = world.scene.add(\n", - " DynamicCuboid(\n", - " prim_path=\"/World/random_cube\",\n", - " name=\"fancy_cube\",\n", - " position=np.array([0, 0, 1.000]),\n", - " scale=np.array([0.5015, 0.505, 0.5015]),\n", - " size=1.0,\n", - " color=np.array([0, 0, 1.0]),\n", - " )\n", - ")\n", - "world.render()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10a83e53", - "metadata": {}, - "outputs": [], - "source": [ - "world.reset()\n", - "for i in range(500):\n", - " position, orientation = fancy_cube.get_world_pose()\n", - " linear_velocity = fancy_cube.get_linear_velocity()\n", - " print(\"Cube position is : \" + str(position))\n", - " print(\"Cube's orientation is : \" + str(orientation))\n", - " print(\"Cube's linear velocity is : \" + str(linear_velocity))\n", - " # we have control over stepping physics and rendering in this workflow\n", - " # things run in sync\n", - " world.step(render=True) # execute one physics step and one rendering step\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "02f57ca0", - "metadata": {}, - "outputs": [], - "source": [ - "# Cleanup application\n", - "simulation_app.close()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Isaac Sim Python 3", - "language": "python", - "name": "isaac_sim_python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/simulation/isaac-sim/standalone_examples/notebooks/scene_generation.ipynb b/simulation/isaac-sim/standalone_examples/notebooks/scene_generation.ipynb deleted file mode 100644 index 33e27af61..000000000 --- a/simulation/isaac-sim/standalone_examples/notebooks/scene_generation.ipynb +++ /dev/null @@ -1,252 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "37e7b5a5", - "metadata": {}, - "outputs": [], - "source": [ - "# Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved.\n", - "#\n", - "# NVIDIA CORPORATION and its licensors retain all intellectual property\n", - "# and proprietary rights in and to this software, related documentation\n", - "# and any modifications thereto. Any use, reproduction, disclosure or\n", - "# distribution of this software and related documentation without an express\n", - "# license agreement from NVIDIA CORPORATION is strictly prohibited." - ] - }, - { - "cell_type": "markdown", - "id": "2f630f7c", - "metadata": {}, - "source": [ - "# Create a Headless IsaacSim\n", - "\n", - "You should only have to do this part once; it will load kit, or at least the headless IsaacSim version of kit, by calling\n", - "simulation_app = SimulationApp(CONFIG).This will take some time, so give it a minute and wait for it to say \"Hi\".\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "09ebaf0b", - "metadata": {}, - "outputs": [], - "source": [ - "from isaacsim import SimulationApp\n", - "\n", - "# Set the path below to your desired nucleus server\n", - "simulation_app = SimulationApp()\n", - "print(\"Hi\")" - ] - }, - { - "cell_type": "markdown", - "id": "dfb64239", - "metadata": {}, - "source": [ - "\n", - "## Create a simulation context and move camera\n", - "\n", - "Here we create a `SimulationContext` object which provides a high level interface to interact with the simulation\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd5b9ad6", - "metadata": {}, - "outputs": [], - "source": [ - "from isaacsim.core.api import World\n", - "from isaacsim.core.utils.prims import create_prim\n", - "from isaacsim.core.utils.viewports import set_camera_view\n", - "from isaacsim.storage.native import get_assets_root_path\n", - "from isaacsim.core.api.materials.omni_glass import OmniGlass\n", - "from isaacsim.core.prims import SingleXFormPrim\n", - "from isaacsim.core.utils.extensions import get_extension_path_from_name\n", - "from isaacsim.core.utils.semantics import add_update_semantics\n", - "\n", - "import omni\n", - "import carb\n", - "import numpy as np\n", - "\n", - "simulation_world = World(stage_units_in_meters=1.0)\n", - "set_camera_view(eye=np.array([-0.9025, 2.1035, 1.0222]), target=np.array([0.6039, 0.30, 0.0950]))\n", - "\n", - "# Step our simulation to ensure everything initialized\n", - "simulation_world.step()\n" - ] - }, - { - "cell_type": "markdown", - "id": "1c02f8f3", - "metadata": {}, - "source": [ - "## Creating the scene\n", - "\n", - "Re-run the cell below to randomize the scene from scratch. The goal here is to make iterating on scene setup easy and not require restarts of the omniverse application.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fd004b53", - "metadata": {}, - "outputs": [], - "source": [ - "# Delete everything in the stage\n", - "simulation_world.clear()\n", - "# SCENE SETUP\n", - "\n", - "# Add a distant light\n", - "create_prim(\"/DistantLight\", \"DistantLight\", attributes={\"inputs:intensity\": 500})\n", - "\n", - "# Add a ground collision plane\n", - "simulation_world.scene.add_ground_plane(size=1000, z_position=-0.5, color=np.array([1, 1, 1]))\n", - "\n", - "# Load a URDF\n", - "status, import_config = omni.kit.commands.execute(\"URDFCreateImportConfig\")\n", - "import_config.merge_fixed_joints = False\n", - "import_config.convex_decomp = False\n", - "import_config.import_inertia_tensor = True\n", - "import_config.fix_base = False\n", - "import_config.distance_scale = 1.0\n", - "import_config.create_physics_scene = False # we already have a physics scene from simulation_world\n", - "\n", - "# Get path to extension data:\n", - "extension_path = get_extension_path_from_name(\"isaacsim.asset.importer.urdf\")\n", - "# Import URDF, stage_path contains the path the path to the usd prim in the stage.\n", - "status, stage_path = omni.kit.commands.execute(\n", - " \"URDFParseAndImportFile\",\n", - " urdf_path=extension_path + \"/data/urdf/robots/carter/urdf/carter.urdf\",\n", - " import_config=import_config,\n", - ")\n", - "stage = simulation_world.stage\n", - "add_update_semantics(stage.GetPrimAtPath(stage_path), \"Robot\")\n", - "\n", - "# Load a mesh\n", - "assets_root_path = get_assets_root_path()\n", - "if assets_root_path is None:\n", - " carb.log_error(\"Could not find Isaac Sim assets folder\")\n", - "usd_path = assets_root_path + \"/Isaac/Props/YCB/Axis_Aligned/006_mustard_bottle.usd\"\n", - "\n", - "prim = create_prim(prim_path=\"/Mesh\", usd_path=usd_path, scale=np.array([10.0, 10.0, 10.0]), semantic_label=\"mustard\")\n", - "xform_prim = SingleXFormPrim(prim.GetPath().pathString)\n", - "\n", - "\n", - "# Apply a glass material to mesh\n", - "material = OmniGlass(\n", - " \"/Looks/GlassMaterial\", name=\"glass_material\", ior=1.25, depth=0.001, thin_walled=False, color=np.random.rand(3)\n", - ")\n", - "xform_prim.apply_visual_material(material)\n", - "\n", - "# Set mesh transform\n", - "xform_prim.set_world_pose(position=np.array([1.00, 0, 0]))\n" - ] - }, - { - "cell_type": "markdown", - "id": "327aed4d", - "metadata": {}, - "source": [ - "## Viewing the scene in the notebook\n", - "\n", - "This next example does not change the scene (but it could if you used commands like the ones above), but it does visualize the synthetic data by accesing the underlying annotator data. Specifically, it shows a color, depth, and segmentation view of the scene, and then displays them within the notebook.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d3ba55ad", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "from omni.syntheticdata import visualize\n", - "from omni.kit.viewport.utility import get_active_viewport\n", - "import omni.replicator.core as rep\n", - "\n", - "viewport_api = get_active_viewport()\n", - "active_cam = viewport_api.get_active_camera()\n", - "resolution = viewport_api.get_texture_resolution()\n", - "render_product = rep.create.render_product(active_cam, resolution)\n", - "\n", - "rgb = rep.AnnotatorRegistry.get_annotator(\"rgb\")\n", - "rgb.attach([render_product])\n", - "depth = rep.AnnotatorRegistry.get_annotator(\"distance_to_image_plane\")\n", - "depth.attach([render_product])\n", - "semantic_segmentation = rep.AnnotatorRegistry.get_annotator(\"semantic_segmentation\")\n", - "semantic_segmentation.attach([render_product])\n", - "\n", - "# Run the application for multiple frames to ensure the synthetic data pipeline is initialized\n", - "timeline = omni.timeline.get_timeline_interface()\n", - "timeline.play()\n", - "for _ in range(10):\n", - " simulation_app.update()\n", - "timeline.pause()\n", - "\n", - "# Get groundtruth\n", - "rgb_data = rgb.get_data()\n", - "depth_data = depth.get_data()\n", - "semantic_segmentation_data = semantic_segmentation.get_data()\n", - "\n", - "# GROUNDTRUTH VISUALIZATION\n", - "# Setup a figure\n", - "_, axes = plt.subplots(1, 3, figsize=(20, 7))\n", - "axes = axes.flat\n", - "for ax in axes:\n", - " ax.axis(\"off\")\n", - "\n", - "# RGB\n", - "axes[0].set_title(\"RGB\")\n", - "axes[0].imshow(rgb_data)\n", - "\n", - "# DEPTH\n", - "axes[1].set_title(\"Depth\")\n", - "depth_data_clipped = np.clip(depth_data, 0, 255)\n", - "axes[1].imshow(visualize.colorize_distance(depth_data.squeeze()))\n", - "\n", - "# SEMANTIC SEGMENTATION\n", - "axes[2].set_title(\"Semantic Segmentation\")\n", - "# Draw the segmentation mask on top of the color image with a transparency\n", - "axes[2].imshow(rgb_data)\n", - "semantic_segmentation_rgb = visualize.colorize_segmentation(semantic_segmentation_data[\"data\"])\n", - "axes[2].imshow(semantic_segmentation_rgb, alpha=0.7)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c1e98ae2", - "metadata": {}, - "outputs": [], - "source": [ - "# Cleanup application\n", - "simulation_app.close()\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Isaac Sim Python 3", - "language": "python", - "name": "isaac_sim_python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.15" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/simulation/isaac-sim/standalone_examples/replicator/amr_navigation.py b/simulation/isaac-sim/standalone_examples/replicator/amr_navigation.py deleted file mode 100644 index c20199ea8..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/amr_navigation.py +++ /dev/null @@ -1,381 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -"""Generate synthetic data from an AMR navigating to random locations -""" - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import argparse -import builtins -import os -import random -from itertools import cycle - -import carb.settings -import omni.client -import omni.kit.app -import omni.replicator.core as rep -import omni.timeline -import omni.usd -import omni.usd.commands -from isaacsim.core.utils.stage import add_reference_to_stage, create_new_stage -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf, PhysxSchema, Usd, UsdGeom, UsdLux, UsdPhysics - - -class NavSDGDemo: - CARTER_URL = "/Isaac/Samples/Replicator/OmniGraph/nova_carter_nav_only.usd" - DOLLY_URL = "/Isaac/Props/Dolly/dolly.usd" - PROPS_URL = "/Isaac/Props/YCB/Axis_Aligned_Physics" - LEFT_CAMERA_PATH = "/NavWorld/CarterNav/chassis_link/front_hawk/left/camera_left" - RIGHT_CAMERA_PATH = "/NavWorld/CarterNav/chassis_link/front_hawk/right/camera_right" - - def __init__(self): - self._carter_chassis = None - self._carter_nav_target = None - self._dolly = None - self._dolly_light = None - self._props = [] - self._cycled_env_urls = None - self._env_interval = 1 - self._timeline = None - self._timeline_sub = None - self._stage_event_sub = None - self._stage = None - self._trigger_distance = 2.0 - self._num_frames = 0 - self._frame_counter = 0 - self._writer = None - self._out_dir = None - self._render_products = [] - self._use_temp_rp = False - self._in_running_state = False - - def start( - self, - num_frames=10, - out_dir=None, - env_urls=[], - env_interval=3, - use_temp_rp=False, - seed=None, - ): - print(f"[NavSDGDemo] Starting") - if seed is not None: - random.seed(seed) - self._num_frames = num_frames - self._out_dir = out_dir if out_dir is not None else os.path.join(os.getcwd(), "_out_nav_sdg_demo") - self._cycled_env_urls = cycle(env_urls) - self._env_interval = env_interval - self._use_temp_rp = use_temp_rp - self._frame_counter = 0 - self._trigger_distance = 2.0 - self._load_env() - self._randomize_dolly_pose() - self._randomize_dolly_light() - self._randomize_prop_poses() - self._setup_sdg() - self._timeline = omni.timeline.get_timeline_interface() - self._timeline.play() - self._timeline_sub = self._timeline.get_timeline_event_stream().create_subscription_to_pop_by_type( - int(omni.timeline.TimelineEventType.CURRENT_TIME_TICKED), self._on_timeline_event - ) - self._stage_event_sub = ( - omni.usd.get_context() - .get_stage_event_stream() - .create_subscription_to_pop_by_type(int(omni.usd.StageEventType.CLOSING), self._on_stage_closing_event) - ) - self._in_running_state = True - - def clear(self): - self._cycled_env_urls = None - self._carter_chassis = None - self._carter_nav_target = None - self._dolly = None - self._dolly_light = None - self._timeline = None - self._frame_counter = 0 - if self._stage_event_sub: - self._stage_event_sub.unsubscribe() - self._stage_event_sub = None - if self._timeline_sub: - self._timeline_sub.unsubscribe() - self._timeline_sub = None - self._clear_sdg_render_products() - self._stage = None - self._in_running_state = False - - def is_running(self): - return self._in_running_state - - def _is_running_in_script_editor(self): - return builtins.ISAAC_LAUNCHED_FROM_TERMINAL is True - - def _on_stage_closing_event(self, e: carb.events.IEvent): - self.clear() - - def _load_env(self): - # Fresh stage with custom physics scene for Nova Carter's navigation - create_new_stage() - self._stage = omni.usd.get_context().get_stage() - self._add_physics_scene() - - # Environment - assets_root_path = get_assets_root_path() - add_reference_to_stage(usd_path=assets_root_path + next(self._cycled_env_urls), prim_path="/Environment") - - # Nova Carter - add_reference_to_stage(usd_path=assets_root_path + self.CARTER_URL, prim_path="/NavWorld/CarterNav") - self._carter_nav_target = self._stage.GetPrimAtPath("/NavWorld/CarterNav/targetXform") - self._carter_chassis = self._stage.GetPrimAtPath("/NavWorld/CarterNav/chassis_link") - - # Dolly - add_reference_to_stage(usd_path=assets_root_path + self.DOLLY_URL, prim_path="/NavWorld/Dolly") - self._dolly = self._stage.GetPrimAtPath("/NavWorld/Dolly") - if not self._dolly.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(self._dolly).AddTranslateOp() - if not self._dolly.GetAttribute("xformOp:rotateXYZ"): - UsdGeom.Xformable(self._dolly).AddRotateXYZOp() - # Add colliders to the mesh and primitive types of the dolly descendent prims - for desc_prim in Usd.PrimRange(self._dolly): - # Enable collisions if the prim is of type mesh or gprim (primitive) - if desc_prim.IsA(UsdGeom.Mesh) or desc_prim.IsA(UsdGeom.Gprim): - if not desc_prim.HasAPI(UsdPhysics.CollisionAPI): - collision_api = UsdPhysics.CollisionAPI.Apply(desc_prim) - else: - collision_api = UsdPhysics.CollisionAPI(desc_prim) - collision_api.CreateCollisionEnabledAttr(True) - # If the prim is a mesh add a collider aproximation type - if desc_prim.IsA(UsdGeom.Mesh): - if not desc_prim.HasAPI(UsdPhysics.MeshCollisionAPI): - mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(desc_prim) - else: - mesh_collision_api = UsdPhysics.MeshCollisionAPI(desc_prim) - # The prim is static, setting the collider type to "none" (defaulting to "TriangleMesh", e.g. no approx) - mesh_collision_api.CreateApproximationAttr().Set("none") - - # Light - light = UsdLux.SphereLight.Define(self._stage, f"/NavWorld/DollyLight") - light.CreateRadiusAttr(0.5) - light.CreateIntensityAttr(35000) - light.CreateColorAttr(Gf.Vec3f(1.0, 1.0, 1.0)) - self._dolly_light = light.GetPrim() - if not self._dolly_light.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(self._dolly_light).AddTranslateOp() - - # Props - props_urls = [] - props_folder_path = assets_root_path + self.PROPS_URL - result, entries = omni.client.list(props_folder_path) - if result != omni.client.Result.OK: - carb.log_error(f"Could not list assets in path: {props_folder_path}") - return - for entry in entries: - _, ext = os.path.splitext(entry.relative_path) - if ext == ".usd": - props_urls.append(f"{props_folder_path}/{entry.relative_path}") - - cycled_props_url = cycle(props_urls) - for i in range(15): - prop_url = next(cycled_props_url) - prop_name = os.path.splitext(os.path.basename(prop_url))[0] - path = f"/NavWorld/Props/Prop_{prop_name}_{i}" - prim = self._stage.DefinePrim(path, "Xform") - prim.GetReferences().AddReference(prop_url) - self._props.append(prim) - - def _add_physics_scene(self): - # Physics setup specific for the navigation graph - physics_scene = UsdPhysics.Scene.Define(self._stage, "/physicsScene") - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(self._stage.GetPrimAtPath("/physicsScene")) - physx_scene.GetEnableCCDAttr().Set(True) - physx_scene.GetEnableGPUDynamicsAttr().Set(False) - physx_scene.GetBroadphaseTypeAttr().Set("MBP") - - def _randomize_dolly_pose(self): - min_dist_from_carter = 4 - carter_loc = self._carter_chassis.GetAttribute("xformOp:translate").Get() - for _ in range(100): - x, y = random.uniform(-6, 6), random.uniform(-6, 6) - dist = (Gf.Vec2f(x, y) - Gf.Vec2f(carter_loc[0], carter_loc[1])).GetLength() - if dist > min_dist_from_carter: - self._dolly.GetAttribute("xformOp:translate").Set((x, y, 0)) - self._carter_nav_target.GetAttribute("xformOp:translate").Set((x, y, 0)) - break - self._dolly.GetAttribute("xformOp:rotateXYZ").Set((0, 0, random.uniform(-180, 180))) - - def _randomize_dolly_light(self): - dolly_loc = self._dolly.GetAttribute("xformOp:translate").Get() - self._dolly_light.GetAttribute("xformOp:translate").Set(dolly_loc + (0, 0, 2.5)) - self._dolly_light.GetAttribute("inputs:color").Set( - (random.uniform(0, 1), random.uniform(0, 1), random.uniform(0, 1)) - ) - - def _randomize_prop_poses(self): - spawn_loc = self._dolly.GetAttribute("xformOp:translate").Get() - spawn_loc[2] = spawn_loc[2] + 0.5 - for prop in self._props: - prop.GetAttribute("xformOp:translate").Set(spawn_loc + (random.uniform(-1, 1), random.uniform(-1, 1), 0)) - spawn_loc[2] = spawn_loc[2] + 0.2 - - def _setup_sdg(self): - # Disable capture on play and async rendering - carb.settings.get_settings().set("/omni/replicator/captureOnPlay", False) - carb.settings.get_settings().set("/omni/replicator/asyncRendering", False) - carb.settings.get_settings().set("/app/asyncRendering", False) - - # Set camera sensors fStop to 0.0 to get well lit sharp images - left_camera_prim = self._stage.GetPrimAtPath(self.LEFT_CAMERA_PATH) - left_camera_prim.GetAttribute("fStop").Set(0.0) - right_camera_prim = self._stage.GetPrimAtPath(self.RIGHT_CAMERA_PATH) - right_camera_prim.GetAttribute("fStop").Set(0.0) - - self._writer = rep.WriterRegistry.get("BasicWriter") - self._writer.initialize(output_dir=self._out_dir, rgb=True) - self._setup_sdg_render_products() - - def _setup_sdg_render_products(self): - print(f"[NavSDGDemo] Creating SDG render products") - rp_left = rep.create.render_product( - self.LEFT_CAMERA_PATH, - (1024, 1024), - name="left_sensor", - force_new=True, - ) - rp_right = rep.create.render_product( - self.RIGHT_CAMERA_PATH, - (1024, 1024), - name="right_sensor", - force_new=True, - ) - self._render_products = [rp_left, rp_right] - # For better performance the render products can be disabled when not in use, and re-enabled only during SDG - if self._use_temp_rp: - self._disable_render_products() - self._writer.attach(self._render_products) - rep.orchestrator.preview() - - def _clear_sdg_render_products(self): - print(f"[NavSDGDemo] Clearing SDG render products") - if self._writer: - self._writer.detach() - for rp in self._render_products: - rp.destroy() - self._render_products.clear() - if self._stage.GetPrimAtPath("/Replicator"): - omni.kit.commands.execute("DeletePrimsCommand", paths=["/Replicator"]) - - def _enable_render_products(self): - print(f"[NavSDGDemo] Enabling render products for SDG..") - for rp in self._render_products: - rp.hydra_texture.set_updates_enabled(True) - - def _disable_render_products(self): - print(f"[NavSDGDemo] Disabling render products (enabled only during SDG)..") - for rp in self._render_products: - rp.hydra_texture.set_updates_enabled(False) - - def _run_sdg(self): - if self._use_temp_rp: - self._enable_render_products() - rep.orchestrator.step(rt_subframes=16) - if self._use_temp_rp: - self._disable_render_products() - - async def _run_sdg_async(self): - if self._use_temp_rp: - self._enable_render_products() - await rep.orchestrator.step_async(rt_subframes=16) - if self._use_temp_rp: - self._disable_render_products() - - def _load_next_env(self): - if self._stage.GetPrimAtPath("/Environment"): - omni.kit.commands.execute("DeletePrimsCommand", paths=["/Environment"]) - assets_root_path = get_assets_root_path() - add_reference_to_stage(usd_path=assets_root_path + next(self._cycled_env_urls), prim_path="/Environment") - - def _on_sdg_done(self, task): - self._setup_next_frame() - - def _setup_next_frame(self): - self._frame_counter += 1 - if self._frame_counter >= self._num_frames: - print(f"[NavSDGDemo] Finished") - # Make sure the data has been written to disk before clearing the state - if self._is_running_in_script_editor(): - import asyncio - - task = asyncio.ensure_future(rep.orchestrator.wait_until_complete_async()) - task.add_done_callback(lambda t: self.clear()) - else: - rep.orchestrator.wait_until_complete() - self.clear() - return - - self._randomize_dolly_pose() - self._randomize_dolly_light() - self._randomize_prop_poses() - if self._frame_counter % self._env_interval == 0: - self._load_next_env() - # Set a new random distance from which to take capture the next frame - self._trigger_distance = random.uniform(1.75, 2.5) - self._timeline.play() - self._timeline_sub = self._timeline.get_timeline_event_stream().create_subscription_to_pop_by_type( - int(omni.timeline.TimelineEventType.CURRENT_TIME_TICKED), self._on_timeline_event - ) - - def _on_timeline_event(self, e: carb.events.IEvent): - carter_loc = self._carter_chassis.GetAttribute("xformOp:translate").Get() - dolly_loc = self._dolly.GetAttribute("xformOp:translate").Get() - dist = (Gf.Vec2f(dolly_loc[0], dolly_loc[1]) - Gf.Vec2f(carter_loc[0], carter_loc[1])).GetLength() - if dist < self._trigger_distance: - print(f"[NavSDGDemo] Starting SDG for frame no. {self._frame_counter}") - self._timeline.pause() - self._timeline_sub.unsubscribe() - if self._is_running_in_script_editor(): - import asyncio - - task = asyncio.ensure_future(self._run_sdg_async()) - task.add_done_callback(self._on_sdg_done) - else: - self._run_sdg() - self._setup_next_frame() - - -ENV_URLS = [ - "/Isaac/Environments/Grid/default_environment.usd", - "/Isaac/Environments/Simple_Warehouse/warehouse.usd", - "/Isaac/Environments/Grid/gridroom_black.usd", -] - -parser = argparse.ArgumentParser() -parser.add_argument("--use_temp_rp", action="store_true", help="Create and destroy render products for each SDG frame") -parser.add_argument("--num_frames", type=int, default=9, help="The number of frames to capture") -parser.add_argument("--env_interval", type=int, default=3, help="Interval at which to change the environments") -args, unknown = parser.parse_known_args() - -out_dir = os.path.join(os.getcwd(), "_out_nav_sdg_demo", "") -nav_demo = NavSDGDemo() -nav_demo.start( - num_frames=args.num_frames, - out_dir=out_dir, - env_urls=ENV_URLS, - env_interval=args.env_interval, - use_temp_rp=args.use_temp_rp, - seed=22, -) - -while simulation_app.is_running() and nav_demo.is_running(): - simulation_app.update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/augmentation/annotator_augmentation.py b/simulation/isaac-sim/standalone_examples/replicator/augmentation/annotator_augmentation.py deleted file mode 100644 index eb6d22539..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/augmentation/annotator_augmentation.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -"""Generate augmented synthetic data from annotators -""" - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import argparse -import os -import time - -import carb.settings -import numpy as np -import omni.replicator.core as rep -import warp as wp -from isaacsim.core.utils.stage import open_stage -from isaacsim.storage.native import get_assets_root_path -from PIL import Image - -parser = argparse.ArgumentParser() -parser.add_argument("--num_frames", type=int, default=25, help="The number of frames to capture") -parser.add_argument("--use_warp", action="store_true", help="Use warp augmentations instead of numpy") -args, unknown = parser.parse_known_args() - -NUM_FRAMES = args.num_frames -USE_WARP = args.use_warp -ENV_URL = "/Isaac/Environments/Grid/default_environment.usd" - -# Enable scripts -carb.settings.get_settings().set_bool("/app/omni.graph.scriptnode/opt_in", True) - - -# Illustrative augmentation switching red and blue channels in rgb data using numpy (CPU) and warp (GPU) -def rgb_to_bgr_np(data_in): - data_in[:, :, [0, 2]] = data_in[:, :, [2, 0]] - return data_in - - -@wp.kernel -def rgb_to_bgr_wp(data_in: wp.array3d(dtype=wp.uint8), data_out: wp.array3d(dtype=wp.uint8)): - i, j = wp.tid() - data_out[i, j, 0] = data_in[i, j, 2] - data_out[i, j, 1] = data_in[i, j, 1] - data_out[i, j, 2] = data_in[i, j, 0] - data_out[i, j, 3] = data_in[i, j, 3] - - -# Gaussian noise augmentation on depth data in numpy (CPU) and warp (GPU) -def gaussian_noise_depth_np(data_in, sigma: float, seed: int): - np.random.seed(seed) - return data_in + np.random.randn(*data_in.shape) * sigma - - -rep.AnnotatorRegistry.register_augmentation( - "gn_depth_np", rep.annotators.Augmentation.from_function(gaussian_noise_depth_np, sigma=0.1, seed=None) -) - - -@wp.kernel -def gaussian_noise_depth_wp( - data_in: wp.array2d(dtype=wp.float32), data_out: wp.array2d(dtype=wp.float32), sigma: float, seed: int -): - i, j = wp.tid() - state = wp.rand_init(seed, wp.tid()) - data_out[i, j] = data_in[i, j] + sigma * wp.randn(state) - - -rep.AnnotatorRegistry.register_augmentation( - "gn_depth_wp", rep.annotators.Augmentation.from_function(gaussian_noise_depth_wp, sigma=0.1, seed=None) -) - -# Helper functions for writing images from annotator data -def write_rgb(data, path): - rgb_img = Image.fromarray(data, mode="RGBA") - rgb_img.save(path + ".png") - - -def write_depth(data, path): - # Convert to numpy (if warp), normalize, handle any nan values, and convert to from float32 to 8-bit int array - if isinstance(data, wp.array): - data = data.numpy() - # Replace any -inf and inf values with nan, then calculate the mean value and replace nan with the mean - data[np.isinf(data)] = np.nan - data = np.nan_to_num(data, nan=np.nanmean(data), copy=False) - normalized_array = (data - np.min(data)) / (np.max(data) - np.min(data)) - integer_array = (normalized_array * 255).astype(np.uint8) - depth_img = Image.fromarray(integer_array, mode="L") - depth_img.save(path + ".png") - - -# Setup the environment -assets_root_path = get_assets_root_path() -open_stage(assets_root_path + ENV_URL) - -# Disable capture on play and async rendering -carb.settings.get_settings().set("/omni/replicator/captureOnPlay", False) -carb.settings.get_settings().set("/omni/replicator/asyncRendering", False) -carb.settings.get_settings().set("/app/asyncRendering", False) - -# Create a red cube and a render product from a camera looking at the cube from the top -red_mat = rep.create.material_omnipbr(diffuse=(1, 0, 0)) -red_cube = rep.create.cube(position=(0, 0, 0.71), material=red_mat) -cam = rep.create.camera(position=(0, 0, 5), look_at=(0, 0, 0)) -rp = rep.create.render_product(cam, (512, 512)) - -# Update the app a couple of times to fully load texture/materials -for _ in range(5): - simulation_app.update() - -# Get the local augmentations, either from function or from the registry -rgb_to_bgr_augm = None -gn_depth_augm = None -if USE_WARP: - rgb_to_bgr_augm = rep.annotators.Augmentation.from_function(rgb_to_bgr_wp) - gn_depth_augm = rep.AnnotatorRegistry.get_augmentation("gn_depth_wp") -else: - rgb_to_bgr_augm = rep.annotators.Augmentation.from_function(rgb_to_bgr_np) - gn_depth_augm = rep.AnnotatorRegistry.get_augmentation("gn_depth_np") - -# Output directories -out_dir = os.path.join(os.getcwd(), "_out_augm_annot") -print(f"Writing data to: {out_dir}") -os.makedirs(out_dir, exist_ok=True) - -# Register the annotator together with its augmentation -rep.annotators.register( - name="rgb_to_bgr_augm", - annotator=rep.annotators.augment( - source_annotator=rep.AnnotatorRegistry.get_annotator("rgb"), - augmentation=rgb_to_bgr_augm, - ), -) - -rgb_to_bgr_annot = rep.AnnotatorRegistry.get_annotator("rgb_to_bgr_augm") -depth_annot_1 = rep.AnnotatorRegistry.get_annotator("distance_to_camera") -depth_annot_1.augment(gn_depth_augm) -depth_annot_2 = rep.AnnotatorRegistry.get_annotator("distance_to_camera") -depth_annot_2.augment(gn_depth_augm, sigma=0.5) - -rgb_to_bgr_annot.attach(rp) -depth_annot_1.attach(rp) -depth_annot_2.attach(rp) - - -# Generate a replicator graph to rotate the cube every capture frame -with rep.trigger.on_frame(): - with red_cube: - rep.randomizer.rotation() - -# Evaluate the graph -rep.orchestrator.preview() - -# Measure the duration of capturing the data -start_time = time.time() - -# The `step()` function will trigger the randomization graph, feed annotators with new data, and trigger the writers -for i in range(NUM_FRAMES): - rep.orchestrator.step() - rgb_data = rgb_to_bgr_annot.get_data() - depth_data_1 = depth_annot_1.get_data() - depth_data_2 = depth_annot_2.get_data() - write_rgb(rgb_data, os.path.join(out_dir, f"annot_rgb_{i}")) - write_depth(depth_data_1, os.path.join(out_dir, f"annot_depth_1_{i}")) - write_depth(depth_data_2, os.path.join(out_dir, f"annot_depth_2_{i}")) - -print( - f"The duration for capturing {NUM_FRAMES} frames using '{'warp' if USE_WARP else 'numpy'}' was: {time.time() - start_time:.4f} seconds, with an average of {(time.time() - start_time) / NUM_FRAMES:.4f} seconds per frame." -) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/augmentation/writer_augmentation.py b/simulation/isaac-sim/standalone_examples/replicator/augmentation/writer_augmentation.py deleted file mode 100644 index a1526711e..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/augmentation/writer_augmentation.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -"""Generate augmented synthetic from a writer -""" - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import argparse -import os -import time - -import carb.settings -import numpy as np -import omni.replicator.core as rep -import warp as wp -from isaacsim.core.utils.stage import open_stage -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--num_frames", type=int, default=25, help="The number of frames to capture") -parser.add_argument("--use_warp", action="store_true", help="Use warp augmentations instead of numpy") -args, unknown = parser.parse_known_args() - -NUM_FRAMES = args.num_frames -USE_WARP = args.use_warp -ENV_URL = "/Isaac/Environments/Grid/default_environment.usd" - -# Enable scripts -carb.settings.get_settings().set_bool("/app/omni.graph.scriptnode/opt_in", True) - -# Gaussian noise augmentation on rgba data in numpy (CPU) and warp (GPU) -def gaussian_noise_rgb_np(data_in, sigma: float, seed: int): - np.random.seed(seed) - data_in[:, :, 0] = data_in[:, :, 0] + np.random.randn(*data_in.shape[:-1]) * sigma - data_in[:, :, 1] = data_in[:, :, 1] + np.random.randn(*data_in.shape[:-1]) * sigma - data_in[:, :, 2] = data_in[:, :, 2] + np.random.randn(*data_in.shape[:-1]) * sigma - return data_in - - -@wp.kernel -def gaussian_noise_rgb_wp( - data_in: wp.array3d(dtype=wp.uint8), data_out: wp.array3d(dtype=wp.uint8), sigma: float, seed: int -): - i, j = wp.tid() - state = wp.rand_init(seed, wp.tid()) - data_out[i, j, 0] = wp.uint8(wp.int32(data_in[i, j, 0]) + wp.int32(sigma * wp.randn(state))) - data_out[i, j, 1] = wp.uint8(wp.int32(data_in[i, j, 1]) + wp.int32(sigma * wp.randn(state))) - data_out[i, j, 2] = wp.uint8(wp.int32(data_in[i, j, 2]) + wp.int32(sigma * wp.randn(state))) - data_out[i, j, 3] = data_in[i, j, 3] - - -# Gaussian noise augmentation on depth data in numpy (CPU) and warp (GPU) -def gaussian_noise_depth_np(data_in, sigma: float, seed: int): - np.random.seed(seed) - return data_in + np.random.randn(*data_in.shape) * sigma - - -rep.AnnotatorRegistry.register_augmentation( - "gn_depth_np", rep.annotators.Augmentation.from_function(gaussian_noise_depth_np, sigma=0.1, seed=None) -) - - -@wp.kernel -def gaussian_noise_depth_wp( - data_in: wp.array2d(dtype=wp.float32), data_out: wp.array2d(dtype=wp.float32), sigma: float, seed: int -): - i, j = wp.tid() - state = wp.rand_init(seed, wp.tid()) - data_out[i, j] = data_in[i, j] + sigma * wp.randn(state) - - -rep.AnnotatorRegistry.register_augmentation( - "gn_depth_wp", rep.annotators.Augmentation.from_function(gaussian_noise_depth_wp, sigma=0.1, seed=None) -) - -# Setup the environment -assets_root_path = get_assets_root_path() -open_stage(assets_root_path + ENV_URL) - -# Disable capture on play and async rendering -carb.settings.get_settings().set("/omni/replicator/captureOnPlay", False) -carb.settings.get_settings().set("/omni/replicator/asyncRendering", False) -carb.settings.get_settings().set("/app/asyncRendering", False) - -# Create a red cube and a render product from a camera looking at the cube from the top -red_mat = rep.create.material_omnipbr(diffuse=(1, 0, 0)) -red_cube = rep.create.cube(position=(0, 0, 0.71), material=red_mat) -cam = rep.create.camera(position=(0, 0, 5), look_at=(0, 0, 0)) -rp = rep.create.render_product(cam, (512, 512)) - -# Update the app a couple of times to fully load texture/materials -for _ in range(5): - simulation_app.update() - -# Access default annotators from replicator -rgb_to_hsv_augm = rep.annotators.Augmentation.from_function(rep.augmentations_default.aug_rgb_to_hsv) -hsv_to_rgb_augm = rep.annotators.Augmentation.from_function(rep.augmentations_default.aug_hsv_to_rgb) - -# Access the custom annotators as functions or from the registry -gn_rgb_augm = None -gn_depth_augm = None -if USE_WARP: - gn_rgb_augm = rep.annotators.Augmentation.from_function(gaussian_noise_rgb_wp, sigma=6.0, seed=None) - gn_depth_augm = rep.AnnotatorRegistry.get_augmentation("gn_depth_wp") -else: - gn_rgb_augm = rep.annotators.Augmentation.from_function(gaussian_noise_rgb_np, sigma=6.0, seed=None) - gn_depth_augm = rep.AnnotatorRegistry.get_augmentation("gn_depth_np") - -# Create a writer and apply the augmentations to its corresponding annotators -out_dir = os.path.join(os.getcwd(), "_out_augm_writer") -print(f"Writing data to: {out_dir}") -writer = rep.WriterRegistry.get("BasicWriter") -writer.initialize(output_dir=out_dir, rgb=True, distance_to_camera=True) - -augmented_rgb_annot = rep.annotators.get("rgb").augment_compose( - [rgb_to_hsv_augm, gn_rgb_augm, hsv_to_rgb_augm], name="rgb" -) -writer.add_annotator(augmented_rgb_annot) -writer.augment_annotator("distance_to_camera", gn_depth_augm) - -# Attach render product to writer -writer.attach([rp]) - -# Generate a replicator graph randomizing the cube's rotation every frame -with rep.trigger.on_frame(): - with red_cube: - rep.randomizer.rotation() - -# Evaluate the graph -rep.orchestrator.preview() - -# Measure the duration of capturing the data -start_time = time.time() - -# The `step()` function will trigger the randomization graph, feed annotators with new data, and trigger the writers -for i in range(NUM_FRAMES): - rep.orchestrator.step() - -print( - f"The duration for capturing {NUM_FRAMES} frames using '{'warp' if USE_WARP else 'numpy'}' was: {time.time() - start_time:.4f} seconds, with an average of {(time.time() - start_time) / NUM_FRAMES:.4f} seconds per frame." -) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/infinigen/config/infinigen_multi_writers_pt.yaml b/simulation/isaac-sim/standalone_examples/replicator/infinigen/config/infinigen_multi_writers_pt.yaml deleted file mode 100644 index 4edc144cb..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/infinigen/config/infinigen_multi_writers_pt.yaml +++ /dev/null @@ -1,111 +0,0 @@ -environments: - # List of background environments (list of folders or files) - folders: - - /Isaac/Samples/Replicator/Infinigen/dining_rooms/ - files: [] - -capture: - # Number of captures (frames = total_captures * num_cameras) - total_captures: 12 - # Number of captures per environment before running the simulation (objects in the air) - num_floating_captures_per_env: 2 - # Number of captures per environment after running the simulation (objects dropped) - num_dropped_captures_per_env: 3 - # Number of cameras to capture from (each camera will have a render product attached) - num_cameras: 2 - # Resolution of the captured frames - resolution: [640, 480] - # Disable render products throughout the pipeline, enable them only when capturing the frames - disable_render_products: true - # Number of subframes to render (RayTracedLighting) to avoid temporal rendering artifacts (e.g. ghosting) - rt_subframes: 8 - # Use PathTracing renderer instead of RayTracedLighting when capturing the frames - path_tracing: true - # Offset to avoid the images always being in the image center - camera_look_at_target_offset: 0.1 - # Distance between the camera and the target object - camera_distance_to_target_range: [1.05, 1.25] - # Number of scene lights to create in the working area - num_scene_lights: 4 - -writers: - # Type of the writer to use (e.g. PoseWriter, BasicWriter, etc.) and the kwargs to pass to the writer init - - type: BasicWriter - kwargs: - output_dir: "_out_infinigen_basicwriter_pt" - rgb: true - semantic_segmentation: true - colorize_semantic_segmentation: true - use_common_output_dir: false - - type: DataVisualizationWriter - kwargs: - output_dir: "_out_infinigen_dataviswriter_pt" - bounding_box_2d_tight: true - bounding_box_2d_tight_params: - background: rgb - bounding_box_3d: true - bounding_box_3d_params: - background: normals - -labeled_assets: - # Labeled assets with auto-labeling (e.g. 002_banana -> banana) using regex pattern replacement on the asset name - auto_label: - # Number of labeled assets to create from the given files/folders list - num: 5 - # Chance to disable gravity for the labeled assets (0.0 - all the assets will fall, 1.0 - all the assets will float) - gravity_disabled_chance: 0.25 - # List of folders and files to search for the labeled assets - folders: - - /Isaac/Props/YCB/Axis_Aligned/ - files: - - /Isaac/Props/YCB/Axis_Aligned/036_wood_block.usd - # Regex pattern to replace in the asset name (e.g. "002_banana" -> "banana") - regex_replace_pattern: "^\\d+_" - regex_replace_repl: "" - - # Manually labeled assets with specific labels and properties - manual_label: - - url: /Isaac/Props/YCB/Axis_Aligned/008_pudding_box.usd - label: pudding_box - num: 2 - gravity_disabled_chance: 0.25 - - url: /Isaac/Props/YCB/Axis_Aligned_Physics/006_mustard_bottle.usd - label: mustard_bottle - num: 2 - gravity_disabled_chance: 0.25 - -distractors: - # Shape distractors (unlabeled background assets) to drop in the scene (e.g. capsules, cones, cylinders) - shape_distractors: - # Amount of shape distractors to create - num: 30 - # Chance to disable gravity for the shape distractors - gravity_disabled_chance: 0.25 - # List of shape types to randomly choose from - types: ["capsule", "cone", "cylinder", "sphere", "cube"] - - # Mesh distractors (unlabeled background assets) to drop in the scene - mesh_distractors: - # Amount of mesh distractors to create - num: 10 - # Chance to disable gravity for the mesh distractors - gravity_disabled_chance: 0.25 - # List of folders and files to search to randomly choose from - folders: - - /NVIDIA/Assets/DigitalTwin/Assets/Warehouse/Safety/Floor_Signs/ - - /NVIDIA/Assets/DigitalTwin/Assets/Warehouse/Safety/Cones/ - files: - - /Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxD_04_1847.usd - - /Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxA_01_414.usd - - /Isaac/Environments/Simple_Warehouse/Props/S_TrafficCone.usd - - /Isaac/Environments/Simple_Warehouse/Props/S_WetFloorSign.usd - - /Isaac/Environments/Office/Props/SM_Book_03.usd - - /Isaac/Environments/Office/Props/SM_Book_34.usd - - /Isaac/Environments/Office/Props/SM_BookOpen_01.usd - - /Isaac/Environments/Office/Props/SM_Briefcase.usd - - /Isaac/Environments/Office/Props/SM_Extinguisher.usd - - /Isaac/Environments/Hospital/Props/SM_MedicalBag_01a.usd - - /Isaac/Environments/Hospital/Props/SM_MedicalBox_01g.usd - -# Hide ceiling, move viewport camera to top-down view above the working area -debug_mode: true \ No newline at end of file diff --git a/simulation/isaac-sim/standalone_examples/replicator/infinigen/infinigen_sdg.py b/simulation/isaac-sim/standalone_examples/replicator/infinigen/infinigen_sdg.py deleted file mode 100644 index 8d1efe34f..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/infinigen/infinigen_sdg.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -"""Generate synthetic datasets using infinigen (https://infinigen.org/) generated environments. -""" - - -import argparse -import json -import os - -import yaml -from isaacsim import SimulationApp - -# Default config dict, can be updated/replaced using json/yaml config files ('--config' cli argument) -config = { - "environments": { - # List of background environments (list of folders or files) - "folders": ["/Isaac/Samples/Replicator/Infinigen/dining_rooms/"], - "files": [], - }, - "capture": { - # Number of captures (frames = total_captures * num_cameras) - "total_captures": 15, - # Number of captures per environment before running the simulation (objects in the air) - "num_floating_captures_per_env": 3, - # Number of captures per environment after running the simulation (objects fallen) - "num_dropped_captures_per_env": 4, - # Number of cameras to capture from (each camera will have a render product attached) - "num_cameras": 2, - # Resolution of the captured frames - "resolution": (720, 480), - # Disable render products throughout the piepline, enable them only when capturing the frames - "disable_render_products": False, - # Number of subframes to render (RayTracedLighting) to avoid temporal rendering artifacts (e.g. ghosting) - "rt_subframes": 8, - # Use PathTracing renderer or RayTracedLighting when capturing the frames - "path_tracing": False, - # Offset to avoid the images always being in the image center - "camera_look_at_target_offset": 0.1, - # Distance between the camera and the target object - "camera_distance_to_target_range": (1.15, 1.45), - # Number of scene lights to create in the working area - "num_scene_lights": 3, - }, - "writers": [ - { - # Type of the writer to use (e.g. PoseWriter, BasicWriter, etc.) and the kwargs to pass to the writer init - "type": "PoseWriter", - "kwargs": { - "output_dir": "_out_infinigen_posewriter", - "format": None, - "use_subfolders": True, - "write_debug_images": True, - "skip_empty_frames": False, - }, - } - ], - "labeled_assets": { - # Labeled assets with auto-labeling (e.g. 002_banana -> banana) using regex pattern replacement on the asset name - "auto_label": { - # Number of labeled assets to create from the given files/folders list - "num": 10, - # Chance to disable gravity for the labeled assets (0.0 - all the assets will fall, 1.0 - all the assets will float) - "gravity_disabled_chance": 0.25, - # List of folders and files to search for the labeled assets - "folders": ["/Isaac/Props/YCB/Axis_Aligned/"], - "files": ["/Isaac/Props/YCB/Axis_Aligned/036_wood_block.usd"], - # Regex pattern to replace in the asset name (e.g. "002_banana" -> "banana") - "regex_replace_pattern": r"^\d+_", - "regex_replace_repl": "", - }, - # Manually labeled assets with specific labels and properties - "manual_label": [ - { - "url": "/Isaac/Props/YCB/Axis_Aligned/008_pudding_box.usd", - "label": "pudding_box", - "num": 2, - "gravity_disabled_chance": 0.25, - }, - { - "url": "/Isaac/Props/YCB/Axis_Aligned_Physics/006_mustard_bottle.usd", - "label": "mustard_bottle", - "num": 2, - "gravity_disabled_chance": 0.25, - }, - ], - }, - "distractors": { - # Shape distractors (unlabeled background assets) to drop in the scene (e.g. capsules, cones, cylinders) - "shape_distractors": { - # Amount of shape distractors to create - "num": 20, - # Chance to disable gravity for the shape distractors - "gravity_disabled_chance": 0.25, - # List of shape types to randomly choose from - "types": ["capsule", "cone", "cylinder", "sphere", "cube"], - }, - # Mesh distractors (unlabeled background assets) to drop in the scene - "mesh_distractors": { - # Amount of mesh distractors to create - "num": 10, - # Chance to disable gravity for the mesh distractors - "gravity_disabled_chance": 0.25, - # List of folders and files to search to randomly choose from - "folders": [ - "/NVIDIA/Assets/DigitalTwin/Assets/Warehouse/Safety/Floor_Signs/", - "/NVIDIA/Assets/DigitalTwin/Assets/Warehouse/Safety/Cones/", - ], - "files": [ - "/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxD_04_1847.usd", - "/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxA_01_414.usd", - "/Isaac/Environments/Simple_Warehouse/Props/S_TrafficCone.usd", - "/Isaac/Environments/Simple_Warehouse/Props/S_WetFloorSign.usd", - "/Isaac/Environments/Office/Props/SM_Book_03.usd", - "/Isaac/Environments/Office/Props/SM_Book_34.usd", - "/Isaac/Environments/Office/Props/SM_BookOpen_01.usd", - "/Isaac/Environments/Office/Props/SM_Briefcase.usd", - "/Isaac/Environments/Office/Props/SM_Extinguisher.usd", - "/Isaac/Environments/Hospital/Props/SM_MedicalBag_01a.usd", - "/Isaac/Environments/Hospital/Props/SM_MedicalBox_01g.usd", - ], - }, - }, - # Hide ceilling to get a top-down view of the scene, move viewport camera to the top-down view - "debug_mode": True, -} - - -# Check if there are any config files (yaml or json) are passed as arguments -parser = argparse.ArgumentParser() -parser.add_argument("--config", required=False, help="Include specific config parameters (json or yaml))") -parser.add_argument( - "--close-on-completion", action="store_true", help="Ensure the app closes on completion even in debug mode" -) -args, unknown = parser.parse_known_args() -args_config = {} -if args.config and os.path.isfile(args.config): - with open(args.config, "r") as f: - if args.config.endswith(".json"): - args_config = json.load(f) - elif args.config.endswith(".yaml"): - args_config = yaml.safe_load(f) - else: - print(f"[SDG-Infinigen] Config file {args.config} is not json or yaml, will use default config") -else: - print(f"[SDG-Infinigen] Config file {args.config} does not exist, will use default config") - -# Update the default config dict with the external one -config.update(args_config) - -simulation_app = SimulationApp(launch_config={"headless": False}) - - -import random -from itertools import cycle - -import carb.settings -import infinigen_sdg_utils as infinigen_utils -import numpy as np -import omni.client -import omni.kit -import omni.kit.app -import omni.physx -import omni.replicator.core as rep -import omni.timeline -import omni.usd -from isaacsim.core.utils.viewports import set_camera_view - - -# Run the SDG pipeline on the scenarios -def run_sdg(config): - # Load the config parameters - env_config = config.get("environments", {}) - env_urls = infinigen_utils.get_usd_paths( - files=env_config.get("files", []), folders=env_config.get("folders", []), skip_folder_keywords=[".thumbs"] - ) - capture_config = config.get("capture", {}) - writers_config = config.get("writers", {}) - labeled_assets_config = config.get("labeled_assets", {}) - distractors_config = config.get("distractors", {}) - - # Create a new stage - print(f"[SDG-Infinigen] Creating a new stage") - omni.usd.get_context().new_stage() - stage = omni.usd.get_context().get_stage() - - # Disable capture on play - rep.orchestrator.set_capture_on_play(False) - - # Disable UJITSO cooking ([Warning] [omni.ujitso] UJITSO : Build storage validation failed) - carb.settings.get_settings().set("/physics/cooking/ujitsoCollisionCooking", False) - - # Debug mode (hide ceiling, move viewport camera to the top-down view) - debug_mode = config.get("debug_mode", False) - - # Create the cameras - cameras = [] - num_cameras = capture_config.get("num_cameras", 0) - for i in range(num_cameras): - cam_prim = stage.DefinePrim(f"/Cameras/cam_{i}", "Camera") - cam_prim.GetAttribute("clippingRange").Set((0.25, 1000)) - cameras.append(cam_prim) - print(f"[SDG-Infinigen] Created {len(cameras)} cameras") - - # Create the render products for the cameras - render_products = [] - resolution = capture_config.get("resolution", (1280, 720)) - disable_render_products = capture_config.get("disable_render_products", False) - for cam in cameras: - rp = rep.create.render_product(cam.GetPath(), resolution, name=f"rp_{cam.GetName()}") - if disable_render_products: - rp.hydra_texture.set_updates_enabled(False) - render_products.append(rp) - print(f"[SDG-Infinigen] Created {len(render_products)} render products") - - # Only create the writers if there are render products to attach to - writers = [] - if render_products: - for writer_config in writers_config: - writer = infinigen_utils.setup_writer(writer_config) - if writer: - writer.attach(render_products) - writers.append(writer) - print(f"\t {writer_config['type']}'s out dir: {writer_config.get('kwargs', {}).get('output_dir', '')}") - print(f"[SDG-Infinigen] Created {len(writers)} writers") - - # Load target assets with auto-labeling (e.g. 002_banana -> banana) - auto_label_config = labeled_assets_config.get("auto_label", {}) - auto_floating_assets, auto_falling_assets = infinigen_utils.load_auto_labeled_assets(auto_label_config) - print(f"[SDG-Infinigen] Loaded {len(auto_floating_assets)} floating auto-labeled assets") - print(f"[SDG-Infinigen] Loaded {len(auto_falling_assets)} falling auto-labeled assets") - - # Load target assets with manual labels - manual_label_config = labeled_assets_config.get("manual_label", []) - manual_floating_assets, manual_falling_assets = infinigen_utils.load_manual_labeled_assets(manual_label_config) - print(f"[SDG-Infinigen] Loaded {len(manual_floating_assets)} floating manual-labeled assets") - print(f"[SDG-Infinigen] Loaded {len(manual_falling_assets)} falling manual-labeled assets") - target_assets = auto_floating_assets + auto_falling_assets + manual_floating_assets + manual_falling_assets - - # Load the shape distractors - shape_distractors_config = distractors_config.get("shape_distractors", {}) - floating_shapes, falling_shapes = infinigen_utils.load_shape_distractors(shape_distractors_config) - print(f"[SDG-Infinigen] Loaded {len(floating_shapes)} floating shape distractors") - print(f"[SDG-Infinigen] Loaded {len(falling_shapes)} falling shape distractors") - shape_distractors = floating_shapes + falling_shapes - - # Load the mesh distractors - mesh_distractors_config = distractors_config.get("mesh_distractors", {}) - floating_meshes, falling_meshes = infinigen_utils.load_mesh_distractors(mesh_distractors_config) - print(f"[SDG-Infinigen] Loaded {len(floating_meshes)} floating mesh distractors") - print(f"[SDG-Infinigen] Loaded {len(falling_meshes)} falling mesh distractors") - mesh_distractors = floating_meshes + falling_meshes - - # Resolve any centimeter-meter scale issues of the assets - infinigen_utils.resolve_scale_issues_with_metrics_assembler() - - # Create lights to randomize in the working area - scene_lights = [] - num_scene_lights = capture_config.get("num_scene_lights", 0) - for i in range(num_scene_lights): - light_prim = stage.DefinePrim(f"/Lights/SphereLight_scene_{i}", "SphereLight") - scene_lights.append(light_prim) - print(f"[SDG-Infinigen] Created {len(scene_lights)} scene lights") - - # Register replicator randomizers and trigger them once - print(f"[SDG-Infinigen] Registering replicator graph randomizers") - infinigen_utils.register_dome_light_randomizer() - infinigen_utils.register_shape_distractors_color_randomizer(shape_distractors) - - # Check if the render mode needs to be switched to path tracing for the capture (by default: RayTracedLighting) - use_path_tracing = capture_config.get("path_tracing", False) - - # Capture detail using subframes (https://docs.omniverse.nvidia.com/extensions/latest/ext_replicator/subframes_examples.html) - rt_subframes = capture_config.get("rt_subframes", 3) - - # Min and max distance between the camera and the target object - camera_distance_to_target_range = capture_config.get("camera_distance_to_target_range", (0.5, 1.5)) - - # Number of captures (frames = total_captures * num_cameras) - # NOTE: if captured frames have no labeled data, they can be skipped (e.g. PoseWriter with skip_empty_frames=True) - total_captures = capture_config.get("total_captures", 0) - - # Number of captures per environment with the objects in the air or dropped - num_floating_captures_per_env = capture_config.get("num_floating_captures_per_env", 0) - num_dropped_captures_per_env = capture_config.get("num_dropped_captures_per_env", 0) - - # Start the SDG loop - env_cycle = cycle(env_urls) - capture_counter = 0 - while capture_counter < total_captures: - # Load the next environment - env_url = next(env_cycle) - - # Load the new environment - print(f"[SDG-Infinigen] Loading environment: {env_url}") - infinigen_utils.load_env(env_url, prim_path="/Environment") - - # Setup the environment (add collision, fix lights, etc.) and update the app once to apply the changes - print(f"[SDG-Infinigen] Setting up the environment") - infinigen_utils.setup_env(root_path="/Environment", hide_top_walls=debug_mode) - simulation_app.update() - - # Get the location of the prim above which the assets will be randomized - working_area_loc = infinigen_utils.get_matching_prim_location( - match_string="TableDining", root_path="/Environment" - ) - - # Move viewport above the working area to get a top-down view of the scene - if debug_mode: - camera_loc = (working_area_loc[0], working_area_loc[1], working_area_loc[2] + 10) - set_camera_view(eye=np.array(camera_loc), target=np.array(working_area_loc)) - - # Get the spawn areas as offseted location ranges from the working area (min_x, min_y, min_z, max_x, max_y, max_z) - print(f"\tRandomizing {len(target_assets)} target assets around the working area") - target_loc_range = infinigen_utils.offset_range((-0.5, -0.5, 1, 0.5, 0.5, 1.5), working_area_loc) - infinigen_utils.randomize_poses( - target_assets, - location_range=target_loc_range, - rotation_range=(0, 360), - scale_range=(0.95, 1.15), - ) - - # Mesh distractors - print(f"\tRandomizing {len(mesh_distractors)} mesh distractors around the working area") - mesh_loc_range = infinigen_utils.offset_range((-1, -1, 1, 1, 1, 2), working_area_loc) - infinigen_utils.randomize_poses( - mesh_distractors, - location_range=mesh_loc_range, - rotation_range=(0, 360), - scale_range=(0.3, 1.0), - ) - - # Shape distractors - print(f"\tRandomizing {len(shape_distractors)} shape distractors around the working area") - shape_loc_range = infinigen_utils.offset_range((-1.5, -1.5, 1, 1.5, 1.5, 2), working_area_loc) - infinigen_utils.randomize_poses( - shape_distractors, - location_range=shape_loc_range, - rotation_range=(0, 360), - scale_range=(0.01, 0.1), - ) - - print(f"\tRandomizing {len(scene_lights)} scene lights properties and locations around the working area") - lights_loc_range = infinigen_utils.offset_range((-2, -2, 1, 2, 2, 3), working_area_loc) - infinigen_utils.randomize_lights( - scene_lights, - location_range=lights_loc_range, - intensity_range=(500, 2500), - color_range=(0.1, 0.1, 0.1, 0.9, 0.9, 0.9), - ) - - print(f"\tRandomizing dome lights") - rep.utils.send_og_event(event_name="randomize_dome_lights") - - print(f"\tRandomizing shape distractor colors") - rep.utils.send_og_event(event_name="randomize_shape_distractor_colors") - - # Run the physics simulation for a few frames to solve any collisions - print(f"\tFixing collisions through physics simulation") - simulation_app.update() - infinigen_utils.run_simulation(num_frames=4, render=True) - - # Check if the render products need to be enabled for the capture - if disable_render_products: - for rp in render_products: - rp.hydra_texture.set_updates_enabled(True) - - # Check if the render mode needs to be switched to path tracing for the capture - if use_path_tracing: - print(f"\tSwitching to PathTracing render mode") - carb.settings.get_settings().set("/rtx/rendermode", "PathTracing") - - # Capture frames with the objects in the air - for i in range(num_floating_captures_per_env): - # Check if the total captures have been reached - if capture_counter >= total_captures: - break - # Randomize the camera poses - print(f"\tRandomizing {len(cameras)} camera poses") - infinigen_utils.randomize_camera_poses( - cameras, target_assets, camera_distance_to_target_range, polar_angle_range=(0, 75) - ) - print( - f"\tCapturing floating assets {i+1}/{num_floating_captures_per_env}; total captures: {capture_counter+1}/{total_captures};" - ) - rep.orchestrator.step(rt_subframes=rt_subframes, delta_time=0.0) - capture_counter += 1 - - # Check if the render products need to be disabled until the next capture - if disable_render_products: - for rp in render_products: - rp.hydra_texture.set_updates_enabled(False) - - # Check if the render mode needs to be switched back to raytracing until the next capture - if use_path_tracing: - carb.settings.get_settings().set("/rtx/rendermode", "RayTracedLighting") - - print(f"\tRunning the simulation") - infinigen_utils.run_simulation(num_frames=200, render=False) - - # Check if the render products need to be enabled for the capture - if disable_render_products: - for rp in render_products: - rp.hydra_texture.set_updates_enabled(True) - - # Check if the render mode needs to be switched to path tracing for the capture - if use_path_tracing: - carb.settings.get_settings().set("/rtx/rendermode", "PathTracing") - - for i in range(num_dropped_captures_per_env): - # Check if the total captures have been reached - if capture_counter >= total_captures: - break - # Spawn the cameras with a smaller polar angle to have mostly a top-down view of the objects - print(f"\tRandomizing camera poses") - infinigen_utils.randomize_camera_poses( - cameras, target_assets, distance_range=camera_distance_to_target_range, polar_angle_range=(0, 45) - ) - print( - f"\tCapturing dropped assets {i+1}/{num_dropped_captures_per_env}; total captures: {capture_counter+1}/{total_captures};" - ) - rep.orchestrator.step(rt_subframes=rt_subframes, delta_time=0.0) - capture_counter += 1 - - # Check if the render products need to be disabled until the next capture - if disable_render_products: - for rp in render_products: - rp.hydra_texture.set_updates_enabled(False) - - # Check if the render mode needs to be switched back to raytracing until the next capture - if use_path_tracing: - carb.settings.get_settings().set("/rtx/rendermode", "RayTracedLighting") - - # Wait until the data is written to the disk - rep.orchestrator.wait_until_complete() - - # Detach the writers - print(f"[SDG-Infinigen] Detaching writers") - for writer in writers: - writer.detach() - - # Destroy render products - print(f"[SDG-Infinigen] Destroying render products") - for rp in render_products: - rp.destroy() - - print(f"[SDG-Infinigen] SDG Finished, captured {capture_counter * num_cameras} frames..") - - -# Check if debug mode is enabled -debug_mode = config.get("debug_mode", False) - -if debug_mode: - np.random.seed(10) - random.seed(10) - rep.set_global_seed(10) - -# Start the SDG pipeline -print(f"[SDG-Infinigen] Starting the SDG pipeline.") -run_sdg(config) -print(f"[SDG-Infinigen] SDG pipeline finished.") - -# Make sure the app closes on completion even if in debug mode -if args.close_on_completion: - simulation_app.close() - -# In debug mode, keep the app running until manually closed -if debug_mode: - while simulation_app.is_running(): - simulation_app.update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/infinigen/infinigen_sdg_utils.py b/simulation/isaac-sim/standalone_examples/replicator/infinigen/infinigen_sdg_utils.py deleted file mode 100644 index 149b23927..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/infinigen/infinigen_sdg_utils.py +++ /dev/null @@ -1,661 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import math -import os -import random -import re -from itertools import chain - -import omni.kit.app -import omni.kit.commands -import omni.physx -import omni.replicator.core as rep -import omni.timeline -import omni.usd -from isaacsim.core.utils.semantics import add_update_semantics, remove_all_semantics -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf, PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics - - -def set_transform_attributes( - prim: Usd.Prim, - location: Gf.Vec3d | None = None, - orientation: Gf.Quatf | None = None, - rotation: Gf.Vec3f | None = None, - scale: Gf.Vec3f | None = None, -) -> None: - """Set transformation attributes (location, orientation, rotation, scale) on a prim.""" - if location is not None: - if not prim.HasAttribute("xformOp:translate"): - UsdGeom.Xformable(prim).AddTranslateOp() - prim.GetAttribute("xformOp:translate").Set(location) - if orientation is not None: - if not prim.HasAttribute("xformOp:orient"): - UsdGeom.Xformable(prim).AddOrientOp() - prim.GetAttribute("xformOp:orient").Set(orientation) - if rotation is not None: - if not prim.HasAttribute("xformOp:rotateXYZ"): - UsdGeom.Xformable(prim).AddRotateXYZOp() - prim.GetAttribute("xformOp:rotateXYZ").Set(rotation) - if scale is not None: - if not prim.HasAttribute("xformOp:scale"): - UsdGeom.Xformable(prim).AddScaleOp() - prim.GetAttribute("xformOp:scale").Set(scale) - - -def add_colliders(root_prim: Usd.Prim, approximation_type: str = "convexHull") -> None: - """Add collision attributes to mesh and geometry primitives under the root prim.""" - for desc_prim in Usd.PrimRange(root_prim): - if desc_prim.IsA(UsdGeom.Gprim): - if not desc_prim.HasAPI(UsdPhysics.CollisionAPI): - collision_api = UsdPhysics.CollisionAPI.Apply(desc_prim) - else: - collision_api = UsdPhysics.CollisionAPI(desc_prim) - collision_api.CreateCollisionEnabledAttr(True) - - if desc_prim.IsA(UsdGeom.Mesh): - if not desc_prim.HasAPI(UsdPhysics.MeshCollisionAPI): - mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(desc_prim) - else: - mesh_collision_api = UsdPhysics.MeshCollisionAPI(desc_prim) - mesh_collision_api.CreateApproximationAttr().Set(approximation_type) - - -def has_colliders(root_prim: Usd.Prim) -> bool: - """Check if any descendant prims under the root prim have collision attributes.""" - for desc_prim in Usd.PrimRange(root_prim): - if desc_prim.HasAPI(UsdPhysics.CollisionAPI): - return True - return False - - -def add_rigid_body_dynamics(prim: Usd.Prim, disable_gravity: bool = False) -> None: - """Add rigid body dynamics properties to a prim if it has colliders, with optional gravity setting.""" - if has_colliders(prim): - if not prim.HasAPI(UsdPhysics.RigidBodyAPI): - rigid_body_api = UsdPhysics.RigidBodyAPI.Apply(prim) - else: - rigid_body_api = UsdPhysics.RigidBodyAPI(prim) - rigid_body_api.CreateRigidBodyEnabledAttr(True) - - # Apply PhysX rigid body dynamics - if not prim.HasAPI(PhysxSchema.PhysxRigidBodyAPI): - physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI.Apply(prim) - else: - physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI(prim) - physx_rigid_body_api.GetDisableGravityAttr().Set(disable_gravity) - else: - print( - f"[SDG-Infinigen] Prim '{prim.GetPath()}' has no colliders. Skipping adding rigid body dynamics properties." - ) - - -def add_colliders_and_rigid_body_dynamics(prim: Usd.Prim, disable_gravity: bool = False) -> None: - """Add colliders and rigid body dynamics properties to a prim, with optional gravity setting.""" - add_colliders(prim) - add_rigid_body_dynamics(prim, disable_gravity) - - -def get_random_pose_on_sphere( - origin: tuple[float, float, float], - radius_range: tuple[float, float], - polar_angle_range: tuple[float, float], - camera_forward_axis: tuple[float, float, float] = (0, 0, -1), -) -> tuple[Gf.Vec3d, Gf.Quatf]: - """Generate a random pose on a sphere looking at the origin, with specified radius and polar angle ranges.""" - # https://docs.omniverse.nvidia.com/isaacsim/latest/reference_conventions.html - # Convert degrees to radians for polar angles (theta) - polar_angle_min_rad = math.radians(polar_angle_range[0]) - polar_angle_max_rad = math.radians(polar_angle_range[1]) - - # Generate random spherical coordinates - radius = random.uniform(radius_range[0], radius_range[1]) - polar_angle = random.uniform(polar_angle_min_rad, polar_angle_max_rad) - azimuthal_angle = random.uniform(0, 2 * math.pi) - - # Convert spherical coordinates to Cartesian coordinates - x = radius * math.sin(polar_angle) * math.cos(azimuthal_angle) - y = radius * math.sin(polar_angle) * math.sin(azimuthal_angle) - z = radius * math.cos(polar_angle) - - # Calculate the location in 3D space - location = Gf.Vec3d(origin[0] + x, origin[1] + y, origin[2] + z) - - # Calculate direction vector from camera to look_at point - direction = Gf.Vec3d(origin) - location - direction_normalized = direction.GetNormalized() - - # Calculate rotation from forward direction (rotateFrom) to direction vector (rotateTo) - rotation = Gf.Rotation(Gf.Vec3d(camera_forward_axis), direction_normalized) - orientation = Gf.Quatf(rotation.GetQuat()) - - return location, orientation - - -def randomize_camera_poses( - cameras: list[Usd.Prim], - targets: list[Usd.Prim], - distance_range: tuple[float, float], - polar_angle_range: tuple[float, float] = (0, 180), - look_at_offset: tuple[float, float] = (-0.1, 0.1), -) -> None: - """Randomize the poses of cameras to look at random targets with adjustable distance and offset.""" - for cam in cameras: - # Get a random target asset to look at - target_asset = random.choice(targets) - - # Add a look_at offset so the target is not always in the center of the camera view - target_loc = target_asset.GetAttribute("xformOp:translate").Get() - target_loc = ( - target_loc[0] + random.uniform(look_at_offset[0], look_at_offset[1]), - target_loc[1] + random.uniform(look_at_offset[0], look_at_offset[1]), - target_loc[2] + random.uniform(look_at_offset[0], look_at_offset[1]), - ) - - # Generate random camera pose - loc, quat = get_random_pose_on_sphere(target_loc, distance_range, polar_angle_range) - - # Set the camera's transform attributes to the generated location and orientation - set_transform_attributes(cam, location=loc, orientation=quat) - - -def get_usd_paths_from_folder( - folder_path: str, recursive: bool = True, usd_paths: list[str] = None, skip_keywords: list[str] = None -) -> list[str]: - """Retrieve USD file paths from a folder, optionally searching recursively and filtering by keywords.""" - if usd_paths is None: - usd_paths = [] - skip_keywords = skip_keywords or [] - - # Make sure the omni.client extension is enabled - import omni.kit.app - - ext_manager = omni.kit.app.get_app().get_extension_manager() - if not ext_manager.is_extension_enabled("omni.client"): - ext_manager.set_extension_enabled_immediate("omni.client", True) - import omni.client - - result, entries = omni.client.list(folder_path) - if result != omni.client.Result.OK: - print(f"[SDG-Infinigen] Could not list assets in path: {folder_path}") - return usd_paths - - for entry in entries: - if any(keyword.lower() in entry.relative_path.lower() for keyword in skip_keywords): - continue - _, ext = os.path.splitext(entry.relative_path) - if ext in [".usd", ".usda", ".usdc"]: - path_posix = os.path.join(folder_path, entry.relative_path).replace("\\", "/") - usd_paths.append(path_posix) - elif recursive and entry.flags & omni.client.ItemFlags.CAN_HAVE_CHILDREN: - sub_folder = os.path.join(folder_path, entry.relative_path).replace("\\", "/") - get_usd_paths_from_folder(sub_folder, recursive=recursive, usd_paths=usd_paths, skip_keywords=skip_keywords) - - return usd_paths - - -def get_usd_paths( - files: list[str] = None, folders: list[str] = None, skip_folder_keywords: list[str] = None -) -> list[str]: - """Retrieve USD paths from specified files and folders, optionally filtering out specific folder keywords.""" - files = files or [] - folders = folders or [] - skip_folder_keywords = skip_folder_keywords or [] - - assets_root_path = get_assets_root_path() - env_paths = [] - - for file_path in files: - file_path = ( - file_path - if file_path.startswith(("omniverse://", "http://", "https://", "file://")) - else assets_root_path + file_path - ) - env_paths.append(file_path) - - for folder_path in folders: - folder_path = ( - folder_path - if folder_path.startswith(("omniverse://", "http://", "https://", "file://")) - else assets_root_path + folder_path - ) - env_paths.extend(get_usd_paths_from_folder(folder_path, recursive=True, skip_keywords=skip_folder_keywords)) - - return env_paths - - -def load_env(usd_path: str, prim_path: str, remove_existing: bool = True) -> Usd.Prim: - """Load an environment from a USD file into the stage at the specified prim path, optionally removing any existing prim.""" - stage = omni.usd.get_context().get_stage() - - # Remove existing prim if specified - if remove_existing and stage.GetPrimAtPath(prim_path): - omni.kit.commands.execute("DeletePrimsCommand", paths=[prim_path]) - - root_prim = add_reference_to_stage(usd_path=usd_path, prim_path=prim_path) - return root_prim - - -def add_colliders_to_env(root_path: str | None = None, approximation_type: str = "none") -> None: - """Add colliders to all mesh prims within the specified root path in the stage.""" - stage = omni.usd.get_context().get_stage() - prim = stage.GetPseudoRoot() if root_path is None else stage.GetPrimAtPath(root_path) - - for prim in Usd.PrimRange(prim): - if prim.IsA(UsdGeom.Mesh): - add_colliders(prim, approximation_type) - - -def find_matching_prims( - match_strings: list[str], root_path: str | None = None, prim_type: str | None = None, first_match_only: bool = False -) -> Usd.Prim | list[Usd.Prim] | None: - """Find prims matching specified strings, with optional type filtering and single match return.""" - stage = omni.usd.get_context().get_stage() - root_prim = stage.GetPseudoRoot() if root_path is None else stage.GetPrimAtPath(root_path) - - matching_prims = [] - for prim in Usd.PrimRange(root_prim): - if any(match in str(prim.GetPath()) for match in match_strings): - if prim_type is None or prim.GetTypeName() == prim_type: - if first_match_only: - return prim - matching_prims.append(prim) - - return matching_prims if not first_match_only else None - - -def hide_matching_prims(match_strings: list[str], root_path: str | None = None, prim_type: str | None = None) -> None: - """Set visibility of prims matching specified strings to 'invisible' within the root path.""" - stage = omni.usd.get_context().get_stage() - root_prim = stage.GetPseudoRoot() if root_path is None else stage.GetPrimAtPath(root_path) - - for prim in Usd.PrimRange(root_prim): - if prim_type is None or prim.GetTypeName() == prim_type: - if any(match in str(prim.GetPath()) for match in match_strings): - prim.GetAttribute("visibility").Set("invisible") - - -def setup_env(root_path: str | None = None, approximation_type: str = "none", hide_top_walls: bool = False) -> None: - """Set up the environment with colliders, ceiling light adjustments, and optional top wall hiding.""" - # Fix ceiling lights: meshes are blocking the light and need to be set to invisible - ceiling_light_meshes = find_matching_prims(["001_SPLIT_GLA"], root_path, "Xform") - for light_mesh in ceiling_light_meshes: - light_mesh.GetAttribute("visibility").Set("invisible") - - # Hide ceiling light meshes for lighting fix - hide_matching_prims(["001_SPLIT_GLA"], root_path, "Xform") - - # Hide top walls for better debug view, if specified - if hide_top_walls: - hide_matching_prims(["_exterior", "_ceiling"], root_path) - - # Add colliders to the environment - add_colliders_to_env(root_path, approximation_type) - - # Fix dining table collision by setting it to a bounding cube approximation - table_prim = find_matching_prims( - match_strings=["TableDining"], root_path=root_path, prim_type="Xform", first_match_only=True - ) - if table_prim is not None: - add_colliders(table_prim, approximation_type="boundingCube") - else: - print("[SDG-Infinigen] Could not find dining table prim in the environment.") - - -def create_shape_distractors( - num_distractors: int, shape_types: list[str], root_path: str, gravity_disabled_chance: float -) -> tuple[list[Usd.Prim], list[Usd.Prim]]: - """Create shape distractors with optional gravity settings, returning lists of floating and falling shapes.""" - stage = omni.usd.get_context().get_stage() - floating_shapes = [] - falling_shapes = [] - for _ in range(num_distractors): - rand_shape = random.choice(shape_types) - disable_gravity = random.random() < gravity_disabled_chance - name_prefix = "floating_" if disable_gravity else "falling_" - prim_path = omni.usd.get_stage_next_free_path(stage, f"{root_path}/{name_prefix}{rand_shape}", False) - prim = stage.DefinePrim(prim_path, rand_shape.capitalize()) - add_colliders_and_rigid_body_dynamics(prim, disable_gravity=disable_gravity) - (floating_shapes if disable_gravity else falling_shapes).append(prim) - return floating_shapes, falling_shapes - - -def load_shape_distractors(shape_distractors_config: dict) -> tuple[list[Usd.Prim], list[Usd.Prim]]: - """Load shape distractors based on configuration, returning lists of floating and falling shapes.""" - num_shapes = shape_distractors_config.get("num", 0) - shape_types = shape_distractors_config.get("shape_types", ["capsule", "cone", "cylinder", "sphere", "cube"]) - shape_gravity_disabled_chance = shape_distractors_config.get("gravity_disabled_chance", 0.0) - return create_shape_distractors(num_shapes, shape_types, "/Distractors", shape_gravity_disabled_chance) - - -def create_mesh_distractors( - num_distractors: int, mesh_urls: list[str], root_path: str, gravity_disabled_chance: float -) -> tuple[list[Usd.Prim], list[Usd.Prim]]: - """Create mesh distractors from specified URLs with optional gravity settings.""" - stage = omni.usd.get_context().get_stage() - floating_meshes = [] - falling_meshes = [] - for _ in range(num_distractors): - rand_mesh_url = random.choice(mesh_urls) - disable_gravity = random.random() < gravity_disabled_chance - name_prefix = "floating_" if disable_gravity else "falling_" - prim_name = os.path.basename(rand_mesh_url).split(".")[0] - prim_path = omni.usd.get_stage_next_free_path(stage, f"{root_path}/{name_prefix}{prim_name}", False) - try: - prim = add_reference_to_stage(usd_path=rand_mesh_url, prim_path=prim_path) - except Exception as e: - print(f"[SDG-Infinigen] Failed to load mesh distractor reference {rand_mesh_url} with exception: {e}") - continue - add_colliders_and_rigid_body_dynamics(prim, disable_gravity=disable_gravity) - (floating_meshes if disable_gravity else falling_meshes).append(prim) - return floating_meshes, falling_meshes - - -def load_mesh_distractors(mesh_distractors_config: dict) -> tuple[list[Usd.Prim], list[Usd.Prim]]: - """Load mesh distractors based on configuration, returning lists of floating and falling meshes.""" - num_meshes = mesh_distractors_config.get("num", 0) - mesh_gravity_disabled_chance = mesh_distractors_config.get("gravity_disabled_chance", 0.0) - mesh_folders = mesh_distractors_config.get("folders", []) - mesh_files = mesh_distractors_config.get("files", []) - mesh_urls = get_usd_paths( - files=mesh_files, folders=mesh_folders, skip_folder_keywords=["material", "texture", ".thumbs"] - ) - floating_meshes, falling_meshes = create_mesh_distractors( - num_meshes, mesh_urls, "/Distractors", mesh_gravity_disabled_chance - ) - for prim in chain(floating_meshes, falling_meshes): - remove_all_semantics(prim, recursive=True) - return floating_meshes, falling_meshes - - -def create_auto_labeled_assets( - num_assets: int, - asset_urls: list[str], - root_path: str, - regex_replace_pattern: str, - regex_replace_repl: str, - gravity_disabled_chance: float, -) -> tuple[list[Usd.Prim], list[Usd.Prim]]: - """Create assets with automatic labels, applying optional gravity settings.""" - stage = omni.usd.get_context().get_stage() - floating_assets = [] - falling_assets = [] - for _ in range(num_assets): - asset_url = random.choice(asset_urls) - disable_gravity = random.random() < gravity_disabled_chance - name_prefix = "floating_" if disable_gravity else "falling_" - basename = os.path.basename(asset_url) - name_without_ext = os.path.splitext(basename)[0] - label = re.sub(regex_replace_pattern, regex_replace_repl, name_without_ext) - prim_path = omni.usd.get_stage_next_free_path(stage, f"{root_path}/{name_prefix}{label}", False) - try: - prim = add_reference_to_stage(usd_path=asset_url, prim_path=prim_path) - except Exception as e: - print(f"[SDG-Infinigen] Failed to load mesh distractor reference {asset_url} with exception: {e}") - continue - add_colliders_and_rigid_body_dynamics(prim, disable_gravity=disable_gravity) - remove_all_semantics(prim, recursive=True) - add_update_semantics(prim, label) - (floating_assets if disable_gravity else falling_assets).append(prim) - return floating_assets, falling_assets - - -def load_auto_labeled_assets(auto_label_config: dict) -> tuple[list[Usd.Prim], list[Usd.Prim]]: - """Load auto-labeled assets based on configuration, returning lists of floating and falling assets.""" - num_assets = auto_label_config.get("num", 0) - gravity_disabled_chance = auto_label_config.get("gravity_disabled_chance", 0.0) - assets_files = auto_label_config.get("files", []) - assets_folders = auto_label_config.get("folders", []) - assets_urls = get_usd_paths( - files=assets_files, folders=assets_folders, skip_folder_keywords=["material", "texture", ".thumbs"] - ) - regex_replace_pattern = auto_label_config.get("regex_replace_pattern", "") - regex_replace_repl = auto_label_config.get("regex_replace_repl", "") - return create_auto_labeled_assets( - num_assets, - assets_urls, - "/Assets", - regex_replace_pattern, - regex_replace_repl, - gravity_disabled_chance, - ) - - -def create_labeled_assets( - num_assets: int, asset_url: str, label: str, root_path: str, gravity_disabled_chance: float -) -> tuple[list[Usd.Prim], list[Usd.Prim]]: - """Create labeled assets with optional gravity settings, returning lists of floating and falling assets.""" - stage = omni.usd.get_context().get_stage() - assets_root_path = get_assets_root_path() - asset_url = ( - asset_url - if asset_url.startswith(("omniverse://", "http://", "https://", "file://")) - else assets_root_path + asset_url - ) - floating_assets = [] - falling_assets = [] - for _ in range(num_assets): - disable_gravity = random.random() < gravity_disabled_chance - name_prefix = "floating_" if disable_gravity else "falling_" - prim_path = omni.usd.get_stage_next_free_path(stage, f"{root_path}/{name_prefix}{label}", False) - - prim = add_reference_to_stage(usd_path=asset_url, prim_path=prim_path) - add_colliders_and_rigid_body_dynamics(prim, disable_gravity=disable_gravity) - remove_all_semantics(prim, recursive=True) - add_update_semantics(prim, label) - (floating_assets if disable_gravity else falling_assets).append(prim) - return floating_assets, falling_assets - - -def load_manual_labeled_assets(manual_labeled_assets_config: list[dict]) -> tuple[list[Usd.Prim], list[Usd.Prim]]: - """Load manually labeled assets based on configuration, returning lists of floating and falling assets.""" - labeled_floating_assets = [] - labeled_falling_assets = [] - for labeled_asset_config in manual_labeled_assets_config: - asset_url = labeled_asset_config.get("url", "") - asset_label = labeled_asset_config.get("label", "") - num_assets = labeled_asset_config.get("num", 0) - gravity_disabled_chance = labeled_asset_config.get("gravity_disabled_chance", 0.0) - floating_assets, falling_assets = create_labeled_assets( - num_assets, - asset_url, - asset_label, - "/Assets", - gravity_disabled_chance, - ) - labeled_floating_assets.extend(floating_assets) - labeled_falling_assets.extend(falling_assets) - return labeled_floating_assets, labeled_falling_assets - - -def resolve_scale_issues_with_metrics_assembler() -> None: - """Enable and execute metrics assembler to resolve scale issues in the stage.""" - import omni.kit.app - - ext_manager = omni.kit.app.get_app().get_extension_manager() - if not ext_manager.is_extension_enabled("omni.usd.metrics.assembler"): - ext_manager.set_extension_enabled_immediate("omni.usd.metrics.assembler", True) - from omni.metrics.assembler.core import get_metrics_assembler_interface - - stage_id = omni.usd.get_context().get_stage_id() - get_metrics_assembler_interface().resolve_stage(stage_id) - - -def get_matching_prim_location(match_string, root_path=None): - prim = find_matching_prims( - match_strings=[match_string], root_path=root_path, prim_type="Xform", first_match_only=True - ) - if prim is None: - print(f"[SDG-Infinigen] Could not find matching prim, returning (0, 0, 0)") - return (0, 0, 0) - if prim.HasAttribute("xformOp:translate"): - return prim.GetAttribute("xformOp:translate").Get() - elif prim.HasAttribute("xformOp:transform"): - return prim.GetAttribute("xformOp:transform").Get().ExtractTranslation() - else: - print(f"[SDG-Infinigen] Could not find location attribute for '{prim.GetPath()}', returning (0, 0, 0)") - return (0, 0, 0) - - -def offset_range( - range_coords: tuple[float, float, float, float, float, float], offset: tuple[float, float, float] -) -> tuple[float, float, float, float, float, float]: - """Offset the min and max coordinates of a range by the specified offset.""" - return ( - range_coords[0] + offset[0], # min_x - range_coords[1] + offset[1], # min_y - range_coords[2] + offset[2], # min_z - range_coords[3] + offset[0], # max_x - range_coords[4] + offset[1], # max_y - range_coords[5] + offset[2], # max_z - ) - - -def randomize_poses( - prims: list[Usd.Prim], - location_range: tuple[float, float, float, float, float, float], - rotation_range: tuple[float, float], - scale_range: tuple[float, float], -) -> None: - """Randomize the location, rotation, and scale of a list of prims within specified ranges.""" - for prim in prims: - rand_loc = ( - random.uniform(location_range[0], location_range[3]), - random.uniform(location_range[1], location_range[4]), - random.uniform(location_range[2], location_range[5]), - ) - rand_rot = ( - random.uniform(rotation_range[0], rotation_range[1]), - random.uniform(rotation_range[0], rotation_range[1]), - random.uniform(rotation_range[0], rotation_range[1]), - ) - rand_scale = random.uniform(scale_range[0], scale_range[1]) - set_transform_attributes(prim, location=rand_loc, rotation=rand_rot, scale=(rand_scale, rand_scale, rand_scale)) - - -def run_simulation(num_frames: int, render: bool = True) -> None: - """Run a simulation for a specified number of frames, optionally without rendering.""" - if render: - # Start the timeline and advance the app, this will render the physics simulation results every frame - timeline = omni.timeline.get_timeline_interface() - timeline.set_start_time(0) - timeline.set_end_time(1000000) - timeline.set_looping(False) - timeline.play() - for _ in range(num_frames): - omni.kit.app.get_app().update() - timeline.pause() - else: - # Run the physics simulation steps without advancing the app - stage = omni.usd.get_context().get_stage() - physx_scene = None - - # Search for or create a physics scene - for prim in stage.Traverse(): - if prim.IsA(UsdPhysics.Scene): - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(prim) - break - - if physx_scene is None: - physics_scene = UsdPhysics.Scene.Define(stage, "/PhysicsScene") - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(stage.GetPrimAtPath("/PhysicsScene")) - - # Get simulation parameters - physx_dt = 1 / physx_scene.GetTimeStepsPerSecondAttr().Get() - physx_sim_interface = omni.physx.get_physx_simulation_interface() - - # Run physics simulation for each frame - for _ in range(num_frames): - physx_sim_interface.simulate(physx_dt, 0) - physx_sim_interface.fetch_results() - - -def register_dome_light_randomizer() -> None: - """Register a replicator graph randomizer for dome lights using various sky textures.""" - assets_root_path = get_assets_root_path() - dome_textures = [ - assets_root_path + "/NVIDIA/Assets/Skies/Cloudy/champagne_castle_1_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Cloudy/kloofendal_48d_partly_cloudy_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Clear/evening_road_01_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Clear/mealie_road_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Clear/qwantani_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Clear/noon_grass_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Evening/evening_road_01_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Night/kloppenheim_02_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Night/moonlit_golf_4k.hdr", - ] - with rep.trigger.on_custom_event(event_name="randomize_dome_lights"): - rep.create.light(light_type="Dome", texture=rep.distribution.choice(dome_textures)) - - -def register_shape_distractors_color_randomizer(shape_distractors: list[Usd.Prim]) -> None: - """Register a replicator graph randomizer to change colors of shape distractors.""" - with rep.trigger.on_custom_event(event_name="randomize_shape_distractor_colors"): - shape_distractors_paths = [prim.GetPath() for prim in shape_distractors] - shape_distractors_group = rep.create.group(shape_distractors_paths) - with shape_distractors_group: - rep.randomizer.color(colors=rep.distribution.uniform((0, 0, 0), (1, 1, 1))) - - -def randomize_lights( - lights: list[Usd.Prim], - location_range: tuple[float, float, float, float, float, float] | None = None, - color_range: tuple[float, float, float, float, float, float] | None = None, - intensity_range: tuple[float, float] | None = None, -) -> None: - """Randomize location, color, and intensity of specified lights within given ranges.""" - for light in lights: - # Randomize the location of the light - if location_range is not None: - rand_loc = ( - random.uniform(location_range[0], location_range[3]), - random.uniform(location_range[1], location_range[4]), - random.uniform(location_range[2], location_range[5]), - ) - set_transform_attributes(light, location=rand_loc) - - # Randomize the color of the light - if color_range is not None: - rand_color = ( - random.uniform(color_range[0], color_range[3]), - random.uniform(color_range[1], color_range[4]), - random.uniform(color_range[2], color_range[5]), - ) - light.GetAttribute("inputs:color").Set(rand_color) - - # Randomize the intensity of the light - if intensity_range is not None: - rand_intensity = random.uniform(intensity_range[0], intensity_range[1]) - light.GetAttribute("inputs:intensity").Set(rand_intensity) - - -def setup_writer(config: dict) -> None: - """Setup a writer based on configuration settings, initializing with specified arguments.""" - writer_type = config.get("type", None) - if writer_type is None: - print("[Infinigen-SDG] No writer type specified. No writer will be used.") - return None - - try: - writer = rep.writers.get(writer_type) - except Exception as e: - print(f"[Infinigen-SDG] Writer type '{writer_type}' not found. No writer will be used. Error: {e}") - return None - - writer_kwargs = config.get("kwargs", {}) - if out_dir := writer_kwargs.get("output_dir"): - # If not an absolute path, make path relative to the current working directory - if not os.path.isabs(out_dir): - out_dir = os.path.join(os.getcwd(), out_dir) - writer_kwargs["output_dir"] = out_dir - - writer.initialize(**writer_kwargs) - return writer diff --git a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_centerpose_config.yaml b/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_centerpose_config.yaml deleted file mode 100644 index d7ead97d0..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_centerpose_config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -writer_type: PoseWriter -writer_kwargs: - output_dir: _out_obj_based_sdg_pose_writer_centerpose - format: centerpose - write_debug_images: true - skip_empty_frames: false \ No newline at end of file diff --git a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_config.yaml b/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_config.yaml deleted file mode 100644 index ddae7b656..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_config.yaml +++ /dev/null @@ -1,78 +0,0 @@ -launch_config: - renderer: RaytracedLighting - headless: false -env_url: '' -working_area_size: -- 4 -- 4 -- 3 -rt_subframes: 4 -num_frames: 10 -num_cameras: 2 -disable_render_products_between_captures: false -simulation_duration_between_captures: 0.0 -resolution: -- 640 -- 480 -camera_look_at_target_offset: 0.15 -camera_distance_to_target_min_max: - - 0.25 - - 0.75 -writer_type: BasicWriter -writer_kwargs: - output_dir: _out_obj_based_sdg_basic_writer - rgb: true - semantic_segmentation: true - use_common_output_dir: true -labeled_assets_and_properties: -- url: /Isaac/Props/YCB/Axis_Aligned/008_pudding_box.usd - label: pudding_box - count: 5 - floating: true - scale_min_max: - - 0.85 - - 1.25 -- url: /Isaac/Props/YCB/Axis_Aligned/011_banana.usd - label: banana - count: 10 - floating: false - scale_min_max: - - 0.85 - - 1.25 -- url: /Isaac/Props/YCB/Axis_Aligned_Physics/006_mustard_bottle.usd - label: mustard_bottle - count: 7 - floating: true - scale_min_max: - - 0.85 - - 1.25 -shape_distractors_types: -- capsule -- cone -- cylinder -- sphere -- cube -shape_distractors_scale_min_max: - - 0.015 - - 0.15 -shape_distractors_num: 350 -mesh_distractors_urls: -- /Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxD_04_1847.usd -- /Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxA_01_414.usd -- /Isaac/Environments/Simple_Warehouse/Props/S_TrafficCone.usd -- /Isaac/Environments/Simple_Warehouse/Props/S_WetFloorSign.usd -- /Isaac/Environments/Simple_Warehouse/Props/SM_BarelPlastic_B_03.usd -- /Isaac/Environments/Office/Props/SM_Board.usd -- /Isaac/Environments/Office/Props/SM_Book_03.usd -- /Isaac/Environments/Office/Props/SM_Book_34.usd -- /Isaac/Environments/Office/Props/SM_BookOpen_01.usd -- /Isaac/Environments/Office/Props/SM_Briefcase.usd -- /Isaac/Environments/Office/Props/SM_Extinguisher.usd -- /Isaac/Environments/Hospital/Props/SM_GasCart_01b.usd -- /Isaac/Environments/Hospital/Props/SM_MedicalBag_01a.usd -- /Isaac/Environments/Hospital/Props/SM_MedicalBox_01g.usd -- /Isaac/Environments/Hospital/Props/SM_Toweldispenser_01a.usd -mesh_distractors_scale_min_max: - - 0.35 - - 1.35 -mesh_distractors_num: 75 diff --git a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_dope_config.yaml b/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_dope_config.yaml deleted file mode 100644 index 5b465a3d8..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/config/object_based_sdg_dope_config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -writer_type: PoseWriter -writer_kwargs: - output_dir: _out_obj_based_sdg_pose_writer_dope - format: dope - write_debug_images: true - skip_empty_frames: false \ No newline at end of file diff --git a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/object_based_sdg.py b/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/object_based_sdg.py deleted file mode 100644 index 036aaa6bc..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/object_based_sdg.py +++ /dev/null @@ -1,610 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import json -import os - -import yaml -from isaacsim import SimulationApp - -# Default config dict, can be updated/replaced using json/yaml config files ('--config' cli argument) -config = { - "launch_config": { - "renderer": "RaytracedLighting", - "headless": False, - }, - "env_url": "", - "working_area_size": (4, 4, 3), - "rt_subframes": 4, - "num_frames": 10, - "num_cameras": 3, - "camera_collider_radius": 0.5, - "disable_render_products_between_captures": False, - "simulation_duration_between_captures": 0.05, - "resolution": (640, 480), - "camera_properties_kwargs": { - "focalLength": 24.0, - "focusDistance": 400, - "fStop": 0.0, - "clippingRange": (0.01, 10000), - }, - "camera_look_at_target_offset": 0.15, - "camera_distance_to_target_min_max": (0.25, 0.75), - "writer_type": "PoseWriter", - "writer_kwargs": { - "output_dir": "_out_obj_based_sdg_pose_writer", - "format": None, - "use_subfolders": False, - "write_debug_images": True, - "skip_empty_frames": False, - }, - "labeled_assets_and_properties": [ - { - "url": "/Isaac/Props/YCB/Axis_Aligned/008_pudding_box.usd", - "label": "pudding_box", - "count": 5, - "floating": True, - "scale_min_max": (0.85, 1.25), - }, - { - "url": "/Isaac/Props/YCB/Axis_Aligned_Physics/006_mustard_bottle.usd", - "label": "mustard_bottle", - "count": 7, - "floating": True, - "scale_min_max": (0.85, 1.25), - }, - ], - "shape_distractors_types": ["capsule", "cone", "cylinder", "sphere", "cube"], - "shape_distractors_scale_min_max": (0.015, 0.15), - "shape_distractors_num": 350, - "mesh_distractors_urls": [ - "/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxD_04_1847.usd", - "/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxA_01_414.usd", - "/Isaac/Environments/Simple_Warehouse/Props/S_TrafficCone.usd", - ], - "mesh_distractors_scale_min_max": (0.35, 1.35), - "mesh_distractors_num": 75, -} - -import carb - -# Check if there are any config files (yaml or json) are passed as arguments -parser = argparse.ArgumentParser() -parser.add_argument("--config", required=False, help="Include specific config parameters (json or yaml))") -args, unknown = parser.parse_known_args() -args_config = {} -if args.config and os.path.isfile(args.config): - with open(args.config, "r") as f: - if args.config.endswith(".json"): - args_config = json.load(f) - elif args.config.endswith(".yaml"): - args_config = yaml.safe_load(f) - else: - carb.log_warn(f"File {args.config} is not json or yaml, will use default config") -else: - carb.log_warn(f"File {args.config} does not exist, will use default config") - -# Update the default config dict with the external one -config.update(args_config) - -print(f"[SDG] Using config:\n{config}") - -launch_config = config.get("launch_config", {}) -simulation_app = SimulationApp(launch_config=launch_config) - -import random -import time -from itertools import chain - -import carb.settings - -# Custom util functions for the example -import object_based_sdg_utils -import omni.replicator.core as rep -import omni.timeline -import omni.usd -import usdrt -from isaacsim.core.utils.semantics import add_update_semantics, remove_all_semantics -from isaacsim.storage.native import get_assets_root_path -from omni.physx import get_physx_interface, get_physx_scene_query_interface -from pxr import PhysxSchema, Sdf, UsdGeom, UsdPhysics - -# Isaac nucleus assets root path -assets_root_path = get_assets_root_path() -stage = None - -# ENVIRONMENT -# Create an empty or load a custom stage (clearing any previous semantics) -env_url = config.get("env_url", "") -if env_url: - env_path = env_url if env_url.startswith("omniverse://") else assets_root_path + env_url - omni.usd.get_context().open_stage(env_path) - stage = omni.usd.get_context().get_stage() - # Remove any previous semantics in the loaded stage - for prim in stage.Traverse(): - remove_all_semantics(prim) -else: - omni.usd.get_context().new_stage() - stage = omni.usd.get_context().get_stage() - # Add a distant light to the empty stage - distant_light = stage.DefinePrim("/World/Lights/DistantLight", "DistantLight") - distant_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(400.0) - if not distant_light.HasAttribute("xformOp:rotateXYZ"): - UsdGeom.Xformable(distant_light).AddRotateXYZOp() - distant_light.GetAttribute("xformOp:rotateXYZ").Set((0, 60, 0)) - -# Get the working area size and bounds (width=x, depth=y, height=z) -working_area_size = config.get("working_area_size", (3, 3, 3)) -working_area_min = (working_area_size[0] / -2, working_area_size[1] / -2, working_area_size[2] / -2) -working_area_max = (working_area_size[0] / 2, working_area_size[1] / 2, working_area_size[2] / 2) - -# Create a collision box area around the assets to prevent them from drifting away -object_based_sdg_utils.create_collision_box_walls( - stage, "/World/CollisionWalls", working_area_size[0], working_area_size[1], working_area_size[2] -) - -# Create a physics scene to add or modify custom physics settings -usdrt_stage = usdrt.Usd.Stage.Attach(omni.usd.get_context().get_stage_id()) -physics_scenes = usdrt_stage.GetPrimsWithAppliedAPIName("PhysxSceneAPI") -if physics_scenes: - physics_scene = physics_scenes[0] -else: - physics_scene = UsdPhysics.Scene.Define(stage, "/PhysicsScene") - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(stage.GetPrimAtPath("/PhysicsScene")) -physx_scene.GetTimeStepsPerSecondAttr().Set(60) - - -# TRAINING ASSETS -# Add the objects to be trained in the environment with their labels and properties -labeled_assets_and_properties = config.get("labeled_assets_and_properties", []) -floating_labeled_prims = [] -falling_labeled_prims = [] -labeled_prims = [] -for obj in labeled_assets_and_properties: - obj_url = obj.get("url", "") - label = obj.get("label", "unknown") - count = obj.get("count", 1) - floating = obj.get("floating", False) - scale_min_max = obj.get("randomize_scale", (1, 1)) - for i in range(count): - # Create a prim and add the asset reference - rand_loc, rand_rot, rand_scale = object_based_sdg_utils.get_random_transform_values( - loc_min=working_area_min, loc_max=working_area_max, scale_min_max=scale_min_max - ) - prim_path = omni.usd.get_stage_next_free_path(stage, f"/World/Labeled/{label}", False) - prim = stage.DefinePrim(prim_path, "Xform") - asset_path = obj_url if obj_url.startswith("omniverse://") else assets_root_path + obj_url - prim.GetReferences().AddReference(asset_path) - object_based_sdg_utils.set_transform_attributes(prim, location=rand_loc, rotation=rand_rot, scale=rand_scale) - object_based_sdg_utils.add_colliders(prim) - object_based_sdg_utils.add_rigid_body_dynamics(prim, disable_gravity=floating) - # Label the asset (any previous 'class' label will be overwritten) - add_update_semantics(prim, label) - if floating: - floating_labeled_prims.append(prim) - else: - falling_labeled_prims.append(prim) -labeled_prims = floating_labeled_prims + falling_labeled_prims - - -# DISTRACTORS -# Add shape distractors to the environment as floating or falling objects -shape_distractors_types = config.get("shape_distractors_types", ["capsule", "cone", "cylinder", "sphere", "cube"]) -shape_distractors_scale_min_max = config.get("shape_distractors_scale_min_max", (0.02, 0.2)) -shape_distractors_num = config.get("shape_distractors_num", 350) -shape_distractors = [] -floating_shape_distractors = [] -falling_shape_distractors = [] -for i in range(shape_distractors_num): - rand_loc, rand_rot, rand_scale = object_based_sdg_utils.get_random_transform_values( - loc_min=working_area_min, loc_max=working_area_max, scale_min_max=shape_distractors_scale_min_max - ) - rand_shape = random.choice(shape_distractors_types) - prim_path = omni.usd.get_stage_next_free_path(stage, f"/World/Distractors/{rand_shape}", False) - prim = stage.DefinePrim(prim_path, rand_shape.capitalize()) - object_based_sdg_utils.set_transform_attributes(prim, location=rand_loc, rotation=rand_rot, scale=rand_scale) - object_based_sdg_utils.add_colliders(prim) - disable_gravity = random.choice([True, False]) - object_based_sdg_utils.add_rigid_body_dynamics(prim, disable_gravity) - if disable_gravity: - floating_shape_distractors.append(prim) - else: - falling_shape_distractors.append(prim) - shape_distractors.append(prim) - -# Add mesh distractors to the environment as floating of falling objects -mesh_distactors_urls = config.get("mesh_distractors_urls", []) -mesh_distactors_scale_min_max = config.get("mesh_distractors_scale_min_max", (0.1, 2.0)) -mesh_distactors_num = config.get("mesh_distractors_num", 10) -mesh_distractors = [] -floating_mesh_distractors = [] -falling_mesh_distractors = [] -for i in range(mesh_distactors_num): - rand_loc, rand_rot, rand_scale = object_based_sdg_utils.get_random_transform_values( - loc_min=working_area_min, loc_max=working_area_max, scale_min_max=mesh_distactors_scale_min_max - ) - mesh_url = random.choice(mesh_distactors_urls) - prim_name = os.path.basename(mesh_url).split(".")[0] - prim_path = omni.usd.get_stage_next_free_path(stage, f"/World/Distractors/{prim_name}", False) - prim = stage.DefinePrim(prim_path, "Xform") - asset_path = mesh_url if mesh_url.startswith("omniverse://") else assets_root_path + mesh_url - prim.GetReferences().AddReference(asset_path) - object_based_sdg_utils.set_transform_attributes(prim, location=rand_loc, rotation=rand_rot, scale=rand_scale) - object_based_sdg_utils.add_colliders(prim) - disable_gravity = random.choice([True, False]) - object_based_sdg_utils.add_rigid_body_dynamics(prim, disable_gravity=disable_gravity) - if disable_gravity: - floating_mesh_distractors.append(prim) - else: - falling_mesh_distractors.append(prim) - mesh_distractors.append(prim) - # Remove any previous semantics on the mesh distractor - remove_all_semantics(prim, recursive=True) - -# REPLICATOR -# Disable capturing every frame (capture will be triggered manually using the step function) -rep.orchestrator.set_capture_on_play(False) - -# Create the camera prims and their properties -cameras = [] -num_cameras = config.get("num_cameras", 1) -camera_properties_kwargs = config.get("camera_properties_kwargs", {}) -for i in range(num_cameras): - # Create camera and add its properties (focal length, focus distance, f-stop, clipping range, etc.) - cam_prim = stage.DefinePrim(f"/World/Cameras/cam_{i}", "Camera") - for key, value in camera_properties_kwargs.items(): - if cam_prim.HasAttribute(key): - cam_prim.GetAttribute(key).Set(value) - else: - print(f"Unknown camera attribute with {key}:{value}") - cameras.append(cam_prim) - -# Add collision spheres (disabled by default) to cameras to avoid objects overlaping with the camera view -camera_colliders = [] -camera_collider_radius = config.get("camera_collider_radius", 0) -if camera_collider_radius > 0: - for cam in cameras: - cam_path = cam.GetPath() - cam_collider = stage.DefinePrim(f"{cam_path}/CollisionSphere", "Sphere") - cam_collider.GetAttribute("radius").Set(camera_collider_radius) - object_based_sdg_utils.add_colliders(cam_collider) - collision_api = UsdPhysics.CollisionAPI(cam_collider) - collision_api.GetCollisionEnabledAttr().Set(False) - UsdGeom.Imageable(cam_collider).MakeInvisible() - camera_colliders.append(cam_collider) - -# Wait an app update to ensure the prim changes are applied -simulation_app.update() - -# Create render products using the cameras -render_products = [] -resolution = config.get("resolution", (640, 480)) -for cam in cameras: - rp = rep.create.render_product(cam.GetPath(), resolution) - render_products.append(rp) - -# Enable rendering only at capture time -disable_render_products_between_captures = config.get("disable_render_products_between_captures", True) -if disable_render_products_between_captures: - object_based_sdg_utils.set_render_products_updates(render_products, False, include_viewport=False) - -# Create the writer and attach the render products -writer_type = config.get("writer_type", "PoseWriter") -writer_kwargs = config.get("writer_kwargs", {}) -# If not an absolute path, set it relative to the current working directory -if out_dir := writer_kwargs.get("output_dir"): - if not os.path.isabs(out_dir): - out_dir = os.path.join(os.getcwd(), out_dir) - writer_kwargs["output_dir"] = out_dir - print(f"[SDG] Writing data to: {out_dir}") -if writer_type is not None and len(render_products) > 0: - writer = rep.writers.get(writer_type) - writer.initialize(**writer_kwargs) - writer.attach(render_products) - -# RANDOMIZERS -# Apply a random (mostly) uppwards velocity to the objects overlapping the 'bounce' area -def on_overlap_hit(hit): - prim = stage.GetPrimAtPath(hit.rigid_body) - # Skip the camera collision spheres - if prim not in camera_colliders: - rand_vel = (random.uniform(-2, 2), random.uniform(-2, 2), random.uniform(4, 8)) - prim.GetAttribute("physics:velocity").Set(rand_vel) - return True # return True to continue the query - - -# Area to check for overlapping objects (above the bottom collision box) -overlap_area_thickness = 0.1 -overlap_area_origin = (0, 0, (-working_area_size[2] / 2) + (overlap_area_thickness / 2)) -overlap_area_extent = ( - working_area_size[0] / 2 * 0.99, - working_area_size[1] / 2 * 0.99, - overlap_area_thickness / 2 * 0.99, -) - - -# Triggered every physics update step to check for overlapping objects -def on_physics_step(dt: float): - hit_info = get_physx_scene_query_interface().overlap_box( - carb.Float3(overlap_area_extent), - carb.Float3(overlap_area_origin), - carb.Float4(0, 0, 0, 1), - on_overlap_hit, - False, # pass 'False' to indicate an 'overlap multiple' query. - ) - - -# Subscribe to the physics step events to check for objects overlapping the 'bounce' area -physx_sub = get_physx_interface().subscribe_physics_step_events(on_physics_step) - - -# Pull assets towards the working area center by applying a random velocity towards the given target -def apply_velocities_towards_target(assets, target=(0, 0, 0)): - for prim in assets: - loc = prim.GetAttribute("xformOp:translate").Get() - strength = random.uniform(0.1, 1.0) - pull_vel = ((target[0] - loc[0]) * strength, (target[1] - loc[1]) * strength, (target[2] - loc[2]) * strength) - prim.GetAttribute("physics:velocity").Set(pull_vel) - - -# Randomize camera poses to look at a random target asset (random distance and center offset) -camera_distance_to_target_min_max = config.get("camera_distance_to_target_min_max", (0.1, 0.5)) -camera_look_at_target_offset = config.get("camera_look_at_target_offset", 0.2) - - -def randomize_camera_poses(): - for cam in cameras: - # Get a random target asset to look at - target_asset = random.choice(labeled_prims) - # Add a look_at offset so the target is not always in the center of the camera view - loc_offset = ( - random.uniform(-camera_look_at_target_offset, camera_look_at_target_offset), - random.uniform(-camera_look_at_target_offset, camera_look_at_target_offset), - random.uniform(-camera_look_at_target_offset, camera_look_at_target_offset), - ) - target_loc = target_asset.GetAttribute("xformOp:translate").Get() + loc_offset - # Get a random distance to the target asset - distance = random.uniform(camera_distance_to_target_min_max[0], camera_distance_to_target_min_max[1]) - # Get a random pose of the camera looking at the target asset from the given distance - cam_loc, quat = object_based_sdg_utils.get_random_pose_on_sphere(origin=target_loc, radius=distance) - object_based_sdg_utils.set_transform_attributes(cam, location=cam_loc, orientation=quat) - - -# Temporarily enable camera colliders and simulate for the given number of frames to push out any overlapping objects -def simulate_camera_collision(num_frames=1): - for cam_collider in camera_colliders: - collision_api = UsdPhysics.CollisionAPI(cam_collider) - collision_api.GetCollisionEnabledAttr().Set(True) - if not timeline.is_playing(): - timeline.play() - for _ in range(num_frames): - simulation_app.update() - for cam_collider in camera_colliders: - collision_api = UsdPhysics.CollisionAPI(cam_collider) - collision_api.GetCollisionEnabledAttr().Set(False) - - -# Create a randomizer for the shape distractors colors, manually triggered at custom events -with rep.trigger.on_custom_event(event_name="randomize_shape_distractor_colors"): - shape_distractors_paths = [prim.GetPath() for prim in chain(floating_shape_distractors, falling_shape_distractors)] - shape_distractors_group = rep.create.group(shape_distractors_paths) - with shape_distractors_group: - rep.randomizer.color(colors=rep.distribution.uniform((0, 0, 0), (1, 1, 1))) - -# Create a randomizer to apply random velocities to the floating shape distractors -with rep.trigger.on_custom_event(event_name="randomize_floating_distractor_velocities"): - shape_distractors_paths = [prim.GetPath() for prim in chain(floating_shape_distractors, floating_mesh_distractors)] - shape_distractors_group = rep.create.group(shape_distractors_paths) - with shape_distractors_group: - rep.physics.rigid_body( - velocity=rep.distribution.uniform((-2.5, -2.5, -2.5), (2.5, 2.5, 2.5)), - angular_velocity=rep.distribution.uniform((-45, -45, -45), (45, 45, 45)), - ) - - -# Create a randomizer for lights in the working area, manually triggered at custom events -with rep.trigger.on_custom_event(event_name="randomize_lights"): - lights = rep.create.light( - light_type="Sphere", - color=rep.distribution.uniform((0, 0, 0), (1, 1, 1)), - temperature=rep.distribution.normal(6500, 500), - intensity=rep.distribution.normal(35000, 5000), - position=rep.distribution.uniform(working_area_min, working_area_max), - scale=rep.distribution.uniform(0.1, 1), - count=3, - ) - - -# Create a randomizer for the dome background, manually triggered at custom events -with rep.trigger.on_custom_event(event_name="randomize_dome_background"): - dome_textures = [ - assets_root_path + "/NVIDIA/Assets/Skies/Indoor/autoshop_01_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Indoor/carpentry_shop_01_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Indoor/hotel_room_4k.hdr", - assets_root_path + "/NVIDIA/Assets/Skies/Indoor/wooden_lounge_4k.hdr", - ] - dome_light = rep.create.light(light_type="Dome") - with dome_light: - rep.modify.attribute("inputs:texture:file", rep.distribution.choice(dome_textures)) - rep.randomizer.rotation() - - -# Capture motion blur by combining the number of pathtraced subframes samples simulated for the given duration -def capture_with_motion_blur_and_pathtracing(duration=0.05, num_samples=8, spp=64): - # For small step sizes the physics FPS needs to be temporarily increased to provide movements every syb sample - orig_physics_fps = physx_scene.GetTimeStepsPerSecondAttr().Get() - target_physics_fps = 1 / duration * num_samples - if target_physics_fps > orig_physics_fps: - print(f"[SDG] Changing physics FPS from {orig_physics_fps} to {target_physics_fps}") - physx_scene.GetTimeStepsPerSecondAttr().Set(target_physics_fps) - - # Enable motion blur (if not enabled) - is_motion_blur_enabled = carb.settings.get_settings().get("/omni/replicator/captureMotionBlur") - if not is_motion_blur_enabled: - carb.settings.get_settings().set("/omni/replicator/captureMotionBlur", True) - # Number of sub samples to render for motion blur in PathTracing mode - carb.settings.get_settings().set("/omni/replicator/pathTracedMotionBlurSubSamples", num_samples) - - # Set the render mode to PathTracing - prev_render_mode = carb.settings.get_settings().get("/rtx/rendermode") - carb.settings.get_settings().set("/rtx/rendermode", "PathTracing") - carb.settings.get_settings().set("/rtx/pathtracing/spp", spp) - carb.settings.get_settings().set("/rtx/pathtracing/totalSpp", spp) - carb.settings.get_settings().set("/rtx/pathtracing/optixDenoiser/enabled", 0) - - # Make sure the timeline is playing - if not timeline.is_playing(): - timeline.play() - - # Capture the frame by advancing the simulation for the given duration and combining the sub samples - rep.orchestrator.step(delta_time=duration, pause_timeline=False) - - # Restore the original physics FPS - if target_physics_fps > orig_physics_fps: - print(f"[SDG] Restoring physics FPS from {target_physics_fps} to {orig_physics_fps}") - physx_scene.GetTimeStepsPerSecondAttr().Set(orig_physics_fps) - - # Restore the previous render and motion blur settings - carb.settings.get_settings().set("/omni/replicator/captureMotionBlur", is_motion_blur_enabled) - print(f"[SDG] Restoring render mode from 'PathTracing' to '{prev_render_mode}'") - carb.settings.get_settings().set("/rtx/rendermode", prev_render_mode) - - -# Update the app until a given simulation duration has passed (simulate the world between captures) -def run_simulation_loop(duration): - timeline = omni.timeline.get_timeline_interface() - elapsed_time = 0.0 - previous_time = timeline.get_current_time() - if not timeline.is_playing(): - timeline.play() - app_updates_counter = 0 - while elapsed_time <= duration: - simulation_app.update() - elapsed_time += timeline.get_current_time() - previous_time - previous_time = timeline.get_current_time() - app_updates_counter += 1 - print( - f"\t Simulation loop at {timeline.get_current_time():.2f}, current elapsed time: {elapsed_time:.2f}, counter: {app_updates_counter}" - ) - print( - f"[SDG] Simulation loop finished in {elapsed_time:.2f} seconds at {timeline.get_current_time():.2f} with {app_updates_counter} app updates." - ) - - -# SDG -# Number of frames to capture -num_frames = config.get("num_frames", 10) - -# Increase subframes if materials are not loaded on time, or ghosting artifacts appear on moving objects, -# see: https://docs.omniverse.nvidia.com/extensions/latest/ext_replicator/subframes_examples.html -rt_subframes = config.get("rt_subframes", -1) - -# Amount of simulation time to wait between captures -sim_duration_between_captures = config.get("simulation_duration_between_captures", 0.025) - -# Initial trigger for randomizers before the SDG loop with several app updates (ensures materials/textures are loaded) -rep.utils.send_og_event(event_name="randomize_shape_distractor_colors") -rep.utils.send_og_event(event_name="randomize_dome_background") -for _ in range(5): - simulation_app.update() - -# Set the timeline parameters (start, end, no looping) and start the timeline -timeline = omni.timeline.get_timeline_interface() -timeline.set_start_time(0) -timeline.set_end_time(1000000) -timeline.set_looping(False) -# If no custom physx scene is created, a default one will be created by the physics engine once the timeline starts -timeline.play() -timeline.commit() -simulation_app.update() - -# Store the wall start time for stats -wall_time_start = time.perf_counter() - -# Run the simulation and capture data triggering randomizations and actions at custom frame intervals -for i in range(num_frames): - # Cameras will be moved to a random position and look at a randomly selected labeled asset - if i % 3 == 0: - print(f"\t Randomizing camera poses") - randomize_camera_poses() - # Temporarily enable camera colliders and simulate for a few frames to push out any overlapping objects - if camera_colliders: - simulate_camera_collision(num_frames=4) - - # Apply a random velocity towards the origin to the working area to pull the assets closer to the center - if i % 10 == 0: - print(f"\t Applying velocity towards the origin") - apply_velocities_towards_target(chain(labeled_prims, shape_distractors, mesh_distractors)) - - # Randomize lights locations and colors - if i % 5 == 0: - print(f"\t Randomizing lights") - rep.utils.send_og_event(event_name="randomize_lights") - - # Randomize the colors of the primitive shape distractors - if i % 15 == 0: - print(f"\t Randomizing shape distractors colors") - rep.utils.send_og_event(event_name="randomize_shape_distractor_colors") - - # Randomize the texture of the dome background - if i % 25 == 0: - print(f"\t Randomizing dome background") - rep.utils.send_og_event(event_name="randomize_dome_background") - - # Apply a random velocity on the floating distractors (shapes and meshes) - if i % 17 == 0: - print(f"\t Randomizing shape distractors velocities") - rep.utils.send_og_event(event_name="randomize_floating_distractor_velocities") - - # Enable render products only at capture time - if disable_render_products_between_captures: - object_based_sdg_utils.set_render_products_updates(render_products, True, include_viewport=False) - - # Capture the current frame - print(f"[SDG] Capturing frame {i}/{num_frames}, at simulation time: {timeline.get_current_time():.2f}") - if i % 5 == 0: - capture_with_motion_blur_and_pathtracing(duration=0.025, num_samples=8, spp=128) - else: - rep.orchestrator.step(delta_time=0.0, rt_subframes=rt_subframes, pause_timeline=False) - - # Disable render products between captures - if disable_render_products_between_captures: - object_based_sdg_utils.set_render_products_updates(render_products, False, include_viewport=False) - - # Run the simulation for a given duration between frame captures - if sim_duration_between_captures > 0: - run_simulation_loop(duration=sim_duration_between_captures) - else: - simulation_app.update() - -# Wait for the data to be written (default writer backends are asynchronous) -rep.orchestrator.wait_until_complete() - -# Get the stats -wall_duration = time.perf_counter() - wall_time_start -sim_duration = timeline.get_current_time() -avg_frame_fps = num_frames / wall_duration -num_captures = num_frames * num_cameras -avg_capture_fps = num_captures / wall_duration -print( - f"[SDG] Captured {num_frames} frames, {num_captures} entries (frames * cameras) in {wall_duration:.2f} seconds.\n" - f"\t Simulation duration: {sim_duration:.2f}\n" - f"\t Simulation duration between captures: {sim_duration_between_captures:.2f}\n" - f"\t Average frame FPS: {avg_frame_fps:.2f}\n" - f"\t Average capture entries (frames * cameras) FPS: {avg_capture_fps:.2f}\n" -) - -# Unsubscribe the physics overlap checks and stop the timeline -physx_sub.unsubscribe() -physx_sub = None -simulation_app.update() -timeline.stop() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/object_based_sdg_utils.py b/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/object_based_sdg_utils.py deleted file mode 100644 index a93653b44..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/object_based_sdg/object_based_sdg_utils.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import random - -import numpy as np -from omni.kit.viewport.utility import get_active_viewport -from pxr import Gf, PhysxSchema, Usd, UsdGeom, UsdPhysics - - -# Add transformation properties to the prim (if not already present) -def set_transform_attributes(prim, location=None, orientation=None, rotation=None, scale=None): - if location is not None: - if not prim.HasAttribute("xformOp:translate"): - UsdGeom.Xformable(prim).AddTranslateOp() - prim.GetAttribute("xformOp:translate").Set(location) - if orientation is not None: - if not prim.HasAttribute("xformOp:orient"): - UsdGeom.Xformable(prim).AddOrientOp() - prim.GetAttribute("xformOp:orient").Set(orientation) - if rotation is not None: - if not prim.HasAttribute("xformOp:rotateXYZ"): - UsdGeom.Xformable(prim).AddRotateXYZOp() - prim.GetAttribute("xformOp:rotateXYZ").Set(rotation) - if scale is not None: - if not prim.HasAttribute("xformOp:scale"): - UsdGeom.Xformable(prim).AddScaleOp() - prim.GetAttribute("xformOp:scale").Set(scale) - - -# Enables collisions with the asset (without rigid body dynamics the asset will be static) -def add_colliders(root_prim): - # Iterate descendant prims (including root) and add colliders to mesh or primitive types - for desc_prim in Usd.PrimRange(root_prim): - if desc_prim.IsA(UsdGeom.Mesh) or desc_prim.IsA(UsdGeom.Gprim): - # Physics - if not desc_prim.HasAPI(UsdPhysics.CollisionAPI): - collision_api = UsdPhysics.CollisionAPI.Apply(desc_prim) - else: - collision_api = UsdPhysics.CollisionAPI(desc_prim) - collision_api.CreateCollisionEnabledAttr(True) - # PhysX - if not desc_prim.HasAPI(PhysxSchema.PhysxCollisionAPI): - physx_collision_api = PhysxSchema.PhysxCollisionAPI.Apply(desc_prim) - else: - physx_collision_api = PhysxSchema.PhysxCollisionAPI(desc_prim) - # Set PhysX specific properties - physx_collision_api.CreateContactOffsetAttr(0.001) - physx_collision_api.CreateRestOffsetAttr(0.0) - - # Add mesh specific collision properties only to mesh types - if desc_prim.IsA(UsdGeom.Mesh): - # Add mesh collision properties to the mesh (e.g. collider aproximation type) - if not desc_prim.HasAPI(UsdPhysics.MeshCollisionAPI): - mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(desc_prim) - else: - mesh_collision_api = UsdPhysics.MeshCollisionAPI(desc_prim) - mesh_collision_api.CreateApproximationAttr().Set("convexHull") - - -# Check if prim (or its descendants) has colliders -def has_colliders(root_prim): - for desc_prim in Usd.PrimRange(root_prim): - if desc_prim.HasAPI(UsdPhysics.CollisionAPI): - return True - return False - - -# Enables rigid body dynamics (physics simulation) on the prim -def add_rigid_body_dynamics(prim, disable_gravity=False, angular_damping=None): - if has_colliders(prim): - # Physics - if not prim.HasAPI(UsdPhysics.RigidBodyAPI): - rigid_body_api = UsdPhysics.RigidBodyAPI.Apply(prim) - else: - rigid_body_api = UsdPhysics.RigidBodyAPI(prim) - rigid_body_api.CreateRigidBodyEnabledAttr(True) - # PhysX - if not prim.HasAPI(PhysxSchema.PhysxRigidBodyAPI): - physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI.Apply(prim) - else: - physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI(prim) - physx_rigid_body_api.GetDisableGravityAttr().Set(disable_gravity) - if angular_damping is not None: - physx_rigid_body_api.CreateAngularDampingAttr().Set(angular_damping) - else: - print(f"Prim '{prim.GetPath()}' has no colliders. Skipping rigid body dynamics properties.") - - -# Add dynamics properties to the prim (if mesh or primitive) (rigid body to root + colliders to the meshes) -# https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/rigid-bodies.html#rigid-body-simulation -def add_colliders_and_rigid_body_dynamics(prim, disable_gravity=False): - # Add colliders to mesh or primitive types of the descendants of the prim (including root) - add_colliders(prim) - # Add rigid body dynamics properties (to the root only) only if it has colliders - add_rigid_body_dynamics(prim, disable_gravity=disable_gravity) - - -# Createa collision box area wrapping the given working area with origin in (0, 0, 0) with thickness towards outside -def create_collision_box_walls(stage, path, width, depth, height, thickness=0.5, visible=False): - # Define the walls (name, location, size) with thickness towards outside of the working area - walls = [ - ("floor", (0, 0, (height + thickness) / -2.0), (width, depth, thickness)), - ("ceiling", (0, 0, (height + thickness) / 2.0), (width, depth, thickness)), - ("left_wall", ((width + thickness) / -2.0, 0, 0), (thickness, depth, height)), - ("right_wall", ((width + thickness) / 2.0, 0, 0), (thickness, depth, height)), - ("front_wall", (0, (depth + thickness) / 2.0, 0), (width, thickness, height)), - ("back_wall", (0, (depth + thickness) / -2.0, 0), (width, thickness, height)), - ] - for name, location, size in walls: - prim = stage.DefinePrim(f"{path}/{name}", "Cube") - scale = (size[0] / 2.0, size[1] / 2.0, size[2] / 2.0) - set_transform_attributes(prim, location=location, scale=scale) - add_colliders(prim) - if not visible: - UsdGeom.Imageable(prim).MakeInvisible() - - -# Create a random transformation values for location, rotation, and scale -def get_random_transform_values( - loc_min=(0, 0, 0), loc_max=(1, 1, 1), rot_min=(0, 0, 0), rot_max=(360, 360, 360), scale_min_max=(0.1, 1.0) -): - location = ( - random.uniform(loc_min[0], loc_max[0]), - random.uniform(loc_min[1], loc_max[1]), - random.uniform(loc_min[2], loc_max[2]), - ) - rotation = ( - random.uniform(rot_min[0], rot_max[0]), - random.uniform(rot_min[1], rot_max[1]), - random.uniform(rot_min[2], rot_max[2]), - ) - scale = tuple([random.uniform(scale_min_max[0], scale_min_max[1])] * 3) - return location, rotation, scale - - -# Generate a random pose on a sphere looking at the origin -# https://docs.omniverse.nvidia.com/isaacsim/latest/reference_conventions.html -def get_random_pose_on_sphere(origin, radius, camera_forward_axis=(0, 0, -1)): - origin = Gf.Vec3f(origin) - camera_forward_axis = Gf.Vec3f(camera_forward_axis) - - # Generate random angles for spherical coordinates - theta = np.random.uniform(0, 2 * np.pi) - phi = np.arcsin(np.random.uniform(-1, 1)) - - # Spherical to Cartesian conversion - x = radius * np.cos(theta) * np.cos(phi) - y = radius * np.sin(phi) - z = radius * np.sin(theta) * np.cos(phi) - - location = origin + Gf.Vec3f(x, y, z) - - # Calculate direction vector from camera to look_at point - direction = origin - location - direction_normalized = direction.GetNormalized() - - # Calculate rotation from forward direction (rotateFrom) to direction vector (rotateTo) - rotation = Gf.Rotation(Gf.Vec3d(camera_forward_axis), Gf.Vec3d(direction_normalized)) - orientation = Gf.Quatf(rotation.GetQuat()) - - return location, orientation - - -# Enable or disable the render products and viewport rendering -def set_render_products_updates(render_products, enabled, include_viewport=False): - for rp in render_products: - rp.hydra_texture.set_updates_enabled(enabled) - if include_viewport: - get_active_viewport().updates_enabled = enabled diff --git a/simulation/isaac-sim/standalone_examples/replicator/online_generation/generate_shapenet.py b/simulation/isaac-sim/standalone_examples/replicator/online_generation/generate_shapenet.py deleted file mode 100644 index 845bcd586..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/online_generation/generate_shapenet.py +++ /dev/null @@ -1,435 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - - -"""Dataset with online randomized scene generation for Instance Segmentation training. - -Use OmniKit to generate a simple scene. At each iteration, the scene is populated by -adding assets from the user-specified classes with randomized pose and colour. -The camera position is also randomized before capturing groundtruth consisting of -an RGB rendered image, Tight 2D Bounding Boxes and Instance Segmentation masks. -""" - - -import glob -import os -import signal -import sys - -import numpy as np -import torch -from isaacsim import SimulationApp - -LABEL_TO_SYNSET = { - "table": "04379243", - "monitor": "03211117", - "phone": "04401088", - "watercraft": "04530566", - "chair": "03001627", - "lamp": "03636649", - "speaker": "03691459", - "bench": "02828884", - "plane": "02691156", - "bathtub": "02808440", - "bookcase": "02871439", - "bag": "02773838", - "basket": "02801938", - "bowl": "02880940", - "bus": "02924116", - "cabinet": "02933112", - "camera": "02942699", - "car": "02958343", - "dishwasher": "03207941", - "file": "03337140", - "knife": "03624134", - "laptop": "03642806", - "mailbox": "03710193", - "microwave": "03761084", - "piano": "03928116", - "pillow": "03938244", - "pistol": "03948459", - "printer": "04004475", - "rocket": "04099429", - "sofa": "04256520", - "washer": "04554684", - "rifle": "04090263", - "can": "02946921", - "bottle": "02876657", - "bowl": "02880940", - "earphone": "03261776", - "mug": "03797390", -} - -SYNSET_TO_LABEL = {v: k for k, v in LABEL_TO_SYNSET.items()} - -# Setup default variables -RESOLUTION = (1024, 1024) -OBJ_LOC_MIN = (-50, 5, -50) -OBJ_LOC_MAX = (50, 5, 50) -CAM_LOC_MIN = (100, 0, -100) -CAM_LOC_MAX = (100, 100, 100) -SCALE_MIN = 15 -SCALE_MAX = 40 - -# Default rendering parameters -RENDER_CONFIG = {"headless": False} - - -class RandomObjects(torch.utils.data.IterableDataset): - """Dataset of random ShapeNet objects. - Objects are randomly chosen from selected categories and are positioned, rotated and coloured - randomly in an empty room. RGB, BoundingBox2DTight and Instance Segmentation are captured by moving a - camera aimed at the centre of the scene which is positioned at random at a fixed distance from the centre. - - This dataset is intended for use with ShapeNet but will function with any dataset of USD models - structured as `root/category/**/*.usd. One note is that this is designed for assets without materials - attached. This is to avoid requiring to compile MDLs and load textures while training. - - Args: - categories (tuple of str): Tuple or list of categories. For ShapeNet, these will be the synset IDs. - max_asset_size (int): Maximum asset file size that will be loaded. This prevents out of memory errors - due to loading large meshes. - num_assets_min (int): Minimum number of assets populated in the scene. - num_assets_max (int): Maximum number of assets populated in the scene. - split (float): Fraction of the USDs found to use for training. - train (bool): If true, use the first training split and generate infinite random scenes. - """ - - def __init__( - self, root, categories, max_asset_size=None, num_assets_min=3, num_assets_max=5, split=0.7, train=True - ): - assert len(categories) > 1 - assert (split > 0) and (split <= 1.0) - - self.kit = SimulationApp(RENDER_CONFIG) - - import carb - import omni.replicator.core as rep - import warp as wp - - self.rep = rep - self.wp = wp - - from isaacsim.storage.native import get_assets_root_path - - self.assets_root_path = get_assets_root_path() - if self.assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - return - - # If ShapeNet categories are specified with their names, convert to synset ID - # Remove this if using with a different dataset than ShapeNet - category_ids = [LABEL_TO_SYNSET.get(c, c) for c in categories] - self.categories = category_ids - self.range_num_assets = (num_assets_min, max(num_assets_min, num_assets_max)) - try: - self.references = self._find_usd_assets(root, category_ids, max_asset_size, split, train) - except ValueError as err: - carb.log_error(str(err)) - self.kit.close() - sys.exit() - - # Setup the scene, lights, walls, camera, etc. - self.setup_scene() - - # Setup replicator randomizer graph - self.setup_replicator() - - self.cur_idx = 0 - self.exiting = False - - signal.signal(signal.SIGINT, self._handle_exit) - - def _get_textures(self): - return [ - self.assets_root_path + "/Isaac/Samples/DR/Materials/Textures/checkered.png", - self.assets_root_path + "/Isaac/Samples/DR/Materials/Textures/marble_tile.png", - self.assets_root_path + "/Isaac/Samples/DR/Materials/Textures/picture_a.png", - self.assets_root_path + "/Isaac/Samples/DR/Materials/Textures/picture_b.png", - self.assets_root_path + "/Isaac/Samples/DR/Materials/Textures/textured_wall.png", - self.assets_root_path + "/Isaac/Samples/DR/Materials/Textures/checkered_color.png", - ] - - def _handle_exit(self, *args, **kwargs): - print("exiting dataset generation...") - self.exiting = True - - def close(self): - self.rep.orchestrator.stop() - self.kit.close() - - def setup_scene(self): - from isaacsim.core.utils.prims import create_prim - from isaacsim.core.utils.rotations import euler_angles_to_quat - from isaacsim.core.utils.stage import set_stage_up_axis - - """Setup lights, walls, floor, ceiling and camera""" - # Set stage up axis to Y-up - set_stage_up_axis("y") - - # In a practical setting, the room parameters should attempt to match those of the - # target domain. Here, we instead opt for simplicity. - create_prim("/World/Room", "Sphere", attributes={"radius": 1e3, "primvars:displayColor": [(1.0, 1.0, 1.0)]}) - create_prim( - "/World/Ground", - "Cylinder", - position=np.array([0.0, -0.5, 0.0]), - orientation=euler_angles_to_quat(np.array([90.0, 0.0, 0.0]), degrees=True), - attributes={"height": 1, "radius": 1e4, "primvars:displayColor": [(1.0, 1.0, 1.0)]}, - ) - create_prim("/World/Asset", "Xform") - - self.camera = self.rep.create.camera() - self.render_product = self.rep.create.render_product(self.camera, RESOLUTION) - - # Setup annotators that will report groundtruth - self.rgb = self.rep.AnnotatorRegistry.get_annotator("rgb") - self.bbox_2d_tight = self.rep.AnnotatorRegistry.get_annotator("bounding_box_2d_tight") - self.instance_seg = self.rep.AnnotatorRegistry.get_annotator("instance_segmentation") - self.rgb.attach(self.render_product) - self.bbox_2d_tight.attach(self.render_product) - self.instance_seg.attach(self.render_product) - - self.kit.update() - - def _find_usd_assets(self, root, categories, max_asset_size, split, train=True): - """Look for USD files under root/category for each category specified. - For each category, generate a list of all USD files found and select - assets up to split * len(num_assets) if `train=True`, otherwise select the - remainder. - """ - references = {} - for category in categories: - all_assets = glob.glob(os.path.join(root, category, "*/*.usd"), recursive=True) - print(os.path.join(root, category, "*/*.usd")) - # Filter out large files (which can prevent OOM errors during training) - if max_asset_size is None: - assets_filtered = all_assets - else: - assets_filtered = [] - for a in all_assets: - if os.stat(a).st_size > max_asset_size * 1e6: - print(f"{a} skipped as it exceeded the max size {max_asset_size} MB.") - else: - assets_filtered.append(a) - - num_assets = len(assets_filtered) - if num_assets == 0: - raise ValueError(f"No USDs found for category {category} under max size {max_asset_size} MB.") - - if train: - references[category] = assets_filtered[: int(num_assets * split)] - else: - references[category] = assets_filtered[int(num_assets * split) :] - return references - - def _instantiate_category(self, category, references): - with self.rep.randomizer.instantiate(references, size=1, mode="reference"): - self.rep.modify.semantics([("class", category)]) - self.rep.modify.pose( - position=self.rep.distribution.uniform(OBJ_LOC_MIN, OBJ_LOC_MAX), - rotation=self.rep.distribution.uniform((0, -180, 0), (0, 180, 0)), - scale=self.rep.distribution.uniform(SCALE_MIN, SCALE_MAX), - ) - self.rep.randomizer.texture(self._get_textures(), project_uvw=True) - - def setup_replicator(self): - """Setup the replicator graph with various attributes.""" - - # Create two sphere lights - light1 = self.rep.create.light(light_type="sphere", position=(-450, 350, 350), scale=100, intensity=30000.0) - light2 = self.rep.create.light(light_type="sphere", position=(450, 350, 350), scale=100, intensity=30000.0) - - with self.rep.new_layer(): - with self.rep.trigger.on_frame(): - # Randomize light colors - with self.rep.create.group([light1, light2]): - self.rep.modify.attribute("color", self.rep.distribution.uniform((0.1, 0.1, 0.1), (1.0, 1.0, 1.0))) - - # Randomize camera position - with self.camera: - self.rep.modify.pose( - position=self.rep.distribution.uniform(CAM_LOC_MIN, CAM_LOC_MAX), look_at=(0, 0, 0) - ) - - # Randomize asset positions and textures - for category, references in self.references.items(): - self._instantiate_category(category, references) - - # Run replicator for a single iteration without triggering any writes - self.rep.orchestrator.preview() - - def __iter__(self): - return self - - def __next__(self): - # Step - trigger a randomization and a render - self.rep.orchestrator.step(rt_subframes=4) - - # Collect Groundtruth - gt = { - "rgb": self.rgb.get_data(device="cuda"), - "boundingBox2DTight": self.bbox_2d_tight.get_data(device="cpu"), - "instanceSegmentation": self.instance_seg.get_data(device="cuda"), - } - - # RGB - # Drop alpha channel - image = self.wp.to_torch(gt["rgb"])[..., :3] - - # Normalize between 0. and 1. and change order to channel-first. - image = image.float() / 255.0 - image = image.permute(2, 0, 1) - - # Bounding Box - gt_bbox = gt["boundingBox2DTight"]["data"] - - # Create mapping from categories to index - bboxes = torch.tensor(gt_bbox[["x_min", "y_min", "x_max", "y_max"]].tolist(), device="cuda") - id_to_labels = gt["boundingBox2DTight"]["info"]["idToLabels"] - prim_paths = gt["boundingBox2DTight"]["info"]["primPaths"] - - # For each bounding box, map semantic label to label index - cat_to_id = {cat: i + 1 for i, cat in enumerate(self.categories)} - semantic_labels_mapping = {int(k): v.get("class", "") for k, v in id_to_labels.items()} - semantic_labels = [cat_to_id[semantic_labels_mapping[i]] for i in gt_bbox["semanticId"]] - labels = torch.tensor(semantic_labels, device="cuda") - - # Calculate bounding box area for each area - areas = (bboxes[:, 2] - bboxes[:, 0]) * (bboxes[:, 3] - bboxes[:, 1]) - # Identify invalid bounding boxes to filter final output - valid_areas = (areas > 0.0) * (areas < (image.shape[1] * image.shape[2])) - - # Instance Segmentation - instance_data = self.wp.to_torch(gt["instanceSegmentation"]["data"].view(self.wp.int32)).squeeze() - path_to_instance_id = {v: int(k) for k, v in gt["instanceSegmentation"]["info"]["idToLabels"].items()} - - instance_list = [im[0] for im in gt_bbox] - masks = torch.zeros((len(instance_list), *instance_data.shape), dtype=bool, device="cuda") - - # Filter for the mask of each object - for i, prim_path in enumerate(prim_paths): - # Merge child instances of prim_path as one instance - for instance in path_to_instance_id: - if prim_path in instance: - masks[i] += torch.isin(instance_data, path_to_instance_id[instance]) - - target = { - "boxes": bboxes[valid_areas], - "labels": labels[valid_areas], - "masks": masks[valid_areas], - "image_id": torch.LongTensor([self.cur_idx]), - "area": areas[valid_areas], - "iscrowd": torch.BoolTensor([False] * len(bboxes[valid_areas])), # Assume no crowds - } - - self.cur_idx += 1 - return image, target - - -if __name__ == "__main__": - "Typical usage" - import argparse - import struct - - import matplotlib - import matplotlib.pyplot as plt - - parser = argparse.ArgumentParser("Dataset test") - parser.add_argument("--categories", type=str, nargs="+", required=True, help="List of object classes to use") - parser.add_argument( - "--max_asset_size", - type=float, - default=10.0, - help="Maximum asset size to use in MB. Larger assets will be skipped.", - ) - parser.add_argument( - "--num_test_images", type=int, default=10, help="number of test images to generate when executing main" - ) - parser.add_argument( - "--root", - type=str, - default=None, - help="Root directory containing USDs. If not specified, use {SHAPENET_LOCAL_DIR}_mat as root.", - ) - args, unknown_args = parser.parse_known_args() - - # If root is not specified use the environment variable SHAPENET_LOCAL_DIR with the _mat suffix as root - if args.root is None: - if "SHAPENET_LOCAL_DIR" in os.environ: - shapenet_local_dir = f"{os.path.abspath(os.environ['SHAPENET_LOCAL_DIR'])}_mat" - if os.path.exists(shapenet_local_dir): - args.root = shapenet_local_dir - if args.root is None: - print( - "root argument not specified and SHAPENET_LOCAL_DIR environment variable was not set or the path did not exist" - ) - exit() - - dataset = RandomObjects(args.root, args.categories, max_asset_size=args.max_asset_size) - from omni.replicator.core import random_colours - - categories = [LABEL_TO_SYNSET.get(c, c) for c in args.categories] - - # Iterate through dataset and visualize the output - plt.ion() - _, axes = plt.subplots(1, 2, figsize=(10, 5)) - plt.tight_layout() - - # Directory to save the example images to - out_dir = os.path.join(os.getcwd(), "_out_gen_imgs", "") - print(f"[Online-SDG] Saving images to {out_dir}") - os.makedirs(out_dir, exist_ok=True) - - image_num = 0 - for image, target in dataset: - for ax in axes: - ax.clear() - ax.axis("off") - - np_image = image.permute(1, 2, 0).cpu().numpy() - axes[0].imshow(np_image) - - num_instances = len(target["boxes"]) - # Create random colors for each instance as rgb float lists - colours = random_colours(num_instances, num_channels=3) - colours = colours.astype(float) / 255.0 - colours = colours.tolist() - - overlay = np.zeros_like(np_image) - for mask, colour in zip(target["masks"].cpu().numpy(), colours): - overlay[mask, :3] = colour - - axes[1].imshow(overlay) - mapping = {i + 1: cat for i, cat in enumerate(categories)} - labels = [SYNSET_TO_LABEL[mapping[label.item()]] for label in target["labels"]] - for bb, label, colour in zip(target["boxes"].tolist(), labels, colours): - maxint = 2 ** (struct.Struct("i").size * 8 - 1) - 1 - # if a bbox is not visible, do not draw - if bb[0] != maxint and bb[1] != maxint: - x = bb[0] - y = bb[1] - w = bb[2] - x - h = bb[3] - y - box = plt.Rectangle((x, y), w, h, fill=False, edgecolor=colour) - ax.add_patch(box) - ax.text(bb[0], bb[1], label, fontdict={"family": "sans-serif", "color": colour, "size": 10}) - - # Use plt.pause only if the backend is interactive - if matplotlib.get_backend() in ["TkAgg", "nbAgg"]: - plt.draw() - plt.pause(0.01) - fig_name = os.path.join(out_dir, f"domain_randomization_test_image_{image_num}.png") - plt.savefig(fig_name) - image_num += 1 - if dataset.exiting or (image_num >= args.num_test_images): - break - - # cleanup - dataset.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/online_generation/shapenet_utils.py b/simulation/isaac-sim/standalone_examples/replicator/online_generation/shapenet_utils.py deleted file mode 100644 index 96dfd1ce2..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/online_generation/shapenet_utils.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - - -import os - -import carb - -LABEL_TO_SYNSET = { - "table": "04379243", - "monitor": "03211117", - "phone": "04401088", - "watercraft": "04530566", - "chair": "03001627", - "lamp": "03636649", - "speaker": "03691459", - "bench": "02828884", - "plane": "02691156", - "bathtub": "02808440", - "bookcase": "02871439", - "bag": "02773838", - "basket": "02801938", - "bowl": "02880940", - "bus": "02924116", - "cabinet": "02933112", - "camera": "02942699", - "car": "02958343", - "dishwasher": "03207941", - "file": "03337140", - "knife": "03624134", - "laptop": "03642806", - "mailbox": "03710193", - "microwave": "03761084", - "piano": "03928116", - "pillow": "03938244", - "pistol": "03948459", - "printer": "04004475", - "rocket": "04099429", - "sofa": "04256520", - "washer": "04554684", - "rifle": "04090263", - "can": "02946921", - "bottle": "02876657", - "bowl": "02880940", - "earphone": "03261776", - "mug": "03797390", -} - -SYNSET_TO_LABEL = {v: k for k, v in LABEL_TO_SYNSET.items()} - - -def get_local_shape_loc(): - g_local_shape_loc = None - env_path = os.getenv("SHAPENET_LOCAL_DIR") - if env_path == None: - resolved_data_path = carb.tokens.get_tokens_interface().resolve("${data}") - g_local_shape_loc = resolved_data_path + "/shapenet" - print(f"env var SHAPENET_LOCAL_DIR not set, using default data dir {g_local_shape_loc}") - else: - g_local_shape_loc = env_path - print(f"Using local env var SHAPENET_LOCAL_DIR {env_path}") - - return g_local_shape_loc - - -async def convert(in_file, out_file, load_materials=False): - # This import causes conflicts when global - import asyncio - - import omni.kit.asset_converter - - def progress_callback(progress, total_steps): - pass - - converter_context = omni.kit.asset_converter.AssetConverterContext() - # setup converter and flags - converter_context.ignore_materials = not load_materials - # converter_context.ignore_animation = False - # converter_context.ignore_cameras = True - # converter_context.single_mesh = True - # converter_context.smooth_normals = True - # converter_context.preview_surface = False - # converter_context.support_point_instancer = False - # converter_context.embed_mdl_in_usd = False - # converter_context.use_meter_as_world_unit = True - # converter_context.create_world_as_default_root_prim = False - instance = omni.kit.asset_converter.get_instance() - task = instance.create_converter_task(in_file, out_file, progress_callback, converter_context) - - success = True - while True: - success = await task.wait_until_finished() - if not success: - await asyncio.sleep(0.1) - else: - break - return success - - -def shapenet_convert(categories=None, max_models=50, load_materials=False): - """Helper to convert shapenet assets to USD - - - Args: - categories (list of string): List of ShapeNet categories to convert. - max_models (int): Maximum number of models to convert. - load_materials (bool): If true, materials will be loaded from shapenet meshes. - """ - import asyncio - import pprint - - print("[DEPRECATION WARNING] the omni.isaac.shapenet extension will be removed next release.") - if categories is None: - print("The following categories and id's are supported:") - pprint.pprint(LABEL_TO_SYNSET) - raise ValueError(f"No categories specified via --categories argument") - # Ensure all categories specified are valid - invalid_categories = [] - for c in categories: - if c not in LABEL_TO_SYNSET.keys() and c not in LABEL_TO_SYNSET.values(): - invalid_categories.append(c) - - if invalid_categories: - raise ValueError(f"The following are not valid ShapeNet categories: {invalid_categories}") - - # This import needs to occur after kit is loaded so that physx can be discovered - local_shapenet = get_local_shape_loc() - local_shapenet_output = f"{os.path.abspath(local_shapenet)}_nomat" - if load_materials: - local_shapenet_output = f"{os.path.abspath(local_shapenet)}_mat" - os.makedirs(local_shapenet_output, exist_ok=True) - - synsets = categories - if synsets is None: - synsets = LABEL_TO_SYNSET.values() - - for synset in synsets: - print(f"\nConverting synset {synset}...") - # If synset is specified by label, convert to synset - if synset in LABEL_TO_SYNSET: - synset = LABEL_TO_SYNSET[synset] - - model_dirs = os.listdir(os.path.join(local_shapenet, synset)) - for i, model_id in enumerate(model_dirs): - if i >= max_models: - print(f"max models ({max_models}) reached, exiting conversion") - break - local_path = os.path.join(local_shapenet, synset, model_id, "models/model_normalized.obj") - - shape_name = "model_normalized_nomat" - if load_materials: - shape_name = "model_normalized_mat" - - out_dir = os.path.join(local_shapenet_output, synset, model_id) - os.makedirs(out_dir, exist_ok=True) - out_path = os.path.join(out_dir, f"{shape_name}.usd") - if not os.path.exists(out_path): - status = asyncio.get_event_loop().run_until_complete(convert(local_path, out_path, load_materials)) - if not status: - print(f"ERROR OmniConverterStatus is {status}") - print(f"---Added {out_path}") diff --git a/simulation/isaac-sim/standalone_examples/replicator/online_generation/train_shapenet.py b/simulation/isaac-sim/standalone_examples/replicator/online_generation/train_shapenet.py deleted file mode 100644 index 5046a8b72..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/online_generation/train_shapenet.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - - -"""Instance Segmentation Training Demonstration - -Use a PyTorch dataloader together with OmniKit to generate scenes and groundtruth to -train a [Mask-RCNN](https://arxiv.org/abs/1703.06870) model. -""" - - -import os -import signal - -import matplotlib.pyplot as plt -import numpy as np -from generate_shapenet import LABEL_TO_SYNSET, SYNSET_TO_LABEL, RandomObjects - - -def main(args): - device = "cuda" - - train_set = RandomObjects( - args.root, args.categories, num_assets_min=3, num_assets_max=5, max_asset_size=args.max_asset_size - ) - - def handle_exit(self, *args, **kwargs): - print("exiting dataset generation...") - train_set.exiting = True - - signal.signal(signal.SIGINT, handle_exit) - - import struct - - import torch - import torchvision - from omni.replicator.core import random_colours - from torch.utils.data import DataLoader - - # Setup data - train_loader = DataLoader(train_set, batch_size=2, collate_fn=lambda x: tuple(zip(*x))) - - # Setup Model - model = torchvision.models.detection.maskrcnn_resnet50_fpn(weights=None, num_classes=1 + len(args.categories)) - model = model.to(device) - optimizer = torch.optim.Adam(model.parameters(), lr=args.learning_rate) - - if args.visualize: - plt.ion() - fig, axes = plt.subplots(1, 2, figsize=(14, 7)) - - # Directory to save the train images to - out_dir = os.path.join(os.getcwd(), "_out_train_imgs", "") - os.makedirs(out_dir, exist_ok=True) - - for i, train_batch in enumerate(train_loader): - if i > args.max_iters or train_set.exiting: - print("Exiting ...") - train_set.close() - break - - model.train() - images, targets = train_batch - images = [i.to(device) for i in images] - targets = [{k: v.to(device) for k, v in t.items()} for t in targets] - loss_dict = model(images, targets) - loss = sum(loss for loss in loss_dict.values()) - - print(f"ITER {i} | {loss:.6f}") - - optimizer.zero_grad() - loss.backward() - optimizer.step() - - if i % 10 == 0: - model.eval() - with torch.no_grad(): - predictions = model(images[:1]) - - if args.visualize: - idx = 0 - score_thresh = 0.5 - mask_thresh = 0.5 - - pred = predictions[idx] - - np_image = images[idx].permute(1, 2, 0).cpu().numpy() - for ax in axes: - fig.suptitle(f"Iteration {i:05}", fontsize=14) - ax.cla() - ax.axis("off") - ax.imshow(np_image) - axes[0].set_title("Input") - axes[1].set_title("Input + Predictions") - - score_filter = [i for i in range(len(pred["scores"])) if pred["scores"][i] > score_thresh] - num_instances = len(score_filter) - # Create random colors for each instance as rgb float lists - colours = random_colours(num_instances, num_channels=3) - colours = colours.astype(float) / 255.0 - colours = colours.tolist() - - overlay = np.zeros_like(np_image) - for mask, colour in zip(pred["masks"], colours): - overlay[mask.squeeze().cpu().numpy() > mask_thresh, :3] = colour - - axes[1].imshow(overlay, alpha=0.5) - # If ShapeNet categories are specified with their names, convert to synset ID - # Remove this if using with a different dataset than ShapeNet - args.categories = [LABEL_TO_SYNSET.get(c, c) for c in args.categories] - mapping = {i + 1: cat for i, cat in enumerate(args.categories)} - labels = [SYNSET_TO_LABEL[mapping[label.item()]] for label in pred["labels"]] - for bb, label, colour in zip(pred["boxes"].cpu().numpy(), labels, colours): - maxint = 2 ** (struct.Struct("i").size * 8 - 1) - 1 - # if a bbox is not visible, do not draw - if bb[0] != maxint and bb[1] != maxint: - x = bb[0] - y = bb[1] - w = bb[2] - x - h = bb[3] - y - box = plt.Rectangle((x, y), w, h, fill=False, edgecolor=colour) - ax.add_patch(box) - ax.text(bb[0], bb[1], label, fontdict={"family": "sans-serif", "color": colour, "size": 10}) - - plt.draw() - fig_name = os.path.join(out_dir, f"train_image_{i}.png") - plt.savefig(fig_name) - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser("Dataset test") - parser.add_argument( - "--root", - type=str, - default=None, - help="Root directory containing ShapeNet USDs. If not specified, use {SHAPENET_LOCAL_DIR}_nomat as root.", - ) - parser.add_argument( - "--categories", type=str, nargs="+", required=True, help="List of ShapeNet categories to use (space seperated)." - ) - parser.add_argument( - "--max_asset_size", - type=float, - default=10.0, - help="Maximum asset size to use in MB. Larger assets will be skipped.", - ) - parser.add_argument("-lr", "--learning_rate", type=float, default=1e-4, help="Learning rate") - parser.add_argument("--max_iters", type=float, default=1000, help="Number of training iterations.") - parser.add_argument("--visualize", action="store_true", help="Visualize predicted masks during training.") - args, unknown_args = parser.parse_known_args() - - # If root is not specified use the environment variable SHAPENET_LOCAL_DIR with the _nomat suffix as root - if args.root is None: - if "SHAPENET_LOCAL_DIR" in os.environ: - shapenet_local_dir = f"{os.path.abspath(os.environ['SHAPENET_LOCAL_DIR'])}_nomat" - if os.path.exists(shapenet_local_dir): - args.root = shapenet_local_dir - if args.root is None: - print( - "root argument not specified and SHAPENET_LOCAL_DIR environment variable was not set or the path did not exist" - ) - exit() - - main(args) diff --git a/simulation/isaac-sim/standalone_examples/replicator/online_generation/usd_convertor.py b/simulation/isaac-sim/standalone_examples/replicator/online_generation/usd_convertor.py deleted file mode 100644 index ea17afa59..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/online_generation/usd_convertor.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - - -"""Convert ShapeNetCore V2 to USD without materials. -By only converting the ShapeNet geometry, we can more quickly load assets into scenes for the purpose of creating -large datasets or for online training of Deep Learning models. -""" - - -import argparse -import os - -from isaacsim import SimulationApp - -if "SHAPENET_LOCAL_DIR" not in os.environ: - import carb - - carb.log_error("SHAPENET_LOCAL_DIR not defined:") - carb.log_error( - "Please specify the SHAPENET_LOCAL_DIR environment variable to the location of your local shapenet database, exiting" - ) - exit() - -kit = SimulationApp() - -from isaacsim.core.utils.extensions import enable_extension - -enable_extension("omni.kit.asset_converter") - -from shapenet_utils import shapenet_convert - -parser = argparse.ArgumentParser("Convert ShapeNet assets to USD") -parser.add_argument( - "--categories", type=str, nargs="+", default=None, help="List of ShapeNet categories to convert (space seperated)." -) -parser.add_argument( - "--max_models", type=int, default=50, help="If specified, convert up to `max_models` per category, default is 50" -) -parser.add_argument( - "--load_materials", action="store_true", help="If specified, materials will be loaded from shapenet meshes" -) -args, unknown_args = parser.parse_known_args() - -# Ensure Omniverse Kit is launched via SimulationApp before shapenet_convert() is called -shapenet_convert(args.categories, args.max_models, args.load_materials) -# cleanup -kit.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/__init__.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/__init__.py deleted file mode 100644 index 3a8c414ef..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -from flying_distractors.collision_box import CollisionBox -from flying_distractors.dynamic_asset_set import DynamicAssetSet -from flying_distractors.dynamic_object import DynamicObject -from flying_distractors.dynamic_object_set import DynamicObjectSet -from flying_distractors.dynamic_shape_set import DynamicShapeSet -from flying_distractors.flying_distractors import FlyingDistractors -from utils import save_points_xyz diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/centerpose_config.yaml b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/centerpose_config.yaml deleted file mode 100644 index 2ecf687a4..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/centerpose_config.yaml +++ /dev/null @@ -1,155 +0,0 @@ -# Default rendering parameters -CONFIG: - renderer: RaytracedLighting - headless: false - width: 1920 - height: 1440 - - -# prim_type is determined by the usd file. -# To determine, open the usd file in Isaac Sim and see the prim path. If you load it in /World, the path will be /World/ -OBJECTS_TO_GENERATE: -- { part_name: SM_Mug_A2, num: 1, prim_type: SM_Mug_A2 } -- { part_name: SM_Mug_B1, num: 1, prim_type: SM_Mug_B1 } -- { part_name: SM_Mug_C1, num: 1, prim_type: SM_Mug_C1 } -- { part_name: SM_Mug_D1, num: 1, prim_type: SM_Mug_D1 } - -# Maximum force component to apply to objects to keep them in motion -FORCE_RANGE: 30 - -# Camera Intrinsics -WIDTH: 1920 -HEIGHT: 1440 -F_X: 1544.0 -F_Y: 1544.0 # F_X, F_Y should be the same. -pixel_size: 0.018 # in mm - -# Number of sphere lights added to the scene -NUM_LIGHTS: 6 - -# Minimum and maximum distances of objects away from the camera (along the optical axis) -MIN_DISTANCE: 0.4 -MAX_DISTANCE: 1.4 - -# Rotation of camera rig with respect to world frame, expressed as XYZ euler angles -CAMERA_RIG_ROTATION: -- 0 -- 0 -- 0 - -# Rotation of camera with respect to camera rig, expressed as XYZ euler angles. Please note that in this example, we -# define poses with respect to the camera rig instead of the camera prim. By using the rig's frame as a surrogate for -# the camera's frame, we effectively change the coordinate system of the camera. When -# CAMERA_RIG_ROTATION = np.array([0, 0, 0]) and CAMERA_ROTATION = np.array([0, 0, 0]), this corresponds to the default -# Isaac-Sim camera coordinate system of -z out the face of the camera, +x to the right, and +y up. When -# CAMERA_RIG_ROTATION = np.array([0, 0, 0]) and CAMERA_ROTATION = np.array([180, 0, 0]), this corresponds to -# the YCB Video Dataset camera coordinate system of +z out the face of the camera, +x to the right, and +y down. -CAMERA_ROTATION: -- 180 -- 0 -- 0 - -# Minimum and maximum XYZ euler angles for the part being trained on to be rotated, with respect to the camera rig -MIN_ROTATION_RANGE: -- -180 -- -90 -- -180 - -# Minimum and maximum XYZ euler angles for the part being trained on to be rotated, with respect to the camera rig -MAX_ROTATION_RANGE: -- 180 -- 90 -- 180 - -# How close the center of the part being trained on is allowed to be to the edge of the screen -FRACTION_TO_SCREEN_EDGE: 0.8 - -# MESH and DOME datasets -SHAPE_SCALE: -- 0.05 -- 0.05 -- 0.05 -SHAPE_MASS: 1 -OBJECT_SCALE: -- 1 -- 1 -- 1 -OBJECT_MASS: 1 - -TRAIN_PART_SCALE: # Scale for the training objects -- 0.01 -- 0.01 -- 0.01 - -# Asset paths -DISTRACTOR_ASSET_PATH: /Isaac/Props/YCB/Axis_Aligned/ -TRAIN_ASSET_PATH: /Isaac/Props/Mugs/ -DOME_TEXTURE_PATH: /NVIDIA/Assets/Skies/ - -# MESH dataset -NUM_MESH_SHAPES: 500 -NUM_MESH_OBJECTS: 200 -MESH_FRACTION_GLASS: 0.15 -MESH_FILENAMES: -- 002_master_chef_can -- 004_sugar_box -- 005_tomato_soup_can -- 006_mustard_bottle -- 007_tuna_fish_can -- 008_pudding_box -- 009_gelatin_box -- 010_potted_meat_can -- 011_banana -# - 019_pitcher_base # Do not add YCB Pitcher if using mugs to avoid confusion -- 021_bleach_cleanser -- 024_bowl -# - 025_mug # Do not add YCB Mug as distractor if training objects are already mugs -- 035_power_drill -- 036_wood_block -- 037_scissors -- 040_large_marker -- 051_large_clamp -- 052_extra_large_clamp -- 061_foam_brick - -# DOME dataset -NUM_DOME_SHAPES: 100 -NUM_DOME_OBJECTS: 100 -DOME_FRACTION_GLASS: 0.2 -DOME_TEXTURES: -- Clear/evening_road_01_4k -- Clear/kloppenheim_02_4k -- Clear/mealie_road_4k -- Clear/noon_grass_4k -- Clear/qwantani_4k -- Clear/signal_hill_sunrise_4k -- Clear/sunflowers_4k -- Clear/syferfontein_18d_clear_4k -- Clear/venice_sunset_4k -- Clear/white_cliff_top_4k -- Cloudy/abandoned_parking_4k -- Cloudy/champagne_castle_1_4k -- Cloudy/evening_road_01_4k -- Cloudy/kloofendal_48d_partly_cloudy_4k -- Cloudy/lakeside_4k -- Cloudy/sunflowers_4k -- Cloudy/table_mountain_1_4k -- Evening/evening_road_01_4k -- Indoor/adams_place_bridge_4k -- Indoor/autoshop_01_4k -- Indoor/bathroom_4k -- Indoor/carpentry_shop_01_4k -- Indoor/en_suite_4k -- Indoor/entrance_hall_4k -- Indoor/hospital_room_4k -- Indoor/hotel_room_4k -- Indoor/lebombo_4k -- Indoor/old_bus_depot_4k -- Indoor/small_empty_house_4k -- Indoor/studio_small_04_4k -- Indoor/surgery_4k -- Indoor/vulture_hide_4k -- Indoor/wooden_lounge_4k -- Night/kloppenheim_02_4k -- Night/moonlit_golf_4k -- Storm/approaching_storm_4k diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/dope_config.yaml b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/dope_config.yaml deleted file mode 100644 index 7d7427351..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/dope_config.yaml +++ /dev/null @@ -1,155 +0,0 @@ ---- -# Default rendering parameters -CONFIG: - renderer: RaytracedLighting - headless: false - width: 512 - height: 512 - -# prim_type is determined by the usd file. -# To determine, open the usd file in Isaac Sim and see the prim path. If you load it in /World, the path will be /World/ -OBJECTS_TO_GENERATE: -- { part_name: 003_cracker_box, num: 1, prim_type: _03_cracker_box } -- { part_name: 035_power_drill, num: 1, prim_type: _35_power_drill } - -# Maximum force component to apply to objects to keep them in motion -FORCE_RANGE: 30 - -# Camera Intrinsics -WIDTH: 512 -HEIGHT: 512 -F_X: 768 -F_Y: 768 -pixel_size: 0.04 # in mm - -# Number of sphere lights added to the scene -NUM_LIGHTS: 6 - -# Minimum and maximum distances of objects away from the camera (along the optical axis) -# MIN_DISTANCE: 1.0 -MIN_DISTANCE: 0.4 -# MAX_DISTANCE: 2.0 -MAX_DISTANCE: 1.4 - -# Rotation of camera rig with respect to world frame, expressed as XYZ euler angles -CAMERA_RIG_ROTATION: -- 0 -- 0 -- 0 - -# Rotation of camera with respect to camera rig, expressed as XYZ euler angles. Please note that in this example, we -# define poses with respect to the camera rig instead of the camera prim. By using the rig's frame as a surrogate for -# the camera's frame, we effectively change the coordinate system of the camera. When -# CAMERA_RIG_ROTATION = np.array([0, 0, 0]) and CAMERA_ROTATION = np.array([0, 0, 0]), this corresponds to the default -# Isaac-Sim camera coordinate system of -z out the face of the camera, +x to the right, and +y up. When -# CAMERA_RIG_ROTATION = np.array([0, 0, 0]) and CAMERA_ROTATION = np.array([180, 0, 0]), this corresponds to -# the YCB Video Dataset camera coordinate system of +z out the face of the camera, +x to the right, and +y down. -CAMERA_ROTATION: -- 180 -- 0 -- 0 - -# Minimum and maximum XYZ euler angles for the part being trained on to be rotated, with respect to the camera rig -MIN_ROTATION_RANGE: -- -180 -- -90 -- -180 - -# Minimum and maximum XYZ euler angles for the part being trained on to be rotated, with respect to the camera rig -MAX_ROTATION_RANGE: -- 180 -- 90 -- 180 - -# How close the center of the part being trained on is allowed to be to the edge of the screen -FRACTION_TO_SCREEN_EDGE: 0.9 - -# MESH and DOME datasets -SHAPE_SCALE: -- 0.05 -- 0.05 -- 0.05 -SHAPE_MASS: 1 -OBJECT_SCALE: -- 1 -- 1 -- 1 -OBJECT_MASS: 1 - -TRAIN_PART_SCALE: # Scale for the training objects -- 1 -- 1 -- 1 - -# Asset paths -DISTRACTOR_ASSET_PATH: /Isaac/Props/YCB/Axis_Aligned/ -TRAIN_ASSET_PATH: /Isaac/Props/YCB/Axis_Aligned/ -DOME_TEXTURE_PATH: /NVIDIA/Assets/Skies/ - -# MESH dataset -NUM_MESH_SHAPES: 400 -NUM_MESH_OBJECTS: 150 -MESH_FRACTION_GLASS: 0.15 -MESH_FILENAMES: -- 002_master_chef_can -- 004_sugar_box -- 005_tomato_soup_can -- 006_mustard_bottle -- 007_tuna_fish_can -- 008_pudding_box -- 009_gelatin_box -- 010_potted_meat_can -- 011_banana -- 019_pitcher_base -- 021_bleach_cleanser -- 024_bowl -- 025_mug -- 035_power_drill -- 036_wood_block -- 037_scissors -- 040_large_marker -- 051_large_clamp -- 052_extra_large_clamp -- 061_foam_brick - -# DOME dataset -NUM_DOME_SHAPES: 30 -NUM_DOME_OBJECTS: 20 -DOME_FRACTION_GLASS: 0.2 -DOME_TEXTURES: -- Clear/evening_road_01_4k -- Clear/kloppenheim_02_4k -- Clear/mealie_road_4k -- Clear/noon_grass_4k -- Clear/qwantani_4k -- Clear/signal_hill_sunrise_4k -- Clear/sunflowers_4k -- Clear/syferfontein_18d_clear_4k -- Clear/venice_sunset_4k -- Clear/white_cliff_top_4k -- Cloudy/abandoned_parking_4k -- Cloudy/champagne_castle_1_4k -- Cloudy/evening_road_01_4k -- Cloudy/kloofendal_48d_partly_cloudy_4k -- Cloudy/lakeside_4k -- Cloudy/sunflowers_4k -- Cloudy/table_mountain_1_4k -- Evening/evening_road_01_4k -- Indoor/adams_place_bridge_4k -- Indoor/autoshop_01_4k -- Indoor/bathroom_4k -- Indoor/carpentry_shop_01_4k -- Indoor/en_suite_4k -- Indoor/entrance_hall_4k -- Indoor/hospital_room_4k -- Indoor/hotel_room_4k -- Indoor/lebombo_4k -- Indoor/old_bus_depot_4k -- Indoor/small_empty_house_4k -- Indoor/studio_small_04_4k -- Indoor/surgery_4k -- Indoor/vulture_hide_4k -- Indoor/wooden_lounge_4k -- Night/kloppenheim_02_4k -- Night/moonlit_golf_4k -- Storm/approaching_storm_4k diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/ycb_config.yaml b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/ycb_config.yaml deleted file mode 100644 index 0d08ccdd1..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/config/ycb_config.yaml +++ /dev/null @@ -1,160 +0,0 @@ ---- -# Default rendering parameters -CONFIG: - renderer: RaytracedLighting - headless: false - width: 1280 - height: 720 - -# Index of part in array of classes in PoseCNN training -CLASS_NAME_TO_INDEX: - _03_cracker_box: 1 - _35_power_drill: 2 - -# prim_type is determined by the usd file. -# To determine, open the usd file in Isaac Sim and see the prim path. If you load it in /World, the path will be /World/ -OBJECTS_TO_GENERATE: -- { part_name: 003_cracker_box, num: 1, prim_type: _03_cracker_box } -- { part_name: 035_power_drill, num: 1, prim_type: _35_power_drill } - -# Maximum force component to apply to objects to keep them in motion -FORCE_RANGE: 30 - -# Camera Intrinsics -WIDTH: 1280 -HEIGHT: 720 -F_X: 665.80768 -F_Y: 665.80754 -C_X: 637.642 -C_Y: 367.56 -pixel_size: 0.04 # in mm - -# Number of sphere lights added to the scene -NUM_LIGHTS: 6 - -# Minimum and maximum distances of objects away from the camera (along the optical axis) -MIN_DISTANCE: 0.2 -MAX_DISTANCE: 1.2 - -# Rotation of camera rig with respect to world frame, expressed as XYZ euler angles -CAMERA_RIG_ROTATION: -- 0 -- 0 -- 0 - -# Rotation of camera with respect to camera rig, expressed as XYZ euler angles. Please note that in this example, we -# define poses with respect to the camera rig instead of the camera prim. By using the rig's frame as a surrogate for -# the camera's frame, we effectively change the coordinate system of the camera. When -# CAMERA_RIG_ROTATION = np.array([0, 0, 0]) and CAMERA_ROTATION = np.array([0, 0, 0]), this corresponds to the default -# Isaac-Sim camera coordinate system of -z out the face of the camera, +x to the right, and +y up. When -# CAMERA_RIG_ROTATION = np.array([0, 0, 0]) and CAMERA_ROTATION = np.array([180, 0, 0]), this corresponds to -# the YCB Video Dataset camera coordinate system of +z out the face of the camera, +x to the right, and +y down. -CAMERA_ROTATION: -- 180 -- 0 -- 0 - -# Minimum and maximum XYZ euler angles for the part being trained on to be rotated, with respect to the camera rig -MIN_ROTATION_RANGE: -- -180 -- -90 -- -180 - -# Minimum and maximum XYZ euler angles for the part being trained on to be rotated, with respect to the camera rig -MAX_ROTATION_RANGE: -- 180 -- 90 -- 180 - -# How close the center of the part being trained on is allowed to be to the edge of the screen -FRACTION_TO_SCREEN_EDGE: 0.9 - -# MESH and DOME datasets -SHAPE_SCALE: -- 0.05 -- 0.05 -- 0.05 -SHAPE_MASS: 1 -OBJECT_SCALE: -- 1 -- 1 -- 1 -OBJECT_MASS: 1 - -TRAIN_PART_SCALE: # Scale for the training objects -- 1 -- 1 -- 1 - -# Asset paths -DISTRACTOR_ASSET_PATH: /Isaac/Props/YCB/Axis_Aligned/ -TRAIN_ASSET_PATH: /Isaac/Props/YCB/Axis_Aligned/ -DOME_TEXTURE_PATH: /NVIDIA/Assets/Skies/ - -# MESH dataset -NUM_MESH_SHAPES: 500 -NUM_MESH_OBJECTS: 200 -MESH_FRACTION_GLASS: 0.15 -MESH_FILENAMES: -- 002_master_chef_can -- 004_sugar_box -- 005_tomato_soup_can -- 006_mustard_bottle -- 007_tuna_fish_can -- 008_pudding_box -- 009_gelatin_box -- 010_potted_meat_can -- 011_banana -- 019_pitcher_base -- 021_bleach_cleanser -- 024_bowl -- 025_mug -- 035_power_drill -- 036_wood_block -- 037_scissors -- 040_large_marker -- 051_large_clamp -- 052_extra_large_clamp -- 061_foam_brick - -# DOME dataset -NUM_DOME_SHAPES: 30 -NUM_DOME_OBJECTS: 20 -DOME_FRACTION_GLASS: 0.2 -DOME_TEXTURES: -- Clear/evening_road_01_4k -- Clear/kloppenheim_02_4k -- Clear/mealie_road_4k -- Clear/noon_grass_4k -- Clear/qwantani_4k -- Clear/signal_hill_sunrise_4k -- Clear/sunflowers_4k -- Clear/syferfontein_18d_clear_4k -- Clear/venice_sunset_4k -- Clear/white_cliff_top_4k -- Cloudy/abandoned_parking_4k -- Cloudy/champagne_castle_1_4k -- Cloudy/evening_road_01_4k -- Cloudy/kloofendal_48d_partly_cloudy_4k -- Cloudy/lakeside_4k -- Cloudy/sunflowers_4k -- Cloudy/table_mountain_1_4k -- Evening/evening_road_01_4k -- Indoor/adams_place_bridge_4k -- Indoor/autoshop_01_4k -- Indoor/bathroom_4k -- Indoor/carpentry_shop_01_4k -- Indoor/en_suite_4k -- Indoor/entrance_hall_4k -- Indoor/hospital_room_4k -- Indoor/hotel_room_4k -- Indoor/lebombo_4k -- Indoor/old_bus_depot_4k -- Indoor/small_empty_house_4k -- Indoor/studio_small_04_4k -- Indoor/surgery_4k -- Indoor/vulture_hide_4k -- Indoor/wooden_lounge_4k -- Night/kloppenheim_02_4k -- Night/moonlit_golf_4k -- Storm/approaching_storm_4k diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/__init__.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/__init__.py deleted file mode 100644 index 4375330fe..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -from .collision_box import CollisionBox -from .dynamic_asset_set import DynamicAssetSet -from .dynamic_object import DynamicObject -from .dynamic_object_set import DynamicObjectSet -from .dynamic_shape_set import DynamicShapeSet -from .flying_distractors import FlyingDistractors diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/collision_box.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/collision_box.py deleted file mode 100644 index bebba0eb9..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/collision_box.py +++ /dev/null @@ -1,181 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -from typing import Optional - -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.api.objects import FixedCuboid -from isaacsim.core.prims import XFormPrim -from isaacsim.core.utils.prims import define_prim -from pxr import Usd, UsdGeom - - -class CollisionBox(XFormPrim): - """Creates a fixed box with collisions enabled, and provides an API to determine world coordinates of a random - location in the interior of the collision box. - - Args: - prim_path (str): top-level prim path (of the collision box) of the Prim to encapsulate or create. - name (str): shortname to be used as a key by Scene class. Note: needs to be unique if the object is added to the - Scene. - position (Optional[np.ndarray], optional): position in the world frame of the collision box. Shape is (3, ). - Defaults to None, which means left unchanged. - translation (Optional[np.ndarray], optional): translation in the local frame of the collision box (with respect - to its parent prim). Shape is (3, ). Defaults to None, which means - left unchanged. - orientation (Optional[np.ndarray], optional): quaternion orientation in the world/local frame of the collision - box (depends if translation or position is specified). Quaternion - is scalar-first (w, x, y, z). Shape is (4, ). Defaults to None, - which means left unchanged. - scale (Optional[np.ndarray], optional): local scale to be applied to the collision box's dimensions. Shape is - (3, ). Defaults to None, which means left unchanged. - width (float): width of the collision box interior in world units (if unrotated, corresponds to x direction). - Defaults to 1.0. - height (float): height of the collision box interior in world units (if unrotated, corresponds to y direction). - Defaults to 1.0. - depth (float): depth of the collision box interior in world units (if unrotated, corresponds to z direction). - Defaults to 1.0. - thickness (float, optional): thickness of the collision box walls in world units. Defaults to 0.2. - visible (bool, optional): set to false for an invisible prim in the stage while rendering. Defaults to False. - """ - - def __init__( - self, - prim_path: str, - name: str, - position: Optional[np.ndarray] = None, - translation: Optional[np.ndarray] = None, - orientation: Optional[np.ndarray] = None, - scale: Optional[np.ndarray] = None, - width: float = 1.0, - height: float = 1.0, - depth: float = 1.0, - thickness: float = 0.2, - visible: bool = False, - ): - self.world = World.instance() - - define_prim(prim_path=prim_path, prim_type="Xform") - XFormPrim.__init__( - self, - prim_paths_expr=prim_path, - name=name, - positions=None if position is None else np.array([position]), - translations=None if translation is None else np.array([translation]), - orientations=None if orientation is None else np.array([orientation]), - scales=None if scale is None else np.array([scale]), - visibilities=None if visible is None else np.array([visible]), - ) - - self.width = width - self.height = height - self.depth = depth - self.thickness = thickness - self.visible = visible - self._create_collision_box() - - def _create_face(self, suffix, translation, size): - """Create a face/wall of the Collision Box, which has collisions enabled. - - Args: - suffix (str): suffix used for the name of the face so it can be retrieved from the scene. The name of the - face has the form "{collision_box_name}_{suffix}" - translation (np.ndarray): translation of the center of the face (wall) from the center of the Collision - Box, in stage units. Shape is (3, ). - size (np.ndarray): dimensions of the face (wall) in the X, Y, and Z directions. Dimensions are in stage - units. Shape is (3, ). - """ - face_name = f"{self.name}_{suffix}" - face_path = f"{self.prim_paths[0]}/{face_name}" - face_cuboid = FixedCuboid( - prim_path=face_path, # The prim path of the cube in the USD stage - name=face_name, # The unique name used to retrieve the object from the scene later on - translation=translation, # Using the current stage units which is cms by default. - scale=size, # most arguments accept mainly numpy arrays. - size=1.0, - visible=self.visible, - ) - self.world.scene.add(face_cuboid) - - def _create_collision_box(self): - """Create a Collision Box. The Collision Box consists of 6 faces/walls forming a static box-like volume. Each - wall of the Collision Box has collisions enabled. - """ - - dx = self.width / 2.0 + self.thickness / 2.0 - dy = self.height / 2.0 + self.thickness / 2.0 - dz = self.depth / 2.0 + self.thickness / 2.0 - - floor_center = np.array([0, 0, -dz]) - floor_dimensions = np.array([self.width, self.height, self.thickness]) - self._create_face("floor", floor_center, floor_dimensions) - - ceiling_center = np.array([0, 0, +dz]) - ceiling_dimensions = np.array([self.width, self.height, self.thickness]) - self._create_face("ceiling", ceiling_center, ceiling_dimensions) - - left_wall_center = np.array([dx, 0, 0]) - left_wall_dimensions = np.array([self.thickness, self.height, self.depth]) - self._create_face("left_wall", left_wall_center, left_wall_dimensions) - - right_wall_center = np.array([-dx, 0, 0]) - right_wall_dimensions = np.array([self.thickness, self.height, self.depth]) - self._create_face("right_wall", right_wall_center, right_wall_dimensions) - - front_wall_center = np.array([0, dy, 0]) - front_wall_dimensions = np.array([self.width, self.thickness, self.depth]) - self._create_face("front_wall", front_wall_center, front_wall_dimensions) - - back_wall_center = np.array([0, -dy, 0]) - back_wall_dimensions = np.array([self.width, self.thickness, self.depth]) - self._create_face("back_wall", back_wall_center, back_wall_dimensions) - - def get_random_local_translation(self): - """Get a random translation within the Collision Box in local coordinates. Translations are within the - volumetric region contained by the inner walls of the Collision Box. The local coordinate frame is considered - to be the frame of the prim at self.prim_path (center of the Collision Box). - - Returns: - np.ndarray: random translation within the Collision Box in the local frame of the Collision Box. Shape is - (3, ). - """ - - dim_fractions = np.random.rand(3) - - tx = dim_fractions[0] * self.width - self.width / 2.0 - ty = dim_fractions[1] * self.height - self.height / 2.0 - tz = dim_fractions[2] * self.depth - self.depth / 2.0 - - translation = np.array([tx, ty, tz]) - - return translation - - def get_random_position(self): - """Get a random position within the Collision Box in world coordinates. Positions are within the volumetric - region contained by the inner walls of the Collision Box. - - Returns: - np.ndarray: random position within the Collision Box in the world frame. Shape is (3, ). - """ - - box_prim = self.world.stage.GetPrimAtPath(self.prim_paths[0]) - - box_transform_matrix = UsdGeom.Xformable(box_prim).ComputeLocalToWorldTransform(Usd.TimeCode.Default()) - - box_to_world = np.transpose(box_transform_matrix) - - random_local_translation = self.get_random_local_translation() - - random_local_translation_homogenous = np.pad(random_local_translation, ((0, 1)), constant_values=1.0) - - position_homogenous = box_to_world @ random_local_translation_homogenous - - position = position_homogenous[:-1] - - return position diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_asset_set.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_asset_set.py deleted file mode 100644 index 8457ccfab..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_asset_set.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import itertools -import math -from abc import ABC, abstractmethod -from typing import Optional - -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.prims import RigidPrim - -from .collision_box import CollisionBox - - -class DynamicAssetSet(ABC): - """Container class to hold and manage dynamic assets, providing an API to keep assets in motion within a collision - box, and to allow various properties of the assets to be randomized. - - Args: - set_prim_path (str): prim path of the parent Prim to create, which contains all the assets in the asset set as - its children. - set_name (str): name of the parent prim in the scene. - asset_prim_path_base_prefix (str): prefix of what the assets are called in the stage (prim path base name). - asset_name_prefix (str): prefix of the assets' names in the scene. - num_assets (int): number of assets in the asset set. - collision_box (CollisionBox): collision box in which to place assets, and allow assets to move within. - scale (Optional[np.ndarray], optional): local scale to be applied to each asset's dimensions. Shape is (3, ). - Defaults to None, which means left unchanged. - mass (Optional[float], optional): mass of each asset in kg. Defaults to None. - fraction_glass (int, optional): fraction of assets for which glass material should be applied. - """ - - def __init__( - self, - set_prim_path: str, - set_name: str, - asset_prim_path_base_prefix: str, - asset_name_prefix: str, - num_assets: int, - collision_box: CollisionBox, - scale: Optional[np.ndarray] = None, - mass: Optional[float] = None, - fraction_glass: float = 0.0, - ): - self.world = World.instance() - - self.set_prim_path = set_prim_path - self.set_name = set_name - self.asset_prim_path_base_prefix = asset_prim_path_base_prefix - self.asset_name_prefix = asset_name_prefix - self.num_assets = num_assets - self.collision_box = collision_box - self.scale = scale - self.mass = mass - self.fraction_glass = fraction_glass - self.asset_count = 0 - self.asset_names = [] - self.glass_asset_paths = [] - self.nonglass_asset_paths = [] - self.glass_assets = [] - self.nonglass_assets = [] - self.glass_mats = [] - self._rigid_prims = None - - def _create_random_dynamic_asset_set(self): - """Create self.num_assets assets and add them to the dynamic asset set.""" - - self.world.stage.DefinePrim(self.set_prim_path, "Xform") - - num_glass = math.floor(self.num_assets * self.fraction_glass) - - for i in range(self.num_assets): - - if i < num_glass: - self._create_random_dynamic_asset(glass=True) - else: - self._create_random_dynamic_asset() - - @abstractmethod - def _create_random_dynamic_asset(self, glass=False): - pass - - def apply_force_to_assets(self, force_limit): - """Apply a force in a random direction to each asset in the dynamic asset set. - - Args: - force_limit (float): maximum force component to apply. - """ - if self._rigid_prims is None: - self._rigid_prims = [] - for path in itertools.chain(self.glass_asset_paths, self.nonglass_asset_paths): - rigid_prim = RigidPrim(path) - rigid_prim.initialize() - self._rigid_prims.append(rigid_prim) - - for rigid_prim in self._rigid_prims: - random_force = np.random.uniform(-force_limit, force_limit, 3).tolist() - rigid_prim.apply_forces_and_torques_at_pos(random_force, is_global=False) - - def randomize_glass_color(self): - """Randomize the color of the assets in the dynamic asset set with a glass material applied.""" - - for asset in itertools.chain(self.glass_assets): - try: - glass_mat = asset.get_applied_visual_materials()[0] - except: - glass_mat = asset.get_applied_visual_material() - glass_mat.set_color(np.random.rand(3)) - - def reset_position(self): - """Reset the positions of assets in the dynamic asset set. The positions at which to place assets are randomly - chosen such that they are within the collision box. - """ - - for asset in itertools.chain(self.glass_assets, self.nonglass_assets): - position = self.collision_box.get_random_position() - asset.set_world_pose(position) diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_object.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_object.py deleted file mode 100644 index 3b4ebf6f4..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_object.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -from typing import Optional - -import numpy as np -from isaacsim.core.prims import GeometryPrim, RigidPrim -from isaacsim.core.utils.prims import get_prim_at_path, is_prim_path_valid -from isaacsim.core.utils.stage import add_reference_to_stage -from pxr import UsdGeom - - -class DynamicObject(RigidPrim): - """Creates and adds a prim to stage from USD reference path, and wraps the prim with RigidPrim and GeometryPrim to - provide access to APIs for rigid body attributes, physics materials and collisions. Please note that this class - assumes the object has only a single mesh prim defining its geometry. - - Args: - usd_path (str): USD reference path the Prim refers to. - prim_path (str): prim path of the Prim to encapsulate or create. - mesh_path (str): prim path of the underlying mesh Prim. - name (str, optional): shortname to be used as a key by Scene class. Note: needs to be unique if the object is - added to the Scene. Defaults to "dynamic_object". - position (Optional[np.ndarray], optional): position in the world frame of the prim. Shape is (3, ). Defaults to - None, which means left unchanged. - translation (Optional[np.ndarray], optional): translation in the local frame of the prim (with respect to its - parent prim). Shape is (3, ). Defaults to None, which means left - unchanged. - orientation (Optional[np.ndarray], optional): quaternion orientation in the world/local frame of the prim - (depends if translation or position is specified). Quaternion is - scalar-first (w, x, y, z). Shape is (4, ). Defaults to None, which - means left unchanged. - scale (Optional[np.ndarray], optional): local scale to be applied to the prim's dimensions. Shape is (3, ). - Defaults to None, which means left unchanged. - visible (bool, optional): set to false for an invisible prim in the stage while rendering. Defaults to True. - mass (Optional[float], optional): mass in kg. Defaults to None. - linear_velocity (Optional[np.ndarray], optional): linear velocity in the world frame. Defaults to None. - angular_velocity (Optional[np.ndarray], optional): angular velocity in the world frame. Defaults to None. - """ - - def __init__( - self, - usd_path: str, - prim_path: str, - mesh_path: str, - name: str = "dynamic_object", - position: Optional[np.ndarray] = None, - translation: Optional[np.ndarray] = None, - orientation: Optional[np.ndarray] = None, - scale: Optional[np.ndarray] = None, - visible: bool = True, - mass: Optional[float] = None, - linear_velocity: Optional[np.ndarray] = None, - angular_velocity: Optional[np.ndarray] = None, - ) -> None: - - if is_prim_path_valid(mesh_path): - prim = get_prim_at_path(mesh_path) - if not prim.IsA(UsdGeom.Mesh): - raise Exception("The prim at path {} cannot be parsed as a Mesh object".format(mesh_path)) - - self.usd_path = usd_path - - add_reference_to_stage(usd_path=usd_path, prim_path=prim_path) - - GeometryPrim( - mesh_path, - name=name, - translations=None if translation is None else np.array([translation]), - orientations=None if orientation is None else np.array([orientation]), - visibilities=None if visible is None else np.array([visible]), - collisions=[True], - ).set_collision_approximations(["convexHull"]) - - RigidPrim.__init__( - self, - prim_paths_expr=prim_path, - name=name, - positions=None if position is None else np.array([position]), - translations=None if translation is None else np.array([translation]), - orientations=None if orientation is None else np.array([orientation]), - scales=None if scale is None else np.array([scale]), - visibilities=None if visible is None else np.array([visible]), - masses=None if mass is None else np.array([mass]), - linear_velocities=None if linear_velocity is None else np.array([linear_velocity]), - angular_velocities=None if angular_velocity is None else np.array([angular_velocity]), - ) diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_object_set.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_object_set.py deleted file mode 100644 index 3c8ae3c46..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_object_set.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import random -from typing import List, Optional - -import numpy as np -from isaacsim.core.api.materials.omni_glass import OmniGlass - -from .collision_box import CollisionBox -from .dynamic_asset_set import DynamicAssetSet -from .dynamic_object import DynamicObject - - -class DynamicObjectSet(DynamicAssetSet): - """Container class to hold and manage dynamic objects, providing an API to keep objects in motion within a collision - box, and to allow various properties of the assets to be randomized. Please note that this class assumes that - each referenced asset in usd_path_list has only a single mesh prim defining its geometry. - - Args: - set_prim_path (str): prim path of the parent Prim to create, which contains all the objects in the object set - as its children. - set_name (str): name of the parent prim in the scene. - usd_path_list (List[str]): list of possible USD reference paths that the prims of each dynamic object in the - dynamic object set refer to. - mesh_list (List[str]): list of prim path base names for underlying mesh prims. Each base name in mesh_list - corresponds to the mesh prim of the referenced asset in usd_path_list. - asset_prim_path_base_prefix (str): prefix of what the objects are called in the stage (prim path base name). - asset_name_prefix (str): prefix of the objects' names in the scene. - num_assets (int): number of objects in the object set. - collision_box (CollisionBox): collision box in which to place objects, and allow objects to move within. - scale (Optional[np.ndarray], optional): local scale to be applied to each object's dimensions. Shape is (3, ). - Defaults to None, which means left unchanged. - mass (Optional[float], optional): mass of each object in kg. Defaults to None. - fraction_glass (int, optional): fraction of objects for which glass material should be applied. - """ - - def __init__( - self, - set_prim_path: str, - set_name: str, - usd_path_list: List[str], - mesh_list: List[str], - asset_prim_path_base_prefix: str, - asset_name_prefix: str, - num_assets: int, - collision_box: CollisionBox, - scale: Optional[np.ndarray] = None, - mass: Optional[float] = None, - fraction_glass: float = 0.0, - ): - - self.usd_path_list = usd_path_list - self.mesh_list = mesh_list - self.glass_object_mesh_paths = [] - self.nonglass_object_mesh_paths = [] - - if len(usd_path_list) != len(mesh_list): - raise Exception("usd_path_list and mesh_list must contain the same number of elements") - - self.mesh_map = self._create_mesh_map(usd_path_list, mesh_list) - - super().__init__( - set_prim_path, - set_name, - asset_prim_path_base_prefix, - asset_name_prefix, - num_assets, - collision_box, - scale, - mass, - fraction_glass, - ) - - self._create_random_dynamic_asset_set() - - def _create_mesh_map(self, usd_path_list, mesh_list): - """Gets a mapping from USD reference paths to the base name of the corresponding mesh prim in the referenced USD - file. - - Args: - usd_path_list (List[str]): List of possible USD reference paths that the prims of each dynamic object in the - dynamic object set refer to. - mesh_list (List[str]): List of prim path base names for underlying mesh prims. Each base name in mesh_list - corresponds to the mesh prim of the referenced asset in usd_path_list. - - Returns: - Dict: Mapping from USD reference paths to the base name of the corresponding mesh prim in the referenced USD - file. - """ - - mesh_map = {} - - for usd_path, mesh_name in zip(usd_path_list, mesh_list): - mesh_map[usd_path] = mesh_name - - return mesh_map - - def _create_random_dynamic_asset(self, glass=False): - """Creates a random dynamic object and adds it to the scene. The reference path of the object is randomly chosen - from self.usd_path_list. - - Args: - glass (bool, optional): flag to specify whether the created object should have a glass material applied. - Defaults to False. - """ - - object_name = f"{self.asset_name_prefix}_{self.asset_count}" - - if glass: - object_path = f"{self.set_prim_path}/{self.asset_prim_path_base_prefix}_{self.asset_count}" - else: - object_path = f"{self.set_prim_path}/{self.asset_prim_path_base_prefix}_nonglass_{self.asset_count}" - - usd_path = random.choice(self.usd_path_list) - mesh_path = f"{object_path}/{self.mesh_map[usd_path]}" - - position = self.collision_box.get_random_position() - - dynamic_prim = DynamicObject( - usd_path=usd_path, - prim_path=object_path, - mesh_path=mesh_path, - name=object_name, - position=position, - scale=self.scale, - mass=self.mass, - ) - - self.asset_names.append(object_name) - - if glass: - color = np.random.rand(3) - material = OmniGlass( - object_path + "_glass", - name=object_name + "_glass", - ior=1.25, - depth=0.001, - thin_walled=False, - color=color, - ) - self.glass_mats.append(material) - dynamic_prim.apply_visual_materials([material]) - self.glass_asset_paths.append(object_path) - self.glass_assets.append(dynamic_prim) - self.glass_object_mesh_paths.append(mesh_path) - else: - self.nonglass_asset_paths.append(object_path) - self.nonglass_assets.append(dynamic_prim) - self.nonglass_object_mesh_paths.append(mesh_path) - - self.world.scene.add(dynamic_prim) - self.asset_count += 1 diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_shape_set.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_shape_set.py deleted file mode 100644 index 13dd75550..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/dynamic_shape_set.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import random -from typing import Optional - -import numpy as np -from isaacsim.core.api.materials.omni_glass import OmniGlass -from isaacsim.core.api.objects import DynamicCapsule, DynamicCone, DynamicCuboid, DynamicCylinder, DynamicSphere - -from .collision_box import CollisionBox -from .dynamic_asset_set import DynamicAssetSet - - -class DynamicShapeSet(DynamicAssetSet): - """Container class to hold and manage dynamic shapes, providing an API to keep shapes in motion within a collision - box, and to allow various properties of the shapes to be randomized. - - Args: - set_prim_path (str): prim path of the parent Prim to create, which contains all the shapes in the shape set - as its children. - set_name (str): name of the parent prim in the scene. - asset_prim_path_base_prefix (str): prefix of what the shapes are called in the stage (prim path base name). - asset_name_prefix (str): prefix of the shapes' names in the scene. - num_assets (int): number of shapes in the shape set. - collision_box (CollisionBox): collision box in which to place shapes, and allow shapes to move within. - scale (Optional[np.ndarray], optional): local scale to be applied to each shape's dimensions. Shape is (3, ). - Defaults to None, which means left unchanged. - mass (Optional[float], optional): mass of each shape in kg. Defaults to None. - fraction_glass (int, optional): fraction of shapes for which glass material should be applied. - """ - - def __init__( - self, - set_prim_path: str, - set_name: str, - asset_prim_path_base_prefix: str, - asset_name_prefix: str, - num_assets: int, - collision_box: CollisionBox, - scale: Optional[np.ndarray] = None, - mass: Optional[float] = None, - fraction_glass: float = 0.0, - ): - - super().__init__( - set_prim_path, - set_name, - asset_prim_path_base_prefix, - asset_name_prefix, - num_assets, - collision_box, - scale, - mass, - fraction_glass, - ) - - self._create_random_dynamic_asset_set() - - def _create_random_dynamic_asset(self, glass=False): - """Creates a random dynamic shape (Cuboid, Sphere, Cylinder, Cone, or Capsule) and adds it to the scene. - - Args: - glass (bool, optional): flag to specify whether the created shape should have a glass material applied. - Defaults to False. - """ - - prim_type = [DynamicCapsule, DynamicCone, DynamicCuboid, DynamicCylinder, DynamicSphere] - - shape_name = f"{self.asset_name_prefix}_{self.asset_count}" - - if glass: - shape_path = f"{self.set_prim_path}/{self.asset_prim_path_base_prefix}_{self.asset_count}" - else: - shape_path = f"{self.set_prim_path}/{self.asset_prim_path_base_prefix}_nonglass_{self.asset_count}" - - position = self.collision_box.get_random_position() - - shape_prim = random.choice(prim_type)( - prim_path=shape_path, # The prim path of the cube in the USD stage - name=shape_name, # The unique name used to retrieve the object from the scene later on - position=position, # Using the current stage units which is meters by default. - scale=self.scale, - mass=self.mass, - ) - - self.asset_names.append(shape_name) - - if glass: - color = np.random.rand(3) - material = OmniGlass( - shape_path + "_glass", name=shape_name + "_glass", ior=1.25, depth=0.001, thin_walled=False, color=color - ) - self.glass_mats.append(material) - shape_prim.apply_visual_material(material) - self.glass_asset_paths.append(shape_path) - self.glass_assets.append(shape_prim) - else: - self.nonglass_asset_paths.append(shape_path) - self.nonglass_assets.append(shape_prim) - - self.world.scene.add(shape_prim) - self.asset_count += 1 diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/flying_distractors.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/flying_distractors.py deleted file mode 100644 index ea6cab680..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/flying_distractors/flying_distractors.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import itertools - -from isaacsim.core.api import World - -from .dynamic_object_set import DynamicObjectSet -from .dynamic_shape_set import DynamicShapeSet - - -class FlyingDistractors: - """Container class to hold and manage both dynamic shape sets and dynamic object sets simultaneously. This class - provides an API to keep assets in each asset set in motion within their respective collision boxes, to show/hide - the assets of all the asset sets managed by this class, and to allow various properties of the assets of all the - asset sets managed by this class to be randomized. - """ - - def __init__(self): - self.world = World.instance() - self.shape_sets = [] - self.object_sets = [] - - def add(self, asset_set): - """Add an asset set to be managed by this FlyingDistractors object. - - Args: - asset_set (Union[DynamicShapeSet, DynamicObjectSet]): the asset set to add. - - Raises: - Exception: if asset_set is neither a DynamicShapeSet nor a DynamicObjectSet. - """ - - if isinstance(asset_set, DynamicShapeSet): - self.shape_sets.append(asset_set) - elif isinstance(asset_set, DynamicObjectSet): - self.object_sets.append(asset_set) - else: - raise Exception("The asset set provided is not of type DynamicShapeSet or DynamicObjectSet") - - def set_visible(self, visible): - """Sets the visibility of all assets contained in the managed asset sets. - - Args: - visible (bool): flag to set the visibility of all assets contained in the managed asset sets. - """ - - for asset_set in itertools.chain(self.shape_sets, self.object_sets): - for asset_name in asset_set.asset_names: - object_xform = self.world.scene.get_object(asset_name) - try: - object_xform.set_visibilities([visible]) - except: - object_xform.set_visibility(visible) - - def reset_asset_positions(self): - """Reset the positions of all assets contained in the managed asset sets to be within its corresponding - collision box. - """ - - for asset_set in itertools.chain(self.shape_sets, self.object_sets): - asset_set.reset_position() - - def apply_force_to_assets(self, force_limit): - """Apply random forces to all assets contained in the managed asset sets. - - Args: - force_limit (float): maximum force component to apply. - """ - - for asset_set in itertools.chain(self.shape_sets, self.object_sets): - asset_set.apply_force_to_assets(force_limit) - - def randomize_asset_glass_color(self): - """Randomize color of assets in the managed asset sets with glass material applied.""" - - for asset_set in itertools.chain(self.shape_sets, self.object_sets): - asset_set.randomize_glass_color() diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_generation.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_generation.py deleted file mode 100644 index e511d28ff..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_generation.py +++ /dev/null @@ -1,613 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -"""Generate a [DOPE, CenterPose, YCBVideo] synthetic datasets -""" - -import argparse -import datetime -import os -import signal - -import numpy as np -import torch -import yaml -from isaacsim import SimulationApp - -parser = argparse.ArgumentParser("Pose Generation data generator") -parser.add_argument("--num_mesh", type=int, default=30, help="Number of frames to record similar to MESH dataset") -parser.add_argument("--num_dome", type=int, default=30, help="Number of frames to record similar to DOME dataset") -parser.add_argument( - "--dome_interval", - type=int, - default=1, - help="Number of frames to capture before switching DOME background. When generating large datasets, increasing this interval will reduce time taken. A good value to set is 10.", -) -parser.add_argument("--output_folder", "-o", type=str, default="output", help="Output directory.") -parser.add_argument("--use_s3", action="store_true", help="Saves output to s3 bucket. Only supported by DOPE writer.") -parser.add_argument( - "--bucket", - type=str, - default=None, - help="Bucket name to store output in. See naming rules: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html", -) -parser.add_argument("--s3_region", type=str, default="us-east-1", help="s3 region.") -parser.add_argument("--endpoint", "--endpoint_url", type=str, default=None, help="s3 endpoint to write to.") -parser.add_argument( - "--writer", - type=str, - default="dope", - help="Which writer to use to output data. Choose between: [DOPE, CenterPose, YCBVideo]", -) -parser.add_argument("--debug", action="store_true", help="Write debug images for the writer.") -parser.add_argument( - "--test", - action="store_true", - help="Generates data for testing. Hardcodes the pose of the object to compare output data with expected data to ensure that generation is correct.", -) - -args, unknown_args = parser.parse_known_args() - -# Do not write to s3 if in test mode -if args.test: - args.use_s3 = False - -if args.use_s3 and (args.endpoint is None or args.bucket is None): - raise Exception("To use s3, --endpoint and --bucket must be specified.") - -CONFIG_FILES = { - "dope": "config/dope_config.yaml", - "ycbvideo": "config/ycb_config.yaml", - "centerpose": "config/centerpose_config.yaml", -} -TEST_CONFIG_FILES = { - "dope": "pose_tests/dope/test_dope_config.yaml", - "ycbvideo": "pose_tests/ycbvideo/test_ycb_config.yaml", -} - -# Path to config file: -cf_map = TEST_CONFIG_FILES if args.test else CONFIG_FILES -CONFIG_FILE = cf_map[args.writer.lower()] - -CONFIG_FILE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), CONFIG_FILE) - -with open(CONFIG_FILE_PATH) as f: - config_data = yaml.full_load(f) - -OBJECTS_TO_GENERATE = config_data["OBJECTS_TO_GENERATE"] - -kit = SimulationApp(launch_config=config_data["CONFIG"]) - -import math - -import carb -import omni.replicator.core as rep -from isaacsim.core.api import World -from isaacsim.core.prims import XFormPrim -from isaacsim.core.utils.rotations import euler_angles_to_quat -from isaacsim.core.utils.semantics import add_update_semantics -from isaacsim.replicator.writers import PoseWriter, YCBVideoWriter -from isaacsim.storage.native import get_assets_root_path - -# Since the simulation is mostly collision checking, a larger physics dt can be used to speed up the object movements -world = World(physics_dt=1.0 / 30.0) -world.reset() - -from flying_distractors.collision_box import CollisionBox -from flying_distractors.dynamic_object import DynamicObject -from flying_distractors.dynamic_object_set import DynamicObjectSet -from flying_distractors.dynamic_shape_set import DynamicShapeSet -from flying_distractors.flying_distractors import FlyingDistractors -from isaacsim.core.utils.random import get_random_world_pose_in_view -from isaacsim.core.utils.transformations import get_world_pose_from_relative -from pose_tests.test_utils import clean_output_dir, run_pose_generation_test - - -class RandomScenario(torch.utils.data.IterableDataset): - def __init__( - self, - num_mesh, - num_dome, - dome_interval, - output_folder, - use_s3=False, - endpoint="", - s3_region="us-east-1", - writer="dope", - bucket="", - test=False, - debug=False, - ): - self.test = test - self.writer_format = writer.lower() - self.debug = debug - - if writer == "ycbvideo": - self.writer_helper = YCBVideoWriter - elif writer == "dope" or writer == "centerpose": - self.writer_helper = PoseWriter - else: - raise Exception( - "Invalid writer specified. Choose between [DOPE, CenterPose, YCBVideo]. Run with --help for more options." - ) - - self.result = True - assets_root_path = get_assets_root_path() - if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - self.result = False - return - else: - print(f"[SDG] Using Isaac Sim assets from: {assets_root_path}") - self.dome_texture_path = assets_root_path + config_data["DOME_TEXTURE_PATH"] - self.distractor_asset_path = assets_root_path + config_data["DISTRACTOR_ASSET_PATH"] - self.train_asset_path = assets_root_path + config_data["TRAIN_ASSET_PATH"] - - self.train_parts = [] - self.train_part_mesh_path_to_prim_path_map = {} - self.mesh_distractors = FlyingDistractors() - self.dome_distractors = FlyingDistractors() - self.current_distractors = None - - self.num_mesh = max(0, num_mesh) if not self.test else 5 - self.num_dome = max(0, num_dome) if not self.test else 0 - self.train_size = self.num_mesh + self.num_dome - self.dome_interval = dome_interval - - self._output_folder = output_folder if use_s3 else os.path.join(os.getcwd(), output_folder) - self.use_s3 = use_s3 - self.endpoint = endpoint - self.s3_region = s3_region - self.bucket = bucket - - self._setup_world() - - self.cur_idx = 0 - self.exiting = False - self.last_frame_reached = False - - # Clean up output folder ahead of test - if not self.use_s3 and self.test: - clean_output_dir(self._output_folder) - - # Disable capture on play and async rendering - self._carb_settings = carb.settings.get_settings() - self._carb_settings.set("/omni/replicator/captureOnPlay", False) - self._carb_settings.set("/app/asyncRendering", False) - self._carb_settings.set("/omni/replicator/asyncRendering", False) - - signal.signal(signal.SIGINT, self._handle_exit) - - def _handle_exit(self, *args, **kwargs): - print("[SDG] Exiting dataset generation..") - self.exiting = True - - def _setup_world(self): - """Populate scene with assets and prepare for synthetic data generation.""" - self._setup_camera() - - rep.settings.set_render_rtx_realtime() - - # Allow flying distractors to float - world.get_physics_context().set_gravity(0.0) - - collision_box = self._setup_collision_box() - - world.scene.add(collision_box) - - self._setup_distractors(collision_box) - - self._setup_train_objects() - - self._setup_randomizers() - - # Update the app a few times to make sure the materials are fully loaded and world scene objects are registered - for _ in range(5): - kit.app.update() - - # Setup writer - if self.writer_helper == PoseWriter: - self.writer = rep.WriterRegistry.get("PoseWriter") - self.writer.initialize( - output_dir=self._output_folder, - write_debug_images=self.debug, - format=self.writer_format, - skip_empty_frames=False, - use_s3=self.use_s3, - s3_bucket=self.bucket, - s3_endpoint_url=self.endpoint, - s3_region=self.s3_region, - ) - else: - self.writer_helper.register_pose_annotator(config_data=config_data) - self.writer = self.writer_helper.setup_writer( - config_data=config_data, - writer_config={ - "output_folder": self._output_folder, - "train_size": self.train_size, - }, - ) - self.writer.attach([self.render_product]) - - self.dome_distractors.set_visible(False) - - # Generate the replicator graphs without triggering any writing - rep.orchestrator.preview() - - def _setup_camera(self): - focal_length_mm = (config_data["F_X"] + config_data["F_Y"]) * config_data["pixel_size"] / 2 - horiztonal_aperture_mm = config_data["pixel_size"] * config_data["WIDTH"] - - print( - f"[SDG] Creating camera with focal length: {round(focal_length_mm, 2)}mm, horizontal aperture: {round(horiztonal_aperture_mm, 2)}mm" - ) - # Setup camera and render product - # See https://docs.omniverse.nvidia.com/py/replicator/1.10.10/source/extensions/omni.replicator.core/docs/API.html#cameras - self.camera = rep.create.camera( - position=(0, 0, 0), - rotation=np.array(config_data["CAMERA_RIG_ROTATION"]), - focal_length=focal_length_mm, - horizontal_aperture=horiztonal_aperture_mm, - clipping_range=(0.01, 10000), - ) - - self.render_product = rep.create.render_product(self.camera, (config_data["WIDTH"], config_data["HEIGHT"])) - - camera_rig_path = str(rep.utils.get_node_targets(self.camera.node, "inputs:primsIn")[0]) - self.camera_path = camera_rig_path + "/Camera" - - with rep.get.prims(prim_types=["Camera"]): - rep.modify.pose( - rotation=rep.distribution.uniform( - np.array(config_data["CAMERA_ROTATION"]), np.array(config_data["CAMERA_ROTATION"]) - ) - ) - - self.rig = XFormPrim(camera_rig_path) - - def _setup_collision_box(self): - # Create a collision box in view of the camera, allowing distractors placed in the box to be within - # [MIN_DISTANCE, MAX_DISTANCE] of the camera. The collision box will be placed in front of the camera, - # regardless of CAMERA_ROTATION or CAMERA_RIG_ROTATION. - - self.fov_x = 2 * math.atan(config_data["WIDTH"] / (2 * config_data["F_X"])) - self.fov_y = 2 * math.atan(config_data["HEIGHT"] / (2 * config_data["F_Y"])) - theta_x = self.fov_x / 2.0 - theta_y = self.fov_y / 2.0 - - # Collision box dimensions lower than 1.3 do not work properly - collision_box_width = max(2 * config_data["MAX_DISTANCE"] * math.tan(theta_x), 1.3) - collision_box_height = max(2 * config_data["MAX_DISTANCE"] * math.tan(theta_y), 1.3) - collision_box_depth = config_data["MAX_DISTANCE"] - config_data["MIN_DISTANCE"] - - collision_box_path = "/World/collision_box" - collision_box_name = "collision_box" - - # Collision box is centered between MIN_DISTANCE and MAX_DISTANCE, with translation relative to camera in the z - # direction being negative due to cameras in Isaac Sim having coordinates of -z out, +y up, and +x right. - collision_box_translation_from_camera = np.array( - [0, 0, (config_data["MIN_DISTANCE"] + config_data["MAX_DISTANCE"]) / 2.0] - ) - - # Collision box has no rotation with respect to the camera. - collision_box_rotation_from_camera = np.array([0, 0, 0]) - collision_box_orientation_from_camera = euler_angles_to_quat(collision_box_rotation_from_camera, degrees=True) - - # Get the desired pose of the collision box from a pose defined locally with respect to the camera. - camera_prim = world.stage.GetPrimAtPath(self.camera_path) - collision_box_center, collision_box_orientation = get_world_pose_from_relative( - camera_prim, collision_box_translation_from_camera, collision_box_orientation_from_camera - ) - - return CollisionBox( - collision_box_path, - collision_box_name, - position=collision_box_center, - orientation=collision_box_orientation, - width=collision_box_width, - height=collision_box_height, - depth=collision_box_depth, - ) - - def _setup_distractors(self, collision_box): - # List of distractor objects should not contain objects that are being used for training - train_objects = [object["part_name"] for object in OBJECTS_TO_GENERATE] - distractor_mesh_filenames = [ - file_name for file_name in config_data["MESH_FILENAMES"] if file_name not in train_objects - ] - - usd_path_list = [ - f"{self.distractor_asset_path}{usd_filename_prefix}.usd" - for usd_filename_prefix in distractor_mesh_filenames - ] - mesh_list = [f"_{usd_filename_prefix[1:]}" for usd_filename_prefix in distractor_mesh_filenames] - - if self.num_mesh > 0: - # Distractors for the MESH dataset - mesh_shape_set = DynamicShapeSet( - "/World/mesh_shape_set", - "mesh_shape_set", - "mesh_shape", - "mesh_shape", - config_data["NUM_MESH_SHAPES"], - collision_box, - scale=np.array(config_data["SHAPE_SCALE"]), - mass=config_data["SHAPE_MASS"], - fraction_glass=config_data["MESH_FRACTION_GLASS"], - ) - self.mesh_distractors.add(mesh_shape_set) - - mesh_object_set = DynamicObjectSet( - "/World/mesh_object_set", - "mesh_object_set", - usd_path_list, - mesh_list, - "mesh_object", - "mesh_object", - config_data["NUM_MESH_OBJECTS"], - collision_box, - scale=np.array(config_data["OBJECT_SCALE"]), - mass=config_data["OBJECT_MASS"], - fraction_glass=config_data["MESH_FRACTION_GLASS"], - ) - self.mesh_distractors.add(mesh_object_set) - # Set the current distractors to the mesh dataset type - self.current_distractors = self.mesh_distractors - - if self.num_dome > 0: - # Distractors for the DOME dataset - dome_shape_set = DynamicShapeSet( - "/World/dome_shape_set", - "dome_shape_set", - "dome_shape", - "dome_shape", - config_data["NUM_DOME_SHAPES"], - collision_box, - scale=np.array(config_data["SHAPE_SCALE"]), - mass=config_data["SHAPE_MASS"], - fraction_glass=config_data["DOME_FRACTION_GLASS"], - ) - self.dome_distractors.add(dome_shape_set) - - dome_object_set = DynamicObjectSet( - "/World/dome_object_set", - "dome_object_set", - usd_path_list, - mesh_list, - "dome_object", - "dome_object", - config_data["NUM_DOME_OBJECTS"], - collision_box, - scale=np.array(config_data["OBJECT_SCALE"]), - mass=config_data["OBJECT_MASS"], - fraction_glass=config_data["DOME_FRACTION_GLASS"], - ) - self.dome_distractors.add(dome_object_set) - - def _setup_train_objects(self): - # Add the part to train the network on - train_part_idx = 0 - for object in OBJECTS_TO_GENERATE: - for prim_idx in range(object["num"]): - part_name = object["part_name"] - ref_path = self.train_asset_path + part_name + ".usd" - prim_type = object["prim_type"] - - if self.writer_helper == YCBVideoWriter and prim_type not in config_data["CLASS_NAME_TO_INDEX"]: - raise Exception(f"Train object {prim_type} is not in CLASS_NAME_TO_INDEX in config.yaml.") - - path = "/World/" + prim_type + f"_{prim_idx}" - - mesh_path = path + "/" + prim_type - name = f"train_part_{train_part_idx}" - - self.train_part_mesh_path_to_prim_path_map[mesh_path] = path - - train_part = DynamicObject( - usd_path=ref_path, - prim_path=path, - mesh_path=mesh_path, - name=name, - position=np.array([0.0, 0.0, 0.0]), - scale=config_data["TRAIN_PART_SCALE"], - mass=1.0, - ) - - train_part.prims[0].GetAttribute("physics:rigidBodyEnabled").Set(True) - - self.train_parts.append(train_part) - - # Add semantic information - mesh_prim = world.stage.GetPrimAtPath(mesh_path) - add_update_semantics(mesh_prim, prim_type) - - train_part_idx += 1 - - if prim_idx == 0 and self.writer_helper == YCBVideoWriter: - # Save the vertices of the part in '.xyz' format. This will be used in one of PoseCNN's loss functions - coord_prim = world.stage.GetPrimAtPath(path) - self.writer_helper.save_mesh_vertices(mesh_prim, coord_prim, prim_type, self._output_folder) - - def _setup_randomizers(self): - """Add domain randomization with Replicator Randomizers""" - # Create and randomize sphere lights - def randomize_sphere_lights(): - lights = rep.create.light( - light_type="Sphere", - color=rep.distribution.uniform((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), - intensity=rep.distribution.uniform(100000, 3000000), - position=rep.distribution.uniform((-250, -250, -250), (250, 250, 100)), - scale=rep.distribution.uniform(1, 20), - count=config_data["NUM_LIGHTS"], - ) - return lights.node - - # Randomize prim colors - def randomize_colors(prim_path_regex): - prims = rep.get.prims(path_pattern=prim_path_regex) - mats = rep.create.material_omnipbr( - metallic=rep.distribution.uniform(0.0, 1.0), - roughness=rep.distribution.uniform(0.0, 1.0), - diffuse=rep.distribution.uniform((0, 0, 0), (1, 1, 1)), - count=100, - ) - with prims: - rep.randomizer.materials(mats) - return prims.node - - rep.randomizer.register(randomize_sphere_lights, override=True) - rep.randomizer.register(randomize_colors, override=True) - - with rep.trigger.on_frame(): - rep.randomizer.randomize_sphere_lights() - rep.randomizer.randomize_colors("(?=.*shape)(?=.*nonglass).*") - - def _setup_dome_randomizers(self): - """Add domain randomization with Replicator Randomizers""" - - # Create and randomize a dome light for the DOME dataset - def randomize_domelight(texture_paths): - lights = rep.create.light( - light_type="Dome", - rotation=rep.distribution.uniform((0, 0, 0), (360, 360, 360)), - texture=rep.distribution.choice(texture_paths), - ) - return lights.node - - rep.randomizer.register(randomize_domelight, override=True) - - dome_texture_paths = [ - self.dome_texture_path + dome_texture + ".hdr" for dome_texture in config_data["DOME_TEXTURES"] - ] - - with rep.trigger.on_frame(interval=self.dome_interval): - rep.randomizer.randomize_domelight(dome_texture_paths) - - def randomize_movement_in_view(self, prim): - """Randomly move and rotate prim such that it stays in view of camera. - - Args: - prim (DynamicObject): prim to randomly move and rotate. - """ - if not self.test: - camera_prim = world.stage.GetPrimAtPath(self.camera_path) - rig_prim = world.stage.GetPrimAtPath(self.rig.prim_paths[0]) - translation, orientation = get_random_world_pose_in_view( - camera_prim, - config_data["MIN_DISTANCE"], - config_data["MAX_DISTANCE"], - self.fov_x, - self.fov_y, - config_data["FRACTION_TO_SCREEN_EDGE"], - rig_prim, - np.array(config_data["MIN_ROTATION_RANGE"]), - np.array(config_data["MAX_ROTATION_RANGE"]), - ) - else: - translation, orientation = np.array([0.0, 0.0, 1.0]), np.array([0.0, 0.0, 0.0, 1.0]) - - prim.set_world_poses(np.array([translation]), np.array([orientation])) - - def __iter__(self): - return self - - def __next__(self): - # First frame of DOME dataset - if self.cur_idx == self.num_mesh: # MESH datset generation complete, switch to DOME dataset - print(f"[SDG] Starting DOME dataset generation of {self.num_dome} frames..") - - # Hide the FlyingDistractors used for the MESH dataset - self.mesh_distractors.set_visible(False) - - # Show the FlyingDistractors used for the DOME dataset - self.dome_distractors.set_visible(True) - - # Switch the distractors to DOME - self.current_distractors = self.dome_distractors - - # Randomize the dome backgrounds - self._setup_dome_randomizers() - - # Run another preview to generate the replicator graphs for the DOME dataset without triggering any writing - rep.orchestrator.preview() - - # Randomize the distractors by applying forces to them and changing their materials - self.current_distractors.apply_force_to_assets(config_data["FORCE_RANGE"]) - self.current_distractors.randomize_asset_glass_color() - - # Randomize the pose of the object(s) of interest in the camera view - for train_part in self.train_parts: - self.randomize_movement_in_view(train_part) - - # Simulate the applied forces for a couple of frames - for _ in range(50): - world.step(render=False) - - print(f"[SDG] ID: {self.cur_idx}/{self.train_size - 1}") - rep.orchestrator.step(rt_subframes=4) - - self.cur_idx += 1 - - # Check if last frame has been reached - if self.cur_idx >= self.train_size: - print(f"[SDG] Dataset of size {self.train_size} has been reached, generation loop will be stopped..") - print(f"[SDG] Data outputted to: {self._output_folder}") - self.last_frame_reached = True - - -dataset = RandomScenario( - num_mesh=args.num_mesh, - num_dome=args.num_dome, - dome_interval=args.dome_interval, - output_folder=args.output_folder, - use_s3=args.use_s3, - bucket=args.bucket, - s3_region=args.s3_region, - endpoint=args.endpoint, - writer=args.writer.lower(), - test=args.test, - debug=args.debug, -) - -if dataset.result: - # Iterate through dataset and visualize the output - print("[SDG] Loading materials. Will generate data soon...") - - start_time = datetime.datetime.now() - print("[SDG] Start timestamp:", start_time.strftime("%m/%d/%Y, %H:%M:%S")) - - if dataset.train_size > 0: - print(f"[SDG] Starting dataset generation of {dataset.train_size} frames..") - - if dataset.num_mesh > 0: - print(f"[SDG] Starting MESH dataset generation of {dataset.num_mesh} frames..") - - # Dataset generation loop - for _ in dataset: - if dataset.last_frame_reached: - print(f"[SDG] Stopping generation loop at index {dataset.cur_idx}..") - break - if dataset.exiting: - break - else: - print( - f"[SDG] Dataset size is set to 0 (num_mesh={dataset.num_mesh} num_dope={dataset.num_dome}), nothing to write.." - ) - - print("[SDG] End timestamp:", datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S")) - print("[SDG] Total time taken:", str(datetime.datetime.now() - start_time).split(".")[0]) - -if args.test: - run_pose_generation_test( - writer=args.writer, - output_folder=dataset._output_folder, - test_folder=os.path.join(os.path.dirname(os.path.abspath(__file__)), "pose_tests"), - ) - -# Close the app -kit.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/dope/000000_groundtruth.json b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/dope/000000_groundtruth.json deleted file mode 100644 index 7e54c1cb1..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/dope/000000_groundtruth.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "camera_data": {}, - "objects": [ - { - "class": "003_cracker_box", - "visibility": 1.0, - "location": [ - 9.801993292057887e-05, - -0.0002873009070754051, - 0.9662346243858337 - ], - "quaternion_xyzw": [ - 0.0005634104498548971, - 0.9999385283761221, - 0.007785274017060836, - -0.00787474102047689 - ], - "projected_cuboid": [ - [ - 317, - 177 - ], - [ - 195, - 177 - ], - [ - 195, - 335 - ], - [ - 317, - 335 - ], - [ - 321, - 171 - ], - [ - 191, - 171 - ], - [ - 191, - 341 - ], - [ - 321, - 341 - ], - [ - 256, - 256 - ] - ] - } - ] -} \ No newline at end of file diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/dope/test_dope_config.yaml b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/dope/test_dope_config.yaml deleted file mode 100644 index 6b13cf3ee..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/dope/test_dope_config.yaml +++ /dev/null @@ -1,66 +0,0 @@ -# DO NOT MODIFY OR TESTS WILL FAIL ---- -CONFIG: - renderer: RaytracedLighting - headless: false - width: 512 - height: 512 -CLASS_NAME_TO_INDEX: - _03_cracker_box: 1 -OBJECTS_TO_GENERATE: -- { part_name: 003_cracker_box, num: 1, prim_type: _03_cracker_box } -FORCE_RANGE: 30 -WIDTH: 512 -HEIGHT: 512 -F_X: 768.1605834960938 -F_Y: 768.1605834960938 -pixel_size: 0.003 - -HORIZONTAL_APERTURE: 20.955 -NUM_LIGHTS: 6 -MIN_DISTANCE: 1.0 -MAX_DISTANCE: 1.0 -CAMERA_RIG_ROTATION: -- 0 -- 0 -- 0 -CAMERA_ROTATION: -- 180 -- 0 -- 0 -MIN_ROTATION_RANGE: -- 100 -- 100 -- 100 -MAX_ROTATION_RANGE: -- 100 -- 100 -- 100 -FRACTION_TO_SCREEN_EDGE: 0.0 -SHAPE_SCALE: -- 0.05 -- 0.05 -- 0.05 -SHAPE_MASS: 1 -OBJECT_SCALE: -- 1 -- 1 -- 1 -TRAIN_PART_SCALE: # Scale for the training objects -- 1 -- 1 -- 1 -OBJECT_MASS: 1 -NUM_MESH_SHAPES: 0 -NUM_MESH_OBJECTS: 0 -MESH_FRACTION_GLASS: 0.15 -NUM_DOME_SHAPES: 0 -NUM_DOME_OBJECTS: 0 -DOME_FRACTION_GLASS: 0.2 -DOME_TEXTURES: [] -MESH_FILENAMES: [] - -# Asset paths -DISTRACTOR_ASSET_PATH: '' # Can leave empty, distactors not used in tests -TRAIN_ASSET_PATH: /Isaac/Props/YCB/Axis_Aligned/ -DOME_TEXTURE_PATH: '' # Can leave empty, textures not used in tests diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/test_utils.py b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/test_utils.py deleted file mode 100644 index deb7e3deb..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/test_utils.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import json -import os -import shutil - -import numpy as np -import scipy.io as sio - - -def run_pose_generation_test(writer, output_folder, test_folder): - if writer.lower() == "dope": - run_dope_test(test_folder, output_folder) - elif writer.lower() == "ycbvideo": - run_ycbvideo_tests(test_folder, output_folder) - else: - raise Exception(f"No tests exist for the selected writer: {writer}") - - -# Cleans up output directory so tests are not reading output files from previous run -def clean_output_dir(output_folder): - if os.path.isdir(output_folder): - shutil.rmtree(output_folder, ignore_errors=True) - - -# Checks if distance between points is within threshold -def within_threshold(p1, p2, threshold=20): - return np.linalg.norm(np.array(p1) - np.array(p2)) < threshold - - -def run_dope_test(test_folder, output_folder): - - groundtruth_path = os.path.join(test_folder, "dope/000000_groundtruth.json") - # Look at output for 2nd frame because 1st frame does not get generated properly sometimes - output_path = os.path.join(output_folder, "000001.json") - - with open(groundtruth_path) as gt_f: - gt_data = json.load(gt_f) - - with open(output_path) as op_f: - op_data = json.load(op_f) - - gt_objects, op_objects = gt_data["objects"], op_data["objects"] - - # Does not work with multiple objects. There should be only one object in testing mode. - if not (len(gt_objects) == 1 and len(op_objects) == 1): - raise Exception( - f"Mismatch in .json files between number of objects. gt_objects: {len(gt_objects)}, op_objects: {len(op_objects)}" - ) - - for gt_obj, op_obj in zip(gt_objects, op_objects): - if not within_threshold(gt_obj["location"], op_obj["location"], 10): - raise Exception( - f"Distance between groundtruth location and output location exceeds threshold. (location) {gt_obj['location']} and {op_obj['location']}" - ) - for gt_pt, op_pt in zip(gt_obj["projected_cuboid"], op_obj["projected_cuboid"]): - if not within_threshold(gt_pt, op_pt, 20.0): - raise Exception( - f"Distance between groundtruth points and output points exceeds threshold. (projected_cuboid) {gt_pt} and {op_pt}" - ) - - print("Tests pass for DOPE Writer.") - - -def run_ycbvideo_tests(test_folder, output_folder, threshold=10): - groundtruth_bbox_path = os.path.join(test_folder, "ycbvideo/000000-box_groundtruth.txt") - groundtruth_meta_path = os.path.join(test_folder, "ycbvideo/000000-meta_groundtruth.mat") - - # Look at output for 2nd frame because 1st frame does not get generated properly sometimes - output_bbox_path = os.path.join(output_folder, "data/YCB_Video/data/0000", "000001-box.txt") - output_meta_path = os.path.join(output_folder, "data/YCB_Video/data/0000", "000001-meta.mat") - - # Compare BBox - gt_bb = open(groundtruth_bbox_path, "r") - op_bb = open(output_bbox_path, "r") - - for l1, l2 in zip(gt_bb, op_bb): - for gt_point, bb_point in zip(l1.strip().split()[1:5], l2.strip().split()[1:5]): - if not within_threshold([int(gt_point)], [int(bb_point)], 10): - raise Exception(f"Mismatch between files {groundtruth_bbox_path} and {output_bbox_path}") - - gt_bb.close() - op_bb.close() - - # Compare Meta File - gt_meta = sio.loadmat(groundtruth_meta_path) - op_meta = sio.loadmat(output_meta_path) - - keys_to_compare = ["poses", "intrinsic_matrix", "center"] - - print(f"gt_meta:\n{gt_meta}") - print(f"op_meta:\n{op_meta}") - - for key in keys_to_compare: - - gt = gt_meta[key].flatten() - op = op_meta[key].flatten() - - if not len(gt) == len(op): - raise Exception(f"Mismatch between length of pose in {groundtruth_meta_path} and {output_meta_path}") - - for i in range(len(gt)): - if abs(gt[i] - op[i]) > threshold: - raise Exception( - f"Mismatch between {key} values in groundtruth and output at index {i}. Groundtruth: {gt[i]} Output: {op[i]}" - ) - - print(f"{key} matches between groundtruth and output.") - - print("Tests pass for YCBVideo Writer.") diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/000000-box_groundtruth.txt b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/000000-box_groundtruth.txt deleted file mode 100644 index 75c943688..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/000000-box_groundtruth.txt +++ /dev/null @@ -1 +0,0 @@ -_03_cracker_box 583 285 697 435 diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/000000-meta_groundtruth.mat b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/000000-meta_groundtruth.mat deleted file mode 100644 index ad28cc527..000000000 Binary files a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/000000-meta_groundtruth.mat and /dev/null differ diff --git a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/test_ycb_config.yaml b/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/test_ycb_config.yaml deleted file mode 100644 index 87ee43ffe..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/pose_generation/pose_tests/ycbvideo/test_ycb_config.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# DO NOT MODIFY OR TESTS WILL FAIL ---- -CONFIG: - renderer: RaytracedLighting - headless: false - width: 1280 - height: 720 -CLASS_NAME_TO_INDEX: - _03_cracker_box: 1 -OBJECTS_TO_GENERATE: -- { part_name: 003_cracker_box, num: 1, prim_type: _03_cracker_box } -FORCE_RANGE: 30 -WIDTH: 1280 -HEIGHT: 720 -F_X: 665.80768 -F_Y: 665.80754 -C_X: 637.642 -C_Y: 367.56 -pixel_size: 0.003 - -HORIZONTAL_APERTURE: 20.955 -NUM_LIGHTS: 6 -MIN_DISTANCE: 1.0 -MAX_DISTANCE: 1.0 -CAMERA_RIG_ROTATION: -- 0 -- 0 -- 0 -CAMERA_ROTATION: -- 180 -- 0 -- 0 -MIN_ROTATION_RANGE: -- 100 -- 100 -- 100 -MAX_ROTATION_RANGE: -- 100 -- 100 -- 100 -FRACTION_TO_SCREEN_EDGE: 0.0 -SHAPE_SCALE: -- 0.05 -- 0.05 -- 0.05 -SHAPE_MASS: 1 -OBJECT_SCALE: -- 1 -- 1 -- 1 -TRAIN_PART_SCALE: # Scale for the training objects -- 1 -- 1 -- 1 -OBJECT_MASS: 1 -NUM_MESH_SHAPES: 0 -NUM_MESH_OBJECTS: 0 -MESH_FRACTION_GLASS: 0.15 -NUM_DOME_SHAPES: 0 -NUM_DOME_OBJECTS: 0 -DOME_FRACTION_GLASS: 0.2 -DOME_TEXTURES: [] -MESH_FILENAMES: [] - -# Asset paths -DISTRACTOR_ASSET_PATH: '' # Can leave empty, distactors not used in tests -TRAIN_ASSET_PATH: /Isaac/Props/YCB/Axis_Aligned/ -DOME_TEXTURE_PATH: '' # Can leave empty, textures not used in tests diff --git a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_basic_writer.yaml b/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_basic_writer.yaml deleted file mode 100644 index bd685d865..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_basic_writer.yaml +++ /dev/null @@ -1,10 +0,0 @@ -launch_config: - renderer: RaytracedLighting - headless: false -resolution: [512, 512] -env_url: "/Isaac/Environments/Grid/default_environment.usd" -rt_subframes: 32 -writer: BasicWriter -writer_config: - output_dir: _out_basicwriter - rgb: true \ No newline at end of file diff --git a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_coco_writer.yaml b/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_coco_writer.yaml deleted file mode 100644 index c95ff9867..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_coco_writer.yaml +++ /dev/null @@ -1,22 +0,0 @@ -launch_config: - renderer: RaytracedLighting - headless: true -resolution: [512, 512] -num_frames: 5 -clear_previous_semantics: true -writer: CocoWriter -writer_config: - output_dir: _out_coco - coco_categories: - forklift: - name: forklift - id: 333 - supercategory: warehouse - color: [211, 111, 211] - isthing: 1 - pallet: - name: pallet - id: 313 - supercategory: warehouse - color: [141, 111, 131] - isthing: 1 diff --git a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_default_writer.json b/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_default_writer.json deleted file mode 100644 index 1a776b11e..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_default_writer.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "launch_config": { - "renderer": "RaytracedLighting", - "headless": false - }, - "resolution": [512, 512], - "writer_config": { - "output_dir": "_out_defaultwriter", - "rgb": true, - "instance_segmentation": true - } -} \ No newline at end of file diff --git a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_kitti_writer.yaml b/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_kitti_writer.yaml deleted file mode 100644 index 1c773f342..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/config/config_kitti_writer.yaml +++ /dev/null @@ -1,13 +0,0 @@ -launch_config: - renderer: RaytracedLighting - headless: true -resolution: [512, 512] -num_frames: 5 -clear_previous_semantics: false -writer: KittiWriter -writer_config: - output_dir: _out_kitti - colorize_instance_segmentation: true - mapping_dict: - forklift: [11, 110, 223, 255] - pallet: [211, 210, 223, 255] \ No newline at end of file diff --git a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/scene_based_sdg.py b/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/scene_based_sdg.py deleted file mode 100644 index b4d1fedf2..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/scene_based_sdg.py +++ /dev/null @@ -1,274 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -"""Generate offline synthetic dataset -""" - -import argparse -import json -import math -import os -import random - -import yaml -from isaacsim import SimulationApp - -# Default config (will be updated/extended by any other passed config arguments) -config = { - "launch_config": { - "renderer": "RaytracedLighting", - "headless": False, - }, - "resolution": [512, 512], - "rt_subframes": 16, - "num_frames": 20, - "env_url": "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd", - "writer": "BasicWriter", - "writer_config": { - "output_dir": "_out_scene_based_sdg", - "rgb": True, - "bounding_box_2d_tight": True, - "semantic_segmentation": True, - "distance_to_image_plane": True, - "bounding_box_3d": True, - "occlusion": True, - }, - "clear_previous_semantics": True, - "forklift": { - "url": "/Isaac/Props/Forklift/forklift.usd", - "class": "forklift", - }, - "cone": { - "url": "/Isaac/Environments/Simple_Warehouse/Props/S_TrafficCone.usd", - "class": "traffic_cone", - }, - "pallet": { - "url": "/Isaac/Environments/Simple_Warehouse/Props/SM_PaletteA_01.usd", - "class": "pallet", - }, - "cardbox": { - "url": "/Isaac/Environments/Simple_Warehouse/Props/SM_CardBoxD_04.usd", - "class": "cardbox", - }, - "close_app_after_run": True, -} - -import carb - -# Check if there are any config files (yaml or json) are passed as arguments -parser = argparse.ArgumentParser() -parser.add_argument("--config", required=False, help="Include specific config parameters (json or yaml))") -args, unknown = parser.parse_known_args() -args_config = {} -if args.config and os.path.isfile(args.config): - print("File exist") - with open(args.config, "r") as f: - if args.config.endswith(".json"): - args_config = json.load(f) - elif args.config.endswith(".yaml"): - args_config = yaml.safe_load(f) - else: - carb.log_warn(f"File {args.config} is not json or yaml, will use default config") -else: - carb.log_warn(f"File {args.config} does not exist, will use default config") - -# If there are specific writer parameters in the input config file make sure they are not mixed with the default ones -if "writer_config" in args_config: - config["writer_config"].clear() - -# Update the default config dictionay with any new parameters or values from the config file -config.update(args_config) - -# Create the simulation app with the given launch_config -simulation_app = SimulationApp(launch_config=config["launch_config"]) - -# Late import of runtime modules (the SimulationApp needs to be created before loading the modules) -import omni.replicator.core as rep -import omni.usd - -# Custom util functions for the example -import scene_based_sdg_utils -from isaacsim.core.utils import prims -from isaacsim.core.utils.rotations import euler_angles_to_quat -from isaacsim.core.utils.stage import get_current_stage, open_stage -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf - -# Get server path -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not get nucleus server path, closing application..") - simulation_app.close() - -# Open the given environment in a new stage -print(f"[scene_based_sdg] Loading Stage {config['env_url']}") -if not open_stage(assets_root_path + config["env_url"]): - carb.log_error(f"Could not open stage{config['env_url']}, closing application..") - simulation_app.close() - -# Disable capture on play (data generation will be triggered manually) -rep.orchestrator.set_capture_on_play(False) - -# Clear any previous semantic data in the loaded stage -if config["clear_previous_semantics"]: - stage = get_current_stage() - scene_based_sdg_utils.remove_previous_semantics(stage) - -# Spawn a new forklift at a random pose -forklift_prim = prims.create_prim( - prim_path="/World/Forklift", - position=(random.uniform(-20, -2), random.uniform(-1, 3), 0), - orientation=euler_angles_to_quat([0, 0, random.uniform(0, math.pi)]), - usd_path=assets_root_path + config["forklift"]["url"], - semantic_label=config["forklift"]["class"], -) - -# Spawn the pallet in front of the forklift with a random offset on the Y (pallet's forward) axis -forklift_tf = omni.usd.get_world_transform_matrix(forklift_prim) -pallet_offset_tf = Gf.Matrix4d().SetTranslate(Gf.Vec3d(0, random.uniform(-1.2, -1.8), 0)) -pallet_pos_gf = (pallet_offset_tf * forklift_tf).ExtractTranslation() -forklift_quat_gf = forklift_tf.ExtractRotationQuat() -forklift_quat_xyzw = (forklift_quat_gf.GetReal(), *forklift_quat_gf.GetImaginary()) - -pallet_prim = prims.create_prim( - prim_path="/World/Pallet", - position=pallet_pos_gf, - orientation=forklift_quat_xyzw, - usd_path=assets_root_path + config["pallet"]["url"], - semantic_label=config["pallet"]["class"], -) - -# Register randomization graphs -scene_based_sdg_utils.register_scatter_boxes(pallet_prim, assets_root_path, config) -scene_based_sdg_utils.register_cone_placement(forklift_prim, assets_root_path, config) -scene_based_sdg_utils.register_lights_placement(forklift_prim, pallet_prim) - -# Spawn a camera in the driver's location looking at the pallet -foklift_pos_gf = forklift_tf.ExtractTranslation() -driver_cam_pos_gf = foklift_pos_gf + Gf.Vec3d(0.0, 0.0, 1.9) - -driver_cam = rep.create.camera( - focus_distance=400.0, focal_length=24.0, clipping_range=(0.1, 10000000.0), name="DriverCam" -) - -# Camera looking at the pallet -pallet_cam = rep.create.camera(name="PalletCam") - -# Camera looking at the forklift from a top view with large min clipping to see the scene through the ceiling -top_view_cam = rep.create.camera(clipping_range=(6.0, 1000000.0), name="TopCam") - -# Create render products for the custom cameras and attach them to the writer -resolution = config.get("resolution", (512, 512)) -forklift_rp = rep.create.render_product(top_view_cam, resolution, name="TopView") -driver_rp = rep.create.render_product(driver_cam, resolution, name="DriverView") -pallet_rp = rep.create.render_product(pallet_cam, resolution, name="PalletView") -# Disable the render products until SDG to improve perf by avoiding unnecessary rendering -rps = [forklift_rp, driver_rp, pallet_rp] -for rp in rps: - rp.hydra_texture.set_updates_enabled(False) - -# If output directory is relative, set it relative to the current working directory -if not os.path.isabs(config["writer_config"]["output_dir"]): - config["writer_config"]["output_dir"] = os.path.join(os.getcwd(), config["writer_config"]["output_dir"]) -print(f"[scene_based_sdg] Output directory={config['writer_config']['output_dir']}") - -# Make sure the writer type is in the registry -writer_type = config.get("writer", "BasicWriter") -if writer_type not in rep.WriterRegistry.get_writers(): - carb.log_error(f"Writer type {writer_type} not found in the registry, closing application..") - simulation_app.close() - -# Get the writer from the registry and initialize it with the given config parameters -writer = rep.WriterRegistry.get(writer_type) -writer_kwargs = config["writer_config"] -print(f"[scene_based_sdg] Initializing {writer_type} with: {writer_kwargs}") -writer.initialize(**writer_kwargs) - -# Attach writer to the render products -writer.attach(rps) - -# Setup the randomizations to be triggered every frame -with rep.trigger.on_frame(): - rep.randomizer.scatter_boxes() - rep.randomizer.randomize_lights() - - # Randomize the camera position in the given area above the pallet and look at the pallet prim - pallet_cam_min = (pallet_pos_gf[0] - 2, pallet_pos_gf[1] - 2, 2) - pallet_cam_max = (pallet_pos_gf[0] + 2, pallet_pos_gf[1] + 2, 4) - with pallet_cam: - rep.modify.pose( - position=rep.distribution.uniform(pallet_cam_min, pallet_cam_max), - look_at=str(pallet_prim.GetPrimPath()), - ) - - # Randomize the camera position in the given height above the forklift driver's seat and look at the pallet prim - driver_cam_min = (driver_cam_pos_gf[0], driver_cam_pos_gf[1], driver_cam_pos_gf[2] - 0.25) - driver_cam_max = (driver_cam_pos_gf[0], driver_cam_pos_gf[1], driver_cam_pos_gf[2] + 0.25) - with driver_cam: - rep.modify.pose( - position=rep.distribution.uniform(driver_cam_min, driver_cam_max), - look_at=str(pallet_prim.GetPrimPath()), - ) - -# Setup the randomizations to be triggered at every nth frame (interval) -with rep.trigger.on_frame(interval=4): - top_view_cam_min = (foklift_pos_gf[0], foklift_pos_gf[1], 9) - top_view_cam_max = (foklift_pos_gf[0], foklift_pos_gf[1], 11) - with top_view_cam: - rep.modify.pose( - position=rep.distribution.uniform(top_view_cam_min, top_view_cam_max), - rotation=rep.distribution.uniform((0, -90, -30), (0, -90, 30)), - ) - -# Setup the randomizations to be manually triggered at specific times -with rep.trigger.on_custom_event("randomize_cones"): - rep.randomizer.place_cones() - -# Run a simulation by dropping randomly placed boxes on a pallet next to the forklift -scene_based_sdg_utils.simulate_falling_objects(forklift_prim, assets_root_path, config) - -# Increase subframes if materials are not loaded on time, or ghosting artifacts appear on moving objects, -# see: https://docs.omniverse.nvidia.com/extensions/latest/ext_replicator/subframes_examples.html -rt_subframes = config.get("rt_subframes", -1) - -# Enable the render products for SDG -for rp in rps: - rp.hydra_texture.set_updates_enabled(True) - -# Start the SDG -num_frames = config.get("num_frames", 0) -print(f"[scene_based_sdg] Running SDG for {num_frames} frames") -for i in range(num_frames): - print(f"[scene_based_sdg] \t Capturing frame {i}") - # Trigger the custom event to randomize the cones at specific frames - if i % 2 == 0: - rep.utils.send_og_event(event_name="randomize_cones") - # Trigger any on_frame registered randomizers and the writers (delta_time=0.0 to avoid advancing the timeline) - rep.orchestrator.step(delta_time=0.0, rt_subframes=rt_subframes) - -# Wait for the data to be written to disk -rep.orchestrator.wait_until_complete() - -# Cleanup writer and render products -writer.detach() -for rp in rps: - rp.destroy() - -# Check if the application should keep running after the data generation (debug purposes) -close_app_after_run = config.get("close_app_after_run", True) -if config["launch_config"]["headless"]: - if not close_app_after_run: - print( - "[scene_based_sdg] 'close_app_after_run' is ignored when running headless. The application will be closed." - ) -elif not close_app_after_run: - print("[scene_based_sdg] The application will not be closed after the run. Make sure to close it manually.") - while simulation_app.is_running(): - simulation_app.update() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/scene_based_sdg_utils.py b/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/scene_based_sdg_utils.py deleted file mode 100644 index 43f5f3234..000000000 --- a/simulation/isaac-sim/standalone_examples/replicator/scene_based_sdg/scene_based_sdg_utils.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import math -import random - -import numpy as np -import omni.replicator.core as rep -import omni.usd -from isaacsim.core.api import World -from isaacsim.core.prims import SingleRigidPrim -from isaacsim.core.utils import prims -from isaacsim.core.utils.bounds import compute_combined_aabb, compute_obb, create_bbox_cache, get_obb_corners -from isaacsim.core.utils.rotations import euler_angles_to_quat, quat_to_euler_angles -from isaacsim.core.utils.semantics import remove_all_semantics -from pxr import Gf, PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics - - -# Add colliders to Gprim and Mesh descendants of the root prim -def add_colliders(root_prim, approx_type="convexHull"): - # Iterate descendant prims (including root) and add colliders to mesh or primitive types - for desc_prim in Usd.PrimRange(root_prim): - if desc_prim.IsA(UsdGeom.Mesh) or desc_prim.IsA(UsdGeom.Gprim): - # Physics - if not desc_prim.HasAPI(UsdPhysics.CollisionAPI): - collision_api = UsdPhysics.CollisionAPI.Apply(desc_prim) - else: - collision_api = UsdPhysics.CollisionAPI(desc_prim) - collision_api.CreateCollisionEnabledAttr(True) - # Add mesh specific collision properties only to mesh types - if desc_prim.IsA(UsdGeom.Mesh): - # Add mesh collision properties to the mesh (e.g. collider aproximation type) - if not desc_prim.HasAPI(UsdPhysics.MeshCollisionAPI): - mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(desc_prim) - else: - mesh_collision_api = UsdPhysics.MeshCollisionAPI(desc_prim) - mesh_collision_api.CreateApproximationAttr().Set(approx_type) - - -# Clear any previous semantic data in the stage -def remove_previous_semantics(stage, recursive: bool = False): - prims = stage.Traverse() - for prim in prims: - remove_all_semantics(prim, recursive) - - -# Run a simulation -def simulate_falling_objects(forklift_prim, assets_root_path, config, max_sim_steps=250, num_boxes=8): - # Create the isaac sim world to run any physics simulations - world = World(physics_dt=1.0 / 90.0, stage_units_in_meters=1.0) - - # Set a random relative offset to the pallet using the forklift transform as a base frame - forklift_tf = omni.usd.get_world_transform_matrix(forklift_prim) - pallet_offset_tf = Gf.Matrix4d().SetTranslate(Gf.Vec3d(random.uniform(-1, 1), random.uniform(-4, -3.6), 0)) - pallet_pos = (pallet_offset_tf * forklift_tf).ExtractTranslation() - - # Spawn a pallet prim at a random offset from the forklift - pallet_prim = prims.create_prim( - prim_path=f"/World/SimulatedPallet", - position=pallet_pos, - orientation=euler_angles_to_quat([0, 0, random.uniform(0, math.pi)]), - usd_path=assets_root_path + config["pallet"]["url"], - semantic_label=config["pallet"]["class"], - ) - - # Wrap the pallet as simulation ready with a simplified collider - add_colliders(pallet_prim, approx_type="boundingCube") - pallet_rigid_prim = SingleRigidPrim(prim_path=str(pallet_prim.GetPrimPath())) - pallet_rigid_prim.enable_rigid_body_physics() - - # Use the height of the pallet as a spawn base for the boxes - bb_cache = create_bbox_cache() - spawn_height = bb_cache.ComputeLocalBound(pallet_prim).GetRange().GetSize()[2] * 1.1 - - # Keep track of the last box to stop the simulation early once it stops moving - last_box = None - # Spawn boxes falling on the pallet - for i in range(num_boxes): - # Spawn the carbox prim by creating a new Xform prim and adding the USD reference to it - box_prim = prims.create_prim( - prim_path=f"/World/SimulatedCardbox_{i}", - position=pallet_pos + Gf.Vec3d(random.uniform(-0.2, 0.2), random.uniform(-0.2, 0.2), spawn_height), - orientation=euler_angles_to_quat([0, 0, random.uniform(0, math.pi)]), - usd_path=assets_root_path + config["cardbox"]["url"], - semantic_label=config["cardbox"]["class"], - ) - - # Get the next spawn height for the box - spawn_height += bb_cache.ComputeLocalBound(box_prim).GetRange().GetSize()[2] * 1.1 - - # Wrap the prim as simulation ready with a simplified collider - add_colliders(box_prim, approx_type="boundingCube") - box_rigid_prim = SingleRigidPrim(prim_path=str(box_prim.GetPrimPath())) - box_rigid_prim.enable_rigid_body_physics() - - # Cache the rigid prim - last_box = box_rigid_prim - - # Reset the world to handle the physics of the newly created rigid prims - world.reset() - - # Simulate the world for the given number of steps or until the highest box stops moving - for i in range(max_sim_steps): - world.step(render=False) - if last_box and np.linalg.norm(last_box.get_linear_velocity()) < 0.001: - print(f"[scene_based_sdg] Simulation finished at step {i}..") - break - - -# Register the boxes and materials randomizer graph -def register_scatter_boxes(pallet_prim, assets_root_path, config): - # Calculate the bounds of the prim to create a scatter plane of its size - bb_cache = create_bbox_cache() - bbox3d_gf = bb_cache.ComputeLocalBound(pallet_prim) - prim_tf_gf = omni.usd.get_world_transform_matrix(pallet_prim) - - # Calculate the bounds of the prim - bbox3d_gf.Transform(prim_tf_gf) - range_size = bbox3d_gf.GetRange().GetSize() - - # Get the quaterion of the prim in xyzw format from usd - prim_quat_gf = prim_tf_gf.ExtractRotation().GetQuaternion() - prim_quat_xyzw = (prim_quat_gf.GetReal(), *prim_quat_gf.GetImaginary()) - - # Create a plane on the pallet to scatter the boxes on - plane_scale = (range_size[0] * 0.8, range_size[1] * 0.8, 1) - plane_pos_gf = prim_tf_gf.ExtractTranslation() + Gf.Vec3d(0, 0, range_size[2]) - plane_rot_euler_deg = quat_to_euler_angles(np.array(prim_quat_xyzw), degrees=True) - scatter_plane = rep.create.plane( - scale=plane_scale, position=plane_pos_gf, rotation=plane_rot_euler_deg, visible=False - ) - - cardbox_mats = [ - f"{assets_root_path}/Isaac/Environments/Simple_Warehouse/Materials/MI_PaperNotes_01.mdl", - f"{assets_root_path}/Isaac/Environments/Simple_Warehouse/Materials/MI_CardBoxB_05.mdl", - ] - - def scatter_boxes(): - cardboxes = rep.create.from_usd( - assets_root_path + config["cardbox"]["url"], semantics=[("class", config["cardbox"]["class"])], count=5 - ) - with cardboxes: - rep.randomizer.scatter_2d(scatter_plane, check_for_collisions=True) - rep.randomizer.materials(cardbox_mats) - return cardboxes.node - - rep.randomizer.register(scatter_boxes) - - -# Register the place cones randomizer graph -def register_cone_placement(forklift_prim, assets_root_path, config): - # Get the bottom corners of the oriented bounding box (OBB) of the forklift - bb_cache = create_bbox_cache() - centroid, axes, half_extent = compute_obb(bb_cache, forklift_prim.GetPrimPath()) - larger_xy_extent = (half_extent[0] * 1.3, half_extent[1] * 1.3, half_extent[2]) - obb_corners = get_obb_corners(centroid, axes, larger_xy_extent) - bottom_corners = [ - obb_corners[0].tolist(), - obb_corners[2].tolist(), - obb_corners[4].tolist(), - obb_corners[6].tolist(), - ] - - # Orient the cone using the OBB (Oriented Bounding Box) - obb_quat = Gf.Matrix3d(axes).ExtractRotation().GetQuaternion() - obb_quat_xyzw = (obb_quat.GetReal(), *obb_quat.GetImaginary()) - obb_euler = quat_to_euler_angles(np.array(obb_quat_xyzw), degrees=True) - - def place_cones(): - cones = rep.create.from_usd( - assets_root_path + config["cone"]["url"], semantics=[("class", config["cone"]["class"])] - ) - with cones: - rep.modify.pose(position=rep.distribution.sequence(bottom_corners), rotation_z=obb_euler[2]) - return cones.node - - rep.randomizer.register(place_cones) - - -# Register light randomization graph -def register_lights_placement(forklift_prim, pallet_prim): - bb_cache = create_bbox_cache() - combined_range_arr = compute_combined_aabb(bb_cache, [forklift_prim.GetPrimPath(), pallet_prim.GetPrimPath()]) - pos_min = (combined_range_arr[0], combined_range_arr[1], 6) - pos_max = (combined_range_arr[3], combined_range_arr[4], 7) - - def randomize_lights(): - lights = rep.create.light( - light_type="Sphere", - color=rep.distribution.uniform((0.2, 0.1, 0.1), (0.9, 0.8, 0.8)), - intensity=rep.distribution.uniform(500, 2000), - position=rep.distribution.uniform(pos_min, pos_max), - scale=rep.distribution.uniform(5, 10), - count=3, - ) - return lights.node - - rep.randomizer.register(randomize_lights) diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.benchmark.services/test_no_rendering.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.benchmark.services/test_no_rendering.py deleted file mode 100644 index 647c73944..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.benchmark.services/test_no_rendering.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import sys - -import carb -import omni.kit.test -from isaacsim.core.api import PhysicsContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf, UsdGeom - -enable_extension("isaacsim.benchmark.services") -from isaacsim.benchmark.services import BaseIsaacBenchmark - -# ---------------------------------------------------------------------- -# Create benchmark -benchmark = BaseIsaacBenchmark( - benchmark_name="benchmark_physx_lidar", -) - - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - - -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka_1") - -benchmark.set_phase("benchmark") - -timeline = omni.timeline.get_timeline_interface() -timeline.play() - -physics_context = PhysicsContext(physics_dt=1.0 / 60.0) -time = 0 -for _ in range(0, 1000): - physics_context._step(time) - time += physics_context.get_physics_dt() - -benchmark.store_measurements() -min_physics_time = 0 -for measurement in benchmark._test_phases[0].measurements: - if "Min Physics Frametime" in measurement.name: - min_physics_time = measurement.value - -benchmark.stop() -timeline.stop() -assert min_physics_time > 0 -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/articulation.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/articulation.py deleted file mode 100644 index 8e47a429a..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/articulation.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import random -import sys - -import carb -import numpy as np -import torch -from isaacsim.core.api import World -from isaacsim.core.api.materials.omni_glass import OmniGlass -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.numpy.rotations import euler_angles_to_quats -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0, backend="torch", device="cuda:0") -my_world.scene.add_default_ground_plane() - -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka_1") -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka_2") -# define_prim(prim_path="/World/Frame_1") -# define_prim(prim_path="/World/Frame_2") -# define_prim(prim_path="/World/Frame_3") -# define_prim(prim_path="/World/Frame_1/Target") -# define_prim(prim_path="/World/Frame_2/Target") -# define_prim(prim_path="/World/Frame_3/Target") -new_positions = torch.tensor([[10.0, 10.0, 0], [100.0, 100.0, 0]], dtype=torch.float32) / 100.0 -new_orientations = torch.tensor( - euler_angles_to_quats(np.array([[0, 0, np.pi / 2.0], [0, 0, -np.pi / 2.0]])), dtype=torch.float32 -) -frankas_view = Articulation(prim_paths_expr="/World/Franka_[1-2]", name="frankas_view") -my_world.scene.add(frankas_view) -glass_1 = OmniGlass( - prim_path=f"/World/franka_glass_material_1", - ior=1.25, - depth=0.001, - thin_walled=False, - color=np.array([random.random(), random.random(), random.random()]), -) - -glass_2 = OmniGlass( - prim_path=f"/World/franka_glass_material_2", - ior=1.25, - depth=0.001, - thin_walled=False, - color=np.array([random.random(), random.random(), random.random()]), -) - -my_world.reset() -frankas_view.set_world_poses(positions=new_positions) - -frankas_view.apply_visual_materials(visual_materials=[glass_1, glass_2], indices=[1, 0]) -frankas_view.set_gains( - kps=torch.tensor( - [ - [100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 500.0], - [100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 400.0], - ] - ) -) -frankas_view.switch_control_mode(mode="velocity") -print("Gains here", frankas_view.get_gains()) -frankas_view.set_effort_modes("force") -print(frankas_view.get_effort_modes()) -print(frankas_view.get_max_efforts()) -my_world.reset() -frankas_view.set_world_poses(positions=new_positions, orientations=new_orientations) -frankas_view.set_joint_positions( - torch.tensor([[1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5], [1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5]]) -) -for i in range(10000): - my_world.step(render=True) - if i % 100 == 0: - frankas_view.apply_action(ArticulationAction(joint_positions=torch.randn(2, 9))) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/data/orientation_bug.usd b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/data/orientation_bug.usd deleted file mode 100644 index ec98e949a..000000000 Binary files a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/data/orientation_bug.usd and /dev/null differ diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/hello_world.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/hello_world.py deleted file mode 100644 index 0e54f8657..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/hello_world.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import numpy as np -import torch -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -from abc import abstractmethod - -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid, DynamicSphere -from isaacsim.core.api.tasks import BaseTask -from isaacsim.core.cloner import GridCloner -from isaacsim.core.prims import RigidPrim - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - - -class HelloWorld(BaseTask): - def __init__(self, name, num_envs, env_spacing, offset=None) -> None: - """[summary]""" - BaseTask.__init__(self, name=name, offset=offset) - - self._num_envs = num_envs - self._env_spacing = env_spacing - - self._cloner = GridCloner(self._env_spacing) - - return - - def set_up_scene(self, scene) -> None: - """[summary] - - Args: - scene (Scene): [description] - """ - - super().set_up_scene(scene) - scene.add_default_ground_plane() - task_object = self.set_object() - prim_paths = self._cloner.generate_paths("/World/object", self._num_envs) - self._cloner.clone( - source_prim_path=task_object.prim_path, - prim_paths=prim_paths, - position_offsets=np.array([[0, 0, 1.0]] * self._num_envs), - ) - self._object = RigidPrim(prim_paths_expr=f"/World/object_[0-{self._num_envs-1}]", name="object_view") - scene.add(self._object) - - return - - @abstractmethod - def set_object(self): - raise NotImplementedError - - def get_observations(self) -> dict: - """[summary] - - Returns: - dict: [description] - """ - object_positions, _ = self._object.get_world_poses() - object_velocities = self._object.get_velocities() - - observations = {self._object.name: {"positions": object_positions, "velocities": object_velocities}} - return observations - - def calculate_metrics(self) -> None: - """[summary]""" - return torch.zeros(self._num_envs, device=self._device) - - def is_done(self) -> None: - """[summary]""" - return torch.zeros(self._num_envs, device=self._device) - - -class HelloWorldSphere(HelloWorld): - def __init__(self, name, num_envs, env_spacing, offset=None) -> None: - """[summary]""" - super().__init__(name=name, num_envs=num_envs, env_spacing=env_spacing, offset=offset) - - def set_object(self): - radius = 0.1 - density = 1000.0 - - return DynamicSphere(prim_path="/World/object_0", name="object_0", radius=radius, mass=None, density=density) - - -class HelloWorldCuboid(HelloWorld): - def __init__(self, name, num_envs, env_spacing, offset=None) -> None: - """[summary]""" - super().__init__(name=name, num_envs=num_envs, env_spacing=env_spacing, offset=offset) - - def set_object(self): - size = np.array([0.2, 0.2, 0.2]) - density = 1000.0 - - return DynamicCuboid( - prim_path="/World/object_0", name="object_0", size=1.0, scale=size, mass=None, density=density - ) - - -num_envs = 10 -env_spacing = 1 -physicsscene_path = "/physicsScene" - -my_world = World(stage_units_in_meters=1.0, physics_prim_path=physicsscene_path, backend="torch", device="cuda:0") -my_task = HelloWorldSphere(name="hello_world", num_envs=num_envs, env_spacing=env_spacing) -my_world.add_task(my_task) -my_world.reset() - -reset_needed = False -while simulation_app.is_running(): - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - # deal with sim re-initialization after restarting sim - if reset_needed: - # initialize simulation views - my_world.reset(soft=True) - reset_needed = False - observations = my_world.get_observations() - - my_world.step(render=True) - if args.test is True: - break - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/rigid_prim_view.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/rigid_prim_view.py deleted file mode 100644 index 9b02bc475..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/rigid_prim_view.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import sys - -import carb -import numpy as np -import torch -from isaacsim.core.api import World -from isaacsim.core.api.materials.physics_material import PhysicsMaterial -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.cloner import Cloner -from isaacsim.core.prims import GeometryPrim, RigidPrim -from isaacsim.core.utils.torch.rotations import euler_angles_to_quats -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_cloner = Cloner() -my_world = World(stage_units_in_meters=1.0, backend="torch") -my_world.scene.add_default_ground_plane() - -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" - -cube = DynamicCuboid(prim_path="/World/cube_0") -prim_paths = my_cloner.generate_paths("/World/cube", 3) -my_cloner.clone(cube.prim_path, prim_paths) - -rigid_prim_view = RigidPrim(prim_paths_expr="/World/cube_[0-2]") - -physics_material_1 = PhysicsMaterial( - prim_path="/Physics_material_1", dynamic_friction=0.2, static_friction=0.2, restitution=0.0 -) -physics_material_2 = PhysicsMaterial( - prim_path="/Physics_material_2", dynamic_friction=0.2, static_friction=0.2, restitution=0.0 -) -physics_material_3 = PhysicsMaterial( - prim_path="/Physics_material_3", dynamic_friction=0.2, static_friction=0.2, restitution=0.0 -) -geometry_prim_view = GeometryPrim( - prim_paths_expr="/World/cube_[0-2]", collisions=torch.tensor([True, True, True], dtype=torch.bool) -) -geometry_prim_view.apply_physics_materials(physics_materials=[physics_material_1, physics_material_3], indices=[2, 0]) -geometry_prim_view.set_contact_offsets(offsets=torch.tensor([0.3, 0.3, 0.3], dtype=torch.float32)) -my_world.scene.add(rigid_prim_view) -my_world.reset() -new_positions = torch.tensor([[10.0, 10.0, 100], [40, 40, 100]], dtype=torch.float32) -new_orientations = euler_angles_to_quats(torch.tensor([[0, 0, np.pi / 2.0], [0, 0, -np.pi / 2.0]], dtype=torch.float32)) -linear_velocities = torch.tensor([[0, 0, -10.0], [0, 0, -10.0], [0, 0, -10.0]], dtype=torch.float32) -rigid_prim_view.set_world_poses(positions=new_positions, orientations=new_orientations, indices=[0, 1]) -rigid_prim_view.set_linear_velocities(velocities=linear_velocities) -rigid_prim_view.set_local_poses(translations=new_positions, orientations=new_orientations, indices=[0, 1]) -print(rigid_prim_view.get_world_poses()) -rigid_prim_view.set_masses(torch.tensor([10.0, 10.0, 10.0], dtype=torch.float32)) -print(rigid_prim_view.get_local_poses(indices=[0, 1])) -print(rigid_prim_view.get_linear_velocities()) -print(rigid_prim_view.get_masses()) -for i in range(10000): - my_world.step(render=True) - print(rigid_prim_view.get_linear_velocities()) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/tensor_api_handles.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/tensor_api_handles.py deleted file mode 100644 index d13eda261..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/tensor_api_handles.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) -import omni.physx as _physx -from isaacsim.core.api import World -from isaacsim.core.api.robots import Robot -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() -assets_root_path = get_assets_root_path() -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka") -articulated_system_1 = my_world.scene.add(Robot(prim_path="/World/Franka", name="my_franka_1")) - - -def step_callback_1(step_size): - b = articulated_system_1.get_joint_velocities() - - -physx_subs = _physx.get_physx_interface().subscribe_physics_step_events(step_callback_1) -my_world.reset() -for j in range(10): - for i in range(5): - my_world.step(render=False) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_articulation_determinism.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_articulation_determinism.py deleted file mode 100644 index 22b29d23b..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_articulation_determinism.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import asyncio - -import carb -import numpy as np -import omni.kit.test -from isaacsim.core.api import World -from isaacsim.core.api.robots import Robot -from isaacsim.core.utils.stage import open_stage, update_stage -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() - - -my_world = World(stage_units_in_meters=1.0) -my_world.reset() - - -def test_franka_slow_convergence(): - open_stage(get_assets_root_path() + "/Isaac/Robots/Franka/franka.usd") - robot_prim_path = "/panda" - - # Start Simulation and wait - my_world = World(stage_units_in_meters=1.0) - my_world.reset() - - robot = Robot(robot_prim_path) - robot.initialize() - robot.get_articulation_controller().set_gains(1e4 * np.ones(9), 1e3 * np.ones(9)) - robot.set_solver_position_iteration_count(64) - robot.set_solver_velocity_iteration_count(64) - robot.post_reset() - - my_world.step(render=True) - - timeout = 200 - - action = ArticulationAction( - joint_positions=np.array( - [ - -0.40236897393760085, - -0.44815597748391767, - -0.16028112816211953, - -2.4554393933564986, - -0.34608791253975374, - 2.9291361940824485, - 0.4814803907662416, - None, - None, - ] - ) - ) - - robot.get_articulation_controller().apply_action(action) - - for i in range(timeout): - my_world.step() - diff = robot.get_joint_positions() - action.joint_positions - if np.linalg.norm(diff) < 0.01: - return i - - return timeout - - -frames_to_converge = np.empty(5) -for i in range(5): - num_frames = test_franka_slow_convergence() - frames_to_converge[i] = num_frames - -# Takes the same number of frames to converge every time -print(f"Over 5 trials, the Franka converged to target in {frames_to_converge} frames.") -if np.unique(frames_to_converge).shape[0] != 1: - print(f"Non-deterministic test converged in varying number of frames: {frames_to_converge}") - raise Exception - -# On the develop branch, this test always takes 26 frames to converge -if frames_to_converge[0] != 26: - print("Didn't converge in the right number of frames") - raise Exception - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_articulation_root.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_articulation_root.py deleted file mode 100644 index 373f061ef..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_articulation_root.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import os - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.stage import add_reference_to_stage - -asset_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data/orientation_bug.usd") -my_world = World(stage_units_in_meters=1.0) -add_reference_to_stage(usd_path=asset_path, prim_path="/World") -articulated = Articulation("/World/microwave") -my_world.scene.add(articulated) -my_world.reset() -for i in range(3): - my_world.step(render=True) -if not (np.isclose(articulated.get_world_poses()[1], [-0.50, -0.49, 0.49, 0.50], atol=1e-02)).all(): - raise ( - ValueError( - f"Articulation is not using the correct default state due to a mismatch in the ArticulationRoot representation" - ) - ) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_delete_in_contact.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_delete_in_contact.py deleted file mode 100644 index 1f0dd95f0..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_delete_in_contact.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -app = SimulationApp({"headless": False}) - -import sys - -import carb -import numpy as np -from isaacsim.core.api import World as Simulator -from isaacsim.core.prims import RigidPrim -from isaacsim.core.utils.prims import add_reference_to_stage, delete_prim -from isaacsim.sensors.physics import _sensor -from isaacsim.storage.native import get_assets_root_path -from pxr import PhysxSchema - -################################################# -# Set this! -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - app.close() - sys.exit() - -################################################# - - -sim = Simulator(stage_units_in_meters=1.0) -sim.scene.add_ground_plane() -stage = sim.stage -sim.stop() - -sim.get_physics_context().enable_gpu_dynamics(True) -sim.get_physics_context().set_broadphase_type("GPU") - -block_0_prim = add_reference_to_stage( - prim_path="/World/block_0", usd_path=assets_root_path + "/Isaac/Props/Blocks/basic_block.usd" -) -block_0 = RigidPrim("/World/block_0/Cube", name="block_0", positions=np.array([[0, 0, 0.5]]), scales=[np.ones(3) * 1.0]) -PhysxSchema.PhysxContactReportAPI.Apply(block_0.prims[0]) - -block_1_prim = add_reference_to_stage( - prim_path="/World/block_1", usd_path=assets_root_path + "/Isaac/Props/Blocks/basic_block.usd" -) -block_1 = RigidPrim( - "/World/block_1/Cube", name="block_1", positions=np.array([[0, 0, 10.0]]), scales=[np.ones(3) * 1.0] -) -PhysxSchema.PhysxContactReportAPI.Apply(block_1.prims[0]) - -cs = _sensor.acquire_contact_sensor_interface() - - -def block_1_is_contacting_block_0(): - raw_data = cs.get_rigid_body_raw_data(block_1.prim_paths[0]) - in_contact = False - for c in raw_data: - c = [*c] - print(c) - if block_0.prim_paths[0] in {cs.decode_body_name(c[2]), cs.decode_body_name(c[3])}: - in_contact = True - break - - return in_contact - - -sim.play() -sim.step() - -while not block_1_is_contacting_block_0(): - sim.step() - -# delete prim once its in contact. -delete_prim(block_0_prim.GetPrimPath().pathString) - -for i in range(1000): - sim.step() - -sim.stop() -app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_rendering.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_rendering.py deleted file mode 100644 index 5dfdf2b72..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_rendering.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import torch -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.prims import XFormPrim - -my_world = World(stage_units_in_meters=1.0, device="cuda:0", backend="torch") -cube_2 = my_world.scene.add( - DynamicCuboid( - prim_path="/new_cube_2", - name="cube_1", - position=torch.tensor([0, 0, 1.0]), - scale=torch.tensor([0.6, 0.5, 0.2]), - size=1.0, - color=torch.tensor([255, 0, 0]), - ) -) -xfrom_cube = XFormPrim("/new_cube_2") -my_world.scene.add_default_ground_plane() -my_world.reset() -for i in range(500): - my_world.step(render=False) -my_world.render() -if not (xfrom_cube.get_world_poses()[0][:, -1].item() < 10e-02): - raise (ValueError(f"PhysX status is not updated in the rendering call")) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_save_stage.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_save_stage.py deleted file mode 100644 index 942ddb3ad..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_save_stage.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.stage import add_reference_to_stage, save_stage -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -simulation_context = SimulationContext() -add_reference_to_stage(asset_path, "/Franka") -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() -simulation_context.play() - -simulation_context.step(render=True) - -assets_root = get_assets_root_path() -if simulation_context._sim_context_initialized == False: - raise (ValueError(f"simulation context is not initialized")) -save_stage(assets_root + "/Users/test/save_stage.usd", save_and_reload_in_place=False) -if simulation_context._sim_context_initialized == False: - raise (ValueError(f"simulation context is not initialized")) -simulation_context.step(render=True) -save_stage(assets_root + "/Users/test/save_stage.usd", save_and_reload_in_place=True) -# this should reload the stage and the context should not be initialized anymore -if simulation_context._sim_context_initialized == True: - raise (ValueError(f"simulation context should not be initialized")) -simulation_context.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_time_stepping.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_time_stepping.py deleted file mode 100644 index bfad599c2..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/test_time_stepping.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import math -import unittest - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" - -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) -if not math.isclose(simulation_context.get_physics_dt(), 1.0 / 60.0): - raise ValueError() -if not math.isclose(simulation_context.get_rendering_dt(), 1.0 / 60.0): - raise ValueError() -simulation_context.clear_instance() -simulation_context = SimulationContext(stage_units_in_meters=1.0) -if not math.isclose(simulation_context.get_physics_dt(), 1.0 / 60.0): - raise ValueError() -if not math.isclose(simulation_context.get_rendering_dt(), 1.0 / 60.0): - raise ValueError() -add_reference_to_stage(asset_path, "/Franka") -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() - - -class TimeStepTester(unittest.TestCase): - def __init__(self): - self.physics_steps = 0 - self.render_steps = 0 - self.physics_dt = 1.0 / 60.0 - self.render_dt = 1.0 / 60.0 - - def step_callback(self, step_size): - print("simulate with step: ", step_size) - self.physics_steps = self.physics_steps + 1 - self.physics_dt = step_size - - def render_callback(self, event): - print("update app with step: ", event.payload["dt"]) - self.render_steps = self.render_steps + 1 - self.render_dt = event.payload["dt"] - - def check_steps(self, physics_steps, render_steps): - if physics_steps != self.physics_steps: - self.assertAlmostEqual(physics_steps, self.physics_steps) - if render_steps != self.render_steps: - self.assertAlmostEqual(render_steps, self.render_steps) - - def check_dt(self, physics_dt, render_dt): - if physics_dt != self.physics_dt: - self.assertAlmostEqual(physics_dt, self.physics_dt) - if render_dt != self.render_dt: - self.assertAlmostEqual(render_dt, self.render_dt) - - def reset_values(self): - self.physics_steps = 0 - self.render_steps = 0 - self.physics_dt = 1.0 / 60.0 - self.render_dt = 1.0 / 60.0 - - -tester = TimeStepTester() -simulation_context.add_physics_callback("physics_callback", tester.step_callback) -simulation_context.add_render_callback("render_callback", tester.render_callback) -simulation_context.stop() -simulation_context.play() -tester.reset_values() - -print("step physics once with a step size of 1/60 second, these are the default settings") -simulation_context.step(render=False) -tester.check_dt(1.0 / 60.0, 1.0 / 60.0) -tester.check_steps(1, 0) -tester.reset_values() - -print("step physics & rendering once with a step size of 1/60 second, these are the default settings") -simulation_context.step(render=True) -tester.check_dt(1.0 / 60.0, 1.0 / 60.0) -tester.check_steps(1, 1) -tester.reset_values() -print("step physics & rendering once with a step size of 1/60 second") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0) -simulation_context.step(render=True) -tester.check_dt(1.0 / 60.0, 1.0 / 60.0) -tester.check_steps(1, 1) -tester.reset_values() -print("step physics 10 steps at a 1/600s per step and rendering at 1.0/60s") -simulation_context.set_simulation_dt(physics_dt=1.0 / 600.0, rendering_dt=1.0 / 60.0) -simulation_context.step(render=True) -tester.check_dt(1.0 / 600.0, 1.0 / 60.0) -tester.check_steps(10, 1) -tester.reset_values() - -print("step physics once at 600Hz without rendering") -simulation_context.set_simulation_dt(physics_dt=1.0 / 600.0, rendering_dt=1.0 / 60.0) -simulation_context.step(render=False) -tester.check_dt(1.0 / 600.0, 1.0 / 60.0) -tester.check_steps(1, 0) -tester.reset_values() - -print("step physics 10 steps at a 1/600s per step and rendering at 1.0/60s") -simulation_context.set_simulation_dt(physics_dt=1.0 / 600.0, rendering_dt=1.0 / 60.0) -for step in range(10): - simulation_context.step(render=False) -simulation_context.render() -tester.check_dt(1.0 / 600.0, 1.0 / 60.0) -tester.check_steps(10, 1) -tester.reset_values() - -print("render a frame, moving editor timeline forward by 1.0/60s, physics does not simulate") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0) -simulation_context.render() -tester.check_dt(1.0 / 60.0, 1.0 / 60.0) -tester.check_steps(0, 1) -tester.reset_values() - -print("render a frame, moving editor timeline forward by 1.0/60s, physics does not simulate") -simulation_context.set_simulation_dt(physics_dt=0.0, rendering_dt=1.0 / 60) -simulation_context.step(render=True) -tester.check_dt(1.0 / 60.0, 1.0 / 60.0) -tester.check_steps(0, 1) -tester.reset_values() - -print("step physics once 1/60s per step and rendering 10 times at 1.0/600s") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 600.0) -for step in range(10): - simulation_context.step(render=True) -tester.check_dt(1.0 / 60.0, 1.0 / 600.0) -tester.check_steps(1, 10) -tester.reset_values() - -print("step physics once 1/60s per step and rendering once at 1.0/600s by explicitly calling step and render") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 600.0) -simulation_context.step(render=False) -simulation_context.render() -tester.check_dt(1.0 / 60.0, 1.0 / 600.0) -tester.check_steps(1, 1) -tester.reset_values() - -print("step physics once 1/60s per step, rendering a frame does not move editor timeline forward") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=0.0) -simulation_context.step(render=False) -simulation_context.render() - -tester.check_dt(1.0 / 60.0, 0) -tester.check_steps(1, 1) -tester.reset_values() - -print("step physics once 1/60s per step, rendering a frame does not move editor timeline forward") -simulation_context.set_simulation_dt(physics_dt=1.0 / 60.0, rendering_dt=0.0) -simulation_context.step(render=True) -tester.check_dt(1.0 / 60.0, 0) -tester.check_steps(1, 1) -tester.reset_values() - -print("render a new frame with simulation stopped, editor timeline does not move forward") -simulation_context.stop() # stop calls render, so clear tester after this -tester.reset_values() - -simulation_context.render() -tester.check_dt(1.0 / 60.0, 0) -tester.check_steps(0, 1) -tester.reset_values() - - -print("cleanup and exit") -simulation_context.stop() -simulation_context.clear_instance() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/xform_prim_view.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/xform_prim_view.py deleted file mode 100644 index b23e45b1a..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.core.api/xform_prim_view.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import argparse -import random -import sys - -import carb -import numpy as np -import torch -from isaacsim.core.api import World -from isaacsim.core.api.materials.omni_glass import OmniGlass -from isaacsim.core.cloner import Cloner -from isaacsim.core.prims import XFormPrim -from isaacsim.core.utils.numpy.rotations import euler_angles_to_quats -from isaacsim.core.utils.prims import define_prim -from isaacsim.core.utils.stage import add_reference_to_stage -from isaacsim.storage.native import get_assets_root_path - -parser = argparse.ArgumentParser() -parser.add_argument("--test", default=False, action="store_true", help="Run in test mode") -args, unknown = parser.parse_known_args() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0, backend="numpy") -my_world.scene.add_default_ground_plane() -num_objects = 3 -my_cloner = Cloner() - -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" -root_path = "/World/Group" -root_group_path = root_path + "_0" -group_paths = my_cloner.generate_paths(root_path, num_objects) -define_prim(prim_path=root_group_path) -add_reference_to_stage(usd_path=asset_path, prim_path=root_group_path + "/Franka") -define_prim(root_group_path + "/Frame") -define_prim(root_group_path + "/Frame/Target") -my_cloner.clone(root_group_path, group_paths) - -frankas_view = XFormPrim(prim_paths_expr=f"/World/Group_[0-{num_objects-1}]/Franka", name="frankas_view") -targets_view = XFormPrim(prim_paths_expr=f"/World/Group_[0-{num_objects-1}]/Frame/Target", name="targets_view") -frames_view = XFormPrim(prim_paths_expr=f"/World/Group_[0-{num_objects-1}]/Frame", name="frames_view") - -glass_1 = OmniGlass( - prim_path=f"/World/franka_glass_material_1", - ior=1.25, - depth=0.001, - thin_walled=False, - color=np.array([random.random(), random.random(), random.random()]), -) - -glass_2 = OmniGlass( - prim_path=f"/World/franka_glass_material_2", - ior=1.25, - depth=0.001, - thin_walled=False, - color=np.array([random.random(), random.random(), random.random()]), -) -# new_positions = torch.tensor([[10.0, 10.0, 0], [-40, -40, 0], [40, 40, 0]]) -# new_orientations = euler_angles_to_quats( -# torch.tensor([[0, 0, np.pi / 2.0], [0, 0, -np.pi / 2.0], [0, 0, -np.pi / 2.0]]) -# ) - -new_positions = np.array([[10.0, 10.0, 0], [-40, -40, 0], [40, 40, 0]]) -new_orientations = euler_angles_to_quats(np.array([[0, 0, np.pi / 2.0], [0, 0, -np.pi / 2.0], [0, 0, -np.pi / 2.0]])) - -frankas_view.set_world_poses(positions=new_positions, orientations=new_orientations) -frankas_view.apply_visual_materials(visual_materials=glass_1, indices=[1]) -frankas_view.apply_visual_materials(visual_materials=[glass_1, glass_2], indices=[2, 0]) -print(frankas_view.get_applied_visual_materials(indices=[2, 0])) -print(frankas_view.get_applied_visual_materials()) - -my_world.reset() - -for i in range(10000): - my_world.step(render=True) -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.cortex.framework/cortex_bringup_test.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.cortex.framework/cortex_bringup_test.py deleted file mode 100644 index 96cdd67e1..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.cortex.framework/cortex_bringup_test.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import time - -import numpy as np -from isaacsim.core.api.objects import VisualSphere -from isaacsim.cortex.framework.cortex_world import CortexWorld -from isaacsim.cortex.framework.df import DfNetwork, DfState, DfStateMachineDecider -from isaacsim.cortex.framework.dfb import DfBasicContext -from isaacsim.cortex.framework.robot import add_franka_to_stage - - -class FollowState(DfState): - """The context object is available as self.context. We have access to everything in the context - object, which in this case is everything in the robot object (the command API and the follow - sphere). - """ - - @property - def robot(self): - return self.context.robot - - @property - def follow_sphere(self): - return self.context.robot.follow_sphere - - def enter(self): - self.robot.gripper.close() - self.follow_sphere.set_world_pose(*self.robot.arm.get_fk_pq().as_tuple()) - - def step(self): - target_position, _ = self.follow_sphere.get_world_pose() - self.robot.arm.send_end_effector(target_position=target_position) - return self # Always transition back to this state. - - -def main(): - world = CortexWorld() - robot = world.add_robot(add_franka_to_stage(name="franka", prim_path="/World/Franka")) - - # Add a sphere to the scene to follow, and store it off in a new member as part of the robot. - robot.follow_sphere = world.scene.add( - VisualSphere( - name="follow_sphere", prim_path="/World/FollowSphere", radius=0.02, color=np.array([0.7, 0.0, 0.7]) - ) - ) - world.scene.add_default_ground_plane() - - # Add a simple state machine decider network with the single state defined above. This state - # will be persistently stepped because it always returns itself. - world.add_decider_network(DfNetwork(DfStateMachineDecider(FollowState()), context=DfBasicContext(robot))) - - start_time = time.time() - world.run(simulation_app, play_on_entry=True, is_done_cb=lambda: time.time() - start_time > 3.0) - simulation_app.close() - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.replicator.examples/amr_navigation_occupancy.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.replicator.examples/amr_navigation_occupancy.py deleted file mode 100644 index b0fab32af..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.replicator.examples/amr_navigation_occupancy.py +++ /dev/null @@ -1,395 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -"""Generate synthetic data from an AMR navigating to random locations -""" - -from isaacsim import SimulationApp - -simulation_app = SimulationApp(launch_config={"headless": False}) - -import argparse -import builtins -import os -import random -from itertools import cycle - -import carb.settings -import omni.client -import omni.kit -import omni.kit.app - -# Run with --enable omni.occupancy_sim-version to use this extension -import omni.occupancy_sim -import omni.replicator.core as rep -import omni.timeline -import omni.usd -from isaacsim.core.utils.render_product import * -from isaacsim.core.utils.stage import add_reference_to_stage, create_new_stage -from isaacsim.core.utils.viewports import add_aov_to_viewport -from isaacsim.storage.native import get_assets_root_path -from omni.kit.viewport.utility import get_active_viewport -from pxr import Gf, PhysxSchema, UsdGeom, UsdLux, UsdPhysics - - -class NavSDGDemo: - CARTER_URL = "/Isaac/Samples/Replicator/OmniGraph/nova_carter_nav_only.usd" - DOLLY_URL = "/Isaac/Props/Dolly/dolly_physics.usd" - PROPS_URL = "/Isaac/Props/YCB/Axis_Aligned_Physics" - LEFT_CAMERA_PATH = "/NavWorld/CarterNav/chassis_link/front_hawk/left/camera_left" - RIGHT_CAMERA_PATH = "/NavWorld/CarterNav/chassis_link/front_hawk/right/camera_right" - - def __init__(self): - self._carter_chassis = None - self._carter_nav_target = None - self._dolly = None - self._dolly_light = None - self._props = [] - self._cycled_env_urls = None - self._env_interval = 1 - self._timeline = None - self._timeline_sub = None - self._stage_event_sub = None - self._stage = None - self._trigger_distance = 2.0 - self._num_frames = 0 - self._frame_counter = 0 - self._writer = None - self._out_dir = None - self._render_products = [] - self._use_temp_rp = False - self._in_running_state = False - - def start( - self, - num_frames=10, - out_dir=None, - env_urls=[], - env_interval=3, - use_temp_rp=False, - seed=None, - ): - print(f"[NavSDGDemo] Starting") - if seed is not None: - random.seed(seed) - self._num_frames = num_frames - self._out_dir = out_dir if out_dir is not None else os.path.join(os.getcwd(), "_out_nav_sdg_demo") - self._cycled_env_urls = cycle(env_urls) - self._env_interval = env_interval - self._use_temp_rp = use_temp_rp - self._frame_counter = 0 - self._trigger_distance = 2.0 - self._load_env() - self._randomize_dolly_pose() - self._randomize_dolly_light() - self._randomize_prop_poses() - self._setup_sdg() - self._timeline = omni.timeline.get_timeline_interface() - self._timeline.play() - self._timeline_sub = self._timeline.get_timeline_event_stream().create_subscription_to_pop_by_type( - int(omni.timeline.TimelineEventType.CURRENT_TIME_TICKED), self._on_timeline_event - ) - self._stage_event_sub = ( - omni.usd.get_context() - .get_stage_event_stream() - .create_subscription_to_pop_by_type(int(omni.usd.StageEventType.CLOSING), self._on_stage_closing_event) - ) - self._in_running_state = True - - def clear(self): - self._cycled_env_urls = None - self._carter_chassis = None - self._carter_nav_target = None - self._dolly = None - self._dolly_light = None - self._timeline = None - self._frame_counter = 0 - if self._stage_event_sub: - self._stage_event_sub.unsubscribe() - self._stage_event_sub = None - if self._timeline_sub: - self._timeline_sub.unsubscribe() - self._timeline_sub = None - self._clear_sdg_render_products() - self._stage = None - self._in_running_state = False - - def is_running(self): - return self._in_running_state - - def _is_running_in_script_editor(self): - return builtins.ISAAC_LAUNCHED_FROM_TERMINAL is True - - def _on_stage_closing_event(self, e: carb.events.IEvent): - self.clear() - - def _load_env(self): - # Fresh stage with custom physics scene for carter's navigation - create_new_stage() - self._stage = omni.usd.get_context().get_stage() - self._add_physics_scene() - - # Environment - assets_root_path = get_assets_root_path() - add_reference_to_stage(usd_path=assets_root_path + next(self._cycled_env_urls), prim_path="/Environment") - - # Carter - add_reference_to_stage(usd_path=assets_root_path + self.CARTER_URL, prim_path="/NavWorld/CarterNav") - self._carter_nav_target = self._stage.GetPrimAtPath("/NavWorld/CarterNav/targetXform") - self._carter_chassis = self._stage.GetPrimAtPath("/NavWorld/CarterNav/chassis_link") - - # Dolly - add_reference_to_stage(usd_path=assets_root_path + self.DOLLY_URL, prim_path="/NavWorld/Dolly") - self._dolly = self._stage.GetPrimAtPath("/NavWorld/Dolly") - if not self._dolly.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(self._dolly).AddTranslateOp() - if not self._dolly.GetAttribute("xformOp:rotateXYZ"): - UsdGeom.Xformable(self._dolly).AddRotateXYZOp() - - # Light - light = UsdLux.SphereLight.Define(self._stage, f"/NavWorld/DollyLight") - light.CreateRadiusAttr(0.5) - light.CreateIntensityAttr(35000) - light.CreateColorAttr(Gf.Vec3f(1.0, 1.0, 1.0)) - self._dolly_light = light.GetPrim() - if not self._dolly_light.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(self._dolly_light).AddTranslateOp() - - # Props - props_urls = [] - props_folder_path = assets_root_path + self.PROPS_URL - result, entries = omni.client.list(props_folder_path) - if result != omni.client.Result.OK: - carb.log_error(f"Could not list assets in path: {props_folder_path}") - return - for entry in entries: - _, ext = os.path.splitext(entry.relative_path) - if ext == ".usd": - props_urls.append(f"{props_folder_path}/{entry.relative_path}") - - cycled_props_url = cycle(props_urls) - for i in range(15): - prop_url = next(cycled_props_url) - prop_name = os.path.splitext(os.path.basename(prop_url))[0] - path = f"/NavWorld/Props/Prop_{prop_name}_{i}" - prim = self._stage.DefinePrim(path, "Xform") - prim.GetReferences().AddReference(prop_url) - self._props.append(prim) - - def _add_physics_scene(self): - # Physics setup specific for the navigation graph - physics_scene = UsdPhysics.Scene.Define(self._stage, "/physicsScene") - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(self._stage.GetPrimAtPath("/physicsScene")) - physx_scene.GetEnableCCDAttr().Set(True) - physx_scene.GetEnableGPUDynamicsAttr().Set(False) - physx_scene.GetBroadphaseTypeAttr().Set("MBP") - - def _randomize_dolly_pose(self): - min_dist_from_carter = 4 - carter_loc = self._carter_chassis.GetAttribute("xformOp:translate").Get() - for _ in range(100): - x, y = random.uniform(-6, 6), random.uniform(-6, 6) - dist = (Gf.Vec2f(x, y) - Gf.Vec2f(carter_loc[0], carter_loc[1])).GetLength() - if dist > min_dist_from_carter: - self._dolly.GetAttribute("xformOp:translate").Set((x, y, 0)) - self._carter_nav_target.GetAttribute("xformOp:translate").Set((x, y, 0)) - break - self._dolly.GetAttribute("xformOp:rotateXYZ").Set((0, 0, random.uniform(-180, 180))) - - def _randomize_dolly_light(self): - dolly_loc = self._dolly.GetAttribute("xformOp:translate").Get() - self._dolly_light.GetAttribute("xformOp:translate").Set(dolly_loc + (0, 0, 2.5)) - self._dolly_light.GetAttribute("inputs:color").Set( - (random.uniform(0, 1), random.uniform(0, 1), random.uniform(0, 1)) - ) - - def _randomize_prop_poses(self): - spawn_loc = self._dolly.GetAttribute("xformOp:translate").Get() - spawn_loc[2] = spawn_loc[2] + 0.5 - for prop in self._props: - prop.GetAttribute("xformOp:translate").Set(spawn_loc + (random.uniform(-1, 1), random.uniform(-1, 1), 0)) - spawn_loc[2] = spawn_loc[2] + 0.2 - - def _setup_sdg(self): - # Disable capture on play and async rendering - carb.settings.get_settings().set("/omni/replicator/captureOnPlay", False) - carb.settings.get_settings().set("/omni/replicator/asyncRendering", False) - carb.settings.get_settings().set("/app/asyncRendering", False) - - # Set camera sensors fStop to 0.0 to get well lit sharp images - left_camera_prim = self._stage.GetPrimAtPath(self.LEFT_CAMERA_PATH) - left_camera_prim.GetAttribute("fStop").Set(0.0) - right_camera_prim = self._stage.GetPrimAtPath(self.RIGHT_CAMERA_PATH) - right_camera_prim.GetAttribute("fStop").Set(0.0) - - self._writer = rep.WriterRegistry.get("OccupancyWriter") - self._writer.initialize( - output_dir="/tmp/replicator_out/", - run_id=None, - compute_occupancy=True, - xmax=50, - ymax=50, - zmax=5, - voxel_size=0.1, - save_vdb=False, - debug=False, - save_numpy=True, - accumulate_grids=False, - save_compressed=True, - ) - - self._setup_sdg_render_products() - - def _setup_sdg_render_products(self): - print(f"[NavSDGDemo] Creating SDG render products") - rp_left = rep.create.render_product( - self.LEFT_CAMERA_PATH, - (1024, 1024), - name="left_sensor", - force_new=True, - ) - rp_right = rep.create.render_product( - self.RIGHT_CAMERA_PATH, - (1024, 1024), - name="right_sensor", - force_new=True, - ) - - _, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxIDS", path="/NavWorld/CarterNav/chassis_link/IDS", parent=None - ) - - texture = rep.create.render_product(sensor.GetPath().pathString, resolution=[1, 1], name="ids") - rp_sensor = texture.path - - occupancy_annotator = rep.AnnotatorRegistry.get_annotator("ConvertOccupancyToEgo") - occupancy_annotator.attach(rp_sensor) - - compute_occupancy = rep.AnnotatorRegistry.get_annotator("compute_occupancy") - - self._render_products = [rp_sensor] - # For better performance the render products can be disabled when not in use, and re-enabled only during SDG - if self._use_temp_rp: - self._disable_render_products() - self._writer.attach(self._render_products) - self._writer.add_annotator(compute_occupancy) - rep.orchestrator.preview() - - def _clear_sdg_render_products(self): - print(f"[NavSDGDemo] Clearing SDG render products") - if self._writer: - self._writer.detach() - for rp in self._render_products: - rp.destroy() - self._render_products.clear() - if self._stage.GetPrimAtPath("/Replicator"): - omni.kit.commands.execute("DeletePrimsCommand", paths=["/Replicator"]) - - def _enable_render_products(self): - print(f"[NavSDGDemo] Enabling render products for SDG..") - for rp in self._render_products: - rp.hydra_texture.set_updates_enabled(True) - - def _disable_render_products(self): - print(f"[NavSDGDemo] Disabling render products (enabled only during SDG)..") - for rp in self._render_products: - rp.hydra_texture.set_updates_enabled(False) - - def _run_sdg(self): - if self._use_temp_rp: - self._enable_render_products() - rep.orchestrator.step(rt_subframes=16) - rep.orchestrator.wait_until_complete() - # print("Calling write explicilty") - # self._writer.schedule_write() - if self._use_temp_rp: - self._disable_render_products() - - async def _run_sdg_async(self): - if self._use_temp_rp: - self._enable_render_products() - await rep.orchestrator.step_async(rt_subframes=16) - await rep.orchestrator.wait_until_complete_async() - # print("Calling write explicilty") - # self._writer.schedule_write() - if self._use_temp_rp: - self._disable_render_products() - - def _load_next_env(self): - if self._stage.GetPrimAtPath("/Environment"): - omni.kit.commands.execute("DeletePrimsCommand", paths=["/Environment"]) - assets_root_path = get_assets_root_path() - add_reference_to_stage(usd_path=assets_root_path + next(self._cycled_env_urls), prim_path="/Environment") - - def _on_sdg_done(self, task): - self._setup_next_frame() - - def _setup_next_frame(self): - self._frame_counter += 1 - if self._frame_counter >= self._num_frames: - print(f"[NavSDGDemo] Finished") - self.clear() - return - self._randomize_dolly_pose() - self._randomize_dolly_light() - self._randomize_prop_poses() - if self._frame_counter % self._env_interval == 0: - self._load_next_env() - # Set a new random distance from which to take capture the next frame - self._trigger_distance = random.uniform(1.75, 2.5) - self._timeline.play() - self._timeline_sub = self._timeline.get_timeline_event_stream().create_subscription_to_pop_by_type( - int(omni.timeline.TimelineEventType.CURRENT_TIME_TICKED), self._on_timeline_event - ) - - def _on_timeline_event(self, e: carb.events.IEvent): - carter_loc = self._carter_chassis.GetAttribute("xformOp:translate").Get() - dolly_loc = self._dolly.GetAttribute("xformOp:translate").Get() - dist = (Gf.Vec2f(dolly_loc[0], dolly_loc[1]) - Gf.Vec2f(carter_loc[0], carter_loc[1])).GetLength() - if dist < self._trigger_distance: - print(f"[NavSDGDemo] Starting SDG for frame no. {self._frame_counter}") - self._timeline.pause() - self._timeline_sub.unsubscribe() - if self._is_running_in_script_editor(): - import asyncio - - task = asyncio.ensure_future(self._run_sdg_async()) - task.add_done_callback(self._on_sdg_done) - else: - self._run_sdg() - self._setup_next_frame() - - -ENV_URLS = [ - # "/Isaac/Environments/Grid/default_environment.usd", - "/Isaac/Environments/Simple_Warehouse/warehouse.usd", - # "/Isaac/Environments/Grid/gridroom_black.usd", -] - -parser = argparse.ArgumentParser() -parser.add_argument("--use_temp_rp", action="store_true", help="Create and destroy render products for each SDG frame") -parser.add_argument("--num_frames", type=int, default=9, help="The number of frames to capture") -parser.add_argument("--env_interval", type=int, default=3, help="Interval at which to change the environments") -args, unknown = parser.parse_known_args() - -out_dir = os.path.join(os.getcwd(), "_out_nav_sdg_demo", "") -nav_demo = NavSDGDemo() -nav_demo.start( - num_frames=args.num_frames, - out_dir=out_dir, - env_urls=ENV_URLS, - env_interval=args.env_interval, - use_temp_rp=args.use_temp_rp, - seed=124, -) - -while simulation_app.is_running() and nav_demo.is_running(): - simulation_app.update() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.replicator.examples/motion_blur_short.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.replicator.examples/motion_blur_short.py deleted file mode 100644 index 4188baa4f..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.replicator.examples/motion_blur_short.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import os - -import carb.settings -import omni.kit.app -import omni.replicator.core as rep -import omni.timeline -import omni.usd -from isaacsim.storage.native import get_assets_root_path -from pxr import PhysxSchema, Sdf, UsdGeom, UsdPhysics - -# Paths to the animated and physics-ready assets -PHYSICS_ASSET_URL = "/Isaac/Props/YCB/Axis_Aligned_Physics/003_cracker_box.usd" -ANIM_ASSET_URL = "/Isaac/Props/YCB/Axis_Aligned/003_cracker_box.usd" - -# -z velocities and start locations of the animated (left side) and physics (right side) assets (stage units/s) -ASSET_VELOCITIES = [0, 5, 10] -ASSET_X_MIRRORED_LOCATIONS = [(0.5, 0, 0.3), (0.3, 0, 0.3), (0.1, 0, 0.3)] - -# Used to calculate how many frames to animate the assets to maintain the same velocity as the physics assets -ANIMATION_DURATION = 10 - -# Create a new stage with animated and physics-enabled assets with synchronized motion -def setup_stage(): - # Create new stage - omni.usd.get_context().new_stage() - stage = omni.usd.get_context().get_stage() - timeline = omni.timeline.get_timeline_interface() - timeline.set_end_time(ANIMATION_DURATION) - - # Create lights - dome_light = stage.DefinePrim("/World/DomeLight", "DomeLight") - dome_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(100.0) - distant_light = stage.DefinePrim("/World/DistantLight", "DistantLight") - if not distant_light.GetAttribute("xformOp:rotateXYZ"): - UsdGeom.Xformable(distant_light).AddRotateXYZOp() - distant_light.GetAttribute("xformOp:rotateXYZ").Set((-75, 0, 0)) - distant_light.CreateAttribute("inputs:intensity", Sdf.ValueTypeNames.Float).Set(2500) - - # Setup the physics assets with gravity disabled and the requested velocity - assets_root_path = get_assets_root_path() - physics_asset_url = assets_root_path + PHYSICS_ASSET_URL - for loc, vel in zip(ASSET_X_MIRRORED_LOCATIONS, ASSET_VELOCITIES): - prim = stage.DefinePrim(f"/World/physics_asset_{int(abs(vel))}", "Xform") - prim.GetReferences().AddReference(physics_asset_url) - if not prim.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(prim).AddTranslateOp() - prim.GetAttribute("xformOp:translate").Set(loc) - prim.GetAttribute("physxRigidBody:disableGravity").Set(True) - prim.GetAttribute("physxRigidBody:angularDamping").Set(0.0) - prim.GetAttribute("physxRigidBody:linearDamping").Set(0.0) - prim.GetAttribute("physics:velocity").Set((0, 0, -vel)) - - # Setup animated assets maintaining the same velocity as the physics asssets - anim_asset_url = assets_root_path + ANIM_ASSET_URL - for loc, vel in zip(ASSET_X_MIRRORED_LOCATIONS, ASSET_VELOCITIES): - start_loc = (-loc[0], loc[1], loc[2]) - prim = stage.DefinePrim(f"/World/anim_asset_{int(abs(vel))}", "Xform") - prim.GetReferences().AddReference(anim_asset_url) - if not prim.GetAttribute("xformOp:translate"): - UsdGeom.Xformable(prim).AddTranslateOp() - anim_distance = vel * ANIMATION_DURATION - end_loc = (start_loc[0], start_loc[1], start_loc[2] - anim_distance) - end_keyframe = timeline.get_time_codes_per_seconds() * ANIMATION_DURATION - # Timesampled keyframe (animated) translation - prim.GetAttribute("xformOp:translate").Set(start_loc, time=0) - prim.GetAttribute("xformOp:translate").Set(end_loc, time=end_keyframe) - - -# Capture motion blur frames with the given delta time step and render mode -def run_motion_blur_example(num_frames=3, custom_delta_time=None, use_path_tracing=True, pt_subsamples=8, pt_spp=64): - # Create a new stage with the assets - setup_stage() - stage = omni.usd.get_context().get_stage() - - # Set replicator settings (capture only on request and enable motion blur) - carb.settings.get_settings().set("/omni/replicator/captureOnPlay", False) - carb.settings.get_settings().set("/omni/replicator/captureMotionBlur", True) - - # Set motion blur settings based on the render mode - if use_path_tracing: - print(f"[MotionBlur] Setting PathTracing render mode motion blur settings") - carb.settings.get_settings().set("/rtx/rendermode", "PathTracing") - # (int): Total number of samples for each rendered pixel, per frame. - carb.settings.get_settings().set("/rtx/pathtracing/spp", pt_spp) - # (int): Maximum number of samples to accumulate per pixel. When this count is reached the rendering stops until a scene or setting change is detected, restarting the rendering process. Set to 0 to remove this limit. - carb.settings.get_settings().set("/rtx/pathtracing/totalSpp", pt_spp) - carb.settings.get_settings().set("/rtx/pathtracing/optixDenoiser/enabled", 0) - # Number of sub samples to render if in PathTracing render mode and motion blur is enabled. - carb.settings.get_settings().set("/omni/replicator/pathTracedMotionBlurSubSamples", pt_subsamples) - else: - print(f"[MotionBlur] Setting RaytracedLighting render mode motion blur settings") - carb.settings.get_settings().set("/rtx/rendermode", "RaytracedLighting") - # 0: Disabled, 1: TAA, 2: FXAA, 3: DLSS, 4:RTXAA - carb.settings.get_settings().set("/rtx/post/aa/op", 2) - # (float): The fraction of the largest screen dimension to use as the maximum motion blur diameter. - carb.settings.get_settings().set("/rtx/post/motionblur/maxBlurDiameterFraction", 0.02) - # (float): Exposure time fraction in frames (1.0 = one frame duration) to sample. - carb.settings.get_settings().set("/rtx/post/motionblur/exposureFraction", 1.0) - # (int): Number of samples to use in the filter. A higher number improves quality at the cost of performance. - carb.settings.get_settings().set("/rtx/post/motionblur/numSamples", 8) - - # Setup camera and writer - camera = rep.create.camera(position=(0, 1.5, 0), look_at=(0, 0, 0), name="MotionBlurCam") - render_product = rep.create.render_product(camera, (1280, 720)) - basic_writer = rep.WriterRegistry.get("BasicWriter") - delta_time_str = "None" if custom_delta_time is None else f"{custom_delta_time:.4f}" - render_mode_str = f"pt_subsamples_{pt_subsamples}_spp_{pt_spp}" if use_path_tracing else "rt" - output_directory = os.getcwd() + f"/_out_motion_blur_dt_{delta_time_str}_{render_mode_str}" - print(f"[MotionBlur] Output directory: {output_directory}") - basic_writer.initialize(output_dir=output_directory, rgb=True) - basic_writer.attach(render_product) - - # Run a few updates to make sure all materials are fully loaded for capture - for _ in range(50): - simulation_app.update() - - # Use the physics scene to modify the physics FPS (if needed) to guarantee motion samples at any custom delta time - physx_scene = None - for prim in stage.Traverse(): - if prim.IsA(UsdPhysics.Scene): - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(prim) - break - if physx_scene is None: - print(f"[MotionBlur] Creating a new PhysicsScene") - physics_scene = UsdPhysics.Scene.Define(stage, "/PhysicsScene") - physx_scene = PhysxSchema.PhysxSceneAPI.Apply(stage.GetPrimAtPath("/PhysicsScene")) - - # Check the target physics depending on the custom delta time and the render mode - target_physics_fps = stage.GetTimeCodesPerSecond() if custom_delta_time is None else 1 / custom_delta_time - if use_path_tracing: - target_physics_fps *= pt_subsamples - - # Check if the physics FPS needs to be increased to match the custom delta time - orig_physics_fps = physx_scene.GetTimeStepsPerSecondAttr().Get() - if target_physics_fps > orig_physics_fps: - print(f"[MotionBlur] Changing physics FPS from {orig_physics_fps} to {target_physics_fps}") - physx_scene.GetTimeStepsPerSecondAttr().Set(target_physics_fps) - - # Start the timeline for physics updates in the step function - timeline = omni.timeline.get_timeline_interface() - timeline.play() - - # Capture frames - for i in range(num_frames): - print(f"[MotionBlur] \tCapturing frame {i}") - rep.orchestrator.step(delta_time=custom_delta_time) - - # Restore the original physics FPS - if target_physics_fps > orig_physics_fps: - print(f"[MotionBlur] Restoring physics FPS from {target_physics_fps} to {orig_physics_fps}") - physx_scene.GetTimeStepsPerSecondAttr().Set(orig_physics_fps) - - # Switch back to the raytracing render mode - if use_path_tracing: - print(f"[MotionBlur] Restoring render mode to RaytracedLighting") - carb.settings.get_settings().set("/rtx/rendermode", "RaytracedLighting") - - # Wait until the data is fully written - rep.orchestrator.wait_until_complete() - - -def run_motion_blur_examples(): - motion_blur_step_duration = [None, 1 / 240] # [None, 1 / 30, 1 / 60, 1 / 240] - for custom_delta_time in motion_blur_step_duration: - # RayTracing examples - run_motion_blur_example(custom_delta_time=custom_delta_time, use_path_tracing=False) - # PathTracing examples - spps = [32] # [32, 128] - motion_blur_sub_samples = [4] # [4, 16] - for motion_blur_sub_sample in motion_blur_sub_samples: - for spp in spps: - run_motion_blur_example( - custom_delta_time=custom_delta_time, - use_path_tracing=True, - pt_subsamples=motion_blur_sub_sample, - pt_spp=spp, - ) - - -run_motion_blur_examples() - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.robot.manipulators.examples.franka/torque_control.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.robot.manipulators.examples.franka/torque_control.py deleted file mode 100644 index 6df36036e..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.robot.manipulators.examples.franka/torque_control.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -from isaacsim.core.api import World -from isaacsim.core.api.controllers.base_controller import BaseController -from isaacsim.core.api.tasks import BaseTask -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.robot.manipulators.examples.franka import Franka - -my_world = World(stage_units_in_meters=1.0) - - -# TODO: this should be converted to a test, for now this is not working, we need to verify if force control works. -class FrankaTask(BaseTask): - def __init__(self): - BaseTask.__init__(self, name="dummy_task", offset=None) - self._my_franka = None - self._pd_gains = None - - def set_up_scene(self, scene): - BaseTask.set_up_scene(self, scene) - scene.add_default_ground_plane() - self._my_franka = scene.add( - Franka( - prim_path="/World/Franka", - name="my_franka", - gripper_dof_names=["panda_finger_joint1", "panda_finger_joint2"], - end_effector_prim_name="panda_rightfinger", - gripper_open_position=np.array([0.4, 0.4]) / 0.01, - gripper_closed_position=np.array([0.0, 0.0]), - ) - ) - return - - def get_observations(self): - joints_state = self.scene.get_object("my_franka").get_joints_state() - return { - "franka": { - "joint_positions": np.array(joints_state.positions), - "joint_velcoities": np.array(joints_state.velocities), - } - } - - def post_reset(self): - self._pd_gains = self._my_franka.get_articulation_controller().get_gains() - self._my_franka.get_articulation_controller().switch_control_mode("effort") - return - - -class PDController(BaseController): - def __init__(self, name, kp, kd): - super().__init__(name) - self._kp = kp - self._kd = kd - return - - def forward(self, observations): - position_error = observations["franka"]["target_joint_positions"] - observations["franka"]["joint_positions"] - velocity_error = -observations["franka"]["joint_velcoities"] - joint_efforts = self._kp * position_error + self._kd * velocity_error - return ArticulationAction(joint_efforts=joint_efforts / 100.0) - - -my_task = FrankaTask() -my_world.add_task(my_task) -my_world.reset() -my_franka = my_world.scene.get_object("my_franka") -my_controller = PDController(name="generic_pd_controller", kp=my_task._pd_gains[0], kd=my_task._pd_gains[1]) -articulation_controller = my_franka.get_articulation_controller() - -reset_needed = False -while simulation_app.is_running(): - my_world.step(render=True) - if my_world.is_stopped() and not reset_needed: - reset_needed = True - if my_world.is_playing(): - if reset_needed: - my_world.reset() - my_controller.reset() - reset_needed = False - observations = my_world.get_observations() - target_joint_positions = np.array([1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5]) - observations["franka"]["target_joint_positions"] = target_joint_positions - actions = my_controller.forward(observations) - articulation_controller.apply_action(actions) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros1.bridge/test_carter_lidar.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros1.bridge/test_carter_lidar.py deleted file mode 100644 index 6615efc7e..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros1.bridge/test_carter_lidar.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import argparse -import sys - -import numpy as np -from isaacsim import SimulationApp - -parser = argparse.ArgumentParser(description="ROS Clock Example") -parser.add_argument("--test", action="store_true") -args, unknown = parser.parse_known_args() - -CARTER_STAGE_PATH = "/Carter" -CARTER_USD_PATH = "/Isaac/Robots/Carter/carter_v1_physx_lidar.usd" -BACKGROUND_STAGE_PATH = "/FlatGrid" -BACKGROUND_USD_PATH = "/Isaac/Environments/Grid/default_environment.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -simulation_app = SimulationApp(CONFIG) -import carb -import omni -import omni.graph.core as og -import usdrt.Sdf -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import extensions, prims, rotations, stage, viewports -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf - -extensions.enable_extension("isaacsim.ros1.bridge") - -simulation_app.update() - -if args.test: - from isaacsim.ros1.bridge.scripts.roscore import Roscore - from isaacsim.ros1.bridge.tests.common import wait_for_rosmaster - - roscore = Roscore() - wait_for_rosmaster() - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -# Preparing stage -viewports.set_camera_view(eye=np.array([1.20, 1.20, 0.80]), target=np.array([0, 0, 0.50])) - -# Loading the flat grid environment -stage.add_reference_to_stage(assets_root_path + BACKGROUND_USD_PATH, BACKGROUND_STAGE_PATH) - -# Loading the carter robot USD -prims.create_prim( - CARTER_STAGE_PATH, - "Xform", - position=np.array([0, 0, 0.25]), - orientation=rotations.gf_rotation_to_np_array(Gf.Rotation(Gf.Vec3d(0, 0, 1), 90)), - usd_path=assets_root_path + CARTER_USD_PATH, -) - -simulation_app.update() - -# Add Lidar publisher -graph_path = "/ActionGraph" - -try: - keys = og.Controller.Keys - (graph, nodes, _, _) = og.Controller.edit( - {"graph_path": graph_path, "evaluator_name": "execution"}, - { - keys.CREATE_NODES: [ - ("OnImpulseEvent", "omni.graph.action.OnImpulseEvent"), - ("ReadSimTime", "isaacsim.core.nodes.IsaacReadSimulationTime"), - # Added nodes used for Lidar Publisher - ("ReadLidarBeams", "omni.isaac.range_sensor.IsaacReadLidarBeams"), - ("PublishLidar", "isaacsim.ros1.bridge.ROS1PublishLaserScan"), - ], - keys.CONNECT: [ - ("OnImpulseEvent.outputs:execOut", "ReadLidarBeams.inputs:execIn"), - ("ReadLidarBeams.outputs:execOut", "PublishLidar.inputs:execIn"), - ("ReadSimTime.outputs:simulationTime", "PublishLidar.inputs:timeStamp"), - ("ReadLidarBeams.outputs:azimuthRange", "PublishLidar.inputs:azimuthRange"), - ("ReadLidarBeams.outputs:depthRange", "PublishLidar.inputs:depthRange"), - ("ReadLidarBeams.outputs:horizontalFov", "PublishLidar.inputs:horizontalFov"), - ("ReadLidarBeams.outputs:horizontalResolution", "PublishLidar.inputs:horizontalResolution"), - ("ReadLidarBeams.outputs:intensitiesData", "PublishLidar.inputs:intensitiesData"), - ("ReadLidarBeams.outputs:linearDepthData", "PublishLidar.inputs:linearDepthData"), - ("ReadLidarBeams.outputs:numCols", "PublishLidar.inputs:numCols"), - ("ReadLidarBeams.outputs:numRows", "PublishLidar.inputs:numRows"), - ("ReadLidarBeams.outputs:rotationRate", "PublishLidar.inputs:rotationRate"), - ], - keys.SET_VALUES: [ - ("ReadLidarBeams.inputs:lidarPrim", [usdrt.Sdf.Path(CARTER_STAGE_PATH + "/chassis_link/carter_lidar")]) - ], - }, - ) -except Exception as e: - print(e) - -simulation_app.update() - -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() -simulation_context.play() - -frame = 0 - -while simulation_app.is_running(): - - # Run with a fixed step size - simulation_context.step(render=True) - - # Publish Lidar each frame - og.Controller.attribute(graph_path + "/OnImpulseEvent.state:enableImpulse").set(True) - - if frame > 120: - break - frame = frame + 1 - -simulation_context.stop() - -if args.test: - roscore.shutdown() - roscore = None - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/enable_extension.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/enable_extension.py deleted file mode 100644 index 54280b370..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/enable_extension.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import time - -from isaacsim import SimulationApp - -# Example ROS bridge sample showing rospy and rosclock interaction -kit = SimulationApp() -import omni -from isaacsim.core.utils.extensions import enable_extension - -# enable ROS bridge extension -enable_extension("isaacsim.ros2.bridge") -kit.update() -kit.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/test_carter_camera_multi_robot_nav.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/test_carter_camera_multi_robot_nav.py deleted file mode 100644 index cf5baa47b..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/test_carter_camera_multi_robot_nav.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import sys - -from isaacsim import SimulationApp - -# Default environment: Hospital - -ENV_USD_PATH = "/Isaac/Samples/ROS2/Scenario/multiple_robot_carter_hospital_navigation.usd" - -CONFIG = {"renderer": "RaytracedLighting", "headless": False} - -# Example ROS2 bridge sample demonstrating the manual loading of Multiple Robot Navigation scenario -simulation_app = SimulationApp(CONFIG) -import carb -import omni -import omni.graph.core as og -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path - -# enable ROS2 bridge extension -enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - -# Locate assets root folder to load sample -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -usd_path = assets_root_path + ENV_USD_PATH - -omni.usd.get_context().open_stage(usd_path, None) - -# Wait two frames so that stage starts loading -simulation_app.update() -simulation_app.update() - -print("Loading stage...") -from isaacsim.core.utils.stage import is_stage_loading - -while is_stage_loading(): - simulation_app.update() -print("Loading Complete") - -simulation_context = SimulationContext(stage_units_in_meters=1.0) - -frame = 0 - -# need to initialize physics getting any articulation..etc -simulation_context.initialize_physics() -simulation_context.play() - -simulation_app.update() - -while simulation_app.is_running(): - - # runs with a realtime clock - simulation_app.update() - - if frame > 120: - break - frame = frame + 1 - -simulation_context.stop() -simulation_app.update() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/test_people_sim.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/test_people_sim.py deleted file mode 100644 index 5389a95a9..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.ros2.bridge/test_people_sim.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import sys - -from isaacsim import SimulationApp - -# The most basic usage for creating a simulation app -kit = SimulationApp() - -ADDITIONAL_EXTENSIONS_PEOPLE = [ - "omni.isaac.core", - "omni.anim.people", - "omni.anim.navigation.bundle", - "omni.anim.timeline", - "omni.anim.graph.bundle", - "omni.anim.graph.core", - "omni.anim.graph.ui", - "omni.anim.retarget.bundle", - "omni.anim.retarget.core", - "omni.anim.retarget.ui", - "omni.kit.scripting", -] - -import carb -import omni -from isaacsim.core.utils.extensions import enable_extension - -for e in ADDITIONAL_EXTENSIONS_PEOPLE: - enable_extension(e) - kit.update() - -enable_extension("isaacsim.ros2.bridge") -kit.update() - -# Locate Isaac Sim assets folder to load sample -from isaacsim.storage.native import get_assets_root_path, is_file - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - kit.close() - sys.exit() -usd_path = assets_root_path + "/Isaac/Samples/NvBlox/nvblox_sample_scene.usd" - -omni.usd.get_context().open_stage(usd_path) - -for i in range(100): - kit.update() - -omni.timeline.get_timeline_interface().play() - -for i in range(100): - kit.update() - -kit.close() # Cleanup application diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.physics/contact_sensor_test.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.physics/contact_sensor_test.py deleted file mode 100644 index 608dba6e4..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.physics/contact_sensor_test.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import carb -import numpy as np -import omni -import omni.kit.commands -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.sensors.physics import _sensor -from pxr import Gf - -timeline = omni.timeline.get_timeline_interface() -cs = _sensor.acquire_contact_sensor_interface() - -world = World(stage_units_in_meters=1.0) - -# add a cube in the world -cube_path = "/World/cube" -cube_1 = world.scene.add(DynamicCuboid(prim_path=cube_path, name="cube_1", position=np.array([0, 0, 1.5]), size=1.0)) -# Add a plane for cube to collide with -world.scene.add_default_ground_plane() - -# Setup contact sensor on cube -result, sensor = omni.kit.commands.execute( - "IsaacSensorCreateContactSensor", - path="/Contact_Sensor", - parent=cube_path, - min_threshold=0, - max_threshold=100000000, - color=Gf.Vec4f(1, 1, 1, 1), - radius=-1, - sensor_period=1.0 / 60.0, - translation=Gf.Vec3d(0, 0, 0), -) - -# start simulation -# We must do one full step with rendering before the sensor will work correctly. -world.step(render=True) -timeline.play() -world.step(render=False) - -for frame in range(100): - world.step(render=False) - -print("cube pose", cube_1.get_world_pose()) - -# Get processed contact data -reading = cs.get_sensor_reading(cube_path + "/Contact_Sensor") - -if not reading.is_valid: - raise ValueError("No contact sensor readings") - -if reading.is_valid: - print(str(reading)) - - -# Cleanup -timeline.stop() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.rtx/rtx_lidar_test.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.rtx/rtx_lidar_test.py deleted file mode 100644 index 8cfdbc197..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.rtx/rtx_lidar_test.py +++ /dev/null @@ -1,281 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -# This file is meant as a tool for the isaac sim developers to test and debug. -# It is not meant for users, so use at your own risk. -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument( - "--geo-type", type=str, choices=["cubes", "sphere"], default="cubes", help="Shape to spawn in scene" -) -parser.add_argument("--config", type=str, default="Example_Rotary", help="Lidar config name") -args, _ = parser.parse_known_args() -geo_type = args.geo_type -lidar_config = args.config - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) -import carb -import omni -import omni.kit.viewport.utility -import omni.replicator.core as rep -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import stage -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf, Sdf, UsdGeom, UsdPhysics - -enable_extension("isaacsim.ros2.bridge") - -simulation_app.update() - - -def printinc(i): - print(f"{i}") - return i + 1 - - -i = 0 - -i = printinc(i) # 0 - - -def add_cube(stage, path, scale, offset, physics=False): - cubeGeom = UsdGeom.Cube.Define(stage, path) - cubePrim = stage.GetPrimAtPath(path) - cubeGeom.CreateSizeAttr(1.0) - cubeGeom.AddTranslateOp().Set(offset) - cubeGeom.AddScaleOp().Set(scale) - if physics: - rigid_api = UsdPhysics.RigidBodyAPI.Apply(cubePrim) - rigid_api.CreateRigidBodyEnabledAttr(True) - - UsdPhysics.CollisionAPI.Apply(cubePrim) - return cubePrim - - -i = printinc(i) # 2 -simulation_app.update() - -if geo_type == "cubes": - # add_cube(stage.get_current_stage(), "/World/cxube_x2", (1, 20, 1000), (-5, 0, 500), physics=True) - add_cube(stage.get_current_stage(), "/World/cxube_x1", (1, 20, 5), (5, 0, 0), physics=False) - add_cube(stage.get_current_stage(), "/World/cxube_x2", (1, 20, 1), (-5, 0, 0), physics=False) - add_cube(stage.get_current_stage(), "/World/cxube_x3", (20, 1, 1), (0, 5, 0), physics=False) - add_cube(stage.get_current_stage(), "/World/cxube_x4", (20, 1, 1), (0, -5, 0), physics=False) - add_cube(stage.get_current_stage(), "/World/cxube_x5", (20, 1, 1), (-5, -5, 0), physics=False) - add_cube( - stage.get_current_stage(), - "/World/cube_5", - (0.1764972, 2.0025313, 1.5832705), - (-3.0258131660928367, 0, 0), - physics=False, - ) - -elif geo_type == "sphere": - omni.kit.commands.execute( - "CreatePrimWithDefaultXform", - prim_type="Sphere", - attributes={"radius": 5, "extent": [(-5, -5, -5), (5, 5, 5)]}, - ) - -lidar_config = "RPLIDAR_S2E" -lidar_config = "SICK_microscan3_ABAZ90ZA1P01" -lidar_config = "Sick_MISC3" -lidar_config = "Example_Rotary" -lidar_config = "Example_Solid_State" - -omni.kit.commands.execute( - "CreatePrim", prim_type="DomeLight", attributes={"inputs:intensity": 1000, "inputs:texture:format": "latlong"} -) - -# configNames = [ -# "SICK_tim781", # ok, ros2 scan looks like curve -# "SICK_tim781_legacy", # ok -# "SICK_picoScan150", # ok -# "SICK_multiScan136", # flat scan strange (OK, not evenly spaced or single elevation in the line) -# "SICK_multiScan165", # flat scan strange (OK, not evenly spaced or single elevation in the line) -# ] -# lidar_config = "SICK_multiScan165" - - -i = printinc(i) # 3 -simulation_app.update() - -# Create the lidar sensor that generates data into "RtxSensorCpu" -# Sensor needs to be rotated 90 degrees about X so that its Z up - -# Possible options are Example_Rotary and Example_Solid_State -# drive sim applies 0.5,-0.5,-0.5,w(-0.5), we have to apply the reverse - -i = printinc(i) # 4 -_, sensor1 = omni.kit.commands.execute( - "IsaacSensorCreateRtxLidar", - path="/sensor_solid_state", - parent=None, - config=lidar_config, - translation=(0, 0, -0.04), - orientation=Gf.Quatd(1, 0, 0, 0), # Gf.Quatd is w,i,j,k -) - -i = printinc(i) # 4 -_, sensor2 = omni.kit.commands.execute( - "IsaacSensorCreateRtxLidar", - path="/sensor_solid_state_2", - parent=None, - config=lidar_config, - translation=(0, 0, -0.04), - orientation=Gf.Quatd(1, 0, 0, 0), # Gf.Quatd is w,i,j,k -) - -i = printinc(i) # 5 -hydra_texture_1 = rep.create.render_product(sensor1.GetPath(), [1, 1], name="Isaac").path -hydra_texture_2 = rep.create.render_product(sensor2.GetPath(), [1, 1], name="Isaac").path - -# Create the debug draw pipeline in the post process graph -from omni.syntheticdata import sensors - -i = printinc(i) -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) - -i = printinc(i) -writerNames = [ - # "Writer" + "IsaacPrintRTXLidarInfo", - # "Writer" + "IsaacReadRTXLidarData", - # "RtxLidar" + "DebugDrawPointCloud", - # "RtxLidar" + "DebugDrawPointCloud" + "Buffer", - "RtxLidar" - + "ROS2PublishLaserScan", -] - -annoNames = [ - # "RtxSensorCpuIsaacReadRTXLidarData", - # "RtxSensorCpuIsaacComputeRTXLidarPointCloud", - # "RtxSensorCpuIsaacCreateRTXLidarScanBuffer", - # "RtxSensorCpuIsaacComputeRTXLidarFlatScan", -] -writers = {} -for writ in writerNames: - writers[writ] = rep.writers.get(writ) - writers[writ].attach([hydra_texture_1]) # , render_product_path2]) - writers[writ].attach([hydra_texture_2]) # , render_product_path2]) -# writer.initialize(testMode=True) -annotators = {} -for anno in annoNames: - annotators[anno] = rep.AnnotatorRegistry.get_annotator(anno) - # annotators[anno].initialize(keepOnlyPositiveDistance=True) - annotators[anno].attach([hydra_texture_1]) - annotators[anno].attach([hydra_texture_2]) - -# disable_extension("omni.replicator.core") -i = printinc(i) -simulation_app.update() -simulation_app.update() - - -# omni.kit.commands.execute( -# "ChangeProperty", -# prop_path=Sdf.Path("/Render/PostProcess/SDGPipeline/DispatchSync.inputs:enabled"), -# value=True, -# prev=None, -# ) - -i = printinc(i) -simulation_context.play() - -i = printinc(i) - - -while simulation_app.is_running(): - simulation_app.update() - if simulation_context.is_playing(): - for anno in annotators: - print(f"~~~{anno} Data~~") - data = annotators[anno].get_data() - for entry in data: - print(f"{entry}: ", end="") - if hasattr(data[entry], "__len__"): - print(f"len {len(data[entry])}::", end="") - print(data[entry]) - -# cleanup and shutdown - -i = printinc(i) -simulation_context.stop() - -i = printinc(i) -simulation_app.close() -""" -# Snippet of similar code to use in script editor. -from isaacsim.core.utils import stage -from isaacsim.storage.native import get_assets_root_path -from pxr import UsdGeom, Gf - -#omni.kit.commands.execute('ToolbarPlayButtonClicked') - -UsdGeom.Cube.Define(stage.get_current_stage(), "/World/cube_1").AddTranslateOp().Set((5, 5, 0)) -import omni.kit.commands -_, sensorR = omni.kit.commands.execute( - "IsaacSensorCreateRtxLidar", - path="/sensorR", - parent=None, - config="Example_Solid_State", - translation=(0, 0, 1.0), - orientation=Gf.Quatd(1.0, 0.0, 0.0, 0.0), -) - -hydra_textureR = rep.create.render_product(sensorR.GetPath(), [1, 1], name="Isaac") - -import omni.replicator.core as rep -# Create the debug draw pipeline in the post process graph -writerR = rep.writers.get("RtxLidar" + "DebugDrawPointCloud") -writerR.attach([hydra_textureR]) - - -~~~Create a Camera then use this to add a lidar to it~~ - -stage = omni.usd.get_context().get_stage() -prim = stage.GetPrimAtPath("/Camera") -import omni.isaac.IsaacSensorSchema as IsaacSensorSchema -IsaacSensorSchema.IsaacRtxLidarSensorAPI.Apply(prim) -from pxr import Sdf -camSensorTypeAttr = prim.CreateAttribute("cameraSensorType", Sdf.ValueTypeNames.Token, False) -camSensorTypeAttr.Set("lidar") -tokens = camSensorTypeAttr.GetMetadata("allowedTokens") -if not tokens: - camSensorTypeAttr.SetMetadata("allowedTokens", ["camera", "radar", "lidar"]) -prim.CreateAttribute("sensorModelPluginName", Sdf.ValueTypeNames.String, False).Set("omni.sensors.nv.lidar.lidar_core.plugin") -prim.CreateAttribute("sensorModelConfig", Sdf.ValueTypeNames.String, False).Set("Example_Rotary") - -import omni.replicator.core as rep -hydra_texture = rep.create.render_product("/Camera", [1, 1], name="Isaac") -writer = rep.writers.get("RtxLidar" + "DebugDrawPointCloudBuffer") -writer.attach([hydra_texture]) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~` -import asyncio -import omni.replicator.core as rep -import carb.settings -import omni.usd - -RP_RESOLUTION = (1280, 720) -NUM_RP = 6 - -async def create_new_stage_with_rp_async(): - omni.usd.get_context().new_stage() - from isaacsim.core.utils import stage - stage.add_reference_to_stage("omniverse://isaac-dev.ov.nvidia.com/Isaac/Environments/Simple_Warehouse/full_warehouse.usd", "/background") - # None (`0`), TAA (`1`), FXAA (`2`), DLSS (`3`) and DLAA (`4`) - # carb.settings.get_settings().set("/rtx/post/aa/op", 4) - for i in range(NUM_RP): - rep.create.render_product("/OmniverseKit_Persp", RP_RESOLUTION, name=f"rp_{i}") - -asyncio.ensure_future(create_new_stage_with_rp_async()) -""" diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.rtx/rtx_radar_test.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.rtx/rtx_radar_test.py deleted file mode 100644 index 7a6b3eb2f..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.sensors.rtx/rtx_radar_test.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -# This file is meant as a tool for the isaac sim developers to test and debug. -# It is not meant for users, so use at your own risk. -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument( - "--geo-type", type=str, choices=["cubes", "sphere"], default="cubes", help="Shape to spawn in scene" -) -parser.add_argument("--config", type=str, default="Example", help="Radar config name") -args, _ = parser.parse_known_args() -geo_type = args.geo_type -config = args.config - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) -import carb -import omni -import omni.kit.viewport.utility -import omni.replicator.core as rep -from isaacsim.core.api import SimulationContext -from isaacsim.core.utils import stage -from isaacsim.storage.native import get_assets_root_path -from pxr import Gf, Sdf, UsdGeom, UsdPhysics - - -def printinc(i): - print(f"{i}") - return i + 1 - - -i = 0 - -i = printinc(i) # 0 - - -def add_cube(stage, path, scale, offset, physics=False): - cubeGeom = UsdGeom.Cube.Define(stage, path) - cubePrim = stage.GetPrimAtPath(path) - cubeGeom.CreateSizeAttr(1.0) - cubeGeom.AddTranslateOp().Set(offset) - cubeGeom.AddScaleOp().Set(scale) - if physics: - rigid_api = UsdPhysics.RigidBodyAPI.Apply(cubePrim) - rigid_api.CreateRigidBodyEnabledAttr(True) - - UsdPhysics.CollisionAPI.Apply(cubePrim) - return cubePrim - - -i = printinc(i) # 2 -simulation_app.update() - -if geo_type == "cubes": - # add_cube(stage.get_current_stage(), "/World/cxube_x2", (1, 20, 1000), (-5, 0, 500), physics=True) - add_cube(stage.get_current_stage(), "/World/cxube_x1", (1, 20, 5), (5, 0, 0), physics=False) - add_cube(stage.get_current_stage(), "/World/cxube_x2", (1, 20, 1), (-5, 0, 0), physics=False) - add_cube(stage.get_current_stage(), "/World/cxube_x3", (20, 1, 1), (0, 5, 0), physics=False) - add_cube(stage.get_current_stage(), "/World/cxube_x4", (20, 1, 1), (0, -5, 0), physics=False) - add_cube(stage.get_current_stage(), "/World/cxube_x5", (20, 1, 1), (-5, -5, 0), physics=False) - add_cube( - stage.get_current_stage(), - "/World/cube_5", - (0.1764972, 2.0025313, 1.5832705), - (-3.0258131660928367, 0, 0), - physics=False, - ) - -elif geo_type == "sphere": - omni.kit.commands.execute( - "CreatePrimWithDefaultXform", - prim_type="Sphere", - attributes={"radius": 5, "extent": [(-5, -5, -5), (5, 5, 5)]}, - ) - -omni.kit.commands.execute( - "CreatePrim", prim_type="DomeLight", attributes={"inputs:intensity": 1000, "inputs:texture:format": "latlong"} -) - -i = printinc(i) # 3 -simulation_app.update() - -# Create the lidar sensor that generates data into "RtxSensorCpu" -# Sensor needs to be rotated 90 degrees about X so that its Z up - -# Possible options are Example_Rotary and Example_Solid_State -# drive sim applies 0.5,-0.5,-0.5,w(-0.5), we have to apply the reverse - -i = printinc(i) # 4 -_, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxRadar", - path="/sensor", - parent=None, - config=config, - translation=(0, 0, -0.04), - orientation=Gf.Quatd(1, 0, 0, 0), # Gf.Quatd is w,i,j,k -) - -i = printinc(i) # 5 -hydra_texture = rep.create.render_product(sensor.GetPath(), [1, 1], name="Isaac") - -# Create the debug draw pipeline in the post process graph -from omni.syntheticdata import sensors - -i = printinc(i) -simulation_context = SimulationContext(physics_dt=1.0 / 60.0, rendering_dt=1.0 / 60.0, stage_units_in_meters=1.0) - -i = printinc(i) -writerNames = [ - "RtxRadar" + "DebugDrawPointCloud", - # "Writer" + "IsaacPrintRTXSensorInfo", -] - - -annoNames = [] -writers = {} -for writ in writerNames: - writers[writ] = rep.writers.get(writ) - writers[writ].attach([hydra_texture]) - -annotators = {} -for anno in annoNames: - annotators[anno] = rep.AnnotatorRegistry.get_annotator(anno) - annotators[anno].attach([hydra_texture]) - - -i = printinc(i) -simulation_app.update() - -i = printinc(i) -simulation_context.play() - -i = printinc(i) -while simulation_app.is_running(): - simulation_app.update() - -# cleanup and shutdown - -i = printinc(i) -simulation_context.stop() - -i = printinc(i) -simulation_app.close() -""" -import omni.replicator.core as rep -from pxr import Gf -_, sensor = omni.kit.commands.execute( - "IsaacSensorCreateRtxRadar", - path="/sensor", - parent=None, - config="Example", - translation=(0, 0, -0.04), - orientation=Gf.Quatd(1, 0, 0, 0), # Gf.Quatd is w,i,j,k - #translation=(-0.937, 1.745, 0.8940), - #orientation=Gf.Quatd(0.70711, 0.70711, 0, 0), # Gf.Quatd is w,i,j,k -) - -hydra_texture = rep.create.render_product(sensor.GetPath(), [1, 1], name="Isaac") -writer = rep.writers.get("RtxRadar" + "DebugDrawPointCloud") -writer.attach([hydra_texture]) -""" - -""" -# Callstack of crash - -__pthread_kill_implementation (@pthread_kill@@GLIBC_2.34:81) -__pthread_kill_internal (@pthread_kill@@GLIBC_2.34:59) -__GI___pthread_kill (@pthread_kill@@GLIBC_2.34:59) -__GI_raise (@raise:10) -__GI_abort (@abort:46) -___lldb_unnamed_symbol7245 (@___lldb_unnamed_symbol7245:27) -___lldb_unnamed_symbol7659 (@___lldb_unnamed_symbol7659:8) -std::terminate() (@7892c0aae277..7892c0aae2f7:3) -__cxa_throw (@7892c0aae4d8..7892c0aae550:3) -thrust::cuda_cub::throw_on_error(cudaError, char const*) (@thrust::cuda_cub::throw_on_error(cudaError, char const*):31) -float* thrust::cuda_cub::gather(thrust::cuda_cub::execution_policy&, int*, int*, float*, float*) (@float* thrust::cuda_cub::gather(thrust::cuda_cub::execution_policy&, int*, int*, float*, float*):133) -float* thrust::gather(thrust::detail::execution_policy_base const&, int*, int*, float*, float*) (@float* thrust::gather(thrust::detail::execution_policy_base const&, int*, int*, float*, float*):24) -omni::sensors::nv::radar::sortPointsByDistance_CUDA(omni::sensors::nv::radar::ProcessingContext&, omni::sensors::nv::radar::SortBuffer*, CUstream_st*, int) (@omni::sensors::nv::radar::sortPointsByDistance_CUDA(omni::sensors::nv::radar::ProcessingContext&, omni::sensors::nv::radar::SortBuffer*, CUstream_st*, int):319) -omni::sensors::nv::radar::processResultsCuda(omni::sensors::wpm::Config*, omni::sensors::wpm::TraceResult*, omni::sensors::nv::radar::ProcessingContext&, CUstream_st*, float*, unsigned char, bool, omni::sensors::nv::radar::SortBuffer*) (@omni::sensors::nv::radar::processResultsCuda(omni::sensors::wpm::Config*, omni::sensors::wpm::TraceResult*, omni::sensors::nv::radar::ProcessingContext&, CUstream_st*, float*, unsigned char, bool, omni::sensors::nv::radar::SortBuffer*):203) -omni::sensors::nv::radar::WpmDmatApproxRadar::closeTrace(void*, unsigned long*, void*, unsigned long*) (@omni::sensors::nv::radar::WpmDmatApproxRadar::closeTrace(void*, unsigned long*, void*, unsigned long*):329) -___lldb_unnamed_symbol934 (@___lldb_unnamed_symbol934:15) -___lldb_unnamed_symbol9313 (@___lldb_unnamed_symbol9313:222) -___lldb_unnamed_symbol9293 (@___lldb_unnamed_symbol9293:102) -___lldb_unnamed_symbol9204 (@___lldb_unnamed_symbol9204:8) -___lldb_unnamed_symbol1176 (@___lldb_unnamed_symbol1176:194) -""" diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_createstage_config.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_createstage_config.py deleted file mode 100644 index fe2e62e01..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_createstage_config.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -# Test app startup without creating new stage -kit = SimulationApp({"create_new_stage": False}) - -import omni - -for i in range(100): - kit.update() - -omni.kit.app.get_app().print_and_log("Config: No empty stage was created") - -kit.close() # Cleanup application diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_extension_count.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_extension_count.py deleted file mode 100644 index 2505659db..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_extension_count.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp() - -simulation_app.update() -simulation_app.update() - -from typing import Tuple - -import omni -import omni.ext -import omni.kit.app - -app = omni.kit.app.get_app() -ext_manager = app.get_extension_manager() -ext_summaries = ext_manager.get_extensions() - - -def get_bundled_exts() -> dict[str, Tuple]: - local_exts: dict[str, Tuple] = {} - - for ext_summary in ext_summaries: - ext_name = ext_summary["name"] - ext_enabled = bool(ext_summary["enabled"]) - if not ext_enabled: - continue - - local_exts[ext_name] = (ext_name, ext_enabled) - return local_exts - - -bundled_exts = get_bundled_exts() -print(f"Enabled extensions count: {len(bundled_exts)}") - -# Cleanup application -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_external.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_external.py deleted file mode 100644 index a3a96cd3e..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_external.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import sys - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp() - -import omni -from isaacsim.core.utils.extensions import disable_extension, enable_extension - -simulation_app.update() - -enable_extension("semantics.schema.editor") -simulation_app.update() -disable_extension("semantics.schema.editor") -simulation_app.update() -enable_extension("omni.cuopt.examples") -simulation_app.update() -disable_extension("omni.cuopt.examples") -simulation_app.update() -enable_extension("omni.anim.people") -simulation_app.update() -disable_extension("omni.anim.people") -simulation_app.update() -# Cleanup application -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_extra_args.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_extra_args.py deleted file mode 100644 index 5ca677a36..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_extra_args.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -# The most basic usage for creating a simulation app -kit = SimulationApp({"extra_args": ["--/app/extra/arg=1", "--/app/some/other/arg=2"]}) - -import carb - -kit.update() - -server_check = carb.settings.get_settings().get_as_string("/persistent/isaac/asset_root/default") - -if server_check != "omniverse://ov-test-this-is-working": - raise ValueError(f"isaac nucleus default setting not omniverse://ov-test-this-is-working, instead: {server_check}") - -arg_1 = carb.settings.get_settings().get_as_int("/app/extra/arg") -arg_2 = carb.settings.get_settings().get_as_int("/app/some/other/arg") - -if arg_1 != 1: - raise ValueError(f"/app/extra/arg was not 1 and was {arg_1} instead") - -if arg_2 != 2: - raise ValueError(f"/app/some/other/arg was not 2 and was {arg_2} instead") - -kit.close() # Cleanup application diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_fabric_frame_delay.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_fabric_frame_delay.py deleted file mode 100644 index ce6a56e50..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_fabric_frame_delay.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import os - -from isaacsim import SimulationApp - -simulation_app = SimulationApp( - {"headless": True}, experience=f'{os.environ["EXP_PATH"]}/isaacsim.exp.base.zero_delay.kit' -) - -import sys - -import carb -import isaacsim.core.utils.numpy.rotations as rot_utils -import isaacsim.core.utils.prims as prim_utils -import isaacsim.core.utils.stage as stage_utils -import matplotlib.pyplot as plt -import numpy as np -import torch -from isaacsim.core.api import SimulationContext -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.prims import Articulation, RigidPrim -from isaacsim.core.utils.prims import add_update_semantics, get_prim_attribute_value -from isaacsim.sensors.camera import Camera -from isaacsim.storage.native import get_assets_root_path - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" - - -def main(): - for device, backend in [["cuda:0", "torch"], ["cpu", "numpy"]]: - SimulationContext.clear_instance() - stage_utils.create_new_stage() - sim = SimulationContext(stage_units_in_meters=1.0, physics_dt=0.01, device=device, backend=backend) - prim_utils.create_prim("/World/Origin1", "Xform", translation=[0.0, 0.0, 0.0]) - cube = DynamicCuboid( - prim_path="/World/Origin1/cube", - name="cube", - position=np.array([-3.0, 0.0, 0.1]), - scale=np.array([1.0, 2.0, 0.2]), - size=1.0, - color=np.array([255, 0, 0]), - ) - stage_utils.add_reference_to_stage(usd_path=asset_path, prim_path="/World/Franka") - articulated_system = Articulation("/World/Franka") - rigid_link = RigidPrim("/World/Franka/panda_link1") - sim.reset() - cube.initialize() - rigid_link.initialize() - articulated_system.initialize() - articulated_system.set_world_poses( - positions=torch.tensor([[-10, -10, 0]], device=device) if backend == "torch" else [[-10, -10, 0]] - ) - position = cube.get_world_pose()[0] - position[0] += 3 - cube.set_world_pose(position=position) - if not ( - np.isclose( - get_prim_attribute_value("/World/Origin1/cube", "_worldPosition", fabric=True), - np.array([-3.0, 0.0, 0.1]), - atol=0.01, - ).all() - ): - raise (ValueError(f"PhysX is not synced with Fabric CPU")) - sim.render() - if not ( - np.isclose( - get_prim_attribute_value("/World/Franka/panda_link1", "_worldPosition", fabric=True), - np.array([-10.0, -10.0, 0.33]), - atol=0.01, - ).all() - ): - raise (ValueError(f"Kinematic Tree is not updated in fabric")) - if not ( - np.isclose( - get_prim_attribute_value("/World/Origin1/cube", "_worldPosition", fabric=True), - np.array([0.0, 0.0, 0.1]), - atol=0.01, - ).all() - ): - raise (ValueError(f"PhysX is not synced with Fabric CPU")) - - -if __name__ == "__main__": - main() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_fetch_results.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_fetch_results.py deleted file mode 100644 index 1cd014a5d..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_fetch_results.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -kit = SimulationApp() - -import omni -import omni.physx -from pxr import PhysxSchema, UsdPhysics - -kit.update() - -stage = omni.usd.get_context().get_stage() -scene = UsdPhysics.Scene.Define(stage, "/physicsScene") -physx_scene_api = PhysxSchema.PhysxSceneAPI.Apply(scene.GetPrim()) - -kit.update() - - -def test_callback(step): - print("callback") - - -print("Start test") -physx_interface = omni.physx.acquire_physx_interface() -physx_sim_interface = omni.physx.get_physx_simulation_interface() -# Commenting out the following line will prevent the deadlock -physics_timer_callback = physx_interface.subscribe_physics_step_events(test_callback) - -# In Isaac Sim we run the following to "warm up" physics without simulating forward in time -physx_interface.start_simulation() -physx_interface.force_load_physics_from_usd() -physx_sim_interface.simulate(1.0 / 60.0, 0.0) -print("Fetch results") -physx_sim_interface.fetch_results() - -print("Finish Test") -kit.update() -kit.close() # Cleanup application diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_frame_delay.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_frame_delay.py deleted file mode 100644 index 0d6e522d2..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_frame_delay.py +++ /dev/null @@ -1,272 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -""" -To generate several frames, create and run a bash script with the following content: - -for i in $(seq 64 512); do - echo ${i}x${i} - PATH/TO/python.sh test_frame_delay.py --resolution ${i}x${i} --/app/updateOrder/checkForHydraRenderComplete=1000 -done -""" - - -USE_REPLICATOR_WRITER = True -CAMERA_PATH = "/camera" -CAMERA_POS = [0, 0, 25] -COLLECTION_STEPS = 10 - -# parse any command-line arguments specific to the standalone application -import argparse -import os - -from isaacsim import SimulationApp - -parser = argparse.ArgumentParser() -parser.add_argument("--resolution", type=str, default="256x256", help="Resolution (WxH)") -# Parse only known arguments, so that any (eg) Kit settings are passed through to the core Kit app -args, _ = parser.parse_known_args() - -RESOLUTION = tuple([int(item) for item in args.resolution.split("x")]) -PIXELS_PER_METER = 0.09765625 * RESOLUTION[0] - -simulation_app = SimulationApp( - {"headless": True}, experience=f'{os.environ["EXP_PATH"]}/isaacsim.exp.base.zero_delay.kit' -) - -import pprint - -import carb -import cv2 -import isaacsim.core.utils.numpy.rotations as rot_utils -import numpy as np -import omni.replicator.core as rep -import omni.usd -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid -from isaacsim.core.utils.extensions import enable_extension -from isaacsim.core.utils.prims import add_update_semantics -from isaacsim.core.utils.viewports import set_camera_view -from isaacsim.sensors.camera import Camera -from omni.replicator.core import AnnotatorRegistry, Writer -from pxr import UsdGeom - -# rep.settings.set_render_rtx_realtime(antialiasing="DLAA") - - -class CustomWriter(Writer): - def __init__(self): - self.annotators.append(AnnotatorRegistry.get_annotator("rgb")) - self.annotators.append(AnnotatorRegistry.get_annotator("semantic_segmentation")) - self.annotators.append(AnnotatorRegistry.get_annotator("bounding_box_2d_tight")) - - def write(self, data): - pass - - -def get_data(sensor: Camera | Writer) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Get RGB, semantic segmentation and BBox from Camera or Writer (according to `USE_REPLICATOR_WRITER`)""" - if USE_REPLICATOR_WRITER: - rgb = sensor.get_data()["rgb"] - semantic_segmentation = sensor.get_data()["semantic_segmentation"]["data"] - bbox = sensor.get_data()["bounding_box_2d_tight"]["data"][0] - else: - rgb = sensor.get_rgba() - semantic_segmentation = sensor._custom_annotators["semantic_segmentation"].get_data()["data"] - bbox = sensor._custom_annotators["bounding_box_2d_tight"].get_data()["data"][0] - - semantic_segmentation = (semantic_segmentation * 255 / np.max(semantic_segmentation)).astype(np.uint8) - semantic_segmentation = np.repeat(semantic_segmentation[:, :, np.newaxis], 3, axis=2) - return rgb[:, :, :3], semantic_segmentation, bbox - - -def draw_data(frame, position, bbox, label): - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - frame = cv2.rectangle( - img=frame, - pt1=( - int(RESOLUTION[1] / 2 + PIXELS_PER_METER * position[0] - PIXELS_PER_METER / 2), - int(RESOLUTION[1] / 2 + PIXELS_PER_METER * position[1] - PIXELS_PER_METER), - ), - pt2=( - int(RESOLUTION[1] / 2 + PIXELS_PER_METER * position[0] + PIXELS_PER_METER / 2), - int(RESOLUTION[1] / 2 + PIXELS_PER_METER * position[1] + PIXELS_PER_METER), - ), - color=(255, 255, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - frame = cv2.rectangle( - img=frame, - pt1=(bbox["x_min"], bbox["y_min"]), - pt2=(bbox["x_max"], bbox["y_max"]), - color=(0, 255, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - frame = cv2.putText( - img=frame, - text=label, - org=(5, 15), - fontFace=cv2.FONT_HERSHEY_PLAIN, - fontScale=0.75, - color=(0, 255, 255), - thickness=1, - lineType=cv2.LINE_AA, - ) - frame = cv2.putText( - img=frame, - text=f"position: {(round(position[0], 2), round(position[1], 2), round(position[2], 2))}", - org=(5, 30), - fontFace=cv2.FONT_HERSHEY_PLAIN, - fontScale=0.75, - color=(255, 255, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - frame = cv2.putText( - img=frame, - text=f'bbox: {(bbox["x_min"], bbox["y_min"])} {(bbox["x_max"], bbox["y_max"])}', - org=(5, 45), - fontFace=cv2.FONT_HERSHEY_PLAIN, - fontScale=0.75, - color=(0, 255, 0), - thickness=1, - lineType=cv2.LINE_AA, - ) - return frame - - -def generate_result(data: list[dict], banner: list[str] = []): - rgb_frames = [] - semantic_segmentation_frames = [] - for item in data: - rgb_frames.append(draw_data(item["rgb"], item["position"], item["bbox"], item["label"])) - semantic_segmentation_frames.append( - draw_data(item["semantic_segmentation"], item["position"], item["bbox"], item["label"]) - ) - - separator = np.full((RESOLUTION[0], 5, 3), 0, dtype=np.uint8) - rgb_frames = [x for item in rgb_frames for x in (item, separator)][:-1] - semantic_segmentation_frames = [x for item in semantic_segmentation_frames for x in (item, separator)][:-1] - - frame = cv2.vconcat([cv2.hconcat(rgb_frames), cv2.hconcat(semantic_segmentation_frames)]) - if banner: - frame = cv2.copyMakeBorder( - frame, top=25, bottom=0, left=0, right=0, borderType=cv2.BORDER_CONSTANT, value=[0] * 3 - ) - frame = cv2.putText( - img=frame, - text=", ".join(banner), - org=(5, 15), - fontFace=cv2.FONT_HERSHEY_PLAIN, - fontScale=0.75, - color=(255, 255, 255), - thickness=1, - lineType=cv2.LINE_AA, - ) - return frame - - -simulation_app.update() - -# Setup scene -set_camera_view(eye=CAMERA_POS, target=[0, 0, 0], camera_prim_path="/OmniverseKit_Persp") - -world = World(stage_units_in_meters=1.0) -world.scene.add_default_ground_plane() - -cube = world.scene.add( - DynamicCuboid( - prim_path="/cube", - name="cube", - position=np.array([-3.0, 0.0, 0.1]), - scale=np.array([1.0, 2.0, 0.2]), - size=1.0, - color=np.array([255, 0, 0]), - ) -) -add_update_semantics(cube.prim, "cube") - -camera = None -writer = None -if USE_REPLICATOR_WRITER: - stage = omni.usd.get_context().get_stage() - camera_prim = stage.DefinePrim(CAMERA_PATH, "Camera") - UsdGeom.Xformable(camera_prim).AddTranslateOp().Set(tuple(CAMERA_POS)) - render_product = rep.create.render_product(str(camera_prim.GetPrimPath()), resolution=RESOLUTION) -else: - camera = Camera( - prim_path=CAMERA_PATH, - position=np.array(CAMERA_POS), - resolution=RESOLUTION, - orientation=rot_utils.euler_angles_to_quats(np.array([0, 90, 90]), degrees=True), - ) - -world.reset() - -if USE_REPLICATOR_WRITER: - rep.WriterRegistry.register(CustomWriter) - writer = rep.WriterRegistry.get("CustomWriter") - writer.initialize() - writer.attach([render_product]) -else: - camera.initialize() - camera.add_bounding_box_2d_tight_to_frame() - camera.add_semantic_segmentation_to_frame() - - -# Do some warmup steps -for _ in range(5): - world.step(render=True) - -data = [] -# Get data and object info before running the collection steps -position = cube.get_world_pose()[0] -rgb, semantic_segmentation, bbox = get_data(camera or writer) -data.append( - {"position": position, "rgb": rgb, "semantic_segmentation": semantic_segmentation, "bbox": bbox, "label": "before"} -) - -# Do some collection steps -for i in range(COLLECTION_STEPS): - # Move object - position = cube.get_world_pose()[0] - position[0] += 0.5 - cube.set_world_pose(position=position) - - # Step the simulation - world.step(render=True) - - # Get data and object info - position = cube.get_world_pose()[0] - rgb, semantic_segmentation, bbox = get_data(camera or writer) - data.append( - { - "position": position, - "rgb": rgb, - "semantic_segmentation": semantic_segmentation, - "bbox": bbox, - "label": f"step {i + 1}", - } - ) - -# Export result -banner = [ - f"source: {'rep.Writer' if USE_REPLICATOR_WRITER else 'Camera'}", - f"checkForHydraRenderComplete: {carb.settings.get_settings().get('/app/updateOrder/checkForHydraRenderComplete')}", - f"app.hydraEngine.waitIdle: {carb.settings.get_settings().get('/app/hydraEngine/waitIdle')}", - f"rtx.post.aa.op: {carb.settings.get_settings().get('/rtx/post/aa/op')}", -] -print("") -pprint.pprint(banner) -print("") -cv2.imwrite(f"result-{args.resolution}.png", generate_result(data, banner)) - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_ogn.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_ogn.py deleted file mode 100644 index 90237e819..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_ogn.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import sys - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp() - -import omni - -simulation_app.update() -omni.usd.get_context().new_stage() -simulation_app.update() - -import omni.graph.core as og - -keys = og.Controller.Keys -(graph, (tick_node, test_node, str_node), _, _) = og.Controller.edit( - {"graph_path": "/controller_graph", "evaluator_name": "execution"}, - { - keys.CREATE_NODES: [ - ("OnTick", "omni.graph.action.OnTick"), - ("IsaacTest", "isaacsim.core.nodes.IsaacTestNode"), - ("TestStr", "omni.graph.nodes.ConstantString"), - ], - keys.SET_VALUES: [("TestStr.inputs:value", "Hello"), ("OnTick.inputs:onlyPlayback", False)], # always tick - keys.CONNECT: [ - ("OnTick.outputs:tick", "IsaacTest.inputs:execIn"), - ("TestStr.inputs:value", "IsaacTest.inputs:input"), - ], - }, -) - -input_attr = og.Controller.attribute("inputs:value", str_node) -output_attr = og.Controller.attribute("outputs:output", test_node) - -simulation_app.update() -value = og.DataView.get(output_attr) -print(value) -if value != "Hello": - raise ValueError("Output does not equal Hello") -simulation_app.update() -og.DataView.set(input_attr, "Goodbye") -simulation_app.update() -value = og.DataView.get(output_attr) -print(value) -if value != "Goodbye": - raise ValueError("Output does not equal Goodbye") - -simulation_app.update() -# Cleanup application -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_ovd.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_ovd.py deleted file mode 100644 index 05a32aa62..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_ovd.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import os -from pathlib import Path - -from isaacsim import SimulationApp - -kit = SimulationApp() - -import carb - -for _ in range(10): - kit.update() - -# get the current output path and check if the file exists -pvd_output_dir = carb.settings.get_settings().get_as_string("/persistent/physics/omniPvdOvdRecordingDirectory") - -print("omniPvdOvdRecordingDirectory: ", pvd_output_dir) -my_file = Path(os.path.join(pvd_output_dir, "tmp.ovd")) -assert my_file.is_file() - -kit.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_syntheticdata.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_syntheticdata.py deleted file mode 100644 index 941b32a75..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_syntheticdata.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import sys - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp() -sys.stdout.flush() -import omni - -simulation_app.update() -omni.usd.get_context().new_stage() -simulation_app.update() - -from isaacsim.core.api.objects import VisualCuboid -from isaacsim.sensors.camera import Camera -from omni.kit.viewport.utility import get_active_viewport - -viewport_api = get_active_viewport() -render_product_path = viewport_api.get_render_product_path() - -camera = Camera( - prim_path="/World/camera", - position=np.array([0.0, 0.0, 25.0]), - resolution=(1280, 720), - render_product_path=render_product_path, -) -# play to start capturing data -omni.timeline.get_timeline_interface().play() -simulation_app.update() -camera.initialize() - - -VisualCuboid( - prim_path="/new_cube_1", - name="visual_cube", - position=np.array([5.0, 3, 1.0]), - scale=np.array([0.6, 0.5, 0.2]), - size=1.0, - color=np.array([255, 0, 0]), -) -simulation_app.update() -for annotator in [ - "pointcloud", - "normals", - "motion_vectors", - "occlusion", - "distance_to_image_plane", - "distance_to_camera", - "bounding_box_2d_tight", - "bounding_box_2d_loose", - "bounding_box_3d", - "semantic_segmentation", - "instance_id_segmentation", - "instance_segmentation", -]: - getattr(camera, "add_{}_to_frame".format(annotator))() - -simulation_app.update() -simulation_app.update() -rgba = camera.get_rgba() -print(rgba.size) - -if rgba.size != 1280 * 720 * 4: - raise ValueError(f"RGB buffer has size of {rgba.size} which is not {1280*720*4}") - -# Cleanup application -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_unsaved_on_exit.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_unsaved_on_exit.py deleted file mode 100644 index 4f1fe2bab..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.simulation_app/test_unsaved_on_exit.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp() - -import omni.kit.app -from isaacsim.core.api import World - -world = World(stage_units_in_meters=1.0, physics_prim_path="/physicsScene", backend="numpy") -world.scene.add_default_ground_plane() -world.reset() - -frame_idx = 0 -while simulation_app.is_running(): - if world.is_playing(): - world.step(render=True) - else: - simulation_app.update() - # we should exit this loop before we hit frame 200 unless we are stuck on an exit screen - assert frame_idx < 200 - # try exiting, it should exit unless a save file dialog shows up. - if frame_idx == 100: - omni.kit.app.get_app().post_quit() - frame_idx += 1 - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/isaacsim.test.docstring/standalone_doctest.py b/simulation/isaac-sim/standalone_examples/testing/isaacsim.test.docstring/standalone_doctest.py deleted file mode 100644 index 8fb4a86e9..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/isaacsim.test.docstring/standalone_doctest.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -# enable the extension -import isaacsim.core.utils.extensions as extensions_utils - -simulation_app.update() -extensions_utils.enable_extension("isaacsim.test.docstring") -simulation_app.update() - -# run test -from isaacsim.test.docstring import StandaloneDocTestCase - -tester = StandaloneDocTestCase() -tester.assertDocTests(StandaloneDocTestCase) - -# quit -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/notebooks/basic_notebook.ipynb b/simulation/isaac-sim/standalone_examples/testing/notebooks/basic_notebook.ipynb deleted file mode 100644 index d4e89ad8e..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/notebooks/basic_notebook.ipynb +++ /dev/null @@ -1,70 +0,0 @@ -{ - "cells": [ - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "# Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved.\n", - "#\n", - "# NVIDIA CORPORATION and its licensors retain all intellectual property\n", - "# and proprietary rights in and to this software, related documentation\n", - "# and any modifications thereto. Any use, reproduction, disclosure or\n", - "# distribution of this software and related documentation without an express\n", - "# license agreement from NVIDIA CORPORATION is strictly prohibited." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from isaacsim import SimulationApp\n", - "# The most basic usage for creating a simulation app\n", - "simulation_app = SimulationApp()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Run for 100 frames\n", - "for i in range(100):\n", - " simulation_app.update()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Cleanup application\n", - "simulation_app.close()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Isaac Sim Python 3", - "language": "python", - "name": "isaac_sim_python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.10" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/simulation/isaac-sim/standalone_examples/testing/notebooks/test_ogn_notebook.ipynb b/simulation/isaac-sim/standalone_examples/testing/notebooks/test_ogn_notebook.ipynb deleted file mode 100644 index 48b8a4a16..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/notebooks/test_ogn_notebook.ipynb +++ /dev/null @@ -1,123 +0,0 @@ -{ - "cells": [ - { - "cell_type": "raw", - "id": "8cbe7a13", - "metadata": {}, - "source": [ - "# Copyright (c) 2020-2022, NVIDIA CORPORATION. All rights reserved.\n", - "#\n", - "# NVIDIA CORPORATION and its licensors retain all intellectual property\n", - "# and proprietary rights in and to this software, related documentation\n", - "# and any modifications thereto. Any use, reproduction, disclosure or\n", - "# distribution of this software and related documentation without an express\n", - "# license agreement from NVIDIA CORPORATION is strictly prohibited." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "15c98eca", - "metadata": {}, - "outputs": [], - "source": [ - "from isaacsim import SimulationApp\n", - "\n", - "simulation_app = SimulationApp()\n", - "import omni\n", - "\n", - "simulation_app.update()\n", - "omni.usd.get_context().new_stage()\n", - "simulation_app.update()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8743b6c2", - "metadata": {}, - "outputs": [], - "source": [ - "import omni.graph.core as og\n", - "\n", - "keys = og.Controller.Keys\n", - "(graph, (tick_node, test_node, str_node), _, _) = og.Controller.edit(\n", - " {\"graph_path\": \"/controller_graph\", \"evaluator_name\": \"push\"},\n", - " {\n", - " keys.CREATE_NODES: [\n", - " (\"OnTick\", \"omni.graph.action.OnTick\"),\n", - " (\"IsaacTest\", \"isaacsim.core.nodes.IsaacTestNode\"),\n", - " (\"TestStr\", \"omni.graph.nodes.ConstantString\"),\n", - " ],\n", - " keys.SET_VALUES: [\n", - " (\"TestStr.inputs:value\", \"Hello\"),\n", - " (\"OnTick.inputs:onlyPlayback\", False), # always tick\n", - " ],\n", - " keys.CONNECT: [\n", - " (\"OnTick.outputs:tick\", \"IsaacTest.inputs:execIn\"),\n", - " (\"TestStr.inputs:value\", \"IsaacTest.inputs:input\"),\n", - " ],\n", - " },\n", - ")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "57ae724f", - "metadata": {}, - "outputs": [], - "source": [ - "input_attr = og.Controller.attribute(\"inputs:value\", str_node)\n", - "output_attr = og.Controller.attribute(\"outputs:output\", test_node)\n", - "og.DataView.set(input_attr, \"Hello\")\n", - "simulation_app.update()\n", - "value = og.DataView.get(output_attr)\n", - "print(value)\n", - "if value != \"Hello\":\n", - " raise ValueError(\"Output does not equal Hello\")\n", - "simulation_app.update()\n", - "og.DataView.set(input_attr, \"Goodbye\")\n", - "simulation_app.update()\n", - "value = og.DataView.get(output_attr)\n", - "print(value)\n", - "if value != \"Goodbye\":\n", - " raise ValueError(\"Output does not equal Goodbye\")\n", - "\n", - "simulation_app.update()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f4687a03", - "metadata": {}, - "outputs": [], - "source": [ - "# Cleanup application\n", - "simulation_app.close()\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Isaac Sim Python 3", - "language": "python", - "name": "isaac_sim_python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/simulation/isaac-sim/standalone_examples/testing/notebooks/test_syntheticdata_notebook.ipynb b/simulation/isaac-sim/standalone_examples/testing/notebooks/test_syntheticdata_notebook.ipynb deleted file mode 100644 index 9a35430a9..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/notebooks/test_syntheticdata_notebook.ipynb +++ /dev/null @@ -1,162 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "8cbe7a13", - "metadata": {}, - "outputs": [], - "source": [ - "# Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved.\n", - "#\n", - "# NVIDIA CORPORATION and its licensors retain all intellectual property\n", - "# and proprietary rights in and to this software, related documentation\n", - "# and any modifications thereto. Any use, reproduction, disclosure or\n", - "# distribution of this software and related documentation without an express\n", - "# license agreement from NVIDIA CORPORATION is strictly prohibited." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "15c98eca", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import numpy as np\n", - "from isaacsim import SimulationApp\n", - "\n", - "simulation_app = SimulationApp(launch_config={\"headless\": True})\n", - "import omni\n", - "\n", - "simulation_app.update()\n", - "omni.usd.get_context().new_stage()\n", - "simulation_app.update()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8743b6c2", - "metadata": {}, - "outputs": [], - "source": [ - "from isaacsim.sensors.camera import Camera\n", - "from isaacsim.core.api.objects import VisualCuboid, DynamicCuboid\n", - "from omni.kit.viewport.utility import get_active_viewport\n", - "\n", - "viewport_api = get_active_viewport()\n", - "render_product_path = viewport_api.get_render_product_path()\n", - "camera = Camera(\n", - " prim_path=\"/World/camera\",\n", - " position=np.array([0.0, 0.0, 25.0]),\n", - " resolution=(1280, 720),\n", - " render_product_path = render_product_path\n", - ")\n", - "simulation_app.update()\n", - "camera.initialize()\n", - "simulation_app.update()\n", - "camera.add_distance_to_image_plane_to_frame()\n", - "camera.add_bounding_box_2d_tight_to_frame()\n", - "camera.add_bounding_box_2d_loose_to_frame()\n", - "camera.add_instance_segmentation_to_frame()\n", - "camera.add_semantic_segmentation_to_frame()\n", - "camera.add_bounding_box_3d_to_frame()\n", - "simulation_app.update()\n", - "\n", - "\n", - "VisualCuboid(\n", - " prim_path=\"/new_cube_1\",\n", - " name=\"visual_cube\",\n", - " position=np.array([0, 0, 0.5]),\n", - " scale=np.array([1, 1, 1]),\n", - " color=np.array([255, 255, 255]),\n", - ")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "57ae724f", - "metadata": {}, - "outputs": [], - "source": [ - "omni.timeline.get_timeline_interface().play()\n", - "\n", - "for _ in range(10):\n", - " simulation_app.update()\n", - "\n", - "rgb = camera.get_rgba()\n", - "\n", - "print(rgb.size)\n", - "if rgb.size != 1280 * 720 * 4:\n", - " raise ValueError(f\"RGB buffer has size of {rgb.size} which is not {1280*720*4}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1be0ac67", - "metadata": {}, - "outputs": [], - "source": [ - "import omni.graph.core as og\n", - "import omni\n", - "\n", - "from omni.syntheticdata import sensors\n", - "\n", - "simulation_app.update()\n", - "omni.timeline.get_timeline_interface().play()\n", - "viewport_api = get_active_viewport()\n", - "import omni.syntheticdata._syntheticdata as sd\n", - "\n", - "sensors.enable_sensors(viewport_api, [sd.SensorType.DistanceToImagePlane])\n", - "simulation_app.update()\n", - "graph = og.ObjectLookup.graph(\"/Render/PostProcess/SDGPipeline\")\n", - "raw_node = og.ObjectLookup.node(\n", - " \"/Render/PostProcess/SDGPipeline/PostProcessDispatcher\"\n", - ")\n", - "swh_attr = og.Controller.attribute(\"outputs:referenceTimeNumerator\", raw_node)\n", - "first = og.DataView.get(swh_attr)\n", - "simulation_app.update()\n", - "second = og.DataView.get(swh_attr)\n", - "\n", - "if first + 1 != second:\n", - " raise ValueError(f\"swh frame numbers {first}, {second} should be one apart\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f4687a03", - "metadata": {}, - "outputs": [], - "source": [ - "# Cleanup application\n", - "simulation_app.close()\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Isaac Sim Python 3", - "language": "python", - "name": "isaac_sim_python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/simulation/isaac-sim/standalone_examples/testing/omni.isaac.dynamic_control/test_zero_step.py b/simulation/isaac-sim/standalone_examples/testing/omni.isaac.dynamic_control/test_zero_step.py deleted file mode 100644 index 4d7f7a630..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/omni.isaac.dynamic_control/test_zero_step.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) -import carb -import omni -from isaacsim.core.api import SimulationContext -from isaacsim.storage.native import get_assets_root_path -from omni.isaac.dynamic_control import _dynamic_control - -stage = simulation_app.context.get_stage() -sim_context = SimulationContext(stage_units_in_meters=1.0) - -physx_interface = omni.physx.acquire_physx_interface() -physx_interface.start_simulation() -physx_interface.force_load_physics_from_usd() -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka_alt_fingers.usd" - -prim = stage.DefinePrim("/panda", "Xform") -prim.GetReferences().AddReference(asset_path) -physx_interface.force_load_physics_from_usd() -sim_context._timeline.play() -omni.physx.acquire_physx_interface().update_simulation(elapsedStep=0, currentTime=0) -dc = _dynamic_control.acquire_dynamic_control_interface() -# Get the handle to force it to refresh, this should not crash. -art = dc.get_articulation("/panda") -sim_context._timeline.stop() -sim_context._timeline.play() -simulation_app.update() -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/omni.replicator.agent/test_scripting.py b/simulation/isaac-sim/standalone_examples/testing/omni.replicator.agent/test_scripting.py deleted file mode 100644 index 12a905fdb..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/omni.replicator.agent/test_scripting.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import asyncio - -from isaacsim import SimulationApp - -CONFIG = {"renderer": "RaytracedLighting", "headless": True, "width": 1920, "height": 1080} - -if __name__ == "__main__": - app = SimulationApp(launch_config=CONFIG) - - from isaacsim.core.utils.extensions import enable_extension - - app.update() - - enable_extension("omni.kit.scripting") - - import omni.usd - from omni.kit.scripting import ApplyScriptingAPICommand - from pxr import OmniScriptingSchema, Sdf - - async def work(): - - # Create new prim and attach python scripting api. - await omni.usd.get_context().new_stage_async("tmp") - stage = omni.usd.get_context().get_stage() - stage.DefinePrim("/test") - ApplyScriptingAPICommand(paths=["/test"]).do() - - # Test - prim = stage.GetPrimAtPath("/test") - assert prim.HasAPI(OmniScriptingSchema.OmniScriptingAPI) - - asyncio.run(work()) diff --git a/simulation/isaac-sim/standalone_examples/testing/omni.syntheticdata/test_basic.py b/simulation/isaac-sim/standalone_examples/testing/omni.syntheticdata/test_basic.py deleted file mode 100644 index d898843d3..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/omni.syntheticdata/test_basic.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": True}) - -import carb -import omni.syntheticdata._syntheticdata as sd -from isaacsim.core.utils.stage import get_current_stage -from isaacsim.storage.native import get_assets_root_path -from omni.kit.viewport.utility import get_active_viewport -from omni.syntheticdata import sensors -from omni.syntheticdata.tests.utils import add_semantics - -viewport_api = get_active_viewport() -simulation_app.update() -stage = get_current_stage() -simulation_app.update() - -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - exit() -robot_usd = assets_root_path + "/Isaac/Robots/Carter/carter_v1_physx_lidar.usd" - -# setup high-level robot prim -prim = stage.DefinePrim("/robot", "Xform") -prim.GetReferences().AddReference(robot_usd) -add_semantics(prim, "robot") - -simulation_app.update() - -sensors.enable_sensors( - viewport_api, - [ - sd.SensorType.Rgb, - sd.SensorType.DistanceToImagePlane, - sd.SensorType.InstanceSegmentation, - sd.SensorType.SemanticSegmentation, - sd.SensorType.BoundingBox2DTight, - sd.SensorType.BoundingBox2DLoose, - sd.SensorType.BoundingBox3D, - sd.SensorType.Occlusion, - ], -) - -for frame in range(100): - simulation_app.update() - -print(sensors.get_rgb(viewport_api)) -print(sensors.get_distance_to_image_plane(viewport_api)) -print(sensors.get_instance_segmentation(viewport_api, parsed=True, return_mapping=True)) -print(sensors.get_semantic_segmentation(viewport_api)) -print(sensors.get_bounding_box_2d_tight(viewport_api)) -print(sensors.get_bounding_box_2d_loose(viewport_api)) -print(sensors.get_bounding_box_3d(viewport_api, parsed=True, return_corners=True)) -print(sensors.get_occlusion(viewport_api)) - - -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/testing/python_sh/import_scipy.py b/simulation/isaac-sim/standalone_examples/testing/python_sh/import_scipy.py deleted file mode 100644 index 286c04efd..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/python_sh/import_scipy.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import scipy - -print(scipy.__path__[0]) -assert "omni.pip.compute" in scipy.__path__[0] diff --git a/simulation/isaac-sim/standalone_examples/testing/python_sh/import_sys.py b/simulation/isaac-sim/standalone_examples/testing/python_sh/import_sys.py deleted file mode 100644 index 7364a990f..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/python_sh/import_sys.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import sys - -print(sys.path) diff --git a/simulation/isaac-sim/standalone_examples/testing/python_sh/import_torch.py b/simulation/isaac-sim/standalone_examples/testing/python_sh/import_torch.py deleted file mode 100644 index f5947ceb7..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/python_sh/import_torch.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import torch - -print(torch.__path__[0]) -assert "omni.isaac.ml_archive" in torch.__path__[0] -print(f"Cuda available: {torch.cuda.is_available()}") -assert torch.cuda.is_available() - - -@torch.jit.script -def add(a, b): - return a + b - - -a = torch.ones((10, 2), device="cuda:0") -b = torch.ones((10, 2), device="cuda:0") -c = add(a, b) -d = a + b -assert torch.allclose(c, d) diff --git a/simulation/isaac-sim/standalone_examples/testing/python_sh/path_length.py b/simulation/isaac-sim/standalone_examples/testing/python_sh/path_length.py deleted file mode 100644 index 0b251d0b5..000000000 --- a/simulation/isaac-sim/standalone_examples/testing/python_sh/path_length.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2022-2023, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# -import os - -# test to give us a heads up if the PATH variable gets too long, can be an issue on windows -print(len(os.environ["PATH"])) -assert len(os.environ["PATH"]) < 2000 diff --git a/simulation/isaac-sim/standalone_examples/tutorials/getting_started.py b/simulation/isaac-sim/standalone_examples/tutorials/getting_started.py deleted file mode 100644 index 2cdb98b85..000000000 --- a/simulation/isaac-sim/standalone_examples/tutorials/getting_started.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -import numpy as np -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) - -import omni.usd -from isaacsim.core.api import World -from isaacsim.core.api.objects import DynamicCuboid, VisualCuboid -from isaacsim.core.api.objects.ground_plane import GroundPlane -from pxr import Sdf, UsdLux - -# Add Ground Plane -GroundPlane(prim_path="/World/GroundPlane", z_position=0) - -# Add Light Source -stage = omni.usd.get_context().get_stage() -distantLight = UsdLux.DistantLight.Define(stage, Sdf.Path("/DistantLight")) -distantLight.CreateIntensityAttr(300) - -# Add Visual Cubes -visual_cube = VisualCuboid( - prim_path="/visual_cube", - name="visual_cube", - position=np.array([0, 0.5, 1.0]), - size=0.3, - color=np.array([255, 255, 0]), -) - -visual_cube_static = VisualCuboid( - prim_path="/visual_cube_static", - name="visual_cube_static", - position=np.array([0.5, 0, 0.5]), - size=0.3, - color=np.array([0, 255, 0]), -) - -# Add Physics Cubes -dynamic_cube = DynamicCuboid( - prim_path="/dynamic_cube", - name="dynamic_cube", - position=np.array([0, -0.5, 1.5]), - size=0.3, - color=np.array([0, 255, 255]), -) - -# start a world to step simulator -my_world = World(stage_units_in_meters=1.0) - -# start the simulator -for i in range(3): - my_world.reset() - print("simulator running", i) - if i == 1: - print("Adding Physics Properties to the Visual Cube") - from isaacsim.core.prims import RigidPrim - - RigidPrim("/visual_cube") - - if i == 2: - print("Adding Collision Properties to the Visual Cube") - from isaacsim.core.prims import GeometryPrim - - prim = GeometryPrim("/visual_cube") - prim.apply_collision_apis() - - for j in range(100): - my_world.step(render=True) # stepping through the simulation - -# shutdown the simulator automatically -simulation_app.close() diff --git a/simulation/isaac-sim/standalone_examples/tutorials/getting_started_robot.py b/simulation/isaac-sim/standalone_examples/tutorials/getting_started_robot.py deleted file mode 100644 index 1c35e05c1..000000000 --- a/simulation/isaac-sim/standalone_examples/tutorials/getting_started_robot.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. All rights reserved. -# -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. -# - -from isaacsim import SimulationApp - -simulation_app = SimulationApp({"headless": False}) # start the simulation app, with GUI open - -import sys - -import carb -import numpy as np -from isaacsim.core.api import World -from isaacsim.core.prims import Articulation -from isaacsim.core.utils.stage import add_reference_to_stage, get_stage_units -from isaacsim.core.utils.types import ArticulationAction -from isaacsim.core.utils.viewports import set_camera_view -from isaacsim.storage.native import get_assets_root_path - -# preparing the scene -assets_root_path = get_assets_root_path() -if assets_root_path is None: - carb.log_error("Could not find Isaac Sim assets folder") - simulation_app.close() - sys.exit() - -my_world = World(stage_units_in_meters=1.0) -my_world.scene.add_default_ground_plane() # add ground plane -set_camera_view( - eye=[5.0, 0.0, 1.5], target=[0.00, 0.00, 1.00], camera_prim_path="/OmniverseKit_Persp" -) # set camera view - -# Add Franka -asset_path = assets_root_path + "/Isaac/Robots/Franka/franka.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Arm") # add robot to stage -arm = Articulation(prim_paths_expr="/World/Arm", name="my_arm") # create an articulation object - -# Add Carter -asset_path = assets_root_path + "/Isaac/Robots/NVIDIA/Carter/nova_carter/nova_carter.usd" -add_reference_to_stage(usd_path=asset_path, prim_path="/World/Car") -car = Articulation(prim_paths_expr="/World/Car", name="my_car") - -# set the initial poses of the arm and the car so they don't collide BEFORE the simulation starts -arm.set_world_poses(positions=np.array([[0.0, 1.0, 0.0]]) / get_stage_units()) -car.set_world_poses(positions=np.array([[0.0, -1.0, 0.0]]) / get_stage_units()) - -# initialize the world -my_world.reset() - -for i in range(4): - print("running cycle: ", i) - if i == 1 or i == 3: - print("moving") - # move the arm - arm.set_joint_positions([[-1.5, 0.0, 0.0, -1.5, 0.0, 1.5, 0.5, 0.04, 0.04]]) - # move the car - car.set_joint_velocities([[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]]) - if i == 2: - print("stopping") - # reset the arm - arm.set_joint_positions([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]) - # stop the car - car.set_joint_velocities([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]) - - for j in range(100): - # step the simulation, both rendering and physics - my_world.step(render=True) - # print the joint positions of the car at every physics step - if i == 3: - car_joint_positions = car.get_joint_positions() - print("car joint positions:", car_joint_positions) - -simulation_app.close() diff --git a/simulation/isaac-sim/utils/scene_prep.py b/simulation/isaac-sim/utils/scene_prep.py index a18b9f250..de2ea11d5 100644 --- a/simulation/isaac-sim/utils/scene_prep.py +++ b/simulation/isaac-sim/utils/scene_prep.py @@ -10,12 +10,11 @@ """ import asyncio -import os -import re -import time as _time +import math + import omni.kit.app import omni.usd -from pxr import Gf, UsdGeom, UsdPhysics, UsdLux, Sdf +from pxr import Gf, Usd, UsdGeom, UsdPhysics, UsdLux, Sdf # --------------------------------------------------------------------------- @@ -168,6 +167,50 @@ def add_dome_light(stage, prim_path: str = "/World/DomeLight", intensity: float # convert into a textured ground in Foxglove's 3D panel. # --------------------------------------------------------------------------- +def boost_scene_lights(stage, factor: float, root_path: str = "/World") -> int: + """Multiply every scene light's brightness under ``root_path`` by ``factor``. + + Applied as ``exposure += log2(factor)`` so it composes with whatever the + asset authored. Instanceable subtrees that contain lights (e.g. the NVIDIA + office/hospital ``BP_CeilingLight*`` prims) are de-instanced first so their + lights become editable. The AirStack dome light is skipped — tune that via + ``add_dome_light`` / ``ISAAC_SIM_DOME_LIGHT`` instead. + + Returns the number of lights adjusted. + """ + root = stage.GetPrimAtPath(root_path) + if not root.IsValid() or factor <= 0 or factor == 1.0: + return 0 + + proxies = Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate) + + def subtree_has_light(prim): + return any(p.HasAPI(UsdLux.LightAPI) for p in Usd.PrimRange(prim, proxies)) + + # De-instance light-bearing instances (repeat for nested instancing). + for _ in range(3): + changed = False + for prim in Usd.PrimRange(root): + if prim.IsInstance() and subtree_has_light(prim): + prim.SetInstanceable(False) + changed = True + if not changed: + break + + add = math.log2(factor) + count = 0 + for prim in Usd.PrimRange(root): + if prim.GetPath() == Sdf.Path("/World/DomeLight"): + continue + if not prim.HasAPI(UsdLux.LightAPI): + continue + exposure = UsdLux.LightAPI(prim).GetExposureAttr() + current = exposure.Get() or 0.0 + exposure.Set(float(current) + add) + count += 1 + return count + + def add_orthographic_camera(stage, prim_path: str = "/World/MapCamera", altitude_m: float = 80.0, @@ -419,81 +462,6 @@ def reference_root_prims_under_world(stage, source_usd_url: str) -> list: return siblings -def move_root_prims_to_world_live(stage) -> list: - """Move any non-/World root prims (e.g. /Environment, /Sun, /Sky) under /World - on the currently active live stage. - - Useful when loading a USD from Nucleus whose sky/sun/environment prims sit at - the root rather than under /World, causing them to be invisible to the sim. - - Args: - stage: Active USD stage (from omni.usd.get_context().get_stage()). - - Returns: - List of prim names that were moved. - """ - root_layer = stage.GetRootLayer() - all_root = [spec.name for spec in root_layer.rootPrims] - print(f"[scene_prep] move_root_prims_to_world_live: root prims = {all_root}", flush=True) - - to_move = [name for name in all_root if name != 'World'] - if not to_move: - print("[scene_prep] move_root_prims_to_world_live: nothing to move", flush=True) - return [] - - edit = Sdf.BatchNamespaceEdit() - for name in to_move: - edit.Add(Sdf.Path(f"/{name}"), Sdf.Path(f"/World/{name}")) - - if not root_layer.Apply(edit): - print(f"[scene_prep] move_root_prims_to_world_live: namespace edit failed for {to_move}", flush=True) - return [] - - print(f"[scene_prep] Moved root prims under /World: {to_move}", flush=True) - return to_move - - -def move_root_prims_to_world(usd_path: str) -> list: - """Move any non-/World root prims (e.g. /Environment) under /World. - - After export_as_stage_async, sibling root prims like /Environment are - excluded when pg.load_environment references the file via defaultPrim=/World. - This function opens the flat exported USD layer directly and relocates - those prims under /World so they are included in the reference. - - Args: - usd_path: Path to the flat exported USD file to patch in-place. - - Returns: - List of prim names that were moved. - """ - layer = Sdf.Layer.Find(usd_path) or Sdf.Layer.FindOrOpen(usd_path) - if layer is None: - print(f"[scene_prep] move_root_prims_to_world: could not open {usd_path}", flush=True) - return [] - - all_root = [spec.name for spec in layer.rootPrims] - print(f"[scene_prep] move_root_prims_to_world: root prims in exported USD: {all_root}", flush=True) - print(f"[scene_prep] move_root_prims_to_world: sublayers: {layer.subLayerPaths}", flush=True) - - to_move = [name for name in all_root if name != 'World'] - if not to_move: - print(f"[scene_prep] move_root_prims_to_world: nothing to move", flush=True) - return [] - - edit = Sdf.BatchNamespaceEdit() - for name in to_move: - edit.Add(Sdf.Path(f"/{name}"), Sdf.Path(f"/World/{name}")) - - if not layer.Apply(edit): - print(f"[scene_prep] move_root_prims_to_world: namespace edit failed for {to_move}", flush=True) - return [] - - layer.Save() - print(f"[scene_prep] Moved root prims under /World: {to_move}", flush=True) - return to_move - - # --------------------------------------------------------------------------- # Save as self-contained USD collection # --------------------------------------------------------------------------- @@ -552,99 +520,3 @@ def on_finish(): collector.destroy() return result[0] - - -# --------------------------------------------------------------------------- -# Fix missing MDL textures -# --------------------------------------------------------------------------- - -def _resolve_nucleus_url(base_url: str, relative: str) -> str: - """Resolve a relative path against a Nucleus base directory URL.""" - parts = base_url.rstrip('/').split('/') - for segment in relative.replace('\\', '/').split('/'): - if segment == '..': - parts.pop() - elif segment and segment != '.': - parts.append(segment) - return '/'.join(parts) - - -def fix_missing_mdl_textures(output_dir: str, nucleus_env_url: str) -> int: - """Download textures referenced in MDL files that the Collector missed. - - The Collector rewrites texture paths inside MDL files to relative local - paths but does not always copy the actual texture files. This function - downloads the original Nucleus MDL to find the real texture URLs, then - downloads any missing textures to the expected local paths. - - Args: - output_dir: Local directory written by the Collector. - nucleus_env_url: Original omniverse:// URL of the source scene. - - Returns: - Number of textures downloaded. - """ - import omni.client - import tempfile - - nucleus_base = nucleus_env_url.rsplit('/', 1)[0] - nucleus_materials_dir = f"{nucleus_base}/Materials" - - texture_pattern = re.compile( - r'["\']([^"\']*\.(?:png|jpg|jpeg|exr|hdr|dds|tga|bmp))["\']', - re.IGNORECASE, - ) - downloaded = 0 - - for root, dirs, files in os.walk(output_dir): - for fname in files: - if not fname.endswith('.mdl'): - continue - - local_mdl = os.path.join(root, fname) - - # Download the original Nucleus MDL to a temp file - nucleus_mdl_url = f"{nucleus_materials_dir}/{fname}" - tmp_mdl = os.path.join(tempfile.gettempdir(), f"orig_{fname}") - copy_result = omni.client.copy( - nucleus_mdl_url, tmp_mdl, omni.client.CopyBehavior.OVERWRITE - ) - if copy_result != omni.client.Result.OK: - print(f"[scene_prep] Could not fetch original MDL from Nucleus: {nucleus_mdl_url}") - continue - - with open(tmp_mdl, 'r', errors='replace') as f: - orig_content = f.read() - os.remove(tmp_mdl) - - with open(local_mdl, 'r', errors='replace') as f: - local_content = f.read() - - orig_refs = [m.group(1) for m in texture_pattern.finditer(orig_content)] - local_refs = [m.group(1) for m in texture_pattern.finditer(local_content)] - - for orig_ref, local_ref in zip(orig_refs, local_refs): - # Resolve local expected path - local_abs = os.path.normpath(os.path.join(os.path.dirname(local_mdl), local_ref)) - - if os.path.exists(local_abs): - continue - - # Resolve absolute Nucleus URL for the texture - if orig_ref.startswith('omniverse:'): - nucleus_tex_url = orig_ref - else: - nucleus_tex_url = _resolve_nucleus_url(nucleus_materials_dir, orig_ref) - - os.makedirs(os.path.dirname(local_abs), exist_ok=True) - result = omni.client.copy( - nucleus_tex_url, local_abs, omni.client.CopyBehavior.OVERWRITE - ) - if result == omni.client.Result.OK: - downloaded += 1 - print(f"[scene_prep] Downloaded: {os.path.basename(local_abs)}") - else: - print(f"[scene_prep] Failed ({result}): {nucleus_tex_url}") - - print(f"[scene_prep] fix_missing_mdl_textures: {downloaded} texture(s) downloaded.") - return downloaded diff --git a/simulation/ms-airsim/assets/scenes/fetch_scene.sh b/simulation/ms-airsim/assets/scenes/fetch_scene.sh index ba977a57d..04dc3cf79 100755 --- a/simulation/ms-airsim/assets/scenes/fetch_scene.sh +++ b/simulation/ms-airsim/assets/scenes/fetch_scene.sh @@ -4,28 +4,41 @@ # the scene has already been extracted. # # Usage: fetch_scene.sh [scene] +# fetch_scene.sh --name # print the scene's directory name +# # (e.g. blocks → Blocks) and exit # scene (default: blocks) — one of: -# blocks, airsimnh, abandonedpark, forest, -# landscapemountains, soccerfield, building99, zhangjiajie +# blocks, airsimnh, abandonedpark, landscapemountains, +# zhangjiajie, africasavannah, msbuild2018 +# (only assets that actually exist in the v1.8.1 release; Building_99.zip +# is published but 0 bytes, and Forest/SoccerField were never released) set -euo pipefail +NAME_ONLY="" +if [ "${1:-}" = "--name" ]; then + NAME_ONLY=1 + shift +fi SCENE="${1:-blocks}" SCENES_DIR="${SCENES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" RELEASE_URL="https://github.com/microsoft/AirSim/releases/download/v1.8.1" case "$SCENE" in blocks) NAME=Blocks; ZIP=Blocks.zip ;; - airsimnh) NAME=AirSimNH; ZIP=Neighborhood.zip ;; + airsimnh) NAME=AirSimNH; ZIP=AirSimNH.zip ;; abandonedpark) NAME=AbandonedPark; ZIP=AbandonedPark.zip ;; - forest) NAME=Forest; ZIP=Forest.zip ;; landscapemountains) NAME=LandscapeMountains; ZIP=LandscapeMountains.zip ;; - soccerfield) NAME=SoccerField; ZIP=SoccerField.zip ;; - building99) NAME=Building99; ZIP=Building99.zip ;; zhangjiajie) NAME=ZhangJiajie; ZIP=ZhangJiajie.zip ;; + africasavannah) NAME=Africa_Savannah; ZIP=Africa_Savannah.zip ;; + msbuild2018) NAME=MSBuild2018; ZIP=MSBuild2018.zip ;; *) echo "unknown scene: $SCENE" >&2; exit 2 ;; esac +if [ -n "$NAME_ONLY" ]; then + echo "$NAME" + exit 0 +fi + DEST="$SCENES_DIR/$NAME" if compgen -G "$DEST/LinuxNoEditor/*.sh" > /dev/null; then echo "$NAME already present in $DEST" diff --git a/simulation/ms-airsim/docker/docker-compose.yaml b/simulation/ms-airsim/docker/docker-compose.yaml index 9b49c9098..4151ee8b3 100644 --- a/simulation/ms-airsim/docker/docker-compose.yaml +++ b/simulation/ms-airsim/docker/docker-compose.yaml @@ -21,6 +21,9 @@ services: privileged: true networks: airstack_network: + # INVARIANT: every sim service (isaac-sim, ms-airsim, simple-sim) + # binds this same fixed address — robot containers reach the sim at + # SIM_IP (default 172.31.0.200), so only one sim can run at a time. ipv4_address: 172.31.0.200 environment: - DISPLAY @@ -30,8 +33,21 @@ services: - NUM_ROBOTS=${NUM_ROBOTS:-1} - AUTOLAUNCH=${AUTOLAUNCH:-true} - MS_AIRSIM_BINARY_PATH=${MS_AIRSIM_BINARY_PATH:-} + # Scene selection (`airstack up --scene ` → simulation/scenes.yaml): + # a fetch_scene.sh key; the entrypoint auto-fetches it when the UE binary + # is absent. Empty = blocks. Ignored when MS_AIRSIM_BINARY_PATH is set. + - MS_AIRSIM_SCENE=${MS_AIRSIM_SCENE:-} - MS_AIRSIM_HEADLESS=${MS_AIRSIM_HEADLESS:-false} - MS_AIRSIM_PX4_START_DELAY=${MS_AIRSIM_PX4_START_DELAY:-3} + # settings.json template knobs (config/generate_settings.py defaults) + - AIRSIM_CAM_WIDTH=${AIRSIM_CAM_WIDTH:-480} + - AIRSIM_CAM_HEIGHT=${AIRSIM_CAM_HEIGHT:-300} + - AIRSIM_CAM_FOV=${AIRSIM_CAM_FOV:-90} + - AIRSIM_CAM_X=${AIRSIM_CAM_X:-0.4} + - AIRSIM_CAM_Y=${AIRSIM_CAM_Y:-0.06} + - AIRSIM_CAM_Z=${AIRSIM_CAM_Z:-0} + - AIRSIM_CAM_PITCH=${AIRSIM_CAM_PITCH:-0} + - AIRSIM_SPAWN_SPACING=${AIRSIM_SPAWN_SPACING:-3} deploy: resources: reservations: diff --git a/simulation/ms-airsim/docker/entrypoint.sh b/simulation/ms-airsim/docker/entrypoint.sh index 8034434a6..1163c198d 100755 --- a/simulation/ms-airsim/docker/entrypoint.sh +++ b/simulation/ms-airsim/docker/entrypoint.sh @@ -18,15 +18,25 @@ tmux new -d -s ms-airsim -n airsim # window list (airsim, robot__px4, robot__bridge) has room to breathe. tmux set-option -t ms-airsim status-right '' -# Scene resolution. If MS_AIRSIM_BINARY_PATH is unset, the airsim tmux window -# auto-fetches Blocks so the download is visible. If set, the file must exist. +# Scene resolution. An explicit MS_AIRSIM_BINARY_PATH wins (and must exist). +# Otherwise MS_AIRSIM_SCENE (a fetch_scene.sh key, set by `airstack up +# --scene `; default blocks) picks the scene, and the airsim tmux +# window auto-fetches it so the download is visible. FETCH_PREFIX="" if [ -z "$MS_AIRSIM_BINARY_PATH" ]; then - MS_AIRSIM_BINARY_PATH="/ms-airsim-env/Blocks/LinuxNoEditor/Blocks.sh" - FETCH_PREFIX="SCENES_DIR=/ms-airsim-env bash /ms-airsim-env/fetch_scene.sh blocks && chown -R ms-airsim:ms-airsim /ms-airsim-env/Blocks && chmod -R a+rwX /ms-airsim-env/Blocks && " + MS_AIRSIM_SCENE="${MS_AIRSIM_SCENE:-blocks}" + SCENE_NAME="$(bash /ms-airsim-env/fetch_scene.sh --name "$MS_AIRSIM_SCENE")" || { + echo "ERROR: unknown MS_AIRSIM_SCENE='$MS_AIRSIM_SCENE' (see fetch_scene.sh for valid keys)." >&2 + exit 1 + } + # The launcher is usually .sh, but resolve by glob AFTER the fetch + # (in the tmux shell) so zips whose inner launcher is named differently + # still work. Single-quoted: expands post-download, not here. + MS_AIRSIM_BINARY_PATH='$(ls /ms-airsim-env/'"$SCENE_NAME"'/LinuxNoEditor/*.sh | head -1)' + FETCH_PREFIX="SCENES_DIR=/ms-airsim-env bash /ms-airsim-env/fetch_scene.sh $MS_AIRSIM_SCENE && chown -R ms-airsim:ms-airsim /ms-airsim-env/$SCENE_NAME && chmod -R a+rwX /ms-airsim-env/$SCENE_NAME && " elif [ ! -f "$MS_AIRSIM_BINARY_PATH" ]; then echo "ERROR: MS_AIRSIM_BINARY_PATH=$MS_AIRSIM_BINARY_PATH does not exist." >&2 - echo "Extract the scene into the mounted volume, or unset MS_AIRSIM_BINARY_PATH to auto-fetch Blocks." >&2 + echo "Extract the scene into the mounted volume, or unset MS_AIRSIM_BINARY_PATH to auto-fetch a scene (MS_AIRSIM_SCENE, default blocks)." >&2 exit 1 fi diff --git a/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/config/bridge.yaml b/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/config/bridge.yaml deleted file mode 100644 index 494911c8b..000000000 --- a/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/config/bridge.yaml +++ /dev/null @@ -1,4 +0,0 @@ -ms_airsim_ros_bridge: - ros__parameters: - ms_airsim_ip: "127.0.0.1" - publish_rate: 15.0 diff --git a/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/launch/bridge.launch.xml b/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/launch/bridge.launch.xml deleted file mode 100644 index 7e3bf027a..000000000 --- a/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/launch/bridge.launch.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/setup.py b/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/setup.py index 9d64f14da..9af97086e 100644 --- a/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/setup.py +++ b/simulation/ms-airsim/ros_ws/src/ms_airsim_ros_bridge/setup.py @@ -9,8 +9,6 @@ data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), - ('share/' + package_name + '/launch', ['launch/bridge.launch.xml']), - ('share/' + package_name + '/config', ['config/bridge.yaml']), ], install_requires=['setuptools'], entry_points={ diff --git a/simulation/resolve_scene.py b/simulation/resolve_scene.py new file mode 100644 index 000000000..deab74037 --- /dev/null +++ b/simulation/resolve_scene.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Resolve an `airstack up --scene ` against simulation/scenes.yaml. + +Called host-side by airstack.sh. Prints KEY=VALUE lines for the selected +simulator on stdout; on an unknown or unavailable scene it prints the +availability table on stderr and exits 1. + +Usage: + resolve_scene.py --sim isaac|msairsim --scene NAME # emit export lines + resolve_scene.py --table # print the table +""" + +import argparse +import os +import sys + +import yaml + +CATALOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scenes.yaml") + +SIM_ALIASES = { + "isaac": "isaac", "isaacsim": "isaac", "isaac-sim": "isaac", + "airsim": "msairsim", "msairsim": "msairsim", "ms-airsim": "msairsim", +} +SIM_LABELS = {"isaac": "Isaac", "msairsim": "MS AirSim"} + + +def load_catalog(path=CATALOG): + with open(path) as f: + data = yaml.safe_load(f) or {} + scenes = data.get("scenes") or {} + if not isinstance(scenes, dict): + raise ValueError(f"{path}: top-level 'scenes' must be a mapping") + return scenes + + +def isaac_entry(spec): + """Normalize an isaac scene spec to (ref, stage_scale).""" + if isinstance(spec, str): + return spec, 1.0 + if isinstance(spec, dict) and "ref" in spec: + return str(spec["ref"]), float(spec.get("stage_scale", 1.0)) + raise ValueError(f"invalid isaac scene spec: {spec!r}") + + +def short_ref(sim, spec): + """Human-readable cell for the availability table.""" + if spec is None: + return "-" + if sim == "isaac": + ref, _ = isaac_entry(spec) + if "://" in ref or ref.endswith((".usd", ".usda", ".usdc", ".usdz")): + return f"nucleus:{os.path.basename(ref)}" if "airlab-nucleus" in ref \ + else os.path.basename(ref) + return f"pegasus:{ref}" + return str(spec) + + +def print_table(scenes, out=sys.stderr): + rows = [("SCENE", SIM_LABELS["isaac"].upper(), SIM_LABELS["msairsim"].upper())] + for name in sorted(scenes): + entry = scenes[name] or {} + rows.append((name, + short_ref("isaac", entry.get("isaac")), + short_ref("msairsim", entry.get("msairsim")))) + widths = [max(len(r[i]) for r in rows) for i in range(3)] + for i, r in enumerate(rows): + print(" ".join(c.ljust(w) for c, w in zip(r, widths)).rstrip(), file=out) + if i == 0: + print(" ".join("-" * w for w in widths), file=out) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--sim", help="target simulator (isaac|airsim aliases ok)") + ap.add_argument("--scene", help="scene shortname from scenes.yaml") + ap.add_argument("--table", action="store_true", + help="print the availability table to stdout and exit") + ap.add_argument("--catalog", default=CATALOG) + args = ap.parse_args() + + scenes = load_catalog(args.catalog) + + if args.table: + print_table(scenes, out=sys.stdout) + return 0 + + if not args.sim or not args.scene: + ap.error("--sim and --scene are required (or use --table)") + sim = SIM_ALIASES.get(args.sim) + if sim is None: + print(f"ERROR: unknown simulator '{args.sim}' " + f"(expected one of: {', '.join(sorted(set(SIM_ALIASES)))})", + file=sys.stderr) + return 1 + + entry = scenes.get(args.scene) + spec = (entry or {}).get(sim) + if spec is None: + if entry is None: + print(f"ERROR: unknown scene '{args.scene}'. Available scenes:", + file=sys.stderr) + else: + others = [SIM_LABELS[s] for s in ("isaac", "msairsim") + if s != sim and entry.get(s) is not None] + print(f"ERROR: scene '{args.scene}' is not available for " + f"{SIM_LABELS[sim]} (only: {', '.join(others) or 'none'}). " + f"Available scenes:", file=sys.stderr) + print_table(scenes) + return 1 + + if sim == "isaac": + ref, scale = isaac_entry(spec) + print(f"ISAAC_SIM_SCENE={ref}") + print(f"ISAAC_SIM_STAGE_SCALE={scale}") + else: + print(f"MS_AIRSIM_SCENE={spec}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/simulation/scenes.yaml b/simulation/scenes.yaml new file mode 100644 index 000000000..a1f549990 --- /dev/null +++ b/simulation/scenes.yaml @@ -0,0 +1,99 @@ +# Scene catalog for `airstack up --scene ` (resolved by +# simulation/resolve_scene.py). +# +# Each entry maps a simulator-agnostic shortname to the scene reference each +# simulator understands. A scene may declare one or both simulators: +# +# : +# isaac: # Pegasus SIMULATION_ENVIRONMENTS key, or an +# # omniverse:// / https:// / *.usd reference +# isaac: # long form, when the stage needs extras +# ref: +# stage_scale: 0.01 # scale applied to /World/stage (default 1.0); +# # 0.01 converts cm-authored stages to meters +# msairsim: # fetch_scene.sh catalog key (pre-built UE4 +# # binary downloaded on first use) +# +# `airstack up --sim isaac --scene ` exports ISAAC_SIM_SCENE (+ +# ISAAC_SIM_STAGE_SCALE); `--sim airsim --scene ` exports +# MS_AIRSIM_SCENE. Launch scripts / entrypoints do the final resolution. + +scenes: + # ── Pegasus SIMULATION_ENVIRONMENTS catalog (NVIDIA Isaac assets) ───────── + default: + isaac: Default Environment + msairsim: blocks + black-gridroom: + isaac: Black Gridroom + curved-gridroom: + isaac: Curved Gridroom + hospital: + isaac: Hospital + office: + isaac: Office + simple-room: + isaac: Simple Room + warehouse: + isaac: Warehouse + warehouse-forklifts: + isaac: Warehouse with Forklifts + warehouse-shelves: + isaac: Warehouse with Shelves + full-warehouse: + isaac: Full Warehouse + flat-plane: + isaac: Flat Plane + rough-plane: + isaac: Rough Plane + slope-plane: + isaac: Slope Plane + stairs-plane: + isaac: Stairs Plane + exhibition-hall: + isaac: Exhibition Hall + + # ── AirLab Nucleus public stages (guest-readable) ───────────────────────── + # omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/ + # stage_scale mirrors each root layer's metersPerUnit (verified 2026-08-24): + # cm-authored stages get 0.01, meter-authored stages 1.0. + abandoned-factory: + isaac: + ref: omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/AbandonedFactory/AbandonedFactory.stage.usd + stage_scale: 1.0 + abandoned-warehouse-night: + isaac: + ref: omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/AbandonedWarehouse/Warehouse_01_night.stage.usd + stage_scale: 0.01 + abandoned-warehouse-day: + isaac: + ref: omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/AbandonedWarehouse/Warehouse_02_day.stage.usd + stage_scale: 0.01 + chemical-plant: + isaac: + ref: omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/ChemicalPlant/Map_ChemicalPlant_2.stage.usd + stage_scale: 1.0 + construction-site: + isaac: + ref: omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/ConstructionSite/ConstructionSite.stage.usd + stage_scale: 0.01 + retro-neighborhood: + isaac: + ref: omniverse://airlab-nucleus.andrew.cmu.edu:443/Public/AirStack/Stages/RetroNeighborhood/RetroNeighborhood.stage.usd + stage_scale: 0.01 + + # ── MS AirSim pre-built UE4 scenes (fetch_scene.sh) ─────────────────────── + # Only the UE4 binaries that actually ship in the AirSim v1.8.1 release. + blocks: + msairsim: blocks + neighborhood: + msairsim: airsimnh + abandoned-park: + msairsim: abandonedpark + landscape-mountains: + msairsim: landscapemountains + zhangjiajie: + msairsim: zhangjiajie + africa-savannah: + msairsim: africasavannah + msbuild2018: + msairsim: msbuild2018 diff --git a/simulation/simple-sim/docker/Dockerfile.sim b/simulation/simple-sim/docker/Dockerfile.sim index 2fc6a145e..9fda78ee0 100644 --- a/simulation/simple-sim/docker/Dockerfile.sim +++ b/simulation/simple-sim/docker/Dockerfile.sim @@ -20,7 +20,7 @@ RUN apt-get -o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngrad # Install Python dependencies #RUN pip3 install empy future lxml matplotlib numpy pkgconfig psutil pygments \ -# wheel pymavlink pyyaml requests setuptools six toml scipy pytak paho-mqtt sphinx utm +# wheel pymavlink pyyaml requests setuptools six toml scipy sphinx utm # Configure SSH RUN mkdir /var/run/sshd && echo 'root:airstack' | chpasswd && \ diff --git a/simulation/simple-sim/docker/bashrc b/simulation/simple-sim/docker/bashrc index d6d6b2a93..8c6205ef1 100644 --- a/simulation/simple-sim/docker/bashrc +++ b/simulation/simple-sim/docker/bashrc @@ -118,5 +118,6 @@ fi alias emacs='emacs -nw' -source /opt/ros/humble/setup.bash -source /root/ros_ws/install/setup.bash \ No newline at end of file +source /opt/ros/jazzy/setup.bash +# The sim workspace colcon-builds at container start; absent on first boot. +[ -f /root/ros_ws/install/setup.bash ] && source /root/ros_ws/install/setup.bash \ No newline at end of file diff --git a/simulation/simple-sim/docker/docker-compose.yaml b/simulation/simple-sim/docker/docker-compose.yaml index 198fa39b0..fdea83ca9 100644 --- a/simulation/simple-sim/docker/docker-compose.yaml +++ b/simulation/simple-sim/docker/docker-compose.yaml @@ -1,7 +1,15 @@ +# simple-sim: lightweight kinematic simulator (no PX4, no MAVROS). One node +# mocks the MAVROS surface the autonomy stack talks to (state/odom topics, +# set_mode/arming/takeoff services, attitude setpoint input — hardcoded to +# robot_1 on ROS_DOMAIN_ID=1) and renders stereo camera images of a single +# FBX world via OpenGL. Launch with `airstack up --sim simple`, which pairs +# this service with the simple-robot service (SIM_TYPE=simple) instead of +# robot-desktop. Actively used by core maintainer John Keller. services: simple-sim: profiles: - simple + container_name: simple-sim # stable name, mirrors isaac-sim / ms-airsim image: &simple_sim_image ${PROJECT_DOCKER_REGISTRY}/${PROJECT_NAME}:v${VERSION}_simple_sim build: context: ./ @@ -10,19 +18,21 @@ services: - *simple_sim_image entrypoint: "" command: > - bash -c "ssh service restart; + bash -c "service ssh restart; tmux new -d -s sim && tmux send-keys -t sim - 'cd /models && ./download.sh && cd ~/ros_ws/ && colcon build --symlink-install && source install/setup.bash && ROS_DOMAIN_ID=1 ros2 launch sim sim.launch.xml' ENTER + 'source /opt/ros/jazzy/setup.bash && cd /models && ./download.sh && cd ~/ros_ws/ && colcon build --symlink-install && source install/setup.bash && ROS_DOMAIN_ID=1 ros2 launch sim sim.launch.xml' ENTER && sleep infinity" # Interactive shell stdin_open: true tty: true ipc: host privileged: true - # runtime: nvidia <-- Removed deprecated runtime key networks: airstack_network: + # INVARIANT: every sim service (isaac-sim, ms-airsim, simple-sim) + # binds this same fixed address — robot containers reach the sim at + # SIM_IP (default 172.31.0.200), so only one sim can run at a time. ipv4_address: 172.31.0.200 environment: - DISPLAY @@ -47,4 +57,4 @@ services: # autonomy stack stuff - ../ros_ws:/root/ros_ws:rw # sim-specific ROS packages # - ../../../ros_ws/src/fastdds.xml:/root/ros_ws/fastdds.xml:rw # fastdds.xml - commented out, may not be needed - # - ../../../common/inputrc:/etc/inputrc:rw # commented out - common/ no longer exists \ No newline at end of file + # - ../../../common/inputrc:/etc/inputrc:rw # optional page-up/down history search; kept unmounted \ No newline at end of file diff --git a/simulation/simple-sim/ros_ws/src/sim/sim.launch.xml b/simulation/simple-sim/ros_ws/src/sim/sim.launch.xml deleted file mode 100644 index 3344381d1..000000000 --- a/simulation/simple-sim/ros_ws/src/sim/sim.launch.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/stacks/full_default/README.md b/stacks/full_default/README.md new file mode 100644 index 000000000..ea19a447a --- /dev/null +++ b/stacks/full_default/README.md @@ -0,0 +1,57 @@ +# `full_default` — trunk reference stack + +The full-autonomy topology, as a self-contained stack folder. This is the +stack most users start from, the default when no stack is selected, and the +baseline other stacks are copied from. + +## What it launches + +The entry point `launch/stack.launch.xml` composes **every layer as a flat +set of module-launch includes**: 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. Two cross-domain +extras run alongside: the DDS-router domain bridge to the GCS and the gossip +coordination layer. Two blocks stay wrapped by design: `interface.launch.py` +(the safety boundary) and `logging.launch.xml` (already a single +self-contained module). + +## Baseline + +This stack is the default: with no stack selected, `robot.launch.xml` +launches it. Its committed [wiring.md](wiring.md) is the observed-graph +baseline that other full stacks are compared against. Verify with the wiring +snapshot test: + +```bash +airstack test -m wiring --stack full_default --sim isaacsim --num-robots 1 +``` + +## How to run + +```bash +airstack up --stack full_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) runs in +`autonomy_bringup/launch/robot.launch.xml`, which dispatches to this stack when +`AIRSTACK_STACK_DIR` is set. + +## Known limits + +- 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. +- `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 + +This stack's observed wiring diagram is committed at [wiring.md](wiring.md); +CI drift-checks the running graph against it. Regenerate via +`airstack test -m wiring --stack full_default`. diff --git a/stacks/full_default/docker-compose.yaml b/stacks/full_default/docker-compose.yaml new file mode 100644 index 000000000..b3c94abe6 --- /dev/null +++ b/stacks/full_default/docker-compose.yaml @@ -0,0 +1,11 @@ +# Per-stack image composition 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`. +# +# full_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/full_default/launch/stack.launch.xml b/stacks/full_default/launch/stack.launch.xml new file mode 100644 index 000000000..104d464a9 --- /dev/null +++ b/stacks/full_default/launch/stack.launch.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/full_default/modules.repos b/stacks/full_default/modules.repos new file mode 100644 index 000000000..7ed436432 --- /dev/null +++ b/stacks/full_default/modules.repos @@ -0,0 +1,15 @@ +# modules.repos : module pins for the full_default reference stack. +# +# 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/full_default/wiring.md b/stacks/full_default/wiring.md new file mode 100644 index 000000000..e3b9fddc8 --- /dev/null +++ b/stacks/full_default/wiring.md @@ -0,0 +1,483 @@ +# Wiring snapshot: full_default + +- **generated-by**: tests/system/test_wiring_snapshot.py +- **date**: 2026-08-22 00:06:32 +- **sim**: isaacsim +- **num_robots**: 1 +- **source-sha**: 57e0b8c34ec9 + +```mermaid +graph LR + subgraph g0["behavior"] + n2["/robot_1/behavior/drone_safety_monitor/drone_safety_monitor"] + end + subgraph g1["control"] + n3["/robot_1/control/pid_controller"] + end + subgraph g2["droan"] + n4["/robot_1/droan/disparity_expander_node"] + end + subgraph g3["interface"] + n6["/robot_1/interface/mavros/actuator_control"] + n7["/robot_1/interface/mavros/adsb"] + n8["/robot_1/interface/mavros/altitude"] + n9["/robot_1/interface/mavros/cam_imu_sync"] + n10["/robot_1/interface/mavros/camera"] + n11["/robot_1/interface/mavros/cellular_status"] + n12["/robot_1/interface/mavros/cmd"] + n13["/robot_1/interface/mavros/companion_process"] + n14["/robot_1/interface/mavros/debug_value"] + n15["/robot_1/interface/mavros/esc_status"] + n16["/robot_1/interface/mavros/esc_telemetry"] + n17["/robot_1/interface/mavros/fake_gps"] + n18["/robot_1/interface/mavros/ftp"] + n19["/robot_1/interface/mavros/geofence"] + n20["/robot_1/interface/mavros/gimbal_control"] + n21["/robot_1/interface/mavros/global_position"] + n22["/robot_1/interface/mavros/gps_input"] + n23["/robot_1/interface/mavros/gps_rtk"] + n24["/robot_1/interface/mavros/gpsstatus"] + n25["/robot_1/interface/mavros/guided_target"] + n26["/robot_1/interface/mavros/hil"] + n27["/robot_1/interface/mavros/home_position"] + n28["/robot_1/interface/mavros/imu"] + n29["/robot_1/interface/mavros/landing_target"] + n30["/robot_1/interface/mavros/local_position"] + n31["/robot_1/interface/mavros/log_transfer"] + n32["/robot_1/interface/mavros/mag_calibration"] + n33["/robot_1/interface/mavros/manual_control"] + n34["/robot_1/interface/mavros/mavros"] + n35["/robot_1/interface/mavros/mavros_node"] + n36["/robot_1/interface/mavros/mavros_router"] + n37["/robot_1/interface/mavros/mission"] + n38["/robot_1/interface/mavros/mocap"] + n39["/robot_1/interface/mavros/mount_control"] + n40["/robot_1/interface/mavros/nav_controller_output"] + n41["/robot_1/interface/mavros/obstacle"] + n42["/robot_1/interface/mavros/obstacle_distance_3d"] + n43["/robot_1/interface/mavros/odometry"] + n44["/robot_1/interface/mavros/onboard_computer"] + n45["/robot_1/interface/mavros/open_drone_id"] + n46["/robot_1/interface/mavros/optical_flow"] + n47["/robot_1/interface/mavros/param"] + n48["/robot_1/interface/mavros/play_tune"] + n49["/robot_1/interface/mavros/px4flow"] + n50["/robot_1/interface/mavros/rallypoint"] + n51["/robot_1/interface/mavros/rc"] + n52["/robot_1/interface/mavros/setpoint_accel"] + n53["/robot_1/interface/mavros/setpoint_attitude"] + n54["/robot_1/interface/mavros/setpoint_position"] + n55["/robot_1/interface/mavros/setpoint_raw"] + n56["/robot_1/interface/mavros/setpoint_trajectory"] + n57["/robot_1/interface/mavros/setpoint_velocity"] + n58["/robot_1/interface/mavros/sim_state"] + n59["/robot_1/interface/mavros/sys"] + n60["/robot_1/interface/mavros/tdr_radio"] + n61["/robot_1/interface/mavros/terrain"] + n62["/robot_1/interface/mavros/time"] + n63["/robot_1/interface/mavros/trajectory"] + n64["/robot_1/interface/mavros/tunnel"] + n65["/robot_1/interface/mavros/vfr_hud"] + n66["/robot_1/interface/mavros/vision_pose"] + n67["/robot_1/interface/mavros/vision_speed"] + n68["/robot_1/interface/mavros/wind"] + n69["/robot_1/interface/odom_modifier"] + n70["/robot_1/interface/robot_interface"] + end + subgraph g4["odometry_conversion"] + n71["/robot_1/odometry_conversion/odometry_conversion"] + end + subgraph g5["perception"] + n72["/robot_1/perception/stereo_image_proc/disparity_node"] + n73["/robot_1/perception/stereo_pointcloud"] + end + subgraph g6["robot_1"] + n1["/robot_1/Container"] + n5["/robot_1/gossip_node"] + n74["/robot_1/random_walk_node"] + n75["/robot_1/robot_state_publisher"] + n78["/robot_1/topic_keepalive"] + n81["/robot_1/vdb_mapping"] + n82["/robot_1/world_to_map_broadcaster"] + end + subgraph g7["root"] + n0["/action_relay_client"] + end + subgraph g8["sensors"] + n76["/robot_1/sensors/lidar_point_cloud_filter"] + end + subgraph g9["takeoff_landing_planner"] + n77["/robot_1/takeoff_landing_planner/takeoff_landing_task"] + end + subgraph g10["trajectory_controller"] + n79["/robot_1/trajectory_controller/fixed_trajectory_task"] + n80["/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 + d0 -->|"/clock
Clock"| n80 + d0 -->|"/clock
Clock"| n81 + d0 -->|"/clock
Clock"| n82 + 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"| n77 + 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)"]) + n3 -->|"/robot_1/control/vz_pid_info
PIDInfo"| d6(["/robot_1/control/vz_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/x_pid_info
PIDInfo"| d7(["/robot_1/control/x_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/y_pid_info
PIDInfo"| d8(["/robot_1/control/y_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/z_pid_info
PIDInfo"| d9(["/robot_1/control/z_pid_info (no subscribers)"]) + n5 -->|"/robot_1/coordination/peer_registry
PeerProfile"| d10(["/robot_1/coordination/peer_registry (no subscribers)"]) + 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"| n78 + d15(["/robot_1/droan/disparity_map_debug (no publishers)"]) -->|"/robot_1/droan/disparity_map_debug
MarkerArray"| n78 + d16(["/robot_1/droan/expansion_cloud (no publishers)"]) -->|"/robot_1/droan/expansion_cloud
PointCloud2"| n78 + d17(["/robot_1/droan/expansion_poly (no publishers)"]) -->|"/robot_1/droan/expansion_poly
MarkerArray"| n78 + n4 -->|"/robot_1/droan/fg_bg_cloud
PointCloud2"| n78 + 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"| n78 + n4 -->|"/robot_1/droan/graph_vis
MarkerArray"| n78 + n4 -->|"/robot_1/droan/local_planner_global_plan_vis
MarkerArray"| n78 + d20(["/robot_1/droan/reset_stuck (no publishers)"]) -->|"/robot_1/droan/reset_stuck
Empty"| n4 + n4 -->|"/robot_1/droan/rewind_info
MarkerArray"| n78 + n4 -->|"/robot_1/droan/stuck
Bool"| d21(["/robot_1/droan/stuck (no subscribers)"]) + n4 -->|"/robot_1/droan/traj_debug
MarkerArray"| n78 + d22(["/robot_1/droan/trajectory_library_vis (no publishers)"]) -->|"/robot_1/droan/trajectory_library_vis
MarkerArray"| n78 + d23(["/robot_1/droan/virtual_obstacles (no publishers)"]) -->|"/robot_1/droan/virtual_obstacles
MarkerArray"| n78 + n69 -->|"/robot_1/global_plan
Path"| n4 + n69 -->|"/robot_1/global_plan
Path"| n5 + n69 -->|"/robot_1/global_plan
Path"| n78 + n74 -->|"/robot_1/global_plan
Path"| n4 + n74 -->|"/robot_1/global_plan
Path"| n5 + n74 -->|"/robot_1/global_plan
Path"| n78 + 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 + d26(["/robot_1/interface/cmd_rate_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_rate_thrust
RateThrust"| n70 + 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"| n77 + n70 -->|"/robot_1/interface/is_armed
Bool"| n77 + 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)"]) + n8 -->|"/robot_1/interface/mavros/altitude
Altitude"| d31(["/robot_1/interface/mavros/altitude (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/battery
BatteryState"| d32(["/robot_1/interface/mavros/battery (no subscribers)"]) + n9 -->|"/robot_1/interface/mavros/cam_imu_sync/cam_imu_stamp
CamIMUStamp"| d33(["/robot_1/interface/mavros/cam_imu_sync/cam_imu_stamp (no subscribers)"]) + n10 -->|"/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"| n11 + d36(["/robot_1/interface/mavros/companion_process/status (no publishers)"]) -->|"/robot_1/interface/mavros/companion_process/status
CompanionProcessStatus"| n13 + n14 -->|"/robot_1/interface/mavros/debug_value/debug
DebugValue"| d37(["/robot_1/interface/mavros/debug_value/debug (no subscribers)"]) + n14 -->|"/robot_1/interface/mavros/debug_value/debug_float_array
DebugValue"| d38(["/robot_1/interface/mavros/debug_value/debug_float_array (no subscribers)"]) + n14 -->|"/robot_1/interface/mavros/debug_value/debug_vector
DebugValue"| d39(["/robot_1/interface/mavros/debug_value/debug_vector (no subscribers)"]) + n14 -->|"/robot_1/interface/mavros/debug_value/named_value_float
DebugValue"| d40(["/robot_1/interface/mavros/debug_value/named_value_float (no subscribers)"]) + n14 -->|"/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"| n14 + n15 -->|"/robot_1/interface/mavros/esc_status/info
ESCInfo"| d43(["/robot_1/interface/mavros/esc_status/info (no subscribers)"]) + 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"| n77 + 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)"]) + n20 -->|"/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"| n20 + n20 -->|"/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"| n20 + d54(["/robot_1/interface/mavros/gimbal_control/manager/set_manual_control (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_manual_control
GimbalManagerSetPitchyaw"| n20 + d55(["/robot_1/interface/mavros/gimbal_control/manager/set_pitchyaw (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_pitchyaw
GimbalManagerSetPitchyaw"| n20 + n20 -->|"/robot_1/interface/mavros/gimbal_control/manager/status
GimbalManagerStatus"| d56(["/robot_1/interface/mavros/gimbal_control/manager/status (no subscribers)"]) + n21 -->|"/robot_1/interface/mavros/global_position/compass_hdg
Float64"| n5 + n21 -->|"/robot_1/interface/mavros/global_position/global
NavSatFix"| n54 + n21 -->|"/robot_1/interface/mavros/global_position/global
NavSatFix"| n70 + n21 -->|"/robot_1/interface/mavros/global_position/gp_lp_offset
PoseStamped"| d57(["/robot_1/interface/mavros/global_position/gp_lp_offset (no subscribers)"]) + n21 -->|"/robot_1/interface/mavros/global_position/gp_origin
GeoPointStamped"| n25 + n21 -->|"/robot_1/interface/mavros/global_position/local
Odometry"| n70 + n21 -->|"/robot_1/interface/mavros/global_position/raw/fix
NavSatFix"| n5 + n21 -->|"/robot_1/interface/mavros/global_position/raw/gps_vel
TwistStamped"| d58(["/robot_1/interface/mavros/global_position/raw/gps_vel (no subscribers)"]) + n21 -->|"/robot_1/interface/mavros/global_position/raw/satellites
UInt32"| d59(["/robot_1/interface/mavros/global_position/raw/satellites (no subscribers)"]) + n21 -->|"/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"| n21 + d62(["/robot_1/interface/mavros/gps_input/gps_input (no publishers)"]) -->|"/robot_1/interface/mavros/gps_input/gps_input
GPSINPUT"| n22 + n23 -->|"/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"| n23 + n24 -->|"/robot_1/interface/mavros/gpsstatus/gps1/raw
GPSRAW"| d65(["/robot_1/interface/mavros/gpsstatus/gps1/raw (no subscribers)"]) + n24 -->|"/robot_1/interface/mavros/gpsstatus/gps1/rtk
GPSRTK"| d66(["/robot_1/interface/mavros/gpsstatus/gps1/rtk (no subscribers)"]) + n24 -->|"/robot_1/interface/mavros/gpsstatus/gps2/raw
GPSRAW"| d67(["/robot_1/interface/mavros/gpsstatus/gps2/raw (no subscribers)"]) + n24 -->|"/robot_1/interface/mavros/gpsstatus/gps2/rtk
GPSRTK"| d68(["/robot_1/interface/mavros/gpsstatus/gps2/rtk (no subscribers)"]) + n26 -->|"/robot_1/interface/mavros/hil/actuator_controls
HilActuatorControls"| d69(["/robot_1/interface/mavros/hil/actuator_controls (no subscribers)"]) + n26 -->|"/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"| n26 + d72(["/robot_1/interface/mavros/hil/imu_ned (no publishers)"]) -->|"/robot_1/interface/mavros/hil/imu_ned
HilSensor"| n26 + d73(["/robot_1/interface/mavros/hil/optical_flow (no publishers)"]) -->|"/robot_1/interface/mavros/hil/optical_flow
OpticalFlowRad"| n26 + d74(["/robot_1/interface/mavros/hil/rc_inputs (no publishers)"]) -->|"/robot_1/interface/mavros/hil/rc_inputs
RCIn"| n26 + d75(["/robot_1/interface/mavros/hil/state (no publishers)"]) -->|"/robot_1/interface/mavros/hil/state
HilStateQuaternion"| n26 + n27 -->|"/robot_1/interface/mavros/home_position/home
HomePosition"| n21 + n27 -->|"/robot_1/interface/mavros/home_position/home
HomePosition"| n70 + n70 -->|"/robot_1/interface/mavros/home_position/set
HomePosition"| n27 + n28 -->|"/robot_1/interface/mavros/imu/data
Imu"| d76(["/robot_1/interface/mavros/imu/data (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/data_raw
Imu"| d77(["/robot_1/interface/mavros/imu/data_raw (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/diff_pressure
FluidPressure"| d78(["/robot_1/interface/mavros/imu/diff_pressure (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/mag
MagneticField"| d79(["/robot_1/interface/mavros/imu/mag (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/static_pressure
FluidPressure"| d80(["/robot_1/interface/mavros/imu/static_pressure (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/temperature_baro
Temperature"| d81(["/robot_1/interface/mavros/imu/temperature_baro (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/temperature_imu
Temperature"| d82(["/robot_1/interface/mavros/imu/temperature_imu (no subscribers)"]) + n29 -->|"/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"| n29 + n29 -->|"/robot_1/interface/mavros/landing_target/pose_in
PoseStamped"| d85(["/robot_1/interface/mavros/landing_target/pose_in (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/accel
AccelWithCovarianceStamped"| d86(["/robot_1/interface/mavros/local_position/accel (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/odom
Odometry"| n71 + n30 -->|"/robot_1/interface/mavros/local_position/pose
PoseStamped"| n54 + n30 -->|"/robot_1/interface/mavros/local_position/pose_cov
PoseWithCovarianceStamped"| d87(["/robot_1/interface/mavros/local_position/pose_cov (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/velocity_body
TwistStamped"| d88(["/robot_1/interface/mavros/local_position/velocity_body (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/velocity_body_cov
TwistWithCovarianceStamped"| d89(["/robot_1/interface/mavros/local_position/velocity_body_cov (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/velocity_local
TwistStamped"| d90(["/robot_1/interface/mavros/local_position/velocity_local (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/log_transfer/raw/log_data
LogData"| d91(["/robot_1/interface/mavros/log_transfer/raw/log_data (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/log_transfer/raw/log_entry
LogEntry"| d92(["/robot_1/interface/mavros/log_transfer/raw/log_entry (no subscribers)"]) + n32 -->|"/robot_1/interface/mavros/mag_calibration/report
MagnetometerReporter"| d93(["/robot_1/interface/mavros/mag_calibration/report (no subscribers)"]) + n32 -->|"/robot_1/interface/mavros/mag_calibration/status
UInt8"| d94(["/robot_1/interface/mavros/mag_calibration/status (no subscribers)"]) + n33 -->|"/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"| n33 + n37 -->|"/robot_1/interface/mavros/mission/reached
WaypointReached"| d97(["/robot_1/interface/mavros/mission/reached (no subscribers)"]) + n37 -->|"/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"| n38 + d100(["/robot_1/interface/mavros/mocap/tf (no publishers)"]) -->|"/robot_1/interface/mavros/mocap/tf
TransformStamped"| n38 + d101(["/robot_1/interface/mavros/mount_control/command (no publishers)"]) -->|"/robot_1/interface/mavros/mount_control/command
MountControl"| n39 + n39 -->|"/robot_1/interface/mavros/mount_control/orientation
Quaternion"| d102(["/robot_1/interface/mavros/mount_control/orientation (no subscribers)"]) + n39 -->|"/robot_1/interface/mavros/mount_control/status
Vector3Stamped"| d103(["/robot_1/interface/mavros/mount_control/status (no subscribers)"]) + n40 -->|"/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"| n41 + d106(["/robot_1/interface/mavros/obstacle_distance_3d/send (no publishers)"]) -->|"/robot_1/interface/mavros/obstacle_distance_3d/send
ObstacleDistance3D"| n42 + n43 -->|"/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"| n43 + d109(["/robot_1/interface/mavros/onboard_computer/status (no publishers)"]) -->|"/robot_1/interface/mavros/onboard_computer/status
OnboardComputerStatus"| n44 + d110(["/robot_1/interface/mavros/open_drone_id/basic_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/basic_id
OpenDroneIDBasicID"| n45 + d111(["/robot_1/interface/mavros/open_drone_id/operator_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/operator_id
OpenDroneIDOperatorID"| n45 + d112(["/robot_1/interface/mavros/open_drone_id/self_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/self_id
OpenDroneIDSelfID"| n45 + d113(["/robot_1/interface/mavros/open_drone_id/system (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/system
OpenDroneIDSystem"| n45 + d114(["/robot_1/interface/mavros/open_drone_id/system_update (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/system_update
OpenDroneIDSystemUpdate"| n45 + n46 -->|"/robot_1/interface/mavros/optical_flow/ground_distance
Range"| d115(["/robot_1/interface/mavros/optical_flow/ground_distance (no subscribers)"]) + n46 -->|"/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"| n46 + n47 -->|"/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"| n48 + n49 -->|"/robot_1/interface/mavros/px4flow/ground_distance
Range"| d120(["/robot_1/interface/mavros/px4flow/ground_distance (no subscribers)"]) + n49 -->|"/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"| n49 + n49 -->|"/robot_1/interface/mavros/px4flow/temperature
Temperature"| d123(["/robot_1/interface/mavros/px4flow/temperature (no subscribers)"]) + n60 -->|"/robot_1/interface/mavros/radio_status
RadioStatus"| d124(["/robot_1/interface/mavros/radio_status (no subscribers)"]) + n50 -->|"/robot_1/interface/mavros/rallypoint/rallypoints
WaypointList"| d125(["/robot_1/interface/mavros/rallypoint/rallypoints (no subscribers)"]) + n51 -->|"/robot_1/interface/mavros/rc/in
RCIn"| d126(["/robot_1/interface/mavros/rc/in (no subscribers)"]) + n51 -->|"/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"| n51 + d129(["/robot_1/interface/mavros/setpoint_accel/accel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_accel/accel
Vector3Stamped"| n52 + d130(["/robot_1/interface/mavros/setpoint_attitude/cmd_vel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_attitude/cmd_vel
TwistStamped"| n53 + d131(["/robot_1/interface/mavros/setpoint_attitude/thrust (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_attitude/thrust
Thrust"| n53 + n70 -->|"/robot_1/interface/mavros/setpoint_position/global
GeoPoseStamped"| n54 + d132(["/robot_1/interface/mavros/setpoint_position/global_to_local (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_position/global_to_local
GeoPoseStamped"| n54 + n70 -->|"/robot_1/interface/mavros/setpoint_position/local
PoseStamped"| n54 + n70 -->|"/robot_1/interface/mavros/setpoint_raw/attitude
AttitudeTarget"| n55 + d133(["/robot_1/interface/mavros/setpoint_raw/global (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_raw/global
GlobalPositionTarget"| n55 + n70 -->|"/robot_1/interface/mavros/setpoint_raw/local
PositionTarget"| n55 + n55 -->|"/robot_1/interface/mavros/setpoint_raw/target_attitude
AttitudeTarget"| d134(["/robot_1/interface/mavros/setpoint_raw/target_attitude (no subscribers)"]) + n55 -->|"/robot_1/interface/mavros/setpoint_raw/target_global
GlobalPositionTarget"| d135(["/robot_1/interface/mavros/setpoint_raw/target_global (no subscribers)"]) + n55 -->|"/robot_1/interface/mavros/setpoint_raw/target_local
PositionTarget"| d136(["/robot_1/interface/mavros/setpoint_raw/target_local (no subscribers)"]) + n56 -->|"/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"| n56 + d139(["/robot_1/interface/mavros/setpoint_velocity/cmd_vel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_velocity/cmd_vel
TwistStamped"| n57 + d140(["/robot_1/interface/mavros/setpoint_velocity/cmd_vel_unstamped (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_velocity/cmd_vel_unstamped
Twist"| n57 + n58 -->|"/robot_1/interface/mavros/sim_state/acceleration
Vector3Stamped"| d141(["/robot_1/interface/mavros/sim_state/acceleration (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/sim_state/attitude
Imu"| d142(["/robot_1/interface/mavros/sim_state/attitude (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/sim_state/global_position
NavSatFix"| d143(["/robot_1/interface/mavros/sim_state/global_position (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/sim_state/velocity_body
TwistStamped"| d144(["/robot_1/interface/mavros/sim_state/velocity_body (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/sim_state/velocity_local
TwistStamped"| d145(["/robot_1/interface/mavros/sim_state/velocity_local (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/state
State"| n70 + n59 -->|"/robot_1/interface/mavros/status_event
StatusEvent"| d146(["/robot_1/interface/mavros/status_event (no subscribers)"]) + n59 -->|"/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"| n59 + n59 -->|"/robot_1/interface/mavros/sys_status
SysStatus"| d149(["/robot_1/interface/mavros/sys_status (no subscribers)"]) + n6 -->|"/robot_1/interface/mavros/target_actuator_control
ActuatorControl"| d150(["/robot_1/interface/mavros/target_actuator_control (no subscribers)"]) + n61 -->|"/robot_1/interface/mavros/terrain/report
TerrainReport"| d151(["/robot_1/interface/mavros/terrain/report (no subscribers)"]) + n62 -->|"/robot_1/interface/mavros/time_reference
TimeReference"| d152(["/robot_1/interface/mavros/time_reference (no subscribers)"]) + n62 -->|"/robot_1/interface/mavros/timesync_status
TimesyncStatus"| d153(["/robot_1/interface/mavros/timesync_status (no subscribers)"]) + n63 -->|"/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"| n63 + d156(["/robot_1/interface/mavros/trajectory/path (no publishers)"]) -->|"/robot_1/interface/mavros/trajectory/path
Path"| n63 + d157(["/robot_1/interface/mavros/tunnel/in (no publishers)"]) -->|"/robot_1/interface/mavros/tunnel/in
Tunnel"| n64 + n64 -->|"/robot_1/interface/mavros/tunnel/out
Tunnel"| d158(["/robot_1/interface/mavros/tunnel/out (no subscribers)"]) + n65 -->|"/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"| n66 + d161(["/robot_1/interface/mavros/vision_pose/pose_cov (no publishers)"]) -->|"/robot_1/interface/mavros/vision_pose/pose_cov
PoseWithCovarianceStamped"| n66 + d162(["/robot_1/interface/mavros/vision_speed/speed_twist (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_twist
TwistStamped"| n67 + d163(["/robot_1/interface/mavros/vision_speed/speed_twist_cov (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_twist_cov
TwistWithCovarianceStamped"| n67 + d164(["/robot_1/interface/mavros/vision_speed/speed_vector (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_vector
Vector3Stamped"| n67 + n68 -->|"/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"| n70 + d167(["/robot_1/interface/rate_thrust_command (no publishers)"]) -->|"/robot_1/interface/rate_thrust_command
RateThrust"| n70 + 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"| n75 + 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"| n74 + n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n77 + n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n78 + n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n79 + n71 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n80 + 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"| 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 + 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"| 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"| 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"| 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"| 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 + n34 -->|"/tf
TFMessage"| n78 + n71 -->|"/tf
TFMessage"| n69 + n71 -->|"/tf
TFMessage"| n78 + n75 -->|"/tf
TFMessage"| n69 + n75 -->|"/tf
TFMessage"| n78 + n80 -->|"/tf
TFMessage"| n69 + n80 -->|"/tf
TFMessage"| n78 + n34 -->|"/tf_static
TFMessage"| n69 + n34 -->|"/tf_static
TFMessage"| n78 + n75 -->|"/tf_static
TFMessage"| n69 + n75 -->|"/tf_static
TFMessage"| n78 + n82 -->|"/tf_static
TFMessage"| n69 + n82 -->|"/tf_static
TFMessage"| n78 + n34 -->|"/uas2/mavlink_sink
Mavlink"| n36 + n36 -->|"/uas2/mavlink_source
Mavlink"| n34 +``` + + diff --git a/stacks/full_droan_cpu/README.md b/stacks/full_droan_cpu/README.md new file mode 100644 index 000000000..3ecc2916b --- /dev/null +++ b/stacks/full_droan_cpu/README.md @@ -0,0 +1,39 @@ +# `full_droan_cpu` — trunk reference stack + +Full autonomy with the **CPU DROAN local planner** (`droan_local_planner` + +a live `disparity_expansion` world model) instead of the GPU `droan_gl` node. +Local-planner variants are expressed as named stacks a few include lines +apart — rather than as launch-file arguments — so each variant is directly +selectable and carries its own observed wiring baseline. + +## What it launches + +Identical to [`full_default`](../full_default/README.md) except the DROAN +include lines — `droan_local_planner.launch.xml` plus +`disparity_expansion.launch.xml` instead of `droan_gl.launch.xml`. Everything +else — interface, sensors, perception, the other local modules, global, +behavior, logging, DDS router, gossip — matches `full_default` exactly. + +## How to run + +```bash +airstack up --stack full_droan_cpu --sim isaac --robots 1 +airstack ready +``` + +## Known limits + +- Every layer is composed module-by-module in `stack.launch.xml`, except + `interface.launch.py` (wrapped by design — the safety boundary) 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`. +- `modules.repos` pins no external modules yet; `docker-compose.yaml` is a + stub (trunk compose profiles provide all services). + +## wiring.md + +This stack's observed wiring diagram is committed at [wiring.md](wiring.md); +CI drift-checks the running graph against it. Regenerate via +`airstack test -m wiring --stack full_droan_cpu`. diff --git a/stacks/full_droan_cpu/docker-compose.yaml b/stacks/full_droan_cpu/docker-compose.yaml new file mode 100644 index 000000000..1f087c506 --- /dev/null +++ b/stacks/full_droan_cpu/docker-compose.yaml @@ -0,0 +1,11 @@ +# Per-stack image composition 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`. +# +# full_droan_cpu 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/full_droan_cpu/launch/stack.launch.xml b/stacks/full_droan_cpu/launch/stack.launch.xml new file mode 100644 index 000000000..c3a0469c7 --- /dev/null +++ b/stacks/full_droan_cpu/launch/stack.launch.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/full_droan_cpu/modules.repos b/stacks/full_droan_cpu/modules.repos new file mode 100644 index 000000000..85ed6f4bc --- /dev/null +++ b/stacks/full_droan_cpu/modules.repos @@ -0,0 +1,15 @@ +# modules.repos : module pins for the full_droan_cpu reference stack. +# +# 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/full_droan_cpu/wiring.md b/stacks/full_droan_cpu/wiring.md new file mode 100644 index 000000000..4276b40c8 --- /dev/null +++ b/stacks/full_droan_cpu/wiring.md @@ -0,0 +1,492 @@ +# Wiring snapshot: full_droan_cpu + +- **generated-by**: tests/system/test_wiring_snapshot.py +- **date**: 2026-08-22 00:07:31 +- **sim**: isaacsim +- **num_robots**: 1 +- **source-sha**: 57e0b8c34ec9 + +```mermaid +graph LR + subgraph g0["behavior"] + n2["/robot_1/behavior/drone_safety_monitor/drone_safety_monitor"] + end + subgraph g1["control"] + n3["/robot_1/control/pid_controller"] + end + subgraph g2["droan"] + n4["/robot_1/droan/disparity_expansion"] + n5["/robot_1/droan/droan_local_planner"] + end + subgraph g3["interface"] + n7["/robot_1/interface/mavros/actuator_control"] + n8["/robot_1/interface/mavros/adsb"] + n9["/robot_1/interface/mavros/altitude"] + n10["/robot_1/interface/mavros/cam_imu_sync"] + n11["/robot_1/interface/mavros/camera"] + n12["/robot_1/interface/mavros/cellular_status"] + n13["/robot_1/interface/mavros/cmd"] + n14["/robot_1/interface/mavros/companion_process"] + n15["/robot_1/interface/mavros/debug_value"] + n16["/robot_1/interface/mavros/esc_status"] + n17["/robot_1/interface/mavros/esc_telemetry"] + n18["/robot_1/interface/mavros/fake_gps"] + n19["/robot_1/interface/mavros/ftp"] + n20["/robot_1/interface/mavros/geofence"] + n21["/robot_1/interface/mavros/gimbal_control"] + n22["/robot_1/interface/mavros/global_position"] + n23["/robot_1/interface/mavros/gps_input"] + n24["/robot_1/interface/mavros/gps_rtk"] + n25["/robot_1/interface/mavros/gpsstatus"] + n26["/robot_1/interface/mavros/guided_target"] + n27["/robot_1/interface/mavros/hil"] + n28["/robot_1/interface/mavros/home_position"] + n29["/robot_1/interface/mavros/imu"] + n30["/robot_1/interface/mavros/landing_target"] + n31["/robot_1/interface/mavros/local_position"] + n32["/robot_1/interface/mavros/log_transfer"] + n33["/robot_1/interface/mavros/mag_calibration"] + n34["/robot_1/interface/mavros/manual_control"] + n35["/robot_1/interface/mavros/mavros"] + n36["/robot_1/interface/mavros/mavros_node"] + n37["/robot_1/interface/mavros/mavros_router"] + n38["/robot_1/interface/mavros/mission"] + n39["/robot_1/interface/mavros/mocap"] + n40["/robot_1/interface/mavros/mount_control"] + n41["/robot_1/interface/mavros/nav_controller_output"] + n42["/robot_1/interface/mavros/obstacle"] + n43["/robot_1/interface/mavros/obstacle_distance_3d"] + n44["/robot_1/interface/mavros/odometry"] + n45["/robot_1/interface/mavros/onboard_computer"] + n46["/robot_1/interface/mavros/open_drone_id"] + n47["/robot_1/interface/mavros/optical_flow"] + n48["/robot_1/interface/mavros/param"] + n49["/robot_1/interface/mavros/play_tune"] + n50["/robot_1/interface/mavros/px4flow"] + n51["/robot_1/interface/mavros/rallypoint"] + n52["/robot_1/interface/mavros/rc"] + n53["/robot_1/interface/mavros/setpoint_accel"] + n54["/robot_1/interface/mavros/setpoint_attitude"] + n55["/robot_1/interface/mavros/setpoint_position"] + n56["/robot_1/interface/mavros/setpoint_raw"] + n57["/robot_1/interface/mavros/setpoint_trajectory"] + n58["/robot_1/interface/mavros/setpoint_velocity"] + n59["/robot_1/interface/mavros/sim_state"] + n60["/robot_1/interface/mavros/sys"] + n61["/robot_1/interface/mavros/tdr_radio"] + n62["/robot_1/interface/mavros/terrain"] + n63["/robot_1/interface/mavros/time"] + n64["/robot_1/interface/mavros/trajectory"] + n65["/robot_1/interface/mavros/tunnel"] + n66["/robot_1/interface/mavros/vfr_hud"] + n67["/robot_1/interface/mavros/vision_pose"] + n68["/robot_1/interface/mavros/vision_speed"] + n69["/robot_1/interface/mavros/wind"] + n70["/robot_1/interface/odom_modifier"] + n71["/robot_1/interface/robot_interface"] + end + subgraph g4["odometry_conversion"] + n72["/robot_1/odometry_conversion/odometry_conversion"] + end + subgraph g5["perception"] + n73["/robot_1/perception/stereo_image_proc/disparity_node"] + n74["/robot_1/perception/stereo_pointcloud"] + end + subgraph g6["robot_1"] + n1["/robot_1/Container"] + n6["/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"] + end + subgraph g7["root"] + n0["/action_relay_client"] + end + subgraph g8["sensors"] + n77["/robot_1/sensors/lidar_point_cloud_filter"] + end + subgraph g9["takeoff_landing_planner"] + n78["/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"] + 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 + d0 -->|"/clock
Clock"| n80 + d0 -->|"/clock
Clock"| n81 + d0 -->|"/clock
Clock"| n82 + d0 -->|"/clock
Clock"| n83 + n35 -->|"/diagnostics
DiagnosticArray"| d1(["/diagnostics (no subscribers)"]) + n37 -->|"/diagnostics
DiagnosticArray"| d1 + n6 -->|"/gossip/peers
PeerProfile"| n6 + n26 -->|"/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 + n71 -->|"/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)"]) + n3 -->|"/robot_1/control/vz_pid_info
PIDInfo"| d6(["/robot_1/control/vz_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/x_pid_info
PIDInfo"| d7(["/robot_1/control/x_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/y_pid_info
PIDInfo"| d8(["/robot_1/control/y_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/z_pid_info
PIDInfo"| d9(["/robot_1/control/z_pid_info (no subscribers)"]) + n6 -->|"/robot_1/coordination/peer_registry
PeerProfile"| d10(["/robot_1/coordination/peer_registry (no subscribers)"]) + n70 -->|"/robot_1/cross_track_error
PoseStamped"| d11(["/robot_1/cross_track_error (no subscribers)"]) + d12(["/robot_1/droan/clear_map (no publishers)"]) -->|"/robot_1/droan/clear_map
Empty"| n5 + d13(["/robot_1/droan/custom_waypoint (no publishers)"]) -->|"/robot_1/droan/custom_waypoint
PoseStamped"| n5 + n5 -->|"/robot_1/droan/disparity_graph
MarkerArray"| n79 + n5 -->|"/robot_1/droan/disparity_map_debug
MarkerArray"| n79 + n4 -->|"/robot_1/droan/expanded_disparity_bg
Image"| n5 + n4 -->|"/robot_1/droan/expanded_disparity_fg
Image"| n5 + n4 -->|"/robot_1/droan/expansion_cloud
PointCloud2"| n79 + n4 -->|"/robot_1/droan/expansion_poly
MarkerArray"| n79 + d14(["/robot_1/droan/fg_bg_cloud (no publishers)"]) -->|"/robot_1/droan/fg_bg_cloud
PointCloud2"| n79 + n4 -->|"/robot_1/droan/frustum
Marker"| n79 + d15(["/robot_1/droan/graph_vis (no publishers)"]) -->|"/robot_1/droan/graph_vis
MarkerArray"| n79 + n5 -->|"/robot_1/droan/local_planner_global_plan_vis
MarkerArray"| n79 + n5 -->|"/robot_1/droan/map_clearing_point
PoseStamped"| d16(["/robot_1/droan/map_clearing_point (no subscribers)"]) + d17(["/robot_1/droan/none (no publishers)"]) -->|"/robot_1/droan/none
Image"| n4 + n5 -->|"/robot_1/droan/obstacle_vis
Range"| d18(["/robot_1/droan/obstacle_vis (no subscribers)"]) + d19(["/robot_1/droan/reset_stuck (no publishers)"]) -->|"/robot_1/droan/reset_stuck
Empty"| n5 + n5 -->|"/robot_1/droan/rewind_info
MarkerArray"| n79 + n5 -->|"/robot_1/droan/stuck
Bool"| d20(["/robot_1/droan/stuck (no subscribers)"]) + d21(["/robot_1/droan/traj_debug (no publishers)"]) -->|"/robot_1/droan/traj_debug
MarkerArray"| n79 + n5 -->|"/robot_1/droan/trajectory_library_vis
MarkerArray"| n79 + n5 -->|"/robot_1/droan/trajectory_override
TrajectoryXYZVYaw"| d22(["/robot_1/droan/trajectory_override (no subscribers)"]) + n5 -->|"/robot_1/droan/virtual_obstacles
MarkerArray"| n79 + d23(["/robot_1/droan/way_point (no publishers)"]) -->|"/robot_1/droan/way_point
PointStamped"| n5 + n70 -->|"/robot_1/global_plan
Path"| n5 + n70 -->|"/robot_1/global_plan
Path"| n6 + n70 -->|"/robot_1/global_plan
Path"| n79 + n75 -->|"/robot_1/global_plan
Path"| n5 + n75 -->|"/robot_1/global_plan
Path"| n6 + n75 -->|"/robot_1/global_plan
Path"| n79 + d24(["/robot_1/interface/attitude_thrust_command (no publishers)"]) -->|"/robot_1/interface/attitude_thrust_command
AttitudeThrust"| n71 + d25(["/robot_1/interface/cmd_attitude_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_attitude_thrust
AttitudeThrust"| n71 + n70 -->|"/robot_1/interface/cmd_pose
PoseStamped"| n71 + d26(["/robot_1/interface/cmd_rate_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_rate_thrust
RateThrust"| n71 + n3 -->|"/robot_1/interface/cmd_roll_pitch_yawrate_thrust
RollPitchYawrateThrust"| n71 + d27(["/robot_1/interface/cmd_torque_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_torque_thrust
TorqueThrust"| n71 + n70 -->|"/robot_1/interface/cmd_velocity
TwistStamped"| n71 + n71 -->|"/robot_1/interface/has_control
Bool"| n78 + n71 -->|"/robot_1/interface/is_armed
Bool"| n78 + d28(["/robot_1/interface/mavros/actuator_control (no publishers)"]) -->|"/robot_1/interface/mavros/actuator_control
ActuatorControl"| n7 + d29(["/robot_1/interface/mavros/adsb/send (no publishers)"]) -->|"/robot_1/interface/mavros/adsb/send
ADSBVehicle"| n8 + n8 -->|"/robot_1/interface/mavros/adsb/vehicle
ADSBVehicle"| d30(["/robot_1/interface/mavros/adsb/vehicle (no subscribers)"]) + n9 -->|"/robot_1/interface/mavros/altitude
Altitude"| d31(["/robot_1/interface/mavros/altitude (no subscribers)"]) + n60 -->|"/robot_1/interface/mavros/battery
BatteryState"| d32(["/robot_1/interface/mavros/battery (no subscribers)"]) + n10 -->|"/robot_1/interface/mavros/cam_imu_sync/cam_imu_stamp
CamIMUStamp"| d33(["/robot_1/interface/mavros/cam_imu_sync/cam_imu_stamp (no subscribers)"]) + n11 -->|"/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"| n12 + d36(["/robot_1/interface/mavros/companion_process/status (no publishers)"]) -->|"/robot_1/interface/mavros/companion_process/status
CompanionProcessStatus"| n14 + n15 -->|"/robot_1/interface/mavros/debug_value/debug
DebugValue"| d37(["/robot_1/interface/mavros/debug_value/debug (no subscribers)"]) + n15 -->|"/robot_1/interface/mavros/debug_value/debug_float_array
DebugValue"| d38(["/robot_1/interface/mavros/debug_value/debug_float_array (no subscribers)"]) + n15 -->|"/robot_1/interface/mavros/debug_value/debug_vector
DebugValue"| d39(["/robot_1/interface/mavros/debug_value/debug_vector (no subscribers)"]) + n15 -->|"/robot_1/interface/mavros/debug_value/named_value_float
DebugValue"| d40(["/robot_1/interface/mavros/debug_value/named_value_float (no subscribers)"]) + n15 -->|"/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"| n15 + n16 -->|"/robot_1/interface/mavros/esc_status/info
ESCInfo"| d43(["/robot_1/interface/mavros/esc_status/info (no subscribers)"]) + n16 -->|"/robot_1/interface/mavros/esc_status/status
ESCStatus"| d44(["/robot_1/interface/mavros/esc_status/status (no subscribers)"]) + n17 -->|"/robot_1/interface/mavros/esc_telemetry/telemetry
ESCTelemetry"| d45(["/robot_1/interface/mavros/esc_telemetry/telemetry (no subscribers)"]) + n60 -->|"/robot_1/interface/mavros/estimator_status
EstimatorStatus"| d46(["/robot_1/interface/mavros/estimator_status (no subscribers)"]) + n60 -->|"/robot_1/interface/mavros/extended_state
ExtendedState"| n78 + d47(["/robot_1/interface/mavros/fake_gps/mocap/tf (no publishers)"]) -->|"/robot_1/interface/mavros/fake_gps/mocap/tf
TransformStamped"| n18 + n20 -->|"/robot_1/interface/mavros/geofence/fences
WaypointList"| d48(["/robot_1/interface/mavros/geofence/fences (no subscribers)"]) + n21 -->|"/robot_1/interface/mavros/gimbal_control/device/attitude_status
GimbalDeviceAttitudeStatus"| d49(["/robot_1/interface/mavros/gimbal_control/device/attitude_status (no subscribers)"]) + n21 -->|"/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"| n21 + n21 -->|"/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"| n21 + d54(["/robot_1/interface/mavros/gimbal_control/manager/set_manual_control (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_manual_control
GimbalManagerSetPitchyaw"| n21 + d55(["/robot_1/interface/mavros/gimbal_control/manager/set_pitchyaw (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_pitchyaw
GimbalManagerSetPitchyaw"| n21 + n21 -->|"/robot_1/interface/mavros/gimbal_control/manager/status
GimbalManagerStatus"| d56(["/robot_1/interface/mavros/gimbal_control/manager/status (no subscribers)"]) + n22 -->|"/robot_1/interface/mavros/global_position/compass_hdg
Float64"| n6 + n22 -->|"/robot_1/interface/mavros/global_position/global
NavSatFix"| n55 + n22 -->|"/robot_1/interface/mavros/global_position/global
NavSatFix"| n71 + n22 -->|"/robot_1/interface/mavros/global_position/gp_lp_offset
PoseStamped"| d57(["/robot_1/interface/mavros/global_position/gp_lp_offset (no subscribers)"]) + n22 -->|"/robot_1/interface/mavros/global_position/gp_origin
GeoPointStamped"| n26 + n22 -->|"/robot_1/interface/mavros/global_position/local
Odometry"| n71 + n22 -->|"/robot_1/interface/mavros/global_position/raw/fix
NavSatFix"| n6 + n22 -->|"/robot_1/interface/mavros/global_position/raw/gps_vel
TwistStamped"| d58(["/robot_1/interface/mavros/global_position/raw/gps_vel (no subscribers)"]) + n22 -->|"/robot_1/interface/mavros/global_position/raw/satellites
UInt32"| d59(["/robot_1/interface/mavros/global_position/raw/satellites (no subscribers)"]) + n22 -->|"/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"| n22 + d62(["/robot_1/interface/mavros/gps_input/gps_input (no publishers)"]) -->|"/robot_1/interface/mavros/gps_input/gps_input
GPSINPUT"| n23 + n24 -->|"/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"| n24 + n25 -->|"/robot_1/interface/mavros/gpsstatus/gps1/raw
GPSRAW"| d65(["/robot_1/interface/mavros/gpsstatus/gps1/raw (no subscribers)"]) + n25 -->|"/robot_1/interface/mavros/gpsstatus/gps1/rtk
GPSRTK"| d66(["/robot_1/interface/mavros/gpsstatus/gps1/rtk (no subscribers)"]) + n25 -->|"/robot_1/interface/mavros/gpsstatus/gps2/raw
GPSRAW"| d67(["/robot_1/interface/mavros/gpsstatus/gps2/raw (no subscribers)"]) + n25 -->|"/robot_1/interface/mavros/gpsstatus/gps2/rtk
GPSRTK"| d68(["/robot_1/interface/mavros/gpsstatus/gps2/rtk (no subscribers)"]) + n27 -->|"/robot_1/interface/mavros/hil/actuator_controls
HilActuatorControls"| d69(["/robot_1/interface/mavros/hil/actuator_controls (no subscribers)"]) + n27 -->|"/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"| n27 + d72(["/robot_1/interface/mavros/hil/imu_ned (no publishers)"]) -->|"/robot_1/interface/mavros/hil/imu_ned
HilSensor"| n27 + d73(["/robot_1/interface/mavros/hil/optical_flow (no publishers)"]) -->|"/robot_1/interface/mavros/hil/optical_flow
OpticalFlowRad"| n27 + d74(["/robot_1/interface/mavros/hil/rc_inputs (no publishers)"]) -->|"/robot_1/interface/mavros/hil/rc_inputs
RCIn"| n27 + d75(["/robot_1/interface/mavros/hil/state (no publishers)"]) -->|"/robot_1/interface/mavros/hil/state
HilStateQuaternion"| n27 + n28 -->|"/robot_1/interface/mavros/home_position/home
HomePosition"| n22 + n28 -->|"/robot_1/interface/mavros/home_position/home
HomePosition"| n71 + n71 -->|"/robot_1/interface/mavros/home_position/set
HomePosition"| n28 + n29 -->|"/robot_1/interface/mavros/imu/data
Imu"| d76(["/robot_1/interface/mavros/imu/data (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/imu/data_raw
Imu"| d77(["/robot_1/interface/mavros/imu/data_raw (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/imu/diff_pressure
FluidPressure"| d78(["/robot_1/interface/mavros/imu/diff_pressure (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/imu/mag
MagneticField"| d79(["/robot_1/interface/mavros/imu/mag (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/imu/static_pressure
FluidPressure"| d80(["/robot_1/interface/mavros/imu/static_pressure (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/imu/temperature_baro
Temperature"| d81(["/robot_1/interface/mavros/imu/temperature_baro (no subscribers)"]) + n29 -->|"/robot_1/interface/mavros/imu/temperature_imu
Temperature"| d82(["/robot_1/interface/mavros/imu/temperature_imu (no subscribers)"]) + n30 -->|"/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"| n30 + n30 -->|"/robot_1/interface/mavros/landing_target/pose_in
PoseStamped"| d85(["/robot_1/interface/mavros/landing_target/pose_in (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/local_position/accel
AccelWithCovarianceStamped"| d86(["/robot_1/interface/mavros/local_position/accel (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/local_position/odom
Odometry"| n72 + n31 -->|"/robot_1/interface/mavros/local_position/pose
PoseStamped"| n55 + n31 -->|"/robot_1/interface/mavros/local_position/pose_cov
PoseWithCovarianceStamped"| d87(["/robot_1/interface/mavros/local_position/pose_cov (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/local_position/velocity_body
TwistStamped"| d88(["/robot_1/interface/mavros/local_position/velocity_body (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/local_position/velocity_body_cov
TwistWithCovarianceStamped"| d89(["/robot_1/interface/mavros/local_position/velocity_body_cov (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/local_position/velocity_local
TwistStamped"| d90(["/robot_1/interface/mavros/local_position/velocity_local (no subscribers)"]) + n32 -->|"/robot_1/interface/mavros/log_transfer/raw/log_data
LogData"| d91(["/robot_1/interface/mavros/log_transfer/raw/log_data (no subscribers)"]) + n32 -->|"/robot_1/interface/mavros/log_transfer/raw/log_entry
LogEntry"| d92(["/robot_1/interface/mavros/log_transfer/raw/log_entry (no subscribers)"]) + n33 -->|"/robot_1/interface/mavros/mag_calibration/report
MagnetometerReporter"| d93(["/robot_1/interface/mavros/mag_calibration/report (no subscribers)"]) + n33 -->|"/robot_1/interface/mavros/mag_calibration/status
UInt8"| d94(["/robot_1/interface/mavros/mag_calibration/status (no subscribers)"]) + n34 -->|"/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"| n34 + n38 -->|"/robot_1/interface/mavros/mission/reached
WaypointReached"| d97(["/robot_1/interface/mavros/mission/reached (no subscribers)"]) + n38 -->|"/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"| n39 + d100(["/robot_1/interface/mavros/mocap/tf (no publishers)"]) -->|"/robot_1/interface/mavros/mocap/tf
TransformStamped"| n39 + d101(["/robot_1/interface/mavros/mount_control/command (no publishers)"]) -->|"/robot_1/interface/mavros/mount_control/command
MountControl"| n40 + n40 -->|"/robot_1/interface/mavros/mount_control/orientation
Quaternion"| d102(["/robot_1/interface/mavros/mount_control/orientation (no subscribers)"]) + n40 -->|"/robot_1/interface/mavros/mount_control/status
Vector3Stamped"| d103(["/robot_1/interface/mavros/mount_control/status (no subscribers)"]) + n41 -->|"/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"| n42 + d106(["/robot_1/interface/mavros/obstacle_distance_3d/send (no publishers)"]) -->|"/robot_1/interface/mavros/obstacle_distance_3d/send
ObstacleDistance3D"| n43 + n44 -->|"/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"| n44 + d109(["/robot_1/interface/mavros/onboard_computer/status (no publishers)"]) -->|"/robot_1/interface/mavros/onboard_computer/status
OnboardComputerStatus"| n45 + d110(["/robot_1/interface/mavros/open_drone_id/basic_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/basic_id
OpenDroneIDBasicID"| n46 + d111(["/robot_1/interface/mavros/open_drone_id/operator_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/operator_id
OpenDroneIDOperatorID"| n46 + d112(["/robot_1/interface/mavros/open_drone_id/self_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/self_id
OpenDroneIDSelfID"| n46 + d113(["/robot_1/interface/mavros/open_drone_id/system (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/system
OpenDroneIDSystem"| n46 + d114(["/robot_1/interface/mavros/open_drone_id/system_update (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/system_update
OpenDroneIDSystemUpdate"| n46 + n47 -->|"/robot_1/interface/mavros/optical_flow/ground_distance
Range"| d115(["/robot_1/interface/mavros/optical_flow/ground_distance (no subscribers)"]) + n47 -->|"/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"| n47 + n48 -->|"/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"| n49 + n50 -->|"/robot_1/interface/mavros/px4flow/ground_distance
Range"| d120(["/robot_1/interface/mavros/px4flow/ground_distance (no subscribers)"]) + n50 -->|"/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"| n50 + n50 -->|"/robot_1/interface/mavros/px4flow/temperature
Temperature"| d123(["/robot_1/interface/mavros/px4flow/temperature (no subscribers)"]) + n61 -->|"/robot_1/interface/mavros/radio_status
RadioStatus"| d124(["/robot_1/interface/mavros/radio_status (no subscribers)"]) + n51 -->|"/robot_1/interface/mavros/rallypoint/rallypoints
WaypointList"| d125(["/robot_1/interface/mavros/rallypoint/rallypoints (no subscribers)"]) + n52 -->|"/robot_1/interface/mavros/rc/in
RCIn"| d126(["/robot_1/interface/mavros/rc/in (no subscribers)"]) + n52 -->|"/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"| n52 + d129(["/robot_1/interface/mavros/setpoint_accel/accel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_accel/accel
Vector3Stamped"| n53 + d130(["/robot_1/interface/mavros/setpoint_attitude/cmd_vel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_attitude/cmd_vel
TwistStamped"| n54 + d131(["/robot_1/interface/mavros/setpoint_attitude/thrust (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_attitude/thrust
Thrust"| n54 + n71 -->|"/robot_1/interface/mavros/setpoint_position/global
GeoPoseStamped"| n55 + d132(["/robot_1/interface/mavros/setpoint_position/global_to_local (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_position/global_to_local
GeoPoseStamped"| n55 + n71 -->|"/robot_1/interface/mavros/setpoint_position/local
PoseStamped"| n55 + n71 -->|"/robot_1/interface/mavros/setpoint_raw/attitude
AttitudeTarget"| n56 + d133(["/robot_1/interface/mavros/setpoint_raw/global (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_raw/global
GlobalPositionTarget"| n56 + n71 -->|"/robot_1/interface/mavros/setpoint_raw/local
PositionTarget"| n56 + n56 -->|"/robot_1/interface/mavros/setpoint_raw/target_attitude
AttitudeTarget"| d134(["/robot_1/interface/mavros/setpoint_raw/target_attitude (no subscribers)"]) + n56 -->|"/robot_1/interface/mavros/setpoint_raw/target_global
GlobalPositionTarget"| d135(["/robot_1/interface/mavros/setpoint_raw/target_global (no subscribers)"]) + n56 -->|"/robot_1/interface/mavros/setpoint_raw/target_local
PositionTarget"| d136(["/robot_1/interface/mavros/setpoint_raw/target_local (no subscribers)"]) + n57 -->|"/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"| n57 + d139(["/robot_1/interface/mavros/setpoint_velocity/cmd_vel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_velocity/cmd_vel
TwistStamped"| n58 + d140(["/robot_1/interface/mavros/setpoint_velocity/cmd_vel_unstamped (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_velocity/cmd_vel_unstamped
Twist"| n58 + n59 -->|"/robot_1/interface/mavros/sim_state/acceleration
Vector3Stamped"| d141(["/robot_1/interface/mavros/sim_state/acceleration (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/sim_state/attitude
Imu"| d142(["/robot_1/interface/mavros/sim_state/attitude (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/sim_state/global_position
NavSatFix"| d143(["/robot_1/interface/mavros/sim_state/global_position (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/sim_state/velocity_body
TwistStamped"| d144(["/robot_1/interface/mavros/sim_state/velocity_body (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/sim_state/velocity_local
TwistStamped"| d145(["/robot_1/interface/mavros/sim_state/velocity_local (no subscribers)"]) + n60 -->|"/robot_1/interface/mavros/state
State"| n71 + n60 -->|"/robot_1/interface/mavros/status_event
StatusEvent"| d146(["/robot_1/interface/mavros/status_event (no subscribers)"]) + n60 -->|"/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"| n60 + n60 -->|"/robot_1/interface/mavros/sys_status
SysStatus"| d149(["/robot_1/interface/mavros/sys_status (no subscribers)"]) + n7 -->|"/robot_1/interface/mavros/target_actuator_control
ActuatorControl"| d150(["/robot_1/interface/mavros/target_actuator_control (no subscribers)"]) + n62 -->|"/robot_1/interface/mavros/terrain/report
TerrainReport"| d151(["/robot_1/interface/mavros/terrain/report (no subscribers)"]) + n63 -->|"/robot_1/interface/mavros/time_reference
TimeReference"| d152(["/robot_1/interface/mavros/time_reference (no subscribers)"]) + n63 -->|"/robot_1/interface/mavros/timesync_status
TimesyncStatus"| d153(["/robot_1/interface/mavros/timesync_status (no subscribers)"]) + n64 -->|"/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"| n64 + d156(["/robot_1/interface/mavros/trajectory/path (no publishers)"]) -->|"/robot_1/interface/mavros/trajectory/path
Path"| n64 + d157(["/robot_1/interface/mavros/tunnel/in (no publishers)"]) -->|"/robot_1/interface/mavros/tunnel/in
Tunnel"| n65 + n65 -->|"/robot_1/interface/mavros/tunnel/out
Tunnel"| d158(["/robot_1/interface/mavros/tunnel/out (no subscribers)"]) + n66 -->|"/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"| n67 + d161(["/robot_1/interface/mavros/vision_pose/pose_cov (no publishers)"]) -->|"/robot_1/interface/mavros/vision_pose/pose_cov
PoseWithCovarianceStamped"| n67 + d162(["/robot_1/interface/mavros/vision_speed/speed_twist (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_twist
TwistStamped"| n68 + d163(["/robot_1/interface/mavros/vision_speed/speed_twist_cov (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_twist_cov
TwistWithCovarianceStamped"| n68 + d164(["/robot_1/interface/mavros/vision_speed/speed_vector (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_vector
Vector3Stamped"| n68 + n69 -->|"/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"| n71 + d167(["/robot_1/interface/rate_thrust_command (no publishers)"]) -->|"/robot_1/interface/rate_thrust_command
RateThrust"| n71 + d168(["/robot_1/interface/roll_pitch_yawrate_thrust_command (no publishers)"]) -->|"/robot_1/interface/roll_pitch_yawrate_thrust_command
RollPitchYawrateThrust"| n71 + 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 + n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n2 + n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n3 + n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n70 + n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n75 + n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n78 + n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n79 + n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n80 + n72 -->|"/robot_1/odometry_conversion/odometry
Odometry"| n81 + 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"| 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 + 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"| 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"| 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"| 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"| 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 + n35 -->|"/tf
TFMessage"| n79 + n72 -->|"/tf
TFMessage"| n70 + n72 -->|"/tf
TFMessage"| n79 + n76 -->|"/tf
TFMessage"| n70 + n76 -->|"/tf
TFMessage"| n79 + n81 -->|"/tf
TFMessage"| n70 + n81 -->|"/tf
TFMessage"| n79 + n35 -->|"/tf_static
TFMessage"| n70 + n35 -->|"/tf_static
TFMessage"| n79 + n76 -->|"/tf_static
TFMessage"| n70 + n76 -->|"/tf_static
TFMessage"| n79 + n83 -->|"/tf_static
TFMessage"| n70 + n83 -->|"/tf_static
TFMessage"| n79 + n35 -->|"/uas2/mavlink_sink
Mavlink"| n37 + n37 -->|"/uas2/mavlink_source
Mavlink"| n35 +``` + + diff --git a/stacks/full_macvo/README.md b/stacks/full_macvo/README.md new file mode 100644 index 000000000..975d91692 --- /dev/null +++ b/stacks/full_macvo/README.md @@ -0,0 +1,59 @@ +# `full_macvo` — trunk reference stack + +Full autonomy with **MAC-VO** as the disparity source for the local planner. +Local-planner variants are expressed as named stacks a few include lines +apart — rather than as launch-file arguments — so each variant is directly +selectable and carries its own observed wiring baseline. + +**Requires: `airstack module add asm_macvo`.** MAC-VO is not trunk-resident — +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 + +Identical to [`full_default`](../full_default/README.md) except: + +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. + +## 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 +``` + +## Known limits + +- Every layer is composed module-by-module in `stack.launch.xml`, except + `interface.launch.py` (wrapped by design — the safety boundary) and + `logging.launch.xml` (already a single self-contained module). +- `stereo_image_proc` 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. +- The committed `wiring.md` — captured from this stack's own first validated + snapshot run — 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 + +This stack's observed wiring diagram is committed at [wiring.md](wiring.md); +CI drift-checks the running graph against it. Regenerate via +`airstack test -m wiring --stack full_macvo`. diff --git a/stacks/full_macvo/docker-compose.yaml b/stacks/full_macvo/docker-compose.yaml new file mode 100644 index 000000000..c3362027f --- /dev/null +++ b/stacks/full_macvo/docker-compose.yaml @@ -0,0 +1,13 @@ +# Per-stack image composition: 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`. +# +# 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 new file mode 100644 index 000000000..af8d5cd4b --- /dev/null +++ b/stacks/full_macvo/launch/stack.launch.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/full_macvo/modules.repos b/stacks/full_macvo/modules.repos new file mode 100644 index 000000000..8731cbe61 --- /dev/null +++ b/stacks/full_macvo/modules.repos @@ -0,0 +1,21 @@ +# modules.repos : module pins for the full_macvo reference stack. +# +# 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. +# +# 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: + asm_macvo: + type: git + url: https://github.com/castacks/asm_macvo.git + # SHA pin matching the root modules.repos; retag to v0.1.0 at first release. + version: 431d7faf1f6fed20d415bb5e5a88d8dcb0d180df +x-local-modules: [] diff --git a/stacks/full_macvo/wiring.md b/stacks/full_macvo/wiring.md new file mode 100644 index 000000000..028116c14 --- /dev/null +++ b/stacks/full_macvo/wiring.md @@ -0,0 +1,494 @@ +# Wiring snapshot: full_macvo + +- **generated-by**: tests/system/test_wiring_snapshot.py +- **date**: 2026-08-22 00:23:29 +- **sim**: isaacsim +- **num_robots**: 1 +- **source-sha**: 57e0b8c34ec9 + +```mermaid +graph LR + subgraph g0["behavior"] + n2["/robot_1/behavior/drone_safety_monitor/drone_safety_monitor"] + end + subgraph g1["control"] + n3["/robot_1/control/pid_controller"] + end + subgraph g2["droan"] + n4["/robot_1/droan/disparity_expander_node"] + end + subgraph g3["interface"] + n6["/robot_1/interface/mavros/actuator_control"] + n7["/robot_1/interface/mavros/adsb"] + n8["/robot_1/interface/mavros/altitude"] + n9["/robot_1/interface/mavros/cam_imu_sync"] + n10["/robot_1/interface/mavros/camera"] + n11["/robot_1/interface/mavros/cellular_status"] + n12["/robot_1/interface/mavros/cmd"] + n13["/robot_1/interface/mavros/companion_process"] + n14["/robot_1/interface/mavros/debug_value"] + n15["/robot_1/interface/mavros/esc_status"] + n16["/robot_1/interface/mavros/esc_telemetry"] + n17["/robot_1/interface/mavros/fake_gps"] + n18["/robot_1/interface/mavros/ftp"] + n19["/robot_1/interface/mavros/geofence"] + n20["/robot_1/interface/mavros/gimbal_control"] + n21["/robot_1/interface/mavros/global_position"] + n22["/robot_1/interface/mavros/gps_input"] + n23["/robot_1/interface/mavros/gps_rtk"] + n24["/robot_1/interface/mavros/gpsstatus"] + n25["/robot_1/interface/mavros/guided_target"] + n26["/robot_1/interface/mavros/hil"] + n27["/robot_1/interface/mavros/home_position"] + n28["/robot_1/interface/mavros/imu"] + n29["/robot_1/interface/mavros/landing_target"] + n30["/robot_1/interface/mavros/local_position"] + n31["/robot_1/interface/mavros/log_transfer"] + n32["/robot_1/interface/mavros/mag_calibration"] + n33["/robot_1/interface/mavros/manual_control"] + n34["/robot_1/interface/mavros/mavros"] + n35["/robot_1/interface/mavros/mavros_node"] + n36["/robot_1/interface/mavros/mavros_router"] + n37["/robot_1/interface/mavros/mission"] + n38["/robot_1/interface/mavros/mocap"] + n39["/robot_1/interface/mavros/mount_control"] + n40["/robot_1/interface/mavros/nav_controller_output"] + n41["/robot_1/interface/mavros/obstacle"] + n42["/robot_1/interface/mavros/obstacle_distance_3d"] + n43["/robot_1/interface/mavros/odometry"] + n44["/robot_1/interface/mavros/onboard_computer"] + n45["/robot_1/interface/mavros/open_drone_id"] + n46["/robot_1/interface/mavros/optical_flow"] + n47["/robot_1/interface/mavros/param"] + n48["/robot_1/interface/mavros/play_tune"] + n49["/robot_1/interface/mavros/px4flow"] + n50["/robot_1/interface/mavros/rallypoint"] + n51["/robot_1/interface/mavros/rc"] + n52["/robot_1/interface/mavros/setpoint_accel"] + n53["/robot_1/interface/mavros/setpoint_attitude"] + n54["/robot_1/interface/mavros/setpoint_position"] + n55["/robot_1/interface/mavros/setpoint_raw"] + n56["/robot_1/interface/mavros/setpoint_trajectory"] + n57["/robot_1/interface/mavros/setpoint_velocity"] + n58["/robot_1/interface/mavros/sim_state"] + n59["/robot_1/interface/mavros/sys"] + n60["/robot_1/interface/mavros/tdr_radio"] + n61["/robot_1/interface/mavros/terrain"] + n62["/robot_1/interface/mavros/time"] + n63["/robot_1/interface/mavros/trajectory"] + n64["/robot_1/interface/mavros/tunnel"] + n65["/robot_1/interface/mavros/vfr_hud"] + n66["/robot_1/interface/mavros/vision_pose"] + n67["/robot_1/interface/mavros/vision_speed"] + n68["/robot_1/interface/mavros/wind"] + n69["/robot_1/interface/odom_modifier"] + n70["/robot_1/interface/robot_interface"] + end + subgraph g4["odometry_conversion"] + n71["/robot_1/odometry_conversion/odometry_conversion"] + end + subgraph g5["perception"] + 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"] + 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"] + n78["/robot_1/sensors/lidar_point_cloud_filter"] + end + subgraph g9["takeoff_landing_planner"] + n79["/robot_1/takeoff_landing_planner/takeoff_landing_task"] + end + subgraph g10["trajectory_controller"] + 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 + 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 + d0 -->|"/clock
Clock"| n80 + 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"| 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)"]) + n3 -->|"/robot_1/control/vz_pid_info
PIDInfo"| d6(["/robot_1/control/vz_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/x_pid_info
PIDInfo"| d7(["/robot_1/control/x_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/y_pid_info
PIDInfo"| d8(["/robot_1/control/y_pid_info (no subscribers)"]) + n3 -->|"/robot_1/control/z_pid_info
PIDInfo"| d9(["/robot_1/control/z_pid_info (no subscribers)"]) + n5 -->|"/robot_1/coordination/peer_registry
PeerProfile"| d10(["/robot_1/coordination/peer_registry (no subscribers)"]) + 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"| 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"| 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"| n80 + n4 -->|"/robot_1/droan/stuck
Bool"| d21(["/robot_1/droan/stuck (no subscribers)"]) + 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"| 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 + d26(["/robot_1/interface/cmd_rate_thrust (no publishers)"]) -->|"/robot_1/interface/cmd_rate_thrust
RateThrust"| n70 + 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"| 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)"]) + n8 -->|"/robot_1/interface/mavros/altitude
Altitude"| d31(["/robot_1/interface/mavros/altitude (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/battery
BatteryState"| d32(["/robot_1/interface/mavros/battery (no subscribers)"]) + n9 -->|"/robot_1/interface/mavros/cam_imu_sync/cam_imu_stamp
CamIMUStamp"| d33(["/robot_1/interface/mavros/cam_imu_sync/cam_imu_stamp (no subscribers)"]) + n10 -->|"/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"| n11 + d36(["/robot_1/interface/mavros/companion_process/status (no publishers)"]) -->|"/robot_1/interface/mavros/companion_process/status
CompanionProcessStatus"| n13 + n14 -->|"/robot_1/interface/mavros/debug_value/debug
DebugValue"| d37(["/robot_1/interface/mavros/debug_value/debug (no subscribers)"]) + n14 -->|"/robot_1/interface/mavros/debug_value/debug_float_array
DebugValue"| d38(["/robot_1/interface/mavros/debug_value/debug_float_array (no subscribers)"]) + n14 -->|"/robot_1/interface/mavros/debug_value/debug_vector
DebugValue"| d39(["/robot_1/interface/mavros/debug_value/debug_vector (no subscribers)"]) + n14 -->|"/robot_1/interface/mavros/debug_value/named_value_float
DebugValue"| d40(["/robot_1/interface/mavros/debug_value/named_value_float (no subscribers)"]) + n14 -->|"/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"| n14 + n15 -->|"/robot_1/interface/mavros/esc_status/info
ESCInfo"| d43(["/robot_1/interface/mavros/esc_status/info (no subscribers)"]) + 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"| 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)"]) + n20 -->|"/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"| n20 + n20 -->|"/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"| n20 + d54(["/robot_1/interface/mavros/gimbal_control/manager/set_manual_control (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_manual_control
GimbalManagerSetPitchyaw"| n20 + d55(["/robot_1/interface/mavros/gimbal_control/manager/set_pitchyaw (no publishers)"]) -->|"/robot_1/interface/mavros/gimbal_control/manager/set_pitchyaw
GimbalManagerSetPitchyaw"| n20 + n20 -->|"/robot_1/interface/mavros/gimbal_control/manager/status
GimbalManagerStatus"| d56(["/robot_1/interface/mavros/gimbal_control/manager/status (no subscribers)"]) + n21 -->|"/robot_1/interface/mavros/global_position/compass_hdg
Float64"| n5 + n21 -->|"/robot_1/interface/mavros/global_position/global
NavSatFix"| n54 + n21 -->|"/robot_1/interface/mavros/global_position/global
NavSatFix"| n70 + n21 -->|"/robot_1/interface/mavros/global_position/gp_lp_offset
PoseStamped"| d57(["/robot_1/interface/mavros/global_position/gp_lp_offset (no subscribers)"]) + n21 -->|"/robot_1/interface/mavros/global_position/gp_origin
GeoPointStamped"| n25 + n21 -->|"/robot_1/interface/mavros/global_position/local
Odometry"| n70 + n21 -->|"/robot_1/interface/mavros/global_position/raw/fix
NavSatFix"| n5 + n21 -->|"/robot_1/interface/mavros/global_position/raw/gps_vel
TwistStamped"| d58(["/robot_1/interface/mavros/global_position/raw/gps_vel (no subscribers)"]) + n21 -->|"/robot_1/interface/mavros/global_position/raw/satellites
UInt32"| d59(["/robot_1/interface/mavros/global_position/raw/satellites (no subscribers)"]) + n21 -->|"/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"| n21 + d62(["/robot_1/interface/mavros/gps_input/gps_input (no publishers)"]) -->|"/robot_1/interface/mavros/gps_input/gps_input
GPSINPUT"| n22 + n23 -->|"/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"| n23 + n24 -->|"/robot_1/interface/mavros/gpsstatus/gps1/raw
GPSRAW"| d65(["/robot_1/interface/mavros/gpsstatus/gps1/raw (no subscribers)"]) + n24 -->|"/robot_1/interface/mavros/gpsstatus/gps1/rtk
GPSRTK"| d66(["/robot_1/interface/mavros/gpsstatus/gps1/rtk (no subscribers)"]) + n24 -->|"/robot_1/interface/mavros/gpsstatus/gps2/raw
GPSRAW"| d67(["/robot_1/interface/mavros/gpsstatus/gps2/raw (no subscribers)"]) + n24 -->|"/robot_1/interface/mavros/gpsstatus/gps2/rtk
GPSRTK"| d68(["/robot_1/interface/mavros/gpsstatus/gps2/rtk (no subscribers)"]) + n26 -->|"/robot_1/interface/mavros/hil/actuator_controls
HilActuatorControls"| d69(["/robot_1/interface/mavros/hil/actuator_controls (no subscribers)"]) + n26 -->|"/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"| n26 + d72(["/robot_1/interface/mavros/hil/imu_ned (no publishers)"]) -->|"/robot_1/interface/mavros/hil/imu_ned
HilSensor"| n26 + d73(["/robot_1/interface/mavros/hil/optical_flow (no publishers)"]) -->|"/robot_1/interface/mavros/hil/optical_flow
OpticalFlowRad"| n26 + d74(["/robot_1/interface/mavros/hil/rc_inputs (no publishers)"]) -->|"/robot_1/interface/mavros/hil/rc_inputs
RCIn"| n26 + d75(["/robot_1/interface/mavros/hil/state (no publishers)"]) -->|"/robot_1/interface/mavros/hil/state
HilStateQuaternion"| n26 + n27 -->|"/robot_1/interface/mavros/home_position/home
HomePosition"| n21 + n27 -->|"/robot_1/interface/mavros/home_position/home
HomePosition"| n70 + n70 -->|"/robot_1/interface/mavros/home_position/set
HomePosition"| n27 + n28 -->|"/robot_1/interface/mavros/imu/data
Imu"| d76(["/robot_1/interface/mavros/imu/data (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/data_raw
Imu"| d77(["/robot_1/interface/mavros/imu/data_raw (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/diff_pressure
FluidPressure"| d78(["/robot_1/interface/mavros/imu/diff_pressure (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/mag
MagneticField"| d79(["/robot_1/interface/mavros/imu/mag (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/static_pressure
FluidPressure"| d80(["/robot_1/interface/mavros/imu/static_pressure (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/temperature_baro
Temperature"| d81(["/robot_1/interface/mavros/imu/temperature_baro (no subscribers)"]) + n28 -->|"/robot_1/interface/mavros/imu/temperature_imu
Temperature"| d82(["/robot_1/interface/mavros/imu/temperature_imu (no subscribers)"]) + n29 -->|"/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"| n29 + n29 -->|"/robot_1/interface/mavros/landing_target/pose_in
PoseStamped"| d85(["/robot_1/interface/mavros/landing_target/pose_in (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/accel
AccelWithCovarianceStamped"| d86(["/robot_1/interface/mavros/local_position/accel (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/odom
Odometry"| n71 + n30 -->|"/robot_1/interface/mavros/local_position/pose
PoseStamped"| n54 + n30 -->|"/robot_1/interface/mavros/local_position/pose_cov
PoseWithCovarianceStamped"| d87(["/robot_1/interface/mavros/local_position/pose_cov (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/velocity_body
TwistStamped"| d88(["/robot_1/interface/mavros/local_position/velocity_body (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/velocity_body_cov
TwistWithCovarianceStamped"| d89(["/robot_1/interface/mavros/local_position/velocity_body_cov (no subscribers)"]) + n30 -->|"/robot_1/interface/mavros/local_position/velocity_local
TwistStamped"| d90(["/robot_1/interface/mavros/local_position/velocity_local (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/log_transfer/raw/log_data
LogData"| d91(["/robot_1/interface/mavros/log_transfer/raw/log_data (no subscribers)"]) + n31 -->|"/robot_1/interface/mavros/log_transfer/raw/log_entry
LogEntry"| d92(["/robot_1/interface/mavros/log_transfer/raw/log_entry (no subscribers)"]) + n32 -->|"/robot_1/interface/mavros/mag_calibration/report
MagnetometerReporter"| d93(["/robot_1/interface/mavros/mag_calibration/report (no subscribers)"]) + n32 -->|"/robot_1/interface/mavros/mag_calibration/status
UInt8"| d94(["/robot_1/interface/mavros/mag_calibration/status (no subscribers)"]) + n33 -->|"/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"| n33 + n37 -->|"/robot_1/interface/mavros/mission/reached
WaypointReached"| d97(["/robot_1/interface/mavros/mission/reached (no subscribers)"]) + n37 -->|"/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"| n38 + d100(["/robot_1/interface/mavros/mocap/tf (no publishers)"]) -->|"/robot_1/interface/mavros/mocap/tf
TransformStamped"| n38 + d101(["/robot_1/interface/mavros/mount_control/command (no publishers)"]) -->|"/robot_1/interface/mavros/mount_control/command
MountControl"| n39 + n39 -->|"/robot_1/interface/mavros/mount_control/orientation
Quaternion"| d102(["/robot_1/interface/mavros/mount_control/orientation (no subscribers)"]) + n39 -->|"/robot_1/interface/mavros/mount_control/status
Vector3Stamped"| d103(["/robot_1/interface/mavros/mount_control/status (no subscribers)"]) + n40 -->|"/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"| n41 + d106(["/robot_1/interface/mavros/obstacle_distance_3d/send (no publishers)"]) -->|"/robot_1/interface/mavros/obstacle_distance_3d/send
ObstacleDistance3D"| n42 + n43 -->|"/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"| n43 + d109(["/robot_1/interface/mavros/onboard_computer/status (no publishers)"]) -->|"/robot_1/interface/mavros/onboard_computer/status
OnboardComputerStatus"| n44 + d110(["/robot_1/interface/mavros/open_drone_id/basic_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/basic_id
OpenDroneIDBasicID"| n45 + d111(["/robot_1/interface/mavros/open_drone_id/operator_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/operator_id
OpenDroneIDOperatorID"| n45 + d112(["/robot_1/interface/mavros/open_drone_id/self_id (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/self_id
OpenDroneIDSelfID"| n45 + d113(["/robot_1/interface/mavros/open_drone_id/system (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/system
OpenDroneIDSystem"| n45 + d114(["/robot_1/interface/mavros/open_drone_id/system_update (no publishers)"]) -->|"/robot_1/interface/mavros/open_drone_id/system_update
OpenDroneIDSystemUpdate"| n45 + n46 -->|"/robot_1/interface/mavros/optical_flow/ground_distance
Range"| d115(["/robot_1/interface/mavros/optical_flow/ground_distance (no subscribers)"]) + n46 -->|"/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"| n46 + n47 -->|"/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"| n48 + n49 -->|"/robot_1/interface/mavros/px4flow/ground_distance
Range"| d120(["/robot_1/interface/mavros/px4flow/ground_distance (no subscribers)"]) + n49 -->|"/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"| n49 + n49 -->|"/robot_1/interface/mavros/px4flow/temperature
Temperature"| d123(["/robot_1/interface/mavros/px4flow/temperature (no subscribers)"]) + n60 -->|"/robot_1/interface/mavros/radio_status
RadioStatus"| d124(["/robot_1/interface/mavros/radio_status (no subscribers)"]) + n50 -->|"/robot_1/interface/mavros/rallypoint/rallypoints
WaypointList"| d125(["/robot_1/interface/mavros/rallypoint/rallypoints (no subscribers)"]) + n51 -->|"/robot_1/interface/mavros/rc/in
RCIn"| d126(["/robot_1/interface/mavros/rc/in (no subscribers)"]) + n51 -->|"/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"| n51 + d129(["/robot_1/interface/mavros/setpoint_accel/accel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_accel/accel
Vector3Stamped"| n52 + d130(["/robot_1/interface/mavros/setpoint_attitude/cmd_vel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_attitude/cmd_vel
TwistStamped"| n53 + d131(["/robot_1/interface/mavros/setpoint_attitude/thrust (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_attitude/thrust
Thrust"| n53 + n70 -->|"/robot_1/interface/mavros/setpoint_position/global
GeoPoseStamped"| n54 + d132(["/robot_1/interface/mavros/setpoint_position/global_to_local (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_position/global_to_local
GeoPoseStamped"| n54 + n70 -->|"/robot_1/interface/mavros/setpoint_position/local
PoseStamped"| n54 + n70 -->|"/robot_1/interface/mavros/setpoint_raw/attitude
AttitudeTarget"| n55 + d133(["/robot_1/interface/mavros/setpoint_raw/global (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_raw/global
GlobalPositionTarget"| n55 + n70 -->|"/robot_1/interface/mavros/setpoint_raw/local
PositionTarget"| n55 + n55 -->|"/robot_1/interface/mavros/setpoint_raw/target_attitude
AttitudeTarget"| d134(["/robot_1/interface/mavros/setpoint_raw/target_attitude (no subscribers)"]) + n55 -->|"/robot_1/interface/mavros/setpoint_raw/target_global
GlobalPositionTarget"| d135(["/robot_1/interface/mavros/setpoint_raw/target_global (no subscribers)"]) + n55 -->|"/robot_1/interface/mavros/setpoint_raw/target_local
PositionTarget"| d136(["/robot_1/interface/mavros/setpoint_raw/target_local (no subscribers)"]) + n56 -->|"/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"| n56 + d139(["/robot_1/interface/mavros/setpoint_velocity/cmd_vel (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_velocity/cmd_vel
TwistStamped"| n57 + d140(["/robot_1/interface/mavros/setpoint_velocity/cmd_vel_unstamped (no publishers)"]) -->|"/robot_1/interface/mavros/setpoint_velocity/cmd_vel_unstamped
Twist"| n57 + n58 -->|"/robot_1/interface/mavros/sim_state/acceleration
Vector3Stamped"| d141(["/robot_1/interface/mavros/sim_state/acceleration (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/sim_state/attitude
Imu"| d142(["/robot_1/interface/mavros/sim_state/attitude (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/sim_state/global_position
NavSatFix"| d143(["/robot_1/interface/mavros/sim_state/global_position (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/sim_state/velocity_body
TwistStamped"| d144(["/robot_1/interface/mavros/sim_state/velocity_body (no subscribers)"]) + n58 -->|"/robot_1/interface/mavros/sim_state/velocity_local
TwistStamped"| d145(["/robot_1/interface/mavros/sim_state/velocity_local (no subscribers)"]) + n59 -->|"/robot_1/interface/mavros/state
State"| n70 + n59 -->|"/robot_1/interface/mavros/status_event
StatusEvent"| d146(["/robot_1/interface/mavros/status_event (no subscribers)"]) + n59 -->|"/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"| n59 + n59 -->|"/robot_1/interface/mavros/sys_status
SysStatus"| d149(["/robot_1/interface/mavros/sys_status (no subscribers)"]) + n6 -->|"/robot_1/interface/mavros/target_actuator_control
ActuatorControl"| d150(["/robot_1/interface/mavros/target_actuator_control (no subscribers)"]) + n61 -->|"/robot_1/interface/mavros/terrain/report
TerrainReport"| d151(["/robot_1/interface/mavros/terrain/report (no subscribers)"]) + n62 -->|"/robot_1/interface/mavros/time_reference
TimeReference"| d152(["/robot_1/interface/mavros/time_reference (no subscribers)"]) + n62 -->|"/robot_1/interface/mavros/timesync_status
TimesyncStatus"| d153(["/robot_1/interface/mavros/timesync_status (no subscribers)"]) + n63 -->|"/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"| n63 + d156(["/robot_1/interface/mavros/trajectory/path (no publishers)"]) -->|"/robot_1/interface/mavros/trajectory/path
Path"| n63 + d157(["/robot_1/interface/mavros/tunnel/in (no publishers)"]) -->|"/robot_1/interface/mavros/tunnel/in
Tunnel"| n64 + n64 -->|"/robot_1/interface/mavros/tunnel/out
Tunnel"| d158(["/robot_1/interface/mavros/tunnel/out (no subscribers)"]) + n65 -->|"/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"| n66 + d161(["/robot_1/interface/mavros/vision_pose/pose_cov (no publishers)"]) -->|"/robot_1/interface/mavros/vision_pose/pose_cov
PoseWithCovarianceStamped"| n66 + d162(["/robot_1/interface/mavros/vision_speed/speed_twist (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_twist
TwistStamped"| n67 + d163(["/robot_1/interface/mavros/vision_speed/speed_twist_cov (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_twist_cov
TwistWithCovarianceStamped"| n67 + d164(["/robot_1/interface/mavros/vision_speed/speed_vector (no publishers)"]) -->|"/robot_1/interface/mavros/vision_speed/speed_vector
Vector3Stamped"| n67 + n68 -->|"/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"| n70 + d167(["/robot_1/interface/rate_thrust_command (no publishers)"]) -->|"/robot_1/interface/rate_thrust_command
RateThrust"| n70 + 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"| 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"| 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 + 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"| n80 + n71 -->|"/tf
TFMessage"| n69 + 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"| 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 +``` + + diff --git a/stacks/full_mighty/README.md b/stacks/full_mighty/README.md new file mode 100644 index 000000000..0893f5643 --- /dev/null +++ b/stacks/full_mighty/README.md @@ -0,0 +1,39 @@ +# full_mighty + +`full_default` with the local planner swapped: the DROAN GPU planner +(`droan_gl`) is replaced by the **MIGHTY** Hermite-spline planner from the +external **asm_mighty** module (MIT ACL, RA-L 2026), together with its +acl-mapping voxel world model (fed by the filtered Ouster cloud) and a +`mighty_bridge` adapter that serves the same `tasks/navigate` NavigateTask +action and publishes `trajectory_controller/trajectory_segment_to_add` +segments — so the rest of the stack (trajectory controller, PID, safety +monitor, takeoff/landing, GCS) is unchanged from `full_default`. + +This stack is the module-swap demonstration for the modular architecture: +the only difference vs `full_default` is one include in +`launch/stack.launch.xml` plus the `asm_mighty` pin in `modules.repos`. + +Bring-up: + +```bash +airstack module sync # pulls asm_mighty per this stack's modules.repos +airstack up --stack full_mighty --sim isaac +``` + +Notes: + +- MIGHTY is CPU-only (no GPU contention with Isaac). +- The planner needs the module's `nlohmann-json3-dev` dep layer: + `airstack module lock --build` before `airstack up`. +- Planner/world-model tuning lives in the module + (`mighty_bridge/config/*_airstack.yaml`). +- The [asm_mighty repo](https://github.com/castacks/asm_mighty) is **private + until the AirStack agent study concludes** (then public) — until the flip, + `airstack module sync` needs castacks-member credentials. Registry entry: + [modules/mighty.yaml](https://github.com/castacks/airstack-modules-index/blob/main/modules/mighty.yaml); + catalog page: [mighty](../../docs/modules/mighty.md). +- Validation at the pinned version (Isaac Sim, judged on ground truth): + 44/44 vendored gtests, empty-world NavigateTask route (goal error 0.14 m), + 7/7 pillar-field traversals, and 5/5 judged obstacle-route flights with + min clearances 1.59–1.65 m against a 1.0 m gate — the motivating DROAN + comparison (figures + numbers) is in the module README. diff --git a/stacks/full_mighty/docker-compose.yaml b/stacks/full_mighty/docker-compose.yaml new file mode 100644 index 000000000..ce3ebf780 --- /dev/null +++ b/stacks/full_mighty/docker-compose.yaml @@ -0,0 +1,11 @@ +# Per-stack image composition: 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`. +# +# full_mighty pins the asm_mighty module (see modules.repos); its apt dep +# (nlohmann-json3-dev) enters the robot image via the module's +# content-addressed dependency layer — `airstack module lock --build` +# GENERATES that override at lock time, so nothing is committed here. 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/full_mighty/launch/stack.launch.xml b/stacks/full_mighty/launch/stack.launch.xml new file mode 100644 index 000000000..b032f9562 --- /dev/null +++ b/stacks/full_mighty/launch/stack.launch.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/full_mighty/modules.repos b/stacks/full_mighty/modules.repos new file mode 100644 index 000000000..4bd6cdfe9 --- /dev/null +++ b/stacks/full_mighty/modules.repos @@ -0,0 +1,23 @@ +# modules.repos : module pins for the full_mighty stack. +# +# 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. +# +# full_mighty pulls the asm_mighty module (MIGHTY local planner + acl-mapping +# world model + NavigateTask bridge); everything else is trunk-resident. +# asm_mighty lives at castacks/asm_mighty (PRIVATE until the AirStack agent +# study concludes, then public — until the flip, syncing this stack needs +# castacks-member credentials). v0.1.1 is code-identical to the validated +# v0.1.0 (README-only delta). +airstack_compat: ">=0.20.0-alpha.16 <0.21.0" +repositories: + asm_mighty: + type: git + url: git@github.com:castacks/asm_mighty.git + version: v0.1.1 +x-local-modules: [] diff --git a/stacks/lite_default/README.md b/stacks/lite_default/README.md new file mode 100644 index 000000000..7a9538a0a --- /dev/null +++ b/stacks/lite_default/README.md @@ -0,0 +1,70 @@ +# `lite_default` — onboard-lite reference stack + +The compute-lite topology, unsplit, as a self-contained stack folder: +everything a small vehicle runs on its own compute, with the heavy global +layer left out entirely. It exists so compute-constrained vehicles can fly +task-driven missions without paying for global mapping and planning they +cannot host. + +## What it launches + +The single entry point `launch/stack.launch.xml` composes: + +- **Interface** (wrapped by design) plus **Sensors, Perception, Local, + Behavior — all flat**: module launch files with canonical defaults, the + same blocks as `full_default` (LiDAR filter, stereo_image_proc + topic + keepalive, takeoff/land task server, fixed-trajectory task server, GPU + DROAN planner, trajectory controller, PID controller, safety monitor). +- **Cross-domain extras**: the robot↔GCS DDS router (the shared + `autonomy_bringup/config/dds_router.yaml` allowlist) and the gossip + coordination layer. + +**Deliberately absent:** the global layer (vdb_mapping, random_walk) and the +logging layer. + +## 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`. + +## How to run + +```bash +airstack up --stack lite_default --sim isaac --robots 1 +airstack ready +``` + +Verify the topology with the wiring snapshot test: + +```bash +airstack test -m wiring --stack lite_default --sim isaacsim --num-robots 1 +``` + +The shared per-robot preamble (ROBOT_NAME namespace, `use_sim_time`, +`robot_state_publisher`, world→map static TF) 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. +- All layers are flattened in `launch/stack.launch.xml` (same layout as + `full_default`); the interface stays a wrapped include by design, so read + [wiring.md](wiring.md) for the observed MAVROS 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 + +This stack's observed wiring diagram is committed at [wiring.md](wiring.md); +CI drift-checks the running graph against it. Regenerate via +`airstack test -m wiring --stack lite_default`. diff --git a/stacks/lite_default/docker-compose.yaml b/stacks/lite_default/docker-compose.yaml new file mode 100644 index 000000000..b45951a35 --- /dev/null +++ b/stacks/lite_default/docker-compose.yaml @@ -0,0 +1,11 @@ +# Per-stack image composition 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..92cf778ad --- /dev/null +++ b/stacks/lite_default/launch/stack.launch.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/lite_default/modules.repos b/stacks/lite_default/modules.repos new file mode 100644 index 000000000..4ba67bb83 --- /dev/null +++ b/stacks/lite_default/modules.repos @@ -0,0 +1,15 @@ +# modules.repos : module pins for the lite_default reference stack. +# +# 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_default/wiring.md b/stacks/lite_default/wiring.md new file mode 100644 index 000000000..15023d4fb --- /dev/null +++ b/stacks/lite_default/wiring.md @@ -0,0 +1,466 @@ +# Wiring snapshot: lite_default + +- **generated-by**: tests/system/test_wiring_snapshot.py +- **date**: 2026-08-22 00:09:33 +- **sim**: isaacsim +- **num_robots**: 1 +- **source-sha**: 57e0b8c34ec9 + +```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 +``` + + diff --git a/stacks/lite_offload_global/README.md b/stacks/lite_offload_global/README.md new file mode 100644 index 000000000..e7dca9130 --- /dev/null +++ b/stacks/lite_offload_global/README.md @@ -0,0 +1,83 @@ +# `lite_offload_global` — split stack: lite vehicle, offboard global planning + +A **split stack**: a lite vehicle half plus an offboard global-planning half, +with an explicit [`bridge.yaml`](bridge.yaml) listing everything that crosses +the machine boundary. It exists so a compute-constrained vehicle can still +run exploration-style missions: the heavy global layer moves to a ground +host, while command authority stays onboard so link loss leaves the vehicle +able to failsafe. The DDS-router config the onboard half loads is generated +from `bridge.yaml`, never hand-maintained. + +## 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 same flat includes as `full_default`'s global layer) | +| `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 (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):** `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. + +## 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 can be declared instead of conventional: a fleet entry + with `stack: stacks/lite_offload_global` and `hosts: {offboard: gcs}` places + the offboard half on the named ground host (`airstack fleet generate` emits + its service with `AIRSTACK_STACK_ENTRY=offboard`; the robot gets `onboard`). + See `config/fleets/sim_three_mixed.yaml` (robot_3) and + [docs/development/fleets.md](../../docs/development/fleets.md). The manual + `--stack lite_offload_global:onboard|:offboard` form above remains for + single-machine runs. +- `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..f7de88553 --- /dev/null +++ b/stacks/lite_offload_global/bridge.yaml @@ -0,0 +1,122 @@ +# bridge.yaml — the machine boundary of the lite_offload_global split stack +# (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: 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 extended). Both legacy +# role folders are REMOVED — the base allowlist now lives at +# autonomy_bringup/config/dds_router.yaml, and THIS file (via +# tools/gen_dds_router.py) fully replaces the legacy split's router config. +# 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 the deleted legacy 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..896ed212f --- /dev/null +++ b/stacks/lite_offload_global/docker-compose.yaml @@ -0,0 +1,12 @@ +# Per-stack image composition 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 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..d96424b17 --- /dev/null +++ b/stacks/lite_offload_global/launch/offboard.launch.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + 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..25b582dee --- /dev/null +++ b/stacks/lite_offload_global/launch/onboard.launch.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/lite_offload_global/modules.repos b/stacks/lite_offload_global/modules.repos new file mode 100644 index 000000000..78e3d1da7 --- /dev/null +++ b/stacks/lite_offload_global/modules.repos @@ -0,0 +1,15 @@ +# modules.repos : module pins for the lite_offload_global split stack. +# +# 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/README.md b/tests/README.md index 306567a1a..71caf65b9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,6 +5,7 @@ AirStack's **pytest** tree under `tests/` has these roles: 1. **`tests/system/`** — Docker stack tests (sim + robot + GCS): liveliness, sensor Hz, takeoff/hover/land, image/workspace builds. 2. **Unit tests** — Fast hermetic tests (`unit` mark) whose **source is co-located** with each ROS 2 package at `/test/`. [`colcon_unit_test_packages.yaml`](colcon_unit_test_packages.yaml) lists which packages have unit tests, and `pytest tests/` collects them from there. 3. **`tests/integration/`** — Cross-component tests (`integration` mark) that wire the robot container to a host-side component, without a sim or GPU. +4. **`tests/meta/`** — Contract tests (`unit` mark) that pin the modular-AirStack CLI/docs/stack contracts (see [`tests/meta/README.md`](meta/README.md)). Pytest hooks and the shared fixtures live in `tests/conftest.py`; reusable helpers are split by concern into the [`tests/harness/`](harness/) package (re-exported through `conftest`). Use `airstack test -m unit -v` for hermetic tests only, or the marks below for the full stack. @@ -20,6 +21,7 @@ Pytest hooks and the shared fixtures live in `tests/conftest.py`; reusable helpe |--------|------|---------------|-------------------| | [`system/test_build_docker.py`](system/test_build_docker.py) | `build_docker` | Docker image builds (robot-desktop, gcs, isaac-sim, ms-airsim); records image sizes | Docker daemon | | [`system/test_build_packages.py`](system/test_build_packages.py) | `build_packages` | `colcon build` inside each container (robot, GCS, ms-airsim ROS workspace) | Docker daemon | +| [`system/test_wiring_snapshot.py`](system/test_wiring_snapshot.py) | `wiring` | Observed wiring snapshot of the running ROS graph (via [`wiring_snapshot.py`](wiring_snapshot.py)), drift-checked against the stack's committed `stacks//wiring.md`; writes the observed snapshot to `/wiring/` | Docker daemon, GPU, sim license | | [`system/test_liveliness.py`](system/test_liveliness.py) | `liveliness` | Stack bring-up: container Running state, ``/clock`` readiness, tmux panes, sentinel ROS 2 nodes, compute snapshot, infra-only ``test_stable`` (tmux + nodes + compute) | Docker daemon, GPU, sim license | | [`system/test_sensors.py`](system/test_sensors.py) | `sensors` | After liveliness in collection order: sim + robot stereo/depth Hz (**Isaac:** batched ``ros2 topic hz`` to avoid bridge overload; **ms-airsim:** single batch), filtered LiDAR via ``echo --once`` + cloud sanity (isaacsim), sim RTF, ``test_sensor_streams_stable`` | Docker daemon, GPU, sim license | | [`system/test_takeoff_hover_land.py`](system/test_takeoff_hover_land.py) | `takeoff_hover_land` | End-to-end flight: PX4 readiness gate, takeoff to 10 m, hover stability, land — one chain per (sim, num_robots, iteration, velocity) | Docker daemon, GPU, sim license | @@ -47,6 +49,28 @@ tests the numpy-only range validation rules in See [Unit Testing Guide](../docs/development/intermediate/testing/unit_testing.md) and the `add-unit-tests` agent skill for full details. +### Meta / contract tests (`tests/meta/`) + +Fast contract tests (`unit` mark, no Docker) that pin the modular-AirStack CLI, +docs, and stack contracts so refactors cannot silently break them — see +[`tests/meta/README.md`](meta/README.md). One line each: + +| File | Pins | +|------|------| +| [`meta/test_bridge_contract.py`](meta/test_bridge_contract.py) | Split-stack `bridge.yaml` → generated DDS-router config (`tools/gen_dds_router.py`), incl. the no-control-setpoint bridge hard gate | +| [`meta/test_collection_contract.py`](meta/test_collection_contract.py) | Pytest collection rules: co-located unit-test injection, path narrowing, rejection of repo-root collection | +| [`meta/test_docker_layer_plan_contract.py`](meta/test_docker_layer_plan_contract.py) | Module Docker layer composition plan (`modules.lock` → layered image build) | +| [`meta/test_docs_catalog_contract.py`](meta/test_docs_catalog_contract.py) | Generated `docs/modules/` catalog: determinism, drift `--check`, nav entries exist, deploy workflows fetch module docs | +| [`meta/test_doctor_contract.py`](meta/test_doctor_contract.py) | `airstack doctor` checks: observe-and-report semantics and the two hard gates | +| [`meta/test_fleet_contract.py`](meta/test_fleet_contract.py) | Fleet file schema/validation, per-robot compose generation, fleet resolver | +| [`meta/test_launch_intent_contract.py`](meta/test_launch_intent_contract.py) | `airstack up` intent flags → derived env-var sets (profiles, URDF, sim script) | +| [`meta/test_launch_single_locus.py`](meta/test_launch_single_locus.py) | Repo-wide launch lint: wiring lives in exactly one locus (stack entry files), allowlist in [`meta/launch_lint_allowlist.txt`](meta/launch_lint_allowlist.txt) | +| [`meta/test_metrics_reporting_contract.py`](meta/test_metrics_reporting_contract.py) | `parse_metrics.py` campaign comparability and regression semantics | +| [`meta/test_module_manifest_contract.py`](meta/test_module_manifest_contract.py) | `module.yaml` manifest schema + validator behavior | +| [`meta/test_module_overlay_contract.py`](meta/test_module_overlay_contract.py) | Module workspace overlay (sync/clone/overlay/remove artifacts) | +| [`meta/test_stack_layout_contract.py`](meta/test_stack_layout_contract.py) | Stack folder anatomy: entry launch files, `modules.repos`, no dispatcher inside entries | +| [`meta/test_wiring_snapshot_contract.py`](meta/test_wiring_snapshot_contract.py) | The wiring-snapshot tool itself: snapshot format, normalization, drift detection | + ### Integration tests (`tests/integration/`) Cross-component tests (`integration` mark) that wire a few real components together — the @@ -75,6 +99,8 @@ Marks can be combined with pytest logic: | [`harness/commands.py`](harness/commands.py) | Subprocess / `docker exec` / `ros2` helpers with per-test output capture (`airstack_cmd`, `docker_exec`, `ros2_exec`, `read_log_tail`) | | [`harness/containers.py`](harness/containers.py) | Container discovery, compute-usage sampling, image checks (`find_container`, `wait_for_container`, `sample_compute_usage`, `missing_images`) | | [`harness/metrics.py`](harness/metrics.py) | `MetricsRecorder`, `get_metrics`, `current_test_id` (writes `metrics.json`) | +| [`harness/run_meta.py`](harness/run_meta.py) | `run_meta.json` outcome metadata: pytest exit status, campaign fingerprint, completed-vs-infrastructure outcomes | +| [`harness/test_ids.py`](harness/test_ids.py) | Test-id parsing/formatting shared by metrics recording and reporting | | [`harness/sim.py`](harness/sim.py) | `SIM_CONFIG` sim targets + ros2 topic sampling (`sample_hz`, `parallel_sample_hz`, `wait_for_first_message`) | | [`harness/collection.py`](harness/collection.py) | Cross-module test ordering + parametrize-id rewrite (`modify_items`) | @@ -116,23 +142,27 @@ Writes custom metrics to `tests/results//metrics.json` after each `re ### Output files -Every test run produces a timestamped directory containing only `summary.txt`, -`results.xml`, `run_meta.json`, and `metrics.json` — there is **no** `logs/` subdirectory and no -per-test log files are written under the run directory. +Every test run produces a timestamped directory containing `summary.txt`, +`results.xml`, `run_meta.json`, and `metrics.json` (plus a `wiring/` subdirectory +when the `wiring` mark runs, and a bounded `diagnostics/` JSON bundle on +simulator/startup failures). There is **no** `logs/` subdirectory and no +per-test log files are written under the run directory. Full unbounded logs +are never copied into the artifact. ``` tests/results/ └── 2025-04-21_14-30-00/ ├── summary.txt # Human-readable key metrics — open this first ├── results.xml # JUnit XML — test durations and pass/fail status - ├── run_meta.json # Completion/outcome and campaign fingerprint - └── metrics.json # Custom metrics (image sizes, Hz, compute, timing) + ├── run_meta.json # Schema-v2 completion/failure class + exact campaign + ├── metrics.json # Custom metrics (image sizes, Hz, compute, timing) + ├── diagnostics/ # On failure: config, panes, log tails, ROS/GPU/commands + └── wiring/ # (wiring mark only) observed_.md graph snapshots ``` -Live test output goes to the terminal (pytest `log_cli`). On failure, assertion -messages include the tail of the last subprocess output (the in-memory -`read_log_tail` of the relevant `docker` / `ros2` subprocess) — no per-test log -files are written under the run directory. +Live test output goes to the terminal (pytest `log_cli`). Diagnostics are +bounded (container log tails and a 30-command ring) and exclude secret-bearing +environment variables. --- @@ -224,11 +254,13 @@ pytest tests/ -m sensors \ |--------|---------|-------------| | `--sim` | `isaacsim` | Comma-separated sim targets (`msairsim` opt-in) | | `--num-robots` | `1,3` | Comma-separated robot counts | -| `--stress-iterations` | `3` | Up/down cycles per (sim, num_robots) config | +| `--stack` | _(none)_ | Stack folder under `stacks/` to launch (sets `AIRSTACK_STACK_DIR`); default dispatch is `stacks/full_default`. The `wiring` mark drift-checks against `stacks//wiring.md` | +| `--fleet` | _(none)_ | Fleet preset under `config/fleets/` (sets `FLEET_CONFIG_FILE`); derives `NUM_ROBOTS` from the fleet's robot count, overriding `--num-robots` | +| `--stress-iterations` | `1` | Up/down cycles per (sim, num_robots) config | | `--stable-duration` | `120` | Seconds ``test_stable`` / ``test_sensor_streams_stable`` poll for | | `--stable-interval` | `10` | Seconds between polls in those stability tests | | `--gui` | off | Show simulator GUI (disables headless mode) | -| `--takeoff-velocities` | `0.5,1,2` | Takeoff/land speeds in m/s | +| `--takeoff-velocities` | `0.5` | Takeoff/land speeds in m/s (e.g. `0.5,1,2` to sweep) | --- @@ -336,7 +368,7 @@ runs so the drone returns to the ground before the next trajectory type starts. | `land_duration_sim_s` | s | Sim-time from 80 % peak descent to < 0.5 m | -Metrics reported in one .txt file called summary.txt which automatically populates once your run completes +Metrics are also summarized in the run's `summary.txt`, written when the run completes. ### Default trajectory parameters @@ -358,7 +390,7 @@ airstack test -m autonomy \ --trajectory-types Circle,Figure8,Racetrack,Line \ -v -# Circle only (quick check of the known failure case) +# Circle only (quick single-pattern run) airstack test -m autonomy \ --sim msairsim \ --num-robots 1 \ @@ -506,17 +538,19 @@ python tests/parse_metrics.py \ Prints a markdown table of all recorded metrics. Always exits 0. -### Diff / regression check +### Advisory comparison ```bash python tests/parse_metrics.py \ --current tests/results/2025-04-21_14-30-00/ \ --baseline tests/results/2025-04-20_09-00-00/ \ - --threshold 20 # optional: regression if change% exceeds this (default 20) + --threshold 20 # optional: highlight if change% exceeds this (default 20) --output report.md # optional: also write to file ``` -Prints a side-by-side comparison. Exits **1** if any metric regresses beyond the threshold; exits 0 otherwise. +Prints a side-by-side comparison. Numeric deltas are advisory and always exit +0. Report parsing/integrity failures exit 2 and block CI; pytest assertions and +infrastructure failures are enforced by the test job. For a completed test campaign, the report has three sections per test module: @@ -528,7 +562,10 @@ Regressions are flagged with :red_circle:, improvements with :green_circle:. Collection errors, command/internal errors, zero-test runs, and jobs that stop before pytest finalizes are labeled **not comparable**. Their pass-rate and regression tables are suppressed so an infrastructure failure cannot appear as 0% policy performance. -`run_meta.json` records the pytest exit status and simulation tests selected/completed. +`run_meta.json` records normalized selected IDs, behavior-changing CLI options, +completion state, and failure class. Its fingerprint includes both tests and +configuration, preventing unlike robot counts, trajectories, tolerances, or +stress settings from being compared. Per-robot metric keys remain visible. --- @@ -561,6 +598,8 @@ opened, updated, or reopened against `main` or `develop`. | `num_robots` | `1` | Robot counts | | `stress_iterations` | `1` | Iterations per config | | `stable_duration` | `120` | Stability polling seconds | +| `trajectory_types` | `Circle,Figure8,Racetrack,Line` | Fixed-trajectory sweep; set `Circle` for a minimal campaign | +| `takeoff_velocities` | `0.5` | Takeoff velocity sweep | | `baseline_run_id` | _(blank)_ | Run ID for comparison; blank = latest `main` run | #### Jobs @@ -570,20 +609,16 @@ opened, updated, or reopened against `main` or `develop`. **`report`** runs on `ubuntu-latest` after `run-tests` (even if it failed). It: 1. Downloads the current artifact -2. Downloads a baseline artifact (from the base branch for PRs, from `main` for manual runs, or from the specified `baseline_run_id`) -3. Runs `parse_metrics.py` in diff mode only when both artifacts have the same complete simulation campaign fingerprint; otherwise reports the current run without comparison +2. Downloads baseline candidates (from the base branch for PRs, from `main` for manual runs, or from the specified `baseline_run_id`) +3. Selects the newest completed candidate with the exact same test/configuration fingerprint; otherwise reports the current run without comparison 4. Posts the markdown report as a PR comment (PR runs) or to the job summary (all runs) -5. Fails with `::error::` only for a comparable metric regression; invalid/incomplete campaigns are reported as infrastructure outcomes - -#### Required third-party action - -The workflow uses [`dawidd6/action-download-artifact@v6`](https://github.com/dawidd6/action-download-artifact) to download artifacts from other workflow runs by branch name. This is a community action and must be trusted in your repository's Actions settings if you use a restricted allowed-actions policy. +5. Fails only if report generation/integrity fails. Comparable metric deltas are advisory; assertions and infrastructure failures remain blocking in `run-tests` --- ## CI/CD Orchestrator (OSMO-backed ephemeral runners) -AirStack's tests require a GPU, Docker, and a clean filesystem per run, so they execute on **truly ephemeral [NVIDIA OSMO](https://nvidia.github.io/OSMO/) pods** submitted per-job by an orchestrator. Each test job gets a fresh GPU pod that is destroyed once the job completes — no Docker layer carryover, no leaked containers, no shared host state. (This replaced an OpenStack-Nova backend; the GitHub side and the per-job-destroy model are unchanged.) +AirStack's tests require a GPU, Docker, and a clean filesystem per run, so they execute on **truly ephemeral [NVIDIA OSMO](https://nvidia.github.io/OSMO/) pods** submitted per-job by an orchestrator. Each test job gets a fresh GPU pod that is destroyed once the job completes — no Docker layer carryover, no leaked containers, no shared host state. ### Architecture diff --git a/tests/colcon_unit_test_packages.yaml b/tests/colcon_unit_test_packages.yaml index eb605498f..78f761039 100644 --- a/tests/colcon_unit_test_packages.yaml +++ b/tests/colcon_unit_test_packages.yaml @@ -9,7 +9,6 @@ robot: packages: - - natnet_ros2 - lidar_point_cloud_filter # Linter skip lives in lidar_point_cloud_filter setup.cfg + test/conftest.py. # ament pytest does not honor PYTEST_ADDOPTS -m. @@ -18,6 +17,7 @@ robot: # Simulation-side extensions (globbed under simulation/**//test). Collected by # `pytest tests/` on the host runner; not part of the robot colcon workspace. +# Currently empty: optitrack.natnet.emulator moved to the asm_optitrack module +# (its tests run in the module repo's CI). sim: - packages: - - optitrack.natnet.emulator + packages: [] diff --git a/tests/conftest.py b/tests/conftest.py index 38aec5c5f..3a9e92498 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,10 +23,27 @@ def pytest_addoption(parser): parser.addoption("--sim", default="isaacsim", - help="Comma-separated sim targets: isaacsim, msairsim. " - "Default isaacsim; pass --sim msairsim to opt in.") + help="Comma-separated sim targets: isaacsim, msairsim, " + "simplesim. Default isaacsim; pass --sim msairsim " + "to opt in. simplesim only drives the simple_sim " + "smoke test (-m simple_sim).") parser.addoption("--num-robots", default="1,3", help="Comma-separated robot counts, e.g. 1,3") + parser.addoption("--stack", default=None, + help="Stack to launch as [:], same syntax " + "as `airstack up --stack` (sets AIRSTACK_STACK_DIR/" + "AIRSTACK_STACK_ENTRY). Split stacks need the entry " + "(e.g. lite_offload_global:onboard). Default: None " + "= the default dispatch, stacks/full_default. The " + "wiring test drift-checks against " + "stacks//wiring.md.") + parser.addoption("--fleet", default=None, + help="Fleet preset under config/fleets/ (RFC #380 §2), " + "e.g. sim_three_mixed. Sets FLEET_CONFIG_FILE for " + "airstack up and derives NUM_ROBOTS from the " + "fleet's robot count (overriding --num-robots). " + "Default: None (legacy --num-robots behavior, " + "unchanged).") parser.addoption("--stress-iterations", type=int, default=1, help="Number of up/down iterations per (sim, num_robots) config") parser.addoption("--stable-duration", type=int, default=120, @@ -38,7 +55,7 @@ def pytest_addoption(parser): "Default: headless (no X, good for CI).") parser.addoption("--takeoff-velocities", default="0.5", help="Comma-separated takeoff/land velocities (m/s) to " - "sweep in test_takeoff_hover_land. Default: 0.5,1,2") + "sweep in test_takeoff_hover_land. Default: 0.5") parser.addoption("--trajectory-types", default="Circle,Figure8,Racetrack,Line", help="Comma-separated fixed trajectory types to sweep in " "test_fixed_trajectory. Default: Circle,Figure8,Racetrack,Line") @@ -78,7 +95,7 @@ def pytest_configure(config): run_dir = harness_session.init_run_dir(AIRSTACK_ROOT) config.option.xmlpath = str(run_dir / "results.xml") - # Co-located unit tests import their own package (e.g. `optitrack.natnet.emulator`, + # Co-located unit tests import their own package (e.g. # `lidar_point_cloud_filter.validation_core`). Put each package/extension import # root (the parent of its test/ dir) on sys.path so they resolve without a # per-package conftest.py — a second conftest.py collides with this root one as @@ -137,12 +154,26 @@ def pytest_sessionfinish(session, exitstatus): for entries in getattr(terminal, "stats", {}).values() for report in entries ] + campaign_config = {} + for key in ( + "sim", "num_robots", "stress_iterations", "stable_duration", + "stable_interval", "gui", "takeoff_velocities", + "trajectory_types", "waypoints", "waypoint_tolerance", + "goal_tolerance", "waypoint_timeout", + ): + try: + campaign_config[key] = session.config.getoption( + f"--{key.replace('_', '-')}" + ) + except (ValueError, AttributeError): + continue meta_path = write_run_meta( run_dir, session.items, exitstatus, session.config.option.markexpr, reports, + campaign_config, ) logger.info("Wrote run metadata to %s", meta_path) except Exception as exc: @@ -157,9 +188,20 @@ def pytest_sessionfinish(session, exitstatus): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): - """Attach phase reports to the item so fixtures can inspect pass/fail.""" + """Attach phase reports and preserve the assertion/infrastructure boundary.""" outcome = yield rep = outcome.get_result() + if rep.failed: + text = str(rep.longrepr).lower() + is_infrastructure = bool( + item.get_closest_marker("infrastructure") + or rep.when in ("setup", "teardown") + or "infrastructure prerequisite" in text + or "infrastructure simulator process failure" in text + ) + rep.airstack_failure_class = ( + "infrastructure" if is_infrastructure else "assertion" + ) setattr(item, f"_rep_{rep.when}", rep) @@ -179,6 +221,17 @@ def pytest_generate_tests(metafunc): return sims = [s.strip() for s in metafunc.config.getoption("--sim").split(",") if s.strip()] nums = [int(x) for x in metafunc.config.getoption("--num-robots").split(",") if x.strip()] + fleet = metafunc.config.getoption("--fleet") + if fleet: + # A fleet defines its own robot roster: campaigns run at exactly the + # fleet's robot count — the --num-robots matrix would otherwise spawn + # campaigns expecting robots the fleet never declares. + import os as _os + import yaml as _yaml + fleet_path = _os.path.join(AIRSTACK_ROOT, "config", "fleets", f"{fleet}.yaml") + with open(fleet_path, encoding="utf-8") as fh: + fleet_doc = _yaml.safe_load(fh) or {} + nums = [len(fleet_doc.get("robots") or {})] iterations = metafunc.config.getoption("--stress-iterations") params = [(s, n, i) for s in sims for n in nums for i in range(iterations)] ids = [f"{s}-{n}-iter{i}" for s, n, i in params] @@ -211,7 +264,9 @@ def airstack_env(request): env_overrides = { "AUTOLAUNCH": "true", "NUM_ROBOTS": str(num_robots), - "COMPOSE_PROFILES": f"desktop,{cfg['profile']}", + # simplesim overrides this: its simple-robot service replaces + # robot-desktop, so the desktop profile must stay off (see SIM_CONFIG). + "COMPOSE_PROFILES": cfg.get("compose_profiles", f"desktop,{cfg['profile']}"), "MS_AIRSIM_HEADLESS": "true" if headless else "false", "ISAAC_SIM_HEADLESS": "true" if headless else "false", } @@ -220,6 +275,40 @@ def airstack_env(request): env_overrides["QT_QPA_PLATFORM"] = "offscreen" env_overrides.update(cfg.get("extra_env", {})) + # Stack dispatch (RFC #379 §3): route robot.launch.xml to the stack's + # entry launch file (unset = the full_default default). Container path — + # stacks/ is bind-mounted at /root/AirStack/stacks. + # Accepts the same [:] syntax as `airstack up --stack` — + # split stacks (e.g. lite_offload_global:onboard) have no stack.launch.xml, + # only per-half entries, so the bare name would dispatch to a nonexistent + # entry and strand the launch after the dispatcher preamble. + stack = request.config.getoption("--stack") + if stack: + stack_name, _, stack_entry = stack.partition(":") + env_overrides["AIRSTACK_STACK_DIR"] = f"/root/AirStack/stacks/{stack_name}" + env_overrides["AIRSTACK_STACK_ENTRY"] = stack_entry or "stack" + + # Fleet dispatch (RFC #380 §2): FLEET_CONFIG_FILE (container path) opts + # the run into fleet resolution; NUM_ROBOTS is derived from the fleet's + # robot count (overriding this parametrization's num_robots). `airstack + # up` sees the env var, validates the fleet, and auto-includes the + # generated per-robot compose when the fleet is heterogeneous. Isaac runs + # pin the generic fleet spawner explicitly (parametrized sim scripts in + # extra_env would otherwise shadow it). --fleet absent = byte-identical + # legacy behavior. + fleet = request.config.getoption("--fleet") + if fleet: + import yaml as _yaml + fleet_path = Path(AIRSTACK_ROOT) / "config" / "fleets" / f"{fleet}.yaml" + assert fleet_path.is_file(), f"--fleet {fleet}: no such file {fleet_path}" + with fleet_path.open(encoding="utf-8") as f: + fleet_robots = len((_yaml.safe_load(f) or {}).get("robots") or {}) + env_overrides["FLEET_CONFIG_FILE"] = f"/root/AirStack/config/fleets/{fleet}.yaml" + env_overrides["NUM_ROBOTS"] = str(fleet_robots) + num_robots = fleet_robots + if sim == "isaacsim": + env_overrides["ISAAC_SIM_SCRIPT_NAME"] = "fleet_spawn.py" + with logger_to(log): missing = missing_images(env=env_overrides) if missing: @@ -227,7 +316,7 @@ def airstack_env(request): "Required docker images not built locally:\n - " + "\n - ".join(missing) + "\nBuild them first, e.g. `airstack test -m build_docker` " - "or `airstack image-build `." + "or `airstack images build `." ) logger.info("Shutting down any previously running stack") airstack_cmd("down", timeout=120, log_name=log) @@ -239,17 +328,34 @@ def airstack_env(request): up_cmd_duration_s = round(time.time() - t0, 2) logger.info("airstack up returned %d in %.2fs", up_result.returncode, up_cmd_duration_s) - assert up_result.returncode == 0, \ - f"airstack up failed:\n{read_log_tail(log)}" + if up_result.returncode != 0: + diagnostics = collect_failure_diagnostics( + env_overrides, + f"airstack up failed with status {up_result.returncode}", + harness_session.current_item().nodeid, + ) + pytest.fail( + f"airstack up failed:\n{read_log_tail(log)}\n" + f"diagnostics: {diagnostics}" + ) env = { "sim": sim, "num_robots": num_robots, "iteration": iteration, "sim_container": cfg["sim_container"], - "robot_pattern": "robot.*desktop", + "robot_pattern": cfg.get("robot_pattern", "robot.*desktop"), "up_started_at": t0, "cfg": cfg, + # None = the default dispatch (stacks/full_default); else the + # stacks/ explicitly launched (entry suffix stripped — goldens + # and doctor lookups key on the stack folder, not the entry). + "stack": stack.partition(":")[0] if stack else None, + # None = the folder's default entry (stack.launch.xml); else the + # split-stack half explicitly launched (e.g. "onboard"). + "stack_entry": (stack.partition(":")[2] or None) if stack else None, + # None = legacy NUM_ROBOTS behavior; else the config/fleets/ flown. + "fleet": fleet, } tid = current_test_id() diff --git a/tests/fixtures/modules/heavy_module/Dockerfile.module b/tests/fixtures/modules/heavy_module/Dockerfile.module new file mode 100644 index 000000000..14036658d --- /dev/null +++ b/tests/fixtures/modules/heavy_module/Dockerfile.module @@ -0,0 +1,6 @@ +# Tier-2 Dockerfile fragment fixture (RFC #379 §6): always written against +# ARG BASE_IMAGE — never a fixed base — so it composes onto whatever the +# previous link in the module layer chain produced. +ARG BASE_IMAGE +FROM ${BASE_IMAGE} +RUN echo heavy-module-layer > /heavy_module_marker diff --git a/tests/fixtures/modules/heavy_module/module.yaml b/tests/fixtures/modules/heavy_module/module.yaml new file mode 100644 index 000000000..c23b31f98 --- /dev/null +++ b/tests/fixtures/modules/heavy_module/module.yaml @@ -0,0 +1,17 @@ +# Docker-relevant module fixture for tests/meta/test_docker_layer_plan_contract.py: +# exercises dep tier 1 (apt + pip lists) and tier 2 (Dockerfile.module) of the +# layer composition chain (RFC #379 §6). Schema: common/module_schema/module.schema.json. +name: heavy_module +description: Docker-layer fixture with apt/pip deps and a Dockerfile fragment. +maintainer: test@example.com +license: BSD-3-Clause-Clear +type: ros_package +airstack_compat: ">=0.19.0 <0.21.0" +targets: [robot] + +deps: {apt: [cowsay], pip: [tabulate]} +dockerfile: Dockerfile.module # tier 2: built with BASE_IMAGE = previous chain link + +tests: + packages: [] # no colcon packages — docker-layer fixture only + marks: [] diff --git a/tests/fixtures/modules/hello_module/hello_module/hello_module/__init__.py b/tests/fixtures/modules/hello_module/hello_module/hello_module/__init__.py new file mode 100644 index 000000000..a300dc53f --- /dev/null +++ b/tests/fixtures/modules/hello_module/hello_module/hello_module/__init__.py @@ -0,0 +1,3 @@ +"""hello_module — minimal fixture package for module manifest contract tests.""" + +__version__ = "0.1.0" diff --git a/tests/fixtures/modules/hello_module/hello_module/hello_module/hello_node.py b/tests/fixtures/modules/hello_module/hello_module/hello_module/hello_node.py new file mode 100644 index 000000000..f011988b1 --- /dev/null +++ b/tests/fixtures/modules/hello_module/hello_module/hello_module/hello_node.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Tiny rclpy node: logs one greeting, then exits.""" + +import rclpy +from rclpy.node import Node + + +class HelloNode(Node): + def __init__(self): + super().__init__("hello_node") + self.get_logger().info("hello_module says hello") + + +def main(args=None): + rclpy.init(args=args) + node = HelloNode() + node.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/modules/hello_module/hello_module/package.xml b/tests/fixtures/modules/hello_module/hello_module/package.xml new file mode 100644 index 000000000..396b0c192 --- /dev/null +++ b/tests/fixtures/modules/hello_module/hello_module/package.xml @@ -0,0 +1,17 @@ + + + + hello_module + 0.1.0 + Minimal hello-world fixture package for module manifest contract tests. + AirStack Test Fixture + BSD-3-Clause-Clear + + rclpy + + python3-pytest + + + ament_python + + diff --git a/common/ros_packages/gui/rqt/rqt_behavior_tree/resource/rqt_behavior_tree b/tests/fixtures/modules/hello_module/hello_module/resource/hello_module similarity index 100% rename from common/ros_packages/gui/rqt/rqt_behavior_tree/resource/rqt_behavior_tree rename to tests/fixtures/modules/hello_module/hello_module/resource/hello_module diff --git a/tests/fixtures/modules/hello_module/hello_module/setup.cfg b/tests/fixtures/modules/hello_module/hello_module/setup.cfg new file mode 100644 index 000000000..a52c75a12 --- /dev/null +++ b/tests/fixtures/modules/hello_module/hello_module/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/hello_module +[install] +install_scripts=$base/lib/hello_module diff --git a/tests/fixtures/modules/hello_module/hello_module/setup.py b/tests/fixtures/modules/hello_module/hello_module/setup.py new file mode 100644 index 000000000..d62aaec7b --- /dev/null +++ b/tests/fixtures/modules/hello_module/hello_module/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup + +package_name = "hello_module" + +setup( + name=package_name, + version="0.1.0", + packages=[package_name], + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + ], + install_requires=["setuptools"], + zip_safe=True, + maintainer="AirStack Test Fixture", + maintainer_email="test@example.com", + description="Minimal hello-world fixture package for module manifest contract tests.", + license="BSD-3-Clause-Clear", + extras_require={"test": ["pytest"]}, + entry_points={ + "console_scripts": [ + "hello_node = hello_module.hello_node:main", + ], + }, +) diff --git a/tests/fixtures/modules/hello_module/hello_module/test/test_import.py b/tests/fixtures/modules/hello_module/hello_module/test/test_import.py new file mode 100644 index 000000000..2412484c4 --- /dev/null +++ b/tests/fixtures/modules/hello_module/hello_module/test/test_import.py @@ -0,0 +1,32 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Import smoke tests for the hello_module fixture package. + +This fixture lives under ``tests/``, so broad pytest recursion collects it directly +(unlike real co-located colcon unit tests, which are injected via +``colcon_unit_test_packages.yaml``). It is marked ``unit`` by hand and made +self-sufficient: the package import root goes on ``sys.path`` here. +""" +import sys +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.unit + +_PKG_ROOT = Path(__file__).resolve().parents[1] +if str(_PKG_ROOT) not in sys.path: + sys.path.insert(0, str(_PKG_ROOT)) + + +def test_package_imports(): + import hello_module + + assert hello_module.__version__ + + +def test_node_module_imports_when_rclpy_available(): + pytest.importorskip("rclpy") + from hello_module import hello_node + + assert callable(hello_node.main) diff --git a/tests/fixtures/modules/hello_module/module.yaml b/tests/fixtures/modules/hello_module/module.yaml new file mode 100644 index 000000000..45f8af10b --- /dev/null +++ b/tests/fixtures/modules/hello_module/module.yaml @@ -0,0 +1,13 @@ +# Minimal valid module manifest — fixture for tests/meta/test_module_manifest_contract.py. +# Schema: common/module_schema/module.schema.json (RFC #379 §2). +name: hello_module +description: Minimal hello-world module fixture for manifest-contract tests. +maintainer: test@example.com +license: BSD-3-Clause-Clear +type: ros_package +airstack_compat: ">=0.19.0 <0.21.0" +targets: [robot] + +tests: + packages: [hello_module] + marks: [] diff --git a/tests/harness/__init__.py b/tests/harness/__init__.py index 7b1210a7c..c9a209a62 100644 --- a/tests/harness/__init__.py +++ b/tests/harness/__init__.py @@ -34,10 +34,12 @@ unit_test_dirs, unit_test_files, ) +from harness.diagnostics import collect_failure_diagnostics from harness.metrics import MetricsRecorder, current_test_id, get_metrics from harness.session import logger from harness.sim import ( SIM_CONFIG, + SimulatorHealthError, parallel_echo_once_robot_topics, parallel_sample_hz, sample_hz, @@ -50,7 +52,7 @@ "colcon_test_robot_command", "collection_is_broad", "format_pytest_addopts", "load_colcon_unit_test_config", "unit_test_dirs", "unit_test_files", # session - "logger", + "logger", "collect_failure_diagnostics", # commands "ROS_DISTRO_SETUP", "airstack_cmd", "current_log", "docker_exec", "read_log_tail", "ros2_env", "ros2_exec", @@ -61,6 +63,6 @@ # metrics "MetricsRecorder", "get_metrics", "current_test_id", # sim - "SIM_CONFIG", "wait_for_first_message", "sample_hz", "parallel_sample_hz", + "SIM_CONFIG", "SimulatorHealthError", "wait_for_first_message", "sample_hz", "parallel_sample_hz", "parallel_echo_once_robot_topics", ] diff --git a/tests/harness/baseline.py b/tests/harness/baseline.py new file mode 100644 index 000000000..39e106061 --- /dev/null +++ b/tests/harness/baseline.py @@ -0,0 +1,44 @@ +"""Select a completed, configuration-identical simulation baseline.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Iterable + +from harness.run_meta import classify_run, comparability_reason + + +def select_baseline( + candidate_dirs: Iterable[Path], + current_meta: dict, +) -> tuple[Path | None, list[str]]: + """Return newest comparable candidate and human-readable rejection reasons.""" + matches: list[Path] = [] + rejected: list[str] = [] + for candidate in {Path(path) for path in candidate_dirs}: + meta = classify_run(candidate) + reason = comparability_reason(current_meta, meta) + if reason: + rejected.append(f"{candidate}: {reason}") + else: + matches.append(candidate) + if not matches: + return None, sorted(rejected) + matches.sort( + key=lambda path: (path / "run_meta.json").stat().st_mtime + if (path / "run_meta.json").exists() + else path.stat().st_mtime, + reverse=True, + ) + return matches[0], sorted(rejected) + + +def select_baseline_path(current_dir: Path, baseline_root: Path) -> Path | None: + """Convenience API used by CI after downloading several artifacts.""" + current_meta = classify_run(Path(current_dir)) + candidates = [ + path.parent + for path in Path(baseline_root).rglob("run_meta.json") + ] + selected, _ = select_baseline(candidates, current_meta) + return selected diff --git a/tests/harness/collection.py b/tests/harness/collection.py index f0eada6ba..629f0b9ea 100644 --- a/tests/harness/collection.py +++ b/tests/harness/collection.py @@ -5,6 +5,8 @@ parametrize ids. conftest's ``pytest_collection_modifyitems`` hook delegates to ``modify_items``. """ +import pytest + from harness.discovery import _is_unit_item # Run cheap/fast-fail tests first so real problems surface early: @@ -25,10 +27,13 @@ # build, so they run after build_packages and before the sim tiers. "__integration__", "system.test_liveliness", + # simple-sim smoke test: own mark (simple_sim), never part of the + # isaac/airsim campaigns (not in run_meta.SIMULATION_MODULES). + "system.test_simple_sim", + "system.test_wiring_snapshot", "system.test_sensors", "system.test_takeoff_hover_land", "system.test_fixed_trajectory", - "system.test_optitrack_e2e", ] # Within test_takeoff_hover_land, each (env, velocity) runs phases in this chain order. @@ -73,7 +78,40 @@ def _module_key(item): return _rank(getattr(item.module, "__name__", ""), _MODULE_ORDER) +def _apply_simplesim_guard(items): + """Skip mismatched (test, sim-target) pairs involving simplesim. + + The `simple_sim` smoke test gates only on what simple-sim provides, and the + isaac/airsim campaign tests gate on PX4/MAVROS that simple-sim never runs. + Skipping at collection time (rather than inside a test) prevents the + class-scoped airstack_env fixture from bringing up a stack that the test + would immediately skip. isaac/airsim campaigns are untouched: without a + simplesim target or a simple_sim item nothing here fires. + """ + for item in items: + cs = getattr(item, "callspec", None) + env = cs.params.get("airstack_env") if cs else None + if not env: + continue + sim, num_robots, _ = env + is_simple_test = item.get_closest_marker("simple_sim") is not None + if is_simple_test and sim != "simplesim": + item.add_marker(pytest.mark.skip( + reason="simple_sim smoke test runs only with --sim simplesim")) + elif is_simple_test and num_robots != 1: + item.add_marker(pytest.mark.skip( + reason="simple-sim is single-robot (topics hardcoded to " + "robot_1); use --num-robots 1")) + elif not is_simple_test and sim == "simplesim": + item.add_marker(pytest.mark.skip( + reason="--sim simplesim only drives the simple_sim smoke test " + "(-m simple_sim)")) + + def modify_items(items): + # 0. simplesim pairing guard (skip markers only; no reordering). + _apply_simplesim_guard(items) + # 1. Cross-module: enforce `_MODULE_ORDER`. Stable sort keeps within-module # order intact, so pytest's default file/class order survives. items.sort(key=_module_key) diff --git a/tests/harness/commands.py b/tests/harness/commands.py index 7c593e4de..a8d687c8b 100644 --- a/tests/harness/commands.py +++ b/tests/harness/commands.py @@ -51,7 +51,7 @@ def _run_teed(cmd_list, timeout, log_name=None, env=None, cwd=None): cmd_list, capture_output=True, text=True, timeout=timeout, env=env, cwd=cwd, ) combined = (result.stdout or "") + (result.stderr or "") - record_cmd_output(combined, log_name) + record_cmd_output(combined, log_name, quoted) return result diff --git a/tests/harness/diagnostics.py b/tests/harness/diagnostics.py new file mode 100644 index 000000000..554120669 --- /dev/null +++ b/tests/harness/diagnostics.py @@ -0,0 +1,125 @@ +"""Bounded, best-effort simulator failure diagnostics.""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +from pathlib import Path + +from harness import session + +MAX_OUTPUT_CHARS = 16_000 +SAFE_ENV_KEYS = ( + "COMPOSE_PROFILES", + "NUM_ROBOTS", + "URDF_FILE", + "AUTOLAUNCH", + "PLAY_SIM_ON_START", + "ISAAC_SIM_SCRIPT_NAME", + "ISAAC_SIM_HEADLESS", + "MS_AIRSIM_HEADLESS", + "MS_AIRSIM_ENV_DIR", + "MS_AIRSIM_BINARY_PATH", + "PX4_PARAM_SET", +) + + +def _bounded_run(args, timeout=10) -> dict: + try: + result = subprocess.run( + args, + capture_output=True, + text=True, + timeout=timeout, + ) + output = (result.stdout or "") + (result.stderr or "") + return { + "returncode": result.returncode, + "output": output[-MAX_OUTPUT_CHARS:], + } + except Exception as exc: + return {"error": f"{type(exc).__name__}: {exc}"} + + +def _safe_name(value: str) -> str: + return re.sub(r"[^A-Za-z0-9_.-]+", "_", value)[:180] + + +def collect_failure_diagnostics( + env: dict | None = None, + reason: str = "", + test_id: str = "session", +) -> Path | None: + """Persist a bounded JSON bundle; diagnostic failures never mask the test.""" + run_dir = session.run_dir() + if run_dir is None: + return None + env = env or {} + containers_result = _bounded_run( + ["docker", "ps", "--format", "{{.Names}}"], timeout=10 + ) + containers = containers_result.get("output", "").splitlines()[:20] + container_data = {} + pane_cmd = ( + "tmux list-panes -a -F " + "'#{session_name}:#{window_name}|#{pane_pid}|#{pane_title}|#{pane_dead}'" + ) + for container in containers: + container_data[container] = { + "logs": _bounded_run( + ["docker", "logs", "--tail", "200", container], timeout=15 + ), + "tmux": _bounded_run( + ["docker", "exec", container, "bash", "-c", pane_cmd], timeout=10 + ), + } + robot_graph = {} + for index, container in enumerate( + [name for name in containers if "robot" in name and "desktop" in name], + start=1, + ): + robot_graph[container] = _bounded_run( + [ + "docker", "exec", "-e", f"ROS_DOMAIN_ID={index}", container, + "bash", "-lc", + "source /opt/ros/jazzy/setup.bash 2>/dev/null; " + "source /root/AirStack/robot/ros_ws/install/setup.bash 2>/dev/null; " + "echo NODES; ros2 node list 2>&1; " + "echo TOPICS; ros2 topic list 2>&1", + ], + timeout=15, + ) + payload = { + "schema_version": 1, + "reason": reason[:4000], + "effective_config": { + key: str(env.get(key, os.environ.get(key, "")))[:2000] + for key in SAFE_ENV_KEYS + if env.get(key, os.environ.get(key)) is not None + }, + "containers": container_data, + "ros_graph": robot_graph, + "gpu": _bounded_run( + [ + "nvidia-smi", + "--query-gpu=name,driver_version,utilization.gpu,memory.used,memory.total", + "--format=csv,noheader", + ], + timeout=10, + ), + "command_ring": [ + { + "command": str(entry.get("command", ""))[:1000], + "log_name": str(entry.get("log_name", ""))[:300], + "output": str(entry.get("output", ""))[-12000:], + } + for entry in session.recent_cmd_outputs()[-30:] + ], + } + diagnostics_dir = Path(run_dir) / "diagnostics" + diagnostics_dir.mkdir(parents=True, exist_ok=True) + path = diagnostics_dir / f"{_safe_name(test_id)}.json" + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + return path diff --git a/tests/harness/run_meta.py b/tests/harness/run_meta.py index 8cd0821a9..48b22cad8 100644 --- a/tests/harness/run_meta.py +++ b/tests/harness/run_meta.py @@ -4,21 +4,22 @@ import hashlib import json +import os import xml.etree.ElementTree as ET from pathlib import Path -from harness.test_ids import canonical_test_id +from harness.test_ids import canonical_test_id, normalize_csv RUN_META_FILENAME = "run_meta.json" SIMULATION_MODULES = ( "system.test_liveliness.", + "system.test_wiring_snapshot.", "system.test_sensors.", "system.test_takeoff_hover_land.", "system.test_fixed_trajectory.", "system.test_waypoint_flight.", - "system.test_optitrack_e2e.", ) @@ -28,15 +29,54 @@ def is_simulation_test_id(test_id: str) -> bool: return canonical.startswith(SIMULATION_MODULES) -def campaign_fingerprint(test_ids) -> str: - """Stable identity for the exact selected simulation campaign.""" +CAMPAIGN_OPTION_KEYS = ( + "sim", + "num_robots", + "stress_iterations", + "stable_duration", + "stable_interval", + "gui", + "takeoff_velocities", + "trajectory_types", + "waypoints", + "waypoint_tolerance", + "goal_tolerance", + "waypoint_timeout", +) + + +def normalize_campaign_config(raw: dict | None) -> dict: + """Return stable, JSON-safe behavior-changing campaign configuration.""" + raw = raw or {} + result = {} + for key in CAMPAIGN_OPTION_KEYS: + value = raw.get(key) + if key in ("sim", "num_robots", "takeoff_velocities", "trajectory_types"): + cast = int if key == "num_robots" else str + result[key] = normalize_csv(value, cast=cast) + elif isinstance(value, Path): + result[key] = str(value) + elif value is not None: + result[key] = value + return result + + +def campaign_fingerprint(test_ids, campaign_config: dict | None = None) -> str: + """Stable identity for exact tests plus behavior-changing configuration.""" canonical_ids = sorted( canonical_test_id(str(test_id).replace("::", ".")).replace(".py.", ".") for test_id in test_ids ) if not canonical_ids: return "" - payload = "\n".join(canonical_ids).encode() + payload = json.dumps( + { + "test_ids": canonical_ids, + "config": normalize_campaign_config(campaign_config), + }, + sort_keys=True, + separators=(",", ":"), + ).encode() return hashlib.sha256(payload).hexdigest() @@ -70,7 +110,10 @@ def _report_details(reports) -> tuple[dict[str, str], set[str], set[str]]: continue if report.failed: outcome = "failed" - if when != "call": + if ( + when != "call" + or getattr(report, "airstack_failure_class", "") == "infrastructure" + ): infrastructure_error_nodeids.add(nodeid) elif report.skipped: outcome = "skipped" @@ -86,7 +129,8 @@ def _report_details(reports) -> tuple[dict[str, str], set[str], set[str]]: def build_run_meta(items, exitstatus: int, mark_expression: str = "", - reports=None) -> dict: + reports=None, campaign_config: dict | None = None, + tested_identity: dict | None = None) -> dict: """Build serializable run metadata from a completed pytest session.""" report_outcomes, call_nodeids, infrastructure_error_nodeids = _report_details( reports @@ -116,45 +160,72 @@ def build_run_meta(items, exitstatus: int, mark_expression: str = "", ) } completed = list(completed_by_id.values()) + selected_test_ids = sorted(canonical_test_id(item.nodeid) for item in items) simulation_items = [ item for item in items if is_simulation_test_id(str(item.nodeid)) ] simulation_completed = [ item for item in simulation_items if str(item.nodeid) in call_nodeids ] + simulation_finalized = [ + item for item in simulation_items + if str(item.nodeid) in completed_by_id + ] simulation_infrastructure_errors = [ item for item in simulation_items if str(item.nodeid) in infrastructure_error_nodeids ] + call_failures = [ + nodeid for nodeid, status in completed_by_id.items() + if status == "failed" and nodeid in call_nodeids + ] if exitstatus == 2: # Pytest uses exit 2 for both collection aborts and user/runner # interruption. Reports prove that execution had already begun. outcome = "incomplete" if completed else "collection_error" + failure_class = "interrupted" if completed else "collection" elif exitstatus in (3, 4): outcome = "internal_error" + failure_class = "ci_integrity" elif exitstatus == 5 or not items: outcome = "no_tests" + failure_class = "no_tests" elif not call_nodeids: outcome = ( "simulation_not_executed" if simulation_items else "tests_not_executed" ) + failure_class = "infrastructure" elif simulation_items and not simulation_completed: outcome = "simulation_not_executed" + failure_class = "infrastructure" elif simulation_infrastructure_errors: outcome = "incomplete" - elif len(simulation_completed) != len(simulation_items): + failure_class = "infrastructure" + elif len(simulation_finalized) != len(simulation_items): outcome = "incomplete" + failure_class = "infrastructure" elif simulation_items: outcome = "simulation" + failure_class = "assertion" if call_failures else "none" else: outcome = "non_simulation" + failure_class = "assertion" if call_failures else "none" + normalized_config = normalize_campaign_config(campaign_config) + simulation_ids = [ + canonical_test_id(item.nodeid) + for item in simulation_items + ] + fingerprint = campaign_fingerprint(simulation_ids, normalized_config) + complete = outcome in ("simulation", "non_simulation") return { - "schema_version": 1, - "complete": outcome != "incomplete", + "schema_version": 2, + "complete": complete, + "completion_state": "completed" if complete else outcome, + "failure_class": failure_class, "outcome": outcome, "pytest_exitstatus": int(exitstatus), "mark_expression": mark_expression, @@ -165,18 +236,38 @@ def build_run_meta(items, exitstatus: int, mark_expression: str = "", "skipped": completed.count("skipped"), "simulation_selected": len(simulation_items), "simulation_completed": len(simulation_completed), - "campaign_fingerprint": campaign_fingerprint( - item.nodeid for item in simulation_items - ), + "simulation_finalized": len(simulation_finalized), + "selected_test_ids": selected_test_ids, + "campaign_config": normalized_config, + "campaign_fingerprint": fingerprint, + "campaign": { + "schema_version": 1, + "selected_test_ids": sorted(simulation_ids), + "config": normalized_config, + "fingerprint": fingerprint, + }, + "tested_identity": tested_identity or { + "sha": os.environ.get("AIRSTACK_TESTED_SHA", ""), + "pr_number": os.environ.get("AIRSTACK_PR_NUMBER", ""), + }, } def write_run_meta(run_dir: Path, items, exitstatus: int, - mark_expression: str = "", reports=None) -> Path: + mark_expression: str = "", reports=None, + campaign_config: dict | None = None, + tested_identity: dict | None = None) -> Path: """Write ``run_meta.json`` for a normally completed pytest session.""" path = Path(run_dir) / RUN_META_FILENAME path.write_text(json.dumps( - build_run_meta(items, exitstatus, mark_expression, reports), + build_run_meta( + items, + exitstatus, + mark_expression, + reports, + campaign_config, + tested_identity, + ), indent=2, sort_keys=True, ) + "\n") @@ -215,7 +306,16 @@ def _classify_junit(results_xml: Path) -> dict: return { "schema_version": 1, - "complete": outcome != "incomplete", + "complete": outcome in ("simulation", "non_simulation"), + "completion_state": ( + "completed" if outcome in ("simulation", "non_simulation") else outcome + ), + "failure_class": ( + "infrastructure" if outcome == "incomplete" + else "collection" if outcome == "collection_error" + else "no_tests" if outcome == "no_tests" + else "assertion" if failures else "none" + ), "outcome": outcome, "pytest_exitstatus": None, "mark_expression": "", @@ -308,3 +408,22 @@ def simulation_metrics_comparable(meta: dict, baseline: dict | None = None) -> b and baseline.get("outcome") == "simulation" and baseline.get("campaign_fingerprint") == meta["campaign_fingerprint"] ) + + +def comparability_reason(meta: dict, baseline: dict | None = None) -> str: + """Human explanation shared by summaries, reports, and baseline selection.""" + if not meta: + return "run metadata is missing" + if not meta.get("complete"): + return f"campaign is not complete ({meta.get('completion_state', meta.get('outcome'))})" + if meta.get("outcome") != "simulation": + return f"run outcome is {meta.get('outcome', 'unknown')}, not a simulation campaign" + if not meta.get("campaign_fingerprint"): + return "campaign fingerprint is missing" + if baseline is None: + return "no baseline campaign was supplied" + if not baseline.get("complete") or baseline.get("outcome") != "simulation": + return "baseline is not a completed simulation campaign" + if baseline.get("campaign_fingerprint") != meta.get("campaign_fingerprint"): + return "baseline campaign configuration does not match" + return "" diff --git a/tests/harness/session.py b/tests/harness/session.py index 54277dacc..bc8c10a4e 100644 --- a/tests/harness/session.py +++ b/tests/harness/session.py @@ -7,6 +7,7 @@ back into conftest globals. """ import logging +from collections import deque from datetime import datetime from pathlib import Path @@ -19,6 +20,7 @@ _run_dir = None _current_item = None _last_cmd_output: dict[str, str] = {} +_command_ring = deque(maxlen=30) def init_run_dir(airstack_root) -> Path: @@ -46,14 +48,24 @@ def current_item(): return _current_item -def record_cmd_output(text, log_name=None): +def record_cmd_output(text, log_name=None, command=""): """Store the latest subprocess output, keyed by ``log_name`` and as the default.""" key = log_name or _DEFAULT_LOG_KEY _last_cmd_output[key] = text _last_cmd_output[_DEFAULT_LOG_KEY] = text + _command_ring.append({ + "command": str(command)[:1000], + "log_name": key, + "output": str(text)[-12000:], + }) def last_cmd_output(log_name=None) -> str: """The most recent subprocess output for ``log_name`` (or the default).""" key = log_name or _DEFAULT_LOG_KEY return _last_cmd_output.get(key) or _last_cmd_output.get(_DEFAULT_LOG_KEY, "") + + +def recent_cmd_outputs() -> list[dict[str, str]]: + """Bounded command/output history for failure diagnostics.""" + return list(_command_ring) diff --git a/tests/harness/sim.py b/tests/harness/sim.py index ec4c8a159..e0cc76972 100644 --- a/tests/harness/sim.py +++ b/tests/harness/sim.py @@ -38,10 +38,41 @@ "ENABLE_LIDAR": "true", }, }, + # simple-sim (lightweight kinematic sim, no PX4/MAVROS): the sim node mocks + # the MAVROS surface directly, hardcoded to robot_1 on domain 1. Only the + # `simple_sim` smoke test (tests/system/test_simple_sim.py) targets it — + # run `airstack test -m simple_sim --sim simplesim --num-robots 1`. + "simplesim": { + "profile": "simple", + "sim_container": "simple-sim", # container_name in simple-sim compose + # /clock is rosgraph_msgs — the base distro setup is enough (the sim + # workspace colcon-builds at container start, so its setup.bash may + # not exist yet on early probe attempts). + "sim_setup_bash": "/opt/ros/jazzy/setup.bash", + "robot_setup_bash": "/root/AirStack/robot/ros_ws/install/setup.bash", + # simple-robot (SIM_TYPE=simple) REPLACES robot-desktop: the desktop + # profile must stay off or both robot containers claim robot_1/domain 1. + # No GCS either — matches `airstack up --sim simple`. + "compose_profiles": "simple", + "robot_pattern": "simple-robot", + "extra_env": {}, + }, } -def wait_for_first_message(container, topic, domain_id, setup_bash, timeout=60): +class SimulatorHealthError(RuntimeError): + """A readiness wait stopped because its simulator process became unhealthy.""" + + +def wait_for_first_message( + container, + topic, + domain_id, + setup_bash, + timeout=60, + health_check=None, + health_grace=15, +): """Wait up to `timeout` seconds for one message on `topic`. Returns seconds elapsed on success, None on timeout. Each attempt sources the workspace and runs `ros2 topic echo --once`; if the workspace isn't built yet or the @@ -54,6 +85,17 @@ def wait_for_first_message(container, topic, domain_id, setup_bash, timeout=60): attempt = 0 while time.time() < deadline: attempt += 1 + if health_check is not None and time.time() - start >= health_grace: + health = health_check() + if isinstance(health, tuple): + healthy, detail = health + else: + healthy, detail = bool(health), "simulator health probe failed" + if not healthy: + raise SimulatorHealthError( + f"infrastructure simulator process failure while waiting " + f"for {topic}: {detail}" + ) per_attempt = min(max(1, int(deadline - time.time())), 10) try: result = ros2_exec( diff --git a/tests/harness/test_ids.py b/tests/harness/test_ids.py index 5b0fd825c..df3da11de 100644 --- a/tests/harness/test_ids.py +++ b/tests/harness/test_ids.py @@ -1,4 +1,6 @@ -"""Canonical test identifiers shared by metrics and summary reporting.""" +"""Canonical test identifiers shared by collection, metadata, and reporting.""" + +import re def canonical_test_id(name: str) -> str: @@ -8,8 +10,24 @@ def canonical_test_id(name: str) -> str: ``system/test_liveliness.Class.test`` while JUnit uses ``system.test_liveliness.Class.test``. """ - head, dot, rest = name.partition(".") - if "/" in head: - head = head.replace("/", ".") - return head + dot + rest if dot else head - return name + value = str(name).replace("\\", "/") + value = value.replace(".py::", ".").replace("::", ".") + value = value.replace(".py.", ".") + return value.replace("/", ".").lstrip(".") + + +def normalize_csv(value, cast=str) -> list: + """Normalize a comma-separated pytest option into a stable sorted list.""" + if value is None: + return [] + if isinstance(value, (list, tuple, set)): + parts = value + else: + parts = str(value).split(",") + normalized = [cast(str(part).strip()) for part in parts if str(part).strip()] + return sorted(normalized) + + +def base_iteration_test_id(name: str) -> str: + """Canonical test ID with only the generated stress-iteration suffix removed.""" + return re.sub(r"-iter\d+(?=\])", "", canonical_test_id(name)) diff --git a/tests/integration/natnet/README.md b/tests/integration/natnet/README.md deleted file mode 100644 index f373e7d85..000000000 --- a/tests/integration/natnet/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# NatNet ↔ robot autonomy integration - -Host-side NatNet wire-protocol tests that drive the Python emulator against -`natnet_ros2_node` in a real robot container. First resident of the -[`integration`](../README.md) tier (no sim, no GPU). - -Mark: `integration`. Filter this scenario with `tests/integration/natnet/`. - -For the **in-sim** end-to-end check (Isaac emulator + full stack), see -[Liveliness sentinel](#liveliness-sentinel-sim-end-to-end) below and -[`tests/system/test_liveliness.py`](../../system/test_liveliness.py). - -## What it verifies - -Three variants in [`test_natnet_integration.py`](test_natnet_integration.py). -All start a host-side `NatNetUnicastServer`, launch `natnet_ros2_node` in the -robot container pointed at the Docker bridge gateway, and assert a sustained -pose stream at **≥ 5 Hz** on the configured topic(s), e.g.: - -- `/{ROBOT_NAME}/perception/optitrack/drone/pose_cov` (wait for first message) -- `/{ROBOT_NAME}/perception/optitrack/drone` (Hz sample) - -| Test | Path | -|------|------| -| **`test_natnet_ros2_receives_drone_pose_hz`** | Hand-built `sFrameOfMocapData` frames enqueued on a raw `NatNetUnicastServer` (no USD). Minimal wire + SDK check. | -| **`test_natnet_ros2_receives_isaac_wrapper_pose_hz`** | Full Isaac data path: in-memory USD stage, `NatNetInterfaceConfig`, `author_interface`, `NatNetServerManager.sample_once()` on a moving prim — same sampling logic as the in-sim physics-step callback. Skips without `usd-core` (`pxr`). Pose-value fidelity is covered hermetically by the emulator's `test_pose_streaming.py` loopback. | -| **`test_natnet_ros2_multi_body_drone_and_target`** | Two bodies (drone id 1 + target id 100) with distinct relative topics; asserts both pose streams and that the target's `pose_cov` topic is **absent** (`body_pose_cov=false`). Exercises the multi-body profile + per-body `pose`/`pose_cov` toggles. | - -These tests **do not** start the full perception bringup or `LAUNCH_NATNET`; they -exec `natnet_ros2_node` directly with the flattened per-body params -(`body_names`/`body_ids`/`body_topics`/`body_pose`/`body_pose_cov`) and no MAVROS bridge. - -## Requirements - -- Docker daemon (robot-desktop container reachable from pytest). -- **`natnet_ros2_node` built** in the robot image (OptiTrack NatNet SDK is - license-gated — run `airstack setup --natnet`, then - `bws --packages-select natnet_ros2` in the container). Tests **skip** if the - node binary is missing. -- Host-side emulator package on `PYTHONPATH` (the test adds - `simulation/isaac-sim/extensions/optitrack.natnet.emulator` — not pip-installed - on the host). -- Ephemeral UDP ports on the host gateway IP (Docker default route as seen from - inside the container). - -The robot container comes from the shared **`robot_autonomy_stack`** fixture in -[`tests/conftest.py`](../../conftest.py) (see the [integration tier README](../README.md)). - -## Running - -```bash -# 1. One-time: NatNet SDK + build natnet_ros2 in the robot image -airstack setup --natnet # or NATNET_ACCEPT_LICENSE=1 airstack setup --natnet -docker exec airstack-robot-desktop-1 bash -lc 'bws --packages-select natnet_ros2' - -# 2a. Reuse an existing robot container (fast local iteration): -AUTOLAUNCH=false airstack up robot-desktop -pytest tests/integration/natnet/ -m integration -v - -# 2b. Let the harness bring the container up/down: -pytest tests/integration/natnet/ -m integration -v -``` -On CI / PR (write access): `/pytest -m integration` - -## Architecture - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Host (pytest) │ -│ NatNetUnicastServer @ docker bridge gateway IP │ -│ • raw variant: hand-built frame queue │ -│ • Isaac variant: NatNetServerManager.sample_once(USD) │ -└────────────────────────────┬─────────────────────────────────┘ - │ UDP unicast (cmd + data ports) -┌────────────────────────────▼─────────────────────────────────┐ -│ Robot container (robot-desktop) │ -│ natnet_ros2_node (libNatNet 4.4 client) │ -│ → /{ROBOT_NAME}/{body topic}[/pose_cov] per configured body │ -└──────────────────────────────────────────────────────────────┘ -``` - -**In sim (liveliness tier):** the server runs inside the Isaac Sim container -(`172.31.0.200` by default). Use a NatNet Pegasus launch script -(`example_one_px4_pegasus_natnet_launch_script.py` or -`example_multi_px4_pegasus_natnet_launch_script.py`); `natnet_ros2` in the -robot stack connects via `natnet_config.yaml` (`server_ip` → emulator IP). - -**Catalog / MODELDEF:** The server holds a MODELDEF **wire cache** only -(`set_model_def_payload()`). Scene semantics (body names, streaming IDs, target -prim paths) come from the Isaac layer (`NatNetInterfaceConfig`, USD interface -prim, or launch-script `build_drone_config`). See the -[emulator README](../../../simulation/isaac-sim/extensions/optitrack.natnet.emulator/README.md). - -## Liveliness sentinel (sim end-to-end) - -The integration tier proves **robot client + host emulator** without Isaac. -The matching **system** check is -`TestLiveliness::test_natnet_pose_alive` in -[`test_liveliness.py`](../../system/test_liveliness.py): - -- **Gated on `LAUNCH_NATNET=true`** (skipped otherwise — normal liveliness runs - are unaffected). -- Asserts `/{robot_n}/{natnet pose topic}/pose_cov` ≥ 5 Hz per robot (the drone - body's configured topic — default `perception/optitrack/drone`). -- Override the checked topic with `NATNET_POSE_TOPIC` (default - `perception/optitrack/drone`). The body name and the published topic are decoupled; - both are set in the robot's `natnet_config.yaml` profile. - -Sim auto-start: set `ISAAC_SIM_SCRIPT_NAME` to a NatNet launch script and -`LAUNCH_NATNET=true` on the robot. Convenience bundle: -`airstack up --env-file overrides/isaac-optitrack-simulation.env` (NatNet script + -PX4 external-vision SITL profile). - -## libNatNet 4.4 unicast — verified wire contract - -The emulator is validated against the **real `libNatNet.so`** (not just the Python -`NatNetClient`) with a minimal C probe that registers `SetFrameReceivedCallback` -and `NatNet_SetLogCallback`. All of the following must hold for the SDK to deliver -frames to the callback: - -| Requirement | Why | -|-------------|-----| -| `NAT_CONNECT` → `sSender_Server` (279 B), name `Motive` | libNatNet reads `Motive 3.1 / NatNet 4.4` | -| `NAT_ECHOREQUEST` → `NAT_ECHORESPONSE` (16 B) | Prevents libNatNet assert | -| Frame ends with a **4-byte end-of-data tag** after `params` | libNatNet's frame unpacker reads it; without it the unpacked size mismatches `nDataBytes` and **every frame is silently dropped** | -| `NAT_FRAMEOFDATA` sent from the **data port** (source port == `data_port`) | libNatNet routes unicast frames by the server's data port. Frames sent from the **command** port are treated as command traffic and dropped — no error, no callback | -| `NAT_KEEPALIVE` gets **no reply** | An echo reply makes libNatNet log `Received unrecognized message Message=10` | - -With these in place the C probe reports `Server: Motive 3.1.0.0 NatNet 4.4.0.0`, -`data descriptions: 1`, and **~74 Hz** of frame callbacks. - -> The lenient Python `NatNetClient` accepts frames *without* the end-of-data tag -> and *on the command port*, which is why it appeared to work while libNatNet did -> not. Always validate against the C SDK. - -Full handshake notes and sniffing workflow: -[optitrack-development skill](../../../.agents/skills/optitrack-development/SKILL.md). - -## After changing natnet_ros2 or the emulator - -Rebuild in the robot container: - -```bash -docker exec airstack-robot-desktop-1 bash -lc 'bws --packages-select natnet_ros2' -``` - -Unit tests (protocol, serializers, Isaac wrapper loopback): - -```bash -airstack test -m unit -v -``` diff --git a/tests/integration/natnet/test_natnet_integration.py b/tests/integration/natnet/test_natnet_integration.py deleted file mode 100644 index 43fa4a7f3..000000000 --- a/tests/integration/natnet/test_natnet_integration.py +++ /dev/null @@ -1,376 +0,0 @@ -# Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. -"""NatNet - robot autonomy integration tests. - -Host-side variants stream frames to ``natnet_ros2_node`` in the robot container and -assert pose topics stay alive at >= 5 Hz: (1) raw ``NatNetUnicastServer`` hand-built -single-body frames; (2) ``NatNetServerManager`` sampling an in-memory USD stage -(Isaac wrapper path, no sim/GPU); (3) a multi-body profile (drone + target) that -exercises per-body topic overrides and the pose / pose_cov toggles. - -The node is parameterised with the flattened per-body arrays -(``body_names`` / ``body_ids`` / ``body_topics`` / ``body_pose`` / ``body_pose_cov``) -that natnet_ros2.launch.py derives from a robot's natnet_config.yaml profile. - -Multi-robot (NUM_ROBOTS=3, per-robot profiles) is exercised in-sim by -``tests/system/test_liveliness.py::test_natnet_pose_alive``. -""" - -from __future__ import annotations - -import subprocess -import sys -import threading -import time - -import pytest - -from conftest import ( # noqa: E402 — pytest adds tests/ to sys.path - docker_exec, - repo_path, - ros2_env, - sample_hz, - wait_for_first_message, -) - -# Emulator is not pip-installed on the host; add extension root + test helpers. -_EXT_ROOT = repo_path("simulation/isaac-sim/extensions/optitrack.natnet.emulator") -for _path in (_EXT_ROOT, _EXT_ROOT / "test"): - if str(_path) not in sys.path: - sys.path.insert(0, str(_path)) - -from optitrack.natnet.emulator import NatNetUnicastServer, TransmissionType # noqa: E402 -from optitrack.natnet.emulator.server import natnet_data_types as dt # noqa: E402 -from natnet_test_helpers import ephemeral_udp_port # noqa: E402 - -pytestmark = pytest.mark.integration - -_ROBOT_SETUP = "/root/AirStack/robot/ros_ws/install/setup.bash" -_NATNET_NODE = "/root/AirStack/robot/ros_ws/install/natnet_ros2/lib/natnet_ros2/natnet_ros2_node" -_WARMUP_S = 2.0 -_STREAM_HOLD_S = 12.0 -_MIN_HZ = 5.0 - -# Robot image has route/netstat but not `ip`; /proc/net/route is always present. -_DEFAULT_GATEWAY_CMD = ( - """awk '$2 == "00000000" { printf "%d.%d.%d.%d\\n", """ - """"0x" substr($3,7,2), "0x" substr($3,5,2), "0x" substr($3,3,2), "0x" substr($3,1,2); exit }' """ - """/proc/net/route""" -) - - -def _docker_default_gateway(container: str) -> str: - result = docker_exec(container, _DEFAULT_GATEWAY_CMD, timeout=10) - gateway = result.stdout.strip() - if not gateway: - pytest.skip(f"Could not resolve default gateway inside {container}") - return gateway - - -def _container_env(container: str, var: str, default: str) -> str: - # ROBOT_NAME / ROS_DOMAIN_ID are set in .bashrc (login shell), not container ENV. - # .bashrc may print "Sourcing ..." to stdout; take the last line as the value. - result = docker_exec(container, f"bash -lc 'echo ${var}'") - lines = [line.strip() for line in result.stdout.splitlines() if line.strip()] - value = lines[-1] if lines else "" - return value if value else default - - -def _natnet_node_available(container: str) -> bool: - result = docker_exec(container, f"test -x {_NATNET_NODE} && echo yes || echo no") - return "yes" in result.stdout - - -def _stop_stale_natnet_nodes(container: str) -> None: - docker_exec(container, "pkill -f natnet_ros2_node || true") - time.sleep(0.5) - - -# Each body: (streaming_id, rigid_body_name). The raw server frame carries ids only; -# the node maps ids → topics via its body_* params. -_DRONE_BODY = (1, "Drone") -_TARGET_BODY = (100, "Target") - - -def _make_frame(frame_num: int, body_ids) -> dt.sFrameOfMocapData: - frame = dt.sFrameOfMocapData() - frame.iFrame = frame_num - frame.nRigidBodies = len(body_ids) - for slot, body_id in enumerate(body_ids): - rb = frame.RigidBodies[slot] - rb.ID = body_id - rb.qw = 1.0 - # Bit 0 = tracking valid; natnet_ros2 skips bodies without it (natnet_logic.hpp). - rb.params = 1 - return frame - - -def _frame_publisher( - server: NatNetUnicastServer, stop_event: threading.Event, body_ids=(1,) -) -> None: - frame_num = 0 - interval = 1.0 / server.publish_rate - while not stop_event.is_set(): - server.enqueue_mocap_data(_make_frame(frame_num, body_ids)) - frame_num += 1 - time.sleep(interval) - - -def _launch_natnet_node(container, host_ip, command_port, domain_id, bodies=None): - """Start natnet_ros2_node in the container pointed at the host emulator. - - ``bodies`` is a list of (id, name, topic, pose, pose_cov); defaults to a single - Drone body on topic ``perception/optitrack/drone`` (the shipped config default). - """ - if bodies is None: - bodies = [(1, "Drone", "perception/optitrack/drone", "true", "true")] - ids = ",".join(str(b[0]) for b in bodies) - names = ",".join(b[1] for b in bodies) - topics = ",".join(b[2] for b in bodies) - pose = ",".join(b[3] for b in bodies) - pose_cov = ",".join(b[4] for b in bodies) - launch_cmd = ( - f"bash -lc '{ros2_env(_ROBOT_SETUP, domain_id)} && " - f"exec {_NATNET_NODE} --ros-args " - f"-p server_ip:={host_ip} " - f"-p command_port:={command_port} " - f"-p body_names:=[{names}] " - f"-p body_ids:=[{ids}] " - f"-p body_topics:=[{topics}] " - f"-p body_pose:=[{pose}] " - f"-p body_pose_cov:=[{pose_cov}]'" - ) - return subprocess.Popen( - ["docker", "exec", container, "bash", "-c", launch_cmd], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - - -def _assert_pose_stream( - container, robot_name, domain_id, topic="perception/optitrack/drone", pose_cov=True -): - """Wait for the pose topic then assert a sustained rate >= _MIN_HZ. - - A body configured with ``body_pose_cov=false`` never publishes the ``/pose_cov`` - variant, so detect the first message on whichever topic the body actually emits. - """ - pose_topic = f"/{robot_name}/{topic}" - detect_topic = f"{pose_topic}/pose_cov" if pose_cov else pose_topic - - time.sleep(_WARMUP_S) - first_msg_s = wait_for_first_message( - container, detect_topic, domain_id, _ROBOT_SETUP, timeout=int(_STREAM_HOLD_S) - ) - assert first_msg_s is not None, ( - f"No messages on {detect_topic} within {_STREAM_HOLD_S}s " - "(NatNet connect or frame stream failed)" - ) - hz = sample_hz( - container, - pose_topic, - domain_id, - _ROBOT_SETUP, - duration=min(8, int(_STREAM_HOLD_S - first_msg_s)), - window=20, - ) - assert hz is not None, f"No sustained stream on {pose_topic}" - assert hz >= _MIN_HZ, f"Expected >= {_MIN_HZ} Hz on {pose_topic}, got {hz}" - - -def _terminate(proc) -> None: - if proc is None: - return - proc.terminate() - try: - proc.wait(timeout=5) - except subprocess.TimeoutExpired: - proc.kill() - - -def test_natnet_ros2_receives_drone_pose_hz(robot_autonomy_stack): - """Raw-server path: hand-built frames on NatNetUnicastServer.""" - container = robot_autonomy_stack["container"] - - if not _natnet_node_available(container): - pytest.skip( - "natnet_ros2_node not built — run airstack setup (NatNet SDK) and " - "bws --packages-select natnet_ros2 in the robot container" - ) - - _stop_stale_natnet_nodes(container) - - host_ip = _docker_default_gateway(container) - command_port = ephemeral_udp_port(host_ip) - robot_name = _container_env(container, "ROBOT_NAME", "robot_1") - domain_id = int(_container_env(container, "ROS_DOMAIN_ID", "0")) - - server = NatNetUnicastServer( - local_interface=host_ip, - transmission_type=TransmissionType.UNICAST, - multicast_address=None, - command_port=command_port, - ) - server.publish_rate = 50 - - stop_event = threading.Event() - publisher = threading.Thread( - target=_frame_publisher, args=(server, stop_event), daemon=True - ) - - node_proc: subprocess.Popen[str] | None = None - try: - # Seed dummy frames before the client connects; keep streaming the whole window. - publisher.start() - time.sleep(0.1) - server.start() - node_proc = _launch_natnet_node(container, host_ip, command_port, domain_id) - _assert_pose_stream(container, robot_name, domain_id) - finally: - stop_event.set() - publisher.join(timeout=2.0) - _terminate(node_proc) - server.shutdown() - - -def test_natnet_ros2_receives_isaac_wrapper_pose_hz(robot_autonomy_stack): - """Isaac-wrapper path: NatNetServerManager.sample_once on a moving USD prim. - - Tests that the wrapper feeds the real robot client end-to-end. Pose-value fidelity - is covered by test_pose_streaming.py loopback. - """ - pytest.importorskip("pxr") - import math - - from pxr import Gf, Usd, UsdGeom - - from optitrack.natnet.emulator.isaac import ( - BodyBinding, - NatNetInterfaceConfig, - NatNetServerManager, - author_interface, - ) - - container = robot_autonomy_stack["container"] - if not _natnet_node_available(container): - pytest.skip("natnet_ros2_node not built — run airstack setup (NatNet SDK)") - - _stop_stale_natnet_nodes(container) - - host_ip = _docker_default_gateway(container) - command_port = ephemeral_udp_port(host_ip) - data_port = ephemeral_udp_port(host_ip) - while data_port == command_port: - data_port = ephemeral_udp_port(host_ip) - robot_name = _container_env(container, "ROBOT_NAME", "robot_1") - domain_id = int(_container_env(container, "ROS_DOMAIN_ID", "0")) - - stage = Usd.Stage.CreateInMemory() - xform = UsdGeom.Xform.Define(stage, "/World/base_link") - translate_op = xform.AddTranslateOp() - translate_op.Set(Gf.Vec3d(0.0, 0.0, 1.0)) - cfg = NatNetInterfaceConfig( - server_ip=host_ip, - command_port=command_port, - data_port=data_port, - publish_rate=50.0, - bodies=[BodyBinding("Drone", "/World/base_link", streaming_id=1)], - ) - author_interface(stage, "/World/NatNetInterface", cfg) - - manager = NatNetServerManager(server_factory=None) # real server factory - stop_event = threading.Event() - - def _sampler(): - # Stand in for the in-sim physics-step callback: move the prim and sample. - interval = 1.0 / cfg.publish_rate - t = 0.0 - while not stop_event.is_set(): - translate_op.Set(Gf.Vec3d(math.sin(t), 0.0, 1.0)) - manager.sample_once(stage) - t += interval - time.sleep(interval) - - sampler = threading.Thread(target=_sampler, daemon=True) - - node_proc: subprocess.Popen[str] | None = None - try: - assert manager.start_server(cfg) is True - sampler.start() - time.sleep(0.1) - node_proc = _launch_natnet_node(container, host_ip, command_port, domain_id) - _assert_pose_stream(container, robot_name, domain_id) - finally: - stop_event.set() - sampler.join(timeout=2.0) - _terminate(node_proc) - manager.stop_server() - - -def test_natnet_ros2_multi_body_drone_and_target(robot_autonomy_stack): - """Multi-body profile: one robot tracks a drone + a static target. - - Streams two bodies (drone id 1, target id 100) and configures the node like a - robot profile with two bodies and distinct relative topics. Asserts: the drone - pose streams >= 5 Hz on its custom topic; the target pose streams on its own - topic; and the target's pose_cov topic is absent (body_pose_cov=false). - """ - container = robot_autonomy_stack["container"] - - if not _natnet_node_available(container): - pytest.skip("natnet_ros2_node not built — run airstack setup (NatNet SDK)") - - _stop_stale_natnet_nodes(container) - - host_ip = _docker_default_gateway(container) - command_port = ephemeral_udp_port(host_ip) - robot_name = _container_env(container, "ROBOT_NAME", "robot_1") - domain_id = int(_container_env(container, "ROS_DOMAIN_ID", "0")) - - server = NatNetUnicastServer( - local_interface=host_ip, - transmission_type=TransmissionType.UNICAST, - multicast_address=None, - command_port=command_port, - ) - server.publish_rate = 50 - - bodies = [ - (_DRONE_BODY[0], _DRONE_BODY[1], "perception/optitrack/drone", "true", "true"), - (_TARGET_BODY[0], _TARGET_BODY[1], "perception/optitrack/target", "true", "false"), - ] - body_ids = (_DRONE_BODY[0], _TARGET_BODY[0]) - - stop_event = threading.Event() - publisher = threading.Thread( - target=_frame_publisher, args=(server, stop_event, body_ids), daemon=True - ) - - node_proc: subprocess.Popen[str] | None = None - try: - publisher.start() - time.sleep(0.1) - server.start() - node_proc = _launch_natnet_node(container, host_ip, command_port, domain_id, bodies) - # Drone (pose + pose_cov) and target (pose only) both stream. - _assert_pose_stream(container, robot_name, domain_id, "perception/optitrack/drone") - _assert_pose_stream( - container, robot_name, domain_id, "perception/optitrack/target", pose_cov=False - ) - - # body_pose_cov=false → the target pose_cov publisher must not exist. - target_cov = f"/{robot_name}/perception/optitrack/target/pose_cov" - topics = docker_exec( - container, - f"bash -lc '{ros2_env(_ROBOT_SETUP, domain_id)} && ros2 topic list'", - timeout=15, - ).stdout - assert target_cov not in topics.split(), ( - f"{target_cov} should not exist when body_pose_cov=false; topics:\n{topics}" - ) - finally: - stop_event.set() - publisher.join(timeout=2.0) - _terminate(node_proc) - server.shutdown() diff --git a/tests/meta/README.md b/tests/meta/README.md new file mode 100644 index 000000000..89650b3ce --- /dev/null +++ b/tests/meta/README.md @@ -0,0 +1,33 @@ +# Contract tests (`tests/meta/`) + +Fast, hermetic **contract tests** — plain pytest under the `unit` mark, no +Docker, no GPU — that pin the modular-AirStack (RFC #379/#380) contracts: +the `airstack` CLI's derived configuration, the `module.yaml` manifest and +workspace overlay, stack folder anatomy, fleet files, the bridge → DDS-router +generation, the doctor gates, the generated docs catalog, and the metrics +reporting semantics. A "contract" here is a promise another part of the +system (CI, docs deploys, an agent workflow, a module repo) relies on; +these tests exist so a refactor cannot silently break the promise. They run +in every `airstack test -m unit` invocation and in the `unit-tests.yml` PR +gate. + +Each `test_*_contract.py` file names the contract it pins in its module +docstring — start there. Fixtures (e.g. the registry snapshot under +[`fixtures/modules_index/`](fixtures/modules_index/)) are committed copies of +the external inputs the contracts were blessed against. + +Two naming notes: + +- **`tests/meta/` vs `tests/harness/run_meta.py`** — unrelated despite the + shared word. This directory holds contract tests ("meta" as in tests about + the system's contracts rather than its flight behavior); + `harness/run_meta.py` writes the `run_meta.json` **run metadata** file into + each results directory. Neither imports the other. +- **The lint outlier** — [`test_launch_single_locus.py`](test_launch_single_locus.py) + is not a fixture-based contract but a repo-wide **lint**: it walks the real + launch tree and enforces the single-locus wiring rule (topic wiring lives + only in stack entry files), with escape hatches listed in + [`launch_lint_allowlist.txt`](launch_lint_allowlist.txt). + +See the [main testing README](../README.md#meta--contract-tests-testsmeta) +for the one-line-per-file index. diff --git a/tests/meta/fixtures/modules_index/modules/dfm2_disturbances.yaml b/tests/meta/fixtures/modules_index/modules/dfm2_disturbances.yaml new file mode 100644 index 000000000..e722293d4 --- /dev/null +++ b/tests/meta/fixtures/modules_index/modules/dfm2_disturbances.yaml @@ -0,0 +1,16 @@ +# Registry entry for the dfm2_disturbances module (schema/module-entry.schema.json). +# airstack_compat is the DECLARED range copied from the module's module.yaml; +# the VERIFIED matrix lives in compat/dfm2_disturbances.yaml (CI-stamped only). +name: dfm2_disturbances +repo: https://github.com/castacks/asm_dfm2_disturbances +description: Isaac Sim disturbance library (fan/vent force fields, strobe lights, lens flare) +maintainer: ajong@andrew.cmu.edu +license: BSD-3-Clause-Clear +type: isaac_extension +registered_ref: cf82cdbe5e44757e0034a24248ae887c8d91c0b4 # main HEAD, 2026-08-22 +airstack_compat: ">=0.19.0-alpha.18 <0.20.0" # DECLARED (from module.yaml) +notes: >- + registered_ref is a commit SHA because no release tag exists yet: v0.1.0 is + pending the first green module-system-tests.yml CI run. Validated + end-to-end locally on 2026-08-20 — declared marks liveliness + + takeoff_hover_land in the module's test_stack on Isaac Sim. diff --git a/tests/meta/fixtures/modules_index/modules/macvo.yaml b/tests/meta/fixtures/modules_index/modules/macvo.yaml new file mode 100644 index 000000000..c43e10509 --- /dev/null +++ b/tests/meta/fixtures/modules_index/modules/macvo.yaml @@ -0,0 +1,23 @@ +# Registry entry for the macvo module (schema/module-entry.schema.json). +# airstack_compat is the DECLARED range copied from the module's module.yaml; +# the VERIFIED matrix lives in compat/macvo.yaml (CI-stamped only). +name: macvo +repo: https://github.com/castacks/asm_macvo +description: >- + MAC-VO learned stereo visual odometry (ICRA 2025 best paper) — macvo_ros2 + wrapper around the MAC-VO network, publishing odometry, a covariance-aware + point cloud, and the disparity image the local planner can consume +maintainer: ajong@andrew.cmu.edu # Andrew Jong +license: BSD-3-Clause-Clear +type: ros_package +registered_ref: 431d7faf1f6fed20d415bb5e5a88d8dcb0d180df # main HEAD, 2026-08-22 +airstack_compat: ">=0.19.0-alpha.18 <0.20.0" # DECLARED (from module.yaml) +notes: >- + registered_ref is a commit SHA because no release tag exists yet: v0.1.0 is + pending the first green module-system-tests.yml CI run. MAC-VO's heavy deps + (TensorRT, torch, model weights) live in the module's Dockerfile.module + (Docker dependency tier 2), keeping them out of the base robot image. + Composed-image CI validation (declared marks build_docker + liveliness + against the tier-2 layer chain) is still pending — unlike + dfm2_disturbances/optitrack, this module has not yet been validated + end-to-end. Consumed by trunk reference stack full_macvo. diff --git a/tests/meta/fixtures/modules_index/modules/mighty.yaml b/tests/meta/fixtures/modules_index/modules/mighty.yaml new file mode 100644 index 000000000..5044b5aba --- /dev/null +++ b/tests/meta/fixtures/modules_index/modules/mighty.yaml @@ -0,0 +1,26 @@ +# Registry entry for the mighty module (schema/module-entry.schema.json). +# airstack_compat is the DECLARED range copied from the module's module.yaml; +# the VERIFIED matrix lives in compat/mighty.yaml (CI-stamped only). +name: mighty +repo: https://github.com/castacks/asm_mighty +description: >- + MIGHTY Hermite-spline local planner (MIT ACL, RA-L 2026) with its + acl-mapping voxel world model and a bridge to AirStack's NavigateTask / + trajectory_controller seam — a map-based replacement for the DROAN local + planner +maintainer: ajong@andrew.cmu.edu # Andrew Jong +license: BSD-3-Clause-Clear +type: ros_package +registered_ref: v0.1.1 +airstack_compat: ">=0.20.0-alpha.16 <0.21.0" # DECLARED (from module.yaml) +notes: >- + Repo is PRIVATE until the AirStack agent study (ICRA 2027 paper, Sec. VI-C) + concludes, then flips public — until then the repo/README links 404 for + non-members. v0.1.1 is code-identical to v0.1.0 (README-only delta); v0.1.0 + was validated end-to-end on Isaac Sim: 44/44 vendored gtests, synthetic + smoke harness, empty-world NavigateTask flight, 7/7 practice pillar-field + traversals, and 5/5 judged obstacle-route flights (min clearances + 1.59–1.65 m vs a 1.0 m gate). Wrapper packages are BSD-3-Clause-Clear; + vendored upstream packages (mighty, DecompROS2, acl-mapping) keep their own + permissive licenses — see the module's VENDORED.md. Consumed by trunk + reference stack full_mighty. diff --git a/tests/meta/fixtures/modules_index/modules/optitrack.yaml b/tests/meta/fixtures/modules_index/modules/optitrack.yaml new file mode 100644 index 000000000..58e2e19a1 --- /dev/null +++ b/tests/meta/fixtures/modules_index/modules/optitrack.yaml @@ -0,0 +1,22 @@ +# Registry entry for the optitrack module (schema/module-entry.schema.json). +# airstack_compat is the DECLARED range copied from the module's module.yaml; +# the VERIFIED matrix lives in compat/optitrack.yaml (CI-stamped only). +name: optitrack +repo: https://github.com/castacks/asm_optitrack +description: >- + OptiTrack NatNet mocap integration — natnet_ros2 client + PX4 + external-vision fusion bridges on the robot, and the Motive-compatible + NatNet server emulator for Isaac Sim +maintainer: ajong@andrew.cmu.edu # Andrew Jong +license: BSD-3-Clause-Clear +type: ros_package +registered_ref: be4e0c141ab8fd7248119faa7950004dd56136a3 # main HEAD, 2026-08-22 +airstack_compat: ">=0.19.0-alpha.18 <0.20.0" # DECLARED (from module.yaml) +notes: >- + registered_ref is a commit SHA because no release tag exists yet: v0.1.0 is + pending the first green module-system-tests.yml CI run. Validated + end-to-end locally on 2026-08-20 — declared marks + integration + liveliness + optitrack (full EV-fusion flight e2e) in the + module's test_stack on Isaac Sim. Builds against the proprietary OptiTrack + NatNet SDK, fetched host-side by hooks.host_setup (never in git, never in + images); CI passes NATNET_ACCEPT_LICENSE=1 via hook_env. diff --git a/tests/meta/fixtures/modules_index/stacks/full_default.yaml b/tests/meta/fixtures/modules_index/stacks/full_default.yaml new file mode 100644 index 000000000..df2a6d5d9 --- /dev/null +++ b/tests/meta/fixtures/modules_index/stacks/full_default.yaml @@ -0,0 +1,14 @@ +# Registry entry for the full_default trunk reference stack (schema/stack-entry.schema.json). +name: full_default +repo: https://github.com/castacks/AirStack +path: stacks/full_default +description: >- + The current full-autonomy topology as a self-contained stack folder — the + baseline most users start from and the stack other stacks are copied from +airstack_compat: ">=0.19.0-alpha.18 <0.21.0" # from the stack's modules.repos airstack_compat key +wiring: stacks/full_default/wiring.md # docs site embeds the CI-generated wiring.md from trunk +notes: >- + Pulls no external modules (repositories: {} — every launched package is + trunk-resident). Equivalence claim: `airstack up --stack full_default` + produces a ROS graph identical to the legacy AUTONOMY_ROLE=full dispatch, + verified by the wiring snapshot test (-m wiring). diff --git a/tests/meta/fixtures/modules_index/stacks/full_droan_cpu.yaml b/tests/meta/fixtures/modules_index/stacks/full_droan_cpu.yaml new file mode 100644 index 000000000..c12a2331f --- /dev/null +++ b/tests/meta/fixtures/modules_index/stacks/full_droan_cpu.yaml @@ -0,0 +1,14 @@ +# Registry entry for the full_droan_cpu trunk reference stack (schema/stack-entry.schema.json). +name: full_droan_cpu +repo: https://github.com/castacks/AirStack +path: stacks/full_droan_cpu +description: >- + Full autonomy with the CPU DROAN local planner (droan_local_planner + live + disparity_expansion world model) instead of the GPU droan_gl node +airstack_compat: ">=0.19.0-alpha.18 <0.21.0" # from the stack's modules.repos airstack_compat key +wiring: stacks/full_droan_cpu/wiring.md # docs site embeds the CI-generated wiring.md from trunk +notes: >- + One of the presets absorbing trunk's local_*.launch.xml variant explosion + into named stacks a few include lines apart (absorbed the deleted + local_droan_cpu.launch.xml). Identical to full_default except the DROAN + include lines; pulls no external modules. diff --git a/tests/meta/fixtures/modules_index/stacks/full_macvo.yaml b/tests/meta/fixtures/modules_index/stacks/full_macvo.yaml new file mode 100644 index 000000000..a43ea90c3 --- /dev/null +++ b/tests/meta/fixtures/modules_index/stacks/full_macvo.yaml @@ -0,0 +1,15 @@ +# Registry entry for the full_macvo trunk reference stack (schema/stack-entry.schema.json). +name: full_macvo +repo: https://github.com/castacks/AirStack +path: stacks/full_macvo +description: >- + Full autonomy with MAC-VO learned stereo visual odometry as the disparity + source for the local planner (droan_gl consumes + /$ROBOT_NAME/perception/macvo/disparity) +airstack_compat: ">=0.19.0-alpha.18 <0.21.0" # from the stack's modules.repos airstack_compat key +wiring: stacks/full_macvo/wiring.md # docs site embeds the CI-generated wiring.md from trunk +notes: >- + Requires the macvo module (registry: modules/macvo.yaml) — its modules.repos + pins asm_macvo, currently at a placeholder v0.1.0 tag pending the module's + first release (asm_macvo TRUNK_REMOVAL.md §0). One of the presets absorbing + trunk's local_*.launch.xml variant explosion into named stacks. diff --git a/tests/meta/fixtures/modules_index/stacks/full_mighty.yaml b/tests/meta/fixtures/modules_index/stacks/full_mighty.yaml new file mode 100644 index 000000000..5dea3ffe3 --- /dev/null +++ b/tests/meta/fixtures/modules_index/stacks/full_mighty.yaml @@ -0,0 +1,17 @@ +# Registry entry for the full_mighty trunk reference stack (schema/stack-entry.schema.json). +name: full_mighty +repo: https://github.com/castacks/AirStack +path: stacks/full_mighty +description: >- + Full autonomy with the local planning layer swapped to the MIGHTY module + (mighty planner + acl-mapping voxel world model fed by the filtered Ouster + cloud + NavigateTask bridge) in place of droan_gl; everything else is + identical to full_default +airstack_compat: ">=0.20.0-alpha.16 <0.21.0" # from the stack's modules.repos airstack_compat key +wiring: stacks/full_mighty/wiring.md # docs site embeds the CI-generated wiring.md from trunk +notes: >- + The module-swap demonstration for the modular architecture: the only + difference vs full_default is one include in launch/stack.launch.xml plus + the asm_mighty pin in modules.repos (registry: modules/mighty.yaml — repo + private until the agent study concludes). wiring.md is pending the stack's + first validated wiring-snapshot run. diff --git a/tests/meta/launch_lint_allowlist.txt b/tests/meta/launch_lint_allowlist.txt new file mode 100644 index 000000000..3c6548823 --- /dev/null +++ b/tests/meta/launch_lint_allowlist.txt @@ -0,0 +1,20 @@ +# Launch files allowed to carry /remappings= OUTSIDE stacks/*/launch/ +# (tests/meta/test_launch_single_locus.py — RFC #379 §4 single-locus rule). +# +# This list is FROZEN at the wrap-form baseline (P5-E1): it names every launch +# file that carried remaps when the rule landed. It only shrinks — the legacy +# AUTONOMY_ROLE layer bringups (local/perception/sensors/global/behavior +# *.launch.xml) were flattened into the stack entry files and deleted, so +# their lines are gone. A listed file that no longer contains a remap FAILS +# the lint until its line is removed; adding a NEW line needs the same +# scrutiny as an RFC change. +# +# What remains: a standalone utility, a vendored driver tree, a module launch +# awaiting its canonical rewrite, and the interface safety boundary (wrapped +# by design until RFC #380 Part 2). +# +# Paths are relative to the repo root, one per line; '#' starts a comment. +common/ros_packages/airstack_common/launch/playback.launch.xml +robot/docker/zed/ws/src/zed_wrapper/launch/zed_camera.launch.py +robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml +robot/ros_ws/src/interface/interface_bringup/launch/interface.launch.py diff --git a/tests/meta/test_bridge_contract.py b/tests/meta/test_bridge_contract.py new file mode 100644 index 000000000..67dd5a6e2 --- /dev/null +++ b/tests/meta/test_bridge_contract.py @@ -0,0 +1,258 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""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 shared + ``autonomy_bringup/config/dds_router.yaml`` conventions (inherited from the + removed legacy split's ``onboard_local_offboard_global`` router config): + ``$(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_campaign_reporting_contract.py b/tests/meta/test_campaign_reporting_contract.py new file mode 100644 index 000000000..24fa19882 --- /dev/null +++ b/tests/meta/test_campaign_reporting_contract.py @@ -0,0 +1,160 @@ +"""Campaign classification, fingerprint, baseline, and advisory contracts.""" + +import json +import sys +from types import SimpleNamespace + +import pytest + +from harness.baseline import select_baseline +from harness.run_meta import build_run_meta, campaign_fingerprint +import parse_metrics +from parse_metrics import _score + +pytestmark = pytest.mark.unit + +NODE = "system/test_liveliness.py::TestLiveliness::test_sim_ready_time[isaacsim-1-iter0]" + + +def _report(when, outcome): + return SimpleNamespace( + nodeid=NODE, + when=when, + failed=outcome == "failed", + skipped=outcome == "skipped", + passed=outcome == "passed", + ) + + +def _item(): + return SimpleNamespace(nodeid=NODE) + + +def test_schema_v2_distinguishes_assertion_from_infrastructure(): + assertion = build_run_meta( + [_item()], + 1, + reports=[_report("setup", "passed"), _report("call", "failed")], + campaign_config={"sim": "isaacsim", "num_robots": "1"}, + ) + infrastructure = build_run_meta( + [_item()], + 1, + reports=[_report("setup", "failed")], + campaign_config={"sim": "isaacsim", "num_robots": "1"}, + ) + assert assertion["schema_version"] == 2 + assert assertion["complete"] is True + assert assertion["failure_class"] == "assertion" + assert infrastructure["complete"] is False + assert infrastructure["failure_class"] == "infrastructure" + + +def test_behavior_options_participate_in_campaign_fingerprint(): + first = campaign_fingerprint([NODE], {"sim": "isaacsim", "num_robots": "1"}) + second = campaign_fingerprint([NODE], {"sim": "isaacsim", "num_robots": "3"}) + assert first != second + + +def test_assertion_with_downstream_dependency_skip_is_finalized_campaign(): + downstream = NODE.replace("test_sim_ready_time", "test_stable") + skipped = SimpleNamespace( + nodeid=downstream, + when="setup", + failed=False, + skipped=True, + passed=False, + ) + meta = build_run_meta( + [_item(), SimpleNamespace(nodeid=downstream)], + 1, + reports=[_report("call", "failed"), skipped], + campaign_config={"sim": "isaacsim", "num_robots": "1"}, + ) + assert meta["outcome"] == "simulation" + assert meta["failure_class"] == "assertion" + assert meta["simulation_completed"] == 1 + assert meta["simulation_finalized"] == 2 + + +def test_call_phase_infrastructure_failure_is_not_an_algorithm_assertion(): + report = _report("call", "failed") + report.airstack_failure_class = "infrastructure" + meta = build_run_meta( + [_item()], + 1, + reports=[report], + campaign_config={"sim": "msairsim", "num_robots": "1"}, + ) + assert meta["outcome"] == "incomplete" + assert meta["failure_class"] == "infrastructure" + assert meta["complete"] is False + + +def _write_run(path, fingerprint, complete=True): + path.mkdir() + (path / "results.xml").write_text("") + (path / "run_meta.json").write_text(json.dumps({ + "schema_version": 2, + "complete": complete, + "completion_state": "completed" if complete else "interrupted", + "outcome": "simulation" if complete else "incomplete", + "campaign_fingerprint": fingerprint, + })) + + +def test_baseline_selector_ignores_newer_mismatch_and_partial(tmp_path): + matching = tmp_path / "matching" + mismatch = tmp_path / "mismatch" + partial = tmp_path / "partial" + _write_run(matching, "wanted") + _write_run(mismatch, "other") + _write_run(partial, "wanted", complete=False) + selected, rejected = select_baseline( + [mismatch, partial, matching], + {"complete": True, "outcome": "simulation", "campaign_fingerprint": "wanted"}, + ) + assert selected == matching + assert len(rejected) == 2 + + +def test_timeout_or_missing_data_is_never_numeric_regression(): + numeric = {"value": 1.0, "direction": "lower_is_better"} + assert _score({"value": "timeout"}, numeric, 20)[1] == "" + assert _score(None, numeric, 20)[1] == "" + + +def test_metric_delta_cli_is_advisory(monkeypatch, tmp_path): + output = tmp_path / "report.md" + monkeypatch.setattr( + parse_metrics, + "generate_report", + lambda *args, **kwargs: ("advisory", True), + ) + monkeypatch.setattr( + sys, + "argv", + ["parse_metrics.py", "--current", str(tmp_path), "--output", str(output)], + ) + with pytest.raises(SystemExit) as exc: + parse_metrics.main() + assert exc.value.code == 0 + assert output.read_text() == "advisory" + + +def test_report_parser_crash_remains_blocking(monkeypatch, tmp_path): + output = tmp_path / "report.md" + + def crash(*args, **kwargs): + raise RuntimeError("broken parser") + + monkeypatch.setattr(parse_metrics, "generate_report", crash) + monkeypatch.setattr( + sys, + "argv", + ["parse_metrics.py", "--current", str(tmp_path), "--output", str(output)], + ) + with pytest.raises(SystemExit) as exc: + parse_metrics.main() + assert exc.value.code == 2 + assert "Report generation failed" in output.read_text() diff --git a/tests/meta/test_cli_help_contract.py b/tests/meta/test_cli_help_contract.py new file mode 100644 index 000000000..95dfc79ca --- /dev/null +++ b/tests/meta/test_cli_help_contract.py @@ -0,0 +1,110 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""CLI help truthfulness contract. + +``airstack help `` text drifts from the code because nothing ties the +two together. This test does — at grep level, matching flag/subcommand +TOKENS (not prose), so wording can evolve freely while lies cannot: + +- every ``parse_launch_intent`` flag appears in the ``up`` help arm +- every pytest ``addoption`` long-name appears in the ``test`` help arm +- every dispatched subcommand of the ``module``/``fleet``/``stack`` command + groups appears in its help arm +""" +import re + +import pytest + +from harness.discovery import repo_path + +pytestmark = pytest.mark.unit + +REPO = repo_path() +AIRSTACK_SH = (REPO / "airstack.sh").read_text(encoding="utf-8") +CONFTEST = (REPO / "tests" / "conftest.py").read_text(encoding="utf-8") + + +def _function_body(text, name): + """Body of a bash ``function {`` ... ``}`` (closing brace at col 0).""" + marker = f"function {name} {{" + start = text.index(marker) + end = text.index("\n}", start) + return text[start:end] + + +def _help_arm(command): + """The ``)`` arm of print_command_help's case statement.""" + body = _function_body(AIRSTACK_SH, "print_command_help") + match = re.search( + rf"^ {re.escape(command)}\)\n(.*?)^\s*;;", body, + re.MULTILINE | re.DOTALL, + ) + assert match, f"print_command_help has no '{command})' help arm" + return match.group(1) + + +def _dispatch_subcommands(module_file, dispatch_fn): + """Word tokens of a dispatcher's ``case "$sub" in`` arms (help/* excluded).""" + text = (REPO / ".airstack" / "modules" / module_file).read_text(encoding="utf-8") + body = _function_body(text, dispatch_fn) + case_body = body[body.index('case "$sub" in'):] + subs = set() + for arm in re.findall(r"^\s*([A-Za-z0-9_|-]+)\)", case_body, re.MULTILINE): + for token in arm.split("|"): + if token and token not in {"help", "-h", "--help", "*"}: + subs.add(token) + return subs + + +# ── up: every launch-intent flag is documented ─────────────────────────────── + +def test_up_help_names_every_parse_launch_intent_flag(): + body = _function_body(AIRSTACK_SH, "parse_launch_intent") + flags = set(re.findall(r"(--[a-z][a-z-]*)(?:=\*)?\)", body)) + assert flags, "no flags extracted from parse_launch_intent — parser drifted?" + arm = _help_arm("up") + missing = sorted(f for f in flags if f not in arm) + assert not missing, f"'airstack help up' does not mention: {missing}" + + +# ── test: every pytest addoption long-name is documented ───────────────────── + +def test_test_help_names_every_pytest_addoption(): + body = CONFTEST[CONFTEST.index("def pytest_addoption"):] + body = body[:body.index("\ndef ", 1)] + options = set(re.findall(r"addoption\(\s*\"(--[a-z][a-z-]*)\"", body)) + assert options, "no addoptions extracted from tests/conftest.py — drifted?" + arm = _help_arm("test") + missing = sorted(o for o in options if o not in arm) + assert not missing, f"'airstack help test' does not mention: {missing}" + + +def test_test_help_names_every_pytest_ini_mark(): + ini = (REPO / "tests" / "pytest.ini").read_text(encoding="utf-8") + marks_block = ini[ini.index("markers ="):ini.index("testpaths")] + marks = set(re.findall(r"^\s{4}(\w+):", marks_block, re.MULTILINE)) + assert marks, "no marks extracted from tests/pytest.ini — drifted?" + arm = _help_arm("test") + missing = sorted(m for m in marks if not re.search(rf"\b{m}\b", arm)) + assert not missing, f"'airstack help test' does not list marks: {missing}" + + +# ── command groups: every dispatched subcommand is documented ──────────────── + +@pytest.mark.parametrize( + ("help_command", "module_file", "dispatch_fn"), + [ + ("module", "module.sh", "cmd_module_dispatch"), + ("fleet", "fleet.sh", "cmd_fleet_dispatch"), + ("stack", "stack.sh", "cmd_stack_dispatch"), + ], +) +def test_group_help_names_every_dispatched_subcommand( + help_command, module_file, dispatch_fn): + subs = _dispatch_subcommands(module_file, dispatch_fn) + assert subs, f"no subcommands extracted from {dispatch_fn} — drifted?" + arm = _help_arm(help_command) + missing = sorted(s for s in subs if not re.search(rf"\b{re.escape(s)}\b", arm)) + assert not missing, ( + f"'airstack help {help_command}' does not mention subcommand(s): {missing}" + ) diff --git a/tests/meta/test_collection_contract.py b/tests/meta/test_collection_contract.py index 998de89a4..3b58e1e77 100644 --- a/tests/meta/test_collection_contract.py +++ b/tests/meta/test_collection_contract.py @@ -1,5 +1,5 @@ # Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. +# SPDX-License-Identifier: BSD-3-Clause-Clear """Contract tests for co-located unit-test collection. Unit-test source lives outside ``tests/``, so ``conftest.pytest_configure`` appends it to @@ -52,8 +52,8 @@ def test_broad_invocations_collect_unit_tests(cwd, args): (TESTS_DIR, ["system/test_liveliness.py"]), (TESTS_DIR, ["system/test_liveliness.py::TestLiveliness::test_x"]), (_REPO, ["tests/system/test_sensors.py"]), - (_REPO, ["tests/integration/natnet"]), - (_REPO, ["simulation/isaac-sim/extensions/optitrack.natnet.emulator/test/test_frames.py"]), + (_REPO, ["tests/integration"]), + (_REPO, ["robot/ros_ws/src/sensors/lidar_point_cloud_filter/test/test_validation_core.py"]), (_REPO, ["."]), # never recurse over the repo on host (_REPO, [""]), (_REPO, ["tests/", ""]), @@ -118,7 +118,7 @@ def test_report_uses_the_revision_that_was_actually_tested(): def test_pr_head_check_is_finalized_after_metrics(): workflow = repo_path(".github", "workflows", "system-tests.yml").read_text() assert workflow.index("- name: Finalize check on PR head") > workflow.index( - "- name: Fail on regression" + "- name: Fail on report integrity error" ) assert "ref: ${{ needs.run-tests.outputs.tested_sha }}" in workflow assert "conclusion: '${{ job.status }}'" not in workflow diff --git a/tests/meta/test_diagnostics_contract.py b/tests/meta/test_diagnostics_contract.py new file mode 100644 index 000000000..6a063bcdf --- /dev/null +++ b/tests/meta/test_diagnostics_contract.py @@ -0,0 +1,51 @@ +"""Hermetic contracts for bounded diagnostics and fail-fast readiness.""" + +import json +from types import SimpleNamespace + +import pytest + +from harness import diagnostics +from harness.sim import SimulatorHealthError, wait_for_first_message + +pytestmark = pytest.mark.unit + + +def test_diagnostic_bundle_is_bounded_and_secret_free(tmp_path, monkeypatch): + monkeypatch.setattr(diagnostics.session, "run_dir", lambda: tmp_path) + monkeypatch.setattr( + diagnostics.session, + "recent_cmd_outputs", + lambda: [{"command": "probe", "output": "x" * 50_000}], + ) + + def fake_run(args, **kwargs): + output = "container-a\n" if args[:2] == ["docker", "ps"] else "y" * 50_000 + return SimpleNamespace(returncode=0, stdout=output, stderr="") + + monkeypatch.setattr(diagnostics.subprocess, "run", fake_run) + path = diagnostics.collect_failure_diagnostics( + { + "COMPOSE_PROFILES": "desktop,isaac-sim", + "DOCKER_REGISTRY_PASSWORD": "must-not-leak", + }, + "pane died", + "system/test", + ) + payload = json.loads(path.read_text()) + assert payload["schema_version"] == 1 + assert "DOCKER_REGISTRY_PASSWORD" not in payload["effective_config"] + assert path.stat().st_size < 150_000 + + +def test_message_wait_aborts_immediately_on_dead_process(): + with pytest.raises(SimulatorHealthError, match="pane exited"): + wait_for_first_message( + "sim", + "/clock", + 1, + "/setup.bash", + timeout=600, + health_check=lambda: (False, "pane exited"), + health_grace=0, + ) diff --git a/tests/meta/test_docker_layer_plan_contract.py b/tests/meta/test_docker_layer_plan_contract.py new file mode 100644 index 000000000..2415ae881 --- /dev/null +++ b/tests/meta/test_docker_layer_plan_contract.py @@ -0,0 +1,475 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Contract tests for tools/compose_module_layers.py (RFC #379 §6, Phase P4). + +The layer planner turns synced modules' docker declarations (deps.apt/deps.pip, +dockerfile, overlay_image) into a per-host build plan +(.airstack/generated/layer_plan.json + layers//Dockerfile.composed) and a +deterministic modules.lock at the repo root. Contracts pinned here: + +- **Zero-module identity rule** — no docker-relevant declarations means every + host's plan is exactly {base_image: trunk tag, steps: [], final_tag: trunk + tag}: no Dockerfile.composed, and the overlay-generated compose carries no + ``image:`` overrides. A dep-free checkout keeps today's images byte-for-byte. +- **Determinism** — identical inputs produce byte-identical lock and plan, and + the lock never leaks machine-local absolute paths. +- **Tier chaining** — a module with apt/pip deps AND a Dockerfile.module + contributes a tier-1 step (one RUN per package manager, chained via ARG + BASE_IMAGE) followed by a tier-2 step, in that order. +- **Conflict gate** — two modules pinning the same pip package differently + fail --check-conflicts naming both; same-spec duplicates pass. `module sync` + fails on a conflict (doctor hard gate #1). +- **Tier-3 sole-overlay rule** — a dockerfile-less overlay_image is accepted + only as the sole docker-relevant module for its host; any other composition + errors citing RFC #379 §6. +- **dep_hash** — changes when deps change, stable under unrelated manifest edits. + +Tests run the real planner (imported once) against hermetic checkouts built in +tmp_path, mirroring test_module_overlay_contract.py; the two sync-integration +tests shell the real ./airstack.sh in the same sandbox pattern. No Docker, no +network, and the developer's real checkout is never mutated. +""" +import importlib.util +import json +import os +import shutil +import stat +import subprocess + +import pytest +import yaml + +from harness.discovery import repo_path + +pytestmark = pytest.mark.unit + +REPO = repo_path() +HELLO_REL = os.path.join("tests", "fixtures", "modules", "hello_module") +HEAVY_REL = os.path.join("tests", "fixtures", "modules", "heavy_module") + +PLAN_REL = os.path.join(".airstack", "generated", "layer_plan.json") +LOCK_REL = "modules.lock" +LAYERS_REL = os.path.join(".airstack", "generated", "layers") +COMPOSE_REL = os.path.join(".airstack", "generated", "docker-compose.modules.yaml") + +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' +) +ROBOT_BASE = "registry.example.com/airstack/airstack:v0.19.0_robot-x86-64_dev" + + +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 planner(): + return _load(REPO / "tools" / "compose_module_layers.py", "airstack_layer_planner") + + +@pytest.fixture(scope="module") +def overlay(): + return _load(REPO / "tools" / "module_overlay.py", "airstack_module_overlay") + + +def make_checkout(tmp_path, *fixtures, synthetic=()): + """A minimal checkout: .env + modules// copies + x-local modules.repos.""" + root = tmp_path / "airstack" + (root / "modules").mkdir(parents=True) + (root / ".env").write_text(ENV_TEXT, encoding="utf-8") + names = [] + for rel in fixtures: + name = os.path.basename(rel) + shutil.copytree(REPO / rel, root / "modules" / name, + ignore=shutil.ignore_patterns("__pycache__")) + names.append(name) + for name, manifest in synthetic: + mdir = root / "modules" / name + mdir.mkdir() + (mdir / "module.yaml").write_text(yaml.safe_dump(manifest), encoding="utf-8") + names.append(name) + (root / "modules.repos").write_text( + yaml.safe_dump({ + "repositories": {}, + "x-local-modules": [{"name": n, "path": f"modules/{n}"} for n in names], + }), + encoding="utf-8", + ) + return root + + +def manifest_for(name, **overrides): + data = { + "name": name, + "description": f"Synthetic layer-plan fixture {name}.", + "maintainer": "test@example.com", + "license": "MIT", + "type": "ros_package", + "airstack_compat": ">=0.19.0 <0.21.0", + "targets": ["robot"], + "tests": {"packages": [], "marks": []}, + } + data.update(overrides) + return data + + +def read_plan(root): + return json.loads((root / PLAN_REL).read_text(encoding="utf-8")) + + +def read_lock(root): + return json.loads((root / LOCK_REL).read_text(encoding="utf-8")) + + +# ── zero-module identity rule ──────────────────────────────────────────────── + +def test_identity_rule_dep_free_module(planner, overlay, tmp_path, capsys): + root = make_checkout(tmp_path, HELLO_REL) + # run the P2 overlay too, then the planner — the sync order + (root / "robot" / "ros_ws" / "src").mkdir(parents=True) + (root / "simulation" / "isaac-sim" / "launch_scripts").mkdir(parents=True) + assert overlay.run(root) == 0 + assert planner.main(["--project-root", str(root)]) == 0 + + plan = read_plan(root) + assert set(plan) == {"robot", "gcs", "isaac-sim", "ms-airsim"} + for host, entry in plan.items(): + assert entry["steps"] == [], f"{host} has steps for a dep-free module" + assert entry["final_tag"] == entry["base_image"] + assert plan["robot"]["base_image"] == ROBOT_BASE + assert plan["gcs"]["base_image"].endswith("_gcs") + + # no composed dockerfile anywhere + assert not (root / LAYERS_REL).exists() + # lock still records the module (identity affects images, not bookkeeping) + lock = read_lock(root) + assert [m["name"] for m in lock["modules"]] == ["hello_module"] + assert lock["modules"][0]["pin"] == "local" + assert lock["plan_hash"] + + # the overlay-generated compose must NOT gain image: overrides + compose = yaml.safe_load((root / COMPOSE_REL).read_text(encoding="utf-8")) + for service, definition in compose["services"].items(): + assert "image" not in definition, f"identity rule violated: {service} sets image" + + out = capsys.readouterr().out + assert "0 docker-relevant modules — base images unchanged" in out + + +def test_zero_modules_cleans_generated_artifacts(planner, tmp_path): + root = make_checkout(tmp_path, HELLO_REL) + assert planner.main(["--project-root", str(root)]) == 0 + assert (root / PLAN_REL).is_file() and (root / LOCK_REL).is_file() + + shutil.rmtree(root / "modules") + (root / "modules.repos").unlink() + assert planner.main(["--project-root", str(root)]) == 0 + assert not (root / PLAN_REL).exists() + assert not (root / LOCK_REL).exists() + assert not (root / ".airstack" / "generated").exists() + + +# ── determinism ────────────────────────────────────────────────────────────── + +def test_two_runs_are_byte_identical(planner, tmp_path): + root = make_checkout(tmp_path, HELLO_REL, HEAVY_REL) + assert planner.main(["--project-root", str(root)]) == 0 + lock1 = (root / LOCK_REL).read_bytes() + plan1 = (root / PLAN_REL).read_bytes() + + assert planner.main(["--project-root", str(root)]) == 0 + assert (root / LOCK_REL).read_bytes() == lock1 + assert (root / PLAN_REL).read_bytes() == plan1 + + # deterministic serialization must not embed machine-local paths + assert str(tmp_path).encode() not in lock1 + + +def test_lock_identical_across_checkout_locations(planner, tmp_path): + root_a = make_checkout(tmp_path / "a", HEAVY_REL) + root_b = make_checkout(tmp_path / "b", HEAVY_REL) + assert planner.main(["--project-root", str(root_a)]) == 0 + assert planner.main(["--project-root", str(root_b)]) == 0 + assert (root_a / LOCK_REL).read_bytes() == (root_b / LOCK_REL).read_bytes() + + +# ── tier-1 + tier-2 chaining (heavy_module) ────────────────────────────────── + +def test_heavy_module_tier1_then_tier2(planner, tmp_path): + root = make_checkout(tmp_path, HEAVY_REL) + assert planner.main(["--project-root", str(root)]) == 0 + + plan = read_plan(root) + robot = plan["robot"] + assert [(s["module"], s["tier"]) for s in robot["steps"]] == [ + ("heavy_module", 1), + ("heavy_module", 2), + ] + tier1, tier2 = robot["steps"] + assert tier1["dockerfile"] is None + assert tier2["dockerfile"] == os.path.join("modules", "heavy_module", "Dockerfile.module") + assert tier1["dep_hash"] == tier2["dep_hash"] # same module, same declaration hash + + # final tag: trunk scheme + -m<8-char plan_hash prefix> + lock = read_lock(root) + assert robot["base_image"] == ROBOT_BASE + assert robot["final_tag"] == f"{ROBOT_BASE}-m{lock['plan_hash'][:8]}" + + # hosts the module does not target stay identity + for host in ("gcs", "isaac-sim", "ms-airsim"): + assert plan[host]["steps"] == [] + assert plan[host]["final_tag"] == plan[host]["base_image"] + + composed = (root / LAYERS_REL / "robot" / "Dockerfile.composed").read_text(encoding="utf-8") + assert "ARG BASE_IMAGE" in composed + assert "FROM ${BASE_IMAGE}" in composed + assert "apt-get install -y --no-install-recommends cowsay" in composed + assert "RUN pip3 install --no-cache-dir --break-system-packages tabulate" in composed + # only the robot host composes + assert not (root / LAYERS_REL / "gcs").exists() + + +# ── conflict gate ──────────────────────────────────────────────────────────── + +def test_conflicting_pip_pins_fail_naming_both_modules(planner, tmp_path, capsys): + root = make_checkout( + tmp_path, + synthetic=[ + ("mod_new", manifest_for("mod_new", deps={"apt": [], "pip": ["tabulate==0.9.0"]})), + ("mod_old", manifest_for("mod_old", deps={"apt": [], "pip": ["tabulate==0.8.0"]})), + ], + ) + assert planner.main(["--project-root", str(root), "--check-conflicts"]) == 1 + out = capsys.readouterr().out + assert "CONFLICT" in out + assert "tabulate" in out + assert "mod_new" in out and "mod_old" in out + + +def test_same_spec_and_unpinned_duplicates_pass(planner, tmp_path): + root = make_checkout( + tmp_path, + synthetic=[ + ("mod_a", manifest_for("mod_a", deps={"apt": ["cowsay"], "pip": ["tabulate==0.9.0"]})), + ("mod_b", manifest_for("mod_b", deps={"apt": ["cowsay"], "pip": ["tabulate==0.9.0"]})), + ("mod_c", manifest_for("mod_c", deps={"apt": [], "pip": ["tabulate"]})), + ], + ) + assert planner.main(["--project-root", str(root), "--check-conflicts"]) == 0 + + +def test_conflicts_on_different_hosts_do_not_fight(planner, tmp_path): + root = make_checkout( + tmp_path, + synthetic=[ + ("mod_robot", manifest_for("mod_robot", targets=["robot"], + deps={"apt": [], "pip": ["numpy<2"]})), + ("mod_gcs", manifest_for("mod_gcs", targets=["gcs"], + deps={"apt": [], "pip": ["numpy>=2"]})), + ], + ) + assert planner.main(["--project-root", str(root), "--check-conflicts"]) == 0 + + +# ── tier-3 sole-overlay rule ───────────────────────────────────────────────── + +def test_sole_overlay_used_as_is(planner, tmp_path): + ref = "ghcr.io/example/vla-overlay:0.19.0" + root = make_checkout( + tmp_path, + synthetic=[("vla_planner", manifest_for("vla_planner", overlay_image=ref))], + ) + assert planner.main(["--project-root", str(root)]) == 0 + robot = read_plan(root)["robot"] + assert robot["final_tag"] == ref # pulled, never rebuilt + assert [(s["tier"], s["dockerfile"]) for s in robot["steps"]] == [(3, None)] + assert not (root / LAYERS_REL).exists() # nothing to build + + +def test_overlay_without_dockerfile_alongside_other_module_errors(planner, tmp_path, capsys): + ref = "ghcr.io/example/vla-overlay:0.19.0" + root = make_checkout( + tmp_path, + HEAVY_REL, + synthetic=[("vla_planner", manifest_for("vla_planner", overlay_image=ref))], + ) + assert planner.main(["--project-root", str(root)]) == 1 + out = capsys.readouterr().out + assert "RFC #379 §6" in out + assert "vla_planner" in out + assert "heavy_module" in out # names what else is composing + + +def test_two_overlays_error_unless_they_carry_dockerfiles(planner, tmp_path, capsys): + root = make_checkout( + tmp_path, + synthetic=[ + ("ovl_a", manifest_for("ovl_a", overlay_image="ghcr.io/example/a:1")), + ("ovl_b", manifest_for("ovl_b", overlay_image="ghcr.io/example/b:1")), + ], + ) + assert planner.main(["--project-root", str(root)]) == 1 + assert "RFC #379 §6" in capsys.readouterr().out + + +def test_overlay_with_dockerfile_composes_as_build(planner, tmp_path): + root = make_checkout( + tmp_path, + HEAVY_REL, + synthetic=[("ovl_a", manifest_for("ovl_a", overlay_image="ghcr.io/example/a:1"))], + ) + # give the overlay module its fragment (fragment = source of truth, overlay = cache) + dockerfile = root / "modules" / "ovl_a" / "Dockerfile.module" + dockerfile.write_text("ARG BASE_IMAGE\nFROM ${BASE_IMAGE}\nRUN true\n", encoding="utf-8") + manifest_path = root / "modules" / "ovl_a" / "module.yaml" + data = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + data["dockerfile"] = "Dockerfile.module" + manifest_path.write_text(yaml.safe_dump(data), encoding="utf-8") + + assert planner.main(["--project-root", str(root)]) == 0 + robot = read_plan(root)["robot"] + assert [(s["module"], s["tier"]) for s in robot["steps"]] == [ + ("heavy_module", 1), # tier 1 first + ("heavy_module", 2), # then tier-2 fragments + ("ovl_a", 3), # tier-3-as-build last + ] + assert robot["steps"][2]["dockerfile"] == os.path.join("modules", "ovl_a", "Dockerfile.module") + assert robot["final_tag"] != robot["base_image"] + + +# ── dep_hash sensitivity ───────────────────────────────────────────────────── + +def _lock_entry(root, name): + return next(m for m in read_lock(root)["modules"] if m["name"] == name) + + +def test_dep_hash_changes_with_deps_and_not_with_metadata(planner, tmp_path): + root = make_checkout(tmp_path, HEAVY_REL) + assert planner.main(["--project-root", str(root)]) == 0 + baseline = _lock_entry(root, "heavy_module")["dep_hash"] + + manifest_path = root / "modules" / "heavy_module" / "module.yaml" + data = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + + # unrelated metadata change → hash stable + data["description"] = "A different description, same dependencies." + data["maintainer"] = "other@example.com" + manifest_path.write_text(yaml.safe_dump(data), encoding="utf-8") + assert planner.main(["--project-root", str(root)]) == 0 + assert _lock_entry(root, "heavy_module")["dep_hash"] == baseline + + # dep change → hash moves (and so does the plan hash / final tag) + old_plan_hash = read_lock(root)["plan_hash"] + data["deps"] = {"apt": ["cowsay"], "pip": ["tabulate", "rich"]} + manifest_path.write_text(yaml.safe_dump(data), encoding="utf-8") + assert planner.main(["--project-root", str(root)]) == 0 + assert _lock_entry(root, "heavy_module")["dep_hash"] != baseline + assert read_lock(root)["plan_hash"] != old_plan_hash + + +def test_dep_hash_changes_when_dockerfile_bytes_change(planner, tmp_path): + root = make_checkout(tmp_path, HEAVY_REL) + assert planner.main(["--project-root", str(root)]) == 0 + baseline = _lock_entry(root, "heavy_module")["dep_hash"] + + dockerfile = root / "modules" / "heavy_module" / "Dockerfile.module" + dockerfile.write_text(dockerfile.read_text(encoding="utf-8") + "RUN true\n", + encoding="utf-8") + assert planner.main(["--project-root", str(root)]) == 0 + assert _lock_entry(root, "heavy_module")["dep_hash"] != baseline + + +# ── sync integration (shells the real airstack.sh, like the overlay tests) ─── + +@pytest.fixture() +def sandbox(tmp_path): + """A minimal AirStack checkout containing only what `airstack module` needs.""" + sb = tmp_path / "airstack" + (sb / ".airstack" / "modules").mkdir(parents=True) + (sb / "tools").mkdir() + (sb / "robot" / "ros_ws" / "src").mkdir(parents=True) + (sb / "simulation" / "isaac-sim" / "launch_scripts").mkdir(parents=True) + + shutil.copy(REPO / "airstack.sh", sb / "airstack.sh") + (sb / "airstack.sh").chmod((sb / "airstack.sh").stat().st_mode | stat.S_IXUSR) + shutil.copy(REPO / ".airstack" / "modules" / "module.sh", + sb / ".airstack" / "modules" / "module.sh") + # shared helper library (sourced explicitly by airstack.sh; module.sh + # relies on _require_python_yaml etc.) + shutil.copy(REPO / ".airstack" / "modules" / "_lib.sh", + sb / ".airstack" / "modules" / "_lib.sh") + for tool in ("module_overlay.py", "validate_module.py", "compose_module_layers.py"): + shutil.copy(REPO / "tools" / tool, sb / "tools" / tool) + shutil.copytree(REPO / "common" / "module_schema", sb / "common" / "module_schema") + for rel in (HELLO_REL, HEAVY_REL): + shutil.copytree(REPO / rel, sb / rel, ignore=shutil.ignore_patterns("__pycache__")) + + (sb / ".env").write_text(ENV_TEXT, encoding="utf-8") + (sb / "docker-compose.yaml").write_text("services: {}\n", encoding="utf-8") + return sb + + +def run_airstack(sb, *args, check=True): + result = subprocess.run( + [str(sb / "airstack.sh"), *args], + capture_output=True, text=True, cwd=str(sb), timeout=180, + ) + out = result.stdout + result.stderr + if check: + assert result.returncode == 0, f"airstack {' '.join(args)} failed:\n{out}" + return result.returncode, out + + +def test_sync_runs_planner_and_logs_identity_summary(sandbox): + _, out = run_airstack(sandbox, "module", "add", HELLO_REL) + assert "0 docker-relevant modules — base images unchanged" in out + assert (sandbox / PLAN_REL).is_file() + assert (sandbox / LOCK_REL).is_file() + # identity: no image: overrides in the generated compose + compose = yaml.safe_load((sandbox / COMPOSE_REL).read_text(encoding="utf-8")) + for definition in compose["services"].values(): + assert "image" not in definition + + +def test_sync_fails_on_dependency_conflict(sandbox): + conflicted = sandbox / "conflicted_module" + conflicted.mkdir() + (conflicted / "module.yaml").write_text( + yaml.safe_dump(manifest_for( + "conflicted_module", deps={"apt": [], "pip": ["tabulate==0.8.0"]})), + encoding="utf-8", + ) + heavy = sandbox / HEAVY_REL + heavy_manifest = yaml.safe_load((heavy / "module.yaml").read_text(encoding="utf-8")) + heavy_manifest["deps"] = {"apt": ["cowsay"], "pip": ["tabulate==0.9.0"]} + (heavy / "module.yaml").write_text(yaml.safe_dump(heavy_manifest), encoding="utf-8") + + run_airstack(sandbox, "module", "add", HEAVY_REL) # alone: fine + code, out = run_airstack(sandbox, "module", "add", "conflicted_module", check=False) + assert code != 0 + assert "CONFLICT" in out + assert "heavy_module" in out and "conflicted_module" in out + + +def test_module_lock_subcommand(sandbox): + run_airstack(sandbox, "module", "add", HEAVY_REL) + lock_before = (sandbox / LOCK_REL).read_bytes() + (sandbox / LOCK_REL).unlink() + _, out = run_airstack(sandbox, "module", "lock") + assert (sandbox / LOCK_REL).read_bytes() == lock_before + + +def test_remove_cleans_layer_artifacts(sandbox): + run_airstack(sandbox, "module", "add", HEAVY_REL) + assert (sandbox / PLAN_REL).is_file() and (sandbox / LOCK_REL).is_file() + run_airstack(sandbox, "module", "remove", "heavy_module") + assert not (sandbox / PLAN_REL).exists() + assert not (sandbox / LOCK_REL).exists() + assert not (sandbox / LAYERS_REL).exists() + assert not (sandbox / ".airstack" / "generated").exists() diff --git a/tests/meta/test_docs_catalog_contract.py b/tests/meta/test_docs_catalog_contract.py new file mode 100644 index 000000000..91dc24143 --- /dev/null +++ b/tests/meta/test_docs_catalog_contract.py @@ -0,0 +1,282 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Docs-catalog contract (RFC #379 §9). + +The marketplace catalog under ``docs/modules/`` is GENERATED by +``tools/gen_docs_catalog.py`` from the ``airstack-modules-index`` registry and +committed; the docs deploy workflows regenerate it against the live registry +at build time. This contract pins the pieces that must stay true: + +* the generator is deterministic (two runs => byte-identical output); +* ``--check`` (the CI drift mode) passes against the committed pages when run + from the registry snapshot fixture (``tests/meta/fixtures/modules_index/``, + a copy of the registry entries the committed pages were generated from); +* the catalog table lists every registered module; +* the new-developer walkthrough page exists and is reachable from the nav, + along with the Modules nav section; +* the three docs deploy workflows parse as YAML and carry the module-docs + fetch step with per-clone failure isolation (an unreachable module repo + must never fail a docs deploy). +""" +import re +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +from harness.discovery import TESTS_DIR + +pytestmark = pytest.mark.unit + +REPO = TESTS_DIR.parent +GENERATOR = REPO / "tools" / "gen_docs_catalog.py" +FIXTURE_INDEX = TESTS_DIR / "meta" / "fixtures" / "modules_index" +CATALOG_DIR = REPO / "docs" / "modules" +WALKTHROUGH = REPO / "docs" / "getting_started" / "modular_airstack.md" +MKDOCS_YML = REPO / "mkdocs.yml" +DEPLOY_WORKFLOWS = [ + REPO / ".github" / "workflows" / name + for name in ( + "deploy_docs_from_develop.yaml", + "deploy_docs_from_main.yaml", + "deploy_docs_from_release.yaml", + ) +] + +MODULE_NAMES = sorted(p.stem for p in (FIXTURE_INDEX / "modules").glob("*.yaml")) + + +def _run_generator(*args: str) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(GENERATOR), *args], + capture_output=True, + text=True, + cwd=REPO, + ) + + +def _generate_into(out_dir: Path, tmp_path: Path) -> "dict[str, str]": + empty_modules = tmp_path / "no-fetched-modules" + result = _run_generator( + "--index", str(FIXTURE_INDEX), + "--out", str(out_dir), + "--modules-dir", str(empty_modules), + ) + assert result.returncode == 0, ( + f"generator failed:\nstdout: {result.stdout}\nstderr: {result.stderr}" + ) + return {p.name: p.read_text() for p in sorted(out_dir.glob("*.md"))} + + +# ------------------------------------------------------------ generator + + +def test_fixture_snapshot_is_populated(): + assert MODULE_NAMES, f"no registry snapshot under {FIXTURE_INDEX}/modules" + assert (FIXTURE_INDEX / "stacks").is_dir() + + +def test_generator_is_deterministic(tmp_path): + first = _generate_into(tmp_path / "run1", tmp_path) + second = _generate_into(tmp_path / "run2", tmp_path) + assert first == second, "two generator runs produced different output" + assert set(first) == {"index.md", *(f"{n}.md" for n in MODULE_NAMES)} + + +def test_check_mode_passes_against_committed_pages(tmp_path): + """CI drift style: committed docs/modules/ must match regeneration.""" + empty_modules = tmp_path / "no-fetched-modules" + result = _run_generator( + "--index", str(FIXTURE_INDEX), + "--out", str(CATALOG_DIR), + "--modules-dir", str(empty_modules), + "--check", + ) + assert result.returncode == 0, ( + "committed docs/modules/ pages drift from regeneration — rerun\n" + " python3 tools/gen_docs_catalog.py --index " + "--modules-dir \n" + f"stderr:\n{result.stderr}" + ) + + +def test_check_mode_detects_drift(tmp_path): + out = tmp_path / "pages" + _generate_into(out, tmp_path) + (out / "index.md").write_text("tampered\n") + empty_modules = tmp_path / "no-fetched-modules" + result = _run_generator( + "--index", str(FIXTURE_INDEX), + "--out", str(out), + "--modules-dir", str(empty_modules), + "--check", + ) + assert result.returncode != 0, "--check did not flag a tampered page" + assert "DRIFT" in result.stderr + + +def test_catalog_lists_every_registered_module(): + index_md = (CATALOG_DIR / "index.md").read_text() + for name in MODULE_NAMES: + assert f"[{name}]({name}.md)" in index_md, ( + f"catalog table is missing module {name}" + ) + assert (CATALOG_DIR / f"{name}.md").is_file(), ( + f"missing per-module page docs/modules/{name}.md" + ) + + +def test_module_pages_carry_the_contracted_sections(): + for name in MODULE_NAMES: + entry = yaml.safe_load( + (FIXTURE_INDEX / "modules" / f"{name}.yaml").read_text() + ) + page = (CATALOG_DIR / f"{name}.md").read_text() + repo = entry["repo"].rstrip("/") + ref = entry["registered_ref"] + assert f"airstack module add {repo} --version {ref}" in page, ( + f"{name}.md: install snippet missing or unpinned" + ) + assert "DECLARED" in page and "VERIFIED" in page, ( + f"{name}.md: declared-vs-verified compat note missing" + ) + assert f"{repo}/blob/{ref}/README.md" in page, ( + f"{name}.md: README link at the registered ref missing" + ) + + +# ----------------------------------------------------------- docs + nav + + +def _load_mkdocs() -> dict: + """Parse mkdocs.yml, tolerating the !!python/name superfences tag.""" + + class Loader(yaml.SafeLoader): + pass + + Loader.add_multi_constructor( + "tag:yaml.org,2002:python/name:", lambda loader, suffix, node: suffix + ) + return yaml.load(MKDOCS_YML.read_text(), Loader=Loader) + + +def _flatten_nav(nav) -> "list[str]": + flat = [] + if isinstance(nav, str): + flat.append(nav) + elif isinstance(nav, list): + for item in nav: + flat.extend(_flatten_nav(item)) + elif isinstance(nav, dict): + for value in nav.values(): + flat.extend(_flatten_nav(value)) + return flat + + +def test_walkthrough_page_exists_and_is_in_nav(): + assert WALKTHROUGH.is_file(), "docs/getting_started/modular_airstack.md missing" + nav_paths = _flatten_nav(_load_mkdocs()["nav"]) + assert "docs/getting_started/modular_airstack.md" in nav_paths + index_md = (REPO / "docs" / "getting_started" / "index.md").read_text() + assert "modular_airstack.md" in index_md, ( + "getting_started/index.md must link the walkthrough" + ) + + +def test_modules_nav_section(): + config = _load_mkdocs() + nav_paths = _flatten_nav(config["nav"]) + assert "docs/modules/index.md" in nav_paths, "catalog missing from nav" + for name in MODULE_NAMES: + assert f"docs/modules/{name}.md" in nav_paths, f"{name} page not in nav" + for stack_dir in sorted((REPO / "stacks").iterdir()): + if stack_dir.is_dir() and not stack_dir.name.startswith("."): + assert f"stacks/{stack_dir.name}/README.md" in nav_paths, ( + f"reference stack {stack_dir.name} README not in nav" + ) + + +def test_every_nav_entry_points_at_an_existing_file(): + """Every mkdocs nav target must exist (docs_dir is the repo root). + + Guards the 404 class: a nav entry naming a moved/renamed page ships a + dead link on the published site without failing the build (we cannot + run ``mkdocs --strict`` while pre-existing warnings stand). + + Submodule paths are skipped: unit CI does not checkout submodules, and + those READMEs are owned by the submodule repo. + """ + gitmodules = REPO / ".gitmodules" + submodule_roots = tuple( + re.findall(r"^\s*path\s*=\s*(\S+)", gitmodules.read_text(), re.M) + ) if gitmodules.is_file() else () + missing = [ + path + for path in _flatten_nav(_load_mkdocs()["nav"]) + if not path.startswith(("http://", "https://")) + and not any(path == root or path.startswith(root + "/") for root in submodule_roots) + and not (REPO / path).is_file() + ] + assert not missing, f"mkdocs.yml nav entries with no file on disk: {missing}" + + +def test_fetched_module_checkouts_are_not_site_pages(): + exclude = _load_mkdocs().get("exclude_docs", "") + assert "modules/**" in exclude, ( + "mkdocs exclude_docs must exclude the fetched modules/ checkouts" + ) + + +# ------------------------------------------------------ deploy workflows + + +@pytest.mark.parametrize( + "workflow", DEPLOY_WORKFLOWS, ids=lambda p: p.name +) +def test_deploy_workflow_fetches_module_docs(workflow): + data = yaml.safe_load(workflow.read_text()) + assert isinstance(data, dict), f"{workflow.name} did not parse to a mapping" + + steps = data["jobs"]["deploy"]["steps"] + fetch = [ + s for s in steps + if "registry index" in str(s.get("name", "")).lower() + ] + assert fetch, f"{workflow.name}: no registry/module-docs fetch step" + script = fetch[0]["run"] + assert "airstack-modules-index" in script + assert "gen_docs_catalog.py" in script, ( + f"{workflow.name}: fetch step must regenerate docs/modules/" + ) + # Failure isolation (RFC #379 §9): the registry clone, every per-module + # clone, and the regeneration itself are all wrapped so an unreachable + # repo degrades to committed pages / stub notes instead of a red deploy. + assert script.count("|| echo") + script.count('echo "skipped') >= 2 + assert "skipped" in script + # The fetch step must run before mike deploys the site. + step_names = [str(s.get("name", "")) for s in steps] + fetch_idx = step_names.index(str(fetch[0]["name"])) + build_idx = next( + i for i, s in enumerate(steps) if "mike deploy" in str(s.get("run", "")) + ) + assert fetch_idx < build_idx, f"{workflow.name}: fetch step must precede mike deploy" + + +def test_develop_workflow_freshness_triggers(): + develop = yaml.safe_load(DEPLOY_WORKFLOWS[0].read_text()) + triggers = develop.get("on") or develop.get(True) + assert "workflow_dispatch" in triggers + assert "schedule" in triggers, "weekly freshness rebuild missing" + for path in ("stacks/**", "tools/gen_docs_catalog.py"): + assert path in triggers["push"]["paths"], ( + f"develop docs deploy must trigger on {path}" + ) + + +def test_main_workflow_paths_extended(): + main = yaml.safe_load(DEPLOY_WORKFLOWS[1].read_text()) + triggers = main.get("on") or main.get(True) + for path in ("stacks/**", "tools/gen_docs_catalog.py"): + assert path in triggers["push"]["paths"] diff --git a/tests/meta/test_doctor_contract.py b/tests/meta/test_doctor_contract.py new file mode 100644 index 000000000..c02fc8443 --- /dev/null +++ b/tests/meta/test_doctor_contract.py @@ -0,0 +1,327 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""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_fleet_contract.py b/tests/meta/test_fleet_contract.py new file mode 100644 index 000000000..b8850aa69 --- /dev/null +++ b/tests/meta/test_fleet_contract.py @@ -0,0 +1,532 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Contract tests for fleets (RFC #380 §2, Phase P6). + +Pins the promises the fleet machinery makes: + +- **Resolver parity** — ``sim_one_default`` resolves ``robot_1`` to exactly + what the legacy ``robot_name_map`` resolver produces for + ``airstack-robot-desktop-1`` (same ROBOT_NAME / ROS_DOMAIN_ID): opting into + the fleet changes nothing for today's default checkout. +- **Generation** — ``fleet generate`` is deterministic (byte-identical + re-runs), detects homogeneity (writes nothing for replica-able fleets), and + emits the SPLIT placement: robot_3's ground host gets a service running the + same split stack with ``AIRSTACK_STACK_ENTRY=offboard``. +- **The trajectory hard-gate holds through fleet placement** — every split + stack the generated compose places passes ``gen_dds_router.py --check`` + (doctor hard gate #2: command authority stays onboard). +- **Launch intent** — ``airstack up --config-only --fleet`` exports + FLEET_CONFIG_FILE + derived NUM_ROBOTS + the fleet spawner; explicit env + NUM_ROBOTS beats the fleet (banner); no fleet ⇒ no new effective-config + keys (byte-identical legacy contract). +- **Schema errors are named** — bad hosts/stack/robots produce errors naming + the offender. +- **Spawner mapping** — ``fleet_spawn.py``'s fleet→drone-config mapping is a + pure stdlib function (no Isaac import at module scope) and matches the + fleet file. +""" +import importlib.util +import os +import subprocess +import sys + +import pytest +import yaml + +from harness.discovery import repo_path + +pytestmark = pytest.mark.unit + +REPO = repo_path() +AIRSTACK = str(REPO / "airstack.sh") +RESOLVER = REPO / "tools" / "fleet" / "resolve_fleet.py" +GENERATOR = REPO / "tools" / "fleet" / "generate_fleet_compose.py" +GEN_DDS_ROUTER = REPO / "tools" / "gen_dds_router.py" +LEGACY_RESOLVER = REPO / "robot" / "docker" / "robot_name_map" / "resolve_robot_name.py" +LEGACY_MAP = REPO / "robot" / "docker" / "robot_name_map" / "default_robot_name_map.yaml" +FLEETS = REPO / "config" / "fleets" + +CONFIG_BEGIN = "--- effective launch config ---" +CONFIG_END = "--- end effective launch config ---" + + +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 rf(): + return _load(RESOLVER, "airstack_resolve_fleet") + + +@pytest.fixture(scope="module") +def legacy(): + return _load(LEGACY_RESOLVER, "airstack_resolve_robot_name") + + +def run_tool(script, *args): + result = subprocess.run( + [sys.executable, str(script), *args], + capture_output=True, text=True, cwd=str(REPO), timeout=60, + ) + return result.returncode, result.stdout, result.stderr + + +def run_up_dry(*flags, env=None, check=True): + # Scrub AUTONOMY_ROLE: preflight hard-errors on a leftover value, and + # --config-only still enforces that configuration contract. + full_env = {**os.environ, "AUTONOMY_ROLE": "", **(env or {})} + result = subprocess.run( + [AIRSTACK, "up", "--config-only", *flags], + capture_output=True, text=True, cwd=str(REPO), env=full_env, timeout=120, + ) + out = result.stdout + result.stderr + cfg = {} + in_cfg = False + for line in out.splitlines(): + line = line.strip() + if line == CONFIG_BEGIN: + in_cfg = True + continue + if line == CONFIG_END: + in_cfg = False + continue + if in_cfg and "=" in line: + key, _, value = line.partition("=") + cfg[key] = value + if check: + assert result.returncode == 0, f"config-only failed unexpectedly:\n{out}" + assert cfg, f"no effective-config block in output:\n{out}" + return result.returncode, out, cfg + + +# ── resolver parity with the legacy robot_name_map ────────────────────────── + +def test_sim_one_default_matches_legacy_resolver(rf, legacy): + """Fleet resolution of today's default fleet == legacy resolver output for + the default single-robot container name.""" + legacy_name, legacy_domain = legacy.resolve_robot_name( + "airstack-robot-desktop-1", str(LEGACY_MAP) + ) + fleet = rf.load_fleet(FLEETS / "sim_one_default.yaml") + key = rf.resolve_identity(fleet, "airstack-robot-desktop-1") + resolved = rf.resolve_robot(fleet, REPO, key) + assert resolved["robot_name"] == legacy_name == "robot_1" + assert str(resolved["domain_id"]) == legacy_domain == "1" + + +def test_replica_indices_match_legacy_for_three_robots(rf, legacy): + fleet = rf.load_fleet(FLEETS / "sim_three_mixed.yaml") + for i in (1, 2, 3): + container = f"airstack-robot-desktop-{i}" + legacy_name, legacy_domain = legacy.resolve_robot_name(container, str(LEGACY_MAP)) + key = rf.resolve_identity(fleet, container) + resolved = rf.resolve_robot(fleet, REPO, key) + assert resolved["robot_name"] == legacy_name + assert str(resolved["domain_id"]) == legacy_domain + + +def test_resolver_exports_full_entry(rf): + """The exports carry the whole fleet entry, not just name+domain.""" + fleet = rf.load_fleet(FLEETS / "sim_three_mixed.yaml") + r3 = rf.resolve_robot(fleet, REPO, "robot_3") + assert r3["stack"] == "stacks/lite_offload_global" + assert r3["entry"] == "onboard" # hosts: ⇒ the robot runs the onboard half + assert r3["vehicle"] == "quad_default" + assert r3["urdf_file"].endswith("iris_with_sensors.pegasus.robot.urdf") + assert r3["hosts"] == {"offboard": "gcs"} + r2 = rf.resolve_robot(fleet, REPO, "robot_2") + assert r2["stack"] == "stacks/lite_default" + assert r2["entry"] == "stack" + + +# ── fleet generate: determinism, homogeneity, split placement ─────────────── + +@pytest.fixture(scope="module") +def gen(): + return _load(GENERATOR, "airstack_generate_fleet_compose") + + +def test_generate_is_deterministic(gen, rf): + fleet = rf.load_fleet(FLEETS / "sim_three_mixed.yaml") + first = gen.render(gen.build_compose(fleet, REPO, "config/fleets/sim_three_mixed.yaml")) + second = gen.render(gen.build_compose(fleet, REPO, "config/fleets/sim_three_mixed.yaml")) + assert first == second + assert "generated" in first.lower() + yaml.safe_load(first) # parses + + +def test_homogeneous_fleet_needs_no_generation(gen, rf): + fleet = rf.load_fleet(FLEETS / "sim_one_default.yaml") + assert rf.fleet_is_homogeneous(fleet, REPO) + code, out, err = run_tool(GENERATOR, str(FLEETS / "sim_one_default.yaml"), + "--project-root", str(REPO), "--check-homogeneous") + assert code == 0 and out.strip() == "homogeneous", err + code, out, _ = run_tool(GENERATOR, str(FLEETS / "sim_one_default.yaml"), + "--project-root", str(REPO)) + assert code == 0 + assert "NUM_ROBOTS=1" in out and "No generation needed" in out + + +def test_mixed_fleet_is_heterogeneous(gen, rf): + fleet = rf.load_fleet(FLEETS / "sim_three_mixed.yaml") + assert not rf.fleet_is_homogeneous(fleet, REPO) + + +def test_generated_split_placement(gen, rf): + """robot_3's onboard half + its ground host's offboard half, from one file.""" + fleet = rf.load_fleet(FLEETS / "sim_three_mixed.yaml") + compose = gen.build_compose(fleet, REPO, "config/fleets/sim_three_mixed.yaml") + services = compose["services"] + assert set(services) == {"robot_1", "robot_2", "robot_3", "gcs-robot_3"} + + def env_of(svc): + return dict(e.split("=", 1) for e in services[svc]["environment"] if "=" in e) + + onboard = env_of("robot_3") + assert onboard["AIRSTACK_STACK_DIR"] == "/root/AirStack/stacks/lite_offload_global" + assert onboard["AIRSTACK_STACK_ENTRY"] == "onboard" + assert onboard["ROBOT_NAME"] == "robot_3" + assert onboard["ROS_DOMAIN_ID"] == "3" + + offboard = env_of("gcs-robot_3") + assert offboard["AIRSTACK_STACK_DIR"] == "/root/AirStack/stacks/lite_offload_global" + assert offboard["AIRSTACK_STACK_ENTRY"] == "offboard" + assert offboard["ROBOT_NAME"] == "robot_3" # serves this tenant + assert offboard["ROS_DOMAIN_ID"] == "0" # the fleet's gcs_domain + assert offboard["LAUNCH_PACKAGE"] == "autonomy_bringup" + assert "ports" not in services["gcs-robot_3"] # mirrors legacy robot-offboard + + # every service is self-contained: explicit identity + fleet env + for name in services: + env = env_of(name) + assert env["FLEET_CONFIG_FILE"] == "/root/AirStack/config/fleets/sim_three_mixed.yaml" + assert "ROBOT_NAME" in env and "AIRSTACK_STACK_DIR" in env + assert "extends" not in services[name] + + +def test_split_stacks_placed_by_fleet_pass_bridge_hard_gate(gen, rf): + """Doctor hard gate #2 must hold for every split stack the generated + compose places: gen_dds_router.py --check exits 0 on its bridge.yaml.""" + fleet = rf.load_fleet(FLEETS / "sim_three_mixed.yaml") + compose = gen.build_compose(fleet, REPO, "config/fleets/sim_three_mixed.yaml") + split_stacks = set() + for svc in compose["services"].values(): + env = dict(e.split("=", 1) for e in svc["environment"] if "=" in e) + if env.get("AIRSTACK_STACK_ENTRY", "stack") != "stack": + split_stacks.add(env["AIRSTACK_STACK_DIR"].replace("/root/AirStack/", "")) + assert split_stacks == {"stacks/lite_offload_global"} + for stack_rel in split_stacks: + bridge = REPO / stack_rel / "bridge.yaml" + assert bridge.is_file(), f"split stack {stack_rel} has no bridge.yaml" + code, out, err = run_tool(GEN_DDS_ROUTER, str(bridge), "--check", + "--project-root", str(REPO)) + assert code == 0, f"bridge gate failed for {stack_rel}:\n{out}\n{err}" + + +# ── launch intent: --fleet config-only exports + precedence ───────────────── + +def test_dry_run_fleet_exports(): + code, out, cfg = run_up_dry("--fleet", "sim_one_default", "--sim", "isaac") + assert code == 0, out + assert cfg["FLEET_CONFIG_FILE"] == "/root/AirStack/config/fleets/sim_one_default.yaml" + assert cfg["NUM_ROBOTS"] == "1" + assert cfg["ISAAC_SIM_SCRIPT_NAME"] == "fleet_spawn.py" + assert "robot_1" in out # the resolved robot table prints + + +def test_dry_run_heterogeneous_fleet_swaps_profile_and_would_generate(): + """--config-only derives the fleet config but WRITES NOTHING: the generator + prints what it would generate (compose services + split-stack routers).""" + code, out, cfg = run_up_dry("--fleet", "sim_three_mixed", "--sim", "isaac") + assert code == 0, out + assert cfg["NUM_ROBOTS"] == "3" + profiles = cfg["COMPOSE_PROFILES"].split(",") + assert "fleet" in profiles and "desktop" not in profiles + assert "isaac-sim" in profiles + assert "docker-compose.fleet.yaml" in out + assert "Would write" in out + # split-stack routers are part of the same one-pipeline messaging + assert "Would generate DDS-router config" in out + assert "lite_offload_global" in out + + +def test_fleet_generate_cli_writes_compose_and_split_stack_routers(): + """The `airstack fleet generate` path (the real-run pipeline) emits BOTH + the per-robot compose services and the DDS-router configs for every split + stack the fleet places.""" + result = subprocess.run( + [AIRSTACK, "fleet", "generate", "sim_three_mixed"], + capture_output=True, text=True, cwd=str(REPO), timeout=120, + ) + out = result.stdout + result.stderr + assert result.returncode == 0, out + assert "docker-compose.fleet.yaml" in out + assert "dds_router.lite_offload_global.yaml" in out + + generated = REPO / ".airstack" / "generated" / "docker-compose.fleet.yaml" + assert generated.is_file() + assert yaml.safe_load(generated.read_text())["x-airstack-fleet"] == ( + "config/fleets/sim_three_mixed.yaml" + ) + router = REPO / ".airstack" / "generated" / "dds_router.lite_offload_global.yaml" + assert router.is_file() + assert "allowlist:" in router.read_text() + + +def _write_external_split_checkout(tmp_path): + """Synthetic checkout: an / EXTERNAL split stack (fetched + into stacks/.external/ by `airstack sync`) placed by a fleet.""" + root = tmp_path / "checkout" + veh = root / "config" / "vehicles" / "quadx" + veh.mkdir(parents=True) + (veh / "vehicle.yaml").write_text( + "airframe: {base_urdf: robot_descriptions/x/x.urdf}\n", encoding="utf-8" + ) + stack = root / "stacks" / ".external" / "ext" / "split_x" + (stack / "launch").mkdir(parents=True) + (stack / "launch" / "onboard.launch.xml").write_text("\n") + (stack / "launch" / "offboard.launch.xml").write_text("\n") + (stack / "bridge.yaml").write_text( + "stack: split_x\n" + "bridge:\n" + " - topic: odometry\n" + " type: nav_msgs/msg/Odometry\n" + " direction: onboard_to_offboard\n" + " qos: reliable\n", + encoding="utf-8", + ) + fleets = root / "config" / "fleets" + fleets.mkdir(parents=True) + fleet_path = fleets / "ext_split.yaml" + fleet_path.write_text( + "defaults: {vehicle: quadx}\n" + "robots:\n" + " r1: {stack: ext/split_x, hosts: {offboard: gcs}}\n" + "ground:\n" + " gcs: {}\n", + encoding="utf-8", + ) + return root, fleet_path + + +def test_generator_emits_router_for_external_alias_split_stack(tmp_path): + """Resolve-aware router generation: a split stack referenced as + / (stacks/.external/) still gets its DDS-router config.""" + root, fleet_path = _write_external_split_checkout(tmp_path) + code, out, err = run_tool(GENERATOR, str(fleet_path), "--project-root", str(root)) + assert code == 0, err + assert (root / ".airstack" / "generated" / "docker-compose.fleet.yaml").is_file() + router = root / ".airstack" / "generated" / "dds_router.split_x.yaml" + assert router.is_file(), out + assert "dds_router.split_x.yaml" in out + assert "rt/$(env ROBOT_NAME)/odometry" in router.read_text() + + +def test_generator_dry_run_writes_nothing_for_external_alias_split_stack(tmp_path): + root, fleet_path = _write_external_split_checkout(tmp_path) + code, out, err = run_tool(GENERATOR, str(fleet_path), + "--project-root", str(root), "--dry-run") + assert code == 0, err + assert "Would write" in out and "Would generate DDS-router config" in out + assert not (root / ".airstack" / "generated" / "docker-compose.fleet.yaml").exists() + assert not (root / ".airstack" / "generated" / "dds_router.split_x.yaml").exists() + + +def test_explicit_num_robots_beats_fleet_with_banner(): + code, out, cfg = run_up_dry("--fleet", "sim_one_default", "--sim", "isaac", + env={"NUM_ROBOTS": "5"}) + assert code == 0, out + assert cfg["NUM_ROBOTS"] == "5" + assert "OVERRIDE" in out and "NUM_ROBOTS=5" in out + + +def test_explicit_isaac_script_beats_fleet_spawner(): + _, out, cfg = run_up_dry( + "--fleet", "sim_one_default", "--sim", "isaac", + env={"ISAAC_SIM_SCRIPT_NAME": "my_custom_scene.py"}, + ) + assert cfg["ISAAC_SIM_SCRIPT_NAME"] == "my_custom_scene.py" + assert "OVERRIDE" in out + + +def test_fleet_and_robots_flags_are_mutually_exclusive(): + code, out, _ = run_up_dry("--fleet", "sim_one_default", "--robots", "2", + check=False) + assert code != 0 + assert "mutually exclusive" in out + + +def test_unknown_fleet_is_fatal_and_lists_available(): + code, out, _ = run_up_dry("--fleet", "no_such_fleet", check=False) + assert code != 0 + assert "no_such_fleet" in out + assert "sim_one_default" in out + + +def test_no_fleet_keeps_effective_config_key_free(): + """Byte-identical legacy contract: no fleet anywhere ⇒ no FLEET_CONFIG_FILE + key in the effective config at all (not even empty).""" + _, _, cfg = run_up_dry("--sim", "isaac", env={"FLEET_CONFIG_FILE": ""}) + assert "FLEET_CONFIG_FILE" not in cfg + + +def test_env_fleet_config_file_opts_in_like_the_flag(): + """The harness path: FLEET_CONFIG_FILE via env (container path) triggers + the same validation + derivation as --fleet.""" + code, out, cfg = run_up_dry( + "--sim", "isaac", + env={"FLEET_CONFIG_FILE": "/root/AirStack/config/fleets/sim_one_default.yaml"}, + ) + assert code == 0, out + assert cfg["FLEET_CONFIG_FILE"] == "/root/AirStack/config/fleets/sim_one_default.yaml" + assert cfg["NUM_ROBOTS"] == "1" + + +# ── schema errors are named ────────────────────────────────────────────────── + +def _write_fleet(tmp_path, body): + root = tmp_path / "checkout" + fleets = root / "config" / "fleets" + fleets.mkdir(parents=True) + path = fleets / "bad.yaml" + path.write_text(body, encoding="utf-8") + return path + + +def _validate(path): + return run_tool(RESOLVER, str(path), "--project-root", str(REPO), "--validate") + + +def test_error_hosts_naming_missing_ground(tmp_path): + path = _write_fleet(tmp_path, """ +defaults: {vehicle: quad_default, stack: stacks/full_default} +robots: + r1: {stack: stacks/lite_offload_global, hosts: {offboard: edge_box}} +""") + code, _, err = _validate(path) + assert code == 1 + assert "edge_box" in err and "ground" in err + + +def test_error_split_stack_without_hosts(tmp_path): + path = _write_fleet(tmp_path, """ +defaults: {vehicle: quad_default, stack: stacks/lite_offload_global} +robots: + r1: {} +""") + code, _, err = _validate(path) + assert code == 1 + assert "hosts" in err and "split" in err + + +def test_error_unknown_stack_named(tmp_path): + path = _write_fleet(tmp_path, """ +defaults: {vehicle: quad_default, stack: stacks/no_such_stack} +robots: + r1: {} +""") + code, _, err = _validate(path) + assert code == 1 + assert "no_such_stack" in err + + +def test_error_unknown_vehicle_named(tmp_path): + path = _write_fleet(tmp_path, """ +defaults: {vehicle: no_such_vehicle, stack: stacks/full_default} +robots: + r1: {} +""") + code, _, err = _validate(path) + assert code == 1 + assert "no_such_vehicle" in err + + +def test_error_empty_robots(tmp_path): + path = _write_fleet(tmp_path, "robots: {}\n") + code, _, err = _validate(path) + assert code == 1 + assert "robots" in err + + +def test_error_hosts_role_without_entry_point(tmp_path): + """A hosts role must match a launch entry file of the robot's stack.""" + path = _write_fleet(tmp_path, """ +defaults: {vehicle: quad_default, stack: stacks/lite_offload_global} +robots: + r1: {hosts: {edge_compute: gcs}} +ground: + gcs: {} +""") + code, _, err = _validate(path) + assert code == 1 + assert "edge_compute" in err and "launch/edge_compute.launch.xml" in err + + +def test_error_unknown_robot_key_named(tmp_path): + path = _write_fleet(tmp_path, """ +defaults: {vehicle: quad_default, stack: stacks/full_default} +robots: + r1: {vehicel: quad_default} +""") + code, _, err = _validate(path) + assert code == 1 + assert "vehicel" in err + + +def test_error_bad_domain_policy(tmp_path): + path = _write_fleet(tmp_path, """ +defaults: {vehicle: quad_default, stack: stacks/full_default} +robots: + r1: {} +network: {domain_policy: static} +""") + code, _, err = _validate(path) + assert code == 1 + assert "domain_policy" in err and "static" in err + + +# ── fleet spawner mapping (no Isaac needed) ────────────────────────────────── + +@pytest.fixture(scope="module") +def spawner(): + return _load( + REPO / "simulation" / "isaac-sim" / "launch_scripts" / "fleet_spawn.py", + "airstack_fleet_spawn", + ) + + +def test_fleet_spawn_imports_without_isaac(spawner): + """Module scope must stay stdlib+PyYAML (deferred-import contract).""" + assert callable(spawner.fleet_to_drone_configs) + + +def test_fleet_spawn_mapping_matches_fleet(spawner): + fleet = yaml.safe_load((FLEETS / "sim_three_mixed.yaml").read_text()) + cfgs = spawner.fleet_to_drone_configs(fleet, str(REPO)) + assert [c["domain_id"] for c in cfgs] == [1, 2, 3] + assert [c["robot_name"] for c in cfgs] == ["robot_1", "robot_2", "robot_3"] + assert [c["x_m"] for c in cfgs] == [-2.0, 0.0, 2.0] + assert all(c["z_m"] == 0.07 for c in cfgs) + assert all(c["lidar"] for c in cfgs) # quad_default carries a lidar_3d + + +def test_fleet_spawn_remaps_robot_container_path(spawner): + assert spawner.remap_fleet_path("/root/AirStack/config/fleets/f.yaml") == ( + "/isaac-sim/AirStack/config/fleets/f.yaml" + ) + assert spawner.remap_fleet_path("/elsewhere/f.yaml") == "/elsewhere/f.yaml" + + +def test_fleet_spawn_scene_resolution(spawner): + envs = {"Default Environment": "omniverse://default", "Curved Gridroom": "omniverse://curved"} + fleet = {"sim": {"scene": "default"}} + assert spawner.fleet_env_url(fleet, envs) == "omniverse://default" + assert spawner.fleet_env_url({}, envs) == "omniverse://default" + assert spawner.fleet_env_url({"sim": {"scene": "Curved Gridroom"}}, envs) == "omniverse://curved" + assert spawner.fleet_env_url({"sim": {"scene": "/scenes/x.usd"}}, envs) == "/scenes/x.usd" + with pytest.raises(ValueError): + spawner.fleet_env_url({"sim": {"scene": "nope"}}, envs) diff --git a/tests/meta/test_launch_intent_contract.py b/tests/meta/test_launch_intent_contract.py index 2d541e138..26fc63f9d 100644 --- a/tests/meta/test_launch_intent_contract.py +++ b/tests/meta/test_launch_intent_contract.py @@ -1,16 +1,15 @@ # Copyright (c) 2026 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. +# SPDX-License-Identifier: BSD-3-Clause-Clear """Contract tests for `airstack up` launch-intent flags (--sim/--robots/...). -`airstack up --dry-run` derives the launch configuration (compose profiles, -URDF, Isaac script selection, robot count), runs the preflight checks, prints +`airstack up --config-only` derives the launch configuration (compose profiles, +URDF, Isaac script selection, robot count), runs logical preflight checks, prints the effective config between marker lines, and exits without starting services. These tests pin that contract: the derivations the flags promise, the preflight guards, and the exit codes. -They shell the real ./airstack.sh (no mocking) but never start containers — ---dry-run stops before compose up. Docker itself is required (the preflight -image check runs `docker compose config`), which CI's ubuntu-latest provides. +They shell the real ./airstack.sh (no mocking) but never contact Docker or +require simulator credentials, images, GPUs, or populated submodules. """ import os import subprocess @@ -31,9 +30,12 @@ def run_up_dry(*flags, env=None, check=True): """Run `airstack up --dry-run `; return (exit_code, stdout+stderr, config_dict).""" - full_env = {**os.environ, **(env or {})} + # Scrub AUTONOMY_ROLE from the invoking shell by default: it was removed + # (preflight hard-errors on it) and must only be set by tests that pin + # exactly that error. + full_env = {**os.environ, "AUTONOMY_ROLE": "", **(env or {})} result = subprocess.run( - [AIRSTACK, "up", "--dry-run", *flags], + [AIRSTACK, "up", "--config-only", *flags], capture_output=True, text=True, cwd=str(REPO), env=full_env, timeout=120, ) out = result.stdout + result.stderr @@ -90,14 +92,6 @@ def test_robots_1_selects_single_script_even_if_env_says_multi(): assert cfg["ISAAC_SIM_SCRIPT_NAME"] == "example_one_px4_pegasus_launch_script.py" -def test_robots_respects_natnet_script_pair(): - _, _, cfg = run_up_dry( - "--sim", "isaac", "--robots", "2", - env={"ISAAC_SIM_SCRIPT_NAME": "example_one_px4_pegasus_natnet_launch_script.py"}, - ) - assert cfg["ISAAC_SIM_SCRIPT_NAME"] == "example_multi_px4_pegasus_natnet_launch_script.py" - - def test_robots_never_overrides_custom_script(): code, out, cfg = run_up_dry( "--sim", "isaac", "--robots", "2", @@ -165,12 +159,18 @@ def test_effective_config_dump_written(): if not os.access(REPO, os.W_OK): pytest.skip("checkout mounted read-only (tests container) — dump is best-effort") runs_dir = REPO / ".airstack" / "runs" - before = set(runs_dir.glob("*/effective_config.env")) if runs_dir.exists() else set() + before = { + path: path.stat().st_mtime_ns + for path in runs_dir.glob("*/effective_config.env") + } if runs_dir.exists() else {} run_up_dry("--sim", "isaac") - after = set(runs_dir.glob("*/effective_config.env")) - new = after - before - assert new, "dry-run did not write an effective_config.env under .airstack/runs/" - content = max(new, key=lambda p: p.stat().st_mtime).read_text() + after = list(runs_dir.glob("*/effective_config.env")) + changed = [ + path for path in after + if path not in before or path.stat().st_mtime_ns != before[path] + ] + assert changed, "config-only did not write effective_config.env under .airstack/runs/" + content = max(changed, key=lambda p: p.stat().st_mtime_ns).read_text() assert "COMPOSE_PROFILES=" in content @@ -178,3 +178,120 @@ def test_invalid_sim_is_fatal(): code, out, _ = run_up_dry("--sim", "gazebo", check=False) assert code != 0 assert "gazebo" in out + + +# ── --stack dispatch (RFC #379 §3, P5-E1) ────────────────────────────────── + +def test_stack_flag_exports_container_paths(): + """--stack validates host-side but exports the CONTAINER path (stacks/ is + bind-mounted at /root/AirStack/stacks).""" + _, _, cfg = run_up_dry("--sim", "isaac", "--stack", "full_default") + assert cfg["AIRSTACK_STACK_DIR"] == "/root/AirStack/stacks/full_default" + assert cfg["AIRSTACK_STACK_ENTRY"] == "stack" + + +def test_stack_split_entry_form(): + """--stack : selects launch/.launch.xml (reserved for + split stacks; the default entry file also resolves through it).""" + _, _, cfg = run_up_dry("--sim", "isaac", "--stack", "full_default:stack") + assert cfg["AIRSTACK_STACK_DIR"] == "/root/AirStack/stacks/full_default" + assert cfg["AIRSTACK_STACK_ENTRY"] == "stack" + + +def test_unknown_stack_is_fatal(): + code, out, _ = run_up_dry("--stack", "no_such_stack", check=False) + assert code != 0 + assert "no_such_stack" in out + assert "full_default" in out # error lists the available stacks + + +def test_stack_missing_entry_is_fatal(): + code, out, _ = run_up_dry("--stack", "full_default:onboard", check=False) + assert code != 0 + assert "onboard.launch.xml" in out + + +def test_no_stack_defaults_to_full_default(): + """Stacks are the only dispatch: no --stack (and no stack env) → the + effective config names the trunk reference stack full_default.""" + _, _, cfg = run_up_dry( + "--sim", "isaac", + # Scrub any stack vars inherited from the invoking shell. + env={"AIRSTACK_STACK_DIR": "", "AIRSTACK_STACK_ENTRY": ""}, + ) + assert cfg["AIRSTACK_STACK_DIR"] == "/root/AirStack/stacks/full_default" + assert cfg["AIRSTACK_STACK_ENTRY"] == "stack" + + +def test_autonomy_role_set_is_fatal(): + """AUTONOMY_ROLE was removed — an explicitly set value (env / --env-file / + .env) hard-fails preflight with the removal message.""" + code, out, _ = run_up_dry( + "--sim", "isaac", + env={"AUTONOMY_ROLE": "full", "AIRSTACK_STACK_DIR": ""}, + check=False, + ) + assert code != 0 + assert "AUTONOMY_ROLE was removed" in out + assert "--stack" in out + + +def test_empty_autonomy_role_does_not_trip_removal_error(): + """Only an explicitly SET value trips the removal error — an empty/unset + AUTONOMY_ROLE must pass.""" + code, out, _ = run_up_dry( + "--sim", "isaac", + env={"AUTONOMY_ROLE": "", "AIRSTACK_STACK_DIR": ""}, + ) + assert code == 0 + assert "AUTONOMY_ROLE was removed" not in out + + +def test_autonomy_role_fatal_even_with_stack_selected(): + """The removal error is unconditional: a stale AUTONOMY_ROLE next to a + valid --stack still fails (no silent 'stack wins' anymore).""" + code, out, _ = run_up_dry( + "--sim", "isaac", "--stack", "full_default", + env={"AUTONOMY_ROLE": "full"}, + check=False, + ) + assert code != 0 + assert "AUTONOMY_ROLE was removed" in out + + +# ── override-file golden equivalence (RFC #380 P6, deliverable 8) ─────────── +# overrides/*.env select sims/hardware, not topology — they must keep passing +# `up --dry-run` unchanged as the fleet/stack machinery lands on top of them. + +def test_override_ms_airsim_env_still_derives_expected_config(): + code, out, cfg = run_up_dry( + "--env-file", "overrides/ms-airsim.env", + # Scrub stack/fleet vars a developer shell might carry. + env={"AIRSTACK_STACK_DIR": "", "FLEET_CONFIG_FILE": ""}, + ) + assert code == 0, out + profiles = cfg["COMPOSE_PROFILES"].split(",") + assert "ms-airsim" in profiles and "desktop" in profiles + assert "isaac-sim" not in profiles + assert cfg["URDF_FILE"].endswith("iris_stereo.ms-airsim.urdf") + # a sim override selects no stack of its own → the full_default default + assert cfg["AIRSTACK_STACK_DIR"] == "/root/AirStack/stacks/full_default" + # sim/hardware override files never opt into fleets on their own + assert "FLEET_CONFIG_FILE" not in cfg + + +def test_override_l4t_px4_realrobot_env_still_derives_expected_config(): + code, out, cfg = run_up_dry( + "--env-file", "overrides/l4t-px4-realrobot.env", + # Scrub stack/fleet vars a developer shell might carry. + env={"AIRSTACK_STACK_DIR": "", "FLEET_CONFIG_FILE": ""}, + ) + assert code == 0, out + assert cfg["COMPOSE_PROFILES"] == "l4t" + assert cfg["NUM_ROBOTS"] == "1" + assert cfg["URDF_FILE"].endswith("iris_with_sensors.pegasus.robot.urdf") + assert "FLEET_CONFIG_FILE" not in cfg + # the override file is stack-form now (AUTONOMY_ROLE was removed): it + # pins the full stack explicitly and must not draw the removal error + assert cfg["AIRSTACK_STACK_DIR"] == "/root/AirStack/stacks/full_default" + assert "AUTONOMY_ROLE was removed" not in out diff --git a/tests/meta/test_launch_single_locus.py b/tests/meta/test_launch_single_locus.py new file mode 100644 index 000000000..631e831eb --- /dev/null +++ b/tests/meta/test_launch_single_locus.py @@ -0,0 +1,142 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Single-locus wiring lint (RFC #379 §4.1). + +What this lint enforces, precisely: no ```` (XML) and no +``remappings=`` (Python launch) in any launch file outside +``stacks/*/launch/``, except the shrinking grandfather allowlist. Module +launch files declare their topic endpoints as ````s with canonical +defaults and apply them to their own node locally via ```` — that +is the sanctioned pattern and is deliberately NOT matched by this lint's +regex. Cross-module REWIRING (pointing one module at another's non-canonical +topic) lives only in stack entry files, as include args. + +The rule lands mid-migration (wrap form, P5-E1), so the launch files that +carried remaps at freeze time are grandfathered in +``launch_lint_allowlist.txt``. The allowlist only shrinks: + +* rule 1 — no remap outside ``stacks/*/launch/`` except allowlisted files; +* rule 2 — every allowlist entry must still exist AND still contain a remap + (once E2/E3 flatten a file's wiring away, its line must be deleted); +* rule 3 — stack launch files must carry ``description=`` on every ```` + they declare (they are the wiring document — args must be self-describing). + +Scan scope: ``*.xml`` / ``*.py`` under any ``launch/`` directory inside +``robot/``, ``gcs/``, ``common/``, and ``simulation/``. +""" +import re +import xml.etree.ElementTree as ET + +import pytest + +from harness.discovery import TESTS_DIR + +pytestmark = pytest.mark.unit + +REPO = TESTS_DIR.parent +ALLOWLIST_PATH = TESTS_DIR / "meta" / "launch_lint_allowlist.txt" +SCAN_ROOTS = ("robot", "gcs", "common", "simulation") + +# in XML launch; remappings=[...] in Python launch. +_REMAP_RE = re.compile(r"/remappings= " + "but are not in the frozen allowlist " + f"({ALLOWLIST_PATH.relative_to(REPO)}):\n " + + "\n ".join(offenders) + + "\nCross-module wiring belongs in the stack entry launch file " + "(RFC #379 §4 single-locus rule; see " + ".agents/skills/write-launch-file/SKILL.md). The allowlist only " + "shrinks — do not add new entries." + ) + + +def test_allowlist_entries_exist_and_still_have_remaps(): + """Rule 2 (shrink-only): stale allowlist lines must be deleted.""" + problems = [] + for rel in _allowlist(): + path = REPO / rel + if not path.is_file(): + problems.append(f"{rel}: file no longer exists — delete its " + "allowlist line") + elif not _has_remap(path): + problems.append(f"{rel}: no longer contains a remap — its wiring " + "moved (good!); delete its allowlist line so the " + "file stays remap-free") + assert not problems, ( + f"Stale entries in {ALLOWLIST_PATH.relative_to(REPO)}:\n " + + "\n ".join(problems) + ) + + +def _stack_launch_files(): + stacks = REPO / "stacks" + if not stacks.is_dir(): + return [] + return sorted( + p for p in stacks.glob("*/launch/*.launch.xml") + if not p.parts[len(stacks.parts)].startswith(".") + ) + + +def test_stack_launch_args_have_descriptions(): + """Rule 3: stack entry files are the wiring document — every they + declare needs a description=. Vacuously true while stack files declare no + args (wrap form passes include args instead).""" + stack_files = _stack_launch_files() + assert stack_files, "no stack launch files found under stacks/*/launch/" + offenders = [] + for path in stack_files: + tree = ET.parse(path) # also asserts well-formed XML + # Only top-level children of are declarations; + # inside passes a value and needs no description. + for arg in tree.getroot().findall("arg"): + if not arg.get("description"): + offenders.append( + f"{path.relative_to(REPO).as_posix()}: " + f" missing description=" + ) + assert not offenders, ( + "Stack launch files must describe every declared :\n " + + "\n ".join(offenders) + ) diff --git a/tests/meta/test_metrics_reporting_contract.py b/tests/meta/test_metrics_reporting_contract.py index f016ec71f..9cc41f874 100644 --- a/tests/meta/test_metrics_reporting_contract.py +++ b/tests/meta/test_metrics_reporting_contract.py @@ -1,5 +1,5 @@ # Copyright (c) 2024 Carnegie Mellon University -# MIT License - see LICENSE in the repository root for full text. +# SPDX-License-Identifier: BSD-3-Clause-Clear """Contracts that keep infrastructure failures out of simulation metrics.""" import json diff --git a/tests/meta/test_module_manifest_contract.py b/tests/meta/test_module_manifest_contract.py new file mode 100644 index 000000000..ae27414db --- /dev/null +++ b/tests/meta/test_module_manifest_contract.py @@ -0,0 +1,241 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Contract tests for the module manifest schema + validator (RFC #379 §2, Phase P1). + +``tools/validate_module.py`` interprets ``common/module_schema/module.schema.json`` +with a generic walker — these tests pin the contract both sides must keep: the +required fields and their shapes, the custom format hooks (semver ranges, path +safety), dir-mode cross-file checks, and the stable JSON verdict emitted for +scripts. The valid fixture module lives at ``tests/fixtures/modules/hello_module``. + +The validator is exercised both through its Python API (fast, most cases) and as a +subprocess (the CLI contract: verdict on stdout, exit code). +""" +import copy +import importlib.util +import json +import os +import subprocess +import sys + +import pytest +import yaml + +from harness.discovery import repo_path + +pytestmark = pytest.mark.unit + +VALIDATOR = repo_path("tools", "validate_module.py") +SCHEMA = repo_path("common", "module_schema", "module.schema.json") +FIXTURE = repo_path("tests", "fixtures", "modules", "hello_module") + + +def _load_validator(): + spec = importlib.util.spec_from_file_location("airstack_validate_module", VALIDATOR) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.fixture(scope="module") +def vm(): + return _load_validator() + + +@pytest.fixture() +def manifest(): + """A fresh copy of the valid fixture manifest, ready to mutate.""" + with (FIXTURE / "module.yaml").open(encoding="utf-8") as f: + return yaml.safe_load(f) + + +def _validate(vm, tmp_path, data, as_dir=True): + """Write a manifest into tmp_path and validate; return (verdict, warnings).""" + (tmp_path / "module.yaml").write_text(yaml.safe_dump(data), encoding="utf-8") + target = tmp_path if as_dir else tmp_path / "module.yaml" + return vm.validate_module(target) + + +def _error_paths(verdict): + return {e["path"] for e in verdict["errors"]} + + +# ── the valid fixture passes ─────────────────────────────────────────────── + +def test_valid_fixture_passes_via_import(vm): + verdict, warnings = vm.validate_module(FIXTURE) + assert verdict["valid"] is True, verdict["errors"] + assert verdict["errors"] == [] + assert warnings == [] # tests.packages: [hello_module] matches a real directory + + +def test_valid_fixture_passes_via_subprocess(): + assert os.access(VALIDATOR, os.X_OK), "tools/validate_module.py must be executable" + result = subprocess.run( + [sys.executable, str(VALIDATOR), str(FIXTURE)], + capture_output=True, text=True, timeout=60, + ) + assert result.returncode == 0, result.stderr + verdict = json.loads(result.stdout) + assert verdict == {"valid": True, "errors": []} + + +# ── schema violations ────────────────────────────────────────────────────── + +def test_missing_maintainer_fails_naming_the_path(vm, tmp_path, manifest): + del manifest["maintainer"] + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is False + assert "maintainer" in _error_paths(verdict) + + +def test_bad_type_enum_fails(vm, tmp_path, manifest): + manifest["type"] = "launch_bundle" + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is False + assert "type" in _error_paths(verdict) + + +@pytest.mark.parametrize("bad_range", ["main", "latest", "0.19"]) +def test_malformed_airstack_compat_fails(vm, tmp_path, manifest, bad_range): + manifest["airstack_compat"] = bad_range + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is False + assert "airstack_compat" in _error_paths(verdict) + + +@pytest.mark.parametrize( + "good_range", + [">=0.19.0 <0.21.0", ">=0.19.0-alpha.18 <0.20.0", "0.20.0", "^0.19.0"], +) +def test_wellformed_airstack_compat_passes(vm, tmp_path, manifest, good_range): + manifest["airstack_compat"] = good_range + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is True, verdict["errors"] + + +def test_asset_with_http_url_fails(vm, tmp_path, manifest): + manifest["assets"] = [{ + "url": "http://example.com/asset.tar", + "sha256": "0" * 64, + "dest": "assets/asset.tar", + }] + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is False + assert "assets[0].url" in _error_paths(verdict) + + +def test_asset_dest_with_traversal_fails(vm, tmp_path, manifest): + manifest["assets"] = [{ + "url": "https://example.com/asset.tar", + "sha256": "0" * 64, + "dest": "../outside/asset.tar", + }] + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is False + assert "assets[0].dest" in _error_paths(verdict) + + +def test_unknown_top_level_key_fails(vm, tmp_path, manifest): + manifest["slot"] = "local_planner" # wiring metadata is deliberately absent + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is False + assert "slot" in _error_paths(verdict) + + +def test_unknown_mark_fails(vm, tmp_path, manifest): + manifest["tests"] = {"packages": [], "marks": ["hover_forever"]} + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is False + assert "tests.marks[0]" in _error_paths(verdict) + + +def test_empty_targets_fails(vm, tmp_path, manifest): + manifest["targets"] = [] + verdict, _ = _validate(vm, tmp_path, manifest) + assert verdict["valid"] is False + assert "targets" in _error_paths(verdict) + + +# ── dir-mode cross-file checks ───────────────────────────────────────────── + +def test_dockerfile_pointing_at_nonexistent_file_fails_in_dir_mode(vm, tmp_path, manifest): + manifest["dockerfile"] = "Dockerfile.module" # declared but never created + verdict, _ = _validate(vm, tmp_path, manifest, as_dir=True) + assert verdict["valid"] is False + assert "dockerfile" in _error_paths(verdict) + + +def test_dockerfile_declared_and_present_passes(vm, tmp_path, manifest): + manifest["dockerfile"] = "Dockerfile.module" + (tmp_path / "Dockerfile.module").write_text("ARG BASE_IMAGE\nFROM ${BASE_IMAGE}\n") + verdict, _ = _validate(vm, tmp_path, manifest, as_dir=True) + assert verdict["valid"] is True, verdict["errors"] + + +def test_file_mode_skips_cross_file_checks(vm, tmp_path, manifest): + """Given a bare module.yaml (not a dir), declared paths are not probed.""" + manifest["dockerfile"] = "Dockerfile.module" + verdict, _ = _validate(vm, tmp_path, manifest, as_dir=False) + assert verdict["valid"] is True, verdict["errors"] + + +def test_missing_tests_package_dir_warns_but_stays_valid(vm, tmp_path, manifest): + manifest["tests"] = {"packages": ["no_such_package"], "marks": []} + verdict, warnings = _validate(vm, tmp_path, manifest, as_dir=True) + assert verdict["valid"] is True, verdict["errors"] + assert warnings and "no_such_package" in warnings[0] + + +# ── verdict shape ────────────────────────────────────────────────────────── + +def test_json_verdict_shape_is_stable(vm, tmp_path, manifest): + """Scripts parse the stdout verdict: exactly {valid, errors:[{path,message}]}.""" + broken = copy.deepcopy(manifest) + del broken["maintainer"] + for data in (manifest, broken): + verdict, _ = _validate(vm, tmp_path, data) + assert set(verdict.keys()) == {"valid", "errors"} + assert isinstance(verdict["valid"], bool) + assert isinstance(verdict["errors"], list) + for error in verdict["errors"]: + assert set(error.keys()) == {"path", "message"} + + +def test_invalid_manifest_exits_1_via_subprocess(tmp_path, manifest): + del manifest["maintainer"] + (tmp_path / "module.yaml").write_text(yaml.safe_dump(manifest), encoding="utf-8") + result = subprocess.run( + [sys.executable, str(VALIDATOR), str(tmp_path)], + capture_output=True, text=True, timeout=60, + ) + assert result.returncode == 1 + verdict = json.loads(result.stdout) + assert verdict["valid"] is False + assert "maintainer" in result.stderr # human-readable error names the path + + +def test_schema_keeps_to_the_interpreted_subset(): + """The walker only interprets a fixed keyword subset — the schema must not + quietly grow keywords (oneOf, $ref, ...) the validator would silently ignore.""" + allowed = { + "$schema", "title", "description", "default", + "type", "required", "properties", "enum", "pattern", "items", + "additionalProperties", "minLength", "minItems", + "x-airstack-format", "x-airstack-check-exists", "x-airstack-warn-missing-dir", + } + + def keys(node): + if isinstance(node, dict): + for key, value in node.items(): + yield key + if key in ("properties",): + for sub in value.values(): + yield from keys(sub) + elif key in ("items", "additionalProperties"): + yield from keys(value) + return + + schema = json.loads(SCHEMA.read_text(encoding="utf-8")) + unknown = set(keys(schema)) - allowed + assert not unknown, f"schema uses keywords the validator does not interpret: {unknown}" diff --git a/tests/meta/test_module_overlay_contract.py b/tests/meta/test_module_overlay_contract.py new file mode 100644 index 000000000..70cf0ebcd --- /dev/null +++ b/tests/meta/test_module_overlay_contract.py @@ -0,0 +1,219 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Contract tests for `airstack module` + tools/module_overlay.py (RFC #379, Phase P2). + +The module machinery has host-visible contracts scripts and users depend on: +`module add ` records an ``x-local-modules`` entry in +./modules.repos, sync links the checkout into ``modules/`` and overlays it +(colcon symlink under robot/ros_ws/src/modules/, generated compose override in +.airstack/generated/), `module remove` restores a clean tree, remote adds are +refused without a pin (and refused *with* a branch-looking pin), and sync is +idempotent. + +These tests shell the real ./airstack.sh — but inside a hermetic sandbox built +in tmp_path from copies of only the files the module commands touch +(airstack.sh, .airstack/modules/module.sh, tools/, common/module_schema/, the +hello_module fixture, stub repo dirs). No Docker, no network, no git state in +the sandbox, and the developer's real checkout is never mutated. +""" +import os +import shutil +import stat +import subprocess + +import pytest +import yaml + +from harness.discovery import repo_path + +pytestmark = pytest.mark.unit + +REPO = repo_path() +FIXTURE_REL = os.path.join("tests", "fixtures", "modules", "hello_module") + +GENERATED_REL = os.path.join(".airstack", "generated", "docker-compose.modules.yaml") +ROBOT_LINK_REL = os.path.join("robot", "ros_ws", "src", "modules", "hello_module") + + +@pytest.fixture() +def sandbox(tmp_path): + """A minimal AirStack checkout containing only what `airstack module` needs.""" + sb = tmp_path / "airstack" + (sb / ".airstack" / "modules").mkdir(parents=True) + (sb / "tools").mkdir() + (sb / "robot" / "ros_ws" / "src").mkdir(parents=True) + (sb / "simulation" / "isaac-sim" / "launch_scripts").mkdir(parents=True) + + shutil.copy(REPO / "airstack.sh", sb / "airstack.sh") + (sb / "airstack.sh").chmod((sb / "airstack.sh").stat().st_mode | stat.S_IXUSR) + shutil.copy(REPO / ".airstack" / "modules" / "module.sh", + sb / ".airstack" / "modules" / "module.sh") + # shared helper library (sourced explicitly by airstack.sh before the + # module loader; module.sh relies on _require_python_yaml etc.) + shutil.copy(REPO / ".airstack" / "modules" / "_lib.sh", + sb / ".airstack" / "modules" / "_lib.sh") + shutil.copy(REPO / "tools" / "module_overlay.py", sb / "tools" / "module_overlay.py") + shutil.copy(REPO / "tools" / "validate_module.py", sb / "tools" / "validate_module.py") + # (P4) sync also runs the Docker layer planner (conflict gate + plan + lock) + shutil.copy(REPO / "tools" / "compose_module_layers.py", + sb / "tools" / "compose_module_layers.py") + shutil.copytree(REPO / "common" / "module_schema", sb / "common" / "module_schema") + shutil.copytree( + REPO / FIXTURE_REL, + sb / FIXTURE_REL, + ignore=shutil.ignore_patterns("__pycache__"), + ) + + (sb / ".env").write_text('VERSION="0.19.0"\nPROJECT_NAME="airstack"\n', encoding="utf-8") + (sb / "docker-compose.yaml").write_text("services: {}\n", encoding="utf-8") + return sb + + +def run_airstack(sb, *args, check=True): + result = subprocess.run( + [str(sb / "airstack.sh"), *args], + capture_output=True, text=True, cwd=str(sb), timeout=180, + ) + out = result.stdout + result.stderr + if check: + assert result.returncode == 0, f"airstack {' '.join(args)} failed:\n{out}" + return result.returncode, out + + +def add_hello(sb): + return run_airstack(sb, "module", "add", FIXTURE_REL) + + +def read_repos(sb): + with (sb / "modules.repos").open(encoding="utf-8") as f: + return yaml.safe_load(f) + + +# ── add: repos entry, symlinks, generated compose ─────────────────────────── + +def test_add_local_module_records_x_local_entry(sandbox): + add_hello(sandbox) + data = read_repos(sandbox) + assert data["repositories"] == {} # local modules never enter the vcs list + assert data["x-local-modules"] == [ + {"name": "hello_module", "path": FIXTURE_REL} + ] + + +def test_add_local_module_links_checkout_and_colcon_overlay(sandbox): + add_hello(sandbox) + checkout = sandbox / "modules" / "hello_module" + assert checkout.is_symlink() + assert (checkout / "module.yaml").is_file() # resolves through the link + + robot_link = sandbox / ROBOT_LINK_REL + assert robot_link.is_symlink() + assert os.readlink(robot_link) == os.path.join("..", "..", "..", "..", "modules", "hello_module") + # the chain resolves on the host: link -> modules/hello_module -> fixture + assert (robot_link / "module.yaml").is_file() + + +def test_add_local_module_generates_compose_volume(sandbox): + add_hello(sandbox) + generated = sandbox / GENERATED_REL + assert generated.is_file() + compose = yaml.safe_load(generated.read_text(encoding="utf-8")) + fixture_real = os.path.realpath(sandbox / FIXTURE_REL) + expected = f"{fixture_real}:/root/AirStack/modules/hello_module:rw" + services = compose["services"] + assert services, "generated compose declares no services" + for service, definition in services.items(): + assert expected in definition["volumes"], ( + f"service {service} missing module volume; got {definition['volumes']}" + ) + + +def test_doctor_passes_after_add(sandbox): + add_hello(sandbox) + code, out = run_airstack(sandbox, "module", "doctor") + assert code == 0 + assert "overlay OK" in out + + +# ── list ───────────────────────────────────────────────────────────────────── + +def test_list_shows_module_as_valid(sandbox): + add_hello(sandbox) + _, out = run_airstack(sandbox, "module", "list") + row = next((l for l in out.splitlines() if l.startswith("hello_module")), None) + assert row is not None, f"no hello_module row in:\n{out}" + assert "ros_package" in row + assert "local" in row + assert "yes" in row # VALID column + + +# ── remove: everything is restored ─────────────────────────────────────────── + +def test_remove_cleans_all_artifacts(sandbox): + add_hello(sandbox) + run_airstack(sandbox, "module", "remove", "hello_module") + + assert not (sandbox / "modules.repos").exists() + assert not (sandbox / "modules").exists() + assert not (sandbox / ROBOT_LINK_REL).exists() + assert not os.path.lexists(sandbox / ROBOT_LINK_REL) # not even a dangling link + assert not (sandbox / "robot" / "ros_ws" / "src" / "modules").exists() + assert not (sandbox / GENERATED_REL).exists() + assert not (sandbox / ".airstack" / "generated").exists() + # the fixture source itself is untouched + assert (sandbox / FIXTURE_REL / "module.yaml").is_file() + + +def test_remove_unknown_module_fails(sandbox): + code, out = run_airstack(sandbox, "module", "remove", "no_such_module", check=False) + assert code != 0 + assert "no_such_module" in out + + +# ── pinning rules for remote adds ──────────────────────────────────────────── + +def test_url_add_without_version_fails(sandbox): + code, out = run_airstack( + sandbox, "module", "add", "https://github.com/example/asm_thing.git", + check=False, + ) + assert code != 0 + assert "--version" in out + assert not (sandbox / "modules.repos").exists() # nothing was recorded + + +@pytest.mark.parametrize("branch", ["main", "develop"]) +def test_url_add_with_branch_ref_fails(sandbox, branch): + code, out = run_airstack( + sandbox, "module", "add", "https://github.com/example/asm_thing.git", + "--version", branch, check=False, + ) + assert code != 0 + assert "branch" in out.lower() + assert not (sandbox / "modules.repos").exists() + + +# ── idempotence ────────────────────────────────────────────────────────────── + +def test_sync_twice_is_idempotent(sandbox): + add_hello(sandbox) + generated = sandbox / GENERATED_REL + compose_before = generated.read_text(encoding="utf-8") + repos_before = (sandbox / "modules.repos").read_text(encoding="utf-8") + + run_airstack(sandbox, "module", "sync") + + assert generated.read_text(encoding="utf-8") == compose_before + assert (sandbox / "modules.repos").read_text(encoding="utf-8") == repos_before + compose = yaml.safe_load(compose_before) + for definition in compose["services"].values(): + volumes = definition["volumes"] + assert len(volumes) == len(set(volumes)), f"duplicate volume entries: {volumes}" + + +def test_add_twice_keeps_single_entry(sandbox): + add_hello(sandbox) + add_hello(sandbox) + data = read_repos(sandbox) + names = [e["name"] for e in data["x-local-modules"]] + assert names == ["hello_module"] diff --git a/tests/meta/test_package_metadata_contract.py b/tests/meta/test_package_metadata_contract.py new file mode 100644 index 000000000..4a564db35 --- /dev/null +++ b/tests/meta/test_package_metadata_contract.py @@ -0,0 +1,121 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""ROS 2 package.xml metadata contract. + +Every first-party package.xml in the three ROS workspace trees +(``robot/ros_ws/src``, ``gcs/ros_ws/src``, ``common/ros_packages``) must carry +real metadata: + +* a non-empty ```` with no TODO placeholder; +* ```` entries with a real name and email — no ``todo@todo.todo``, + ``user@todo.todo``, or ``example.com`` placeholders left over from + ``ros2 pkg create``; +* a non-empty ```` tag. + +The license VALUE is deliberately not asserted: first-party packages are +BSD-3-Clause-Clear, but vendored third-party packages (mav_comm from ETH +Zurich, the rqt-derived GUI plugins, ...) keep their upstream licenses. + +Git submodules (vdb_mapping, vdb_mapping_ros2, rviz_polygon_selection_tool, +...) are excluded — their metadata belongs to their own repos — as are the +gitignored module overlay directories (``robot/ros_ws/src/modules/``). +""" +import re +import xml.etree.ElementTree as ET + +import pytest + +from harness.discovery import TESTS_DIR + +pytestmark = pytest.mark.unit + +REPO = TESTS_DIR.parent +WS_TREES = ("robot/ros_ws/src", "gcs/ros_ws/src", "common/ros_packages") + +# Substrings that mark a maintainer name/email as a ros2-pkg-create leftover. +PLACEHOLDER_PATTERNS = ("todo", "example.com") +# Directory names never containing first-party package sources. +SKIP_DIR_NAMES = {"modules", "build", "install", "log", "__pycache__"} + + +def _submodule_paths(): + """Relative paths of git submodules, parsed from .gitmodules.""" + gitmodules = REPO / ".gitmodules" + if not gitmodules.is_file(): + return [] + return re.findall(r"^\s*path\s*=\s*(\S+)", gitmodules.read_text(), re.M) + + +def _package_xmls(): + submodules = tuple(_submodule_paths()) + found = [] + for tree in WS_TREES: + root = REPO / tree + if not root.is_dir(): + continue + for path in sorted(root.rglob("package.xml")): + rel = path.relative_to(REPO).as_posix() + if any(rel.startswith(sub + "/") for sub in submodules): + continue # submodule: metadata is owned by its own repo + if any(part in SKIP_DIR_NAMES for part in path.parts): + continue # module overlays / colcon artifacts + found.append(path) + return found + + +def _pkg_ids(): + return [p.relative_to(REPO).as_posix() for p in _package_xmls()] + + +def test_workspace_trees_have_packages(): + assert _package_xmls(), ( + f"no package.xml found under {WS_TREES} — workspace layout changed?" + ) + + +@pytest.mark.parametrize("package_xml", _package_xmls(), ids=_pkg_ids()) +class TestPackageMetadata: + + def test_description_is_real(self, package_xml): + root = ET.parse(package_xml).getroot() + desc = root.find("description") + assert desc is not None, f"{package_xml}: missing " + text = "".join(desc.itertext()).strip() + assert text, f"{package_xml}: is empty" + assert "todo" not in text.lower(), ( + f"{package_xml}: is a TODO placeholder ({text!r}) — " + "write one real sentence about what the package does" + ) + + def test_maintainers_are_real(self, package_xml): + root = ET.parse(package_xml).getroot() + maintainers = root.findall("maintainer") + assert maintainers, f"{package_xml}: no tag" + for tag in maintainers: + name = (tag.text or "").strip() + email = (tag.get("email") or "").strip() + assert name and email, ( + f"{package_xml}: needs both a name and an " + f"email attribute (got name={name!r}, email={email!r})" + ) + for value in (name, email): + hits = [p for p in PLACEHOLDER_PATTERNS + if p in value.lower()] + assert not hits, ( + f"{package_xml}: maintainer {name!r} <{email}> looks " + f"like a ros2-pkg-create placeholder (matched " + f"{hits!r}) — put a real maintainer here" + ) + + def test_license_is_non_empty(self, package_xml): + root = ET.parse(package_xml).getroot() + licenses = root.findall("license") + assert licenses, f"{package_xml}: no tag" + for tag in licenses: + text = (tag.text or "").strip() + # Value deliberately unchecked: vendored packages keep upstream + # licenses; only emptiness/TODO is a contract violation. + assert text, f"{package_xml}: tag is empty" + assert "todo" not in text.lower(), ( + f"{package_xml}: is a TODO placeholder ({text!r})" + ) diff --git a/tests/meta/test_robot_name_map_contract.py b/tests/meta/test_robot_name_map_contract.py new file mode 100644 index 000000000..59ea34dfc --- /dev/null +++ b/tests/meta/test_robot_name_map_contract.py @@ -0,0 +1,112 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Robot-name-map resolution contract. + +Drives ``robot/docker/robot_name_map/resolve_robot_name.py`` as a subprocess +(exactly how ``robot/docker/.bashrc`` invokes it) against the committed +``default_robot_name_map.yaml`` and asserts the resolved name/domain pairs. + +Regression anchor for the double-digit-replica bug: the old rule +``.*robot-.*(\\d+)`` let the greedy ``.*`` swallow leading digits, so +``airstack-robot-desktop-12`` resolved to robot_2/domain 2 — a silent DDS +collision with the real robot 2. The fixed rule ``.*robot-\\D*(\\d+)`` captures +the whole trailing number. + +Also checks identity parity with the fleet resolver +(``tools/fleet/resolve_fleet.py``): for a homogeneous fleet whose robots are +robot_1..robot_N in file order, a ``airstack-robot-desktop-`` container +must land on the same robot index in both resolvers. +""" +import subprocess +import sys + +import pytest + +from harness.discovery import TESTS_DIR + +pytestmark = pytest.mark.unit + +REPO = TESTS_DIR.parent +RESOLVER = REPO / "robot" / "docker" / "robot_name_map" / "resolve_robot_name.py" +MAP_FILE = ( + REPO / "robot" / "docker" / "robot_name_map" / "default_robot_name_map.yaml" +) + + +def _resolve(container_name): + """Run the resolver as .bashrc does; return (exit_code, stdout, stderr).""" + proc = subprocess.run( + [sys.executable, str(RESOLVER), container_name, str(MAP_FILE)], + capture_output=True, text=True, timeout=30, + ) + return proc.returncode, proc.stdout, proc.stderr + + +def _parse_exports(stdout): + exports = {} + for line in stdout.splitlines(): + key, _, value = line.partition("=") + exports[key] = value + return exports + + +@pytest.mark.parametrize( + "container_name,robot_name,domain_id", + [ + ("airstack-robot-desktop-1", "robot_1", "1"), + ("airstack-robot-desktop-3", "robot_3", "3"), + # double digits: the greedy legacy rule resolved these to the LAST + # digit only (robot_0 / robot_2 / robot_1) — the collision bug + ("airstack-robot-desktop-10", "robot_10", "10"), + ("airstack-robot-desktop-12", "robot_12", "12"), + ("airstack-robot-desktop-21", "robot_21", "21"), + # bare hostname form (ROBOT_NAME_SOURCE=hostname) + ("robot-21", "robot_21", "21"), + ], +) +def test_container_names_resolve_to_full_index(container_name, robot_name, + domain_id): + code, stdout, stderr = _resolve(container_name) + assert code == 0, f"resolver failed for {container_name}: {stderr}" + exports = _parse_exports(stdout) + assert exports.get("ROBOT_NAME") == robot_name, ( + f"{container_name}: expected ROBOT_NAME={robot_name}, " + f"got {exports.get('ROBOT_NAME')!r} (stdout: {stdout!r})" + ) + assert exports.get("ROS_DOMAIN_ID") == domain_id, ( + f"{container_name}: expected ROS_DOMAIN_ID={domain_id}, " + f"got {exports.get('ROS_DOMAIN_ID')!r}" + ) + + +def test_unmatched_name_falls_through_to_catch_all(): + code, stdout, stderr = _resolve("some-unrelated-host") + assert code == 0, f"catch-all rule should match anything: {stderr}" + exports = _parse_exports(stdout) + assert exports.get("ROBOT_NAME") == "unknown_robot" + assert exports.get("ROS_DOMAIN_ID") == "0" + + +def test_fleet_resolver_identity_parity(): + """The fleet resolver's trailing-index identity rule must agree with the + legacy map for the homogeneous case (robot N in file order = robot_N).""" + fleet_dir = REPO / "tools" / "fleet" + sys.path.insert(0, str(fleet_dir)) + try: + import resolve_fleet + except Exception as exc: # pragma: no cover - environment-dependent + pytest.skip(f"tools/fleet/resolve_fleet.py not importable: {exc}") + finally: + sys.path.remove(str(fleet_dir)) + + fleet = {"robots": {f"robot_{i}": {} for i in range(1, 25)}} + for index in (1, 3, 10, 12, 21): + container = f"airstack-robot-desktop-{index}" + fleet_key = resolve_fleet.resolve_identity(fleet, container) + code, stdout, _ = _resolve(container) + assert code == 0 + legacy_name = _parse_exports(stdout)["ROBOT_NAME"] + assert fleet_key == legacy_name == f"robot_{index}", ( + f"{container}: fleet resolver says {fleet_key!r}, legacy map says " + f"{legacy_name!r} — the two identity rules must agree" + ) diff --git a/tests/meta/test_stack_layout_contract.py b/tests/meta/test_stack_layout_contract.py new file mode 100644 index 000000000..5f86d1824 --- /dev/null +++ b/tests/meta/test_stack_layout_contract.py @@ -0,0 +1,179 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Stack folder anatomy contract (RFC #379 §3, RFC #385 §1). + +Every stack directory under ``stacks/`` (ignoring ``.external``, the gitignored +home of fetched third-party stack repos) must ship the four committed anatomy +files: + +* ``modules.repos`` — YAML with a top-level ``airstack_compat`` semver range + (sibling of ``repositories:``; vcstool ignores it, our tooling reads it); +* ``launch/`` — at least one ``*.launch.xml`` entry point; +* ``docker-compose.yaml`` — valid YAML (a documented stub until module pins + arrive); +* ``README.md`` — non-trivial. + +``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 + +import pytest +import yaml + +from harness.discovery import TESTS_DIR + +pytestmark = pytest.mark.unit + +REPO = TESTS_DIR.parent +STACKS_DIR = REPO / "stacks" + + +def _stack_dirs(): + if not STACKS_DIR.is_dir(): + return [] + return sorted( + d for d in STACKS_DIR.iterdir() + if d.is_dir() and not d.name.startswith(".") + ) + + +def _stack_ids(): + return [d.name for d in _stack_dirs()] + + +def test_stacks_dir_has_reference_stacks(): + assert _stack_dirs(), ( + f"{STACKS_DIR} has no stack folders — trunk ships reference stacks " + "(full_default, ...)" + ) + + +@pytest.mark.parametrize("stack", _stack_dirs(), ids=_stack_ids()) +class TestStackAnatomy: + + def test_modules_repos(self, stack): + path = stack / "modules.repos" + assert path.is_file(), f"{stack.name}: missing modules.repos" + data = yaml.safe_load(path.read_text()) + assert isinstance(data, dict), ( + f"{stack.name}: modules.repos did not parse to a YAML mapping" + ) + compat = data.get("airstack_compat") + assert isinstance(compat, str) and compat.strip(), ( + f"{stack.name}: modules.repos needs a top-level airstack_compat " + "semver-range string (the trunk range the stack was tested " + "against — RFC #379 §3)" + ) + assert "repositories" in data, ( + f"{stack.name}: modules.repos needs a repositories: key " + "(vcstool format; {} when the stack pins no modules)" + ) + + def test_launch_entry_points(self, stack): + launch_dir = stack / "launch" + assert launch_dir.is_dir(), f"{stack.name}: missing launch/ dir" + entries = list(launch_dir.glob("*.launch.xml")) + assert entries, ( + f"{stack.name}: launch/ has no *.launch.xml entry point " + "(unsplit stacks have exactly stack.launch.xml)" + ) + + def test_docker_compose_is_valid_yaml(self, stack): + path = stack / "docker-compose.yaml" + assert path.is_file(), f"{stack.name}: missing docker-compose.yaml" + data = yaml.safe_load(path.read_text()) + assert data is not None, ( + f"{stack.name}: docker-compose.yaml is empty — keep at least " + "'services: {}' plus the explanatory comments" + ) + + def test_readme_non_trivial(self, stack): + path = stack / "README.md" + assert path.is_file(), f"{stack.name}: missing README.md" + text = path.read_text().strip() + assert len(text) >= 200, ( + f"{stack.name}: README.md is trivial ({len(text)} chars) — state " + "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" + if not path.exists(): + pytest.skip(f"{stack.name}: wiring.md not committed yet " + "(generated by the first wiring-snapshot run)") + import wiring_snapshot as ws + try: + graph = ws.extract_graph_from_md(path.read_text()) + except ValueError as exc: + pytest.fail( + f"{stack.name}: wiring.md lacks a parseable wiring-graph-v1 " + f"trailer ({exc}) — regenerate it from a wiring-snapshot " + "run, never hand-edit" + ) + assert graph.get("nodes") is not None, ( + f"{stack.name}: wiring.md trailer has no nodes key — regenerate " + "it from a wiring-snapshot run" + ) + + def test_wiring_md_title_names_this_stack(self, stack): + """wiring.md's H1 must name the stack dir it lives in (a copied + baseline that still says its source stack's name is a lie — found + the hard way when full_droan_cpu shipped titled full_default).""" + path = stack / "wiring.md" + if not path.exists(): + pytest.skip(f"{stack.name}: wiring.md not committed yet " + "(generated by the first wiring-snapshot run)") + title = path.read_text(encoding="utf-8").splitlines()[0].strip() + assert title.startswith("# "), ( + f"{stack.name}: wiring.md must start with an H1 title line " + f"(got {title!r})" + ) + assert title.endswith(f" {stack.name}") or title == f"#{stack.name}", ( + f"{stack.name}: wiring.md H1 title must end with the stack dir " + f"name (got {title!r}) — regenerate via the wiring-snapshot run " + "for THIS stack, don't copy another stack's baseline" + ) + + +@pytest.mark.parametrize("stack", _stack_dirs(), ids=_stack_ids()) +def test_no_dispatcher_include(stack): + """A stack entry file must never include the dispatcher. + + robot.launch.xml is the DISPATCHER that includes the stack entry file + when AIRSTACK_STACK_DIR is set; a stack entry that wraps it recurses + infinitely (robot_1/robot_1/... namespace explosion — found the hard way + by the asm_optitrack test_stack). + """ + for entry in sorted((stack / "launch").glob("*.launch.xml")): + text = entry.read_text(encoding="utf-8") + # strip XML comments so prose warnings about the rule don't trip it + uncommented = re.sub(r"", "", text, flags=re.DOTALL) + assert "robot.launch.xml" not in uncommented, ( + f"{stack.name}/{entry.name} includes robot.launch.xml — the " + "dispatcher includes stack entries, never the reverse " + "(infinite recursion)" + ) diff --git a/tests/meta/test_wiring_snapshot_contract.py b/tests/meta/test_wiring_snapshot_contract.py new file mode 100644 index 000000000..d9770de0b --- /dev/null +++ b/tests/meta/test_wiring_snapshot_contract.py @@ -0,0 +1,253 @@ +# Copyright (c) 2024 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Contract tests for the wiring-snapshot tool (RFC #379 §4.4). + +``tests/wiring_snapshot.py`` turns ``ros2 topic info --verbose`` output into a +committed ``wiring.md`` and diffs snapshots for drift. These tests pin the +contracts the system test and the golden files depend on: the Jazzy verbose +parser, infra-noise normalization, deterministic mermaid rendering, the +render→extract round trip, drift verdicts, and the CLI exit codes. +""" +import json + +import pytest + +import wiring_snapshot as ws # noqa: E402 — pytest adds tests/ to sys.path + +pytestmark = pytest.mark.unit + + +# Realistic ROS 2 Jazzy `ros2 topic info --verbose` output: one publisher and +# one subscription, each with a full QoS block; GIDs and type hashes present +# so the parser is proven to drop them. +ODOM_INFO = """\ +Type: nav_msgs/msg/Odometry + +Publisher count: 1 + +Node name: mavros +Node namespace: /robot_1/interface/mavros +Topic type: nav_msgs/msg/Odometry +Topic type hash: RIHS01_9f3c1cb1c9e3d2b6c4a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0 +Endpoint type: PUBLISHER +GID: 01.0f.cc.d1.62.5c.9e.ac.01.00.00.00.00.00.15.03 +QoS profile: + Reliability: RELIABLE + History (Depth): KEEP_LAST (10) + Durability: VOLATILE + Lifespan: Infinite + Deadline: Infinite + Liveliness: AUTOMATIC + Liveliness lease duration: Infinite + +Subscription count: 1 + +Node name: trajectory_control_node +Node namespace: /robot_1/trajectory_controller +Topic type: nav_msgs/msg/Odometry +Topic type hash: RIHS01_9f3c1cb1c9e3d2b6c4a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0 +Endpoint type: SUBSCRIPTION +GID: 01.0f.cc.d1.62.5c.9e.ac.01.00.00.00.00.00.16.04 +QoS profile: + Reliability: BEST_EFFORT + History (Depth): UNKNOWN + Durability: VOLATILE + Lifespan: Infinite + Deadline: Infinite + Liveliness: AUTOMATIC + Liveliness lease duration: Infinite +""" + +TF_INFO = """\ +Type: tf2_msgs/msg/TFMessage + +Publisher count: 1 + +Node name: robot_state_publisher +Node namespace: /robot_1 +Topic type: tf2_msgs/msg/TFMessage +Endpoint type: PUBLISHER +GID: 01.0f.cc.d1.62.5c.9e.ac.01.00.00.00.00.00.17.03 +QoS profile: + Reliability: RELIABLE + History (Depth): KEEP_LAST (100) + Durability: VOLATILE + Lifespan: Infinite + Deadline: Infinite + Liveliness: AUTOMATIC + Liveliness lease duration: Infinite + +Subscription count: 0 +""" + +ROSOUT_INFO = """\ +Type: rcl_interfaces/msg/Log + +Publisher count: 1 + +Node name: mavros +Node namespace: /robot_1/interface/mavros +Topic type: rcl_interfaces/msg/Log +Endpoint type: PUBLISHER +GID: 01.0f.cc.d1.62.5c.9e.ac.01.00.00.00.00.00.18.03 +QoS profile: + Reliability: RELIABLE + History (Depth): KEEP_LAST (1000) + Durability: TRANSIENT_LOCAL + Lifespan: 10000000000 nanoseconds + Deadline: Infinite + Liveliness: AUTOMATIC + Liveliness lease duration: Infinite + +Subscription count: 0 +""" + + +def _sample_graph(): + """Assemble a graph the way the system test does, then normalize it.""" + topics = {} + edges = [] + for topic, text in ( + ("/robot_1/odometry", ODOM_INFO), + ("/tf", TF_INFO), + ("/rosout", ROSOUT_INFO), + ): + entry, topic_edges = ws.parse_topic_info_verbose(topic, text) + topics[topic] = entry + edges.extend(topic_edges) + nodes = [ + "/launch_ros_12345", + "/robot_1/interface/mavros/mavros", + "/robot_1/robot_state_publisher", + "/robot_1/trajectory_controller/trajectory_control_node", + "/robot_1/transform_listener_impl_55d0a1b2c3d4", + ] + return ws.normalize_graph( + {"version": 1, "nodes": nodes, "topics": topics, "edges": edges} + ) + + +def test_parser_extracts_endpoints_types_and_qos(): + entry, edges = ws.parse_topic_info_verbose("/robot_1/odometry", ODOM_INFO) + + assert entry["type"] == "nav_msgs/msg/Odometry" + # Topic-level QoS comes from the first publisher. + assert entry["qos"] == {"reliability": "RELIABLE", "durability": "VOLATILE", + "history": "KEEP_LAST", "depth": "10"} + + assert len(edges) == 2 + pub, sub = edges + assert pub["dir"] == "pub" + assert pub["node"] == "/robot_1/interface/mavros/mavros" + assert pub["type"] == "nav_msgs/msg/Odometry" + assert sub["dir"] == "sub" + assert sub["node"] == "/robot_1/trajectory_controller/trajectory_control_node" + assert sub["qos_profile"] == {"reliability": "BEST_EFFORT", + "durability": "VOLATILE", + "history": "UNKNOWN", "depth": "UNKNOWN"} + # GIDs are non-deterministic and must not leak into the model. + assert "GID" not in json.dumps(edges) + + +def test_normalize_excludes_infra_but_keeps_tf(): + graph = _sample_graph() + + assert "/rosout" not in graph["topics"] + assert not any(e["topic"] == "/rosout" for e in graph["edges"]) + # /tf and /tf_static are wiring — they stay. + assert "/tf" in graph["topics"] + # launch/CLI helper nodes (pid/hex-suffixed) are excluded. + assert "/launch_ros_12345" not in graph["nodes"] + assert "/robot_1/transform_listener_impl_55d0a1b2c3d4" not in graph["nodes"] + assert "/robot_1/robot_state_publisher" in graph["nodes"] + # Normalization is idempotent (diff_graphs re-normalizes both sides). + assert ws.normalize_graph(graph) == graph + + +def test_mermaid_render_is_deterministic_and_grouped(): + graph = _sample_graph() + first = ws.render_mermaid(graph) + second = ws.render_mermaid(graph) + + assert first == second + assert first.startswith("graph LR") + # Nodes group by the namespace segment after the robot namespace. + assert 'subgraph g0["interface"]' in first + # pub×sub edge with topic + shortened type as the label. + assert '-->|"/robot_1/odometry
Odometry"|' in first + # /tf has a publisher but no subscriber → dangling annotation node. + assert "(no subscribers)" in first + + +def test_render_extract_round_trips_graph_exactly(): + graph = _sample_graph() + meta = {"stack": "full_default", "generated-by": "contract-test", + "date": "2026-08-20 00:00:00", "sim": "isaacsim", + "num_robots": 1, "source-sha": "deadbee"} + text = ws.render_wiring_md(graph, meta) + + assert text.startswith("# Wiring snapshot: full_default") + assert "- **sim**: isaacsim" in text + assert "```mermaid" in text + assert ws.extract_graph_from_md(text) == graph + + # The JSON trailer is rendered compact (single line, no spaces) so future + # baselines stay ~1k lines; extract must round-trip it regardless. + open_i = text.index(ws._TRAILER_OPEN) + len(ws._TRAILER_OPEN) + close_i = text.index(ws._TRAILER_CLOSE) + trailer_body = text[open_i:close_i].strip("\n") + assert "\n" not in trailer_body, "trailer JSON must be one compact line" + assert trailer_body == json.dumps( + graph, sort_keys=True, separators=(",", ":") + ), "trailer JSON must use compact separators (',', ':') and sort_keys" + + +def test_diff_identical_graphs(): + graph = _sample_graph() + verdict = ws.diff_graphs(graph, graph) + + assert verdict["identical"] is True + assert verdict["missing_edges"] == [] + assert verdict["extra_edges"] == [] + assert verdict["qos_mismatches"] == [] + assert verdict["type_mismatches"] == [] + + +def test_diff_reports_removed_edge_as_missing(): + expected = _sample_graph() + observed = json.loads(json.dumps(expected)) + removed = next(e for e in observed["edges"] if e["dir"] == "sub") + observed["edges"].remove(removed) + + verdict = ws.diff_graphs(expected, observed) + + assert verdict["identical"] is False + assert verdict["missing_edges"] == [ + f"sub:{removed['node']}:{removed['topic']}" + ] + assert verdict["extra_edges"] == [] + + +def test_main_diff_exit_codes(tmp_path, capsys): + graph = _sample_graph() + expected_md = tmp_path / "wiring.md" + expected_md.write_text(ws.render_wiring_md(graph, {"stack": "full_default"})) + + same = tmp_path / "observed_same.json" + same.write_text(json.dumps(graph)) + rc = ws.main(["diff", "--expected", str(expected_md), + "--observed", str(same)]) + verdict = json.loads(capsys.readouterr().out) + assert rc == 0 + assert verdict["identical"] is True + + drifted_graph = json.loads(json.dumps(graph)) + drifted_graph["edges"] = drifted_graph["edges"][1:] + drifted = tmp_path / "observed_drifted.json" + drifted.write_text(json.dumps(drifted_graph)) + rc = ws.main(["diff", "--expected", str(expected_md), + "--observed", str(drifted)]) + verdict = json.loads(capsys.readouterr().out) + assert rc == 1 + assert verdict["identical"] is False + assert verdict["missing_edges"] diff --git a/tests/meta/test_workflow_contract.py b/tests/meta/test_workflow_contract.py new file mode 100644 index 000000000..91a4e40d1 --- /dev/null +++ b/tests/meta/test_workflow_contract.py @@ -0,0 +1,67 @@ +"""Contracts for trustworthy GitHub Actions result identity and policy.""" + +import pytest + +from harness.discovery import repo_path + +pytestmark = pytest.mark.unit + + +def _workflow() -> str: + return repo_path(".github", "workflows", "system-tests.yml").read_text() + + +def test_comment_head_resolution_never_falls_back_to_default_branch(): + workflow = _workflow() + assert "${COMMENT_HEAD_SHA:-$EVENT_SHA}" not in workflow + assert "PR head SHA was not resolved" in workflow + assert 'echo "tested_sha=$COMMENT_HEAD_SHA"' in workflow + + +def test_comment_runs_cancel_older_run_for_same_pr(): + workflow = _workflow() + assert "github.event.issue.number || github.run_id" in workflow + assert "cancel-in-progress: true" in workflow + + +def test_report_job_installs_declared_dependencies(): + # parse_metrics.py imports the tests/harness package, so the report job + # must install the full test requirements — a bare `pip install tabulate` + # crashed every report with ModuleNotFoundError: yaml (issue behind #407). + report = _workflow().split("\n report:", 1)[1] + assert "pip install -r tests/requirements.txt" in report + assert "pip install tabulate" not in report + requirements = repo_path("tests", "requirements.txt").read_text().lower() + assert "pyyaml" in requirements + assert "tabulate" in requirements + + +def test_metric_deltas_are_advisory_but_parser_errors_block(): + workflow = _workflow() + assert "- name: Fail on report integrity error" in workflow + assert "Metric regression detected" not in workflow + assert "parser_exit=2" in workflow + + +def test_tested_identity_is_written_into_campaign_metadata(): + system = _workflow() + unit = repo_path(".github", "workflows", "unit-tests.yml").read_text() + assert "AIRSTACK_TESTED_SHA: ${{ steps.identity.outputs.tested_sha }}" in system + assert "AIRSTACK_PR_NUMBER: ${{ steps.identity.outputs.pr_number }}" in system + assert "AIRSTACK_TESTED_SHA: ${{ github.sha }}" in unit + + +def test_baseline_search_downloads_candidates_then_selects_by_fingerprint(): + workflow = _workflow() + assert "-f per_page=20" in workflow + assert 'gh run download "$run_id"' in workflow + assert "select_baseline_path" in workflow + assert "dawidd6/action-download-artifact" not in workflow + + +def test_manual_campaign_can_select_minimal_algorithm_sweeps(): + workflow = _workflow() + assert "trajectory_types:" in workflow + assert "takeoff_velocities:" in workflow + assert "args.extend(['--trajectory-types', trajectories])" in workflow + assert "args.extend(['--takeoff-velocities', velocities])" in workflow diff --git a/tests/parse_metrics.py b/tests/parse_metrics.py index 9d8d77210..5ccbba549 100644 --- a/tests/parse_metrics.py +++ b/tests/parse_metrics.py @@ -3,7 +3,8 @@ between two runs when --baseline is supplied. Reads results.xml (JUnit XML) for test durations and metrics.json for custom -metrics. In diff mode, exits 1 on regression; in single mode, always exits 0. +metrics. Numeric deltas are advisory and never change the process exit status; +report-generation errors exit 2. Usage: python parse_metrics.py --current tests/results// @@ -21,7 +22,11 @@ from tabulate import tabulate -from harness.run_meta import classify_run, simulation_metrics_comparable +from harness.run_meta import ( + classify_run, + comparability_reason, + simulation_metrics_comparable, +) from harness.test_ids import canonical_test_id FLAG_SUFFIX = {"regression": " :red_circle:", "improved": " :green_circle:"} @@ -234,7 +239,9 @@ def merge_metrics(run_dir): if test_name not in merged: merged[test_name] = {} merged[test_name].update(test_metrics) - _collapse_robots(merged) + # Keep robot/container identities visible. Exact campaign fingerprints + # already require matching robot counts, so pooling replicas would hide + # asymmetric failures without improving comparability. _expand_time_series(merged) return _collapse_iterations(merged) @@ -366,14 +373,11 @@ def _score(c, b, threshold): """Compute change% and regression flag for a metric pair. Returns (change_str, flag). flag ∈ {"", "regression", "improved"}. When either entry is missing/sentinel/time-series, returns a stub with an empty flag - (except: `timeout` current after numeric baseline → regression).""" + Missing/sentinel data is never converted into a numeric regression.""" if not c or not b: return ("new" if c and not b else "removed"), "" if not _is_scored(c) or not _is_scored(b): - cv = c.get("value") if isinstance(c, dict) else None - bv = b.get("value") if isinstance(b, dict) else None - flag = "regression" if (cv == "timeout" and isinstance(bv, (int, float))) else "" - return "—", flag + return "—", "" cv, bv = c["value"], b["value"] direction = c.get("direction", "lower_is_better") change_pct = ((cv - bv) / bv) * 100 if bv != 0 else 0 @@ -608,7 +612,10 @@ def render_passrates(mod): has_regression = regressions[0] if diff_mode and has_regression: - sections.append("**Regression detected** — some metrics exceeded the threshold.") + sections.append( + "**Advisory metric changes:** some comparable metrics exceeded the " + "display threshold. These deltas do not fail CI." + ) return "\n\n".join(sections), has_regression @@ -637,6 +644,8 @@ def _non_comparable_report(meta): ) fields = [ ("Outcome", outcome), + ("Failure class", meta.get("failure_class", "unavailable")), + ("Completion state", meta.get("completion_state", "unavailable")), ("Pytest exit status", meta.get("pytest_exitstatus", "unavailable")), ("Selected tests", meta.get("selected_tests", "unavailable")), ("Completed tests", meta.get("completed_tests", "unavailable")), @@ -690,9 +699,10 @@ def generate_report(current_dir, baseline_dir=None, threshold=20): "does not apply." ) elif baseline_dir and not diff_mode: + reason = comparability_reason(current_meta, baseline_meta) notices.append( "> The baseline is not the same complete simulation campaign. " - "Showing current results without a regression comparison." + f"Showing current results without a comparison: {reason}." ) if not md: md = "_No per-test metrics were recorded._" @@ -729,7 +739,9 @@ def main(): if args.output: Path(args.output).write_text(md) - sys.exit(1 if has_regression else 0) + # Assertions and infrastructure failures are enforced by pytest/run-tests. + # Comparable numeric deltas are intentionally advisory. + sys.exit(0) if __name__ == "__main__": diff --git a/tests/pytest.ini b/tests/pytest.ini index 69538af34..2a6adce8d 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -5,11 +5,14 @@ markers = build_packages: Colcon workspace build tests integration: Cross-component integration tests (robot container + a host-side component; no sim/GPU) liveliness: Container and process health (Docker, tmux, sentinel ROS 2 nodes) + wiring: Observed wiring snapshot of the running ROS graph, drift-checked against the stack's committed stacks//wiring.md (test_wiring_snapshot.py) sensors: Sim and robot sensor topic rates, LiDAR validation, sim RTF takeoff_hover_land: End-to-end takeoff / hover / land action tests autonomy: Fixed-pattern trajectory path-tracker benchmark (test_fixed_trajectory.py) waypoint_flight: Ordered-waypoint navigation judged on the odometry track (test_waypoint_flight.py) - optitrack: OptiTrack NatNet end-to-end (sim emulator → natnet_ros2 → PX4 EV fusion) + simple_sim: Simple-sim smoke test — containers, /clock, SIM_TYPE=simple sentinel nodes; run with --sim simplesim (test_simple_sim.py) + optitrack: OptiTrack NatNet end-to-end — registered for asm_optitrack module CI (tests live in the module repo) + infrastructure: Readiness/prerequisite checks whose failures are CI environment faults testpaths = . addopts = -v --durations=0 --import-mode=importlib cache_dir = /tmp/.pytest_cache diff --git a/tests/run_summary.py b/tests/run_summary.py index 3e60e7112..a8d121a21 100644 --- a/tests/run_summary.py +++ b/tests/run_summary.py @@ -14,7 +14,7 @@ import xml.etree.ElementTree as ET from pathlib import Path -from harness.run_meta import classify_run +from harness.run_meta import classify_run, comparability_reason from harness.test_ids import canonical_test_id PARAM_RE = re.compile(r"\[(.+)\]$") @@ -298,9 +298,11 @@ def build_summary_lines(run_dir: Path) -> list[str]: "", ] if run_meta.get("outcome") not in ("simulation", "non_simulation"): - reason = run_meta.get("reason", run_meta.get("outcome", "unknown")) + reason = run_meta.get("reason") or comparability_reason(run_meta) lines.extend([ f"Run status: {run_meta.get('outcome', 'unknown')}", + f"Failure class: {run_meta.get('failure_class', 'unavailable')}", + f"Completion state: {run_meta.get('completion_state', 'unavailable')}", f"Simulation metrics are not comparable: {reason}.", "", ]) @@ -345,6 +347,22 @@ def build_summary_lines(run_dir: Path) -> list[str]: if not emitted: lines.append("(no key metrics recorded)") + per_robot: dict[str, list[str]] = {} + for name in test_names: + for key, entry in _metrics_blob(metrics, name).items(): + match = ROBOT_METRIC_RE.match(key) + if not match or match.group(1) not in {item[0] for item in schema}: + continue + robot = key.split(".", 1)[0] + per_robot.setdefault(robot, []).append( + f"{match.group(1)}={_format_value(match.group(1), entry)}" + ) + if len(per_robot) > 1: + lines.append("") + lines.append("Per-robot metrics:") + for robot, values in sorted(per_robot.items()): + lines.append(f" {robot}: {', '.join(values)}") + if n_iter > 1: lines.append("") lines.append(f"Aggregated over {n_iter} stress iterations (mean ± stddev).") diff --git a/tests/sim/README.md b/tests/sim/README.md index 46344d94b..457d8bef3 100644 --- a/tests/sim/README.md +++ b/tests/sim/README.md @@ -16,5 +16,7 @@ are not part of the onboard ROS workspace, so `colcon test` does not run them. Tests needing a GPU, a full sim, or Docker belong in [`../system/`](../system/) instead. -Currently listed: `optitrack.natnet.emulator` -([source](../../simulation/isaac-sim/extensions/optitrack.natnet.emulator/)). +Currently listed: none. The OptiTrack NatNet emulator moved to the +[asm_optitrack module](https://github.com/castacks/asm_optitrack), whose unit tests run +in the module repo's CI. Add new sim-side components under the `sim:` key when they +gain a co-located `test/` dir. diff --git a/tests/system/test_build_docker.py b/tests/system/test_build_docker.py index 556e885e5..58f1eb547 100644 --- a/tests/system/test_build_docker.py +++ b/tests/system/test_build_docker.py @@ -7,7 +7,7 @@ class TestDockerBuilds: def _build_and_record(self, service, env=None): - result = airstack_cmd("image-build", service, timeout=3600) + result = airstack_cmd("images", "build", service, timeout=3600) assert result.returncode == 0, f"{service} build failed (exit {result.returncode}):\n{read_log_tail()}" size = docker_image_size_mb(service, env=env) diff --git a/tests/system/test_liveliness.py b/tests/system/test_liveliness.py index 342e5f49a..ac0675e96 100644 --- a/tests/system/test_liveliness.py +++ b/tests/system/test_liveliness.py @@ -12,6 +12,8 @@ import pytest from conftest import ( + SimulatorHealthError, + collect_failure_diagnostics, container_running, current_test_id, docker_exec, @@ -94,6 +96,32 @@ def _check_tmux_panes(env): return True, f"all tmux panes active ({summary})" +def _check_sim_startup_process(env): + """Fast simulator-specific process/prerequisite health probe.""" + if not container_running(env["sim_container"]): + return False, f"{env['sim_container']} stopped" + ok, message = _check_tmux_panes(env) + if not ok: + return ok, message + if env["sim"] != "msairsim": + return True, message + result = docker_exec( + env["sim_container"], + "binary=${MS_AIRSIM_BINARY_PATH:-" + "/ms-airsim-env/Blocks/LinuxNoEditor/Blocks.sh}; " + "test -x \"$binary\" && " + "nvidia-smi -L >/dev/null && " + "pgrep -fa 'Blocks|AirSim|UE4' >/dev/null", + timeout=10, + ) + if result.returncode != 0: + return False, ( + "Microsoft AirSim infrastructure prerequisite failed: scene binary " + "or GPU is unavailable, or the UE4 process exited" + ) + return True, "Microsoft AirSim scene and UE4 process are healthy" + + def _check_sentinel_nodes(env): """Return (ok, msg). Expected sentinels per robot domain.""" cfg = env["cfg"] @@ -155,6 +183,7 @@ def _poll_until(predicate, timeout, interval, fail_msg): @pytest.mark.liveliness +@pytest.mark.infrastructure @pytest.mark.timeout(1800) class TestLiveliness: @@ -203,24 +232,37 @@ def ready(): @pytest.mark.dependency(name="sim_ready", depends=["sim_container"]) def test_sim_ready_time(self, airstack_env): - """Wait for first /clock message from the sim container (600s hard timeout).""" + """Wait for /clock while failing fast if the simulator process dies.""" cfg = airstack_env["cfg"] m = get_metrics() tid = current_test_id() start = airstack_env["up_started_at"] - if ( - wait_for_first_message( + try: + ready = wait_for_first_message( airstack_env["sim_container"], "/clock", domain_id=1, setup_bash=cfg["sim_setup_bash"], timeout=600, + health_check=lambda: _check_sim_startup_process(airstack_env), + health_grace=20, + ) + except SimulatorHealthError as exc: + path = collect_failure_diagnostics( + airstack_env, str(exc), current_test_id() ) - is None - ): + pytest.fail(f"{exc}; diagnostics: {path}") + if ready is None: m.record(tid, "sim_ready_duration_s", "timeout", unit="s") - pytest.fail("sim never published /clock within 600s") + path = collect_failure_diagnostics( + airstack_env, + "sim never published /clock within 600s", + current_test_id(), + ) + pytest.fail( + f"sim never published /clock within 600s; diagnostics: {path}" + ) m.record(tid, "sim_ready_duration_s", round(time.time() - start, 2), unit="s") @pytest.mark.dependency(name="tmux", depends=["containers"]) diff --git a/tests/system/test_optitrack_e2e.py b/tests/system/test_optitrack_e2e.py deleted file mode 100644 index 18530c7da..000000000 --- a/tests/system/test_optitrack_e2e.py +++ /dev/null @@ -1,299 +0,0 @@ -"""OptiTrack NatNet end-to-end (sim). - -A single dedicated bring-up that exercises the whole OptiTrack path in Isaac Sim: -the in-sim NatNet **emulator** streams rigid-body poses → ``natnet_ros2`` publishes -the drone pose → the ``vision_pose`` bridge feeds MAVROS → PX4 EKF2 fuses it. - -This brings the NatNet stack up **once** and asserts only one NatNet-specific test. -The cheap, GPU-free half of this (host emulator → ``natnet_ros2`` Hz) lives in ``tests/integration/natnet/``. - -Mark: ``optitrack``. Needs Docker + GPU + Isaac Sim license; skips cleanly when the -isaac-sim image isn't built locally. -""" -import os -import re -import time - -import pytest - -from conftest import ( # noqa: E402 — pytest adds tests/ to sys.path - airstack_cmd, - container_running, - find_container, - get_metrics, - get_robot_containers, - logger, - missing_images, - read_log_tail, - ros2_exec, - sample_hz, - wait_for_container, - wait_for_first_message, -) -from system.test_fixed_trajectory import ( - TARGET_ALTITUDE_M, - _landing_one_robot, - _run_parallel, - _takeoff_one_robot, - _trajectory_one_robot, -) - -pytestmark = pytest.mark.optitrack - -# Single-drone NatNet Isaac stack: the natnet Pegasus script spawns the emulator -# alongside PX4, and LAUNCH_NATNET=true brings up natnet_ros2 + the vision_pose / -# gp_origin / param bridges on the robot. -# -# PX4_PARAM_SET selects simulation/isaac-sim/docker/px4-params/external-vision.env, which -# switches PX4 SITL's EKF2 to mocap external vision and turns GPS, baro and range aiding -# OFF, so the OptiTrack stream is the vehicle's ONLY position source. PX4's rcS applies -# those PX4_PARAM_* entries at boot; they mirror the deployment-validated set in -# robot/ros_ws/src/perception/natnet_ros2/config/px4_params.yaml. -# -# Without it EKF2_EV_CTRL is 0, PX4 silently discards the vision and flies on sim GPS — -# which is what made the previous version of this module's fusion check vacuous. -_E2E_ENV = { - "NUM_ROBOTS": "1", - "COMPOSE_PROFILES": "desktop,isaac-sim", - "AUTOLAUNCH": "true", - "ISAAC_SIM_USE_STANDALONE": "true", - "ISAAC_SIM_SCRIPT_NAME": "example_one_px4_pegasus_natnet_launch_script.py", - "PLAY_SIM_ON_START": "true", - "LAUNCH_NATNET": "true", - # EKF2 external-vision (mocap) configuration — see comment above. Selects - # simulation/isaac-sim/docker/px4-params/external-vision.env, which turns GPS, baro - # and range aiding off, leaving mocap as the vehicle's only position source. - "PX4_PARAM_SET": "external-vision", - # Headless: no X on the CI runner. - "QT_QPA_PLATFORM": "offscreen", -} - -# The trajectory flown to prove fusion. Circle is the enforced PR gate: sustained lateral -# motion is where a wrong EV delay or a too-tight innovation gate actually shows up, which -# a stationary hover would never reveal. -_E2E_TRAJECTORY = "Circle" - -_ROBOT_PATTERN = "robot.*desktop" -_ROBOT_SETUP_BASH = "/root/AirStack/robot/ros_ws/install/setup.bash" -_ROBOT_DOMAIN = 1 -# Drone body's relative pose topic from natnet_config.yaml, namespaced per robot. -_NATNET_POSE_TOPIC = os.environ.get("NATNET_POSE_TOPIC", "perception/optitrack/drone") -_NATNET_MIN_HZ = 5.0 -# PX4 fused local position (proves EKF2 accepted the external vision). odom, not pose: -# it goes live only once EKF2 has converged and home is set, which is what PX4's arming -# preflight requires. See test_px4_ready in test_takeoff_hover_land.py — pose-era signals -# fire ~25s earlier, and arming in that window returns "failed to arm". -_PX4_LOCAL_POSE_TOPIC = "interface/mavros/local_position/odom" -# MAVROS param plugin node; hosts the FCU param table as ROS 2 parameters. Same -# service px4_param_setter reads through. -_EV_PARAM_NODE = "interface/mavros/param" -# One proves vision fusion is on, the other proves GPS aiding is off — together they -# are the precondition every test below assumes but none of them check. -_EV_EXPECTED = {"EKF2_EV_CTRL": 11, "EKF2_GPS_CTRL": 0} -# MAVROS pulls the param table lazily after FCU connect; px4_param_setter budgets -# settle_sec 10 + 30 retries for the same reason. -_EV_PARAM_TIMEOUT = 90 -# `ros2 param get` prints "Integer value is: 11" / "Double value is: 7.0". -_PARAM_VALUE_RE = re.compile(r"value is:\s*(-?[\d.]+)") -# Cold Isaac boot: Pegasus load + Play + emulator UDP connect. -_FIRST_MSG_TIMEOUT = 180 - -# Belt-and-braces behind the odom gate: EV-only convergence is slower and less predictable -# than the GPS case the autonomy suites were tuned against, and TakeoffTask does not retry -# its own ARM (takeoff_landing_task.cpp send_robot_command). -_ARM_ATTEMPTS = 5 -_ARM_RETRY_S = 2.0 -_ARM_SERVICE = "interface/robot_command" -_ARM_COMMAND = 1 # airstack_msgs/srv/RobotCommand.Request.ARM - -# The flight helpers imported from test_fixed_trajectory take an `airstack_env`-style cfg; -# `robot_setup_bash` is the only key any of them reads. -_TRAJ_CFG = {"robot_setup_bash": _ROBOT_SETUP_BASH} - - -def _arm_with_retries(container: str) -> None: - """Arm the vehicle, retrying while PX4's preflight is still rejecting it. - - Uses the same robot_command service TakeoffTask arms through, so a successful - call here leaves TakeoffTask's `is_armed_` set and it skips its own arming. - """ - service = f"/robot_{_ROBOT_DOMAIN}/{_ARM_SERVICE}" - last = "" - started = time.time() - for attempt in range(1, _ARM_ATTEMPTS + 1): - result = ros2_exec( - container, - f'timeout 10 ros2 service call {service} ' - f'airstack_msgs/srv/RobotCommand "{{command: {_ARM_COMMAND}}}"', - domain_id=_ROBOT_DOMAIN, setup_bash=_ROBOT_SETUP_BASH, timeout=20, - ) - last = (result.stdout or "") + (result.stderr or "") - if "success=True" in last.replace(" ", ""): - logger.info("armed on attempt %d/%d", attempt, _ARM_ATTEMPTS) - return - logger.info("arm attempt %d/%d refused (PX4 preflight not ready yet)", - attempt, _ARM_ATTEMPTS) - if attempt < _ARM_ATTEMPTS: - time.sleep(_ARM_RETRY_S) - - pytest.fail( - f"could not arm after {_ARM_ATTEMPTS} attempts over " - f"{time.time() - started:.0f}s — PX4 preflight still rejecting. " - "With GPS/baro/range aiding off, this means EKF2 has not converged on the " - f"vision estimate. Last response:\n{last.strip()[-400:]}" - ) - - -@pytest.fixture(scope="module") -def optitrack_sim_stack(request): - """Bring the NatNet Isaac stack up once for the module; tear it down after. - - Reuses an already-running robot-desktop container (fast local iteration); - otherwise brings the stack up. Skips when the isaac-sim image isn't built. - """ - existing = find_container(_ROBOT_PATTERN) - if existing and container_running(existing): - yield {"container": existing, "brought_up": False} - return - - missing = missing_images(env=_E2E_ENV) - if missing: - pytest.skip("isaac-sim / robot image not built locally: " + ", ".join(missing)) - - airstack_cmd("down", timeout=120, log_name="optitrack_e2e") - result = airstack_cmd("up", env_overrides=_E2E_ENV, timeout=300, log_name="optitrack_e2e") - if result.returncode != 0: - pytest.fail(f"`airstack up` (natnet isaac) failed:\n{read_log_tail('optitrack_e2e')}") - - container = wait_for_container(_ROBOT_PATTERN, timeout=180) - assert container, "robot-desktop container not Running after 180s" - try: - yield {"container": container, "brought_up": True} - finally: - airstack_cmd("down", timeout=120, log_name="optitrack_e2e") - - -def _robot_container(stack): - # robot_1 lives on the first (index-1) replica. - return get_robot_containers(_ROBOT_PATTERN)[0] if not stack["brought_up"] \ - else wait_for_container(_ROBOT_PATTERN, timeout=60) - - -class TestOptitrackE2E: - - @pytest.mark.dependency(name="natnet_pose") - def test_natnet_pose_alive(self, optitrack_sim_stack): - """Emulator → natnet_ros2 → vision_pose: the drone pose_cov streams >= 5 Hz.""" - container = _robot_container(optitrack_sim_stack) - topic = f"/robot_{_ROBOT_DOMAIN}/{_NATNET_POSE_TOPIC}/pose_cov" - - first = wait_for_first_message( - container, topic, domain_id=_ROBOT_DOMAIN, - setup_bash=_ROBOT_SETUP_BASH, timeout=_FIRST_MSG_TIMEOUT, - ) - assert first is not None, ( - f"no NatNet pose on {topic} within {_FIRST_MSG_TIMEOUT}s " - "(emulator → natnet_ros2 path down)" - ) - hz = sample_hz(container, topic, domain_id=_ROBOT_DOMAIN, - setup_bash=_ROBOT_SETUP_BASH, duration=5, window=20) - get_metrics().record("test_optitrack_e2e.natnet_pose_hz", - "natnet_pose_hz", hz if hz is not None else "none", unit="Hz") - assert hz is not None and hz >= _NATNET_MIN_HZ, \ - f"{topic} at {hz} Hz (< {_NATNET_MIN_HZ})" - - @pytest.mark.dependency(name="ev_params", depends=["natnet_pose"]) - def test_ev_params_applied(self, optitrack_sim_stack): - """The external-vision param set actually reached the FCU. - - Everything below assumes PX4_PARAM_SET=external-vision took effect. If it - silently did not, EKF2_EV_CTRL stays 0 and EKF2_GPS_CTRL stays 7, the vehicle - flies the Circle on sim GPS, and every other test here still passes. This reads - the live values back off the FCU, so it covers the whole chain: compose env_file - -> container env -> Pegasus -> PX4 rcS -> FCU. - """ - container = _robot_container(optitrack_sim_stack) - node = f"/robot_{_ROBOT_DOMAIN}/{_EV_PARAM_NODE}" - - unread = dict(_EV_EXPECTED) - actual = {} - deadline = time.time() + _EV_PARAM_TIMEOUT - while unread and time.time() < deadline: - for name in list(unread): - result = ros2_exec( - container, f"ros2 param get {node} {name}", - domain_id=_ROBOT_DOMAIN, setup_bash=_ROBOT_SETUP_BASH, timeout=20, - ) - # An unpulled param prints "Parameter not set." and still exits 0, so - # match on the value line rather than the return code. - match = _PARAM_VALUE_RE.search(result.stdout or "") - if match: - actual[name] = float(match.group(1)) - del unread[name] - if unread: - time.sleep(2.0) - - assert not unread, ( - f"{', '.join(sorted(unread))} never appeared in the MAVROS param table " - f"within {_EV_PARAM_TIMEOUT}s — the MAVROS/FCU link is down, which is a " - "different failure from a wrong parameter." - ) - wrong = {k: v for k, v in sorted(actual.items()) if v != _EV_EXPECTED[k]} - assert not wrong, ( - f"PX4 is not configured for external vision: {wrong} (expected " - f"{ {k: _EV_EXPECTED[k] for k in wrong} }). PX4_PARAM_SET=external-vision " - "did not reach the FCU, so the flight below would fly on GPS and pass anyway." - ) - logger.info("EV params confirmed on FCU: %s", actual) - - @pytest.mark.dependency(name="ev_ready", depends=["ev_params"]) - def test_px4_fuses_vision(self, optitrack_sim_stack): - """PX4 publishes local_position/odom, so EKF2 has converged and home is set. - - This only establishes that a converged estimate EXISTS — it is deliberately not - the proof that vision is being fused, because odom publishes off any aiding - source. The flight below is the proof: with GPS, baro and range aiding disabled in - _E2E_ENV, mocap is the only thing that can produce this estimate at all. - """ - container = _robot_container(optitrack_sim_stack) - topic = f"/robot_{_ROBOT_DOMAIN}/{_PX4_LOCAL_POSE_TOPIC}" - - first = wait_for_first_message( - container, topic, domain_id=_ROBOT_DOMAIN, - setup_bash=_ROBOT_SETUP_BASH, timeout=_FIRST_MSG_TIMEOUT, - ) - assert first is not None, ( - f"no PX4 local_position/odom on {topic} within {_FIRST_MSG_TIMEOUT}s — " - "EKF2 never converged or never set a home position. With GPS/baro/range " - "aiding off, that means the external-vision path never reached it." - ) - - @pytest.mark.dependency(name="ev_takeoff", depends=["ev_ready"]) - @pytest.mark.timeout(2400) - def test_takeoff(self, optitrack_sim_stack): - """Take off to TARGET_ALTITUDE_M flying on the mocap-fused estimate.""" - container = _robot_container(optitrack_sim_stack) - _arm_with_retries(container) - _run_parallel(1, lambda n: _takeoff_one_robot( - n, container, _TRAJ_CFG, TARGET_ALTITUDE_M)) - - @pytest.mark.dependency(name="ev_circle", depends=["ev_takeoff"]) - @pytest.mark.timeout(2400) - def test_circle_trajectory(self, optitrack_sim_stack): - """Fly a Circle with mocap as the only position source. - - This is the end-to-end proof: emulator → natnet_ros2 → vision_pose → MAVROS → - EKF2 → controller → airframe. Cross-track error is scored by the same code the - autonomy benchmark uses, so a mocap regression shows up as path deviation rather - than as a topic that merely exists. - """ - container = _robot_container(optitrack_sim_stack) - _run_parallel(1, lambda n: _trajectory_one_robot( - n, container, _TRAJ_CFG, _E2E_TRAJECTORY)) - - @pytest.mark.dependency(name="ev_land", depends=["ev_takeoff"]) - @pytest.mark.timeout(2400) - def test_landing(self, optitrack_sim_stack): - """Land the drone; runs even when the trajectory phase fails.""" - container = _robot_container(optitrack_sim_stack) - _run_parallel(1, lambda n: _landing_one_robot(n, container, _TRAJ_CFG)) diff --git a/tests/system/test_sensors.py b/tests/system/test_sensors.py index 8170286d6..35e212e5d 100644 --- a/tests/system/test_sensors.py +++ b/tests/system/test_sensors.py @@ -10,7 +10,14 @@ import pytest -from conftest import current_test_id, get_metrics, logger, wait_for_first_message +from conftest import ( + SimulatorHealthError, + collect_failure_diagnostics, + current_test_id, + get_metrics, + logger, + wait_for_first_message, +) from sensor_probes import ( STABLE_HZ_DURATION_S, STABLE_HZ_WINDOW, @@ -20,7 +27,11 @@ check_robot_stereo_hz, check_sim_publishing, ) -from system.test_liveliness import _check_sentinel_nodes, _poll_until +from system.test_liveliness import ( + _check_sentinel_nodes, + _check_sim_startup_process, + _poll_until, +) @pytest.mark.sensors @@ -28,24 +39,38 @@ class TestSensors: @pytest.mark.dependency(name="sensors_sim_ready") + @pytest.mark.infrastructure def test_sim_clock_available(self, airstack_env): """Wait for ``/clock`` on the sim container (same readiness gate as liveliness).""" cfg = airstack_env["cfg"] m = get_metrics() tid = current_test_id() start = airstack_env["up_started_at"] - if ( - wait_for_first_message( + try: + ready = wait_for_first_message( airstack_env["sim_container"], "/clock", domain_id=1, setup_bash=cfg["sim_setup_bash"], timeout=600, + health_check=lambda: _check_sim_startup_process(airstack_env), + health_grace=20, + ) + except SimulatorHealthError as exc: + path = collect_failure_diagnostics( + airstack_env, str(exc), current_test_id() ) - is None - ): + pytest.fail(f"{exc}; diagnostics: {path}") + if ready is None: m.record(tid, "sensors_sim_ready_duration_s", "timeout", unit="s") - pytest.fail("sim never published /clock within 600s") + path = collect_failure_diagnostics( + airstack_env, + "sim never published /clock within 600s", + current_test_id(), + ) + pytest.fail( + f"sim never published /clock within 600s; diagnostics: {path}" + ) m.record(tid, "sensors_sim_ready_duration_s", round(time.time() - start, 2), unit="s") @pytest.mark.dependency(name="sensors_nodes", depends=["sensors_sim_ready"]) diff --git a/tests/system/test_simple_sim.py b/tests/system/test_simple_sim.py new file mode 100644 index 000000000..8d525133b --- /dev/null +++ b/tests/system/test_simple_sim.py @@ -0,0 +1,158 @@ +"""Simple-sim smoke test — mark ``simple_sim``. + +Brings the stack up with ``COMPOSE_PROFILES=simple`` (``SIM_CONFIG["simplesim"]``: +the simple-robot service replaces robot-desktop, no GCS) and gates only on what +simple-sim actually provides: + +- containers: ``simple-sim`` + ``simple-robot`` Running +- sim ready: ``/clock`` published by the sim node (domain 1; also proves the + sim workspace's at-startup colcon build finished) +- the sim's mock-MAVROS odometry visible from the robot container + (cross-container DDS on the fixed SIM_IP network) +- sentinel ROS 2 nodes: ``robot_state_publisher`` + ``trajectory_control_node``. + MAVROS is deliberately NOT a sentinel — ``interface.launch.py`` skips it when + ``SIM_TYPE=simple`` (the sim node mocks the MAVROS surface itself). + +simple-sim is single-robot: its topics/services are hardcoded to ``robot_1`` on +``ROS_DOMAIN_ID=1``. Collection guards in ``harness.collection`` skip this module +unless ``--sim simplesim`` (and skip other sim campaigns' tests under it). + +Run: ``airstack test -m simple_sim --sim simplesim --num-robots 1 -v`` +""" +import time + +import pytest + +from conftest import ( + container_running, + current_test_id, + get_metrics, + get_robot_containers, + logger, + ros2_exec, + wait_for_first_message, +) + +# What actually runs per robot under SIM_TYPE=simple (full_default stack minus +# MAVROS). Kept deliberately small and honest. +SENTINEL_NODE_TEMPLATES = [ + "/robot_{N}/robot_state_publisher", + "/robot_{N}/trajectory_controller/trajectory_control_node", +] + +# Published by the sim node (MavrosMockNode) in place of real MAVROS — +# simulation/simple-sim/ros_ws/src/sim/src/sim_node.cpp. +MOCK_ODOM_TOPIC = "/robot_1/interface/mavros/local_position/odom" + + +def _poll_until(predicate, timeout, interval, fail_msg): + """Sleep-poll ``predicate`` up to ``timeout`` seconds.""" + deadline = time.time() + timeout + while time.time() < deadline: + if predicate(): + return + time.sleep(interval) + pytest.fail(fail_msg() if callable(fail_msg) else fail_msg) + + +@pytest.mark.simple_sim +@pytest.mark.timeout(1200) +class TestSimpleSimSmoke: + + @pytest.mark.dependency(name="ss_containers") + def test_containers_running(self, airstack_env): + """simple-robot and simple-sim containers Running within 120s.""" + pattern = airstack_env["robot_pattern"] + sim_container = airstack_env["sim_container"] + + def ready(): + robots = get_robot_containers(pattern) + return ( + len(robots) >= airstack_env["num_robots"] + and all(container_running(c) for c in robots) + and container_running(sim_container) + ) + + _poll_until( + ready, + timeout=120, + interval=3, + fail_msg=lambda: ( + f"simple-sim stack not Running after 120s: robots=" + f"{get_robot_containers(pattern)} sim_running=" + f"{container_running(sim_container)}" + ), + ) + + @pytest.mark.dependency(name="ss_clock", depends=["ss_containers"]) + def test_sim_publishes_clock(self, airstack_env): + """The sim node publishes /clock on domain 1 (600s hard timeout — + the sim workspace colcon-builds at container start).""" + cfg = airstack_env["cfg"] + m = get_metrics() + tid = current_test_id() + start = airstack_env["up_started_at"] + + if ( + wait_for_first_message( + airstack_env["sim_container"], + "/clock", + domain_id=1, + setup_bash=cfg["sim_setup_bash"], + timeout=600, + ) + is None + ): + m.record(tid, "sim_ready_duration_s", "timeout", unit="s") + pytest.fail("simple-sim never published /clock within 600s") + m.record(tid, "sim_ready_duration_s", round(time.time() - start, 2), unit="s") + + @pytest.mark.dependency(name="ss_odom", depends=["ss_clock"]) + def test_mock_mavros_odometry_reaches_robot(self, airstack_env): + """The sim's mock-MAVROS odometry is visible from the robot container.""" + cfg = airstack_env["cfg"] + robots = get_robot_containers(airstack_env["robot_pattern"]) + assert robots, "no simple-robot container found" + elapsed = wait_for_first_message( + robots[0], + MOCK_ODOM_TOPIC, + domain_id=1, + setup_bash=cfg["robot_setup_bash"], + timeout=180, + ) + assert elapsed is not None, ( + f"{MOCK_ODOM_TOPIC} never reached {robots[0]} within 180s" + ) + + @pytest.mark.dependency(depends=["ss_containers"]) + def test_sentinel_nodes_present(self, airstack_env): + """robot_state_publisher + trajectory_control_node up within 300s + (MAVROS intentionally absent under SIM_TYPE=simple).""" + cfg = airstack_env["cfg"] + expected = {t.format(N=1) for t in SENTINEL_NODE_TEMPLATES} + last_missing = [expected] + + def ready(): + robots = get_robot_containers(airstack_env["robot_pattern"]) + if not robots: + return False + result = ros2_exec( + robots[0], + "ros2 node list 2>/dev/null", + domain_id=1, + setup_bash=cfg["robot_setup_bash"], + timeout=20, + ) + if result.returncode != 0: + return False + missing = expected - set(result.stdout.splitlines()) + last_missing[0] = missing + return not missing + + _poll_until( + ready, + timeout=300, + interval=5, + fail_msg=lambda: f"sentinel nodes missing after 300s: {sorted(last_missing[0])}", + ) + logger.info("All %d simple-sim sentinel nodes present", len(expected)) diff --git a/tests/system/test_takeoff_hover_land.py b/tests/system/test_takeoff_hover_land.py index b9c954e9a..bf2e29d29 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 ─────────────────────────────────────────────────────────── @@ -366,7 +373,33 @@ def _run_parallel(num_robots, fn): list(ex.map(fn, range(1, num_robots + 1))) +def _wait_state_estimate_healthy(n, robot_container, cfg, budget_s=45): + """Wait for the safety monitor to report the state estimate healthy. + + px4_ready proves EKF/MAVROS signals, but the takeoff task server rejects + (or PX4 refuses arming for) goals sent before the drone_safety_monitor's + state-estimate watchdog has cleared — a race observed as 'Goal was + rejected' / 'failed to arm' right after slow sim loads. One message with + data: false = healthy. + """ + deadline = time.time() + budget_s + while time.time() < deadline: + result = ros2_exec( + robot_container, + f"timeout 8 ros2 topic echo --once " + f"/robot_{n}/behavior/drone_safety_monitor/state_estimate_timed_out " + f"2>/dev/null", + domain_id=n, setup_bash=cfg["robot_setup_bash"], timeout=20, + ) + if "data: false" in result.stdout: + return + time.sleep(2) + logger.warning("robot_%d: state-estimate watchdog not confirmed healthy " + "after %ds; sending takeoff anyway", n, budget_s) + + def _takeoff_one_robot(n, robot_container, cfg, velocity): + _wait_state_estimate_healthy(n, robot_container, cfg) timeout = _phase_timeout(velocity) target = TARGET_ALTITUDE_M streams = _start_captures(robot_container, cfg["robot_setup_bash"], diff --git a/tests/system/test_wiring_snapshot.py b/tests/system/test_wiring_snapshot.py new file mode 100644 index 000000000..fcbb1bf05 --- /dev/null +++ b/tests/system/test_wiring_snapshot.py @@ -0,0 +1,322 @@ +"""Observed wiring snapshot + golden drift check (RFC #379 §4.4). + +Runs after ``system.test_liveliness`` (see ``_MODULE_ORDER``). Once the +sentinel nodes are up, snapshots the *running* ROS graph per robot — +``ros2 node list`` + ``ros2 topic list`` + one batched ``docker exec`` of +``ros2 topic info --verbose`` probes per robot — merges it into one graph +(node/edge names keep their robot namespaces), and renders it to +``/wiring/observed_full_default.md`` via ``tests/wiring_snapshot.py``. + +Golden logic (RFC #379 §3/§4): every run compares against the launched +stack's committed wiring baseline, ``stacks//wiring.md`` (one +observed-wiring document per stack, committed in the stack folder). No +``--stack`` means the default dispatch — stacks/full_default (stacks are the +only dispatch; the legacy AUTONOMY_ROLE path was removed) — so the default +golden is ``stacks/full_default/wiring.md``. If the stack has no wiring.md +yet, the test logs a bootstrap INSTRUCTION and PASSES (baselines are +committed from a validated run — mechanism documented in +docs/development/stacks.md); if present, ``diff_graphs`` must report +identical or the test fails with the JSON drift verdict. The observed +snapshot is written as ``observed_.md`` under the run dir. +""" +import json +import os +import subprocess +import time + +import pytest + +from conftest import ( + AIRSTACK_ROOT, + ROS_DISTRO_SETUP, + current_test_id, + docker_exec, + get_metrics, + get_robot_containers, + logger, + repo_path, + ros2_exec, +) +from harness.session import run_dir +from system.test_liveliness import _check_sentinel_nodes, _poll_until + +import wiring_snapshot as ws + +pytestmark = pytest.mark.wiring + +# Topics per batched `docker exec` (each probe backgrounded into its own /tmp +# file — the parallel_sample_hz idiom). +_TOPICS_PER_EXEC = 40 +# Wall seconds each backgrounded `ros2 topic info --verbose` gets. +_INFO_TIMEOUT_S = 20 +# Node-set settle poll: snapshot only once two consecutive samples agree. +_SETTLE_TIMEOUT_S = 120 +_SETTLE_INTERVAL_S = 10 + + +def _node_list(container, domain_id, setup_bash): + result = ros2_exec( + container, + "ros2 node list 2>/dev/null", + domain_id=domain_id, + setup_bash=setup_bash, + timeout=30, + ) + return ws.parse_node_list(result.stdout) + + +def _topic_list(container, domain_id, setup_bash): + result = ros2_exec( + container, + "ros2 topic list 2>/dev/null", + domain_id=domain_id, + setup_bash=setup_bash, + timeout=30, + ) + return sorted({ + line.strip() for line in result.stdout.splitlines() + if line.strip().startswith("/") + }) + + +def _batched_topic_info(container, topics, domain_id, setup_bash): + """``ros2 topic info --verbose`` for every topic, batched per docker exec. + + One ``docker exec`` per chunk of ``_TOPICS_PER_EXEC`` topics: each probe is + backgrounded into its own /tmp file, then the files are catted back with + ``===FILE ...===`` sentinels (same idiom as ``parallel_sample_hz``). + Returns ``{topic: verbose_output_text}``. + """ + 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/wiring_{i}.out" + temp_files[topic] = fname + probes.append( + f"(ROS_DOMAIN_ID={domain_id} timeout {_INFO_TIMEOUT_S} " + f"ros2 topic info --verbose {topic} > {fname} 2>&1) &" + ) + # Newlines, not `&& ... &`: bash precedence makes `A && B && C & D &` + # only apply the && chain to C (see parallel_sample_hz). + lines = [f"source {ROS_DISTRO_SETUP}", f"source {setup_bash}"] + lines += probes + ["wait"] + for fname in temp_files.values(): + lines.append(f"echo '===FILE {fname}==='") + lines.append(f"cat {fname} 2>/dev/null || true") + result = docker_exec(container, "\n".join(lines), + timeout=_INFO_TIMEOUT_S + 60) + for piece in result.stdout.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 _wait_for_settled_node_sets(env): + """Poll per-robot node sets until two consecutive samples agree. + + Sentinel presence proves the core stack is up, but late modules can still + be joining the graph; snapshotting mid-bring-up would produce flaky drift. + Proceeds (with a warning) if the graph is still changing after the budget. + """ + cfg = env["cfg"] + containers = get_robot_containers(env["robot_pattern"]) + previous = None + deadline = time.time() + _SETTLE_TIMEOUT_S + while time.time() < deadline: + sample = tuple( + tuple(_node_list(containers[n - 1], n, cfg["robot_setup_bash"])) + for n in range(1, env["num_robots"] + 1) + ) + if sample == previous: + logger.info("Node graph settled (%d nodes total)", + sum(len(s) for s in sample)) + return + previous = sample + time.sleep(_SETTLE_INTERVAL_S) + logger.warning("Node graph still changing after %ds; snapshotting anyway", + _SETTLE_TIMEOUT_S) + + +def _capture_merged_graph(env): + """Snapshot every robot's ROS graph and merge into one normalized graph. + + Robot container index n-1 hosts ``robot_n`` on ROS_DOMAIN_ID n; node and + edge names carry their robot namespaces as-is, so the merge is a plain + union (first-seen wins for a topic observed on multiple domains, e.g. + ``/clock``). + """ + cfg = env["cfg"] + containers = get_robot_containers(env["robot_pattern"]) + assert len(containers) >= env["num_robots"], ( + f"only {len(containers)}/{env['num_robots']} robot containers visible" + ) + nodes = set() + topics = {} + edges = [] + for n in range(1, env["num_robots"] + 1): + container = containers[n - 1] + setup_bash = cfg["robot_setup_bash"] + robot_nodes = _node_list(container, n, setup_bash) + robot_topics = _topic_list(container, n, setup_bash) + logger.info("robot_%d graph: %d nodes, %d topics (%s, domain %d)", + n, len(robot_nodes), len(robot_topics), container, n) + nodes.update(robot_nodes) + infos = _batched_topic_info(container, robot_topics, n, setup_bash) + for topic in robot_topics: + text = infos.get(topic) + if not text: + logger.warning("no `topic info --verbose` output for %s " + "(robot_%d)", topic, n) + 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) + + +def _git_short_sha(): + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=AIRSTACK_ROOT, capture_output=True, text=True, timeout=10, + ) + if result.stdout.strip(): + return result.stdout.strip() + except (OSError, subprocess.TimeoutExpired): + pass + # The tests container has no git binary; read .git/HEAD directly (repo is + # mounted read-only, which is fine for reads). + try: + git_dir = os.path.join(AIRSTACK_ROOT, ".git") + head = open(os.path.join(git_dir, "HEAD")).read().strip() + if head.startswith("ref:"): + ref = head.split(None, 1)[1] + ref_path = os.path.join(git_dir, ref) + if os.path.exists(ref_path): + return open(ref_path).read().strip()[:12] + packed = os.path.join(git_dir, "packed-refs") + if os.path.exists(packed): + for line in open(packed): + if line.strip().endswith(" " + ref): + return line.split()[0][:12] + elif head: + return head[:12] + except OSError: + pass + return "unknown" + + +@pytest.mark.timeout(1800) +class TestWiringSnapshot: + + @pytest.mark.dependency(name="wiring_nodes") + def test_sentinel_nodes_present(self, airstack_env): + """Wait up to 300s for the expected sentinel nodes per robot.""" + last_msg = [""] + + def ready(): + ok, msg = _check_sentinel_nodes(airstack_env) + last_msg[0] = msg + return ok + + _poll_until( + ready, + timeout=300, + interval=5, + fail_msg=lambda: f"sentinel nodes not ready after 300s: {last_msg[0]}", + ) + + @pytest.mark.dependency(depends=["wiring_nodes"]) + def test_wiring_matches_golden(self, airstack_env): + """Snapshot the running graph, render wiring.md, diff against golden.""" + sim = airstack_env["sim"] + num_robots = airstack_env["num_robots"] + m = get_metrics() + tid = current_test_id() + + # Each stack owns its golden at stacks//wiring.md. No --stack = + # the default dispatch, stacks/full_default. + stack_name = airstack_env.get("stack") or "full_default" + + _wait_for_settled_node_sets(airstack_env) + golden_probe = repo_path("stacks", stack_name, "wiring.md") + if golden_probe.exists(): + # The settle poll proves the graph stopped GROWING, not that it is + # COMPLETE: slow-starting nodes (e.g. MAC-VO's multi-minute model + # load on a cold container) can join after two identical samples. + # When a golden exists it names the expected node set — wait for + # it; on timeout capture anyway and let the drift check report. + expected_nodes = set( + ws.extract_graph_from_md(golden_probe.read_text())["nodes"]) + deadline = time.time() + 300 + while time.time() < deadline: + live = set() + cfg = airstack_env["cfg"] + containers = get_robot_containers(airstack_env["robot_pattern"]) + for n in range(1, airstack_env["num_robots"] + 1): + live.update(_node_list(containers[n - 1], n, + cfg["robot_setup_bash"])) + missing = expected_nodes - live + if not missing: + break + logger.info("waiting for %d golden node(s) still absent " + "(e.g. %s)", len(missing), sorted(missing)[0]) + time.sleep(10) + else: + logger.warning("golden nodes still missing after 300s — " + "capturing anyway; drift will report them") + t0 = time.time() + graph = _capture_merged_graph(airstack_env) + m.record(tid, "wiring_capture_duration_s", + round(time.time() - t0, 2), unit="s") + + meta = { + "stack": stack_name, + "generated-by": "tests/system/test_wiring_snapshot.py", + "date": time.strftime("%Y-%m-%d %H:%M:%S"), + "sim": sim, + "num_robots": num_robots, + "source-sha": _git_short_sha(), + } + out_dir = run_dir() / "wiring" + out_dir.mkdir(parents=True, exist_ok=True) + observed_path = out_dir / f"observed_{stack_name}.md" + observed_path.write_text(ws.render_wiring_md(graph, meta)) + logger.info("Wrote observed wiring snapshot to %s", observed_path) + + # Counts are drift telemetry: a silently vanishing node/edge should + # read as a regression, hence higher_is_better. + m.record(tid, "wiring_node_count", len(graph["nodes"]), + unit="count", direction="higher_is_better") + m.record(tid, "wiring_edge_count", len(graph["edges"]), + unit="count", direction="higher_is_better") + m.record(tid, "wiring_topic_count", len(graph["topics"]), + unit="count", direction="higher_is_better") + + golden_path = repo_path("stacks", stack_name, "wiring.md") + if not golden_path.exists(): + logger.info( + "INSTRUCTION: no golden at %s — bootstrap by validating the " + "observed snapshot (%s) and copying it to that path, then " + "commit it (mechanism: docs/development/stacks.md).", + golden_path, observed_path, + ) + return + + expected = ws.extract_graph_from_md(golden_path.read_text()) + verdict = ws.diff_graphs(expected, graph) + if not verdict["identical"]: + pytest.fail( + f"observed wiring drifted from golden {golden_path.name} — " + "if the change is intentional, regenerate the golden from " + f"{observed_path} and commit it:\n" + + json.dumps(verdict, indent=2) + ) diff --git a/tests/wiring_snapshot.py b/tests/wiring_snapshot.py new file mode 100644 index 000000000..baaf02125 --- /dev/null +++ b/tests/wiring_snapshot.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +"""Standalone observed-wiring snapshot renderer and drift differ. + +Turns a snapshot of a *running* ROS 2 graph (``ros2 node list`` plus +``ros2 topic info --verbose`` per topic) into a committed ``wiring.md`` — a +mermaid dataflow diagram with a machine-readable JSON trailer — and diffs two +such snapshots for drift (RFC #379 §4.4: the wiring picture is observed, never +generated from configuration, so it cannot lie or rot). Used by +``tests/system/test_wiring_snapshot.py``, but deliberately dependency-free +(stdlib only) and runnable outside the AirStack harness: the input is plain +``ros2`` CLI output, so the same tool can snapshot any ROS 2 system:: + + python3 wiring_snapshot.py render --graph-json graph.json --out wiring.md \ + --meta sim=isaacsim --meta num_robots=1 + python3 wiring_snapshot.py diff --expected wiring.md --observed observed.md + +Exit code 0 iff the two graphs are identical after normalization; a JSON +verdict is printed to stdout either way. + +Graph data model (``version`` 1):: + + {"version": 1, + "nodes": ["/robot_1/...", ...], + "topics": {"/robot_1/odometry": {"type": "...", "qos": {...}}, ...}, + "edges": [{"node": ..., "topic": ..., "dir": "pub"|"sub", + "type": ..., "qos_profile": {...}}, ...]} + +QoS is normalized to ``{reliability, durability, history, depth}`` strings; +GIDs and type hashes are dropped (non-deterministic across bring-ups). +""" + +import argparse +import json +import os +import re +import sys + +# Topics that exist on every ROS 2 system and carry no wiring information. +# /tf and /tf_static are deliberately NOT excluded — frame plumbing is wiring. +DEFAULT_EXCLUDES = ("/parameter_events", "/rosout") + +# Node-name (final segment) prefixes that are per-process/per-pid artifacts of +# the launch system or CLI tooling, not stack wiring. transform_listener_impl_* +# and launch_ros_* carry hex/pid suffixes; _ros2cli* nodes are the probes +# themselves (ros2 topic info/echo spin one up); _CREATED_BY_BARE_DDS_APP_ is +# the placeholder name rmw reports for non-rclcpp DDS participants (sim +# bridges, uXRCE agents) — real endpoints, but not stack nodes. +_EXCLUDED_NODE_PREFIXES = ( + "launch_ros_", + "transform_listener_impl", + "_ros2cli", + "_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 and sim render-pipeline nodes.""" + segment = name.rsplit("/", 1)[-1] + if segment.startswith(_EXCLUDED_NODE_PREFIXES): + return True + return any(s in name for s in _EXCLUDED_NODE_SUBSTRINGS) + + +def parse_node_list(text): + """Parse ``ros2 node list`` output into a sorted list of node names.""" + return sorted({ + line.strip() for line in (text or "").splitlines() + if line.strip().startswith("/") + }) + + +def _empty_qos(): + return {"reliability": _QOS_UNKNOWN, "durability": _QOS_UNKNOWN, + "history": _QOS_UNKNOWN, "depth": _QOS_UNKNOWN} + + +def _full_node_name(namespace, name): + ns = (namespace or "/").rstrip("/") + return f"{ns}/{name}" + + +def parse_topic_info_verbose(topic_name, text): + """Parse ``ros2 topic info --verbose`` output for one topic. + + The output (ROS 2 Jazzy) is a sequence of endpoint blocks, each opened by + a ``Node name:`` line and carrying ``Node namespace:``, ``Topic type:``, + ``Endpoint type: PUBLISHER|SUBSCRIPTION``, a ``GID:`` and an indented + ``QoS profile:`` block. Unknown lines are ignored (defensive parsing); + GIDs and type hashes are dropped as non-deterministic. + + Returns ``(topic_entry, edges)`` where ``topic_entry`` is the ``topics`` + dict value ``{"type": ..., "qos": {...}}`` (QoS taken from the first + publisher, else the first endpoint) and ``edges`` is a list of edge dicts. + """ + topic_type = None + endpoints = [] + current = None + for raw in (text or "").splitlines(): + line = raw.strip() + if not line: + continue + if line.startswith("Type:") and topic_type is None: + topic_type = line.split(":", 1)[1].strip() + elif line.startswith("Node name:"): + current = {"name": line.split(":", 1)[1].strip(), "namespace": "/", + "type": None, "endpoint": None, "qos": _empty_qos()} + endpoints.append(current) + elif current is None: + continue + elif line.startswith("Node namespace:"): + current["namespace"] = line.split(":", 1)[1].strip() or "/" + elif line.startswith("Topic type:"): + current["type"] = line.split(":", 1)[1].strip() + elif line.startswith("Endpoint type:"): + current["endpoint"] = line.split(":", 1)[1].strip().upper() + elif line.startswith("Reliability:"): + current["qos"]["reliability"] = line.split(":", 1)[1].strip() + elif line.startswith("Durability:"): + current["qos"]["durability"] = line.split(":", 1)[1].strip() + elif line.startswith("History"): + m = _HISTORY_RE.match(line) + if m: + current["qos"]["history"] = m.group(1) + current["qos"]["depth"] = m.group(2) or _QOS_UNKNOWN + + dir_map = {"PUBLISHER": "pub", "SUBSCRIPTION": "sub"} + edges = [] + first_pub_qos = None + first_qos = None + for ep in endpoints: + direction = dir_map.get(ep["endpoint"]) + if direction is None: + continue + if first_qos is None: + first_qos = ep["qos"] + if direction == "pub" and first_pub_qos is None: + first_pub_qos = ep["qos"] + edges.append({ + "node": _full_node_name(ep["namespace"], ep["name"]), + "topic": topic_name, + "dir": direction, + "type": ep["type"] or topic_type, + "qos_profile": dict(ep["qos"]), + }) + topic_entry = { + "type": topic_type, + "qos": dict(first_pub_qos or first_qos or _empty_qos()), + } + return topic_entry, edges + + +def _edge_sort_key(edge): + return (edge.get("topic") or "", edge.get("dir") or "", edge.get("node") or "") + + +def normalize_graph(graph, exclude_topics=DEFAULT_EXCLUDES): + """Return a normalized copy of ``graph``: infra topics and per-pid helper + nodes removed, edges deduped, everything deterministically sorted. + + Idempotent — ``diff_graphs`` normalizes both sides, so re-normalizing an + already-normalized graph must be a no-op. + """ + excluded = set(exclude_topics) + topics = { + name: {"type": entry.get("type"), "qos": dict(entry.get("qos") or {})} + for name, entry in (graph.get("topics") or {}).items() + if name not in excluded + } + seen = set() + edges = [] + for edge in sorted(graph.get("edges") or [], key=_edge_sort_key): + node = edge.get("node") or "" + topic = edge.get("topic") or "" + if topic in excluded or _is_excluded_node(node): + continue + key = (node, topic, edge.get("dir")) + if key in seen: + continue + seen.add(key) + edges.append({ + "node": node, + "topic": topic, + "dir": edge.get("dir"), + "type": edge.get("type"), + "qos_profile": dict(edge.get("qos_profile") or {}), + }) + nodes = {n for n in (graph.get("nodes") or []) if not _is_excluded_node(n)} + nodes |= {e["node"] for e in edges} + return { + "version": graph.get("version", 1), + "nodes": sorted(nodes), + "topics": topics, + "edges": edges, + } + + +# ── mermaid rendering ────────────────────────────────────────────────────── + +def _esc(label): + """Escape mermaid-special characters inside a quoted label.""" + return str(label).replace('"', "#quot;") + + +def _short_type(type_name): + """``nav_msgs/msg/Odometry`` → ``Odometry``.""" + return (type_name or "?").rsplit("/", 1)[-1] + + +def _default_group(node): + """Subgraph for a node: the namespace segment after the robot namespace + (``/robot_1/perception/foo`` → ``perception``), the sole namespace segment + for shallow nodes (``/robot_1/foo`` → ``robot_1``), else ``root``.""" + segments = [s for s in node.split("/") if s] + if len(segments) >= 3: + return segments[1] + if len(segments) == 2: + return segments[0] + return "root" + + +def render_mermaid(graph, group_fn=None): + """Render the graph as deterministic mermaid ``graph LR`` text. + + Nodes are grouped into subgraphs by ``group_fn`` (default: namespace + segment after the robot namespace). Topics appear as edge labels + (``topic
ShortType``) with one edge per pub×sub pair; a topic with + publishers but no subscribers (or vice versa) gets a dangling stadium + annotation node so the loose end is visible. + """ + group_fn = group_fn or _default_group + nodes = sorted(set(graph.get("nodes") or []) + | {e["node"] for e in (graph.get("edges") or [])}) + ids = {n: f"n{i}" for i, n in enumerate(nodes)} + groups = {} + for n in nodes: + groups.setdefault(group_fn(n), []).append(n) + + lines = ["graph LR"] + for gi, gname in enumerate(sorted(groups)): + lines.append(f' subgraph g{gi}["{_esc(gname)}"]') + for n in groups[gname]: + lines.append(f' {ids[n]}["{_esc(n)}"]') + lines.append(" end") + + by_topic = {} + for e in graph.get("edges") or []: + info = by_topic.setdefault(e["topic"], {"pub": set(), "sub": set(), + "type": e.get("type")}) + info[e["dir"]].add(e["node"]) + + dangling = 0 + for topic in sorted(by_topic): + info = by_topic[topic] + label = _esc(f"{topic}
{_short_type(info['type'])}") + pubs = sorted(info["pub"]) + subs = sorted(info["sub"]) + if pubs and subs: + for pub in pubs: + for sub in subs: + lines.append(f' {ids[pub]} -->|"{label}"| {ids[sub]}') + elif pubs: + annotation = f'd{dangling}(["{_esc(topic)} (no subscribers)"])' + dangling += 1 + for pub in pubs: + lines.append(f' {ids[pub]} -->|"{label}"| {annotation}') + annotation = f"d{dangling - 1}" # define once, reference after + elif subs: + annotation = f'd{dangling}(["{_esc(topic)} (no publishers)"])' + dangling += 1 + for sub in subs: + lines.append(f' {annotation} -->|"{label}"| {ids[sub]}') + annotation = f"d{dangling - 1}" + return "\n".join(lines) + "\n" + + +# ── wiring.md rendering / parsing ────────────────────────────────────────── + +_TRAILER_OPEN = "" +_TRAILER_RE = re.compile( + re.escape(_TRAILER_OPEN) + r"\n(.*?)\n" + re.escape(_TRAILER_CLOSE), + re.DOTALL, +) + +# Provenance keys rendered first, in this order; extra meta keys follow sorted. +_META_ORDER = ("generated-by", "date", "sim", "num_robots", "source-sha") + + +def render_wiring_md(graph, meta): + """Render the full ``wiring.md`` text: title, provenance from ``meta``, + the mermaid fence, and a machine-readable canonical-JSON trailer that + ``extract_graph_from_md`` round-trips exactly.""" + lines = [f"# Wiring snapshot: {meta.get('stack', 'full_default')}", ""] + extra = sorted(set(meta) - set(_META_ORDER) - {"stack"}) + for key in list(_META_ORDER) + extra: + if key in meta: + lines.append(f"- **{key}**: {meta[key]}") + lines += ["", "```mermaid", render_mermaid(graph).rstrip("\n"), "```", ""] + # Compact canonical JSON (one line): keeps future baselines ~1k lines + # instead of ~7k. Drift compares parsed graphs, not bytes, so older + # indent=0 baselines stay valid until re-blessed. + canonical = json.dumps(graph, sort_keys=True, separators=(",", ":")) + lines += [_TRAILER_OPEN, canonical, _TRAILER_CLOSE, ""] + return "\n".join(lines) + + +def extract_graph_from_md(text): + """Parse the graph JSON back out of a ``wiring.md`` trailer.""" + m = _TRAILER_RE.search(text or "") + if not m: + raise ValueError( + f"no `{_TRAILER_OPEN} ... {_TRAILER_CLOSE}` trailer found" + ) + return json.loads(m.group(1)) + + +# ── drift diffing ────────────────────────────────────────────────────────── + +def _edge_key(edge): + return f'{edge["dir"]}:{edge["node"]}:{edge["topic"]}' + + +def diff_graphs(expected, observed): + """Compare two graphs in normalized form; return a drift verdict dict. + + ``verdict["identical"]`` is the pass/fail judgment; the remaining keys + name exactly what drifted (nodes, edges, per-edge QoS, per-topic types). + """ + exp = normalize_graph(expected) + obs = normalize_graph(observed) + + exp_nodes, obs_nodes = set(exp["nodes"]), set(obs["nodes"]) + exp_edges = {_edge_key(e): e for e in exp["edges"]} + obs_edges = {_edge_key(e): e for e in obs["edges"]} + + qos_mismatches = [] + for key in sorted(set(exp_edges) & set(obs_edges)): + if exp_edges[key]["qos_profile"] != obs_edges[key]["qos_profile"]: + qos_mismatches.append({ + "edge": key, + "expected": exp_edges[key]["qos_profile"], + "observed": obs_edges[key]["qos_profile"], + }) + + type_mismatches = [] + for topic in sorted(set(exp["topics"]) & set(obs["topics"])): + exp_type = exp["topics"][topic].get("type") + obs_type = obs["topics"][topic].get("type") + if exp_type != obs_type: + type_mismatches.append({ + "topic": topic, "expected": exp_type, "observed": obs_type, + }) + + verdict = { + "identical": False, + "missing_nodes": sorted(exp_nodes - obs_nodes), + "extra_nodes": sorted(obs_nodes - exp_nodes), + "missing_edges": sorted(set(exp_edges) - set(obs_edges)), + "extra_edges": sorted(set(obs_edges) - set(exp_edges)), + "qos_mismatches": qos_mismatches, + "type_mismatches": type_mismatches, + } + verdict["identical"] = not any( + verdict[k] for k in verdict if k != "identical" + ) + return verdict + + +# ── CLI ──────────────────────────────────────────────────────────────────── + +def _load_graph(path): + """Load a graph from a ``wiring.md`` (trailer) or a raw ``graph.json``.""" + with open(path) as fh: + text = fh.read() + if _TRAILER_OPEN in text: + return extract_graph_from_md(text) + return json.loads(text) + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + sub = ap.add_subparsers(dest="cmd", required=True) + + render = sub.add_parser("render", help="Render a graph JSON to wiring.md") + render.add_argument("--graph-json", required=True, + help="Path to the graph JSON (data model above)") + render.add_argument("--out", required=True, help="wiring.md output path") + render.add_argument("--meta", action="append", default=[], metavar="K=V", + help="Provenance entries, repeatable (e.g. sim=isaacsim)") + + diff = sub.add_parser("diff", help="Diff two snapshots; exit 0 iff identical") + diff.add_argument("--expected", required=True, + help="Committed wiring.md (or graph.json)") + diff.add_argument("--observed", required=True, + help="Observed wiring.md or graph.json") + + args = ap.parse_args(argv) + + if args.cmd == "render": + graph = _load_graph(args.graph_json) + meta = {} + for kv in args.meta: + key, _, value = kv.partition("=") + meta[key] = value + out_dir = os.path.dirname(os.path.abspath(args.out)) + os.makedirs(out_dir, exist_ok=True) + with open(args.out, "w") as fh: + fh.write(render_wiring_md(graph, meta)) + return 0 + + verdict = diff_graphs(_load_graph(args.expected), _load_graph(args.observed)) + print(json.dumps(verdict, indent=2)) + return 0 if verdict["identical"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/compose_module_layers.py b/tools/compose_module_layers.py new file mode 100644 index 000000000..451bc3871 --- /dev/null +++ b/tools/compose_module_layers.py @@ -0,0 +1,612 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Compose per-module Docker layers into per-host image plans (RFC #379 §6, Phase P4). + +Trunk publishes one signed base image per host type per version; modules bring +their dependencies in three tiers declared in ``module.yaml``: + +- **tier 1** — ``deps.apt`` / ``deps.pip`` lists → one generated Dockerfile + layer per module (``RUN apt-get install …`` / ``RUN pip3 install …``), so a + module's dep change invalidates only its own layer cache. +- **tier 2** — ``dockerfile:`` → the module's ``Dockerfile.module``, written + against ``ARG BASE_IMAGE``, built with BASE_IMAGE = the previous chain link. +- **tier 3** — ``overlay_image:`` → a prebuilt overlay. Used **as-is** only + when it is the sole docker-relevant module for that host; in every other + composition the module must also carry a ``dockerfile:`` (fragment = source + of truth, overlay = cache) — RFC #379 §6, failure mode 2. + +The chain is deterministic: modules sorted by name within each tier, tiers in +order 1 → 2 → 3-as-build, grouped per target host (``robot`` | ``gcs`` | +``isaac-sim`` | ``ms-airsim``). + +**Zero-module identity rule (hard requirement):** when no module contributes a +docker-relevant declaration, every host's plan is exactly +``{base_image: , steps: [], final_tag: }`` — no +``Dockerfile.composed`` is generated and no ``image:`` override is ever added +to the generated compose file. A module-free (or dep-free) checkout uses +today's images, byte-identically. + +Outputs (default mode — plan + lock, no docker calls): + +- ``.airstack/generated/layer_plan.json`` — + ``{host: {base_image, steps: [{module, tier, dockerfile|null, dep_hash}], final_tag}}`` +- ``.airstack/generated/layers//Dockerfile.composed`` — the tier-1 stage, + only for hosts that have tier-1 steps +- ``modules.lock`` at the repo root (gitignored) — per module + ``{name, pin, dep_hash, targets}`` plus ``plan_hash``; serialization is + deterministic (sorted keys/names), so identical inputs → byte-identical lock + +``final_tag`` naming extends the trunk scheme: +``${PROJECT_DOCKER_REGISTRY}/${PROJECT_NAME}:v${VERSION}_-m``. +Composed tags are **per-checkout artifacts** — they are never pushed by trunk +CI and never enter the docker-build.yml publish pipeline. + +CLI:: + + compose_module_layers.py [--project-root DIR] # plan + modules.lock (default) + compose_module_layers.py --check-conflicts [...] # static apt/pip conflict gate + compose_module_layers.py --build [...] # run the docker build chain + # + compose image overrides + # (CI/orchestrator only) + +``--check-conflicts`` is doctor hard gate #1 (RFC #379 §4): duplicate apt/pip +packages with *different* version specs across modules on the same host fail, +naming the fighting modules. Same-spec duplicates and unpinned duplicates pass. + +Exit 0 on success; 1 on a plan error or a dependency conflict. +""" +import argparse +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +import yaml + +MANIFEST_NAME = "module.yaml" +MODULES_REL = Path("modules") +REPOS_FILE_NAME = "modules.repos" +LOCK_REL = Path("modules.lock") +PLAN_REL = Path(".airstack/generated/layer_plan.json") +LAYERS_REL = Path(".airstack/generated/layers") +GENERATED_COMPOSE_REL = Path(".airstack/generated/docker-compose.modules.yaml") + +# Target hosts a module may declare (module.schema.json `targets` enum) mapped +# to the trunk image-tag suffix scheme (see robot/docker/docker-compose.yaml, +# gcs/docker/gcs-base-docker-compose.yaml, simulation/*/docker/docker-compose.yaml). +# The robot suffix carries DOCKER_IMAGE_BUILD_MODE, the others do not — that is +# the existing trunk scheme, mirrored here on purpose. +HOSTS = ("robot", "gcs", "isaac-sim", "ms-airsim") + +# Compose services that get an `image:` override under --build, per host. +# robot-l4t is deliberately absent: its image chains from robot-l4t-stack-base +# (aarch64), so pointing it at an x86-64 composed image would be wrong. Override +# with AIRSTACK_MODULE_LAYER_ROBOT_SERVICES if your checkout differs. +HOST_SERVICES = { + "robot": ("robot-desktop",), + "gcs": ("gcs",), + "isaac-sim": ("isaac-sim",), + "ms-airsim": ("ms-airsim",), +} + +COMPOSED_HEADER = """\ +# GENERATED by tools/compose_module_layers.py (`airstack module sync`) — DO NOT EDIT. +# Tier-1 module dependency layers (RFC #379 §6): one RUN per module per package +# manager, so a module's dep change invalidates only its own layer cache. +# Built with: docker build --build-arg BASE_IMAGE= … +""" + +COMPOSE_HEADER = """\ +# GENERATED by tools/module_overlay.py + tools/compose_module_layers.py — DO NOT EDIT. +# `image:` keys below were added by `compose_module_layers.py --build` and point +# services at the composed module-layer images for this checkout. Re-running +# `airstack module sync` regenerates the file without them (plan-only). +""" + + +def log(msg): + print(f"[layers] {msg}") + + +class LayerPlanError(Exception): + pass + + +# ── inputs: .env, modules.repos pins, module manifests ─────────────────────── + +def read_env(root): + """Parse the top-level .env (KEY=VALUE lines, quotes stripped, comments ignored).""" + env = {} + env_path = root / ".env" + if not env_path.is_file(): + return env + for line in env_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, raw = line.partition("=") + raw = raw.strip() + if raw[:1] in ("'", '"'): + quote = raw[0] + end = raw.find(quote, 1) + raw = raw[1:end] if end > 0 else raw[1:] + else: + raw = raw.split("#", 1)[0].strip() + env[key.strip()] = raw + return env + + +def read_pins(root): + """{module_name: pin} from modules.repos ('local' for x-local-modules entries).""" + pins = {} + repos_path = root / REPOS_FILE_NAME + if not repos_path.is_file(): + return pins + data = yaml.safe_load(repos_path.read_text(encoding="utf-8")) or {} + for name, repo in (data.get("repositories") or {}).items(): + pins[name] = str((repo or {}).get("version", "?")) + for entry in data.get("x-local-modules") or []: + pins[entry.get("name")] = "local" + return pins + + +def discover_modules(root): + """Map → manifest for every modules//module.yaml.""" + modules = {} + modules_dir = root / MODULES_REL + if not modules_dir.is_dir(): + return modules + for child in sorted(modules_dir.iterdir()): + if not child.is_dir(): # follows symlinks; dangling links land here + continue + manifest_path = child / MANIFEST_NAME + if not manifest_path.is_file(): + continue + try: + manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + raise LayerPlanError(f"{manifest_path}: invalid YAML: {exc}") + if not isinstance(manifest, dict): + raise LayerPlanError(f"{manifest_path}: manifest top level must be a mapping") + modules[child.name] = manifest + return modules + + +# ── per-module docker declarations ─────────────────────────────────────────── + +def module_decl(root, name, manifest): + """Normalize one module's docker-relevant declarations.""" + deps = manifest.get("deps") or {} + decl = { + "apt": [str(p) for p in (deps.get("apt") or [])], + "pip": [str(p) for p in (deps.get("pip") or [])], + "dockerfile": manifest.get("dockerfile") or None, + "overlay_image": manifest.get("overlay_image") or None, + "targets": [t for t in (manifest.get("targets") or []) if t in HOSTS], + } + if decl["dockerfile"]: + dockerfile_path = root / MODULES_REL / name / decl["dockerfile"] + if not dockerfile_path.is_file(): + raise LayerPlanError( + f"{name}: declared dockerfile not found: {dockerfile_path}" + ) + decl["dockerfile_bytes"] = dockerfile_path.read_bytes() + if b"BASE_IMAGE" not in decl["dockerfile_bytes"]: + log(f"warning: {name}/{decl['dockerfile']} does not reference BASE_IMAGE " + "— tier-2 fragments must build against ARG BASE_IMAGE, never a fixed base") + else: + decl["dockerfile_bytes"] = None + return decl + + +def is_docker_relevant(decl): + return bool(decl["apt"] or decl["pip"] or decl["dockerfile"] or decl["overlay_image"]) + + +def dep_hash(decl): + """sha256 over canonicalized deps + dockerfile bytes + overlay_image.""" + payload = { + "apt": sorted(decl["apt"]), + "pip": sorted(decl["pip"]), + "dockerfile": decl["dockerfile"], + "dockerfile_sha256": ( + hashlib.sha256(decl["dockerfile_bytes"]).hexdigest() + if decl["dockerfile_bytes"] is not None else None + ), + "overlay_image": decl["overlay_image"], + } + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +# ── conflict gate (doctor hard gate #1 — RFC #379 §4/§6) ───────────────────── + +_PKG_NAME_RE = re.compile(r"^([A-Za-z0-9][A-Za-z0-9._+-]*)\s*(.*)$") + + +def _split_spec(entry, manager): + """'tabulate==0.9.0' → ('tabulate', '==0.9.0'); apt 'pkg=1.2' → ('pkg', '=1.2').""" + entry = entry.strip() + if manager == "apt": + name, _, spec = entry.partition("=") + return name.strip(), ("=" + spec.strip()) if spec else "" + match = _PKG_NAME_RE.match(entry) + if not match: + return entry, "" + # PEP 503 name normalization for pip + name = re.sub(r"[-_.]+", "-", match.group(1)).lower() + return name, match.group(2).strip() + + +def find_conflicts(decls): + """Static text analysis: same package, different non-empty specs, same host. + + Returns a list of human-readable conflict strings (empty = clean). Unpinned + duplicates and identical-spec duplicates are fine — only *different* pins + on the same package fight. + """ + conflicts = [] + for host in HOSTS: + for manager in ("apt", "pip"): + by_name = {} # pkg → {spec: [module, …]} + for module in sorted(decls): + decl = decls[module] + if host not in decl["targets"]: + continue + for entry in decl[manager]: + name, spec = _split_spec(entry, manager) + by_name.setdefault(name, {}).setdefault(spec, []).append( + (module, entry) + ) + for name, specs in sorted(by_name.items()): + pinned = {s: mods for s, mods in specs.items() if s} + if len(pinned) <= 1: + continue + sides = "; ".join( + f"'{entry}' ({module})" + for spec in sorted(pinned) + for module, entry in pinned[spec] + ) + conflicts.append( + f"{manager} package '{name}' pinned differently for host " + f"'{host}': {sides}" + ) + return conflicts + + +# ── plan construction ──────────────────────────────────────────────────────── + +def base_image(host, env): + registry = env.get("PROJECT_DOCKER_REGISTRY", "airstack") + project = env.get("PROJECT_NAME", "airstack") + version = env.get("VERSION", "0.0.0") + return f"{registry}/{project}:v{version}_{host_suffix(host, env)}" + + +def host_suffix(host, env): + if host == "robot": + return f"robot-x86-64_{env.get('DOCKER_IMAGE_BUILD_MODE', 'dev')}" + return host + + +def build_plan(root, modules, env): + """Compute (plan, lock_entries, plan_hash, decls). + + plan: {host: {base_image, steps, final_tag}} for every host — hosts with no + docker-relevant modules get the identity entry (steps: [], final == base). + """ + pins = read_pins(root) + decls = {name: module_decl(root, name, manifest) for name, manifest in modules.items()} + + lock_entries = [ + { + "name": name, + "pin": pins.get(name, "local"), + "dep_hash": dep_hash(decls[name]), + "targets": sorted(decls[name]["targets"]), + } + for name in sorted(modules) + ] + plan_hash = hashlib.sha256( + json.dumps(lock_entries, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + + plan = {} + for host in HOSTS: + base = base_image(host, env) + on_host = {n: d for n, d in decls.items() if host in d["targets"]} + tier1 = sorted(n for n, d in on_host.items() if d["apt"] or d["pip"]) + tier2 = sorted(n for n, d in on_host.items() + if d["dockerfile"] and not d["overlay_image"]) + tier3 = sorted(n for n, d in on_host.items() if d["overlay_image"]) + + # Tier-3 rule (RFC #379 §6, failure mode 2): a prebuilt overlay was + # published FROM the plain trunk base, so docker cannot merge it into a + # locally-built chain. It is used as-is only when it is the sole + # docker-relevant module for this host; in any other composition its + # module must also carry a dockerfile (fragment = source of truth, + # overlay = cache) and the fragment is built in chain order. + as_is = None + for name in tier3: + if on_host[name]["dockerfile"]: + continue # tier-3-as-build: fragment is the source of truth + sole = ( + len(tier3) == 1 + and not tier1 + and not tier2 + ) + if sole: + as_is = name + else: + others = sorted(set(tier1) | set(tier2) | set(tier3) - {name}) + raise LayerPlanError( + f"host '{host}': module '{name}' declares overlay_image " + f"'{on_host[name]['overlay_image']}' without a dockerfile, but it is " + f"not the sole docker-relevant module (also composing: {', '.join(others)}). " + "A prebuilt overlay is used as-is only when it stands alone; otherwise " + "the module must also carry a Dockerfile.module — fragment = source of " + "truth, overlay = cache (RFC #379 §6)." + ) + + steps = [] + for name in tier1: + steps.append({"module": name, "tier": 1, "dockerfile": None, + "dep_hash": dep_hash(on_host[name])}) + for name in tier2: + steps.append({"module": name, "tier": 2, + "dockerfile": str(MODULES_REL / name / on_host[name]["dockerfile"]), + "dep_hash": dep_hash(on_host[name])}) + for name in tier3: + decl = on_host[name] + steps.append({"module": name, "tier": 3, + "dockerfile": (str(MODULES_REL / name / decl["dockerfile"]) + if decl["dockerfile"] else None), + "dep_hash": dep_hash(decl)}) + + if not steps: + final_tag = base # identity: base images unchanged + elif as_is is not None: + final_tag = on_host[as_is]["overlay_image"] # pulled, never rebuilt + else: + final_tag = f"{base}-m{plan_hash[:8]}" + + plan[host] = {"base_image": base, "steps": steps, "final_tag": final_tag} + + return plan, lock_entries, plan_hash, decls + + +def render_composed_dockerfile(host, plan_entry, decls): + """The tier-1 stage: ARG BASE_IMAGE chain link, one RUN per module per manager.""" + lines = [COMPOSED_HEADER, "ARG BASE_IMAGE", "FROM ${BASE_IMAGE}", ""] + for step in plan_entry["steps"]: + if step["tier"] != 1: + continue + decl = decls[step["module"]] + lines.append(f"# module: {step['module']} (tier 1, dep_hash {step['dep_hash'][:12]})") + if decl["apt"]: + pkgs = " ".join(_shell_quote(p) for p in decl["apt"]) + lines.append( + "RUN apt-get update && " + f"apt-get install -y --no-install-recommends {pkgs} && " + "rm -rf /var/lib/apt/lists/*" + ) + if decl["pip"]: + pkgs = " ".join(_shell_quote(p) for p in decl["pip"]) + lines.append(f"RUN pip3 install --no-cache-dir --break-system-packages {pkgs}") + lines.append("") + return "\n".join(lines).rstrip("\n") + "\n" + + +def _shell_quote(value): + if re.fullmatch(r"[A-Za-z0-9._+=:@/-]+", value): + return value + return "'" + value.replace("'", "'\\''") + "'" + + +# ── output writing ─────────────────────────────────────────────────────────── + +def _write_if_changed(path, text): + if path.exists() and path.read_text(encoding="utf-8") == text: + return False + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return True + + +def write_outputs(root, plan, lock_entries, plan_hash, decls): + plan_path = root / PLAN_REL + lock_path = root / LOCK_REL + layers_dir = root / LAYERS_REL + + if not lock_entries: + # No modules at all — remove every generated layer artifact. + removed = False + for path in (plan_path, lock_path): + if path.exists(): + path.unlink() + removed = True + if layers_dir.is_dir(): + shutil.rmtree(layers_dir) + removed = True + for parent in (plan_path.parent,): + try: + parent.rmdir() + except OSError: + pass + if removed: + log("no modules — removed generated layer artifacts") + return + + _write_if_changed(plan_path, json.dumps(plan, indent=2, sort_keys=True) + "\n") + lock = {"modules": lock_entries, "plan_hash": plan_hash} + _write_if_changed(lock_path, json.dumps(lock, indent=2, sort_keys=True) + "\n") + + # Dockerfile.composed only for hosts with tier-1 steps; prune the rest. + wanted = {} + for host, entry in plan.items(): + if any(step["tier"] == 1 for step in entry["steps"]): + wanted[host] = render_composed_dockerfile(host, entry, decls) + if layers_dir.is_dir(): + for child in layers_dir.iterdir(): + if child.name not in wanted: + shutil.rmtree(child) if child.is_dir() else child.unlink() + try: + layers_dir.rmdir() + except OSError: + pass + for host, text in wanted.items(): + _write_if_changed(layers_dir / host / "Dockerfile.composed", text) + + +def log_summary(plan, decls): + relevant = sorted(n for n, d in decls.items() if is_docker_relevant(d)) + if not relevant: + log("module layers: 0 docker-relevant modules — base images unchanged") + return + log(f"module layers: {len(relevant)} docker-relevant module(s): {', '.join(relevant)}") + for host in HOSTS: + entry = plan[host] + if entry["steps"]: + log(f" {host}: {len(entry['steps'])} step(s) → {entry['final_tag']}") + else: + log(f" {host}: base image unchanged") + + +# ── --build: run the chain (CI/orchestrator path — needs docker) ───────────── + +def robot_services(): + raw = os.environ.get("AIRSTACK_MODULE_LAYER_ROBOT_SERVICES") + if raw: + return tuple(s.strip() for s in raw.split(",") if s.strip()) + return HOST_SERVICES["robot"] + + +def run_builds(root, plan, decls): + for host in sorted(plan): + entry = plan[host] + if not entry["steps"]: + continue + if entry["final_tag"] == _sole_overlay_ref(entry, decls): + log(f"{host}: pulling prebuilt overlay as-is: {entry['final_tag']}") + subprocess.run(["docker", "pull", entry["final_tag"]], check=True) + continue + builds = [] + composed = root / LAYERS_REL / host / "Dockerfile.composed" + if any(step["tier"] == 1 for step in entry["steps"]): + builds.append((composed, composed.parent)) + for step in entry["steps"]: + if step["dockerfile"]: + dockerfile = root / step["dockerfile"] + context = Path(os.path.realpath(root / MODULES_REL / step["module"])) + builds.append((dockerfile, context)) + current = entry["base_image"] + for index, (dockerfile, context) in enumerate(builds): + last = index == len(builds) - 1 + tag = entry["final_tag"] if last else f"{entry['final_tag']}-l{index}" + cmd = ["docker", "build", "-f", str(dockerfile), + "--build-arg", f"BASE_IMAGE={current}", "-t", tag, str(context)] + log(f"{host}: {' '.join(cmd)}") + subprocess.run(cmd, check=True) + current = tag + + +def _sole_overlay_ref(entry, decls): + if len(entry["steps"]) != 1: + return None + step = entry["steps"][0] + if step["tier"] == 3 and step["dockerfile"] is None: + return decls[step["module"]]["overlay_image"] + return None + + +def apply_compose_image_overrides(root, plan): + """Point host services at the composed tags in the generated compose override. + + Only called under --build. The plan-only path never writes `image:` keys — + that is the zero-module identity guarantee. + """ + compose_path = root / GENERATED_COMPOSE_REL + data = {} + if compose_path.exists(): + data = yaml.safe_load(compose_path.read_text(encoding="utf-8")) or {} + services = data.setdefault("services", {}) + changed = False + for host, entry in plan.items(): + if entry["final_tag"] == entry["base_image"]: + continue + host_svcs = robot_services() if host == "robot" else HOST_SERVICES[host] + for service in host_svcs: + services.setdefault(service, {})["image"] = entry["final_tag"] + changed = True + if not changed: + return + compose_path.parent.mkdir(parents=True, exist_ok=True) + compose_path.write_text( + COMPOSE_HEADER + yaml.safe_dump(data, sort_keys=False, default_flow_style=False), + encoding="utf-8", + ) + log(f"wrote image overrides into {compose_path}") + + +# ── entry point ────────────────────────────────────────────────────────────── + +def run(root, mode="plan"): + root = Path(root).resolve() + env = read_env(root) + modules = discover_modules(root) + + if mode == "check-conflicts": + decls = {name: module_decl(root, name, manifest) + for name, manifest in modules.items()} + conflicts = find_conflicts(decls) + for conflict in conflicts: + log(f"CONFLICT: {conflict}") + if conflicts: + log(f"{len(conflicts)} dependency conflict(s) — modules cannot compose " + "into one image (RFC #379 §6). Fix or align the pins above.") + return 1 + log(f"no apt/pip conflicts across {len(modules)} module(s)") + return 0 + + plan, lock_entries, plan_hash, decls = build_plan(root, modules, env) + write_outputs(root, plan, lock_entries, plan_hash, decls) + log_summary(plan, decls) + + if mode == "build": + run_builds(root, plan, decls) + apply_compose_image_overrides(root, plan) + return 0 + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--project-root", default=str(Path(__file__).resolve().parents[1]), + help="AirStack checkout root (default: parent of tools/)") + group = parser.add_mutually_exclusive_group() + group.add_argument("--check-conflicts", action="store_true", + help="report apt/pip packages pinned differently across modules; " + "exit 1 on conflict (doctor hard gate #1 — sync fails on this)") + group.add_argument("--build", action="store_true", + help="after planning, run the docker build chain and point the " + "generated compose override at the composed tags " + "(CI/orchestrator path — requires docker)") + args = parser.parse_args(argv) + + mode = "check-conflicts" if args.check_conflicts else "build" if args.build else "plan" + try: + return run(args.project_root, mode=mode) + except LayerPlanError as exc: + log(f"ERROR: {exc}") + return 1 + except subprocess.CalledProcessError as exc: + log(f"ERROR: build command failed: {exc}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/doctor/__init__.py b/tools/doctor/__init__.py new file mode 100644 index 000000000..ee51d89b0 --- /dev/null +++ b/tools/doctor/__init__.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""``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..5c3524c8f --- /dev/null +++ b/tools/doctor/checks.py @@ -0,0 +1,606 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""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 + +# NOTE: the yaml import doubles as doctor's python3+PyYAML availability check +# (the shell CLI's twin lives in .airstack/modules/_lib.sh:_require_python_yaml +# — cross-language dedupe deferred; keep the two behaviors in sync). +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, sorted (mirrors ready.sh): compose replicas + (airstack-robot-desktop-N) and fleet-generated services + (airstack-robot_N-1); ground-host tenants (gcs-robot_N) are not robots.""" + out = _docker(["ps", "--format", "{{.Names}}"]) + return sorted( + n for n in out.splitlines() + if ("-robot-" in n or "-robot_" in n) and "gcs-" not 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 container_stack_dir(container): + """The container's AIRSTACK_STACK_DIR ('' when legacy role dispatch).""" + try: + out = _docker(["exec", container, "printenv", "AIRSTACK_STACK_DIR"]) + return out.strip() + except Exception: + return "" + + +def capture_live_graph(log=print, stack=None): + """Snapshot running robots' graphs 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. + + In a heterogeneous fleet, robots run different stacks; when ``stack`` is + given, only containers whose AIRSTACK_STACK_DIR names that stack are + captured — one wiring.md describes one stack, not the fleet union.""" + 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") + if stack: + matched = [c for c in containers + if container_stack_dir(c).rstrip("/").endswith(f"/{stack}")] + skipped = [c for c in containers if c not in matched] + if skipped: + log(f"[doctor] skipping {len(skipped)} robot container(s) on other " + f"stacks: {', '.join(skipped)}") + if not matched: + raise RuntimeError( + f"no running robot container has AIRSTACK_STACK_DIR ending in " + f"/{stack} — is that stack actually up?") + containers = matched + 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, stack=stack) + + 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, stack=stack) + 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/fleet/generate_fleet_compose.py b/tools/fleet/generate_fleet_compose.py new file mode 100755 index 000000000..fc4e78e5b --- /dev/null +++ b/tools/fleet/generate_fleet_compose.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Generate per-robot compose services for a HETEROGENEOUS fleet (RFC #380 §2). + +``deploy.replicas`` can only stamp identical containers, so a fleet whose +robots differ (stack, vehicle, or split placement) needs one compose service +per robot — plus one per (ground host × offboard tenant). This tool writes +them to ``.airstack/generated/docker-compose.fleet.yaml`` (gitignored, +machine-local, regenerated on demand), which ``airstack up --fleet `` +includes automatically. + +Homogeneous fleets need NO generation: the tool detects homogeneity, says so, +and writes nothing (deploy.replicas + the fleet resolver handle them). + +Service shape: self-contained definitions — extending nothing — that copy +robot-desktop's essentials (image, command, network, GPU reservation, mounts) +with **explicit per-robot env**: ROBOT_NAME, ROS_DOMAIN_ID, AIRSTACK_STACK_DIR, +AIRSTACK_STACK_ENTRY, FLEET_CONFIG_FILE (plus VEHICLE / CALIBRATION_DIR). +Explicit ROBOT_NAME means the container's .bashrc skips name resolution +entirely (pre-set env wins — the documented contract). + +SPLIT placement: a robot with ``hosts: {offboard: }`` gets its +``onboard`` entry; the named ground host gets one service per tenant robot +running the SAME stack with ``AIRSTACK_STACK_ENTRY=offboard`` on the fleet's +``gcs_domain`` (mirroring the legacy robot-offboard service). + +Output is deterministic: a pure function of the fleet file + checkout layout +(no timestamps; host paths are absolute on purpose — relative bind sources +resolve against ambiguous bases when compose merges ``-f`` files, the same +rule ``tools/module_overlay.py`` follows). + +Bridge routers: split stacks placed by the fleet (resolve-aware, so +``/`` external stacks are covered) need their bridge-derived +DDS-router configs materialized before launch — the onboard entry loads +``.airstack/generated/dds_router..yaml`` and fails fast without it. +This tool generates them alongside the compose file, so both +``airstack fleet generate`` and ``airstack up --fleet`` share ONE pipeline. + +CLI:: + + generate_fleet_compose.py [--project-root DIR] [--out PATH] + [--check-homogeneous] [--dry-run] + +Exit 0 always on success; ``--check-homogeneous`` prints ``homogeneous`` or +``heterogeneous`` and writes nothing; ``--dry-run`` prints what WOULD be +generated and writes nothing. +""" +import argparse +import importlib.util +import sys +from pathlib import Path + +import yaml + +_TOOLS_FLEET_DIR = Path(__file__).resolve().parent +if str(_TOOLS_FLEET_DIR) not in sys.path: + sys.path.insert(0, str(_TOOLS_FLEET_DIR)) + +from resolve_fleet import ( # noqa: E402 + FleetError, + fleet_is_homogeneous, + load_fleet, + project_root_of, + resolve_fleet, + validate_fleet, +) + + +def _load_gen_dds_router(): + """Import tools/gen_dds_router.py relative to THIS file (not the project + root argument), so synthetic checkout roots still find it.""" + path = _TOOLS_FLEET_DIR.parent / "gen_dds_router.py" + spec = importlib.util.spec_from_file_location("airstack_gen_dds_router", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + +GENERATED_REL = Path(".airstack/generated/docker-compose.fleet.yaml") +CONTAINER_ROOT = "/root/AirStack" + +HEADER = """\ +# GENERATED by tools/fleet/generate_fleet_compose.py — DO NOT EDIT (RFC #380 §2). +# Per-robot services for a heterogeneous fleet: deploy.replicas can only stamp +# identical containers, so each robot (and each ground host's offboard tenant) +# gets its own self-contained service with explicit identity/placement env. +# Regenerate: airstack fleet generate (airstack up --fleet +# regenerates and includes this file automatically; profile: fleet). +# Host paths are absolute on purpose: relative bind sources resolve against +# ambiguous bases when compose merges -f files (same rule as the module +# overlay compose). +""" + +# The robot-desktop bring-up command, copied verbatim (docker-compose.yaml is +# the source of truth; every fleet service sets an explicit stack dir/entry). +ROBOT_COMMAND = ( + "bash -c \" " + "if [ -z \\\"$$DISPLAY\\\" ] && command -v Xvfb >/dev/null 2>&1; then " + "tmux new -d -s xvfb 'Xvfb :99 -screen 0 1280x720x24 -ac +extension GLX +render -noreset 2>&1 | tee /tmp/xvfb.log'; " + "export DISPLAY=:99; " + "for i in 1 2 3 4 5 6 7 8 9 10; do [ -e /tmp/.X11-unix/X99 ] && break; sleep 1; done; " + "fi; " + "service ssh restart; " + "tmux new -d -s bringup; " + "if [ $$AUTOLAUNCH == 'true' ]; then " + "tmux send-keys -t bringup:0.0 'bws && sws && ros2 launch $$LAUNCH_PACKAGE robot.launch.xml' ENTER; " + "fi; " + "sleep infinity\"" +) + + +def _base_volumes(root): + """robot_base's bind mounts (robot/docker/robot-base-docker-compose.yaml), + with host sides made absolute.""" + r = str(root) + return [ + "$HOME/.Xauthority:/.Xauthority", + "/tmp/.X11-unix:/tmp/.X11-unix", + f"{r}/robot/docker/.dev:/root/.dev:rw", + f"{r}/common/.bash_profile:/root/.bash_profile:rw", + f"{r}/robot/docker/.bashrc:/root/.bashrc:rw", + f"{r}/common/inputrc:/etc/inputrc:rw", + f"{r}/common/.tmux.conf:/root/.tmux.conf:rw", + f"{r}/robot/docker/robot_name_map:/root/AirStack/robot/docker/robot_name_map:rw", + f"{r}/common/ros_packages:/root/AirStack/robot/ros_ws/src/common:rw", + f"{r}/common/fastdds.xml:/root/AirStack/robot/ros_ws/src/fastdds.xml", + f"{r}/robot/ros_ws:/root/AirStack/robot/ros_ws:rw", + f"{r}/stacks:/root/AirStack/stacks:rw", + f"{r}/robot/bags:/bags:rw", + # fleet + vehicle configs (FLEET_CONFIG_FILE, CALIBRATION_DIR) + f"{r}/config:/root/AirStack/config:ro", + f"{r}/tools/fleet:/root/AirStack/tools/fleet:ro", + # generated artifacts split-stack entries read at launch (bridge-derived + # DDS-router configs) + f"{r}/.airstack/generated:/root/AirStack/.airstack/generated:ro", + ] + + +def _common_environment(): + """Env shared by every fleet service (robot_base + robot-desktop essentials, + interpolated from .env at compose time exactly like the originals).""" + return [ + "DISPLAY=${DISPLAY}", + "QT_X11_NO_MITSHM=1", + "QT_QPA_PLATFORM", + "RECORD_BAGS=${RECORD_BAGS}", + "LOG_CONFIG=${LOG_CONFIG:-log.yaml}", + "URDF_FILE=${URDF_FILE}", + "OFFBOARD_BASE_PORT=${OFFBOARD_BASE_PORT}", + "ONBOARD_BASE_PORT=${ONBOARD_BASE_PORT}", + "ROBOT_NAME_MAP_CONFIG_FILE=${ROBOT_NAME_MAP_CONFIG_FILE:-default_robot_name_map.yaml}", + "DEBUG_RVIZ=${DEBUG_RVIZ:-false}", + "AUTOLAUNCH=${AUTOLAUNCH:-true}", + "NVIDIA_DRIVER_CAPABILITIES=all", + "SIM_IP=${SIM_IP:-172.31.0.200}", + ] + + +def _identity_environment(robot_name, domain_id, stack_rel, entry, + fleet_container_path, vehicle, calibration_rel): + return [ + f"ROBOT_NAME={robot_name}", + f"ROS_DOMAIN_ID={domain_id}", + f"AIRSTACK_STACK_DIR={CONTAINER_ROOT}/{stack_rel}", + f"AIRSTACK_STACK_ENTRY={entry}", + f"FLEET_CONFIG_FILE={fleet_container_path}", + f"VEHICLE={vehicle}", + "CALIBRATION_DIR=" + ( + f"{CONTAINER_ROOT}/{calibration_rel}" if calibration_rel else "" + ), + ] + + +def _service_skeleton(root): + return { + "profiles": ["fleet"], + "image": "${PROJECT_DOCKER_REGISTRY}/${PROJECT_NAME}:v${VERSION}_robot-x86-64_${DOCKER_IMAGE_BUILD_MODE}", + "stdin_open": True, + "tty": True, + "privileged": True, + "command": ROBOT_COMMAND, + "networks": ["airstack_network"], + "volumes": _base_volumes(root), + "deploy": { + "resources": { + "reservations": { + "devices": [ + {"driver": "nvidia", "count": 1, "capabilities": ["gpu"]} + ] + } + } + }, + } + + +def build_compose(fleet, root, fleet_rel): + """Compose dict for a heterogeneous fleet. Raises FleetError on problems.""" + resolved = resolve_fleet(fleet, root) + fleet_container_path = f"{CONTAINER_ROOT}/{fleet_rel}" + services = {} + + for robot in resolved["robots"]: + svc = _service_skeleton(root) + svc["environment"] = _common_environment() + [ + "LAUNCH_PACKAGE=desktop_bringup", # desktop parity: adds RViz + ] + _identity_environment( + robot["robot_name"], robot["domain_id"], robot["stack"], + robot["entry"], fleet_container_path, robot["vehicle"], + robot["calibration_dir"], + ) + # Same host port ranges as robot-desktop: docker binds the first free + # port in the range per container. + svc["ports"] = ["2223-2243:22", "8767-8787:8765"] + services[robot["robot_name"]] = svc + + gcs_domain = resolved["network"]["gcs_domain"] + for host, cfg in resolved["ground"].items(): + for tenant in cfg["tenants"]: + svc = _service_skeleton(root) + # Mirrors the legacy robot-offboard service: no ssh/foxglove ports, + # autonomy_bringup (no per-robot RViz), gcs domain, offboard entry + # of the SAME split stack, serving one tenant robot. + svc["environment"] = _common_environment() + [ + "LAUNCH_PACKAGE=autonomy_bringup", + ] + _identity_environment( + tenant["robot_name"], gcs_domain, tenant["stack"], + tenant["role"], fleet_container_path, "-", "", + ) + # VEHICLE is meaningless on a ground host; drop the placeholder. + svc["environment"] = [ + e for e in svc["environment"] if e != "VEHICLE=-" + ] + services[f"{host}-{tenant['robot_name']}"] = svc + + return { + "x-airstack-fleet": fleet_rel, + "services": services, + } + + +def render(compose): + return HEADER + yaml.safe_dump( + compose, sort_keys=True, default_flow_style=False, width=100 + ) + + +def bridge_stacks(fleet, root): + """Checkout-relative dirs of every stack the fleet places that carries a + bridge.yaml (split stacks). Resolve-aware: robots' stacks come out of + ``resolve_fleet`` (which resolves ``/`` external references + via ``resolve_stack_path``), and ground tenants reuse those resolved dirs. + """ + resolved = resolve_fleet(fleet, root) + stacks = {r["stack"] for r in resolved["robots"]} + for cfg in resolved["ground"].values(): + stacks.update(t["stack"] for t in cfg["tenants"]) + return sorted(s for s in stacks if (Path(root) / s / "bridge.yaml").is_file()) + + +def generate_bridge_routers(fleet, root, dry_run=False): + """Materialize DDS-router configs for every split stack the fleet places. + + Writes ``/.airstack/generated/dds_router..yaml`` per split + stack and prints one line per config. With ``dry_run`` nothing is written. + Raises FleetError when a bridge.yaml fails validation (incl. doctor hard + gate #2 — command authority stays onboard). + """ + stacks = bridge_stacks(fleet, root) + if not stacks: + return [] + gdr = _load_gen_dds_router() + root = Path(root) + written = [] + for stack_rel in stacks: + bridge_path = root / stack_rel / "bridge.yaml" + data = gdr.load_bridge(bridge_path) + errors = gdr.validate_bridge(data) + gate_errors = [f"{e['path']}: {e['message']}" for e in errors] + if gate_errors: + raise FleetError( + f"bridge.yaml of split stack '{stack_rel}' is invalid:\n " + + "\n ".join(gate_errors) + ) + out = gdr.default_out_path(bridge_path, root, stack=(data or {}).get("stack")) + if dry_run: + print(f"Would generate DDS-router config for split stack " + f"'{stack_rel}': {out}") + else: + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text( + gdr.render_router_config(data, source_rel=f"{stack_rel}/bridge.yaml"), + encoding="utf-8", + ) + print(f"Generated DDS-router config for split stack " + f"'{stack_rel}': {out}") + written.append(out) + return written + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="Generate per-robot compose services for a heterogeneous " + "fleet (RFC #380 §2)." + ) + parser.add_argument("fleet_file", help="path to config/fleets/.yaml") + parser.add_argument("--project-root", default=None) + parser.add_argument("--out", default=None, + help="output path (default: /.airstack/generated/" + "docker-compose.fleet.yaml)") + parser.add_argument("--check-homogeneous", action="store_true", + help="print 'homogeneous' or 'heterogeneous'; write nothing") + parser.add_argument("--dry-run", action="store_true", + help="print what would be generated (compose services " + "+ split-stack DDS-router configs); write nothing") + args = parser.parse_args(argv) + + root = Path(args.project_root) if args.project_root else project_root_of(args.fleet_file) + fleet_path = Path(args.fleet_file).resolve() + try: + fleet_rel = str(fleet_path.relative_to(root.resolve())) + except ValueError: + fleet_rel = f"config/fleets/{fleet_path.name}" + + try: + fleet = load_fleet(fleet_path) + errors = validate_fleet(fleet, root) + if errors: + for err in errors: + print(f"Error: {err}", file=sys.stderr) + return 1 + + homogeneous = fleet_is_homogeneous(fleet, root) + if args.check_homogeneous: + print("homogeneous" if homogeneous else "heterogeneous") + return 0 + if homogeneous: + n = len(fleet["robots"]) + print( + f"Fleet '{fleet_path.stem}' is homogeneous ({n} identical robot(s)) — " + f"deploy.replicas handles it: use NUM_ROBOTS={n} " + f"(airstack up --fleet {fleet_path.stem} derives it). " + f"No generation needed." + ) + return 0 + + out = Path(args.out) if args.out else root / GENERATED_REL + compose = build_compose(fleet, root, fleet_rel) + n_services = len(compose["services"]) + if args.dry_run: + print(f"Would write {out} ({n_services} service(s), profile 'fleet').") + else: + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(render(compose), encoding="utf-8") + print(f"Wrote {out} ({n_services} service(s), profile 'fleet').") + + # Split stacks the fleet places need their bridge-derived DDS-router + # configs materialized before launch (the onboard entry loads + # .airstack/generated/dds_router..yaml and fails fast without it). + generate_bridge_routers(fleet, root, dry_run=args.dry_run) + return 0 + except FleetError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/fleet/resolve_fleet.py b/tools/fleet/resolve_fleet.py new file mode 100755 index 000000000..e4db0b64c --- /dev/null +++ b/tools/fleet/resolve_fleet.py @@ -0,0 +1,472 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Resolve a robot's whole fleet entry from a fleet file (RFC #380 §2). + +The fleet-file successor of ``robot/docker/robot_name_map/resolve_robot_name.py``: +where the legacy resolver maps a container/host name to ``ROBOT_NAME`` + +``ROS_DOMAIN_ID`` only, this one resolves the **whole fleet entry** — name, +domain, stack placement (dir + entry point), vehicle, URDF, and per-unit +calibration overlay — from a ``config/fleets/*.yaml`` file. + +Opt-in: the robot container's ``.bashrc`` calls this ONLY when +``FLEET_CONFIG_FILE`` is set (``airstack up --fleet ``); otherwise the +legacy resolver runs and behavior is byte-identical to before. + +Usage (mirrors resolve_robot_name.py's eval-able stdout contract):: + + resolve_fleet.py --name airstack-robot-desktop-2 # exports + resolve_fleet.py --index 2 # exports + resolve_fleet.py --robot robot_2 # exports + resolve_fleet.py --validate # whole-fleet schema check + resolve_fleet.py --json # full resolved fleet (machine) + resolve_fleet.py --table # resolved robot table (human) + +Export mode prints ``NAME=value`` lines (eval'd by ``robot/docker/.bashrc``):: + + ROBOT_NAME=robot_1 + ROS_DOMAIN_ID=1 + AIRSTACK_STACK_DIR=/root/AirStack/stacks/full_default + AIRSTACK_STACK_ENTRY=stack + URDF_FILE=robot_descriptions/iris/urdf/iris_with_sensors.pegasus.robot.urdf + VEHICLE=quad_default + CALIBRATION_DIR= + +Identity resolution for ``--name`` (top-down, first match wins): + 1. exact robot key match (``--name wanda`` → robot ``wanda``) + 2. trailing replica/host index (``airstack-robot-desktop-2`` / ``robot-2`` + → the 2nd robot in file order) — same convention as the legacy + ``default_robot_name_map.yaml`` rule ``.*robot-\\D*(\\d+)``. + +``network.domain_policy: auto`` (the only policy implemented) assigns robot N +(1-based file order) → domain N — today's rule, so a fleet whose robots are +named ``robot_1..robot_N`` resolves identically to the legacy resolver. + +All resolution is stdlib + PyYAML; paths in exports are rooted at the fleet +file's checkout root (``/config/fleets/.yaml`` → ````), which is +``/root/AirStack`` inside robot containers and the checkout on the host. +""" +import argparse +import json +import re +import sys +from pathlib import Path + +import yaml + +DEFAULT_ENTRY = "stack" +ONBOARD_ENTRY = "onboard" +SPAWN_DEFAULT = [0.0, 0.0, 0.07] + +# Robot-level keys the schema accepts. Unknown keys are named errors so typos +# (``vehicel:``) fail loudly instead of silently applying defaults. +ROBOT_KEYS = {"vehicle", "unit", "stack", "spawn", "hosts", "overrides"} +GROUND_KEYS = {"stack"} +FLEET_KEYS = {"defaults", "robots", "ground", "sim", "network"} +NETWORK_KEYS = {"domain_policy", "gossip_domain", "gcs_domain"} + +_TRAILING_INDEX_RE = re.compile(r"(\d+)$") + + +class FleetError(Exception): + """A named fleet-file problem (schema or resolution).""" + + +# ── loading ────────────────────────────────────────────────────────────────── + +def project_root_of(fleet_path): + """Checkout root for a fleet file at ``/config/fleets/.yaml``. + + Falls back to the file's grandparent's parent regardless of naming, so a + fleet file elsewhere still resolves relative to a sensible root. + """ + return Path(fleet_path).resolve().parents[2] + + +def load_fleet(fleet_path): + path = Path(fleet_path) + if not path.is_file(): + raise FleetError(f"fleet file not found: {path}") + try: + with path.open(encoding="utf-8") as f: + data = yaml.safe_load(f) + except yaml.YAMLError as exc: + raise FleetError(f"fleet file is not valid YAML: {path}: {exc}") from exc + if not isinstance(data, dict): + raise FleetError(f"fleet file must be a YAML mapping: {path}") + return data + + +def load_vehicle(root, name): + """Load ``config/vehicles//vehicle.yaml`` under ``root``.""" + manifest = Path(root) / "config" / "vehicles" / name / "vehicle.yaml" + if not manifest.is_file(): + raise FleetError( + f"vehicle '{name}' has no manifest at config/vehicles/{name}/vehicle.yaml" + ) + with manifest.open(encoding="utf-8") as f: + data = yaml.safe_load(f) or {} + if not isinstance(data, dict): + raise FleetError(f"vehicle manifest must be a YAML mapping: {manifest}") + return data + + +# ── resolution helpers ─────────────────────────────────────────────────────── + +def _robots(fleet): + robots = fleet.get("robots") + if not isinstance(robots, dict) or not robots: + raise FleetError("fleet has no robots: — 'robots:' must be a non-empty mapping") + return list(robots.items()) + + +def resolve_stack_path(root, stack_ref): + """Resolve a fleet ``stack:`` value to a checkout-relative stack dir. + + Order (RFC #380 §3): a path in the checkout first (``stacks/``), + then ``/`` against external stack repos fetched by + ``airstack sync`` into ``stacks/.external//``. + """ + root = Path(root) + if not isinstance(stack_ref, str) or not stack_ref: + raise FleetError(f"stack reference must be a non-empty string (got {stack_ref!r})") + if (root / stack_ref).is_dir(): + return stack_ref + if "/" in stack_ref and not stack_ref.startswith("stacks/"): + alias, _, name = stack_ref.partition("/") + external = Path("stacks") / ".external" / alias / name + if (root / external).is_dir(): + return str(external) + raise FleetError( + f"stack '{stack_ref}' not found: no {stack_ref} in the checkout and no " + f"external checkout at {external} — declare the repo under 'stacks:' in " + f"airstack.yaml and run 'airstack sync'" + ) + raise FleetError(f"stack '{stack_ref}' not found under {root}") + + +def stack_entries(root, stack_rel): + launch_dir = Path(root) / stack_rel / "launch" + if not launch_dir.is_dir(): + raise FleetError(f"stack '{stack_rel}' has no launch/ directory") + return sorted( + p.name[: -len(".launch.xml")] + for p in launch_dir.glob("*.launch.xml") + ) + + +def _domain_policy(fleet): + network = fleet.get("network") or {} + if not isinstance(network, dict): + raise FleetError("'network:' must be a mapping") + policy = network.get("domain_policy", "auto") + if policy != "auto": + raise FleetError( + f"network.domain_policy '{policy}' is not implemented — only 'auto' " + f"(robot N → domain N) is" + ) + return policy + + +def resolve_robot(fleet, root, key): + """Resolve one robot's full entry. Returns a plain dict (JSON-safe).""" + robots = _robots(fleet) + keys = [k for k, _ in robots] + if key not in keys: + raise FleetError(f"no robot '{key}' in fleet (robots: {', '.join(keys)})") + index = keys.index(key) + 1 + entry = dict(robots[index - 1][1] or {}) + unknown = set(entry) - ROBOT_KEYS + if unknown: + raise FleetError( + f"robot '{key}' has unknown key(s): {', '.join(sorted(unknown))} " + f"(allowed: {', '.join(sorted(ROBOT_KEYS))})" + ) + + defaults = fleet.get("defaults") or {} + _domain_policy(fleet) # auto: robot N → domain N + + vehicle = entry.get("vehicle", defaults.get("vehicle")) + if not vehicle: + raise FleetError(f"robot '{key}' has no vehicle and the fleet declares no defaults.vehicle") + vehicle_manifest = load_vehicle(root, vehicle) + urdf = ((vehicle_manifest.get("airframe") or {}).get("base_urdf")) or "" + if not urdf: + raise FleetError(f"vehicle '{vehicle}' declares no airframe.base_urdf") + + stack_ref = entry.get("stack", defaults.get("stack")) + if not stack_ref: + raise FleetError(f"robot '{key}' has no stack and the fleet declares no defaults.stack") + stack_rel = resolve_stack_path(root, stack_ref) + entries = stack_entries(root, stack_rel) + + hosts = entry.get("hosts") or {} + if hosts and not isinstance(hosts, dict): + raise FleetError(f"robot '{key}': 'hosts:' must be a mapping of role → ground host") + ground = fleet.get("ground") or {} + if hosts: + if ONBOARD_ENTRY not in entries: + raise FleetError( + f"robot '{key}' names hosts: but stack '{stack_ref}' has no " + f"launch/onboard.launch.xml entry point (entries: {', '.join(entries)})" + ) + for role, host in hosts.items(): + if role == ONBOARD_ENTRY: + raise FleetError( + f"robot '{key}': hosts role 'onboard' is the robot itself — " + f"name only offboard roles" + ) + if role not in entries: + raise FleetError( + f"robot '{key}': hosts role '{role}' has no matching entry point " + f"launch/{role}.launch.xml in stack '{stack_ref}' " + f"(entries: {', '.join(entries)})" + ) + if host not in ground: + raise FleetError( + f"robot '{key}': hosts.{role} names ground host '{host}' but the " + f"fleet declares no ground.{host} entry " + f"(ground hosts: {', '.join(ground) or ''})" + ) + launch_entry = ONBOARD_ENTRY + else: + if DEFAULT_ENTRY not in entries: + raise FleetError( + f"robot '{key}': stack '{stack_ref}' is a split stack " + f"(entries: {', '.join(entries)}) — a robot using it must declare " + f"hosts: {{: }} placement" + ) + launch_entry = DEFAULT_ENTRY + + spawn = entry.get("spawn", SPAWN_DEFAULT) + if (not isinstance(spawn, (list, tuple)) or len(spawn) != 3 + or not all(isinstance(v, (int, float)) for v in spawn)): + raise FleetError(f"robot '{key}': spawn must be [x, y, z] numbers (got {spawn!r})") + + unit = entry.get("unit") + calibration_rel = f"config/local/calibration/{unit}" if unit else "" + + overrides = entry.get("overrides") or {} + if overrides and not isinstance(overrides, dict): + raise FleetError(f"robot '{key}': 'overrides:' must be a mapping of leaf values") + + return { + "robot_name": key, + "index": index, + "domain_id": index, # domain_policy auto: robot N → domain N + "vehicle": vehicle, + "urdf_file": urdf, + "stack": stack_rel, + "stack_ref": stack_ref, + "entry": launch_entry, + "hosts": dict(hosts), + "spawn": [float(v) for v in spawn], + "unit": unit, + "calibration_dir": calibration_rel, + "overrides": overrides, + "lidar": vehicle_has_lidar(vehicle_manifest), + } + + +def vehicle_has_lidar(vehicle_manifest): + """True when the vehicle's sensor list carries any lidar entry.""" + for sensor in vehicle_manifest.get("sensors") or []: + if isinstance(sensor, dict) and "lidar" in str(sensor.get("type", "")): + return True + return False + + +def resolve_fleet(fleet, root): + """Resolve every robot + ground host. Returns the full machine-readable view.""" + robots = [resolve_robot(fleet, root, key) for key, _ in _robots(fleet)] + ground = {} + for host, cfg in (fleet.get("ground") or {}).items(): + cfg = cfg or {} + unknown = set(cfg) - GROUND_KEYS + if unknown: + raise FleetError( + f"ground host '{host}' has unknown key(s): {', '.join(sorted(unknown))}" + ) + tenants = [ + {"robot_name": r["robot_name"], "role": role, "stack": r["stack"], + "domain_id": r["domain_id"]} + for r in robots + for role, h in r["hosts"].items() + if h == host + ] + ground[host] = {"stack": cfg.get("stack"), "tenants": tenants} + network = fleet.get("network") or {} + return { + "robots": robots, + "ground": ground, + "sim": fleet.get("sim") or {}, + "network": { + "domain_policy": network.get("domain_policy", "auto"), + "gossip_domain": network.get("gossip_domain", 99), + "gcs_domain": network.get("gcs_domain", 0), + }, + } + + +def fleet_is_homogeneous(fleet, root): + """True when deploy.replicas can stamp this fleet: every robot runs the same + vehicle and stack, none has hosts: placement, and there are no ground hosts.""" + resolved = resolve_fleet(fleet, root) + robots = resolved["robots"] + if resolved["ground"]: + return False + first = robots[0] + return all( + r["vehicle"] == first["vehicle"] + and r["stack"] == first["stack"] + and not r["hosts"] + for r in robots + ) + + +def validate_fleet(fleet, root): + """Return a list of named error strings (empty = valid).""" + errors = [] + unknown = set(fleet) - FLEET_KEYS + if unknown: + errors.append( + f"unknown top-level key(s): {', '.join(sorted(unknown))} " + f"(allowed: {', '.join(sorted(FLEET_KEYS))})" + ) + network = fleet.get("network") or {} + if isinstance(network, dict): + unknown_net = set(network) - NETWORK_KEYS + if unknown_net: + errors.append(f"unknown network key(s): {', '.join(sorted(unknown_net))}") + try: + resolve_fleet(fleet, root) + except FleetError as exc: + errors.append(str(exc)) + return errors + + +def resolve_identity(fleet, name): + """Map a container/host name to a robot key (see module docstring).""" + keys = [k for k, _ in _robots(fleet)] + if name in keys: + return name + match = _TRAILING_INDEX_RE.search(name) + if match: + index = int(match.group(1)) + if 1 <= index <= len(keys): + return keys[index - 1] + raise FleetError( + f"'{name}' resolves to index {index} but the fleet has only " + f"{len(keys)} robot(s)" + ) + raise FleetError( + f"no robot identity for '{name}': not a robot key " + f"({', '.join(keys)}) and no trailing index" + ) + + +# ── output modes ───────────────────────────────────────────────────────────── + +def print_exports(resolved, root): + root = Path(root) + stack_dir = str(root / resolved["stack"]) + cal = str(root / resolved["calibration_dir"]) if resolved["calibration_dir"] else "" + print(f"ROBOT_NAME={resolved['robot_name']}") + print(f"ROS_DOMAIN_ID={resolved['domain_id']}") + print(f"AIRSTACK_STACK_DIR={stack_dir}") + print(f"AIRSTACK_STACK_ENTRY={resolved['entry']}") + print(f"URDF_FILE={resolved['urdf_file']}") + print(f"VEHICLE={resolved['vehicle']}") + print(f"CALIBRATION_DIR={cal}") + + +def print_table(resolved_fleet): + rows = [ + ( + r["robot_name"], str(r["domain_id"]), r["vehicle"], r["stack_ref"], + r["entry"], + ",".join(f"{role}:{host}" for role, host in r["hosts"].items()) or "-", + "[" + ", ".join(f"{v:g}" for v in r["spawn"]) + "]", + ) + for r in resolved_fleet["robots"] + ] + for host, cfg in resolved_fleet["ground"].items(): + for tenant in cfg["tenants"]: + rows.append(( + f"{host} (ground)", str(resolved_fleet["network"]["gcs_domain"]), + "-", tenant["stack"], tenant["role"], + f"serves:{tenant['robot_name']}", "-", + )) + headers = ("ROBOT", "DOMAIN", "VEHICLE", "STACK", "ENTRY", "HOSTS", "SPAWN") + widths = [max(len(r[i]) for r in rows + [headers]) for i in range(len(headers))] + fmt = " ".join("{:<%d}" % w for w in widths) + print(fmt.format(*headers)) + for row in rows: + print(fmt.format(*row)) + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="Resolve robot identity/placement from a fleet file (RFC #380 §2)." + ) + parser.add_argument("fleet_file", help="path to config/fleets/.yaml") + parser.add_argument("--project-root", default=None, + help="checkout root (default: derived from the fleet file " + "path — /config/fleets/.yaml)") + who = parser.add_mutually_exclusive_group() + who.add_argument("--name", help="container or host name to resolve") + who.add_argument("--index", type=int, help="1-based robot index to resolve") + who.add_argument("--robot", help="explicit robot key to resolve") + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--validate", action="store_true", + help="validate the whole fleet; named errors, exit 1 on any") + mode.add_argument("--json", action="store_true", + help="dump the fully resolved fleet as JSON") + mode.add_argument("--table", action="store_true", + help="print the resolved robot table (human)") + args = parser.parse_args(argv) + + root = Path(args.project_root) if args.project_root else project_root_of(args.fleet_file) + + try: + fleet = load_fleet(args.fleet_file) + if args.validate: + errors = validate_fleet(fleet, root) + if errors: + for err in errors: + print(f"Error: {err}", file=sys.stderr) + return 1 + n = len(_robots(fleet)) + homogeneous = fleet_is_homogeneous(fleet, root) + print(f"OK: {n} robot(s), " + f"{'homogeneous' if homogeneous else 'heterogeneous'} fleet") + return 0 + if args.json: + print(json.dumps(resolve_fleet(fleet, root), indent=2, sort_keys=True)) + return 0 + if args.table: + print_table(resolve_fleet(fleet, root)) + return 0 + + if args.robot: + key = args.robot + elif args.index is not None: + keys = [k for k, _ in _robots(fleet)] + if not 1 <= args.index <= len(keys): + raise FleetError( + f"--index {args.index} out of range (fleet has {len(keys)} robot(s))" + ) + key = keys[args.index - 1] + elif args.name: + key = resolve_identity(fleet, args.name) + else: + parser.error("one of --name/--index/--robot (or a mode flag) is required") + print_exports(resolve_robot(fleet, root, key), root) + return 0 + except FleetError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/gen_dds_router.py b/tools/gen_dds_router.py new file mode 100644 index 000000000..8a58ab5fa --- /dev/null +++ b/tools/gen_dds_router.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""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 the shared +``autonomy_bringup/config/dds_router.yaml`` (inherited from the removed +legacy split's router config) 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 + autonomy_bringup/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/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/gen_docs_catalog.py b/tools/gen_docs_catalog.py new file mode 100644 index 000000000..5ca932a41 --- /dev/null +++ b/tools/gen_docs_catalog.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Generate the docs-site module/stack catalog from the registry index. + +RFC #379 §9 ("Docs"): the main site owns an auto-generated marketplace +catalog rendered from the `airstack-modules-index` registry repo +(https://github.com/castacks/airstack-modules-index). This script reads a +LOCAL CHECKOUT of that registry (the docs deploy workflows shallow-clone it; +developers point at any clone) and emits deterministic Markdown pages under +``docs/modules/``: + +* ``docs/modules/index.md`` — the catalog: one table row per registered + module (name, description, type, maintainer, DECLARED compat, links) and + one per registered stack. +* ``docs/modules/.md`` — one page per module: description, install + snippet, maintainer, DECLARED-vs-VERIFIED compatibility note pointing at + the registry's ``compat/`` matrix, and a link to the module README on + GitHub at the registered ref. + +The pages are committed so the site never depends on registry availability; +the deploy workflows regenerate them against the live registry at build time +(failure-isolated: an unreachable registry or module repo falls back to the +committed pages / a stub note — RFC #379 §9). + +Determinism contract: byte-identical output for identical inputs (registry +checkout + trunk tree + fetched-modules dir). No timestamps, no environment +leakage. ``--check`` regenerates into a temp dir and diffs against the +committed pages (CI drift style; exit 1 on drift). + +stdlib + PyYAML only. +""" +from __future__ import annotations + +import argparse +import difflib +import re +import sys +import tempfile +from pathlib import Path + +import yaml + +TRUNK = Path(__file__).resolve().parent.parent +REGISTRY_URL = "https://github.com/castacks/airstack-modules-index" + +GENERATED_MARKER = ( + "" +) + + +# ---------------------------------------------------------------- helpers + + +def _die(msg: str) -> "NoReturn": # noqa: F821 (py<3.11 typing) + print(f"gen_docs_catalog: error: {msg}", file=sys.stderr) + sys.exit(2) + + +def _load_yaml(path: Path) -> dict: + data = yaml.safe_load(path.read_text()) + if not isinstance(data, dict): + _die(f"{path} did not parse to a YAML mapping") + return data + + +def _load_entries(directory: Path) -> "list[dict]": + """Load all registry entries in a directory, sorted by name.""" + entries = [] + if not directory.is_dir(): + return entries + for path in sorted(directory.glob("*.yaml")): + entry = _load_yaml(path) + entry.setdefault("name", path.stem) + entries.append(entry) + entries.sort(key=lambda e: e["name"]) + return entries + + +def _norm_repo_url(url: str) -> str: + """Normalize a git URL for equality checks (ssh/https, .git suffix).""" + url = url.strip() + m = re.match(r"^git@([^:]+):(.+)$", url) + if m: + url = f"https://{m.group(1)}/{m.group(2)}" + if url.endswith(".git"): + url = url[:-4] + return url.rstrip("/") + + +def _repo_slug(url: str) -> str: + """castacks/asm_optitrack from any GitHub URL form; else the URL.""" + norm = _norm_repo_url(url) + m = re.match(r"^https://github\.com/(.+)$", norm) + return m.group(1) if m else norm + + +def _short_ref(ref: str) -> str: + return ref[:12] if re.fullmatch(r"[0-9a-f]{40}", ref) else ref + + +def _md_cell(text: str) -> str: + """Collapse whitespace and escape pipes for a Markdown table cell.""" + return " ".join(str(text).split()).replace("|", "\\|") + + +def _stack_pins(stack_dir: Path) -> "list[str]": + """Normalized module-repo URLs pinned by a trunk stack's modules.repos.""" + repos_file = stack_dir / "modules.repos" + if not repos_file.is_file(): + return [] + try: + data = yaml.safe_load(repos_file.read_text()) or {} + except yaml.YAMLError: + return [] + repositories = data.get("repositories") or {} + if not isinstance(repositories, dict): + return [] + return sorted( + _norm_repo_url(str(spec.get("url", ""))) + for spec in repositories.values() + if isinstance(spec, dict) and spec.get("url") + ) + + +def _stacks_using(module: dict, trunk: Path, stack_entries: "list[dict]") -> "list[str]": + """Registered trunk stacks whose modules.repos pin this module's repo.""" + target = _norm_repo_url(module.get("repo", "")) + users = [] + for stack in stack_entries: + if _repo_slug(stack.get("repo", "")) != "castacks/AirStack": + continue + stack_dir = trunk / stack.get("path", f"stacks/{stack['name']}") + if target and target in _stack_pins(stack_dir): + users.append(stack["name"]) + return sorted(users) + + +# ------------------------------------------------------------- rendering + + +def _stack_link(stack: dict, trunk: Path, from_depth: int = 2) -> str: + """Markdown link to a stack's README (relative for trunk stacks).""" + name = stack["name"] + rel_readme = Path(stack.get("path", f"stacks/{name}")) / "README.md" + if ( + _repo_slug(stack.get("repo", "")) == "castacks/AirStack" + and (trunk / rel_readme).is_file() + ): + return f"[{name}]({'../' * from_depth}{rel_readme.as_posix()})" + return f"[{name}]({_norm_repo_url(stack.get('repo', ''))})" + + +def render_index( + modules: "list[dict]", + stacks: "list[dict]", + trunk: Path, +) -> str: + lines = [ + "# Module & Stack Catalog", + "", + GENERATED_MARKER, + "", + "The **marketplace catalog** of registered AirStack modules and stacks,", + f"rendered from the", + f"[airstack-modules-index]({REGISTRY_URL}) registry — one YAML entry per", + "module or stack, rosdistro-style. Getting listed = a PR to the registry", + f"(see the [registry README]({REGISTRY_URL}#how-to-register-a-module)).", + "", + "Compatibility shown here is the author-**DECLARED** semver range; the", + f"**VERIFIED** matrix is CI-stamped into the registry's [compat/]({REGISTRY_URL}/tree/main/compat)", + "directory and is never hand-edited.", + "", + "## Registered modules", + "", + "| Module | Description | Type | Maintainer | Declared compat | Links |", + "|--------|-------------|------|------------|-----------------|-------|", + ] + for mod in modules: + name = mod["name"] + repo = _norm_repo_url(mod.get("repo", "")) + users = _stacks_using(mod, trunk, stacks) + links = [f"[repo]({repo})"] + links += [ + f"[{u}](../../stacks/{u}/README.md)" + for u in users + if (trunk / "stacks" / u / "README.md").is_file() + ] + lines.append( + "| [{n}]({n}.md) | {d} | `{t}` | {m} | `{c}` | {l} |".format( + n=name, + d=_md_cell(mod.get("description", "")), + t=mod.get("type", "?"), + m=_md_cell(mod.get("maintainer", "?")), + c=_md_cell(mod.get("airstack_compat", "?")), + l=" · ".join(links), + ) + ) + lines += [ + "", + "## Registered stacks", + "", + "A stack is a self-contained topology folder; its pinned `modules.repos`", + "*is* a tested-together release set.", + "The stacks below are the ones REGISTERED in the index; the site nav's", + "**Modules → Reference Stacks** additionally lists every trunk stack", + "(not every trunk stack is registered in the index).", + "", + "| Stack | Description | Declared compat | Wiring | Registry entry |", + "|-------|-------------|-----------------|--------|----------------|", + ] + for stack in stacks: + name = stack["name"] + wiring_rel = Path(stack.get("path", f"stacks/{name}")) / "wiring.md" + if ( + _repo_slug(stack.get("repo", "")) == "castacks/AirStack" + and (trunk / wiring_rel).is_file() + ): + wiring = f"[wiring.md](../../{wiring_rel.as_posix()})" + else: + wiring = "*not committed yet*" + lines.append( + "| {s} | {d} | `{c}` | {w} | [{n}.yaml]({u}/blob/main/stacks/{n}.yaml) |".format( + s=_stack_link(stack, trunk), + d=_md_cell(stack.get("description", "")), + c=_md_cell(stack.get("airstack_compat", "?")), + w=wiring, + n=name, + u=REGISTRY_URL, + ) + ) + lines += [ + "", + "## See also", + "", + "- [Modular AirStack walkthrough](../getting_started/modular_airstack.md) — the", + " new-developer journey: reference stack → add a module → own stack → fleet", + "- [AirStack Modules](../development/modules.md) — `airstack module` CLI, the", + " pinning rule, hooks, and the overlay", + "- [AirStack Stacks](../development/stacks.md) — stack anatomy, `stack new|diff`,", + " wiring snapshots, `doctor`", + "- [AirStack Fleets](../development/fleets.md) — fleet files composing stacks", + " into deployments", + "- [Module CI](../development/module_ci.md) — the reusable system-test workflow", + " module repos call; how compat badges are earned", + "- [Interface Conventions Spec](../robot/autonomy/interface_conventions.md) —", + " the canonical names/types/QoS modules default to", + "", + ] + return "\n".join(lines) + + +def render_module_page( + mod: dict, + trunk: Path, + stacks: "list[dict]", + modules_dir: Path, +) -> str: + name = mod["name"] + repo = _norm_repo_url(mod.get("repo", "")) + ref = str(mod.get("registered_ref", "main")) + users = _stacks_using(mod, trunk, stacks) + fetched = (modules_dir / name / "README.md").is_file() + + lines = [ + f"# {name}", + "", + GENERATED_MARKER, + "", + f"> {' '.join(str(mod.get('description', '')).split())}", + "", + "| | |", + "|---|---|", + f"| Repository | [{_repo_slug(repo)}]({repo}) |", + f"| Type | `{mod.get('type', '?')}` |", + f"| Maintainer | {_md_cell(mod.get('maintainer', '?'))} |", + f"| License | {_md_cell(mod.get('license', '?'))} |", + f"| Registered ref | [`{_short_ref(ref)}`]({repo}/tree/{ref}) |", + f"| Declared compat | `{_md_cell(mod.get('airstack_compat', '?'))}` |", + f"| Registry entry | [modules/{name}.yaml]({REGISTRY_URL}/blob/main/modules/{name}.yaml) |", + "", + "## Install", + "", + "From an AirStack checkout ([AirStack Modules guide](../development/modules.md)):", + "", + "```bash", + f"airstack module add {repo} --version {ref}", + "airstack up", + "```", + "", + "`module add` pins the module in `modules.repos` and syncs it into the", + "gitignored `modules/` overlay; `airstack up` automatically includes the", + "generated compose override that mounts it into the containers.", + "", + "## Compatibility: declared vs verified", + "", + f"The range `{_md_cell(mod.get('airstack_compat', '?'))}` is **DECLARED** by the module author", + "(copied from the module's `module.yaml`). The **VERIFIED** record — rows", + "stamped exclusively by CI runs of the reusable", + "[module-system-tests workflow](../development/module_ci.md) — lives in the", + f"registry's [compat/ matrix]({REGISTRY_URL}/tree/main/compat)", + f"([compat/{name}.yaml]({REGISTRY_URL}/blob/main/compat/{name}.yaml) once stamped).", + "A compatibility claim that isn't CI-verified rots: trust the", + "matrix, read the declaration as intent.", + "", + "## Documentation", + "", + f"- [Module README on GitHub @ `{_short_ref(ref)}`]({repo}/blob/{ref}/README.md)", + ] + if fetched: + lines += [ + f"- A snapshot of the module repo was fetched into `modules/{name}/` when", + " this page was generated (docs deploy fetch step).", + ] + else: + lines += [ + f"- *The module repo was not fetched when this page was generated — the*", + " *links above go to GitHub at the registered ref (failure isolation:*", + " *an unreachable module repo never fails the docs deploy).*", + ] + lines += [ + "", + "## Registered stacks using this module", + "", + ] + if users: + lines += [ + f"- [{u}](../../stacks/{u}/README.md)" + for u in users + if (trunk / "stacks" / u / "README.md").is_file() + ] + else: + lines.append( + "- None yet. Any stack can pin it in its `modules.repos` " + "([AirStack Stacks](../development/stacks.md))." + ) + notes = str(mod.get("notes", "")).strip() + if notes: + lines += ["", "## Registry notes", ""] + lines += [f"> {ln}".rstrip() for ln in " ".join(notes.split()).splitlines()] + lines.append("") + return "\n".join(lines) + + +# ------------------------------------------------------------------ main + + +def generate(index: Path, out: Path, trunk: Path, modules_dir: Path) -> "dict[str, str]": + modules = _load_entries(index / "modules") + stacks = _load_entries(index / "stacks") + if not modules: + _die(f"no module entries found under {index / 'modules'}") + pages = {"index.md": render_index(modules, stacks, trunk)} + for mod in modules: + pages[f"{mod['name']}.md"] = render_module_page(mod, trunk, stacks, modules_dir) + return pages + + +def write_pages(pages: "dict[str, str]", out: Path) -> None: + out.mkdir(parents=True, exist_ok=True) + for rel, content in sorted(pages.items()): + (out / rel).write_text(content) + + +def check_pages(pages: "dict[str, str]", out: Path) -> int: + """CI drift check: committed pages must match regeneration. 0 = clean.""" + drift = 0 + committed = {p.name for p in out.glob("*.md")} if out.is_dir() else set() + for rel in sorted(set(pages) | committed): + want = pages.get(rel) + have_path = out / rel + have = have_path.read_text() if have_path.is_file() else None + if want == have: + continue + drift = 1 + if want is None: + print(f"DRIFT: {have_path} is committed but no longer generated", file=sys.stderr) + elif have is None: + print(f"DRIFT: {have_path} is generated but not committed", file=sys.stderr) + else: + print(f"DRIFT: {have_path} differs from regeneration:", file=sys.stderr) + diff = difflib.unified_diff( + have.splitlines(), want.splitlines(), + fromfile=f"committed/{rel}", tofile=f"generated/{rel}", lineterm="", + ) + for line in list(diff)[:40]: + print(f" {line}", file=sys.stderr) + if drift: + print( + "gen_docs_catalog --check: docs/modules/ pages are stale — rerun\n" + " python3 tools/gen_docs_catalog.py --index \n" + "and commit the result.", + file=sys.stderr, + ) + return drift + + +def main(argv: "list[str] | None" = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--index", required=True, type=Path, + help="local checkout of castacks/airstack-modules-index", + ) + parser.add_argument( + "--out", type=Path, default=None, + help="output directory (default: /docs/modules)", + ) + parser.add_argument( + "--trunk", type=Path, default=TRUNK, + help="AirStack checkout root (default: this script's repo)", + ) + parser.add_argument( + "--modules-dir", type=Path, default=None, + help="dir of fetched module repos, modules// " + "(default: /modules; absence per module => stub note)", + ) + parser.add_argument( + "--check", action="store_true", + help="verify committed pages match regeneration; exit 1 on drift", + ) + parser.add_argument( + "--list-refs", action="store_true", + help="print 'namereporegistered_ref' per module and exit " + "(used by the docs deploy workflows' fetch loop)", + ) + args = parser.parse_args(argv) + + trunk = args.trunk.resolve() + index = args.index.resolve() + if not (index / "modules").is_dir(): + _die(f"{index} does not look like a registry checkout (no modules/ dir)") + out = (args.out if args.out is not None else trunk / "docs" / "modules").resolve() + modules_dir = ( + args.modules_dir if args.modules_dir is not None else trunk / "modules" + ).resolve() + + if args.list_refs: + for mod in _load_entries(index / "modules"): + print( + f"{mod['name']}\t{_norm_repo_url(mod.get('repo', ''))}" + f"\t{mod.get('registered_ref', 'main')}" + ) + return 0 + + pages = generate(index, out, trunk, modules_dir) + if args.check: + return check_pages(pages, out) + write_pages(pages, out) + print(f"gen_docs_catalog: wrote {len(pages)} pages to {out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/module_overlay.py b/tools/module_overlay.py new file mode 100755 index 000000000..243147741 --- /dev/null +++ b/tools/module_overlay.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Place synced AirStack modules into the trunk checkout (RFC #379 §3, Phase P2). + +``airstack module sync`` clones/links module repos into the gitignored +``modules/`` directory at the repo root. This tool performs the *overlay*: by +manifest ``type``/``targets`` it places each module where the build systems and +containers can see it, and it regenerates the compose override that makes the +placements resolve **inside** the containers. + +What it places, per module under ``modules//``: + +- ``ros_package`` targeting ``robot`` → symlink + ``robot/ros_ws/src/modules/`` → ``../../../../modules/`` so colcon + (host-side and in-container) picks the packages up. The robot container only + mounts ``robot/ros_ws`` (see robot/docker/robot-base-docker-compose.yaml), so + that symlink would DANGLE in-container; the generated compose file therefore + bind-mounts the module checkout at ``/root/AirStack/modules/`` — the + symlink's in-container resolution target. + +- ``isaac_extension`` targeting ``isaac-sim`` → symlink every + ``modules//launch_scripts/*.py`` into + ``simulation/isaac-sim/launch_scripts/modules//``. The isaac container + mounts the whole repo at ``/isaac-sim/AirStack``, so these repo-internal + symlinks resolve in-container (local-path modules — where ``modules/`` + is itself a symlink out of the repo — additionally get a bind mount at + ``/isaac-sim/AirStack/modules/``). Launch scripts become addressable as + ``ISAAC_SIM_SCRIPT_NAME=modules//