diff --git a/Dockerfile b/Dockerfile index 4001cf7..1af1bb6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,9 +45,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ARG BRIDGE_FILE="bridge.yaml" -COPY --chmod=0755 script/entrypoint.sh /entrypoint.sh -COPY --chmod=0755 script/ros_entrypoint.sh /ros_entrypoint.sh +COPY --chmod=0755 script/ / COPY --chmod=0644 "${BRIDGE_FILE}" /bridge.yaml +COPY --chmod=0644 config/demo_bridge.yaml /demo_bridge.yaml ENTRYPOINT ["/ros_entrypoint.sh"] CMD ["bash"] diff --git a/README.md b/README.md index d928419..67db8a1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - [Quick Start](#quick-start) - [Usage](#usage) - [Bridge Configuration](#bridge-configuration) +- [Demo](#demo) - [Architecture](#architecture) - [Directory Structure](#directory-structure) @@ -127,6 +128,41 @@ services_2_to_1: type: rcl_interfaces/GetParameters ``` +## Demo + +Two-terminal end-to-end bridge demo using `std_msgs/String`. Both demos +use the same pattern: a **server** terminal that owns `roscore` + +`parameter_bridge` (loaded from the baked-in `/demo_bridge.yaml`), and a +**client** terminal that just subscribes. + +| Demo | Terminal 1 (server) | Terminal 2 (client) | +|------|---------------------|---------------------| +| A — ROS 1 → ROS 2 | `./exec.sh /ros1_server.sh` | `./exec.sh /ros2_client.sh` | +| B — ROS 2 → ROS 1 | `./exec.sh /ros2_server.sh` | `./exec.sh /ros1_client.sh` | + +Steps (assuming the container is up via `./run.sh -d`): + +```bash +# Terminal 1 (server) — pick one demo +./exec.sh /ros1_server.sh # Demo A +./exec.sh /ros2_server.sh # Demo B + +# Terminal 2 (client) — matching pair +./exec.sh /ros2_client.sh # Demo A +./exec.sh /ros1_client.sh # Demo B +``` + +Server scripts log every step (`[ros1_server] step N/5: ...`) so it's +clear when `roscore` and `parameter_bridge` are up. Override the +published string with `MESSAGE`: + +```bash +./exec.sh env MESSAGE="hi from ROS 1" /ros1_server.sh +``` + +`Ctrl+C` on the server terminal tears down `parameter_bridge` and +`roscore`; the client terminal then EOFs. + ## Architecture ```mermaid @@ -169,11 +205,16 @@ ros1_bridge/ ├── template/ # Shared scripts, tests, CI (git subtree) ├── script/ │ ├── entrypoint.sh # Sources ROS 1 + ROS 2, loads bridge config -│ └── ros_entrypoint.sh # ROS env only (osrf-compatible) +│ ├── ros_entrypoint.sh # ROS env only (osrf-compatible) +│ ├── ros1_server.sh # Demo A publisher (bootstraps roscore + bridge) +│ ├── ros1_client.sh # Demo B subscriber +│ ├── ros2_server.sh # Demo B publisher (bootstraps roscore + bridge) +│ └── ros2_client.sh # Demo A subscriber ├── bridge.yaml # Default bridge configuration ├── config/ # Additional bridge configs │ ├── scan_bridge.yaml # LaserScan bridge -│ └── release_bridge.yaml # Camera + depth bridge +│ ├── release_bridge.yaml # Camera + depth bridge +│ └── demo_bridge.yaml # Demo bidirectional std_msgs/String ├── doc/ # Translated READMEs │ ├── README.zh-TW.md # Traditional Chinese │ ├── README.zh-CN.md # Simplified Chinese diff --git a/config/demo_bridge.yaml b/config/demo_bridge.yaml new file mode 100644 index 0000000..1dff798 --- /dev/null +++ b/config/demo_bridge.yaml @@ -0,0 +1,9 @@ +# Demo bridge config — std_msgs/String, both directions. +# Used by script/ros{1,2}_server.sh via parameter_bridge. +topics: + - topic: /chatter_1to2 + type: std_msgs/msg/String + queue_size: 10 + - topic: /chatter_2to1 + type: std_msgs/msg/String + queue_size: 10 diff --git a/doc/README.ja.md b/doc/README.ja.md index 2576eae..1adcc1e 100644 --- a/doc/README.ja.md +++ b/doc/README.ja.md @@ -16,6 +16,7 @@ - [クイックスタート](#クイックスタート) - [使い方](#使い方) - [ブリッジ設定](#ブリッジ設定) +- [Demo](#demo) - [アーキテクチャ](#アーキテクチャ) - [ディレクトリ構成](#ディレクトリ構成) @@ -127,6 +128,43 @@ services_2_to_1: type: rcl_interfaces/GetParameters ``` +## Demo + +2 つの terminal でエンドツーエンドの bridge デモを実行します。 +メッセージ型は `std_msgs/String`。パターンは対称で、**server** +terminal が `roscore` + `parameter_bridge`(ビルド時にイメージへ +焼き込まれた `/demo_bridge.yaml` を読み込む)を起動し、**client** +terminal は subscribe するだけです。 + +| Demo | Terminal 1 (server) | Terminal 2 (client) | +|------|---------------------|---------------------| +| A — ROS 1 → ROS 2 | `./exec.sh /ros1_server.sh` | `./exec.sh /ros2_client.sh` | +| B — ROS 2 → ROS 1 | `./exec.sh /ros2_server.sh` | `./exec.sh /ros1_client.sh` | + +実際の手順(コンテナを `./run.sh -d` で起動済みとして): + +```bash +# Terminal 1 (server) — どちらかを選択 +./exec.sh /ros1_server.sh # Demo A +./exec.sh /ros2_server.sh # Demo B + +# Terminal 2 (client) — 対応するもう一方 +./exec.sh /ros2_client.sh # Demo A +./exec.sh /ros1_client.sh # Demo B +``` + +Server スクリプトは各ステップを明示的にログ出力します +(`[ros1_server] step N/5: ...`)。`roscore` と `parameter_bridge` が +いつ準備完了になったかが一目で分かります。Publish するメッセージは +`MESSAGE` 環境変数で上書き可能です: + +```bash +./exec.sh env MESSAGE="hi from ROS 1" /ros1_server.sh +``` + +Server terminal で `Ctrl+C` を押すと `parameter_bridge` と `roscore` +を停止し、client terminal も EOF になります。 + ## アーキテクチャ ```mermaid @@ -169,11 +207,16 @@ ros1_bridge/ ├── template/ # 共有スクリプト、テスト、CI(git subtree) ├── script/ │ ├── entrypoint.sh # ROS 1 + ROS 2 を source、bridge 設定を読み込み -│ └── ros_entrypoint.sh # ROS 環境のみ source(osrf 互換) +│ ├── ros_entrypoint.sh # ROS 環境のみ source(osrf 互換) +│ ├── ros1_server.sh # Demo A publisher(roscore + bridge を自起動) +│ ├── ros1_client.sh # Demo B subscriber +│ ├── ros2_server.sh # Demo B publisher(roscore + bridge を自起動) +│ └── ros2_client.sh # Demo A subscriber ├── bridge.yaml # デフォルト bridge 設定 ├── config/ # 追加 bridge 設定 │ ├── scan_bridge.yaml # LaserScan bridge -│ └── release_bridge.yaml # Camera + depth bridge +│ ├── release_bridge.yaml # Camera + depth bridge +│ └── demo_bridge.yaml # Demo 双方向 std_msgs/String ├── doc/ # 翻訳版 README │ ├── README.zh-TW.md # 繁体字中国語 │ ├── README.zh-CN.md # 簡体字中国語 diff --git a/doc/README.zh-CN.md b/doc/README.zh-CN.md index 2508cdf..206972d 100644 --- a/doc/README.zh-CN.md +++ b/doc/README.zh-CN.md @@ -16,6 +16,7 @@ - [快速开始](#快速开始) - [使用方式](#使用方式) - [Bridge 设置](#bridge-设置) +- [Demo](#demo) - [架构](#架构) - [目录结构](#目录结构) @@ -126,6 +127,40 @@ services_2_to_1: type: rcl_interfaces/GetParameters ``` +## Demo + +两个 terminal 跑 end-to-end bridge demo,消息类型 `std_msgs/String`。 +规则对称:**server** terminal 负责起 `roscore` + `parameter_bridge` +(读 build 时烤进 image 的 `/demo_bridge.yaml`),**client** terminal 只订阅。 + +| Demo | Terminal 1 (server) | Terminal 2 (client) | +|------|---------------------|---------------------| +| A — ROS 1 → ROS 2 | `./exec.sh /ros1_server.sh` | `./exec.sh /ros2_client.sh` | +| B — ROS 2 → ROS 1 | `./exec.sh /ros2_server.sh` | `./exec.sh /ros1_client.sh` | + +实际操作(假设容器已用 `./run.sh -d` 起好): + +```bash +# Terminal 1 (server) — 二选一 +./exec.sh /ros1_server.sh # Demo A +./exec.sh /ros2_server.sh # Demo B + +# Terminal 2 (client) — 对应的另一半 +./exec.sh /ros2_client.sh # Demo A +./exec.sh /ros1_client.sh # Demo B +``` + +Server 脚本每一步都打印进度(`[ros1_server] step N/5: ...`),所以 +`roscore` 跟 `parameter_bridge` 何时就绪一目了然。要换消息字串用 +`MESSAGE` 环境变量: + +```bash +./exec.sh env MESSAGE="hi from ROS 1" /ros1_server.sh +``` + +Server terminal 按 `Ctrl+C` 会收掉 `parameter_bridge` 跟 `roscore`, +client terminal 接着就 EOF。 + ## 架构 ```mermaid @@ -168,11 +203,16 @@ ros1_bridge/ ├── template/ # 共用脚本、测试、CI(git subtree) ├── script/ │ ├── entrypoint.sh # Source ROS 1 + ROS 2,载入 bridge 设置 -│ └── ros_entrypoint.sh # 仅 source ROS 环境(兼容 osrf) +│ ├── ros_entrypoint.sh # 仅 source ROS 环境(兼容 osrf) +│ ├── ros1_server.sh # Demo A publisher(自起 roscore + bridge) +│ ├── ros1_client.sh # Demo B subscriber +│ ├── ros2_server.sh # Demo B publisher(自起 roscore + bridge) +│ └── ros2_client.sh # Demo A subscriber ├── bridge.yaml # 默认 bridge 设置 ├── config/ # 额外 bridge 设置 │ ├── scan_bridge.yaml # LaserScan bridge -│ └── release_bridge.yaml # Camera + depth bridge +│ ├── release_bridge.yaml # Camera + depth bridge +│ └── demo_bridge.yaml # Demo 双向 std_msgs/String ├── doc/ # 翻译版 README │ ├── README.zh-TW.md # 繁体中文 │ ├── README.zh-CN.md # 简体中文 diff --git a/doc/README.zh-TW.md b/doc/README.zh-TW.md index 101c129..6526256 100644 --- a/doc/README.zh-TW.md +++ b/doc/README.zh-TW.md @@ -16,6 +16,7 @@ - [快速開始](#快速開始) - [使用方式](#使用方式) - [Bridge 設定](#bridge-設定) +- [Demo](#demo) - [架構](#架構) - [目錄結構](#目錄結構) @@ -126,6 +127,40 @@ services_2_to_1: type: rcl_interfaces/GetParameters ``` +## Demo + +兩個 terminal 跑 end-to-end bridge demo,訊息型別 `std_msgs/String`。 +規則對稱:**server** terminal 負責起 `roscore` + `parameter_bridge` +(讀 build 時烤進 image 的 `/demo_bridge.yaml`),**client** terminal 只訂閱。 + +| Demo | Terminal 1 (server) | Terminal 2 (client) | +|------|---------------------|---------------------| +| A — ROS 1 → ROS 2 | `./exec.sh /ros1_server.sh` | `./exec.sh /ros2_client.sh` | +| B — ROS 2 → ROS 1 | `./exec.sh /ros2_server.sh` | `./exec.sh /ros1_client.sh` | + +實際操作(假設容器已用 `./run.sh -d` 起好): + +```bash +# Terminal 1 (server) — 二選一 +./exec.sh /ros1_server.sh # Demo A +./exec.sh /ros2_server.sh # Demo B + +# Terminal 2 (client) — 對應的另一半 +./exec.sh /ros2_client.sh # Demo A +./exec.sh /ros1_client.sh # Demo B +``` + +Server 腳本每一步都印出進度(`[ros1_server] step N/5: ...`),所以 +`roscore` 跟 `parameter_bridge` 何時就緒一目了然。要換訊息字串用 +`MESSAGE` 環境變數: + +```bash +./exec.sh env MESSAGE="hi from ROS 1" /ros1_server.sh +``` + +Server terminal 按 `Ctrl+C` 會收掉 `parameter_bridge` 跟 `roscore`, +client terminal 接著就 EOF。 + ## 架構 ```mermaid @@ -168,11 +203,16 @@ ros1_bridge/ ├── template/ # 共用腳本、測試、CI(git subtree) ├── script/ │ ├── entrypoint.sh # Source ROS 1 + ROS 2,載入 bridge 設定 -│ └── ros_entrypoint.sh # 僅 source ROS 環境(相容 osrf) +│ ├── ros_entrypoint.sh # 僅 source ROS 環境(相容 osrf) +│ ├── ros1_server.sh # Demo A publisher(自起 roscore + bridge) +│ ├── ros1_client.sh # Demo B subscriber +│ ├── ros2_server.sh # Demo B publisher(自起 roscore + bridge) +│ └── ros2_client.sh # Demo A subscriber ├── bridge.yaml # 預設 bridge 設定 ├── config/ # 額外 bridge 設定 │ ├── scan_bridge.yaml # LaserScan bridge -│ └── release_bridge.yaml # Camera + depth bridge +│ ├── release_bridge.yaml # Camera + depth bridge +│ └── demo_bridge.yaml # Demo 雙向 std_msgs/String ├── doc/ # 翻譯版 README │ ├── README.zh-TW.md # 繁體中文 │ ├── README.zh-CN.md # 簡體中文 diff --git a/doc/changelog/CHANGELOG.md b/doc/changelog/CHANGELOG.md index 2f4ed23..cda6992 100644 --- a/doc/changelog/CHANGELOG.md +++ b/doc/changelog/CHANGELOG.md @@ -19,12 +19,20 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Rebuild `devel` stage from `ros:foxy-ros-base-focal` (multi-arch) plus the ROS 1 snapshot apt repo instead of the amd64-only `osrf/ros:foxy-ros1-bridge`. Enables Jetson (arm64) support. - `ENV ROS1_DISTRO=noetic` / `ENV ROS2_DISTRO=foxy` now baked into the image so downstream scripts can reference the distro names without hardcoding. - Test stage lint target uses `COPY script/*.sh /lint/` (glob) to pick up new scripts automatically. +- Dockerfile `COPY` for `script/` switched from per-file (`entrypoint.sh`, `ros_entrypoint.sh`) to directory glob (`COPY --chmod=0755 script/ /`) so new helpers (the four demo scripts) are picked up without further Dockerfile edits. - Bridge YAML examples now document the full `parameter_bridge` schema: `bridge.yaml` ships `services_1_to_2` / `services_2_to_1` entries and an inline QoS block on `/scan`; `config/scan_bridge.yaml` and `config/release_bridge.yaml` set sensor-data QoS (BEST_EFFORT for image streams, RELIABLE for `camera_info`). READMEs in all four languages note the topic (ROS 2) vs service (ROS 1) `type` format asymmetry. - **Split `devel` and `runtime` into separate stages (USER-VISIBLE BEHAVIOR CHANGE).** `devel` CMD is now `bash` — `./run.sh` drops into an interactive shell instead of auto-launching `parameter_bridge`. `devel` ENTRYPOINT is `/ros_entrypoint.sh` (sources ROS1+ROS2 env only, no `rosparam load`) so the shell is usable immediately. The new `runtime` stage (`FROM devel`) keeps `CMD ["ros2", "run", "ros1_bridge", "parameter_bridge"]` and switches ENTRYPOINT back to `/entrypoint.sh` (which does `rosparam load /bridge.yaml` before launch) for production-style auto-bridge deployments. CI builds both (`build_runtime: true` in `main.yaml`). Note: `./run.sh runtime` does not yet work because the auto-generated `compose.yaml` does not emit a `runtime` service (tracked upstream in template); invoke runtime via direct `docker build --target runtime && docker run` until template provides this. ### Added - `script/ros_entrypoint.sh` — osrf-compatible entrypoint that only sources both ROS distros (no `rosparam load`), available at `/ros_entrypoint.sh` in the image. `devel` stage uses this as its `ENTRYPOINT`; `runtime` stage keeps `/entrypoint.sh` (with `rosparam load`). -- Smoke tests: `ROS1_DISTRO`/`ROS2_DISTRO` env vars, `/ros_entrypoint.sh` existence + ability to source both ROS envs + expose `ros2`. +- **Demo scripts** — symmetric server/client pairs that run a 2-terminal end-to-end bridge demo with std_msgs/String. Each `*_server.sh` self-bootstraps `roscore` + `parameter_bridge` (loading `/demo_bridge.yaml`) before publishing, with explicit step-by-step logs (`[ros1_server] step N/5: ...`); each `*_client.sh` only sources the relevant distro and subscribes. Trap on the server tears down `roscore` + bridge on Ctrl+C. + - `script/ros1_server.sh` — Demo A publisher: bootstraps + `rostopic pub /chatter_1to2 std_msgs/String`. Pair with `ros2_client.sh`. + - `script/ros2_client.sh` — Demo A subscriber: `ros2 topic echo /chatter_1to2`. + - `script/ros2_server.sh` — Demo B publisher: bootstraps + `ros2 topic pub /chatter_2to1 std_msgs/msg/String`. Pair with `ros1_client.sh`. + - `script/ros1_client.sh` — Demo B subscriber: `rostopic echo /chatter_2to1`. + - `MESSAGE` env var overrides the published string on either server. +- `config/demo_bridge.yaml` — bidirectional `std_msgs/msg/String` bridge for `/chatter_1to2` and `/chatter_2to1`, baked into the image as `/demo_bridge.yaml` so the demo scripts pick it up without env overrides. +- Smoke tests: `ROS1_DISTRO`/`ROS2_DISTRO` env vars, `/ros_entrypoint.sh` existence + ability to source both ROS envs + expose `ros2`, plus 9 new demo-helper tests (4 scripts exist+executable, 4 `-h` prints `Usage:`, `/demo_bridge.yaml` exists). Total: 28 → 37. ### Removed - `COPY config/ /config/` from Dockerfile and the `config directory exists` smoke test — the `/config/` directory was never read at runtime (entrypoint only loads `/bridge.yaml`). `config/*.yaml` files remain in the repo as reference examples and can still be consumed via `--build-arg BRIDGE_FILE=config/.yaml`. diff --git a/doc/test/TEST.md b/doc/test/TEST.md index ff6b26d..655fb28 100644 --- a/doc/test/TEST.md +++ b/doc/test/TEST.md @@ -1,6 +1,6 @@ # TEST.md -**29 tests** total. +**38 tests** total. ## test/smoke/ros_env.bats @@ -21,7 +21,7 @@ |------|-------------| | `ros1_bridge package is available` | `ros2 pkg list` includes ros1_bridge | -### Bridge config (5) +### Bridge config (6) | Test | Description | |------|-------------| @@ -32,6 +32,20 @@ | `ros_entrypoint.sh exposes ros2 command` | `ros2` binary is on `PATH` after entrypoint | | `entrypoint.sh skips rosparam load when roscore unreachable` | `timeout 2 rosparam list` guards the `rosparam load` so a missing roscore doesn't hang container boot | +### Demo helpers (9) + +| Test | Description | +|------|-------------| +| `demo_bridge.yaml exists` | `/demo_bridge.yaml` exists (built into image from `config/demo_bridge.yaml`) | +| `ros1_server.sh exists and is executable` | `/ros1_server.sh` is executable | +| `ros1_client.sh exists and is executable` | `/ros1_client.sh` is executable | +| `ros2_server.sh exists and is executable` | `/ros2_server.sh` is executable | +| `ros2_client.sh exists and is executable` | `/ros2_client.sh` is executable | +| `ros1_server.sh -h prints usage` | Help exits 0 with "Usage:" | +| `ros1_client.sh -h prints usage` | Help exits 0 with "Usage:" | +| `ros2_server.sh -h prints usage` | Help exits 0 with "Usage:" | +| `ros2_client.sh -h prints usage` | Help exits 0 with "Usage:" | + ## test/smoke/script_help.bats ### build.sh (3) diff --git a/script/ros1_client.sh b/script/ros1_client.sh new file mode 100755 index 0000000..76d9e86 --- /dev/null +++ b/script/ros1_client.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# Demo B — ROS 1 client (subscriber). +# +# Subscribes to /chatter_2to1 from ROS 1. Run ros2_server.sh on the other +# terminal first — it owns roscore + parameter_bridge. + +set -e + +readonly TOPIC="/chatter_2to1" + +usage() { + cat >&2 < ROS 1) — client / subscriber side. + +Subscribes to ${TOPIC} from ROS 1. Start ros2_server.sh on a second +terminal first — it owns roscore + parameter_bridge. +EOF +} + +log() { + printf '[ros1_client] %s\n' "$*" +} + +main() { + case "${1:-}" in + -h|--help) usage; exit 0 ;; + esac + + log "step 1/2: sourcing ROS 1 (${ROS1_DISTRO})" + unset ROS_DISTRO + # shellcheck source=/dev/null + source "/opt/ros/${ROS1_DISTRO}/setup.bash" + + log "step 2/2: subscribing to ${TOPIC} (Ctrl+C to stop)" + rostopic echo "${TOPIC}" +} + +main "$@" diff --git a/script/ros1_server.sh b/script/ros1_server.sh new file mode 100755 index 0000000..4609d56 --- /dev/null +++ b/script/ros1_server.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# +# Demo A — ROS 1 server (publisher). +# +# Bootstraps roscore + parameter_bridge (using /demo_bridge.yaml), then +# publishes std_msgs/String "${MESSAGE}" on /chatter_1to2 at 1 Hz from ROS 1. +# Pair with ros2_client.sh on a second terminal. +# +# Each command runs in its own subshell so ROS 1 and ROS 2 envs never +# collide — sourcing both into one shell breaks roscore (Python imports +# ROS 2's rosgraph_msgs.Log instead of ROS 1's). + +set -e + +readonly TOPIC="/chatter_1to2" +readonly BRIDGE_YAML="/demo_bridge.yaml" +readonly DEFAULT_MESSAGE="hello from ROS 1" + +usage() { + cat >&2 < ROS 2) — server / publisher side. + +Bootstraps roscore + parameter_bridge from ${BRIDGE_YAML}, then publishes +std_msgs/String on ${TOPIC} at 1 Hz from ROS 1. + +Pair with ros2_client.sh on a second terminal. + +Environment: + MESSAGE Override the published string (default: "${DEFAULT_MESSAGE}"). +EOF +} + +log() { + printf '[ros1_server] %s\n' "$*" +} + +# Source ROS 1 only (for roscore, rostopic, rosparam). +ros1_env() { + unset ROS_DISTRO + # shellcheck source=/dev/null + source "/opt/ros/${ROS1_DISTRO}/setup.bash" >/dev/null 2>&1 +} + +# Source ROS 1 then ROS 2 (for parameter_bridge — needs both). +both_env() { + ros1_env + unset ROS_DISTRO + # shellcheck source=/dev/null + source "/opt/ros/${ROS2_DISTRO}/setup.bash" >/dev/null 2>&1 +} + +cleanup() { + log "shutting down (parameter_bridge + roscore)..." + [[ -n "${BRIDGE_PID:-}" ]] && kill "${BRIDGE_PID}" 2>/dev/null || true + [[ -n "${ROSCORE_PID:-}" ]] && kill "${ROSCORE_PID}" 2>/dev/null || true + wait 2>/dev/null || true + log "done." +} + +main() { + case "${1:-}" in + -h|--help) usage; exit 0 ;; + esac + + local message="${MESSAGE:-${DEFAULT_MESSAGE}}" + + log "step 1/4: starting roscore in background — ROS 1 only env (log: /tmp/roscore.log)" + ( ros1_env; exec roscore ) >/tmp/roscore.log 2>&1 & + ROSCORE_PID="${!}" + trap cleanup EXIT INT TERM + + log " waiting for rosmaster..." + until ( ros1_env; rostopic list ) >/dev/null 2>&1; do sleep 0.2; done + log " rosmaster ready (pid=${ROSCORE_PID})" + + log "step 2/4: loading bridge config from ${BRIDGE_YAML} — ROS 1 env" + ( ros1_env; exec rosparam load "${BRIDGE_YAML}" ) + + log "step 3/4: starting parameter_bridge in background — ROS 1 + ROS 2 env (log: /tmp/bridge.log)" + ( both_env; exec ros2 run ros1_bridge parameter_bridge ) >/tmp/bridge.log 2>&1 & + BRIDGE_PID="${!}" + + log " waiting for bridge to expose ${TOPIC} (timeout 10s)..." + local i=0 + until ( ros1_env; rostopic list ) 2>/dev/null | grep -qx "${TOPIC}"; do + i=$((i + 1)) + if [[ "${i}" -gt 50 ]]; then + log " WARNING: ${TOPIC} not seen after 10s — continuing anyway." + break + fi + sleep 0.2 + done + log " bridge ready (pid=${BRIDGE_PID})" + + log "step 4/4: publishing on ${TOPIC} at 1 Hz: \"${message}\" — ROS 1 env" + log " Ctrl+C to stop everything." + ( ros1_env; exec rostopic pub -r 1 "${TOPIC}" std_msgs/String "data: '${message}'" ) +} + +main "$@" diff --git a/script/ros2_client.sh b/script/ros2_client.sh new file mode 100755 index 0000000..76f2462 --- /dev/null +++ b/script/ros2_client.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# Demo A — ROS 2 client (subscriber). +# +# Subscribes to /chatter_1to2 from ROS 2. Run ros1_server.sh on the other +# terminal first — it owns roscore + parameter_bridge. + +set -e + +readonly TOPIC="/chatter_1to2" + +usage() { + cat >&2 < ROS 2) — client / subscriber side. + +Subscribes to ${TOPIC} from ROS 2. Start ros1_server.sh on a second +terminal first — it owns roscore + parameter_bridge. +EOF +} + +log() { + printf '[ros2_client] %s\n' "$*" +} + +main() { + case "${1:-}" in + -h|--help) usage; exit 0 ;; + esac + + log "step 1/2: sourcing ROS 2 (${ROS2_DISTRO})" + unset ROS_DISTRO + # shellcheck source=/dev/null + source "/opt/ros/${ROS2_DISTRO}/setup.bash" + + log "step 2/2: subscribing to ${TOPIC} (Ctrl+C to stop)" + ros2 topic echo "${TOPIC}" std_msgs/msg/String +} + +main "$@" diff --git a/script/ros2_server.sh b/script/ros2_server.sh new file mode 100755 index 0000000..46a5736 --- /dev/null +++ b/script/ros2_server.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# +# Demo B — ROS 2 server (publisher). +# +# Bootstraps roscore + parameter_bridge (using /demo_bridge.yaml), then +# publishes std_msgs/msg/String "${MESSAGE}" on /chatter_2to1 at 1 Hz from +# ROS 2. Pair with ros1_client.sh on a second terminal. +# +# Each command runs in its own subshell so ROS 1 and ROS 2 envs never +# collide — sourcing both into one shell breaks roscore (Python imports +# ROS 2's rosgraph_msgs.Log instead of ROS 1's). + +set -e + +readonly TOPIC="/chatter_2to1" +readonly BRIDGE_YAML="/demo_bridge.yaml" +readonly DEFAULT_MESSAGE="hello from ROS 2" + +usage() { + cat >&2 < ROS 1) — server / publisher side. + +Bootstraps roscore + parameter_bridge from ${BRIDGE_YAML}, then publishes +std_msgs/msg/String on ${TOPIC} at 1 Hz from ROS 2. + +Pair with ros1_client.sh on a second terminal. + +Environment: + MESSAGE Override the published string (default: "${DEFAULT_MESSAGE}"). +EOF +} + +log() { + printf '[ros2_server] %s\n' "$*" +} + +# Source ROS 1 only (for roscore, rostopic, rosparam). +ros1_env() { + unset ROS_DISTRO + # shellcheck source=/dev/null + source "/opt/ros/${ROS1_DISTRO}/setup.bash" >/dev/null 2>&1 +} + +# Source ROS 2 only (for ros2 CLI). +ros2_env() { + unset ROS_DISTRO + # shellcheck source=/dev/null + source "/opt/ros/${ROS2_DISTRO}/setup.bash" >/dev/null 2>&1 +} + +# Source ROS 1 then ROS 2 (for parameter_bridge — needs both). +both_env() { + ros1_env + ros2_env +} + +cleanup() { + log "shutting down (parameter_bridge + roscore)..." + [[ -n "${BRIDGE_PID:-}" ]] && kill "${BRIDGE_PID}" 2>/dev/null || true + [[ -n "${ROSCORE_PID:-}" ]] && kill "${ROSCORE_PID}" 2>/dev/null || true + wait 2>/dev/null || true + log "done." +} + +main() { + case "${1:-}" in + -h|--help) usage; exit 0 ;; + esac + + local message="${MESSAGE:-${DEFAULT_MESSAGE}}" + + log "step 1/4: starting roscore in background — ROS 1 only env (log: /tmp/roscore.log)" + ( ros1_env; exec roscore ) >/tmp/roscore.log 2>&1 & + ROSCORE_PID="${!}" + trap cleanup EXIT INT TERM + + log " waiting for rosmaster..." + until ( ros1_env; rostopic list ) >/dev/null 2>&1; do sleep 0.2; done + log " rosmaster ready (pid=${ROSCORE_PID})" + + log "step 2/4: loading bridge config from ${BRIDGE_YAML} — ROS 1 env" + ( ros1_env; exec rosparam load "${BRIDGE_YAML}" ) + + log "step 3/4: starting parameter_bridge in background — ROS 1 + ROS 2 env (log: /tmp/bridge.log)" + ( both_env; exec ros2 run ros1_bridge parameter_bridge ) >/tmp/bridge.log 2>&1 & + BRIDGE_PID="${!}" + + log " waiting for bridge to expose ${TOPIC} (timeout 10s)..." + local i=0 + until ( ros2_env; ros2 topic list ) 2>/dev/null | grep -qx "${TOPIC}"; do + i=$((i + 1)) + if [[ "${i}" -gt 50 ]]; then + log " WARNING: ${TOPIC} not seen after 10s — continuing anyway." + break + fi + sleep 0.2 + done + log " bridge ready (pid=${BRIDGE_PID})" + + log "step 4/4: publishing on ${TOPIC} at 1 Hz: \"${message}\" — ROS 2 env" + log " Ctrl+C to stop everything." + ( ros2_env; exec ros2 topic pub -r 1 "${TOPIC}" std_msgs/msg/String "{data: '${message}'}" ) +} + +main "$@" diff --git a/test/smoke/ros_env.bats b/test/smoke/ros_env.bats index 077c1aa..5b47a9c 100644 --- a/test/smoke/ros_env.bats +++ b/test/smoke/ros_env.bats @@ -79,3 +79,49 @@ setup() { assert_output --partial "roscore not reachable" assert_output --partial "hello" } + +# -------------------- Demo helpers -------------------- + +@test "demo_bridge.yaml exists" { + assert [ -f "/demo_bridge.yaml" ] +} + +@test "ros1_server.sh exists and is executable" { + assert [ -x "/ros1_server.sh" ] +} + +@test "ros1_client.sh exists and is executable" { + assert [ -x "/ros1_client.sh" ] +} + +@test "ros2_server.sh exists and is executable" { + assert [ -x "/ros2_server.sh" ] +} + +@test "ros2_client.sh exists and is executable" { + assert [ -x "/ros2_client.sh" ] +} + +@test "ros1_server.sh -h prints usage" { + run bash /ros1_server.sh -h + assert_success + assert_line --partial "Usage:" +} + +@test "ros1_client.sh -h prints usage" { + run bash /ros1_client.sh -h + assert_success + assert_line --partial "Usage:" +} + +@test "ros2_server.sh -h prints usage" { + run bash /ros2_server.sh -h + assert_success + assert_line --partial "Usage:" +} + +@test "ros2_client.sh -h prints usage" { + run bash /ros2_client.sh -h + assert_success + assert_line --partial "Usage:" +}