From b25443553b67cd6463dceaf58f1fc6274b8b57d9 Mon Sep 17 00:00:00 2001 From: Staaaaaaaaar <2300012435@stu.pku.edu.cn> Date: Wed, 19 Aug 2026 17:59:49 +0800 Subject: [PATCH 1/3] docs(architecture): select Isaac Sim ANYmal C baseline --- README.md | 4 +- configs/experiments/mvp.yaml | 2 +- deployment/README.md | 6 +- deployment/env/versions.env | 12 +++- docs/adr/0001-development-platform.md | 21 ++++-- docs/architecture-and-development-plan.md | 86 ++++++++++++----------- packages/rh_bringup/README.md | 2 +- packages/rh_core/test/conftest.py | 2 +- packages/rh_core/test/test_config.py | 2 +- simulators/README.md | 8 ++- 10 files changed, 87 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 9c82849..b8f52c3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ RoboHarness 是面向机器人导航任务的快速实验、自动运行与统 严格限定为: - Simulator:NVIDIA Isaac Sim -- Robot:Unitree Go2 +- Robot:ANYmal C - Agent:ROS 2 Keyboard Agent - Task:PointNav - Evaluation:Simple Navigation Evaluation @@ -126,4 +126,4 @@ MVP 不实现 Gazebo、Nav2、Reactive/RL/VLA/VLN Agent、其他机器人、复 ## License -License 尚未确定。在明确许可证之前,请勿假定仓库内容可被再分发;引入 Go2/Isaac 资源时也必须单独核对其许可证与再分发条件。 +License 尚未确定。在明确许可证之前,请勿假定仓库内容可被再分发;引入 ANYmal C/Isaac 资源时也必须单独核对其许可证与再分发条件。 diff --git a/configs/experiments/mvp.yaml b/configs/experiments/mvp.yaml index ef985da..f0e819f 100644 --- a/configs/experiments/mvp.yaml +++ b/configs/experiments/mvp.yaml @@ -1,6 +1,6 @@ schema_version: 1 experiment: - name: go2_keyboard_pointnav + name: anymal_c_keyboard_pointnav execution_mode: manual episodes: - episode_id: "0000" diff --git a/deployment/README.md b/deployment/README.md index 0cbe7af..f3b4da8 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -67,4 +67,8 @@ container snapshots, ROS graph snapshots, and Compose logs are retained under `.build/mock-e2e/`; the stack is always removed on exit. This CPU image is a reproducible protocol and orchestration reference, not an -Isaac production image. Isaac/GPU versions and images remain the scope of PR 13. +Isaac production image. Isaac Sim 4.5.0, Ubuntu 22.04, and ROS 2 Humble are the +selected runtime baseline; the exact GPU image digest and host-driver validation +remain the scope of PR 13. The MVP uses the ANYmal C locomotion policy bundled +with Isaac Sim, so Isaac Lab is not installed in the runtime image. Policy +training or export tooling will be selected and pinned separately if required. diff --git a/deployment/env/versions.env b/deployment/env/versions.env index 5fda99c..f8711bf 100644 --- a/deployment/env/versions.env +++ b/deployment/env/versions.env @@ -17,6 +17,12 @@ RH_VALIDATED_MAKE_VERSION=4.3 RH_VALIDATED_COLCON_CORE_VERSION=0.21.0 RH_VALIDATED_RUFF_VERSION=0.11.13 -# Exact Isaac Sim, NVIDIA driver, and production image versions are validated -# and pinned by PR 13. Do not infer them from the CPU development image. -RH_ISAAC_SIM_VERSION=UNVERIFIED_UNTIL_PR13 +# The Isaac runtime baseline is selected for the Ubuntu 22.04 / ROS 2 Humble +# project platform. PR 13 validates the exact image digest, host driver, and +# GPU runtime; do not infer those from the CPU development image. +RH_ISAAC_SIM_VERSION=4.5.0 +RH_ISAAC_REFERENCE_ROBOT=anymal_c + +# The MVP deploys the ANYmal C policy bundled with Isaac Sim. Isaac Lab is not +# installed in the runtime image; add and pin it only if policy training or +# export becomes a confirmed requirement. diff --git a/docs/adr/0001-development-platform.md b/docs/adr/0001-development-platform.md index b9ca746..a8c1bf4 100644 --- a/docs/adr/0001-development-platform.md +++ b/docs/adr/0001-development-platform.md @@ -22,6 +22,9 @@ The supported development baseline is: - Colcon and ament for ROS package discovery, build, and tests; - Docker Compose v2 and a root Makefile as thin repository-level entry points. - the Docker Official ROS image from AWS ECR Public, pinned by manifest digest. +- Isaac Sim 4.5.0 for the GPU environment backend; +- ANYmal C as the first reference quadruped, using the official Isaac Sim + locomotion-policy example as the initial simulator-only locomotion backend. The host requires Git, Docker Engine, Docker Compose v2, and Make. It does not require a native ROS installation. ROS and system dependencies are installed in @@ -33,15 +36,20 @@ Production remains exactly three services: `env`, `agent`, and `experiment`. Isaac development will reuse the future `env` image with a development Compose override rather than create a second, divergent GPU image. -The exact Isaac Sim version, NVIDIA driver requirements, production image tags -and digests, and ROS bridge compatibility remain unverified until PR 13. Humble -is selected now because Ubuntu 22.04 is its native Tier 1 platform and it is a -recommended Isaac Sim ROS 2 integration target. +The Isaac Sim and reference-robot versions are selected now to keep the Ubuntu +22.04 / ROS 2 Humble / Python 3.10 baseline intact. PR 13 still validates the +exact production image digest, NVIDIA driver requirements, GPU runtime, and +native ROS 2 Bridge compatibility. The initial ANYmal C policy is bundled with +Isaac Sim and is a simulator reference rather than a claim of real-robot +deployment capability. Isaac Lab is not an MVP runtime dependency: the project +does not train or export policies, and a future need for that tooling requires a +separate version and dependency decision. References: - [ROS 2 Humble platform support](https://www.ros.org/reps/rep-2000.html) -- [Isaac Sim ROS 2 support](https://docs.isaacsim.omniverse.nvidia.com/latest/ros2_tutorials/ros2_landing_page.html) +- [Isaac Sim 4.5 ROS 2 support](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/installation/install_ros.html) +- [Isaac Sim 4.5 policy examples](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/robot_simulation/ext_isaacsim_robot_policy_example.html) - [Docker Official Images on Amazon ECR Public](https://www.docker.com/blog/news-from-aws-reinvent-docker-official-images-on-amazon-ecr-public/) ## Consequences @@ -51,5 +59,8 @@ References: - Initial image download and build take longer than a native setup. - GUI, GPU, device, and DDS cross-container concerns are deferred to the PR that introduces the relevant runtime integration. +- Isaac-related runtime code and images must pin `4.5.0`; Isaac Lab must not be + added transitively through the policy example. Introducing policy training or + export tooling requires an explicit scope and version decision. - A baseline change requires a superseding ADR and corresponding CI/container update. diff --git a/docs/architecture-and-development-plan.md b/docs/architecture-and-development-plan.md index f66bd03..5a812d7 100644 --- a/docs/architecture-and-development-plan.md +++ b/docs/architecture-and-development-plan.md @@ -33,11 +33,13 @@ RoboHarness 是一套机器人导航实验平台,由以下能力组成: 唯一承诺的纵向组合是: ```text -Isaac Sim × Unitree Go2 × ROS 2 Keyboard Agent × PointNav × Simple Eval +Isaac Sim 4.5.0 × ANYmal C × ROS 2 Keyboard Agent × PointNav × Simple Eval ``` 成功标准不是“支持很多实现”,而是五个替换维度已建立清晰边界,且上述组合能稳定连续运行多个 Episode。 +MVP 只运行 Isaac Sim 4.5.0 自带的 ANYmal C locomotion policy,不训练或重新导出策略,因此 Isaac Lab 不是开发或运行依赖。若后续确认需要 policy training/export,再通过独立 ADR 选择并固定 Isaac Lab 版本,不将其预先加入 Env 镜像。 + ### 1.4 未来扩展维度 Simulator、Robot、Agent、Task、Evaluation 均通过 ROS contract、配置约定和目录边界扩展。MVP 使用显式 factory 和类型枚举,不建设动态插件注册、依赖注入框架或复杂继承树。 @@ -61,7 +63,7 @@ Simulator、Robot、Agent、Task、Evaluation 均通过 ROS contract、配置约 ┌─────────────────┐ telemetry/results │ env │───────────────► │ Isaac backend │ - │ + Go2 binding │ + │ + ANYmal C bind │ └───────┬─────────┘ │ observations v @@ -102,8 +104,8 @@ Simulator、Robot、Agent、Task、Evaluation 均通过 ROS contract、配置约 | Evaluator | 订阅 ground truth/episode/task,维护轨迹并计算 metrics、提出终止候选 | 发布 `cmd_vel`、改变 world、决定调度策略 | | Result Recorder | 原子写入 config、metadata、episode spec、events、trajectory、metrics 和 summary | 计算控制或拥有生命周期状态 | | Simulator Backend | Isaac app/extension、timeline、stage、physics、world、原生 ROS 2 Bridge 配置与 backend readiness | Agent、Task/Eval、跨 simulator 的机器人声明 | -| Backend-local Robot Definition | 位于 `simulators//robots/` 的 Go2 identity、frames、limits 与 asset manifest | 跨 backend 的全局 robot registry;MVP 不单独发布 robot package | -| Simulator × Robot Binding | Go2 asset spawn、articulation、传感器、`cmd_vel` 到 locomotion controller、joint/velocity reset、Isaac-specific physics 与安全门控 | Core protocol、Keyboard policy、跨 simulator 通用控制实现 | +| Backend-local Robot Definition | 位于 `simulators//robots/` 的 ANYmal C identity、frames、limits 与 asset manifest | 跨 backend 的全局 robot registry;MVP 不单独发布 robot package | +| Simulator × Robot Binding | ANYmal C asset spawn、articulation、传感器、`cmd_vel` 到官方 locomotion policy、joint/velocity reset、Isaac-specific physics 与安全门控 | Core protocol、Keyboard policy、跨 simulator 通用控制实现 | 安全速度门控由两端共同保证:Agent 在非 `RUNNING` 时停止发布并立即发零速度;Env 无条件拒绝/归零非 `RUNNING` 命令。Env 是最后安全边界,不能只信任 Agent。 @@ -175,7 +177,7 @@ load config -> wait env/agent READY ### 6.1 Env Contract -必须提供 component status、幂等 `reset_episode`、`cmd_vel` 输入、标准 observation、TF、simulation clock 与 Episode state awareness。READY 表示 Isaac、world、Go2 binding、physics、原生 ROS 2 Bridge 和必需接口全部可用。reset 成功表示 world、robot root/joint pose、velocity、locomotion controller 与 physics episode state 已恢复且输出为零。Go2 的底层运控属于 Env backend,不属于 Agent 或独立 robot driver。 +必须提供 component status、幂等 `reset_episode`、`cmd_vel` 输入、标准 observation、TF、simulation clock 与 Episode state awareness。READY 表示 Isaac、world、ANYmal C binding、physics、原生 ROS 2 Bridge 和必需接口全部可用。reset 成功表示 world、robot root/joint pose、velocity、locomotion policy/controller 与 physics episode state 已恢复且输出为零。ANYmal C 的底层仿真运控属于 Env backend,不属于 Agent 或独立 robot driver。 ### 6.2 Agent Contract @@ -197,7 +199,7 @@ Core contract 由 ROS interfaces、配置 schema 和行为测试共同定义;P 平台语义统一在 `/roboharness` 下;标准机器人接口使用 `/robot` namespace,以便未来多机器人时显式 remap:`/robot/cmd_vel`、`/robot/odom`、`/robot/imu`、`/robot/scan`、`/robot/camera/*`。`/tf`、`/tf_static` 和 `/clock` 保留 ROS 惯例。 -自定义 `msg/srv` 全部置于独立 `rh_interfaces` package。接口定义不得依赖 Isaac、Go2 或 Python 实现包。 +自定义 `msg/srv` 全部置于独立 `rh_interfaces` package。接口定义不得依赖 Isaac、ANYmal C 或 Python 实现包。 ### 7.2 平台协议 @@ -279,7 +281,7 @@ MVP YAML: ```yaml schema_version: 1 experiment: - name: go2_keyboard_pointnav + name: anymal_c_keyboard_pointnav execution_mode: manual episodes: - episode_id: "0000" @@ -362,7 +364,7 @@ roboharness/ │ └─ rh_bringup/ # concrete Experiment composition root │ ├─ simulators/ # Simulator backends;Robot 位于内部 -│ └─ isaac_sim/ +│ └─ isaac/ │ ├─ apps/ │ │ └─ rh.kit # 启用原生 Bridge 与项目 extension │ ├─ extensions/ @@ -373,7 +375,7 @@ roboharness/ │ │ ├─ action_graphs/ # 原生 ROS 2 Bridge graph 定义 │ │ └─ topics.yaml # topic/frame/QoS mapping │ ├─ robots/ -│ │ └─ go2/ # Isaac Sim × Go2 完整 binding +│ │ └─ anymal_c/ # Isaac Sim × ANYmal C 完整 binding │ │ ├─ config/ │ │ ├─ assets/ │ │ ├─ assets.lock @@ -397,7 +399,7 @@ roboharness/ │ ├─ scenarios/warehouse_default.yaml │ ├─ agents/keyboard.yaml │ ├─ tasks/pointnav.yaml -│ └─ simulators/isaac_sim_go2.yaml +│ └─ simulators/isaac_anymal_c.yaml │ ├─ deployment/ # 部署的权威定义 │ ├─ README.md # mounts、profiles、commands、host 要求 @@ -476,32 +478,32 @@ tasks/ evaluators/ ``` -新增实现使用明确配置 ID 和小型 factory/entry point 选择,不建设动态插件市场。MVP 只创建 `isaac_sim/robots/go2`、`keyboard`、`pointnav` 和 `simple_navigation`。Task/Evaluator 不得 import simulator backend;Agent 只能依赖公开 ROS data plane 和 platform contract。 +新增实现使用明确配置 ID 和小型 factory/entry point 选择,不建设动态插件市场。MVP 只创建 `isaac/robots/anymal_c`、`keyboard`、`pointnav` 和 `simple_navigation`。Task/Evaluator 不得 import simulator backend;Agent 只能依赖公开 ROS data plane 和 platform contract。 每个 leaf implementation 可以是独立 ament/Python package,但父目录不是 package。这允许 Agent 后续分别携带 Nav2、PyTorch 或 VLA runtime,而不污染平台依赖。 -### 10.3 Isaac Sim backend and Go2 ownership +### 10.3 Isaac Sim backend and ANYmal C ownership Env 的可执行实现为: ```text -Environment Backend = Isaac Sim runtime + Go2 binding +Environment Backend = Isaac Sim 4.5.0 runtime + ANYmal C binding ``` -`simulators/isaac_sim` 不是普通 ROS adapter package。`rh.kit` 与 `rh.isaac` extension 管理 timeline、stage、physics、world、readiness、Episode reset 和 safety。标准 sensors、TF、clock、command transport 使用 Isaac Sim 原生 `isaacsim.ros2.bridge` OmniGraph/Action Graph nodes;项目只保存官方方式下的 graph、topic/frame 配置和必要的 lifecycle glue,不复制 ROS 2 Bridge。Bridge publishers/subscribers/services 只在 simulation playback 时活跃,因此 Env READY 必须验证 timeline 正在运行且所需 Action Graph 已激活。[Isaac Sim ROS 2 Bridge](https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.ros2.bridge/docs/index.html) +`simulators/isaac` 不是普通 ROS adapter package。`rh.kit` 与 `rh.isaac` extension 管理 timeline、stage、physics、world、readiness、Episode reset 和 safety。标准 sensors、TF、clock、command transport 使用 Isaac Sim 4.5 原生 ROS 2 Bridge OmniGraph/Action Graph nodes;项目只保存官方方式下的 graph、topic/frame 配置和必要的 lifecycle glue,不复制 ROS 2 Bridge。Bridge publishers/subscribers/services 只在 simulation playback 时活跃,因此 Env READY 必须验证 timeline 正在运行且所需 Action Graph 已激活。[Isaac Sim 4.5 ROS 2 Bridge](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/py/source/extensions/isaacsim.ros2.bridge/docs/index.html) -`simulators/isaac_sim/robots/go2` 拥有完整低层仿真实现:asset、spawn、articulation、sensor prim、`cmd_vel` 到 locomotion controller、root/joint state reset、physics 参数与最终安全门控。Agent 只输出平台控制命令,不承担 Go2 底层运控。 +`simulators/isaac/robots/anymal_c` 拥有完整低层仿真实现:asset、spawn、articulation、sensor prim、`cmd_vel` 到官方 locomotion policy、root/joint state reset、physics 参数与最终安全门控。Agent 只输出平台控制命令,不承担 ANYmal C 底层仿真运控。MVP 直接运行 Isaac Sim 4.5.0 自带的官方策略,不安装 Isaac Lab,也不训练或实现新的关节级运控策略;策略的输入、输出、适用地形、来源和版本必须写入 asset manifest 并经 PR 14 验证。 -当前不创建顶层 `robots/` 或 `rh_go2` package。未来增加 `Gazebo + Go2` 时,在 `simulators/gazebo/robots/go2` 实现对应 binding;只有出现经过验证的跨 simulator 复用代码后,才将纯数据或模型提取到 `packages/rh_robot_model`,避免为理论复用预先制造抽象。 +当前不创建顶层 `robots/` 或 `rh_anymal_c` package。未来增加 `Gazebo + ANYmal C` 时,在 `simulators/gazebo/robots/anymal_c` 实现对应 binding;只有出现经过验证的跨 simulator 复用代码后,才将纯数据或模型提取到 `packages/rh_robot_model`,避免为理论复用预先制造抽象。 -Isaac backend 的实现以官方能力为准:通过 Kit/extension dependency 启用 Bridge;通过 `isaacsim.ros2.nodes` 提供的 OmniGraph nodes 建立 publisher/subscriber/service graph;需要精确控制发布频率时采用 Standalone/OnImpulseEvent 工作流;timeline、entity/world 等通用控制优先评估官方 `isaacsim.ros2.sim_control`,RoboHarness 只补充原子 Episode reset 与 Go2 controller state 等平台语义。参考:[ROS 2 Bridge](https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.ros2.bridge/docs/index.html)、[ROS 2 Nodes](https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.ros2.nodes/docs/index.html)、[Standalone Workflow](https://docs.isaacsim.omniverse.nvidia.com/latest/ros2_tutorials/tutorial_ros2_python.html)、[Simulation Control](https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.ros2.sim_control/docs/index.html)。 +Isaac backend 的实现以 4.5 官方能力为准:通过 Kit/extension dependency 启用 Bridge;通过 `isaacsim.ros2.nodes` 提供的 OmniGraph nodes 建立 publisher/subscriber/service graph;需要精确控制发布频率时采用 Standalone/OnImpulseEvent 工作流;timeline、entity/world 等通用控制优先评估官方 `isaacsim.ros2.sim_control`,RoboHarness 只补充原子 Episode reset 与 ANYmal C policy state 等平台语义。参考:[ROS 2 Bridge](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/py/source/extensions/isaacsim.ros2.bridge/docs/index.html)、[ROS 2 Nodes](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/py/source/extensions/isaacsim.ros2.nodes/docs/index.html)、[Standalone Workflow](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/ros2_tutorials/tutorial_ros2_python.html)、[Simulation Control](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/py/source/extensions/isaacsim.ros2.sim_control/docs/index.html)。 ### 10.4 Build domains without `ros2_ws` | Domain | Content | Tooling | |---|---|---| | Platform / ROS | `packages/`、`agents/`、`tasks/`、`evaluators/`、`tests/fixtures/` | Colcon / ament | -| Isaac Backend | Kit app、extension、Action Graph、Go2 binding | Isaac Sim / Kit extension system | +| Isaac Backend | Kit app、extension、Action Graph、ANYmal C binding | Isaac Sim / Kit extension system | 推荐构建命令: @@ -512,7 +514,7 @@ colcon build \ --install-base .build/colcon/install ``` -Isaac Kit extension 不伪装成 ROS package,由 `simulators/isaac_sim/apps/rh.kit` 加载。`colcon.defaults.yaml` 固定 `.build/colcon/*`,根目录保持整洁。 +Isaac Kit extension 不伪装成 ROS package,由 `simulators/isaac/apps/rh.kit` 加载。`colcon.defaults.yaml` 固定 `.build/colcon/*`,根目录保持整洁。 ### 10.5 Deployment ownership @@ -539,7 +541,7 @@ services: | Host/source | Container target | Mode | Consumer | |---|---|---|---| | `configs/` | `/opt/rh/configs` | read-only | all | -| `simulators/isaac_sim/` | `/opt/rh/simulators/isaac_sim` | image copy;dev 时 read-only mount | env | +| `simulators/isaac/` | `/opt/rh/simulators/isaac` | image copy;dev 时 read-only mount | env | | `results/` | `/data/results` | read-write | experiment | | `logs//` | `/data/logs` | read-write | corresponding service | | named Isaac caches | NVIDIA/Omniverse cache paths | read-write | env | @@ -580,7 +582,7 @@ docker compose --env-file deployment/env/.env \ ```yaml services: - env: # Isaac Sim app + native ROS 2 Bridge + Go2 binding; NVIDIA runtime + env: # Isaac Sim app + native ROS 2 Bridge + ANYmal C binding; NVIDIA runtime agent: # Keyboard Agent; interactive stdin in manual profile experiment: # orchestrator + task + evaluator + recorder ``` @@ -597,7 +599,7 @@ services: | Image | Installed content | |---|---| -| `env` | `rh_interfaces`、必要的 `rh_ros`、Isaac Kit app/extension、原生 `isaacsim.ros2.bridge`、Isaac Sim × Go2 binding | +| `env` | `rh_interfaces`、必要的 `rh_ros`、Isaac Kit app/extension、原生 `isaacsim.ros2.bridge`、Isaac Sim × ANYmal C binding | | `agent` | `rh_interfaces`、`rh_ros`、`rh_agent_keyboard` | | `experiment` | `rh_interfaces`、`rh_core`、`rh_ros`、`rh_experiment`、`rh_task_pointnav`、`rh_eval_simple_navigation` | @@ -608,7 +610,7 @@ Env 的高频数据平面由 Isaac 原生 ROS 2 Bridge Action Graph 直接发布 | Mount | Mode | Consumer | Purpose | |---|---|---|---| | `configs/` | read-only | all | experiment/env/agent config | -| `simulators/isaac_sim/` | image copy;dev override read-only | env | Kit app、extension、Go2 binding 与 assets | +| `simulators/isaac/` | image copy;dev override read-only | env | Kit app、extension、ANYmal C binding 与 assets | | `results/` | read-write | experiment | durable result artifacts | | `logs//` | read-write | each service | runtime diagnostics | | Isaac caches | read-write | env | shader/content cache acceleration | @@ -628,7 +630,7 @@ docker compose up -> orchestrator prepares Episode 0 ``` -Env 只有在 Isaac、stage、Go2、physics、ROS bridge、clock、required topics/services 均可用后发布 READY。Agent 只有在 node、subscriptions、publisher、reset service 和 task/state inputs 均建立后发布 READY。status 使用 transient local,周期心跳 1 Hz;状态变化立即发布。 +Env 只有在 Isaac、stage、ANYmal C、physics、ROS bridge、clock、required topics/services 均可用后发布 READY。Agent 只有在 node、subscriptions、publisher、reset service 和 task/state inputs 均建立后发布 READY。status 使用 transient local,周期心跳 1 Hz;状态变化立即发布。 配置提供 `startup_timeout_s`、`status_stale_timeout_s`、`reset_timeout_s`、`safe_stop_timeout_s`,禁止硬编码 `sleep 20/30`。建议 MVP 默认分别为 300、5、30、2 秒,Isaac startup timeout 可配置增大。Startup 使用 wall/steady clock,因为 simulation clock 可能尚未前进;Episode timeout 使用 simulation time,同时以 wall-clock watchdog 检测 Isaac freeze。 @@ -673,7 +675,7 @@ MVP 不包含 SPL、语义目标、动态场景、复杂碰撞惩罚或 initial | Mock ROS integration | status QoS、service timeout/idempotency、task/state propagation | CPU CI + ROS 2 | | Multi-Episode | reset order、无进程重启、无状态泄漏、结果隔离 | CPU CI + mocks | | Compose smoke | 三个 CPU mock services 的 network/discovery/startup | Docker CI | -| Isaac integration | stage、Go2 reset、clock、TF、sensors、cmd_vel gate | GPU self-hosted/manual/nightly | +| Isaac integration | stage、ANYmal C reset、clock、TF、sensors、cmd_vel gate | GPU self-hosted/manual/nightly | | MVP E2E | GUI keyboard、多 Episode、metrics/results | GPU 人工;稳定后 nightly | Mock Env/Agent 是 protocol test fixture,不是第二 simulator。它们必须 CPU-only、deterministic、fast,并支持注入 readiness delay、reset error、crash/stale status、motion trajectory 和 timeout。 @@ -852,8 +854,8 @@ CPU CI 至少执行 `colcon build`、lint/type checks、unit/interface tests、m **Goal:** 尽早验证 Isaac Kit backend、官方 ROS 2 Bridge、DDS 和容器 GPU 风险。 **Changes:** pinned env image、`rh.kit`、`rh.isaac` extension skeleton、headless/GUI launcher、启用 `isaacsim.ros2.bridge`、clock/Action Graph smoke、manual validation script。 -**Out of Scope:** Go2 spawn/control、完整 reset、MVP E2E。 -**Files / Modules:** `simulators/isaac_sim/apps/`、`extensions/`、`bridge/`、`deployment/docker/env/`、Compose env profile、GPU tests。 +**Out of Scope:** ANYmal C spawn/control、Isaac Lab、policy training/export、完整 reset、MVP E2E。 +**Files / Modules:** `simulators/isaac/apps/`、`extensions/`、`bridge/`、`deployment/docker/env/`、Compose env profile、GPU tests。 **ROS Interfaces:** STARTING/ERROR status;READY 暂不承诺,或仅在 smoke mode 使用明确 capability detail。 **Tests:** image build、GPU launch、stage/clock/native Bridge/ROS discovery manual or self-hosted smoke。 **Acceptance Criteria:** 锁定可复现 Isaac/driver/ROS 版本;外部容器能看到 Isaac 原生 Bridge ROS graph;项目未复制 sensor/cmd ROS bridge;验证记录齐全。 @@ -861,18 +863,18 @@ CPU CI 至少执行 `colcon build`、lint/type checks、unit/interface tests、m **Risks:** Isaac/ROS distro/GPU driver compatibility 是最高外部风险,应早期暴露。 **After this PR:** 首个 Isaac/GPU 依赖点已被隔离并验证。 -#### PR 14 — Isaac Sim × Go2 Binding and Locomotion +#### PR 14 — Isaac Sim × ANYmal C Binding and Locomotion -**Goal:** 在 Isaac backend 内实现 Go2 asset、传感器和底层仿真运控,不创建独立 Go2 runtime package。 -**Changes:** asset manifest/license、Isaac spawn、articulation、`cmd_vel` 到 locomotion controller、sensor/physics config、Bridge Action Graph wiring。 -**Out of Scope:** Keyboard Agent、multi-Episode orchestration changes、Gazebo binding。 -**Files / Modules:** `simulators/isaac_sim/robots/go2/`、`simulators/isaac_sim/bridge/`、GPU tests。 +**Goal:** 在 Isaac backend 内实现 ANYmal C asset、传感器和官方 locomotion policy binding,不创建独立 ANYmal C runtime package。 +**Changes:** asset/policy manifest/license、Isaac spawn、articulation、`cmd_vel` 到官方 policy、sensor/physics config、Bridge Action Graph wiring。 +**Out of Scope:** policy training/export、Isaac Lab runtime、Keyboard Agent、multi-Episode orchestration changes、Gazebo binding。 +**Files / Modules:** `simulators/isaac/robots/anymal_c/`、`simulators/isaac/bridge/`、GPU tests。 **ROS Interfaces:** 通过 Isaac 原生 Bridge 暴露 odom/imu/scan/TF/cmd_vel,保持 Part VII names/types。 **Tests:** asset/license check、TF uniqueness、spawn pose、zero command、manual GPU motion smoke。 -**Acceptance Criteria:** Go2 在 Isaac 中可 spawn 并由 Twist 驱动;标准 Data Plane 可见;底层 controller 位于 simulator binding;不修改 Core interface。 +**Acceptance Criteria:** ANYmal C 在 Isaac 中可 spawn 并由 Twist 驱动;标准 Data Plane 可见;官方 policy 位于 simulator binding;不修改 Core interface。 **Dependencies:** PR 13。 **Risks:** asset redistributability、controller semantics;以 manifest/license 和限幅测试缓解。 -**After this PR:** Isaac Sim × Go2 真实数据与低层运控闭环成立。 +**After this PR:** Isaac Sim × ANYmal C 真实数据与基础低层运控闭环成立。 #### PR 15 — ROS 2 Keyboard Agent @@ -887,12 +889,12 @@ CPU CI 至少执行 `colcon build`、lint/type checks、unit/interface tests、m **Risks:** Docker TTY/input portability;明确支持矩阵并将输入 adapter 与 agent logic 分离。 **After this PR:** MVP Agent 可接入 mock 或真实 Env。 -#### PR 16 — Isaac Go2 Env Contract +#### PR 16 — Isaac ANYmal C Env Contract **Goal:** 使真实 Env 达到 READY/reset/safe-gate 的完整平台 contract。 **Changes:** readiness probes、idempotent reset、world/root/joint pose、velocity、locomotion controller 与 physics reset、Episode gate、watchdogs、error mapping。 **Out of Scope:** 新 robot/simulator、复杂 scene、自动 restart。 -**Files / Modules:** `simulators/isaac_sim/extensions/rh.isaac/`、`simulators/isaac_sim/robots/go2/reset.py`、env deployment、GPU integration tests。 +**Files / Modules:** `simulators/isaac/extensions/rh.isaac/`、`simulators/isaac/robots/anymal_c/reset.py`、env deployment、GPU integration tests。 **ROS Interfaces:** 完整 Env status/reset/state/data plane。 **Tests:** 两次以上 reset、pose/velocity verification、clock/TF、non-RUNNING command rejection、freeze/error manual cases。 **Acceptance Criteria:** 多次 reset 不重启 Isaac;每次 start state 在容差内;READY 含义满足 Part VI/XII。 @@ -904,7 +906,7 @@ CPU CI 至少执行 `colcon build`、lint/type checks、unit/interface tests、m #### PR 17 — MVP End-to-End Integration -**Goal:** 打通唯一承诺的 Isaac × Go2 × Keyboard × PointNav × Simple Eval 纵向路径。 +**Goal:** 打通唯一承诺的 Isaac × ANYmal C × Keyboard × PointNav × Simple Eval 纵向路径。 **Changes:** production Compose/config、GUI manual profile、integration wiring、操作/验收手册、bug fixes limited to contract compliance。 **Out of Scope:** 新 feature、性能重构、第二 implementation。 **Files / Modules:** `deployment/`、configs、guides、e2e tests;必要的现有 package 小修。 @@ -941,7 +943,7 @@ CPU CI 至少执行 `colcon build`、lint/type checks、unit/interface tests、m ### 16.2 Commit convention -使用 Conventional Commits:`feat`、`fix`、`refactor`、`test`、`docs`、`build`、`ci`、`chore`。建议 scope:`protocol`、`core`、`experiment`、`task`、`eval`、`env`、`isaac`、`go2`、`agent`、`docker`。 +使用 Conventional Commits:`feat`、`fix`、`refactor`、`test`、`docs`、`build`、`ci`、`chore`。建议 scope:`protocol`、`core`、`experiment`、`task`、`eval`、`env`、`isaac`、`anymal`、`agent`、`docker`。 ```text feat(protocol): define component status interfaces @@ -983,7 +985,7 @@ test(eval): cover reset discontinuity in path length - 只有 `env`、`agent`、`experiment` 三个 runtime services,职责符合本文; - Env/Agent 直接交换 observations/`cmd_vel`,Experiment 不在闭环中; - Env/Agent readiness 与 reset contract 生效,禁止固定 sleep; -- Isaac + Go2 + Keyboard + PointNav + Simple Eval 连续执行至少 3 局且不重启组件; +- Isaac + ANYmal C + Keyboard + PointNav + Simple Eval 连续执行至少 3 局且不重启组件; - success、timeout、abort 和至少一个 component error 路径经验证; - 每局输出 metrics/termination reason/artifacts,Experiment 输出 summary; - 非 RUNNING 命令被 Agent 和 Env 双重归零/拒绝; @@ -1007,7 +1009,7 @@ test(eval): cover reset discontinuity in path length 审查规则:第二 implementation 若要求修改 Core,首先判断是 contract 缺陷、implementation detail 泄漏,还是确有跨实现通用能力。只有前两者修复或经 ADR 证明的通用语义才可改变 Core;不得为单个 integration 特例污染协议。 -最有价值的验证顺序是 second Agent → second Simulator(建议 Gazebo + Go2)→ second Robot → second Task → second Eval。Gazebo + Go2 应保持 Agent、PointNav、Evaluator 和 Experiment protocol 不变。 +最有价值的验证顺序是 second Agent → second Simulator(建议 Gazebo + ANYmal C)→ second Robot → second Task → second Eval。Gazebo + ANYmal C 应保持 Agent、PointNav、Evaluator 和 Experiment protocol 不变。 --- @@ -1017,7 +1019,7 @@ test(eval): cover reset discontinuity in path length 先做它的原因:当前仓库为空,后续 interface、mock 和 Isaac 工作都需要统一 ROS distro、构建命令、目录、CI 与 review 规则。它提供可验证底座,同时不把尚未被代码检验的 protocol 设计与大量 scaffolding 一次绑定。 -它不实现 ROS interface、runtime node、Docker image、Isaac、Go2 或 Keyboard Agent,也不声称完成任何实验能力。 +它不实现 ROS interface、runtime node、Docker image、Isaac、ANYmal C 或 Keyboard Agent,也不声称完成任何实验能力。 具体 acceptance criteria: @@ -1042,7 +1044,7 @@ PR01 Repository Foundation ├─ PR07 Single-Episode Orch ────┤ ├─ PR08 PointNav ──┬─ PR09 Eval ┤ │ └─ PR15 Keyboard Agent ──────────────┐ - └─ PR13 Isaac/Bridge ── PR14 Go2 Binding ─ PR16 Env ───┤ + └─ PR13 Isaac/Bridge ── PR14 ANYmal C Binding ─ PR16 Env ───┤ PR03 ─────────── PR10 Recorder ─────────────────┐ │ v │ PR05 + PR06 + PR07 + PR08 + PR09 + PR10 ──> PR11 Multi-Episode │ diff --git a/packages/rh_bringup/README.md b/packages/rh_bringup/README.md index c85e07f..8dfbcc4 100644 --- a/packages/rh_bringup/README.md +++ b/packages/rh_bringup/README.md @@ -18,7 +18,7 @@ Run it with: ```bash ros2 run rh_bringup experiment --profile pointnav_simple --ros-args \ -p config_path:=/workspace/roboharness/configs/experiments/mvp.yaml \ - -p experiment_id:=go2-keyboard-run-001 \ + -p experiment_id:=anymal-c-keyboard-run-001 \ -p results_root:=/workspace/roboharness/results ``` diff --git a/packages/rh_core/test/conftest.py b/packages/rh_core/test/conftest.py index 5fffbfc..bafcfc1 100644 --- a/packages/rh_core/test/conftest.py +++ b/packages/rh_core/test/conftest.py @@ -10,7 +10,7 @@ def valid_document() -> dict[str, Any]: return { "schema_version": 1, "experiment": { - "name": "go2_keyboard_pointnav", + "name": "anymal_c_keyboard_pointnav", "execution_mode": "manual", "episodes": [ { diff --git a/packages/rh_core/test/test_config.py b/packages/rh_core/test/test_config.py index 7b437ca..a301940 100644 --- a/packages/rh_core/test/test_config.py +++ b/packages/rh_core/test/test_config.py @@ -20,7 +20,7 @@ def test_canonical_mvp_config_loads_to_immutable_models() -> None: config = load_experiment_config(repository_root / "configs/experiments/mvp.yaml") assert config.schema_version == 1 - assert config.experiment.name == "go2_keyboard_pointnav" + assert config.experiment.name == "anymal_c_keyboard_pointnav" assert config.experiment.execution_mode is ExecutionMode.MANUAL assert isinstance(config.experiment.episodes, tuple) assert config.experiment.episodes[0].initial_pose.frame_id == "map" diff --git a/simulators/README.md b/simulators/README.md index 62a706a..0698b6b 100644 --- a/simulators/README.md +++ b/simulators/README.md @@ -4,4 +4,10 @@ Simulator backends live under `simulators//`. A backend owns its world, physics, native ROS bridge, and backend-local robot bindings under `robots//`. -Isaac Sim and Go2 implementation files are intentionally out of scope for PR 01. +The selected GPU baseline is Isaac Sim 4.5.0 running alongside Ubuntu 22.04 and +ROS 2 Humble. ANYmal C is the first reference quadruped; its simulator-only +official locomotion-policy binding belongs to the Isaac backend. The MVP runs +the policy bundled with Isaac Sim and does not install Isaac Lab. Training or +exporting a policy is outside the current runtime scope. + +Isaac Sim and ANYmal C implementation files are intentionally out of scope for PR 01. From c73850cd620ec584626e18fb95f6e793dc8f0a55 Mon Sep 17 00:00:00 2001 From: Staaaaaaaaar <2300012435@stu.pku.edu.cn> Date: Mon, 24 Aug 2026 16:32:31 +0800 Subject: [PATCH 2/3] feat(isaac): add native ROS 2 Bridge backend --- Makefile | 35 +++- deployment/compose/compose.isaac.yaml | 62 ++++++ deployment/docker/env/Dockerfile | 56 ++++++ deployment/docker/env/entrypoint.sh | 9 + deployment/env/.env.example | 12 ++ deployment/env/versions.env | 9 + simulators/isaac/apps/rh.kit | 22 +++ simulators/isaac/bridge/topics.yaml | 14 ++ .../extensions/rh.isaac/config/extension.toml | 20 ++ .../isaac/extensions/rh.isaac/rh/__init__.py | 1 + .../extensions/rh.isaac/rh/isaac/__init__.py | 17 ++ .../rh.isaac/rh/isaac/clock_graph.py | 44 +++++ .../extensions/rh.isaac/rh/isaac/extension.py | 187 ++++++++++++++++++ simulators/isaac/scripts/launch.sh | 56 ++++++ tools/dev/run_isaac_gui.sh | 81 ++++++++ tools/e2e/run_isaac_bridge_smoke.sh | 127 ++++++++++++ tools/validation/check_isaac_skeleton.py | 103 ++++++++++ tools/validation/check_repository.py | 7 + 18 files changed, 860 insertions(+), 2 deletions(-) create mode 100644 deployment/compose/compose.isaac.yaml create mode 100644 deployment/docker/env/Dockerfile create mode 100755 deployment/docker/env/entrypoint.sh create mode 100644 deployment/env/.env.example create mode 100644 simulators/isaac/apps/rh.kit create mode 100644 simulators/isaac/bridge/topics.yaml create mode 100644 simulators/isaac/extensions/rh.isaac/config/extension.toml create mode 100644 simulators/isaac/extensions/rh.isaac/rh/__init__.py create mode 100644 simulators/isaac/extensions/rh.isaac/rh/isaac/__init__.py create mode 100644 simulators/isaac/extensions/rh.isaac/rh/isaac/clock_graph.py create mode 100644 simulators/isaac/extensions/rh.isaac/rh/isaac/extension.py create mode 100755 simulators/isaac/scripts/launch.sh create mode 100755 tools/dev/run_isaac_gui.sh create mode 100755 tools/e2e/run_isaac_bridge_smoke.sh create mode 100755 tools/validation/check_isaac_skeleton.py diff --git a/Makefile b/Makefile index dd84cc6..5eebe6f 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ DEV_COMPOSE := docker compose --env-file deployment/env/versions.env \ -f deployment/compose/compose.dev.yaml MOCK_COMPOSE := docker compose --env-file deployment/env/versions.env \ -f deployment/compose/compose.mock.yaml +ISAAC_ENV_FILE ?= deployment/env/.env +ISAAC_COMPOSE := docker compose --env-file deployment/env/versions.env \ + --env-file $(ISAAC_ENV_FILE) -f deployment/compose/compose.isaac.yaml BASE_PATHS := packages agents tasks evaluators tests COLCON_ENV := COLCON_DEFAULTS_FILE=$(CURDIR)/colcon.defaults.yaml @@ -12,7 +15,7 @@ export HOST_UID := $(shell id -u $(CURRENT_USER)) export HOST_GID := $(shell id -g $(CURRENT_USER)) .PHONY: help dev-image dev-shell dev-list dev-build dev-test dev-lint dev-check \ - mock-image mock-e2e \ + mock-image mock-e2e isaac-config isaac-image isaac-up isaac-gui isaac-down isaac-smoke \ list-local build-local test-local lint-local check-local help: @@ -26,6 +29,12 @@ help: @echo " make dev-check Run the complete PR/CI check" @echo " make mock-image Build the CPU mock runtime image" @echo " make mock-e2e Run and validate the three-container mock stack" + @echo " make isaac-config Validate the Isaac Compose configuration" + @echo " make isaac-image Build the pinned Isaac env image" + @echo " make isaac-up Start the Isaac env using the configured mode" + @echo " make isaac-gui Build and run the Isaac GUI until Ctrl-C" + @echo " make isaac-down Stop the Isaac validation stack" + @echo " make isaac-smoke Run the manual GPU/ROS 2 Bridge smoke" dev-image: $(DEV_COMPOSE) build dev @@ -54,6 +63,24 @@ mock-image: mock-e2e: tools/e2e/run_mock_compose.sh +isaac-config: + $(ISAAC_COMPOSE) config --quiet + +isaac-image: + $(ISAAC_COMPOSE) build env + +isaac-up: + $(ISAAC_COMPOSE) up --detach env + +isaac-gui: + RH_ISAAC_ENV_FILE=$(ISAAC_ENV_FILE) tools/dev/run_isaac_gui.sh + +isaac-down: + $(ISAAC_COMPOSE) down --remove-orphans + +isaac-smoke: + RH_ISAAC_ENV_FILE=$(ISAAC_ENV_FILE) tools/e2e/run_isaac_bridge_smoke.sh + # The *-local targets are internal entry points used inside the development image. list-local: $(COLCON_ENV) colcon list --base-paths $(BASE_PATHS) @@ -68,6 +95,10 @@ test-local: build-local lint-local: python3 tools/validation/check_repository.py - ruff check tools tests packages agents tasks evaluators + python3 tools/validation/check_isaac_skeleton.py + bash -n deployment/docker/env/entrypoint.sh \ + simulators/isaac/scripts/launch.sh tools/dev/run_isaac_gui.sh \ + tools/e2e/run_isaac_bridge_smoke.sh + ruff check tools tests packages agents tasks evaluators simulators check-local: lint-local list-local test-local diff --git a/deployment/compose/compose.isaac.yaml b/deployment/compose/compose.isaac.yaml new file mode 100644 index 0000000..6fa6678 --- /dev/null +++ b/deployment/compose/compose.isaac.yaml @@ -0,0 +1,62 @@ +name: roboharness-isaac + +x-isaac-environment: &isaac-environment + ACCEPT_EULA: ${ACCEPT_EULA:-N} + PRIVACY_CONSENT: ${PRIVACY_CONSENT:-N} + ROS_DISTRO: ${RH_ROS_DISTRO} + ROS_DOMAIN_ID: ${RH_ISAAC_ROS_DOMAIN_ID:-42} + ROS_LOCALHOST_ONLY: "0" + RMW_IMPLEMENTATION: rmw_fastrtps_cpp + +services: + env: + image: ${RH_ISAAC_ENV_IMAGE} + build: + context: ../.. + dockerfile: deployment/docker/env/Dockerfile + args: + ISAAC_SIM_IMAGE: ${RH_ISAAC_SIM_IMAGE} + ROS_BASE_IMAGE: ${RH_ROS_BASE_IMAGE} + init: true + restart: "no" + stop_grace_period: 30s + network_mode: host + ipc: host + gpus: all + shm_size: 2gb + environment: + <<: *isaac-environment + DISPLAY: ${DISPLAY:-} + command: ["${RH_ISAAC_MODE:-headless}"] + volumes: + - isaac-kit-cache:/isaac-sim/kit/cache + - isaac-ov-cache:/root/.cache/ov + - isaac-pip-cache:/root/.cache/pip + - isaac-gl-cache:/root/.cache/nvidia/GLCache + - isaac-compute-cache:/root/.nv/ComputeCache + - isaac-data:/root/.local/share/ov/data + - isaac-logs:/root/.nvidia-omniverse/logs + - /tmp/.X11-unix:/tmp/.X11-unix:rw + + bridge-observer: + profiles: ["validation"] + image: ${RH_ROS_BASE_IMAGE} + init: true + restart: "no" + network_mode: host + ipc: host + environment: + ROS_DISTRO: ${RH_ROS_DISTRO} + ROS_DOMAIN_ID: ${RH_ISAAC_ROS_DOMAIN_ID:-42} + ROS_LOCALHOST_ONLY: "0" + RMW_IMPLEMENTATION: rmw_fastrtps_cpp + command: ["sleep", "infinity"] + +volumes: + isaac-kit-cache: + isaac-ov-cache: + isaac-pip-cache: + isaac-gl-cache: + isaac-compute-cache: + isaac-data: + isaac-logs: diff --git a/deployment/docker/env/Dockerfile b/deployment/docker/env/Dockerfile new file mode 100644 index 0000000..2cda607 --- /dev/null +++ b/deployment/docker/env/Dockerfile @@ -0,0 +1,56 @@ +ARG ROS_BASE_IMAGE=public.ecr.aws/docker/library/ros:humble-ros-base-jammy +ARG ISAAC_SIM_IMAGE=nvcr.io/nvidia/isaac-sim:4.5.0 + +FROM ${ROS_BASE_IMAGE} AS ros-builder + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + build-essential \ + cmake \ + python3-colcon-common-extensions \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace/roboharness +COPY packages/rh_interfaces packages/rh_interfaces + +RUN source /opt/ros/humble/setup.bash \ + && colcon build \ + --merge-install \ + --install-base /opt/roboharness \ + --base-paths packages \ + --packages-select rh_interfaces + +FROM ${ISAAC_SIM_IMAGE} AS isaac-env + +ARG ISAAC_SIM_IMAGE + +LABEL org.opencontainers.image.title="RoboHarness Isaac environment" \ + org.opencontainers.image.description="Isaac Sim 4.5 backend and native ROS 2 Bridge" \ + org.opencontainers.image.version="4.5.0" \ + org.opencontainers.image.base.name="${ISAAC_SIM_IMAGE}" + +USER root + +COPY --from=ros-builder /opt/roboharness /opt/roboharness +COPY simulators/isaac /opt/roboharness/simulators/isaac +COPY deployment/docker/env/entrypoint.sh /usr/local/bin/roboharness-isaac-entrypoint + +RUN chmod 0755 \ + /usr/local/bin/roboharness-isaac-entrypoint \ + /opt/roboharness/simulators/isaac/scripts/launch.sh + +ENV RH_ISAAC_ROOT=/isaac-sim \ + RH_ISAAC_APPS=/isaac-sim/apps \ + RH_ISAAC_BASE_APP=/isaac-sim/apps/isaacsim.exp.base.kit \ + RH_ISAAC_APP=/opt/roboharness/simulators/isaac/apps/rh.kit \ + RH_ISAAC_EXTENSIONS=/opt/roboharness/simulators/isaac/extensions \ + RH_ROS_PYTHON_PATH=/opt/roboharness/local/lib/python3.10/dist-packages \ + AMENT_PREFIX_PATH=/opt/roboharness \ + PYTHONPATH=/opt/roboharness/local/lib/python3.10/dist-packages \ + LD_LIBRARY_PATH=/opt/roboharness/lib + +WORKDIR /isaac-sim +ENTRYPOINT ["/usr/local/bin/roboharness-isaac-entrypoint"] +CMD ["headless"] diff --git a/deployment/docker/env/entrypoint.sh b/deployment/docker/env/entrypoint.sh new file mode 100755 index 0000000..e0905bf --- /dev/null +++ b/deployment/docker/env/entrypoint.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -eo pipefail + +if [[ "${ACCEPT_EULA:-N}" != "Y" ]]; then + echo "Isaac Sim EULA is not accepted. Set ACCEPT_EULA=Y in deployment/env/.env." >&2 + exit 64 +fi + +exec /opt/roboharness/simulators/isaac/scripts/launch.sh "$@" diff --git a/deployment/env/.env.example b/deployment/env/.env.example new file mode 100644 index 0000000..d4cb736 --- /dev/null +++ b/deployment/env/.env.example @@ -0,0 +1,12 @@ +# Copy this file to deployment/env/.env before running the Isaac profile. +# Setting ACCEPT_EULA=Y confirms acceptance of the NVIDIA Omniverse EULA for +# the Isaac Sim container. RoboHarness never enables this automatically. +ACCEPT_EULA=N + +# Y opts in to NVIDIA data collection; N keeps it disabled. +PRIVACY_CONSENT=N + +# headless is the PR 13 validation default. `make isaac-gui` overrides this +# value only for its own process and requires a local DISPLAY/X11 socket. +RH_ISAAC_MODE=headless +DISPLAY= diff --git a/deployment/env/versions.env b/deployment/env/versions.env index f8711bf..2dedd96 100644 --- a/deployment/env/versions.env +++ b/deployment/env/versions.env @@ -21,7 +21,16 @@ RH_VALIDATED_RUFF_VERSION=0.11.13 # project platform. PR 13 validates the exact image digest, host driver, and # GPU runtime; do not infer those from the CPU development image. RH_ISAAC_SIM_VERSION=4.5.0 +RH_ISAAC_SIM_IMAGE=nvcr.io/nvidia/isaac-sim:4.5.0@sha256:c2f47dc82a7714af08d3766efe80ac9d084c2b37b5d0dfbd074797ec56390fc7 +RH_ISAAC_ENV_IMAGE=roboharness-isaac-env:4.5.0 RH_ISAAC_REFERENCE_ROBOT=anymal_c +RH_ISAAC_ROS_DOMAIN_ID=42 +RH_ISAAC_MIN_DRIVER_VERSION=535.129.03 +RH_ISAAC_KERNEL_6_8_DRIVER_VERSION=535.216.01 +RH_ISAAC_VALIDATED_DRIVER_VERSION=580.173.02 +RH_ISAAC_VALIDATED_CONTAINER_TOOLKIT_VERSION=1.20.0 +RH_ISAAC_VALIDATED_DOCKER_ENGINE_VERSION=29.7.2 +RH_ISAAC_VALIDATED_DOCKER_COMPOSE_VERSION=5.5.0 # The MVP deploys the ANYmal C policy bundled with Isaac Sim. Isaac Lab is not # installed in the runtime image; add and pin it only if policy training or diff --git a/simulators/isaac/apps/rh.kit b/simulators/isaac/apps/rh.kit new file mode 100644 index 0000000..75070ce --- /dev/null +++ b/simulators/isaac/apps/rh.kit @@ -0,0 +1,22 @@ +[package] +title = "RoboHarness Isaac Backend" +description = "Minimal Isaac Sim 4.5 application for the RoboHarness environment backend" +version = "0.1.0" +keywords = ["app", "isaac", "robotics", "ros2"] + +[dependencies] +"omni.kit.loop-isaac" = {} +"isaacsim.ros2.bridge" = {} +"rh.isaac" = {} + +[settings] +app.name = "RoboHarness Isaac Backend" +app.version = "0.1.0" +app.fastShutdown = true +app.file.ignoreUnsavedOnExit = true +app.runLoopsGlobal.syncToPresent = false +app.runLoops.main.rateLimitEnabled = true +app.runLoops.main.rateLimitFrequency = 60 +app.runLoops.main.rateLimitUseBusyLoop = false +persistent.simulation.minFrameRate = 60 +exts."isaacsim.ros2.bridge".ros_distro = "humble" diff --git a/simulators/isaac/bridge/topics.yaml b/simulators/isaac/bridge/topics.yaml new file mode 100644 index 0000000..dc3e994 --- /dev/null +++ b/simulators/isaac/bridge/topics.yaml @@ -0,0 +1,14 @@ +schema_version: 1 +backend: isaac_sim_4_5 + +native_bridge: + extension: isaacsim.ros2.bridge + clock: + graph_path: /RoboHarness/ROS2Clock + topic: /clock + type: rosgraph_msgs/msg/Clock + source: simulation_time + +# Sensor, TF, odometry, and command graphs are intentionally introduced with +# the ANYmal C binding in PR 14. They must use native Bridge nodes rather than +# project-owned ROS transport copies. diff --git a/simulators/isaac/extensions/rh.isaac/config/extension.toml b/simulators/isaac/extensions/rh.isaac/config/extension.toml new file mode 100644 index 0000000..9dbec32 --- /dev/null +++ b/simulators/isaac/extensions/rh.isaac/config/extension.toml @@ -0,0 +1,20 @@ +[package] +title = "RoboHarness Isaac Backend" +description = "Lifecycle glue and native ROS 2 Bridge graph for RoboHarness" +version = "0.1.0" +category = "Robotics" +keywords = ["isaac", "ros2", "roboharness"] + +[core] +reloadable = false + +[dependencies] +"omni.graph.core" = {} +"omni.timeline" = {} +"omni.usd" = {} +"isaacsim.core.api" = {} +"isaacsim.core.nodes" = {} +"isaacsim.ros2.bridge" = {} + +[[python.module]] +name = "rh.isaac" diff --git a/simulators/isaac/extensions/rh.isaac/rh/__init__.py b/simulators/isaac/extensions/rh.isaac/rh/__init__.py new file mode 100644 index 0000000..d50b2c5 --- /dev/null +++ b/simulators/isaac/extensions/rh.isaac/rh/__init__.py @@ -0,0 +1 @@ +"""RoboHarness Isaac Kit extension namespace.""" diff --git a/simulators/isaac/extensions/rh.isaac/rh/isaac/__init__.py b/simulators/isaac/extensions/rh.isaac/rh/isaac/__init__.py new file mode 100644 index 0000000..cba2b98 --- /dev/null +++ b/simulators/isaac/extensions/rh.isaac/rh/isaac/__init__.py @@ -0,0 +1,17 @@ +"""RoboHarness Isaac backend extension.""" + +from __future__ import annotations + +import os +import sys + +_ROS_PYTHON_PATH = os.environ.get( + "RH_ROS_PYTHON_PATH", + "/opt/roboharness/local/lib/python3.10/dist-packages", +) +if _ROS_PYTHON_PATH not in sys.path: + sys.path.insert(0, _ROS_PYTHON_PATH) + +from .extension import Extension # noqa: E402 - overlay must precede this import + +__all__ = ["Extension"] diff --git a/simulators/isaac/extensions/rh.isaac/rh/isaac/clock_graph.py b/simulators/isaac/extensions/rh.isaac/rh/isaac/clock_graph.py new file mode 100644 index 0000000..e68d5ba --- /dev/null +++ b/simulators/isaac/extensions/rh.isaac/rh/isaac/clock_graph.py @@ -0,0 +1,44 @@ +"""Native Isaac Sim ROS 2 Bridge graph construction.""" + +from __future__ import annotations + +CLOCK_GRAPH_PATH = "/RoboHarness/ROS2Clock" + + +def ensure_clock_graph() -> str: + """Create the idempotent simulation-clock publishing Action Graph.""" + + import omni.graph.core as og + import omni.usd + + stage = omni.usd.get_context().get_stage() + if stage is None: + raise RuntimeError("USD stage is unavailable") + if stage.GetPrimAtPath(CLOCK_GRAPH_PATH).IsValid(): + return CLOCK_GRAPH_PATH + + keys = og.Controller.Keys + og.Controller.edit( + { + "graph_path": CLOCK_GRAPH_PATH, + "evaluator_name": "execution", + "pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_SIMULATION, + }, + { + keys.CREATE_NODES: [ + ("OnPlaybackTick", "omni.graph.action.OnPlaybackTick"), + ("RosContext", "isaacsim.ros2.bridge.ROS2Context"), + ("ReadSimulationTime", "isaacsim.core.nodes.IsaacReadSimulationTime"), + ("PublishClock", "isaacsim.ros2.bridge.ROS2PublishClock"), + ], + keys.CONNECT: [ + ("OnPlaybackTick.outputs:tick", "PublishClock.inputs:execIn"), + ("RosContext.outputs:context", "PublishClock.inputs:context"), + ( + "ReadSimulationTime.outputs:simulationTime", + "PublishClock.inputs:timeStamp", + ), + ], + }, + ) + return CLOCK_GRAPH_PATH diff --git a/simulators/isaac/extensions/rh.isaac/rh/isaac/extension.py b/simulators/isaac/extensions/rh.isaac/rh/isaac/extension.py new file mode 100644 index 0000000..74df679 --- /dev/null +++ b/simulators/isaac/extensions/rh.isaac/rh/isaac/extension.py @@ -0,0 +1,187 @@ +"""Kit lifecycle entry point for the PR 13 Isaac backend skeleton.""" + +from __future__ import annotations + +import asyncio +import time +from contextlib import suppress + +import carb +import omni.ext +import omni.kit.app +import omni.timeline +import rclpy +from rclpy.node import Node +from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy +from rh_interfaces.msg import ComponentStatus + +from .clock_graph import ensure_clock_graph + +_COMPONENT_ID = "isaac_env" +_STATUS_TOPIC = "/roboharness/env/status" +_INITIALIZATION_ERROR = 1301 + + +def _status_qos() -> QoSProfile: + return QoSProfile( + history=HistoryPolicy.KEEP_LAST, + depth=1, + reliability=ReliabilityPolicy.RELIABLE, + durability=DurabilityPolicy.TRANSIENT_LOCAL, + ) + + +class _StatusPublisher: + """Minimal Env status publisher; reusable protocol helpers stay outside Kit.""" + + def __init__(self, node: Node) -> None: + self._node = node + self._publisher = node.create_publisher( + ComponentStatus, + _STATUS_TOPIC, + _status_qos(), + ) + self._message = ComponentStatus() + self.transition(ComponentStatus.STARTING) + + def transition( + self, + state: int, + *, + error_code: int = 0, + detail: str = "", + restart_required: bool = False, + ) -> None: + self._message.stamp = self._node.get_clock().now().to_msg() + self._message.component_id = _COMPONENT_ID + self._message.state = state + self._message.error_code = error_code + self._message.detail = detail + self._message.restart_required = restart_required + self.publish() + + def publish(self) -> None: + self._publisher.publish(self._message) + + +class Extension(omni.ext.IExt): + """Start the Bridge smoke graph without claiming a complete Env READY state.""" + + def on_startup(self, ext_id: str) -> None: + self._ext_id = ext_id + self._node = None + self._status = None + self._owns_rclpy = False + self._next_heartbeat = time.monotonic() + self._init_task = None + self._spin_task = None + self._simulation_context = None + self._app_ready_sub = None + self._graph_initialized = False + carb.log_info("[rh.isaac] STARTING Isaac backend and native ROS 2 Bridge") + try: + self._start_status_publisher() + except Exception as exc: + carb.log_error(f"[rh.isaac] status publisher failed: {exc!r}") + return + + app = omni.kit.app.get_app() + if app.is_app_ready(): + self._on_app_ready(None) + return + self._app_ready_sub = ( + app.get_startup_event_stream().create_subscription_to_pop_by_type( + omni.kit.app.EVENT_APP_READY, + self._on_app_ready, + name="rh.isaac initialize Bridge graph", + ) + ) + if app.is_app_ready(): + self._on_app_ready(None) + + def _on_app_ready(self, _event) -> None: + self._app_ready_sub = None + if self._graph_initialized: + return + self._graph_initialized = True + self._init_task = asyncio.ensure_future(self._initialize_backend()) + + async def _initialize_backend(self) -> None: + try: + if omni.usd.get_context().get_stage() is None: + raise RuntimeError("USD stage is unavailable after Kit app ready") + + # In an extension workflow Kit owns the render/update loop, while + # SimulationContext owns Isaac's physics/timeline lifecycle. + from isaacsim.core.api import SimulationContext + + self._simulation_context = SimulationContext( + physics_dt=1.0 / 60.0, + rendering_dt=1.0 / 60.0, + ) + await self._simulation_context.initialize_simulation_context_async() + graph_path = ensure_clock_graph() + await self._simulation_context.play_async() + self._status.transition( + ComponentStatus.STARTING, + detail="native ROS 2 Bridge clock smoke active; full READY deferred to PR 16", + ) + self._spin_task = asyncio.ensure_future(self._spin_status()) + carb.log_info(f"[rh.isaac] clock graph active at {graph_path}") + except Exception as exc: + carb.log_error(f"[rh.isaac] initialization failed: {exc!r}") + if self._status is not None: + self._status.transition( + ComponentStatus.ERROR, + error_code=_INITIALIZATION_ERROR, + detail=str(exc), + restart_required=True, + ) + + def _start_status_publisher(self) -> None: + if not rclpy.ok(): + rclpy.init(args=None) + self._owns_rclpy = True + self._node = rclpy.create_node("rh_isaac_backend") + self._status = _StatusPublisher(self._node) + + async def _spin_status(self) -> None: + try: + while self._node is not None and rclpy.ok(): + rclpy.spin_once(self._node, timeout_sec=0.0) + now = time.monotonic() + if now >= self._next_heartbeat: + self._status.publish() + self._next_heartbeat = now + 1.0 + await omni.kit.app.get_app().next_update_async() + except asyncio.CancelledError: + raise + except Exception as exc: + carb.log_error(f"[rh.isaac] clock/status loop failed: {exc!r}") + if self._status is not None: + self._status.transition( + ComponentStatus.ERROR, + error_code=_INITIALIZATION_ERROR, + detail=str(exc), + restart_required=True, + ) + + def on_shutdown(self) -> None: + omni.timeline.get_timeline_interface().stop() + self._app_ready_sub = None + if self._init_task is not None and not self._init_task.done(): + self._init_task.cancel() + if self._spin_task is not None and not self._spin_task.done(): + self._spin_task.cancel() + if self._simulation_context is not None: + with suppress(Exception): + type(self._simulation_context).clear_instance() + self._simulation_context = None + if self._node is not None: + with suppress(Exception): + self._node.destroy_node() + self._node = None + if self._owns_rclpy and rclpy.ok(): + with suppress(Exception): + rclpy.shutdown() + carb.log_info("[rh.isaac] extension stopped") diff --git a/simulators/isaac/scripts/launch.sh b/simulators/isaac/scripts/launch.sh new file mode 100755 index 0000000..7881aff --- /dev/null +++ b/simulators/isaac/scripts/launch.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -eo pipefail + +ISAAC_ROOT="${RH_ISAAC_ROOT:-/isaac-sim}" +ISAAC_APPS_PATH="${RH_ISAAC_APPS:-${ISAAC_ROOT}/apps}" +BASE_APP="${RH_ISAAC_BASE_APP:-${ISAAC_APPS_PATH}/isaacsim.exp.base.kit}" +APP_PATH="${RH_ISAAC_APP:-/opt/roboharness/simulators/isaac/apps/rh.kit}" +EXTENSIONS_PATH="${RH_ISAAC_EXTENSIONS:-/opt/roboharness/simulators/isaac/extensions}" +ROS_DISTRO="${ROS_DISTRO:-humble}" +BRIDGE_ROS_LIB="${ISAAC_ROOT}/exts/isaacsim.ros2.bridge/${ROS_DISTRO}/lib" +MODE="${1:-headless}" +shift || true + +if [[ -f "${ISAAC_ROOT}/setup_python_env.sh" ]]; then + # The NVIDIA launcher uses this environment for Kit's bundled Python and + # native ROS 2 Bridge libraries. + source "${ISAAC_ROOT}/setup_python_env.sh" +fi + +export AMENT_PREFIX_PATH="/opt/roboharness${AMENT_PREFIX_PATH:+:${AMENT_PREFIX_PATH}}" +export PYTHONPATH="/opt/roboharness/local/lib/python3.10/dist-packages${PYTHONPATH:+:${PYTHONPATH}}" +export ROS_DISTRO +export RMW_IMPLEMENTATION="${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp}" +export LD_LIBRARY_PATH="${BRIDGE_ROS_LIB}:/opt/roboharness/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +KIT=( + "${ISAAC_ROOT}/kit/kit" + "${BASE_APP}" + "--merge-config=${APP_PATH}" + --ext-folder "${ISAAC_APPS_PATH}" + --ext-folder "${EXTENSIONS_PATH}" + --enable isaacsim.ros2.bridge + --enable rh.isaac +) + +if [[ "${EUID}" -eq 0 ]]; then + KIT+=(--allow-root) +fi + +case "${MODE}" in + headless) + KIT+=(--no-window) + ;; + gui) + if [[ -z "${DISPLAY:-}" ]]; then + echo "GUI mode requires DISPLAY and the X11 socket." >&2 + exit 64 + fi + ;; + *) + echo "Unsupported Isaac launch mode: ${MODE}; expected headless or gui." >&2 + exit 64 + ;; +esac + +exec "${KIT[@]}" "$@" diff --git a/tools/dev/run_isaac_gui.sh b/tools/dev/run_isaac_gui.sh new file mode 100755 index 0000000..d2e7e8e --- /dev/null +++ b/tools/dev/run_isaac_gui.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +VERSIONS_FILE="${ROOT_DIR}/deployment/env/versions.env" +ISAAC_ENV_FILE="${RH_ISAAC_ENV_FILE:-${ROOT_DIR}/deployment/env/.env}" +HOST_DISPLAY="${DISPLAY:-}" +XHOST_ENTRY="SI:localuser:root" +x_access_added=false + +fail() { + echo "Isaac GUI launch failed: $*" >&2 + exit 1 +} + +[[ -n "${HOST_DISPLAY}" ]] \ + || fail "DISPLAY is empty; run this command from a local graphical session" +[[ -f "${ISAAC_ENV_FILE}" ]] \ + || fail "missing ${ISAAC_ENV_FILE}; copy deployment/env/.env.example first" + +display_number="${HOST_DISPLAY##*:}" +display_number="${display_number%%.*}" +[[ -S "/tmp/.X11-unix/X${display_number}" ]] \ + || fail "the local X11 socket for DISPLAY=${HOST_DISPLAY} is unavailable" + +for command_name in docker nvidia-container-cli nvidia-smi xhost; do + command -v "${command_name}" >/dev/null \ + || fail "required command is unavailable: ${command_name}" +done + +set -a +source "${VERSIONS_FILE}" +source "${ISAAC_ENV_FILE}" +set +a + +[[ "${ACCEPT_EULA:-N}" == "Y" ]] \ + || fail "set ACCEPT_EULA=Y only after accepting the NVIDIA Omniverse EULA" + +# The GUI launcher is intentionally an ephemeral override. It does not rewrite +# the developer's default headless setting in deployment/env/.env. +export DISPLAY="${HOST_DISPLAY}" +export RH_ISAAC_MODE=gui + +COMPOSE=( + docker compose + --env-file "${VERSIONS_FILE}" + --env-file "${ISAAC_ENV_FILE}" + --project-name roboharness-isaac + -f "${ROOT_DIR}/deployment/compose/compose.isaac.yaml" +) + +cleanup() { + local exit_code=$? + trap - EXIT + "${COMPOSE[@]}" down --remove-orphans >/dev/null 2>&1 || true + if [[ "${x_access_added}" == true ]]; then + xhost -si:localuser:root >/dev/null 2>&1 || true + fi + exit "${exit_code}" +} + +docker compose version >/dev/null +docker info >/dev/null +nvidia-smi >/dev/null +nvidia-container-cli --version >/dev/null +"${COMPOSE[@]}" config --quiet + +if [[ -n "$("${COMPOSE[@]}" ps --quiet)" ]]; then + fail "the roboharness-isaac stack is already running; stop it with make isaac-down" +fi + +# Preserve a pre-existing ACL entry. Revoke only the access added by this run. +trap cleanup EXIT +if ! xhost | grep -Fq "${XHOST_ENTRY}"; then + xhost +si:localuser:root >/dev/null + x_access_added=true +fi + +echo "Starting Isaac Sim GUI on DISPLAY=${DISPLAY}. Press Ctrl-C to stop and clean up." +echo "The first RTX shader warm-up may take several minutes; choose Wait, not Force Quit." +"${COMPOSE[@]}" up --build env diff --git a/tools/e2e/run_isaac_bridge_smoke.sh b/tools/e2e/run_isaac_bridge_smoke.sh new file mode 100755 index 0000000..9e2b0be --- /dev/null +++ b/tools/e2e/run_isaac_bridge_smoke.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +VERSIONS_FILE="${ROOT_DIR}/deployment/env/versions.env" +ISAAC_ENV_FILE="${RH_ISAAC_ENV_FILE:-${ROOT_DIR}/deployment/env/.env}" +RUN_TOKEN="${RH_ISAAC_RUN_TOKEN:-$(date -u +%Y%m%dT%H%M%SZ)-$$}" +RUN_DIR="${RH_ISAAC_RESULTS_ROOT:-${ROOT_DIR}/.build/isaac-smoke}/${RUN_TOKEN}" +DEADLINE_SECONDS="${RH_ISAAC_TIMEOUT_S:-600}" + +fail() { + echo "Isaac Bridge smoke failed: $*" >&2 + return 1 +} + +[[ -f "${ISAAC_ENV_FILE}" ]] \ + || fail "missing ${ISAAC_ENV_FILE}; copy deployment/env/.env.example first" + +set -a +source "${VERSIONS_FILE}" +source "${ISAAC_ENV_FILE}" +set +a + +[[ "${ACCEPT_EULA:-N}" == "Y" ]] \ + || fail "set ACCEPT_EULA=Y only after accepting the NVIDIA Omniverse EULA" +[[ "${RH_ISAAC_MODE:-headless}" == "headless" ]] \ + || fail "the automated smoke requires RH_ISAAC_MODE=headless" + +for command_name in docker nvidia-container-cli nvidia-smi timeout; do + command -v "${command_name}" >/dev/null \ + || fail "required command is unavailable: ${command_name}" +done + +mkdir -p "${RUN_DIR}" + +COMPOSE=( + docker compose + --env-file "${VERSIONS_FILE}" + --env-file "${ISAAC_ENV_FILE}" + --project-name roboharness-isaac + --profile validation + -f "${ROOT_DIR}/deployment/compose/compose.isaac.yaml" +) + +observer_exec() { + "${COMPOSE[@]}" exec -T bridge-observer bash -lc \ + 'source "/opt/ros/${ROS_DISTRO}/setup.bash"; exec "$@"' bash "$@" +} + +cleanup() { + local exit_code=$? + "${COMPOSE[@]}" logs --no-color >"${RUN_DIR}/compose.log" 2>&1 || true + "${COMPOSE[@]}" ps --all >"${RUN_DIR}/compose-ps.txt" 2>&1 || true + "${COMPOSE[@]}" down --remove-orphans >/dev/null 2>&1 || true + return "${exit_code}" +} +trap cleanup EXIT + +nvidia-smi >"${RUN_DIR}/nvidia-smi.txt" +nvidia-container-cli --version >"${RUN_DIR}/nvidia-container-toolkit-version.txt" +docker version >"${RUN_DIR}/docker-version.txt" +docker compose version >"${RUN_DIR}/compose-version.txt" +"${COMPOSE[@]}" config >"${RUN_DIR}/compose-resolved.yaml" + +echo "Building the pinned Isaac Sim 4.5 environment image..." +"${COMPOSE[@]}" build env +docker image inspect "${RH_ISAAC_ENV_IMAGE}" >"${RUN_DIR}/env-image.json" + +echo "Starting Isaac env and the external ROS 2 observer..." +"${COMPOSE[@]}" up --detach env bridge-observer + +env_id="$("${COMPOSE[@]}" ps -q env)" +observer_id="$("${COMPOSE[@]}" ps -q bridge-observer)" +[[ -n "${env_id}" && -n "${observer_id}" ]] \ + || fail "Compose did not create both validation containers" +docker inspect --format '{{.Id}} {{.State.Pid}} {{.RestartCount}}' \ + "${env_id}" "${observer_id}" >"${RUN_DIR}/containers.before" + +deadline=$((SECONDS + DEADLINE_SECONDS)) +clock_seen=false +while ((SECONDS < deadline)); do + if [[ "$(docker inspect --format '{{.State.Running}}' "${env_id}")" != true ]]; then + fail "Isaac env exited before the Bridge became observable" + fi + if observer_exec timeout 10 ros2 topic echo /clock --once \ + >"${RUN_DIR}/clock.txt" 2>"${RUN_DIR}/clock.stderr"; then + clock_seen=true + break + fi + sleep 2 +done +[[ "${clock_seen}" == true ]] || fail "/clock was not received before the deadline" + +observer_exec ros2 node list \ + >"${RUN_DIR}/ros-nodes.txt" +observer_exec ros2 topic list -t \ + >"${RUN_DIR}/ros-topics.txt" +observer_exec ros2 topic info /clock --verbose \ + >"${RUN_DIR}/clock-info.txt" + +grep -Fq '/clock [rosgraph_msgs/msg/Clock]' "${RUN_DIR}/ros-topics.txt" \ + || fail "external observer reported an unexpected /clock type" +grep -Fq '/roboharness/env/status [rh_interfaces/msg/ComponentStatus]' \ + "${RUN_DIR}/ros-topics.txt" \ + || fail "Isaac extension status topic is absent from the external ROS graph" + +docker inspect --format '{{.Id}} {{.State.Pid}} {{.RestartCount}}' \ + "${env_id}" "${observer_id}" >"${RUN_DIR}/containers.after" +cmp --silent "${RUN_DIR}/containers.before" "${RUN_DIR}/containers.after" \ + || fail "a validation container restarted during the smoke" + +"${COMPOSE[@]}" logs --no-color env >"${RUN_DIR}/env.log" +# Kit persists carb Info records separately and does not reliably mirror them +# to Compose stdout. The externally received clock plus its publisher metadata +# are the runtime contract; do not make the smoke depend on log routing. +grep -Eq 'Publisher count: [1-9][0-9]*' "${RUN_DIR}/clock-info.txt" \ + || fail "external observer did not find a /clock publisher" + +cat >"${RUN_DIR}/result.txt" < str: + return (ROOT / relative).read_text(encoding="utf-8") + + +def version_values() -> dict[str, str]: + values: dict[str, str] = {} + for line in read("deployment/env/versions.env").splitlines(): + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", maxsplit=1) + values[key] = value + return values + + +def main() -> int: + errors: list[str] = [] + versions = version_values() + if versions.get("RH_ISAAC_SIM_VERSION") != "4.5.0": + errors.append("RH_ISAAC_SIM_VERSION must remain 4.5.0") + if not DIGEST.fullmatch(versions.get("RH_ISAAC_SIM_IMAGE", "")): + errors.append("RH_ISAAC_SIM_IMAGE must pin the 4.5.0 sha256 manifest") + if any("LAB" in key for key in versions): + errors.append("Isaac Lab must not be a pinned MVP runtime dependency") + + app = read("simulators/isaac/apps/rh.kit") + for dependency in ( + '"omni.kit.loop-isaac"', + '"isaacsim.ros2.bridge"', + '"rh.isaac"', + ): + if dependency not in app: + errors.append(f"rh.kit is missing dependency {dependency}") + + extension = read("simulators/isaac/extensions/rh.isaac/config/extension.toml") + if 'name = "rh.isaac"' not in extension: + errors.append("extension.toml does not expose the rh.isaac Python module") + extension_init = read( + "simulators/isaac/extensions/rh.isaac/rh/isaac/__init__.py" + ) + if "RH_ROS_PYTHON_PATH" not in extension_init: + errors.append("rh.isaac must register the colcon Python overlay with Kit") + + bridge = yaml.safe_load(read("simulators/isaac/bridge/topics.yaml")) + clock = bridge.get("native_bridge", {}).get("clock", {}) + if clock.get("topic") != "/clock" or clock.get("type") != "rosgraph_msgs/msg/Clock": + errors.append("native Bridge mapping must expose rosgraph_msgs/msg/Clock on /clock") + + graph = read( + "simulators/isaac/extensions/rh.isaac/rh/isaac/clock_graph.py" + ) + for node_type in ( + "omni.graph.action.OnPlaybackTick", + "isaacsim.core.nodes.IsaacReadSimulationTime", + "isaacsim.ros2.bridge.ROS2PublishClock", + ): + if node_type not in graph: + errors.append(f"clock graph is missing native node {node_type}") + extension_runtime = read( + "simulators/isaac/extensions/rh.isaac/rh/isaac/extension.py" + ) + for ready_signal in ("EVENT_APP_READY", "is_app_ready"): + if ready_signal not in extension_runtime: + errors.append(f"rh.isaac is missing Kit readiness guard {ready_signal}") + + launcher = read("simulators/isaac/scripts/launch.sh") + if "isaacsim.exp.base.kit" not in launcher or "--merge-config" not in launcher: + errors.append("launcher must merge rh.kit over the bundled Isaac base app") + if '"${ISAAC_APPS_PATH}"' not in launcher: + errors.append("launcher must register the bundled Isaac apps directory") + for extension_id in ("isaacsim.ros2.bridge", "rh.isaac"): + if f"--enable {extension_id}" not in launcher: + errors.append(f"launcher must explicitly enable {extension_id}") + if "isaacsim.ros2.bridge/${ROS_DISTRO}/lib" not in launcher: + errors.append("launcher must expose the bundled Bridge ROS libraries") + if "/opt/roboharness/local/lib/python3.10/dist-packages" not in launcher: + errors.append("launcher must expose the merged colcon Python install") + if "--allow-root" not in launcher: + errors.append("container launcher must explicitly allow Kit to run as root") + + if errors: + print("Isaac skeleton validation failed:") + for error in errors: + print(f"- {error}") + return 1 + print("Isaac Sim image pin, Kit extension, and native clock graph are valid.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validation/check_repository.py b/tools/validation/check_repository.py index 4161d9a..22c3419 100755 --- a/tools/validation/check_repository.py +++ b/tools/validation/check_repository.py @@ -22,19 +22,26 @@ "configs/README.md", "deployment/README.md", "deployment/compose/compose.dev.yaml", + "deployment/compose/compose.isaac.yaml", "deployment/compose/compose.mock.yaml", "deployment/docker/dev/Dockerfile", + "deployment/docker/env/Dockerfile", "deployment/docker/mock-runtime/Dockerfile", "docs/adr/0001-development-platform.md", "evaluators/README.md", "packages/README.md", "pyproject.toml", "simulators/README.md", + "simulators/isaac/apps/rh.kit", + "simulators/isaac/extensions/rh.isaac/config/extension.toml", "tasks/README.md", "tests/README.md", "tools/README.md", + "tools/dev/run_isaac_gui.sh", "tools/e2e/run_mock_compose.sh", + "tools/e2e/run_isaac_bridge_smoke.sh", "tools/e2e/validate_mock_result.py", + "tools/validation/check_isaac_skeleton.py", ) MARKDOWN_LINK = re.compile(r"(? Date: Mon, 24 Aug 2026 16:32:46 +0800 Subject: [PATCH 3/3] docs(isaac): document host setup and validation --- README.md | 30 +- deployment/README.md | 80 +++++- docs/README.md | 3 + docs/guides/README.md | 7 +- .../isaac-pr13-development-environment.md | 202 +++++++++++++ docs/guides/isaac-pr13-validation.md | 162 +++++++++++ .../nvidia-container-toolkit-host-setup.md | 266 ++++++++++++++++++ simulators/isaac/README.md | 52 ++++ tools/README.md | 10 +- 9 files changed, 795 insertions(+), 17 deletions(-) create mode 100644 docs/guides/isaac-pr13-development-environment.md create mode 100644 docs/guides/isaac-pr13-validation.md create mode 100644 docs/guides/nvidia-container-toolkit-host-setup.md create mode 100644 simulators/isaac/README.md diff --git a/README.md b/README.md index b8f52c3..34e1589 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,26 @@ make dev-lint make mock-e2e ``` +PR13 的 Isaac backend / 原生 ROS 2 Bridge 骨架使用: + +```bash +cp deployment/env/.env.example deployment/env/.env +# 阅读 NVIDIA EULA 后,将 ACCEPT_EULA 改为 Y。 +make isaac-config +make isaac-smoke +``` + +本地 Linux X11 图形会话可直接运行: + +```bash +make isaac-gui +``` + +首次配置 GPU 开发机请按 +[PR13 Isaac 开发环境与调整记录](docs/guides/isaac-pr13-development-environment.md) +区分宿主机、运行基线和本地 consent 配置,再执行 +[NVIDIA Container Toolkit 安装指南](docs/guides/nvidia-container-toolkit-host-setup.md)。 + 开发容器只是临时工具环境,不属于 RoboHarness 的运行服务。详细说明见 [部署与开发环境](deployment/README.md)和 [开发平台 ADR](docs/adr/0001-development-platform.md)。 @@ -89,13 +109,9 @@ make mock-e2e 三个独立容器,等待 ROS 图和三个 Episode 结果完成,校验产物后自动清理服务。 结果和容器日志保存在 `.build/mock-e2e/`。 -面向 Isaac Sim 的完整 MVP 运行方式仍将在后续 PR 中补齐: - -```bash -cp deployment/env/.env.example deployment/env/.env -docker compose --env-file deployment/env/.env \ - -f deployment/compose/compose.yaml up --build -``` +当前 Isaac profile 只验证 pinned image、Kit application、原生 Bridge、`/clock` +与跨容器 DDS discovery;它不包含 ANYmal C、运控、完整 Env reset 或 MVP E2E。 +完整运行方式仍将在 PR14–PR17 中补齐。 运行链: diff --git a/deployment/README.md b/deployment/README.md index f3b4da8..499ffa1 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -11,6 +11,7 @@ RoboHarness keeps development and execution concerns in separate images: | --- | --- | --- | --- | | `roboharness-dev` | edit, build, lint, and test the monorepo | yes | Colcon, compiler, CMake, Ruff | | `roboharness-mock-runtime` | execute the installed CPU mock stack | no | no repository lint/dev layer | +| `roboharness-isaac-env` | run the Isaac backend and native ROS 2 Bridge | no | no repository lint/dev layer | The mock runtime image is built with a multi-stage Dockerfile. Its builder stage compiles the required Colcon packages, while the final stage copies only @@ -67,8 +68,77 @@ container snapshots, ROS graph snapshots, and Compose logs are retained under `.build/mock-e2e/`; the stack is always removed on exit. This CPU image is a reproducible protocol and orchestration reference, not an -Isaac production image. Isaac Sim 4.5.0, Ubuntu 22.04, and ROS 2 Humble are the -selected runtime baseline; the exact GPU image digest and host-driver validation -remain the scope of PR 13. The MVP uses the ANYmal C locomotion policy bundled -with Isaac Sim, so Isaac Lab is not installed in the runtime image. Policy -training or export tooling will be selected and pinned separately if required. +Isaac production image. + +## Isaac backend skeleton + +PR 13 pins the Linux x86_64 Isaac Sim image to both version and manifest: + +```text +nvcr.io/nvidia/isaac-sim:4.5.0 +sha256:c2f47dc82a7714af08d3766efe80ac9d084c2b37b5d0dfbd074797ec56390fc7 +``` + +The env image adds only the installed `rh_interfaces` overlay and the +`simulators/isaac` application files. It contains no source workspace, Isaac +Lab, ANYmal C binding, policy training tools, Agent, Task, Evaluator, or +Experiment implementation. The `rh.isaac` extension enables the native +`isaacsim.ros2.bridge`, creates the simulation-clock Action Graph, and publishes +the platform Env status as `STARTING` or `ERROR`. It deliberately cannot claim +`READY` before PR 14 and PR 16 provide the robot binding and full Env contract. + +Create the local consent/configuration file and explicitly accept NVIDIA's EULA: + +```bash +cp deployment/env/.env.example deployment/env/.env +# Edit deployment/env/.env and set ACCEPT_EULA=Y after reviewing the EULA. +make isaac-config +make isaac-image +make isaac-up +``` + +On a local Linux X11 session, run the foreground GUI workflow without changing +the default headless value in `.env`: + +```bash +make isaac-gui +``` + +The GUI helper validates `DISPLAY`, temporarily grants only the container root +user local X access, and cleans up both Compose and the added X access on exit. + +`compose.isaac.yaml` uses host networking and a shared host IPC namespace for +the Linux-only PR 13 Fast DDS smoke, passes through all NVIDIA GPUs, and +persists shader/content caches in named volumes. Sharing IPC is required for +Fast DDS shared-memory user data; discovery alone can work across isolated IPC +namespaces while `/clock` delivery fails. `bridge-observer` is a +validation-profile helper, not a fourth runtime service. Stop the stack with +`make isaac-down`. + +The bounded manual validation command is: + +```bash +make isaac-smoke +``` + +It checks the host driver and Docker runtime, builds the pinned image, starts an +external ROS 2 Humble observer, receives one `/clock` message, checks the Env +status topic and container stability, and writes evidence under +`.build/isaac-smoke/`. See the +[PR 13 validation guide](../docs/guides/isaac-pr13-validation.md) for host +requirements, GUI launch, evidence contents, and limitations. Developers +preparing a new machine should first follow the +[NVIDIA Container Toolkit host setup guide](../docs/guides/nvidia-container-toolkit-host-setup.md). + +The complete separation between host configuration, repository pins, local +consent, and implementation changes is recorded in the +[PR 13 development environment notes](../docs/guides/isaac-pr13-development-environment.md). +The 2026-08-24 smoke passed on Ubuntu 22.04.5/kernel 6.8 with driver +`580.173.02`, NVIDIA Container Toolkit `1.20.0`, Docker Engine `29.7.2`, and +Compose `5.5.0`. These are validation evidence, not blanket minimum versions +for every host. + +Isaac Sim 4.5.0, Ubuntu 22.04, and ROS 2 Humble are the selected runtime +baseline. The MVP later uses the ANYmal C locomotion policy bundled with Isaac +Sim, so Isaac Lab is not installed in the runtime image. Policy training or +export tooling will be selected and pinned separately if required. diff --git a/docs/README.md b/docs/README.md index 54546a3..90ce568 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,9 @@ - [软件架构与分阶段 PR 开发规划](architecture-and-development-plan.md):项目定义、运行架构、ROS 2 接口、生命周期、仓库与 Docker 结构、测试策略、逐 PR 路线图及依赖关系。 - [ADR 0001:开发平台基线](adr/0001-development-platform.md):固定 Ubuntu 22.04、ROS 2 Humble、Python 3.10 和容器唯一开发路径。 +- [PR13 Isaac backend 验证](guides/isaac-pr13-validation.md):GPU/driver 前置条件、headless/GUI 启动、原生 ROS 2 Bridge smoke 与证据格式。 +- [PR13 Isaac 开发环境与调整记录](guides/isaac-pr13-development-environment.md):环境分层、已验证宿主机快照、实现调整、故障边界与新机器复现顺序。 +- [NVIDIA Container Toolkit 宿主机配置](guides/nvidia-container-toolkit-host-setup.md):Ubuntu/Docker/CDI 安装、不同宿主机边界、验证与故障分层。 ## 文档约定 diff --git a/docs/guides/README.md b/docs/guides/README.md index 0582dfd..33e25fc 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -1,5 +1,8 @@ # Guides User and developer workflows are added here when their corresponding capability -exists and can be verified. PR 01 keeps the authoritative bootstrap instructions -in the root README and deployment README. +exists and can be verified. + +- [PR 13 Isaac backend and native ROS 2 Bridge validation](isaac-pr13-validation.md) +- [PR13 Isaac development environment and implementation adjustments](isaac-pr13-development-environment.md) +- [NVIDIA Container Toolkit host setup](nvidia-container-toolkit-host-setup.md) diff --git a/docs/guides/isaac-pr13-development-environment.md b/docs/guides/isaac-pr13-development-environment.md new file mode 100644 index 0000000..89642bb --- /dev/null +++ b/docs/guides/isaac-pr13-development-environment.md @@ -0,0 +1,202 @@ +# PR13 Isaac 开发环境与调整记录 + +本文把 PR13 开发过程中确认的宿主机环境、仓库配置、实现调整和最终验证结果 +整理在一起。新开发者应先阅读本文了解全貌,再按 +[NVIDIA Container Toolkit 宿主机配置](nvidia-container-toolkit-host-setup.md) +完成安装,最后执行 +[PR13 验证流程](isaac-pr13-validation.md)。 + +本文记录的是一次可复现的验证快照,不把某台工作站的软件包版本误当成所有 +开发者必须采用的唯一版本。项目真正固定的运行边界保存在 +`deployment/env/versions.env`;宿主机驱动、Docker 和 Toolkit 仍需在每台机器上 +单独验证。 + +## 1. 环境分层 + +PR13 涉及四个相互独立的配置层: + +| 层 | 负责内容 | 权威位置 | +| --- | --- | --- | +| 宿主机 | NVIDIA 驱动、Docker Engine、Compose、Container Toolkit、CDI | 操作系统与 Docker daemon | +| 固定运行基线 | Isaac/ROS 镜像、digest、ROS domain、参考机器人 | `deployment/env/versions.env` | +| 开发者本地选择 | EULA、隐私选择、headless/GUI、`DISPLAY` | 被 Git 忽略的 `deployment/env/.env` | +| 仓库实现 | Kit App、Extension、Bridge graph、Compose、smoke harness | `simulators/isaac/`、`deployment/`、`tools/` | + +CPU 开发容器不需要 NVIDIA GPU、原生 ROS 2、CUDA Toolkit 或 Isaac Lab。只有 +启动 `compose.isaac.yaml` 中声明了 `gpus: all` 的 Isaac `env` service 时,Docker +才必须通过 NVIDIA Container Toolkit 将宿主机 GPU 和匹配的驱动库注入容器。 +构建镜像本身不执行 GPU 仿真,因此不应把“镜像构建成功”等同于“GPU 运行已 +验证”。 + +## 2. 当前固定基线 + +- 宿主机/容器平台:Ubuntu 22.04 x86_64 +- ROS:ROS 2 Humble,Python 3.10 +- Isaac Sim:4.5.0,镜像按 manifest digest 固定 +- ROS middleware:`rmw_fastrtps_cpp` +- 验证用 ROS domain:42 +- Robot:ANYmal C,具体 binding 属于后续 PR14 +- Locomotion:计划使用 Isaac Sim 自带策略 +- Isaac Lab:不属于 MVP runtime,不安装 + +`ACCEPT_EULA=Y` 表示实际运行容器的人已经阅读并接受 NVIDIA Omniverse EULA; +RoboHarness 不会代替操作者设置它。`PRIVACY_CONSENT=Y` 是独立的数据收集选择, +默认保持 `N`,接受 EULA 不代表同时同意数据收集。 + +## 3. 本次宿主机验证快照 + +2026-08-24 的通过证据来自 +`.build/isaac-smoke/20260824T083123Z-112676/`: + +| 项目 | 通过时的值 | +| --- | --- | +| Host OS | Ubuntu 22.04.5 LTS, x86_64 | +| Kernel | `6.8.0-138-generic` | +| NVIDIA driver | `580.173.02` | +| NVIDIA Container Toolkit CLI/library | `1.20.0` | +| Docker Engine | `29.7.2` | +| Docker Compose | `5.5.0` | +| Isaac Sim | `4.5.0`,固定 digest | +| ROS domain | `42` | +| 结果 | 外部 Humble observer 收到原生 `/clock`,PASS | + +驱动 580.173.02 是这台 kernel 6.8 工作站的已验证版本,不是项目最低版本。 +其他满足 Isaac Sim 4.5 官方要求的驱动可以使用,但必须重新运行 smoke 并记录 +证据。升级驱动后必须重启宿主机;仅完成 DKMS 安装还不能证明新内核模块已经 +加载。 + +## 4. 从新机器到通过 smoke + +先验证宿主机驱动: + +```bash +uname -m +cat /etc/os-release +uname -r +nvidia-smi +``` + +然后安装和配置 Toolkit。完整命令及 rootless、WSL2、Jetson、远程 Docker +差异见专门指南;rootful Docker 的关键步骤是: + +```bash +sudo apt-get install --yes nvidia-container-toolkit +sudo nvidia-ctk runtime configure --runtime=docker +sudo systemctl restart docker +nvidia-ctk cdi list +docker run --rm --gpus all ubuntu:22.04 nvidia-smi +``` + +最后配置并验证仓库: + +```bash +cp deployment/env/.env.example deployment/env/.env +# 阅读 EULA 后编辑 deployment/env/.env,将 ACCEPT_EULA 改为 Y。 +make isaac-config +make isaac-smoke +``` + +`make isaac-smoke` 会构建固定镜像、启动 Isaac `env` 和验证专用 observer、等待 +真实 `/clock` 数据、检查 Env status topic 与容器重启次数,并在退出时清理 +Compose services。每次运行的日志和环境信息保存在新的 +`.build/isaac-smoke//` 中。 + +## 5. PR13 开发过程中确认的调整 + +### 5.1 不引入 Isaac Lab + +最初规划同时保留 Isaac Sim 和 Isaac Lab。确认 MVP 直接使用 Isaac Sim 自带的 +ANYmal C locomotion policy 后,PR13 runtime 移除了 Isaac Lab 依赖。以后只有在 +明确需要训练、导出或转换 policy 时,才单独选择并固定对应工具版本。 + +### 5.2 使用基础 App 加增量配置 + +项目没有把 `isaacsim.exp.base` 当作普通 Extension 依赖。容器实际启动 +`/isaac-sim/apps/isaacsim.exp.base.kit`,再通过 +`--merge-config=` 合并项目 App 配置,同时注册 NVIDIA apps 和 +项目 Extension 搜索目录。容器以 root 运行 Kit 时显式传入 `--allow-root`。 + +### 5.3 复用 Isaac 自带的 ROS 2 Humble 库 + +启动脚本执行 Isaac 的 `setup_python_env.sh`,设置 `ROS_DISTRO=humble`、 +`RMW_IMPLEMENTATION=rmw_fastrtps_cpp`,并把 Bridge 内置 ROS 2 library path 与 +构建好的 `rh_interfaces` overlay 暴露给 Kit。项目没有在 Isaac 镜像内再安装一 +套完整 ROS desktop,也没有复制实现 ROS 2 Bridge。 + +### 5.4 等待 Kit ready 后初始化仿真 + +`rh.isaac` Extension 等待 `EVENT_APP_READY`,异步创建 `SimulationContext`,然后 +创建并启动 clock graph。Kit 保持 update/render loop 的所有权,Extension 负责 +RoboHarness 生命周期 glue,避免在 Extension 内启动第二套主循环。 + +### 5.5 使用原生 OmniGraph 发布仿真时钟 + +`/clock` graph 使用 `OnPlaybackTick`、`ROS2Context`、 +`IsaacReadSimulationTime` 和 `ROS2PublishClock`。这与 NVIDIA 官方工作流一致; +Bridge publisher 只有在 timeline playback 时才活跃。 + +### 5.6 Linux 验证栈共享 host network 与 IPC + +Isaac `env` 和验证 observer 使用 host network,并在这项限定的 Fast DDS smoke +中共享 host IPC。仅发现 `/clock` publisher 不足以证明数据可用;验收条件是外部 +observer 实际收到一条 `rosgraph_msgs/msg/Clock`。 + +### 5.7 将宿主机故障与项目故障分层 + +开发早期出现的 `failed to discover GPU vendor from CDI` 发生在容器启动前, +原因属于 Toolkit/CDI/Docker 设备发现层。处理顺序固定为: + +```text +host nvidia-smi + -> nvidia-container-cli / CDI + -> generic docker --gpus all probe + -> Isaac container + -> Kit App / Extension + -> ROS 2 Bridge /clock +``` + +在上游层未通过前,不调试 `rh.isaac` 代码,也不通过把宿主机 driver/CUDA 库 +复制进镜像来绕过设备注入。 + +## 6. 当前验证边界 + +PR13 PASS 只证明:固定 Isaac Sim 镜像能使用该宿主机 GPU 启动,项目 Kit App +和 Extension 能加载,外部 ROS 2 Humble observer 能收到原生 `/clock`,并能发现 +`/roboharness/env/status`。 + +它不证明 ANYmal C spawn、locomotion、sensor/TF/odometry、`cmd_vel`、Episode +reset、安全门控或完整 MVP E2E。Env 因而仍保持 `STARTING`,不能提前宣称 +`READY`。 + +## 7. 运行后风扇或 GPU 负载升高 + +Isaac/Kit 初始化 physics、renderer 和 shader cache 时出现明显 GPU/CPU 负载是 +正常现象。先检查是否仍有项目容器运行: + +```bash +docker compose \ + --env-file deployment/env/versions.env \ + --env-file deployment/env/.env \ + -f deployment/compose/compose.isaac.yaml ps +nvidia-smi +``` + +本地 GUI 推荐使用前台启动脚本: + +```bash +make isaac-gui +``` + +它不会修改 `.env` 中默认的 headless 模式;脚本会临时授权容器 root 用户访问 +当前 X11 display,在 Ctrl-C 或异常退出后清理 Compose stack,并只撤销本次新增的 +X11 ACL。首次启动可能需要数分钟编译 RTX shader;若桌面弹出 Wait/Force Quit, +且日志仍在更新 `Waiting for RtPso async group async compilation`,应选择 Wait。 +缓存保存在命名 volume 中,后续启动会复用。手动通过 `make isaac-up` 启动的后台 +服务仍需要显式停止: + +```bash +make isaac-down +``` + +`make isaac-smoke` 自带退出清理;即使验证失败,也会尽力保存日志并停止本次 +Compose stack。 diff --git a/docs/guides/isaac-pr13-validation.md b/docs/guides/isaac-pr13-validation.md new file mode 100644 index 0000000..c777f58 --- /dev/null +++ b/docs/guides/isaac-pr13-validation.md @@ -0,0 +1,162 @@ +# PR 13 Isaac Backend and Native ROS 2 Bridge Validation + +This guide validates the bounded PR 13 skeleton. Passing it proves that the +pinned Isaac Sim 4.5 container can start the RoboHarness Kit application on a +specific host and that an external ROS 2 Humble container can discover and +receive the native Bridge simulation clock. It does not validate ANYmal C, +sensors, locomotion, reset, command gating, or an Episode. + +## Fixed software boundary + +- Host OS: Ubuntu 22.04 x86_64 +- ROS: ROS 2 Humble +- Isaac Sim: `4.5.0` +- Isaac image manifest: + `sha256:c2f47dc82a7714af08d3766efe80ac9d084c2b37b5d0dfbd074797ec56390fc7` +- Native Bridge extension: `isaacsim.ros2.bridge` +- RMW for this smoke: `rmw_fastrtps_cpp` +- Isaac Lab: not installed + +NVIDIA documents Linux driver `535.129.03` as the 4.5 baseline and recommends +at least `535.216.01` with Ubuntu 22.04.5 kernel 6.8. Record the actual tested +driver; do not infer compatibility from the package version alone. + +The repository has a passing reference snapshot from 2026-08-24 using Ubuntu +22.04.5, kernel `6.8.0-138-generic`, NVIDIA driver `580.173.02`, Container +Toolkit `1.20.0`, Docker Engine `29.7.2`, and Compose `5.5.0`. Driver 580 is a +known-good result for that host, not the project minimum. + +## Host preflight + +The host must have an RTX-capable NVIDIA GPU, a functioning driver, Docker +Engine, Docker Compose v2, and NVIDIA Container Toolkit. All of these commands +must succeed before building the large image: + +Install or repair the host runtime with the +[NVIDIA Container Toolkit host setup guide](nvidia-container-toolkit-host-setup.md) +before running this PR-specific validation. + +```bash +nvidia-smi +nvidia-container-cli --version +docker version +docker compose version +docker run --rm --gpus all ubuntu nvidia-smi +``` + +The last command may download a small image. A missing `nvidia-container-cli` +means NVIDIA Container Toolkit is not installed. A failed `nvidia-smi`, an +unreachable Docker socket, or a failed container GPU probe is a host/runtime +failure, not evidence that the RoboHarness extension is broken. + +## Consent and static configuration + +The repository does not accept NVIDIA terms on behalf of the operator: + +```bash +cp deployment/env/.env.example deployment/env/.env +``` + +Review NVIDIA's Omniverse EULA, then set `ACCEPT_EULA=Y`. Keep +`PRIVACY_CONSENT=N` unless the operator explicitly opts in. The local `.env` +file is ignored by Git. + +Validate Compose interpolation without starting a container: + +```bash +make isaac-config +``` + +## Headless GPU and Bridge smoke + +Run: + +```bash +make isaac-smoke +``` + +The harness performs observable checks rather than assuming a fixed startup +delay: + +1. capture GPU, Docker, Compose, resolved configuration, and image metadata; +2. build the env image from the digest-pinned Isaac Sim base; +3. start `env` and the validation-only ROS 2 observer with host networking and + a shared host IPC namespace; +4. wait for an external `ros2 topic echo /clock --once` to succeed; +5. verify `/clock` has type `rosgraph_msgs/msg/Clock`; +6. verify `/roboharness/env/status` is discoverable with the platform type; +7. confirm neither validation container restarted; and +8. require the external observer to report at least one `/clock` publisher. + +Evidence is retained under `.build/isaac-smoke//` even when the run +fails. A passing directory includes `result.txt`, `nvidia-smi.txt`, resolved +Compose, image metadata, node/topic snapshots, one clock message, container +snapshots, and Isaac/Compose logs. Attach this directory to the PR GPU evidence. + +Both containers use `ipc: host` because Fast DDS can select shared-memory +transport for colocated participants. With separate IPC namespaces, DDS topic +discovery can still succeed while user data such as `/clock` never reaches the +observer. Seeing a publisher is therefore not equivalent to receiving data. + +## Local GUI launch + +GUI is a manual supplement to the headless acceptance test: + +```bash +make isaac-gui +``` + +The launcher requires a real local `DISPLAY` and X11 socket, checks the host GPU +and Docker runtime, temporarily grants the container root user X access, builds +and starts the `env` service in the foreground, and restores the X access rule +after Ctrl-C. It does not rewrite the default `RH_ISAAC_MODE=headless` in the +developer's `.env`. Remote GUI/WebRTC streaming is not configured by PR 13. + +The first GUI run can spend several minutes compiling RTX pipeline shaders. A +desktop "Wait or Force Quit" prompt during this phase does not by itself mean +Kit crashed: choose **Wait** while the log continues to report +`Waiting for RtPso async group async compilation`. The named Omniverse cache +volume is preserved, so later starts normally reuse the compiled data. Treat a +CUDA/Vulkan error, container exit, or a non-progressing warm-up beyond the host +timeout as a separate failure; do not delete the cache during diagnosis. + +## Expected PR 13 state semantics + +`/roboharness/env/status` remains `STARTING` with detail indicating that the +native clock smoke is active. This is intentional. The extension publishes +`ERROR` with a restart-required flag if initialization fails, but it cannot +publish `READY` until the stage, ANYmal C binding, required sensors/interfaces, +reset behavior, and command safety gate are implemented and validated. + +## Acceptance record + +Record the evidence directory and actual values in the PR description: + +```text +host_os= +kernel= +gpu= +driver= +docker_engine= +docker_compose= +isaac_image_digest= +headless_clock_smoke=PASS|FAIL +gui_launch=PASS|FAIL|NOT_RUN +evidence_directory= +known_limitations= +``` + +Reference result: + +```text +headless_clock_smoke=PASS +gui_launch=PASS (window opened; first-run RtPso warm-up observed) +driver=580.173.02 +toolkit=1.20.0 +evidence_directory=.build/isaac-smoke/20260824T083123Z-112676 +known_limitations=PR13 clock/status skeleton only; Env remains STARTING +``` + +The evidence directory is local generated data and is intentionally ignored by +Git. Record the values and result in the PR description; do not commit the +large image metadata and runtime logs. diff --git a/docs/guides/nvidia-container-toolkit-host-setup.md b/docs/guides/nvidia-container-toolkit-host-setup.md new file mode 100644 index 0000000..62d650b --- /dev/null +++ b/docs/guides/nvidia-container-toolkit-host-setup.md @@ -0,0 +1,266 @@ +# NVIDIA Container Toolkit Host Setup + +This guide prepares a Linux developer host to run the RoboHarness Isaac Sim +container with an NVIDIA GPU. It configures the host, not the project image. +The project image can be built without a GPU; the Toolkit is required when +Docker starts that image with `gpus: all`. + +The supported PR 13 validation host is Ubuntu 22.04 x86_64 with rootful Docker +Engine. Follow the variant notes below instead of applying the primary commands +unchanged to rootless Docker, WSL2, Jetson, or a remote Docker daemon. + +## What the Toolkit provides + +The four layers are separate: + +1. the host NVIDIA driver owns the physical GPU and kernel modules; +2. NVIDIA Container Toolkit exposes devices and matching driver libraries to a + container; +3. CDI (Container Device Interface) describes the available NVIDIA devices to + Docker/containerd; and +4. CUDA or Isaac Sim inside the image consumes the injected GPU. + +Do not install the CUDA development toolkit merely to fix Docker GPU access. +Do not copy host driver libraries into the Isaac image. NVIDIA's +[Container Toolkit installation guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) +is the authority for supported distributions and current repository commands. + +## Choose the correct host path + +| Host | Project support | Action | +| --- | --- | --- | +| Ubuntu 22.04 x86_64, rootful Docker | PR 13 validation baseline | Follow this guide | +| Other supported Debian/Ubuntu release | Developer-local only until recorded | Use NVIDIA's matching `apt` instructions, then run the same probes | +| Rootless Docker | Not the current baseline | Use the rootless variant below | +| Docker Desktop / WSL2 | Not a PR 13 acceptance host | Use Docker Desktop GPU support; do not apply the systemd steps blindly | +| Jetson/aarch64 | Cannot run the pinned x86_64 Isaac Sim image | Use a remote x86_64 RTX host | +| CPU-only or non-NVIDIA GPU | CPU development and mock stack only | Do not run `make isaac-smoke` | +| Remote Docker context | Toolkit belongs on the daemon host | Configure the remote machine, not the CLI workstation | + +## 1. Verify the driver before changing Docker + +Run on the Docker daemon host: + +```bash +uname -m +cat /etc/os-release +uname -r +nvidia-smi +``` + +Stop here if `nvidia-smi` fails. Container Toolkit cannot repair a missing, +unloaded, Secure-Boot-blocked, or incompatible host driver. For the pinned +Isaac Sim 4.5 runtime, also check the driver requirements in the +[Isaac Sim requirements](https://docs.isaacsim.omniverse.nvidia.com/4.5.0/installation/requirements.html). +After installing or upgrading a driver, reboot before continuing: a successful +package/DKMS installation does not prove that the running kernel loaded the new +module. + +Record the current Docker mode and version: + +```bash +docker context show +docker info +docker compose version +``` + +The commands below assume the active context points to the local, rootful +Docker daemon. Restarting Docker can stop every container on the host; schedule +that interruption before continuing. + +## 2. Add NVIDIA's `apt` repository + +Install the repository prerequisites: + +```bash +sudo apt-get update +sudo apt-get install --yes --no-install-recommends curl gnupg +``` + +Download and install NVIDIA's repository key: + +```bash +curl --fail --silent --show-error --location \ + https://nvidia.github.io/libnvidia-container/gpgkey \ + --output /tmp/nvidia-container-toolkit.gpgkey +sudo gpg --dearmor --yes \ + --output /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \ + /tmp/nvidia-container-toolkit.gpgkey +``` + +Add the stable Debian repository with the key restricted to that source: + +```bash +curl --fail --silent --show-error --location \ + https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \ + --output /tmp/nvidia-container-toolkit.list +sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \ + /tmp/nvidia-container-toolkit.list \ + | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list +sudo apt-get update +``` + +Before installation, `apt-cache policy nvidia-container-toolkit` should show a +candidate from `nvidia.github.io`. If it does not, inspect the generated source +file, proxy, DNS, system clock, and keyring rather than proceeding with an +unrelated package. + +## 3. Install and record the Toolkit + +Install the stable package set: + +```bash +sudo apt-get install --yes nvidia-container-toolkit +nvidia-ctk --version +nvidia-container-cli --version +dpkg-query --show 'nvidia-container-toolkit*' 'libnvidia-container*' +``` + +The host package is deliberately not pinned in the project image. Record the +actual version in validation evidence so failures can be reproduced. Teams +that centrally pin host packages should select a version visible in +`apt-cache madison nvidia-container-toolkit` and keep all Toolkit/library +packages on the same release. + +## 4. Configure rootful Docker + +If `/etc/docker/daemon.json` already exists, inspect and back it up because it +may contain registry mirrors, proxies, or other site policy. Then let +`nvidia-ctk` add the NVIDIA runtime without replacing unrelated keys: + +```bash +sudo nvidia-ctk runtime configure --runtime=docker +sudo systemctl restart docker +``` + +Verify that Docker came back and registered the runtime: + +```bash +systemctl is-active docker +docker info +``` + +If the developer uses Docker without `sudo`, membership changes take effect +only in a new login session: + +```bash +getent group docker +id +newgrp docker +docker version +``` + +Only add a trusted local user to the `docker` group. That group is effectively +root-equivalent on the host. + +## 5. Verify or refresh CDI + +Recent Toolkit releases generate a transient NVIDIA CDI specification using +`nvidia-cdi-refresh`. Inspect it with: + +```bash +nvidia-ctk cdi list +systemctl status nvidia-cdi-refresh.path +systemctl status nvidia-cdi-refresh.service +ls -l /var/run/cdi/nvidia.yaml +``` + +After changing the GPU driver or device configuration, refresh and check it: + +```bash +sudo systemctl restart nvidia-cdi-refresh.service +nvidia-ctk cdi list +``` + +If the installed Toolkit predates `nvidia-cdi-refresh`, follow the +[NVIDIA CDI support guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html) +for that exact version. A manual fallback is: + +```bash +sudo mkdir -p /etc/cdi +sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml +nvidia-ctk cdi list +``` + +Do not keep stale specifications in both `/etc/cdi` and `/var/run/cdi`. Prefer +the managed refresh service when it is available. + +## 6. Prove generic Docker GPU access + +First test the same `--gpus all` path used by RoboHarness: + +```bash +docker run --rm --gpus all ubuntu:22.04 nvidia-smi +``` + +The GPU name and driver version should match the host. An optional explicit CDI +probe on Docker versions that support CDI is: + +```bash +docker run --rm --device=nvidia.com/gpu=all ubuntu:22.04 nvidia-smi +``` + +These probes are independent of Isaac Sim and are based on Docker's +[GPU container documentation](https://docs.docker.com/engine/containers/resource_constraints/#gpu). +Do not diagnose the RoboHarness extension until the generic probe passes. + +## 7. Validate RoboHarness + +From the repository root: + +```bash +cp deployment/env/.env.example deployment/env/.env +``` + +Review NVIDIA's Omniverse EULA, then set `ACCEPT_EULA=Y` in the untracked +`deployment/env/.env`. Continue with: + +```bash +make isaac-config +make isaac-smoke +``` + +Passing `make isaac-smoke` proves that the pinned Isaac image starts on this +host and an external ROS 2 observer receives the native `/clock`. Evidence is +written under `.build/isaac-smoke/`; retain the directory and record the host +OS, kernel, GPU, driver, Docker, Compose, Toolkit, and image digest. + +The reference PR 13 run on 2026-08-24 passed with Ubuntu 22.04.5, kernel +`6.8.0-138-generic`, driver `580.173.02`, and Toolkit `1.20.0`. Treat this as a +known-good snapshot, not a requirement to replace another driver that already +satisfies Isaac Sim 4.5 and passes the generic GPU probe. + +## Rootless Docker variant + +Do not modify `/etc/docker/daemon.json` for a rootless daemon. Confirm rootless +mode in `docker info`, then follow NVIDIA's rootless Docker section. The current +upstream flow uses the user's daemon configuration and user service: + +```bash +nvidia-ctk runtime configure \ + --runtime=docker \ + --config="${HOME}/.config/docker/daemon.json" +systemctl --user restart docker +sudo nvidia-ctk config \ + --set nvidia-container-cli.no-cgroups \ + --in-place +``` + +Run the generic GPU probe again. Rootless results are developer-local until the +project records that host configuration as a validated target. + +## Failure routing + +| Symptom | Layer to investigate | First checks | +| --- | --- | --- | +| Host `nvidia-smi` fails | Driver/kernel/Secure Boot | driver module, kernel log, reboot state | +| `nvidia-container-cli` is missing | Toolkit package/repository | `apt-cache policy`, NVIDIA source file | +| Docker socket permission denied | User session/context | `docker context show`, `id`, new login | +| `no known GPU vendor found` | CDI generation/discovery | `nvidia-ctk cdi list`, refresh service, CDI files | +| `could not select device driver` | Docker runtime configuration | `docker info`, `nvidia-ctk runtime configure`, daemon restart | +| Container cannot load `libcuda.so.1` | GPU/driver-library injection | generic `docker run --gpus all` probe | +| Generic probe passes but Isaac fails | Isaac/Kit/project layer | `.build/isaac-smoke/*/compose.log` and `env.log` | + +Never work around these failures by embedding the host driver in the image, +running a privileged container, weakening device permissions globally, or +claiming GPU validation from a successful image build alone. diff --git a/simulators/isaac/README.md b/simulators/isaac/README.md new file mode 100644 index 0000000..2d7457a --- /dev/null +++ b/simulators/isaac/README.md @@ -0,0 +1,52 @@ +# Isaac Sim backend + +This directory contains the Isaac Sim 4.5 application boundary for the +RoboHarness `env` service. PR 13 intentionally provides only: + +- the `rh.kit` application definition; +- the `rh.isaac` lifecycle extension; +- the native ROS 2 Bridge simulation-clock Action Graph; and +- headless and local-X11 launch modes. + +The extension publishes `/roboharness/env/status` as `STARTING` while the Bridge +smoke is active and changes it to `ERROR` if initialization fails. It does not +publish `READY`: a complete READY claim requires the world, ANYmal C binding, +sensors, command gate, and reset contract planned for PR 14 and PR 16. + +The Kit extension waits for app readiness, initializes Isaac Sim's +`SimulationContext` asynchronously, creates the clock graph in the simulation +pipeline, and starts the timeline. This follows the extension workflow where +Kit owns rendering/update timing and `SimulationContext` owns physics state. + +No Isaac Lab dependency is installed. ANYmal C assets, policy integration, +sensor graphs, odometry, TF, and command handling are deliberately absent from +this skeleton. + +## Directory ownership + +```text +simulators/isaac/ +├── apps/rh.kit # incremental Kit application config +├── bridge/topics.yaml # native Bridge contract manifest +├── extensions/rh.isaac/ +│ ├── config/extension.toml # Kit extension declaration +│ └── rh/isaac/ +│ ├── extension.py # lifecycle and status glue +│ └── clock_graph.py # native OmniGraph construction +└── scripts/launch.sh # container headless/GUI launcher +``` + +Container builds, Compose services, consent values, and host GPU setup do not +belong here. They remain under `deployment/` and `docs/guides/`. The structure +follows the Isaac Sim App Template split between `.kit` applications and Kit +extensions, adapted to RoboHarness's domain-oriented monorepo. + +The launcher starts the bundled `isaacsim.exp.base.kit` and merges `rh.kit` as +project configuration. The clock graph uses the native Bridge nodes +`OnPlaybackTick`, `ROS2Context`, `IsaacReadSimulationTime`, and +`ROS2PublishClock`; it does not implement a parallel ROS transport. + +See the +[development environment and adjustment record](../../docs/guides/isaac-pr13-development-environment.md) +for the decisions confirmed while bringing up the container, Toolkit/CDI, Kit, +and ROS 2 Bridge. diff --git a/tools/README.md b/tools/README.md index 6ebea7d..f8d6c38 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,9 +1,13 @@ # Repository tools -- `dev/` contains dependencies and thin helpers for the development environment. +- `dev/` contains dependencies and thin host-side helpers for the development + environment, including the foreground Isaac GUI launcher with scoped X11 + access and cleanup. - `e2e/` starts the isolated CPU mock Compose project and independently validates - its committed three-Episode result tree. + its committed three-Episode result tree. It also contains the manual PR 13 + GPU/ROS 2 Bridge smoke harness and evidence collector. - `validation/` contains repository-wide checks used identically by developers - and CI. + and CI, including dependency-light checks for the Isaac image pin, Kit app, + extension declaration, and native clock graph. Business logic and runtime lifecycle behavior do not belong in this directory.