Plugin System
The gateway supports a plugin system for extending functionality with shared libraries (.so files).
Plugins can provide software update backends, platform-specific introspection, and custom REST endpoints.
Overview
Plugins implement the GatewayPlugin C++ base class plus one or more typed provider interfaces:
UpdateProvider - software update backend (CRUD, prepare/execute, automated, status)
IntrospectionProvider - provides platform-specific metadata and can introduce new entities into the entity cache. Called during each discovery cycle by the merge pipeline’s PluginLayer. Plugin-provided metadata is accessible via the plugin API, not automatically merged into entity responses. See Discovery Options Reference for merge pipeline configuration.
LogProvider - replaces or augments the default
/rosoutlog backend. Can operate in observer mode (receives log entries) or full-ingestion mode (owns the entire log pipeline). See the/logsendpoints in REST API Reference.ScriptProvider - replaces or augments the default filesystem-based script backend. Plugins can provide script listings, create custom scripts, and execute them using alternative runtimes. See the
/scriptsendpoints in REST API Reference.
A single plugin can implement multiple provider interfaces. For example, a “systemd” plugin could provide both introspection (discover systemd units) and updates (manage service restarts).
Configuration
Add plugins to gateway_params.yaml:
ros2_medkit_gateway:
ros__parameters:
plugins: ["my_ota_plugin"]
plugins.my_ota_plugin.path: "/opt/ros2_medkit/lib/libmy_ota_plugin.so"
plugins.my_ota_plugin.server_url: "https://updates.example.com"
plugins.my_ota_plugin.api_key: "secret123"
plugins.my_ota_plugin.timeout_ms: 5000
# Enable updates if your plugin implements UpdateProvider
updates:
enabled: true
Each plugin name in the plugins array requires a corresponding plugins.<name>.path
parameter with the absolute path to the .so file. Any additional plugins.<name>.<key>
parameters are collected into a JSON object and passed to the plugin’s configure() method.
For the example above, configure() receives:
{"server_url": "https://updates.example.com", "api_key": "secret123", "timeout_ms": 5000}
Plugins are loaded in the order listed. An empty list (default) means no plugins are loaded, with zero overhead.
Plugin names must contain only alphanumeric characters, underscores, and hyphens (max 256 chars).
All standard ROS 2 parameter types are supported: strings, integers, doubles, booleans, and their array variants. They are automatically converted to their JSON equivalents.
Writing a Plugin
Create a C++ shared library implementing
GatewayPluginand optionally one or more providers:
#include "ros2_medkit_gateway/plugins/gateway_plugin.hpp"
#include "ros2_medkit_gateway/plugins/plugin_types.hpp"
#include "ros2_medkit_gateway/providers/update_provider.hpp"
using namespace ros2_medkit_gateway;
class MyPlugin : public GatewayPlugin, public UpdateProvider {
public:
std::string name() const override { return "my_plugin"; }
void configure(const nlohmann::json& config) override {
// Read plugin-specific configuration (env vars, files, etc.)
}
void shutdown() override {
// Clean up resources
}
// UpdateProvider methods - all 7 must be implemented:
tl::expected<std::vector<std::string>, UpdateBackendErrorInfo>
list_updates(const UpdateFilter& filter) override { /* ... */ }
tl::expected<nlohmann::json, UpdateBackendErrorInfo>
get_update(const std::string& id) override { /* ... */ }
tl::expected<void, UpdateBackendErrorInfo>
register_update(const nlohmann::json& metadata) override { /* ... */ }
tl::expected<void, UpdateBackendErrorInfo>
delete_update(const std::string& id) override { /* ... */ }
tl::expected<void, UpdateBackendErrorInfo>
prepare(const std::string& id, UpdateProgressReporter& reporter) override { /* ... */ }
tl::expected<void, UpdateBackendErrorInfo>
execute(const std::string& id, UpdateProgressReporter& reporter) override { /* ... */ }
tl::expected<bool, UpdateBackendErrorInfo>
supports_automated(const std::string& id) override { /* ... */ }
};
Export the required
extern "C"symbols:
// Required: API version check
extern "C" GATEWAY_PLUGIN_EXPORT int plugin_api_version() {
return ros2_medkit_gateway::PLUGIN_API_VERSION;
}
// Required: factory function
extern "C" GATEWAY_PLUGIN_EXPORT GatewayPlugin* create_plugin() {
return new MyPlugin();
}
// Required if your plugin implements UpdateProvider:
extern "C" GATEWAY_PLUGIN_EXPORT UpdateProvider* get_update_provider(GatewayPlugin* p) {
return static_cast<MyPlugin*>(p);
}
The get_update_provider (and get_introspection_provider, get_log_provider, get_script_provider) functions use extern "C"
to avoid RTTI issues across shared library boundaries. The static_cast is safe because
these functions execute inside the plugin’s own .so where the type hierarchy is known.
Without the corresponding get_*_provider export, the gateway cannot detect that your plugin
implements the provider interface, even if the class inherits from it.
Build as a MODULE library:
add_library(my_plugin MODULE src/my_plugin.cpp)
target_link_libraries(my_plugin gateway_lib)
Install the
.soand add its path togateway_params.yaml.
Complete Minimal Plugin
A self-contained plugin implementing UpdateProvider (copy-paste starting point):
// my_ota_plugin.cpp
#include "ros2_medkit_gateway/plugins/gateway_plugin.hpp"
#include "ros2_medkit_gateway/plugins/plugin_types.hpp"
#include "ros2_medkit_gateway/providers/update_provider.hpp"
#include <httplib.h>
#include <nlohmann/json.hpp>
using namespace ros2_medkit_gateway;
class MyOtaPlugin : public GatewayPlugin, public UpdateProvider {
public:
std::string name() const override { return "my_ota"; }
void configure(const nlohmann::json& /*config*/) override {}
void shutdown() override {}
// UpdateProvider CRUD
tl::expected<std::vector<std::string>, UpdateBackendErrorInfo>
list_updates(const UpdateFilter& /*filter*/) override {
return std::vector<std::string>{};
}
tl::expected<nlohmann::json, UpdateBackendErrorInfo>
get_update(const std::string& id) override {
return tl::make_unexpected(
UpdateBackendErrorInfo{UpdateBackendError::NotFound, "not found: " + id});
}
tl::expected<void, UpdateBackendErrorInfo>
register_update(const nlohmann::json& /*metadata*/) override { return {}; }
tl::expected<void, UpdateBackendErrorInfo>
delete_update(const std::string& /*id*/) override { return {}; }
// UpdateProvider async operations
tl::expected<void, UpdateBackendErrorInfo>
prepare(const std::string& /*id*/, UpdateProgressReporter& reporter) override {
reporter.set_progress(100);
return {};
}
tl::expected<void, UpdateBackendErrorInfo>
execute(const std::string& /*id*/, UpdateProgressReporter& reporter) override {
reporter.set_progress(100);
return {};
}
tl::expected<bool, UpdateBackendErrorInfo>
supports_automated(const std::string& /*id*/) override { return false; }
};
// Required exports
extern "C" GATEWAY_PLUGIN_EXPORT int plugin_api_version() {
return PLUGIN_API_VERSION;
}
extern "C" GATEWAY_PLUGIN_EXPORT GatewayPlugin* create_plugin() {
return new MyOtaPlugin();
}
// Required for UpdateProvider detection
extern "C" GATEWAY_PLUGIN_EXPORT UpdateProvider* get_update_provider(GatewayPlugin* p) {
return static_cast<MyOtaPlugin*>(p);
}
Plugin Lifecycle
dlopenloads the.sowithRTLD_NOW | RTLD_LOCALplugin_api_version()is checked against the gateway’sPLUGIN_API_VERSIONcreate_plugin()factory function creates the plugin instanceProvider interfaces are queried via
get_update_provider()/get_introspection_provider()/get_log_provider()/get_script_provider()configure()is called with per-plugin JSON configset_context()providesPluginContextwith ROS 2 node, entity cache, faults, and HTTP utilitiesregister_routes()allows registering custom REST endpointsRuntime: subsystem managers call provider methods as needed
shutdown()is called before the plugin is destroyed
PluginContext
After configure(), the gateway calls set_context() with a PluginContext reference
providing access to gateway data and utilities:
node()- ROS 2 node pointer for subscriptions, service clients, timers, etc.get_entity(id)- look up any entity (area, component, app, function) from the discovery cachelist_entity_faults(entity_id)- query faults for an entityvalidate_entity_for_route(req, res, entity_id)- validate entity exists and matches the route type, auto-sending SOVD errors on failuresend_error()/send_json()- SOVD-compliant HTTP response helpers (static methods)register_capability()/register_entity_capability()- register custom capabilities on entitiescheck_lock(entity_id, client_id, collection)- verify lock access before mutating operations; returnsLockAccessResultwithallowedflag and denial detailsacquire_lock()/release_lock()- acquire and release entity locks with optional scope and TTLget_entity_snapshot()- returns anIntrospectionInputpopulated from the current entity cachelist_all_faults()- returns JSON object with a"faults"array containing all active faults across all entitiesregister_sampler(collection, fn)- registers a cyclic subscription sampler for a custom collection name
void set_context(PluginContext& ctx) override {
ctx_ = &ctx;
// Register a custom capability for all apps
ctx.register_capability(SovdEntityType::APP, "x-medkit-traces");
// Register a capability for a specific entity
ctx.register_entity_capability("sensor1", "x-medkit-calibration");
// Get a snapshot of all currently discovered entities
IntrospectionInput snapshot = ctx.get_entity_snapshot();
// Query all active faults (returns {"faults": [...]})
nlohmann::json all_faults = ctx.list_all_faults();
// Register a sampler so clients can subscribe to "x-medkit-metrics" cyclically
ctx.register_sampler("x-medkit-metrics",
[this](const std::string& entity_id, const std::string& /*resource_path*/)
-> tl::expected<nlohmann::json, std::string> {
auto data = collect_metrics(entity_id);
if (!data) return tl::make_unexpected("no data for: " + entity_id);
return *data;
});
}
PluginContext* ctx_ = nullptr;
get_entity_snapshot() returns an IntrospectionInput with vectors for all discovered
areas, components, apps, and functions at the moment of the call. The snapshot is read-only
and reflects the state of the gateway’s thread-safe entity cache.
list_all_faults() is useful for plugins that need cross-entity fault visibility (e.g.
mapping fault codes to topics). Returns {} if the fault manager is unavailable.
register_sampler(collection, fn) wires a sampler into the ResourceSamplerRegistry
so that cyclic subscriptions created for collection (e.g. "x-medkit-metrics")
call fn(entity_id, resource_path) on each tick. The function must return
tl::expected<nlohmann::json, std::string>. See Cyclic Subscription Extensions
for the lower-level registry API.
Note
The PluginContext interface is versioned alongside PLUGIN_API_VERSION.
Breaking changes to existing methods or removal of methods increment the version.
New non-breaking methods (like check_lock, get_entity_snapshot,
list_all_faults, and register_sampler) provide default no-op implementations
so plugins that do not use these methods need no code changes. However, a rebuild is
still required because plugin_api_version() must return the current version
(exact-match check).
PluginContext API (v4)
Version 4 of the plugin API introduced several new methods on PluginContext.
These methods have default no-op implementations, so existing plugins continue to
compile without changes (though a rebuild is required to match the new
PLUGIN_API_VERSION).
check_lock(entity_id, client_id, collection)
Verify whether a lock blocks access to a resource collection on an entity. Plugins that perform mutating operations (writing configurations, executing scripts, etc.) should call this before proceeding:
auto result = ctx_->check_lock(entity_id, client_id, "configurations");
if (!result.allowed) {
PluginContext::send_error(res, 409, result.denied_code, result.denied_reason);
return;
}
The returned LockAccessResult contains an allowed flag and, when denied,
denied_by_lock_id, denied_code, and denied_reason fields. Companion
methods acquire_lock() and release_lock() let plugins manage locks directly.
get_entity_snapshot()
Returns an IntrospectionInput populated from the current entity cache. The
snapshot contains read-only vectors for all discovered areas, components, apps, and
functions at the moment of the call:
IntrospectionInput snapshot = ctx_->get_entity_snapshot();
for (const auto& app : snapshot.apps) {
RCLCPP_INFO(ctx_->node()->get_logger(), "App: %s", app.id.c_str());
}
This is useful for plugins that need a consistent view of all entities without subscribing to discovery events.
list_all_faults()
Returns a JSON object with a "faults" array containing all active faults across
all entities. Returns an empty object if the fault manager is unavailable:
nlohmann::json faults = ctx_->list_all_faults();
for (const auto& fault : faults.value("faults", nlohmann::json::array())) {
// Process each fault
}
register_sampler(collection, fn)
Registers a cyclic subscription sampler for a custom collection name. Once registered, clients can create cyclic subscriptions on that collection for any entity:
ctx_->register_sampler("x-medkit-metrics",
[this](const std::string& entity_id, const std::string& /*resource_path*/)
-> tl::expected<nlohmann::json, std::string> {
auto data = collect_metrics(entity_id);
if (!data) return tl::make_unexpected("no data for: " + entity_id);
return *data;
});
This is a convenience wrapper around the lower-level ResourceSamplerRegistry
API described in Cyclic Subscription Extensions.
Custom REST Endpoints
Any plugin can register vendor-specific endpoints via register_routes().
Use PluginContext utilities for entity validation and SOVD-compliant responses:
void register_routes(httplib::Server& server, const std::string& api_prefix) override {
// Global vendor endpoint
server.Get(api_prefix + "/x-myvendor/status",
[this](const httplib::Request&, httplib::Response& res) {
PluginContext::send_json(res, get_status_json());
});
// Entity-scoped endpoint (matches a registered capability)
server.Get((api_prefix + R"(/apps/([^/]+)/x-medkit-traces)").c_str(),
[this](const httplib::Request& req, httplib::Response& res) {
auto entity = ctx_->validate_entity_for_route(req, res, req.matches[1]);
if (!entity) return; // Error already sent
auto faults = ctx_->list_entity_faults(entity->id);
PluginContext::send_json(res, {{"entity", entity->id}, {"faults", faults}});
});
}
Use the x- prefix for vendor-specific endpoints per SOVD convention.
For entity-scoped endpoints, register a matching capability via register_capability()
or register_entity_capability() in set_context() so the endpoint appears in the
entity’s capabilities array in discovery responses.
Cyclic Subscription Extensions
Plugins can extend cyclic subscriptions by registering custom resource samplers
and transport providers during set_context().
Resource Samplers provide the data for a collection when sampled by a subscription.
Built-in samplers (data, faults, configurations, updates) are registered
by the gateway during startup. Custom samplers are registered via ResourceSamplerRegistry
on the GatewayNode:
auto* sampler_registry = node->get_sampler_registry();
sampler_registry->register_sampler("x-medkit-metrics",
[this](const std::string& entity_id, const std::string& resource_path)
-> tl::expected<nlohmann::json, std::string> {
return get_metrics(entity_id, resource_path);
});
Once registered, clients can create cyclic subscriptions on the x-medkit-metrics
collection for any entity.
Transport Providers deliver subscription data via alternative protocols (beyond
the built-in SSE transport). Register via TransportRegistry on the GatewayNode:
auto* transport_registry = node->get_transport_registry();
transport_registry->register_transport(
std::make_unique<MqttTransportProvider>(mqtt_client_));
The transport must implement SubscriptionTransportProvider (start, stop,
notify_update, protocol()). Clients specify the protocol in the subscription
creation request.
IntrospectionProvider Example
An IntrospectionProvider enriches entities in the merge pipeline. The plugin
receives all entities from earlier layers (manifest + runtime) and returns new or
enriched entities with ENRICHMENT merge policy.
The ros2_medkit_topic_beacon (push-based) and ros2_medkit_param_beacon
(pull-based) plugins are the reference implementations. ros2_medkit_topic_beacon
subscribes to /ros2_medkit/discovery, stores incoming
MedkitDiscoveryHint messages, and injects their fields during introspect().
ros2_medkit_param_beacon polls ROS 2 parameters instead:
#include "ros2_medkit_gateway/plugins/gateway_plugin.hpp"
#include "ros2_medkit_gateway/providers/introspection_provider.hpp"
using namespace ros2_medkit_gateway;
class MyIntrospectionPlugin : public GatewayPlugin, public IntrospectionProvider {
public:
std::string name() const override { return "my_introspection"; }
void configure(const nlohmann::json& config) override {
// Read plugin configuration
}
void shutdown() override {}
// IntrospectionProvider: called on every merge pipeline refresh
IntrospectionResult introspect(const IntrospectionInput& input) override {
IntrospectionResult result;
for (const auto& app : input.apps) {
// Shadow the existing app with enriched metadata
App shadow;
shadow.id = app.id;
result.new_entities.apps.push_back(shadow);
result.metadata[app.id] = {
{"x-platform-version", get_platform_version()}
};
}
return result;
}
};
// Required exports
extern "C" GATEWAY_PLUGIN_EXPORT int plugin_api_version() {
return PLUGIN_API_VERSION;
}
extern "C" GATEWAY_PLUGIN_EXPORT GatewayPlugin* create_plugin() {
return new MyIntrospectionPlugin();
}
extern "C" GATEWAY_PLUGIN_EXPORT IntrospectionProvider* get_introspection_provider(GatewayPlugin* p) {
return static_cast<MyIntrospectionPlugin*>(p);
}
The IntrospectionInput contains entity vectors from previous layers:
input.areas,input.components,input.apps,input.functions- read-only vectors of discovered entities to use as context
The returned IntrospectionResult wraps two fields:
new_entities- aNewEntitiesstruct with vectors forareas,components,apps, andfunctionsthat the plugin introducesmetadata- a map from entity ID to anlohmann::jsonobject with plugin-specific enrichment data (e.g. vendor extension fields)
New entities in new_entities only appear in responses when
allow_new_entities is true in the plugin configuration (or an equivalent
policy is set).
ScriptProvider Example
A ScriptProvider replaces the built-in filesystem-based script backend with a
custom implementation. This is useful for plugins that store scripts in a database,
fetch them from a remote service, or execute them in a sandboxed runtime.
The interface mirrors the /scripts REST endpoints - list, get, upload, delete,
execute, and control executions:
#include "ros2_medkit_gateway/plugins/gateway_plugin.hpp"
#include "ros2_medkit_gateway/providers/script_provider.hpp"
using namespace ros2_medkit_gateway;
class MyScriptPlugin : public GatewayPlugin, public ScriptProvider {
public:
std::string name() const override { return "my_scripts"; }
void configure(const nlohmann::json& /*config*/) override {}
void shutdown() override {}
// ScriptProvider: list scripts available for an entity
tl::expected<std::vector<ScriptInfo>, ScriptBackendErrorInfo>
list_scripts(const std::string& /*entity_id*/) override {
return std::vector<ScriptInfo>{};
}
// ScriptProvider: get metadata for a specific script
tl::expected<ScriptInfo, ScriptBackendErrorInfo>
get_script(const std::string& /*entity_id*/, const std::string& script_id) override {
return tl::make_unexpected(
ScriptBackendErrorInfo{ScriptBackendError::NotFound, "not found: " + script_id});
}
// ScriptProvider: upload a new script
tl::expected<ScriptUploadResult, ScriptBackendErrorInfo>
upload_script(const std::string& /*entity_id*/, const std::string& /*filename*/,
const std::string& /*content*/,
const std::optional<nlohmann::json>& /*metadata*/) override {
return tl::make_unexpected(
ScriptBackendErrorInfo{ScriptBackendError::UnsupportedType, "uploads not supported"});
}
// ScriptProvider: delete a script
tl::expected<void, ScriptBackendErrorInfo>
delete_script(const std::string& /*entity_id*/, const std::string& /*script_id*/) override {
return {};
}
// ScriptProvider: start executing a script
tl::expected<ExecutionInfo, ScriptBackendErrorInfo>
start_execution(const std::string& /*entity_id*/, const std::string& /*script_id*/,
const ExecutionRequest& /*request*/) override {
return tl::make_unexpected(
ScriptBackendErrorInfo{ScriptBackendError::NotFound, "no scripts available"});
}
// ScriptProvider: query execution status
tl::expected<ExecutionInfo, ScriptBackendErrorInfo>
get_execution(const std::string& /*entity_id*/, const std::string& /*script_id*/,
const std::string& /*execution_id*/) override {
return tl::make_unexpected(
ScriptBackendErrorInfo{ScriptBackendError::NotFound, "no executions"});
}
// ScriptProvider: control a running execution (stop or force-terminate)
tl::expected<ExecutionInfo, ScriptBackendErrorInfo>
control_execution(const std::string& /*entity_id*/, const std::string& /*script_id*/,
const std::string& /*execution_id*/,
const std::string& /*action*/) override {
return tl::make_unexpected(
ScriptBackendErrorInfo{ScriptBackendError::NotRunning, "no running execution"});
}
// ScriptProvider: delete a completed execution record
tl::expected<void, ScriptBackendErrorInfo>
delete_execution(const std::string& /*entity_id*/, const std::string& /*script_id*/,
const std::string& /*execution_id*/) override {
return {};
}
};
// Required exports
extern "C" GATEWAY_PLUGIN_EXPORT int plugin_api_version() {
return PLUGIN_API_VERSION;
}
extern "C" GATEWAY_PLUGIN_EXPORT GatewayPlugin* create_plugin() {
return new MyScriptPlugin();
}
// Required for ScriptProvider detection
extern "C" GATEWAY_PLUGIN_EXPORT ScriptProvider* get_script_provider(GatewayPlugin* p) {
return static_cast<MyScriptPlugin*>(p);
}
When a plugin ScriptProvider is detected, it replaces the built-in
DefaultScriptProvider. Only the first ScriptProvider plugin is used
(same semantics as UpdateProvider). All 8 methods must be implemented -
the ScriptManager wraps calls with null-safety and exception isolation.
Configuration - enable scripts and load the plugin:
ros2_medkit_gateway:
ros__parameters:
plugins: ["my_scripts"]
plugins.my_scripts.path: "/opt/ros2_medkit/lib/libmy_scripts.so"
scripts:
scripts_dir: "/var/lib/ros2_medkit/scripts"
The scripts.scripts_dir parameter must be set for the scripts subsystem to
initialize, even when using a plugin backend. The plugin replaces how scripts are
stored and executed, but the subsystem must be enabled first.
Multiple Plugins
Multiple plugins can be loaded simultaneously:
UpdateProvider: Only one plugin’s UpdateProvider is used (first in config order)
IntrospectionProvider: All plugins’
IntrospectionResultvalues are merged via the PluginLayer in the discovery pipeline -new_entitiesvectors are concatenated andmetadatamaps are merged. Bothros2_medkit_topic_beaconandros2_medkit_param_beaconcan be active at the same time, each contributing their own discovered entities and metadata.LogProvider: Only the first plugin’s LogProvider is used for queries (same as UpdateProvider). All LogProvider plugins receive
on_log_entry()calls as observers.ScriptProvider: Only the first plugin’s ScriptProvider is used (same as UpdateProvider).
Custom routes: All plugins can register endpoints (use unique path prefixes)
Graph Provider Plugin (ros2_medkit_graph_provider)
The gateway ships with an optional first-party plugin that exposes a ROS 2 topic graph for
each SOVD Function entity. It lives in a separate colcon package,
ros2_medkit_graph_provider, under src/ros2_medkit_plugins/.
What it does
Registers the
x-medkit-graphvendor capability on allFunctionentities.Exposes
GET /api/v1/functions/{id}/x-medkit-graphreturning a graph document with nodes (apps), edges (topic connections), per-edge frequency/latency/drop-rate metrics (sourced from the/diagnosticstopic), and an overallpipeline_status(healthy,degraded, orbroken).Supports cyclic subscriptions on the
x-medkit-graphcollection so clients can stream live graph updates.
Package layout
src/ros2_medkit_plugins/
└── ros2_medkit_graph_provider/
├── CMakeLists.txt
├── package.xml
├── include/ros2_medkit_graph_provider/graph_provider_plugin.hpp
└── src/
├── graph_provider_plugin.cpp
└── graph_provider_plugin_exports.cpp
Loading the plugin
The plugin is loaded via the gateway_params.yaml plugin list. The .so path is
resolved at launch time by gateway.launch.py using get_package_prefix(); if the
package is not installed the gateway starts normally without graph functionality:
# Excerpt from gateway.launch.py
try:
graph_provider_prefix = get_package_prefix('ros2_medkit_graph_provider')
graph_provider_path = os.path.join(
graph_provider_prefix, 'lib', 'ros2_medkit_graph_provider',
'libros2_medkit_graph_provider_plugin.so')
except PackageNotFoundError:
pass # Plugin not installed - gateway runs without graph provider
The path is then injected into the node parameters as plugins.graph_provider.path.
YAML configuration
ros2_medkit_gateway:
ros__parameters:
plugins: ["graph_provider"]
# Absolute path to the .so - set automatically by gateway.launch.py
plugins.graph_provider.path: "/opt/ros/jazzy/lib/ros2_medkit_graph_provider/libros2_medkit_graph_provider_plugin.so"
# Default expected publish frequency for topics without per-topic overrides.
# An edge whose measured frequency is below
# expected_frequency_hz_default * degraded_frequency_ratio is marked degraded.
plugins.graph_provider.expected_frequency_hz_default: 30.0
# Fraction of expected frequency below which an edge is "degraded" (0.0-1.0).
plugins.graph_provider.degraded_frequency_ratio: 0.5
# Drop-rate percentage above which an edge is marked degraded.
plugins.graph_provider.drop_rate_percent_threshold: 5.0
Per-function overrides are also supported:
# Override thresholds for a specific function
plugins.graph_provider.function_overrides.my_pipeline.expected_frequency_hz: 10.0
plugins.graph_provider.function_overrides.my_pipeline.degraded_frequency_ratio: 0.3
plugins.graph_provider.function_overrides.my_pipeline.drop_rate_percent_threshold: 2.0
Disabling the plugin
To disable graph functionality without uninstalling the package, remove "graph_provider"
from the plugins list in your params file:
plugins: []
Alternatively, simply do not install the ros2_medkit_graph_provider package -
gateway.launch.py will skip the plugin automatically.
Error Handling
If a plugin throws during any lifecycle method (configure, set_context, register_routes,
shutdown), the exception is caught and logged. The plugin is disabled but the gateway continues
operating. A failing plugin never crashes the gateway.
API Versioning
Plugins export plugin_api_version() which must return the gateway’s PLUGIN_API_VERSION.
If the version does not match, the plugin is rejected with a clear error message suggesting
a rebuild against matching gateway headers.
The current API version is 4. It is incremented when the PluginContext vtable changes
or breaking changes are made to GatewayPlugin or provider interfaces.
Build Requirements
Same compiler and ABI as the gateway executable
RTTI must be enabled for in-process plugins - the
add_plugin()path usesdynamic_castto query provider interfaces, so-fno-rttiwill break it. Shared-library plugins loaded viaPluginLoader::load()useextern "C"query functions and do not require RTTI.The
GATEWAY_PLUGIN_EXPORTmacro ensures correct symbol visibility