C++ API Reference

This section contains the automatically generated API documentation for the ros2_medkit C++ codebase.

Note

This documentation is generated from source code comments using Doxygen and Breathe. Run the “Docs: Build Doxygen” VS Code task or doxygen Doxyfile in the docs/ directory before building Sphinx locally. In CI, Doxygen XML is generated automatically.

ros2_medkit_gateway

The HTTP/REST gateway that exposes ROS 2 graph via SOVD-compatible API.

Classes

class GatewayNode : public rclcpp::Node

Public Functions

explicit GatewayNode(const rclcpp::NodeOptions &options = rclcpp::NodeOptions{})
~GatewayNode() override
GatewayNode(const GatewayNode&) = delete
GatewayNode &operator=(const GatewayNode&) = delete
GatewayNode(GatewayNode&&) = delete
GatewayNode &operator=(GatewayNode&&) = delete
const ThreadSafeEntityCache &get_thread_safe_cache() const

Get the thread-safe entity cache with O(1) lookups.

Returns:

Reference to ThreadSafeEntityCache

DataAccessManager *get_data_access_manager() const

Get the DataAccessManager instance.

Note

The returned pointer is valid as long as the GatewayNode exists. REST server is stopped before GatewayNode destruction to ensure safe access.

Returns:

Raw pointer to DataAccessManager (valid for lifetime of GatewayNode)

void set_topic_data_provider(std::shared_ptr<TopicDataProvider> provider)

Attach a TopicDataProvider to route topic sampling through.

