agenticros 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/commands/config.d.ts.map +1 -1
  2. package/dist/commands/config.js +37 -1
  3. package/dist/commands/config.js.map +1 -1
  4. package/dist/commands/down.d.ts +6 -4
  5. package/dist/commands/down.d.ts.map +1 -1
  6. package/dist/commands/down.js +16 -6
  7. package/dist/commands/down.js.map +1 -1
  8. package/dist/commands/logs.js +2 -2
  9. package/dist/commands/logs.js.map +1 -1
  10. package/dist/commands/status.d.ts.map +1 -1
  11. package/dist/commands/status.js +10 -6
  12. package/dist/commands/status.js.map +1 -1
  13. package/dist/commands/up.d.ts +1 -0
  14. package/dist/commands/up.d.ts.map +1 -1
  15. package/dist/commands/up.js +33 -1
  16. package/dist/commands/up.js.map +1 -1
  17. package/dist/index.js +25 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/runners/sim.d.ts +2 -0
  20. package/dist/runners/sim.d.ts.map +1 -1
  21. package/dist/runners/sim.js +2 -0
  22. package/dist/runners/sim.js.map +1 -1
  23. package/package.json +1 -1
  24. package/runtime/BUNDLE.json +1 -1
  25. package/runtime/packages/core/src/transport/local/transport.ts +25 -5
  26. package/runtime/ros2_ws/src/agenticros_sim/CMakeLists.txt +25 -0
  27. package/runtime/ros2_ws/src/agenticros_sim/README.md +120 -0
  28. package/runtime/ros2_ws/src/agenticros_sim/config/agenticros-sim.config.json +28 -0
  29. package/runtime/ros2_ws/src/agenticros_sim/config/amr_bridge.yaml +111 -0
  30. package/runtime/ros2_ws/src/agenticros_sim/config/amr_view.rviz +192 -0
  31. package/runtime/ros2_ws/src/agenticros_sim/env-hooks/gz_resource_path.dsv.in +3 -0
  32. package/runtime/ros2_ws/src/agenticros_sim/env-hooks/gz_resource_path.sh.in +7 -0
  33. package/runtime/ros2_ws/src/agenticros_sim/launch/sim_amr.launch.py +184 -0
  34. package/runtime/ros2_ws/src/agenticros_sim/models/agenticros_amr/model.config +17 -0
  35. package/runtime/ros2_ws/src/agenticros_sim/models/agenticros_amr/model.sdf +251 -0
  36. package/runtime/ros2_ws/src/agenticros_sim/package.xml +27 -0
  37. package/runtime/ros2_ws/src/agenticros_sim/urdf/agenticros_amr.urdf.xacro +127 -0
  38. package/runtime/ros2_ws/src/agenticros_sim/worlds/agenticros_indoor.sdf +183 -0
  39. package/runtime/scripts/configure_for_sim.sh +64 -0
  40. package/runtime/scripts/sim/run_sim.sh +169 -0
  41. package/runtime/scripts/test-follow-me-sim.mjs +135 -0
  42. package/runtime/scripts/test-mcp-e2e.mjs +184 -0
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/sim/run_sim.sh
3
+ #
4
+ # Worker script invoked by `agenticros up sim-amr` (and later `sim-arm`). Sources
5
+ # ROS 2 + the AgenticROS overlay, runs `ros2 launch agenticros_sim ...`, writes
6
+ # its PID into /tmp/agenticros-sim.pid so `agenticros down` can stop it, and
7
+ # tees output to /tmp/agenticros-sim.log so `agenticros logs sim` works.
8
+ #
9
+ # Usage:
10
+ # run_sim.sh --robot amr [--namespace sim_robot] [--rviz] [--no-gui]
11
+ #
12
+ # Flags:
13
+ # --robot amr|arm Which sim launch to start (default: amr).
14
+ # --namespace <ns> Robot namespace; exported as AGENTICROS_ROBOT_NAMESPACE.
15
+ # --rviz Bring up RViz alongside Gazebo.
16
+ # --no-gui Run gz-sim headless (CI / docker).
17
+ # --ros-distro <distro> Override ROS 2 distro (auto-detect by default).
18
+ # --colcon-ws <path> Override the colcon workspace (default: <repo>/ros2_ws).
19
+ # --help Print this help and exit.
20
+ #
21
+ # Exit codes:
22
+ # 0 sim exited cleanly
23
+ # 1 missing dependency (gz, ros2, ros_gz_*, our launch file)
24
+ # 2 bad CLI usage
25
+ # 3 sim crashed at runtime — check /tmp/agenticros-sim.log
26
+
27
+ # Strict mode, but `set -u` clashes with ROS setup scripts that reference
28
+ # AMENT_TRACE_SETUP_FILES etc. We toggle nounset off around each `source`.
29
+ set -eo pipefail
30
+ shopt -s expand_aliases || true
31
+
32
+ # ---------- args ----------
33
+ ROBOT="amr"
34
+ NAMESPACE=""
35
+ USE_RVIZ="false"
36
+ GUI="true"
37
+ ROS_DISTRO_OVERRIDE=""
38
+ COLCON_WS=""
39
+
40
+ while [[ $# -gt 0 ]]; do
41
+ case "$1" in
42
+ --robot) ROBOT="$2"; shift 2 ;;
43
+ --namespace) NAMESPACE="$2"; shift 2 ;;
44
+ --rviz) USE_RVIZ="true"; shift ;;
45
+ --no-gui) GUI="false"; shift ;;
46
+ --ros-distro) ROS_DISTRO_OVERRIDE="$2"; shift 2 ;;
47
+ --colcon-ws) COLCON_WS="$2"; shift 2 ;;
48
+ -h|--help)
49
+ sed -n '2,24p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
50
+ exit 0 ;;
51
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
52
+ esac
53
+ done
54
+
55
+ if [[ "$ROBOT" != "amr" && "$ROBOT" != "arm" ]]; then
56
+ echo "--robot must be 'amr' or 'arm' (got '$ROBOT')" >&2
57
+ exit 2
58
+ fi
59
+
60
+ # ---------- paths ----------
61
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
62
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
63
+ COLCON_WS="${COLCON_WS:-$REPO_ROOT/ros2_ws}"
64
+
65
+ LOG_FILE="/tmp/agenticros-sim.log"
66
+ PID_FILE="/tmp/agenticros-sim.pid"
67
+
68
+ log() { printf "\033[36m[run_sim]\033[0m %s\n" "$*"; }
69
+ err() { printf "\033[31m[run_sim]\033[0m %s\n" "$*" >&2; }
70
+
71
+ # ---------- ROS 2 distro detection ----------
72
+ if [[ -n "$ROS_DISTRO_OVERRIDE" ]]; then
73
+ ROS_DISTRO="$ROS_DISTRO_OVERRIDE"
74
+ elif [[ -n "${ROS_DISTRO:-}" ]]; then
75
+ : # already sourced
76
+ else
77
+ for d in humble jazzy iron rolling; do
78
+ if [[ -f "/opt/ros/$d/setup.bash" ]]; then ROS_DISTRO="$d"; break; fi
79
+ done
80
+ fi
81
+ if [[ -z "${ROS_DISTRO:-}" ]] || [[ ! -f "/opt/ros/$ROS_DISTRO/setup.bash" ]]; then
82
+ err "ROS 2 not found under /opt/ros/. Install Humble or Jazzy first."
83
+ exit 1
84
+ fi
85
+
86
+ log "ROS_DISTRO=$ROS_DISTRO"
87
+ log "COLCON_WS=$COLCON_WS"
88
+ log "robot=$ROBOT namespace=${NAMESPACE:-<none>} rviz=$USE_RVIZ gui=$GUI"
89
+
90
+ # shellcheck disable=SC1090
91
+ source "/opt/ros/$ROS_DISTRO/setup.bash"
92
+
93
+ # ---------- Build agenticros_sim if not yet built ----------
94
+ if [[ ! -f "$COLCON_WS/install/agenticros_sim/share/agenticros_sim/launch/sim_amr.launch.py" ]]; then
95
+ log "agenticros_sim not built in $COLCON_WS — building now..."
96
+ (cd "$COLCON_WS" && colcon build --symlink-install --packages-select agenticros_sim agenticros_msgs)
97
+ fi
98
+
99
+ # shellcheck disable=SC1090
100
+ source "$COLCON_WS/install/setup.bash"
101
+
102
+ # ---------- Dependency checks ----------
103
+ for bin in gz ros2 rviz2; do
104
+ if ! command -v "$bin" >/dev/null; then
105
+ if [[ "$bin" == "rviz2" ]] && [[ "$USE_RVIZ" != "true" ]]; then
106
+ continue
107
+ fi
108
+ err "Required binary '$bin' not found on PATH."
109
+ exit 1
110
+ fi
111
+ done
112
+
113
+ # ---------- Wire up environment ----------
114
+ if [[ -n "$NAMESPACE" ]]; then
115
+ export AGENTICROS_ROBOT_NAMESPACE="$NAMESPACE"
116
+ fi
117
+
118
+ # Jetson rendering fix. On Tegra boards Mesa is picked first and tries to load
119
+ # nvidia-drm_dri.so which doesn't exist, so the gz GUI viewport comes up solid
120
+ # white. We:
121
+ # 1. Point libglvnd at the NVIDIA EGL vendor (works for some Jetson L4T images)
122
+ # 2. As a fallback, allow AGENTICROS_GZ_SOFTWARE_RENDER=1 to force llvmpipe -
123
+ # slow (~5 fps) but actually renders the world so the demo is viewable.
124
+ # Honor AGENTICROS_GZ_NO_TWEAKS=1 to skip both (useful on x86/laptops).
125
+ if [[ "$GUI" == "true" ]] && [[ -z "${AGENTICROS_GZ_NO_TWEAKS:-}" ]]; then
126
+ if [[ -f /usr/lib/aarch64-linux-gnu/tegra-egl/libEGL_nvidia.so.0 ]]; then
127
+ log "Jetson detected: forcing NVIDIA EGL/GL vendor"
128
+ export __GLX_VENDOR_LIBRARY_NAME="${__GLX_VENDOR_LIBRARY_NAME:-nvidia}"
129
+ export __EGL_VENDOR_LIBRARY_FILENAMES="${__EGL_VENDOR_LIBRARY_FILENAMES:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}"
130
+ export LD_LIBRARY_PATH="/usr/lib/aarch64-linux-gnu/tegra-egl:/usr/lib/aarch64-linux-gnu/tegra:${LD_LIBRARY_PATH:-}"
131
+ fi
132
+ if [[ -n "${AGENTICROS_GZ_SOFTWARE_RENDER:-}" ]]; then
133
+ log "AGENTICROS_GZ_SOFTWARE_RENDER set - forcing Mesa llvmpipe software renderer"
134
+ export LIBGL_ALWAYS_SOFTWARE=1
135
+ export GALLIUM_DRIVER=llvmpipe
136
+ export MESA_GL_VERSION_OVERRIDE=4.5
137
+ export OGRE_RTT_MODE=Copy
138
+ fi
139
+ fi
140
+
141
+ LAUNCH_ARGS=(
142
+ "use_rviz:=$USE_RVIZ"
143
+ "gui:=$GUI"
144
+ )
145
+
146
+ # ---------- Pick launch file ----------
147
+ case "$ROBOT" in
148
+ amr) LAUNCH_FILE="sim_amr.launch.py" ;;
149
+ arm) LAUNCH_FILE="sim_arm.launch.py" ;; # Phase 3
150
+ esac
151
+
152
+ if ! ros2 launch --help >/dev/null 2>&1; then
153
+ err "ros2 launch not available — is ROS sourced properly?"
154
+ exit 1
155
+ fi
156
+
157
+ log "Logging to $LOG_FILE"
158
+ log "ros2 launch agenticros_sim $LAUNCH_FILE ${LAUNCH_ARGS[*]}"
159
+ log "Press Ctrl+C to stop (or run \`agenticros down\` from another terminal)."
160
+
161
+ # Run in foreground so Ctrl+C in the parent shell still works, but also tee to
162
+ # the log so `agenticros logs sim -f` can follow concurrently. The PID we record
163
+ # is this script's own PID so `agenticros down` SIGTERMs the whole subtree.
164
+ echo "$$" > "$PID_FILE"
165
+
166
+ # Use `exec` so the ros2 process replaces our shell — that way signals from the
167
+ # CLI (or Ctrl+C) hit ros2 directly instead of bash.
168
+ exec 1> >(tee -a "$LOG_FILE") 2>&1
169
+ exec ros2 launch agenticros_sim "$LAUNCH_FILE" "${LAUNCH_ARGS[@]}"
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Drive ros2_follow_me_start (mode=depth) against the AMR sim.
4
+ *
5
+ * Sequence:
6
+ * 1. start follow-me in depth mode
7
+ * 2. poll status every 1.5s for 15s, capturing detection events
8
+ * 3. stop follow-me
9
+ * 4. send a zero cmd_vel just in case
10
+ *
11
+ * Reports: did the depth loop see a target, what distance/lateral did it pick,
12
+ * how many cmd_vel commands were issued.
13
+ */
14
+
15
+ import { spawn } from "node:child_process";
16
+ import { fileURLToPath } from "node:url";
17
+ import { dirname, join, resolve } from "node:path";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const repoRoot = resolve(__dirname, "..");
21
+ const serverDist = join(repoRoot, "packages/agenticros-claude-code/dist/index.js");
22
+
23
+ const child = spawn(process.execPath, [serverDist], {
24
+ stdio: ["pipe", "pipe", "pipe"],
25
+ env: { ...process.env },
26
+ });
27
+
28
+ child.stderr.on("data", (d) => {
29
+ process.stderr.write(`[mcp-stderr] ${d}`);
30
+ });
31
+
32
+ let nextId = 1;
33
+ const pending = new Map();
34
+ let buf = "";
35
+
36
+ child.stdout.on("data", (chunk) => {
37
+ buf += chunk.toString();
38
+ let nl;
39
+ while ((nl = buf.indexOf("\n")) !== -1) {
40
+ const line = buf.slice(0, nl).trim();
41
+ buf = buf.slice(nl + 1);
42
+ if (!line) continue;
43
+ let msg;
44
+ try {
45
+ msg = JSON.parse(line);
46
+ } catch {
47
+ continue;
48
+ }
49
+ if (msg.id !== undefined && pending.has(msg.id)) {
50
+ const { resolve, reject } = pending.get(msg.id);
51
+ pending.delete(msg.id);
52
+ if (msg.error) reject(new Error(msg.error.message));
53
+ else resolve(msg.result);
54
+ }
55
+ }
56
+ });
57
+
58
+ function rpc(method, params = {}, timeoutMs = 15000) {
59
+ const id = nextId++;
60
+ return new Promise((resolveOuter, reject) => {
61
+ const t = setTimeout(() => {
62
+ pending.delete(id);
63
+ reject(new Error(`Timeout: ${method}`));
64
+ }, timeoutMs);
65
+ pending.set(id, {
66
+ resolve: (v) => {
67
+ clearTimeout(t);
68
+ resolveOuter(v);
69
+ },
70
+ reject: (e) => {
71
+ clearTimeout(t);
72
+ reject(e);
73
+ },
74
+ });
75
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
76
+ });
77
+ }
78
+
79
+ function pickText(result) {
80
+ return result?.content?.map((c) => c.text ?? "").join("\n") ?? "";
81
+ }
82
+
83
+ async function main() {
84
+ console.log("=== follow_me (depth) E2E ===");
85
+
86
+ await rpc("initialize", {
87
+ protocolVersion: "2024-11-05",
88
+ capabilities: { tools: {} },
89
+ clientInfo: { name: "fm-e2e", version: "0.0.1" },
90
+ });
91
+ await rpc("notifications/initialized", {}).catch(() => {});
92
+
93
+ console.log("\n-- start follow_me mode=depth --");
94
+ const start = await rpc("tools/call", {
95
+ name: "ros2_follow_me_start",
96
+ arguments: { mode: "depth", targetDistance: 1.5 },
97
+ });
98
+ console.log(pickText(start));
99
+
100
+ console.log("\n-- poll status for 15s --");
101
+ const polls = [];
102
+ for (let i = 0; i < 10; i++) {
103
+ await new Promise((r) => setTimeout(r, 1500));
104
+ const s = await rpc("tools/call", { name: "ros2_follow_me_status", arguments: {} });
105
+ const text = pickText(s);
106
+ polls.push({ t: (i + 1) * 1.5, text });
107
+ console.log(`t+${((i + 1) * 1.5).toFixed(1)}s :: ${text.replace(/\s+/g, " ").slice(0, 200)}`);
108
+ }
109
+
110
+ console.log("\n-- stop follow_me --");
111
+ const stop = await rpc("tools/call", {
112
+ name: "ros2_follow_me_stop",
113
+ arguments: {},
114
+ });
115
+ console.log(pickText(stop));
116
+
117
+ console.log("\n-- safety zero-twist --");
118
+ await rpc("tools/call", {
119
+ name: "ros2_publish",
120
+ arguments: {
121
+ topic: "/cmd_vel",
122
+ type: "geometry_msgs/msg/Twist",
123
+ message: { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } },
124
+ },
125
+ });
126
+
127
+ child.kill("SIGTERM");
128
+ console.log("\n=== Done ===");
129
+ }
130
+
131
+ main().catch((e) => {
132
+ console.error("Failed:", e);
133
+ child.kill("SIGKILL");
134
+ process.exit(1);
135
+ });
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Spin up the @agenticros/claude-code MCP server as a child process and exercise
4
+ * a real JSON-RPC session over stdio. Used for end-to-end testing of the MCP
5
+ * tools against a live sim (or real robot).
6
+ *
7
+ * Usage:
8
+ * node scripts/test-mcp-e2e.mjs
9
+ *
10
+ * Honours AGENTICROS_CONFIG_PATH if set; otherwise uses ~/.agenticros/config.json.
11
+ */
12
+
13
+ import { spawn } from "node:child_process";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, join, resolve } from "node:path";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const repoRoot = resolve(__dirname, "..");
19
+ const serverDist = join(repoRoot, "packages/agenticros-claude-code/dist/index.js");
20
+
21
+ const child = spawn(process.execPath, [serverDist], {
22
+ stdio: ["pipe", "pipe", "pipe"],
23
+ env: { ...process.env },
24
+ });
25
+
26
+ child.stderr.on("data", (d) => {
27
+ process.stderr.write(`[mcp-stderr] ${d}`);
28
+ });
29
+ child.on("exit", (code, sig) => {
30
+ process.stderr.write(`[mcp] exited code=${code} sig=${sig}\n`);
31
+ });
32
+
33
+ let nextId = 1;
34
+ const pending = new Map();
35
+ let buf = "";
36
+
37
+ child.stdout.on("data", (chunk) => {
38
+ buf += chunk.toString();
39
+ let nl;
40
+ while ((nl = buf.indexOf("\n")) !== -1) {
41
+ const line = buf.slice(0, nl).trim();
42
+ buf = buf.slice(nl + 1);
43
+ if (!line) continue;
44
+ let msg;
45
+ try {
46
+ msg = JSON.parse(line);
47
+ } catch (e) {
48
+ process.stderr.write(`[mcp] non-json line: ${line}\n`);
49
+ continue;
50
+ }
51
+ if (msg.id !== undefined && pending.has(msg.id)) {
52
+ const { resolve, reject } = pending.get(msg.id);
53
+ pending.delete(msg.id);
54
+ if (msg.error) reject(new Error(`${msg.error.code}: ${msg.error.message}`));
55
+ else resolve(msg.result);
56
+ }
57
+ }
58
+ });
59
+
60
+ function rpc(method, params = {}, timeoutMs = 20000) {
61
+ const id = nextId++;
62
+ const payload = { jsonrpc: "2.0", id, method, params };
63
+ return new Promise((resolveOuter, reject) => {
64
+ const t = setTimeout(() => {
65
+ pending.delete(id);
66
+ reject(new Error(`Timeout: ${method}`));
67
+ }, timeoutMs);
68
+ pending.set(id, {
69
+ resolve: (v) => {
70
+ clearTimeout(t);
71
+ resolveOuter(v);
72
+ },
73
+ reject: (e) => {
74
+ clearTimeout(t);
75
+ reject(e);
76
+ },
77
+ });
78
+ child.stdin.write(JSON.stringify(payload) + "\n");
79
+ });
80
+ }
81
+
82
+ function summarise(content, max = 4000) {
83
+ if (!Array.isArray(content)) return JSON.stringify(content).slice(0, max);
84
+ return content
85
+ .map((c) => {
86
+ if (c.type === "text") {
87
+ const t = c.text ?? "";
88
+ return t.length > max ? `${t.slice(0, max)} … (+${t.length - max} chars)` : t;
89
+ }
90
+ if (c.type === "image") return `[image base64 len=${(c.data ?? "").length} mime=${c.mimeType}]`;
91
+ return `[${c.type}]`;
92
+ })
93
+ .join("\n");
94
+ }
95
+
96
+ async function callTool(name, args = {}, timeoutMs = 20000) {
97
+ const t0 = Date.now();
98
+ try {
99
+ const result = await rpc("tools/call", { name, arguments: args }, timeoutMs);
100
+ const ms = Date.now() - t0;
101
+ const ok = result?.isError ? "ERR" : "ok ";
102
+ console.log(`[${ok}] (${ms.toString().padStart(5)}ms) ${name}`);
103
+ console.log(` ${summarise(result?.content)}`);
104
+ return result;
105
+ } catch (e) {
106
+ const ms = Date.now() - t0;
107
+ console.log(`[ERR] (${ms.toString().padStart(5)}ms) ${name} ${e.message}`);
108
+ return null;
109
+ }
110
+ }
111
+
112
+ async function main() {
113
+ console.log("=== MCP E2E harness ===");
114
+
115
+ console.log("\n-- initialize --");
116
+ const init = await rpc("initialize", {
117
+ protocolVersion: "2024-11-05",
118
+ capabilities: { tools: {} },
119
+ clientInfo: { name: "agenticros-e2e", version: "0.0.1" },
120
+ });
121
+ console.log(`server: ${init.serverInfo?.name} v${init.serverInfo?.version}`);
122
+ await rpc("notifications/initialized", {}).catch(() => {});
123
+
124
+ console.log("\n-- tools/list --");
125
+ const tl = await rpc("tools/list", {});
126
+ console.log(` ${tl.tools?.length ?? 0} tools advertised`);
127
+ for (const t of tl.tools ?? []) {
128
+ console.log(` - ${t.name}`);
129
+ }
130
+
131
+ console.log("\n-- ros2_list_topics --");
132
+ await callTool("ros2_list_topics", {}, 30000);
133
+
134
+ console.log("\n-- ros2_publish /cmd_vel (linear.x=0.2) --");
135
+ await callTool(
136
+ "ros2_publish",
137
+ {
138
+ topic: "/cmd_vel",
139
+ type: "geometry_msgs/msg/Twist",
140
+ message: { linear: { x: 0.2, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } },
141
+ },
142
+ 10000,
143
+ );
144
+
145
+ console.log("\n-- ros2_subscribe_once /imu/data --");
146
+ await callTool(
147
+ "ros2_subscribe_once",
148
+ { topic: "/imu/data", type: "sensor_msgs/msg/Imu", timeoutMs: 5000 },
149
+ 10000,
150
+ );
151
+
152
+ console.log("\n-- ros2_subscribe_once /scan --");
153
+ await callTool(
154
+ "ros2_subscribe_once",
155
+ { topic: "/scan", type: "sensor_msgs/msg/LaserScan", timeoutMs: 5000 },
156
+ 10000,
157
+ );
158
+
159
+ console.log("\n-- ros2_camera_snapshot (RGB) --");
160
+ await callTool("ros2_camera_snapshot", {}, 15000);
161
+
162
+ console.log("\n-- ros2_depth_distance --");
163
+ await callTool("ros2_depth_distance", {}, 15000);
164
+
165
+ console.log("\n-- stop --");
166
+ await callTool(
167
+ "ros2_publish",
168
+ {
169
+ topic: "/cmd_vel",
170
+ type: "geometry_msgs/msg/Twist",
171
+ message: { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } },
172
+ },
173
+ 5000,
174
+ );
175
+
176
+ console.log("\n=== Done ===");
177
+ child.kill("SIGTERM");
178
+ }
179
+
180
+ main().catch((e) => {
181
+ console.error("Harness failed:", e);
182
+ child.kill("SIGKILL");
183
+ process.exit(1);
184
+ });