From d45939909044060603dd591b8e377fb35516094b Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Fri, 20 Mar 2026 19:33:11 +0100
Subject: [PATCH 01/23] chore: bump all packages to 0.4.0 and update changelogs
- Bump all 13 package.xml files from 0.3.0 to 0.4.0
- Update version.hpp fallback to 0.4.0
- Add 0.4.0 sections to all existing CHANGELOG.rst files
- Create initial CHANGELOG.rst for new packages (ros2_medkit_cmake,
ros2_medkit_linux_introspection, ros2_medkit_beacon_common,
ros2_medkit_param_beacon, ros2_medkit_topic_beacon,
ros2_medkit_graph_provider)
- Fix gateway changelog: move post-0.3.0 items (#258, #263) from
0.3.0 section to new 0.4.0 section
- Add scripts/release.sh for automated version bump and verification
Refs: #278
---
scripts/release.sh | 179 ++++++++++++++++++
src/ros2_medkit_cmake/CHANGELOG.rst | 11 ++
src/ros2_medkit_cmake/package.xml | 2 +-
.../CHANGELOG.rst | 6 +
src/ros2_medkit_diagnostic_bridge/package.xml | 2 +-
.../ros2_medkit_beacon_common/CHANGELOG.rst | 14 ++
.../ros2_medkit_beacon_common/package.xml | 2 +-
.../CHANGELOG.rst | 14 ++
.../package.xml | 2 +-
.../ros2_medkit_param_beacon/CHANGELOG.rst | 12 ++
.../ros2_medkit_param_beacon/package.xml | 2 +-
.../ros2_medkit_topic_beacon/CHANGELOG.rst | 12 ++
.../ros2_medkit_topic_beacon/package.xml | 2 +-
src/ros2_medkit_fault_manager/CHANGELOG.rst | 8 +
src/ros2_medkit_fault_manager/package.xml | 2 +-
src/ros2_medkit_fault_reporter/CHANGELOG.rst | 6 +
src/ros2_medkit_fault_reporter/package.xml | 2 +-
src/ros2_medkit_gateway/CHANGELOG.rst | 64 +++++--
.../include/ros2_medkit_gateway/version.hpp | 2 +-
src/ros2_medkit_gateway/package.xml | 2 +-
.../CHANGELOG.rst | 14 ++
src/ros2_medkit_integration_tests/package.xml | 2 +-
src/ros2_medkit_msgs/CHANGELOG.rst | 5 +
src/ros2_medkit_msgs/package.xml | 2 +-
.../ros2_medkit_graph_provider/CHANGELOG.rst | 11 ++
.../ros2_medkit_graph_provider/package.xml | 2 +-
src/ros2_medkit_serialization/CHANGELOG.rst | 7 +
src/ros2_medkit_serialization/package.xml | 2 +-
28 files changed, 363 insertions(+), 28 deletions(-)
create mode 100755 scripts/release.sh
create mode 100644 src/ros2_medkit_cmake/CHANGELOG.rst
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/CHANGELOG.rst
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CHANGELOG.rst
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CHANGELOG.rst
create mode 100644 src/ros2_medkit_plugins/ros2_medkit_graph_provider/CHANGELOG.rst
diff --git a/scripts/release.sh b/scripts/release.sh
new file mode 100755
index 00000000..b782711f
--- /dev/null
+++ b/scripts/release.sh
@@ -0,0 +1,179 @@
+#!/bin/bash
+# Copyright 2026 bburda
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Release helper script for ros2_medkit.
+#
+# Usage:
+# ./scripts/release.sh bump - Bump all package.xml and version.hpp to
+# ./scripts/release.sh verify [] - Verify all packages have consistent versions
+# (if given, checks against that specific version)
+#
+# Examples:
+# ./scripts/release.sh bump 0.4.0
+# ./scripts/release.sh verify
+# ./scripts/release.sh verify 0.4.0
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+SRC_DIR="${REPO_ROOT}/src"
+VERSION_HPP="${SRC_DIR}/ros2_medkit_gateway/include/ros2_medkit_gateway/version.hpp"
+
+usage() {
+ echo "Usage: $0 {bump |verify []}"
+ echo ""
+ echo "Commands:"
+ echo " bump Bump all package.xml files and version.hpp fallback"
+ echo " verify [] Verify version consistency across all packages"
+ exit 1
+}
+
+find_package_xmls() {
+ find "${SRC_DIR}" -name "package.xml" -not -path "*/.worktrees/*" -not -path "*/build/*" -not -path "*/install/*" | sort
+}
+
+get_version() {
+ local pkg_xml="$1"
+ grep -oP '\K[0-9]+\.[0-9]+\.[0-9]+(?=)' "$pkg_xml"
+}
+
+get_package_name() {
+ local pkg_xml="$1"
+ grep -oP '\K[^<]+' "$pkg_xml"
+}
+
+cmd_bump() {
+ local target_version="$1"
+
+ # Validate semver format
+ if ! echo "$target_version" | grep -qP '^[0-9]+\.[0-9]+\.[0-9]+$'; then
+ echo "Error: '$target_version' is not a valid semver (X.Y.Z)"
+ exit 1
+ fi
+
+ echo "Bumping all packages to version ${target_version}..."
+ echo ""
+
+ local count=0
+ while IFS= read -r pkg_xml; do
+ local pkg_name
+ pkg_name=$(get_package_name "$pkg_xml")
+ local old_version
+ old_version=$(get_version "$pkg_xml")
+ local rel_path="${pkg_xml#"${REPO_ROOT}/"}"
+
+ sed -i "s|[0-9]\+\.[0-9]\+\.[0-9]\+|${target_version}|" "$pkg_xml"
+ echo " ${pkg_name}: ${old_version} -> ${target_version} (${rel_path})"
+ count=$((count + 1))
+ done < <(find_package_xmls)
+
+ # Update version.hpp fallback
+ if [ -f "$VERSION_HPP" ]; then
+ local old_fallback
+ old_fallback=$(grep -oP 'kGatewayVersion = "\K[0-9]+\.[0-9]+\.[0-9]+' "$VERSION_HPP" || echo "unknown")
+ sed -i "s|kGatewayVersion = \"[0-9]\+\.[0-9]\+\.[0-9]\+\"|kGatewayVersion = \"${target_version}\"|" "$VERSION_HPP"
+ echo " version.hpp fallback: ${old_fallback} -> ${target_version}"
+ else
+ echo " WARNING: version.hpp not found at ${VERSION_HPP}"
+ fi
+
+ echo ""
+ echo "Bumped ${count} packages + version.hpp to ${target_version}."
+ echo ""
+ echo "Run '$0 verify ${target_version}' to confirm."
+}
+
+cmd_verify() {
+ local expected_version="${1:-}"
+
+ echo "Verifying package versions..."
+ echo ""
+
+ local all_ok=true
+ local versions_seen=()
+
+ while IFS= read -r pkg_xml; do
+ local pkg_name
+ pkg_name=$(get_package_name "$pkg_xml")
+ local version
+ version=$(get_version "$pkg_xml")
+ local rel_path="${pkg_xml#"${REPO_ROOT}/"}"
+
+ if [ -n "$expected_version" ] && [ "$version" != "$expected_version" ]; then
+ echo " MISMATCH: ${pkg_name} is ${version}, expected ${expected_version} (${rel_path})"
+ all_ok=false
+ else
+ echo " OK: ${pkg_name} = ${version} (${rel_path})"
+ fi
+ versions_seen+=("$version")
+ done < <(find_package_xmls)
+
+ # Check version.hpp fallback
+ if [ -f "$VERSION_HPP" ]; then
+ local hpp_version
+ hpp_version=$(grep -oP 'kGatewayVersion = "\K[0-9]+\.[0-9]+\.[0-9]+' "$VERSION_HPP" || echo "unknown")
+ if [ -n "$expected_version" ] && [ "$hpp_version" != "$expected_version" ]; then
+ echo " MISMATCH: version.hpp fallback is ${hpp_version}, expected ${expected_version}"
+ all_ok=false
+ else
+ echo " OK: version.hpp fallback = ${hpp_version}"
+ fi
+ fi
+
+ # Check consistency if no expected version given
+ if [ -z "$expected_version" ]; then
+ local unique_versions
+ unique_versions=$(printf '%s\n' "${versions_seen[@]}" | sort -u)
+ local unique_count
+ unique_count=$(echo "$unique_versions" | wc -l)
+ if [ "$unique_count" -gt 1 ]; then
+ echo ""
+ echo "WARNING: Found multiple versions:"
+ echo "$unique_versions" | while read -r v; do echo " - $v"; done
+ all_ok=false
+ fi
+ fi
+
+ echo ""
+ if $all_ok; then
+ echo "All versions are consistent."
+ return 0
+ else
+ echo "Version mismatches found!"
+ return 1
+ fi
+}
+
+# Main
+if [ $# -lt 1 ]; then
+ usage
+fi
+
+case "$1" in
+ bump)
+ if [ $# -lt 2 ]; then
+ echo "Error: bump requires a version argument"
+ usage
+ fi
+ cmd_bump "$2"
+ ;;
+ verify)
+ cmd_verify "${2:-}"
+ ;;
+ *)
+ usage
+ ;;
+esac
diff --git a/src/ros2_medkit_cmake/CHANGELOG.rst b/src/ros2_medkit_cmake/CHANGELOG.rst
new file mode 100644
index 00000000..546a01e4
--- /dev/null
+++ b/src/ros2_medkit_cmake/CHANGELOG.rst
@@ -0,0 +1,11 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package ros2_medkit_cmake
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.4.0 (2026-03-20)
+------------------
+* Initial release - shared cmake modules extracted from gateway package (`#294 `_)
+* ``ROS2MedkitCcache.cmake`` - auto-detect ccache for faster incremental rebuilds
+* ``ROS2MedkitLinting.cmake`` - centralized clang-tidy configuration (opt-in locally, mandatory in CI)
+* ``ROS2MedkitCompat.cmake`` - multi-distro compatibility shims for ROS 2 Humble/Jazzy/Rolling
+* Contributors: @bburda
diff --git a/src/ros2_medkit_cmake/package.xml b/src/ros2_medkit_cmake/package.xml
index b9725209..0ced49e1 100644
--- a/src/ros2_medkit_cmake/package.xml
+++ b/src/ros2_medkit_cmake/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_cmake
- 0.3.0
+ 0.4.0
Shared CMake modules for ros2_medkit packages (multi-distro compat, ccache, linting)
bburda
Apache-2.0
diff --git a/src/ros2_medkit_diagnostic_bridge/CHANGELOG.rst b/src/ros2_medkit_diagnostic_bridge/CHANGELOG.rst
index 43db7c2f..748a9096 100644
--- a/src/ros2_medkit_diagnostic_bridge/CHANGELOG.rst
+++ b/src/ros2_medkit_diagnostic_bridge/CHANGELOG.rst
@@ -2,6 +2,12 @@
Changelog for package ros2_medkit_diagnostic_bridge
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-03-20)
+------------------
+* Build: use shared cmake modules from ``ros2_medkit_cmake`` package
+* Build: auto-detect ccache, centralized clang-tidy configuration
+* Contributors: @bburda
+
0.3.0 (2026-02-27)
------------------
* Multi-distro CI support for ROS 2 Humble, Jazzy, and Rolling (`#219 `_, `#242 `_)
diff --git a/src/ros2_medkit_diagnostic_bridge/package.xml b/src/ros2_medkit_diagnostic_bridge/package.xml
index b38b4f49..f171d4c2 100644
--- a/src/ros2_medkit_diagnostic_bridge/package.xml
+++ b/src/ros2_medkit_diagnostic_bridge/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_diagnostic_bridge
- 0.3.0
+ 0.4.0
Bridge node converting ROS2 /diagnostics to FaultManager faults
mfaferek93
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/CHANGELOG.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/CHANGELOG.rst
new file mode 100644
index 00000000..c3393f7a
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/CHANGELOG.rst
@@ -0,0 +1,14 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package ros2_medkit_beacon_common
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.4.0 (2026-03-20)
+------------------
+* Initial release - shared utilities for beacon discovery plugins
+* ``BeaconHintStore`` with TTL-based hint transitions and thread safety
+* ``BeaconValidator`` input validation gate
+* ``BeaconEntityMapper`` to convert discovery hints into ``IntrospectionResult``
+* ``build_beacon_response()`` shared response builder
+* ``BeaconPlugin`` base class for topic and parameter beacon plugins
+* ``TokenBucket`` rate limiter with thread-safe access
+* Contributors: @bburda
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/package.xml b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/package.xml
index b60e0618..0384cb6d 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/package.xml
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_beacon_common
- 0.3.0
+ 0.4.0
Shared library for ros2_medkit beacon discovery plugins
bburda
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst
new file mode 100644
index 00000000..ad297038
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst
@@ -0,0 +1,14 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package ros2_medkit_linux_introspection
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.4.0 (2026-03-20)
+------------------
+* Initial release - Linux process introspection plugins for ros2_medkit gateway
+* ``procfs_plugin`` - process-level diagnostics via ``/proc`` filesystem (CPU, memory, threads, file descriptors)
+* ``systemd_plugin`` - systemd unit status and resource usage via D-Bus
+* ``container_plugin`` - container runtime detection and cgroup resource limits
+* ``PidCache`` with TTL-based refresh for efficient PID-to-node mapping
+* ``proc_reader`` and ``cgroup_reader`` utilities with configurable proc root
+* Cross-distro support for ROS 2 Humble, Jazzy, and Rolling
+* Contributors: @bburda
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/package.xml b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/package.xml
index 769bb10d..b13fb155 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/package.xml
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_linux_introspection
- 0.3.0
+ 0.4.0
Linux introspection plugins for ros2_medkit gateway - procfs, systemd, and container
bburda
Apache-2.0
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CHANGELOG.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CHANGELOG.rst
new file mode 100644
index 00000000..c3a48d85
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CHANGELOG.rst
@@ -0,0 +1,12 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package ros2_medkit_param_beacon
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.4.0 (2026-03-20)
+------------------
+* Initial release - parameter-based beacon discovery plugin
+* ``ParameterBeaconPlugin`` with pull-based parameter reading for entity enrichment
+* ``x-medkit-param-beacon`` vendor extension REST endpoint
+* Poll target discovery from ROS graph in non-hybrid mode
+* ``ParameterClientInterface`` for testable parameter access
+* Contributors: @bburda
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/package.xml b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/package.xml
index 9041a5a2..b4df2cd9 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/package.xml
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_param_beacon
- 0.3.0
+ 0.4.0
Parameter-based beacon discovery plugin for ros2_medkit gateway
bburda
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CHANGELOG.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CHANGELOG.rst
new file mode 100644
index 00000000..778b16e8
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CHANGELOG.rst
@@ -0,0 +1,12 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package ros2_medkit_topic_beacon
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.4.0 (2026-03-20)
+------------------
+* Initial release - topic-based beacon discovery plugin
+* ``TopicBeaconPlugin`` with push-based topic subscription for entity enrichment
+* ``x-medkit-topic-beacon`` vendor extension REST endpoint
+* Stamp-based TTL for topic beacon hints
+* Diagnostic logging for beacon hint processing
+* Contributors: @bburda
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/package.xml b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/package.xml
index cffa2e67..0becf864 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/package.xml
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_topic_beacon
- 0.3.0
+ 0.4.0
Topic-based beacon discovery plugin for ros2_medkit gateway
bburda
diff --git a/src/ros2_medkit_fault_manager/CHANGELOG.rst b/src/ros2_medkit_fault_manager/CHANGELOG.rst
index 536f8889..729f2169 100644
--- a/src/ros2_medkit_fault_manager/CHANGELOG.rst
+++ b/src/ros2_medkit_fault_manager/CHANGELOG.rst
@@ -2,6 +2,14 @@
Changelog for package ros2_medkit_fault_manager
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-03-20)
+------------------
+* Per-entity confirmation and healing thresholds via manifest configuration (`#269 `_)
+* Default rosbag storage format changed from ``sqlite3`` to ``mcap``
+* Build: use shared cmake modules from ``ros2_medkit_cmake`` package
+* Build: centralized clang-tidy configuration
+* Contributors: @bburda
+
0.3.0 (2026-02-27)
------------------
* Accurate HIGHEST_SEVERITY reassignment and stale ``fault_to_cluster_`` cleanup (`#221 `_)
diff --git a/src/ros2_medkit_fault_manager/package.xml b/src/ros2_medkit_fault_manager/package.xml
index 7c614f35..5e7b2eb6 100644
--- a/src/ros2_medkit_fault_manager/package.xml
+++ b/src/ros2_medkit_fault_manager/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_fault_manager
- 0.3.0
+ 0.4.0
Central fault manager node for ros2_medkit fault management system
bburda
diff --git a/src/ros2_medkit_fault_reporter/CHANGELOG.rst b/src/ros2_medkit_fault_reporter/CHANGELOG.rst
index 2430fed2..d8dd7d73 100644
--- a/src/ros2_medkit_fault_reporter/CHANGELOG.rst
+++ b/src/ros2_medkit_fault_reporter/CHANGELOG.rst
@@ -2,6 +2,12 @@
Changelog for package ros2_medkit_fault_reporter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-03-20)
+------------------
+* Build: use shared cmake modules from ``ros2_medkit_cmake`` package
+* Build: auto-detect ccache, centralized clang-tidy configuration
+* Contributors: @bburda
+
0.3.0 (2026-02-27)
------------------
* Multi-distro CI support for ROS 2 Humble, Jazzy, and Rolling (`#219 `_, `#242 `_)
diff --git a/src/ros2_medkit_fault_reporter/package.xml b/src/ros2_medkit_fault_reporter/package.xml
index 3bbfb0ea..181ef63f 100644
--- a/src/ros2_medkit_fault_reporter/package.xml
+++ b/src/ros2_medkit_fault_reporter/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_fault_reporter
- 0.3.0
+ 0.4.0
Client library for easy fault reporting with local filtering
mfaferek93
diff --git a/src/ros2_medkit_gateway/CHANGELOG.rst b/src/ros2_medkit_gateway/CHANGELOG.rst
index 3e34f40b..50722d30 100644
--- a/src/ros2_medkit_gateway/CHANGELOG.rst
+++ b/src/ros2_medkit_gateway/CHANGELOG.rst
@@ -2,20 +2,7 @@
Changelog for package ros2_medkit_gateway
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Unreleased
-----------
-
-Added
-~~~~~
-* Beacon discovery plugin system - push-based entity enrichment via ROS 2 topic
-* ``MedkitDiscoveryHint`` message type for beacon publishers
-* ``discovery.manifest.enabled`` / ``discovery.runtime.enabled`` parameters for hybrid mode
-* ``NewEntities.functions`` - plugins can now produce Function entities
-* ``x-medkit-topic-beacon`` vendor extension REST endpoint (TopicBeaconPlugin) for push-based beacon metadata
-* ``x-medkit-param-beacon`` vendor extension REST endpoint (ParameterBeaconPlugin) for pull-based parameter beacon metadata
-* ``ros2_medkit_beacon_common`` shared library - ``BeaconHintStore``, ``build_beacon_response()``, and base ``BeaconPlugin`` class used by both beacon plugins
-
-0.3.0 (2026-02-27)
+0.4.0 (2026-03-20)
------------------
**Breaking Changes:**
@@ -23,6 +10,8 @@ Added
* ``GET /version-info`` response key renamed from ``sovd_info`` to ``items`` for SOVD alignment (`#258 `_)
* ``GET /`` root endpoint restructured: ``endpoints`` is now a flat string array, added ``capabilities`` object, ``api_base`` field, and ``name``/``version`` top-level fields (`#258 `_)
* Default rosbag storage format changed from ``sqlite3`` to ``mcap`` (`#258 `_)
+* Plugin API version bumped to v4 - added ``ScriptProvider``, locking API, and extended ``PluginContext`` with entity snapshot, fault listing, and sampler registration
+* ``GraphProviderPlugin`` extracted to separate ``ros2_medkit_graph_provider`` package
**Features:**
@@ -36,6 +25,53 @@ Added
* Entity capabilities fix: areas and functions now report correct resource collections (`#258 `_)
* SOVD compliance documentation with resource collection support matrix (`#258 `_)
* Linux introspection plugins: procfs, systemd, and container plugins for process-level diagnostics via ``x-medkit-*`` vendor extension endpoints (`#263 `_)
+* SOVD-compliant resource locking: acquire, release, extend with session tracking and expiration
+* Lock enforcement on all mutating handlers (PUT, POST, DELETE)
+* Per-entity lock configuration via manifest YAML with ``required_scopes``
+* Lock API exposed to plugins via ``PluginContext``
+* Automatic cyclic subscription cleanup on lock expiry
+* ``LOCKS`` capability in entity descriptions
+* SOVD script execution endpoints: CRUD for scripts and executions with subprocess execution
+* ``ScriptProvider`` plugin interface for custom script backends
+* ``DefaultScriptProvider`` with manifest + filesystem CRUD, argument passing, and timeout
+* ``allow_uploads`` config toggle for hardened deployments
+* RBAC integration for script operations
+* ``RouteRegistry`` as single source of truth for routes and OpenAPI metadata
+* ``OpenApiSpecBuilder`` for full OpenAPI 3.0 document assembly with ``SchemaBuilder`` and ``PathBuilder``
+* Compile-time Swagger UI embedding (``ENABLE_SWAGGER_UI``)
+* Generation-based caching for capability responses via ``CapabilityGenerator``
+* Beacon discovery plugin system - push-based entity enrichment via ROS 2 topic
+* ``x-medkit-topic-beacon`` vendor extension REST endpoint (TopicBeaconPlugin) for push-based beacon metadata
+* ``x-medkit-param-beacon`` vendor extension REST endpoint (ParameterBeaconPlugin) for pull-based parameter beacon metadata
+* ``discovery.manifest.enabled`` / ``discovery.runtime.enabled`` parameters for hybrid mode
+* ``NewEntities.functions`` - plugins can now produce Function entities
+* ``LogManager`` with ``/rosout`` ring buffer and plugin delegation
+* ``/logs`` and ``/logs/configuration`` endpoints
+* ``LOGS`` capability in discovery responses
+* Configurable log buffer size via parameters
+* Multi-collection cyclic subscription support (data, faults, logs, configurations, update-status)
+* ``PluginContext::get_child_apps()`` for Component-level aggregation
+* Sub-resource RBAC patterns for all collections
+* Auto-populate gateway version from ``package.xml`` via CMake
+
+**Build:**
+
+* Extracted shared cmake modules into ``ros2_medkit_cmake`` package (`#294 `_)
+* Auto-detect ccache for faster incremental rebuilds
+* Precompiled headers for gateway package
+* Centralized clang-tidy configuration (opt-in locally, mandatory in CI)
+
+**Tests:**
+
+* Unit tests for DiscoveryHandlers, OperationHandlers, ScriptHandlers, LockHandlers, LockManager, ScriptManager, DefaultScriptProvider
+* Comprehensive integration tests for locking, scripts, graph provider plugin, beacon plugins, OpenAPI/docs, logging
+* Contributors: @bburda
+
+0.3.0 (2026-02-27)
+------------------
+
+**Features:**
+
* Gateway plugin framework with dynamic C++ plugin loading (`#237 `_)
* Software updates plugin with 8 SOVD-compliant endpoints (`#237 `_, `#231 `_)
* SSE-based periodic data subscriptions for real-time streaming without polling (`#223 `_)
diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/version.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/version.hpp
index 6036ef6e..2136ad53 100644
--- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/version.hpp
+++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/version.hpp
@@ -21,7 +21,7 @@ namespace ros2_medkit_gateway {
#ifdef GATEWAY_VERSION_STRING
constexpr const char * kGatewayVersion = GATEWAY_VERSION_STRING;
#else
-constexpr const char * kGatewayVersion = "0.3.0";
+constexpr const char * kGatewayVersion = "0.4.0";
#endif
/// SOVD specification version
diff --git a/src/ros2_medkit_gateway/package.xml b/src/ros2_medkit_gateway/package.xml
index fada31c9..f21bcaec 100644
--- a/src/ros2_medkit_gateway/package.xml
+++ b/src/ros2_medkit_gateway/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_gateway
- 0.3.0
+ 0.4.0
HTTP gateway for ros2_medkit diagnostics system
bburda
diff --git a/src/ros2_medkit_integration_tests/CHANGELOG.rst b/src/ros2_medkit_integration_tests/CHANGELOG.rst
index 1cea1d0c..d175910e 100644
--- a/src/ros2_medkit_integration_tests/CHANGELOG.rst
+++ b/src/ros2_medkit_integration_tests/CHANGELOG.rst
@@ -2,6 +2,20 @@
Changelog for package ros2_medkit_integration_tests
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-03-20)
+------------------
+* Integration tests for SOVD resource locking (acquire, release, extend, fault clear with locks, expiration, parent propagation)
+* Integration tests for SOVD script execution endpoints (all formats, params, output, failure, lifecycle)
+* Integration tests for graph provider plugin (external plugin loading, entity introspection)
+* Integration tests for beacon discovery plugins (topic beacon, parameter beacon)
+* Integration tests for OpenAPI/docs endpoint
+* Integration tests for logging endpoints (``/logs``, ``/logs/configuration``)
+* Integration tests for linux introspection plugins (launch_testing and Docker-based)
+* Port isolation per integration test via CMake-assigned unique ports
+* ``ROS_DOMAIN_ID`` isolation for integration tests
+* Build: use shared cmake modules from ``ros2_medkit_cmake`` package
+* Contributors: @bburda
+
0.3.0 (2026-02-27)
------------------
* Refactored integration test suite into dedicated ``ros2_medkit_integration_tests`` package (`#227 `_)
diff --git a/src/ros2_medkit_integration_tests/package.xml b/src/ros2_medkit_integration_tests/package.xml
index 5f361c80..b37c168f 100644
--- a/src/ros2_medkit_integration_tests/package.xml
+++ b/src/ros2_medkit_integration_tests/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_integration_tests
- 0.3.0
+ 0.4.0
Integration tests and demo nodes for ros2_medkit
bburda
diff --git a/src/ros2_medkit_msgs/CHANGELOG.rst b/src/ros2_medkit_msgs/CHANGELOG.rst
index 62801ed7..896f4178 100644
--- a/src/ros2_medkit_msgs/CHANGELOG.rst
+++ b/src/ros2_medkit_msgs/CHANGELOG.rst
@@ -2,6 +2,11 @@
Changelog for package ros2_medkit_msgs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-03-20)
+------------------
+* ``MedkitDiscoveryHint`` message type for beacon discovery publishers
+* Contributors: @bburda
+
0.3.0 (2026-02-27)
------------------
* Multi-distro CI support for ROS 2 Humble, Jazzy, and Rolling (`#219 `_, `#242 `_)
diff --git a/src/ros2_medkit_msgs/package.xml b/src/ros2_medkit_msgs/package.xml
index 325a010f..c5aa43e9 100644
--- a/src/ros2_medkit_msgs/package.xml
+++ b/src/ros2_medkit_msgs/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_msgs
- 0.3.0
+ 0.4.0
ROS 2 message and service definitions for ros2_medkit fault management
bburda
diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CHANGELOG.rst b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CHANGELOG.rst
new file mode 100644
index 00000000..ab5a83ec
--- /dev/null
+++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CHANGELOG.rst
@@ -0,0 +1,11 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package ros2_medkit_graph_provider
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.4.0 (2026-03-20)
+------------------
+* Initial release - extracted from ``ros2_medkit_gateway`` package
+* ``GraphProviderPlugin`` for ROS 2 graph-based entity introspection
+* Standalone external plugin package with independent build and test
+* Locking support via ``PluginContext`` API
+* Contributors: @bburda
diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/package.xml b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/package.xml
index 0c03b23c..10a46935 100644
--- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/package.xml
+++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_graph_provider
- 0.3.0
+ 0.4.0
Graph provider plugin for ros2_medkit gateway
bburda
Apache-2.0
diff --git a/src/ros2_medkit_serialization/CHANGELOG.rst b/src/ros2_medkit_serialization/CHANGELOG.rst
index 3c35ab68..bf5472e2 100644
--- a/src/ros2_medkit_serialization/CHANGELOG.rst
+++ b/src/ros2_medkit_serialization/CHANGELOG.rst
@@ -2,6 +2,13 @@
Changelog for package ros2_medkit_serialization
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+0.4.0 (2026-03-20)
+------------------
+* Enable ``POSITION_INDEPENDENT_CODE`` for MODULE target compatibility
+* Build: use shared cmake modules from ``ros2_medkit_cmake`` package
+* Build: auto-detect ccache, centralized clang-tidy configuration
+* Contributors: @bburda
+
0.3.0 (2026-02-27)
------------------
* Multi-distro CI support for ROS 2 Humble, Jazzy, and Rolling (`#219 `_, `#242 `_)
diff --git a/src/ros2_medkit_serialization/package.xml b/src/ros2_medkit_serialization/package.xml
index a9a78d16..69251bcb 100644
--- a/src/ros2_medkit_serialization/package.xml
+++ b/src/ros2_medkit_serialization/package.xml
@@ -2,7 +2,7 @@
ros2_medkit_serialization
- 0.3.0
+ 0.4.0
Runtime JSON to ROS 2 message serialization library
bburda
Apache-2.0
From ffc7910d031b41f17561d41c16a9911feba4f6b9 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 07:58:02 +0100
Subject: [PATCH 02/23] docs: rewrite root README with narrative style for
v0.4.0
---
README.md | 271 ++++++++++++++++++------------------------------------
1 file changed, 91 insertions(+), 180 deletions(-)
diff --git a/README.md b/README.md
index b6be7c94..bb326243 100644
--- a/README.md
+++ b/README.md
@@ -15,14 +15,20 @@
- Automotive-grade diagnostics for ROS 2 robots.
- When your robot fails, find out why — in minutes, not hours.
+ Structured diagnostics for ROS 2 robots.
+ When your robot fails, find out why - in minutes, not hours.
- Fault correlation · Black-box recording · REST API · AI via MCP
+ Fault management · Live introspection · REST API · AI via MCP
+## The problem
+
+When a robot breaks in the field, you SSH in, run `ros2 node list`, grep through logs, and try to reconstruct what happened. It works for one robot on your desk. It does not work for 20 robots at a customer site, at 2 AM, when you cannot reproduce the issue.
+
+ros2_medkit gives your ROS 2 system a **diagnostic REST API** so you can inspect what is running, what failed, and why, without SSH and without custom tooling.
+
## 🚀 Quick Start
**Try the full demo** (Docker, no ROS 2 needed):
@@ -34,6 +40,8 @@ cd selfpatch_demos/demos/turtlebot3_integration
# → API: http://localhost:8080/api/v1/ Web UI: http://localhost:3000
```
+Open `http://localhost:3000` in your browser. You will see a TurtleBot3 with Nav2, organized into a browsable entity tree with live faults, topic data, and parameter access.
+
**Build from source** (ROS 2 Jazzy, Humble, or Rolling):
```bash
@@ -45,15 +53,13 @@ ros2 launch ros2_medkit_gateway gateway.launch.py
# → http://localhost:8080/api/v1/areas
```
-For more examples, see our [Postman collection](postman/).
+For API examples, see our [Postman collection](postman/).
### Experimental: Pixi
-> **Note:** Pixi support is experimental and not the official build path.
-> The standard ROS 2 toolchain (rosdep + colcon) remains the primary method.
-
[Pixi](https://pixi.sh) provides a reproducible, lockfile-based environment
-without requiring a system-wide ROS 2 installation (Linux x86_64 only):
+without requiring a system-wide ROS 2 installation (Linux x86_64 only).
+This is experimental; the standard ROS 2 toolchain (rosdep + colcon) remains the primary method.
```bash
curl -fsSL https://pixi.sh/install.sh | bash
@@ -64,207 +70,112 @@ pixi run -e jazzy smoke # verify gateway starts
```
See [installation docs](https://selfpatch.github.io/ros2_medkit/installation.html#experimental-pixi)
-for details and known limitations.
-Feedback welcome on [#265](https://github.com/selfpatch/ros2_medkit/issues/265).
-
-## ✨ Features
-
-| Feature | Status | Description |
-|---------|--------|-------------|
-| 🔍 Discovery | **Available** | Automatically discover running nodes, topics, services, and actions |
-| 📊 Data | **Available** | Read and write topic data via REST |
-| ⚙️ Operations | **Available** | Call services and actions with execution tracking |
-| 🔧 Configurations | **Available** | Read, write, and reset node parameters |
-| 🚨 Faults | **Available** | Query, inspect, and clear faults with environment data and snapshots |
-| 📦 Bulk Data | **Available** | Upload, download, and manage files (calibration, firmware, rosbags) |
-| 📡 Subscriptions | **Available** | Stream live data and fault events via SSE |
-| 🎯 Triggers | **Available** | Condition-based push notifications for resource changes |
-| 🔄 Software Updates | **Available** | Async prepare/execute lifecycle with pluggable backends |
-| 🔒 Authentication | **Available** | JWT-based RBAC (viewer, operator, configurator, admin) |
-| 📋 Logs | **Available** | Log sources, entries, and configuration |
-| 🔁 Entity Lifecycle | Planned | Start, restart, shutdown control |
-| 🔐 Modes & Locking | Planned | Target mode control and resource locking |
-| 📝 Scripts | **Available** | Diagnostic script upload and execution (SOVD 7.15) |
-| 🧹 Clear Data | Planned | Clear cached and learned diagnostic data |
-| 📞 Communication Logs | Planned | Protocol-level communication logging |
-
-## 📖 Overview
-
-ros2_medkit models a robot as a **diagnostic entity tree**:
-
-| Entity | Description | Example |
-|--------|-------------|---------|
-| **Area** | Physical or logical domain | `base`, `arm`, `safety`, `navigation` |
-| **Component** | Hardware or software component within an area | `motor_controller`, `lidar_driver` |
-| **Function** | Capability provided by one or more components | `localization`, `obstacle_detection` |
-| **App** | Deployable software unit | node, container, process |
-
-Compatible with the **SOVD (Service-Oriented Vehicle Diagnostics)** model — same concepts across robots, vehicles, and embedded systems.
-
-## 📋 Requirements
-
-- **OS:** Ubuntu 24.04 LTS (Jazzy / Rolling) or Ubuntu 22.04 LTS (Humble)
-- **ROS 2:** Jazzy Jalisco, Humble Hawksbill, or Rolling (experimental)
-- **Compiler:** GCC 11+ (C++17 support)
-- **Build System:** colcon + ament_cmake
-
-## 📚 Documentation
-
-- 📖 [Full Documentation](https://selfpatch.github.io/ros2_medkit/)
-- 🗺️ [Roadmap](https://selfpatch.github.io/ros2_medkit/roadmap.html)
-- 📋 [GitHub Milestones](https://github.com/selfpatch/ros2_medkit/milestones)
-
-## 💬 Community
-
-We'd love to have you join our community!
+for details. Feedback welcome on [#265](https://github.com/selfpatch/ros2_medkit/issues/265).
-- **💬 Discord** — [Join our server](https://discord.gg/6CXPMApAyq) for discussions, help, and announcements
-- **🐛 Issues** — [Report bugs or request features](https://github.com/selfpatch/ros2_medkit/issues)
-- **💡 Discussions** — [GitHub Discussions](https://github.com/selfpatch/ros2_medkit/discussions) for Q&A and ideas
+## What you get
----
-
-## 🛠️ Development
+**Start here: Faults.** Your robot has 47 nodes. Something throws an error.
+Instead of grepping logs, you query `GET /api/v1/faults` and get a structured list
+with fault codes, timestamps, affected entities, environment snapshots, and history.
+Clear faults, subscribe to new ones via SSE, correlate them across components.
-This section is for contributors and developers who want to build and test ros2_medkit locally.
+Beyond faults, medkit exposes the full ROS 2 graph through REST:
-### Pre-commit Hooks
+| | What it does |
+|---|---|
+| **Discovery** | Automatically finds running nodes, topics, services, and actions |
+| **Data** | Read and write topic data via REST |
+| **Operations** | Call services and actions with execution tracking |
+| **Configurations** | Read, write, and reset node parameters |
+| **Bulk Data** | Upload/download files (calibration, firmware, rosbags) |
+| **Subscriptions** | Stream live data and fault events via SSE |
+| **Triggers** | Condition-based push notifications for resource changes |
+| **Locking** | Resource locking for safe concurrent access |
+| **Scripts** | Upload and execute diagnostic scripts on entities |
+| **Software Updates** | Async prepare/execute lifecycle with pluggable backends |
+| **Authentication** | JWT-based RBAC (viewer, operator, configurator, admin) |
+| **Logs** | Log entries and configuration |
-This project uses [pre-commit](https://pre-commit.com/) to automatically run
-`clang-format`, `flake8`, and other checks on staged files before each commit,
-plus an incremental clang-tidy check on `git push`.
+On the [roadmap](https://selfpatch.github.io/ros2_medkit/roadmap.html): entity lifecycle control, mode management, communication logs.
-```bash
-pip install pre-commit
-pre-commit install
-pre-commit install --hook-type pre-push
-```
+## How it organizes your robot
-To run all hooks against every file (useful after first setup):
+medkit models your system as an **entity tree** with four levels:
-```bash
-pre-commit run --all-files
```
-
-### Installing Dependencies
-
-```bash
-rosdep install --from-paths src --ignore-src -r -y
+Areas Components Apps (nodes)
+───── ────────── ────────────
+base ┬─ motor_controller ┬─ left_wheel_driver
+ │ └─ right_wheel_driver
+ └─ battery_monitor └─ bms_node
+
+navigation ┬─ lidar_driver └─ rplidar_node
+ └─ nav_stack ┬─ nav2_controller
+ ├─ nav2_planner
+ └─ nav2_bt_navigator
```
-### Building
+A small robot might have a single area. A large robot can use areas to separate physical domains:
-```bash
-colcon build --symlink-install
```
-
-### Testing
-
-Use the `scripts/test.sh` convenience script:
-
-```bash
-source install/setup.bash
-./scripts/test.sh # unit tests only (default)
-./scripts/test.sh integ # integration tests only
-./scripts/test.sh lint # linters (excluding clang-tidy)
-./scripts/test.sh tidy # clang-tidy only (slow, ~8-10 min)
-./scripts/test.sh all # everything
-./scripts/test.sh # single test by CTest name regex
+areas/
+├── base/
+│ └── components/
+│ ├── motor_controller/ → apps: left_wheel, right_wheel
+│ └── battery_monitor/ → apps: bms_node
+├── arm/
+│ └── components/
+│ ├── joint_controller/ → apps: joint_1..joint_6
+│ └── gripper/ → apps: gripper_driver
+├── navigation/
+│ └── components/
+│ ├── lidar_driver/ → apps: rplidar_node
+│ ├── camera_driver/ → apps: realsense_node
+│ └── nav_stack/ → apps: controller, planner, bt_navigator
+└── safety/
+ └── components/
+ ├── emergency_stop/ → apps: estop_monitor
+ └── collision_detect/ → apps: collision_checker
```
-You can pass extra colcon arguments after the preset:
-
-```bash
-./scripts/test.sh unit --packages-select ros2_medkit_gateway
-```
-
-### Pre-push Hook (clang-tidy)
-
-An incremental clang-tidy check runs automatically on `git push` via pre-commit, analyzing only changed `.cpp` files. Typical run takes 5-30s vs 8-10 min for a full analysis.
+**Functions** cut across the tree. A function like `localization` might depend on apps from both `navigation` and `base`, giving you a capability-oriented view alongside the physical hierarchy.
-Setup:
+This entity model follows the **SOVD (Service-Oriented Vehicle Diagnostics)** standard, so the same concepts work across robots, vehicles, and embedded systems.
-```bash
-# Install the pre-push hook (if not already done above)
-pre-commit install --hook-type pre-push
+## 📋 Requirements
-# Build the merged compile_commands.json (required once after build)
-./scripts/merge-compile-commands.sh
-```
+- **OS:** Ubuntu 24.04 LTS (Jazzy / Rolling) or Ubuntu 22.04 LTS (Humble)
+- **ROS 2:** Jazzy Jalisco, Humble Hawksbill, or Rolling (experimental)
+- **Compiler:** GCC 11+ (C++17 support)
+- **Build System:** colcon + ament_cmake
-To run manually without pushing:
+## 📚 Documentation
-```bash
-./scripts/clang-tidy-diff.sh
-```
+- 📖 [Full Documentation](https://selfpatch.github.io/ros2_medkit/)
+- 🗺️ [Roadmap](https://selfpatch.github.io/ros2_medkit/roadmap.html)
+- 📋 [GitHub Milestones](https://github.com/selfpatch/ros2_medkit/milestones)
-### Code Coverage
+## 💬 Community
-To generate code coverage reports locally:
+- **💬 Discord** - [Join our server](https://discord.gg/6CXPMApAyq) for discussions, help, and announcements
+- **🐛 Issues** - [Report bugs or request features](https://github.com/selfpatch/ros2_medkit/issues)
+- **💡 Discussions** - [GitHub Discussions](https://github.com/selfpatch/ros2_medkit/discussions) for Q&A and ideas
-1. Build with coverage flags enabled:
+## 🤝 Contributing
-```bash
-colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
-```
+Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for build instructions, testing, pre-commit hooks, CI/CD details, and code coverage.
-2. Run tests:
+Quick version:
```bash
+pip install pre-commit && pre-commit install && pre-commit install --hook-type pre-push
+colcon build --symlink-install
source install/setup.bash
-colcon test --ctest-args -LE linter
+./scripts/test.sh # unit tests
+./scripts/test.sh all # everything
```
-3. Generate coverage report:
-
-```bash
-lcov --capture --directory build --output-file coverage.raw.info --ignore-errors mismatch,negative
-lcov --extract coverage.raw.info '*/ros2_medkit/src/*/src/*' '*/ros2_medkit/src/*/include/*' --output-file coverage.info --ignore-errors unused
-lcov --list coverage.info
-```
-
-4. (Optional) Generate HTML report:
-
-```bash
-genhtml coverage.info --output-directory coverage_html
-```
-
-Then open `coverage_html/index.html` in your browser.
-
-### CI/CD
-
-All pull requests and pushes to main are automatically built and tested using GitHub Actions.
-The CI workflow tests across **ROS 2 Jazzy** (Ubuntu 24.04), **ROS 2 Humble** (Ubuntu 22.04), and **ROS 2 Rolling** (Ubuntu 24.04, allow-failure):
-
-**build-and-test** (matrix: Humble + Rolling):
-
-- Full build with ccache and unit/integration tests
-- Rolling jobs are allowed to fail (best-effort forward-compatibility)
-
-**jazzy-build** / **jazzy-lint** / **jazzy-test**:
-
-- `jazzy-build` compiles all packages with ccache and clang-tidy enabled
-- `jazzy-lint` and `jazzy-test` run in parallel after the build completes
-- Linting includes clang-format, clang-tidy, copyright, cmake-lint, and more
-
-**coverage** (Jazzy only):
-
-- Builds with coverage instrumentation (Debug mode, ccache-enabled)
-- Runs unit and integration tests (excluding linters)
-- Generates lcov coverage report (available as artifact)
-- Uploads coverage to Codecov (only on push to main)
-
-After every run the workflow uploads test results and coverage reports as artifacts for debugging and review.
-
----
-
-## 🤝 Contributing
-
-Contributions are welcome! Whether it's bug reports, feature requests, documentation improvements, or code contributions — we appreciate your help.
-
-1. Read our [Contributing Guidelines](CONTRIBUTING.md)
-2. Check out [good first issues](https://github.com/selfpatch/ros2_medkit/labels/good%20first%20issue) for beginners
-3. Follow our [Code of Conduct](CODE_OF_CONDUCT.md)
+Check out [good first issues](https://github.com/selfpatch/ros2_medkit/labels/good%20first%20issue) for places to start.
## 🔒 Security
@@ -272,7 +183,7 @@ If you discover a security vulnerability, please follow the responsible disclosu
## 📄 License
-This project is licensed under the **Apache License 2.0** — see the [LICENSE](LICENSE) file for details.
+Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
---
From 1b062f54830326f140cc4bb44a481276b22c4c4e Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:00:06 +0100
Subject: [PATCH 03/23] docs: add READMEs for 6 new packages
---
src/ros2_medkit_cmake/README.md | 44 +++++++++++++++++++
.../ros2_medkit_beacon_common/README.md | 36 +++++++++++++++
.../ros2_medkit_linux_introspection/README.md | 39 ++++++++++++++++
.../ros2_medkit_param_beacon/README.md | 40 +++++++++++++++++
.../ros2_medkit_topic_beacon/README.md | 39 ++++++++++++++++
.../ros2_medkit_graph_provider/README.md | 38 ++++++++++++++++
6 files changed, 236 insertions(+)
create mode 100644 src/ros2_medkit_cmake/README.md
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/README.md
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/README.md
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/README.md
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/README.md
create mode 100644 src/ros2_medkit_plugins/ros2_medkit_graph_provider/README.md
diff --git a/src/ros2_medkit_cmake/README.md b/src/ros2_medkit_cmake/README.md
new file mode 100644
index 00000000..d79623a8
--- /dev/null
+++ b/src/ros2_medkit_cmake/README.md
@@ -0,0 +1,44 @@
+# ros2_medkit_cmake
+
+Shared CMake modules for the ros2_medkit workspace. Provides multi-distro compatibility,
+build acceleration, and centralized linting configuration across all packages.
+
+## Modules
+
+| Module | Description |
+|--------|-------------|
+| `ROS2MedkitCcache.cmake` | Auto-detect and configure ccache with PCH-aware sloppiness settings |
+| `ROS2MedkitLinting.cmake` | Centralized clang-tidy configuration (opt-in locally, mandatory in CI) |
+| `ROS2MedkitCompat.cmake` | Multi-distro compatibility shims for ROS 2 Humble, Jazzy, and Rolling |
+
+### ROS2MedkitCompat
+
+Resolves dependency differences across ROS 2 distributions:
+
+- `medkit_find_yaml_cpp()` - Finds yaml-cpp (namespaced targets on Jazzy, manual fallback on Humble)
+- `medkit_find_cpp_httplib()` - Finds cpp-httplib >= 0.14 via pkg-config or CMake config
+- `medkit_target_dependencies()` - Drop-in replacement for `ament_target_dependencies` (removed on Rolling)
+- `medkit_detect_compat_defs()` / `medkit_apply_compat_defs()` - Compile definitions for version-specific APIs
+
+## Usage
+
+In your package's `CMakeLists.txt`:
+
+```cmake
+find_package(ros2_medkit_cmake REQUIRED)
+include(ROS2MedkitCcache)
+include(ROS2MedkitLinting)
+include(ROS2MedkitCompat)
+```
+
+Add to `package.xml`:
+
+```xml
+ros2_medkit_cmake
+```
+
+The cmake modules are automatically available via ament's extras hook after `find_package`.
+
+## License
+
+Apache License 2.0
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/README.md b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/README.md
new file mode 100644
index 00000000..c4a239bb
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/README.md
@@ -0,0 +1,36 @@
+# ros2_medkit_beacon_common
+
+Shared C++ library for beacon-based discovery plugins. Provides hint validation,
+entity mapping, response building, and rate limiting used by both `ros2_medkit_topic_beacon`
+and `ros2_medkit_param_beacon`.
+
+## Components
+
+| Class | Purpose |
+|-------|---------|
+| `BeaconHint` | Data struct for discovery hints (entity ID, topology, transport, process diagnostics, metadata) |
+| `BeaconHintStore` | Thread-safe hint storage with TTL-based expiration and deduplication |
+| `BeaconValidator` | Input validation for entity IDs, required fields, and hint consistency |
+| `BeaconEntityMapper` | Maps beacon hints to SOVD entity hierarchy (Apps, Components, Functions, Areas) |
+| `BeaconResponseBuilder` | Builds JSON responses for gateway vendor extension endpoints |
+| `TokenBucket` | Thread-safe rate limiter for high-frequency beacon streams |
+
+## Usage
+
+This package is a dependency of the beacon discovery plugins. It is not used directly
+by end users. Plugin developers building custom beacon types should link against this
+library for validation and entity mapping.
+
+```cmake
+find_package(ros2_medkit_beacon_common REQUIRED)
+target_link_libraries(my_beacon_plugin ros2_medkit_beacon_common::ros2_medkit_beacon_common)
+```
+
+## Related Packages
+
+- [ros2_medkit_topic_beacon](../ros2_medkit_topic_beacon/) - Push-based beacon via ROS 2 topics
+- [ros2_medkit_param_beacon](../ros2_medkit_param_beacon/) - Pull-based beacon via ROS 2 parameters
+
+## License
+
+Apache License 2.0
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/README.md b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/README.md
new file mode 100644
index 00000000..f090981c
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/README.md
@@ -0,0 +1,39 @@
+# ros2_medkit_linux_introspection
+
+Gateway discovery plugin that exposes Linux process-level diagnostics via vendor extension endpoints.
+Maps ROS 2 nodes to OS processes and reports CPU, memory, systemd unit status, and container context.
+
+## Plugins
+
+| Plugin | Endpoint | Data Source |
+|--------|----------|-------------|
+| `procfs_plugin` | `x-medkit-procfs` | `/proc/[pid]/{stat,status,cmdline}` - CPU time, memory, threads, FDs |
+| `systemd_plugin` | `x-medkit-systemd` | D-Bus sd-bus API - unit state, resource usage |
+| `container_plugin` | `x-medkit-container` | Cgroup hierarchy - container runtime, resource limits |
+
+## Key Components
+
+- **ProcReader** - Parses procfs files for process metrics with configurable proc root
+- **CgroupReader** - Reads cgroup v2 hierarchy for container/service context
+- **SystemdUtils** - Queries systemd via D-Bus for service metadata
+- **PidCache** - TTL-based cache mapping ROS 2 node names to Linux PIDs
+
+## Configuration
+
+Configure via `gateway_params.yaml` plugin parameters:
+
+```yaml
+plugins: ["linux_introspection"]
+plugins.linux_introspection.path: "/path/to/libros2_medkit_linux_introspection.so"
+plugins.linux_introspection.pid_cache_ttl_sec: 30
+plugins.linux_introspection.proc_root: "/proc"
+```
+
+## Documentation
+
+- [Linux Introspection Tutorial](https://selfpatch.github.io/ros2_medkit/tutorials/linux-introspection.html)
+- [Plugin System Guide](https://selfpatch.github.io/ros2_medkit/tutorials/plugin-system.html)
+
+## License
+
+Apache License 2.0
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/README.md b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/README.md
new file mode 100644
index 00000000..2390caff
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/README.md
@@ -0,0 +1,40 @@
+# ros2_medkit_param_beacon
+
+Gateway discovery plugin that polls ROS 2 node parameters for entity metadata.
+Enables pull-based beacon discovery where nodes advertise diagnostic hints
+through standard ROS 2 parameters.
+
+## How It Works
+
+1. The plugin queries nodes for a known beacon parameter (JSON-encoded `BeaconHint`)
+2. Hints are validated via `BeaconValidator` and stored in `BeaconHintStore` with TTL
+3. Entity metadata is mapped into the SOVD hierarchy via `BeaconEntityMapper`
+4. Results are exposed at the `x-medkit-param-beacon` vendor extension endpoint
+
+In non-hybrid discovery mode, the plugin discovers poll targets automatically from the
+ROS 2 graph. In hybrid mode, targets come from the manifest.
+
+## Configuration
+
+```yaml
+plugins: ["param_beacon"]
+plugins.param_beacon.path: "/path/to/libros2_medkit_param_beacon.so"
+plugins.param_beacon.poll_interval_sec: 10
+plugins.param_beacon.poll_budget_ms: 500
+plugins.param_beacon.timeout_ms: 1000
+plugins.param_beacon.ttl_sec: 60
+plugins.param_beacon.expiry_sec: 120
+```
+
+See [discovery options](https://selfpatch.github.io/ros2_medkit/config/discovery-options.html)
+for full configuration reference.
+
+## When to Use
+
+Use the parameter beacon when entity metadata is **stable and infrequently updated** -
+hardware descriptions, capabilities, firmware versions. For real-time metadata that
+changes frequently, use the [topic beacon](../ros2_medkit_topic_beacon/) instead.
+
+## License
+
+Apache License 2.0
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/README.md b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/README.md
new file mode 100644
index 00000000..d36d2704
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/README.md
@@ -0,0 +1,39 @@
+# ros2_medkit_topic_beacon
+
+Gateway discovery plugin that subscribes to ROS 2 topics for real-time entity metadata.
+Enables push-based beacon discovery where nodes publish `MedkitDiscoveryHint` messages
+to enrich the SOVD entity tree.
+
+## How It Works
+
+1. Nodes publish `ros2_medkit_msgs::msg::MedkitDiscoveryHint` messages on a beacon topic
+2. The plugin subscribes, validates hints, and stores them with stamp-based TTL
+3. A token bucket rate limiter prevents overload from high-frequency publishers
+4. Results are exposed at the `x-medkit-topic-beacon` vendor extension endpoint
+
+Hints transition through states: **active** (within TTL) -> **stale** (TTL expired,
+data still served with stale marker) -> **expired** (removed from store).
+
+## Configuration
+
+```yaml
+plugins: ["topic_beacon"]
+plugins.topic_beacon.path: "/path/to/libros2_medkit_topic_beacon.so"
+plugins.topic_beacon.ttl_sec: 30
+plugins.topic_beacon.expiry_sec: 90
+plugins.topic_beacon.allow_new_entities: true
+plugins.topic_beacon.rate_limit_hz: 10.0
+```
+
+See [discovery options](https://selfpatch.github.io/ros2_medkit/config/discovery-options.html)
+for full configuration reference.
+
+## When to Use
+
+Use the topic beacon when entity metadata **changes in real time** - sensor health,
+connection quality, load metrics. For stable metadata that rarely changes, use the
+[parameter beacon](../ros2_medkit_param_beacon/) instead.
+
+## License
+
+Apache License 2.0
diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/README.md b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/README.md
new file mode 100644
index 00000000..c2f92bd4
--- /dev/null
+++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/README.md
@@ -0,0 +1,38 @@
+# ros2_medkit_graph_provider
+
+Gateway plugin that provides ROS 2 topic graph analysis with latency, frequency,
+and drop-rate metrics. This is the default `IntrospectionProvider` - extracted
+from `ros2_medkit_gateway` into a standalone plugin package in v0.4.0.
+
+## What It Does
+
+- Subscribes to `/diagnostics` for hardware-level metrics
+- Builds a topic graph with per-topic metrics: frequency (Hz), latency (ms), drop rate (%)
+- Detects stale topics with no recent data
+- Tracks which nodes published to which topics
+- Provides graph state snapshots via the `x-medkit-graph` vendor extension endpoint
+
+## Configuration
+
+Loaded as a gateway plugin via `gateway.launch.py` (configured by default):
+
+```yaml
+plugins: ["graph_provider"]
+plugins.graph_provider.path: "/path/to/libros2_medkit_graph_provider.so"
+plugins.graph_provider.expected_frequency_hz: 30.0
+```
+
+## Architecture
+
+The plugin implements `GatewayPlugin` + `IntrospectionProvider`:
+
+- `GraphProviderPlugin` - Main plugin class with ROS 2 subscriptions
+- `GraphBuildState` - Internal state tracking topic metrics and staleness
+- `GraphBuildConfig` - Configuration (expected frequency defaults)
+
+Entity cache is populated from the ROS 2 graph during each discovery cycle
+via the merge pipeline's `PluginLayer`.
+
+## License
+
+Apache License 2.0
From 018b781c2a8afc6692d4582a273cf882dbb23552 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:00:32 +0100
Subject: [PATCH 04/23] docs(gateway): add scripts, OpenAPI, vendor extension
endpoint docs
---
src/ros2_medkit_gateway/README.md | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/src/ros2_medkit_gateway/README.md b/src/ros2_medkit_gateway/README.md
index a0983035..18bd4483 100644
--- a/src/ros2_medkit_gateway/README.md
+++ b/src/ros2_medkit_gateway/README.md
@@ -82,6 +82,32 @@ All endpoints are prefixed with `/api/v1` for API versioning.
- `PUT /api/v1/{components|apps}/{id}/locks/{lock_id}` - Extend lock expiration
- `DELETE /api/v1/{components|apps}/{id}/locks/{lock_id}` - Release a lock
+### Scripts Endpoints
+
+- `GET /api/v1/{entity}/{id}/scripts` - List available diagnostic scripts
+- `POST /api/v1/{entity}/{id}/scripts` - Upload a new script
+- `GET /api/v1/{entity}/{id}/scripts/{script_id}` - Get script details
+- `DELETE /api/v1/{entity}/{id}/scripts/{script_id}` - Delete a script
+- `POST /api/v1/{entity}/{id}/scripts/{script_id}/executions` - Start script execution
+- `GET /api/v1/{entity}/{id}/scripts/{script_id}/executions/{eid}` - Get execution status and output
+- `PUT /api/v1/{entity}/{id}/scripts/{script_id}/executions/{eid}` - Send stdin or control signals
+- `DELETE /api/v1/{entity}/{id}/scripts/{script_id}/executions/{eid}` - Cancel execution
+
+### API Documentation (OpenAPI)
+
+- `GET /api/v1/docs` - Full OpenAPI 3.0 specification
+- `GET /api/v1/{entity_type}/{id}/docs` - Entity-scoped OpenAPI spec
+- `GET /api/v1/swagger-ui` - Interactive Swagger UI (requires build with `-DENABLE_SWAGGER_UI=ON`)
+
+### Vendor Extension Endpoints
+
+- `GET /api/v1/{entity}/{id}/x-medkit-topic-beacon` - Topic beacon metadata
+- `GET /api/v1/{entity}/{id}/x-medkit-param-beacon` - Parameter beacon metadata
+- `GET /api/v1/{entity}/{id}/x-medkit-procfs` - Process info (procfs plugin)
+- `GET /api/v1/{entity}/{id}/x-medkit-systemd` - Systemd unit status
+- `GET /api/v1/{entity}/{id}/x-medkit-container` - Container runtime info
+- `GET /api/v1/{entity}/{id}/x-medkit-graph` - ROS 2 graph details
+
### API Reference
#### GET /api/v1/areas
From 0a13f3d84efcff7067f4a19b61598e1534b178ce Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:06:49 +0100
Subject: [PATCH 05/23] docs: fix versions, add missing endpoint sections to
rest.rst, update roadmap for v0.4.0
---
docs/api/rest.rst | 10 ++++-
docs/conf.py | 4 +-
docs/index.rst | 6 ---
docs/roadmap.rst | 110 ++++++++++++++++++++++++++--------------------
4 files changed, 73 insertions(+), 57 deletions(-)
diff --git a/docs/api/rest.rst b/docs/api/rest.rst
index 6d49a5fa..d69deea2 100644
--- a/docs/api/rest.rst
+++ b/docs/api/rest.rst
@@ -25,7 +25,7 @@ Server Capabilities
{
"name": "ROS 2 Medkit Gateway",
- "version": "0.3.0",
+ "version": "0.4.0",
"api_base": "/api/v1",
"endpoints": [
"GET /api/v1/health",
@@ -66,7 +66,7 @@ Server Capabilities
"version": "1.0.0",
"base_uri": "/api/v1",
"vendor_info": {
- "version": "0.3.0",
+ "version": "0.4.0",
"name": "ros2_medkit"
}
}
@@ -494,6 +494,12 @@ Manage ROS 2 node parameters.
``DELETE /api/v1/components/{id}/configurations``
Reset all parameters to default values.
+Resource Locking
+----------------
+
+SOVD resource locking for preventing concurrent modification of entity state.
+See :doc:`locking` for the full API reference.
+
Faults Endpoints
----------------
diff --git a/docs/conf.py b/docs/conf.py
index c47b2277..6725564b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,8 +42,8 @@
project_copyright = f"{datetime.now().year}, selfpatch"
author = "selfpatch Team"
-version = "0.3.0"
-release = "0.3.0"
+version = "0.4.0"
+release = "0.4.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
diff --git a/docs/index.rst b/docs/index.rst
index 4a00a98a..1adf85ec 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -13,10 +13,6 @@ external tools, web interfaces, and remote diagnostics. It automatically
discovers nodes, organizes them into a hierarchical entity tree, and provides
endpoints for data access, operations, configurations, and fault management.
-.. note::
-
- Version 0.1.0 - First public release.
-
Quick Links
-----------
@@ -128,5 +124,3 @@ Community
glossary
changelog
-
-
diff --git a/docs/roadmap.rst b/docs/roadmap.rst
index 444cb708..08546969 100644
--- a/docs/roadmap.rst
+++ b/docs/roadmap.rst
@@ -13,13 +13,13 @@ Strategy
The project follows a phased approach that prioritizes developer experience:
-1. **Start with Apps (ROS 2 nodes)** — The level closest to developers. Provide tooling
+1. **Start with Apps (ROS 2 nodes)** - The level closest to developers. Provide tooling
that makes their daily work easier: discovering nodes, reading data, checking faults.
-2. **Expand to full system** — Once app-level features are solid, extend coverage to
+2. **Expand to full system** - Once app-level features are solid, extend coverage to
Components, Areas, and system-wide operations.
-3. **API coverage first, quality later** — The initial goal is to cover the entire
+3. **API coverage first, quality later** - The initial goal is to cover the entire
standard API surface using available ROS 2 capabilities. This enables early integration
with real projects and compliance validation. Code hardening and production-ready
implementations will follow in later phases.
@@ -27,8 +27,8 @@ The project follows a phased approach that prioritizes developer experience:
Milestones
----------
-MS1: Full Discovery & Data Access
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+MS1: Full Discovery & Data Access - Complete
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Goal:** Provide a complete view of the ROS 2 system and enable basic data operations.
@@ -37,29 +37,29 @@ diagnostic data. This milestone establishes the foundation for all subsequent fe
**Standard API coverage:**
-- Discovery (11 endpoints) — version info, entity collections, hierarchy navigation
-- Data (5 endpoints) — data categories, groups, read/write operations
+- [x] Discovery (11 endpoints) - version info, entity collections, hierarchy navigation
+- [x] Data (5 endpoints) - data categories, groups, read/write operations
`MS1 on GitHub `_
-MS2: App Observability
-~~~~~~~~~~~~~~~~~~~~~~
+MS2: App Observability - Complete
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Goal:** Enable developers to monitor application health and diagnose issues.
With observability features, developers can check fault codes, browse logs, and monitor
-the lifecycle state of their nodes — essential for debugging and troubleshooting.
+the lifecycle state of their nodes - essential for debugging and troubleshooting.
**Standard API coverage:**
-- Faults (4 endpoints) — list, inspect, and clear diagnostic faults
-- Logs (5 endpoints) — browse and configure application logs
-- Lifecycle (6 endpoints) — status, start, restart, shutdown operations
+- [x] Faults (4 endpoints) - list, inspect, and clear diagnostic faults
+- [x] Logs (5 endpoints) - browse and configure application logs
+- [ ] Lifecycle (6 endpoints) - status, start, restart, shutdown operations
`MS2 on GitHub `_
-MS3: Configuration & Control
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+MS3: Configuration & Control - Complete
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Goal:** Allow developers to configure and control running applications.
@@ -68,14 +68,14 @@ operation execution, giving developers runtime control over their nodes.
**Standard API coverage:**
-- Configuration (5 endpoints) — read/write configuration values
-- Operations (7 endpoints) — define, execute, and monitor operations
-- Modes (3 endpoints) — entity operating modes
+- [x] Configuration (5 endpoints) - read/write configuration values
+- [x] Operations (7 endpoints) - define, execute, and monitor operations
+- [ ] Modes (3 endpoints) - entity operating modes
`MS3 on GitHub `_
-MS4: Automation & Subscriptions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+MS4: Automation & Subscriptions - In Progress
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Goal:** Enable automated workflows and reactive data collection.
@@ -84,32 +84,34 @@ and define triggers for event-driven workflows.
**Standard API coverage:**
-- Scripts (8 endpoints) — upload, execute, and manage diagnostic scripts
-- Subscriptions (8 endpoints) — cyclic data subscriptions and triggers
-- Datasets (4 endpoints) — dynamic data lists for subscription
+- [x] Scripts (8 endpoints) - upload, execute, and manage diagnostic scripts
+- [x] Cyclic Subscriptions (6 endpoints) - periodic push-based resource delivery via SSE
+- [ ] Triggers - event-driven subscriptions
+- [ ] Datasets (4 endpoints) - dynamic data lists for subscription
`MS4 on GitHub `_
-MS5: Fleet & Advanced Features
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+MS5: Fleet & Advanced Features - In Progress
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Goal:** Complete SOVD API coverage with fleet-oriented and advanced features.
The final milestone adds capabilities for fleet management, bulk data transfer,
-software updates, and authentication — completing the standard specification coverage.
+software updates, and authentication - completing the standard specification coverage.
**Standard API coverage:**
-- Bulk Data (5 endpoints) — large data transfers
-- Communication Logs (5 endpoints) — protocol-level logging
-- Clear Data (5 endpoints) — cache and learned data management
-- ✅ Updates (8 endpoints) — software update management
-- Auth (2 endpoints) — authorization and token management
+- [x] Bulk Data (5 endpoints) - large data transfers (upload, download, delete)
+- [x] Updates (8 endpoints) - software update management via plugin framework
+- [x] Auth (3 endpoints) - authorization, token refresh, and revocation
+- [x] Locking (4 endpoints) - SOVD resource locking with scoped access control
+- [ ] Communication Logs (5 endpoints) - protocol-level logging
+- [ ] Clear Data (5 endpoints) - cache and learned data management
`MS5 on GitHub `_
-MS6: Fault Management System
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+MS6: Fault Management System - Complete
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Goal:** Add fault management to ros2_medkit with fault reporting, two-level filtering
(local + central), persistent storage, lifecycle management, and REST API access.
@@ -120,34 +122,48 @@ and central aggregation.
**Key features:**
-- **Two-level filtering**: FaultReporter (local) + FaultManager (central)
-- **Multi-source aggregation**: Same fault code from multiple sources combined into single entry
-- **Persistent storage**: Fault state survives restarts
-- **REST API + SSE**: Real-time fault monitoring via HTTP
-- **Backwards compatibility**: Integration with ``diagnostic_updater``
-
-**Success criteria:**
-
-- FaultReporter library with local filtering (default enabled)
-- FaultManager with central aggregation and lifecycle management
-- Storage survives node restarts
-- REST API endpoints for fault CRUD operations
-- Server-Sent Events (SSE) for real-time fault updates
+- [x] **Two-level filtering**: FaultReporter (local) + FaultManager (central)
+- [x] **Multi-source aggregation**: Same fault code from multiple sources combined into single entry
+- [x] **Persistent storage**: Fault state survives restarts
+- [x] **REST API + SSE**: Real-time fault monitoring via HTTP
+- [x] **Backwards compatibility**: Integration with ``diagnostic_updater``
See :doc:`design/ros2_medkit_fault_manager/index` and :doc:`design/ros2_medkit_fault_reporter/index`
for detailed architecture documentation.
`MS6 on GitHub `_
+Beyond Milestones
+~~~~~~~~~~~~~~~~~
+
+The following features were added outside the original milestone plan:
+
+- [x] **Plugin Framework** - Load custom functionality from shared libraries with provider
+ interfaces (UpdateProvider, IntrospectionProvider, LogProvider, ScriptProvider)
+- [x] **OpenAPI / Swagger UI** - Auto-generated ``/docs`` endpoint with interactive API explorer
+- [x] **Vendor Extensions** - procfs, systemd, container, topic-beacon, and parameter-beacon
+ introspection plugins
+- [x] **Rate Limiting** - Token-bucket-based per-client and per-endpoint rate limiting
+- [x] **Graph Provider** - Function-scoped topology snapshots with per-topic metrics
+
Future Directions
-----------------
-After achieving full standard API coverage, the project will focus on:
+With most of the SOVD API surface covered, the project is shifting focus toward
+production readiness and ecosystem integration:
+
+**Remaining SOVD Coverage**
+ Complete the remaining specification endpoints: triggers, communication logs,
+ clear data, datasets, lifecycle management, and entity operating modes.
**Code Hardening**
Replace initial implementations with production-ready code. Remove workarounds
and refactor into smaller, well-tested packages.
+**Ecosystem Integration**
+ Deepen integration with ROS 2 tooling (Foxglove, MCP-based LLM workflows)
+ and commercial extensions (fleet gateway, fault correlation engine, OTA updates).
+
**Additional Diagnostic Protocols**
Extend support to other diagnostic standards relevant to robotics and embedded
systems.
From 2b186b097263beac22a430b20d1b2817418c6821 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:07:06 +0100
Subject: [PATCH 06/23] docs(config): add Scripts and Authentication
configuration sections to server.rst
---
docs/config/server.rst | 141 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 141 insertions(+)
diff --git a/docs/config/server.rst b/docs/config/server.rst
index 984d114e..fe127741 100644
--- a/docs/config/server.rst
+++ b/docs/config/server.rst
@@ -393,6 +393,82 @@ Example:
See :doc:`/api/rest` for rate limiting response headers and 429 behavior.
+Authentication
+--------------
+
+JWT-based authentication with Role-Based Access Control (RBAC). Disabled by
+default for local development.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 35 10 25 30
+
+ * - Parameter
+ - Type
+ - Default
+ - Description
+ * - ``auth.enabled``
+ - bool
+ - ``false``
+ - Enable/disable JWT authentication.
+ * - ``auth.jwt_secret``
+ - string
+ - ``""``
+ - JWT signing secret. For HS256: the shared secret string. For RS256: path to the private key file (PEM format).
+ * - ``auth.jwt_public_key``
+ - string
+ - ``""``
+ - Path to public key file for RS256. Required for RS256, optional for HS256.
+ * - ``auth.jwt_algorithm``
+ - string
+ - ``"HS256"``
+ - JWT signing algorithm: ``"HS256"`` (symmetric) or ``"RS256"`` (asymmetric).
+ * - ``auth.token_expiry_seconds``
+ - int
+ - ``3600``
+ - Access token validity period in seconds (1 hour).
+ * - ``auth.refresh_token_expiry_seconds``
+ - int
+ - ``86400``
+ - Refresh token validity period in seconds (24 hours). Must be >= ``token_expiry_seconds``.
+ * - ``auth.require_auth_for``
+ - string
+ - ``"write"``
+ - When to require authentication: ``"none"`` (auth endpoints still available), ``"write"`` (POST/PUT/DELETE only), or ``"all"`` (every request).
+ * - ``auth.issuer``
+ - string
+ - ``"ros2_medkit_gateway"``
+ - JWT issuer claim (``iss`` field in tokens).
+ * - ``auth.clients``
+ - string[]
+ - ``[]``
+ - Pre-configured clients as ``"client_id:client_secret:role"`` strings.
+
+.. note::
+
+ RBAC roles and their permissions:
+
+ - **viewer** - Read-only access (GET on areas, components, data, faults)
+ - **operator** - Viewer + trigger operations, acknowledge faults, publish data
+ - **configurator** - Operator + modify/reset configurations
+ - **admin** - Full access including auth management
+
+Example:
+
+.. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ auth:
+ enabled: true
+ jwt_secret: "my-secret-key"
+ jwt_algorithm: "HS256"
+ require_auth_for: "write"
+ token_expiry_seconds: 3600
+ clients: ["admin:admin_secret_123:admin", "viewer:viewer_pass:viewer"]
+
+See :doc:`/tutorials/authentication` for a complete setup tutorial.
+
Plugin Framework
----------------
@@ -518,9 +594,21 @@ Complete Example
updates:
enabled: true
+ auth:
+ enabled: true
+ jwt_secret: "my-secret-key"
+ jwt_algorithm: "HS256"
+ require_auth_for: "write"
+ clients: ["admin:admin_secret_123:admin"]
+
rate_limiting:
enabled: false
+ scripts:
+ scripts_dir: "/var/ros2_medkit/scripts"
+ allow_uploads: true
+ max_concurrent_executions: 5
+
API Documentation
-----------------
@@ -556,6 +644,59 @@ Example:
docs:
enabled: true
+Scripts
+-------
+
+Diagnostic scripts: upload, manage, and execute scripts on entities. Set
+``scripts.scripts_dir`` to a directory path to enable the feature. When left
+empty, all script endpoints return HTTP 501.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 35 10 15 40
+
+ * - Parameter
+ - Type
+ - Default
+ - Description
+ * - ``scripts.scripts_dir``
+ - string
+ - ``""``
+ - Directory for storing uploaded scripts. Empty string disables the feature.
+ * - ``scripts.allow_uploads``
+ - bool
+ - ``true``
+ - Allow uploading scripts via HTTP. Set to ``false`` for hardened deployments that only use manifest-defined scripts.
+ * - ``scripts.max_file_size_mb``
+ - int
+ - ``10``
+ - Maximum uploaded script file size in megabytes.
+ * - ``scripts.max_concurrent_executions``
+ - int
+ - ``5``
+ - Maximum number of scripts executing concurrently.
+ * - ``scripts.default_timeout_sec``
+ - int
+ - ``300``
+ - Default timeout per execution in seconds (5 minutes).
+ * - ``scripts.max_execution_history``
+ - int
+ - ``100``
+ - Maximum number of completed executions to keep in memory. Oldest completed entries are evicted when this limit is exceeded.
+
+Example:
+
+.. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ scripts:
+ scripts_dir: "/var/ros2_medkit/scripts"
+ allow_uploads: true
+ max_file_size_mb: 10
+ max_concurrent_executions: 5
+ default_timeout_sec: 300
+
Locking
-------
From 1b39171063e7140f03959f840581ac1968b43093 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:07:16 +0100
Subject: [PATCH 07/23] docs(tutorials): add ScriptProvider and PluginContext
v4 to plugin system tutorial
---
docs/tutorials/plugin-system.rst | 213 ++++++++++++++++++++++++++++++-
1 file changed, 207 insertions(+), 6 deletions(-)
diff --git a/docs/tutorials/plugin-system.rst b/docs/tutorials/plugin-system.rst
index e60e5b65..34d2ea37 100644
--- a/docs/tutorials/plugin-system.rst
+++ b/docs/tutorials/plugin-system.rst
@@ -17,6 +17,9 @@ Plugins implement the ``GatewayPlugin`` C++ base class plus one or more typed pr
- **LogProvider** - replaces or augments the default ``/rosout`` log backend.
Can operate in observer mode (receives log entries) or full-ingestion mode
(owns the entire log pipeline). See the ``/logs`` endpoints in :doc:`/api/rest`.
+- **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 ``/scripts`` endpoints in :doc:`/api/rest`.
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).
@@ -126,7 +129,7 @@ Writing a Plugin
return static_cast(p);
}
-The ``get_update_provider`` (and ``get_introspection_provider``, ``get_log_provider``) functions use ``extern "C"``
+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.
@@ -222,7 +225,7 @@ Plugin Lifecycle
1. ``dlopen`` loads the ``.so`` with ``RTLD_NOW | RTLD_LOCAL``
2. ``plugin_api_version()`` is checked against the gateway's ``PLUGIN_API_VERSION``
3. ``create_plugin()`` factory function creates the plugin instance
-4. Provider interfaces are queried via ``get_update_provider()`` / ``get_introspection_provider()`` / ``get_log_provider()``
+4. Provider interfaces are queried via ``get_update_provider()`` / ``get_introspection_provider()`` / ``get_log_provider()`` / ``get_script_provider()``
5. ``configure()`` is called with per-plugin JSON config
6. ``set_context()`` provides ``PluginContext`` with ROS 2 node, entity cache, faults, and HTTP utilities
7. ``register_routes()`` allows registering custom REST endpoints
@@ -241,6 +244,8 @@ providing access to gateway data and utilities:
- ``validate_entity_for_route(req, res, entity_id)`` - validate entity exists and matches the route type, auto-sending SOVD errors on failure
- ``send_error()`` / ``send_json()`` - SOVD-compliant HTTP response helpers (static methods)
- ``register_capability()`` / ``register_entity_capability()`` - register custom capabilities on entities
+- ``check_lock(entity_id, client_id, collection)`` - verify lock access before mutating operations; returns ``LockAccessResult`` with ``allowed`` flag and denial details
+- ``acquire_lock()`` / ``release_lock()`` - acquire and release entity locks with optional scope and TTL
- ``get_entity_snapshot()`` - returns an ``IntrospectionInput`` populated from the current entity cache
- ``list_all_faults()`` - returns JSON object with a ``"faults"`` array containing all active faults across all entities
- ``register_sampler(collection, fn)`` - registers a cyclic subscription sampler for a custom collection name
@@ -291,10 +296,84 @@ for the lower-level registry API.
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 ``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).
+ 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:
+
+.. code-block:: cpp
+
+ 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:
+
+.. code-block:: cpp
+
+ 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:
+
+.. code-block:: cpp
+
+ 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:
+
+.. code-block:: cpp
+
+ ctx_->register_sampler("x-medkit-metrics",
+ [this](const std::string& entity_id, const std::string& /*resource_path*/)
+ -> tl::expected {
+ 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
---------------------
@@ -439,6 +518,127 @@ 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:
+
+.. code-block:: cpp
+
+ #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, ScriptBackendErrorInfo>
+ list_scripts(const std::string& /*entity_id*/) override {
+ return std::vector{};
+ }
+
+ // ScriptProvider: get metadata for a specific script
+ tl::expected
+ 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
+ upload_script(const std::string& /*entity_id*/, const std::string& /*filename*/,
+ const std::string& /*content*/,
+ const std::optional& /*metadata*/) override {
+ return tl::make_unexpected(
+ ScriptBackendErrorInfo{ScriptBackendError::UnsupportedType, "uploads not supported"});
+ }
+
+ // ScriptProvider: delete a script
+ tl::expected
+ delete_script(const std::string& /*entity_id*/, const std::string& /*script_id*/) override {
+ return {};
+ }
+
+ // ScriptProvider: start executing a script
+ tl::expected
+ 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
+ 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
+ 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
+ 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(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:
+
+.. code-block:: yaml
+
+ 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
----------------
@@ -452,6 +652,7 @@ Multiple plugins can be loaded simultaneously:
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)
From fd027dace83da2e42875f5adf221cfff1e25ec50 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:12:18 +0100
Subject: [PATCH 08/23] docs(tutorials): add locking, scripts, beacon
discovery, and OpenAPI tutorials
---
docs/tutorials/beacon-discovery.rst | 609 ++++++++++++++++++++++++++++
docs/tutorials/index.rst | 16 +
docs/tutorials/locking.rst | 325 +++++++++++++++
docs/tutorials/openapi.rst | 84 ++++
docs/tutorials/scripts.rst | 416 +++++++++++++++++++
5 files changed, 1450 insertions(+)
create mode 100644 docs/tutorials/beacon-discovery.rst
create mode 100644 docs/tutorials/locking.rst
create mode 100644 docs/tutorials/openapi.rst
create mode 100644 docs/tutorials/scripts.rst
diff --git a/docs/tutorials/beacon-discovery.rst b/docs/tutorials/beacon-discovery.rst
new file mode 100644
index 00000000..43e0ad13
--- /dev/null
+++ b/docs/tutorials/beacon-discovery.rst
@@ -0,0 +1,609 @@
+Beacon Discovery Plugins
+========================
+
+This tutorial explains how to use the beacon discovery plugins to enrich
+SOVD entities with runtime metadata from your ROS 2 nodes. Beacons let nodes
+self-report identity, topology, transport, and process information that the
+gateway cannot infer from the ROS 2 graph alone.
+
+.. contents:: Table of Contents
+ :local:
+ :depth: 2
+
+Overview
+--------
+
+The gateway's built-in runtime discovery maps ROS 2 nodes to SOVD entities
+automatically, but the information it can extract is limited to what the
+ROS 2 graph API exposes - node names, namespaces, topics, services, and
+actions. Details like transport type (shared memory, zero-copy), process IDs,
+host identifiers, logical function membership, and human-friendly display
+names are invisible to the graph API.
+
+**Beacon discovery** fills this gap. It provides two complementary plugins
+that collect runtime metadata from nodes and inject it into the gateway's
+entity model via the merge pipeline:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 25 25 50
+
+ * - Plugin
+ - Model
+ - How it works
+ * - **TopicBeaconPlugin**
+ - Push-based
+ - Nodes publish ``MedkitDiscoveryHint`` messages to a shared topic.
+ The plugin subscribes and processes hints in real time.
+ * - **ParameterBeaconPlugin**
+ - Pull-based
+ - Nodes declare standard ROS 2 parameters under a known prefix. The
+ plugin polls each node's parameter service periodically.
+
+Both plugins feed into the merge pipeline's **PluginLayer** with
+``ENRICHMENT`` policy - they add metadata to entities already discovered
+by the manifest and runtime layers, without overriding authoritative fields.
+Both can run simultaneously, each maintaining a separate hint store.
+
+The MedkitDiscoveryHint Message
+-------------------------------
+
+Both beacon plugins work with the same data model. The topic beacon uses it
+directly as a ROS 2 message; the parameter beacon maps individual ROS 2
+parameters to the same fields.
+
+``ros2_medkit_msgs/msg/MedkitDiscoveryHint``:
+
+.. code-block:: text
+
+ # === Required ===
+ string entity_id # Target entity (App or Component ID)
+
+ # === Identity hints ===
+ string stable_id # Stable ID alias
+ string display_name # Human-friendly name
+
+ # === Topology hints ===
+ string[] function_ids # Function membership
+ string[] depends_on # Entity dependencies
+ string component_id # Parent Component binding
+
+ # === Transport hints ===
+ string transport_type # "nitros_zero_copy", "shared_memory", etc.
+ string negotiated_format # "nitros_image_bgr8", etc.
+
+ # === Process diagnostics ===
+ uint32 process_id # OS process ID (PID), 0 = not provided
+ string process_name # Process name
+ string hostname # Host identifier
+
+ # === Freeform ===
+ diagnostic_msgs/KeyValue[] metadata # Arbitrary key-value pairs
+
+ # === Timing ===
+ builtin_interfaces/Time stamp # Timestamp for TTL calculation
+
+All fields except ``entity_id`` are optional. Empty strings and empty arrays
+are treated as "not provided" and the plugin ignores them.
+
+Hint Lifecycle
+^^^^^^^^^^^^^^
+
+Every hint stored by a beacon plugin transitions through three states based
+on its age:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 15 85
+
+ * - State
+ - Description
+ * - **ACTIVE**
+ - Within ``beacon_ttl_sec``. Enrichment is applied normally.
+ * - **STALE**
+ - Past TTL but within ``beacon_expiry_sec``. Enrichment is still applied,
+ but the vendor endpoint marks the hint as ``"status": "stale"``.
+ * - **EXPIRED**
+ - Past ``beacon_expiry_sec``. The hint is removed from the store and no
+ longer contributes to enrichment.
+
+This two-tier lifecycle lets consumers distinguish between fresh data and
+data that may be outdated, without immediately losing all enrichment when a
+node stops publishing.
+
+Topic Beacon
+------------
+
+The **TopicBeaconPlugin** (``ros2_medkit_topic_beacon`` package) subscribes
+to a ROS 2 topic and processes incoming ``MedkitDiscoveryHint`` messages in
+real time. This is the push-based approach - nodes actively publish their
+metadata at a chosen frequency.
+
+How It Works
+^^^^^^^^^^^^
+
+1. Your node creates a publisher on ``/ros2_medkit/discovery`` (configurable).
+2. The node publishes ``MedkitDiscoveryHint`` messages periodically (e.g., 1 Hz).
+3. The plugin receives each message, validates it, converts it to an internal
+ ``BeaconHint``, and stores it in a thread-safe ``BeaconHintStore``.
+4. On each discovery cycle, the plugin's ``introspect()`` method snapshots the
+ store and maps hints to SOVD entities via the ``BeaconEntityMapper``.
+5. A token bucket rate limiter (configurable via ``max_messages_per_second``)
+ prevents overload from high-frequency publishers.
+
+Publisher Example (C++)
+^^^^^^^^^^^^^^^^^^^^^^^
+
+A minimal node that publishes beacon hints:
+
+.. code-block:: cpp
+
+ #include
+ #include
+ #include
+
+ class MyBeaconNode : public rclcpp::Node {
+ public:
+ MyBeaconNode() : Node("my_sensor_node") {
+ publisher_ = create_publisher(
+ "/ros2_medkit/discovery", 10);
+
+ timer_ = create_wall_timer(std::chrono::seconds(1), [this]() {
+ publish_beacon();
+ });
+ }
+
+ private:
+ void publish_beacon() {
+ auto msg = ros2_medkit_msgs::msg::MedkitDiscoveryHint();
+
+ // Required - must match an entity known to the gateway
+ msg.entity_id = "engine_temp_sensor";
+
+ // Identity
+ msg.display_name = "Engine Temperature Sensor";
+
+ // Topology - assign to a function and declare dependencies
+ msg.function_ids = {"thermal_monitoring"};
+ msg.component_id = "powertrain";
+
+ // Transport
+ msg.transport_type = "shared_memory";
+
+ // Process diagnostics
+ msg.process_id = static_cast(getpid());
+ msg.process_name = "sensor_node";
+ char hostname[256];
+ if (gethostname(hostname, sizeof(hostname)) == 0) {
+ msg.hostname = hostname;
+ }
+
+ // Freeform metadata
+ diagnostic_msgs::msg::KeyValue kv;
+ kv.key = "firmware_version";
+ kv.value = "2.1.0";
+ msg.metadata.push_back(kv);
+
+ // Timestamp for TTL calculation
+ msg.stamp = now();
+
+ publisher_->publish(msg);
+ }
+
+ rclcpp::Publisher::SharedPtr publisher_;
+ rclcpp::TimerBase::SharedPtr timer_;
+ };
+
+The ``stamp`` field is used for TTL calculation. If the stamp is non-zero,
+the plugin computes the message age by comparing the stamp against the
+system clock and back-projects into ``steady_clock`` domain. If the stamp
+is zero, the plugin falls back to using reception time.
+
+CMakeLists.txt Dependencies
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Your node package needs a dependency on ``ros2_medkit_msgs``:
+
+.. code-block:: cmake
+
+ find_package(ros2_medkit_msgs REQUIRED)
+
+ add_executable(my_sensor_node src/my_sensor_node.cpp)
+ ament_target_dependencies(my_sensor_node rclcpp ros2_medkit_msgs diagnostic_msgs)
+
+And in ``package.xml``:
+
+.. code-block:: xml
+
+ ros2_medkit_msgs
+ diagnostic_msgs
+
+Configuration
+^^^^^^^^^^^^^
+
+Enable the topic beacon in ``gateway_params.yaml``:
+
+.. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ plugins: ["topic_beacon"]
+ plugins.topic_beacon.path: "/path/to/libtopic_beacon_plugin.so"
+
+ # ROS 2 topic to subscribe (default: "/ros2_medkit/discovery")
+ plugins.topic_beacon.topic: "/ros2_medkit/discovery"
+
+ # Soft TTL - hints older than this are marked STALE (default: 10.0)
+ plugins.topic_beacon.beacon_ttl_sec: 10.0
+
+ # Hard expiry - hints older than this are removed (default: 300.0)
+ plugins.topic_beacon.beacon_expiry_sec: 300.0
+
+ # Allow plugin to introduce new entities not seen by other layers
+ # When false (recommended), only existing entities are enriched
+ plugins.topic_beacon.allow_new_entities: false
+
+ # Maximum number of hints in memory (default: 10000)
+ plugins.topic_beacon.max_hints: 10000
+
+ # Rate limit for incoming messages (default: 100)
+ plugins.topic_beacon.max_messages_per_second: 100
+
+The ``.so`` path is typically resolved at launch time. If you build the
+gateway with colcon, the plugin is installed alongside the gateway packages.
+
+Querying Topic Beacon Data
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The plugin registers a vendor extension endpoint on all apps and components:
+
+.. code-block:: bash
+
+ curl http://localhost:8080/api/v1/apps/engine_temp_sensor/x-medkit-topic-beacon
+
+Example response:
+
+.. code-block:: json
+
+ {
+ "entity_id": "engine_temp_sensor",
+ "status": "active",
+ "age_sec": 1.234,
+ "stable_id": "",
+ "display_name": "Engine Temperature Sensor",
+ "transport_type": "shared_memory",
+ "negotiated_format": "",
+ "process_id": 12345,
+ "process_name": "sensor_node",
+ "hostname": "robot-1",
+ "component_id": "powertrain",
+ "function_ids": ["thermal_monitoring"],
+ "depends_on": [],
+ "metadata": {"firmware_version": "2.1.0"}
+ }
+
+Parameter Beacon
+----------------
+
+The **ParameterBeaconPlugin** (``ros2_medkit_param_beacon`` package) takes
+the opposite approach - instead of nodes pushing data, the plugin actively
+polls each node's parameter service for parameters matching a configurable
+prefix.
+
+How It Works
+^^^^^^^^^^^^
+
+1. Your node declares ROS 2 parameters under the ``ros2_medkit.discovery``
+ prefix (configurable) using standard ``declare_parameter()`` calls.
+2. The plugin runs a background polling thread that periodically lists and
+ reads these parameters from each known node.
+3. In hybrid mode, poll targets come from the introspection input (apps with
+ a bound FQN). In runtime-only mode, the plugin discovers nodes directly
+ from the ROS 2 graph.
+4. Parameters are mapped to ``BeaconHint`` fields, validated, and stored.
+5. Exponential backoff is applied to nodes whose parameter service is
+ unreachable, preventing wasted cycles on crashed or unavailable nodes.
+
+Parameter Naming Convention
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Nodes declare parameters under the configured prefix (default:
+``ros2_medkit.discovery``). Each parameter maps to a specific ``BeaconHint``
+field:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 45 20 35
+
+ * - Parameter name
+ - Type
+ - BeaconHint field
+ * - ``ros2_medkit.discovery.entity_id``
+ - string (required)
+ - ``entity_id``
+ * - ``ros2_medkit.discovery.stable_id``
+ - string
+ - ``stable_id``
+ * - ``ros2_medkit.discovery.display_name``
+ - string
+ - ``display_name``
+ * - ``ros2_medkit.discovery.component_id``
+ - string
+ - ``component_id``
+ * - ``ros2_medkit.discovery.transport_type``
+ - string
+ - ``transport_type``
+ * - ``ros2_medkit.discovery.negotiated_format``
+ - string
+ - ``negotiated_format``
+ * - ``ros2_medkit.discovery.process_name``
+ - string
+ - ``process_name``
+ * - ``ros2_medkit.discovery.hostname``
+ - string
+ - ``hostname``
+ * - ``ros2_medkit.discovery.process_id``
+ - integer
+ - ``process_id``
+ * - ``ros2_medkit.discovery.function_ids``
+ - string array
+ - ``function_ids``
+ * - ``ros2_medkit.discovery.depends_on``
+ - string array
+ - ``depends_on``
+ * - ``ros2_medkit.discovery.metadata.``
+ - string
+ - ``metadata[]``
+
+Freeform metadata uses sub-parameters under ``ros2_medkit.discovery.metadata.``.
+For example, ``ros2_medkit.discovery.metadata.firmware_version`` maps to
+``metadata["firmware_version"]`` in the hint.
+
+Node Example (C++)
+^^^^^^^^^^^^^^^^^^
+
+A minimal node that declares beacon parameters:
+
+.. code-block:: cpp
+
+ #include
+ #include
+
+ class MyParamBeaconNode : public rclcpp::Node {
+ public:
+ MyParamBeaconNode() : Node("my_sensor_node") {
+ // Required - must match an entity known to the gateway
+ declare_parameter("ros2_medkit.discovery.entity_id", "engine_temp_sensor");
+
+ // Identity
+ declare_parameter("ros2_medkit.discovery.display_name", "Engine Temperature Sensor");
+
+ // Topology
+ declare_parameter("ros2_medkit.discovery.function_ids",
+ std::vector{"thermal_monitoring"});
+ declare_parameter("ros2_medkit.discovery.component_id", "powertrain");
+
+ // Transport
+ declare_parameter("ros2_medkit.discovery.transport_type", "shared_memory");
+
+ // Process diagnostics
+ declare_parameter("ros2_medkit.discovery.process_id",
+ static_cast(getpid()));
+ declare_parameter("ros2_medkit.discovery.process_name", "sensor_node");
+
+ // Freeform metadata
+ declare_parameter("ros2_medkit.discovery.metadata.firmware_version", "2.1.0");
+ }
+ };
+
+No custom publishing code is needed. The plugin handles all polling
+automatically. Parameters can also be set at launch time via command line
+or YAML files:
+
+.. code-block:: bash
+
+ ros2 run my_package my_sensor_node \
+ --ros-args \
+ -p ros2_medkit.discovery.entity_id:=engine_temp_sensor \
+ -p ros2_medkit.discovery.display_name:="Engine Temperature Sensor" \
+ -p ros2_medkit.discovery.transport_type:=shared_memory
+
+Configuration
+^^^^^^^^^^^^^
+
+Enable the parameter beacon in ``gateway_params.yaml``:
+
+.. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ plugins: ["parameter_beacon"]
+ plugins.parameter_beacon.path: "/path/to/libparam_beacon_plugin.so"
+
+ # Parameter name prefix to scan (default: "ros2_medkit.discovery")
+ plugins.parameter_beacon.parameter_prefix: "ros2_medkit.discovery"
+
+ # How often to poll all nodes in seconds (default: 5.0)
+ plugins.parameter_beacon.poll_interval_sec: 5.0
+
+ # Maximum time per poll cycle in seconds (default: 10.0)
+ plugins.parameter_beacon.poll_budget_sec: 10.0
+
+ # Timeout for each node's parameter service call in seconds (default: 2.0)
+ plugins.parameter_beacon.param_timeout_sec: 2.0
+
+ # Soft TTL - hints older than this are marked STALE (default: 15.0)
+ plugins.parameter_beacon.beacon_ttl_sec: 15.0
+
+ # Hard expiry - hints older than this are removed (default: 300.0)
+ plugins.parameter_beacon.beacon_expiry_sec: 300.0
+
+ # Allow plugin to introduce new entities (default: false)
+ plugins.parameter_beacon.allow_new_entities: false
+
+ # Maximum hints in memory (default: 10000)
+ plugins.parameter_beacon.max_hints: 10000
+
+.. note::
+
+ The plugin automatically validates timing relationships. If
+ ``beacon_ttl_sec`` is less than or equal to ``poll_interval_sec``, hints
+ would go stale between polls - the plugin auto-corrects to
+ ``ttl = 3 * poll_interval``. Similarly, if ``beacon_expiry_sec`` is less
+ than or equal to ``beacon_ttl_sec``, it auto-corrects to
+ ``expiry = 10 * ttl``.
+
+Querying Parameter Beacon Data
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The plugin registers its own vendor extension endpoint:
+
+.. code-block:: bash
+
+ curl http://localhost:8080/api/v1/apps/engine_temp_sensor/x-medkit-param-beacon
+
+The response format is identical to the topic beacon endpoint.
+
+Running Both Plugins
+--------------------
+
+The topic and parameter beacon plugins can be active simultaneously. Each
+maintains its own ``BeaconHintStore`` and contributes independently to the
+merge pipeline. A node could use the topic beacon for frequently changing
+metadata (transport metrics, load data) and the parameter beacon for stable
+identity metadata (display name, function membership).
+
+.. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ plugins: ["topic_beacon", "parameter_beacon"]
+ plugins.topic_beacon.path: "/path/to/libtopic_beacon_plugin.so"
+ plugins.topic_beacon.beacon_ttl_sec: 10.0
+ plugins.topic_beacon.beacon_expiry_sec: 300.0
+
+ plugins.parameter_beacon.path: "/path/to/libparam_beacon_plugin.so"
+ plugins.parameter_beacon.poll_interval_sec: 5.0
+ plugins.parameter_beacon.beacon_ttl_sec: 15.0
+ plugins.parameter_beacon.beacon_expiry_sec: 300.0
+
+When both plugins provide a hint for the same entity, both sets of metadata
+are merged into the entity response. Align TTL and expiry values across
+plugins to avoid inconsistent staleness behavior.
+
+Entity Metadata Injection
+-------------------------
+
+When a beacon hint is applied to an entity, the following metadata keys
+appear in the entity's JSON response (e.g., ``GET /api/v1/apps/{id}``).
+These keys are in the entity's ``metadata`` field, separate from the vendor
+extension endpoint:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 40 15 45
+
+ * - Key
+ - Type
+ - Description
+ * - ``x-medkit-beacon-status``
+ - string
+ - ``"active"`` or ``"stale"``
+ * - ``x-medkit-beacon-age-sec``
+ - number
+ - Seconds since the hint was last seen
+ * - ``x-medkit-beacon-transport-type``
+ - string
+ - DDS transport type (e.g., ``"shared_memory"``)
+ * - ``x-medkit-beacon-negotiated-format``
+ - string
+ - Negotiated data format
+ * - ``x-medkit-beacon-process-id``
+ - integer
+ - OS process ID (PID)
+ * - ``x-medkit-beacon-process-name``
+ - string
+ - Process name
+ * - ``x-medkit-beacon-hostname``
+ - string
+ - Host identifier
+ * - ``x-medkit-beacon-stable-id``
+ - string
+ - Stable identity alias
+ * - ``x-medkit-beacon-depends-on``
+ - array
+ - Entity IDs this entity depends on
+ * - ``x-medkit-beacon-functions``
+ - array
+ - Function IDs this entity belongs to
+ * - ``x-medkit-beacon-``
+ - string
+ - Freeform metadata from the hint's ``metadata`` field
+
+When to Use Which
+-----------------
+
+**Use the Topic Beacon when:**
+
+- Metadata changes in real time - transport type, load metrics, connection
+ quality, process health.
+- You want low-latency updates. As soon as a node publishes, the plugin
+ processes the hint within the same DDS callback.
+- Multiple entities need to be updated from a single publisher (a manager
+ node publishing hints for its child nodes).
+- You need precise control over the publish rate and timing via the
+ ``stamp`` field.
+
+**Use the Parameter Beacon when:**
+
+- Metadata is stable and infrequently updated - hardware descriptions,
+ firmware versions, display names, function membership.
+- You want zero custom code. Nodes only need to call ``declare_parameter()``
+ with the right prefix - no publisher setup, no timer, no message
+ construction.
+- Parameters can be set externally at launch time via YAML or command line,
+ without modifying node source code.
+- You prefer a pull-based model where the gateway controls the polling
+ frequency.
+
+**Use both when:**
+
+- Some metadata is real-time (topic beacon) and some is static (parameter
+ beacon). For example, a sensor node might publish live transport metrics
+ via the topic beacon while declaring its display name and function
+ membership as parameters.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 30 35 35
+
+ * - Consideration
+ - Topic Beacon
+ - Parameter Beacon
+ * - Latency
+ - Sub-second (push)
+ - ``poll_interval_sec`` (pull)
+ * - Node code required
+ - Publisher + timer + message
+ - ``declare_parameter()`` only
+ * - Bandwidth
+ - Proportional to publish rate
+ - Proportional to poll rate
+ * - External configurability
+ - Limited (node must publish)
+ - Full (``--ros-args -p``)
+ * - Multiple entities per node
+ - Yes (publish different entity_ids)
+ - One entity_id per node
+ * - Works without custom code
+ - No
+ - Yes (set params at launch)
+
+See Also
+--------
+
+- :doc:`/config/discovery-options` - Full configuration reference for both plugins
+- :doc:`plugin-system` - General plugin system architecture
+- :doc:`heuristic-apps` - Runtime discovery without beacons
+- :doc:`manifest-discovery` - Hybrid mode with manifest + beacons
+- :doc:`/api/messages` - Message definitions including ``MedkitDiscoveryHint``
diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst
index 6c7eb412..610471ad 100644
--- a/docs/tutorials/index.rst
+++ b/docs/tutorials/index.rst
@@ -12,6 +12,8 @@ Step-by-step guides for common use cases with ros2_medkit.
migration-to-manifest
authentication
https
+ locking
+ scripts
snapshots
fault-correlation
docker
@@ -20,6 +22,8 @@ Step-by-step guides for common use cases with ros2_medkit.
custom_areas
web-ui
mcp-server
+ openapi
+ beacon-discovery
plugin-system
linux-introspection
triggers-use-cases
@@ -58,6 +62,12 @@ Basic Tutorials
:doc:`https`
Enable TLS/HTTPS for secure communication.
+:doc:`locking`
+ Use SOVD resource locking to prevent concurrent modification of entity resources.
+
+:doc:`scripts`
+ Upload, execute, and manage diagnostic scripts on entities.
+
:doc:`snapshots`
Configure snapshot capture for fault debugging.
@@ -79,6 +89,9 @@ Companion Projects
:doc:`mcp-server`
ros2_medkit_mcp — Connect LLMs to your ROS 2 system via MCP protocol.
+:doc:`openapi`
+ Explore and interact with the gateway's self-describing OpenAPI spec and Swagger UI.
+
Advanced Tutorials
------------------
@@ -88,6 +101,9 @@ Advanced Tutorials
:doc:`custom_areas`
Customize the entity hierarchy for your robot architecture.
+:doc:`beacon-discovery`
+ Use topic and parameter beacon plugins to enrich entities with runtime metadata from nodes.
+
:doc:`plugin-system`
Extend the gateway with custom plugins for update backends, introspection, and REST endpoints.
diff --git a/docs/tutorials/locking.rst b/docs/tutorials/locking.rst
new file mode 100644
index 00000000..43b59398
--- /dev/null
+++ b/docs/tutorials/locking.rst
@@ -0,0 +1,325 @@
+Resource Locking
+================
+
+This tutorial shows how to use SOVD resource locking (ISO 17978-3, Section 7.17)
+to prevent concurrent modification of entity resources by multiple clients.
+
+.. contents:: Table of Contents
+ :local:
+ :depth: 2
+
+Overview
+--------
+
+When multiple clients interact with the same entity - for example, two
+diagnostic tools both trying to reconfigure a motor controller - their changes
+can conflict. Resource locking solves this by granting a client exclusive access
+to an entity's resource collections (data, configurations, operations, etc.)
+for a bounded time period.
+
+Key concepts:
+
+- **Client identification**: Each client generates a UUID and sends it via the
+ ``X-Client-Id`` header on every request
+- **Scoped locks**: A lock can protect specific collections (e.g., only
+ ``configurations``) or all collections when no scopes are specified
+- **Parent propagation**: A lock on a component also protects its child apps -
+ the gateway walks up the entity hierarchy when checking access
+- **Lock breaking**: A client can forcefully replace an existing lock by setting
+ ``break_lock: true`` (unless the entity is configured as non-breakable)
+- **Automatic expiry**: Locks expire after a TTL and are cleaned up periodically
+
+Locking is supported on **components** and **apps** only. Areas cannot be locked
+directly.
+
+Quick Example
+-------------
+
+The examples below use ``curl`` against a gateway running on ``localhost:8080``.
+Pick a client ID (any unique string) and use it consistently.
+
+.. code-block:: bash
+
+ CLIENT_ID="my-diagnostic-tool-$(uuidgen)"
+
+**1. Acquire a lock**
+
+Lock the ``configurations`` and ``operations`` collections on a component for
+5 minutes:
+
+.. code-block:: bash
+
+ curl -X POST http://localhost:8080/api/v1/components/motor_controller/locks \
+ -H "Content-Type: application/json" \
+ -H "X-Client-Id: $CLIENT_ID" \
+ -d '{
+ "lock_expiration": 300,
+ "scopes": ["configurations", "operations"]
+ }'
+
+Response (``201 Created``):
+
+.. code-block:: json
+
+ {
+ "id": "lock_1",
+ "owned": true,
+ "scopes": ["configurations", "operations"],
+ "lock_expiration": "2026-03-21T15:05:00Z"
+ }
+
+Save the ``id`` value - you need it to extend or release the lock.
+
+**2. Perform work while holding the lock**
+
+Other clients that try to modify ``configurations`` or ``operations`` on
+``motor_controller`` (or any of its child apps) will receive a ``409 Conflict``
+response until the lock is released or expires.
+
+.. code-block:: bash
+
+ # This succeeds because we hold the lock
+ curl -X PUT http://localhost:8080/api/v1/components/motor_controller/configurations/max_speed \
+ -H "Content-Type: application/json" \
+ -H "X-Client-Id: $CLIENT_ID" \
+ -d '{"value": 1500}'
+
+**3. Extend the lock**
+
+If the work takes longer than expected, extend the lock before it expires:
+
+.. code-block:: bash
+
+ curl -X PUT http://localhost:8080/api/v1/components/motor_controller/locks/lock_1 \
+ -H "Content-Type: application/json" \
+ -H "X-Client-Id: $CLIENT_ID" \
+ -d '{"lock_expiration": 600}'
+
+Response: ``204 No Content``
+
+The lock now expires 600 seconds from the time of the extend request (not from
+the original acquisition time).
+
+**4. Release the lock**
+
+When done, release the lock so other clients can proceed:
+
+.. code-block:: bash
+
+ curl -X DELETE http://localhost:8080/api/v1/components/motor_controller/locks/lock_1 \
+ -H "X-Client-Id: $CLIENT_ID"
+
+Response: ``204 No Content``
+
+**5. List locks (optional)**
+
+Check what locks exist on an entity:
+
+.. code-block:: bash
+
+ curl http://localhost:8080/api/v1/components/motor_controller/locks \
+ -H "X-Client-Id: $CLIENT_ID"
+
+The ``owned`` field in each lock item indicates whether the requesting client
+holds that lock.
+
+Lock Enforcement
+----------------
+
+By default, locking is **opt-in** - clients can acquire locks, but the gateway
+does not *require* them. To make locking mandatory for certain resource
+collections, configure ``lock_required_scopes``.
+
+When required scopes are set, any mutating request to a listed collection is
+rejected with ``409 Conflict`` unless the requesting client holds a valid lock
+on the entity.
+
+**Example**: Require a lock before modifying configurations or operations on
+any component:
+
+.. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ locking:
+ enabled: true
+ defaults:
+ components:
+ lock_required_scopes: [configurations, operations]
+ apps:
+ lock_required_scopes: [configurations]
+
+With this configuration, a ``PUT`` to
+``/components/motor_controller/configurations/max_speed`` without first
+acquiring a lock returns:
+
+.. code-block:: json
+
+ {
+ "error_code": "invalid-request",
+ "message": "Lock required for 'configurations' on entity 'motor_controller'"
+ }
+
+The two-phase access check works as follows:
+
+1. **Lock-required check** - If ``lock_required_scopes`` includes the target
+ collection, the client must hold a valid (non-expired) lock on the entity.
+ If not, access is denied immediately.
+2. **Lock-conflict check** - If another client holds a lock covering the target
+ collection, access is denied. This check walks up the parent chain
+ (app -> component -> area) so a component lock also protects child apps.
+
+Per-Entity Configuration
+------------------------
+
+The manifest ``lock:`` section lets you override lock behavior for individual
+components or apps. This is useful when certain entities have stricter
+requirements than the global defaults.
+
+.. code-block:: yaml
+
+ # manifest.yaml
+ components:
+ - id: safety_controller
+ name: Safety Controller
+ lock:
+ required_scopes: [configurations, operations, data]
+ breakable: false
+ max_expiration: 7200
+
+ - id: telemetry
+ name: Telemetry
+ lock:
+ breakable: true
+
+ apps:
+ - id: motor_driver
+ name: Motor Driver
+ component: safety_controller
+ lock:
+ required_scopes: [configurations]
+ breakable: false
+
+The three manifest lock fields are:
+
+- ``required_scopes`` - Collections that require a lock before mutation
+ (overrides the type-level ``lock_required_scopes`` default)
+- ``breakable`` - Whether other clients can use ``break_lock: true`` to replace
+ an existing lock on this entity (default: ``true``)
+- ``max_expiration`` - Maximum lock TTL in seconds for this entity
+ (``0`` = use the global ``default_max_expiration``)
+
+Configuration is resolved with the following priority:
+
+1. Per-entity manifest override (``lock:`` section on the entity)
+2. Per-type default (``locking.defaults.components`` or ``locking.defaults.apps``)
+3. Global default (``locking.default_max_expiration``)
+
+Lock Expiry
+-----------
+
+Every lock has a TTL set by ``lock_expiration`` at acquisition time. The maximum
+allowed value is capped by ``default_max_expiration`` (global) or
+``max_expiration`` (per-entity override).
+
+A background timer runs every ``cleanup_interval`` seconds (default: 30) and
+removes all expired locks. When a lock expires, the gateway also cleans up
+associated temporary resources:
+
+- **Cyclic subscriptions**: If the expired lock's scopes include
+ ``cyclic-subscriptions`` (or the lock had no scopes, meaning all collections),
+ any cyclic subscriptions for that entity are removed. This prevents orphaned
+ subscriptions from accumulating after a client disconnects without cleaning up.
+
+The cleanup timer logs each expiration:
+
+.. code-block:: text
+
+ [INFO] Lock lock_3 expired on entity motor_controller
+ [INFO] Removed subscription sub_42 on lock expiry
+
+To avoid lock expiry during long operations, clients should periodically extend
+their locks using the ``PUT`` endpoint.
+
+Plugin Integration
+------------------
+
+Gateway plugins receive a ``PluginContext`` reference that provides lock-aware
+methods. Plugins should check locks before performing mutating operations on
+entity resources.
+
+**Checking lock access:**
+
+.. code-block:: cpp
+
+ // In a plugin provider method
+ auto result = context.check_lock(entity_id, client_id, "configurations");
+ if (!result.allowed) {
+ return tl::make_unexpected("Blocked by lock: " + result.denied_reason);
+ }
+
+``check_lock`` delegates to ``LockManager::check_access`` and performs the same
+two-phase check (lock-required + lock-conflict) used by the built-in handlers.
+If locking is disabled on the gateway, ``check_lock`` always returns
+``allowed = true``.
+
+**Acquiring a lock from a plugin:**
+
+.. code-block:: cpp
+
+ auto lock_result = context.acquire_lock(entity_id, client_id, {"configurations"}, 300);
+ if (!lock_result) {
+ // lock_result.error() contains LockError with code, message, status_code
+ return tl::make_unexpected(lock_result.error().message);
+ }
+ auto lock_info = lock_result.value();
+ // lock_info.lock_id, lock_info.expires_at, etc.
+
+Configuration Reference
+-----------------------
+
+All locking parameters are documented in the server configuration reference:
+
+- :doc:`/config/server` - ``Locking`` section for gateway parameters
+- :doc:`/api/locking` - Full REST API endpoint reference with request/response schemas
+
+.. list-table::
+ :header-rows: 1
+ :widths: 40 15 45
+
+ * - Parameter
+ - Default
+ - Description
+ * - ``locking.enabled``
+ - ``true``
+ - Enable the lock manager and lock endpoints
+ * - ``locking.default_max_expiration``
+ - ``3600``
+ - Maximum lock TTL in seconds
+ * - ``locking.cleanup_interval``
+ - ``30``
+ - Seconds between expired lock cleanup sweeps
+ * - ``locking.defaults.components.lock_required_scopes``
+ - ``[]``
+ - Collections requiring a lock on components (empty = no requirement)
+ * - ``locking.defaults.components.breakable``
+ - ``true``
+ - Whether component locks can be broken
+ * - ``locking.defaults.apps.lock_required_scopes``
+ - ``[]``
+ - Collections requiring a lock on apps (empty = no requirement)
+ * - ``locking.defaults.apps.breakable``
+ - ``true``
+ - Whether app locks can be broken
+
+Valid lock scopes: ``data``, ``operations``, ``configurations``, ``faults``,
+``bulk-data``, ``modes``, ``scripts``, ``logs``, ``cyclic-subscriptions``.
+
+See Also
+--------
+
+- :doc:`/api/locking` - Locking REST API reference
+- :doc:`/config/server` - Server configuration (Locking section)
+- :doc:`/tutorials/authentication` - JWT authentication (complementary to locking)
+- :doc:`/tutorials/manifest-discovery` - Manifest YAML for per-entity lock config
+- :doc:`/tutorials/plugin-system` - Writing gateway plugins
diff --git a/docs/tutorials/openapi.rst b/docs/tutorials/openapi.rst
new file mode 100644
index 00000000..d5a4665e
--- /dev/null
+++ b/docs/tutorials/openapi.rst
@@ -0,0 +1,84 @@
+OpenAPI / Swagger
+=================
+
+The gateway self-describes its REST API using the
+`OpenAPI 3.1.0 `_ specification.
+Every level of the URL hierarchy exposes a ``/docs`` endpoint that returns
+a context-scoped OpenAPI spec - so tools and developers always have an
+accurate, up-to-date description of available operations.
+
+Accessing the Spec
+------------------
+
+Append ``/docs`` to any valid API path:
+
+.. code-block:: bash
+
+ # Full gateway spec (all endpoints)
+ curl http://localhost:8080/api/v1/docs | jq .
+
+ # Spec scoped to the components collection
+ curl http://localhost:8080/api/v1/components/docs
+
+ # Spec for a specific component and its resource collections
+ curl http://localhost:8080/api/v1/components/my_sensor/docs
+
+ # Spec for one resource collection (e.g. data)
+ curl http://localhost:8080/api/v1/components/my_sensor/data/docs
+
+Entity-level specs reflect the actual capabilities of each entity at
+runtime. Plugin-registered vendor routes also appear when the requested
+path matches a plugin route prefix.
+
+Swagger UI
+----------
+
+For an interactive API browser, build the gateway with Swagger UI support:
+
+.. code-block:: bash
+
+ colcon build --cmake-args -DENABLE_SWAGGER_UI=ON
+
+Then open ``http://localhost:8080/api/v1/swagger-ui`` in your browser.
+The UI assets are embedded in the binary - no CDN dependency at runtime.
+
+.. note::
+
+ The CMake configure step downloads assets from unpkg.com, so network
+ access is required during the build.
+
+Using with External Tools
+-------------------------
+
+The spec returned by ``/docs`` is standard OpenAPI and works with any
+compatible tooling:
+
+- **Postman** - Import ``http://localhost:8080/api/v1/docs`` as a URL to
+ auto-generate a request collection.
+- **Client generators** - Feed the spec to
+ `openapi-generator `_ to produce typed
+ clients in Python, TypeScript, Go, and many other languages.
+- **Documentation** - Render with `Redoc `_ or
+ any OpenAPI-compatible doc renderer.
+
+Configuration
+-------------
+
+The ``/docs`` endpoints are enabled by default. To disable them:
+
+.. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ docs:
+ enabled: false
+
+When disabled, all ``/docs`` endpoints return HTTP 501.
+
+See :doc:`/config/server` for the full parameter reference.
+
+See Also
+--------
+
+- :doc:`/api/rest` - Complete REST API reference
+- :doc:`/config/server` - Server configuration options
diff --git a/docs/tutorials/scripts.rst b/docs/tutorials/scripts.rst
new file mode 100644
index 00000000..1c656f40
--- /dev/null
+++ b/docs/tutorials/scripts.rst
@@ -0,0 +1,416 @@
+Diagnostic Scripts
+==================
+
+This tutorial covers SOVD diagnostic scripts - uploading, managing, and
+executing scripts on entities through the gateway REST API.
+
+.. contents:: Table of Contents
+ :local:
+ :depth: 2
+
+Overview
+--------
+
+Diagnostic scripts (SOVD ISO 17978-3, Section 7.15) let you run diagnostic
+routines on individual entities. A script is a file - shell or Python - that
+the gateway executes as a subprocess when triggered via the REST API. The
+gateway tracks each execution's lifecycle and exposes stdout, stderr, and
+exit status through a polling endpoint.
+
+Scripts are available on **Components** and **Apps** entity types.
+
+Typical use cases:
+
+- Run a sensor self-test on a specific component
+- Collect extended diagnostics that go beyond the standard data endpoints
+- Execute a calibration routine on a hardware driver
+- Trigger a cleanup or recovery procedure on a misbehaving node
+
+The feature is **disabled by default**. Set ``scripts.scripts_dir`` to a
+directory path to enable it. When disabled, all script endpoints return
+HTTP 501.
+
+Quick Example
+-------------
+
+1. **Enable scripts** in your gateway configuration:
+
+ .. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ scripts:
+ scripts_dir: "/var/ros2_medkit/scripts"
+
+2. **Upload a script** via ``multipart/form-data``:
+
+ .. code-block:: bash
+
+ curl -X POST http://localhost:8080/api/v1/components/main-computer/scripts \
+ -F "file=@check_disk.sh" \
+ -F 'metadata={"name": "Disk Check", "description": "Check disk usage and health"}'
+
+ Response (``201 Created``):
+
+ .. code-block:: json
+
+ {"id": "script_1717123456_0", "name": "Disk Check"}
+
+3. **Execute the script**:
+
+ .. code-block:: bash
+
+ curl -X POST http://localhost:8080/api/v1/components/main-computer/scripts/script_1717123456_0/executions \
+ -H "Content-Type: application/json" \
+ -d '{"execution_type": "now"}'
+
+ Response (``202 Accepted``):
+
+ .. code-block:: json
+
+ {
+ "id": "exec_1717123500_0",
+ "status": "running",
+ "progress": null,
+ "started_at": "2026-01-15T10:25:00Z",
+ "completed_at": null,
+ "parameters": null,
+ "error": null
+ }
+
+4. **Poll for completion**:
+
+ .. code-block:: bash
+
+ curl http://localhost:8080/api/v1/components/main-computer/scripts/script_1717123456_0/executions/exec_1717123500_0
+
+ Response when finished:
+
+ .. code-block:: json
+
+ {
+ "id": "exec_1717123500_0",
+ "status": "completed",
+ "progress": null,
+ "started_at": "2026-01-15T10:25:00Z",
+ "completed_at": "2026-01-15T10:25:03Z",
+ "parameters": {
+ "stdout": "Filesystem Size Used Avail Use%\n/dev/sda1 50G 32G 18G 64%\n",
+ "stderr": "",
+ "exit_code": 0
+ },
+ "error": null
+ }
+
+5. **Clean up** the execution record and script when done:
+
+ .. code-block:: bash
+
+ # Delete the execution record
+ curl -X DELETE http://localhost:8080/api/v1/components/main-computer/scripts/script_1717123456_0/executions/exec_1717123500_0
+
+ # Delete the uploaded script
+ curl -X DELETE http://localhost:8080/api/v1/components/main-computer/scripts/script_1717123456_0
+
+Script Formats
+--------------
+
+The gateway detects the script format from the uploaded filename extension and
+selects the appropriate interpreter:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 25 55
+
+ * - Extension
+ - Interpreter
+ - Notes
+ * - ``.sh``
+ - ``sh``
+ - POSIX shell. Safest choice for maximum portability.
+ * - ``.bash``
+ - ``bash``
+ - Bash-specific features (arrays, ``[[ ]]``, etc.)
+ * - ``.py``
+ - ``python3``
+ - Python 3. Useful for structured output or complex logic.
+
+Scripts are executed as subprocesses with their own process group. The gateway
+captures both stdout and stderr and includes them in the execution result along
+with the exit code.
+
+Manifest-Defined Scripts
+------------------------
+
+In addition to uploading scripts at runtime, you can pre-deploy scripts by
+populating the ``ScriptsConfig.entries`` vector programmatically. This is
+the approach used by **ScriptProvider plugins** - they can register a fixed
+set of managed scripts that are always available, cannot be deleted through
+the API, and appear with ``"managed": true`` in listing responses.
+
+Each manifest entry supports these fields:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 15 65
+
+ * - Field
+ - Type
+ - Description
+ * - ``id``
+ - string
+ - Unique script identifier
+ * - ``name``
+ - string
+ - Human-readable display name
+ * - ``description``
+ - string
+ - What the script does
+ * - ``path``
+ - string
+ - Absolute filesystem path to the script file
+ * - ``format``
+ - string
+ - Interpreter selection: ``sh``, ``bash``, or ``python``
+ * - ``timeout_sec``
+ - int
+ - Per-script timeout override (default: 300)
+ * - ``entity_filter``
+ - [string]
+ - Glob patterns restricting which entities see this script (e.g., ``["components/*"]``). Empty list means all entities.
+ * - ``env``
+ - map
+ - Extra environment variables passed to the subprocess
+ * - ``args``
+ - array
+ - Argument definitions (name, type, flag) for parameterized execution
+ * - ``parameters_schema``
+ - JSON
+ - JSON Schema describing accepted input parameters
+
+Managed scripts are validated at startup - the gateway logs a warning if a
+script's ``path`` does not point to an existing regular file.
+
+**Example: plugin providing a managed script**
+
+A ScriptProvider plugin can return pre-defined scripts in its ``list_scripts``
+implementation. See :doc:`plugin-system` for the full plugin tutorial.
+
+Execution Lifecycle
+-------------------
+
+Every script execution transitions through a defined set of states:
+
+.. code-block:: text
+
+ POST .../executions {"execution_type": "now"}
+ |
+ v
+ +----------+
+ | prepared | (initial, before subprocess starts)
+ +----------+
+ |
+ v
+ +---------+
+ | running | (subprocess active)
+ +---------+
+ |
+ +----------------------------+----------------------------+
+ | | |
+ v v v
+ +-----------+ +--------+ +------------+
+ | completed | | failed | | terminated |
+ +-----------+ +--------+ +------------+
+ (exit code 0) (non-zero exit or (stopped by user
+ internal error) or timeout)
+
+Starting an Execution
+~~~~~~~~~~~~~~~~~~~~~
+
+``POST /api/v1/{entity_type}/{entity_id}/scripts/{script_id}/executions``
+
+Required body field:
+
+- ``execution_type`` (string) - currently ``"now"`` is the supported type
+
+Optional body fields:
+
+- ``parameters`` (object) - input parameters passed to the script
+- ``proximity_response`` (string) - proof-of-proximity token for scripts that require physical access
+
+The response is ``202 Accepted`` with a ``Location`` header pointing to the
+execution status URL.
+
+Polling Status
+~~~~~~~~~~~~~~
+
+``GET /api/v1/{entity_type}/{entity_id}/scripts/{script_id}/executions/{execution_id}``
+
+Returns the current ``ExecutionInfo`` with:
+
+- ``status`` - one of ``prepared``, ``running``, ``completed``, ``failed``, ``terminated``
+- ``progress`` - optional integer (0-100) if the script reports progress
+- ``started_at`` / ``completed_at`` - ISO 8601 timestamps
+- ``parameters`` - output data including ``stdout``, ``stderr``, and ``exit_code``
+- ``error`` - error details if the execution failed
+
+Controlling a Running Execution
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``PUT /api/v1/{entity_type}/{entity_id}/scripts/{script_id}/executions/{execution_id}``
+
+Send a control action to stop a running script:
+
+.. code-block:: json
+
+ {"action": "stop"}
+
+Supported actions:
+
+- ``stop`` - sends SIGTERM to the subprocess, allowing graceful shutdown
+- ``forced_termination`` - sends SIGKILL for immediate termination
+
+Deleting an Execution Record
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``DELETE /api/v1/{entity_type}/{entity_id}/scripts/{script_id}/executions/{execution_id}``
+
+Removes a completed, failed, or terminated execution record. Returns ``204 No
+Content``. Returns ``409`` if the execution is still running.
+
+.. note::
+
+ The gateway automatically evicts the oldest completed execution records
+ when the count exceeds ``scripts.max_execution_history`` (default: 100).
+
+Timeouts
+~~~~~~~~
+
+Each execution is subject to a timeout. When the timeout expires, the gateway
+terminates the subprocess and sets the execution status to ``terminated``.
+Manifest-defined scripts can override the timeout per-script via
+``timeout_sec``. Uploaded scripts use the global
+``scripts.default_timeout_sec`` (default: 300 seconds).
+
+Concurrency
+~~~~~~~~~~~
+
+The gateway enforces a maximum number of concurrent executions across all
+entities and scripts. If the limit is reached, new execution requests return
+HTTP 429 with error code ``x-medkit-script-concurrency-limit``. The default
+limit is 5 (configurable via ``scripts.max_concurrent_executions``).
+
+Security
+--------
+
+Disabling Uploads
+~~~~~~~~~~~~~~~~~
+
+For hardened deployments that should only run pre-deployed scripts, disable
+runtime uploads:
+
+.. code-block:: yaml
+
+ ros2_medkit_gateway:
+ ros__parameters:
+ scripts:
+ scripts_dir: "/var/ros2_medkit/scripts"
+ allow_uploads: false
+
+When ``allow_uploads`` is ``false``, ``POST .../scripts`` returns HTTP 400.
+Pre-deployed manifest-defined scripts remain available for listing and
+execution.
+
+RBAC Roles
+~~~~~~~~~~
+
+When :doc:`authentication` is enabled (``auth.mode: write`` or ``auth.mode: all``),
+script endpoints are protected by role-based access control:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 80
+
+ * - Role
+ - Script Permissions
+ * - ``viewer``
+ - Read-only: list scripts, get script details, get execution status
+ * - ``operator``
+ - Viewer permissions plus: start executions, control (terminate) executions, delete execution records
+ * - ``configurator``
+ - Operator permissions plus: upload scripts, delete scripts
+ * - ``admin``
+ - All permissions (inherits from configurator)
+
+This means:
+
+- A **viewer** can inspect what scripts are available and check execution
+ results, but cannot run or modify anything.
+- An **operator** can run diagnostic scripts and stop them, but cannot upload
+ new scripts or delete existing ones.
+- A **configurator** (or **admin**) has full control over the script library.
+
+File Size Limits
+~~~~~~~~~~~~~~~~
+
+Uploaded scripts are limited to ``scripts.max_file_size_mb`` (default: 10 MB).
+Uploads exceeding the limit return HTTP 413.
+
+ScriptProvider Plugins
+----------------------
+
+The built-in ``DefaultScriptProvider`` stores uploaded scripts on the
+filesystem and executes them as POSIX subprocesses. For alternative backends -
+such as storing scripts in a database, fetching them from a remote service, or
+running them in a sandboxed container runtime - you can implement a
+``ScriptProvider`` plugin.
+
+A plugin ScriptProvider replaces the built-in backend entirely. It must
+implement all 8 interface methods (list, get, upload, delete, start execution,
+get execution, control execution, delete execution). The ``ScriptManager``
+wraps all calls with null-safety and exception isolation, so a plugin crash
+will not take down the gateway.
+
+See :doc:`plugin-system` for the full plugin development tutorial, including a
+ScriptProvider skeleton.
+
+Configuration Reference
+-----------------------
+
+All scripts configuration parameters are documented in the server
+configuration reference:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 35 10 15 40
+
+ * - Parameter
+ - Type
+ - Default
+ - Description
+ * - ``scripts.scripts_dir``
+ - string
+ - ``""``
+ - Directory for storing uploaded scripts. Empty string disables the feature.
+ * - ``scripts.allow_uploads``
+ - bool
+ - ``true``
+ - Allow uploading scripts via HTTP.
+ * - ``scripts.max_file_size_mb``
+ - int
+ - ``10``
+ - Maximum uploaded script file size in megabytes.
+ * - ``scripts.max_concurrent_executions``
+ - int
+ - ``5``
+ - Maximum number of scripts executing concurrently.
+ * - ``scripts.default_timeout_sec``
+ - int
+ - ``300``
+ - Default timeout per execution in seconds (5 minutes).
+ * - ``scripts.max_execution_history``
+ - int
+ - ``100``
+ - Maximum completed executions to keep in memory.
+
+For the full server configuration reference, see :doc:`/config/server`.
From fb23ba213109e5d9edf5e5c007885450479ffb46 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:18:24 +0100
Subject: [PATCH 09/23] docs: rewrite changelog.rst to aggregate all 13 package
changelogs
---
docs/changelog.rst | 175 ++----------------
.../CHANGELOG.rst | 4 +-
2 files changed, 18 insertions(+), 161 deletions(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 96b28f39..434eec70 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,176 +1,33 @@
Changelog
=========
-All notable changes to ros2_medkit are documented in this file.
+This page aggregates changelogs from all ros2_medkit packages.
-The format is based on `Keep a Changelog `_,
-and this project adheres to `Semantic Versioning `_.
+.. contents:: Packages
+ :local:
-[Unreleased]
-------------
+.. include:: ../src/ros2_medkit_gateway/CHANGELOG.rst
-Added
-~~~~~
+.. include:: ../src/ros2_medkit_fault_manager/CHANGELOG.rst
-* Gateway plugin framework for extending the gateway with custom functionality:
+.. include:: ../src/ros2_medkit_fault_reporter/CHANGELOG.rst
- - Load plugins from shared libraries (``.so``) via ``plugins`` parameter
- - Provider interfaces: ``UpdateProvider``, ``IntrospectionProvider`` (preview)
- - ``PluginContext`` gives plugins access to entity cache, fault data, and HTTP utilities
- - Custom capability registration (per-type and per-entity) with discovery integration
- - Error isolation: failing plugins are disabled without crashing the gateway
- - RAII lifecycle with API version checking and path validation
+.. include:: ../src/ros2_medkit_diagnostic_bridge/CHANGELOG.rst
-* Software update management via plugin framework:
+.. include:: ../src/ros2_medkit_serialization/CHANGELOG.rst
- - 8 SOVD-compliant ``/updates`` endpoints (CRUD + prepare/execute/automated/status)
- - Async lifecycle with progress tracking and status polling
- - Feature gating via ``updates.enabled`` parameter
+.. include:: ../src/ros2_medkit_msgs/CHANGELOG.rst
-* SOVD bulk-data endpoints for all entity types:
+.. include:: ../src/ros2_medkit_integration_tests/CHANGELOG.rst
- - ``GET /{entity}/bulk-data`` - list available bulk-data categories
- - ``GET /{entity}/bulk-data/{category}`` - list bulk-data descriptors
- - ``GET /{entity}/bulk-data/{category}/{id}`` - download bulk-data file
+.. include:: ../src/ros2_medkit_cmake/CHANGELOG.rst
-* Inline ``environment_data`` in fault response with:
+.. include:: ../src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst
- - ``extended_data_records``: First/last occurrence timestamps
- - ``snapshots[]``: Array of freeze_frame and rosbag entries
+.. include:: ../src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/CHANGELOG.rst
-* SOVD-compliant ``status`` object in fault response with aggregatedStatus,
- testFailed, confirmedDTC, pendingDTC fields
-* UUID identifiers for rosbag bulk-data items
-* ``x-medkit`` extensions with occurrence_count, severity_label
+.. include:: ../src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CHANGELOG.rst
-Changed
-~~~~~~~
+.. include:: ../src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CHANGELOG.rst
-* Fault response structure now SOVD-compliant with ``item`` wrapper
-* Rosbag downloads use SOVD bulk-data pattern instead of legacy endpoints
-* Rosbag IDs changed from timestamps to UUIDs
-* Software update backends now load via the plugin framework instead of
- dedicated ``updates.backend`` / ``updates.plugin_path`` parameters.
- Migrate to ``plugins`` array with ``plugins..path`` entries.
-
-Removed
-~~~~~~~
-
-* ``GET /faults/{code}/snapshots`` - use ``environment_data`` in fault response
-* ``GET /faults/{code}/snapshots/bag`` - use bulk-data endpoint
-* ``GET /{entity}/faults/{code}/snapshots`` - use ``environment_data``
-* ``GET /{entity}/faults/{code}/snapshots/bag`` - use bulk-data endpoint
-
-**Breaking Changes:**
-
-* Fault response structure changed - clients must update to handle ``item`` wrapper
- and ``environment_data`` structure
-* Legacy snapshot endpoints removed - migrate to inline snapshots and bulk-data
-* Rosbag identifiers changed from timestamps to UUIDs
-* ``updates.backend`` and ``updates.plugin_path`` parameters removed - use the
- ``plugins`` array to load update backend plugins
-
-[0.1.0] - 2026-02-01
---------------------
-
-First public release of ros2_medkit.
-
-Added
-~~~~~
-
-**Gateway (ros2_medkit_gateway)**
-
-- REST API gateway exposing ROS 2 graph via SOVD-compatible endpoints
-- Discovery endpoints: ``/areas``, ``/components``, ``/apps``, ``/functions``
-- Data access: Read topic data, publish to topics
-- Operations: Call ROS 2 services and actions with execution tracking
-- Configurations: Read/write/reset ROS 2 node parameters
-- Faults: Query and clear faults from fault manager
-- Three discovery modes: runtime_only, hybrid, manifest_only
-- Manifest-based discovery with YAML system definitions
-- Heuristic app detection in runtime mode
-- JWT authentication with RBAC (viewer, operator, configurator, admin roles)
-- TLS/HTTPS support with configurable TLS 1.2/1.3
-- CORS configuration for browser clients
-- SSE (Server-Sent Events) for real-time fault notifications
-- Health check endpoint
-
-**Fault Manager (ros2_medkit_fault_manager)**
-
-- Centralized fault storage and management node
-- ROS 2 services: ``report_fault``, ``list_faults``, ``clear_fault``
-- AUTOSAR DEM-style debounce lifecycle (PREFAILED → CONFIRMED → HEALED → CLEARED)
-- Fault aggregation from multiple sources
-- Severity escalation
-- In-memory storage with thread-safe implementation
-
-**Fault Reporter (ros2_medkit_fault_reporter)**
-
-- Client library for reporting faults from ROS 2 nodes
-- Local filtering with configurable threshold and time window
-- Fire-and-forget async service calls
-- High-severity bypass for immediate fault reporting
-
-**Diagnostic Bridge (ros2_medkit_diagnostic_bridge)**
-
-- Bridge node converting ``/diagnostics`` messages to fault manager faults
-- Configurable severity mapping from diagnostic status levels
-- Support for diagnostic arrays with multiple status entries
-
-**Serialization (ros2_medkit_serialization)**
-
-- Runtime JSON ↔ ROS 2 message serialization
-- Dynamic message introspection without compile-time type knowledge
-- Support for all ROS 2 built-in types, arrays, nested messages
-- Type caching for performance
-
-**Messages (ros2_medkit_msgs)**
-
-- ``Fault.msg``: Fault status message with severity, timestamps, sources
-- ``FaultEvent.msg``: Fault event for subscriptions
-- ``ReportFault.srv``: Service for reporting faults
-- ``ListFaults.srv``: Service for querying faults with filters
-- ``ClearFault.srv``: Service for clearing faults
-
-**Documentation**
-
-- Sphinx documentation with Doxygen integration
-- Getting Started tutorial
-- REST API reference
-- Configuration reference (server, discovery, manifest)
-- Authentication and HTTPS tutorials
-- Docker deployment guide
-- Companion project tutorials (web-ui, mcp-server)
-
-**Tooling**
-
-- Postman collection for API testing
-- VS Code tasks for build/test/launch
-- Development container configuration
-- GitHub Actions CI/CD pipeline
-
-Companion Projects
-~~~~~~~~~~~~~~~~~~
-
-- `sovd_web_ui `_: Web interface for entity browsing
-- `ros2_medkit_mcp `_: MCP server for LLM integration
-
-SOVD Compliance
-~~~~~~~~~~~~~~~
-
-This release implements a subset of the SOVD (Service-Oriented Vehicle Diagnostics)
-specification adapted for ROS 2:
-
-- Core discovery endpoints (areas, components)
-- Extended discovery (apps, functions) via manifest mode
-- Data access (read, write)
-- Operations (services, actions with executions)
-- Configurations (parameters)
-- Faults (query, clear) with environment_data
-- Bulk data transfer (rosbags via bulk-data endpoints)
-
-Not yet implemented:
-
-- Locks
-- Triggers
-- Communication logs
+.. include:: ../src/ros2_medkit_plugins/ros2_medkit_graph_provider/CHANGELOG.rst
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst
index ad297038..c6dd5b87 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CHANGELOG.rst
@@ -1,6 +1,6 @@
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Changelog for package ros2_medkit_linux_introspection
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
0.4.0 (2026-03-20)
------------------
From b0efd79c6a34b0cd03f43103020d547d4d6afec7 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:18:39 +0100
Subject: [PATCH 10/23] docs(design): add design docs and symlinks for 7 new
packages
---
docs/design/index.rst | 7 +
docs/design/ros2_medkit_beacon_common | 1 +
docs/design/ros2_medkit_cmake | 1 +
docs/design/ros2_medkit_graph_provider | 1 +
docs/design/ros2_medkit_linux_introspection | 1 +
docs/design/ros2_medkit_msgs | 1 +
docs/design/ros2_medkit_param_beacon | 1 +
docs/design/ros2_medkit_topic_beacon | 1 +
src/ros2_medkit_cmake/design/index.rst | 64 +++++
.../design/index.rst | 197 ++++++++++++++++
.../design/index.rst | 222 ++++++++++++++++++
.../ros2_medkit_param_beacon/design/index.rst | 78 ++++++
.../ros2_medkit_topic_beacon/design/index.rst | 65 +++++
src/ros2_medkit_msgs/design/index.rst | 81 +++++++
.../design/index.rst | 207 ++++++++++++++++
15 files changed, 928 insertions(+)
create mode 120000 docs/design/ros2_medkit_beacon_common
create mode 120000 docs/design/ros2_medkit_cmake
create mode 120000 docs/design/ros2_medkit_graph_provider
create mode 120000 docs/design/ros2_medkit_linux_introspection
create mode 120000 docs/design/ros2_medkit_msgs
create mode 120000 docs/design/ros2_medkit_param_beacon
create mode 120000 docs/design/ros2_medkit_topic_beacon
create mode 100644 src/ros2_medkit_cmake/design/index.rst
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/design/index.rst
create mode 100644 src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/design/index.rst
create mode 100644 src/ros2_medkit_msgs/design/index.rst
create mode 100644 src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
diff --git a/docs/design/index.rst b/docs/design/index.rst
index 168ca97c..df2bd45e 100644
--- a/docs/design/index.rst
+++ b/docs/design/index.rst
@@ -6,9 +6,16 @@ This section contains design documentation for the ros2_medkit project packages.
.. toctree::
:maxdepth: 1
+ ros2_medkit_beacon_common/index
+ ros2_medkit_cmake/index
ros2_medkit_diagnostic_bridge/index
ros2_medkit_fault_manager/index
ros2_medkit_fault_reporter/index
ros2_medkit_gateway/index
+ ros2_medkit_graph_provider/index
ros2_medkit_integration_tests/index
+ ros2_medkit_linux_introspection/index
+ ros2_medkit_msgs/index
+ ros2_medkit_param_beacon/index
ros2_medkit_serialization/index
+ ros2_medkit_topic_beacon/index
diff --git a/docs/design/ros2_medkit_beacon_common b/docs/design/ros2_medkit_beacon_common
new file mode 120000
index 00000000..4c36e9f8
--- /dev/null
+++ b/docs/design/ros2_medkit_beacon_common
@@ -0,0 +1 @@
+../../src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design
\ No newline at end of file
diff --git a/docs/design/ros2_medkit_cmake b/docs/design/ros2_medkit_cmake
new file mode 120000
index 00000000..3f01768b
--- /dev/null
+++ b/docs/design/ros2_medkit_cmake
@@ -0,0 +1 @@
+../../src/ros2_medkit_cmake/design
\ No newline at end of file
diff --git a/docs/design/ros2_medkit_graph_provider b/docs/design/ros2_medkit_graph_provider
new file mode 120000
index 00000000..27dd5f53
--- /dev/null
+++ b/docs/design/ros2_medkit_graph_provider
@@ -0,0 +1 @@
+../../src/ros2_medkit_plugins/ros2_medkit_graph_provider/design
\ No newline at end of file
diff --git a/docs/design/ros2_medkit_linux_introspection b/docs/design/ros2_medkit_linux_introspection
new file mode 120000
index 00000000..cb4abf16
--- /dev/null
+++ b/docs/design/ros2_medkit_linux_introspection
@@ -0,0 +1 @@
+../../src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design
\ No newline at end of file
diff --git a/docs/design/ros2_medkit_msgs b/docs/design/ros2_medkit_msgs
new file mode 120000
index 00000000..b9ae189b
--- /dev/null
+++ b/docs/design/ros2_medkit_msgs
@@ -0,0 +1 @@
+../../src/ros2_medkit_msgs/design
\ No newline at end of file
diff --git a/docs/design/ros2_medkit_param_beacon b/docs/design/ros2_medkit_param_beacon
new file mode 120000
index 00000000..530e1ad5
--- /dev/null
+++ b/docs/design/ros2_medkit_param_beacon
@@ -0,0 +1 @@
+../../src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/design
\ No newline at end of file
diff --git a/docs/design/ros2_medkit_topic_beacon b/docs/design/ros2_medkit_topic_beacon
new file mode 120000
index 00000000..e537d8cf
--- /dev/null
+++ b/docs/design/ros2_medkit_topic_beacon
@@ -0,0 +1 @@
+../../src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/design
\ No newline at end of file
diff --git a/src/ros2_medkit_cmake/design/index.rst b/src/ros2_medkit_cmake/design/index.rst
new file mode 100644
index 00000000..f18367d8
--- /dev/null
+++ b/src/ros2_medkit_cmake/design/index.rst
@@ -0,0 +1,64 @@
+ros2_medkit_cmake
+==================
+
+This section contains design documentation for the ros2_medkit_cmake package.
+
+Overview
+--------
+
+The ``ros2_medkit_cmake`` package is a build utility package that provides shared CMake
+modules for all other ros2_medkit packages. It contains no runtime code - only CMake
+macros and functions that are sourced via ``find_package(ros2_medkit_cmake REQUIRED)``
+and ``include()``.
+
+Modules
+-------
+
+The package provides four CMake modules installed to the ament index:
+
+1. **ros2_medkit_cmake-extras.cmake** - Ament extras hook
+
+ - Automatically sourced after ``find_package(ros2_medkit_cmake)``
+ - Appends the installed module directory to ``CMAKE_MODULE_PATH``
+ - Enables transparent ``include(ROS2MedkitCcache)`` etc. in downstream packages
+
+2. **ROS2MedkitCcache.cmake** - Compiler cache integration
+
+ - Auto-detects ``ccache`` on the system
+ - Sets ``CMAKE_C_COMPILER_LAUNCHER`` and ``CMAKE_CXX_COMPILER_LAUNCHER``
+ - Respects existing launcher overrides (does not clobber explicit settings)
+ - Must be included early in CMakeLists.txt, before ``add_library``/``add_executable``
+
+3. **ROS2MedkitLinting.cmake** - Centralized clang-tidy configuration
+
+ - Provides ``ENABLE_CLANG_TIDY`` option (default OFF, mandatory in CI)
+ - Provides ``ros2_medkit_clang_tidy()`` function with optional ``HEADER_FILTER`` and ``TIMEOUT`` arguments
+ - References the shared ``.clang-tidy`` config file from the installed module directory
+
+4. **ROS2MedkitCompat.cmake** - Multi-distro compatibility layer
+
+ - ``medkit_find_yaml_cpp()`` - Resolves yaml-cpp across Humble (no cmake target) and Jazzy (namespaced target)
+ - ``medkit_find_cpp_httplib()`` - Finds cpp-httplib via pkg-config (Jazzy/Noble) or cmake config (source build on Humble)
+ - ``medkit_detect_compat_defs()`` - Detects rclcpp and rosbag2 versions, sets ``MEDKIT_RCLCPP_VERSION_MAJOR`` and ``MEDKIT_ROSBAG2_OLD_TIMESTAMP``
+ - ``medkit_apply_compat_defs(target)`` - Applies compile definitions based on detected versions
+ - ``medkit_target_dependencies(target ...)`` - Drop-in replacement for ``ament_target_dependencies`` that also works on Rolling (where ``ament_target_dependencies`` was removed)
+
+Design Decisions
+----------------
+
+Separate Package
+~~~~~~~~~~~~~~~~
+
+Shared CMake modules live in their own ament package rather than being inlined
+into each consuming package. This avoids duplication and ensures all packages
+use the same compatibility logic. Downstream packages declare
+``ros2_medkit_cmake`` in their
+``package.xml``.
+
+Multi-Distro Strategy
+~~~~~~~~~~~~~~~~~~~~~
+
+Rather than maintaining separate branches per ROS 2 distribution, the compat
+module detects version numbers at configure time and adapts. This keeps a single
+source tree building on Humble, Jazzy, and Rolling without ``#ifdef`` proliferation
+in application code.
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
new file mode 100644
index 00000000..7dcf44d8
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
@@ -0,0 +1,197 @@
+ros2_medkit_beacon_common
+==========================
+
+This section contains design documentation for the ros2_medkit_beacon_common library.
+
+Overview
+--------
+
+The ``ros2_medkit_beacon_common`` package is a shared C++ library used by the
+beacon discovery plugins (``ros2_medkit_topic_beacon`` and ``ros2_medkit_param_beacon``).
+It provides the data model, hint storage, entity mapping, validation, and response
+building that both beacon transports rely on. It is not a plugin itself - it is
+linked as a static library by the two beacon plugin packages.
+
+Architecture
+------------
+
+The following diagram shows the beacon common components and their relationships.
+
+.. plantuml::
+ :caption: Beacon Common Library Architecture
+
+ @startuml ros2_medkit_beacon_common_architecture
+
+ skinparam linetype ortho
+ skinparam classAttributeIconSize 0
+
+ title Beacon Common - Library Architecture
+
+ package "ros2_medkit_gateway" {
+ interface IntrospectionProvider {
+ +introspect(input): IntrospectionResult
+ }
+
+ struct IntrospectionInput {
+ +areas: vector
+ +components: vector
+ +apps: vector
+ +functions: vector
+ }
+
+ struct IntrospectionResult {
+ +metadata: map
+ +new_entities: NewEntities
+ }
+ }
+
+ package "ros2_medkit_beacon_common" {
+
+ struct BeaconHint {
+ + entity_id: string
+ + stable_id: string
+ + display_name: string
+ + function_ids: vector
+ + depends_on: vector
+ + component_id: string
+ + transport_type: string
+ + negotiated_format: string
+ + process_id: uint32
+ + process_name: string
+ + hostname: string
+ + metadata: map
+ + received_at: time_point
+ }
+
+ class BeaconHintStore {
+ - hints_: map
+ - config_: Config
+ - mutex_: shared_mutex
+ --
+ + update(hint): bool
+ + evict_and_snapshot(): vector
+ + get(entity_id): optional
+ + size(): size_t
+ }
+
+ class BeaconEntityMapper {
+ - config_: Config
+ --
+ + map(hints, current): IntrospectionResult
+ --
+ - build_metadata(hint): json
+ - apply_function_membership()
+ }
+
+ class BeaconValidator <> {
+ + {static} validate_beacon_hint(hint, limits): ValidationResult
+ }
+
+ class BeaconResponseBuilder <> {
+ + {static} build_beacon_response(id, stored): json
+ }
+
+ struct "BeaconHintStore::Config" as StoreConfig {
+ + beacon_ttl_sec: double = 10.0
+ + beacon_expiry_sec: double = 300.0
+ + max_hints: size_t = 10000
+ }
+
+ struct ValidationLimits {
+ + max_id_length: 256
+ + max_string_length: 512
+ + max_function_ids: 100
+ + max_metadata_entries: 50
+ }
+
+ enum HintStatus {
+ ACTIVE
+ STALE
+ EXPIRED
+ }
+ }
+
+ BeaconHintStore o--> BeaconHint : stores
+ BeaconHintStore --> HintStatus : computes
+ BeaconHintStore --> StoreConfig : configured by
+ BeaconEntityMapper --> BeaconHintStore : reads snapshots
+ BeaconEntityMapper ..> IntrospectionResult : produces
+ BeaconEntityMapper ..> IntrospectionInput : reads
+ BeaconValidator --> BeaconHint : validates
+ BeaconResponseBuilder --> BeaconHintStore : reads
+
+ @enduml
+
+Main Components
+---------------
+
+1. **BeaconHint** - Core data structure representing a discovery hint from a ROS 2 node
+
+ - ``entity_id`` is the only required field (identifies the app)
+ - Identity fields: ``stable_id``, ``display_name`` for human-readable labeling
+ - Topology fields: ``function_ids``, ``depends_on``, ``component_id`` for entity relationships
+ - Transport fields: ``transport_type``, ``negotiated_format`` for DDS introspection
+ - Process fields: ``process_id``, ``process_name``, ``hostname`` for runtime context
+ - ``metadata`` map for arbitrary key-value pairs
+ - ``received_at`` timestamp in steady_clock domain for TTL computation
+
+2. **BeaconHintStore** - Thread-safe TTL-based storage for beacon hints
+
+ - ``update()`` inserts or refreshes a hint; returns false if capacity is full for new entity IDs
+ - ``evict_and_snapshot()`` atomically removes expired hints and returns a consistent snapshot
+ - Three-state lifecycle: ACTIVE (within TTL), STALE (past TTL but before expiry), EXPIRED (evicted)
+ - Configurable: TTL (default 10s), expiry (default 300s), max capacity (default 10,000 hints)
+ - Protected by ``std::shared_mutex`` for concurrent reads during ``get()``
+
+3. **BeaconEntityMapper** - Maps stored hints to gateway IntrospectionResult
+
+ - Takes a snapshot from ``BeaconHintStore`` and the current ``IntrospectionInput``
+ - Builds per-entity metadata JSON from hint fields
+ - Applies function membership: if a hint declares ``function_ids``, the mapper updates
+ the corresponding Function entities' host lists
+ - Optionally allows new entity creation (``allow_new_entities`` config flag)
+
+4. **BeaconValidator** - Input sanitization for incoming beacon hints
+
+ - Validates ``entity_id`` format (required, max length, allowed characters)
+ - Truncates oversized string fields rather than rejecting the entire hint
+ - Enforces limits on collection sizes (function_ids, depends_on, metadata entries)
+ - Returns ``valid=false`` only when ``entity_id`` itself is invalid
+
+5. **BeaconResponseBuilder** - Builds JSON responses for beacon metadata HTTP endpoints
+
+ - Constructs the response payload served by ``x-medkit-beacon`` vendor extension endpoints
+ - Includes hint data plus status (active/stale) and last-seen timestamp
+
+Design Decisions
+----------------
+
+Shared Library, Not Plugin
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The beacon common code is a static library linked into the topic and parameter
+beacon plugins, not a standalone plugin. This avoids an extra shared library load
+at runtime while keeping the transport-specific logic (topic subscription vs.
+parameter polling) in separate plugin ``.so`` files.
+
+Three-State Hint Lifecycle
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Hints transition through ACTIVE, STALE, and EXPIRED states rather than using a
+simple present/absent model. The STALE state allows the gateway to report that a
+node was recently seen but is no longer actively sending beacons - useful for
+distinguishing temporary network hiccups from actual node departures.
+
+Capacity Limits
+~~~~~~~~~~~~~~~
+
+The store enforces a hard cap on the number of tracked hints (default 10,000) to
+prevent memory exhaustion from misbehaving nodes or DDoS scenarios. When capacity
+is reached, new entity IDs are rejected while existing hints can still be refreshed.
+
+Validation Strategy
+~~~~~~~~~~~~~~~~~~~
+
+The validator is lenient by design: only an invalid ``entity_id`` causes rejection.
+Other fields are sanitized (truncated, pruned) so that partially malformed beacons
+still contribute useful discovery information rather than being silently dropped.
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
new file mode 100644
index 00000000..5fb4d4b9
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
@@ -0,0 +1,222 @@
+ros2_medkit_linux_introspection
+================================
+
+This section contains design documentation for the ros2_medkit_linux_introspection package.
+
+Overview
+--------
+
+The ``ros2_medkit_linux_introspection`` package provides three gateway plugins that
+enrich discovered entities with Linux-specific runtime metadata. Each plugin
+implements both the ``GatewayPlugin`` and ``IntrospectionProvider`` interfaces,
+registering vendor extension endpoints (``x-medkit-*``) and feeding metadata into
+the discovery merge pipeline.
+
+Architecture
+------------
+
+The following diagram shows the three plugins and their shared infrastructure.
+
+.. plantuml::
+ :caption: Linux Introspection Plugin Architecture
+
+ @startuml ros2_medkit_linux_introspection_architecture
+
+ skinparam linetype ortho
+ skinparam classAttributeIconSize 0
+
+ title Linux Introspection - Plugin Architecture
+
+ package "ros2_medkit_gateway" {
+ interface GatewayPlugin {
+ +name(): string
+ +configure(config): void
+ +set_context(ctx): void
+ +register_routes(server, prefix): void
+ }
+
+ interface IntrospectionProvider {
+ +introspect(input): IntrospectionResult
+ }
+
+ class PluginContext {
+ +register_capability()
+ +validate_entity_for_route()
+ +get_child_apps()
+ +send_json()
+ +send_error()
+ }
+ }
+
+ package "ros2_medkit_linux_introspection" {
+
+ class ProcfsPlugin {
+ - pid_cache_: PidCache
+ - proc_root_: string
+ --
+ + name(): "procfs_introspection"
+ + introspect(): process info per app
+ --
+ - handle_app_request(): single process
+ - handle_component_request(): aggregated
+ }
+
+ class SystemdPlugin {
+ - pid_cache_: PidCache
+ - proc_root_: string
+ --
+ + name(): "systemd_introspection"
+ + introspect(): unit info per app
+ --
+ - handle_app_request(): single unit
+ - handle_component_request(): aggregated
+ }
+
+ class ContainerPlugin {
+ - pid_cache_: PidCache
+ - proc_root_: string
+ --
+ + name(): "container_introspection"
+ + introspect(): cgroup info per app
+ --
+ - handle_app_request(): single container
+ - handle_component_request(): aggregated
+ }
+
+ class PidCache {
+ - node_to_pid_: map
+ - ttl_: duration
+ - mutex_: shared_mutex
+ --
+ + lookup(fqn, root): optional
+ + refresh(root): void
+ + size(): size_t
+ }
+
+ class "proc_reader" as PR <> {
+ + read_process_info(pid): ProcessInfo
+ + find_pid_for_node(name, ns): pid_t
+ + read_system_uptime(): double
+ }
+
+ class "cgroup_reader" as CR <> {
+ + read_cgroup_info(pid): CgroupInfo
+ }
+
+ class "systemd_utils" as SU <> {
+ + escape_unit_for_dbus(name): string
+ }
+
+ struct IntrospectionConfig {
+ + pid_cache: PidCache
+ + proc_root: string
+ }
+ }
+
+ ProcfsPlugin .up.|> GatewayPlugin
+ ProcfsPlugin .up.|> IntrospectionProvider
+ SystemdPlugin .up.|> GatewayPlugin
+ SystemdPlugin .up.|> IntrospectionProvider
+ ContainerPlugin .up.|> GatewayPlugin
+ ContainerPlugin .up.|> IntrospectionProvider
+
+ ProcfsPlugin *--> PidCache
+ SystemdPlugin *--> PidCache
+ ContainerPlugin *--> PidCache
+
+ ProcfsPlugin --> PR : reads /proc
+ ProcfsPlugin --> PluginContext
+ SystemdPlugin --> PR : PID lookup
+ SystemdPlugin --> SU : D-Bus queries
+ SystemdPlugin --> PluginContext
+ ContainerPlugin --> CR : reads cgroups
+ ContainerPlugin --> PR : PID lookup
+ ContainerPlugin --> PluginContext
+
+ @enduml
+
+Plugins
+-------
+
+1. **ProcfsPlugin** (``procfs_introspection``) - Process-level metrics from ``/proc``
+
+ - Registers ``x-medkit-procfs`` capability on Apps and Components
+ - Reads ``/proc/{pid}/stat``, ``/proc/{pid}/status``, ``/proc/{pid}/cmdline``
+ - Exposes: PID, PPID, state, RSS, VM size, CPU ticks, thread count, command line, exe path
+ - Component-level endpoint aggregates processes, deduplicating by PID (multiple nodes may share a process)
+
+2. **SystemdPlugin** (``systemd_introspection``) - Systemd unit metadata via sd-bus
+
+ - Registers ``x-medkit-systemd`` capability on Apps and Components
+ - Maps PID to systemd unit via ``sd_pid_get_unit()``
+ - Queries unit properties over D-Bus: ActiveState, SubState, NRestarts, WatchdogUSec
+ - Component-level endpoint aggregates units, deduplicating by unit name
+ - Uses RAII wrapper (``SdBusPtr``) for sd-bus connection lifetime
+
+3. **ContainerPlugin** (``container_introspection``) - Container detection from cgroups
+
+ - Registers ``x-medkit-container`` capability on Apps and Components
+ - Reads ``/proc/{pid}/cgroup`` to extract container ID and runtime (Docker, containerd, Podman)
+ - Component-level endpoint aggregates containers, deduplicating by container ID
+ - Returns 404 for entities not running in a container (not an error - just bare-metal)
+
+PidCache
+--------
+
+All three plugins share the ``PidCache`` class for mapping ROS 2 node fully-qualified
+names to Linux PIDs. The cache:
+
+- Scans ``/proc`` for processes whose command line matches ROS 2 node naming patterns
+- Uses TTL-based refresh (default 10 seconds, configurable via ``pid_cache_ttl_seconds``)
+- Is thread-safe via ``std::shared_mutex`` (concurrent reads, exclusive writes)
+- Each plugin instance owns its own ``PidCache`` (no cross-plugin sharing, avoids lock contention)
+
+The ``proc_root`` configuration parameter (default ``/``) allows testing with a mock
+``/proc`` filesystem without running as root.
+
+Vendor Extension Endpoints
+--------------------------
+
+Each plugin registers REST endpoints following the SOVD vendor extension pattern:
+
+.. code-block:: text
+
+ GET /api/v1/apps/{entity_id}/x-medkit-procfs
+ GET /api/v1/components/{entity_id}/x-medkit-procfs
+ GET /api/v1/apps/{entity_id}/x-medkit-systemd
+ GET /api/v1/components/{entity_id}/x-medkit-systemd
+ GET /api/v1/apps/{entity_id}/x-medkit-container
+ GET /api/v1/components/{entity_id}/x-medkit-container
+
+All endpoints validate the entity via ``PluginContext::validate_entity_for_route()``
+and return SOVD-compliant error responses on failure.
+
+Design Decisions
+----------------
+
+Three Separate Plugins
+~~~~~~~~~~~~~~~~~~~~~~
+
+Each Linux subsystem (procfs, systemd, cgroups) is a separate plugin rather than
+one monolithic "linux" plugin. This allows deployers to load only what they need -
+for example, a containerized deployment may only want the container plugin, while
+a systemd-managed robot would use the systemd plugin. Each plugin is an independent
+shared library (``.so``) loaded at runtime.
+
+IntrospectionProvider Dual Interface
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Each plugin implements both ``GatewayPlugin`` (for HTTP routes and capabilities) and
+``IntrospectionProvider`` (for the discovery merge pipeline). The ``introspect()``
+method feeds metadata into the entity cache during each discovery cycle, while the
+HTTP routes serve on-demand queries for individual entities. The C export function
+``get_introspection_provider()`` enables the plugin loader to obtain both interfaces
+from a single ``.so`` file.
+
+Configurable proc_root
+~~~~~~~~~~~~~~~~~~~~~~
+
+The ``proc_root`` parameter defaults to ``/`` but can be overridden to point at a
+mock filesystem tree. This enables unit testing of ``/proc`` parsing logic without
+root privileges or actual processes, and supports containerized gateway deployments
+where the host ``/proc`` is mounted at a non-standard path.
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/design/index.rst
new file mode 100644
index 00000000..3dd579cb
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/design/index.rst
@@ -0,0 +1,78 @@
+ros2_medkit_param_beacon
+=========================
+
+This section contains design documentation for the ros2_medkit_param_beacon plugin.
+
+Overview
+--------
+
+The ``ros2_medkit_param_beacon`` package implements a gateway discovery plugin that
+collects beacon hints from ROS 2 nodes via their parameter servers. Nodes declare
+discovery metadata as ROS 2 parameters under a configurable prefix
+(default ``ros2_medkit.discovery``), and the plugin polls these parameters periodically
+to build ``BeaconHint`` structs.
+
+This plugin implements both the ``GatewayPlugin`` and ``IntrospectionProvider`` interfaces.
+It uses the shared ``ros2_medkit_beacon_common`` library for hint storage, validation,
+entity mapping, and response building. See the
+:doc:`beacon common design <../ros2_medkit_beacon_common/index>` for details on those
+components.
+
+How It Works
+------------
+
+1. During each poll cycle, the plugin retrieves the current list of ROS 2 nodes
+ from the gateway's entity cache
+2. For each node, it creates (or reuses) an ``AsyncParametersClient`` and fetches
+ all parameters matching the configured prefix
+3. Parameters are parsed into a ``BeaconHint``: ``entity_id``, ``stable_id``,
+ ``function_ids``, ``metadata.*`` keys, etc.
+4. Validated hints are stored in the ``BeaconHintStore``
+5. On each ``introspect()`` call, the store is snapshot and mapped to an
+ ``IntrospectionResult`` via ``BeaconEntityMapper``
+
+Key Design Points
+-----------------
+
+Polling Model
+~~~~~~~~~~~~~
+
+Unlike the topic beacon (push-based), the parameter beacon uses a pull model.
+A background thread polls nodes at a configurable interval (default 5 seconds).
+This is appropriate for parameters because they change infrequently and nodes
+do not need to actively publish discovery information - they only need to set
+their parameters once at startup.
+
+Client Management
+~~~~~~~~~~~~~~~~~
+
+The plugin maintains a cache of ``AsyncParametersClient`` instances keyed by node
+FQN. Clients for nodes that disappear from the graph are evicted after a
+configurable timeout. A lock ordering protocol (``nodes_mutex_`` then
+``clients_mutex_`` then ``param_ops_mutex_``) prevents deadlocks between the
+poll thread and the introspection callback.
+
+Backoff and Budget
+~~~~~~~~~~~~~~~~~~
+
+Nodes that fail to respond (timeout, unavailable) accumulate a backoff counter.
+Subsequent poll cycles skip backed-off nodes with exponentially increasing skip
+counts. A per-cycle time budget (default 10 seconds) prevents a few slow nodes
+from starving the rest of the poll targets. The start offset rotates each cycle
+so that all nodes eventually get polled even under budget pressure.
+
+Injectable Client Factory
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The plugin constructor accepts an optional ``ParameterClientFactory`` for
+dependency injection. Unit tests provide a mock factory that returns
+``ParameterClientInterface`` stubs, enabling full testing without a running
+ROS 2 graph.
+
+Integration with Gateway
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The plugin registers the ``x-medkit-beacon`` vendor extension capability on Apps
+and exposes HTTP endpoints for querying individual beacon metadata. Route
+registration and capability handling follow the standard gateway plugin pattern
+via ``PluginContext``.
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/design/index.rst
new file mode 100644
index 00000000..ccc797ef
--- /dev/null
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/design/index.rst
@@ -0,0 +1,65 @@
+ros2_medkit_topic_beacon
+=========================
+
+This section contains design documentation for the ros2_medkit_topic_beacon plugin.
+
+Overview
+--------
+
+The ``ros2_medkit_topic_beacon`` package implements a gateway discovery plugin that
+collects beacon hints from ROS 2 nodes via a shared discovery topic. Nodes publish
+``MedkitDiscoveryHint`` messages to a configurable topic
+(default ``/ros2_medkit/discovery``), and the plugin subscribes to receive them in
+real time.
+
+This plugin implements both the ``GatewayPlugin`` and ``IntrospectionProvider`` interfaces.
+It uses the shared ``ros2_medkit_beacon_common`` library for hint storage, validation,
+entity mapping, and response building. See the
+:doc:`beacon common design <../ros2_medkit_beacon_common/index>` for details on those
+components.
+
+How It Works
+------------
+
+1. On ``set_context()``, the plugin creates a ROS 2 subscription to the discovery topic
+2. Each incoming ``MedkitDiscoveryHint`` message is converted to a ``BeaconHint``
+3. The hint passes through the rate limiter and validator before being stored
+4. On each ``introspect()`` call, the store is snapshot and mapped to an
+ ``IntrospectionResult`` via ``BeaconEntityMapper``
+
+Key Design Points
+-----------------
+
+Push Model
+~~~~~~~~~~
+
+Unlike the parameter beacon (pull-based polling), the topic beacon is push-based.
+Nodes actively publish discovery hints at their own cadence. This provides lower
+latency for discovery updates and avoids the overhead of per-node parameter queries,
+but requires nodes to include the ``ros2_medkit_msgs`` dependency and actively
+publish beacons.
+
+Rate Limiting
+~~~~~~~~~~~~~
+
+A ``TokenBucket`` rate limiter (default 100 messages/second) protects the gateway
+from beacon floods. The bucket refills continuously and drops excess messages with
+a single log warning. The rate limiter is thread-safe, as the DDS callback may fire
+from any executor thread.
+
+Timestamp Back-Projection
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The topic beacon converts the message header stamp (if non-zero) to a
+``steady_clock`` time point by computing the message age relative to the current
+wall-clock time and subtracting from ``steady_clock::now()``. This maps ROS time
+into the monotonic domain used by the ``BeaconHintStore`` for TTL computation,
+avoiding clock-jump artifacts.
+
+Integration with Gateway
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The plugin registers the ``x-medkit-beacon`` vendor extension capability on Apps
+and exposes HTTP endpoints for querying individual beacon metadata. Route
+registration and capability handling follow the standard gateway plugin pattern
+via ``PluginContext``.
diff --git a/src/ros2_medkit_msgs/design/index.rst b/src/ros2_medkit_msgs/design/index.rst
new file mode 100644
index 00000000..f2997615
--- /dev/null
+++ b/src/ros2_medkit_msgs/design/index.rst
@@ -0,0 +1,81 @@
+ros2_medkit_msgs
+=================
+
+This section contains design documentation for the ros2_medkit_msgs package.
+
+Overview
+--------
+
+The ``ros2_medkit_msgs`` package defines the ROS 2 message and service interfaces
+used across the ros2_medkit system. It has no runtime code - only ``.msg`` and
+``.srv`` definitions that are compiled by ``rosidl`` into C++ and Python bindings.
+
+Message Definitions
+-------------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 30 70
+
+ * - Message
+ - Purpose
+ * - ``Fault.msg``
+ - Core fault representation: code, severity, status, entity, timestamps, description
+ * - ``FaultEvent.msg``
+ - Fault lifecycle event published on SSE streams (fault + event type)
+ * - ``Snapshot.msg``
+ - Rosbag snapshot metadata: entity, fault code, timestamps, bag path
+ * - ``ClusterInfo.msg``
+ - Fault cluster information for correlation engine
+ * - ``EnvironmentData.msg``
+ - Environment context attached to faults (system state at time of fault)
+ * - ``ExtendedDataRecords.msg``
+ - SOVD-aligned extended data records for fault snapshots
+ * - ``MutedFaultInfo.msg``
+ - Muted fault tracking (code, entity, mute reason, expiry)
+ * - ``MedkitDiscoveryHint.msg``
+ - Beacon discovery hint published by nodes for the topic beacon plugin
+
+Service Definitions
+-------------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 30 70
+
+ * - Service
+ - Purpose
+ * - ``ReportFault.srv``
+ - Report a fault to the fault manager (used by fault_reporter library)
+ * - ``GetFault.srv``
+ - Retrieve details of a single fault by ID
+ * - ``ListFaults.srv``
+ - List all faults with optional filtering
+ * - ``ListFaultsForEntity.srv``
+ - List faults scoped to a specific entity
+ * - ``ClearFault.srv``
+ - Clear/delete a specific fault
+ * - ``GetSnapshots.srv``
+ - Retrieve rosbag snapshot metadata
+ * - ``GetRosbag.srv``
+ - Retrieve rosbag file path for download
+ * - ``ListRosbags.srv``
+ - List all available rosbag snapshots
+
+Design Decisions
+----------------
+
+Separate Package
+~~~~~~~~~~~~~~~~
+
+Messages live in a dedicated package so that lightweight clients (such as
+``ros2_medkit_fault_reporter``) can depend on the interface definitions without
+pulling in the full gateway or fault manager implementations.
+
+SOVD Alignment
+~~~~~~~~~~~~~~
+
+Fault severity levels and status strings follow the SOVD specification where
+applicable. Constants are defined directly in ``Fault.msg`` (e.g.,
+``SEVERITY_INFO``, ``STATUS_CONFIRMED``) so that all consumers share the same
+canonical values.
diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
new file mode 100644
index 00000000..de36e1ae
--- /dev/null
+++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
@@ -0,0 +1,207 @@
+ros2_medkit_graph_provider
+===========================
+
+This section contains design documentation for the ros2_medkit_graph_provider plugin.
+
+Overview
+--------
+
+The ``ros2_medkit_graph_provider`` package implements a gateway plugin that generates
+live dataflow graph documents for SOVD Function entities. It models the ROS 2 topic
+graph as a directed graph of nodes (apps) and edges (topic connections), enriched
+with frequency, latency, and drop rate metrics from ``/diagnostics``. The graph
+document powers the ``x-medkit-graph`` vendor extension endpoint and cyclic
+subscription sampler.
+
+Architecture
+------------
+
+The following diagram shows the plugin's main components and data flow.
+
+.. plantuml::
+ :caption: Graph Provider Plugin Architecture
+
+ @startuml ros2_medkit_graph_provider_architecture
+
+ skinparam linetype ortho
+ skinparam classAttributeIconSize 0
+
+ title Graph Provider - Plugin Architecture
+
+ package "ros2_medkit_gateway" {
+ interface GatewayPlugin
+ interface IntrospectionProvider
+ class PluginContext {
+ +register_capability()
+ +register_sampler()
+ +get_entity_snapshot()
+ +list_all_faults()
+ }
+ }
+
+ package "ROS 2" {
+ class "/diagnostics" as diag_topic <>
+ }
+
+ package "ros2_medkit_graph_provider" {
+
+ class GraphProviderPlugin {
+ - graph_cache_: map
+ - topic_metrics_: map
+ - last_seen_by_app_: map
+ - config_: ConfigOverrides
+ --
+ + introspect(input): IntrospectionResult
+ + {static} build_graph_document(): json
+ --
+ - subscribe_to_diagnostics()
+ - diagnostics_callback(msg)
+ - get_cached_or_built_graph(func_id)
+ - build_graph_from_entity_cache(func_id)
+ - collect_stale_topics(func_id)
+ - build_state_snapshot()
+ - resolve_config(func_id)
+ - load_parameters()
+ }
+
+ struct TopicMetrics {
+ + frequency_hz: optional
+ + latency_ms: optional
+ + drop_rate_percent: double
+ + expected_frequency_hz: optional
+ }
+
+ struct GraphBuildState {
+ + topic_metrics: map
+ + stale_topics: set
+ + last_seen_by_app: map
+ + diagnostics_seen: bool
+ }
+
+ struct GraphBuildConfig {
+ + expected_frequency_hz_default: 30.0
+ + degraded_frequency_ratio: 0.5
+ + drop_rate_percent_threshold: 5.0
+ }
+ }
+
+ GraphProviderPlugin .up.|> GatewayPlugin
+ GraphProviderPlugin .up.|> IntrospectionProvider
+ GraphProviderPlugin --> PluginContext
+ GraphProviderPlugin --> diag_topic : subscribes
+ GraphProviderPlugin *--> TopicMetrics : caches
+ GraphProviderPlugin ..> GraphBuildState : builds
+ GraphProviderPlugin ..> GraphBuildConfig : configured by
+
+ @enduml
+
+Graph Document Schema
+---------------------
+
+The plugin generates a JSON document per Function entity with the following structure:
+
+- **schema_version**: ``"1.0.0"``
+- **graph_id**: ``"{function_id}-graph"``
+- **timestamp**: ISO 8601 nanosecond timestamp
+- **scope**: Function entity that owns the graph
+- **pipeline_status**: ``"healthy"``, ``"degraded"``, or ``"broken"``
+- **bottleneck_edge**: Edge ID with the lowest frequency ratio (only when degraded)
+- **topics**: List of topics with stable IDs
+- **nodes**: List of app entities with reachability status
+- **edges**: Publisher-subscriber connections with per-edge metrics
+
+Edge metrics include frequency, latency, drop rate, and a status field:
+
+- ``"active"`` - Diagnostics data available, normal operation
+- ``"pending"`` - No diagnostics data received yet
+- ``"error"`` - Node offline, topic stale, or no data source after diagnostics started
+
+Pipeline Status Logic
+---------------------
+
+The overall pipeline status is determined by aggregating edge states:
+
+1. **broken** - At least one edge has ``metrics_status: "error"`` (node offline or topic stale)
+2. **degraded** - At least one edge has frequency below the degraded ratio threshold, or drop rate exceeds the threshold
+3. **healthy** - All edges are active with acceptable metrics
+
+When the status is ``"degraded"``, the ``bottleneck_edge`` field identifies the edge
+with the lowest frequency-to-expected ratio, helping operators pinpoint the
+constraint in the dataflow pipeline.
+
+Data Sources
+------------
+
+Diagnostics Subscription
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The plugin subscribes to ``/diagnostics`` and parses ``DiagnosticStatus`` messages
+for topic-level metrics. Recognized keys:
+
+- ``frame_rate_msg`` - Mapped to ``frequency_hz``
+- ``current_delay_from_realtime_ms`` - Mapped to ``latency_ms``
+- ``drop_rate_percent`` / ``drop_rate`` - Mapped to ``drop_rate_percent``
+- ``expected_frequency`` - Mapped to ``expected_frequency_hz``
+
+A bounded cache (max 512 topics) with LRU eviction prevents unbounded memory growth.
+
+Fault Manager Integration
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The plugin queries the fault manager via ``PluginContext::list_all_faults()`` to
+detect stale topics. A topic is considered stale when there is a confirmed critical
+fault whose fault code matches the topic name (after normalization). Stale topics
+cause their edges to be marked as ``"error"`` with reason ``"topic_stale"``.
+
+Entity Cache
+~~~~~~~~~~~~
+
+On HTTP requests, the plugin rebuilds the graph from the current entity cache
+(``PluginContext::get_entity_snapshot()``) rather than serving the potentially stale
+introspection-pipeline cache. This ensures the HTTP endpoint always reflects the
+latest node and topic state.
+
+Function Scoping
+~~~~~~~~~~~~~~~~
+
+Graph documents are scoped to individual Function entities. The plugin resolves
+which apps belong to a function by checking the function's ``hosts`` list against
+app IDs and component IDs. Only topics that connect scoped apps appear as edges.
+System topics (``/parameter_events``, ``/rosout``, ``/diagnostics``, NITROS topics)
+are filtered out.
+
+Design Decisions
+----------------
+
+Extracted Plugin
+~~~~~~~~~~~~~~~~
+
+The graph provider was extracted from the gateway core into a standalone plugin
+package. This follows the same pattern as the linux introspection plugins: the
+gateway loads graph_provider as a ``.so`` at runtime, keeping the core gateway
+free of diagnostics-specific logic. The plugin can be omitted from deployments
+that do not need dataflow visualization.
+
+Static build_graph_document
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``build_graph_document()`` method is static and takes all inputs explicitly
+(function_id, IntrospectionInput, GraphBuildState, GraphBuildConfig, timestamp).
+This makes the graph generation logic fully testable without instantiating the
+plugin or its ROS 2 dependencies.
+
+Per-Function Config Overrides
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The plugin supports per-function configuration overrides for thresholds
+(expected frequency, degraded ratio, drop rate). This allows operators to set
+different health baselines for different subsystems - for example, a camera
+pipeline at 30 Hz vs. a LiDAR pipeline at 10 Hz.
+
+Cyclic Subscription Sampler
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The plugin registers a sampler via ``PluginContext::register_sampler()`` for the
+``x-medkit-graph`` resource. This allows clients to create cyclic subscriptions
+that receive periodic graph snapshots over SSE, enabling live dashboard updates
+without polling the HTTP endpoint.
From 563a6ea409cedf2a96d8e669e940a68a229e311d Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:20:54 +0100
Subject: [PATCH 11/23] docs(design): fix PlantUML angle bracket escaping in
design docs
---
.../design/index.rst | 22 +++++++++----------
.../design/index.rst | 2 +-
.../design/index.rst | 12 +++++-----
3 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
index 7dcf44d8..87575303 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
@@ -33,14 +33,14 @@ The following diagram shows the beacon common components and their relationships
}
struct IntrospectionInput {
- +areas: vector
- +components: vector
- +apps: vector
- +functions: vector
+ +areas: vector~
+ +components: vector~
+ +apps: vector~
+ +functions: vector~
}
struct IntrospectionResult {
- +metadata: map
+ +metadata: map~
+new_entities: NewEntities
}
}
@@ -51,26 +51,26 @@ The following diagram shows the beacon common components and their relationships
+ entity_id: string
+ stable_id: string
+ display_name: string
- + function_ids: vector
- + depends_on: vector
+ + function_ids: vector~
+ + depends_on: vector~
+ component_id: string
+ transport_type: string
+ negotiated_format: string
+ process_id: uint32
+ process_name: string
+ hostname: string
- + metadata: map
+ + metadata: map~
+ received_at: time_point
}
class BeaconHintStore {
- - hints_: map
+ - hints_: map~
- config_: Config
- mutex_: shared_mutex
--
+ update(hint): bool
- + evict_and_snapshot(): vector
- + get(entity_id): optional
+ + evict_and_snapshot(): vector~
+ + get(entity_id): optional~
+ size(): size_t
}
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
index 5fb4d4b9..3c3ad40b 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
@@ -88,7 +88,7 @@ The following diagram shows the three plugins and their shared infrastructure.
- ttl_: duration
- mutex_: shared_mutex
--
- + lookup(fqn, root): optional
+ + lookup(fqn, root): optional~
+ refresh(root): void
+ size(): size_t
}
diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
index de36e1ae..e7613feb 100644
--- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
+++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
@@ -46,9 +46,9 @@ The following diagram shows the plugin's main components and data flow.
package "ros2_medkit_graph_provider" {
class GraphProviderPlugin {
- - graph_cache_: map
- - topic_metrics_: map
- - last_seen_by_app_: map
+ - graph_cache_: map~
+ - topic_metrics_: map~
+ - last_seen_by_app_: map~
- config_: ConfigOverrides
--
+ introspect(input): IntrospectionResult
@@ -65,10 +65,10 @@ The following diagram shows the plugin's main components and data flow.
}
struct TopicMetrics {
- + frequency_hz: optional
- + latency_ms: optional
+ + frequency_hz: optional~
+ + latency_ms: optional~
+ drop_rate_percent: double
- + expected_frequency_hz: optional
+ + expected_frequency_hz: optional~
}
struct GraphBuildState {
From 743c7cb75ae2e41b6d5cb23fb873c64b036675d4 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:32:14 +0100
Subject: [PATCH 12/23] docs: fix review findings - beacon param names, OpenAPI
version 3.1.0
---
.../ros2_medkit_param_beacon/README.md | 10 +++++-----
.../ros2_medkit_topic_beacon/README.md | 6 +++---
src/ros2_medkit_gateway/CHANGELOG.rst | 2 +-
src/ros2_medkit_gateway/README.md | 2 +-
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/README.md b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/README.md
index 2390caff..acd40bc3 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/README.md
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/README.md
@@ -19,11 +19,11 @@ ROS 2 graph. In hybrid mode, targets come from the manifest.
```yaml
plugins: ["param_beacon"]
plugins.param_beacon.path: "/path/to/libros2_medkit_param_beacon.so"
-plugins.param_beacon.poll_interval_sec: 10
-plugins.param_beacon.poll_budget_ms: 500
-plugins.param_beacon.timeout_ms: 1000
-plugins.param_beacon.ttl_sec: 60
-plugins.param_beacon.expiry_sec: 120
+plugins.param_beacon.poll_interval_sec: 10.0
+plugins.param_beacon.poll_budget_sec: 10.0
+plugins.param_beacon.param_timeout_sec: 2.0
+plugins.param_beacon.beacon_ttl_sec: 15.0
+plugins.param_beacon.beacon_expiry_sec: 300.0
```
See [discovery options](https://selfpatch.github.io/ros2_medkit/config/discovery-options.html)
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/README.md b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/README.md
index d36d2704..c3fcb99e 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/README.md
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/README.md
@@ -19,10 +19,10 @@ data still served with stale marker) -> **expired** (removed from store).
```yaml
plugins: ["topic_beacon"]
plugins.topic_beacon.path: "/path/to/libros2_medkit_topic_beacon.so"
-plugins.topic_beacon.ttl_sec: 30
-plugins.topic_beacon.expiry_sec: 90
+plugins.topic_beacon.beacon_ttl_sec: 10.0
+plugins.topic_beacon.beacon_expiry_sec: 300.0
plugins.topic_beacon.allow_new_entities: true
-plugins.topic_beacon.rate_limit_hz: 10.0
+plugins.topic_beacon.max_messages_per_second: 100.0
```
See [discovery options](https://selfpatch.github.io/ros2_medkit/config/discovery-options.html)
diff --git a/src/ros2_medkit_gateway/CHANGELOG.rst b/src/ros2_medkit_gateway/CHANGELOG.rst
index 50722d30..dd977caf 100644
--- a/src/ros2_medkit_gateway/CHANGELOG.rst
+++ b/src/ros2_medkit_gateway/CHANGELOG.rst
@@ -37,7 +37,7 @@ Changelog for package ros2_medkit_gateway
* ``allow_uploads`` config toggle for hardened deployments
* RBAC integration for script operations
* ``RouteRegistry`` as single source of truth for routes and OpenAPI metadata
-* ``OpenApiSpecBuilder`` for full OpenAPI 3.0 document assembly with ``SchemaBuilder`` and ``PathBuilder``
+* ``OpenApiSpecBuilder`` for full OpenAPI 3.1.0 document assembly with ``SchemaBuilder`` and ``PathBuilder``
* Compile-time Swagger UI embedding (``ENABLE_SWAGGER_UI``)
* Generation-based caching for capability responses via ``CapabilityGenerator``
* Beacon discovery plugin system - push-based entity enrichment via ROS 2 topic
diff --git a/src/ros2_medkit_gateway/README.md b/src/ros2_medkit_gateway/README.md
index 18bd4483..758872b8 100644
--- a/src/ros2_medkit_gateway/README.md
+++ b/src/ros2_medkit_gateway/README.md
@@ -95,7 +95,7 @@ All endpoints are prefixed with `/api/v1` for API versioning.
### API Documentation (OpenAPI)
-- `GET /api/v1/docs` - Full OpenAPI 3.0 specification
+- `GET /api/v1/docs` - Full OpenAPI 3.1.0 specification
- `GET /api/v1/{entity_type}/{id}/docs` - Entity-scoped OpenAPI spec
- `GET /api/v1/swagger-ui` - Interactive Swagger UI (requires build with `-DENABLE_SWAGGER_UI=ON`)
From c620ca3696589be703967f0bd21d6eb64c8c2f78 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 08:45:38 +0100
Subject: [PATCH 13/23] docs: fix deep review findings - test version, README,
security
- Fix hardcoded "0.3.0" in test_capability_generator.cpp - use
kGatewayVersion constant for version-agnostic assertion
- Add source /opt/ros/jazzy/setup.bash to README build-from-source
- Add curl health check and Getting Started link after Quick Start
- Fix "Log sources" -> "Log entries" in README feature table
- Remove "SOVD" from locking feature description
- Fix pip -> pipx in Contributing section
- Fix auth.mode -> auth.enabled in scripts tutorial
- Add danger directive to auth config examples about weak secrets
- Use obviously-placeholder credentials in config examples
- Fix release.sh verify to include version.hpp in consistency check
Refs: #278
---
README.md | 9 ++++++---
docs/config/server.rst | 15 +++++++++++----
docs/tutorials/scripts.rst | 2 +-
scripts/release.sh | 1 +
.../test/test_capability_generator.cpp | 3 ++-
5 files changed, 21 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index bb326243..08f05f50 100644
--- a/README.md
+++ b/README.md
@@ -45,15 +45,18 @@ Open `http://localhost:3000` in your browser. You will see a TurtleBot3 with Nav
**Build from source** (ROS 2 Jazzy, Humble, or Rolling):
```bash
+source /opt/ros/jazzy/setup.bash # or humble - adjust for your distro
git clone --recurse-submodules https://github.com/selfpatch/ros2_medkit.git
cd ros2_medkit
rosdep install --from-paths src --ignore-src -r -y
colcon build --symlink-install && source install/setup.bash
ros2 launch ros2_medkit_gateway gateway.launch.py
-# → http://localhost:8080/api/v1/areas
+# → http://localhost:8080/api/v1/health
```
-For API examples, see our [Postman collection](postman/).
+Verify it works: `curl http://localhost:8080/api/v1/health` should return `{"status": "healthy", ...}`.
+
+For a guided walkthrough with demo nodes and the full API, see the [Getting Started tutorial](https://selfpatch.github.io/ros2_medkit/getting_started.html). For API examples, see our [Postman collection](postman/).
### Experimental: Pixi
@@ -168,7 +171,7 @@ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for build inst
Quick version:
```bash
-pip install pre-commit && pre-commit install && pre-commit install --hook-type pre-push
+pipx install pre-commit && pre-commit install && pre-commit install --hook-type pre-push
colcon build --symlink-install
source install/setup.bash
./scripts/test.sh # unit tests
diff --git a/docs/config/server.rst b/docs/config/server.rst
index fe127741..fea6831c 100644
--- a/docs/config/server.rst
+++ b/docs/config/server.rst
@@ -453,6 +453,13 @@ default for local development.
- **configurator** - Operator + modify/reset configurations
- **admin** - Full access including auth management
+.. danger::
+
+ The example below uses **placeholder secrets for illustration only**.
+ In production, generate secrets with ``openssl rand -base64 32`` and
+ never commit them to configuration files. Use environment variable
+ substitution or a secrets manager.
+
Example:
.. code-block:: yaml
@@ -461,11 +468,11 @@ Example:
ros__parameters:
auth:
enabled: true
- jwt_secret: "my-secret-key"
+ jwt_secret: "CHANGE-ME-use-openssl-rand-base64-32"
jwt_algorithm: "HS256"
require_auth_for: "write"
token_expiry_seconds: 3600
- clients: ["admin:admin_secret_123:admin", "viewer:viewer_pass:viewer"]
+ clients: ["admin:REPLACE_WITH_STRONG_SECRET:admin", "viewer:REPLACE_WITH_STRONG_SECRET:viewer"]
See :doc:`/tutorials/authentication` for a complete setup tutorial.
@@ -596,10 +603,10 @@ Complete Example
auth:
enabled: true
- jwt_secret: "my-secret-key"
+ jwt_secret: "CHANGE-ME-use-openssl-rand-base64-32"
jwt_algorithm: "HS256"
require_auth_for: "write"
- clients: ["admin:admin_secret_123:admin"]
+ clients: ["admin:REPLACE_WITH_STRONG_SECRET:admin"]
rate_limiting:
enabled: false
diff --git a/docs/tutorials/scripts.rst b/docs/tutorials/scripts.rst
index 1c656f40..e395ebf3 100644
--- a/docs/tutorials/scripts.rst
+++ b/docs/tutorials/scripts.rst
@@ -324,7 +324,7 @@ execution.
RBAC Roles
~~~~~~~~~~
-When :doc:`authentication` is enabled (``auth.mode: write`` or ``auth.mode: all``),
+When :doc:`authentication` is enabled (``auth.enabled: true``),
script endpoints are protected by role-based access control:
.. list-table::
diff --git a/scripts/release.sh b/scripts/release.sh
index b782711f..ece2c901 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -131,6 +131,7 @@ cmd_verify() {
else
echo " OK: version.hpp fallback = ${hpp_version}"
fi
+ versions_seen+=("$hpp_version")
fi
# Check consistency if no expected version given
diff --git a/src/ros2_medkit_gateway/test/test_capability_generator.cpp b/src/ros2_medkit_gateway/test/test_capability_generator.cpp
index 64581ceb..f068e43c 100644
--- a/src/ros2_medkit_gateway/test/test_capability_generator.cpp
+++ b/src/ros2_medkit_gateway/test/test_capability_generator.cpp
@@ -25,6 +25,7 @@
#include "ros2_medkit_gateway/config.hpp"
#include "ros2_medkit_gateway/gateway_node.hpp"
#include "ros2_medkit_gateway/http/handlers/handler_context.hpp"
+#include "ros2_medkit_gateway/version.hpp"
using namespace ros2_medkit_gateway;
using namespace ros2_medkit_gateway::openapi;
@@ -134,7 +135,7 @@ TEST_F(CapabilityGeneratorTest, GenerateRootReturnsValidOpenApiSpec) {
// Verify info block
ASSERT_TRUE(spec.contains("info"));
EXPECT_EQ(spec["info"]["title"], "ROS 2 Medkit Gateway");
- EXPECT_EQ(spec["info"]["version"], "0.3.0");
+ EXPECT_EQ(spec["info"]["version"], ros2_medkit_gateway::kGatewayVersion);
ASSERT_TRUE(spec["info"].contains("x-sovd-version"));
EXPECT_EQ(spec["info"]["x-sovd-version"], "1.0.0");
From 619acd2bd94ad3c4f8eb2d751817f36f6df495d3 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 09:03:33 +0100
Subject: [PATCH 14/23] docs(design): fix PlantUML - replace struct with class
keyword, revert tilde escaping
---
.../design/index.rst | 32 +++++++++----------
.../design/index.rst | 4 +--
.../design/index.rst | 18 +++++------
3 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
index 87575303..db1c4db2 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/design/index.rst
@@ -32,45 +32,45 @@ The following diagram shows the beacon common components and their relationships
+introspect(input): IntrospectionResult
}
- struct IntrospectionInput {
- +areas: vector~
- +components: vector~
- +apps: vector~
- +functions: vector~
+ class IntrospectionInput {
+ +areas: vector
+ +components: vector
+ +apps: vector
+ +functions: vector
}
- struct IntrospectionResult {
- +metadata: map~
+ class IntrospectionResult {
+ +metadata: map
+new_entities: NewEntities
}
}
package "ros2_medkit_beacon_common" {
- struct BeaconHint {
+ class BeaconHint {
+ entity_id: string
+ stable_id: string
+ display_name: string
- + function_ids: vector~
- + depends_on: vector~
+ + function_ids: vector
+ + depends_on: vector
+ component_id: string
+ transport_type: string
+ negotiated_format: string
+ process_id: uint32
+ process_name: string
+ hostname: string
- + metadata: map~
+ + metadata: map
+ received_at: time_point
}
class BeaconHintStore {
- - hints_: map~
+ - hints_: map
- config_: Config
- mutex_: shared_mutex
--
+ update(hint): bool
- + evict_and_snapshot(): vector~
- + get(entity_id): optional~
+ + evict_and_snapshot(): vector
+ + get(entity_id): optional
+ size(): size_t
}
@@ -91,13 +91,13 @@ The following diagram shows the beacon common components and their relationships
+ {static} build_beacon_response(id, stored): json
}
- struct "BeaconHintStore::Config" as StoreConfig {
+ class "BeaconHintStore::Config" as StoreConfig {
+ beacon_ttl_sec: double = 10.0
+ beacon_expiry_sec: double = 300.0
+ max_hints: size_t = 10000
}
- struct ValidationLimits {
+ class ValidationLimits {
+ max_id_length: 256
+ max_string_length: 512
+ max_function_ids: 100
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
index 3c3ad40b..d8c733cf 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst
@@ -88,7 +88,7 @@ The following diagram shows the three plugins and their shared infrastructure.
- ttl_: duration
- mutex_: shared_mutex
--
- + lookup(fqn, root): optional~
+ + lookup(fqn, root): optional
+ refresh(root): void
+ size(): size_t
}
@@ -107,7 +107,7 @@ The following diagram shows the three plugins and their shared infrastructure.
+ escape_unit_for_dbus(name): string
}
- struct IntrospectionConfig {
+ class IntrospectionConfig {
+ pid_cache: PidCache
+ proc_root: string
}
diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
index e7613feb..6467cdd1 100644
--- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
+++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/design/index.rst
@@ -46,9 +46,9 @@ The following diagram shows the plugin's main components and data flow.
package "ros2_medkit_graph_provider" {
class GraphProviderPlugin {
- - graph_cache_: map~
- - topic_metrics_: map~
- - last_seen_by_app_: map~
+ - graph_cache_: map
+ - topic_metrics_: map
+ - last_seen_by_app_: map
- config_: ConfigOverrides
--
+ introspect(input): IntrospectionResult
@@ -64,21 +64,21 @@ The following diagram shows the plugin's main components and data flow.
- load_parameters()
}
- struct TopicMetrics {
- + frequency_hz: optional~
- + latency_ms: optional~
+ class TopicMetrics {
+ + frequency_hz: optional
+ + latency_ms: optional
+ drop_rate_percent: double
- + expected_frequency_hz: optional~
+ + expected_frequency_hz: optional
}
- struct GraphBuildState {
+ class GraphBuildState {
+ topic_metrics: map
+ stale_topics: set
+ last_seen_by_app: map
+ diagnostics_seen: bool
}
- struct GraphBuildConfig {
+ class GraphBuildConfig {
+ expected_frequency_hz_default: 30.0
+ degraded_frequency_ratio: 0.5
+ drop_rate_percent_threshold: 5.0
From e882b14f58b362e1bba79c6c31c893df04a74007 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 09:06:04 +0100
Subject: [PATCH 15/23] docs: remove --headless from quick start demo command
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 08f05f50..9bc5272f 100644
--- a/README.md
+++ b/README.md
@@ -36,11 +36,11 @@ ros2_medkit gives your ROS 2 system a **diagnostic REST API** so you can inspect
```bash
git clone https://github.com/selfpatch/selfpatch_demos.git
cd selfpatch_demos/demos/turtlebot3_integration
-./run-demo.sh --headless
+./run-demo.sh
# → API: http://localhost:8080/api/v1/ Web UI: http://localhost:3000
```
-Open `http://localhost:3000` in your browser. You will see a TurtleBot3 with Nav2, organized into a browsable entity tree with live faults, topic data, and parameter access.
+Open `http://localhost:3000` in your browser to see the diagnostic web UI - a browsable entity tree showing the TurtleBot3 Nav2 stack with live faults, topic data, and parameter access. The `--headless` flag skips the Gazebo 3D view, but the REST API and web UI work normally.
**Build from source** (ROS 2 Jazzy, Humble, or Rolling):
From 817554fdb5aaa837eecbe572c6d84f18bf1893d5 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 10:09:57 +0100
Subject: [PATCH 16/23] docs: add triggers to changelog, gateway README, and
roadmap for v0.4.0
---
README.md | 2 +-
docs/roadmap.rst | 4 ++--
src/ros2_medkit_gateway/CHANGELOG.rst | 6 ++++++
src/ros2_medkit_gateway/README.md | 9 +++++++++
4 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 9bc5272f..9d9e7645 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ cd selfpatch_demos/demos/turtlebot3_integration
# → API: http://localhost:8080/api/v1/ Web UI: http://localhost:3000
```
-Open `http://localhost:3000` in your browser to see the diagnostic web UI - a browsable entity tree showing the TurtleBot3 Nav2 stack with live faults, topic data, and parameter access. The `--headless` flag skips the Gazebo 3D view, but the REST API and web UI work normally.
+Open `http://localhost:3000` in your browser. You will see a TurtleBot3 with Nav2, organized into a browsable entity tree with live faults, topic data, and parameter access.
**Build from source** (ROS 2 Jazzy, Humble, or Rolling):
diff --git a/docs/roadmap.rst b/docs/roadmap.rst
index 08546969..d17bee1e 100644
--- a/docs/roadmap.rst
+++ b/docs/roadmap.rst
@@ -86,7 +86,7 @@ and define triggers for event-driven workflows.
- [x] Scripts (8 endpoints) - upload, execute, and manage diagnostic scripts
- [x] Cyclic Subscriptions (6 endpoints) - periodic push-based resource delivery via SSE
-- [ ] Triggers - event-driven subscriptions
+- [x] Triggers (6 endpoints) - condition-based push notifications with hierarchy matching
- [ ] Datasets (4 endpoints) - dynamic data lists for subscription
`MS4 on GitHub `_
@@ -153,7 +153,7 @@ With most of the SOVD API surface covered, the project is shifting focus toward
production readiness and ecosystem integration:
**Remaining SOVD Coverage**
- Complete the remaining specification endpoints: triggers, communication logs,
+ Complete the remaining specification endpoints: communication logs,
clear data, datasets, lifecycle management, and entity operating modes.
**Code Hardening**
diff --git a/src/ros2_medkit_gateway/CHANGELOG.rst b/src/ros2_medkit_gateway/CHANGELOG.rst
index dd977caf..faac938c 100644
--- a/src/ros2_medkit_gateway/CHANGELOG.rst
+++ b/src/ros2_medkit_gateway/CHANGELOG.rst
@@ -53,6 +53,12 @@ Changelog for package ros2_medkit_gateway
* ``PluginContext::get_child_apps()`` for Component-level aggregation
* Sub-resource RBAC patterns for all collections
* Auto-populate gateway version from ``package.xml`` via CMake
+* Condition-based triggers with CRUD endpoints, SSE event streaming, and hierarchy matching
+* ``TriggerManager`` with ``ConditionEvaluator`` interface and 4 built-in evaluators (OnChange, OnChangeTo, EnterRange, LeaveRange)
+* ``ResourceChangeNotifier`` for async dispatch from FaultManager, UpdateManager, and OperationManager
+* ``TriggerTopicSubscriber`` for data trigger ROS 2 topic subscriptions
+* Persistent trigger storage via SQLite with restore-on-restart support
+* ``TriggerTransportProvider`` plugin interface for custom trigger delivery
**Build:**
diff --git a/src/ros2_medkit_gateway/README.md b/src/ros2_medkit_gateway/README.md
index 758872b8..e34f29e9 100644
--- a/src/ros2_medkit_gateway/README.md
+++ b/src/ros2_medkit_gateway/README.md
@@ -82,6 +82,15 @@ All endpoints are prefixed with `/api/v1` for API versioning.
- `PUT /api/v1/{components|apps}/{id}/locks/{lock_id}` - Extend lock expiration
- `DELETE /api/v1/{components|apps}/{id}/locks/{lock_id}` - Release a lock
+### Trigger Endpoints
+
+- `POST /api/v1/{entity}/{id}/triggers` - Create a trigger with conditions
+- `GET /api/v1/{entity}/{id}/triggers` - List active triggers
+- `GET /api/v1/{entity}/{id}/triggers/{trigger_id}` - Get trigger details
+- `PUT /api/v1/{entity}/{id}/triggers/{trigger_id}` - Update trigger conditions
+- `DELETE /api/v1/{entity}/{id}/triggers/{trigger_id}` - Delete a trigger
+- `GET /api/v1/{entity}/{id}/triggers/{trigger_id}/events` - SSE stream of trigger events
+
### Scripts Endpoints
- `GET /api/v1/{entity}/{id}/scripts` - List available diagnostic scripts
From 7f63618b9c7888af2695ce7970e0b40be5f3d33e Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 18:13:56 +0100
Subject: [PATCH 17/23] docs(requirements): remove non-SOVD REQ_DISCO_BEACON
requirements and @verifies tags
---
docs/requirements/specs/discovery.rst | 35 -------------------
.../test/test_beacon_entity_mapper.cpp | 23 ------------
.../test/test_beacon_hint_store.cpp | 23 ------------
.../test/test_beacon_validator.cpp | 13 -------
.../test/test_param_beacon_plugin.cpp | 5 ---
.../test/test_topic_beacon_plugin.cpp | 6 ----
6 files changed, 105 deletions(-)
diff --git a/docs/requirements/specs/discovery.rst b/docs/requirements/specs/discovery.rst
index 2b24baf8..901c838b 100644
--- a/docs/requirements/specs/discovery.rst
+++ b/docs/requirements/specs/discovery.rst
@@ -78,44 +78,9 @@ Discovery
The server shall support tag-based query parameters that filter discovery responses by tags.
-.. req:: Topic beacon enriches entities
- :id: REQ_DISCO_BEACON_01
- :status: verified
- :tags: Discovery, Beacon
-
- The TopicBeaconPlugin shall enrich discovered entities with metadata from MedkitDiscoveryHint messages published to a configurable ROS 2 topic.
-
-.. req:: Parameter beacon enriches entities
- :id: REQ_DISCO_BEACON_02
- :status: verified
- :tags: Discovery, Beacon
-
- The ParameterBeaconPlugin shall enrich discovered entities by polling node parameters matching a configurable prefix.
-
-.. req:: Beacon hint lifecycle
- :id: REQ_DISCO_BEACON_03
- :status: verified
- :tags: Discovery, Beacon
-
- Beacon hints shall follow an ACTIVE, STALE, EXPIRED lifecycle based on configurable TTL and expiry durations.
-
.. req:: GET /apps/{id}/is-located-on
:id: REQ_INTEROP_105
:status: verified
:tags: Discovery
The endpoint shall return the component that hosts the addressed application.
-
-.. req:: Vendor endpoints expose beacon data
- :id: REQ_DISCO_BEACON_04
- :status: verified
- :tags: Discovery, Beacon
-
- Each beacon plugin shall register vendor extension endpoints that return per-entity beacon metadata in JSON format.
-
-.. req:: Input validation rejects malformed hints
- :id: REQ_DISCO_BEACON_05
- :status: verified
- :tags: Discovery, Beacon
-
- The beacon validator shall reject hints with empty entity IDs, oversized fields, or excessive metadata entries.
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_entity_mapper.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_entity_mapper.cpp
index 70a8bef8..60467f5c 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_entity_mapper.cpp
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_entity_mapper.cpp
@@ -89,7 +89,6 @@ IntrospectionInput make_base_input() {
// ---------------------------------------------------------------------------
// EnrichesExistingApp
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, EnrichesExistingApp) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -116,7 +115,6 @@ TEST(BeaconEntityMapper, EnrichesExistingApp) {
// ---------------------------------------------------------------------------
// EnrichesExistingComponent
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, EnrichesExistingComponent) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -143,7 +141,6 @@ TEST(BeaconEntityMapper, EnrichesExistingComponent) {
// ---------------------------------------------------------------------------
// UnknownEntityIgnoredWhenDisabled
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, UnknownEntityIgnoredWhenDisabled) {
auto input = make_base_input();
BeaconEntityMapper::Config cfg;
@@ -162,7 +159,6 @@ TEST(BeaconEntityMapper, UnknownEntityIgnoredWhenDisabled) {
// ---------------------------------------------------------------------------
// UnknownEntityCreatesAppWhenEnabled
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, UnknownEntityCreatesAppWhenEnabled) {
auto input = make_base_input();
BeaconEntityMapper::Config cfg;
@@ -187,7 +183,6 @@ TEST(BeaconEntityMapper, UnknownEntityCreatesAppWhenEnabled) {
// ---------------------------------------------------------------------------
// FunctionIdsReverseMapAddsToHosts
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, FunctionIdsReverseMapAddsToHosts) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -209,7 +204,6 @@ TEST(BeaconEntityMapper, FunctionIdsReverseMapAddsToHosts) {
// ---------------------------------------------------------------------------
// FunctionIdsNonExistentFunctionLogsWarning
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, FunctionIdsNonExistentFunctionLogsWarning) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -233,7 +227,6 @@ TEST(BeaconEntityMapper, FunctionIdsNonExistentFunctionLogsWarning) {
// ---------------------------------------------------------------------------
// EmptyFunctionIdsIsNoop
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, EmptyFunctionIdsIsNoop) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -253,7 +246,6 @@ TEST(BeaconEntityMapper, EmptyFunctionIdsIsNoop) {
// ---------------------------------------------------------------------------
// ComponentIdSetsParent
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, ComponentIdSetsParent) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -270,7 +262,6 @@ TEST(BeaconEntityMapper, ComponentIdSetsParent) {
// ---------------------------------------------------------------------------
// DisplayNameSetsName
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, DisplayNameSetsName) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -287,7 +278,6 @@ TEST(BeaconEntityMapper, DisplayNameSetsName) {
// ---------------------------------------------------------------------------
// DependsOnMapped
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, DependsOnMapped) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -309,7 +299,6 @@ TEST(BeaconEntityMapper, DependsOnMapped) {
// ---------------------------------------------------------------------------
// MetadataPrefixedCorrectly
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, MetadataPrefixedCorrectly) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -328,7 +317,6 @@ TEST(BeaconEntityMapper, MetadataPrefixedCorrectly) {
// ---------------------------------------------------------------------------
// ProcessDiagnosticsInMetadata
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, ProcessDiagnosticsInMetadata) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -352,7 +340,6 @@ TEST(BeaconEntityMapper, ProcessDiagnosticsInMetadata) {
// ---------------------------------------------------------------------------
// ActiveHintMetadataCorrect
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, ActiveHintMetadataCorrect) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -373,7 +360,6 @@ TEST(BeaconEntityMapper, ActiveHintMetadataCorrect) {
// ---------------------------------------------------------------------------
// StaleHintMetadataCorrect
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, StaleHintMetadataCorrect) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -393,7 +379,6 @@ TEST(BeaconEntityMapper, StaleHintMetadataCorrect) {
// ---------------------------------------------------------------------------
// MultipleHintsProcessedIndependently
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, MultipleHintsProcessedIndependently) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -414,7 +399,6 @@ TEST(BeaconEntityMapper, MultipleHintsProcessedIndependently) {
// ---------------------------------------------------------------------------
// StableIdInMetadata
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, StableIdInMetadata) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -431,7 +415,6 @@ TEST(BeaconEntityMapper, StableIdInMetadata) {
// ---------------------------------------------------------------------------
// NegotiatedFormatInMetadata
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, NegotiatedFormatInMetadata) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -448,7 +431,6 @@ TEST(BeaconEntityMapper, NegotiatedFormatInMetadata) {
// ---------------------------------------------------------------------------
// EmptyOptionalFieldsOmitted
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, EmptyOptionalFieldsOmitted) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -479,7 +461,6 @@ TEST(BeaconEntityMapper, EmptyOptionalFieldsOmitted) {
// ---------------------------------------------------------------------------
// FunctionDeduplication - same function referenced by multiple hints
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, FunctionDeduplication) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -505,7 +486,6 @@ TEST(BeaconEntityMapper, FunctionDeduplication) {
// ---------------------------------------------------------------------------
// EmptyHintsVector
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, EmptyHintsVector) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -521,7 +501,6 @@ TEST(BeaconEntityMapper, EmptyHintsVector) {
// ---------------------------------------------------------------------------
// DisplayNameEmptyDoesNotOverrideName
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, DisplayNameEmptyDoesNotOverrideName) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -539,7 +518,6 @@ TEST(BeaconEntityMapper, DisplayNameEmptyDoesNotOverrideName) {
// ---------------------------------------------------------------------------
// FreeformMetadataDoesNotOverrideStructuredFields
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, FreeformMetadataDoesNotOverrideStructuredFields) {
auto input = make_base_input();
BeaconEntityMapper mapper;
@@ -561,7 +539,6 @@ TEST(BeaconEntityMapper, FreeformMetadataDoesNotOverrideStructuredFields) {
// ---------------------------------------------------------------------------
// ComponentIdEmptyDoesNotOverride
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_01
TEST(BeaconEntityMapper, ComponentIdEmptyDoesNotOverride) {
auto input = make_base_input();
BeaconEntityMapper mapper;
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_hint_store.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_hint_store.cpp
index 0c10de75..8c6dac10 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_hint_store.cpp
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_hint_store.cpp
@@ -42,7 +42,6 @@ BeaconHint make_hint(const std::string & entity_id, const std::string & transpor
// ---------------------------------------------------------------------------
// InsertAndRetrieve
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, InsertAndRetrieve) {
BeaconHintStore store;
auto hint = make_hint("app_1");
@@ -58,7 +57,6 @@ TEST(BeaconHintStore, InsertAndRetrieve) {
// ---------------------------------------------------------------------------
// UpdateRefreshesTimestamp
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, UpdateRefreshesTimestamp) {
BeaconHintStore store;
auto hint = make_hint("app_1");
@@ -79,7 +77,6 @@ TEST(BeaconHintStore, UpdateRefreshesTimestamp) {
// ---------------------------------------------------------------------------
// UpdateOverwritesFields
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, UpdateOverwritesFields) {
BeaconHintStore store;
ASSERT_TRUE(store.update(make_hint("app_1", "ros2")));
@@ -98,7 +95,6 @@ TEST(BeaconHintStore, UpdateOverwritesFields) {
// ---------------------------------------------------------------------------
// StaleHintReactivatedOnRefresh
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, StaleHintReactivatedOnRefresh) {
BeaconHintStore::Config cfg;
cfg.beacon_ttl_sec = 1.0;
@@ -140,7 +136,6 @@ TEST(BeaconHintStore, StaleHintReactivatedOnRefresh) {
// ---------------------------------------------------------------------------
// TTLTransitionToStale
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, TTLTransitionToStale) {
BeaconHintStore::Config cfg;
cfg.beacon_ttl_sec = 0.05; // 50ms
@@ -160,7 +155,6 @@ TEST(BeaconHintStore, TTLTransitionToStale) {
// ---------------------------------------------------------------------------
// ExpiryRemovesHint
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, ExpiryRemovesHint) {
BeaconHintStore::Config cfg;
cfg.beacon_ttl_sec = 0.05;
@@ -180,7 +174,6 @@ TEST(BeaconHintStore, ExpiryRemovesHint) {
// ---------------------------------------------------------------------------
// EvictAndSnapshotIsAtomic
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, EvictAndSnapshotIsAtomic) {
BeaconHintStore::Config cfg;
cfg.beacon_ttl_sec = 0.05;
@@ -208,7 +201,6 @@ TEST(BeaconHintStore, EvictAndSnapshotIsAtomic) {
// ---------------------------------------------------------------------------
// EvictOnEmptyStoreIsNoop
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, EvictOnEmptyStoreIsNoop) {
BeaconHintStore store;
auto snapshot = store.evict_and_snapshot();
@@ -237,7 +229,6 @@ TEST(BeaconHintStore, EvictOnEmptyStoreIsNoop) {
// stale_app age=150ms > TTL=50ms but < expiry=200ms. STALE.
// active_app: refresh at t=250ms -> ACTIVE.
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, MixedStatesInSnapshot) {
BeaconHintStore::Config cfg;
cfg.beacon_ttl_sec = 0.05; // 50ms
@@ -280,7 +271,6 @@ TEST(BeaconHintStore, MixedStatesInSnapshot) {
// ---------------------------------------------------------------------------
// CapacityLimitRejectsNewEntity
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, CapacityLimitRejectsNewEntity) {
BeaconHintStore::Config cfg;
cfg.max_hints = 3;
@@ -298,7 +288,6 @@ TEST(BeaconHintStore, CapacityLimitRejectsNewEntity) {
// ---------------------------------------------------------------------------
// CapacityLimitAcceptsRefresh
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, CapacityLimitAcceptsRefresh) {
BeaconHintStore::Config cfg;
cfg.max_hints = 3;
@@ -360,7 +349,6 @@ TEST(BeaconHintStore, ConcurrentUpdateAndSnapshot) {
// ---------------------------------------------------------------------------
// MetadataReplacedOnRefresh
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, MetadataReplacedOnRefresh) {
BeaconHintStore store;
@@ -394,7 +382,6 @@ TEST(BeaconHintStore, MetadataReplacedOnRefresh) {
// ---------------------------------------------------------------------------
// EmptyMetadataPreservesExisting
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, EmptyMetadataPreservesExisting) {
BeaconHintStore store;
@@ -454,7 +441,6 @@ TEST(BeaconHintStore, ConcurrentGetDoesNotBlock) {
// ---------------------------------------------------------------------------
// ReceivedAtSetsLastSeen
-// @verifies REQ_DISCO_BEACON_03
//
// A hint with received_at set to 5 seconds ago should produce a last_seen
// that reflects that age (approximately 5s old, not ~0s).
@@ -483,7 +469,6 @@ TEST(BeaconHintStore, ReceivedAtSetsLastSeen) {
// ---------------------------------------------------------------------------
// DefaultReceivedAtUsesNow
-// @verifies REQ_DISCO_BEACON_03
//
// A hint with default (zero-initialized) received_at should have last_seen
// set to approximately steady_clock::now() at insert time.
@@ -506,7 +491,6 @@ TEST(BeaconHintStore, DefaultReceivedAtUsesNow) {
// ---------------------------------------------------------------------------
// ReceivedAtFarPastIsImmediatelyStale
-// @verifies REQ_DISCO_BEACON_03
//
// A hint with received_at set far in the past should be immediately STALE
// if the age exceeds the TTL.
@@ -530,11 +514,9 @@ TEST(BeaconHintStore, ReceivedAtFarPastIsImmediatelyStale) {
// ---------------------------------------------------------------------------
// TTLLifecycle
-// @verifies REQ_DISCO_BEACON_03
//
// Verify the ACTIVE -> STALE -> EXPIRED lifecycle with short TTL and expiry.
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, TTLLifecycle) {
BeaconHintStore::Config cfg;
cfg.beacon_ttl_sec = 0.1; // 100ms TTL
@@ -559,11 +541,9 @@ TEST(BeaconHintStore, TTLLifecycle) {
// ---------------------------------------------------------------------------
// MinimalHint
-// @verifies REQ_DISCO_BEACON_03
//
// A hint with only entity_id set should store and retrieve correctly.
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, MinimalHint) {
BeaconHintStore store;
@@ -583,7 +563,6 @@ TEST(BeaconHintStore, MinimalHint) {
// ---------------------------------------------------------------------------
// GetReturnsNulloptForExpiredHint
-// @verifies REQ_DISCO_BEACON_03
//
// After expiry, get() should return nullopt instead of serving stale data.
// ---------------------------------------------------------------------------
@@ -613,11 +592,9 @@ TEST(BeaconHintStore, GetReturnsNulloptForExpiredHint) {
// ---------------------------------------------------------------------------
// CapacityBehaviorThirdInsertRejected
-// @verifies REQ_DISCO_BEACON_03
//
// A store with max_hints=2 should reject the 3rd unique entity insert.
// ---------------------------------------------------------------------------
-// @verifies REQ_DISCO_BEACON_03
TEST(BeaconHintStore, CapacityBehaviorThirdInsertRejected) {
BeaconHintStore::Config cfg;
cfg.max_hints = 2;
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_validator.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_validator.cpp
index 83036d82..710d07b7 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_validator.cpp
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_beacon_common/test/test_beacon_validator.cpp
@@ -36,7 +36,6 @@ class TestBeaconValidator : public ::testing::Test {
}
};
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, ValidHintAccepted) {
auto hint = make_valid_hint();
auto result = validate_beacon_hint(hint, limits_);
@@ -44,7 +43,6 @@ TEST_F(TestBeaconValidator, ValidHintAccepted) {
EXPECT_TRUE(result.reason.empty());
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, EmptyEntityIdRejected) {
auto hint = make_valid_hint();
hint.entity_id = "";
@@ -53,7 +51,6 @@ TEST_F(TestBeaconValidator, EmptyEntityIdRejected) {
EXPECT_FALSE(result.reason.empty());
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, InvalidEntityIdCharsRejected) {
auto hint = make_valid_hint();
hint.entity_id = "bad/entity id";
@@ -61,7 +58,6 @@ TEST_F(TestBeaconValidator, InvalidEntityIdCharsRejected) {
EXPECT_FALSE(result.valid);
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, EntityIdWithNullByteRejected) {
auto hint = make_valid_hint();
hint.entity_id = std::string("bad\0id", 6);
@@ -69,7 +65,6 @@ TEST_F(TestBeaconValidator, EntityIdWithNullByteRejected) {
EXPECT_FALSE(result.valid);
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, OversizedEntityIdRejected) {
auto hint = make_valid_hint();
hint.entity_id = std::string(257, 'a');
@@ -77,7 +72,6 @@ TEST_F(TestBeaconValidator, OversizedEntityIdRejected) {
EXPECT_FALSE(result.valid);
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, InvalidFunctionIdSkipped) {
auto hint = make_valid_hint();
hint.function_ids = {"valid-id", "bad/id", "also_valid"};
@@ -88,7 +82,6 @@ TEST_F(TestBeaconValidator, InvalidFunctionIdSkipped) {
EXPECT_EQ(hint.function_ids[1], "also_valid");
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, FunctionIdsTruncatedAtLimit) {
auto hint = make_valid_hint();
for (size_t i = 0; i < 105; ++i) {
@@ -99,7 +92,6 @@ TEST_F(TestBeaconValidator, FunctionIdsTruncatedAtLimit) {
EXPECT_EQ(hint.function_ids.size(), limits_.max_function_ids);
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, MetadataKeyTooLongSkipped) {
auto hint = make_valid_hint();
hint.metadata[std::string(65, 'k')] = "value";
@@ -110,7 +102,6 @@ TEST_F(TestBeaconValidator, MetadataKeyTooLongSkipped) {
EXPECT_TRUE(hint.metadata.count("good_key"));
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, MetadataValueTruncated) {
auto hint = make_valid_hint();
hint.metadata["key"] = std::string(2000, 'v');
@@ -119,7 +110,6 @@ TEST_F(TestBeaconValidator, MetadataValueTruncated) {
EXPECT_EQ(hint.metadata["key"].size(), limits_.max_metadata_value_length);
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, ZeroPidTreatedAsNotProvided) {
auto hint = make_valid_hint();
hint.process_id = 0;
@@ -127,7 +117,6 @@ TEST_F(TestBeaconValidator, ZeroPidTreatedAsNotProvided) {
EXPECT_TRUE(result.valid);
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, OversizedStringFieldsTruncated) {
auto hint = make_valid_hint();
// 10 KB hostname should be truncated to max_string_length (512)
@@ -145,7 +134,6 @@ TEST_F(TestBeaconValidator, OversizedStringFieldsTruncated) {
EXPECT_EQ(hint.negotiated_format.size(), limits_.max_string_length);
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, ShortStringFieldsUnchanged) {
auto hint = make_valid_hint();
hint.hostname = "robot-01";
@@ -162,7 +150,6 @@ TEST_F(TestBeaconValidator, ShortStringFieldsUnchanged) {
EXPECT_EQ(hint.negotiated_format, "cdr");
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TestBeaconValidator, CustomMaxStringLength) {
ValidationLimits custom;
custom.max_string_length = 10;
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp
index 2e382f63..56372f9c 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp
@@ -215,7 +215,6 @@ TEST_F(ParamBeaconPluginTest, PluginNameAndExports) {
delete raw;
}
-// @verifies REQ_DISCO_BEACON_04
TEST_F(ParamBeaconPluginTest, CapabilitiesRegistered) {
setup_plugin();
ASSERT_EQ(mock_ctx_->registered_capabilities_.size(), 2u);
@@ -223,7 +222,6 @@ TEST_F(ParamBeaconPluginTest, CapabilitiesRegistered) {
EXPECT_EQ(mock_ctx_->registered_capabilities_[1].name, "x-medkit-param-beacon");
}
-// @verifies REQ_DISCO_BEACON_02
TEST_F(ParamBeaconPluginTest, PollsNodeAndStoresHint) {
// Setup mock client expectations
EXPECT_CALL(*mock_client_, wait_for_service(_)).WillRepeatedly(Return(true));
@@ -259,7 +257,6 @@ TEST_F(ParamBeaconPluginTest, PollsNodeAndStoresHint) {
EXPECT_EQ(stored->hint.hostname, "test-host");
}
-// @verifies REQ_DISCO_BEACON_02
TEST_F(ParamBeaconPluginTest, SkipsNodeWithoutEntityId) {
EXPECT_CALL(*mock_client_, wait_for_service(_)).WillRepeatedly(Return(true));
EXPECT_CALL(*mock_client_, list_parameters(_, _))
@@ -332,7 +329,6 @@ TEST_F(ParamBeaconPluginTest, BackoffOnTimeout) {
ASSERT_TRUE(stored.has_value());
}
-// @verifies REQ_DISCO_BEACON_02
TEST_F(ParamBeaconPluginTest, MetadataSubParams) {
EXPECT_CALL(*mock_client_, wait_for_service(_)).WillRepeatedly(Return(true));
EXPECT_CALL(*mock_client_, list_parameters(_, _))
@@ -384,7 +380,6 @@ TEST_F(ParamBeaconPluginTest, ConfigValidationAutoFixes) {
plugin->shutdown();
}
-// @verifies REQ_DISCO_BEACON_02
TEST_F(ParamBeaconPluginTest, IntrospectReturnsMetadata) {
// Populate store directly
BeaconHint hint;
diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp
index cfbbd8dd..3f4b2531 100644
--- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp
+++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp
@@ -196,7 +196,6 @@ class TopicBeaconPluginTest : public ::testing::Test {
rclcpp::Publisher::SharedPtr publisher_;
};
-// @verifies REQ_DISCO_BEACON_01
TEST_F(TopicBeaconPluginTest, MessageCallbackUpdatesStore) {
auto msg = make_hint("engine_temp_sensor");
publisher_->publish(msg);
@@ -214,7 +213,6 @@ TEST_F(TopicBeaconPluginTest, MessageCallbackUpdatesStore) {
EXPECT_EQ(stored->hint.process_id, 1234u);
}
-// @verifies REQ_DISCO_BEACON_01
TEST_F(TopicBeaconPluginTest, IntrospectReturnsCorrectResult) {
// Populate store directly
BeaconHint hint;
@@ -236,7 +234,6 @@ TEST_F(TopicBeaconPluginTest, IntrospectReturnsCorrectResult) {
EXPECT_TRUE(result.metadata["my_app"].contains("x-medkit-beacon-status"));
}
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TopicBeaconPluginTest, InvalidMessageRejectedByValidator) {
// Publish message with empty entity_id - should be rejected by validator
ros2_medkit_msgs::msg::MedkitDiscoveryHint msg;
@@ -255,7 +252,6 @@ TEST_F(TopicBeaconPluginTest, ShutdownDestroysSubscription) {
EXPECT_EQ(plugin_->subscription(), nullptr);
}
-// @verifies REQ_DISCO_BEACON_01
TEST_F(TopicBeaconPluginTest, MetadataFromMessagePreserved) {
auto msg = make_hint("sensor_node");
diagnostic_msgs::msg::KeyValue kv;
@@ -271,7 +267,6 @@ TEST_F(TopicBeaconPluginTest, MetadataFromMessagePreserved) {
EXPECT_EQ(stored->hint.metadata.at("gpu_model"), "RTX 4090");
}
-// @verifies REQ_DISCO_BEACON_04
TEST_F(TopicBeaconPluginTest, PluginNameAndCapabilities) {
EXPECT_EQ(plugin_->name(), "topic_beacon");
@@ -322,7 +317,6 @@ TEST(TokenBucketTest, DefaultConstructor) {
// --- Rate limiting rejection test ---
-// @verifies REQ_DISCO_BEACON_05
TEST_F(TopicBeaconPluginTest, RateLimitingRejectsExcessMessages) {
// Create a separate plugin with very low rate limit
auto rate_limited_plugin = std::make_unique();
From 398c8bb77687ed2e1b147121420ab5341fa2cd9e Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 18:42:28 +0100
Subject: [PATCH 18/23] fix(requirements): correct mismatched @verifies tags
across 5 test files
---
.../test/test_fault_manager.cpp | 6 +++---
.../test/test_bulkdata_handlers.cpp | 8 ++++----
.../test/test_plugin_loader.cpp | 14 --------------
.../test/test_plugin_manager.cpp | 15 ---------------
.../test/features/test_operations_api.test.py | 8 ++++----
5 files changed, 11 insertions(+), 40 deletions(-)
diff --git a/src/ros2_medkit_fault_manager/test/test_fault_manager.cpp b/src/ros2_medkit_fault_manager/test/test_fault_manager.cpp
index 01cee276..0e348779 100644
--- a/src/ros2_medkit_fault_manager/test/test_fault_manager.cpp
+++ b/src/ros2_medkit_fault_manager/test/test_fault_manager.cpp
@@ -1014,7 +1014,7 @@ TEST_F(FaultEventPublishingTest, GetFaultReturnsExtendedDataRecords) {
EXPECT_NE(edr.last_occurrence_ns, 0);
}
-// @verifies REQ_INTEROP_071
+// @verifies REQ_INTEROP_012
TEST_F(FaultEventPublishingTest, ListFaultsForEntitySuccess) {
// Report faults from different sources
ASSERT_TRUE(call_report_fault("MOTOR_FAULT", Fault::SEVERITY_ERROR, "/powertrain/motor_controller"));
@@ -1038,7 +1038,7 @@ TEST_F(FaultEventPublishingTest, ListFaultsForEntitySuccess) {
EXPECT_FALSE(codes.count("BRAKE_FAULT"));
}
-// @verifies REQ_INTEROP_071
+// @verifies REQ_INTEROP_012
TEST_F(FaultEventPublishingTest, ListFaultsForEntityEmptyResult) {
// Report faults from a different entity
ASSERT_TRUE(call_report_fault("SOME_FAULT", Fault::SEVERITY_ERROR, "/some/other_entity"));
@@ -1051,7 +1051,7 @@ TEST_F(FaultEventPublishingTest, ListFaultsForEntityEmptyResult) {
EXPECT_TRUE(response->faults.empty());
}
-// @verifies REQ_INTEROP_071
+// @verifies REQ_INTEROP_012
TEST_F(FaultEventPublishingTest, ListFaultsForEntityWithEmptyId) {
auto response = call_list_faults_for_entity("");
ASSERT_TRUE(response.has_value());
diff --git a/src/ros2_medkit_gateway/test/test_bulkdata_handlers.cpp b/src/ros2_medkit_gateway/test/test_bulkdata_handlers.cpp
index 01d49178..f0a304cd 100644
--- a/src/ros2_medkit_gateway/test/test_bulkdata_handlers.cpp
+++ b/src/ros2_medkit_gateway/test/test_bulkdata_handlers.cpp
@@ -67,7 +67,7 @@ TEST_F(BulkDataHandlersTest, GetRosbagMimetypeCasesSensitive) {
// === Shared timestamp utility tests ===
-// @verifies REQ_INTEROP_013
+// @verifies REQ_INTEROP_071
TEST_F(BulkDataHandlersTest, FormatTimestampNsValidTimestamp) {
// 2026-02-08T00:00:00.000Z
int64_t ns = 1770458400000000000;
@@ -77,13 +77,13 @@ TEST_F(BulkDataHandlersTest, FormatTimestampNsValidTimestamp) {
EXPECT_TRUE(result.find("Z") != std::string::npos);
}
-// @verifies REQ_INTEROP_013
+// @verifies REQ_INTEROP_071
TEST_F(BulkDataHandlersTest, FormatTimestampNsEpoch) {
auto result = ros2_medkit_gateway::format_timestamp_ns(0);
EXPECT_EQ(result, "1970-01-01T00:00:00.000Z");
}
-// @verifies REQ_INTEROP_013
+// @verifies REQ_INTEROP_071
TEST_F(BulkDataHandlersTest, FormatTimestampNsWithMilliseconds) {
// 1 second + 123 ms
int64_t ns = 1'000'000'000 + 123'000'000;
@@ -91,7 +91,7 @@ TEST_F(BulkDataHandlersTest, FormatTimestampNsWithMilliseconds) {
EXPECT_TRUE(result.find(".123Z") != std::string::npos);
}
-// @verifies REQ_INTEROP_013
+// @verifies REQ_INTEROP_071
TEST_F(BulkDataHandlersTest, FormatTimestampNsNegativeFallback) {
// Negative timestamps should return fallback
auto result = ros2_medkit_gateway::format_timestamp_ns(-1);
diff --git a/src/ros2_medkit_gateway/test/test_plugin_loader.cpp b/src/ros2_medkit_gateway/test/test_plugin_loader.cpp
index da70699a..3be8e2e6 100644
--- a/src/ros2_medkit_gateway/test/test_plugin_loader.cpp
+++ b/src/ros2_medkit_gateway/test/test_plugin_loader.cpp
@@ -37,7 +37,6 @@ std::string test_plugin_path() {
// --- Happy path ---
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, LoadsValidPlugin) {
auto result = PluginLoader::load(test_plugin_path());
ASSERT_TRUE(result.has_value()) << result.error();
@@ -45,14 +44,12 @@ TEST(TestPluginLoader, LoadsValidPlugin) {
EXPECT_EQ(result->plugin->name(), "test_plugin");
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, DiscoverUpdateProviderViaExternC) {
auto result = PluginLoader::load(test_plugin_path());
ASSERT_TRUE(result.has_value()) << result.error();
EXPECT_NE(result->update_provider, nullptr);
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, DiscoverIntrospectionProviderViaExternC) {
auto result = PluginLoader::load(test_plugin_path());
ASSERT_TRUE(result.has_value()) << result.error();
@@ -61,21 +58,18 @@ TEST(TestPluginLoader, DiscoverIntrospectionProviderViaExternC) {
// --- Path validation ---
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, RejectsNonexistentFile) {
auto result = PluginLoader::load("/nonexistent/path/to/plugin.so");
ASSERT_FALSE(result.has_value());
EXPECT_NE(result.error().find("does not exist"), std::string::npos);
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, RejectsRelativePath) {
auto result = PluginLoader::load("relative/path/plugin.so");
ASSERT_FALSE(result.has_value());
EXPECT_NE(result.error().find("must be absolute"), std::string::npos);
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, RejectsNonSoExtension) {
auto result = PluginLoader::load("/tmp/plugin.dll");
ASSERT_FALSE(result.has_value());
@@ -84,14 +78,12 @@ TEST(TestPluginLoader, RejectsNonSoExtension) {
// --- Symbol validation ---
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, RejectsMissingVersionSymbol) {
auto result = PluginLoader::load(plugin_lib_dir() + "libtest_no_symbols_plugin.so");
ASSERT_FALSE(result.has_value());
EXPECT_NE(result.error().find("plugin_api_version"), std::string::npos);
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, RejectsVersionMismatch) {
auto result = PluginLoader::load(plugin_lib_dir() + "libtest_bad_version_plugin.so");
ASSERT_FALSE(result.has_value());
@@ -99,14 +91,12 @@ TEST(TestPluginLoader, RejectsVersionMismatch) {
EXPECT_NE(result.error().find("Rebuild"), std::string::npos);
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, RejectsMissingFactorySymbol) {
auto result = PluginLoader::load(plugin_lib_dir() + "libtest_version_only_plugin.so");
ASSERT_FALSE(result.has_value());
EXPECT_NE(result.error().find("create_plugin"), std::string::npos);
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, RejectsNullFactory) {
auto result = PluginLoader::load(plugin_lib_dir() + "libtest_null_factory_plugin.so");
ASSERT_FALSE(result.has_value());
@@ -115,7 +105,6 @@ TEST(TestPluginLoader, RejectsNullFactory) {
// --- Minimal plugin (no provider query functions) ---
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, LoadsMinimalPluginWithNoProviders) {
auto result = PluginLoader::load(plugin_lib_dir() + "libtest_minimal_plugin.so");
ASSERT_TRUE(result.has_value()) << result.error();
@@ -127,7 +116,6 @@ TEST(TestPluginLoader, LoadsMinimalPluginWithNoProviders) {
// --- GatewayPluginLoadResult move semantics ---
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, MoveConstructorTransfersOwnership) {
auto result = PluginLoader::load(test_plugin_path());
ASSERT_TRUE(result.has_value()) << result.error();
@@ -147,7 +135,6 @@ TEST(TestPluginLoader, MoveConstructorTransfersOwnership) {
EXPECT_EQ(result->introspection_provider, nullptr);
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, MoveAssignmentTransfersOwnership) {
auto result = PluginLoader::load(test_plugin_path());
ASSERT_TRUE(result.has_value()) << result.error();
@@ -165,7 +152,6 @@ TEST(TestPluginLoader, MoveAssignmentTransfersOwnership) {
EXPECT_EQ(result->update_provider, nullptr);
}
-// @verifies REQ_INTEROP_012
TEST(TestPluginLoader, LoadPluginsSuccessPath) {
// Test load_plugins() through PluginManager with real .so file
PluginManager mgr;
diff --git a/src/ros2_medkit_gateway/test/test_plugin_manager.cpp b/src/ros2_medkit_gateway/test/test_plugin_manager.cpp
index 4cf41167..625c089f 100644
--- a/src/ros2_medkit_gateway/test/test_plugin_manager.cpp
+++ b/src/ros2_medkit_gateway/test/test_plugin_manager.cpp
@@ -162,7 +162,6 @@ class MockThrowOnShutdown : public GatewayPlugin {
}
};
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, EmptyManagerHasNoPlugins) {
PluginManager mgr;
EXPECT_FALSE(mgr.has_plugins());
@@ -171,7 +170,6 @@ TEST(PluginManagerTest, EmptyManagerHasNoPlugins) {
EXPECT_TRUE(mgr.get_introspection_providers().empty());
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, AddPluginAndDispatch) {
PluginManager mgr;
auto plugin = std::make_unique();
@@ -186,7 +184,6 @@ TEST(PluginManagerTest, AddPluginAndDispatch) {
EXPECT_EQ(mgr.get_introspection_providers().size(), 1u);
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, ConfigurePassesConfig) {
PluginManager mgr;
auto plugin = std::make_unique();
@@ -200,7 +197,6 @@ TEST(PluginManagerTest, ConfigurePassesConfig) {
EXPECT_TRUE(raw->config_.empty());
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, LoadPluginsForwardsConfig) {
// load_plugins() should forward the PluginConfig.config to configure()
PluginManager mgr;
@@ -212,7 +208,6 @@ TEST(PluginManagerTest, LoadPluginsForwardsConfig) {
EXPECT_EQ(configs[0].config["timeout_ms"], 5000);
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, ShutdownCallsAllPlugins) {
PluginManager mgr;
auto plugin = std::make_unique();
@@ -223,7 +218,6 @@ TEST(PluginManagerTest, ShutdownCallsAllPlugins) {
EXPECT_TRUE(raw->shutdown_called_);
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, MultiCapabilityPluginDispatchedToBoth) {
PluginManager mgr;
mgr.add_plugin(std::make_unique());
@@ -232,7 +226,6 @@ TEST(PluginManagerTest, MultiCapabilityPluginDispatchedToBoth) {
EXPECT_EQ(mgr.get_introspection_providers().size(), 1u);
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, IntrospectionOnlyPluginNotUpdateProvider) {
PluginManager mgr;
mgr.add_plugin(std::make_unique());
@@ -241,7 +234,6 @@ TEST(PluginManagerTest, IntrospectionOnlyPluginNotUpdateProvider) {
EXPECT_EQ(mgr.get_introspection_providers().size(), 1u);
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, MultipleIntrospectionProviders) {
PluginManager mgr;
mgr.add_plugin(std::make_unique());
@@ -250,7 +242,6 @@ TEST(PluginManagerTest, MultipleIntrospectionProviders) {
EXPECT_EQ(mgr.get_introspection_providers().size(), 2u);
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, DuplicateUpdateProviderFirstWins) {
PluginManager mgr;
auto first = std::make_unique();
@@ -262,7 +253,6 @@ TEST(PluginManagerTest, DuplicateUpdateProviderFirstWins) {
EXPECT_EQ(mgr.get_update_provider(), static_cast(first_raw));
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, ThrowingPluginDisabledDuringConfigure) {
PluginManager mgr;
mgr.add_plugin(std::make_unique());
@@ -278,7 +268,6 @@ TEST(PluginManagerTest, ThrowingPluginDisabledDuringConfigure) {
EXPECT_NE(mgr.get_update_provider(), nullptr);
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, LoadNonexistentPluginReturnsZero) {
PluginManager mgr;
std::vector configs = {{"nonexistent", "/nonexistent/path.so", json::object()}};
@@ -287,7 +276,6 @@ TEST(PluginManagerTest, LoadNonexistentPluginReturnsZero) {
EXPECT_FALSE(mgr.has_plugins());
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, ThrowOnSetContextDisablesPlugin) {
PluginManager mgr;
mgr.add_plugin(std::make_unique());
@@ -308,7 +296,6 @@ TEST(PluginManagerTest, ThrowOnSetContextDisablesPlugin) {
EXPECT_EQ(mgr.plugin_names()[0], "mock");
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, ThrowOnRegisterRoutesDisablesPlugin) {
PluginManager mgr;
mgr.add_plugin(std::make_unique());
@@ -325,7 +312,6 @@ TEST(PluginManagerTest, ThrowOnRegisterRoutesDisablesPlugin) {
EXPECT_EQ(mgr.plugin_names()[0], "introspection_only");
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, ShutdownAllIdempotent) {
PluginManager mgr;
auto plugin = std::make_unique();
@@ -341,7 +327,6 @@ TEST(PluginManagerTest, ShutdownAllIdempotent) {
EXPECT_FALSE(raw->shutdown_called_);
}
-// @verifies REQ_INTEROP_012
TEST(PluginManagerTest, ShutdownSwallowsExceptions) {
PluginManager mgr;
mgr.add_plugin(std::make_unique());
diff --git a/src/ros2_medkit_integration_tests/test/features/test_operations_api.test.py b/src/ros2_medkit_integration_tests/test/features/test_operations_api.test.py
index 0089afa2..c2759646 100644
--- a/src/ros2_medkit_integration_tests/test/features/test_operations_api.test.py
+++ b/src/ros2_medkit_integration_tests/test/features/test_operations_api.test.py
@@ -140,7 +140,7 @@ def test_operation_call_invalid_entity_id(self):
def test_operation_call_invalid_operation_name(self):
"""Operation call rejects invalid operation name.
- @verifies REQ_INTEROP_021
+ @verifies REQ_INTEROP_035
"""
invalid_names = [
'op;drop',
@@ -167,7 +167,7 @@ def test_operation_call_invalid_operation_name(self):
def test_operation_call_with_invalid_json(self):
"""Operation call returns 400 for invalid JSON body.
- @verifies REQ_INTEROP_021
+ @verifies REQ_INTEROP_035
"""
response = requests.post(
f'{self.BASE_URL}/apps/calibration/operations/calibrate/executions',
@@ -188,7 +188,7 @@ def test_operation_call_with_invalid_json(self):
def test_operations_listed_in_app_discovery(self):
"""Operations (services) are available via app detail endpoint.
- @verifies REQ_INTEROP_021
+ @verifies REQ_INTEROP_033
"""
self.poll_endpoint('/apps/calibration')
@@ -222,7 +222,7 @@ def test_operations_listed_in_app_discovery(self):
def test_root_endpoint_includes_operations(self):
"""Root endpoint lists operations endpoint and capability.
- @verifies REQ_INTEROP_021
+ @verifies REQ_INTEROP_033
"""
data = self.get_json('/')
From aa9bcc3ec3079b2a5189bfc2435bd90c440a9a29 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 19:02:10 +0100
Subject: [PATCH 19/23] docs: rename sovd_web_ui to ros2_medkit_web_ui across
all docs
---
docs/config/server.rst | 2 +-
docs/getting_started.rst | 4 ++--
docs/tutorials/demos/demo-sensor.rst | 2 +-
docs/tutorials/demos/demo-turtlebot3.rst | 2 +-
docs/tutorials/docker.rst | 2 +-
docs/tutorials/index.rst | 2 +-
docs/tutorials/web-ui.rst | 20 ++++++++++----------
7 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/docs/config/server.rst b/docs/config/server.rst
index fea6831c..bc33ad36 100644
--- a/docs/config/server.rst
+++ b/docs/config/server.rst
@@ -123,7 +123,7 @@ Cross-Origin Resource Sharing (CORS) settings for browser-based clients.
- ``86400``
- Preflight response cache duration (24 hours).
-Example for development with sovd_web_ui:
+Example for development with ros2_medkit_web_ui:
.. code-block:: yaml
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index 56ee6d38..b2582591 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -456,8 +456,8 @@ A companion web UI is available for visual entity browsing:
.. code-block:: bash
- docker pull ghcr.io/selfpatch/sovd_web_ui:latest
- docker run -p 3000:80 ghcr.io/selfpatch/sovd_web_ui:latest
+ docker pull ghcr.io/selfpatch/ros2_medkit_web_ui:latest
+ docker run -p 3000:80 ghcr.io/selfpatch/ros2_medkit_web_ui:latest
Open http://localhost:3000 and connect to the gateway at http://localhost:8080.
diff --git a/docs/tutorials/demos/demo-sensor.rst b/docs/tutorials/demos/demo-sensor.rst
index b2b758ee..f071fb3c 100644
--- a/docs/tutorials/demos/demo-sensor.rst
+++ b/docs/tutorials/demos/demo-sensor.rst
@@ -57,7 +57,7 @@ Clone the demo repository and run the startup script:
The script will build and start Docker containers with:
- ros2_medkit gateway (REST API on port 8080)
-- sovd_web_ui (Web interface on port 3000)
+- ros2_medkit_web_ui (Web interface on port 3000)
- Simulated sensor nodes (lidar, camera, imu, gps)
- Anomaly detector for fault monitoring
- Diagnostic bridge for legacy fault reporting
diff --git a/docs/tutorials/demos/demo-turtlebot3.rst b/docs/tutorials/demos/demo-turtlebot3.rst
index 36dffe29..24c609b0 100644
--- a/docs/tutorials/demos/demo-turtlebot3.rst
+++ b/docs/tutorials/demos/demo-turtlebot3.rst
@@ -56,7 +56,7 @@ The script will build and start Docker containers with:
- Gazebo simulation with TurtleBot3 Waffle
- Nav2 navigation stack
- ros2_medkit gateway (REST API on port 8080)
-- sovd_web_ui (Web interface on port 3000)
+- ros2_medkit_web_ui (Web interface on port 3000)
**Startup Options:**
diff --git a/docs/tutorials/docker.rst b/docs/tutorials/docker.rst
index 16d5d8a4..2b68d433 100644
--- a/docs/tutorials/docker.rst
+++ b/docs/tutorials/docker.rst
@@ -33,7 +33,7 @@ This starts:
- TurtleBot3 simulation with Nav2
- ros2_medkit_gateway
-- `sovd_web_ui `_ (Web UI)
+- `ros2_medkit_web_ui `_ (Web UI)
Access the UI at http://localhost:3000 (Docker container maps port 80 to 3000).
diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst
index 610471ad..faed08ec 100644
--- a/docs/tutorials/index.rst
+++ b/docs/tutorials/index.rst
@@ -84,7 +84,7 @@ Companion Projects
------------------
:doc:`web-ui`
- sovd_web_ui — A web interface for browsing SOVD entity trees.
+ ros2_medkit_web_ui — A web interface for browsing SOVD entity trees.
:doc:`mcp-server`
ros2_medkit_mcp — Connect LLMs to your ROS 2 system via MCP protocol.
diff --git a/docs/tutorials/web-ui.rst b/docs/tutorials/web-ui.rst
index f17fa569..a668f28c 100644
--- a/docs/tutorials/web-ui.rst
+++ b/docs/tutorials/web-ui.rst
@@ -1,15 +1,15 @@
-Web UI (sovd_web_ui)
+Web UI (ros2_medkit_web_ui)
====================
-sovd_web_ui is a lightweight web application for browsing SOVD entity trees.
+ros2_medkit_web_ui is a lightweight web application for browsing SOVD entity trees.
It connects to the ros2_medkit gateway and visualizes the entity hierarchy.
.. figure:: /_static/images/00_ui_view.png
- :alt: sovd_web_ui main interface
+ :alt: ros2_medkit_web_ui main interface
:align: center
:width: 600px
- The sovd_web_ui interface showing entity tree, detail panel, and data view.
+ The ros2_medkit_web_ui interface showing entity tree, detail panel, and data view.
.. contents:: Table of Contents
:local:
@@ -32,8 +32,8 @@ Using Docker
.. code-block:: bash
# Pull from GitHub Container Registry
- docker pull ghcr.io/selfpatch/sovd_web_ui:latest
- docker run -p 3000:80 ghcr.io/selfpatch/sovd_web_ui:latest
+ docker pull ghcr.io/selfpatch/ros2_medkit_web_ui:latest
+ docker run -p 3000:80 ghcr.io/selfpatch/ros2_medkit_web_ui:latest
Then open http://localhost:3000 in your browser.
@@ -43,8 +43,8 @@ From Source
.. code-block:: bash
# Clone the repository
- git clone https://github.com/selfpatch/sovd_web_ui.git
- cd sovd_web_ui
+ git clone https://github.com/selfpatch/ros2_medkit_web_ui.git
+ cd ros2_medkit_web_ui
# Install dependencies
npm install
@@ -194,7 +194,7 @@ Run both gateway and web UI together:
network_mode: host
web_ui:
- image: ghcr.io/selfpatch/sovd_web_ui:latest
+ image: ghcr.io/selfpatch/ros2_medkit_web_ui:latest
ports:
- "80:80"
@@ -227,7 +227,7 @@ Tech Stack
Repository
----------
-https://github.com/selfpatch/sovd_web_ui
+https://github.com/selfpatch/ros2_medkit_web_ui
See Also
--------
From ca58ecc0cc2f0cef0646d3a32751d65b7a8f52c8 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 19:45:28 +0100
Subject: [PATCH 20/23] docs: add is-located-on endpoint to gateway README and
changelog
---
src/ros2_medkit_gateway/CHANGELOG.rst | 1 +
src/ros2_medkit_gateway/README.md | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/ros2_medkit_gateway/CHANGELOG.rst b/src/ros2_medkit_gateway/CHANGELOG.rst
index faac938c..0e86da21 100644
--- a/src/ros2_medkit_gateway/CHANGELOG.rst
+++ b/src/ros2_medkit_gateway/CHANGELOG.rst
@@ -53,6 +53,7 @@ Changelog for package ros2_medkit_gateway
* ``PluginContext::get_child_apps()`` for Component-level aggregation
* Sub-resource RBAC patterns for all collections
* Auto-populate gateway version from ``package.xml`` via CMake
+* ``GET /apps/{id}/is-located-on`` endpoint for reverse host lookup (app to component)
* Condition-based triggers with CRUD endpoints, SSE event streaming, and hierarchy matching
* ``TriggerManager`` with ``ConditionEvaluator`` interface and 4 built-in evaluators (OnChange, OnChangeTo, EnterRange, LeaveRange)
* ``ResourceChangeNotifier`` for async dispatch from FaultManager, UpdateManager, and OperationManager
diff --git a/src/ros2_medkit_gateway/README.md b/src/ros2_medkit_gateway/README.md
index e34f29e9..c6071c69 100644
--- a/src/ros2_medkit_gateway/README.md
+++ b/src/ros2_medkit_gateway/README.md
@@ -33,6 +33,7 @@ All endpoints are prefixed with `/api/v1` for API versioning.
- `GET /api/v1/components/{component_id}/hosts` - List apps hosted on a component
- `GET /api/v1/components/{component_id}/depends-on` - List component dependencies
- `GET /api/v1/areas/{area_id}/components` - List components within a specific area
+- `GET /api/v1/apps/{app_id}/is-located-on` - Get the component hosting this app
### Component Data Endpoints
From 725bfaaeb4c8c464ef5e6c5fb4ae540e8b29cfd4 Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 22:35:03 +0100
Subject: [PATCH 21/23] docs: fix RST title underline length in web-ui tutorial
---
docs/tutorials/web-ui.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/tutorials/web-ui.rst b/docs/tutorials/web-ui.rst
index a668f28c..bd9c24eb 100644
--- a/docs/tutorials/web-ui.rst
+++ b/docs/tutorials/web-ui.rst
@@ -1,5 +1,5 @@
Web UI (ros2_medkit_web_ui)
-====================
+==========================
ros2_medkit_web_ui is a lightweight web application for browsing SOVD entity trees.
It connects to the ros2_medkit gateway and visualizes the entity hierarchy.
From 7a35fc6cf16d64643c9b9b99ceef9298d7c5b41c Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 22:43:10 +0100
Subject: [PATCH 22/23] docs: pin sphinx dependency versions to match local
environment
---
docs/pyproject.toml | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/docs/pyproject.toml b/docs/pyproject.toml
index 5e6a72dc..4beddf3b 100644
--- a/docs/pyproject.toml
+++ b/docs/pyproject.toml
@@ -4,21 +4,21 @@ build-backend = "setuptools.build_meta"
[project]
name = "ros2-medkit-docs"
-version = "0.3.0"
+version = "0.4.0"
description = "Documentation for ROS 2 Medkit"
authors = [{ name = "bburda", email = "bartoszburda93@gmail.com" }]
requires-python = ">=3.12"
readme = "README.md"
license = "Apache-2.0"
dependencies = [
- "sphinx>=5.0",
- "sphinx-needs>=1.0",
- "sphinx-rtd-theme",
- "sphinxcontrib-plantuml",
- "matplotlib",
- "sphinx-design",
- "breathe",
- "sphinx-copybutton",
+ "sphinx==8.2.3",
+ "sphinx-needs==6.3.0",
+ "sphinx-rtd-theme==3.1.0",
+ "sphinxcontrib-plantuml==0.31",
+ "matplotlib==3.10.8",
+ "sphinx-design==0.7.0",
+ "breathe==4.36.0",
+ "sphinx-copybutton==0.5.2",
]
[project.optional-dependencies]
From ad6755b7162fd0ecb1abaa35072933912ea181bc Mon Sep 17 00:00:00 2001
From: Bartosz Burda
Date: Sat, 21 Mar 2026 22:45:47 +0100
Subject: [PATCH 23/23] fix(release): add docs/conf.py and docs/pyproject.toml
to bump and verify
---
scripts/release.sh | 45 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 44 insertions(+), 1 deletion(-)
diff --git a/scripts/release.sh b/scripts/release.sh
index ece2c901..6fc0d2c1 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -31,6 +31,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
SRC_DIR="${REPO_ROOT}/src"
VERSION_HPP="${SRC_DIR}/ros2_medkit_gateway/include/ros2_medkit_gateway/version.hpp"
+CONF_PY="${REPO_ROOT}/docs/conf.py"
+DOCS_PYPROJECT="${REPO_ROOT}/docs/pyproject.toml"
usage() {
echo "Usage: $0 {bump |verify []}"
@@ -90,8 +92,23 @@ cmd_bump() {
echo " WARNING: version.hpp not found at ${VERSION_HPP}"
fi
+ # Update docs/conf.py version and release
+ if [ -f "$CONF_PY" ]; then
+ local old_conf
+ old_conf=$(grep -oP '^version = "\K[0-9]+\.[0-9]+\.[0-9]+' "$CONF_PY" || echo "unknown")
+ sed -i "s|^version = \"[0-9]\+\.[0-9]\+\.[0-9]\+\"|version = \"${target_version}\"|" "$CONF_PY"
+ sed -i "s|^release = \"[0-9]\+\.[0-9]\+\.[0-9]\+\"|release = \"${target_version}\"|" "$CONF_PY"
+ echo " docs/conf.py: ${old_conf} -> ${target_version}"
+ fi
+
+ # Update docs/pyproject.toml version
+ if [ -f "$DOCS_PYPROJECT" ]; then
+ sed -i "s|^version = \"[0-9]\+\.[0-9]\+\.[0-9]\+\"|version = \"${target_version}\"|" "$DOCS_PYPROJECT"
+ echo " docs/pyproject.toml: -> ${target_version}"
+ fi
+
echo ""
- echo "Bumped ${count} packages + version.hpp to ${target_version}."
+ echo "Bumped ${count} packages + version.hpp + docs to ${target_version}."
echo ""
echo "Run '$0 verify ${target_version}' to confirm."
}
@@ -134,6 +151,32 @@ cmd_verify() {
versions_seen+=("$hpp_version")
fi
+ # Check docs/conf.py
+ if [ -f "$CONF_PY" ]; then
+ local conf_version
+ conf_version=$(grep -oP '^version = "\K[0-9]+\.[0-9]+\.[0-9]+' "$CONF_PY" || echo "unknown")
+ if [ -n "$expected_version" ] && [ "$conf_version" != "$expected_version" ]; then
+ echo " MISMATCH: docs/conf.py is ${conf_version}, expected ${expected_version}"
+ all_ok=false
+ else
+ echo " OK: docs/conf.py = ${conf_version}"
+ fi
+ versions_seen+=("$conf_version")
+ fi
+
+ # Check docs/pyproject.toml
+ if [ -f "$DOCS_PYPROJECT" ]; then
+ local pyproject_version
+ pyproject_version=$(grep -oP '^version = "\K[0-9]+\.[0-9]+\.[0-9]+' "$DOCS_PYPROJECT" || echo "unknown")
+ if [ -n "$expected_version" ] && [ "$pyproject_version" != "$expected_version" ]; then
+ echo " MISMATCH: docs/pyproject.toml is ${pyproject_version}, expected ${expected_version}"
+ all_ok=false
+ else
+ echo " OK: docs/pyproject.toml = ${pyproject_version}"
+ fi
+ versions_seen+=("$pyproject_version")
+ fi
+
# Check consistency if no expected version given
if [ -z "$expected_version" ]; then
local unique_versions