diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 238ff0935..c60d239d4 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -4,7 +4,7 @@
**ros2_medkit** is a ROS 2 diagnostics gateway that exposes ROS 2 system information via a RESTful HTTP API. It models robots as a **diagnostic entity tree** (Area → Component → Function → App) aligned with the SOVD (Service-Oriented Vehicle Diagnostics) specification.
-**Tech Stack**: C++17, ROS 2 Jazzy, Ubuntu 24.04
+**Tech Stack**: C++17, ROS 2 Jazzy/Humble/Rolling, Ubuntu 24.04/22.04
## Architecture
@@ -25,7 +25,7 @@ GatewayNode (src/ros2_medkit_gateway/src/gateway_node.cpp)
## Code Style & Conventions
- **C++ Standard**: C++17 with `-Wall -Wextra -Wpedantic -Wshadow -Wconversion`
-- **ROS 2 Distribution**: Jazzy (Ubuntu 24.04)
+- **ROS 2 Distribution**: Jazzy (Ubuntu 24.04), Humble (Ubuntu 22.04), or Rolling (Ubuntu 24.04, experimental)
- **Formatting**: Google-based clang-format with 120 column limit, 2-space indent
- **Pointer style**: Middle alignment (`Type * ptr`) per ROS 2 conventions
- **Namespace**: All gateway code lives in `ros2_medkit_gateway` namespace
@@ -65,7 +65,7 @@ When adding a new endpoint or feature:
## Key Dependencies
-- **ROS 2 Jazzy** (Ubuntu 24.04)
+- **ROS 2 Jazzy** (Ubuntu 24.04) / **ROS 2 Humble** (Ubuntu 22.04) / **ROS 2 Rolling** (Ubuntu 24.04, best-effort)
- **cpp-httplib**: HTTP server (found via pkg-config)
- **nlohmann_json**: JSON serialization
- **yaml-cpp**: Configuration parsing
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b4a33c4fb..1a07cca6e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,8 +9,19 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - ros_distro: jazzy
+ os_image: ubuntu:noble
+ - ros_distro: humble
+ os_image: ubuntu:jammy
+ - ros_distro: rolling
+ os_image: ubuntu:noble
+ continue-on-error: ${{ matrix.ros_distro == 'rolling' }}
container:
- image: ubuntu:noble
+ image: ${{ matrix.os_image }}
timeout-minutes: 60
defaults:
run:
@@ -25,29 +36,53 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- - name: Set up ROS 2 Jazzy
+ - name: Set up ROS 2 ${{ matrix.ros_distro }}
uses: ros-tooling/setup-ros@v0.7
with:
- required-ros-distributions: jazzy
+ required-ros-distributions: ${{ matrix.ros_distro }}
+
+ - name: Install cpp-httplib from source (Humble)
+ if: matrix.ros_distro == 'humble'
+ run: |
+ apt-get update
+ apt-get install -y cmake g++ libssl-dev pkg-config
+ git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib
+ cd /tmp/cpp-httplib
+ mkdir build && cd build
+ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON
+ make install
+ # Verify installation — cpp-httplib from source installs cmake config (not pkg-config .pc)
+ test -f /usr/include/httplib.h && echo "cpp-httplib installed successfully" || exit 1
- name: Install dependencies
run: |
apt-get update
- apt-get install -y clang-format clang-tidy ros-jazzy-test-msgs
- source /opt/ros/jazzy/setup.bash
+ apt-get install -y ros-${{ matrix.ros_distro }}-test-msgs
+ # Linter tools only needed on Jazzy (clang versions differ across Ubuntu releases)
+ if [ "${{ matrix.ros_distro }}" = "jazzy" ]; then
+ apt-get install -y clang-format clang-tidy
+ fi
+ source /opt/ros/${{ matrix.ros_distro }}/setup.bash
rosdep update
- rosdep install --from-paths src --ignore-src -r -y
+ # On Humble, skip the libcpp-httplib-dev rosdep key — the apt version (0.10.3)
+ # is too old; cpp-httplib v0.14.3 is built from source in an earlier step.
+ if [ "${{ matrix.ros_distro }}" = "humble" ]; then
+ rosdep install --from-paths src --ignore-src -r -y --skip-keys="libcpp-httplib-dev"
+ else
+ rosdep install --from-paths src --ignore-src -y
+ fi
- name: Build packages
run: |
- source /opt/ros/jazzy/setup.bash
+ source /opt/ros/${{ matrix.ros_distro }}/setup.bash
colcon build --symlink-install \
--cmake-args -DCMAKE_BUILD_TYPE=Release \
--event-handlers console_direct+
- name: Run linters (clang-format, clang-tidy, etc.)
+ if: matrix.ros_distro == 'jazzy'
run: |
- source /opt/ros/jazzy/setup.bash
+ source /opt/ros/${{ matrix.ros_distro }}/setup.bash
source install/setup.bash
colcon test --return-code-on-test-failure \
--ctest-args -L linter \
@@ -56,7 +91,7 @@ jobs:
- name: Run unit and integration tests
timeout-minutes: 15
run: |
- source /opt/ros/jazzy/setup.bash
+ source /opt/ros/${{ matrix.ros_distro }}/setup.bash
source install/setup.bash
colcon test --return-code-on-test-failure \
--ctest-args -LE linter \
@@ -70,7 +105,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
- name: test-results
+ name: test-results-${{ matrix.ros_distro }}
path: |
log/
build/*/test_results/
diff --git a/QUALITY_DECLARATION.md b/QUALITY_DECLARATION.md
index 54d96268d..1ad573aa2 100644
--- a/QUALITY_DECLARATION.md
+++ b/QUALITY_DECLARATION.md
@@ -86,7 +86,7 @@ GitHub Copilot code review is used in addition to human review.
All pull requests must pass CI before merging:
-- **Build & Test job:** Full build + linter tests + unit/integration tests on Ubuntu Noble / ROS 2 Jazzy
+- **Build & Test job:** Full build + unit/integration tests on Ubuntu Noble / ROS 2 Jazzy, Ubuntu Jammy / ROS 2 Humble, and Ubuntu Noble / ROS 2 Rolling (best-effort, allow-failure). Linter tests on Jazzy only
- **Coverage job:** Debug build with coverage. Reports are generated for all PRs as artifacts and uploaded to [Codecov](https://codecov.io/gh/selfpatch/ros2_medkit) on pushes to `main`
- Linting enforced: `clang-format`, `clang-tidy` via `ament_lint_auto`
@@ -201,9 +201,11 @@ Linter tests are enforced in CI on every pull request.
`ros2_medkit` is tested and supported on:
-- **Ubuntu 24.04 (Noble)** with **ROS 2 Jazzy**
+- **Ubuntu 24.04 (Noble)** with **ROS 2 Jazzy** (primary)
+- **Ubuntu 22.04 (Jammy)** with **ROS 2 Humble**
+- **Ubuntu 24.04 (Noble)** with **ROS 2 Rolling** (experimental, best-effort)
-This is the primary Tier 1 platform for ROS 2 Jazzy per [REP-2000](https://www.ros.org/reps/rep-2000.html).
+Jazzy and Humble are the Tier 1 platforms per [REP-2000](https://www.ros.org/reps/rep-2000.html) and are tested in CI. Rolling is tested as best-effort (allow-failure) for forward-compatibility.
---
@@ -232,7 +234,7 @@ Security issues can be reported via GitHub Security Advisories on the
| Feature tests | Met | 65 tests across unit + integration |
| Coverage | Met | 75% line coverage |
| Linting | Met | clang-format, clang-tidy, ament_lint |
-| Platform support | Met | Ubuntu Noble / ROS 2 Jazzy |
+| Platform support | Met | Ubuntu Noble / ROS 2 Jazzy + Ubuntu Jammy / ROS 2 Humble + Rolling (best-effort) |
| Security policy | Met | REP-2006 compliant |
**Caveat:** Version is 0.2.0 (pre-1.0.0, requirement 1.ii). The REST API is versioned (`/api/v1/`)
diff --git a/README.md b/README.md
index 3b4bad549..be3bf1dbd 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,9 @@
[](https://codecov.io/gh/selfpatch/ros2_medkit)
[](https://selfpatch.github.io/ros2_medkit/)
[](LICENSE)
-[](https://docs.ros.org/en/jazzy/)
+[](https://docs.ros.org/en/jazzy/)
+[](https://docs.ros.org/en/humble/)
+[](https://docs.ros.org/en/rolling/)
[](https://discord.gg/6CXPMApAyq)
[](QUALITY_DECLARATION.md)
@@ -32,7 +34,7 @@ cd selfpatch_demos/demos/turtlebot3_integration
# → API: http://localhost:8080/api/v1/ Web UI: http://localhost:3000
```
-**Build from source** (ROS 2 Jazzy):
+**Build from source** (ROS 2 Jazzy, Humble, or Rolling):
```bash
git clone --recurse-submodules https://github.com/selfpatch/ros2_medkit.git
@@ -52,7 +54,7 @@ For more examples, see our [Postman collection](postman/).
- **🔗 SOVD Compatible** — Align with Service-Oriented Vehicle Diagnostics standards
- **🌐 REST API Gateway** — HTTP interface for integration with external tools and UIs
- **📊 Health Modeling** — Track health state per entity for fleet-level observability
-- **🔧 Easy Integration** — Works with existing ROS 2 Jazzy nodes out of the box
+- **🔧 Easy Integration** — Works with existing ROS 2 nodes out of the box (Jazzy, Humble & Rolling)
## 📖 Overview
@@ -69,9 +71,9 @@ Compatible with the **SOVD (Service-Oriented Vehicle Diagnostics)** model — sa
## 📋 Requirements
-- **OS:** Ubuntu 24.04 LTS
-- **ROS 2:** Jazzy Jalisco
-- **Compiler:** GCC 13+ (C++17 support)
+- **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
@@ -192,14 +194,15 @@ 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 runs on Ubuntu 24.04 with ROS 2 Jazzy and consists of two parallel jobs:
+The CI workflow runs a build matrix across **ROS 2 Jazzy** (Ubuntu 24.04), **ROS 2 Humble** (Ubuntu 22.04), and **ROS 2 Rolling** (Ubuntu 24.04, allow-failure) and consists of the following jobs:
-**build-and-test:**
+**build-and-test** (matrix: Jazzy + Humble + Rolling):
-- Code linting and formatting checks (clang-format, clang-tidy)
-- Unit tests and integration tests with demo automotive nodes
+- Full build and unit/integration tests on all distros
+- Rolling jobs are allowed to fail (best-effort forward-compatibility)
+- Code linting and formatting checks (clang-format, clang-tidy) — Jazzy only
-**coverage:**
+**coverage** (Jazzy only):
- Builds with coverage instrumentation (Debug mode)
- Runs unit tests only (for stable coverage metrics)
diff --git a/cmake/ROS2MedkitCompat.cmake b/cmake/ROS2MedkitCompat.cmake
new file mode 100644
index 000000000..30be2198f
--- /dev/null
+++ b/cmake/ROS2MedkitCompat.cmake
@@ -0,0 +1,143 @@
+# 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.
+
+# =============================================================================
+# ROS2MedkitCompat.cmake — Multi-distro compatibility module for ros2_medkit
+# =============================================================================
+#
+# Centralizes all dependency resolution workarounds for supporting multiple
+# ROS 2 distributions (Humble, Jazzy, Rolling) in a single place.
+#
+# Usage (in each package's CMakeLists.txt, after find_package(ament_cmake)):
+# list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake")
+# include(ROS2MedkitCompat)
+#
+# Macros provided:
+# medkit_find_yaml_cpp() — Find yaml-cpp, ensure yaml-cpp::yaml-cpp target
+# medkit_find_cpp_httplib() — Find cpp-httplib, create cpp_httplib_target alias
+# medkit_detect_compat_defs() — Detect rclcpp/rosbag2 versions, set compat variables
+# medkit_apply_compat_defs(target) — Apply compile definitions to a target
+#
+# Variables set by medkit_detect_compat_defs():
+# MEDKIT_RCLCPP_VERSION_MAJOR — integer (e.g., 16 for Humble, 28 for Jazzy)
+# MEDKIT_ROSBAG2_OLD_TIMESTAMP — ON if rosbag2_storage < 0.22.0 (Humble)
+#
+
+include_guard(GLOBAL)
+
+# ---------------------------------------------------------------------------
+# medkit_find_yaml_cpp()
+# ---------------------------------------------------------------------------
+# Jazzy's yaml_cpp_vendor exports a namespaced yaml-cpp::yaml-cpp cmake target.
+# Humble's yaml_cpp_vendor bundles yaml-cpp but does NOT export the cmake target.
+# This macro creates an IMPORTED INTERFACE target when find_package doesn't.
+#
+# Prerequisite: find_package(yaml_cpp_vendor REQUIRED) must be called before.
+# ---------------------------------------------------------------------------
+macro(medkit_find_yaml_cpp)
+ find_package(yaml-cpp QUIET)
+ if(NOT TARGET yaml-cpp::yaml-cpp)
+ find_library(_MEDKIT_YAML_CPP_LIB yaml-cpp)
+ find_path(_MEDKIT_YAML_CPP_INCLUDE yaml-cpp/yaml.h)
+ if(_MEDKIT_YAML_CPP_LIB AND _MEDKIT_YAML_CPP_INCLUDE)
+ add_library(yaml-cpp::yaml-cpp IMPORTED INTERFACE)
+ set_target_properties(yaml-cpp::yaml-cpp PROPERTIES
+ INTERFACE_LINK_LIBRARIES "${_MEDKIT_YAML_CPP_LIB}"
+ INTERFACE_INCLUDE_DIRECTORIES "${_MEDKIT_YAML_CPP_INCLUDE}"
+ )
+ message(STATUS "[MedkitCompat] yaml-cpp: created target from system library (${_MEDKIT_YAML_CPP_LIB})")
+ else()
+ message(FATAL_ERROR "[MedkitCompat] Could not find yaml-cpp library. "
+ "Ensure yaml_cpp_vendor is installed: apt install ros-${ROS_DISTRO}-yaml-cpp-vendor")
+ endif()
+ unset(_MEDKIT_YAML_CPP_LIB)
+ unset(_MEDKIT_YAML_CPP_INCLUDE)
+ else()
+ message(STATUS "[MedkitCompat] yaml-cpp: using native cmake target")
+ endif()
+endmacro()
+
+# ---------------------------------------------------------------------------
+# medkit_find_cpp_httplib()
+# ---------------------------------------------------------------------------
+# On Jazzy/Noble, libcpp-httplib-dev is available via apt and provides a
+# pkg-config .pc file. On Humble/Jammy, cpp-httplib must be built from
+# source, which installs a CMake config file (httplibConfig.cmake).
+#
+# Creates a unified alias target `cpp_httplib_target` for consumers.
+# ---------------------------------------------------------------------------
+macro(medkit_find_cpp_httplib)
+ find_package(PkgConfig QUIET)
+ if(PkgConfig_FOUND)
+ pkg_check_modules(cpp_httplib IMPORTED_TARGET cpp-httplib)
+ endif()
+ if(cpp_httplib_FOUND)
+ add_library(cpp_httplib_target ALIAS PkgConfig::cpp_httplib)
+ message(STATUS "[MedkitCompat] cpp-httplib: using pkg-config (system package)")
+ else()
+ find_package(httplib REQUIRED)
+ add_library(cpp_httplib_target ALIAS httplib::httplib)
+ message(STATUS "[MedkitCompat] cpp-httplib: using cmake config (source build)")
+ endif()
+endmacro()
+
+# ---------------------------------------------------------------------------
+# medkit_detect_compat_defs()
+# ---------------------------------------------------------------------------
+# Detects rclcpp and rosbag2 versions to set compatibility variables.
+# Call AFTER find_package(rclcpp) and optionally find_package(rosbag2_storage).
+#
+# Sets:
+# MEDKIT_RCLCPP_VERSION_MAJOR — integer (16=Humble, 21+=Iron, 28+=Jazzy)
+# MEDKIT_ROSBAG2_OLD_TIMESTAMP — ON if rosbag2_storage < 0.22.0 (Humble)
+# ---------------------------------------------------------------------------
+macro(medkit_detect_compat_defs)
+ # --- rclcpp version ---
+ if(rclcpp_VERSION)
+ string(REGEX MATCH "^([0-9]+)" _medkit_rclcpp_major "${rclcpp_VERSION}")
+ set(MEDKIT_RCLCPP_VERSION_MAJOR ${_medkit_rclcpp_major})
+ unset(_medkit_rclcpp_major)
+ else()
+ set(MEDKIT_RCLCPP_VERSION_MAJOR 0)
+ endif()
+
+ if(MEDKIT_RCLCPP_VERSION_MAJOR GREATER_EQUAL 21)
+ message(STATUS "[MedkitCompat] rclcpp ${rclcpp_VERSION} (Iron+): native GenericClient + BestAvailable QoS")
+ else()
+ message(STATUS "[MedkitCompat] rclcpp ${rclcpp_VERSION} (Humble): using compat shim for GenericClient")
+ endif()
+
+ # --- rosbag2 timestamp API ---
+ if(rosbag2_storage_VERSION)
+ if(rosbag2_storage_VERSION VERSION_LESS "0.22.0")
+ set(MEDKIT_ROSBAG2_OLD_TIMESTAMP ON)
+ message(STATUS "[MedkitCompat] rosbag2_storage ${rosbag2_storage_VERSION}: using time_stamp field (Humble)")
+ else()
+ set(MEDKIT_ROSBAG2_OLD_TIMESTAMP OFF)
+ message(STATUS "[MedkitCompat] rosbag2_storage ${rosbag2_storage_VERSION}: using recv_timestamp (Iron+)")
+ endif()
+ endif()
+endmacro()
+
+# ---------------------------------------------------------------------------
+# medkit_apply_compat_defs(target)
+# ---------------------------------------------------------------------------
+# Applies all relevant compile definitions to a target based on detected
+# compatibility state. Call AFTER medkit_detect_compat_defs().
+# ---------------------------------------------------------------------------
+function(medkit_apply_compat_defs target)
+ if(MEDKIT_ROSBAG2_OLD_TIMESTAMP)
+ target_compile_definitions(${target} PRIVATE ROSBAG2_USE_OLD_TIMESTAMP_FIELD)
+ endif()
+endfunction()
diff --git a/docs/installation.rst b/docs/installation.rst
index a97b63e57..f994bd02a 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -1,7 +1,8 @@
Installation
============
-This guide covers installation of ros2_medkit on Ubuntu 24.04 with ROS 2 Jazzy.
+This guide covers installation of ros2_medkit on Ubuntu 24.04 with ROS 2 Jazzy,
+Ubuntu 22.04 with ROS 2 Humble, or Ubuntu 24.04 with ROS 2 Rolling.
System Requirements
-------------------
@@ -13,21 +14,38 @@ System Requirements
* - Requirement
- Version
* - Operating System
- - Ubuntu 24.04 LTS (Noble Numbat)
+ - Ubuntu 24.04 LTS (Noble) or Ubuntu 22.04 LTS (Jammy)
* - ROS 2 Distribution
- - Jazzy
+ - Jazzy, Humble, or Rolling
* - C++ Compiler
- - GCC 13+ (C++17 support required)
+ - GCC 11+ (C++17 support required)
* - CMake
- 3.22+
* - Python
- - 3.12+
+ - 3.10+ (Humble) / 3.12+ (Jazzy / Rolling)
Prerequisites
-------------
-**ROS 2 Jazzy** must be installed and sourced. Follow the official installation guide:
-https://docs.ros.org/en/jazzy/Installation/Ubuntu-Install-Debs.html
+**ROS 2 Jazzy, Humble, or Rolling** must be installed and sourced. Follow the official installation guide
+for your distribution:
+
+- Jazzy: https://docs.ros.org/en/jazzy/Installation/Ubuntu-Install-Debs.html
+- Humble: https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debs.html
+- Rolling: https://docs.ros.org/en/rolling/Installation/Ubuntu-Install-Debs.html
+
+.. note::
+
+ On Ubuntu 22.04 (Humble), the ``libcpp-httplib-dev`` system package is not available.
+ You must install cpp-httplib from source before building:
+
+ .. code-block:: bash
+
+ sudo apt install cmake g++ libssl-dev
+ git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib
+ cd /tmp/cpp-httplib && mkdir build && cd build
+ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON
+ sudo make install
Installation from Source
------------------------
diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst
index 3f0b186b5..652c539ac 100644
--- a/docs/troubleshooting.rst
+++ b/docs/troubleshooting.rst
@@ -214,7 +214,7 @@ parameter changes) only happen when explicitly requested via the API.
**Q: Can I use ros2_medkit with ROS 1?**
-No. ros2_medkit requires ROS 2 Jazzy. For ROS 1 systems, consider using
+No. ros2_medkit requires ROS 2 (Jazzy, Humble, or Rolling). For ROS 1 systems, consider using
the ``ros1_bridge`` and running ros2_medkit on the ROS 2 side.
**Q: Is ros2_medkit production-ready?**
diff --git a/src/ros2_medkit_diagnostic_bridge/src/diagnostic_bridge_node.cpp b/src/ros2_medkit_diagnostic_bridge/src/diagnostic_bridge_node.cpp
index 46c0eed5c..c761a3cef 100644
--- a/src/ros2_medkit_diagnostic_bridge/src/diagnostic_bridge_node.cpp
+++ b/src/ros2_medkit_diagnostic_bridge/src/diagnostic_bridge_node.cpp
@@ -99,8 +99,10 @@ std::string DiagnosticBridgeNode::map_to_fault_code(const std::string & diagnost
}
// Log warning and return empty string if no mapping and auto-generate disabled
- RCLCPP_WARN_THROTTLE(get_logger(), *get_clock(), 5000,
- "No mapping for diagnostic '%s' and auto_generate_codes is disabled", diagnostic_name.c_str());
+ // Use a mutable clock copy — Humble's RCLCPP_WARN_THROTTLE requires non-const Clock
+ rclcpp::Clock clock(*get_clock());
+ RCLCPP_WARN_THROTTLE(get_logger(), clock, 5000, "No mapping for diagnostic '%s' and auto_generate_codes is disabled",
+ diagnostic_name.c_str());
return "";
}
diff --git a/src/ros2_medkit_fault_manager/CMakeLists.txt b/src/ros2_medkit_fault_manager/CMakeLists.txt
index 175de97b8..0aeb42743 100644
--- a/src/ros2_medkit_fault_manager/CMakeLists.txt
+++ b/src/ros2_medkit_fault_manager/CMakeLists.txt
@@ -19,6 +19,10 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+# Multi-distro compatibility (Humble / Jazzy / Rolling)
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake")
+include(ROS2MedkitCompat)
+
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion)
endif()
@@ -38,11 +42,13 @@ find_package(ros2_medkit_serialization REQUIRED)
find_package(SQLite3 REQUIRED)
find_package(nlohmann_json REQUIRED)
# yaml-cpp is required as transitive dependency from ros2_medkit_serialization
-find_package(yaml-cpp REQUIRED)
+medkit_find_yaml_cpp()
# rosbag2 for time-window snapshot recording
find_package(rosbag2_cpp REQUIRED)
find_package(rosbag2_storage REQUIRED)
+medkit_detect_compat_defs()
+
# Library target (shared between executable and tests)
add_library(fault_manager_lib STATIC
src/fault_manager_node.cpp
@@ -75,6 +81,8 @@ target_link_libraries(fault_manager_lib PUBLIC
yaml-cpp::yaml-cpp
)
+medkit_apply_compat_defs(fault_manager_lib)
+
if(ENABLE_COVERAGE)
target_compile_options(fault_manager_lib PRIVATE --coverage -O0 -g)
target_link_options(fault_manager_lib PRIVATE --coverage)
diff --git a/src/ros2_medkit_fault_manager/package.xml b/src/ros2_medkit_fault_manager/package.xml
index e2025d5c6..bf8a6dba7 100644
--- a/src/ros2_medkit_fault_manager/package.xml
+++ b/src/ros2_medkit_fault_manager/package.xml
@@ -27,6 +27,7 @@
launch_testing_ros
sensor_msgs
std_msgs
+ rosbag2_storage_mcap
ament_cmake
diff --git a/src/ros2_medkit_fault_manager/src/rosbag_capture.cpp b/src/ros2_medkit_fault_manager/src/rosbag_capture.cpp
index 5f9801682..3d42768fa 100644
--- a/src/ros2_medkit_fault_manager/src/rosbag_capture.cpp
+++ b/src/ros2_medkit_fault_manager/src/rosbag_capture.cpp
@@ -53,8 +53,13 @@ std::shared_ptr create_bag_message(const
const rclcpp::Logger & logger) {
auto bag_msg = std::make_shared();
bag_msg->topic_name = topic;
+ // rosbag2 API changed in Iron: Humble uses time_stamp, Iron+ uses recv_timestamp/send_timestamp
+#ifdef ROSBAG2_USE_OLD_TIMESTAMP_FIELD
+ bag_msg->time_stamp = timestamp_ns;
+#else
bag_msg->recv_timestamp = timestamp_ns;
bag_msg->send_timestamp = timestamp_ns;
+#endif
// Create serialized_data with custom deleter that calls rcutils_uint8_array_fini
auto serialized_data = std::shared_ptr(new rcutils_uint8_array_t(), Uint8ArrayDeleter{});
@@ -361,8 +366,8 @@ void RosbagCapture::message_callback(const std::string & topic, const std::strin
}
// Memory is automatically cleaned up by RAII when bag_msg goes out of scope
} catch (const std::exception & e) {
- RCLCPP_WARN_THROTTLE(node_->get_logger(), *node_->get_clock(), 1000, "Failed to write post-fault message: %s",
- e.what());
+ rclcpp::Clock clock(*node_->get_clock());
+ RCLCPP_WARN_THROTTLE(node_->get_logger(), clock, 1000, "Failed to write post-fault message: %s", e.what());
}
}
return; // Don't buffer during post-fault recording
diff --git a/src/ros2_medkit_fault_reporter/include/ros2_medkit_fault_reporter/fault_reporter.hpp b/src/ros2_medkit_fault_reporter/include/ros2_medkit_fault_reporter/fault_reporter.hpp
index fb89a6a5a..9e1696696 100644
--- a/src/ros2_medkit_fault_reporter/include/ros2_medkit_fault_reporter/fault_reporter.hpp
+++ b/src/ros2_medkit_fault_reporter/include/ros2_medkit_fault_reporter/fault_reporter.hpp
@@ -83,13 +83,12 @@ class FaultReporter {
private:
/// Load filter configuration from ROS parameters
- void load_parameters();
+ void load_parameters(const rclcpp::Node::SharedPtr & node);
/// Send the fault report to FaultManager (async, fire-and-forget)
void send_report(const std::string & fault_code, uint8_t event_type, uint8_t severity,
const std::string & description);
- rclcpp::Node::SharedPtr node_;
std::string source_id_;
rclcpp::Client::SharedPtr client_;
LocalFilter filter_;
diff --git a/src/ros2_medkit_fault_reporter/src/fault_reporter.cpp b/src/ros2_medkit_fault_reporter/src/fault_reporter.cpp
index 3e12c472a..a84b97cb8 100644
--- a/src/ros2_medkit_fault_reporter/src/fault_reporter.cpp
+++ b/src/ros2_medkit_fault_reporter/src/fault_reporter.cpp
@@ -18,45 +18,44 @@ namespace ros2_medkit_fault_reporter {
FaultReporter::FaultReporter(const rclcpp::Node::SharedPtr & node, const std::string & source_id,
const std::string & service_name)
- : node_(node), source_id_(source_id), logger_(node->get_logger()) {
+ : source_id_(source_id), logger_(node->get_logger()) {
// Validate source_id
if (source_id_.empty()) {
RCLCPP_WARN(logger_, "FaultReporter created with empty source_id, fault origins will be difficult to trace");
}
- // Create service client
- client_ = node_->create_client(service_name);
+ // Create service client (stored as shared_ptr, independent of node lifetime)
+ client_ = node->create_client(service_name);
- // Load configuration from parameters
- load_parameters();
+ // Load configuration from parameters (node only needed during construction)
+ load_parameters(node);
RCLCPP_DEBUG(logger_, "FaultReporter initialized for source: %s", source_id_.c_str());
}
-void FaultReporter::load_parameters() {
+void FaultReporter::load_parameters(const rclcpp::Node::SharedPtr & node) {
FilterConfig config;
// Declare parameters with defaults if not already declared
- if (!node_->has_parameter("fault_reporter.local_filtering.enabled")) {
- node_->declare_parameter("fault_reporter.local_filtering.enabled", config.enabled);
+ if (!node->has_parameter("fault_reporter.local_filtering.enabled")) {
+ node->declare_parameter("fault_reporter.local_filtering.enabled", config.enabled);
}
- if (!node_->has_parameter("fault_reporter.local_filtering.default_threshold")) {
- node_->declare_parameter("fault_reporter.local_filtering.default_threshold", config.default_threshold);
+ if (!node->has_parameter("fault_reporter.local_filtering.default_threshold")) {
+ node->declare_parameter("fault_reporter.local_filtering.default_threshold", config.default_threshold);
}
- if (!node_->has_parameter("fault_reporter.local_filtering.default_window_sec")) {
- node_->declare_parameter("fault_reporter.local_filtering.default_window_sec", config.default_window_sec);
+ if (!node->has_parameter("fault_reporter.local_filtering.default_window_sec")) {
+ node->declare_parameter("fault_reporter.local_filtering.default_window_sec", config.default_window_sec);
}
- if (!node_->has_parameter("fault_reporter.local_filtering.bypass_severity")) {
- node_->declare_parameter("fault_reporter.local_filtering.bypass_severity",
- static_cast(config.bypass_severity));
+ if (!node->has_parameter("fault_reporter.local_filtering.bypass_severity")) {
+ node->declare_parameter("fault_reporter.local_filtering.bypass_severity", static_cast(config.bypass_severity));
}
- config.enabled = node_->get_parameter("fault_reporter.local_filtering.enabled").as_bool();
+ config.enabled = node->get_parameter("fault_reporter.local_filtering.enabled").as_bool();
config.default_threshold =
- static_cast(node_->get_parameter("fault_reporter.local_filtering.default_threshold").as_int());
- config.default_window_sec = node_->get_parameter("fault_reporter.local_filtering.default_window_sec").as_double();
+ static_cast(node->get_parameter("fault_reporter.local_filtering.default_threshold").as_int());
+ config.default_window_sec = node->get_parameter("fault_reporter.local_filtering.default_window_sec").as_double();
config.bypass_severity =
- static_cast(node_->get_parameter("fault_reporter.local_filtering.bypass_severity").as_int());
+ static_cast(node->get_parameter("fault_reporter.local_filtering.bypass_severity").as_int());
filter_.set_config(config);
diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt
index bd03fde27..e731bc2a0 100644
--- a/src/ros2_medkit_gateway/CMakeLists.txt
+++ b/src/ros2_medkit_gateway/CMakeLists.txt
@@ -5,6 +5,10 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+# Multi-distro compatibility (Humble / Jazzy / Rolling)
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake")
+include(ROS2MedkitCompat)
+
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion)
endif()
@@ -26,7 +30,7 @@ find_package(sensor_msgs REQUIRED)
find_package(rcl_interfaces REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(yaml_cpp_vendor REQUIRED)
-find_package(yaml-cpp REQUIRED)
+medkit_find_yaml_cpp()
find_package(ament_index_cpp REQUIRED)
find_package(action_msgs REQUIRED)
find_package(rclcpp_action REQUIRED)
@@ -34,9 +38,12 @@ find_package(example_interfaces REQUIRED)
find_package(ros2_medkit_msgs REQUIRED)
find_package(ros2_medkit_serialization REQUIRED)
-# Find cpp-httplib using pkg-config
-find_package(PkgConfig REQUIRED)
-pkg_check_modules(cpp_httplib REQUIRED IMPORTED_TARGET cpp-httplib)
+# GenericClient compat shim dependencies (needed explicitly for Humble,
+# transitively available on Iron+/Jazzy but harmless to declare unconditionally)
+find_package(rosidl_typesupport_cpp REQUIRED)
+find_package(rosidl_typesupport_introspection_cpp REQUIRED)
+
+medkit_find_cpp_httplib()
# Find OpenSSL (required by jwt-cpp for RS256 and optional TLS support)
find_package(OpenSSL REQUIRED)
@@ -129,10 +136,12 @@ ament_target_dependencies(gateway_lib
action_msgs
ros2_medkit_msgs
ros2_medkit_serialization
+ rosidl_typesupport_cpp
+ rosidl_typesupport_introspection_cpp
)
target_link_libraries(gateway_lib
- PkgConfig::cpp_httplib
+ cpp_httplib_target
nlohmann_json::nlohmann_json
yaml-cpp::yaml-cpp
OpenSSL::SSL
@@ -241,6 +250,10 @@ if(BUILD_TESTING)
ament_add_gtest(test_auth_manager test/test_auth_manager.cpp)
target_link_libraries(test_auth_manager gateway_lib)
+ ament_add_gtest(test_generic_client_compat test/test_generic_client_compat.cpp)
+ target_link_libraries(test_generic_client_compat gateway_lib)
+ ament_target_dependencies(test_generic_client_compat rclcpp std_srvs)
+
ament_add_gtest(test_operation_manager test/test_operation_manager.cpp)
target_link_libraries(test_operation_manager gateway_lib)
@@ -329,6 +342,7 @@ if(BUILD_TESTING)
set(_test_targets
test_gateway_node
test_auth_manager
+ test_generic_client_compat
test_operation_manager
test_configuration_manager
test_native_topic_sampler
diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/compat/generic_client_compat.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/compat/generic_client_compat.hpp
new file mode 100644
index 000000000..fc018cc09
--- /dev/null
+++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/compat/generic_client_compat.hpp
@@ -0,0 +1,353 @@
+// 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.
+
+/// @file generic_client_compat.hpp
+/// @brief Provides GenericServiceClient — either rclcpp::GenericClient (Iron+) or a shim for Humble.
+///
+/// rclcpp::GenericClient was introduced in Iron (rclcpp ≥ 21.x). Humble (rclcpp 16.x)
+/// does not have it. This header defines HAS_GENERIC_CLIENT based on the rclcpp version
+/// and provides a compatible GenericServiceClient type for OperationManager to use.
+///
+/// When HAS_GENERIC_CLIENT is true (Iron/Jazzy), GenericServiceClient is simply
+/// rclcpp::GenericClient, and create_generic_service_client() delegates to
+/// Node::create_generic_client().
+///
+/// When HAS_GENERIC_CLIENT is false (Humble), GenericServiceClient is a custom class
+/// that replicates GenericClient's behavior using rcl C APIs and the same
+/// rosidl_typesupport_introspection infrastructure available in all distros.
+
+#pragma once
+
+#include
+
+// Detect whether rclcpp::GenericClient is available.
+// rclcpp/version.h exists in all supported distros (Humble, Jazzy, Rolling).
+#include
+
+#if defined(RCLCPP_VERSION_MAJOR) && RCLCPP_VERSION_MAJOR >= 21
+#define HAS_GENERIC_CLIENT 1
+#else
+#define HAS_GENERIC_CLIENT 0
+#endif
+
+#if HAS_GENERIC_CLIENT
+
+// ============================================================================
+// Iron / Jazzy path — just alias to rclcpp::GenericClient
+// ============================================================================
+
+#include
+
+namespace ros2_medkit_gateway {
+namespace compat {
+
+using GenericServiceClient = rclcpp::GenericClient;
+
+/// Create a GenericServiceClient (delegates to Node::create_generic_client)
+inline GenericServiceClient::SharedPtr
+create_generic_service_client(rclcpp::Node * node, const std::string & service_name, const std::string & service_type) {
+ return node->create_generic_client(service_name, service_type);
+}
+
+} // namespace compat
+} // namespace ros2_medkit_gateway
+
+#else // !HAS_GENERIC_CLIENT
+
+// ============================================================================
+// Humble compatibility shim — replicate GenericClient using rcl C APIs
+// ============================================================================
+//
+// On Humble (rclcpp 16.x), the following APIs do NOT exist:
+// - rclcpp::GenericClient
+// - rclcpp::get_service_typesupport_handle()
+// - rclcpp::get_message_typesupport_handle()
+//
+// The following APIs ARE available:
+// - rclcpp::get_typesupport_library() — loads the .so
+// - rclcpp::get_typesupport_handle() — extracts message TS from .so
+// - rclcpp::ClientBase — base class for service clients
+// - rcl_client_init / rcl_send_request — C-level service client API
+// - rosidl_typesupport_introspection_cpp — message introspection
+//
+// This shim manually loads the service typesupport symbol from the library
+// (the same way GenericClient does on Iron+, but with direct dlsym).
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include