Heuristic Runtime Discovery

This tutorial explains how to use heuristic runtime discovery to expose ROS 2 nodes as SOVD Apps, with synthetic Components grouping them logically.

Overview

By default, ros2_medkit gateway uses heuristic discovery to map ROS 2 graph entities to SOVD entities:

  • ROS 2 nodesApps (software applications)

  • NamespacesAreas (logical groupings)

  • Apps grouped by namespaceSynthetic Components

  • Topics, services, actionsData, Operations

This approach requires no configuration and works out of the box with any ROS 2 system.

When to Use Heuristic Discovery

Heuristic discovery is ideal when:

✅ You want to explore a ROS 2 system without prior setup ✅ Entity IDs can be derived from node names ✅ Your system doesn’t require stable IDs across restarts ✅ You’re prototyping or debugging

Consider using Manifest-Based Discovery when:

❌ You need stable, semantic IDs (e.g., front-lidar instead of scan_node) ❌ You need to define entities that don’t exist at runtime ❌ You need offline detection of failed components

Quick Start

Launch the gateway with default settings:

ros2 launch ros2_medkit_gateway gateway.launch.py

The gateway automatically discovers all nodes and maps them to SOVD entities.

Query available Apps:

curl http://localhost:8080/api/v1/apps | jq

Example response:

{
  "items": [
    {
      "id": "lidar_driver",
      "name": "lidar_driver",
      "namespace_path": "/perception",
      "area": "perception",
      "component": "perception",
      "source": "heuristic"
    },
    {
      "id": "camera_node",
      "name": "camera_node",
      "namespace_path": "/perception",
      "area": "perception",
      "component": "perception",
      "source": "heuristic"
    }
  ]
}

Understanding the Entity Hierarchy

With heuristic discovery, the SOVD hierarchy is built as follows:

Area: "perception"                    ← from namespace /perception
└── Component: "perception"           ← synthetic, groups apps in this area
    ├── App: "lidar_driver"           ← from node /perception/lidar_driver
    │   ├── Data: "scan"              ← published topics
    │   └── Operations: ...           ← services/actions
    └── App: "camera_node"            ← from node /perception/camera_node
        └── Data: "image_raw"

Configuration Options

All options are under discovery.runtime in the gateway parameters:

ros2_medkit_gateway:
  ros__parameters:
    discovery:
      mode: "runtime_only"  # or "hybrid"

      runtime:
        create_synthetic_components: true
        grouping_strategy: "namespace"
        synthetic_component_name_pattern: "{area}"

create_synthetic_components

When true (default), the gateway creates synthetic Components to group Apps:

curl http://localhost:8080/api/v1/components
# Returns: [{"id": "perception", "source": "synthetic", ...}]

curl http://localhost:8080/api/v1/components/perception/apps
# Returns: [{"id": "lidar_driver"}, {"id": "camera_node"}]

Note

Synthetic components are logical groupings only. They do not aggregate operations or data from their hosted Apps. To access operations (services/actions), you must query the Apps within the component:

# List Apps in the component
curl http://localhost:8080/api/v1/components/perception/apps

# Get operations for a specific App
curl http://localhost:8080/api/v1/apps/lidar_driver/operations

The component endpoint GET /components/{id}/operations aggregates operation listings from all hosted Apps for convenience, but execution must target the specific App that owns the operation.

When false, no synthetic Components are created (Apps-only mode):

curl http://localhost:8080/api/v1/components
# Returns: [] (empty - no synthetic components)

curl http://localhost:8080/api/v1/apps
# Returns: [{"id": "lidar_driver"}, {"id": "camera_node"}]

grouping_strategy

Controls how Apps are grouped into Components:

  • namespace (default): Group by first namespace segment

  • none: Each app is its own component

Handling Topic-Only Namespaces

Some systems (like Isaac Sim) publish topics without creating ROS 2 nodes. The topic_only_policy controls how these are handled:

discovery:
  runtime:
    topic_only_policy: "create_component"
    min_topics_for_component: 2

Policies:

  • create_component (default): Create a Component for topic namespaces

  • create_area_only: Create only the Area, no Component

  • ignore: Skip topic-only namespaces entirely

Example: Filtering Noise

To ignore orphaned topics from crashed processes:

discovery:
  runtime:
    topic_only_policy: "ignore"

Or require multiple topics before creating a component:

discovery:
  runtime:
    topic_only_policy: "create_component"
    min_topics_for_component: 3  # Need 3+ topics

API Endpoints

Apps Endpoints

Endpoint

Description

GET /apps

List all discovered Apps

GET /apps/{app_id}

Get specific App details

GET /components/{id}/apps

List Apps in a synthetic Component

Components Endpoints

With synthetic components, the /components endpoint returns grouped entities:

curl http://localhost:8080/api/v1/components | jq '.items[] | {id, source}'
{"id": "perception", "source": "synthetic"}
{"id": "navigation", "source": "synthetic"}
{"id": "isaac_sim", "source": "topic"}

The source field indicates how the component was discovered:

  • synthetic: Auto-created from namespace grouping in runtime mode

  • topic: Created from topic-only namespace (no running nodes)

  • manifest: Explicitly defined in manifest file (see Manifest-Based Discovery)

Migrating to Manifest Discovery

When you need more control, consider migrating to hybrid mode. See Migration Guide: Runtime to Hybrid Mode for a step-by-step guide.

Key differences:

Feature

Heuristic

Manifest

Setup required

None

YAML manifest file

Entity IDs

Derived from node names

Custom, semantic IDs

Offline detection

No

Yes (failed components)

Stable across restarts

No (depends on node names)

Yes (defined in manifest)

See Also