Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions demos/sensor_diagnostics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,20 @@ find_package(diagnostic_msgs REQUIRED)
find_package(rcl_interfaces REQUIRED)
find_package(ros2_medkit_msgs REQUIRED)

# Beacon helper library (shared across sensor nodes)
add_library(beacon_helper STATIC src/beacon_helper.cpp)
ament_target_dependencies(beacon_helper PUBLIC
rclcpp
diagnostic_msgs
ros2_medkit_msgs
)
target_include_directories(beacon_helper PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
)

# LiDAR simulator node
add_executable(lidar_sim_node src/lidar_sim_node.cpp)
target_link_libraries(lidar_sim_node beacon_helper)
ament_target_dependencies(lidar_sim_node
rclcpp
sensor_msgs
Expand All @@ -26,6 +38,7 @@ ament_target_dependencies(lidar_sim_node

# IMU simulator node
add_executable(imu_sim_node src/imu_sim_node.cpp)
target_link_libraries(imu_sim_node beacon_helper)
ament_target_dependencies(imu_sim_node
rclcpp
sensor_msgs
Expand All @@ -34,6 +47,7 @@ ament_target_dependencies(imu_sim_node

# GPS simulator node
add_executable(gps_sim_node src/gps_sim_node.cpp)
target_link_libraries(gps_sim_node beacon_helper)
ament_target_dependencies(gps_sim_node
rclcpp
sensor_msgs
Expand All @@ -42,6 +56,7 @@ ament_target_dependencies(gps_sim_node

# Camera simulator node
add_executable(camera_sim_node src/camera_sim_node.cpp)
target_link_libraries(camera_sim_node beacon_helper)
ament_target_dependencies(camera_sim_node
rclcpp
sensor_msgs
Expand Down
53 changes: 53 additions & 0 deletions demos/sensor_diagnostics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This demo showcases ros2_medkit's data monitoring, configuration management, and
- **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
- **Beacon discovery** - Optional push (topic) or pull (parameter) entity enrichment

## Quick Start

Expand Down Expand Up @@ -255,6 +256,58 @@ curl http://localhost:8080/api/v1/faults | jq
| `brightness` | int | 128 | Base brightness (0-255) |
| `inject_black_frames` | bool | false | Randomly inject black frames |

## Beacon Mode (Entity Enrichment)

The gateway's beacon plugins let sensor nodes publish extra metadata (display names, process info, topology hints) that enriches the SOVD entity model at runtime - without modifying the manifest.

Three modes are available, controlled by the `BEACON_MODE` environment variable:

| Mode | Plugin | Mechanism | Description |
|------|--------|-----------|-------------|
| `none` | - | - | Default. No beacon plugins. Entities come from manifest + runtime discovery only. |
| `topic` | topic_beacon | Push (ROS 2 topic) | Sensor nodes publish `MedkitDiscoveryHint` messages on `/ros2_medkit/discovery` every 5s. Gateway subscribes and enriches entities. |
| `param` | parameter_beacon | Pull (ROS 2 parameters) | Sensor nodes declare `ros2_medkit.discovery.*` parameters. Gateway polls them every 5s. |

### Usage

```bash
# Docker - set BEACON_MODE before starting
BEACON_MODE=topic docker compose up -d
BEACON_MODE=param docker compose up -d
docker compose up -d # default: none

# Local (non-Docker)
BEACON_MODE=topic ros2 launch sensor_diagnostics_demo demo.launch.py
```

### Viewing Beacon Data

When a beacon mode is active, each sensor entity gets enriched with extra metadata visible through the API:

```bash
# Topic beacon metadata
curl http://localhost:8080/api/v1/apps/lidar-sim/x-medkit-topic-beacon | jq

# Parameter beacon metadata
curl http://localhost:8080/api/v1/apps/lidar-sim/x-medkit-param-beacon | jq
```

The beacon data includes:
- **entity_id** - Manifest app ID (e.g., `lidar-sim`)
- **display_name** - Human-friendly name (e.g., `LiDAR Simulator`)
- **component_id** - Parent component (e.g., `lidar-unit`)
- **function_ids** - Function membership (e.g., `sensor-monitoring`)
- **process_id** / **hostname** - Process-level diagnostics
- **metadata** - Sensor-specific key-value pairs (sensor_type, data_topic, frame_id)

### How It Works

**Topic beacon** (push): Each sensor node creates a publisher on `/ros2_medkit/discovery` and publishes a `MedkitDiscoveryHint` message every 5 seconds. The gateway's `topic_beacon` plugin subscribes to this topic and merges the hints into the entity model. Hints have a TTL (default 10s) - if a node stops publishing, the data goes stale.

**Parameter beacon** (pull): Each sensor node declares ROS 2 parameters under the `ros2_medkit.discovery.*` prefix. The gateway's `parameter_beacon` plugin polls all nodes for these parameters every 5 seconds. No explicit publishing is needed - the gateway reads the parameters via the ROS 2 parameter service.

Both mechanisms enrich the same entities defined in the manifest. They do not create new entities (the `allow_new_entities` option is disabled). Only one beacon mode should be active at a time - they serve the same purpose via different transport mechanisms.

## Use Cases

1. **CI/CD Testing** - Validate ros2_medkit without heavy simulation
Expand Down
2 changes: 2 additions & 0 deletions demos/sensor_diagnostics/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
container_name: sensor_diagnostics_demo
environment:
- ROS_DOMAIN_ID=40
- BEACON_MODE=${BEACON_MODE:-none}
ports:
- "8080:8080"
stdin_open: true
Expand Down Expand Up @@ -37,6 +38,7 @@ services:
container_name: sensor_diagnostics_demo_ci
environment:
- ROS_DOMAIN_ID=40
- BEACON_MODE=none
ports:
- "8080:8080"
command: >
Expand Down
105 changes: 91 additions & 14 deletions demos/sensor_diagnostics/launch/demo.launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
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
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
2. Modern path: Sensors -> anomaly_detector -> ReportFault service -> fault_manager
- Used by: IMU, GPS
- Direct ros2_medkit fault reporting

Beacon modes (set via BEACON_MODE env var):
none - No beacon plugins (default)
topic - Topic beacon: sensor nodes push MedkitDiscoveryHint messages
param - Parameter beacon: gateway polls sensor node parameters

Namespace structure:
/sensors - Simulated sensor nodes (lidar, imu, gps, camera)
/processing - Anomaly detector
Expand All @@ -19,6 +24,7 @@
"""

import os
import sys

from ament_index_python.packages import get_package_prefix
from ament_index_python.packages import get_package_share_directory
Expand Down Expand Up @@ -50,6 +56,18 @@ def generate_launch_description():
sensor_params_file = os.path.join(pkg_dir, "config", "sensor_params.yaml")
manifest_file = os.path.join(pkg_dir, "config", "sensor_manifest.yaml")

# Beacon mode from environment (controls both plugin loading and node behavior)
beacon_mode = os.environ.get('BEACON_MODE', 'none').strip().lower()
valid_beacon_modes = ('none', 'topic', 'param')
if beacon_mode not in valid_beacon_modes:
print(
f"WARNING: Invalid BEACON_MODE='{beacon_mode}'. "
f"Valid values: {', '.join(valid_beacon_modes)}. "
"Falling back to 'none'.",
file=sys.stderr,
)
beacon_mode = 'none'

# Resolve plugin paths
graph_provider_path = _resolve_plugin_path(
'ros2_medkit_graph_provider', 'ros2_medkit_graph_provider_plugin')
Expand All @@ -65,8 +83,46 @@ def generate_launch_description():
if procfs_plugin_path:
active_plugins.append('procfs_introspection')
plugin_overrides['plugins.procfs_introspection.path'] = procfs_plugin_path

# Beacon plugin (mutually exclusive - only one beacon type at a time)
if beacon_mode == 'topic':
topic_beacon_path = _resolve_plugin_path(
'ros2_medkit_topic_beacon', 'topic_beacon_plugin')
if topic_beacon_path:
active_plugins.append('topic_beacon')
plugin_overrides['plugins.topic_beacon.path'] = topic_beacon_path
plugin_overrides['plugins.topic_beacon.topic'] = \
'/ros2_medkit/discovery'
plugin_overrides['plugins.topic_beacon.beacon_ttl_sec'] = 15.0
else:
print(
"WARNING: BEACON_MODE=topic but topic_beacon plugin not "
"found. Falling back to none.",
file=sys.stderr,
)
beacon_mode = 'none'
elif beacon_mode == 'param':
param_beacon_path = _resolve_plugin_path(
'ros2_medkit_param_beacon', 'param_beacon_plugin')
if param_beacon_path:
active_plugins.append('parameter_beacon')
plugin_overrides['plugins.parameter_beacon.path'] = \
param_beacon_path
plugin_overrides['plugins.parameter_beacon.poll_interval_sec'] = \
5.0
else:
print(
"WARNING: BEACON_MODE=param but param_beacon plugin not "
"found. Falling back to none.",
file=sys.stderr,
)
beacon_mode = 'none'

plugin_overrides['plugins'] = active_plugins

# Sensor node beacon parameter (passed to all sensor nodes)
beacon_params = {"beacon_mode": beacon_mode}

# Launch arguments
use_sim_time = LaunchConfiguration("use_sim_time", default="false")

Expand All @@ -76,7 +132,8 @@ def generate_launch_description():
DeclareLaunchArgument(
"use_sim_time",
default_value="false",
description="Use simulation time (set to true if using with Gazebo)",
description="Use simulation time (set to true if using "
"with Gazebo)",
),
# ===== Sensor Nodes (under /sensors namespace) =====
# Legacy path sensors: publish DiagnosticArray to /diagnostics
Expand All @@ -86,45 +143,64 @@ def generate_launch_description():
name="lidar_sim",
namespace="sensors",
output="screen",
parameters=[sensor_params_file, {"use_sim_time": use_sim_time}],
parameters=[
sensor_params_file,
{"use_sim_time": use_sim_time},
beacon_params,
],
),
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}],
parameters=[
sensor_params_file,
{"use_sim_time": use_sim_time},
beacon_params,
],
),
# Modern path sensors: monitored by anomaly_detector ReportFault
# 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}],
parameters=[
sensor_params_file,
{"use_sim_time": use_sim_time},
beacon_params,
],
),
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}],
parameters=[
sensor_params_file,
{"use_sim_time": use_sim_time},
beacon_params,
],
),
# ===== Processing Nodes (under /processing namespace) =====
# Modern path: anomaly_detector monitors IMU/GPS and calls ReportFault
# Modern path: anomaly_detector monitors IMU/GPS, 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}],
parameters=[
sensor_params_file,
{"use_sim_time": use_sim_time},
],
),
# ===== Diagnostic Bridge (Legacy path) =====
# Bridges /diagnostics topic (DiagnosticArray) fault_manager
# Bridges /diagnostics topic (DiagnosticArray) -> fault_manager
# Handles faults from: LiDAR, Camera
Node(
package="ros2_medkit_diagnostic_bridge",
Expand Down Expand Up @@ -156,13 +232,14 @@ def generate_launch_description():
),
# ===== 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)
# Also handles snapshot and rosbag capture when faults are confirmed
# Both paths report here: diagnostic_bridge (legacy) and
# anomaly_detector (modern)
# Also handles snapshot and rosbag capture on fault confirmation
Node(
package="ros2_medkit_fault_manager",
executable="fault_manager_node",
name="fault_manager",
namespace="", # Root namespace so services are at /fault_manager/*
namespace="", # Root namespace: services at /fault_manager/*
output="screen",
parameters=[
medkit_params_file,
Expand Down
Loading
Loading