diff --git a/demos/moveit_pick_place/CMakeLists.txt b/demos/moveit_pick_place/CMakeLists.txt index 534f8a5..8e3fb5a 100644 --- a/demos/moveit_pick_place/CMakeLists.txt +++ b/demos/moveit_pick_place/CMakeLists.txt @@ -22,9 +22,6 @@ install(DIRECTORY worlds/ install(PROGRAMS scripts/manipulation_monitor.py scripts/pick_place_loop.py - scripts/inject-collision.sh - scripts/inject-planning-failure.sh - scripts/restore-normal.sh DESTINATION lib/${PROJECT_NAME} ) diff --git a/demos/moveit_pick_place/Dockerfile b/demos/moveit_pick_place/Dockerfile index beee585..b88e6ab 100644 --- a/demos/moveit_pick_place/Dockerfile +++ b/demos/moveit_pick_place/Dockerfile @@ -32,7 +32,7 @@ RUN apt-get update && apt-get install -y \ ros-jazzy-rosbag2-storage-mcap \ ros-jazzy-foxglove-bridge \ libsystemd-dev \ - sqlite3 libsqlite3-dev git curl \ + sqlite3 libsqlite3-dev git curl jq \ && rm -rf /var/lib/apt/lists/* # Create persistent directories for fault storage and rosbag recordings @@ -63,6 +63,10 @@ COPY launch/ ${COLCON_WS}/src/moveit_medkit_demo/launch/ COPY scripts/ ${COLCON_WS}/src/moveit_medkit_demo/scripts/ COPY worlds/ ${COLCON_WS}/src/moveit_medkit_demo/worlds/ +# TODO(#49): Move to manifest-defined scripts once ros2_medkit#303 lands +COPY container_scripts/ /var/lib/ros2_medkit/scripts/ +RUN find /var/lib/ros2_medkit/scripts -name "*.bash" -exec chmod +x {} \; + # Build ros2_medkit and demo package # Note: rosdep install uses || true because ros2_medkit packages are not in # rosdep indices (they're built from source). All system deps are already @@ -78,14 +82,6 @@ RUN bash -c "source /opt/ros/jazzy/setup.bash && \ RUN echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc && \ echo "source ${COLCON_WS}/install/setup.bash" >> ~/.bashrc -# Make inject/restore scripts available at a well-known path -ENV DEMO_SCRIPTS=${COLCON_WS}/scripts -RUN mkdir -p ${DEMO_SCRIPTS} && \ - ln -sf ${COLCON_WS}/install/moveit_medkit_demo/lib/moveit_medkit_demo/inject-collision.sh ${DEMO_SCRIPTS}/inject-collision.sh && \ - ln -sf ${COLCON_WS}/install/moveit_medkit_demo/lib/moveit_medkit_demo/inject-planning-failure.sh ${DEMO_SCRIPTS}/inject-planning-failure.sh && \ - ln -sf ${COLCON_WS}/install/moveit_medkit_demo/lib/moveit_medkit_demo/restore-normal.sh ${DEMO_SCRIPTS}/restore-normal.sh -ENV PATH="${DEMO_SCRIPTS}:${PATH}" - EXPOSE 8080 8765 CMD ["bash"] diff --git a/demos/moveit_pick_place/README.md b/demos/moveit_pick_place/README.md index aa1b3c9..6e79903 100644 --- a/demos/moveit_pick_place/README.md +++ b/demos/moveit_pick_place/README.md @@ -23,6 +23,7 @@ This demo demonstrates: ## Prerequisites - Docker and docker-compose +- `curl` and `jq` installed on the host (required for host-side scripts) - X11 display server (for Gazebo GUI) or `--headless` mode - (Optional) NVIDIA GPU + [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) — recommended for smooth Gazebo rendering - ~7 GB disk space for Docker image @@ -239,22 +240,53 @@ curl http://localhost:8080/api/v1/apps/panda-arm-controller/configurations/gains # Set a parameter value curl -X PUT http://localhost:8080/api/v1/apps/panda-arm-controller/configurations/constraints.goal_time \ -H 'Content-Type: application/json' \ - -d '{"data": {"value": 0.5}}' + -d '{"value": 0.5}' ``` -## Fault Injection Scenarios +## Scripts API + +The gateway exposes a Scripts API for the `moveit-planning` component. All fault injection and diagnostic scripts are available via REST without `docker exec`. + +### Available Scripts + +| Script ID | Name | Description | +|-----------|------|-------------| +| `inject-collision` | Inject Collision | Spawn a surprise obstacle in the robot workspace (Gazebo + MoveIt planning scene) | +| `inject-planning-failure` | Inject Planning Failure | Add collision wall blocking the pick-place path (Gazebo + MoveIt planning scene) | +| `restore-normal` | Restore Normal | Remove all injected obstacles and clear faults | +| `arm-self-test` | Arm Self-Test | Check joint states via REST API, verify values are reasonable | +| `planning-benchmark` | Planning Benchmark | Verify MoveIt planning is functional by checking key nodes and operations | + +### List Available Scripts -The fault injection scripts are **baked into the Docker image** under `$DEMO_SCRIPTS/` (on `PATH`). The host-side `./inject-*.sh` and `./restore-normal.sh` wrappers auto-detect the running container and delegate via `docker exec`. +```bash +curl http://localhost:8080/api/v1/components/moveit-planning/scripts | jq +``` -You can also run them directly inside the container: +### Execute a Script via REST ```bash -docker exec -it moveit_medkit_demo inject-collision.sh -docker exec -it moveit_medkit_demo inject-planning-failure.sh -docker exec -it moveit_medkit_demo restore-normal.sh +# Start execution +curl -X POST http://localhost:8080/api/v1/components/moveit-planning/scripts/inject-collision/executions \ + -H "Content-Type: application/json" \ + -d '{"execution_type": "now"}' | jq + +# Poll status (use execution ID from above response) +curl http://localhost:8080/api/v1/components/moveit-planning/scripts/inject-collision/executions/ | jq ``` -> **Future:** When SOVD Scripts endpoints are available, these will be callable via `curl` against the gateway REST API. +### Override Gateway URL + +```bash +# Point scripts at a non-default gateway +GATEWAY_URL=http://192.168.1.10:8080 ./inject-collision.sh +``` + +The host-side wrapper scripts (`./inject-collision.sh`, etc.) call the Scripts API automatically - no `docker exec` needed. Prerequisites: `curl` and `jq` must be installed on the host. + +## Fault Injection Scenarios + +The fault injection scripts run inside the container via the Scripts API. The host-side `./inject-*.sh` and `./restore-normal.sh` wrappers call the gateway REST endpoint - no `docker exec` required. ### 1. Planning Failure @@ -319,17 +351,30 @@ Connect it to the gateway at `http://localhost:8080` to browse: | `move-arm.sh` | **Interactive arm controller** — move to preset positions | | `check-entities.sh` | Explore the full SOVD entity hierarchy with sample data | | `check-faults.sh` | View active faults with severity summary | -| `inject-planning-failure.sh` | Thin wrapper → `docker exec` the in-container script | -| `inject-collision.sh` | Thin wrapper → `docker exec` the in-container script | -| `restore-normal.sh` | Thin wrapper → `docker exec` the in-container script | +| `inject-planning-failure.sh` | Scripts API wrapper — inject planning failure | +| `inject-collision.sh` | Scripts API wrapper — inject collision obstacle | +| `restore-normal.sh` | Scripts API wrapper — restore normal operation | +| `arm-self-test.sh` | Scripts API wrapper — run arm self-test | +| `planning-benchmark.sh` | Scripts API wrapper — run planning benchmark | + +Scripts API wrappers require `curl` and `jq` on the host and call the gateway REST endpoint directly - no `docker exec` needed. + +### In-container scripts (auto-discovery via Scripts API) + +Container scripts are stored under `/var/lib/ros2_medkit/scripts/moveit-planning/` and exposed via the gateway Scripts API. + +| Script ID | Description | +|-----------|-------------| +| `inject-collision` | Spawn visible sphere + MoveIt collision object | +| `inject-planning-failure` | Spawn visible wall + MoveIt collision object | +| `restore-normal` | Remove Gazebo models + MoveIt objects, clear faults | +| `arm-self-test` | Check joint states via REST API | +| `planning-benchmark` | Verify MoveIt planning is functional | -### In-container (baked into Docker image, on `PATH`) +### ROS 2 runtime scripts (baked into colcon install) | Script | Description | |--------|-------------| -| `inject-planning-failure.sh` | Spawn visible wall + MoveIt collision object | -| `inject-collision.sh` | Spawn visible sphere + MoveIt collision object | -| `restore-normal.sh` | Remove Gazebo models + MoveIt objects, clear faults | | `manipulation_monitor.py` | ROS 2 node: monitors topics and reports faults | | `pick_place_loop.py` | ROS 2 node: continuous pick-and-place cycle | diff --git a/demos/moveit_pick_place/arm-self-test.sh b/demos/moveit_pick_place/arm-self-test.sh new file mode 100644 index 0000000..9d5c12c --- /dev/null +++ b/demos/moveit_pick_place/arm-self-test.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Arm self-test via Scripts API +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" + +execute_script "components" "moveit-planning" "arm-self-test" "Arm self-test" diff --git a/demos/moveit_pick_place/config/medkit_params.yaml b/demos/moveit_pick_place/config/medkit_params.yaml index 3c38ebd..dfeb27c 100644 --- a/demos/moveit_pick_place/config/medkit_params.yaml +++ b/demos/moveit_pick_place/config/medkit_params.yaml @@ -29,6 +29,14 @@ diagnostics: # Plugin configuration (set by launch file when .so paths are resolved) plugins: [""] + # Scripts configuration (filesystem auto-discovery) + # TODO(#49): Migrate to manifest-defined scripts once ros2_medkit#303 lands + scripts: + scripts_dir: "/var/lib/ros2_medkit/scripts" + allow_uploads: false + max_concurrent_executions: 3 + default_timeout_sec: 60 + # Fault Manager configuration (runs in root namespace) fault_manager: ros__parameters: diff --git a/demos/moveit_pick_place/container_scripts/moveit-planning/arm-self-test/metadata.json b/demos/moveit_pick_place/container_scripts/moveit-planning/arm-self-test/metadata.json new file mode 100644 index 0000000..301535f --- /dev/null +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/arm-self-test/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Arm Self-Test", + "description": "Check joint states via REST API, verify values are reasonable", + "format": "bash" +} diff --git a/demos/moveit_pick_place/container_scripts/moveit-planning/arm-self-test/script.bash b/demos/moveit_pick_place/container_scripts/moveit-planning/arm-self-test/script.bash new file mode 100644 index 0000000..b95ada2 --- /dev/null +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/arm-self-test/script.bash @@ -0,0 +1,26 @@ +#!/bin/bash +# Arm self-test - verify joint states are within expected limits +set -eu +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +echo "Running arm self-test..." +echo "Checking joint state broadcaster..." +if ! curl -sf "${API_BASE}/apps/joint-state-broadcaster" > /dev/null 2>&1; then + echo "FAIL: joint-state-broadcaster not responding" + exit 1 +fi +echo "OK: joint-state-broadcaster responding" + +echo "Checking move-group..." +if ! curl -sf "${API_BASE}/apps/move-group" > /dev/null 2>&1; then + echo "FAIL: move-group not responding" + exit 1 +fi +echo "OK: move-group responding" + +echo "Checking fault status..." +FAULT_COUNT=$(curl -sf "${API_BASE}/faults" | jq '.items | length' 2>/dev/null || echo "?") +echo "Active faults: $FAULT_COUNT" + +echo "Arm self-test passed" diff --git a/demos/moveit_pick_place/container_scripts/moveit-planning/inject-collision/metadata.json b/demos/moveit_pick_place/container_scripts/moveit-planning/inject-collision/metadata.json new file mode 100644 index 0000000..458070e --- /dev/null +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/inject-collision/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject Collision", + "description": "Spawn a surprise obstacle in the robot workspace (Gazebo + MoveIt planning scene)", + "format": "bash" +} diff --git a/demos/moveit_pick_place/scripts/inject-collision.sh b/demos/moveit_pick_place/container_scripts/moveit-planning/inject-collision/script.bash old mode 100755 new mode 100644 similarity index 86% rename from demos/moveit_pick_place/scripts/inject-collision.sh rename to demos/moveit_pick_place/container_scripts/moveit-planning/inject-collision/script.bash index 3f799c7..3985905 --- a/demos/moveit_pick_place/scripts/inject-collision.sh +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/inject-collision/script.bash @@ -3,20 +3,20 @@ # Adds the object to both Gazebo (visible) and MoveIt planning scene (causes faults) # Runs INSIDE the Docker container. -set -e +set -eu # shellcheck source=/dev/null source /opt/ros/jazzy/setup.bash # shellcheck source=/dev/null source /root/demo_ws/install/setup.bash -echo "🚫 Injecting COLLISION fault..." +echo "Injecting COLLISION fault..." echo " Spawning surprise obstacle in robot workspace" echo "" # 1. Spawn visible model in Gazebo # Robot base (panda_link0) is at z=0.75 in the world frame. -# Obstacle at panda_link0 frame (0.4, 0, 0.4) → world frame (0.4, 0, 1.15) +# Obstacle at panda_link0 frame (0.4, 0, 0.4) -> world frame (0.4, 0, 1.15) echo "Spawning visible red sphere in Gazebo..." cat > /tmp/surprise_obstacle.sdf << 'EOSDF' @@ -41,9 +41,9 @@ if ros2 run ros_gz_sim create \ -file /tmp/surprise_obstacle.sdf \ -name surprise_obstacle \ -x 0.4 -y 0.0 -z 1.15 2>&1 | tail -1; then - echo " ✓ Gazebo model spawned" + echo " Gazebo model spawned" else - echo " ⚠ Gazebo spawn failed (visual only — fault injection still works)" + echo " Gazebo spawn failed (visual only - fault injection still works)" fi # 2. Add to MoveIt planning scene (so planner detects the collision) @@ -91,10 +91,10 @@ rclpy.shutdown() " echo "" -echo "✓ Collision fault injected!" +echo "Collision fault injected!" echo " A red sphere is now visible in Gazebo and registered in MoveIt planning scene." echo "" -echo "Expected faults (via manipulation_monitor → FaultManager):" +echo "Expected faults (via manipulation_monitor -> FaultManager):" echo " - MOTION_PLANNING_FAILED: Cannot find collision-free path" echo "" -echo "Restore with: /root/demo_ws/scripts/restore-normal.sh" +echo "Restore with: ./restore-normal.sh (or via Scripts API)" diff --git a/demos/moveit_pick_place/container_scripts/moveit-planning/inject-planning-failure/metadata.json b/demos/moveit_pick_place/container_scripts/moveit-planning/inject-planning-failure/metadata.json new file mode 100644 index 0000000..b19953e --- /dev/null +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/inject-planning-failure/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject Planning Failure", + "description": "Add collision wall blocking the pick-place path (Gazebo + MoveIt planning scene)", + "format": "bash" +} diff --git a/demos/moveit_pick_place/scripts/inject-planning-failure.sh b/demos/moveit_pick_place/container_scripts/moveit-planning/inject-planning-failure/script.bash old mode 100755 new mode 100644 similarity index 84% rename from demos/moveit_pick_place/scripts/inject-planning-failure.sh rename to demos/moveit_pick_place/container_scripts/moveit-planning/inject-planning-failure/script.bash index 35af337..18d8d8c --- a/demos/moveit_pick_place/scripts/inject-planning-failure.sh +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/inject-planning-failure/script.bash @@ -3,20 +3,20 @@ # Adds the wall to both Gazebo (visible) and MoveIt planning scene (causes faults) # Runs INSIDE the Docker container. -set -e +set -eu # shellcheck source=/dev/null source /opt/ros/jazzy/setup.bash # shellcheck source=/dev/null source /root/demo_ws/install/setup.bash -echo "🚫 Injecting PLANNING FAILURE fault..." +echo "Injecting PLANNING FAILURE fault..." echo " Adding collision wall between pick and place positions" echo "" # 1. Spawn visible wall in Gazebo # Robot base (panda_link0) is at z=0.75 in the world frame. -# Wall at panda_link0 frame (0.3, 0.25, 0.5) → world frame (0.3, 0.25, 1.25) +# Wall at panda_link0 frame (0.3, 0.25, 0.5) -> world frame (0.3, 0.25, 1.25) # Wall dimensions: 2.0 x 0.05 x 1.0 (wide, thin, tall) echo "Spawning visible orange wall in Gazebo..." cat > /tmp/injected_wall.sdf << 'EOSDF' @@ -42,9 +42,9 @@ if ros2 run ros_gz_sim create \ -file /tmp/injected_wall.sdf \ -name injected_wall \ -x 0.3 -y 0.25 -z 1.25 2>&1 | tail -1; then - echo " ✓ Gazebo model spawned" + echo " Gazebo model spawned" else - echo " ⚠ Gazebo spawn failed (visual only — fault injection still works)" + echo " Gazebo spawn failed (visual only - fault injection still works)" fi # 2. Add to MoveIt planning scene (so planner cannot find a path) @@ -92,10 +92,10 @@ rclpy.shutdown() " echo "" -echo "✓ Planning failure injected!" +echo "Planning failure injected!" echo " An orange wall is now visible in Gazebo and registered in MoveIt planning scene." echo "" -echo "Expected faults (via manipulation_monitor → FaultManager):" -echo " - MOTION_PLANNING_FAILED: MoveGroup goal ABORTED — no collision-free path" +echo "Expected faults (via manipulation_monitor -> FaultManager):" +echo " - MOTION_PLANNING_FAILED: MoveGroup goal ABORTED - no collision-free path" echo "" -echo "Restore with: /root/demo_ws/scripts/restore-normal.sh" +echo "Restore with: ./restore-normal.sh (or via Scripts API)" diff --git a/demos/moveit_pick_place/container_scripts/moveit-planning/planning-benchmark/metadata.json b/demos/moveit_pick_place/container_scripts/moveit-planning/planning-benchmark/metadata.json new file mode 100644 index 0000000..7e445f1 --- /dev/null +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/planning-benchmark/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Planning Benchmark", + "description": "Verify MoveIt planning is functional by checking key nodes and operations", + "format": "bash" +} diff --git a/demos/moveit_pick_place/container_scripts/moveit-planning/planning-benchmark/script.bash b/demos/moveit_pick_place/container_scripts/moveit-planning/planning-benchmark/script.bash new file mode 100644 index 0000000..a951352 --- /dev/null +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/planning-benchmark/script.bash @@ -0,0 +1,29 @@ +#!/bin/bash +# Planning benchmark - verify MoveIt planning is functional +set -eu +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +echo "Running planning benchmark..." +echo "Checking move-group operations..." +if ! curl -sf "${API_BASE}/apps/move-group/operations" > /dev/null 2>&1; then + echo "FAIL: Cannot list move-group operations" + exit 1 +fi +echo "OK: move-group operations available" + +echo "Checking pick-place-node..." +if ! curl -sf "${API_BASE}/apps/pick-place-node" > /dev/null 2>&1; then + echo "FAIL: pick-place-node not responding" + exit 1 +fi +echo "OK: pick-place-node responding" + +echo "Checking manipulation monitor..." +if ! curl -sf "${API_BASE}/apps/manipulation-monitor" > /dev/null 2>&1; then + echo "FAIL: manipulation-monitor not responding" + exit 1 +fi +echo "OK: manipulation-monitor responding" + +echo "Planning benchmark passed" diff --git a/demos/moveit_pick_place/container_scripts/moveit-planning/restore-normal/metadata.json b/demos/moveit_pick_place/container_scripts/moveit-planning/restore-normal/metadata.json new file mode 100644 index 0000000..f70616e --- /dev/null +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/restore-normal/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Restore Normal", + "description": "Remove all injected obstacles from Gazebo and MoveIt planning scene, clear faults", + "format": "bash" +} diff --git a/demos/moveit_pick_place/scripts/restore-normal.sh b/demos/moveit_pick_place/container_scripts/moveit-planning/restore-normal/script.bash old mode 100755 new mode 100644 similarity index 96% rename from demos/moveit_pick_place/scripts/restore-normal.sh rename to demos/moveit_pick_place/container_scripts/moveit-planning/restore-normal/script.bash index 82abac1..a50d81a --- a/demos/moveit_pick_place/scripts/restore-normal.sh +++ b/demos/moveit_pick_place/container_scripts/moveit-planning/restore-normal/script.bash @@ -2,7 +2,7 @@ # Restore Normal Operation - Remove all injected faults # Runs INSIDE the Docker container. -set -e +set -eu # shellcheck source=/dev/null source /opt/ros/jazzy/setup.bash @@ -12,7 +12,7 @@ source /root/demo_ws/install/setup.bash GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" API_BASE="${GATEWAY_URL}/api/v1" -echo "🔄 Restoring NORMAL operation..." +echo "Restoring NORMAL operation..." echo "" # 1. Remove injected Gazebo models (visual objects) @@ -72,7 +72,7 @@ sleep 5 curl -sf -X DELETE "${API_BASE}/faults" > /dev/null 2>&1 || true echo "" -echo "✓ Normal operation restored!" +echo "Normal operation restored!" echo "" if command -v jq >/dev/null 2>&1; then echo "Current fault status:" diff --git a/demos/moveit_pick_place/inject-collision.sh b/demos/moveit_pick_place/inject-collision.sh index fca1bfe..33f36f1 100755 --- a/demos/moveit_pick_place/inject-collision.sh +++ b/demos/moveit_pick_place/inject-collision.sh @@ -1,11 +1,8 @@ #!/bin/bash -# Inject Collision - Spawn a surprise obstacle in the robot's workspace +# Inject collision obstacle via Scripts API set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -CONTAINER="${CONTAINER_NAME:-$(docker ps --format '{{.Names}}' | grep -E '^moveit_medkit_demo(_nvidia)?(_local)?$' | head -n1)}" -if [ -z "${CONTAINER}" ]; then - echo "❌ Demo container not running. Start it first: ./run-demo.sh" - exit 1 -fi - -exec docker exec -it "${CONTAINER}" inject-collision.sh +execute_script "components" "moveit-planning" "inject-collision" "Inject collision obstacle" diff --git a/demos/moveit_pick_place/inject-planning-failure.sh b/demos/moveit_pick_place/inject-planning-failure.sh index ff4784d..424badb 100755 --- a/demos/moveit_pick_place/inject-planning-failure.sh +++ b/demos/moveit_pick_place/inject-planning-failure.sh @@ -1,11 +1,8 @@ #!/bin/bash -# Inject Planning Failure - Block the robot's path with a collision wall +# Inject planning failure via Scripts API set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -CONTAINER="${CONTAINER_NAME:-$(docker ps --format '{{.Names}}' | grep -E '^moveit_medkit_demo(_nvidia)?(_local)?$' | head -n1)}" -if [ -z "${CONTAINER}" ]; then - echo "❌ Demo container not running. Start it first: ./run-demo.sh" - exit 1 -fi - -exec docker exec -it "${CONTAINER}" inject-planning-failure.sh +execute_script "components" "moveit-planning" "inject-planning-failure" "Inject planning failure" diff --git a/demos/moveit_pick_place/planning-benchmark.sh b/demos/moveit_pick_place/planning-benchmark.sh new file mode 100644 index 0000000..0e8f55a --- /dev/null +++ b/demos/moveit_pick_place/planning-benchmark.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Planning benchmark via Scripts API +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" + +execute_script "components" "moveit-planning" "planning-benchmark" "Planning benchmark" diff --git a/demos/moveit_pick_place/restore-normal.sh b/demos/moveit_pick_place/restore-normal.sh index 68c7f89..ef972e1 100755 --- a/demos/moveit_pick_place/restore-normal.sh +++ b/demos/moveit_pick_place/restore-normal.sh @@ -1,11 +1,8 @@ #!/bin/bash -# Restore Normal Operation - Remove all injected faults +# Restore normal operation via Scripts API set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -CONTAINER="${CONTAINER_NAME:-$(docker ps --format '{{.Names}}' | grep -E '^moveit_medkit_demo(_nvidia)?(_local)?$' | head -n1)}" -if [ -z "${CONTAINER}" ]; then - echo "❌ Demo container not running. Start it first: ./run-demo.sh" - exit 1 -fi - -exec docker exec -it "${CONTAINER}" restore-normal.sh +execute_script "components" "moveit-planning" "restore-normal" "Restore normal operation" diff --git a/demos/sensor_diagnostics/Dockerfile b/demos/sensor_diagnostics/Dockerfile index 7f2f9be..1e432ff 100644 --- a/demos/sensor_diagnostics/Dockerfile +++ b/demos/sensor_diagnostics/Dockerfile @@ -48,6 +48,10 @@ COPY src/ ${COLCON_WS}/src/sensor_diagnostics_demo/src/ COPY config/ ${COLCON_WS}/src/sensor_diagnostics_demo/config/ COPY launch/ ${COLCON_WS}/src/sensor_diagnostics_demo/launch/ +# TODO(#49): Move to manifest-defined scripts once ros2_medkit#303 lands +COPY container_scripts/ /var/lib/ros2_medkit/scripts/ +RUN find /var/lib/ros2_medkit/scripts -name "*.bash" -exec chmod +x {} \; + # Build all packages (skip test dependencies that aren't in ros-base) WORKDIR ${COLCON_WS} RUN bash -c "source /opt/ros/jazzy/setup.bash && \ diff --git a/demos/sensor_diagnostics/README.md b/demos/sensor_diagnostics/README.md index 976ee02..c6db136 100644 --- a/demos/sensor_diagnostics/README.md +++ b/demos/sensor_diagnostics/README.md @@ -15,6 +15,8 @@ This demo showcases ros2_medkit's data monitoring, configuration management, and ## Quick Start +> **Host prerequisites:** The host-side scripts (`check-demo.sh`, `inject-*.sh`, `restore-normal.sh`) require `curl` and `jq` to be installed on your machine. + ### Using Docker (Recommended) ```bash @@ -139,6 +141,49 @@ Sensor Topics → anomaly_detector monitors | `inject-failure.sh` | IMU | Modern | Complete sensor timeout | | `restore-normal.sh` | All | Both | Clears all faults | +## Scripts API + +The inject and restore scripts run inside the container and are also callable directly via the gateway REST API using the Scripts endpoint. This lets you trigger fault scenarios programmatically without needing the shell scripts on the host. + +### List Available Scripts + +```bash +curl http://localhost:8080/api/v1/components/compute-unit/scripts | jq +``` + +### Execute a Script + +```bash +curl -X POST http://localhost:8080/api/v1/components/compute-unit/scripts/inject-nan/executions \ + -H "Content-Type: application/json" \ + -d '{"execution_type":"now"}' | jq +``` + +### Check Execution Status + +```bash +curl http://localhost:8080/api/v1/components/compute-unit/scripts/inject-nan/executions/ | jq +``` + +### Override Gateway URL + +```bash +# Point scripts at a non-default gateway +GATEWAY_URL=http://192.168.1.10:8080 ./inject-nan.sh +``` + +### Available Scripts + +| Script | Description | +|--------|-------------| +| `run-diagnostics` | Check health of all sensors | +| `inject-fault-scenario` | Composite fault injection (all sensors) | +| `inject-drift` | Inject sensor drift on LiDAR | +| `inject-failure` | Inject sensor failure on IMU | +| `inject-nan` | Inject NaN values on LiDAR, IMU, GPS | +| `inject-noise` | Inject high noise on LiDAR and Camera | +| `restore-normal` | Reset all sensors and clear faults | + ## API Examples ### Read Sensor Data @@ -207,12 +252,16 @@ curl http://localhost:8080/api/v1/faults | jq | `run-demo.sh` | Start Docker services (daemon mode) | | `stop-demo.sh` | Stop Docker services | | `check-demo.sh` | Interactive API demonstration and exploration | +| `run-diagnostics.sh` | Check health of all sensors | +| `inject-fault-scenario.sh` | Composite fault injection across all sensors | | `inject-noise.sh` | Inject high noise fault | | `inject-failure.sh` | Cause sensor timeout | | `inject-nan.sh` | Inject NaN values | | `inject-drift.sh` | Enable sensor drift | | `restore-normal.sh` | Clear all faults | +> **Note:** All diagnostic scripts (`inject-*.sh`, `restore-normal.sh`, `run-diagnostics.sh`, `inject-fault-scenario.sh`) are also available via the [Scripts API](#scripts-api) - callable as REST endpoints without requiring the host-side scripts. + ## Sensor Parameters ### LiDAR (`/sensors/lidar_sim`) diff --git a/demos/sensor_diagnostics/config/medkit_params.yaml b/demos/sensor_diagnostics/config/medkit_params.yaml index 744776e..bb944ba 100644 --- a/demos/sensor_diagnostics/config/medkit_params.yaml +++ b/demos/sensor_diagnostics/config/medkit_params.yaml @@ -27,6 +27,14 @@ diagnostics: # Plugin configuration (set by launch file when .so paths are resolved) plugins: [""] + # Scripts configuration (filesystem auto-discovery) + # TODO(#49): Migrate to manifest-defined scripts once ros2_medkit#303 lands + scripts: + scripts_dir: "/var/lib/ros2_medkit/scripts" + allow_uploads: false + max_concurrent_executions: 3 + default_timeout_sec: 60 + # Fault Manager configuration (runs in root namespace) fault_manager: ros__parameters: diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-drift/metadata.json b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-drift/metadata.json new file mode 100644 index 0000000..9a6aaea --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-drift/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject Drift", + "description": "Inject sensor drift: set drift_rate=0.1 on lidar-sim", + "format": "bash" +} diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-drift/script.bash b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-drift/script.bash new file mode 100644 index 0000000..156d4a7 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-drift/script.bash @@ -0,0 +1,12 @@ +#!/bin/bash +# Inject sensor drift: set drift_rate=0.1 on lidar-sim +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +curl -sf -X PUT "${API_BASE}/apps/lidar-sim/configurations/drift_rate" \ + -H "Content-Type: application/json" -d '{"value": 0.1}' + +echo "Drift injected: lidar-sim drift_rate=0.1" +exit 0 diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-failure/metadata.json b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-failure/metadata.json new file mode 100644 index 0000000..599f327 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-failure/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject Failure", + "description": "Inject sensor failure: set failure_probability=1.0 on imu-sim", + "format": "bash" +} diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-failure/script.bash b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-failure/script.bash new file mode 100644 index 0000000..1d5687b --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-failure/script.bash @@ -0,0 +1,12 @@ +#!/bin/bash +# Inject sensor failure: set failure_probability=1.0 on imu-sim +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +curl -sf -X PUT "${API_BASE}/apps/imu-sim/configurations/failure_probability" \ + -H "Content-Type: application/json" -d '{"value": 1.0}' + +echo "Failure injected: imu-sim failure_probability=1.0" +exit 0 diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-fault-scenario/metadata.json b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-fault-scenario/metadata.json new file mode 100644 index 0000000..6876969 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-fault-scenario/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject Fault Scenario", + "description": "Inject composite fault scenario: NaN values on lidar, IMU, GPS and black frames on camera simultaneously", + "format": "bash" +} diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-fault-scenario/script.bash b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-fault-scenario/script.bash new file mode 100644 index 0000000..7ed2b7f --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-fault-scenario/script.bash @@ -0,0 +1,44 @@ +#!/bin/bash +# Inject composite fault scenario: NaN values on lidar, IMU, GPS and black frames on camera simultaneously +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +echo "Injecting composite fault scenario..." +ERRORS=0 + +if curl -sf -X PUT "${API_BASE}/apps/lidar-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": true}' > /dev/null 2>&1; then + echo "lidar-sim: inject_nan=true" +else + echo "FAIL: lidar-sim"; ERRORS=$((ERRORS + 1)) +fi + +if curl -sf -X PUT "${API_BASE}/apps/imu-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": true}' > /dev/null 2>&1; then + echo "imu-sim: inject_nan=true" +else + echo "FAIL: imu-sim"; ERRORS=$((ERRORS + 1)) +fi + +if curl -sf -X PUT "${API_BASE}/apps/gps-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": true}' > /dev/null 2>&1; then + echo "gps-sim: inject_nan=true" +else + echo "FAIL: gps-sim"; ERRORS=$((ERRORS + 1)) +fi + +if curl -sf -X PUT "${API_BASE}/apps/camera-sim/configurations/inject_black_frames" \ + -H "Content-Type: application/json" -d '{"value": true}' > /dev/null 2>&1; then + echo "camera-sim: inject_black_frames=true" +else + echo "FAIL: camera-sim"; ERRORS=$((ERRORS + 1)) +fi + +if [ $ERRORS -gt 0 ]; then + echo "Composite injection partially failed: $ERRORS error(s)" + exit 1 +fi +echo "Composite fault scenario injected on all sensors" +exit 0 diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-nan/metadata.json b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-nan/metadata.json new file mode 100644 index 0000000..9cf4a14 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-nan/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject NaN", + "description": "Inject NaN values on lidar-sim, imu-sim, and gps-sim simultaneously", + "format": "bash" +} diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-nan/script.bash b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-nan/script.bash new file mode 100644 index 0000000..1cabb39 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-nan/script.bash @@ -0,0 +1,36 @@ +#!/bin/bash +# Inject NaN values on lidar-sim, imu-sim, and gps-sim simultaneously +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +ERRORS=0 + +if curl -sf -X PUT "${API_BASE}/apps/lidar-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": true}' > /dev/null 2>&1; then + echo "lidar-sim: inject_nan=true" +else + echo "FAIL: lidar-sim"; ERRORS=$((ERRORS + 1)) +fi + +if curl -sf -X PUT "${API_BASE}/apps/imu-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": true}' > /dev/null 2>&1; then + echo "imu-sim: inject_nan=true" +else + echo "FAIL: imu-sim"; ERRORS=$((ERRORS + 1)) +fi + +if curl -sf -X PUT "${API_BASE}/apps/gps-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": true}' > /dev/null 2>&1; then + echo "gps-sim: inject_nan=true" +else + echo "FAIL: gps-sim"; ERRORS=$((ERRORS + 1)) +fi + +if [ $ERRORS -gt 0 ]; then + echo "NaN injection partially failed: $ERRORS error(s)" + exit 1 +fi +echo "NaN injection enabled on lidar-sim, imu-sim, gps-sim" +exit 0 diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-noise/metadata.json b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-noise/metadata.json new file mode 100644 index 0000000..165b41f --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-noise/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject Noise", + "description": "Inject high noise: noise_stddev=0.5 on lidar-sim and noise_level=0.3 on camera-sim", + "format": "bash" +} diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/inject-noise/script.bash b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-noise/script.bash new file mode 100644 index 0000000..6980f20 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/inject-noise/script.bash @@ -0,0 +1,29 @@ +#!/bin/bash +# Inject high noise: noise_stddev=0.5 on lidar-sim and noise_level=0.3 on camera-sim +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +ERRORS=0 + +if curl -sf -X PUT "${API_BASE}/apps/lidar-sim/configurations/noise_stddev" \ + -H "Content-Type: application/json" -d '{"value": 0.5}' > /dev/null 2>&1; then + echo "lidar-sim: noise_stddev=0.5" +else + echo "FAIL: lidar-sim"; ERRORS=$((ERRORS + 1)) +fi + +if curl -sf -X PUT "${API_BASE}/apps/camera-sim/configurations/noise_level" \ + -H "Content-Type: application/json" -d '{"value": 0.3}' > /dev/null 2>&1; then + echo "camera-sim: noise_level=0.3" +else + echo "FAIL: camera-sim"; ERRORS=$((ERRORS + 1)) +fi + +if [ $ERRORS -gt 0 ]; then + echo "Noise injection partially failed: $ERRORS error(s)" + exit 1 +fi +echo "High noise injected on lidar-sim and camera-sim" +exit 0 diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/restore-normal/metadata.json b/demos/sensor_diagnostics/container_scripts/compute-unit/restore-normal/metadata.json new file mode 100644 index 0000000..a045ef8 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/restore-normal/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Restore Normal", + "description": "Reset all sensor parameters to defaults and clear all active faults", + "format": "bash" +} diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/restore-normal/script.bash b/demos/sensor_diagnostics/container_scripts/compute-unit/restore-normal/script.bash new file mode 100644 index 0000000..a3c2622 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/restore-normal/script.bash @@ -0,0 +1,65 @@ +#!/bin/bash +# Reset all sensor parameters to defaults and clear all active faults +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +echo "Restoring all sensors to normal operation..." + +# LiDAR +curl -sf -X PUT "${API_BASE}/apps/lidar-sim/configurations/noise_stddev" \ + -H "Content-Type: application/json" -d '{"value": 0.01}' || true +curl -sf -X PUT "${API_BASE}/apps/lidar-sim/configurations/failure_probability" \ + -H "Content-Type: application/json" -d '{"value": 0.0}' || true +curl -sf -X PUT "${API_BASE}/apps/lidar-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": false}' || true +curl -sf -X PUT "${API_BASE}/apps/lidar-sim/configurations/drift_rate" \ + -H "Content-Type: application/json" -d '{"value": 0.0}' || true +echo "lidar-sim: restored to defaults" + +# IMU +curl -sf -X PUT "${API_BASE}/apps/imu-sim/configurations/accel_noise_stddev" \ + -H "Content-Type: application/json" -d '{"value": 0.01}' || true +curl -sf -X PUT "${API_BASE}/apps/imu-sim/configurations/failure_probability" \ + -H "Content-Type: application/json" -d '{"value": 0.0}' || true +curl -sf -X PUT "${API_BASE}/apps/imu-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": false}' || true +curl -sf -X PUT "${API_BASE}/apps/imu-sim/configurations/drift_rate" \ + -H "Content-Type: application/json" -d '{"value": 0.0}' || true +echo "imu-sim: restored to defaults" + +# GPS +curl -sf -X PUT "${API_BASE}/apps/gps-sim/configurations/position_noise_stddev" \ + -H "Content-Type: application/json" -d '{"value": 2.0}' || true +curl -sf -X PUT "${API_BASE}/apps/gps-sim/configurations/failure_probability" \ + -H "Content-Type: application/json" -d '{"value": 0.0}' || true +curl -sf -X PUT "${API_BASE}/apps/gps-sim/configurations/inject_nan" \ + -H "Content-Type: application/json" -d '{"value": false}' || true +curl -sf -X PUT "${API_BASE}/apps/gps-sim/configurations/drift_rate" \ + -H "Content-Type: application/json" -d '{"value": 0.0}' || true +echo "gps-sim: restored to defaults" + +# Camera +curl -sf -X PUT "${API_BASE}/apps/camera-sim/configurations/noise_level" \ + -H "Content-Type: application/json" -d '{"value": 0.0}' || true +curl -sf -X PUT "${API_BASE}/apps/camera-sim/configurations/failure_probability" \ + -H "Content-Type: application/json" -d '{"value": 0.0}' || true +curl -sf -X PUT "${API_BASE}/apps/camera-sim/configurations/inject_black_frames" \ + -H "Content-Type: application/json" -d '{"value": false}' || true +echo "camera-sim: restored to defaults" + +# Clear all faults +curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/LIDAR_SIM" > /dev/null 2>&1 || true +curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/CAMERA_SIM" > /dev/null 2>&1 || true +curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/IMU_SIM" > /dev/null 2>&1 || true +curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/GPS_SIM" > /dev/null 2>&1 || true +curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/SENSOR_TIMEOUT" > /dev/null 2>&1 || true +curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/SENSOR_NAN" > /dev/null 2>&1 || true +curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/SENSOR_OUT_OF_RANGE" > /dev/null 2>&1 || true +curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/RATE_DEGRADED" > /dev/null 2>&1 || true +curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/NO_FIX" > /dev/null 2>&1 || true +echo "All faults cleared" + +echo "All sensors restored to normal operation" +exit 0 diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/run-diagnostics/metadata.json b/demos/sensor_diagnostics/container_scripts/compute-unit/run-diagnostics/metadata.json new file mode 100644 index 0000000..7149962 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/run-diagnostics/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Run Diagnostics", + "description": "Check health of all 4 sensors by querying their data endpoints and reporting active faults", + "format": "bash" +} diff --git a/demos/sensor_diagnostics/container_scripts/compute-unit/run-diagnostics/script.bash b/demos/sensor_diagnostics/container_scripts/compute-unit/run-diagnostics/script.bash new file mode 100644 index 0000000..61a39a1 --- /dev/null +++ b/demos/sensor_diagnostics/container_scripts/compute-unit/run-diagnostics/script.bash @@ -0,0 +1,32 @@ +#!/bin/bash +# Check health of all 4 sensors by querying their data endpoints and reporting active faults +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +SENSORS=("lidar-sim/data/scan" "imu-sim/data/imu" "gps-sim/data/fix" "camera-sim/data/image") +ERRORS=0 + +for sensor_path in "${SENSORS[@]}"; do + app=$(echo "$sensor_path" | cut -d/ -f1) + echo "Checking $app..." + if ! curl -sf "${API_BASE}/apps/${sensor_path}" > /dev/null 2>&1; then + echo " FAIL: No response from $app" + ERRORS=$((ERRORS + 1)) + else + echo " OK: $app responding" + fi +done + +# Also check faults +FAULT_COUNT=$(curl -sf "${API_BASE}/faults" | jq '.items | length' 2>/dev/null || echo "?") +echo "Active faults: $FAULT_COUNT" + +if [ $ERRORS -gt 0 ]; then + echo "DIAGNOSTICS FAILED: $ERRORS sensor(s) not responding" + exit 1 +fi + +echo "All sensors healthy" +exit 0 diff --git a/demos/sensor_diagnostics/inject-drift.sh b/demos/sensor_diagnostics/inject-drift.sh index 0cb30d2..902ca83 100755 --- a/demos/sensor_diagnostics/inject-drift.sh +++ b/demos/sensor_diagnostics/inject-drift.sh @@ -1,25 +1,8 @@ #!/bin/bash -# Inject sensor drift fault - demonstrates LEGACY fault reporting path -# LiDAR drift → DiagnosticArray → /diagnostics → diagnostic-bridge → FaultManager +# Inject sensor drift +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" -API_BASE="${GATEWAY_URL}/api/v1" - -echo "Injecting DRIFT fault (Legacy path: LiDAR → diagnostic-bridge)..." -echo "" - -# LiDAR drift: uses legacy diagnostics path -echo "[LEGACY PATH] Setting LiDAR drift_rate to 0.1 m/s..." -echo " Fault path: lidar-sim → /diagnostics topic → diagnostic-bridge → FaultManager" -curl -s -X PUT "${API_BASE}/apps/lidar-sim/configurations/drift_rate" \ - -H "Content-Type: application/json" \ - -d '{"value": 0.1}' - -echo "" -echo "✓ Drift enabled! LiDAR readings will gradually shift over time." -echo "" -echo "Fault codes expected (auto-generated from diagnostic name):" -echo " - LIDAR_SIM (DRIFTING status, WARN severity)" -echo "" -echo "Watch the drift accumulate with: curl ${GATEWAY_URL}/api/v1/apps/lidar-sim/data/scan | jq '.ranges[:5]'" -echo "Check faults with: curl ${GATEWAY_URL}/api/v1/faults | jq" +execute_script "components" "compute-unit" "inject-drift" "Inject sensor drift" diff --git a/demos/sensor_diagnostics/inject-failure.sh b/demos/sensor_diagnostics/inject-failure.sh index 71ff173..971fbc7 100755 --- a/demos/sensor_diagnostics/inject-failure.sh +++ b/demos/sensor_diagnostics/inject-failure.sh @@ -1,20 +1,8 @@ #!/bin/bash -# Inject sensor failure (timeout) fault - demonstrates MODERN fault reporting path -# IMU sensor → anomaly-detector → FaultManager (via ReportFault service) +# Inject sensor failure +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" -API_BASE="${GATEWAY_URL}/api/v1" - -echo "Injecting SENSOR FAILURE fault (Modern path: IMU → anomaly-detector)..." - -# Set high failure probability - IMU will stop publishing -echo "Setting IMU failure_probability to 1.0 (complete failure)..." -curl -s -X PUT "${API_BASE}/apps/imu-sim/configurations/failure_probability" \ - -H "Content-Type: application/json" \ - -d '{"value": 1.0}' - -echo "" -echo "✓ IMU failure injected!" -echo " Fault reporting path: imu-sim → anomaly-detector → /fault_manager/report_fault" -echo " The anomaly detector should report SENSOR_TIMEOUT fault directly to FaultManager." -echo " Check faults with: curl ${API_BASE}/faults | jq" +execute_script "components" "compute-unit" "inject-failure" "Inject sensor failure" diff --git a/demos/sensor_diagnostics/inject-fault-scenario.sh b/demos/sensor_diagnostics/inject-fault-scenario.sh new file mode 100644 index 0000000..5b6b7c9 --- /dev/null +++ b/demos/sensor_diagnostics/inject-fault-scenario.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Inject composite fault scenario +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" + +execute_script "components" "compute-unit" "inject-fault-scenario" "Inject composite fault scenario" diff --git a/demos/sensor_diagnostics/inject-nan.sh b/demos/sensor_diagnostics/inject-nan.sh index 94f0931..ad8560c 100755 --- a/demos/sensor_diagnostics/inject-nan.sh +++ b/demos/sensor_diagnostics/inject-nan.sh @@ -1,39 +1,8 @@ #!/bin/bash -# Inject NaN values fault - demonstrates BOTH fault reporting paths +# Inject NaN values +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" -API_BASE="${GATEWAY_URL}/api/v1" - -echo "Injecting NaN VALUES fault (demonstrates both fault reporting paths)..." -echo "" - -# LEGACY PATH: LiDAR publishes DiagnosticArray → diagnostic_bridge → FaultManager -echo "[LEGACY PATH] Enabling LiDAR inject_nan..." -echo " Fault path: lidar-sim → /diagnostics topic → diagnostic-bridge → FaultManager" -curl -s -X PUT "${API_BASE}/apps/lidar-sim/configurations/inject_nan" \ - -H "Content-Type: application/json" \ - -d '{"value": true}' -echo "" - -# MODERN PATH: IMU/GPS → anomaly-detector → FaultManager (direct service call) -echo "[MODERN PATH] Enabling IMU inject_nan..." -echo " Fault path: imu-sim → anomaly-detector → /fault_manager/report_fault" -curl -s -X PUT "${API_BASE}/apps/imu-sim/configurations/inject_nan" \ - -H "Content-Type: application/json" \ - -d '{"value": true}' -echo "" - -echo "[MODERN PATH] Enabling GPS inject_nan..." -echo " Fault path: gps-sim → anomaly-detector → /fault_manager/report_fault" -curl -s -X PUT "${API_BASE}/apps/gps-sim/configurations/inject_nan" \ - -H "Content-Type: application/json" \ - -d '{"value": true}' - -echo "" -echo "✓ NaN injection enabled on multiple sensors!" -echo "" -echo "Fault codes expected:" -echo " - LIDAR_SIM (from diagnostic-bridge, auto-generated from diagnostic name)" -echo " - SENSOR_NAN (from anomaly-detector)" -echo "" -echo "Check faults with: curl ${API_BASE}/faults | jq" +execute_script "components" "compute-unit" "inject-nan" "Inject NaN values" diff --git a/demos/sensor_diagnostics/inject-noise.sh b/demos/sensor_diagnostics/inject-noise.sh index a4fdfe7..cff08de 100755 --- a/demos/sensor_diagnostics/inject-noise.sh +++ b/demos/sensor_diagnostics/inject-noise.sh @@ -1,33 +1,8 @@ #!/bin/bash -# Inject high noise fault - demonstrates LEGACY fault reporting path -# LiDAR/Camera → DiagnosticArray → /diagnostics → diagnostic-bridge → FaultManager +# Inject high noise +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" -API_BASE="${GATEWAY_URL}/api/v1" - -echo "Injecting HIGH NOISE fault (Legacy path: LiDAR/Camera → diagnostic-bridge)..." -echo "" - -# LiDAR: increase noise stddev (uses legacy diagnostics path) -echo "[LEGACY PATH] Setting LiDAR noise_stddev to 0.5 (very noisy)..." -echo " Fault path: lidar-sim → /diagnostics topic → diagnostic-bridge → FaultManager" -curl -s -X PUT "${API_BASE}/apps/lidar-sim/configurations/noise_stddev" \ - -H "Content-Type: application/json" \ - -d '{"value": 0.5}' -echo "" - -# Camera: add noise (uses legacy diagnostics path) -echo "[LEGACY PATH] Setting Camera noise_level to 0.3..." -echo " Fault path: camera-sim → /diagnostics topic → diagnostic-bridge → FaultManager" -curl -s -X PUT "${API_BASE}/apps/camera-sim/configurations/noise_level" \ - -H "Content-Type: application/json" \ - -d '{"value": 0.3}' - -echo "" -echo "✓ High noise injected on LiDAR and Camera!" -echo "" -echo "Fault codes expected (auto-generated from diagnostic names):" -echo " - LIDAR_SIM (HIGH_NOISE status)" -echo " - CAMERA_SIM (HIGH_NOISE status)" -echo "" -echo "Check faults with: curl ${API_BASE}/faults | jq" +execute_script "components" "compute-unit" "inject-noise" "Inject high noise" diff --git a/demos/sensor_diagnostics/restore-normal.sh b/demos/sensor_diagnostics/restore-normal.sh index 8e009fc..42e2c9d 100755 --- a/demos/sensor_diagnostics/restore-normal.sh +++ b/demos/sensor_diagnostics/restore-normal.sh @@ -1,69 +1,8 @@ #!/bin/bash -# Restore normal sensor operation (clear all faults) +# Restore normal operation +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" -API_BASE="${GATEWAY_URL}/api/v1" - -echo "Restoring NORMAL operation..." - -# LiDAR -echo "Resetting LiDAR parameters..." -curl -s -X PUT "${API_BASE}/apps/lidar-sim/configurations/noise_stddev" \ - -H "Content-Type: application/json" -d '{"value": 0.01}' -curl -s -X PUT "${API_BASE}/apps/lidar-sim/configurations/failure_probability" \ - -H "Content-Type: application/json" -d '{"value": 0.0}' -curl -s -X PUT "${API_BASE}/apps/lidar-sim/configurations/inject_nan" \ - -H "Content-Type: application/json" -d '{"value": false}' -curl -s -X PUT "${API_BASE}/apps/lidar-sim/configurations/drift_rate" \ - -H "Content-Type: application/json" -d '{"value": 0.0}' - -# IMU -echo "Resetting IMU parameters..." -curl -s -X PUT "${API_BASE}/apps/imu-sim/configurations/accel_noise_stddev" \ - -H "Content-Type: application/json" -d '{"value": 0.01}' -curl -s -X PUT "${API_BASE}/apps/imu-sim/configurations/failure_probability" \ - -H "Content-Type: application/json" -d '{"value": 0.0}' -curl -s -X PUT "${API_BASE}/apps/imu-sim/configurations/inject_nan" \ - -H "Content-Type: application/json" -d '{"value": false}' -curl -s -X PUT "${API_BASE}/apps/imu-sim/configurations/drift_rate" \ - -H "Content-Type: application/json" -d '{"value": 0.0}' - -# GPS -echo "Resetting GPS parameters..." -curl -s -X PUT "${API_BASE}/apps/gps-sim/configurations/position_noise_stddev" \ - -H "Content-Type: application/json" -d '{"value": 2.0}' -curl -s -X PUT "${API_BASE}/apps/gps-sim/configurations/failure_probability" \ - -H "Content-Type: application/json" -d '{"value": 0.0}' -curl -s -X PUT "${API_BASE}/apps/gps-sim/configurations/inject_nan" \ - -H "Content-Type: application/json" -d '{"value": false}' -curl -s -X PUT "${API_BASE}/apps/gps-sim/configurations/drift_rate" \ - -H "Content-Type: application/json" -d '{"value": 0.0}' - -# Camera -echo "Resetting Camera parameters..." -curl -s -X PUT "${API_BASE}/apps/camera-sim/configurations/noise_level" \ - -H "Content-Type: application/json" -d '{"value": 0.0}' -curl -s -X PUT "${API_BASE}/apps/camera-sim/configurations/failure_probability" \ - -H "Content-Type: application/json" -d '{"value": 0.0}' -curl -s -X PUT "${API_BASE}/apps/camera-sim/configurations/inject_black_frames" \ - -H "Content-Type: application/json" -d '{"value": false}' - -# Clear all faults from FaultManager -# All sensors now publish to /diagnostics, so all faults come through diagnostic-bridge -echo "" -echo "Clearing all faults from FaultManager..." -curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/LIDAR_SIM" > /dev/null 2>&1 -curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/CAMERA_SIM" > /dev/null 2>&1 -curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/IMU_SIM" > /dev/null 2>&1 -curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/GPS_SIM" > /dev/null 2>&1 - -# Faults from anomaly-detector (modern path for anomaly detection) -curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/SENSOR_TIMEOUT" > /dev/null 2>&1 -curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/SENSOR_NAN" > /dev/null 2>&1 -curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/SENSOR_OUT_OF_RANGE" > /dev/null 2>&1 -curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/RATE_DEGRADED" > /dev/null 2>&1 -curl -s -X DELETE "${API_BASE}/apps/anomaly-detector/faults/NO_FIX" > /dev/null 2>&1 - -echo "" -echo "✓ Normal operation restored! All fault injections and faults cleared." -echo " Verify with: curl ${API_BASE}/faults | jq" +execute_script "components" "compute-unit" "restore-normal" "Restore normal operation" diff --git a/demos/sensor_diagnostics/run-diagnostics.sh b/demos/sensor_diagnostics/run-diagnostics.sh new file mode 100644 index 0000000..55e4246 --- /dev/null +++ b/demos/sensor_diagnostics/run-diagnostics.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Run sensor diagnostics +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" + +execute_script "components" "compute-unit" "run-diagnostics" "Run sensor diagnostics" diff --git a/demos/turtlebot3_integration/Dockerfile b/demos/turtlebot3_integration/Dockerfile index 7831984..b1ce696 100644 --- a/demos/turtlebot3_integration/Dockerfile +++ b/demos/turtlebot3_integration/Dockerfile @@ -42,6 +42,7 @@ RUN apt-get update && apt-get install -y \ libsqlite3-dev \ git \ curl \ + jq \ && rm -rf /var/lib/apt/lists/* # Clone ros2_medkit from GitHub (gateway + dependencies + plugins) @@ -68,6 +69,10 @@ COPY config/ ${COLCON_WS}/src/turtlebot3_medkit_demo/config/ COPY launch/ ${COLCON_WS}/src/turtlebot3_medkit_demo/launch/ COPY scripts/ ${COLCON_WS}/src/turtlebot3_medkit_demo/scripts/ +# TODO(#49): Move to manifest-defined scripts once ros2_medkit#303 lands +COPY container_scripts/ /var/lib/ros2_medkit/scripts/ +RUN find /var/lib/ros2_medkit/scripts -name "*.bash" -exec chmod +x {} \; + # Build ros2_medkit and demo package WORKDIR ${COLCON_WS} RUN bash -c "source /opt/ros/jazzy/setup.bash && \ diff --git a/demos/turtlebot3_integration/README.md b/demos/turtlebot3_integration/README.md index 67e52df..079e156 100644 --- a/demos/turtlebot3_integration/README.md +++ b/demos/turtlebot3_integration/README.md @@ -25,6 +25,7 @@ This demo demonstrates: - Docker and docker-compose - X11 display server (Linux with GUI, or XQuartz on macOS) - (Optional) NVIDIA GPU + [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) +- `curl` and `jq` (required for host-side scripts) ## Quick Start @@ -356,6 +357,49 @@ This demo uses two fault reporting paths: | Nav2 Controller | diagnostic_bridge | Path following errors | | TurtleBot3 | diagnostic_bridge | Motor/sensor issues | +## Scripts API + +The inject and restore scripts run inside the container and are callable via the gateway REST API. This lets you trigger fault scenarios programmatically or from the web UI. + +> **Host prerequisites:** The host-side scripts require `curl` and `jq`. + +### List Available Scripts + +```bash +curl http://localhost:8080/api/v1/components/nav2-stack/scripts | jq +``` + +### Execute a Script + +```bash +curl -X POST http://localhost:8080/api/v1/components/nav2-stack/scripts/inject-nav-failure/executions \ + -H "Content-Type: application/json" \ + -d '{"execution_type":"now"}' | jq +``` + +### Check Execution Status + +```bash +curl http://localhost:8080/api/v1/components/nav2-stack/scripts/inject-nav-failure/executions/ | jq +``` + +### Override Gateway URL + +```bash +# Point scripts at a non-default gateway +GATEWAY_URL=http://192.168.1.10:8080 ./inject-nav-failure.sh +``` + +### Available Scripts + +| Script | Description | +|--------|-------------| +| `nav-health-check` | Check health of Nav2 stack | +| `reset-navigation` | Cancel goals and reset AMCL | +| `inject-localization-failure` | Inject AMCL localization failure | +| `inject-nav-failure` | Inject navigation failure (unreachable goal) | +| `restore-normal` | Reset parameters and clear faults | + ## Fault Injection Scenarios This demo includes scripts to inject various fault conditions for testing fault management. @@ -472,6 +516,13 @@ demos/turtlebot3_integration/ │ ├── turtlebot3_manifest.yaml # SOVD manifest (entity hierarchy) │ ├── nav2_params.yaml # Nav2 navigation parameters │ └── turtlebot3_world.yaml # Map configuration +├── container_scripts/ +│ └── nav2-stack/ # Scripts API auto-discovery layout +│ ├── nav-health-check/ +│ ├── reset-navigation/ +│ ├── inject-nav-failure/ +│ ├── inject-localization-failure/ +│ └── restore-normal/ ├── launch/ │ └── demo.launch.py # ROS 2 launch file └── scripts/ @@ -487,10 +538,14 @@ demos/turtlebot3_integration/ | `send-nav-goal.sh [x] [y] [yaw]` | Send navigation goal via SOVD API | | `check-entities.sh` | Explore SOVD entity hierarchy | | `check-faults.sh` | View active faults from gateway | +| `nav-health-check.sh` | Check Nav2 stack health | +| `reset-navigation.sh` | Cancel goals and reset AMCL | | `inject-nav-failure.sh` | Inject navigation failure (unreachable goal) | | `inject-localization-failure.sh` | Inject localization failure (AMCL reset) | | `restore-normal.sh` | Restore normal operation and clear faults | +> **Note:** The inject, restore, and diagnostic scripts are also available via the [Scripts API](#scripts-api) - callable as REST endpoints without requiring the host-side scripts. + ## Manual Setup (Alternative) If you prefer not to use Docker: diff --git a/demos/turtlebot3_integration/config/medkit_params.yaml b/demos/turtlebot3_integration/config/medkit_params.yaml index 36d2575..7c1d5a6 100644 --- a/demos/turtlebot3_integration/config/medkit_params.yaml +++ b/demos/turtlebot3_integration/config/medkit_params.yaml @@ -28,6 +28,14 @@ diagnostics: # Plugin configuration (set by launch file when .so paths are resolved) plugins: [""] + # Scripts configuration (filesystem auto-discovery) + # TODO(#49): Migrate to manifest-defined scripts once ros2_medkit#303 lands + scripts: + scripts_dir: "/var/lib/ros2_medkit/scripts" + allow_uploads: false + max_concurrent_executions: 3 + default_timeout_sec: 60 + # Fault Manager configuration (runs in root namespace) fault_manager: ros__parameters: diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-localization-failure/metadata.json b/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-localization-failure/metadata.json new file mode 100644 index 0000000..0c8df34 --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-localization-failure/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject Localization Failure", + "description": "Reinitialize AMCL global localization (scatters particles) then send a navigation goal under high uncertainty", + "format": "bash" +} diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-localization-failure/script.bash b/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-localization-failure/script.bash new file mode 100755 index 0000000..c34b396 --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-localization-failure/script.bash @@ -0,0 +1,36 @@ +#!/bin/bash +# Inject localization failure: reinitialize AMCL then send a navigation goal under high uncertainty +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +echo "Reinitializing AMCL global localization (scatters particles)..." +curl -sf -X POST "${API_BASE}/apps/amcl/operations/reinitialize_global_localization/executions" \ + -H "Content-Type: application/json" \ + -d '{}' + +echo "" +echo "Waiting for particles to scatter..." +sleep 2 + +echo "Sending navigation goal with high localization uncertainty..." +curl -sf -X POST "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions" \ + -H "Content-Type: application/json" \ + -d '{ + "goal": { + "pose": { + "header": {"frame_id": "map"}, + "pose": { + "position": {"x": 2.0, "y": 0.0, "z": 0.0}, + "orientation": {"w": 1.0} + } + } + } + }' + +echo "" +echo "Localization failure injected." +echo "AMCL has been reinitialized - localization uncertainty is high." +echo "Monitor faults at: ${API_BASE}/faults" +exit 0 diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-nav-failure/metadata.json b/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-nav-failure/metadata.json new file mode 100644 index 0000000..0045127 --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-nav-failure/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Inject Navigation Failure", + "description": "Send a navigation goal to an unreachable location (100, 100) far outside the turtlebot3_world map", + "format": "bash" +} diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-nav-failure/script.bash b/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-nav-failure/script.bash new file mode 100755 index 0000000..174eaef --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/inject-nav-failure/script.bash @@ -0,0 +1,44 @@ +#!/bin/bash +# Inject navigation failure: send goal to unreachable location far outside map bounds +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +echo "Sending navigation goal to (100.0, 100.0) - far outside map..." +RESPONSE=$(curl -s -X POST "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions" \ + -H "Content-Type: application/json" \ + -d '{ + "goal": { + "pose": { + "header": {"frame_id": "map"}, + "pose": { + "position": {"x": 100.0, "y": 100.0, "z": 0.0}, + "orientation": {"w": 1.0} + } + } + } + }') + +echo "${RESPONSE}" | jq '.' 2>/dev/null || echo "${RESPONSE}" + +EXEC_ID=$(echo "${RESPONSE}" | jq -r '.execution_id // .id // empty' 2>/dev/null) + +if [ -n "${EXEC_ID}" ] && [ "${EXEC_ID}" != "null" ] && [[ "${EXEC_ID}" =~ ^[a-zA-Z0-9_-]+$ ]]; then + echo "" + echo "Waiting for navigation to fail (checking status)..." + for _ in $(seq 1 10); do + sleep 2 + STATUS=$(curl -s "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions/${EXEC_ID}" \ + | jq -r '.status' 2>/dev/null) + echo " Status: ${STATUS}" + if [ "${STATUS}" = "failed" ] || [ "${STATUS}" = "completed" ]; then + break + fi + done +fi + +echo "" +echo "Navigation failure injected." +echo "Monitor faults at: ${API_BASE}/faults" +exit 0 diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/nav-health-check/metadata.json b/demos/turtlebot3_integration/container_scripts/nav2-stack/nav-health-check/metadata.json new file mode 100644 index 0000000..3b209b0 --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/nav-health-check/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Nav2 Health Check", + "description": "Check nav2 stack health: verify amcl, bt-navigator, controller-server, and planner-server are responding", + "format": "bash" +} diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/nav-health-check/script.bash b/demos/turtlebot3_integration/container_scripts/nav2-stack/nav-health-check/script.bash new file mode 100755 index 0000000..b27d00b --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/nav-health-check/script.bash @@ -0,0 +1,36 @@ +#!/bin/bash +# Check nav2 stack health: verify key apps are responding via gateway +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +APPS=("amcl" "bt-navigator" "controller-server" "planner-server") +ERRORS=0 + +echo "Checking nav2 stack health..." +echo "" + +for app in "${APPS[@]}"; do + echo "Checking ${app}..." + if curl -sf "${API_BASE}/apps/${app}" > /dev/null 2>&1; then + echo " OK: ${app} responding" + else + echo " FAIL: ${app} not responding" + ERRORS=$((ERRORS + 1)) + fi +done + +echo "" +FAULT_COUNT=$(curl -sf "${API_BASE}/faults" | jq '.items | length' 2>/dev/null || echo "?") +echo "Active faults: ${FAULT_COUNT}" + +if [ "${ERRORS}" -eq 0 ]; then + echo "" + echo "All nav2 apps healthy." + exit 0 +else + echo "" + echo "${ERRORS} app(s) not responding." + exit 1 +fi diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/reset-navigation/metadata.json b/demos/turtlebot3_integration/container_scripts/nav2-stack/reset-navigation/metadata.json new file mode 100644 index 0000000..78d32e5 --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/reset-navigation/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Reset Navigation", + "description": "Cancel active navigation goals and reset AMCL global localization", + "format": "bash" +} diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/reset-navigation/script.bash b/demos/turtlebot3_integration/container_scripts/nav2-stack/reset-navigation/script.bash new file mode 100755 index 0000000..cedbfae --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/reset-navigation/script.bash @@ -0,0 +1,29 @@ +#!/bin/bash +# Cancel active navigation goals and reset AMCL global localization +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +echo "Canceling active navigation goals..." +EXECUTIONS=$(curl -sf "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions" 2>/dev/null || echo '{"items":[]}') +if echo "${EXECUTIONS}" | jq -e '.items[]' > /dev/null 2>&1; then + echo "${EXECUTIONS}" | jq -r '.items[].id' | while read -r EXEC_ID; do + if [ -n "${EXEC_ID}" ] && [[ "${EXEC_ID}" =~ ^[a-zA-Z0-9_-]+$ ]]; then + curl -sf -X DELETE "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions/${EXEC_ID}" > /dev/null 2>&1 || true + echo " Canceled execution: ${EXEC_ID}" + fi + done +else + echo " No active executions found." +fi + +echo "" +echo "Resetting AMCL global localization..." +curl -sf -X POST "${API_BASE}/apps/amcl/operations/reinitialize_global_localization/executions" \ + -H "Content-Type: application/json" \ + -d '{}' + +echo "" +echo "Navigation reset complete." +exit 0 diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/restore-normal/metadata.json b/demos/turtlebot3_integration/container_scripts/nav2-stack/restore-normal/metadata.json new file mode 100644 index 0000000..c7bcd64 --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/restore-normal/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Restore Normal Operation", + "description": "Cancel active navigation goals, restore velocity parameters to defaults, and clear all faults", + "format": "bash" +} diff --git a/demos/turtlebot3_integration/container_scripts/nav2-stack/restore-normal/script.bash b/demos/turtlebot3_integration/container_scripts/nav2-stack/restore-normal/script.bash new file mode 100755 index 0000000..d55c7af --- /dev/null +++ b/demos/turtlebot3_integration/container_scripts/nav2-stack/restore-normal/script.bash @@ -0,0 +1,38 @@ +#!/bin/bash +# Restore normal operation: cancel goals, restore velocity params, clear all faults +set -eu + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +echo "Canceling active navigation goals..." +EXECUTIONS=$(curl -s "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions" 2>/dev/null || echo '{"items":[]}') +if echo "${EXECUTIONS}" | jq -e '.items[]' > /dev/null 2>&1; then + echo "${EXECUTIONS}" | jq -r '.items[].id' | while read -r EXEC_ID; do + if [ -n "${EXEC_ID}" ] && [[ "${EXEC_ID}" =~ ^[a-zA-Z0-9_-]+$ ]]; then + curl -s -X DELETE "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions/${EXEC_ID}" > /dev/null 2>&1 || true + echo " Canceled execution: ${EXEC_ID}" + fi + done +else + echo " No active executions found." +fi + +echo "" +echo "Restoring velocity parameters to defaults..." +curl -s -X PUT "${API_BASE}/apps/velocity-smoother/configurations/max_velocity" \ + -H "Content-Type: application/json" \ + -d '{"value": [0.26, 0.0, 1.0]}' > /dev/null 2>&1 || true + +curl -s -X PUT "${API_BASE}/apps/controller-server/configurations/FollowPath.max_vel_x" \ + -H "Content-Type: application/json" \ + -d '{"value": 0.26}' > /dev/null 2>&1 || true + +echo "Clearing all faults..." +curl -s -X DELETE "${API_BASE}/faults" > /dev/null || true + +echo "" +echo "Normal operation restored." +FAULT_COUNT=$(curl -sf "${API_BASE}/faults" | jq '.items | length' 2>/dev/null || echo "?") +echo "Active faults: ${FAULT_COUNT}" +exit 0 diff --git a/demos/turtlebot3_integration/inject-localization-failure.sh b/demos/turtlebot3_integration/inject-localization-failure.sh index a94b56c..965ea2d 100755 --- a/demos/turtlebot3_integration/inject-localization-failure.sh +++ b/demos/turtlebot3_integration/inject-localization-failure.sh @@ -1,51 +1,8 @@ #!/bin/bash -# Inject Localization Failure - Reset AMCL with bad initial pose -# This will cause localization issues and potential navigation failures +# Inject localization failure via Scripts API +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" -API_BASE="${GATEWAY_URL}/api/v1" - -echo "📍 Injecting LOCALIZATION FAILURE fault..." -echo " Reinitializing AMCL with incorrect pose (robot thinks it's somewhere else)" -echo "" - -# Check gateway -if ! curl -sf "${API_BASE}/health" > /dev/null 2>&1; then - echo "❌ Gateway not available at ${GATEWAY_URL}" - exit 1 -fi - -# Reinitialize global localization - this scatters particles and causes uncertainty -echo "Triggering global localization reinitialize..." -curl -s -X POST "${API_BASE}/apps/amcl/operations/reinitialize_global_localization/executions" \ - -H "Content-Type: application/json" \ - -d '{}' - -echo "" -echo "Waiting for particles to scatter..." -sleep 2 - -# Now try to navigate - with scattered particles, localization uncertainty is high -echo "Sending navigation goal (with high localization uncertainty)..." -curl -s -X POST "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions" \ - -H "Content-Type: application/json" \ - -d '{ - "goal": { - "pose": { - "header": {"frame_id": "map"}, - "pose": { - "position": {"x": 2.0, "y": 0.0, "z": 0.0}, - "orientation": {"w": 1.0} - } - } - } - }' | jq '.' 2>/dev/null || true - -echo "" -echo "✓ Localization failure injected!" -echo "" -echo "AMCL has been reinitialized - localization uncertainty is high." -echo "The anomaly_detector monitors AMCL covariance and will report:" -echo " - LOCALIZATION_UNCERTAINTY: High AMCL covariance (uncertainty)" -echo "" -echo "Check faults with: curl ${API_BASE}/faults | jq" +execute_script "components" "nav2-stack" "inject-localization-failure" "Inject localization failure" diff --git a/demos/turtlebot3_integration/inject-nav-failure.sh b/demos/turtlebot3_integration/inject-nav-failure.sh index 07529e3..3e9e757 100755 --- a/demos/turtlebot3_integration/inject-nav-failure.sh +++ b/demos/turtlebot3_integration/inject-nav-failure.sh @@ -1,65 +1,8 @@ #!/bin/bash -# Inject Navigation Failure - Send goal to unreachable location -# This will trigger a path planning failure from Nav2 +# Inject navigation failure via Scripts API +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" -API_BASE="${GATEWAY_URL}/api/v1" - -echo "🚫 Injecting NAVIGATION FAILURE fault..." -echo " Sending goal to unreachable location (outside map bounds)" -echo "" - -# Check for jq dependency -if ! command -v jq >/dev/null 2>&1; then - echo "❌ 'jq' is required but not installed." - echo " Please install jq (e.g., 'sudo apt-get install jq') and retry." - exit 1 -fi - -# Check gateway -if ! curl -sf "${API_BASE}/health" > /dev/null 2>&1; then - echo "❌ Gateway not available at ${GATEWAY_URL}" - exit 1 -fi - -# Send goal to location far outside the map (turtlebot3_world is small ~5x5m) -echo "Sending navigation goal to (100.0, 100.0) - far outside map..." -RESPONSE=$(curl -s -X POST "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions" \ - -H "Content-Type: application/json" \ - -d '{ - "goal": { - "pose": { - "header": {"frame_id": "map"}, - "pose": { - "position": {"x": 100.0, "y": 100.0, "z": 0.0}, - "orientation": {"w": 1.0} - } - } - } - }') - -echo "$RESPONSE" | jq '.' 2>/dev/null || echo "$RESPONSE" - -# Extract execution ID (support both .execution_id and .id) -EXEC_ID=$(echo "$RESPONSE" | jq -r '.execution_id // .id // empty' 2>/dev/null) - -if [ -n "$EXEC_ID" ] && [ "$EXEC_ID" != "null" ]; then - echo "" - echo "Waiting for navigation to fail (checking status)..." - for _ in {1..10}; do - sleep 2 - STATUS=$(curl -s "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions/${EXEC_ID}" | jq -r '.status' 2>/dev/null) - echo " Status: $STATUS" - if [ "$STATUS" = "failed" ] || [ "$STATUS" = "completed" ]; then - break - fi - done -fi - -echo "" -echo "✓ Navigation failure injected!" -echo "" -echo "Expected faults (via anomaly_detector → FaultManager):" -echo " - NAVIGATION_GOAL_ABORTED: Navigation goal ABORTED" -echo "" -echo "Check faults with: curl ${API_BASE}/faults | jq" +execute_script "components" "nav2-stack" "inject-nav-failure" "Inject navigation failure" diff --git a/demos/turtlebot3_integration/nav-health-check.sh b/demos/turtlebot3_integration/nav-health-check.sh new file mode 100755 index 0000000..7f36b3a --- /dev/null +++ b/demos/turtlebot3_integration/nav-health-check.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Nav2 health check via Scripts API +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" + +execute_script "components" "nav2-stack" "nav-health-check" "Nav2 health check" diff --git a/demos/turtlebot3_integration/reset-navigation.sh b/demos/turtlebot3_integration/reset-navigation.sh new file mode 100755 index 0000000..fafa00e --- /dev/null +++ b/demos/turtlebot3_integration/reset-navigation.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Reset navigation via Scripts API +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" + +execute_script "components" "nav2-stack" "reset-navigation" "Reset navigation" diff --git a/demos/turtlebot3_integration/restore-normal.sh b/demos/turtlebot3_integration/restore-normal.sh index d61db86..43877e4 100755 --- a/demos/turtlebot3_integration/restore-normal.sh +++ b/demos/turtlebot3_integration/restore-normal.sh @@ -1,58 +1,8 @@ #!/bin/bash -# Restore Normal Operation - Reset all parameters and clear faults -# Use this after running any inject-*.sh script +# Restore normal operation via Scripts API +set -eu +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/../../lib/scripts-api.sh" -GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" -API_BASE="${GATEWAY_URL}/api/v1" - -echo "🔄 Restoring NORMAL operation..." -echo "" - -# Check for jq dependency -if ! command -v jq >/dev/null 2>&1; then - echo "❌ 'jq' is required but not installed." - echo " Please install jq (e.g., 'sudo apt-get install jq') and retry." - exit 1 -fi - -# Check gateway -if ! curl -sf "${API_BASE}/health" > /dev/null 2>&1; then - echo "❌ Gateway not available at ${GATEWAY_URL}" - exit 1 -fi - -# Cancel any active navigation goals -echo "Canceling any active navigation goals..." -# List active executions and cancel them -EXECUTIONS=$(curl -s "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions" 2>/dev/null) -if echo "$EXECUTIONS" | jq -e '.items[]' > /dev/null 2>&1; then - echo "$EXECUTIONS" | jq -r '.items[].id' | while read -r EXEC_ID; do - if [ -n "$EXEC_ID" ]; then - curl -s -X DELETE "${API_BASE}/apps/bt-navigator/operations/navigate_to_pose/executions/$EXEC_ID" > /dev/null 2>&1 - echo " Canceled execution: $EXEC_ID" - fi - done -fi - -# Restore velocity smoother defaults (in case controller-failure was run before removal) -echo "" -echo "Restoring velocity parameters to defaults..." -curl -s -X PUT "${API_BASE}/apps/velocity-smoother/configurations/max_velocity" \ - -H "Content-Type: application/json" \ - -d '{"value": [0.26, 0.0, 1.0]}' > /dev/null 2>&1 - -curl -s -X PUT "${API_BASE}/apps/controller-server/configurations/FollowPath.max_vel_x" \ - -H "Content-Type: application/json" \ - -d '{"value": 0.26}' > /dev/null 2>&1 - -# Clear all faults -echo "Clearing all faults from FaultManager..." -curl -s -X DELETE "${API_BASE}/faults" > /dev/null - -echo "" -echo "✓ Normal operation restored!" -echo "" -echo "Current fault status:" -curl -s "${API_BASE}/faults" | jq '.items | length' | xargs -I {} echo " Active faults: {}" -echo "" -echo "Robot is ready for normal operation." +execute_script "components" "nav2-stack" "restore-normal" "Restore normal operation" diff --git a/lib/scripts-api.sh b/lib/scripts-api.sh new file mode 100644 index 0000000..1e43169 --- /dev/null +++ b/lib/scripts-api.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# Shared helper for calling gateway Scripts API +# Source this from host-side wrapper scripts: +# SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# source "${SCRIPT_DIR}/../../lib/scripts-api.sh" +# +# Environment variables: +# GATEWAY_URL - Gateway base URL (default: http://localhost:8080) +# POLL_INTERVAL - Seconds between status polls (default: 1) +# MAX_WAIT - Max seconds to wait for completion (default: 120) + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" +POLL_INTERVAL="${POLL_INTERVAL:-1}" +MAX_WAIT="${MAX_WAIT:-120}" + +# Check dependencies +for cmd in curl jq; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Required tool '$cmd' is not installed." + exit 1 + fi +done + +# Check gateway is reachable +check_gateway() { + if ! curl -sf "${API_BASE}/health" > /dev/null 2>&1; then + echo "Gateway not available at ${GATEWAY_URL}. Is the demo running? Try: ./run-demo.sh" + exit 1 + fi +} + +# Execute a script via Scripts API and poll until completion +# Usage: execute_script [description] +# entity_type: "components" or "apps" +execute_script() { + local entity_type="$1" + local entity_id="$2" + local script_id="$3" + local description="${4:-$script_id}" + + check_gateway + + echo "Executing ${description} via Scripts API..." + local result + if ! result=$(curl -sf -X POST "${API_BASE}/${entity_type}/${entity_id}/scripts/${script_id}/executions" \ + -H "Content-Type: application/json" \ + -d '{"execution_type": "now"}' 2>/dev/null); then + echo "Failed to start script execution." + echo "Check that the script '${script_id}' exists:" + echo " curl ${API_BASE}/${entity_type}/${entity_id}/scripts | jq" + exit 1 + fi + + local exec_id + exec_id=$(echo "$result" | jq -r '.id') + if [ -z "$exec_id" ] || [ "$exec_id" = "null" ]; then + echo "Failed to start script execution:" + echo "$result" | jq '.' 2>/dev/null || echo "$result" + exit 1 + fi + if [[ ! "$exec_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then + echo "Unexpected execution ID format: $exec_id" + exit 1 + fi + + echo "Execution started: $exec_id" + + # Poll until done + local start=$SECONDS + while [ $((SECONDS - start)) -lt "$MAX_WAIT" ]; do + local exec_data + exec_data=$(curl -sf "${API_BASE}/${entity_type}/${entity_id}/scripts/${script_id}/executions/${exec_id}" 2>/dev/null) || exec_data="" + + if [ -z "$exec_data" ]; then + # Transient failure - keep polling + printf "." >&2 + sleep "$POLL_INTERVAL" + continue + fi + + local status + status=$(echo "$exec_data" | jq -r '.status') + + case "$status" in + completed) + echo "" + # Show script output if available + local stdout + stdout=$(echo "$exec_data" | jq -r '.parameters.stdout // empty' 2>/dev/null) + if [ -n "$stdout" ]; then + echo "$stdout" + fi + echo "Done." + return 0 + ;; + failed|terminated) + echo "" + echo "Script ${status}!" + echo "$exec_data" | jq -r '.error // empty' 2>/dev/null + local err_stdout + err_stdout=$(echo "$exec_data" | jq -r '.parameters.stdout // empty' 2>/dev/null) + if [ -n "$err_stdout" ]; then + echo "$err_stdout" + fi + return 1 + ;; + *) + printf "." >&2 + sleep "$POLL_INTERVAL" + ;; + esac + done + + echo "" + echo "Timeout waiting for script execution (${MAX_WAIT}s)" + return 1 +} + +# List available scripts for an entity +# Usage: list_scripts +list_scripts() { + local entity_type="$1" + local entity_id="$2" + + check_gateway + + curl -sf "${API_BASE}/${entity_type}/${entity_id}/scripts" | jq '.items[] | {id, name, description}' 2>/dev/null +}