Manifest-Based Discovery
This tutorial explains how to use manifest files to define your ROS 2 system structure for SOVD discovery.
Introduction
The ros2_medkit gateway supports three discovery modes:
Runtime-Only (default): Traditional ROS 2 graph introspection
Hybrid (recommended): Manifest as source of truth + runtime linking
Manifest-Only: Only expose manifest-declared entities
Hybrid mode is recommended because it:
Provides stable entity IDs that don’t change across restarts
Enables semantic groupings (areas, functions) for better organization
Preserves runtime data (topic values, service calls)
Allows documentation and metadata in entity definitions
Supports offline detection for apps not currently running
Discovery Modes Comparison
Feature |
Runtime-Only |
Hybrid |
Manifest-Only |
|---|---|---|---|
Entity IDs |
Derived from ROS graph |
Stable (from manifest) |
Stable (from manifest) |
Live topics/services |
✅ Yes |
✅ Yes |
❌ No |
Custom areas |
❌ No (derived from namespaces) |
✅ Yes |
✅ Yes |
Apps entity type |
✅ Yes (ROS nodes → Apps) |
✅ Yes |
✅ Yes |
Functions entity type |
❌ No |
✅ Yes |
✅ Yes |
Offline detection |
❌ No |
✅ Yes |
N/A |
Writing Your First Manifest
Create a file named system_manifest.yaml:
# SOVD System Manifest
manifest_version: "1.0"
metadata:
name: "my-robot"
version: "1.0.0"
description: "My Robot System"
# Define logical areas (subsystems)
areas:
- id: perception
name: "Perception"
category: "sensor-processing"
description: "Sensor data processing and fusion"
# Define hardware/virtual components
components:
- id: lidar-sensor
name: "LiDAR Sensor"
type: "sensor"
area: perception
# Define apps (map to ROS 2 nodes)
apps:
- id: lidar-driver
name: "LiDAR Driver"
is_located_on: lidar-sensor
ros_binding:
node_name: velodyne_driver
namespace: /sensors
# Define high-level functions
functions:
- id: environment-sensing
name: "Environment Sensing"
hosted_by:
- lidar-driver
Enabling Manifest Mode
Option 1: Using Launch File
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='ros2_medkit_gateway',
executable='gateway_node',
parameters=[{
'discovery.mode': 'hybrid',
'discovery.manifest_path': '/path/to/system_manifest.yaml',
'discovery.manifest_strict_validation': True,
}]
)
])
Option 2: Using Parameter File
Add to your gateway_params.yaml:
ros2_medkit_gateway:
ros__parameters:
# ... existing parameters ...
discovery:
# Discovery mode: "runtime_only", "hybrid", or "manifest_only"
mode: "hybrid"
# Path to manifest YAML file (required for hybrid and manifest_only)
manifest_path: "/path/to/system_manifest.yaml"
# Strict validation: fail on any validation error
manifest_strict_validation: true
Then launch with:
ros2 run ros2_medkit_gateway gateway_node \
--ros-args --params-file /path/to/gateway_params.yaml
Option 3: Command Line Parameters
ros2 run ros2_medkit_gateway gateway_node --ros-args \
-p discovery.mode:=hybrid \
-p discovery.manifest_path:=/path/to/system_manifest.yaml
Verifying the Configuration
Once the gateway is running with a manifest, you can verify the configuration via the REST API.
Check manifest status:
curl http://localhost:8080/api/v1/manifest/status
Expected response:
{
"status": "active",
"discovery_mode": "hybrid",
"manifest_path": "/path/to/system_manifest.yaml",
"statistics": {
"areas_count": 1,
"components_count": 1,
"apps_count": 1,
"functions_count": 1
}
}
List apps:
curl http://localhost:8080/api/v1/apps
List functions:
curl http://localhost:8080/api/v1/functions
Understanding Hybrid Mode
In hybrid mode, discovery uses a merge pipeline that combines entities from multiple discovery layers:
ManifestLayer (highest priority) - entities from the YAML manifest
RuntimeLayer - entities discovered via ROS 2 graph introspection
PluginLayers (optional) - entities from gateway plugins
The pipeline merges entities by ID. When the same entity appears in multiple layers, per-field-group merge policies determine which values win. See Discovery Options Reference for details on merge policies and gap-fill configuration.
After merging, the RuntimeLinker binds manifest apps to running ROS 2 nodes:
Discovery: All layers produce entities
Merging: Pipeline merges entities by ID, applying field-group policies
Linking: For each manifest app, checks
ros_bindingconfigurationBinding: If match found, copies runtime resources (topics, services, actions)
Status: Apps with matched nodes are marked
is_online: true
Merge Report
After each pipeline execution, the gateway produces a MergeReport available
via the health endpoint (GET /health). The report includes:
Layer names and ordering
Total entity count, enrichment count
Conflict details (which layers disagreed on which field groups)
Cross-type ID collision warnings
Gap-fill filtering statistics
In hybrid mode, the GET /health response includes full discovery diagnostics:
{
"discovery": {
"mode": "hybrid",
"strategy": "hybrid",
"pipeline": {
"layers": ["manifest", "runtime"],
"total_entities": 12,
"enriched_count": 8,
"conflict_count": 0,
"id_collisions": 0
},
"linking": {
"linked_count": 5,
"orphan_count": 1,
"binding_conflicts": 0,
"warnings": ["Orphan node: /unmanifested_node"]
}
}
}
Runtime Linking
Note
In hybrid mode, the gateway uses a layered merge pipeline: manifest entities, runtime-discovered entities, and plugin-contributed entities are merged per field-group before linking. See Discovery Options Reference for merge pipeline configuration.
Note
Trigger entity resolution in hybrid mode
In hybrid mode, fault and log triggers are delivered using the manifest entity
ID rather than the raw ROS node FQN. The runtime linker builds a node-to-app
mapping during the linking phase (step 4 above), and the fault manager and log
manager use this mapping to resolve incoming trigger events to the correct
manifest entity. This means fault codes, log subscriptions, and SSE events
reference stable manifest IDs (e.g., lidar-driver) instead of the
ephemeral node FQN (e.g., /sensors/velodyne_driver), even when nodes
restart and re-bind.
ROS Binding Configuration
The ros_binding section specifies how to match an app to a ROS 2 node:
ros_binding:
# Match by node name
node_name: "velodyne_driver"
# Match within a specific namespace (optional)
namespace: "/sensors"
# Or match by topic namespace prefix (alternative)
topic_namespace: "/sensors/lidar"
Match Types:
Name and namespace match (default): Node name must match exactly. Namespace uses path-segment-boundary matching (
/navmatches/navand/nav/subbut NOT/navigation)Wildcard namespace: Use
namespace: "*"to match any namespaceTopic namespace: Match nodes by their topic prefix
# Example: Match node in any namespace
apps:
- id: camera-driver
name: "Camera Driver"
ros_binding:
node_name: usb_cam
namespace: "*"
# Example: Match by topic namespace
apps:
- id: lidar-processing
name: "LiDAR Processing"
ros_binding:
topic_namespace: "/perception/lidar"
Checking Linking Status
Check which apps are online:
curl http://localhost:8080/api/v1/apps | jq '.items[] | {id, name, is_online}'
Example response:
{"id": "lidar-driver", "name": "LiDAR Driver", "is_online": true}
{"id": "camera-driver", "name": "Camera Driver", "is_online": false}
Entity Hierarchy
The manifest supports two hierarchy patterns depending on your robot’s complexity.
With Areas (Complex Robots)
For robots with multiple subsystems (e.g., autonomous vehicles, industrial platforms), use areas to group components by function or location:
Areas (logical/physical groupings)
+-- Components (hardware/virtual units)
+-- Apps (software applications)
+-- Data (topics)
+-- Operations (services/actions)
+-- Configurations (parameters)
Functions (cross-cutting capabilities)
+-- Apps (hosted by)
Areas group related components by function or location:
areas:
- id: perception
name: "Perception Subsystem"
subareas:
- id: lidar-processing
name: "LiDAR Processing"
Components are assigned to areas:
components:
- id: main-computer
name: "Main Computer"
type: "controller"
area: perception
subcomponents:
- id: gpu-unit
name: "GPU Processing Unit"
Without Areas (Simple Robots)
For simple robots where the entire system is a single unit (e.g., TurtleBot3, small mobile platforms), you can omit areas entirely and use a flat component tree. The robot itself is the top-level component, with subcomponents for hardware modules:
Components (top-level)
+-- Subcomponents (hardware modules)
+-- Apps (software applications)
Functions (cross-cutting capabilities)
+-- Apps (hosted by)
# No areas section needed
components:
- id: turtlebot3
name: "TurtleBot3 Burger"
type: "mobile-robot"
- id: raspberry-pi
name: "Raspberry Pi 4"
type: "controller"
parent_component_id: turtlebot3
apps:
- id: nav2-controller
name: "Nav2 Controller"
is_located_on: raspberry-pi
ros_binding:
node_name: controller_server
namespace: /
For runtime-only mode, set discovery.runtime.create_synthetic_areas: false
to prevent automatic area creation from namespaces. See
Flat Entity Tree in the manifest schema reference and
config/examples/flat_robot_manifest.yaml for a complete example.
When to use each pattern:
With areas: Multiple subsystems, deep namespace hierarchy, large teams working on separate domains (perception, navigation, control)
Without areas: Single robot with a handful of nodes, flat or shallow namespace structure, quick prototypes
Common Elements
Both patterns use Apps and Functions the same way.
Apps are software applications (typically ROS 2 nodes):
apps:
- id: slam-node
name: "SLAM Node"
is_located_on: main-computer
depends_on:
- lidar-driver
- imu-driver
ros_binding:
node_name: slam_toolbox
Functions are high-level capabilities spanning multiple apps:
functions:
- id: autonomous-navigation
name: "Autonomous Navigation"
hosted_by:
- planner-node
- controller-node
- localization-node
Controlling Gap-Fill in Hybrid Mode
In hybrid mode, the runtime layer can create heuristic entities for namespaces
not covered by the manifest. The merge_pipeline.gap_fill parameters control
this behavior:
discovery:
merge_pipeline:
gap_fill:
allow_heuristic_areas: true # Create areas from namespaces
allow_heuristic_components: true # Create synthetic components
allow_heuristic_apps: true # Create apps from unbound nodes
allow_heuristic_functions: false # Don't create heuristic functions
# namespace_blacklist: ["/rosout"] # Exclude specific namespaces
# namespace_whitelist: [] # If set, only allow these namespaces
When all allow_heuristic_* options are false, only manifest-declared
entities appear. Runtime nodes are still discovered for linking, but no
heuristic entities (areas, components, apps, functions) are created from
unmatched namespaces or nodes.
See Discovery Options Reference for the full merge pipeline reference.
Hot Reloading
Reload the manifest without restarting the gateway:
curl -X POST http://localhost:8080/api/v1/manifest/reload
This re-parses the manifest file and re-links apps to nodes.
REST API Endpoints
Manifest mode adds the following endpoints:
Endpoint |
Description |
Since |
|---|---|---|
|
List all apps |
0.1.0 |
|
Get app capabilities |
0.1.0 |
|
Get app topic data |
0.1.0 |
|
List app operations |
0.1.0 |
|
List app configurations |
0.1.0 |
|
List all functions |
0.1.0 |
|
Get function capabilities |
0.1.0 |
|
List function host apps |
0.1.0 |
|
Aggregated data from hosts |
0.1.0 |
|
Aggregated operations |
0.1.0 |
Next Steps
Manifest Schema Reference - Complete YAML schema reference
Migration Guide: Runtime to Hybrid Mode - Migrate from runtime-only mode
Getting Started - Basic gateway setup