Called by main() after the rclcpp::Executor is constructed and the gateway node added to it, so the provider can build its subscription node. Stores the shared_ptr to keep the provider alive for the gateway node’s lifetime and forwards the raw pointer into DataAccessManager. The pool-backed provider is the single sampling path (issue #375 race fix).

inline TopicDataProvider *get_topic_data_provider() const
Returns:

Non-owning pointer to the currently attached TopicDataProvider, or nullptr when no provider has been wired up yet.

OperationManager *get_operation_manager() const

Get the OperationManager instance.

Returns:

Raw pointer to OperationManager (valid for lifetime of GatewayNode)

DiscoveryManager *get_discovery_manager() const

Get the DiscoveryManager instance.

Returns:

Raw pointer to DiscoveryManager (valid for lifetime of GatewayNode)

ConfigurationManager *get_configuration_manager() const

Get the ConfigurationManager instance.

Returns:

Raw pointer to ConfigurationManager (valid for lifetime of GatewayNode)

FaultManager *get_fault_manager() const

Get the FaultManager instance.

Returns:

Raw pointer to FaultManager (valid for lifetime of GatewayNode)

BulkDataStore *get_bulk_data_store() const

Get the BulkDataStore instance.

Returns:

Raw pointer to BulkDataStore (valid for lifetime of GatewayNode)

LogManager *get_log_manager() const

Get the LogManager instance.

Returns:

Raw pointer to LogManager (valid for lifetime of GatewayNode)

SubscriptionManager *get_subscription_manager() const

Get the SubscriptionManager instance.

Returns:

Raw pointer to SubscriptionManager (valid for lifetime of GatewayNode)

UpdateManager *get_update_manager() const

Get the UpdateManager instance.

Returns:

Raw pointer to UpdateManager (valid for lifetime of GatewayNode), or nullptr if disabled

LockManager *get_lock_manager() const

Get the LockManager instance.

Returns:

Raw pointer to LockManager (valid for lifetime of GatewayNode), or nullptr if locking disabled

ScriptManager *get_script_manager() const

Get the ScriptManager instance.

Returns:

Raw pointer to ScriptManager (valid for lifetime of GatewayNode), or nullptr if not initialized

PluginManager *get_plugin_manager() const

Get the PluginManager instance.

Returns:

Raw pointer to PluginManager (valid for lifetime of GatewayNode)

void init_entity_freeze_frame_capture(ros2_common::Ros2SubscriptionExecutor &exec)

Wire the zero-config entity freeze-frame capture onto the shared subscription executor.

Call once after the executor is built and plugins are loaded (main.cpp). No-op when disabled or no plugins.

EntityFreezeFrameCapture *get_entity_freeze_frame_capture() const

Get the zero-config entity freeze-frame capture.

Returns:

Raw pointer, or nullptr when disabled / no plugins loaded

void set_trigger_subscription_executor(ros2_common::Ros2SubscriptionExecutor &exec)

Route the trigger topic subscriber through the shared subscription executor (issue #548).

Its per-trigger subscriptions are then created, destroyed, and dispatched on the executor’s single worker thread, so callbacks drain before teardown instead of racing the main multi-threaded executor. Call once after the executor is built (main.cpp). No-op when triggers are disabled.

void shutdown_trigger_subscriber()

Shut down the trigger topic subscriber, draining and destroying its subscriptions on the executor worker.

Idempotent. Must be called BEFORE the subscription executor is destroyed (the trigger subscriptions live on the executor’s node and are torn down via its worker). No-op when triggers are disabled. ~GatewayNode also calls it, which becomes a no-op once this has run.

void stop_rest_server()

Stop and join the REST server.

Callable from the main teardown before the subscription executor is destroyed, so no httplib handler thread can be mid-subscribe()/unsubscribe() (which dereference the subscription executor) when it is freed. Idempotent; ~GatewayNode also calls it.

void init_fault_trigger_engine()

Wire the config-less threshold-rule (fault-trigger) engine and its evaluation loop.

Call once after plugins are loaded (main.cpp). No-op when disabled (fault_triggers.enabled=false) or no plugins.

FaultTriggerEngine *get_fault_trigger_engine() const

Get the config-less fault-trigger (threshold-rule) engine.

Returns:

Raw pointer, or nullptr when disabled / no plugins loaded

ResourceSamplerRegistry *get_sampler_registry() const

Get the ResourceSamplerRegistry instance.

Returns:

Raw pointer to ResourceSamplerRegistry (valid for lifetime of GatewayNode)

TransportRegistry *get_transport_registry() const

Get the TransportRegistry instance.

Returns:

Raw pointer to TransportRegistry (valid for lifetime of GatewayNode)

std::shared_ptr<SSEClientTracker> get_sse_client_tracker() const

Get the SSEClientTracker instance.

Returns:

Shared pointer to SSEClientTracker

ResourceChangeNotifier *get_resource_change_notifier() const

Get the ResourceChangeNotifier instance.

Returns:

Raw pointer to ResourceChangeNotifier (valid for lifetime of GatewayNode)

TriggerManager *get_trigger_manager() const

Get the TriggerManager instance.

Returns:

Raw pointer to TriggerManager (valid for lifetime of GatewayNode), or nullptr if disabled

ConditionRegistry *get_condition_registry() const

Get the ConditionRegistry instance.

Returns:

Raw pointer to ConditionRegistry (valid for lifetime of GatewayNode)

AggregationManager *get_aggregation_manager() const

Get the AggregationManager instance.

Returns:

Raw pointer to AggregationManager (valid for lifetime of GatewayNode), or nullptr if disabled

void handle_entity_change_notification(const EntityChangeScope &scope)

Handle a plugin’s PluginContext::notify_entities_changed request.

Runs a single refresh_cache() pass synchronously. The scope hint is accepted and logged but the current implementation ignores it and always does a full refresh - future work may limit the rediscovery to the indicated area / component. The entry point is safe to call from any thread: refresh passes triggered by plugin notifications, the periodic refresh timer, and startup are serialized by an internal mutex inside refresh_cache() because refresh touches discovery state (e.g., the merge pipeline cached inside DiscoveryManager) that itself assumes single-threaded execution - ThreadSafeEntityCache’s own mutex is not sufficient.

void trigger_reentrant_notification_for_testing(const EntityChangeScope &scope)

Test hook: simulate a plugin calling notify_entities_changed from within its own IntrospectionProvider::introspect() callback.

Sets the per-thread in-refresh flag (the same flag the real refresh path uses) and invokes handle_entity_change_notification(scope). The gateway is expected to short-circuit the call with a warning log and return without reloading the manifest.

Exists purely to let unit tests exercise the reentrancy guard without spinning up a full plugin with a real IntrospectionProvider. Do NOT call this from production code.

void stop_discovery_refresh_for_testing()

Test hook: stop the gateway’s own discovery refresh.

Cancels the graph-event and backstop refresh timers and drops the graph event so refresh_cache() never runs again after this call. Tests that inject a known entity-cache state directly (via get_thread_safe_cache()) must call this first, otherwise a graph-event-driven refresh_cache() reconciles the cache back to the live ROS graph and silently wipes the injected entities. Do NOT call this from production code.

Public Static Functions

static size_t count_peer_nodes(const std::vector<std::pair<std::string, std::string>> &nodes_and_namespaces, const std::string &self_fqn)

Count application (peer) nodes from (name, namespace) pairs: excludes hidden nodes and the gateway’s own nodes (whose FQN starts with self_fqn).

Static and public so the startup-summary counting logic is unit-testable.

static std::string connectable_host(const std::string &bind_host)

Translate a bind address into a connectable address for the sample curl (bind-all “0.0.0.0”/”::”/”” -> loopback).

Static and public for testing.

class DiscoveryManager : public ros2_medkit_gateway::ServiceActionResolver

Orchestrates entity discovery using a configurable merge pipeline.

The discovery_mode parameter selects which layers the merge pipeline activates:

  • RUNTIME_ONLY: ROS 2 graph introspection only (no merge pipeline; the built-in Ros2RuntimeIntrospection provider is queried directly)

  • MANIFEST_ONLY: Manifest as the sole source of truth

  • HYBRID: Manifest + runtime + plugin layers merged through the pipeline

The DiscoveryManager provides a unified interface for discovering:

  • Areas: Logical groupings (manifest-only)

  • Components: Software/hardware units (HostInfoProvider, manifest, plugins)

  • Apps: Software applications (manifest, runtime nodes, plugins)

  • Functions: Functional groupings (manifest, namespaces, plugins)

See also

ros2::Ros2RuntimeIntrospection

See also

discovery::MergePipeline

See also

discovery::ManifestManager

Public Functions

explicit DiscoveryManager(rclcpp::Node *node)

Construct the discovery manager.

Parameters:

node – ROS 2 node for graph introspection (must outlive this manager)

bool initialize(const DiscoveryConfig &config)

Initialize with configuration.

Loads manifest if configured, creates appropriate strategy. For RUNTIME_ONLY mode, this is a no-op.

Parameters:

config – Discovery configuration

Returns:

true if initialization succeeded

std::vector<Area> discover_areas()

Discover all areas.

Returns:

Vector of discovered Area entities

std::vector<Component> discover_components()

Discover all components.

Returns:

Vector of discovered Component entities

std::vector<App> discover_apps()

Discover all apps.

Returns:

Vector of discovered App entities (empty in runtime-only mode)

std::vector<Function> discover_functions()

Discover all functions.

Returns:

Vector of discovered Function entities (empty in runtime-only mode)

std::vector<Function> discover_functions(const std::vector<App> &apps)

Discover functions using pre-discovered apps.

Avoids redundant ROS 2 graph introspection when apps have already been discovered in the same refresh cycle. Falls back to the no-arg overload for modes that don’t support this optimization (manifest-only, hybrid).

Parameters:

apps – Pre-discovered apps (used only in RUNTIME_ONLY mode)

Returns:

Vector of discovered Function entities

std::optional<Area> get_area(const std::string &id)

Get area by ID.

Parameters:

idArea identifier

Returns:

Area if found

std::optional<Component> get_component(const std::string &id)

Get component by ID.

Parameters:

idComponent identifier

Returns:

Component if found

std::optional<App> get_app(const std::string &id)

Get app by ID.

Parameters:

id – App identifier

Returns:

App if found

std::optional<Function> get_function(const std::string &id)

Get function by ID.

Parameters:

id – Function identifier

Returns:

Function if found

std::vector<Area> get_subareas(const std::string &area_id)

Get subareas of an area.

Parameters:

area_id – Parent area ID

Returns:

Vector of child areas

std::vector<Component> get_subcomponents(const std::string &component_id)

Get subcomponents of a component.

Parameters:

component_id – Parent component ID

Returns:

Vector of child components

std::vector<Component> get_components_for_area(const std::string &area_id)

Get components in an area.

Parameters:

area_idArea ID

Returns:

Vector of components in the area

std::vector<App> get_apps_for_component(const std::string &component_id)

Get apps for a component.

Parameters:

component_idComponent ID

Returns:

Vector of apps associated with the component

std::vector<std::string> get_hosts_for_function(const std::string &function_id)

Get host component IDs for a function.

Parameters:

function_id – Function ID

Returns:

Vector of component IDs that host the function

std::vector<ServiceInfo> discover_services()

Discover all services in the system.

Returns:

Vector of ServiceInfo with schema information

std::vector<ActionInfo> discover_actions()

Discover all actions in the system.

Returns:

Vector of ActionInfo with schema information

virtual std::optional<ServiceInfo> find_service(const std::string &component_ns, const std::string &operation_name) const override

Find a service by component namespace and operation name.

Parameters:
  • component_nsComponent namespace

  • operation_name – Service name

Returns:

ServiceInfo if found, nullopt otherwise

virtual std::optional<ActionInfo> find_action(const std::string &component_ns, const std::string &operation_name) const override

Find an action by component namespace and operation name.

Parameters:
  • component_nsComponent namespace

  • operation_name – Action name

Returns:

ActionInfo if found, nullopt otherwise

void set_topic_data_provider(TopicDataProvider *provider)

Set the topic sampler for component-topic mapping.

Parameters:

provider – Pointer to TopicDataProvider (must outlive DiscoveryManager)

void set_type_introspection(ros2_medkit_serialization::TypeIntrospection *introspection)

Set the type introspection for operation schema enrichment.

Parameters:

introspection – Pointer to TypeIntrospection (must outlive DiscoveryManager)

void refresh_topic_map()

Refresh the cached topic map.

bool is_topic_map_ready() const

Check if topic map has been built at least once.

Returns:

true if topic map is ready, false if not yet built

void add_plugin_layer(const std::string &plugin_name, IntrospectionProvider *provider)

Add a plugin layer to the merge pipeline.

Wraps an IntrospectionProvider as a PluginLayer and adds it to the pipeline. Only works in HYBRID mode.

Parameters:
  • plugin_name – Name of the plugin

  • provider – Non-owning pointer to IntrospectionProvider

void register_introspection_provider(const std::string &name, std::shared_ptr<IntrospectionProvider> provider)

Register an introspection provider with the merge pipeline.

Equivalent to add_plugin_layer for shared-ownership providers; the manager keeps the provider alive for the lifetime of the pipeline. Only meaningful in HYBRID mode.

void refresh_pipeline()

Re-execute the merge pipeline (hybrid mode only)

Call after adding plugin layers to trigger a single pipeline refresh.

discovery::ManifestManager *get_manifest_manager()

Get the manifest manager.

Returns:

Pointer to manifest manager (nullptr if not using manifest)

bool has_host_info_provider() const

Check if a host info provider is active.

Returns:

true if default component is enabled and provider exists

std::optional<Component> get_default_component() const

Get the default Component from HostInfoProvider.

Returns:

Component entity representing the local host, or nullopt if not enabled

inline DiscoveryMode get_mode() const

Get current discovery mode.

Returns:

Active discovery mode

std::string get_strategy_name() const

Get a human-readable name for the active discovery configuration.

Returns:

One of “runtime”, “manifest”, “hybrid”.

std::optional<discovery::MergeReport> get_merge_report() const

Get the last merge pipeline report (hybrid mode only)

Returns:

MergeReport if in hybrid mode, nullopt otherwise

std::optional<discovery::LinkingResult> get_linking_result() const

Get the last linking result (hybrid mode only)

Returns:

LinkingResult if in hybrid mode, nullopt otherwise

class DataAccessManager

Application service for topic publish + sample.

Pure C++; ROS-side I/O is performed by the injected TopicTransport adapter (typically Ros2TopicTransport). Type introspection and the per-entity TopicDataProvider pointer remain on the manager because they are consumed directly by handlers and the discovery manager.

Public Functions

explicit DataAccessManager(std::shared_ptr<TopicTransport> transport, double topic_sample_timeout_sec = 1.0)
Parameters:
  • transport – Concrete TopicTransport adapter. Manager takes shared ownership.

  • topic_sample_timeout_sec – Default sample timeout in seconds. Used when callers pass a negative timeout.

~DataAccessManager() = default
DataAccessManager(const DataAccessManager&) = delete
DataAccessManager &operator=(const DataAccessManager&) = delete
DataAccessManager(DataAccessManager&&) = delete
DataAccessManager &operator=(DataAccessManager&&) = delete
json publish_to_topic(const std::string &topic_path, const std::string &msg_type, const json &data, double timeout_sec = 5.0)

Publish data to a specific topic.

Parameters:
  • topic_path – Full topic path (e.g., /chassis/brakes/command).

  • msg_type – ROS 2 message type (e.g., std_msgs/msg/Float32).

  • data – JSON data to publish.

  • timeout_sec – Timeout for the publish operation.

Returns:

JSON with publish status.

json get_topic_sample_with_fallback(const std::string &topic_name, double timeout_sec = -1.0)

Get topic sample with fallback to metadata on timeout.

If the topic is publishing, returns actual data with type info. If the topic times out, returns metadata (type, schema, pub/sub counts) instead of an error.

Parameters:
  • topic_name – Full topic path.

  • timeout_sec – Timeout for data retrieval. Use -1.0 to use the configured default.

json get_topic_sample_native(const std::string &topic_name, double timeout_sec = 1.0)

Get single topic sample using the native fast path.

Fast path for single-topic sampling. When no publishers are present, returns metadata-only without calling into the transport.

Parameters:
  • topic_name – Full topic path.

  • timeout_sec – Timeout for sampling (only used if topic has publishers).

inline ros2_medkit_serialization::TypeIntrospection *get_type_introspection() const

Get the type introspection instance (used by handlers and discovery).

Forwarded to the transport adapter, which owns the rclcpp-coupled implementation. May return nullptr for transports that do not support introspection.

inline void set_topic_data_provider(TopicDataProvider *provider)

Attach a TopicDataProvider for sampling.

The provider owns the pool-backed subscription path. Non-owning pointer; caller retains ownership. Safe to call once at wiring time.

inline TopicDataProvider *get_topic_data_provider() const
inline double get_topic_sample_timeout() const

Get the configured topic sample timeout in seconds.

class OperationManager

Application service for ROS 2 service / action operations.

Pure C++; ROS-side I/O is performed by the injected ServiceTransport and ActionTransport adapters. Goal tracking, UUID generation, type validation and component-namespace resolution remain in the manager body.

Public Types

using LogSink = std::function<void(int level, std::string_view message)>

Sink for internal diagnostics (stuck-goal eviction, etc.).

Keeps OperationManager middleware-neutral while preserving observability when the gateway adapter forwards to /rosout. May be null - the sink is then a no-op, which is the production behaviour today.

Public Functions

OperationManager(std::shared_ptr<ServiceTransport> service_transport, std::shared_ptr<ActionTransport> action_transport, ServiceActionResolver *resolver, int service_call_timeout_sec = 10, LogSink log_sink = nullptr)
Parameters:
  • service_transport – Concrete ServiceTransport adapter (typically Ros2ServiceTransport).

  • action_transport – Concrete ActionTransport adapter (typically Ros2ActionTransport).

  • resolver – Service / action lookup interface (DiscoveryManager implements it). Must outlive this manager. May be nullptr in tests that do not exercise the component-name resolution paths.

  • service_call_timeout_sec – Timeout in seconds applied to every service / action call.

  • log_sink – Optional sink for internal diagnostics. Pass nullptr to silence stuck-goal eviction warnings (default).

~OperationManager()
OperationManager(const OperationManager&) = delete
OperationManager &operator=(const OperationManager&) = delete
OperationManager(OperationManager&&) = delete
OperationManager &operator=(OperationManager&&) = delete
void shutdown()

Idempotent teardown.

Clears tracked goals and unsubscribes from status topics. Subsequent calls are no-ops. Transport teardown is owned by the transport destructors.

void set_notifier(ResourceChangeNotifier *notifier)

Set optional notifier for broadcasting operation status changes to trigger subsystem.

ServiceCallResult call_service(const std::string &service_path, const std::string &service_type, const json &request)

Call a ROS 2 service synchronously through the ServiceTransport.

ServiceCallResult call_component_service(const std::string &component_ns, const std::string &operation_name, const std::optional<std::string> &service_type, const json &request)

Find and call a service by component and operation name.

Resolves the path / type via the ServiceActionResolver when type is unset.

ActionSendGoalResult send_action_goal(const std::string &action_path, const std::string &action_type, const json &goal, const std::string &entity_id = "")

Send a goal to an action server through the ActionTransport.

ActionSendGoalResult send_component_action_goal(const std::string &component_ns, const std::string &operation_name, const std::optional<std::string> &action_type, const json &goal, const std::string &entity_id = "")

Send a goal using component namespace + operation name.

Resolves through the ServiceActionResolver when type is unset.

ActionCancelResult cancel_action_goal(const std::string &action_path, const std::string &goal_id)

Cancel a running action goal.

ActionGetResultResult get_action_result(const std::string &action_path, const std::string &action_type, const std::string &goal_id)

Get the result of a completed action.

std::optional<ActionGoalInfo> get_tracked_goal(const std::string &goal_id) const

Get tracked goal info by goal_id.

std::vector<ActionGoalInfo> list_tracked_goals() const

List all tracked goals.

std::vector<ActionGoalInfo> get_goals_for_action(const std::string &action_path) const

Get all goals for a specific action path, sorted by created_at (newest first).

std::optional<ActionGoalInfo> get_latest_goal_for_action(const std::string &action_path) const

Get the most recent goal for a specific action path.

void update_goal_status(const std::string &goal_id, ActionGoalStatus status)

Update goal status in tracking. No-op if the goal is unknown.

void update_goal_feedback(const std::string &goal_id, const json &feedback)

Update goal feedback in tracking. No-op if the goal is unknown.

void cleanup_old_goals(std::chrono::seconds max_age = std::chrono::seconds(300))

Remove completed goals older than max_age.

Forwards the unsubscribe request to the action transport for each path that becomes empty.

void subscribe_to_action_status(const std::string &action_path)

Subscribe (idempotently) to action status updates.

Wires the transport callback into update_goal_status() so external status changes update the tracking map on the transport’s executor thread.

void unsubscribe_from_action_status(const std::string &action_path)

Unsubscribe from action status updates. Idempotent.

void inject_tracked_goal_for_testing(ActionGoalInfo info)

Test-only helper: inject a fully-formed ActionGoalInfo directly into the tracking map.

Used by unit tests to exercise paths (e.g. stuck-goal eviction) without driving real action server traffic. Not part of the production API.

Public Static Functions

static bool is_valid_message_type(const std::string &type)

Validate message type format (package/srv/Type or package/action/Type or package/msg/Type).

static bool is_valid_uuid_hex(const std::string &uuid_hex)

Validate UUID hex string format (32 hex characters).

static bool is_service_type(const std::string &type)

Check if type is a service type (contains /srv/).

static bool is_action_type(const std::string &type)

Check if type is an action type (contains /action/).

Public Static Attributes

static constexpr int kLogLevelWarn = 30

Severity used by the OperationManager’s log_sink callback.

Numeric values match rcl/rcutils log levels (30=WARN, 40=ERROR) so an adapter can forward to RCLCPP_* macros without translation. Mirrors LogManager’s kLogLevelWarn / kLogLevelError contract.

static constexpr int kLogLevelError = 40
class ConfigurationManager

Application service for ROS 2 node parameters.

Pure C++; ROS-side I/O is performed by the injected ParameterTransport adapter (typically Ros2ParameterTransport). All rclcpp::AsyncParametersClient usage, the rclcpp::Parameter defaults cache, the negative-cache for unreachable nodes, the spin_mutex serialising parameter-client spins, and the JSON <-> rclcpp::ParameterValue conversion helpers live in the adapter.

The manager retains the high-level orchestration:

  • shutdown ordering contract (idempotent shutdown forwards to transport)

  • reset orchestration (compose set_parameter with cached default values)

  • the public CRUD surface consumed by handler_context handlers

Self-node short-circuiting (avoiding IPC for the gateway’s own parameters) is delegated to the transport because identifying the gateway’s own FQN requires the rclcpp node handle.

Public Functions

explicit ConfigurationManager(std::shared_ptr<ParameterTransport> transport)
Parameters:

transport – Concrete ParameterTransport adapter. Manager takes shared ownership.

~ConfigurationManager()
ConfigurationManager(const ConfigurationManager&) = delete
ConfigurationManager &operator=(const ConfigurationManager&) = delete
ConfigurationManager(ConfigurationManager&&) = delete
ConfigurationManager &operator=(ConfigurationManager&&) = delete
void shutdown()

Idempotent teardown.

Forwards to the transport’s shutdown so cached parameter clients drop their references before rclcpp::shutdown() runs. Must be called before rclcpp::shutdown() to prevent use-after-free of the parameter-client internal node held by the transport.

ParameterResult list_parameters(const std::string &node_name)

List all parameters for a node.

Parameters:

node_name – Fully qualified node name (e.g., “/powertrain/engine/engine_temp_sensor”).

Returns:

ParameterResult with array of {name, value, type} objects.

ParameterResult get_parameter(const std::string &node_name, const std::string &param_name)

Get a specific parameter value.

Parameters:
  • node_name – Fully qualified node name.

  • param_name – Parameter name.

Returns:

ParameterResult with {name, value, type, description, read_only}.

ParameterResult set_parameter(const std::string &node_name, const std::string &param_name, const json &value)

Set a parameter value.

Parameters:
  • node_name – Fully qualified node name.

  • param_name – Parameter name.

  • value – New value (JSON type will be converted to the appropriate ROS 2 type by the transport).

Returns:

ParameterResult with {name, value, type}.

ParameterResult reset_parameter(const std::string &node_name, const std::string &param_name)

Reset a specific parameter to its default (initial) value.

Parameters:
  • node_name – Fully qualified node name.

  • param_name – Parameter name.

Returns:

ParameterResult with reset parameter info.

ParameterResult reset_all_parameters(const std::string &node_name)

Reset all parameters of a node to their default (initial) values.

Parameters:

node_name – Fully qualified node name.

Returns:

ParameterResult with count of reset parameters.

Data Models

struct QosProfile

QoS profile information for a topic endpoint.

Public Functions

inline json to_json() const

Public Members

std::string reliability

“reliable”, “best_effort”, “system_default”, “unknown”

std::string durability

“volatile”, “transient_local”, “system_default”, “unknown”

std::string history

“keep_last”, “keep_all”, “system_default”, “unknown”

size_t depth = {0}

History depth (for keep_last)

std::string liveliness

“automatic”, “manual_by_topic”, “system_default”, “unknown”

struct TopicEndpoint

Information about an endpoint (publisher or subscriber) on a topic.

Public Functions

inline std::string fqn() const

Get fully qualified node name.

inline json to_json() const

Public Members

std::string node_name

Name of the node (e.g., “controller_server”)

std::string node_namespace

Namespace of the node (e.g., “/navigation”)

std::string topic_type

Message type (e.g., “geometry_msgs/msg/Twist”)

QosProfile qos

QoS profile of this endpoint.

struct TopicConnection

Topic with its publishers and subscribers.

Public Functions

inline json to_json() const

Public Members

std::string topic_name

Full topic path (e.g., “/cmd_vel”)

std::string topic_type

Message type.

std::vector<TopicEndpoint> publishers
std::vector<TopicEndpoint> subscribers
struct Area

SOVD Area entity - represents a logical grouping (ROS 2 namespace)

Areas are derived from ROS 2 namespaces or defined in manifests. They provide a hierarchical organization of components.

Public Functions

inline json to_json() const

Convert to JSON representation.

SOVD EntityReference fields: id, name, href, translation_id, tags ROS 2 extensions in x-medkit: namespace, type, description, parentAreaId

Returns:

JSON object with area data

inline json to_entity_reference(const std::string &base_url) const

Create SOVD EntityReference format (strictly compliant)

Parameters:

base_url – Base URL for self links

Returns:

JSON object in EntityReference format: id, name, href, [translationId, tags]

inline json to_capabilities(const std::string &base_url) const

Create SOVD Entity Capabilities format (strictly compliant)

Parameters:

base_url – Base URL for capability links

Returns:

JSON object listing available sub-resources

Public Members

std::string id

Unique identifier (e.g., “powertrain”)

std::string name

Human-readable name (e.g., “Powertrain System”)

std::string namespace_path

ROS 2 namespace path (e.g., “/powertrain”)

std::string type = "Area"

Entity type (always “Area”)

std::string translation_id

Internationalization key.

std::string description

Human-readable description.

std::vector<std::string> tags

Tags for filtering.

std::string parent_area_id

Parent area ID for sub-areas.

std::string source

Origin of this area (e.g., “manifest”, “heuristic”)

std::vector<std::string> contributors

Aggregation provenance: “local” and/or “peer:<name>”.

struct Component

SOVD Component entity - represents a software/hardware component (ROS 2 node)

Components are derived from ROS 2 nodes or defined in manifests. They expose operations (services/actions), data (topics), and configurations (parameters).

Public Functions

inline json to_json() const

Convert to JSON representation.

SOVD EntityReference fields: id, name, href, translation_id, tags ROS 2 extensions in x-medkit: namespace, fqn, entityType, area, source, variant, etc.

Returns:

JSON object with component data

inline json to_entity_reference(const std::string &base_url) const

Create SOVD EntityReference format (strictly compliant)

Parameters:

base_url – Base URL for self links

Returns:

JSON object in EntityReference format: id, name, href, [translationId, tags]

inline json to_capabilities(const std::string &base_url) const

Create SOVD Entity Capabilities format (strictly compliant)

Parameters:

base_url – Base URL for capability links

Returns:

JSON object listing available sub-resources

Public Members

std::string id

Unique identifier (node name)

std::string name

Human-readable name.

std::string namespace_path

ROS 2 namespace path.

std::string fqn

Fully qualified name (namespace + id)

std::string type = "Component"

Entity type (always “Component”)

std::string area

Parent area ID.

std::string source = "node"

Discovery source: “node”, “topic”, or “manifest”.

std::string translation_id

Internationalization key.

std::string description

Human-readable description.

std::string variant

Hardware variant identifier.

std::vector<std::string> tags

Tags for filtering.

std::string parent_component_id

Parent component ID for sub-components.

std::vector<std::string> depends_on

Component IDs this component depends on.

std::vector<std::string> contributors

Aggregation provenance: “local” and/or “peer:<name>”.

std::vector<ServiceInfo> services

Services exposed by this component.

std::vector<ActionInfo> actions

Actions exposed by this component.

ComponentTopics topics

Topics this component publishes/subscribes.

std::optional<json> host_metadata

Host system metadata (for runtime default component)

AssetIdentity identity

Asset-identity nameplate (merged across sources, per-field provenance)

std::optional<bool> external

Tri-state: nullopt = no layer classified this component, true = non-ROS external asset (PLC/fieldbus/device), false = explicitly a ROS component.

Mirrors App::external so a device Component owns its fault_manager faults without a synthetic child App (#516).

ros2_medkit_fault_manager

Central fault storage and management node.

class FaultManagerNode : public rclcpp::Node

Central fault manager node.

Provides service interfaces for fault reporting, querying, and clearing. Supports configurable storage backends (memory or SQLite) via ROS parameters.

Parameters:

  • storage_type (string): “memory” or “sqlite” (default: “sqlite”)

  • database_path (string): Path to SQLite database file (default: “/var/lib/ros2_medkit/faults.db”) Use “:memory:” for in-memory SQLite database (useful for testing)

Public Functions

explicit FaultManagerNode(const rclcpp::NodeOptions &options = rclcpp::NodeOptions())
~FaultManagerNode() override
FaultManagerNode(const FaultManagerNode&) = delete
FaultManagerNode &operator=(const FaultManagerNode&) = delete
FaultManagerNode(FaultManagerNode&&) = delete
FaultManagerNode &operator=(FaultManagerNode&&) = delete
inline const FaultStorage &get_storage() const

Get read-only access to fault storage (for testing)

inline FaultStorage &get_storage_for_test()

Get mutable access to fault storage (for testing only).

inline const FaultAuditLog *get_audit_log_for_test() const

Get the tamper-evident audit log (nullptr when disabled), for testing only.

inline uint64_t audit_dropped_writes() const

Health signal: number of audit transitions that failed to append (and were therefore lost from the chain).

0 when the audit is healthy or disabled. IN-PROCESS ONLY: this is a C++ getter, not exposed over any service/REST/ health surface yet, so an operator cannot read it at runtime in this revision (a runtime read/verify/health surface is future work).

inline bool audit_healthy() const

Health signal: false once any audit append has failed.

A compliance-strict deployment should treat a false here (or a non-zero audit_dropped_writes()) as the audit chain being incomplete. IN-PROCESS ONLY (see above): not yet observable at runtime by an operator.

inline void audit_transition_for_test(const char *transition, const ros2_medkit_msgs::msg::Fault &fault)

Test-only: drive one audit append through the same failure-handling path the service handlers use.

Throws when audit_log.fail_closed is set and the append fails; otherwise observe audit_dropped_writes()/audit_healthy().

inline const std::string &get_storage_type() const

Get the storage type being used.

inline int capture_pool_size_for_test() const
inline int capture_queue_depth_for_test() const
inline QueueFullPolicy capture_queue_full_policy_for_test() const

Public Static Functions

static bool matches_entity(const std::vector<std::string> &reporting_sources, const std::string &entity_id)

Check if entity matches any reporting source.

Parameters:
  • reporting_sources – List of reporting sources from fault

  • entity_id – Entity ID to match (exact match or as suffix of FQN)

Returns:

true if entity_id matches any source

class FaultStorage

Abstract interface for fault storage backends.

Subclassed by ros2_medkit_fault_manager::InMemoryFaultStorage, ros2_medkit_fault_manager::SqliteFaultStorage

Public Functions

virtual ~FaultStorage() = default
virtual void set_debounce_config(const DebounceConfig &config) = 0

Set debounce configuration.

virtual DebounceConfig get_debounce_config() const = 0

Get current debounce configuration.

virtual bool report_fault_event(const std::string &fault_code, uint8_t event_type, uint8_t severity, const std::string &description, const std::string &source_id, const rclcpp::Time &timestamp, const DebounceConfig &config) = 0

Report a fault event (FAILED or PASSED)

Parameters:
  • fault_code – Global fault identifier

  • event_type – EVENT_FAILED (0) or EVENT_PASSED (1)

  • severity – Fault severity level (only used for FAILED events)

  • description – Human-readable description (only used for FAILED events)

  • source_id – Reporting source identifier

  • timestamp – Current time for tracking

  • config – Debounce configuration to apply for this event (resolved per-entity by the node)

Returns:

true if this is a new occurrence (new fault or reactivated CLEARED fault), false if existing active fault was updated

virtual std::vector<ros2_medkit_msgs::msg::Fault> list_faults(bool filter_by_severity, uint8_t severity, const std::vector<std::string> &statuses) const = 0

Get faults matching filter criteria.

Parameters:
  • filter_by_severity – Whether to filter by severity

  • severity – Severity level to filter (if filter_by_severity is true)

  • statuses – List of statuses to include (empty = CONFIRMED only)

Returns:

Vector of matching faults

virtual std::optional<ros2_medkit_msgs::msg::Fault> get_fault(const std::string &fault_code) const = 0

Get a single fault by fault_code.

Parameters:

fault_code – The fault code to look up

Returns:

The fault if found, nullopt otherwise

virtual bool clear_fault(const std::string &fault_code) = 0

Clear a fault by fault_code (manual acknowledgment)

Parameters:

fault_code – The fault code to clear

Returns:

true if fault was found and cleared, false if not found

virtual size_t size() const = 0

Get total number of stored faults.

virtual bool contains(const std::string &fault_code) const = 0

Check if a fault exists.

virtual std::vector<std::string> check_time_based_confirmation(const rclcpp::Time &current_time) = 0

Check and confirm PREFAILED faults that have been pending too long (time-based confirmation)

Parameters:

current_time – Current timestamp for age calculation

Returns:

Fault codes that were confirmed by this call (so the caller can audit each).

inline virtual void set_max_snapshots_per_fault(size_t)

Set maximum snapshots per fault code (0 = unlimited)

virtual void store_snapshot(const SnapshotData &snapshot) = 0

Store a snapshot captured when a fault was confirmed.

Parameters:

snapshot – The snapshot data to store

virtual std::vector<SnapshotData> get_snapshots(const std::string &fault_code, const std::string &topic_filter = "") const = 0

Get snapshots for a fault.

Parameters:
  • fault_code – The fault code to get snapshots for

  • topic_filter – Optional topic filter (empty = all topics)

Returns:

Vector of snapshots for the fault

virtual void store_freeze_frame(const FreezeFrameData &frame) = 0

Store the compact freeze-frame captured for a fault (JSON dict of topic values).

Keyed by fault_code: a later capture for the same code replaces the frame. The frame is retained across clear_fault so the confirmed-state record survives acknowledgement. Storage is bounded by the number of distinct fault codes (one row per code, replaced in place); rows are never evicted. Faults themselves are never deleted (clear_fault only flips status), so there is currently no delete hook to tie eviction to.

Parameters:

frame – The freeze-frame to store

virtual std::optional<FreezeFrameData> get_freeze_frame(const std::string &fault_code) const = 0

Get the freeze-frame captured for a fault, if any.

Parameters:

fault_code – The fault code to look up

Returns:

The freeze-frame if one was captured, nullopt otherwise (including fault codes with no capture configured, which never get a row)

virtual void store_rosbag_file(const RosbagFileInfo &info) = 0

Store rosbag file metadata for a fault.

Parameters:

info – The rosbag file info to store (replaces any existing entry for fault_code)

inline virtual void store_rosbag_files(const std::vector<RosbagFileInfo> &infos)

Store one row per fault of a burst that shares a recording.

Backends with real transactions (SQLite) commit all rows atomically, so a crash mid-store never leaves part of the burst without its lookup key. Default: plain loop.

Parameters:

infos – The rows to store (typically all pointing at one file_path)

virtual std::optional<RosbagFileInfo> get_rosbag_file(const std::string &fault_code) const = 0

Get rosbag file info for a fault.

Parameters:

fault_code – The fault code to get rosbag for

Returns:

Rosbag file info if exists, nullopt otherwise

virtual bool delete_rosbag_file(const std::string &fault_code) = 0

Delete rosbag file record and the actual file for a fault.

Faults from one burst can share a recording; the file is unlinked only with the last record that references it.

Parameters:

fault_code – The fault code to delete rosbag for

Returns:

true if record was deleted, false if not found

inline virtual size_t delete_rosbag_files(const std::vector<std::string> &fault_codes)

Delete the records of several faults (typically the whole burst behind one recording).

Backends with real transactions (SQLite) remove the rows atomically and unlink the file only after the commit, so a crash mid-delete never leaves a row pointing at a removed bag. Default: plain loop.

Parameters:

fault_codes – The fault codes to delete rosbag records for

Returns:

Number of records actually deleted

virtual size_t get_total_rosbag_storage_bytes() const = 0

Get total size of all stored rosbag files in bytes, counting a shared recording once regardless of how many faults reference it.

Returns:

Total size in bytes

virtual std::vector<RosbagFileInfo> get_all_rosbag_files() const = 0

Get all rosbag files ordered by creation time (oldest first)

Returns:

Vector of rosbag file info

virtual std::vector<RosbagFileInfo> list_rosbags_for_entity(const std::string &entity_fqn) const = 0

Get rosbags for all faults associated with an entity.

Parameters:

entity_fqn – The entity’s fully qualified name to filter by

Returns:

Vector of rosbag file info for faults reported by this entity

virtual std::vector<ros2_medkit_msgs::msg::Fault> get_all_faults() const = 0

Get all stored faults regardless of status (for filtering)

Returns:

Vector of all faults in storage

inline virtual std::vector<std::string> reclassify_healed_as_cleared()

One-time startup cleanup: reclassify HEALED faults as CLEARED.

Called when healing is disabled, so a HEALED row left by a previous (healing-enabled) run does not behave inconsistently under the latch. Default is a no-op (in-memory storage starts empty).

Returns:

fault codes of the reclassified faults, so the caller can audit each transition

class InMemoryFaultStorage : public ros2_medkit_fault_manager::FaultStorage

Thread-safe in-memory fault storage implementation.

Public Functions

InMemoryFaultStorage() = default
virtual void set_debounce_config(const DebounceConfig &config) override

Set debounce configuration.

virtual DebounceConfig get_debounce_config() const override

Get current debounce configuration.

virtual bool report_fault_event(const std::string &fault_code, uint8_t event_type, uint8_t severity, const std::string &description, const std::string &source_id, const rclcpp::Time &timestamp, const DebounceConfig &config) override

Report a fault event (FAILED or PASSED)

Parameters:
  • fault_code – Global fault identifier

  • event_type – EVENT_FAILED (0) or EVENT_PASSED (1)

  • severity – Fault severity level (only used for FAILED events)

  • description – Human-readable description (only used for FAILED events)

  • source_id – Reporting source identifier

  • timestamp – Current time for tracking

  • config – Debounce configuration to apply for this event (resolved per-entity by the node)

Returns:

true if this is a new occurrence (new fault or reactivated CLEARED fault), false if existing active fault was updated

virtual std::vector<ros2_medkit_msgs::msg::Fault> list_faults(bool filter_by_severity, uint8_t severity, const std::vector<std::string> &statuses) const override

Get faults matching filter criteria.

Parameters:
  • filter_by_severity – Whether to filter by severity

  • severity – Severity level to filter (if filter_by_severity is true)

  • statuses – List of statuses to include (empty = CONFIRMED only)

Returns:

Vector of matching faults

virtual std::optional<ros2_medkit_msgs::msg::Fault> get_fault(const std::string &fault_code) const override

Get a single fault by fault_code.

Parameters:

fault_code – The fault code to look up

Returns:

The fault if found, nullopt otherwise

virtual bool clear_fault(const std::string &fault_code) override

Clear a fault by fault_code (manual acknowledgment)

Parameters:

fault_code – The fault code to clear

Returns:

true if fault was found and cleared, false if not found

virtual size_t size() const override

Get total number of stored faults.

virtual bool contains(const std::string &fault_code) const override

Check if a fault exists.

virtual std::vector<std::string> check_time_based_confirmation(const rclcpp::Time &current_time) override

Check and confirm PREFAILED faults that have been pending too long (time-based confirmation)

Parameters:

current_time – Current timestamp for age calculation

Returns:

Fault codes that were confirmed by this call (so the caller can audit each).

virtual void set_max_snapshots_per_fault(size_t max_count) override

Set maximum snapshots per fault code (0 = unlimited)

virtual void store_snapshot(const SnapshotData &snapshot) override

Store a snapshot captured when a fault was confirmed.

Parameters:

snapshot – The snapshot data to store

virtual std::vector<SnapshotData> get_snapshots(const std::string &fault_code, const std::string &topic_filter = "") const override

Get snapshots for a fault.

Parameters:
  • fault_code – The fault code to get snapshots for

  • topic_filter – Optional topic filter (empty = all topics)

Returns:

Vector of snapshots for the fault

virtual void store_freeze_frame(const FreezeFrameData &frame) override

Store the compact freeze-frame captured for a fault (JSON dict of topic values).

Keyed by fault_code: a later capture for the same code replaces the frame. The frame is retained across clear_fault so the confirmed-state record survives acknowledgement. Storage is bounded by the number of distinct fault codes (one row per code, replaced in place); rows are never evicted. Faults themselves are never deleted (clear_fault only flips status), so there is currently no delete hook to tie eviction to.

Parameters:

frame – The freeze-frame to store

virtual std::optional<FreezeFrameData> get_freeze_frame(const std::string &fault_code) const override

Get the freeze-frame captured for a fault, if any.

Parameters:

fault_code – The fault code to look up

Returns:

The freeze-frame if one was captured, nullopt otherwise (including fault codes with no capture configured, which never get a row)

virtual void store_rosbag_file(const RosbagFileInfo &info) override

Store rosbag file metadata for a fault.

Parameters:

info – The rosbag file info to store (replaces any existing entry for fault_code)

virtual std::optional<RosbagFileInfo> get_rosbag_file(const std::string &fault_code) const override

Get rosbag file info for a fault.

Parameters:

fault_code – The fault code to get rosbag for

Returns:

Rosbag file info if exists, nullopt otherwise

virtual bool delete_rosbag_file(const std::string &fault_code) override

Delete rosbag file record and the actual file for a fault.

Faults from one burst can share a recording; the file is unlinked only with the last record that references it.

Parameters:

fault_code – The fault code to delete rosbag for

Returns:

true if record was deleted, false if not found

virtual size_t get_total_rosbag_storage_bytes() const override

Get total size of all stored rosbag files in bytes, counting a shared recording once regardless of how many faults reference it.

Returns:

Total size in bytes

virtual std::vector<RosbagFileInfo> get_all_rosbag_files() const override

Get all rosbag files ordered by creation time (oldest first)

Returns:

Vector of rosbag file info

virtual std::vector<RosbagFileInfo> list_rosbags_for_entity(const std::string &entity_fqn) const override

Get rosbags for all faults associated with an entity.

Parameters:

entity_fqn – The entity’s fully qualified name to filter by

Returns:

Vector of rosbag file info for faults reported by this entity

virtual std::vector<ros2_medkit_msgs::msg::Fault> get_all_faults() const override

Get all stored faults regardless of status (for filtering)

Returns:

Vector of all faults in storage

virtual std::vector<std::string> reclassify_healed_as_cleared() override

One-time startup cleanup: reclassify HEALED faults as CLEARED.

Called when healing is disabled, so a HEALED row left by a previous (healing-enabled) run does not behave inconsistently under the latch. Default is a no-op (in-memory storage starts empty).

Returns:

fault codes of the reclassified faults, so the caller can audit each transition

ros2_medkit_fault_reporter

Client library for reporting faults to the fault manager.

class FaultReporter

Client library for reporting faults to the central FaultManager.

Provides a simple API for ROS 2 nodes to report faults with optional local filtering to reduce noise from repeated fault occurrences.

Example usage:

class MyNode : public rclcpp::Node {
 public:
  MyNode() : Node("my_node") {
    reporter_ = std::make_unique<FaultReporter>(
        shared_from_this(), get_fully_qualified_name());
  }

  void on_error() {
    reporter_->report("SENSOR_FAILURE", Fault::SEVERITY_ERROR, "Sensor timeout");
  }

 private:
  std::unique_ptr<FaultReporter> reporter_;
};

Inside a lifecycle node, construct it from the node directly (no shared_from_this needed):

class MyLifecycleNode : public rclcpp_lifecycle::LifecycleNode {
 public:
  MyLifecycleNode() : LifecycleNode("my_node") {}

  rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
  on_configure(const rclcpp_lifecycle::State &) override {
    reporter_ = std::make_unique<FaultReporter>(*this, get_fully_qualified_name());
    return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
  }
};

Public Functions

FaultReporter(const rclcpp::node_interfaces::NodeBaseInterface::SharedPtr &node_base, const rclcpp::node_interfaces::NodeGraphInterface::SharedPtr &node_graph, const rclcpp::node_interfaces::NodeServicesInterface::SharedPtr &node_services, const rclcpp::node_interfaces::NodeParametersInterface::SharedPtr &node_params, const rclcpp::Logger &logger, const std::string &source_id, const std::string &service_name = "/fault_manager/report_fault")

Base Constructor for FaultReporter.

Parameters:
  • node_base – ROS2 Node Base Interface for creating the service client

  • node_graph – ROS2 Node Graph Interface for creating the service client

  • node_services – ROS2 Node Services Interface for creating the service client

  • node_params – ROS2 Node Parameters Interface for loading parameters

  • logger – ROS2 Node Logger for reporting logs

  • source_id – Identifier for this reporter (typically node’s FQN)

  • service_name – Name of the ReportFault service (default: /fault_manager/report_fault)

FaultReporter(const rclcpp::Node::SharedPtr &node, const std::string &source_id, const std::string &service_name = "/fault_manager/report_fault")

Construct a FaultReporter via Node Shared Ptr (maintain backwards compatibility)

Parameters:
  • node – The ROS 2 node to use for service client and parameters

  • source_id – Identifier for this reporter (typically node’s FQN)

  • service_name – Name of the ReportFault service (default: /fault_manager/report_fault)

FaultReporter(rclcpp::Node &node, const std::string &source_id, const std::string &service_name = "/fault_manager/report_fault")

Construct a FaultReporter via Node.

Parameters:
  • node – The ROS 2 node to use for service client and parameters

  • source_id – Identifier for this reporter (typically node’s FQN)

  • service_name – Name of the ReportFault service (default: /fault_manager/report_fault)

FaultReporter(rclcpp_lifecycle::LifecycleNode &node, const std::string &source_id, const std::string &service_name = "/fault_manager/report_fault")

Construct a FaultReporter via Lifecycle Node.

The node reference is non-const because the ROS 2 node interface getters (get_node_base_interface() and friends) are non-const member functions.

Parameters:
  • node – The ROS 2 node to use for service client and parameters

  • source_id – Identifier for this reporter (typically node’s FQN)

  • service_name – Name of the ReportFault service (default: /fault_manager/report_fault)

FaultReporter(const rclcpp_lifecycle::LifecycleNode::SharedPtr &node, const std::string &source_id, const std::string &service_name = "/fault_manager/report_fault")

Construct a FaultReporter via Lifecycle Node Shared Ptr.

Parameters:
  • node – The ROS 2 node to use for service client and parameters

  • source_id – Identifier for this reporter (typically node’s FQN)

  • service_name – Name of the ReportFault service (default: /fault_manager/report_fault)

void report(const std::string &fault_code, uint8_t severity, const std::string &description)

Report a FAILED event (fault occurrence)

The fault will be forwarded to the FaultManager if local filtering allows it (threshold met within time window, or high severity).

Parameters:
  • fault_code – Global fault identifier (e.g., “MOTOR_OVERHEAT”)

  • severity – Severity level (use Fault::SEVERITY_* constants)

  • description – Human-readable fault description

void report_passed(const std::string &fault_code)

Report a PASSED event (fault condition cleared)

PASSED events bypass local filtering and are always forwarded to FaultManager. Use this when the condition that caused a fault is no longer present.

Parameters:

fault_code – Global fault identifier (must match a previously reported fault)

bool is_service_ready() const

Check if the FaultManager service is available.

inline const LocalFilter &filter() const

Get read-only access to the local filter (for testing)

class LocalFilter

Local filter for fault reports.

Tracks fault occurrences per fault_code and determines whether a report should be forwarded to the central FaultManager. Implements threshold-based filtering within a sliding time window.

Thread-safe: All public methods can be called from multiple threads.

Public Functions

explicit LocalFilter(const FilterConfig &config = FilterConfig{})
bool should_forward(const std::string &fault_code, uint8_t severity)

Check if a fault report should be forwarded.

Parameters:
  • fault_code – The fault identifier

  • severity – The fault severity level

Returns:

true if the report should be sent to FaultManager

bool should_forward_passed(const std::string &fault_code)

Check if a PASSED event should be forwarded.

void reset(const std::string &fault_code)

Reset tracking for a specific fault code.

void reset_all()

Reset all tracking state.

inline FilterConfig config() const

Get the current configuration (thread-safe copy)

void set_config(const FilterConfig &config)

Update configuration (clears existing state)

ros2_medkit_diagnostic_bridge

Bridge node that converts ROS 2 /diagnostics messages to FaultManager faults.

class DiagnosticBridgeNode : public rclcpp::Node

Bridge node that converts ROS2 /diagnostics messages to FaultManager faults.

Subscribes to /diagnostics topic and forwards diagnostic status messages to the FaultManager via the ReportFault service.

Severity mapping:

  • OK (0) -> PASSED event (healing)

  • WARN (1) -> WARN severity (1)

  • ERROR (2) -> ERROR severity (2)

  • STALE (3) -> CRITICAL severity (3)

Example launch configuration:

diagnostic_bridge:
  ros__parameters:
    diagnostics_topic: "/diagnostics"
    auto_generate_codes: true
    # Custom mappings: "name_to_code.<diagnostic_name>": "<FAULT_CODE>"
    "name_to_code.motor_controller: Status": "MOTOR_001"

Public Functions

explicit DiagnosticBridgeNode(const rclcpp::NodeOptions &options = rclcpp::NodeOptions())
std::string map_to_fault_code(const diagnostic_msgs::msg::DiagnosticStatus &status) const

Map diagnostic status to fault code Mapping order:

  1. Custom name_to_code mapping (parameter overrides)

  2. Check if diagnostic contains any key-value pairs with keys in keyvalue_codes

  3. Auto-generate from diagnostic name (if auto_generate_codes is true)

  4. Return empty string if no mapping found

Public Static Functions

static std::optional<uint8_t> map_to_severity(uint8_t diagnostic_level)

Map DiagnosticStatus level to Fault severity Returns std::nullopt if level is OK (should send PASSED instead)

static bool is_ok_level(uint8_t diagnostic_level)

Check if diagnostic level indicates OK status.

ros2_medkit_log_bridge

Bridge node that promotes ROS 2 /rosout log entries to FaultManager faults.

class LogBridgeNode : public rclcpp::Node

Bridge node that promotes ROS 2 /rosout log entries to FaultManager faults.

Subscribes to /rosout (rcl_interfaces/msg/Log) and forwards entries at or above a configurable severity floor to the FaultManager, attributing each fault to the originating node’s fully-qualified name via a per-source FaultReporter. Drop-in compat adapter, same category as ros2_medkit_diagnostic_bridge: native FaultReporter instrumentation stays the canonical path; this bridge is the fallback for nodes that only log. Level mapping and the WARN/LocalFilter caveat are documented in README.md.

Hard limitation by construction: only sees rclcpp logs that reach /rosout from a still-alive node. Console-only loggers and crash-before-flush are out of reach.

Public Functions

explicit LogBridgeNode(const rclcpp::NodeOptions &options = rclcpp::NodeOptions())
std::string generate_fault_code(const std::string &source_id, const std::string &message) const

Auto-generate a stable fault code from the originating node’s FQN and the log message.

Numbers/hex/paths in the message are normalized away so the same logical message maps to the same code across occurrences. Format: <PREFIX>_<NODE>_<HASH>, clamped to medkit’s [A-Z0-9_] / 64-char rule.

bool node_is_eligible(const std::string &source_id) const

Whether a given originating node should be promoted, honouring the include/exclude lists.

Exposed for unit testing.

bool skips_medkit_stack(const std::string &logger_name) const

Whether this log should be skipped because it comes from the medkit stack itself - applies the exclude_medkit_stack toggle to is_medkit_stack_logger.

Exposed for unit testing the toggle.

bool cooldown_allows(const std::string &fault_code, uint8_t severity, rclcpp::Time now)

Whether a (fault_code, severity) may be forwarded now under the cooldown (first occurrence passes; same code+severity within report_cooldown_sec is suppressed; 0.0 disables).

Keyed by severity so a WARN never suppresses a same-message ERROR escalation. Exposed for unit testing.

ros2_medkit_fault_reporter::FaultReporter *reporter_for(const std::string &source_id)

Fetch (or lazily create) the per-source FaultReporter for an originating node, so the fault’s source_id is the node that logged, not the bridge.

Bounded by max_tracked_nodes_ with LRU eviction. Exposed for unit testing.

size_t tracked_reporter_count()

Number of currently tracked per-node reporters. Exposed for unit testing.

Public Static Functions

static bool map_level_to_severity(uint8_t log_level, uint8_t severity_floor, uint8_t *severity_out)

Map an rcl_interfaces/msg/Log level to a Fault severity.

Returns false when the level is below the floor / not promotable.

static std::string normalize_message(const std::string &message)

Normalize a log message into a stable template (lowercased, digit/hex/path runs stripped, whitespace collapsed, isolated single-letter tokens dropped).

Exposed for unit testing.

static bool is_medkit_stack_logger(const std::string &logger_name)

Whether a logger name belongs to the medkit stack’s own infrastructure (fault_manager, gateway, the other bridges).

Matched on the raw logger name so namespaced nodes are caught. Exposed for unit testing.

static std::string node_source_id(const std::string &log_name)

Map an rcl_interfaces/msg/Log.name (a logger name, e.g.

“bt_navigator” or “controller_manager.resource_manager”) to the originating node’s fully-qualified name (“/bt_navigator”, “/controller_manager”). The gateway discovers runtime entities by node FQN, so the fault’s source_id must use the same form for faults (and their snapshots / rosbag) to associate with the entity in the SOVD tree. Exposed for unit testing.

static std::string fnv1a_hex(const std::string &in)

FNV-1a 32-bit hash, fixed spec, emitted as 8 lowercase hex chars.

Exposed for unit testing so a known input asserts a known constant.

ros2_medkit_action_status_bridge

Bridge node that turns terminal ROS 2 action goal states into FaultManager faults.

class ActionStatusBridgeNode : public rclcpp::Node

Bridge node that turns terminal ROS 2 action goal states into FaultManager faults.

Generic across every action: it observes the per-action /<action>/_action/status topic (action_msgs/msg/GoalStatusArray) that every action server publishes, so no per-project code is needed.

This catches the authoritative “the goal failed” verdict that neither the /diagnostics bridge nor the /rosout log bridge can see - e.g. a Nav2 NavigateToPose aborting or a MoveIt MoveGroup goal aborting. The reason (action-specific error code in the result) is a separate enrichment concern; this bridge delivers the generic terminal status.

Status mapping (action_msgs/msg/GoalStatus):

  • ABORTED (6) -> fault (severity configurable, default ERROR)

  • CANCELED (5) -> fault only if canceled_is_fault (usually intentional)

  • SUCCEEDED (4)-> PASSED (heals the per-action fault code) if enabled

Fault state is per-ACTION, not per-goal: every message is scanned for the net state of the whole GoalStatusArray and a fault is raised/healed only on the action-level transition. See derive_state.

Public Types

enum class ActionState

Net fault state of an action, derived from a whole GoalStatusArray.

  • kUnknown: no terminal goal in the array yet (no transition)

  • kHealthy: array has terminal goals and none of them are failing

  • kFailed: at least one goal is failing (ABORTED, or CANCELED when canceled_is_fault)

Values:

enumerator kUnknown
enumerator kHealthy
enumerator kFailed

Public Functions

explicit ActionStatusBridgeNode(const rclcpp::NodeOptions &options = rclcpp::NodeOptions())
~ActionStatusBridgeNode() override
ActionStatusBridgeNode(const ActionStatusBridgeNode&) = delete
ActionStatusBridgeNode &operator=(const ActionStatusBridgeNode&) = delete
ActionStatusBridgeNode(ActionStatusBridgeNode&&) = delete
ActionStatusBridgeNode &operator=(ActionStatusBridgeNode&&) = delete
std::string fault_code_for(const std::string &action_name, bool canceled) const

Build the fault code for an action name and terminal status.

Format: <PREFIX>_<ACTION>_<ABORTED|CANCELED>, charset/length per medkit rules. canceled selects the CANCELED suffix.

ActionState apply_message(const std::string &action_name, const action_msgs::msg::GoalStatusArray &msg, ros2_medkit_fault_reporter::FaultReporter *reporter)

Update the desired per-action state from a message and reconcile it, acting on the transition only.

Returns the state that was reported on (kFailed on raise, kHealthy on heal) or kUnknown when nothing was reported

  • including when the report could not be delivered yet (the FaultManager service is not discovered), in which case the transition stays pending and is retried on the next rescan. A null reporter is the unit-test seam: delivery is a no-op treated as successful, so the transition decision and stored per-action state still update and tests assert on the return value.

Public Static Functions

static std::string action_name_from_status_topic(const std::string &topic)

Derive the action name from a /<action>/_action/status topic name.

Returns empty when the topic is not an action status topic.

static std::string server_fqn_from_endpoint(const std::string &node_name, const std::string &node_namespace)

Build a node FQN from a publisher endpoint’s node name + namespace, or “” if unresolved.

During DDS discovery rcl reports the placeholders “_NODE_NAME_UNKNOWN_” / “_NODE_NAMESPACE_UNKNOWN_” before the real name propagates; those (and an empty name) are treated as unresolved so a fault is never attributed to the placeholder.

static std::string uuid_to_hex(const std::array<uint8_t, 16> &uuid)

Lowercase hex of a 16-byte goal UUID, for dedup keys and short display.

static ActionState derive_state(const action_msgs::msg::GoalStatusArray &msg, bool canceled_is_fault)

Scan a whole GoalStatusArray and return the action-level net state.

canceled_is_fault decides whether CANCELED counts as failing. Order of the goals in the array does not affect the result (any failing goal wins).

ros2_medkit_serialization

Runtime JSON ↔ ROS 2 message serialization library.

class JsonSerializer

JSON serializer for ROS 2 messages using dynmsg.

This class provides the main API for converting between JSON and ROS 2 messages at runtime. It uses dynmsg’s YAML capabilities internally and converts between JSON and YAML formats.

Note

This class is stateless and thread-safe.

Public Functions

JsonSerializer() = default
~JsonSerializer() = default
JsonSerializer(const JsonSerializer&) = delete
JsonSerializer &operator=(const JsonSerializer&) = delete
JsonSerializer(JsonSerializer&&) = default
JsonSerializer &operator=(JsonSerializer&&) = default
nlohmann::json to_json(const TypeInfo_Cpp *type_info, const void *message_data) const

Convert a ROS 2 message to JSON.

Parameters:
  • type_info – Type introspection info

  • message_data – Pointer to the message data

Throws:

JsonConversionError – if conversion fails

Returns:

JSON representation of the message

nlohmann::json to_json(const std::string &type_string, const void *message_data) const

Convert a ROS 2 message to JSON using type string.

Parameters:
  • type_string – Full type string (e.g., “std_msgs/msg/String”)

  • message_data – Pointer to the message data

Throws:
Returns:

JSON representation of the message

RosMessage_Cpp from_json(const TypeInfo_Cpp *type_info, const nlohmann::json &json) const

Convert JSON to a ROS 2 message.

Note

Caller is responsible for calling ros_message_destroy_with_allocator

Parameters:
  • type_info – Type introspection info

  • json – JSON data to convert

Throws:

JsonConversionError – if conversion fails

Returns:

RosMessage_Cpp containing allocated message data

RosMessage_Cpp from_json(const std::string &type_string, const nlohmann::json &json) const

Convert JSON to a ROS 2 message using type string.

Note

Caller is responsible for calling ros_message_destroy_with_allocator

Parameters:
  • type_string – Full type string (e.g., “std_msgs/msg/String”)

  • json – JSON data to convert

Throws:
Returns:

RosMessage_Cpp containing allocated message data

void from_json_to_message(const TypeInfo_Cpp *type_info, const nlohmann::json &json, void *message_data) const

Populate an existing message from JSON.

Parameters:
  • type_info – Type introspection info

  • json – JSON data to convert

  • message_data – Pointer to pre-allocated message to populate

Throws:

JsonConversionError – if conversion fails

nlohmann::json get_schema(const TypeInfo_Cpp *type_info) const

Generate a JSON schema for a ROS 2 type.

Parameters:

type_info – Type introspection info

Returns:

JSON schema object

nlohmann::json get_schema(const std::string &type_string) const

Generate a JSON schema using type string.

Parameters:

type_string – Full type string

Throws:

TypeNotFoundError – if type cannot be loaded

Returns:

JSON schema object

nlohmann::json get_defaults(const TypeInfo_Cpp *type_info) const

Get default values for a ROS 2 type as JSON.

Parameters:

type_info – Type introspection info

Returns:

JSON object with default values

nlohmann::json get_defaults(const std::string &type_string) const

Get default values using type string.

Parameters:

type_string – Full type string

Throws:

TypeNotFoundError – if type cannot be loaded

Returns:

JSON object with default values

rclcpp::SerializedMessage serialize(const std::string &type_string, const nlohmann::json &json) const

Serialize JSON to CDR format for use with GenericClient.

Parameters:
  • type_string – Full type string (e.g., “std_srvs/srv/SetBool_Request”)

  • json – JSON data to serialize

Throws:
Returns:

SerializedMessage containing CDR data

nlohmann::json deserialize(const std::string &type_string, const rclcpp::SerializedMessage &serialized_msg) const

Deserialize CDR data to JSON.

Parameters:
  • type_string – Full type string

  • serialized_msg – SerializedMessage containing CDR data

Throws:
Returns:

JSON representation of the message

Public Static Functions

static nlohmann::json yaml_to_json(const YAML::Node &yaml)

Convert YAML node to JSON.

Parameters:

yaml – YAML node to convert

Returns:

JSON representation

static YAML::Node json_to_yaml(const nlohmann::json &json)

Convert JSON to YAML node.

Parameters:

json – JSON to convert

Returns:

YAML node representation

class TypeCache

Thread-safe cache for ROS 2 type introspection data.

This class wraps dynmsg’s type loading functions and caches the results to avoid repeated dynamic library lookups. It is implemented as a singleton for global access throughout the gateway.

Note

All public methods are thread-safe.

Public Functions

TypeCache(const TypeCache&) = delete

Delete copy constructor.

TypeCache &operator=(const TypeCache&) = delete

Delete copy assignment.

const TypeInfo_Cpp *get_message_type_info(const std::string &package_name, const std::string &type_name)

Get type info for a message type (C++ introspection)

Parameters:
  • package_name – Package name (e.g., “std_msgs”)

  • type_name – Type name without prefix (e.g., “String”)

Returns:

Pointer to type info, or nullptr if not found

const TypeInfo_Cpp *get_message_type_info(const std::string &package_name, const std::string &interface_type, const std::string &type_name)

Get type info for any interface type (msg/srv/action)

Parameters:
  • package_name – Package name (e.g., “std_srvs”)

  • interface_type – Interface type (“msg”, “srv”, or “action”)

  • type_name – Type name (e.g., “Trigger_Request”)

Returns:

Pointer to type info, or nullptr if not found

const TypeInfo_Cpp *get_message_type_info(const std::string &full_type)

Get type info from full type string.

Parameters:

full_type – Full type string (e.g., “std_msgs/msg/String” or “std_srvs/srv/Trigger_Request”)

Returns:

Pointer to type info, or nullptr if not found

bool is_cached(const std::string &package_name, const std::string &type_name) const

Check if a type is cached.

Parameters:
  • package_name – Package name

  • type_name – Type name

Returns:

true if the type is in the cache

void clear()

Clear the cache.

size_t size() const

Get the number of cached types.

Public Static Functions

static TypeCache &instance()

Get the singleton instance.

static std::optional<std::tuple<std::string, std::string, std::string>> parse_type_string(const std::string &full_type)

Parse a full type string into package, interface type, and type name.

Parameters:

full_type – Full type string (e.g., “std_msgs/msg/String”)

Returns:

Tuple of (package_name, interface_type, type_name), or nullopt if invalid format

class SerializationError : public std::runtime_error

Base class for all serialization errors.

Subclassed by ros2_medkit_serialization::JsonConversionError, ros2_medkit_serialization::MissingFieldError, ros2_medkit_serialization::TypeMismatchError, ros2_medkit_serialization::TypeNotFoundError, ros2_medkit_serialization::YamlConversionError

Public Functions

inline explicit SerializationError(const std::string &message)
class TypeNotFoundError : public ros2_medkit_serialization::SerializationError

Error when type cannot be found or loaded.

Public Functions

inline explicit TypeNotFoundError(const std::string &type_name)
inline const std::string &type_name() const noexcept
class JsonConversionError : public ros2_medkit_serialization::SerializationError

Error during JSON parsing or conversion.

Public Functions

inline explicit JsonConversionError(const std::string &message)