This demo shows how to integrate ros2_medkit with TurtleBot3 and Nav2 navigation stack to provide SOVD-compliant diagnostics, fault management, and control for a mobile robot system via REST API.
✅ Demo Ready - Full navigation demo with Web UI and fault management
This demo demonstrates:
- Launching TurtleBot3 simulation in Gazebo with turtlebot3_world
- Running Nav2 navigation stack (AMCL, planner, controller)
- Running ros2_medkit gateway with manifest-based discovery
- Fault management via diagnostic_bridge (legacy /diagnostics support)
- Rosbag snapshot capture when faults are confirmed (MCAP format)
- Querying robot data via REST API
- Entity hierarchy: Areas → Components → Apps → Functions
- Controlling the robot via ros2_medkit_web_ui
- Docker and docker-compose
- X11 display server (Linux with GUI, or XQuartz on macOS)
- (Optional) NVIDIA GPU + nvidia-container-toolkit
curlandjq(required for host-side scripts)
cd demos/turtlebot3_integration
./run-demo.shThat's it! The script will:
- Build the Docker images (first run takes ~5-10 min, downloads ~4GB)
- Setup X11 forwarding for Gazebo GUI
- Launch TurtleBot3 simulation + Nav2 + ros2_medkit gateway in daemon mode (background)
- Launch ros2_medkit_web_ui at http://localhost:3000
Note: By default, the demo runs in daemon mode with Gazebo GUI enabled. This allows you to interact with ROS 2 while the demo is running.
./run-demo.sh # Daemon mode with GUI (default)
./run-demo.sh --attached # Foreground mode with live logs
./run-demo.sh --headless # Daemon mode without GUI (headless)
./run-demo.sh --nvidia # Use NVIDIA GPU acceleration
./run-demo.sh --update # Pull latest images before running
./run-demo.sh --no-cache # Rebuild without cacheSince the demo runs in daemon mode by default, you can:
# View live logs
docker compose --profile cpu logs -f
# Enter the container to run ROS 2 commands
docker exec -it turtlebot3_medkit_demo bash
# Inside the container:
ros2 node list
ros2 topic list
ros2 topic echo /odom./stop-demo.sh # Stop containers
./stop-demo.sh --images # Stop and remove imagesNote: Fault data and rosbag recordings are ephemeral — they are stored inside the container and cleared on restart.
The Web UI is automatically started by docker-compose and available at http://localhost:3000.
Connect to the gateway using http://localhost:8080/api/v1 in the connection dialog.
For hardware-accelerated Gazebo rendering with NVIDIA GPU:
./run-demo.sh --nvidiaRequirements:
- NVIDIA GPU with recent drivers
- nvidia-container-toolkit installed
You can also use Docker Compose directly:
# CPU version (default profile)
docker compose --profile cpu up -d
# NVIDIA version
docker compose --profile nvidia up -d
# View logs
docker compose --profile cpu logs -f
docker compose --profile nvidia logs -f-
Connect to the gateway in ros2_medkit_web_ui
-
Find entity with
/cmd_veldata -
Enter velocity command JSON (or use form with fields from schema):
{"linear": {"x": 0.2}, "angular": {"z": 0.0}} -
Click "Send" - the robot will move!
# Send velocity command using Apps data endpoint (moves robot forward)
curl -X PUT http://localhost:8080/api/v1/apps/turtlebot3-node/data/cmd_vel \
-H "Content-Type: application/json" \
-d '{"linear": {"x": 0.2, "y": 0.0, "z": 0.0}, "angular": {"x": 0.0, "y": 0.0, "z": 0.0}}'
# Stop the robot
curl -X PUT http://localhost:8080/api/v1/apps/turtlebot3-node/data/cmd_vel \
-H "Content-Type: application/json" \
-d '{"linear": {"x": 0.0}, "angular": {"z": 0.0}}'First, enter the running container:
# Enter the container
docker exec -it turtlebot3_medkit_demo bash
# Inside the container:
# Send navigation goal
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: 'map'}, pose: {position: {x: 2.0, y: 0.5, z: 0.0}, orientation: {w: 1.0}}}}"
# Manual teleop
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.2}, angular: {z: 0.0}}" --once
# Exit container
exit# Check gateway health
curl http://localhost:8080/api/v1/health
# List discovered areas (namespace groupings)
curl http://localhost:8080/api/v1/areas | jq '.items[] | {id, name}'
# List all components (hardware/logical units)
curl http://localhost:8080/api/v1/components | jq '.items[] | {id, name, area}'
# List all apps (ROS 2 nodes)
curl http://localhost:8080/api/v1/apps | jq '.items[] | {id, name, namespace}'
# Get specific app details
curl http://localhost:8080/api/v1/apps/amcl | jq# Get LiDAR scan data
curl http://localhost:8080/api/v1/apps/turtlebot3-node/data/scan | jq '{
angle_min: .angle_min,
angle_max: .angle_max,
sample_ranges: .ranges[:5]
}'
# Get odometry data
curl http://localhost:8080/api/v1/apps/turtlebot3-node/data/odom | jq '{
position: .pose.pose.position,
orientation: .pose.pose.orientation
}'
# List all data topics for an app
curl http://localhost:8080/api/v1/apps/turtlebot3-node/data | jq# List all active faults
curl http://localhost:8080/api/v1/faults | jq
# Get faults for a specific area
curl http://localhost:8080/api/v1/areas/robot/faults | jq
# Get fault details with environment data (includes snapshots)
curl http://localhost:8080/api/v1/faults/NAVIGATION_GOAL_ABORTED | jq
# Clear a specific fault
curl -X DELETE http://localhost:8080/api/v1/apps/diagnostic-bridge/faults/TURTLEBOT3_NODEWhen a fault is confirmed, the FaultManager automatically captures:
- Freeze frame snapshots: Latest messages from key topics (odometry, pose, scan)
- Rosbag recording: 10 seconds before + 2 seconds after fault confirmation
# List bulk-data categories for an entity
curl http://localhost:8080/api/v1/apps/{entity_id}/bulk-data | jq
# List rosbag files available for download
curl http://localhost:8080/api/v1/apps/{entity_id}/bulk-data/rosbags | jq
# Download a rosbag file (returns MCAP format)
curl -O http://localhost:8080/api/v1/apps/{entity_id}/bulk-data/rosbags/{fault_code}
# Get fault detail with snapshots (freeze frames)
curl http://localhost:8080/api/v1/apps/{entity_id}/faults/{fault_code} | jqRecorded Topics:
/odom,/amcl_pose,/scan- Robot state/cmd_vel- Velocity commands/tf,/tf_static- Transforms/navigate_to_pose/_action/status,/navigate_to_pose/_action/feedback- Navigation state/local_costmap/costmap,/global_costmap/costmap- Costmaps/plan- Navigation plan/diagnostics- System diagnostics
# List available operations for an app
curl http://localhost:8080/api/v1/apps/amcl/operations | jq
# Execute an operation (service call)
curl -X POST http://localhost:8080/api/v1/apps/amcl/operations/reinitialize_global_localization/executions \
-H "Content-Type: application/json" \
-d '{}'# List node parameters
curl http://localhost:8080/api/v1/apps/amcl/configurations | jq
# Get a specific parameter
curl http://localhost:8080/api/v1/apps/amcl/configurations/max_particles | jq
# Update a parameter
curl -X PUT http://localhost:8080/api/v1/apps/amcl/configurations/max_particles \
-H "Content-Type: application/json" \
-d '{"value": 3000}'When TurtleBot3 simulation starts with Nav2, ros2_medkit will discover nodes organized into the SOVD entity hierarchy defined by the manifest:
TurtleBot3 Demo (manifest-based discovery)
├── Areas (namespace groupings)
│ ├── robot → TurtleBot3 hardware
│ ├── navigation → Nav2 stack
│ ├── diagnostics → ros2_medkit gateway
│ └── bridge → Diagnostic bridge
├── Components (hardware/logical units)
│ ├── turtlebot3-base → Robot platform (area: robot)
│ ├── lidar-sensor → LiDAR scanner (area: robot)
│ ├── nav2-stack → Navigation (area: navigation)
│ ├── gateway → REST API (area: diagnostics)
│ ├── fault-manager → Fault aggregation (area: diagnostics)
│ └── diagnostic-bridge-unit → Legacy support (area: bridge)
├── Apps (ROS 2 nodes)
│ ├── turtlebot3-node → component: turtlebot3-base
│ ├── robot-state-publisher → component: turtlebot3-base
│ ├── amcl → component: nav2-stack
│ ├── bt-navigator → component: nav2-stack
│ ├── controller-server → component: nav2-stack
│ ├── planner-server → component: nav2-stack
│ ├── medkit-gateway → component: gateway
│ ├── medkit-fault-manager → component: fault-manager
│ ├── diagnostic-bridge → component: diagnostic-bridge-unit
│ └── anomaly-detector → component: diagnostic-bridge-unit
└── Functions (high-level capabilities)
├── autonomous-navigation → hosted by: amcl, bt-navigator, ...
├── robot-control → hosted by: turtlebot3-node, velocity-smoother
└── fault-management → hosted by: gateway, fault-manager, bridge, anomaly-detector
This demo uses two fault reporting paths:
-
Direct Fault Reporting via
anomaly_detector:- Monitors navigation goal status, AMCL covariance, and robot progress
- Reports faults directly to FaultManager via
/fault_manager/report_faultservice
-
Legacy Path via
diagnostic_bridge:- Subscribes to
/diagnosticstopic (DiagnosticArray) - Converts diagnostics to fault reports
- Subscribes to
┌─────────────────────────────────────────────────────────────────────┐
│ Fault Reporting Paths │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ anomaly_detector ─────┬──── /fault_manager/report_fault ──────┐ │
│ (monitors: │ (direct service call) │ │
│ - nav goal status) │ │ │
│ - AMCL covariance) │ ▼ │
│ - robot progress) │ ┌──────────────┐
│ │ │ FaultManager │
│ │ └──────┬───────┘
│ Nav2/TurtleBot3 ──────┼──── /diagnostics topic ──────┐ │ │
│ (DiagnosticArray) │ ▼ │ │
│ │ diagnostic_bridge │ │
│ └───────────────────────────────┘ │ │
│ ▼ │
│ GET /api/v1/faults │
└─────────────────────────────────────────────────────────────────────┘
| Source | Fault Reporter | Example Faults |
|---|---|---|
| Navigation Goals | anomaly_detector | NAVIGATION_GOAL_ABORTED, NAVIGATION_GOAL_CANCELED |
| Localization | anomaly_detector | LOCALIZATION_UNCERTAINTY |
| Robot Progress | anomaly_detector | NAVIGATION_NO_PROGRESS |
| AMCL | diagnostic_bridge | Localization degraded |
| Nav2 Controller | diagnostic_bridge | Path following errors |
| TurtleBot3 | diagnostic_bridge | Motor/sensor issues |
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
curlandjq.
curl http://localhost:8080/api/v1/components/nav2-stack/scripts | jqcurl -X POST http://localhost:8080/api/v1/components/nav2-stack/scripts/inject-nav-failure/executions \
-H "Content-Type: application/json" \
-d '{"execution_type":"now"}' | jqcurl http://localhost:8080/api/v1/components/nav2-stack/scripts/inject-nav-failure/executions/<exec_id> | jq# Point scripts at a non-default gateway
GATEWAY_URL=http://192.168.1.10:8080 ./inject-nav-failure.sh| 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 |
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).
# 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.shsetup-triggers.shcreates a trigger viaPOST /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)
- Resource:
watch-triggers.shconnects to the SSE event stream at the trigger'sevent_sourceURL- When a fault is injected and detected by the gateway, the trigger fires and an SSE event is delivered
# 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_IDThis demo includes scripts to inject various fault conditions for testing fault management.
Faults are detected by anomaly_detector and reported directly to FaultManager.
| Script | Fault Type | Description | Expected Faults |
|---|---|---|---|
inject-nav-failure.sh |
Navigation | Send goal to unreachable location | NAVIGATION_GOAL_ABORTED |
inject-localization-failure.sh |
Localization | Reset AMCL with high uncertainty | LOCALIZATION_UNCERTAINTY |
restore-normal.sh |
Recovery | Restore defaults and clear faults | - |
# Send robot to unreachable goal (outside map bounds)
./inject-nav-failure.sh
# Check resulting faults
curl http://localhost:8080/api/v1/faults | jq '.items[] | {fault_code, severity_label, description}'# Reinitialize AMCL global localization (causes high uncertainty)
./inject-localization-failure.sh
# Watch for LOCALIZATION_UNCERTAINTY fault
curl http://localhost:8080/api/v1/faults | jq# Clear all faults and restore default parameters
./restore-normal.sh# List all active faults
curl http://localhost:8080/api/v1/faults | jq
# Get faults for navigation area
curl http://localhost:8080/api/v1/areas/navigation/faults | jq
# Clear specific fault
curl -X DELETE http://localhost:8080/api/v1/apps/{entity_id}/faults/{fault_code}
# Clear all faults
curl -X DELETE http://localhost:8080/api/v1/faults┌──────────────────────────────────────────────────────────────────────┐
│ Docker Container │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Gazebo Simulation │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌────────┐ ┌───────────┐ │ │
│ │ │ TurtleBot3 │ │ robot_state │ │ LIDAR │ │ Nav2 │ │ │
│ │ │ Node │ │ publisher │ │ sensor │ │ (AMCL, │ │ │
│ │ └──────┬──────┘ └──────┬───────┘ └───┬────┘ │ Planner, │ │ │
│ │ │ │ │ │ Controller│ │ │
│ │ └────────────────┼──────────────┘ └─────┬─────┘ │ │
│ │ ROS 2 Topics ◄────────────────────┘ │ │
│ └──────────────────────────┼────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ /diagnostics │ │ fault_ │ │ ros2_medkit │ │
│ │ topic │ │ manager │ │ Gateway │ │
│ └───────┬────────┘ └──────▲──────┘ │ (REST :8080) │ │
│ │ │ └────────┬─────────┘ │
│ ▼ │ │ │
│ ┌────────────────┐ │ │ │
│ │ diagnostic_ ├─────────┘ │ │
│ │ bridge │ │ │
│ └────────────────┘ │ │
└────────────────────────────────────────────────┼─────────────────────┘
│
HTTP REST API
│
┌───────────────────────────────────────┼────────────────────┐
▼ ▼ ▼ ▼
ros2_medkit_web_ui curl/browser External tools MCP Server
(localhost:3000) (ros2_medkit_mcp)
demos/turtlebot3_integration/
├── Dockerfile # ROS 2 Jazzy + TurtleBot3 + Nav2 + ros2_medkit
├── docker-compose.yml # Docker Compose (CPU & GPU via profiles)
├── run-demo.sh # One-click demo launcher
├── stop-demo.sh # Stop and cleanup demo
├── send-nav-goal.sh # Send navigation goal via SOVD API
├── check-entities.sh # Explore SOVD entity hierarchy
├── check-faults.sh # View active faults
├── inject-nav-failure.sh # Inject navigation failure scenario
├── inject-localization-failure.sh # Inject AMCL localization issues
├── restore-normal.sh # Restore normal operation
├── config/
│ ├── medkit_params.yaml # ros2_medkit gateway config
│ ├── 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/
└── anomaly_detector.py # Navigation anomaly detector node
| Script | Description |
|---|---|
run-demo.sh |
Start the full demo (Docker) |
stop-demo.sh |
Stop containers and cleanup |
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 |
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 - callable as REST endpoints without requiring the host-side scripts.
If you prefer not to use Docker:
-
Install ROS 2 Jazzy on Ubuntu 24.04
-
Install TurtleBot3:
sudo apt install ros-jazzy-turtlebot3-gazebo -
Install Nav2:
sudo apt install ros-jazzy-nav2-bringup -
Build ros2_medkit from source
-
Set environment:
export TURTLEBOT3_MODEL=burger export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:/opt/ros/jazzy/share/turtlebot3_gazebo/models
-
Run:
ros2 launch launch/demo.launch.py
- Ensure X11 forwarding is set up:
xhost +local:docker - Check DISPLAY environment variable
- Wait for AMCL to localize (give an initial pose in RViz or via CLI)
- Check lifecycle states with
ros2 lifecycle list
- Make sure Nav2's velocity smoother isn't overriding commands
- Check if collision monitor is blocking movement