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