diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2cb40d3..f38ab8b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,6 +39,11 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+ - name: Build Sensor Diagnostics demo image
+ run: |
+ cd demos/sensor_diagnostics
+ docker build -t sensor-diagnostics-demo:test -f Dockerfile .
+
- name: Build TurtleBot3 demo image
run: |
cd demos/turtlebot3_integration
diff --git a/README.md b/README.md
index 1e19c68..33c5e0e 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,19 @@ progressing toward more advanced use cases.
| Demo | Description | Status |
|------|-------------|--------|
-| [TurtleBot3 Integration](demos/turtlebot3_integration/) | Basic ros2_medkit integration with TurtleBot3 and Nav2 | š§ In Progress |
+| [Sensor Diagnostics](demos/sensor_diagnostics/) | Lightweight sensor diagnostics demo (no Gazebo required) | ā
Ready |
+| [TurtleBot3 Integration](demos/turtlebot3_integration/) | Full ros2_medkit integration with TurtleBot3 and Nav2 | š§ In Progress |
+
+### Quick Start (Sensor Diagnostics)
+
+The sensor diagnostics demo is the fastest way to try ros2_medkit:
+
+```bash
+cd demos/sensor_diagnostics
+docker compose up
+# Open http://localhost:3000 for the Web UI
+# Run ./run-demo.sh for an interactive walkthrough
+```
## Getting Started
diff --git a/demos/sensor_diagnostics/CMakeLists.txt b/demos/sensor_diagnostics/CMakeLists.txt
new file mode 100644
index 0000000..09144bc
--- /dev/null
+++ b/demos/sensor_diagnostics/CMakeLists.txt
@@ -0,0 +1,86 @@
+cmake_minimum_required(VERSION 3.8)
+project(sensor_diagnostics_demo)
+
+if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+# Find dependencies
+find_package(ament_cmake REQUIRED)
+find_package(rclcpp REQUIRED)
+find_package(std_msgs REQUIRED)
+find_package(sensor_msgs REQUIRED)
+find_package(geometry_msgs REQUIRED)
+find_package(diagnostic_msgs REQUIRED)
+find_package(rcl_interfaces REQUIRED)
+find_package(ros2_medkit_msgs REQUIRED)
+
+# LiDAR simulator node
+add_executable(lidar_sim_node src/lidar_sim_node.cpp)
+ament_target_dependencies(lidar_sim_node
+ rclcpp
+ sensor_msgs
+ diagnostic_msgs
+ rcl_interfaces
+)
+
+# IMU simulator node
+add_executable(imu_sim_node src/imu_sim_node.cpp)
+ament_target_dependencies(imu_sim_node
+ rclcpp
+ sensor_msgs
+ diagnostic_msgs
+)
+
+# GPS simulator node
+add_executable(gps_sim_node src/gps_sim_node.cpp)
+ament_target_dependencies(gps_sim_node
+ rclcpp
+ sensor_msgs
+ diagnostic_msgs
+)
+
+# Camera simulator node
+add_executable(camera_sim_node src/camera_sim_node.cpp)
+ament_target_dependencies(camera_sim_node
+ rclcpp
+ sensor_msgs
+ diagnostic_msgs
+ rcl_interfaces
+)
+
+# Anomaly detector node
+add_executable(anomaly_detector_node src/anomaly_detector_node.cpp)
+ament_target_dependencies(anomaly_detector_node
+ rclcpp
+ sensor_msgs
+ diagnostic_msgs
+ ros2_medkit_msgs
+)
+
+# Install executables
+install(TARGETS
+ lidar_sim_node
+ imu_sim_node
+ gps_sim_node
+ camera_sim_node
+ anomaly_detector_node
+ DESTINATION lib/${PROJECT_NAME}
+)
+
+# Install launch files
+install(DIRECTORY launch/
+ DESTINATION share/${PROJECT_NAME}/launch
+)
+
+# Install config files
+install(DIRECTORY config/
+ DESTINATION share/${PROJECT_NAME}/config
+)
+
+if(BUILD_TESTING)
+ find_package(ament_lint_auto REQUIRED)
+ ament_lint_auto_find_test_dependencies()
+endif()
+
+ament_package()
diff --git a/demos/sensor_diagnostics/Dockerfile b/demos/sensor_diagnostics/Dockerfile
new file mode 100644
index 0000000..4aab92b
--- /dev/null
+++ b/demos/sensor_diagnostics/Dockerfile
@@ -0,0 +1,63 @@
+# Sensor Diagnostics Demo
+# Lightweight ROS 2 image without Gazebo - fast build and startup
+# Image size: ~500MB vs ~4GB for TurtleBot3 demo
+# Startup time: ~5s vs ~60s
+
+FROM ros:jazzy-ros-base
+
+ENV DEBIAN_FRONTEND=noninteractive
+ENV ROS_DISTRO=jazzy
+ENV COLCON_WS=/root/demo_ws
+
+# Install minimal dependencies (no Gazebo, no simulation packages)
+RUN apt-get update && apt-get install -y \
+ ros-jazzy-ament-lint-auto \
+ ros-jazzy-ament-lint-common \
+ ros-jazzy-ament-cmake-gtest \
+ ros-jazzy-yaml-cpp-vendor \
+ ros-jazzy-example-interfaces \
+ python3-colcon-common-extensions \
+ python3-requests \
+ nlohmann-json3-dev \
+ libcpp-httplib-dev \
+ libsqlite3-dev \
+ git \
+ curl \
+ jq \
+ && rm -rf /var/lib/apt/lists/*
+
+# Clone ros2_medkit from GitHub (gateway + dependencies)
+WORKDIR ${COLCON_WS}/src
+RUN git clone --depth 1 --recurse-submodules https://github.com/selfpatch/ros2_medkit.git && \
+ mv ros2_medkit/src/ros2_medkit_gateway . && \
+ mv ros2_medkit/src/ros2_medkit_serialization . && \
+ mv ros2_medkit/src/ros2_medkit_msgs . && \
+ mv ros2_medkit/src/ros2_medkit_fault_manager . && \
+ mv ros2_medkit/src/ros2_medkit_fault_reporter . && \
+ mv ros2_medkit/src/ros2_medkit_diagnostic_bridge . && \
+ mv ros2_medkit/src/dynamic_message_introspection/dynmsg . && \
+ rm -rf ros2_medkit
+
+# Copy demo package
+COPY package.xml CMakeLists.txt ${COLCON_WS}/src/sensor_diagnostics_demo/
+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/
+
+# Build all packages (skip test dependencies that aren't in ros-base)
+WORKDIR ${COLCON_WS}
+RUN bash -c "source /opt/ros/jazzy/setup.bash && \
+ rosdep update && \
+ rosdep install --from-paths src --ignore-src -r -y \
+ --skip-keys='ament_cmake_clang_format ament_cmake_clang_tidy test_msgs example_interfaces sqlite3' && \
+ colcon build --symlink-install --cmake-args -DBUILD_TESTING=OFF"
+
+# Setup environment
+RUN echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc && \
+ echo "source ${COLCON_WS}/install/setup.bash" >> ~/.bashrc
+
+# Expose gateway port
+EXPOSE 8080
+
+# Default command: launch the demo
+CMD ["bash", "-c", "source /opt/ros/jazzy/setup.bash && source /root/demo_ws/install/setup.bash && ros2 launch sensor_diagnostics_demo demo.launch.py"]
diff --git a/demos/sensor_diagnostics/README.md b/demos/sensor_diagnostics/README.md
new file mode 100644
index 0000000..2e338eb
--- /dev/null
+++ b/demos/sensor_diagnostics/README.md
@@ -0,0 +1,257 @@
+# Sensor Diagnostics Demo
+
+Lightweight sensor diagnostics demo for **ros2_medkit** - no Gazebo required!
+
+This demo showcases ros2_medkit's data monitoring, configuration management, and fault detection using simulated sensor nodes with configurable fault injection.
+
+## Features
+
+- **Runs anywhere** - No Gazebo, no GPU, works in CI and GitHub Codespaces
+- **Fast startup** - Seconds vs minutes compared to TurtleBot3 demo
+- **Focus on diagnostics** - Pure ros2_medkit features without robot complexity
+- **Configurable faults** - Runtime fault injection via REST API
+- **Dual fault reporting** - Demonstrates both legacy (diagnostics) and modern (direct) paths
+
+## Quick Start
+
+### Using Docker (Recommended)
+
+```bash
+# Start the demo
+docker compose up
+
+# In another terminal, open the Web UI
+# Navigate to http://localhost:3000
+
+# Or run the demo script
+./run-demo.sh
+```
+
+### Building from Source
+
+```bash
+# In a ROS 2 workspace
+cd ~/ros2_ws/src
+git clone https://github.com/selfpatch/selfpatch_demos.git
+cd ..
+
+# Build
+colcon build --packages-select sensor_diagnostics_demo
+
+# Launch
+source install/setup.bash
+ros2 launch sensor_diagnostics_demo demo.launch.py
+```
+
+## Architecture
+
+```
+Sensor Diagnostics Demo
+āāā /sensors # Simulated sensor nodes
+ā āāā lidar_sim # 2D LiDAR (LaserScan) - Legacy path
+ā āāā camera_sim # RGB camera (Image) - Legacy path
+ā āāā imu_sim # 9-DOF IMU (Imu) - Modern path
+ā āāā gps_sim # GPS receiver (NavSatFix) - Modern path
+āāā /processing # Data processing
+ā āāā anomaly_detector # Fault detection (modern path)
+āāā /bridge # Diagnostic conversion
+ā āāā diagnostic_bridge # /diagnostics ā FaultManager (legacy path)
+āāā /diagnostics # Monitoring
+ āāā ros2_medkit_gateway # REST API gateway
+```
+
+## Fault Reporting Paths
+
+This demo demonstrates **two different fault reporting mechanisms** available in ros2_medkit:
+
+### Legacy Path (ROS 2 Diagnostics ā Diagnostic Bridge)
+
+Standard ROS 2 diagnostics pattern used by **LiDAR** and **Camera** sensors:
+
+```
+Sensor Node ā publishes DiagnosticArray ā /diagnostics topic
+ ā
+ diagnostic_bridge subscribes and converts
+ ā
+ FaultManager receives via ReportFault service
+```
+
+- Sensors publish `diagnostic_msgs/DiagnosticArray` to `/diagnostics`
+- `ros2_medkit_diagnostic_bridge` converts DiagnosticStatus levels to fault severities:
+ - `OK (0)` ā PASSED event (fault healing)
+ - `WARN (1)` ā WARNING severity fault
+ - `ERROR (2)` ā ERROR severity fault
+ - `STALE (3)` ā CRITICAL severity fault
+
+> **Note:** This demo's sensors use only `OK` and `ERROR` levels for clear fault demonstration. All non-OK conditions report as `ERROR` to ensure reliable fault detection through the diagnostic bridge.
+
+### Modern Path (Direct ReportFault Service)
+
+Direct ros2_medkit integration used by **IMU** and **GPS** sensors:
+
+```
+Sensor Topics ā anomaly_detector monitors
+ ā
+ Detects anomalies (NaN, timeout, out-of-range)
+ ā
+ FaultManager receives via ReportFault service
+```
+
+- `anomaly_detector` subscribes to sensor topics
+- Analyzes data for anomalies in real-time
+- Calls `/fault_manager/report_fault` service directly
+
+### Fault Reporting Summary
+
+| Sensor | Reporting Path | Fault Reporter | Fault Types |
+|--------|---------------|----------------|-------------|
+| **LiDAR** | Legacy (diagnostics) | diagnostic_bridge | NAN_VALUES, HIGH_NOISE, DRIFTING, TIMEOUT |
+| **Camera** | Legacy (diagnostics) | diagnostic_bridge | BLACK_FRAME, HIGH_NOISE, LOW_BRIGHTNESS, OVEREXPOSED, TIMEOUT |
+| **IMU** | Modern (direct) | anomaly_detector | SENSOR_NAN, SENSOR_OUT_OF_RANGE, RATE_DEGRADED, SENSOR_TIMEOUT |
+| **GPS** | Modern (direct) | anomaly_detector | SENSOR_NAN, NO_FIX, SENSOR_OUT_OF_RANGE, RATE_DEGRADED, SENSOR_TIMEOUT |
+
+### Inject Scripts and Fault Paths
+
+| Script | Target Sensor | Reporting Path | Fault Scenario |
+|--------|---------------|----------------|----------------|
+| `inject-nan.sh` | LiDAR, IMU, GPS | Both paths | NaN values in sensor data |
+| `inject-noise.sh` | LiDAR, Camera | Legacy | High noise levels |
+| `inject-drift.sh` | LiDAR | Legacy | Gradual sensor drift |
+| `inject-failure.sh` | IMU | Modern | Complete sensor timeout |
+| `restore-normal.sh` | All | Both | Clears all faults |
+
+## API Examples
+
+### Read Sensor Data
+
+```bash
+# Get LiDAR scan
+curl http://localhost:8080/api/v1/apps/lidar_sim/data/scan | jq '.ranges[:5]'
+
+# Get IMU data
+curl http://localhost:8080/api/v1/apps/imu_sim/data/imu | jq '.linear_acceleration'
+
+# Get GPS fix
+curl http://localhost:8080/api/v1/apps/gps_sim/data/fix | jq '{lat: .latitude, lon: .longitude}'
+```
+
+### View Configurations
+
+```bash
+# List all LiDAR configurations
+curl http://localhost:8080/api/v1/apps/lidar_sim/configurations | jq
+
+# Get specific parameter
+curl http://localhost:8080/api/v1/apps/lidar_sim/configurations/noise_stddev | jq
+```
+
+### Inject Faults
+
+```bash
+# Increase sensor noise
+curl -X PUT http://localhost:8080/api/v1/apps/lidar_sim/configurations/noise_stddev \
+ -H "Content-Type: application/json" \
+ -d '{"value": 0.5}'
+
+# Cause sensor timeout
+curl -X PUT http://localhost:8080/api/v1/apps/lidar_sim/configurations/failure_probability \
+ -H "Content-Type: application/json" \
+ -d '{"value": 1.0}'
+
+# Inject NaN values
+curl -X PUT http://localhost:8080/api/v1/apps/lidar_sim/configurations/inject_nan \
+ -H "Content-Type: application/json" \
+ -d '{"value": true}'
+```
+
+### Check Faults
+
+```bash
+# List detected faults
+curl http://localhost:8080/api/v1/faults | jq
+```
+
+## Configurable Fault Scenarios
+
+| Fault | Description | Parameter |
+|-------|-------------|-----------|
+| `SENSOR_TIMEOUT` | No messages published | `failure_probability: 1.0` |
+| `SENSOR_NAN` | Invalid readings | `inject_nan: true` |
+| `HIGH_NOISE` | Degraded accuracy | `noise_stddev: 0.5` |
+| `DRIFTING` | Gradual sensor drift | `drift_rate: 0.1` |
+| `BLACK_FRAME` | Camera black frames | `inject_black_frames: true` |
+
+## Scripts
+
+| Script | Description |
+|--------|-------------|
+| `run-demo.sh` | Interactive demo walkthrough |
+| `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 |
+
+## Sensor Parameters
+
+### LiDAR (`/sensors/lidar_sim`)
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `scan_rate` | double | 10.0 | Publishing rate (Hz) |
+| `range_min` | double | 0.12 | Minimum range (m) |
+| `range_max` | double | 3.5 | Maximum range (m) |
+| `noise_stddev` | double | 0.01 | Noise standard deviation (m) |
+| `failure_probability` | double | 0.0 | Probability of failure per cycle |
+| `inject_nan` | bool | false | Inject NaN values |
+| `drift_rate` | double | 0.0 | Range drift rate (m/s) |
+
+### IMU (`/sensors/imu_sim`)
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `rate` | double | 100.0 | Publishing rate (Hz) |
+| `accel_noise_stddev` | double | 0.01 | Acceleration noise (m/s²) |
+| `gyro_noise_stddev` | double | 0.001 | Gyroscope noise (rad/s) |
+| `drift_rate` | double | 0.0 | Orientation drift (rad/s) |
+
+### GPS (`/sensors/gps_sim`)
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `rate` | double | 1.0 | Publishing rate (Hz) |
+| `position_noise_stddev` | double | 2.0 | Position noise (m) |
+| `altitude_noise_stddev` | double | 5.0 | Altitude noise (m) |
+| `drift_rate` | double | 0.0 | Position drift (m/s) |
+
+### Camera (`/sensors/camera_sim`)
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `rate` | double | 30.0 | Publishing rate (Hz) |
+| `width` | int | 640 | Image width (pixels) |
+| `height` | int | 480 | Image height (pixels) |
+| `noise_level` | double | 0.0 | Fraction of noisy pixels (0-1) |
+| `brightness` | int | 128 | Base brightness (0-255) |
+| `inject_black_frames` | bool | false | Randomly inject black frames |
+
+## Use Cases
+
+1. **CI/CD Testing** - Validate ros2_medkit without heavy simulation
+2. **Tutorials** - Simple environment for learning
+3. **IoT Sensors** - Same patterns work for non-robot sensors
+4. **API Development** - Fast iteration on gateway features
+
+## Comparison with TurtleBot3 Demo
+
+| | Sensor Demo | TurtleBot3 Demo |
+|---|-------------|-----------------|
+| Docker image | ~500 MB | ~4 GB |
+| Startup time | ~5 seconds | ~60 seconds |
+| GPU required | No | Recommended |
+| CI compatible | Yes | Difficult |
+| Focus | Diagnostics | Navigation |
+
+## License
+
+Apache 2.0 - See [LICENSE](../../LICENSE)
diff --git a/demos/sensor_diagnostics/config/medkit_params.yaml b/demos/sensor_diagnostics/config/medkit_params.yaml
new file mode 100644
index 0000000..1539cf8
--- /dev/null
+++ b/demos/sensor_diagnostics/config/medkit_params.yaml
@@ -0,0 +1,24 @@
+# ros2_medkit gateway configuration for Sensor Diagnostics Demo
+# Gateway runs under /diagnostics namespace
+diagnostics:
+ ros2_medkit_gateway:
+ ros__parameters:
+ server:
+ host: "0.0.0.0"
+ port: 8080
+
+ refresh_interval_ms: 1000
+
+ cors:
+ allowed_origins: ["*"]
+ allowed_methods: ["GET", "PUT", "POST", "DELETE", "OPTIONS"]
+ allowed_headers: ["Content-Type", "Accept"]
+ allow_credentials: false
+ max_age_seconds: 86400
+
+ max_parallel_topic_samples: 10
+
+ # Manifest-based discovery
+ manifest:
+ enabled: true
+ path: "" # Will be set via launch argument
diff --git a/demos/sensor_diagnostics/config/sensor_manifest.yaml b/demos/sensor_diagnostics/config/sensor_manifest.yaml
new file mode 100644
index 0000000..a3cf0ed
--- /dev/null
+++ b/demos/sensor_diagnostics/config/sensor_manifest.yaml
@@ -0,0 +1,67 @@
+# SOVD Manifest for Sensor Diagnostics Demo
+# Defines the entity hierarchy for ros2_medkit gateway
+manifest_version: "1.0"
+
+metadata:
+ name: "sensor-diagnostics"
+ description: "Simulated sensors for diagnostics demo (no Gazebo required)"
+ version: "0.1.0"
+
+areas:
+ - id: sensors
+ name: "Sensors"
+ description: "Simulated sensor nodes"
+ namespace: /sensors
+
+ - id: processing
+ name: "Processing"
+ description: "Data processing and anomaly detection"
+ namespace: /processing
+
+ - id: diagnostics
+ name: "Diagnostics"
+ description: "ros2_medkit gateway and monitoring"
+ namespace: /diagnostics
+
+components:
+ - id: lidar-unit
+ name: "LiDAR Unit"
+ description: "Simulated 2D LiDAR sensor with configurable fault injection"
+ area: sensors
+ apps:
+ - lidar_sim
+
+ - id: imu-unit
+ name: "IMU Unit"
+ description: "Simulated 9-DOF IMU sensor"
+ area: sensors
+ apps:
+ - imu_sim
+
+ - id: gps-unit
+ name: "GPS Unit"
+ description: "Simulated GPS receiver"
+ area: sensors
+ apps:
+ - gps_sim
+
+ - id: camera-unit
+ name: "Camera Unit"
+ description: "Simulated RGB camera"
+ area: sensors
+ apps:
+ - camera_sim
+
+ - id: compute-unit
+ name: "Compute Unit"
+ description: "Data processing and anomaly detection"
+ area: processing
+ apps:
+ - anomaly_detector
+
+ - id: gateway
+ name: "SOVD Gateway"
+ description: "ros2_medkit REST API gateway"
+ area: diagnostics
+ apps:
+ - ros2_medkit_gateway
diff --git a/demos/sensor_diagnostics/config/sensor_params.yaml b/demos/sensor_diagnostics/config/sensor_params.yaml
new file mode 100644
index 0000000..a7a2c88
--- /dev/null
+++ b/demos/sensor_diagnostics/config/sensor_params.yaml
@@ -0,0 +1,55 @@
+# Default sensor parameters for the diagnostics demo
+# All sensors run in the /sensors namespace
+
+sensors:
+ lidar_sim:
+ ros__parameters:
+ scan_rate: 10.0 # Hz
+ range_min: 0.12 # meters
+ range_max: 3.5 # meters
+ angle_min: -3.14159 # radians
+ angle_max: 3.14159 # radians
+ num_readings: 360 # number of laser beams
+ noise_stddev: 0.01 # meters (normal sensor)
+ failure_probability: 0.0 # no failures by default
+ inject_nan: false
+ drift_rate: 0.0 # no drift by default
+
+ imu_sim:
+ ros__parameters:
+ rate: 100.0 # Hz
+ accel_noise_stddev: 0.01 # m/s^2
+ gyro_noise_stddev: 0.001 # rad/s
+ failure_probability: 0.0
+ inject_nan: false
+ drift_rate: 0.0 # rad/s
+
+ gps_sim:
+ ros__parameters:
+ rate: 1.0 # Hz (typical GPS rate)
+ base_latitude: 52.2297 # Warsaw, Poland
+ base_longitude: 21.0122
+ base_altitude: 100.0 # meters
+ position_noise_stddev: 2.0 # meters (typical GPS accuracy)
+ altitude_noise_stddev: 5.0 # meters
+ failure_probability: 0.0
+ inject_nan: false
+ drift_rate: 0.0 # meters/second
+
+ camera_sim:
+ ros__parameters:
+ rate: 30.0 # Hz
+ width: 640 # pixels
+ height: 480 # pixels
+ noise_level: 0.0 # 0.0 - 1.0
+ failure_probability: 0.0
+ inject_black_frames: false
+ brightness: 128 # 0-255
+
+processing:
+ anomaly_detector:
+ ros__parameters:
+ rate_timeout_sec: 5.0 # seconds before timeout fault
+ imu_rate_min: 50.0 # Hz (anomaly detector monitors IMU via modern path)
+ gps_rate_min: 0.5 # Hz (anomaly detector monitors GPS via modern path)
+ # Note: LiDAR and Camera use legacy path via diagnostic_bridge
diff --git a/demos/sensor_diagnostics/docker-compose.yml b/demos/sensor_diagnostics/docker-compose.yml
new file mode 100644
index 0000000..2bf4f8a
--- /dev/null
+++ b/demos/sensor_diagnostics/docker-compose.yml
@@ -0,0 +1,48 @@
+services:
+ # Main demo service - runs all sensor nodes + gateway
+ sensor-demo:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: sensor_diagnostics_demo
+ environment:
+ - ROS_DOMAIN_ID=40
+ ports:
+ - "8080:8080"
+ stdin_open: true
+ tty: true
+ # Default command launches the full demo
+ # Override with: docker compose run sensor-demo bash
+ command: >
+ bash -c "source /opt/ros/jazzy/setup.bash &&
+ source /root/demo_ws/install/setup.bash &&
+ ros2 launch sensor_diagnostics_demo demo.launch.py"
+
+ # Web UI for visualization (optional)
+ sovd-web-ui:
+ image: ghcr.io/selfpatch/sovd_web_ui:latest
+ container_name: sovd_web_ui
+ ports:
+ - "3000:80"
+ depends_on:
+ - sensor-demo
+
+ # For CI testing - headless mode with quick exit
+ sensor-demo-ci:
+ profiles: ["ci"]
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: sensor_diagnostics_demo_ci
+ environment:
+ - ROS_DOMAIN_ID=40
+ ports:
+ - "8080:8080"
+ command: >
+ bash -c "source /opt/ros/jazzy/setup.bash &&
+ source /root/demo_ws/install/setup.bash &&
+ ros2 launch sensor_diagnostics_demo demo.launch.py &
+ sleep 10 &&
+ curl -sf http://localhost:8080/api/v1/health &&
+ curl -sf http://localhost:8080/api/v1/apps | jq '.items[] | .id' &&
+ echo 'CI validation passed!'"
diff --git a/demos/sensor_diagnostics/inject-drift.sh b/demos/sensor_diagnostics/inject-drift.sh
new file mode 100755
index 0000000..4b19189
--- /dev/null
+++ b/demos/sensor_diagnostics/inject-drift.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# Inject sensor drift fault - demonstrates LEGACY fault reporting path
+# LiDAR drift ā DiagnosticArray ā /diagnostics ā diagnostic_bridge ā FaultManager
+
+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"
diff --git a/demos/sensor_diagnostics/inject-failure.sh b/demos/sensor_diagnostics/inject-failure.sh
new file mode 100755
index 0000000..8553aff
--- /dev/null
+++ b/demos/sensor_diagnostics/inject-failure.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# Inject sensor failure (timeout) fault - demonstrates MODERN fault reporting path
+# IMU sensor ā anomaly_detector ā FaultManager (via ReportFault service)
+
+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"
diff --git a/demos/sensor_diagnostics/inject-nan.sh b/demos/sensor_diagnostics/inject-nan.sh
new file mode 100755
index 0000000..f46ae2e
--- /dev/null
+++ b/demos/sensor_diagnostics/inject-nan.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Inject NaN values fault - demonstrates BOTH fault reporting paths
+
+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"
diff --git a/demos/sensor_diagnostics/inject-noise.sh b/demos/sensor_diagnostics/inject-noise.sh
new file mode 100755
index 0000000..52849af
--- /dev/null
+++ b/demos/sensor_diagnostics/inject-noise.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# Inject high noise fault - demonstrates LEGACY fault reporting path
+# LiDAR/Camera ā DiagnosticArray ā /diagnostics ā diagnostic_bridge ā FaultManager
+
+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"
diff --git a/demos/sensor_diagnostics/launch/demo.launch.py b/demos/sensor_diagnostics/launch/demo.launch.py
new file mode 100644
index 0000000..68b6d29
--- /dev/null
+++ b/demos/sensor_diagnostics/launch/demo.launch.py
@@ -0,0 +1,137 @@
+"""Launch Sensor Diagnostics Demo with ros2_medkit gateway.
+
+Lightweight demo without Gazebo - pure sensor simulation with fault injection.
+
+Demonstrates two fault reporting paths:
+1. Legacy path: Sensors ā /diagnostics topic ā diagnostic_bridge ā fault_manager
+ - Used by: LiDAR, Camera
+ - Standard ROS 2 diagnostics pattern
+
+2. Modern path: Sensors ā anomaly_detector ā ReportFault service ā fault_manager
+ - Used by: IMU, GPS
+ - Direct ros2_medkit fault reporting
+
+Namespace structure:
+ /sensors - Simulated sensor nodes (lidar, imu, gps, camera)
+ /processing - Anomaly detector
+ /diagnostics - ros2_medkit gateway
+ /bridge - Diagnostic bridge (legacy path)
+"""
+
+import os
+
+from ament_index_python.packages import get_package_share_directory
+from launch import LaunchDescription
+from launch.actions import DeclareLaunchArgument
+from launch.substitutions import LaunchConfiguration
+from launch_ros.actions import Node
+
+
+def generate_launch_description():
+ # Get package directory
+ pkg_dir = get_package_share_directory("sensor_diagnostics_demo")
+
+ # Config file paths
+ medkit_params_file = os.path.join(pkg_dir, "config", "medkit_params.yaml")
+ sensor_params_file = os.path.join(pkg_dir, "config", "sensor_params.yaml")
+ manifest_file = os.path.join(pkg_dir, "config", "sensor_manifest.yaml")
+
+ # Launch arguments
+ use_sim_time = LaunchConfiguration("use_sim_time", default="false")
+
+ return LaunchDescription(
+ [
+ # Declare launch arguments
+ DeclareLaunchArgument(
+ "use_sim_time",
+ default_value="false",
+ description="Use simulation time (set to true if using with Gazebo)",
+ ),
+ # ===== Sensor Nodes (under /sensors namespace) =====
+ # Legacy path sensors: publish DiagnosticArray to /diagnostics
+ Node(
+ package="sensor_diagnostics_demo",
+ executable="lidar_sim_node",
+ name="lidar_sim",
+ namespace="sensors",
+ output="screen",
+ parameters=[sensor_params_file, {"use_sim_time": use_sim_time}],
+ ),
+ Node(
+ package="sensor_diagnostics_demo",
+ executable="camera_sim_node",
+ name="camera_sim",
+ namespace="sensors",
+ output="screen",
+ parameters=[sensor_params_file, {"use_sim_time": use_sim_time}],
+ ),
+ # Modern path sensors: monitored by anomaly_detector ā ReportFault
+ Node(
+ package="sensor_diagnostics_demo",
+ executable="imu_sim_node",
+ name="imu_sim",
+ namespace="sensors",
+ output="screen",
+ parameters=[sensor_params_file, {"use_sim_time": use_sim_time}],
+ ),
+ Node(
+ package="sensor_diagnostics_demo",
+ executable="gps_sim_node",
+ name="gps_sim",
+ namespace="sensors",
+ output="screen",
+ parameters=[sensor_params_file, {"use_sim_time": use_sim_time}],
+ ),
+ # ===== Processing Nodes (under /processing namespace) =====
+ # Modern path: anomaly_detector monitors IMU/GPS and calls ReportFault
+ Node(
+ package="sensor_diagnostics_demo",
+ executable="anomaly_detector_node",
+ name="anomaly_detector",
+ namespace="processing",
+ output="screen",
+ parameters=[sensor_params_file, {"use_sim_time": use_sim_time}],
+ ),
+ # ===== Diagnostic Bridge (Legacy path) =====
+ # Bridges /diagnostics topic (DiagnosticArray) ā fault_manager
+ # Handles faults from: LiDAR, Camera
+ Node(
+ package="ros2_medkit_diagnostic_bridge",
+ executable="diagnostic_bridge_node",
+ name="diagnostic_bridge",
+ namespace="bridge",
+ output="screen",
+ parameters=[
+ {
+ "use_sim_time": use_sim_time,
+ "diagnostics_topic": "/diagnostics",
+ "auto_generate_codes": True,
+ }
+ ],
+ ),
+ # ===== ros2_medkit Gateway (under /diagnostics namespace) =====
+ Node(
+ package="ros2_medkit_gateway",
+ executable="gateway_node",
+ name="ros2_medkit_gateway",
+ namespace="diagnostics",
+ output="screen",
+ parameters=[
+ medkit_params_file,
+ {"use_sim_time": use_sim_time},
+ {"manifest.path": manifest_file},
+ ],
+ ),
+ # ===== Fault Manager (at root namespace) =====
+ # Services at /fault_manager/* (e.g., /fault_manager/report_fault)
+ # Both paths report here: diagnostic_bridge (legacy) and anomaly_detector (modern)
+ Node(
+ package="ros2_medkit_fault_manager",
+ executable="fault_manager_node",
+ name="fault_manager",
+ namespace="", # Root namespace so services are at /fault_manager/*
+ output="screen",
+ parameters=[{"use_sim_time": use_sim_time}],
+ ),
+ ]
+ )
diff --git a/demos/sensor_diagnostics/package.xml b/demos/sensor_diagnostics/package.xml
new file mode 100644
index 0000000..e6d8e11
--- /dev/null
+++ b/demos/sensor_diagnostics/package.xml
@@ -0,0 +1,32 @@
+
+
+
+ sensor_diagnostics_demo
+ 0.1.0
+ Lightweight sensor diagnostics demo for ros2_medkit (no Gazebo required)
+ Demo Maintainer
+ Apache-2.0
+
+ ament_cmake
+
+ rclcpp
+ std_msgs
+ sensor_msgs
+ geometry_msgs
+ diagnostic_msgs
+ rcl_interfaces
+ ros2_medkit_msgs
+
+ ros2launch
+ ros2_medkit_gateway
+ ros2_medkit_diagnostic_bridge
+ ros2_medkit_fault_manager
+
+ ament_lint_auto
+ ament_lint_common
+ ament_cmake_gtest
+
+
+ ament_cmake
+
+
diff --git a/demos/sensor_diagnostics/restore-normal.sh b/demos/sensor_diagnostics/restore-normal.sh
new file mode 100755
index 0000000..075aacd
--- /dev/null
+++ b/demos/sensor_diagnostics/restore-normal.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+# Restore normal sensor operation (clear all faults)
+
+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"
diff --git a/demos/sensor_diagnostics/run-demo.sh b/demos/sensor_diagnostics/run-demo.sh
new file mode 100755
index 0000000..c10db28
--- /dev/null
+++ b/demos/sensor_diagnostics/run-demo.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+# Sensor Diagnostics Demo - Interactive Demo Script
+# Run this script to see ros2_medkit in action with simulated sensors
+
+set -e
+
+GATEWAY_URL="${GATEWAY_URL:-http://localhost:8080}"
+API_BASE="${GATEWAY_URL}/api/v1"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+echo_step() {
+ echo -e "\n${BLUE}=== $1 ===${NC}\n"
+}
+
+echo_success() {
+ echo -e "${GREEN}ā $1${NC}"
+}
+
+echo_warning() {
+ echo -e "${YELLOW}ā $1${NC}"
+}
+
+echo_error() {
+ echo -e "${RED}ā $1${NC}"
+}
+
+wait_for_gateway() {
+ echo "Waiting for gateway to be ready..."
+ for _ in {1..30}; do
+ if curl -sf "${API_BASE}/health" > /dev/null 2>&1; then
+ echo_success "Gateway is ready!"
+ return 0
+ fi
+ sleep 1
+ done
+ echo_error "Gateway not available at ${GATEWAY_URL}"
+ exit 1
+}
+
+# Main demo flow
+echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+echo "ā Sensor Diagnostics Demo with ros2_medkit ā"
+echo "ā (Lightweight - No Gazebo Required) ā"
+echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+
+echo_step "1. Checking Gateway Health"
+wait_for_gateway
+curl -s "${API_BASE}/health" | jq '.'
+
+echo_step "2. Listing All Areas (Namespaces)"
+curl -s "${API_BASE}/areas" | jq '.'
+
+echo_step "3. Listing All Components"
+curl -s "${API_BASE}/components" | jq '.items[] | {id: .id, name: .name, area: .area}'
+
+echo_step "4. Listing All Apps (ROS 2 Nodes)"
+curl -s "${API_BASE}/apps" | jq '.items[] | {id: .id, name: .name, namespace: .namespace}'
+
+echo_step "5. Reading LiDAR Data"
+echo "Getting latest scan from LiDAR simulator..."
+curl -s "${API_BASE}/apps/lidar_sim/data/scan" | jq '{
+ angle_min: .angle_min,
+ angle_max: .angle_max,
+ range_min: .range_min,
+ range_max: .range_max,
+ sample_ranges: .ranges[:5]
+}'
+
+echo_step "6. Reading IMU Data"
+echo "Getting latest IMU reading..."
+curl -s "${API_BASE}/apps/imu_sim/data/imu" | jq '{
+ linear_acceleration: .linear_acceleration,
+ angular_velocity: .angular_velocity
+}'
+
+echo_step "7. Reading GPS Fix"
+echo "Getting current GPS position..."
+curl -s "${API_BASE}/apps/gps_sim/data/fix" | jq '{
+ latitude: .latitude,
+ longitude: .longitude,
+ altitude: .altitude,
+ status: .status
+}'
+
+echo_step "8. Listing LiDAR Configurations"
+echo "These parameters can be modified at runtime to inject faults..."
+curl -s "${API_BASE}/apps/lidar_sim/configurations" | jq '.items[] | {name: .name, value: .value, type: .type}'
+
+echo_step "9. Checking Current Faults"
+curl -s "${API_BASE}/faults" | jq '.'
+
+echo ""
+echo_success "Demo complete!"
+echo ""
+echo "Try injecting faults with these scripts:"
+echo " ./inject-noise.sh - Increase sensor noise"
+echo " ./inject-failure.sh - Cause sensor timeouts"
+echo " ./inject-nan.sh - Inject NaN values"
+echo " ./restore-normal.sh - Restore normal operation"
+echo ""
+echo "Or use the Web UI at http://localhost:3000"
diff --git a/demos/sensor_diagnostics/src/anomaly_detector_node.cpp b/demos/sensor_diagnostics/src/anomaly_detector_node.cpp
new file mode 100644
index 0000000..7907299
--- /dev/null
+++ b/demos/sensor_diagnostics/src/anomaly_detector_node.cpp
@@ -0,0 +1,274 @@
+// Copyright 2026 selfpatch
+// SPDX-License-Identifier: Apache-2.0
+
+/// @file anomaly_detector_node.cpp
+/// @brief Monitors IMU and GPS sensors and reports detected anomalies as faults
+///
+/// This node implements the MODERN fault reporting path:
+/// - Subscribes to IMU and GPS topics
+/// - Detects anomalies (NaN values, out-of-range, timeouts)
+/// - Reports faults directly to FaultManager via ReportFault service
+///
+/// Note: LiDAR and Camera use the LEGACY path (diagnostics ā diagnostic_bridge ā FaultManager)
+
+#include
+#include
+#include