ros2_medkit_gateway
This section contains design documentation for the ros2_medkit_gateway project.
Architecture
Build Layers
The package compiles into two layered static libraries with a strict dependency direction:
gateway_core- middleware-neutral business logic. Sources live undersrc/core/and headers underinclude/ros2_medkit_gateway/core/. Links only header-only and C-level externals (cpp-httplib, nlohmann/json, yaml-cpp, tl::expected, jwt-cpp, OpenSSL, SQLite, dl). Carries no rclcpp / rcl_interfaces / message-package dependency. Hosts the neutral HTTP request handlers, JWT authentication, fault model (debounce, storage, cache, correlation), peer aggregation, manifest parsing, the entity cache, the neutral managers (lock, bulk-data, subscription, script, update, plugin), and every provider interface contract.gateway_ros2- ROS adapter layer. Compiles the remaining sources undersrc/and linksgateway_corepublicly viamedkit_target_dependencies. HostsGatewayNode(therclcpp::Nodeentry point), the ROS-coupled managers (data access, operation, configuration, fault facade, log, trigger), runtime discovery, the native topic sampler, and the ROS-specific provider default implementations.
The gateway_node executable and existing test targets link
gateway_ros2, so they transitively get both layers from a single
dependency. The neutral contract is enforced by two CTest checks:
gateway_core_purity(linter label) - grepscore/for any ROS-package include and fails on any match.test_gateway_core_smoke(unit label) - compiles a translation unit including a sampling ofcore/headers and links exclusively againstgateway_core+ GTest with noament_target_dependencies. Build failure indicates a transitive ROS coupling that the grep guard might miss when an include is reached through a third-party header.
Class Diagram
The following diagram shows the relationships between the main components of the gateway.
ROS 2 Medkit Gateway Class Architecture
Testing with Mock Transports
The Transport ports under core/transports/ are abstract interfaces. A
manager unit test injects a mock implementation and links only against
gateway_core + GTest, with no rclcpp on the link line. This is the
pattern used by the test_*_manager_routing.cpp tests:
class MockTopicTransport : public TopicTransport {
public:
nlohmann::json publish(const std::string &, const std::string &,
const nlohmann::json &,
std::chrono::duration<double>) override {
return nlohmann::json::object();
}
TopicSample sample(const std::string & topic,
std::chrono::duration<double>) override {
TopicSample s;
s.success = true;
s.status = "data";
s.topic = topic;
s.data = next_sample_;
return s;
}
std::pair<uint64_t, uint64_t>
count_publishers_subscribers(const std::string &) const override {
return {1, 0};
}
ros2_medkit_serialization::TypeIntrospection *
get_type_introspection() const override { return nullptr; }
nlohmann::json next_sample_ = nlohmann::json::object();
};
TEST(DataAccessManagerRouting, ReadRoutesThroughTransport) {
MockTopicTransport transport;
transport.next_sample_["value"] = 42;
DataAccessManager mgr(&transport, /* other neutral deps */);
auto result = mgr.get_topic_sample_with_fallback("/sensor", "");
ASSERT_EQ(result["data"]["value"], 42);
}
The mock is purely C++; no ROS context, executor, or domain ID is needed.
Main Components
Each entry below is tagged with the static library it compiles into:
[gateway_core] (middleware-neutral, no ROS dependency) or
[gateway_ros2] (links rclcpp / message packages).
GatewayNode
[gateway_ros2]- The main ROS 2 node that orchestrates the system - Extendsrclcpp::Node- Manages periodic discovery and cache refresh - Runs the REST server in a separate thread - Provides thread-safe access to the entity cache - Manages periodic cleanup of old action goals (60s interval)DiscoveryManager
[gateway_ros2]- Discovers ROS 2 entities and maps them to the SOVD hierarchy - Discovers Areas from node namespaces or manifest definitions - Discovers Components (synthetic groups from runtime, or explicit from manifest) - Discovers Apps from ROS 2 nodes (individual running processes) - Discovers Services and Actions using native rclcpp APIs - Attaches operations (services/actions) to their parent Apps and Components - Routes built-in graph queries through the sameIntrospectionProviderchain used by plugins - Uses O(n+m) algorithm with hash maps for efficient service/action attachmentDiscovery providers and layers:
Ros2RuntimeIntrospection
[gateway_ros2]- IntrospectionProvider wrapping ROS 2 graph queries - Maps nodes to Apps withsource: "heuristic"- Creates Functions from namespace grouping - Never creates Areas or Components (those come from manifest/HostInfoProvider) - Same interface as plugin-provided IntrospectionProviders, so the merge pipeline treats built-in graph queries identically to plugin contributionsManifestLayer
[gateway_core]- Static layer fed byManifestManager- Provides stable, semantic entity IDs from declarative YAML - Supports offline detection of failed components -manifest_onlymode bypasses the merge pipeline and routes directly throughManifestManagerHybrid mode (DiscoveryManager + MergePipeline) - Combines manifest + runtime + plugins - DiscoveryManager constructs a
MergePipelinewith the configured layers - Supports dynamic plugin layers added at runtime viaadd_plugin_layer()- Thread-safe: a mutex protects the cached merged result, all reads return by value
Merge Pipeline:
The
MergePipelineis the core engine for hybrid discovery. It:Maintains an ordered list of
DiscoveryLayerinstances (first = highest priority)Executes all layers, collects entities by ID, and merges them per-field-group
Each layer declares a
MergePolicyperFieldGroup: AUTHORITATIVE (wins), ENRICHMENT (fills empty), FALLBACK (last resort)Runs
RuntimeLinkerpost-merge to bind manifest apps to live ROS 2 nodesSuppresses runtime-origin entities that duplicate manifest entities: components/areas by namespace match, apps by ID match (gap-fill apps in uncovered namespaces survive)
Produces a
MergeReportwith conflict diagnostics, enrichment counts, and ID collision detection
Built-in Layers:
ManifestLayer- Wraps ManifestManager; IDENTITY/HIERARCHY/METADATA are AUTHORITATIVE, LIVE_DATA is ENRICHMENT (runtime wins for topics/services), STATUS is FALLBACKRuntimeLayer- Wraps Ros2RuntimeIntrospection; LIVE_DATA/STATUS are AUTHORITATIVE, METADATA is ENRICHMENT, IDENTITY/HIERARCHY are FALLBACK. SupportsGapFillConfigto control which heuristic entities are allowed when manifest is presentPluginLayer- Wraps IntrospectionProvider; all fields ENRICHMENT (plugins enrich, they don’t override). Before each layer’sdiscover()call, the pipeline populatesIntrospectionInputwith entities from all previous layers, so plugins see the current manifest + runtime entity set
OperationManager
[gateway_core]- Routes SOVD operation execution throughServiceTransportandActionTransportports - Manager body is middleware-neutral;Ros2ServiceTransportandRos2ActionTransportadapters perform the actualrclcpp::GenericClient/ action-client calls - Tracks active action goals with status, feedback, and timestamps - Subscribes to/_action/statustopics via the action transport for real-time goal status updates - Supports goal cancellation via the action transport’s cancel path - Supports SOVD capability-based control (stop maps to ROS 2 cancel) - Automatically cleans up completed goals older than 5 minutes - JSON ↔ ROS 2 message conversion is performed inside the ROS-side transport adapter viaros2_medkit_serializationRESTServer
[gateway_ros2]- Provides the HTTP/REST API (route table couples to gateway lifecycle; the individual handlers it dispatches to live ingateway_core) - Discovery endpoints:/health,/areas,/components- Data endpoints:/components/{id}/data,/components/{id}/data/{topic}- Operations endpoints:/apps/{id}/operations,/apps/{id}/operations/{op}/executions- Configurations endpoints:/apps/{id}/configurations,/apps/{id}/configurations/{param}- Scripts endpoints:/{entity_type}/{id}/scripts,/{entity_type}/{id}/scripts/{script_id}/executions- Retrieves cached entities from the GatewayNode - Uses DataAccessManager for runtime topic data access - Uses OperationManager for service/action execution - Uses ConfigurationManager for parameter CRUD operations - Uses ScriptManager for script upload and execution - Runs on configurable host and port with CORS supportConfigurationManager
[gateway_core]- Routes SOVD configuration CRUD throughParameterTransport- Manager body is middleware-neutral;Ros2ParameterTransportadapter performs therclcpp::SyncParametersClientcalls - Lists / gets / sets individual parameter values with type conversion - Provides parameter descriptors (description, constraints, read-only flag) - The transport caches parameter clients per node for efficiency - JSON ↔ ROS 2 parameter conversion happens inside the transport adapterDataAccessManager
[gateway_core]- Routes SOVD data read/write throughTopicTransport- Manager body is middleware-neutral;Ros2TopicTransportadapter performsrclcpp::GenericPublisher-backed publishing and CDR serialization - Delegates topic sampling to the attachedTopicDataProvider(pool-backed, race-free) - Checks publisher counts before sampling to skip idle topics instantly - Returns metadata (type, schema) for topics without publishers - Returns topic data as JSON with metadata (topic name, timestamp, type info) - Parallel topic sampling with configurable concurrency limit (data_provider.max_parallel_sampleson the TopicDataProvider, default: 8)TopicDataProvider / Ros2TopicDataProvider
[gateway_core / gateway_ros2]- The interface lives in the neutral layer (core/providers/data_provider.hpp); the pool-backed ROS 2 default implementation lives in the adapter layer -TopicDataProvideris a pure C++ interface consumed by HTTP handlers and managers, with no rclcpp headers required on the consumer side -Ros2TopicDataProviderkeeps one shared subscription per topic and serves many sample calls from the cached latest message, avoiding the rcl hash-map race that short-lived per-sample subscriptions used to trigger - All subscription / callback-group creation and destruction runs onRos2SubscriptionExecutor’s single worker thread - LRU cap, idle safety-net sweep, graph-change eviction, cold-wait cap for cpp-httplib liveness, and publisher-matching QoS (reliable / transient_local) - Exposes pool + executor stats onGET /healthunder thex-medkit-subscription-executorandx-medkit-data-providervendor-extension keys - See ROS 2 Subscription Architecture for the full designJsonSerializer (ros2_medkit_serialization) - Converts between JSON and ROS 2 messages (separate package, not part of the gateway layer split) - Uses
dynmsglibrary for dynamic type introspection - Serializes JSON to CDR format for publishing viaserialize()- Deserializes CDR to JSON for subscriptions viadeserialize()- Converts between deserialized ROS 2 messages and JSON viato_json()/from_json()- Provides staticyaml_to_json()utility for YAML to JSON conversion - Thread-safe and stateless design- LockManager
[gateway_core]- SOVD resource locking (ISO 17978-3, Section 7.17) Transport-agnostic lock store with
shared_mutexfor thread safetyAcquire, release, extend locks on components and apps
Scoped locks restrict protection to specific resource collections
Lazy parent propagation (lock on component protects child apps)
Lock-required enforcement via per-entity
required_scopesconfigAutomatic expiry with cyclic subscription cleanup
Plugin access via
PluginContext::check_lock/acquire_lock/release_lock
- LockManager
Data Models
[gateway_core]- Entity representations -Area- Physical or logical domain (namespace grouping) -Component- Logical grouping of Apps; can besynthetic(auto-created from namespace),topic(from topic-only namespace), ormanifest(explicitly defined) -App- Software application (ROS 2 node); individual running process linked to parent Component -ServiceInfo- Service metadata (path, name, type) -ActionInfo- Action metadata (path, name, type) -EntityCache- Thread-safe cache of discovered entities (areas, components, apps)ScriptManager
[gateway_ros2]- Manages diagnostic script upload, storage, and execution (SOVD 7.15) - Delegates to a pluggableScriptProviderbackend (set viaset_backend()) - Lists, uploads, and deletes scripts per entity - Starts script executions as POSIX subprocesses with timeout support - Tracks execution status (prepared,running,completed,failed,terminated) - Supports termination viastop(SIGTERM) andforced_termination(SIGKILL) - Built-inDefaultScriptProviderhandles filesystem storage and manifest-defined scripts - Supports concurrent execution limits and per-script timeout configuration
Triggers
The trigger subsystem implements SOVD condition-based resource change notifications. It consists of five main components:
TriggerManager
[gateway_core]- Central coordinator for trigger lifecycle (CRUD), condition evaluation, and event dispatch.Subscribes to
ResourceChangeNotifierfor resource change eventsEvaluates conditions using registered
ConditionEvaluatorinstances via theConditionRegistryUses O(1) dispatch indexing by
{collection, entity_id}for efficient notification matchingSupports entity hierarchy matching (area-level triggers catch descendant changes)
Manages pending events for SSE stream pickup with per-trigger mutexes
Persists triggers via the
TriggerStoreinterfaceData-trigger ROS subscriptions are routed through
TopicSubscriptionTransport(Ros2TopicSubscriptionTransportadapter wrapsTriggerTopicSubscriber)
ResourceChangeNotifier
[gateway_core]- Async notification hub for resource changes.Producers (FaultManager, DataAccessManager, UpdateManager, OperationManager, LogManager) call
notify()Observers (TriggerManager) register callbacks with filters
notify()is non-blocking - pushes to an internal queue processed by a dedicated worker threadFilters support collection, entity_id, and resource_path matching
ConditionRegistry
[gateway_core]- Thread-safe registry for condition evaluators.Built-in SOVD types:
OnChange,OnChangeTo,EnterRange,LeaveRangePlugins register custom evaluators with
x-prefixed namesUses
shared_mutexfor concurrent read access during evaluation
TriggerStore / SqliteTriggerStore
[gateway_core]- Persistence backend for triggers.Abstract
TriggerStoreinterface allows plugin-provided backendsDefault
SqliteTriggerStoreuses SQLite for persistent triggersStores trigger metadata, condition parameters, and evaluator state (previous values)
Supports partial updates for status changes and lifetime extensions
TriggerTopicSubscriber
[gateway_ros2]- Generic per-handle subscription executor for data triggers.Creates one
rclcpp::GenericSubscriptionperhandle_keyprovided by the callerEach trigger owns its own subscription handle - no reference-counting across triggers
Wrapped by
Ros2TopicSubscriptionTransport, which exposes the neutralTopicSubscriptionTransportinterface toTriggerManagerSubscription lifetime is tied to the trigger entry: removing the trigger drops the handle and tears down the underlying subscription
Per-handle callbacks publish samples back to
ResourceChangeNotifierfor condition evaluation