Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions demos/sensor_diagnostics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ament_target_dependencies(lidar_sim_node
sensor_msgs
diagnostic_msgs
rcl_interfaces
ros2_medkit_msgs
)

# IMU simulator node
Expand All @@ -30,6 +31,7 @@ ament_target_dependencies(imu_sim_node
rclcpp
sensor_msgs
diagnostic_msgs
ros2_medkit_msgs
)

# GPS simulator node
Expand All @@ -38,6 +40,7 @@ ament_target_dependencies(gps_sim_node
rclcpp
sensor_msgs
diagnostic_msgs
ros2_medkit_msgs
)

# Camera simulator node
Expand All @@ -47,6 +50,7 @@ ament_target_dependencies(camera_sim_node
sensor_msgs
diagnostic_msgs
rcl_interfaces
ros2_medkit_msgs
)

# Anomaly detector node
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=${BEACON_MODE:-none}
Comment thread
bburda marked this conversation as resolved.
Outdated
ports:
- "8080:8080"
command: >
Expand Down
81 changes: 67 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 Down Expand Up @@ -50,6 +55,9 @@ 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')
Comment thread
bburda marked this conversation as resolved.
Outdated

# Resolve plugin paths
graph_provider_path = _resolve_plugin_path(
'ros2_medkit_graph_provider', 'ros2_medkit_graph_provider_plugin')
Expand All @@ -65,8 +73,32 @@ 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'] = 10.0
Comment thread
bburda marked this conversation as resolved.
Outdated
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
Comment thread
bburda marked this conversation as resolved.

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 +108,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 +119,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 +208,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