diff --git a/compose.yaml b/compose.yaml index eb1eb8a..39236cf 100644 --- a/compose.yaml +++ b/compose.yaml @@ -58,7 +58,7 @@ services: ROS_DOMAIN_ID: ${ROS_DOMAIN_ID:-42} RMW_IMPLEMENTATION: ${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp} ROS_DISCOVERY_SERVER: discovery-server:11811 - FASTRTPS_DEFAULT_PROFILES_FILE: /etc/ros/dds_discovery.xml + FASTDDS_DEFAULT_PROFILES_FILE: /etc/ros/dds_discovery.xml volumes: - ./workspace/src:/ros_ws/src - ros-workspace:/ros_ws @@ -103,6 +103,10 @@ services: ROS_DOMAIN_ID: ${ROS_DOMAIN_ID:-42} RMW_IMPLEMENTATION: ${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp} ROS_DISCOVERY_SERVER: discovery-server:11811 + # The co-located ros_gz bridge (Theme E) joins the ROS graph over the + # discovery server via unicast; this XML profile (bundled in the image) + # points its DDS participant at discovery-server:11811 (FR-E3/FR-E4). + FASTDDS_DEFAULT_PROFILES_FILE: /etc/ros/dds_discovery.xml volumes: - ./simulation:/simulation:ro - gazebo-home:/root/.gz @@ -139,7 +143,7 @@ services: ROS_DOMAIN_ID: ${ROS_DOMAIN_ID:-42} RMW_IMPLEMENTATION: ${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp} ROS_DISCOVERY_SERVER: discovery-server:11811 - FASTRTPS_DEFAULT_PROFILES_FILE: /etc/ros/dds_discovery.xml + FASTDDS_DEFAULT_PROFILES_FILE: /etc/ros/dds_discovery.xml networks: - ros_net - web_net diff --git a/docs/decisions/ADR-007-discovery-server.md b/docs/decisions/ADR-007-discovery-server.md index 2925620..cc2b306 100644 --- a/docs/decisions/ADR-007-discovery-server.md +++ b/docs/decisions/ADR-007-discovery-server.md @@ -20,7 +20,7 @@ DDS ports and break the single-ingress invariant (INV-04). Run a dedicated **Fast DDS Discovery Server** as its own service (`fastdds discovery -i 0 -l 0.0.0.0 -p 11811`) on the internal `ros_net`. The `ros` and `simulator` services point at it via `ROS_DISCOVERY_SERVER` and a -`FASTRTPS_DEFAULT_PROFILES_FILE` (`dds_discovery.xml`), replacing multicast with +`FASTDDS_DEFAULT_PROFILES_FILE` (`dds_discovery.xml`), replacing multicast with a unicast client/server discovery model. ## Alternatives considered diff --git a/docs/plans/sim-theme-e-ros-integration.md b/docs/plans/sim-theme-e-ros-integration.md new file mode 100644 index 0000000..097771d --- /dev/null +++ b/docs/plans/sim-theme-e-ros-integration.md @@ -0,0 +1,48 @@ +# Theme E — ROS 2 integration for Gazebo (ros_gz) + +> Plan stub. Source of truth: [Simulation & Visualization PRD](../prds/uberos-simulation-visualization.md) §7.5. + +## Scope — FR-E1 … FR-E6 +- FR-E1 — `ros_gz_bridge` runs co-located with `gz sim` in the Gazebo container. +- FR-E2 — Default bridge for `/clock` (`rosgraph_msgs/msg/Clock`) via a bridge config file; further per-world bridges additive. +- FR-E3 — Bridge reaches ROS via the Fast DDS discovery server (unicast, no multicast); gz-transport stays intra-container. +- FR-E4 — Gazebo entrypoint sources `/opt/ros/${ROS_DISTRO}` and provides the DDS discovery config. +- FR-E5 — `ROS_DISTRO` ↔ `GZ_RELEASE` pinned to a compatible pair (kilted ↔ ionic). +- FR-E6 — Remove the now-redundant `ros-gz` from the `ros` image (bridge lives only in the Gazebo container). + +## Dependency / lane +- **Lane 2 (Gazebo backend).** Coordinate with **Theme F** (both reshape the Gazebo container/service); sequence E after F's container reshape or share the lane. +- Architecture rationale: two discovery systems (gz-transport vs DDS) — see PRD §6. + +## Likely files +- `services/simulator|gazebo/` entrypoint + `ros_gz` bridge config (`/clock`) +- `services/ros/Dockerfile` (drop `ros-gz`) + +## Tasks +- [ ] Research: ros_gz_bridge config format for the pinned pair; discovery-server env vs XML in the Gazebo container +- [ ] Plan: bridge launch + `/clock` config + entrypoint ROS sourcing +- [ ] Implement: co-located bridge, `/clock`, ros-image cleanup +- [ ] Tests: `ros2 topic echo /clock rosgraph_msgs/msg/Clock --once` returns a clock sample; no multicast +- [ ] Acceptance (PRD §7.5) + +## Verified test procedure (2026-07-24) + +Use this from the ROS shell (web terminal in the `ros` container) to verify that Gazebo is reachable from ROS through `ros_gz_bridge`: + +```bash +source /opt/ros/${ROS_DISTRO}/setup.bash +export ROS_DOMAIN_ID=42 +export RMW_IMPLEMENTATION=rmw_fastrtps_cpp +export ROS_DISCOVERY_SERVER=discovery-server:11811 +export FASTDDS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml +ros2 topic echo /clock rosgraph_msgs/msg/Clock --once +``` + +Expected result: one `clock` message is printed. + +Notes: +- In this stack, `ros2 topic list` may not show `/clock` even when the bridge is working. +- The explicit type in `ros2 topic echo` is required for a reliable check. +- `ROS_DISCOVERY_SERVER` must be `discovery-server:11811` with no spaces around `:`. +- Use `--once` (no space), not `-- once`. +- Fast DDS deprecates `FASTRTPS_DEFAULT_PROFILES_FILE`; use `FASTDDS_DEFAULT_PROFILES_FILE`. diff --git a/docs/specs/01-Init-research.md b/docs/specs/01-Init-research.md index 9b8f73f..92a8e49 100644 --- a/docs/specs/01-Init-research.md +++ b/docs/specs/01-Init-research.md @@ -661,11 +661,14 @@ exec ros2 launch rosbridge_server rosbridge_websocket_launch.xml port:=9090 ROS_DOMAIN_ID=42 ROS_DISCOVERY_SERVER=discovery-server:11811 RMW_IMPLEMENTATION=rmw_fastrtps_cpp -FASTRTPS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml +FASTDDS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml ``` Domain ID `42` is inside the cross-platform-safe range of 0–101.[^dds-domain] +`FASTRTPS_DEFAULT_PROFILES_FILE` is deprecated in current Fast DDS releases. +Use `FASTDDS_DEFAULT_PROFILES_FILE` for active deployments. + > **Recommendation:** Use a Fast DDS Discovery Server because Docker bridge networks do not reliably carry the multicast discovery used by default DDS configuration. Without explicit discovery, ROS nodes in separate containers can silently fail to find each other.[^dds-multicast] ### Gazebo, World Assets, and GUI Streaming @@ -1014,7 +1017,7 @@ RUN chmod +x /entrypoint.sh ENV ROS_DOMAIN_ID=42 ENV RMW_IMPLEMENTATION=rmw_fastrtps_cpp -ENV FASTRTPS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml +ENV FASTDDS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml EXPOSE 9090 7681 ENTRYPOINT ["/entrypoint.sh"] diff --git a/services/gazebo/Dockerfile b/services/gazebo/Dockerfile index d1f617a..8474008 100644 --- a/services/gazebo/Dockerfile +++ b/services/gazebo/Dockerfile @@ -15,6 +15,10 @@ ARG GZ_RELEASE=ionic ARG ROS_DISTRO=kilted FROM ghcr.io/openrobotics/gazebo:${GZ_RELEASE}-full +# Hadolint DL4006: RUN commands in this file use pipes, so execute RUN with +# bash + pipefail to propagate errors from all pipeline segments. +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + # ROS_DISTRO is not automatically re-exposed after FROM; redeclare it so the # Theme E ros_gz bridge (added later to this same container) can source ROS. ARG ROS_DISTRO @@ -25,8 +29,50 @@ ENV ROS_DISTRO=${ROS_DISTRO} RUN apt-get update \ && apt-get install -y --no-install-recommends \ procps \ + curl \ + gnupg2 \ + ca-certificates \ && rm -rf /var/lib/apt/lists/* +# --- Theme E: ros_gz bridge (FR-E1/FR-E4/FR-E5) ------------------------------- +# The Gazebo `-full` base is NOT a ROS image, so add the packages.ros.org apt +# repo (signed-by keyring, no apt-key) and install the ros_gz BRIDGE only. +# +# We install `ros-${ROS_DISTRO}-ros-gz-bridge` rather than the full `ros-gz` +# metapackage on purpose (FR-E5): the bridge is the ONLY ros_gz component this +# container runs, and it depends solely on the gz-transport/gz-msgs stack for +# the PINNED Gazebo release — which the kilted<->ionic pairing makes identical +# to the Gazebo already in the base image, so apt resolves ONE Gazebo version +# and never pulls a second. The full metapackage would drag in ros_gz_sim / +# ros_gz_image (a redundant sim launcher + camera-render deps) that this +# headless, client-rendered path does not need. Keep ROS_DISTRO<->GZ_RELEASE +# pinned to kilted<->ionic (FR-E5); changing one without the other can pull a +# mismatched Gazebo. +RUN . /etc/os-release \ + && UBUNTU_CODENAME="${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}" \ + && test -n "${UBUNTU_CODENAME}" \ + && curl -fsSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \ + | gpg --dearmor -o /usr/share/keyrings/ros-archive-keyring.gpg \ + # NOTE: HTTP is intentional here. In this environment the HTTPS endpoint + # fails certificate principal validation (`SEC_E_WRONG_PRINCIPAL`), while + # HTTP works reliably for apt index retrieval. + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu ${UBUNTU_CODENAME} main" \ + > /etc/apt/sources.list.d/ros2.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + ros-${ROS_DISTRO}-ros-gz-bridge \ + && rm -rf /var/lib/apt/lists/* + +# DDS discovery-server client profile so the ros_gz bridge finds the ROS graph +# over unicast (FR-E3/FR-E4, RISK-4). Same profile the ros/turtlesim services +# carry; ROS_DISCOVERY_SERVER + ROS_DOMAIN_ID are supplied by compose. +COPY config/dds_discovery.xml /etc/ros/dds_discovery.xml +ENV FASTDDS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml + +# Default bridge topic map (/clock). Additive: more topics append here (FR-E2). +COPY config/ros_gz_bridge.yaml /etc/uberos/ros_gz_bridge.yaml +# ------------------------------------------------------------------------------ + # Disable automatic Fuel downloads; the world uses built-in primitives only. COPY config/fuel.yaml /root/.gz/fuel/config.yaml diff --git a/services/gazebo/config/dds_discovery.xml b/services/gazebo/config/dds_discovery.xml new file mode 100644 index 0000000..7c248d3 --- /dev/null +++ b/services/gazebo/config/dds_discovery.xml @@ -0,0 +1,38 @@ + + + + + + + + + CLIENT + + + + +
discovery-server
+ 11811 +
+
+
+
+
+
+
+
+
diff --git a/services/gazebo/config/ros_gz_bridge.yaml b/services/gazebo/config/ros_gz_bridge.yaml new file mode 100644 index 0000000..68a5cbd --- /dev/null +++ b/services/gazebo/config/ros_gz_bridge.yaml @@ -0,0 +1,27 @@ +# UbeROS ros_gz bridge config (PRD Theme E, FR-E2). +# +# Consumed by ros_gz_bridge's `parameter_bridge` (`--ros-args -p config_file:=`), +# which runs co-located with `gz sim` in THIS container (FR-E1). The bridge is +# the only component that speaks both transports: it reads gz-transport topics +# over localhost (intra-container, reliable) and republishes them on the ROS +# graph, which is discovered over the Fast DDS discovery server via unicast +# (FR-E3). gz sim itself never joins DDS. +# +# Default topic set is intentionally minimal: only the simulation clock, so ROS +# nodes can run on simulated time. Additional per-world/per-model topics +# (e.g. /cmd_vel, /odom, joint states, TF) are ADDITIVE — append more list +# entries here (or supply a world-specific config via UBEROS_BRIDGE_CONFIG) +# without changing the entrypoint. +# +# Entry schema (ros_gz_bridge): each list item maps one ROS topic to one gz +# topic with an explicit message-type pair and a direction. Directions are +# GZ_TO_ROS, ROS_TO_GZ, or BIDIRECTIONAL. + +# /clock — simulation time published by gz sim's physics; bridged into ROS as +# rosgraph_msgs/msg/Clock so `use_sim_time` consumers follow the simulator. +# One-way (GZ_TO_ROS): nothing in ROS should drive the simulator's clock. +- ros_topic_name: "/clock" + gz_topic_name: "/clock" + ros_type_name: "rosgraph_msgs/msg/Clock" + gz_type_name: "gz.msgs.Clock" + direction: GZ_TO_ROS diff --git a/services/gazebo/entrypoint.sh b/services/gazebo/entrypoint.sh index 58973c5..d6965f5 100644 --- a/services/gazebo/entrypoint.sh +++ b/services/gazebo/entrypoint.sh @@ -22,18 +22,40 @@ GZ_PID=$! gz launch -v 1 "${LAUNCH_FILE}" & WS_PID=$! -# --- Theme E integration point (ros_gz bridge) -------------------------------- -# The ros_gz bridge is OUT OF SCOPE for Theme F. Theme E adds ROS + the bridge -# to THIS container: install ros-${ROS_DISTRO}-ros-gz in the Dockerfile, then -# source ROS and start the bridge here, e.g. -# set +u; source "/opt/ros/${ROS_DISTRO}/setup.bash"; set -u -# ros2 run ros_gz_bridge parameter_bridge ... & -# Nothing above depends on ROS, so the headless web-viz path works standalone. +# --- Theme E integration point (ros_gz bridge, FR-E1..FR-E4) ------------------ +# The ros_gz bridge runs co-located with `gz sim` in THIS container so the +# gz-transport hop stays intra-container (localhost) and only the DDS side +# crosses the network, via the Fast DDS discovery server (unicast, no +# multicast). Source ROS first (its setup scripts reference unbound vars, so +# relax nounset while sourcing), then start parameter_bridge with the /clock +# config. gz sim/gz launch were started ABOVE, before ROS is on PATH, so they +# keep their native gz environment and are unaffected by the ROS overlay. +if [[ -z "${ROS_DISTRO:-}" ]]; then + echo "ERROR: ROS_DISTRO is required for Theme E bridge startup" >&2 + exit 1 +fi + +set +u +# shellcheck source=/dev/null +source "/opt/ros/${ROS_DISTRO}/setup.bash" +set -u + +BRIDGE_CONFIG="${UBEROS_BRIDGE_CONFIG:-/etc/uberos/ros_gz_bridge.yaml}" + +# Invoke the parameter_bridge executable by its full install path instead of +# `ros2 run`: this headless image installs only ros-gz-bridge (no ros2cli), so +# the `ros2` CLI plugin is not present. The bridge reads the YAML topic map and +# registers on the ROS graph through the discovery server (env from compose). +# Bridge may start before `gz sim` advertises /clock; this is intentional (it reconnects when /clock appears). +"/opt/ros/${ROS_DISTRO}/lib/ros_gz_bridge/parameter_bridge" \ + --ros-args -p "config_file:=${BRIDGE_CONFIG}" & +BRIDGE_PID=$! # ------------------------------------------------------------------------------ -# Forward termination to both children for a clean shutdown. -trap 'kill -TERM "${GZ_PID}" "${WS_PID}" 2>/dev/null || true' TERM INT +# Forward termination to all children for a clean shutdown. +trap 'kill -TERM "${GZ_PID}" "${WS_PID}" "${BRIDGE_PID}" 2>/dev/null || true' TERM INT -# Exit as soon as either process dies so Docker's restart policy can recover the -# pair (a lone websocket server or a lone sim is not useful). -wait -n "${GZ_PID}" "${WS_PID}" +# Exit as soon as any core process dies so Docker's restart policy can recover +# the group (a lone websocket server, a lone sim, or a lone bridge is not +# useful). +wait -n "${GZ_PID}" "${WS_PID}" "${BRIDGE_PID}" diff --git a/services/ros/Dockerfile b/services/ros/Dockerfile index 412747a..7b908f9 100644 --- a/services/ros/Dockerfile +++ b/services/ros/Dockerfile @@ -11,8 +11,14 @@ FROM ros:${ROS_DISTRO}-ros-base ARG ROS_DISTRO ENV ROS_DISTRO=${ROS_DISTRO} -# Install colcon toolchain, rosbridge, rosapi, ros-gz integration, and utilities. +# Install colcon toolchain, rosbridge, rosapi, and utilities. # procps provides pgrep; curl/wget used by tooling and health probing. +# +# The ros_gz bridge is NOT installed here (Theme E, FR-E6): it runs co-located +# with `gz sim` in the gazebo container so the gz-transport hop stays +# intra-container and only its DDS side crosses the network. This image would +# only need `ros-gz` if a ROS-side node consumed ros_gz_interfaces, which none +# currently do. RUN apt-get update \ && apt-get install -y --no-install-recommends \ python3-colcon-common-extensions \ @@ -20,7 +26,6 @@ RUN apt-get update \ python3-vcstool \ ros-${ROS_DISTRO}-rosbridge-suite \ ros-${ROS_DISTRO}-rosapi \ - ros-${ROS_DISTRO}-ros-gz \ procps \ tmux \ curl \ @@ -55,7 +60,7 @@ RUN chmod +x /entrypoint.sh /usr/local/bin/uberos-term ENV ROS_DOMAIN_ID=42 ENV RMW_IMPLEMENTATION=rmw_fastrtps_cpp -ENV FASTRTPS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml +ENV FASTDDS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml # rosbridge WebSocket and ttyd. Exposed for inter-container use only; the proxy # is the sole ingress and these ports must never be host-published. diff --git a/services/turtlesim/Dockerfile b/services/turtlesim/Dockerfile index 3789ffd..6dfbe79 100644 --- a/services/turtlesim/Dockerfile +++ b/services/turtlesim/Dockerfile @@ -42,7 +42,7 @@ RUN chmod +x /entrypoint.sh ENV DISPLAY=:99 ENV ROS_DOMAIN_ID=42 ENV RMW_IMPLEMENTATION=rmw_fastrtps_cpp -ENV FASTRTPS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml +ENV FASTDDS_DEFAULT_PROFILES_FILE=/etc/ros/dds_discovery.xml # noVNC (6080) and VNC (5900) are reachable only through the proxy; never # host-published (INV-04, WP-13 security baseline).