Skip to content
Open
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
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}
ports:
- "8080:8080"
command: >
Expand Down
107 changes: 93 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,19 @@ 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:
import sys
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,48 @@ 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
else:
import sys
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:
import sys
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 +134,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 +145,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 +234,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