diff --git a/demos/moveit_pick_place/README.md b/demos/moveit_pick_place/README.md index 6e79903..61aeab8 100644 --- a/demos/moveit_pick_place/README.md +++ b/demos/moveit_pick_place/README.md @@ -1,10 +1,10 @@ # MoveIt 2 Pick-and-Place Integration Demo -A comprehensive integration demo combining a **Panda 7-DOF robot arm** with **MoveIt 2** motion planning and **ros2_medkit** SOVD-compliant diagnostics. The robot performs continuous pick-and-place cycles in a **Gazebo Harmonic factory scene** while a manipulation monitor detects faults — planning failures, collisions — and reports them through the SOVD REST API with environment snapshots. +A comprehensive integration demo combining a **Panda 7-DOF robot arm** with **MoveIt 2** motion planning and **ros2_medkit** SOVD-compliant diagnostics. The robot performs continuous pick-and-place cycles in a **Gazebo Harmonic factory scene** while a manipulation monitor detects faults - planning failures, collisions - and reports them through the SOVD REST API with environment snapshots. ## Status -✅ **Demo Ready** — Docker-based deployment with MoveIt 2, Gazebo Harmonic physics simulation, factory environment, and full ros2_medkit stack. +✅ **Demo Ready** - Docker-based deployment with MoveIt 2, Gazebo Harmonic physics simulation, factory environment, and full ros2_medkit stack. ## Overview @@ -14,7 +14,7 @@ This demo demonstrates: - **Gazebo Harmonic simulation** with a realistic factory scene (conveyor belt, work table, storage, lighting) - **Continuous pick-and-place** loop as a realistic manipulation workload - **Manipulation fault monitoring** (planning failures, collision detection) -- **Fault snapshots** — environment state captured at fault time (joint states, diagnostics) +- **Fault snapshots** - environment state captured at fault time (joint states, diagnostics) - **SOVD-compliant REST API** with Areas → Components → Apps → Functions hierarchy - **Manifest-based entity discovery** (hybrid mode with runtime enrichment) - **2 fault injection scenarios** with visible Gazebo models and one-click scripts @@ -25,7 +25,7 @@ This demo demonstrates: - 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 +- (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 ## Quick Start @@ -129,37 +129,37 @@ docker exec -it moveit_medkit_demo bash # Shell into container ``` Areas -├── manipulation/ — Robot arm and gripper hardware +├── manipulation/ - Robot arm and gripper hardware │ Components -│ ├── panda-arm — 7-DOF Franka Emika Panda +│ ├── panda-arm - 7-DOF Franka Emika Panda │ │ Apps: joint-state-broadcaster, panda-arm-controller, robot-state-publisher -│ └── panda-gripper — 2-finger parallel gripper +│ └── panda-gripper - 2-finger parallel gripper │ Apps: panda-hand-controller │ -├── planning/ — MoveIt 2 motion planning stack +├── planning/ - MoveIt 2 motion planning stack │ Components -│ ├── moveit-planning — OMPL planning pipeline +│ ├── moveit-planning - OMPL planning pipeline │ │ Apps: move-group -│ └── pick-place-loop — Pick-and-place demo node +│ └── pick-place-loop - Pick-and-place demo node │ Apps: pick-place-node │ -├── diagnostics/ — ros2_medkit gateway and fault management +├── diagnostics/ - ros2_medkit gateway and fault management │ Components -│ ├── gateway — REST API +│ ├── gateway - REST API │ │ Apps: medkit-gateway -│ └── fault-manager — Fault aggregation +│ └── fault-manager - Fault aggregation │ Apps: medkit-fault-manager │ -└── bridge/ — Legacy diagnostics bridge +└── bridge/ - Legacy diagnostics bridge Components └── diagnostic-bridge Apps: diagnostic-bridge-app, manipulation-monitor Functions -├── pick-and-place — Pick objects and place at target positions -├── motion-planning — Plan collision-free motion trajectories -├── gripper-control — Open and close the Panda gripper -└── fault-management — Collect and expose faults via SOVD API +├── pick-and-place - Pick objects and place at target positions +├── motion-planning - Plan collision-free motion trajectories +├── gripper-control - Open and close the Panda gripper +└── fault-management - Collect and expose faults via SOVD API ``` ## REST API Examples @@ -223,8 +223,8 @@ curl http://localhost:8080/api/v1/apps/manipulation-monitor/faults/MOTION_PLANNI ``` Captured topics (background capture, always available): -- `/joint_states` — Current joint positions at fault time -- `/diagnostics` — Active diagnostics messages +- `/joint_states` - Current joint positions at fault time +- `/diagnostics` - Active diagnostics messages ### Modify Configurations via REST API @@ -284,6 +284,59 @@ 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. +## Triggers (Condition-Based Alerts) + +The gateway supports condition-based triggers that fire when specific events occur, delivering notifications via Server-Sent Events (SSE). This demo creates a fault-monitoring trigger that alerts on any new or updated faults reported by the manipulation monitor (including planning failures and collisions). + +### Setup + +```bash +# Terminal 1: Start the demo +./run-demo.sh + +# Terminal 2: Create the fault trigger +./setup-triggers.sh + +# Terminal 3: Watch for trigger events (blocking - Ctrl+C to stop) +./watch-triggers.sh + +# Terminal 2: Inject a fault - the trigger fires in Terminal 3! +./inject-planning-failure.sh +``` + +### How It Works + +1. `setup-triggers.sh` creates a trigger via `POST /api/v1/apps/manipulation_monitor/triggers`: + - **Resource:** `/api/v1/apps/manipulation_monitor/faults` (watches fault collection) + - **Condition:** `OnChange` (fires on any new or updated fault) + - **Multishot:** `true` (fires repeatedly, not just once) + - **Lifetime:** 3600 seconds (auto-expires after 1 hour) +2. `watch-triggers.sh` connects to the SSE event stream at the trigger's `event_source` URL +3. When a fault is injected and detected by the gateway, the trigger fires and an SSE event is delivered + +### Manual API Usage + +```bash +# Create a trigger +curl -X POST http://localhost:8080/api/v1/apps/manipulation_monitor/triggers \ + -H "Content-Type: application/json" \ + -d '{ + "resource": "/api/v1/apps/manipulation_monitor/faults", + "trigger_condition": {"condition_type": "OnChange"}, + "multishot": true, + "lifetime": 3600 + }' | jq + +# List triggers +curl http://localhost:8080/api/v1/apps/manipulation_monitor/triggers | jq + +# Watch events (replace TRIGGER_ID) +curl -N http://localhost:8080/api/v1/apps/manipulation_monitor/triggers/TRIGGER_ID/events + +# Delete a trigger +curl -X DELETE http://localhost:8080/api/v1/apps/manipulation_monitor/triggers/TRIGGER_ID +``` + ## 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. @@ -298,7 +351,7 @@ Blocks the robot's path with a large collision wall (visible as orange wall in G | Code | Severity | Description | |------|----------|-------------| -| `MOTION_PLANNING_FAILED` | ERROR | MoveGroup goal ABORTED — no collision-free path | +| `MOTION_PLANNING_FAILED` | ERROR | MoveGroup goal ABORTED - no collision-free path | ### 2. Collision Detection @@ -346,16 +399,18 @@ Connect it to the gateway at `http://localhost:8080` to browse: | Script | Description | |--------|-------------| -| `run-demo.sh` | **Start the demo** — build and launch the Docker container | +| `run-demo.sh` | **Start the demo** - build and launch the Docker container | | `stop-demo.sh` | Stop demo containers | -| `move-arm.sh` | **Interactive arm controller** — move to preset positions | +| `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` | 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 | +| `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 | +| `setup-triggers.sh` | Create OnChange fault trigger | +| `watch-triggers.sh` | Watch trigger events via SSE stream | Scripts API wrappers require `curl` and `jq` on the host and call the gateway REST endpoint directly - no `docker exec` needed. diff --git a/demos/moveit_pick_place/run-demo.sh b/demos/moveit_pick_place/run-demo.sh index ad7baca..6b7a0a5 100755 --- a/demos/moveit_pick_place/run-demo.sh +++ b/demos/moveit_pick_place/run-demo.sh @@ -166,5 +166,10 @@ if [[ "$DETACH_MODE" == "true" ]]; then echo " docker exec -it moveit_medkit_demo bash # CPU" echo " docker exec -it moveit_medkit_demo_nvidia bash # NVIDIA" echo "" + echo "📡 Triggers (condition-based alerts):" + echo " 1. ./setup-triggers.sh # Create fault alert trigger" + echo " 2. ./watch-triggers.sh # Watch events in a new terminal (blocking)" + echo " 3. ./inject-planning-failure.sh # Inject a fault to fire the trigger" + echo "" echo "🛑 To stop: ./stop-demo.sh" fi diff --git a/demos/moveit_pick_place/setup-triggers.sh b/demos/moveit_pick_place/setup-triggers.sh new file mode 100755 index 0000000..06ff53e --- /dev/null +++ b/demos/moveit_pick_place/setup-triggers.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Create fault-monitoring trigger for moveit pick-and-place demo +# Alerts on any fault change reported by the manipulation monitor +export ENTITY_TYPE="apps" +# Uses ROS node name (underscore) - must match reporting_sources in FaultEvent +export ENTITY_ID="manipulation_monitor" +export INJECT_HINT="./inject-planning-failure.sh" +# shellcheck disable=SC1091 +source "$(cd "$(dirname "$0")" && pwd)/../../lib/setup-trigger.sh" diff --git a/demos/moveit_pick_place/watch-triggers.sh b/demos/moveit_pick_place/watch-triggers.sh new file mode 100755 index 0000000..4cae6e7 --- /dev/null +++ b/demos/moveit_pick_place/watch-triggers.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Watch trigger events for moveit pick-and-place demo +# Connects to SSE stream and prints fault events in real time +export ENTITY_TYPE="apps" +export ENTITY_ID="manipulation_monitor" +# shellcheck disable=SC1091 +source "$(cd "$(dirname "$0")" && pwd)/../../lib/watch-trigger.sh" "$@" diff --git a/demos/sensor_diagnostics/README.md b/demos/sensor_diagnostics/README.md index c6db136..d89a033 100644 --- a/demos/sensor_diagnostics/README.md +++ b/demos/sensor_diagnostics/README.md @@ -184,6 +184,59 @@ GATEWAY_URL=http://192.168.1.10:8080 ./inject-nan.sh | `inject-noise` | Inject high noise on LiDAR and Camera | | `restore-normal` | Reset all sensors and clear faults | +## Triggers (Condition-Based Alerts) + +The gateway supports condition-based triggers that fire when specific events occur, delivering notifications via Server-Sent Events (SSE). This demo creates a fault-monitoring trigger that alerts whenever a new fault is reported. + +### Setup + +```bash +# Terminal 1: Start the demo +./run-demo.sh + +# Terminal 2: Create the fault trigger +./setup-triggers.sh + +# Terminal 3: Watch for trigger events (blocking - Ctrl+C to stop) +./watch-triggers.sh + +# Terminal 2: Inject a fault - the trigger fires in Terminal 3! +./inject-nan.sh +``` + +### How It Works + +1. `setup-triggers.sh` creates a trigger via `POST /api/v1/apps/diagnostic_bridge/triggers`: + - **Resource:** `/api/v1/apps/diagnostic_bridge/faults` (watches fault collection) + - **Condition:** `OnChange` (fires on any new or updated fault) + - **Multishot:** `true` (fires repeatedly, not just once) + - **Lifetime:** 3600 seconds (auto-expires after 1 hour) +2. `watch-triggers.sh` connects to the SSE event stream at the trigger's `event_source` URL +3. When a fault is injected and detected by the gateway, the trigger fires and an SSE event is delivered + +### Manual API Usage + +```bash +# Create a trigger +curl -X POST http://localhost:8080/api/v1/apps/diagnostic_bridge/triggers \ + -H "Content-Type: application/json" \ + -d '{ + "resource": "/api/v1/apps/diagnostic_bridge/faults", + "trigger_condition": {"condition_type": "OnChange"}, + "multishot": true, + "lifetime": 3600 + }' | jq + +# List triggers +curl http://localhost:8080/api/v1/apps/diagnostic_bridge/triggers | jq + +# Watch events (replace TRIGGER_ID) +curl -N http://localhost:8080/api/v1/apps/diagnostic_bridge/triggers/TRIGGER_ID/events + +# Delete a trigger +curl -X DELETE http://localhost:8080/api/v1/apps/diagnostic_bridge/triggers/TRIGGER_ID +``` + ## API Examples ### Read Sensor Data @@ -259,6 +312,8 @@ curl http://localhost:8080/api/v1/faults | jq | `inject-nan.sh` | Inject NaN values | | `inject-drift.sh` | Enable sensor drift | | `restore-normal.sh` | Clear all faults | +| `setup-triggers.sh` | Create OnChange fault trigger | +| `watch-triggers.sh` | Watch trigger events via SSE stream | > **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. diff --git a/demos/sensor_diagnostics/run-demo.sh b/demos/sensor_diagnostics/run-demo.sh index c6e9a30..1559a6a 100755 --- a/demos/sensor_diagnostics/run-demo.sh +++ b/demos/sensor_diagnostics/run-demo.sh @@ -126,6 +126,11 @@ if [[ "$DETACH_MODE" == "true" ]]; then echo " ./inject-drift.sh # Inject sensor drift" echo " ./restore-normal.sh # Restore normal operation" echo "" + echo "📡 Triggers (condition-based alerts):" + echo " 1. ./setup-triggers.sh # Create fault alert trigger" + echo " 2. ./watch-triggers.sh # Watch events in a new terminal (blocking)" + echo " 3. ./inject-nan.sh # Inject a fault to fire the trigger" + echo "" echo "🌐 Web UI: http://localhost:3000" echo "🌐 REST API: http://localhost:8080/api/v1/" echo "" diff --git a/demos/sensor_diagnostics/setup-triggers.sh b/demos/sensor_diagnostics/setup-triggers.sh new file mode 100755 index 0000000..f7dcbaf --- /dev/null +++ b/demos/sensor_diagnostics/setup-triggers.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Create fault-monitoring trigger for sensor diagnostics demo +# Alerts on any new fault reported via the diagnostic bridge +export ENTITY_TYPE="apps" +# Uses ROS node name (underscore) - must match reporting_sources in FaultEvent +export ENTITY_ID="diagnostic_bridge" +export INJECT_HINT="./inject-nan.sh" +# shellcheck disable=SC1091 +source "$(cd "$(dirname "$0")" && pwd)/../../lib/setup-trigger.sh" diff --git a/demos/sensor_diagnostics/watch-triggers.sh b/demos/sensor_diagnostics/watch-triggers.sh new file mode 100755 index 0000000..298f3b4 --- /dev/null +++ b/demos/sensor_diagnostics/watch-triggers.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Watch trigger events for sensor diagnostics demo +# Connects to SSE stream and prints fault events in real time +export ENTITY_TYPE="apps" +export ENTITY_ID="diagnostic_bridge" +# shellcheck disable=SC1091 +source "$(cd "$(dirname "$0")" && pwd)/../../lib/watch-trigger.sh" "$@" diff --git a/demos/turtlebot3_integration/README.md b/demos/turtlebot3_integration/README.md index 079e156..2487961 100644 --- a/demos/turtlebot3_integration/README.md +++ b/demos/turtlebot3_integration/README.md @@ -400,6 +400,59 @@ GATEWAY_URL=http://192.168.1.10:8080 ./inject-nav-failure.sh | `inject-nav-failure` | Inject navigation failure (unreachable goal) | | `restore-normal` | Reset parameters and clear faults | +## Triggers (Condition-Based Alerts) + +The gateway supports condition-based triggers that fire when specific events occur, delivering notifications via Server-Sent Events (SSE). This demo creates a fault-monitoring trigger that alerts on any new or updated faults reported by the anomaly detector (including navigation failures). + +### Setup + +```bash +# Terminal 1: Start the demo +./run-demo.sh + +# Terminal 2: Create the fault trigger +./setup-triggers.sh + +# Terminal 3: Watch for trigger events (blocking - Ctrl+C to stop) +./watch-triggers.sh + +# Terminal 2: Inject a fault - the trigger fires in Terminal 3! +./inject-nav-failure.sh +``` + +### How It Works + +1. `setup-triggers.sh` creates a trigger via `POST /api/v1/apps/anomaly_detector/triggers`: + - **Resource:** `/api/v1/apps/anomaly_detector/faults` (watches fault collection) + - **Condition:** `OnChange` (fires on any new or updated fault) + - **Multishot:** `true` (fires repeatedly, not just once) + - **Lifetime:** 3600 seconds (auto-expires after 1 hour) +2. `watch-triggers.sh` connects to the SSE event stream at the trigger's `event_source` URL +3. When a fault is injected and detected by the gateway, the trigger fires and an SSE event is delivered + +### Manual API Usage + +```bash +# Create a trigger +curl -X POST http://localhost:8080/api/v1/apps/anomaly_detector/triggers \ + -H "Content-Type: application/json" \ + -d '{ + "resource": "/api/v1/apps/anomaly_detector/faults", + "trigger_condition": {"condition_type": "OnChange"}, + "multishot": true, + "lifetime": 3600 + }' | jq + +# List triggers +curl http://localhost:8080/api/v1/apps/anomaly_detector/triggers | jq + +# Watch events (replace TRIGGER_ID) +curl -N http://localhost:8080/api/v1/apps/anomaly_detector/triggers/TRIGGER_ID/events + +# Delete a trigger +curl -X DELETE http://localhost:8080/api/v1/apps/anomaly_detector/triggers/TRIGGER_ID +``` + ## Fault Injection Scenarios This demo includes scripts to inject various fault conditions for testing fault management. @@ -543,6 +596,8 @@ demos/turtlebot3_integration/ | `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 | +| `setup-triggers.sh` | Create OnChange fault trigger | +| `watch-triggers.sh` | Watch trigger events via SSE stream | > **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. diff --git a/demos/turtlebot3_integration/run-demo.sh b/demos/turtlebot3_integration/run-demo.sh index 3af1a39..0b0dbf2 100755 --- a/demos/turtlebot3_integration/run-demo.sh +++ b/demos/turtlebot3_integration/run-demo.sh @@ -173,5 +173,10 @@ if [[ "$DETACH_MODE" == "true" ]]; then echo " docker exec -it turtlebot3_medkit_demo bash # CPU" echo " docker exec -it turtlebot3_medkit_demo_nvidia bash # NVIDIA" echo "" + echo "📡 Triggers (condition-based alerts):" + echo " 1. ./setup-triggers.sh # Create fault alert trigger" + echo " 2. ./watch-triggers.sh # Watch events in a new terminal (blocking)" + echo " 3. ./inject-nav-failure.sh # Inject a fault to fire the trigger" + echo "" echo "🛑 To stop: ./stop-demo.sh" fi diff --git a/demos/turtlebot3_integration/setup-triggers.sh b/demos/turtlebot3_integration/setup-triggers.sh new file mode 100755 index 0000000..8092c13 --- /dev/null +++ b/demos/turtlebot3_integration/setup-triggers.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Create fault-monitoring trigger for turtlebot3 integration demo +# Alerts on any fault change reported by the anomaly detector +export ENTITY_TYPE="apps" +# Uses ROS node name (underscore) - must match reporting_sources in FaultEvent +export ENTITY_ID="anomaly_detector" +export INJECT_HINT="./inject-nav-failure.sh" +# shellcheck disable=SC1091 +source "$(cd "$(dirname "$0")" && pwd)/../../lib/setup-trigger.sh" diff --git a/demos/turtlebot3_integration/watch-triggers.sh b/demos/turtlebot3_integration/watch-triggers.sh new file mode 100755 index 0000000..6f96330 --- /dev/null +++ b/demos/turtlebot3_integration/watch-triggers.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Watch trigger events for turtlebot3 integration demo +# Connects to SSE stream and prints fault events in real time +export ENTITY_TYPE="apps" +export ENTITY_ID="anomaly_detector" +# shellcheck disable=SC1091 +source "$(cd "$(dirname "$0")" && pwd)/../../lib/watch-trigger.sh" "$@" diff --git a/lib/setup-trigger.sh b/lib/setup-trigger.sh new file mode 100644 index 0000000..83c937a --- /dev/null +++ b/lib/setup-trigger.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Generic trigger setup - called by per-demo setup-triggers.sh wrappers +# Requires ENTITY_TYPE, ENTITY_ID, and INJECT_HINT to be set before sourcing +set -eu + +if [ -z "${ENTITY_TYPE:-}" ] || [ -z "${ENTITY_ID:-}" ]; then + echo "ENTITY_TYPE and ENTITY_ID must be set before sourcing this script." >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/triggers-api.sh" + +# Check for existing active trigger +existing=$(find_active_trigger "$ENTITY_TYPE" "$ENTITY_ID") +if [ -n "$existing" ]; then + echo "Active trigger already exists: ${existing}" + echo "Run ./watch-triggers.sh to connect, or delete it first:" + echo " curl -X DELETE ${GATEWAY_URL}/api/v1/${ENTITY_TYPE}/${ENTITY_ID}/triggers/${existing}" + exit 0 +fi + +echo "Setting up fault trigger for ${ENTITY_TYPE}/${ENTITY_ID}..." +echo "" + +result=$(create_fault_trigger "$ENTITY_TYPE" "$ENTITY_ID") +trigger_id=$(echo "$result" | jq -r '.id') + +if [ -z "$trigger_id" ] || [ "$trigger_id" = "null" ]; then + echo "Failed to parse trigger response." >&2 + echo "$result" >&2 + exit 1 +fi + +status=$(echo "$result" | jq -r '.status') +event_source=$(echo "$result" | jq -r '.event_source') + +echo "Trigger created successfully!" +echo " ID: ${trigger_id}" +echo " Status: ${status}" +echo " Events: ${GATEWAY_URL}${event_source}" +echo "" +echo "To watch for events:" +echo " ./watch-triggers.sh ${trigger_id}" +echo "" +echo "Then inject a fault in another terminal:" +echo " ${INJECT_HINT:-./inject-nan.sh}" diff --git a/lib/triggers-api.sh b/lib/triggers-api.sh new file mode 100644 index 0000000..0859d4e --- /dev/null +++ b/lib/triggers-api.sh @@ -0,0 +1,183 @@ +#!/bin/bash +# Shared helper for gateway Triggers API +# Source this from host-side trigger scripts: +# SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# source "${SCRIPT_DIR}/../../lib/triggers-api.sh" +# +# Environment variables: +# GATEWAY_URL - Gateway base URL (default: http://localhost:8080) + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +# 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 +} + +# Create an OnChange trigger on an entity's faults collection +# Usage: create_fault_trigger [lifetime] +# Returns: trigger JSON on stdout +create_fault_trigger() { + local entity_type="$1" + local entity_id="$2" + local lifetime="${3:-3600}" + + check_gateway + + local resource="/api/v1/${entity_type}/${entity_id}/faults" + local body + body=$(jq -n \ + --arg resource "$resource" \ + --argjson lifetime "$lifetime" \ + '{ + resource: $resource, + trigger_condition: {condition_type: "OnChange"}, + multishot: true, + lifetime: $lifetime + }') + + local result + local http_code + if ! result=$(curl -s -w "\n%{http_code}" -X POST \ + "${API_BASE}/${entity_type}/${entity_id}/triggers" \ + -H "Content-Type: application/json" \ + -d "$body" 2>/dev/null); then + echo "Failed to reach gateway at ${GATEWAY_URL}." >&2 + return 1 + fi + + http_code=$(echo "$result" | tail -1) + result=$(echo "$result" | sed '$d') + + if [ "$http_code" != "201" ]; then + echo "Failed to create trigger (HTTP $http_code):" >&2 + echo "$result" | jq '.' 2>/dev/null >&2 || echo "$result" >&2 + return 1 + fi + + echo "$result" +} + +# List triggers for an entity +# Usage: list_triggers +list_triggers() { + local entity_type="$1" + local entity_id="$2" + + check_gateway + + curl -sf "${API_BASE}/${entity_type}/${entity_id}/triggers" 2>/dev/null | jq '.' +} + +# Delete a trigger +# Usage: delete_trigger +delete_trigger() { + local entity_type="$1" + local entity_id="$2" + local trigger_id="$3" + + if [[ ! "$trigger_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then + echo "Invalid trigger ID: '${trigger_id}'" >&2 + return 1 + fi + + check_gateway + + local http_code + http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + "${API_BASE}/${entity_type}/${entity_id}/triggers/${trigger_id}" 2>/dev/null) || true + + if [ "$http_code" = "204" ]; then + echo "Trigger ${trigger_id} deleted." + else + echo "Failed to delete trigger ${trigger_id} (HTTP $http_code)." >&2 + return 1 + fi +} + +# Watch SSE events for a trigger (blocking - Ctrl+C to stop) +# Usage: watch_trigger_events +watch_trigger_events() { + local entity_type="$1" + local entity_id="$2" + local trigger_id="$3" + + if [[ ! "$trigger_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then + echo "Invalid trigger ID: '${trigger_id}'" >&2 + return 1 + fi + + check_gateway + + local url="${API_BASE}/${entity_type}/${entity_id}/triggers/${trigger_id}/events" + + echo "Listening for trigger events..." + echo " Entity: ${entity_type}/${entity_id}" + echo " Trigger: ${trigger_id}" + echo " SSE URL: ${url}" + echo "" + echo "Waiting for events (Ctrl+C to stop)..." + echo "---" + + # Stream SSE events, filter out keepalives, pretty-print JSON data lines + curl -sf -N --connect-timeout 10 "${url}" 2>/dev/null | while IFS= read -r line; do + # Skip empty lines and keepalive comments + if [ -z "$line" ] || [[ "$line" == ":"* ]]; then + continue + fi + # Parse SSE data lines + if [[ "$line" == data:* ]]; then + local data="${line#data:}" + # Trim leading space if present + data="${data# }" + local timestamp + timestamp=$(echo "$data" | jq -r '.timestamp // empty' 2>/dev/null) + if [ -n "$timestamp" ]; then + echo "[${timestamp}] Event received:" + echo "$data" | jq '.' 2>/dev/null || echo "$data" + echo "---" + else + echo "$data" | jq '.' 2>/dev/null || echo "$data" + echo "---" + fi + fi + done + + echo "" + echo "SSE stream closed. The trigger may have expired or the gateway restarted." + echo "Re-run ./setup-triggers.sh and ./watch-triggers.sh to reconnect." +} + +# Find the first active trigger for an entity +# Usage: find_active_trigger +# Returns: trigger_id on stdout, or empty string if none found +find_active_trigger() { + local entity_type="$1" + local entity_id="$2" + + check_gateway + + local result + result=$(curl -sf "${API_BASE}/${entity_type}/${entity_id}/triggers" 2>/dev/null) || true + + if [ -z "$result" ]; then + echo "" + return 0 + fi + + local tid + tid=$(echo "$result" | jq -r '.items[] | select(.status == "active") | .id' 2>/dev/null | head -1) || true + echo "${tid:-}" +} diff --git a/lib/watch-trigger.sh b/lib/watch-trigger.sh new file mode 100644 index 0000000..57dfc1a --- /dev/null +++ b/lib/watch-trigger.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Generic trigger watcher - called by per-demo watch-triggers.sh wrappers +# Requires ENTITY_TYPE and ENTITY_ID to be set before sourcing +set -eu + +if [ -z "${ENTITY_TYPE:-}" ] || [ -z "${ENTITY_ID:-}" ]; then + echo "ENTITY_TYPE and ENTITY_ID must be set before sourcing this script." >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/triggers-api.sh" + +trigger_id="${1:-}" + +if [ -z "$trigger_id" ]; then + # Auto-detect: find first active trigger + trigger_id=$(find_active_trigger "$ENTITY_TYPE" "$ENTITY_ID") + if [ -z "$trigger_id" ]; then + echo "No active triggers found for ${ENTITY_TYPE}/${ENTITY_ID}." + echo "Create one first: ./setup-triggers.sh" + exit 1 + fi + echo "Found active trigger: ${trigger_id}" + echo "" +fi + +watch_trigger_events "$ENTITY_TYPE" "$ENTITY_ID" "$trigger_id" diff --git a/tests/smoke_test.sh b/tests/smoke_test.sh index b4f61fa..1c991ce 100755 --- a/tests/smoke_test.sh +++ b/tests/smoke_test.sh @@ -114,6 +114,69 @@ if [ "$cleared" = false ]; then fail "LIDAR_SIM fault cleared after cleanup" "fault still present after 5s" fi +section "Triggers" + +# Create a trigger on diagnostic-bridge faults +echo " Creating OnChange fault trigger..." +TRIGGER_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}/apps/diagnostic-bridge/triggers" \ + -H "Content-Type: application/json" \ + -d '{"resource":"/api/v1/apps/diagnostic-bridge/faults","trigger_condition":{"condition_type":"OnChange"},"multishot":true,"lifetime":60}' 2>/dev/null) || true + +TRIGGER_HTTP=$(echo "$TRIGGER_RESPONSE" | tail -1) +TRIGGER_BODY=$(echo "$TRIGGER_RESPONSE" | sed '$d') + +if [ "$TRIGGER_HTTP" = "201" ]; then + pass "POST /apps/diagnostic-bridge/triggers returns 201" +else + fail "POST /apps/diagnostic-bridge/triggers returns 201" "got HTTP $TRIGGER_HTTP" +fi + +TRIGGER_ID=$(echo "$TRIGGER_BODY" | jq -r '.id') +if [ -n "$TRIGGER_ID" ] && [ "$TRIGGER_ID" != "null" ]; then + pass "trigger response contains valid id" +else + fail "trigger response contains valid id" "id is null or empty" +fi + +TRIGGER_STATUS=$(echo "$TRIGGER_BODY" | jq -r '.status') +if [ "$TRIGGER_STATUS" = "active" ]; then + pass "trigger status is 'active'" +else + fail "trigger status is 'active'" "got '$TRIGGER_STATUS'" +fi + +# List triggers - verify it appears +if api_get "/apps/diagnostic-bridge/triggers"; then + if echo "$RESPONSE" | jq -e --arg id "$TRIGGER_ID" '.items[] | select(.id == $id)' > /dev/null 2>&1; then + pass "GET /apps/diagnostic-bridge/triggers lists created trigger" + else + fail "GET /apps/diagnostic-bridge/triggers lists created trigger" "trigger $TRIGGER_ID not found" + fi +else + fail "GET /apps/diagnostic-bridge/triggers returns 200" "unexpected status code" +fi + +# Delete trigger +TRIGGER_DELETE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + "${API_BASE}/apps/diagnostic-bridge/triggers/${TRIGGER_ID}" 2>/dev/null) || true + +if [ "$TRIGGER_DELETE_STATUS" = "204" ]; then + pass "DELETE /apps/diagnostic-bridge/triggers/$TRIGGER_ID returns 204" +else + fail "DELETE /apps/diagnostic-bridge/triggers/$TRIGGER_ID returns 204" "got HTTP $TRIGGER_DELETE_STATUS" +fi + +# Verify trigger is gone +if api_get "/apps/diagnostic-bridge/triggers"; then + if ! echo "$RESPONSE" | jq -e --arg id "$TRIGGER_ID" '.items[] | select(.id == $id)' > /dev/null 2>&1; then + pass "trigger no longer listed after deletion" + else + fail "trigger no longer listed after deletion" "still found in list" + fi +else + fail "GET /apps/diagnostic-bridge/triggers returns 200 after delete" "unexpected status code" +fi + # --- Summary --- print_summary