From e6d3a7b0e8f45449100868f38205fe1956d49586 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Mon, 13 May 2024 14:44:05 +0200 Subject: [PATCH 01/34] only one distro per branch Signed-off-by: Christian Henkel --- .github/workflows/test.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1e07c0adc..ceb6ffb09 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,14 +21,9 @@ jobs: diagnostic_updater, self_test, ] - distro: [humble, iron, rolling] include: - distro: humble os: 22.04 - - distro: iron - os: 22.04 - - distro: rolling - os: 24.04 runs-on: ubuntu-latest container: ubuntu:${{ matrix.os }} steps: From 483f3b84c807365aec4c3e3137071a438e2398f0 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Mon, 13 May 2024 14:56:00 +0200 Subject: [PATCH 02/34] readme update (#343) Signed-off-by: Christian Henkel (cherry picked from commit e319448b9821b7fcdb5af0e07f762f61307500d7) --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 96c20ae59..6766d3186 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Test diagnostics](https://img.shields.io/github/actions/workflow/status/ros/diagnostics/test.yaml?label=test&style=flat-square)](https://github.com/ros/diagnostics/actions/workflows/test.yaml) [![Lint diagnostics](https://img.shields.io/github/actions/workflow/status/ros/diagnostics/lint.yaml?label=lint&style=flat-square)](https://github.com/ros/diagnostics/actions/workflows/lint.yaml) [![ROS2 Humble](https://img.shields.io/ros/v/humble/diagnostics.svg?style=flat-square)](https://index.ros.org/r/diagnostics/#humble) [![ROS2 Iron](https://img.shields.io/ros/v/iron/diagnostics.svg?style=flat-square)](https://index.ros.org/r/diagnostics/#iron) [![ROS2 Rolling](https://img.shields.io/ros/v/rolling/diagnostics.svg?style=flat-square)](https://index.ros.org/r/diagnostics/#rolling) +[![Test diagnostics](https://img.shields.io/github/actions/workflow/status/ros/diagnostics/test.yaml?label=test&style=flat-square)](https://github.com/ros/diagnostics/actions/workflows/test.yaml) [![Lint diagnostics](https://img.shields.io/github/actions/workflow/status/ros/diagnostics/lint.yaml?label=lint&style=flat-square)](https://github.com/ros/diagnostics/actions/workflows/lint.yaml) [![ROS2 Humble](https://img.shields.io/ros/v/humble/diagnostics.svg?style=flat-square)](https://index.ros.org/r/diagnostics/#humble) [![ROS2 Iron](https://img.shields.io/ros/v/iron/diagnostics.svg?style=flat-square)](https://index.ros.org/r/diagnostics/#iron) [![ROS2 Jazzy](https://img.shields.io/ros/v/jazzy/diagnostics.svg?style=flat-square)](https://index.ros.org/r/diagnostics/#jazzy) [![ROS2 Rolling](https://img.shields.io/ros/v/rolling/diagnostics.svg?style=flat-square)](https://index.ros.org/r/diagnostics/#rolling) # Overview @@ -34,15 +34,10 @@ Diagnostics messages that are not aggregated can be visualized by [`rqt_runtime_ # Target Distribution -The [`ros2` branch](https://github.com/ros/diagnostics/tree/ros2) targets - -- *Humble Hawksbill* -- *Iron Irwini* - -The [`ros2-jazzy` branch](https://github.com/ros/diagnostics/tree/ros2-jazzy) targets - -- *Jazzy Jalisco* -- *Rolling Ridley* +- **Rolling Ridley** by the [`ros2` branch](https://github.com/ros/diagnostics/tree/ros2) +- **Humble Hawksbill** by the [`ros2-humble` branch](https://github.com/ros/diagnostics/tree/ros2-humble) +- **Iron Irwini** by the [`ros2-iron` branch](https://github.com/ros/diagnostics/tree/ros2-iron) +- **Jazzy Jalisco** by the [`ros2-jazzy` branch](https://github.com/ros/diagnostics/tree/ros2-jazzy) # License From ab7af61e3cd90bf94a210d09287af9aab36d26ad Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Tue, 14 May 2024 11:28:13 +0200 Subject: [PATCH 03/34] Using ubuntu ntp server in systemtest (#346) (#347) * testing with ubuntu ntp server * message makes a little more sense with abs * bigger offsets allowed * organizing imports with isort profile `google` --------- Signed-off-by: Christian Henkel (cherry picked from commit c4f723161e95277a89d48ed4acc38b8a008368c3) --- .../diagnostic_common_diagnostics/ntp_monitor.py | 8 +++----- .../test/systemtest/test_ntp_monitor_launchtest.py | 13 ++++--------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ntp_monitor.py b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ntp_monitor.py index 9462cebb3..8671ddb5b 100755 --- a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ntp_monitor.py +++ b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ntp_monitor.py @@ -37,9 +37,7 @@ import threading import diagnostic_updater as DIAG - import ntplib - import rclpy from rclpy.node import Node @@ -145,16 +143,16 @@ def add_kv(stat_values, key, value): if (abs(measured_offset) > self.offset): st.level = DIAG.DiagnosticStatus.WARN st.message = \ - f'NTP offset above threshold: {measured_offset}>'\ + f'NTP offset above threshold: abs({measured_offset})>'\ f'{self.offset} us' if (abs(measured_offset) > self.error_offset): st.level = DIAG.DiagnosticStatus.ERROR st.message = \ - f'NTP offset above error threshold: {measured_offset}>'\ + f'NTP offset above error threshold: abs({measured_offset})>'\ f'{self.error_offset} us' if (abs(measured_offset) < self.offset): st.level = DIAG.DiagnosticStatus.OK - st.message = f'NTP Offset OK: {measured_offset} us' + st.message = f'NTP Offset OK: abs({measured_offset}) us' return st diff --git a/diagnostic_common_diagnostics/test/systemtest/test_ntp_monitor_launchtest.py b/diagnostic_common_diagnostics/test/systemtest/test_ntp_monitor_launchtest.py index 495c9b684..ede17b0f4 100644 --- a/diagnostic_common_diagnostics/test/systemtest/test_ntp_monitor_launchtest.py +++ b/diagnostic_common_diagnostics/test/systemtest/test_ntp_monitor_launchtest.py @@ -35,17 +35,11 @@ import unittest from diagnostic_msgs.msg import DiagnosticArray - import launch - import launch_ros - import launch_testing - from launch_testing_ros import WaitForTopics - import pytest - import rclpy @@ -58,9 +52,10 @@ def generate_test_description(): executable='ntp_monitor.py', name='ntp_monitor', output='screen', - arguments=['--offset-tolerance', '10000', - '--error-offset-tolerance', '20000'] - # 10s, 20s, we are not testing if your clock is correct + arguments=['--offset-tolerance', '100000', + '--error-offset-tolerance', '200000', + '--ntp_hostname', 'ntp.ubuntu.com'] + # 100s, 200s, we are not testing if your clock is correct ), launch_testing.actions.ReadyToTest() ]) From 294e5a4ae04f42d865fa42435267bd9b1e78b43d Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Tue, 14 May 2024 11:30:24 +0200 Subject: [PATCH 04/34] Update test.yaml --- .github/workflows/test.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ceb6ffb09..e0b8d81cc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -3,7 +3,7 @@ on: pull_request: push: branches: - - ros2 + - ros2-humble schedule: # Run every week at 20:00 on Sunday - cron: "0 20 * * 0" @@ -14,8 +14,7 @@ jobs: strategy: fail-fast: false matrix: - package: - [ + package: [ diagnostic_aggregator, diagnostic_common_diagnostics, diagnostic_updater, From 5076eb18a7c92dea05eaa368bea3f3a2072a4444 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:28:39 +0200 Subject: [PATCH 05/34] NTP monitor improvements (#342) (#350) * NTP monitor improvements * commit to run tests again * bring back double callback * remove whiteline --------- Co-authored-by: Angsa Deployment Team (cherry picked from commit 7efb71ad5118f6b7d57d32d8e6b93ce4aa53e41e) Co-authored-by: Tony Najjar --- .../diagnostic_common_diagnostics/ntp_monitor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ntp_monitor.py b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ntp_monitor.py index 8671ddb5b..461d17bde 100755 --- a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ntp_monitor.py +++ b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ntp_monitor.py @@ -50,6 +50,8 @@ def __init__(self, ntp_hostname, ntp_port, offset=500, self_offset=500, do_self_test=True): """Initialize the NTPMonitor.""" super().__init__(__class__.__name__) + self.declare_parameter('frequency', 10.0) + frequency = self.get_parameter('frequency').get_parameter_value().double_value self.ntp_hostname = ntp_hostname self.ntp_port = ntp_port @@ -85,8 +87,8 @@ def __init__(self, ntp_hostname, ntp_port, offset=500, self_offset=500, # we need to periodically republish this self.current_msg = None - self.pubtimer = self.create_timer(0.1, self.pubCB) - self.checktimer = self.create_timer(0.1, self.checkCB) + self.pubtimer = self.create_timer(1/frequency, self.pubCB) + self.checktimer = self.create_timer(1/frequency, self.checkCB) def pubCB(self): with self.mutex: @@ -95,6 +97,7 @@ def pubCB(self): def checkCB(self): new_msg = DIAG.DiagnosticArray() + new_msg.header.stamp = self.get_clock().now().to_msg() st = self.ntp_diag(self.stat) if st is not None: @@ -159,7 +162,7 @@ def add_kv(stat_values, key, value): def ntp_monitor_main(argv=sys.argv[1:]): # filter out ROS args - argv = [a for a in argv if not a.startswith('__') and not a == '--ros-args' and not a == '-r'] + argv = argv[:argv.index('--ros-args')] if '--ros-args' in argv else argv import argparse parser = argparse.ArgumentParser() From ac82a29e89e2e698eea3374761f1acadd5738146 Mon Sep 17 00:00:00 2001 From: Rein Appeldoorn Date: Wed, 26 Jun 2024 17:40:27 +0200 Subject: [PATCH 06/34] refactor(ram_monitor): ros2 port (#338) * refactor(ram_monitor): ros2 port * Update diagnostic_common_diagnostics/diagnostic_common_diagnostics/ram_monitor.py * Apply suggestions from code review * Update diagnostic_common_diagnostics/README.md * fstrings * python3 --------- Co-authored-by: Christian Henkel <6976069+ct2034@users.noreply.github.com> --- diagnostic_common_diagnostics/CMakeLists.txt | 1 + diagnostic_common_diagnostics/README.md | 22 ++++- .../ram_monitor.py | 94 +++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100755 diagnostic_common_diagnostics/diagnostic_common_diagnostics/ram_monitor.py diff --git a/diagnostic_common_diagnostics/CMakeLists.txt b/diagnostic_common_diagnostics/CMakeLists.txt index e62c86ec0..99569f20d 100644 --- a/diagnostic_common_diagnostics/CMakeLists.txt +++ b/diagnostic_common_diagnostics/CMakeLists.txt @@ -10,6 +10,7 @@ ament_python_install_package(${PROJECT_NAME}) install(PROGRAMS ${PROJECT_NAME}/cpu_monitor.py ${PROJECT_NAME}/ntp_monitor.py + ${PROJECT_NAME}/ram_monitor.py DESTINATION lib/${PROJECT_NAME} ) diff --git a/diagnostic_common_diagnostics/README.md b/diagnostic_common_diagnostics/README.md index 42df1c8dc..41c500d17 100644 --- a/diagnostic_common_diagnostics/README.md +++ b/diagnostic_common_diagnostics/README.md @@ -70,7 +70,27 @@ Disable self test. **To be ported** ## ram_monitor.py -**To be ported** +The `ram_monitor` module allows users to monitor the RAM usage of their system in real-time. +It publishes the usage percentage in a diagnostic message. + +* Name of the node is "ram_monitor_" + hostname. +* Uses the following args: + * warning_percentage: If the RAM usage is > warning_percentage, a WARN status will be published. + * window: the maximum length of the used collections.deque for queuing RAM readings. + +### Published Topics +#### /diagnostics +diagnostic_msgs/DiagnosticArray +The diagnostics information. + +### Parameters +#### warning_percentage +(default: 90) +warning percentage threshold. + +#### window +(default: 1) +Length of RAM readings queue. ## sensors_monitor.py **To be ported** diff --git a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ram_monitor.py b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ram_monitor.py new file mode 100755 index 000000000..d6250b6b0 --- /dev/null +++ b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/ram_monitor.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# +# Software License Agreement (BSD License) +# +# Copyright (c) 2017, TNO IVS, Helmond, Netherlands +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the TNO IVS nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# \author Rein Appeldoorn + +import collections +import socket + +from diagnostic_msgs.msg import DiagnosticStatus + +from diagnostic_updater import DiagnosticTask, Updater + +import psutil + +import rclpy + + +class RamTask(DiagnosticTask): + + def __init__(self, warning_percentage, window): + DiagnosticTask.__init__(self, 'RAM Information') + self._warning_percentage = int(warning_percentage) + self._readings = collections.deque(maxlen=window) + + def run(self, stat): + self._readings.append(psutil.virtual_memory().percent) + ram_average = sum(self._readings) / len(self._readings) + + stat.add('RAM Load Average', f'{ram_average:.2f}') + + if ram_average > self._warning_percentage: + stat.summary( + DiagnosticStatus.WARN, + f'RAM Average exceeds {self._warning_percentage:d} percent', + ) + else: + stat.summary(DiagnosticStatus.OK, f'RAM Average {ram_average:.2f} percent') + + return stat + + +def main(): + hostname = socket.gethostname() + rclpy.init() + node = rclpy.create_node(f'ram_monitor_{hostname.replace("-", "_")}') + + updater = Updater(node) + updater.setHardwareID(hostname) + updater.add( + RamTask( + node.declare_parameter('warning_percentage', 90).value, + node.declare_parameter('window', 1).value, + ) + ) + + rclpy.spin(node) + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + pass From c8499c77b73f918130bb6f787c74b1ddcb14fff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Audren?= <101862279+haudren-woven@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:48:45 +0900 Subject: [PATCH 07/34] Fix usage of rclcpp::ok with a non-default context (#352) * Fix usage of rclcpp::ok with a non-default context The current implementation calls `rclcpp::ok` without any arguments, which amounts to verifying that the global default context is valid. In the case where a node is added to a custom context, and the global context is not used, `rclcpp::ok` without any arguments will always return `false` since the global context has never been initialized. To fix it, pass to rclcpp the context that's available via the node's base interface. * Add a test for custom context --- .../diagnostic_updater/diagnostic_updater.hpp | 2 +- .../test/diagnostic_updater_test.cpp | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/diagnostic_updater/include/diagnostic_updater/diagnostic_updater.hpp b/diagnostic_updater/include/diagnostic_updater/diagnostic_updater.hpp index a301ada59..2ecb08ab1 100644 --- a/diagnostic_updater/include/diagnostic_updater/diagnostic_updater.hpp +++ b/diagnostic_updater/include/diagnostic_updater/diagnostic_updater.hpp @@ -511,7 +511,7 @@ class Updater : public DiagnosticTaskVector */ void update() { - if (rclcpp::ok()) { + if (rclcpp::ok(base_interface_->get_context())) { bool warn_nohwid = hwid_.empty(); std::vector status_vec; diff --git a/diagnostic_updater/test/diagnostic_updater_test.cpp b/diagnostic_updater/test/diagnostic_updater_test.cpp index 0362b2897..6fd7d1998 100644 --- a/diagnostic_updater/test/diagnostic_updater_test.cpp +++ b/diagnostic_updater/test/diagnostic_updater_test.cpp @@ -134,6 +134,42 @@ TEST(DiagnosticUpdater, testUpdaterAsNodeClassMember) { SUCCEED(); } +class SaveIfCalled : public diagnostic_updater::DiagnosticTask +{ +public: + SaveIfCalled() + : DiagnosticTask("SaveIfCalled") {} + + void run(diagnostic_updater::DiagnosticStatusWrapper & s) + { + s.summary(0, "Have been called!"); + has_been_called_ = true; + } + + bool has_been_called() const + { + return has_been_called_; + } + +private: + bool has_been_called_ = false; +}; + + +TEST(DiagnosticUpdater, testCustomContext) { + auto context = std::make_shared(); + context->init(0, nullptr, rclcpp::InitOptions()); + + auto node = + std::make_shared("CustomContextNode", rclcpp::NodeOptions().context(context)); + diagnostic_updater::Updater updater(node); + SaveIfCalled save_if_called; + updater.add(save_if_called); + updater.force_update(); + ASSERT_TRUE(save_if_called.has_been_called()); + context->shutdown("End test"); +} + TEST(DiagnosticUpdater, testDiagnosticStatusWrapperKeyValuePairs) { diagnostic_updater::DiagnosticStatusWrapper stat; From e91447470ca7d364404598cbf711d3701af5e987 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:06:37 +0200 Subject: [PATCH 08/34] Aggregator: publish diagnostics_toplevel_state immediately on every degradation (#324) (#355) * Aggregator: publish diagnostics_toplevel_state immediately on every degradation * Update docs * Re-use the publishData function such that also the actual diagnostics are available * Expand test with more degradation cases (cherry picked from commit dbaec0420396bd4e33bb23d0453823d7df31546d) Co-authored-by: Tim Clephas --- diagnostic_aggregator/src/aggregator.cpp | 28 +++++------- .../test/test_critical_pub.py | 45 ++++++++++++------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/diagnostic_aggregator/src/aggregator.cpp b/diagnostic_aggregator/src/aggregator.cpp index d9576c737..64b716caf 100644 --- a/diagnostic_aggregator/src/aggregator.cpp +++ b/diagnostic_aggregator/src/aggregator.cpp @@ -151,28 +151,11 @@ void Aggregator::diagCallback(const DiagnosticArray::SharedPtr diag_msg) checkTimestamp(diag_msg); bool analyzed = false; + bool immediate_report = false; { // lock the whole loop to ensure nothing in the analyzer group changes during it. std::lock_guard lock(mutex_); for (auto j = 0u; j < diag_msg->status.size(); ++j) { analyzed = false; - - const bool top_level_state_transition_to_error = - (last_top_level_state_ != DiagnosticStatus::ERROR) && - (diag_msg->status[j].level == DiagnosticStatus::ERROR); - - if (critical_ && top_level_state_transition_to_error) { - RCLCPP_DEBUG( - logger_, "Received error message: %s, publishing error immediately", - diag_msg->status[j].name.c_str()); - DiagnosticStatus diag_toplevel_state; - diag_toplevel_state.name = "toplevel_state_critical"; - diag_toplevel_state.level = diag_msg->status[j].level; - toplevel_state_pub_->publish(diag_toplevel_state); - - // store the last published state - last_top_level_state_ = diag_toplevel_state.level; - } - auto item = std::make_shared(&diag_msg->status[j]); if (analyzer_group_->match(item->getName())) { @@ -182,8 +165,17 @@ void Aggregator::diagCallback(const DiagnosticArray::SharedPtr diag_msg) if (!analyzed) { other_analyzer_->analyze(item); } + + // In case there is a degraded state, publish immediately + if (critical_ && item->getLevel() > last_top_level_state_) { + immediate_report = true; + } } } + + if (immediate_report) { + publishData(); + } } Aggregator::~Aggregator() diff --git a/diagnostic_aggregator/test/test_critical_pub.py b/diagnostic_aggregator/test/test_critical_pub.py index 279e82957..adad7ed7d 100644 --- a/diagnostic_aggregator/test/test_critical_pub.py +++ b/diagnostic_aggregator/test/test_critical_pub.py @@ -67,41 +67,56 @@ def publish_message(self, level): rclpy.spin_once(self.node) return self.node.get_clock().now() - def test_critical_publisher(self): + def critical_publisher_test( + self, initial_state=DiagnosticStatus.OK, new_state=DiagnosticStatus.ERROR + ): # Publish the ok message and wait till the toplevel state is received - state = DiagnosticStatus.OK - time_0 = self.publish_message(state) + time_0 = self.publish_message(initial_state) - assert (self.received_state[0] == state), \ + assert (self.received_state[0] == initial_state), \ ('Received state is not the same as the sent state:' - + f"'{self.received_state[0]}' != '{state}'") + + f"'{self.received_state[0]}' != '{initial_state}'") self.received_state.clear() # Publish the ok message and expect the toplevel state after 1 second period - time_1 = self.publish_message(state) + time_1 = self.publish_message(initial_state) assert (time_1 - time_0 > rclpy.duration.Duration(seconds=0.99)), \ 'OK message received too early' - assert (self.received_state[0] == state), \ + assert (self.received_state[0] == initial_state), \ ('Received state is not the same as the sent state:' - + f"'{self.received_state[0]}' != '{state}'") + + f"'{self.received_state[0]}' != '{initial_state}'") self.received_state.clear() # Publish the message and expect the critical error message immediately - state = DiagnosticStatus.ERROR - time_2 = self.publish_message(state) + time_2 = self.publish_message(new_state) assert (time_2 - time_1 < rclpy.duration.Duration(seconds=0.1)), \ 'Critical error message not received within 0.1 second' - assert (self.received_state[0] == state), \ + assert (self.received_state[0] == new_state), \ ('Received state is not the same as the sent state:' - + f"'{self.received_state[0]}' != '{state}'") + + f"'{self.received_state[0]}' != '{new_state}'") self.received_state.clear() # Next error message should be sent at standard 1 second rate - time_3 = self.publish_message(state) + time_3 = self.publish_message(new_state) assert (time_3 - time_1 > rclpy.duration.Duration(seconds=0.99)), \ 'Periodic error message received too early' - assert (self.received_state[0] == state), \ + assert (self.received_state[0] == new_state), \ ('Received state is not the same as the sent state:' - + f"'{self.received_state[0]}' != '{state}'") + + f"'{self.received_state[0]}' != '{new_state}'") + + def test_critical_publisher_ok_error(self): + self.critical_publisher_test( + initial_state=DiagnosticStatus.OK, new_state=DiagnosticStatus.ERROR + ) + + def test_critical_publisher_ok_warn(self): + self.critical_publisher_test( + initial_state=DiagnosticStatus.OK, new_state=DiagnosticStatus.WARN + ) + + def test_critical_publisher_warn_error(self): + self.critical_publisher_test( + initial_state=DiagnosticStatus.WARN, new_state=DiagnosticStatus.ERROR + ) From 5933c90cd20eb03ff1a17660d080051fda9bfe82 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:47:33 +0200 Subject: [PATCH 09/34] Add add_analyzer functionality (#329) (#359) * Add add_analyzer functionality * Add copyright notice and license, remove unused includes, re-order includes correctly * Increase clarity of prefix_ by renaming it to analyzers_ns_ * Add add_analyzer functionality * Fix bug where base_path is not reset correctly * Make the parameter forwarding condition more generic, fix the default service namespace from diagnostics_agg to analyzers * Add an add_analyzer example to the diagnostic_aggregator * Update the relevant READMEs * Fix linter errors * Add test for add_analyzer at runtime, remove unnecessary ros info logger, remove unnecessary hardcoded namespace from yaml files * Remove the now redundant analyzers_ns_ * Change the copyright of add_analyzer, forgot to update it to Nobleo after copying the notice (cherry picked from commit 5e1415c042e770bab4a7925a55a1325e3b5fb15c) Co-authored-by: MartinCornelis2 <51268547+MartinCornelis2@users.noreply.github.com> --- diagnostic_aggregator/CMakeLists.txt | 35 +++++- diagnostic_aggregator/README.md | 27 +++++ diagnostic_aggregator/example/README.md | 4 +- .../example/example.launch.py.in | 8 ++ .../example/example_add_analyzers.yaml | 6 + diagnostic_aggregator/example/example_pub.py | 4 + .../diagnostic_aggregator/aggregator.hpp | 12 ++ diagnostic_aggregator/package.xml | 3 +- diagnostic_aggregator/src/add_analyzer.cpp | 110 ++++++++++++++++++ diagnostic_aggregator/src/aggregator.cpp | 62 ++++++---- .../test/add_analyzers.launch.py.in | 74 ++++++++++++ diagnostic_aggregator/test/all_analyzers.yaml | 2 +- .../test/analyzer_group.yaml | 2 +- diagnostic_aggregator/test/default.yaml | 9 ++ .../test/empty_root_path.yaml | 2 +- .../expected_output/add_all_analyzers.txt | 6 + .../test/expected_stale_analyzers.yaml | 2 +- .../test/multiple_match_analyzers.yaml | 2 +- .../test/primitive_analyzers.yaml | 2 +- 19 files changed, 343 insertions(+), 29 deletions(-) create mode 100644 diagnostic_aggregator/example/example_add_analyzers.yaml create mode 100644 diagnostic_aggregator/src/add_analyzer.cpp create mode 100644 diagnostic_aggregator/test/add_analyzers.launch.py.in create mode 100644 diagnostic_aggregator/test/default.yaml create mode 100644 diagnostic_aggregator/test/expected_output/add_all_analyzers.txt diff --git a/diagnostic_aggregator/CMakeLists.txt b/diagnostic_aggregator/CMakeLists.txt index 12aac3b1f..a72335e6f 100644 --- a/diagnostic_aggregator/CMakeLists.txt +++ b/diagnostic_aggregator/CMakeLists.txt @@ -14,6 +14,7 @@ find_package(ament_cmake REQUIRED) find_package(diagnostic_msgs REQUIRED) find_package(pluginlib REQUIRED) find_package(rclcpp REQUIRED) +find_package(rcl_interfaces REQUIRED) find_package(std_msgs REQUIRED) add_library(${PROJECT_NAME} SHARED @@ -67,6 +68,10 @@ add_executable(aggregator_node src/aggregator_node.cpp) target_link_libraries(aggregator_node ${PROJECT_NAME}) +# Add analyzer +add_executable(add_analyzer src/add_analyzer.cpp) +ament_target_dependencies(add_analyzer rclcpp rcl_interfaces) + # Testing macro if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -77,6 +82,7 @@ if(BUILD_TESTING) find_package(launch_testing_ament_cmake REQUIRED) file(TO_CMAKE_PATH "${CMAKE_INSTALL_PREFIX}/lib/${PROJECT_NAME}/aggregator_node" AGGREGATOR_NODE) + file(TO_CMAKE_PATH "${CMAKE_INSTALL_PREFIX}/lib/${PROJECT_NAME}/add_analyzer" ADD_ANALYZER) file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/test/test_listener.py" TEST_LISTENER) set(create_analyzers_tests "primitive_analyzers" @@ -124,6 +130,27 @@ if(BUILD_TESTING) ) endforeach() + set(add_analyzers_tests + "all_analyzers") + + foreach(test_name ${add_analyzers_tests}) + file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/test/default.yaml" PARAMETER_FILE) + file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/test/${test_name}.yaml" ADD_PARAMETER_FILE) + file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/test/expected_output/add_${test_name}" EXPECTED_OUTPUT) + + configure_file( + "test/add_analyzers.launch.py.in" + "test_add_${test_name}.launch.py" + @ONLY + ) + add_launch_test( + "${CMAKE_CURRENT_BINARY_DIR}/test_add_${test_name}.launch.py" + TARGET "test_add_${test_name}" + TIMEOUT 30 + ENV + ) + endforeach() + add_launch_test( test/test_critical_pub.py TIMEOUT 30 @@ -140,6 +167,11 @@ install( DESTINATION lib/${PROJECT_NAME} ) +install( + TARGETS add_analyzer + DESTINATION lib/${PROJECT_NAME} +) + install( TARGETS ${PROJECT_NAME} ${ANALYZERS} EXPORT ${PROJECT_NAME}Targets @@ -157,6 +189,7 @@ ament_python_install_package(${PROJECT_NAME}) # Install Example set(ANALYZER_PARAMS_FILEPATH "${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}/example_analyzers.yaml") +set(ADD_ANALYZER_PARAMS_FILEPATH "${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}/example_add_analyzers.yaml") configure_file(example/example.launch.py.in example.launch.py @ONLY) install( # launch descriptor FILES ${CMAKE_CURRENT_BINARY_DIR}/example.launch.py @@ -167,7 +200,7 @@ install( # example publisher DESTINATION lib/${PROJECT_NAME} ) install( # example aggregator configration - FILES example/example_analyzers.yaml + FILES example/example_analyzers.yaml example/example_add_analyzers.yaml DESTINATION share/${PROJECT_NAME} ) diff --git a/diagnostic_aggregator/README.md b/diagnostic_aggregator/README.md index 08178a066..fa0eea7a9 100644 --- a/diagnostic_aggregator/README.md +++ b/diagnostic_aggregator/README.md @@ -135,6 +135,33 @@ You can launch the `aggregator_node` like this (see [example.launch.py.in](examp ]) ``` +You can add analyzers at runtime using the `add_analyzer` node like this (see [example.launch.py.in](example/example.launch.py.in)): +``` + add_analyzer = launch_ros.actions.Node( + package='diagnostic_aggregator', + executable='add_analyzer', + output='screen', + parameters=[add_analyzer_params_filepath]) + return launch.LaunchDescription([ + add_analyzer, + ]) +``` +This node updates the parameters of the `aggregator_node` by calling the service `/analyzers/set_parameters_atomically`. +The `aggregator_node` will detect when a `parameter-event` has introduced new parameters to it. +When this happens the `aggregator_node` will reload all analyzers based on its new set of parameters. +Adding analyzers this way can be done at runtime and can be made conditional. + +In the example, `add_analyzer` will add an analyzer for diagnostics that are marked optional: +``` yaml +/**: + ros__parameters: + optional: + type: diagnostic_aggregator/GenericAnalyzer + path: Optional + startswith: [ '/optional' ] +``` +This will move the `/optional/runtime/analyzer` diagnostic from the "Other" to "Aggregation" where it will not go stale after 5 seconds and will be taken into account for the toplevel state. + # Basic analyzers The `diagnostic_aggregator` package provides a few basic analyzers that you can use to aggregate your diagnostics. diff --git a/diagnostic_aggregator/example/README.md b/diagnostic_aggregator/example/README.md index 10e9b2574..27c593be9 100644 --- a/diagnostic_aggregator/example/README.md +++ b/diagnostic_aggregator/example/README.md @@ -1,5 +1,7 @@ # Aggregator Example -This is a simple example to show the diagnostic_aggregator in action. It involves one python script producing dummy diagnostic data ([example_pub.py](./example_pub.py)), and one diagnostic aggregator configuration ([example.yaml](./example.yaml)) that provides analyzers aggregating it. +This is a simple example to show the diagnostic_aggregator and add_analyzer in action. It involves one python script producing dummy diagnostic data ([example_pub.py](./example_pub.py)), one diagnostic aggregator configuration ([example_analyzers.yaml](./example_analyzers.yaml)) and one add_analyzer configuration ([example_add_analyzers.yaml](./example_add_analyzers.yaml)). + +The aggregator will launch and load all the analyzers listed in ([example_analyzers.yaml](./example_analyzers.yaml)). Then the aggregator will be notified that there are additional analyzers that we also want to load in ([example_add_analyzers.yaml](./example_add_analyzers.yaml)). After this reload all analyzers will be active. Run the example with `ros2 launch diagnostic_aggregator example.launch.py` diff --git a/diagnostic_aggregator/example/example.launch.py.in b/diagnostic_aggregator/example/example.launch.py.in index 48cd62f66..81a749220 100644 --- a/diagnostic_aggregator/example/example.launch.py.in +++ b/diagnostic_aggregator/example/example.launch.py.in @@ -4,6 +4,7 @@ import launch import launch_ros.actions analyzer_params_filepath = "@ANALYZER_PARAMS_FILEPATH@" +add_analyzer_params_filepath = "@ADD_ANALYZER_PARAMS_FILEPATH@" def generate_launch_description(): @@ -12,11 +13,18 @@ def generate_launch_description(): executable='aggregator_node', output='screen', parameters=[analyzer_params_filepath]) + add_analyzer = launch_ros.actions.Node( + package='diagnostic_aggregator', + executable='add_analyzer', + output='screen', + parameters=[add_analyzer_params_filepath] + ) diag_publisher = launch_ros.actions.Node( package='diagnostic_aggregator', executable='example_pub.py') return launch.LaunchDescription([ aggregator, + add_analyzer, diag_publisher, launch.actions.RegisterEventHandler( event_handler=launch.event_handlers.OnProcessExit( diff --git a/diagnostic_aggregator/example/example_add_analyzers.yaml b/diagnostic_aggregator/example/example_add_analyzers.yaml new file mode 100644 index 000000000..1c6c264c7 --- /dev/null +++ b/diagnostic_aggregator/example/example_add_analyzers.yaml @@ -0,0 +1,6 @@ +/**: + ros__parameters: + optional: + type: diagnostic_aggregator/GenericAnalyzer + path: Optional + contains: [ '/optional' ] diff --git a/diagnostic_aggregator/example/example_pub.py b/diagnostic_aggregator/example/example_pub.py index 887dc18df..0c1b10436 100755 --- a/diagnostic_aggregator/example/example_pub.py +++ b/diagnostic_aggregator/example/example_pub.py @@ -81,6 +81,10 @@ def __init__(self): name='/sensors/front/cam', message='OK'), DiagnosticStatus(level=DiagnosticStatus.OK, name='/sensors/rear/cam', message='OK'), + + # Optional + DiagnosticStatus(level=DiagnosticStatus.OK, + name='/optional/runtime/analyzer', message='OK'), ] def timer_callback(self): diff --git a/diagnostic_aggregator/include/diagnostic_aggregator/aggregator.hpp b/diagnostic_aggregator/include/diagnostic_aggregator/aggregator.hpp index b901acdc0..5f48dd6b1 100644 --- a/diagnostic_aggregator/include/diagnostic_aggregator/aggregator.hpp +++ b/diagnostic_aggregator/include/diagnostic_aggregator/aggregator.hpp @@ -133,6 +133,8 @@ class Aggregator rclcpp::Service::SharedPtr add_srv_; /// DiagnosticArray, /diagnostics rclcpp::Subscription::SharedPtr diag_sub_; + /// ParameterEvent, /parameter_events + rclcpp::Subscription::SharedPtr param_sub_; /// DiagnosticArray, /diagnostics_agg rclcpp::Publisher::SharedPtr agg_pub_; /// DiagnosticStatus, /diagnostics_toplevel_state @@ -165,6 +167,16 @@ class Aggregator /// Records all ROS warnings. No warnings are repeated. std::set ros_warnings_; + /* + *!\brief Checks for new parameters to trigger reinitialization of the AnalyzerGroup and OtherAnalyzer + */ + void parameterCallback(const rcl_interfaces::msg::ParameterEvent::SharedPtr param_msg); + + /* + *!\brief (re)initializes the AnalyzerGroup and OtherAnalyzer + */ + void initAnalyzers(); + /* *!\brief Checks timestamp of message, and warns if timestamp is 0 (not set) */ diff --git a/diagnostic_aggregator/package.xml b/diagnostic_aggregator/package.xml index 677e04365..cfe473cd7 100644 --- a/diagnostic_aggregator/package.xml +++ b/diagnostic_aggregator/package.xml @@ -12,7 +12,7 @@ BSD-3-Clause http://www.ros.org/wiki/diagnostic_aggregator - + Kevin Watts Brice Rebsamen Arne Nordmann @@ -22,6 +22,7 @@ diagnostic_msgs pluginlib + rcl_interfaces rclcpp std_msgs diff --git a/diagnostic_aggregator/src/add_analyzer.cpp b/diagnostic_aggregator/src/add_analyzer.cpp new file mode 100644 index 000000000..42ff2b0aa --- /dev/null +++ b/diagnostic_aggregator/src/add_analyzer.cpp @@ -0,0 +1,110 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2024, Nobleo Technology + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +/**< \author Martin Cornelis */ + +#include + +#include "rclcpp/rclcpp.hpp" +#include "rcl_interfaces/srv/set_parameters_atomically.hpp" +#include "rcl_interfaces/msg/parameter.hpp" + +using namespace std::chrono_literals; + +class AddAnalyzer : public rclcpp::Node +{ +public: + AddAnalyzer() + : Node("add_analyzer_node", "", rclcpp::NodeOptions().allow_undeclared_parameters( + true).automatically_declare_parameters_from_overrides(true)) + { + client_ = this->create_client( + "/analyzers/set_parameters_atomically"); + } + + void send_request() + { + while (!client_->wait_for_service(1s)) { + if (!rclcpp::ok()) { + RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for the service. Exiting."); + return; + } + RCLCPP_INFO_ONCE(this->get_logger(), "service not available, waiting ..."); + } + auto request = std::make_shared(); + std::map parameters; + + if (!this->get_parameters("", parameters)) { + RCLCPP_ERROR(this->get_logger(), "Failed to retrieve parameters"); + } + for (const auto & [param_name, param] : parameters) { + // Find the suffix + size_t suffix_start = param_name.find_last_of('.'); + // Remove suffix if it exists + if (suffix_start != std::string::npos) { + std::string stripped_param_name = param_name.substr(0, suffix_start); + // Check in map if the stripped param name with the added suffix "path" exists + // This indicates the parameter is part of an analyzer description + if (parameters.count(stripped_param_name + ".path") > 0) { + auto parameter_msg = param.to_parameter_msg(); + request->parameters.push_back(parameter_msg); + } + } + } + + auto result = client_->async_send_request(request); + // Wait for the result. + if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), result) == + rclcpp::FutureReturnCode::SUCCESS) + { + RCLCPP_INFO(this->get_logger(), "Parameters succesfully set"); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to set parameters"); + } + } + +private: + rclcpp::Client::SharedPtr client_; +}; + +int main(int argc, char ** argv) +{ + rclcpp::init(argc, argv); + + auto add_analyzer = std::make_shared(); + add_analyzer->send_request(); + rclcpp::shutdown(); + + return 0; +} diff --git a/diagnostic_aggregator/src/aggregator.cpp b/diagnostic_aggregator/src/aggregator.cpp index 64b716caf..4619ec297 100644 --- a/diagnostic_aggregator/src/aggregator.cpp +++ b/diagnostic_aggregator/src/aggregator.cpp @@ -59,7 +59,8 @@ using diagnostic_msgs::msg::DiagnosticStatus; Aggregator::Aggregator() : n_(std::make_shared( "analyzers", "", - rclcpp::NodeOptions().automatically_declare_parameters_from_overrides(true))), + rclcpp::NodeOptions().allow_undeclared_parameters(true). + automatically_declare_parameters_from_overrides(true))), logger_(rclcpp::get_logger("Aggregator")), pub_rate_(1.0), history_depth_(1000), @@ -69,6 +70,36 @@ Aggregator::Aggregator() last_top_level_state_(DiagnosticStatus::STALE) { RCLCPP_DEBUG(logger_, "constructor"); + initAnalyzers(); + + diag_sub_ = n_->create_subscription( + "/diagnostics", rclcpp::SystemDefaultsQoS().keep_last(history_depth_), + std::bind(&Aggregator::diagCallback, this, _1)); + agg_pub_ = n_->create_publisher("/diagnostics_agg", 1); + toplevel_state_pub_ = + n_->create_publisher("/diagnostics_toplevel_state", 1); + + int publish_rate_ms = 1000 / pub_rate_; + publish_timer_ = n_->create_wall_timer( + std::chrono::milliseconds(publish_rate_ms), + std::bind(&Aggregator::publishData, this)); + + param_sub_ = n_->create_subscription( + "/parameter_events", 1, std::bind(&Aggregator::parameterCallback, this, _1)); +} + +void Aggregator::parameterCallback(const rcl_interfaces::msg::ParameterEvent::SharedPtr msg) +{ + if (msg->node == "/" + std::string(n_->get_name())) { + if (msg->new_parameters.size() != 0) { + base_path_ = ""; + initAnalyzers(); + } + } +} + +void Aggregator::initAnalyzers() +{ bool other_as_errors = false; std::map parameters; @@ -101,26 +132,17 @@ Aggregator::Aggregator() RCLCPP_DEBUG( logger_, "Aggregator critical publisher configured to: %s", (critical_ ? "true" : "false")); - analyzer_group_ = std::make_unique(); - if (!analyzer_group_->init(base_path_, "", n_)) { - RCLCPP_ERROR(logger_, "Analyzer group for diagnostic aggregator failed to initialize!"); - } - - // Last analyzer handles remaining data - other_analyzer_ = std::make_unique(other_as_errors); - other_analyzer_->init(base_path_); // This always returns true - - diag_sub_ = n_->create_subscription( - "/diagnostics", rclcpp::SystemDefaultsQoS().keep_last(history_depth_), - std::bind(&Aggregator::diagCallback, this, _1)); - agg_pub_ = n_->create_publisher("/diagnostics_agg", 1); - toplevel_state_pub_ = - n_->create_publisher("/diagnostics_toplevel_state", 1); + { // lock the mutex while analyzer_group_ and other_analyzer_ are being updated + std::lock_guard lock(mutex_); + analyzer_group_ = std::make_unique(); + if (!analyzer_group_->init(base_path_, "", n_)) { + RCLCPP_ERROR(logger_, "Analyzer group for diagnostic aggregator failed to initialize!"); + } - int publish_rate_ms = 1000 / pub_rate_; - publish_timer_ = n_->create_wall_timer( - std::chrono::milliseconds(publish_rate_ms), - std::bind(&Aggregator::publishData, this)); + // Last analyzer handles remaining data + other_analyzer_ = std::make_unique(other_as_errors); + other_analyzer_->init(base_path_); // This always returns true + } } void Aggregator::checkTimestamp(const DiagnosticArray::SharedPtr diag_msg) diff --git a/diagnostic_aggregator/test/add_analyzers.launch.py.in b/diagnostic_aggregator/test/add_analyzers.launch.py.in new file mode 100644 index 000000000..8e4eae8da --- /dev/null +++ b/diagnostic_aggregator/test/add_analyzers.launch.py.in @@ -0,0 +1,74 @@ +import os + +import unittest + +from launch import LaunchDescription +from launch.actions import ExecuteProcess +from launch.events import matches_action +from launch.events.process import ShutdownProcess + +import launch_testing +import launch_testing.actions +import launch_testing.asserts +import launch_testing.util +import launch_testing_ros + + +def generate_test_description(): + os.environ['OSPL_VERBOSITY'] = '8' + os.environ['RCUTILS_CONSOLE_OUTPUT_FORMAT'] = '{message}' + + aggregator_node = ExecuteProcess( + cmd=[ + "@AGGREGATOR_NODE@", + "--ros-args", + "--params-file", + "@PARAMETER_FILE@" + ], + name='aggregator_node', + emulate_tty=True, + output='screen') + + add_analyzer = ExecuteProcess( + cmd=[ + "@ADD_ANALYZER@", + "--ros-args", + "--params-file", + "@ADD_PARAMETER_FILE@" + ], + name='add_analyzer', + emulate_tty=True, + output='screen') + + launch_description = LaunchDescription() + launch_description.add_action(aggregator_node) + launch_description.add_action(add_analyzer) + launch_description.add_action(launch_testing.util.KeepAliveProc()) + launch_description.add_action(launch_testing.actions.ReadyToTest()) + return launch_description, locals() + +class TestAggregator(unittest.TestCase): + + def test_processes_output(self, proc_output, aggregator_node): + """Check aggregator logging output for expected strings.""" + + from launch_testing.tools.output import get_default_filtered_prefixes + output_filter = launch_testing_ros.tools.basic_output_filter( + filtered_prefixes=get_default_filtered_prefixes() + ['service not available, waiting...'], + filtered_rmw_implementation='@rmw_implementation@' + ) + proc_output.assertWaitFor( + expected_output=launch_testing.tools.expected_output_from_file(path="@EXPECTED_OUTPUT@"), + process=aggregator_node, + output_filter=output_filter, + timeout=15 + ) + + import time + time.sleep(1) + +@launch_testing.post_shutdown_test() +class TestAggregatorShutdown(unittest.TestCase): + + def test_last_process_exit_code(self, proc_info, aggregator_node): + launch_testing.asserts.assertExitCodes(proc_info, process=aggregator_node) diff --git a/diagnostic_aggregator/test/all_analyzers.yaml b/diagnostic_aggregator/test/all_analyzers.yaml index 84b330e34..4cb012a83 100644 --- a/diagnostic_aggregator/test/all_analyzers.yaml +++ b/diagnostic_aggregator/test/all_analyzers.yaml @@ -1,4 +1,4 @@ -analyzers: +/**: ros__parameters: path: BASIC prefix1: diff --git a/diagnostic_aggregator/test/analyzer_group.yaml b/diagnostic_aggregator/test/analyzer_group.yaml index 72bb5639d..14c938fff 100644 --- a/diagnostic_aggregator/test/analyzer_group.yaml +++ b/diagnostic_aggregator/test/analyzer_group.yaml @@ -1,4 +1,4 @@ -analyzers: +/**: ros__parameters: path: TEST primary: diff --git a/diagnostic_aggregator/test/default.yaml b/diagnostic_aggregator/test/default.yaml new file mode 100644 index 000000000..2da82b92f --- /dev/null +++ b/diagnostic_aggregator/test/default.yaml @@ -0,0 +1,9 @@ +/**: + ros__parameters: + path: BASIC + prefix0: + type: diagnostic_aggregator/GenericAnalyzer + path: Zeroth + contains: [ + 'contain0a', + 'contain0b' ] \ No newline at end of file diff --git a/diagnostic_aggregator/test/empty_root_path.yaml b/diagnostic_aggregator/test/empty_root_path.yaml index 391de4e99..b4b25509f 100644 --- a/diagnostic_aggregator/test/empty_root_path.yaml +++ b/diagnostic_aggregator/test/empty_root_path.yaml @@ -1,4 +1,4 @@ -analyzers: +/**: ros__parameters: primary: type: 'diagnostic_aggregator/AnalyzerGroup' diff --git a/diagnostic_aggregator/test/expected_output/add_all_analyzers.txt b/diagnostic_aggregator/test/expected_output/add_all_analyzers.txt new file mode 100644 index 000000000..9c5c2fc23 --- /dev/null +++ b/diagnostic_aggregator/test/expected_output/add_all_analyzers.txt @@ -0,0 +1,6 @@ +/BASIC/Zeroth +prefix0 +/BASIC/First +prefix1 +/BASIC/Third +prefix3 \ No newline at end of file diff --git a/diagnostic_aggregator/test/expected_stale_analyzers.yaml b/diagnostic_aggregator/test/expected_stale_analyzers.yaml index 9110f84d6..9546204d5 100644 --- a/diagnostic_aggregator/test/expected_stale_analyzers.yaml +++ b/diagnostic_aggregator/test/expected_stale_analyzers.yaml @@ -1,4 +1,4 @@ -analyzers: +/**: my_path: type: diagnostic_aggregator/GenericAnalyzer path: My Path diff --git a/diagnostic_aggregator/test/multiple_match_analyzers.yaml b/diagnostic_aggregator/test/multiple_match_analyzers.yaml index 46153c6a7..3f0ec91f4 100644 --- a/diagnostic_aggregator/test/multiple_match_analyzers.yaml +++ b/diagnostic_aggregator/test/multiple_match_analyzers.yaml @@ -1,4 +1,4 @@ -analyzers: +/**: my_path: type: diagnostic_aggregator/GenericAnalyzer path: Header1 diff --git a/diagnostic_aggregator/test/primitive_analyzers.yaml b/diagnostic_aggregator/test/primitive_analyzers.yaml index 9601cce77..fc98e2ee8 100644 --- a/diagnostic_aggregator/test/primitive_analyzers.yaml +++ b/diagnostic_aggregator/test/primitive_analyzers.yaml @@ -1,4 +1,4 @@ -analyzers: +/**: ros__parameters: log_level: debug primary: From 58c3ac1ccabcbffdaf5faa5bac920d46f26195cc Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:50:55 +0200 Subject: [PATCH 10/34] Adopting CI changes similar to jazzy #358 (#362) * adopting changes similar to jazzy Signed-off-by: Christian Henkel * Can we skip this workaround? Signed-off-by: Christian Henkel --------- Signed-off-by: Christian Henkel --- .github/workflows/lint.yaml | 10 +++++++--- .github/workflows/test.yaml | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 3488e94ea..635d00260 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -3,7 +3,7 @@ on: pull_request: push: branches: - - ros2 + - ros2-humble schedule: # Run every week at 20:00 on Sunday - cron: "0 20 * * 0" @@ -22,13 +22,17 @@ jobs: uncrustify, xmllint, ] - runs-on: ubuntu-latest + include: + - distro: humble + os: ubuntu-22.04 + runs-on: ${{ matrix.os }} env: AMENT_CPPCHECK_ALLOW_SLOW_VERSIONS: 1 steps: - uses: actions/checkout@v1 - uses: ros-tooling/setup-ros@master - - run: sudo pip install pydocstyle==6.1.1 # downgrade to fix https://github.com/ament/ament_lint/pull/428 + with: + required-ros-distributions: ${{ matrix.distro }} - uses: ros-tooling/action-ros-lint@master with: linter: ${{ matrix.linter }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e0b8d81cc..da47273ff 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -27,6 +27,8 @@ jobs: container: ubuntu:${{ matrix.os }} steps: - uses: ros-tooling/setup-ros@master + with: + required-ros-distributions: ${{ matrix.distro }} - uses: ros-tooling/action-ros-ci@master with: target-ros2-distro: ${{ matrix.distro }} From 57c5ca4db15d7d9e22fc839a343dee762a655537 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:02:41 +0200 Subject: [PATCH 11/34] refactor(sensors_monitor): ros2 port #339 (#365) thanks to https://github.com/reinzor Signed-off-by: Christian Henkel (cherry picked from commit e67a69c9b54198521d5dc0bf92e43dc256f24867) --- diagnostic_common_diagnostics/CMakeLists.txt | 1 + diagnostic_common_diagnostics/README.md | 15 +- .../sensors_monitor.py | 255 ++++++++++++++++++ diagnostic_common_diagnostics/package.xml | 1 + 4 files changed, 271 insertions(+), 1 deletion(-) create mode 100755 diagnostic_common_diagnostics/diagnostic_common_diagnostics/sensors_monitor.py diff --git a/diagnostic_common_diagnostics/CMakeLists.txt b/diagnostic_common_diagnostics/CMakeLists.txt index 99569f20d..9ab1c21f9 100644 --- a/diagnostic_common_diagnostics/CMakeLists.txt +++ b/diagnostic_common_diagnostics/CMakeLists.txt @@ -11,6 +11,7 @@ install(PROGRAMS ${PROJECT_NAME}/cpu_monitor.py ${PROJECT_NAME}/ntp_monitor.py ${PROJECT_NAME}/ram_monitor.py + ${PROJECT_NAME}/sensors_monitor.py DESTINATION lib/${PROJECT_NAME} ) diff --git a/diagnostic_common_diagnostics/README.md b/diagnostic_common_diagnostics/README.md index 41c500d17..3b410ee34 100644 --- a/diagnostic_common_diagnostics/README.md +++ b/diagnostic_common_diagnostics/README.md @@ -93,7 +93,20 @@ warning percentage threshold. Length of RAM readings queue. ## sensors_monitor.py -**To be ported** +The `sensors_monitor` module allows users to monitor the temperature, volt and fan speeds of their system in real-time. +It uses the [`LM Sensors` package](https://packages.debian.org/sid/utils/lm-sensors) to get the data. + +* Name of the node is "sensors_monitor_" + hostname. + +### Published Topics +#### /diagnostics +diagnostic_msgs/DiagnosticArray +The diagnostics information. + +### Parameters +#### ignore_fans +(default: false) +Whether to ignore the fan speed. ## tf_monitor.py **To be ported** diff --git a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/sensors_monitor.py b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/sensors_monitor.py new file mode 100755 index 000000000..c6733e119 --- /dev/null +++ b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/sensors_monitor.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 + +# Software License Agreement (BSD License) +# +# Copyright (c) 2012, Willow Garage, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the Willow Garage nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import division, with_statement + +from io import StringIO +import math +import re +import socket +import subprocess + +from diagnostic_msgs.msg import DiagnosticStatus + +import diagnostic_updater as DIAG + +import rclpy +from rclpy.node import Node + + +class Sensor(object): + + def __init__(self): + self.critical = None + self.min = None + self.max = None + self.input = None + self.name = None + self.type = None + self.high = None + self.alarm = None + + def __repr__(self): + return 'Sensor object (name: {}, type: {})'.format(self.name, self.type) + + def getCrit(self): + return self.critical + + def getMin(self): + return self.min + + def getMax(self): + return self.max + + def getInput(self): + return self.input + + def getName(self): + return self.name + + def getType(self): + return self.type + + def getHigh(self): + return self.high + + def getAlarm(self): + return self.alarm + + def __str__(self): + lines = [] + lines.append(str(self.name)) + lines.append('\t' + 'Type: ' + str(self.type)) + if self.input: + lines.append('\t' + 'Input: ' + str(self.input)) + if self.min: + lines.append('\t' + 'Min: ' + str(self.min)) + if self.max: + lines.append('\t' + 'Max: ' + str(self.max)) + if self.high: + lines.append('\t' + 'High: ' + str(self.high)) + if self.critical: + lines.append('\t' + 'Crit: ' + str(self.critical)) + lines.append('\t' + 'Alarm: ' + str(self.alarm)) + return '\n'.join(lines) + + +def parse_sensor_line(line): + sensor = Sensor() + line = line.lstrip() + [name, reading] = line.split(':') + + try: + [sensor.name, sensor.type] = name.rsplit(' ', 1) + except ValueError: + return None + + if sensor.name == 'Core': + sensor.name = name + sensor.type = 'Temperature' + elif sensor.name.find('Physical id') != -1: + sensor.name = name + sensor.type = 'Temperature' + + try: + [reading, params] = reading.lstrip().split('(') + except ValueError: + return None + + sensor.alarm = False + if line.find('ALARM') != -1: + sensor.alarm = True + + if reading.find('°C') == -1: + sensor.input = float(reading.split()[0]) + else: + sensor.input = float(reading.split('°C')[0]) + + params = params.split(',') + for param in params: + m = re.search('[0-9]+.[0-9]*', param) + if param.find('min') != -1: + sensor.min = float(m.group(0)) + elif param.find('max') != -1: + sensor.max = float(m.group(0)) + elif param.find('high') != -1: + sensor.high = float(m.group(0)) + elif param.find('crit') != -1: + sensor.critical = float(m.group(0)) + + return sensor + + +def _rads_to_rpm(rads): + return rads / (2 * math.pi) * 60 + + +def _rpm_to_rads(rpm): + return rpm * (2 * math.pi) / 60 + + +def parse_sensors_output(node: Node, output): + out = StringIO(output if isinstance(output, str) else output.decode('utf-8')) + + sensorList = [] + for line in out.readlines(): + # Check for a colon + if ':' in line and 'Adapter' not in line: + s = None + try: + s = parse_sensor_line(line) + except Exception as exc: + node.get_logger().warn( + 'Unable to parse line "%s", due to %s', line, exc + ) + if s is not None: + sensorList.append(s) + return sensorList + + +def get_sensors(): + p = subprocess.Popen( + 'sensors', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + ) + (o, e) = p.communicate() + if not p.returncode == 0: + return '' + if not o: + return '' + return o + + +class SensorsMonitor(object): + + def __init__(self, node: Node, hostname): + self.node = node + self.hostname = hostname + self.ignore_fans = node.declare_parameter('ignore_fans', False).value + node.get_logger().info('Ignore fanspeed warnings: %s' % self.ignore_fans) + + self.updater = DIAG.Updater(node) + self.updater.setHardwareID('none') + self.updater.add('%s Sensor Status' % self.hostname, self.monitor) + + def monitor(self, stat): + try: + stat.summary(DiagnosticStatus.OK, 'OK') + for sensor in parse_sensors_output(self.node, get_sensors()): + if sensor.getType() == 'Temperature': + if sensor.getInput() > sensor.getCrit(): + stat.mergeSummary( + DiagnosticStatus.ERROR, 'Critical Temperature' + ) + elif sensor.getInput() > sensor.getHigh(): + stat.mergeSummary(DiagnosticStatus.WARN, 'High Temperature') + stat.add( + ' '.join([sensor.getName(), sensor.getType()]), + str(sensor.getInput()), + ) + elif sensor.getType() == 'Voltage': + if sensor.getInput() < sensor.getMin(): + stat.mergeSummary(DiagnosticStatus.ERROR, 'Low Voltage') + elif sensor.getInput() > sensor.getMax(): + stat.mergeSummary(DiagnosticStatus.ERROR, 'High Voltage') + stat.add( + ' '.join([sensor.getName(), sensor.getType()]), + str(sensor.getInput()), + ) + elif sensor.getType() == 'Speed': + if not self.ignore_fans: + if sensor.getInput() < sensor.getMin(): + stat.mergeSummary(DiagnosticStatus.ERROR, 'No Fan Speed') + stat.add( + ' '.join([sensor.getName(), sensor.getType()]), + str(sensor.getInput()), + ) + except Exception: + import traceback + + self.node.get_logger().error('Unable to process lm-sensors data') + self.node.get_logger().error(traceback.format_exc()) + return stat + + +if __name__ == '__main__': + rclpy.init() + hostname = socket.gethostname() + hostname_clean = hostname.translate(hostname.maketrans('-', '_')) + node = rclpy.create_node('sensors_monitor_%s' % hostname_clean) + + monitor = SensorsMonitor(node, hostname) + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass diff --git a/diagnostic_common_diagnostics/package.xml b/diagnostic_common_diagnostics/package.xml index 8496593a0..cc9d820ef 100644 --- a/diagnostic_common_diagnostics/package.xml +++ b/diagnostic_common_diagnostics/package.xml @@ -19,6 +19,7 @@ ament_cmake_python diagnostic_updater + lm-sensors python3-ntplib python3-psutil From 1ebcbd769969dd04c5dbdfa4c9584f9421a0f398 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:27:56 +0200 Subject: [PATCH 12/34] change(diagnosed-publisher): allow specifying node clock (#340) (#370) A new clock was created for every diagnosed publisher. When running with 'use_sim_time', the /clock time was not respected. (cherry picked from commit 16b5e8a7e450003a4a64de34730c85bb56cb56ba) Co-authored-by: Rein Appeldoorn --- diagnostic_updater/include/diagnostic_updater/publisher.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/diagnostic_updater/include/diagnostic_updater/publisher.hpp b/diagnostic_updater/include/diagnostic_updater/publisher.hpp index d23c7fd1f..f2bdcb47a 100644 --- a/diagnostic_updater/include/diagnostic_updater/publisher.hpp +++ b/diagnostic_updater/include/diagnostic_updater/publisher.hpp @@ -226,8 +226,9 @@ class DiagnosedPublisher : public TopicDiagnostic const typename PublisherT::SharedPtr & pub, diagnostic_updater::Updater & diag, const diagnostic_updater::FrequencyStatusParam & freq, - const diagnostic_updater::TimeStampStatusParam & stamp) - : TopicDiagnostic(pub->get_topic_name(), diag, freq, stamp), + const diagnostic_updater::TimeStampStatusParam & stamp, + const rclcpp::Clock::SharedPtr & clock = std::make_shared()) + : TopicDiagnostic(pub->get_topic_name(), diag, freq, stamp, clock), publisher_(pub) { static_assert(has_header::value, "Message type has to have a header."); From a64d8ad59fbfe342dae9ffc5dec7aa42b8ba9128 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Thu, 27 Jun 2024 11:21:49 +0200 Subject: [PATCH 13/34] changelogs Signed-off-by: Christian Henkel --- diagnostic_aggregator/CHANGELOG.rst | 6 ++++++ diagnostic_common_diagnostics/CHANGELOG.rst | 9 +++++++++ diagnostic_updater/CHANGELOG.rst | 6 ++++++ diagnostics/CHANGELOG.rst | 3 +++ self_test/CHANGELOG.rst | 5 +++++ 5 files changed, 29 insertions(+) diff --git a/diagnostic_aggregator/CHANGELOG.rst b/diagnostic_aggregator/CHANGELOG.rst index efda16d09..e7390c53c 100644 --- a/diagnostic_aggregator/CHANGELOG.rst +++ b/diagnostic_aggregator/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog for package diagnostic_aggregator ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Forthcoming +----------- +* Add add_analyzer functionality (`#329 `_) (`#359 `_) +* Aggregator: publish diagnostics_toplevel_state immediately on every degradation (`#324 `_) (`#355 `_) +* Contributors: Christian Henkel + 3.2.0 (2024-03-22) ------------------ * Avoid rolling up an ERROR state when empty GenericAnalyzer blocks are marked discard_stale, or when all of their items are STALE. (`#315 `_) diff --git a/diagnostic_common_diagnostics/CHANGELOG.rst b/diagnostic_common_diagnostics/CHANGELOG.rst index 0c53e9ca1..f54d27fe2 100644 --- a/diagnostic_common_diagnostics/CHANGELOG.rst +++ b/diagnostic_common_diagnostics/CHANGELOG.rst @@ -2,6 +2,15 @@ Changelog for package diagnostic_common_diagnostics ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Forthcoming +----------- +* refactor(sensors_monitor): ros2 port `#339 `_ (`#365 `_) +* refactor(ram_monitor): ros2 port (`#338 `_) +* NTP monitor improvements (`#342 `_) (`#350 `_) +* Using ubuntu ntp server in systemtest (`#346 `_) (`#347 `_) +* Fixing ntp launchtest (`#330 `_) +* Contributors: Christian Henkel, Rein Appeldoorn + 3.2.0 (2024-03-22) ------------------ * Port cpu_monitor to ROS2 (`#326 `_) diff --git a/diagnostic_updater/CHANGELOG.rst b/diagnostic_updater/CHANGELOG.rst index e5d4da069..31b2cadb8 100644 --- a/diagnostic_updater/CHANGELOG.rst +++ b/diagnostic_updater/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog for package diagnostic_updater ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Forthcoming +----------- +* change(diagnosed-publisher): allow specifying node clock +* Fix usage of rclcpp::ok with a non-default context (`#352 `_) +* Contributors: Christian Henkel, Hervé Audren + 3.2.0 (2024-03-22) ------------------ * including depdency (`#322 `_) diff --git a/diagnostics/CHANGELOG.rst b/diagnostics/CHANGELOG.rst index 1f73b1330..e65ea0b99 100644 --- a/diagnostics/CHANGELOG.rst +++ b/diagnostics/CHANGELOG.rst @@ -2,6 +2,9 @@ Changelog for package diagnostics ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Forthcoming +----------- + 3.2.0 (2024-03-22) ------------------ diff --git a/self_test/CHANGELOG.rst b/self_test/CHANGELOG.rst index 1ebff1e4a..b8ae3174b 100644 --- a/self_test/CHANGELOG.rst +++ b/self_test/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog for package self_test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Forthcoming +----------- +* Building in docker (`#335 `_) +* Contributors: Christian Henkel + 3.2.0 (2024-03-22) ------------------ * Self test publishes the service under the node name, again (`#269 `_) From 5e28e01e75a3dce31b47ee523f91b6e159988dab Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Thu, 27 Jun 2024 11:22:09 +0200 Subject: [PATCH 14/34] 3.2.1 --- diagnostic_aggregator/CHANGELOG.rst | 4 ++-- diagnostic_aggregator/package.xml | 2 +- diagnostic_common_diagnostics/CHANGELOG.rst | 4 ++-- diagnostic_common_diagnostics/package.xml | 2 +- diagnostic_updater/CHANGELOG.rst | 4 ++-- diagnostic_updater/package.xml | 2 +- diagnostics/CHANGELOG.rst | 4 ++-- diagnostics/package.xml | 2 +- self_test/CHANGELOG.rst | 4 ++-- self_test/package.xml | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/diagnostic_aggregator/CHANGELOG.rst b/diagnostic_aggregator/CHANGELOG.rst index e7390c53c..4b31fc07c 100644 --- a/diagnostic_aggregator/CHANGELOG.rst +++ b/diagnostic_aggregator/CHANGELOG.rst @@ -2,8 +2,8 @@ Changelog for package diagnostic_aggregator ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Forthcoming ------------ +3.2.1 (2024-06-27) +------------------ * Add add_analyzer functionality (`#329 `_) (`#359 `_) * Aggregator: publish diagnostics_toplevel_state immediately on every degradation (`#324 `_) (`#355 `_) * Contributors: Christian Henkel diff --git a/diagnostic_aggregator/package.xml b/diagnostic_aggregator/package.xml index cfe473cd7..75dabc892 100644 --- a/diagnostic_aggregator/package.xml +++ b/diagnostic_aggregator/package.xml @@ -2,7 +2,7 @@ diagnostic_aggregator - 3.2.0 + 3.2.1 diagnostic_aggregator Austin Hendrix Brice Rebsamen diff --git a/diagnostic_common_diagnostics/CHANGELOG.rst b/diagnostic_common_diagnostics/CHANGELOG.rst index f54d27fe2..a5c40e70a 100644 --- a/diagnostic_common_diagnostics/CHANGELOG.rst +++ b/diagnostic_common_diagnostics/CHANGELOG.rst @@ -2,8 +2,8 @@ Changelog for package diagnostic_common_diagnostics ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Forthcoming ------------ +3.2.1 (2024-06-27) +------------------ * refactor(sensors_monitor): ros2 port `#339 `_ (`#365 `_) * refactor(ram_monitor): ros2 port (`#338 `_) * NTP monitor improvements (`#342 `_) (`#350 `_) diff --git a/diagnostic_common_diagnostics/package.xml b/diagnostic_common_diagnostics/package.xml index cc9d820ef..00d146740 100644 --- a/diagnostic_common_diagnostics/package.xml +++ b/diagnostic_common_diagnostics/package.xml @@ -2,7 +2,7 @@ diagnostic_common_diagnostics - 3.2.0 + 3.2.1 diagnostic_common_diagnostics Austin Hendrix Brice Rebsamen diff --git a/diagnostic_updater/CHANGELOG.rst b/diagnostic_updater/CHANGELOG.rst index 31b2cadb8..d9b9c33df 100644 --- a/diagnostic_updater/CHANGELOG.rst +++ b/diagnostic_updater/CHANGELOG.rst @@ -2,8 +2,8 @@ Changelog for package diagnostic_updater ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Forthcoming ------------ +3.2.1 (2024-06-27) +------------------ * change(diagnosed-publisher): allow specifying node clock * Fix usage of rclcpp::ok with a non-default context (`#352 `_) * Contributors: Christian Henkel, Hervé Audren diff --git a/diagnostic_updater/package.xml b/diagnostic_updater/package.xml index 6935d9acb..346b567e9 100644 --- a/diagnostic_updater/package.xml +++ b/diagnostic_updater/package.xml @@ -2,7 +2,7 @@ diagnostic_updater - 3.2.0 + 3.2.1 diagnostic_updater contains tools for easily updating diagnostics. it is commonly used in device drivers to keep track of the status of output topics, device status, etc. Austin Hendrix Brice Rebsamen diff --git a/diagnostics/CHANGELOG.rst b/diagnostics/CHANGELOG.rst index e65ea0b99..abdc0ea52 100644 --- a/diagnostics/CHANGELOG.rst +++ b/diagnostics/CHANGELOG.rst @@ -2,8 +2,8 @@ Changelog for package diagnostics ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Forthcoming ------------ +3.2.1 (2024-06-27) +------------------ 3.2.0 (2024-03-22) ------------------ diff --git a/diagnostics/package.xml b/diagnostics/package.xml index 9445c1f93..47a8ec1d3 100644 --- a/diagnostics/package.xml +++ b/diagnostics/package.xml @@ -2,7 +2,7 @@ diagnostics - 3.2.0 + 3.2.1 diagnostics Austin Hendrix Brice Rebsamen diff --git a/self_test/CHANGELOG.rst b/self_test/CHANGELOG.rst index b8ae3174b..7ee57f1c8 100644 --- a/self_test/CHANGELOG.rst +++ b/self_test/CHANGELOG.rst @@ -2,8 +2,8 @@ Changelog for package self_test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Forthcoming ------------ +3.2.1 (2024-06-27) +------------------ * Building in docker (`#335 `_) * Contributors: Christian Henkel diff --git a/self_test/package.xml b/self_test/package.xml index 6667d9b8f..91ed7370a 100644 --- a/self_test/package.xml +++ b/self_test/package.xml @@ -2,7 +2,7 @@ self_test - 3.2.0 + 3.2.1 self_test Austin Hendrix Brice Rebsamen From e2a9d17f49224f1d57203bdd603b89214c0e2f29 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Thu, 27 Jun 2024 13:51:55 +0200 Subject: [PATCH 15/34] 4.0.0 --- diagnostic_aggregator/package.xml | 2 +- diagnostic_common_diagnostics/package.xml | 2 +- diagnostic_updater/package.xml | 2 +- diagnostics/package.xml | 2 +- self_test/package.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/diagnostic_aggregator/package.xml b/diagnostic_aggregator/package.xml index 75dabc892..5b9d4fb3b 100644 --- a/diagnostic_aggregator/package.xml +++ b/diagnostic_aggregator/package.xml @@ -2,7 +2,7 @@ diagnostic_aggregator - 3.2.1 + 4.0.0 diagnostic_aggregator Austin Hendrix Brice Rebsamen diff --git a/diagnostic_common_diagnostics/package.xml b/diagnostic_common_diagnostics/package.xml index 00d146740..a713e4d67 100644 --- a/diagnostic_common_diagnostics/package.xml +++ b/diagnostic_common_diagnostics/package.xml @@ -2,7 +2,7 @@ diagnostic_common_diagnostics - 3.2.1 + 4.0.0 diagnostic_common_diagnostics Austin Hendrix Brice Rebsamen diff --git a/diagnostic_updater/package.xml b/diagnostic_updater/package.xml index 346b567e9..2c072fe20 100644 --- a/diagnostic_updater/package.xml +++ b/diagnostic_updater/package.xml @@ -2,7 +2,7 @@ diagnostic_updater - 3.2.1 + 4.0.0 diagnostic_updater contains tools for easily updating diagnostics. it is commonly used in device drivers to keep track of the status of output topics, device status, etc. Austin Hendrix Brice Rebsamen diff --git a/diagnostics/package.xml b/diagnostics/package.xml index 47a8ec1d3..623ae2824 100644 --- a/diagnostics/package.xml +++ b/diagnostics/package.xml @@ -2,7 +2,7 @@ diagnostics - 3.2.1 + 4.0.0 diagnostics Austin Hendrix Brice Rebsamen diff --git a/self_test/package.xml b/self_test/package.xml index 91ed7370a..f4b6b6fc1 100644 --- a/self_test/package.xml +++ b/self_test/package.xml @@ -2,7 +2,7 @@ self_test - 3.2.1 + 4.0.0 self_test Austin Hendrix Brice Rebsamen From 0e22c8c8cb412329fa9357e433d976e84ff829d2 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:39:45 +0200 Subject: [PATCH 16/34] versioning info (#373) (#374) Signed-off-by: Christian Henkel (cherry picked from commit 4f5ae697c6d1dbd00983eead89f735099199cd0a) --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 6766d3186..a17017fce 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,17 @@ Diagnostics messages that are not aggregated can be visualized by [`rqt_runtime_ - **Iron Irwini** by the [`ros2-iron` branch](https://github.com/ros/diagnostics/tree/ros2-iron) - **Jazzy Jalisco** by the [`ros2-jazzy` branch](https://github.com/ros/diagnostics/tree/ros2-jazzy) +# Versioning and Releases + +- (__X__.0.0) We use the major version number to indicate a breaking change. +- (0.__Y__.0) The minor version number is used to differentiate between different ROS distributions: + - x.__0__.z: Humble Hawksbill + - x.__1__.z: Iron Irwini + - x.__2__.z: Jazzy Jalisco + - x.__3__.z: Rolling Ridley + - Future releases (Kilted Kaiju 05/25) will get x.__3__.z and _Rolling_ will be incremented accordingly. +- (0.0.__Z__) The patch version number is used for changes in the current ROS distribution that do not affect the API. + # License The source code is released under a [BSD 3-Clause license](LICENSE). From 56533b2fb3a163111d09a5d9bf13a8cec2efaa4e Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:59:56 +0200 Subject: [PATCH 17/34] writing down the backport tool and its usage (#377) (#378) Signed-off-by: Christian Henkel (cherry picked from commit 0af0de12889f748cf9b29591a1a365ad5ac9eb82) --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index a17017fce..46509be6c 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,22 @@ Diagnostics messages that are not aggregated can be visualized by [`rqt_runtime_ - **Iron Irwini** by the [`ros2-iron` branch](https://github.com/ros/diagnostics/tree/ros2-iron) - **Jazzy Jalisco** by the [`ros2-jazzy` branch](https://github.com/ros/diagnostics/tree/ros2-jazzy) +## Workflow + +New features are to be developed in custom branches and then merged into the `ros2` branch. + +From there, the changes are backported to the other branches. + +## Backport Tooling + +This tool has proven to be useful: [backport](https://www.npmjs.com/package/backport) + +Use this command to port a given PR of `PR_NUMBER` to the other branches: + +```bash +backport --pr PR_NUMBER -b ros2-humble ros2-iron ros2-jazzy +``` + # Versioning and Releases - (__X__.0.0) We use the major version number to indicate a breaking change. From fe98a4806b55cf384f9591503ed713763dace932 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:08:57 +0200 Subject: [PATCH 18/34] [ros2-humble] Port hd_monitor to ROS2 (#334) (#381) * Port hd_monitor to ROS2 (#334) * Port hd_monitor.py * Adapt documentation and CMakeList to new hd_monitor.py * Fix execution flag of hd_monitor * Add launch test for hd_monitor * Implement low and crit parameters in hd_monitor * Improve hd_monitor code quality (cherry picked from commit 05a96450bad1f074156615aaafa00a78b894d312) * fixing pep257 problems introduced by #334 (#384) Signed-off-by: Christian Henkel --------- Signed-off-by: Christian Henkel Co-authored-by: Antoine Lima <7421319+limaanto@users.noreply.github.com> --- diagnostic_common_diagnostics/CMakeLists.txt | 5 + diagnostic_common_diagnostics/README.md | 15 +- .../hd_monitor.py | 156 ++++++++++++++++++ diagnostic_common_diagnostics/mainpage.dox | 1 + .../systemtest/test_hd_monitor_launchtest.py | 110 ++++++++++++ 5 files changed, 286 insertions(+), 1 deletion(-) create mode 100755 diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py create mode 100644 diagnostic_common_diagnostics/test/systemtest/test_hd_monitor_launchtest.py diff --git a/diagnostic_common_diagnostics/CMakeLists.txt b/diagnostic_common_diagnostics/CMakeLists.txt index 9ab1c21f9..261253671 100644 --- a/diagnostic_common_diagnostics/CMakeLists.txt +++ b/diagnostic_common_diagnostics/CMakeLists.txt @@ -12,6 +12,7 @@ install(PROGRAMS ${PROJECT_NAME}/ntp_monitor.py ${PROJECT_NAME}/ram_monitor.py ${PROJECT_NAME}/sensors_monitor.py + ${PROJECT_NAME}/hd_monitor.py DESTINATION lib/${PROJECT_NAME} ) @@ -29,6 +30,10 @@ if(BUILD_TESTING) test/systemtest/test_ntp_monitor_launchtest.py TARGET ntp_monitor_launchtest TIMEOUT 20) + add_launch_test( + test/systemtest/test_hd_monitor_launchtest.py + TARGET hd_monitor_launchtest + TIMEOUT 20) endif() ament_package() diff --git a/diagnostic_common_diagnostics/README.md b/diagnostic_common_diagnostics/README.md index 3b410ee34..fdece7f82 100644 --- a/diagnostic_common_diagnostics/README.md +++ b/diagnostic_common_diagnostics/README.md @@ -67,7 +67,20 @@ Computer name in diagnostics output (ex: 'c1') Disable self test. ## hd_monitor.py -**To be ported** +Runs 'shutil.disk_usage' to check if there is enough space left on a given device. +* Above 5% of free space left, an `OK` status will be published. +* Between 5% and 1%, a `WARN` status will be published, +* Below 1%, an `ERROR` status will be published. + +### Published Topics +#### /diagnostics +diagnostic_msgs/DiagnosticArray +The diagnostics information. + +### Parameters +#### path +(default: home directory "~") +Path in which to check remaining space. ## ram_monitor.py The `ram_monitor` module allows users to monitor the RAM usage of their system in real-time. diff --git a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py new file mode 100755 index 000000000..ac4eae748 --- /dev/null +++ b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py @@ -0,0 +1,156 @@ +#! /usr/bin/env python3 +"""Hard Drive (or any other memory) monitor. Contains a the monitor node and its main function.""" +# -*- coding: utf-8 -*- +# +# Software License Agreement (BSD License) +# +# Copyright (c) 2009, Willow Garage, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the Willow Garage nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# \author Kevin Watts +# \author Antoine Lima + +from pathlib import Path +from shutil import disk_usage +from socket import gethostname +from typing import List + +from diagnostic_msgs.msg import DiagnosticStatus, KeyValue +from diagnostic_updater import Updater +from rcl_interfaces.msg import SetParametersResult +import rclpy +from rclpy.node import Node + + +FREE_PERCENT_LOW = 0.05 +FREE_PERCENT_CRIT = 0.01 +DICT_STATUS = { + DiagnosticStatus.OK: 'OK', + DiagnosticStatus.WARN: 'Warning', + DiagnosticStatus.ERROR: 'Error', +} +DICT_USAGE = { + DiagnosticStatus.OK: 'OK', + DiagnosticStatus.WARN: 'Low Disk Space', + DiagnosticStatus.ERROR: 'Very Low Disk Space', +} + + +class HDMonitor(Node): + """ + Diagnostic node checking the remaining space on the specified hard drive. + + Three ROS parameters: + - path: Path on the filesystem to check (string, default: home directory) + - free_percent_low: Percentage at which to consider the space left as low + - free_percent_crit: Percentage at which to consider the space left as critical + """ + + def __init__(self): + hostname = gethostname().replace('.', '_').replace('-', '_') + super().__init__(f'hd_monitor_{hostname}') + + self._path = '~' + self._free_percent_low = 0.05 + self._free_percent_crit = 0.01 + + self.add_on_set_parameters_callback(self.callback_config) + self.declare_parameter('path', self._path) + self.declare_parameter('free_percent_low', self._free_percent_low) + self.declare_parameter('free_percent_crit', self._free_percent_crit) + + self._updater = Updater(self) + self._updater.setHardwareID(hostname) + self._updater.add(f'{hostname} HD Usage', self.check_disk_usage) + + def callback_config(self, params: List[rclpy.Parameter]): + """ + Retrieve ROS parameters. + + see the class documentation for declared parameters. + """ + for param in params: + match param.name: + case 'path': + self._path = str( + Path(param.value).expanduser().resolve(strict=True) + ) + case 'free_percent_low': + self._free_percent_low = param.value + case 'free_percent_crit': + self._free_percent_crit = param.value + + return SetParametersResult(successful=True) + + def check_disk_usage(self, diag: DiagnosticStatus) -> DiagnosticStatus: + """ + Compute the disk usage and derive a status from it. + + Task periodically ran by the diagnostic updater. + """ + diag.level = DiagnosticStatus.OK + + total, _, free = disk_usage(self._path) + percent = free / total + + if percent > self._free_percent_low: + diag.level = DiagnosticStatus.OK + elif percent > self._free_percent_crit: + diag.level = DiagnosticStatus.WARN + else: + diag.level = DiagnosticStatus.ERROR + + total_go = total // (1024 * 1024) + diag.values.extend( + [ + KeyValue(key='Name', value=self._path), + KeyValue(key='Status', value=DICT_STATUS[diag.level]), + KeyValue(key='Total (Go)', value=str(total_go)), + KeyValue(key='Available (%)', value=str(round(percent, 2))), + ] + ) + + diag.message = DICT_USAGE[diag.level] + return diag + + +def main(args=None): + """Run the HDMonitor class.""" + rclpy.init(args=args) + + node = HDMonitor() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + + +if __name__ == '__main__': + main() diff --git a/diagnostic_common_diagnostics/mainpage.dox b/diagnostic_common_diagnostics/mainpage.dox index 7aa872cef..f9cd677de 100644 --- a/diagnostic_common_diagnostics/mainpage.dox +++ b/diagnostic_common_diagnostics/mainpage.dox @@ -5,6 +5,7 @@ \b diagnostic_common_diagnostics contains a few common diagnostic nodes - cpu_monitor publishes diagnostic messages with the CPU usage of the system. +- hd_monitor publishes diagnostic messages related to the available space on a given storage device. - ntp_monitor publishes diagnostic messages for how well the NTP time sync is working. - tf_monitor used to publish diagnostic messages reporting on the health of the TF tree. It is based on tfwtf. It is not ported to ROS2. diff --git a/diagnostic_common_diagnostics/test/systemtest/test_hd_monitor_launchtest.py b/diagnostic_common_diagnostics/test/systemtest/test_hd_monitor_launchtest.py new file mode 100644 index 000000000..9086c5d75 --- /dev/null +++ b/diagnostic_common_diagnostics/test/systemtest/test_hd_monitor_launchtest.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Software License Agreement (BSD License) +# +# Copyright (c) 2023, Robert Bosch GmbH +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the Willow Garage nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import unittest + +from diagnostic_msgs.msg import DiagnosticArray + +import launch + +import launch_ros + +import launch_testing + +from launch_testing_ros import WaitForTopics + +import pytest + +import rclpy + + +@pytest.mark.launch_test +def generate_test_description(): + """Launch the hd_monitor node and return a launch description.""" + return launch.LaunchDescription( + [ + launch_ros.actions.Node( + package='diagnostic_common_diagnostics', + executable='hd_monitor.py', + name='hd_monitor', + output='screen', + parameters=[{'free_percent_low': 0.20, 'free_percent_crit': 0.05}], + ), + launch_testing.actions.ReadyToTest(), + ] + ) + + +class TestHDMonitor(unittest.TestCase): + """Test if the hd_monitor node is publishing diagnostics.""" + + def __init__(self, methodName: str = 'runTest') -> None: + super().__init__(methodName) + self.received_messages = [] + + def _received_message(self, msg): + self.received_messages.append(msg) + + def _get_min_level(self): + levels = [ + int.from_bytes(status.level, 'little') + for diag in self.received_messages + for status in diag.status + ] + if len(levels) == 0: + return -1 + return min(levels) + + def test_topic_published(self): + """Test if the hd_monitor node is publishing diagnostics.""" + with WaitForTopics([('/diagnostics', DiagnosticArray)], timeout=5): + print('Topic found') + + rclpy.init() + test_node = rclpy.create_node('test_node') + test_node.create_subscription( + DiagnosticArray, '/diagnostics', self._received_message, 1 + ) + + while len(self.received_messages) < 10: + rclpy.spin_once(test_node, timeout_sec=1) + if (min_level := self._get_min_level()) == 0: + break + + test_node.destroy_node() + rclpy.shutdown() + print(f'Got {len(self.received_messages)} messages:') + for msg in self.received_messages: + print(msg) + self.assertEqual(min_level, 0) From a2737ca6c5babc00d611e5424f01f3882b06f9c9 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:05:29 +0100 Subject: [PATCH 19/34] adding buildfarm statuses (#394) (#395) --- README.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 46509be6c..3b7041626 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Use this command to port a given PR of `PR_NUMBER` to the other branches: backport --pr PR_NUMBER -b ros2-humble ros2-iron ros2-jazzy ``` -# Versioning and Releases +## Versioning and Releases - (__X__.0.0) We use the major version number to indicate a breaking change. - (0.__Y__.0) The minor version number is used to differentiate between different ROS distributions: @@ -66,6 +66,51 @@ backport --pr PR_NUMBER -b ros2-humble ros2-iron ros2-jazzy - Future releases (Kilted Kaiju 05/25) will get x.__3__.z and _Rolling_ will be incremented accordingly. - (0.0.__Z__) The patch version number is used for changes in the current ROS distribution that do not affect the API. +## Buildfarm Statuses + +### diagnostic_aggregator + +| | H | I | J | R | +| ------- | - | - | - | - | +| src, ubuntu | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hsrc_uJ__diagnostic_aggregator__ubuntu_jammy__source&style=ball-32x32)](https://build.ros2.org/job/Hsrc_uJ__diagnostic_aggregator__ubuntu_jammy__source) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Isrc_uJ__diagnostic_aggregator__ubuntu_jammy__source&style=ball-32x32)](https://build.ros2.org/job/Isrc_uJ__diagnostic_aggregator__ubuntu_jammy__source) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jsrc_uN__diagnostic_aggregator__ubuntu_noble__source&style=ball-32x32)](https://build.ros2.org/job/Jsrc_uN__diagnostic_aggregator__ubuntu_noble__source) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rsrc_uN__diagnostic_aggregator__ubuntu_noble__source&style=ball-32x32)](https://build.ros2.org/job/Rsrc_uN__diagnostic_aggregator__ubuntu_noble__source) | +| src, rhel | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hsrc_el8__diagnostic_aggregator__rhel_8__source&style=ball-32x32)](https://build.ros2.org/job/Hsrc_el8__diagnostic_aggregator__rhel_8__source) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Isrc_el9__diagnostic_aggregator__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Isrc_el9__diagnostic_aggregator__rhel_9__source) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jsrc_el9__diagnostic_aggregator__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Jsrc_el9__diagnostic_aggregator__rhel_9__source) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rsrc_el9__diagnostic_aggregator__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Rsrc_el9__diagnostic_aggregator__rhel_9__source) | +| bin, ubuntu, amd64 | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__diagnostic_aggregator__ubuntu_jammy_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_uJ64__diagnostic_aggregator__ubuntu_jammy_amd64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__diagnostic_aggregator__ubuntu_jammy_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_uJ64__diagnostic_aggregator__ubuntu_jammy_amd64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_uN64__diagnostic_aggregator__ubuntu_noble_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_uN64__diagnostic_aggregator__ubuntu_noble_amd64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_uN64__diagnostic_aggregator__ubuntu_noble_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_uN64__diagnostic_aggregator__ubuntu_noble_amd64__binary) | +| bin, ubuntu, arm64 | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_ujv8_uJv8__diagnostic_aggregator__ubuntu_jammy_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_ujv8_uJv8__diagnostic_aggregator__ubuntu_jammy_arm64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_ujv8_uJv8__diagnostic_aggregator__ubuntu_jammy_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_ujv8_uJv8__diagnostic_aggregator__ubuntu_jammy_arm64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_unv8_uNv8__diagnostic_aggregator__ubuntu_noble_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_unv8_uNv8__diagnostic_aggregator__ubuntu_noble_arm64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_unv8_uNv8__diagnostic_aggregator__ubuntu_noble_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_unv8_uNv8__diagnostic_aggregator__ubuntu_noble_arm64__binary) | +| bin, rhel | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_rhel_el864__diagnostic_aggregator__rhel_8_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_rhel_el864__diagnostic_aggregator__rhel_8_x86_64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_rhel_el964__diagnostic_aggregator__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_rhel_el964__diagnostic_aggregator__rhel_9_x86_64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_rhel_el964__diagnostic_aggregator__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_rhel_el964__diagnostic_aggregator__rhel_9_x86_64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_rhel_el964__diagnostic_aggregator__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_rhel_el964__diagnostic_aggregator__rhel_9_x86_64__binary) | + + +### diagnostic_common_diagnostics + +| | H | I | J | R | +| ------- | - | - | - | - | +| src, ubuntu | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hsrc_uJ__diagnostic_common_diagnostics__ubuntu_jammy__source&style=ball-32x32)](https://build.ros2.org/job/Hsrc_uJ__diagnostic_common_diagnostics__ubuntu_jammy__source) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Isrc_uJ__diagnostic_common_diagnostics__ubuntu_jammy__source&style=ball-32x32)](https://build.ros2.org/job/Isrc_uJ__diagnostic_common_diagnostics__ubuntu_jammy__source) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jsrc_uN__diagnostic_common_diagnostics__ubuntu_noble__source&style=ball-32x32)](https://build.ros2.org/job/Jsrc_uN__diagnostic_common_diagnostics__ubuntu_noble__source) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rsrc_uN__diagnostic_common_diagnostics__ubuntu_noble__source&style=ball-32x32)](https://build.ros2.org/job/Rsrc_uN__diagnostic_common_diagnostics__ubuntu_noble__source) | +| src, rhel | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hsrc_el8__diagnostic_common_diagnostics__rhel_8__source&style=ball-32x32)](https://build.ros2.org/job/Hsrc_el8__diagnostic_common_diagnostics__rhel_8__source) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Isrc_el9__diagnostic_common_diagnostics__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Isrc_el9__diagnostic_common_diagnostics__rhel_9__source) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jsrc_el9__diagnostic_common_diagnostics__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Jsrc_el9__diagnostic_common_diagnostics__rhel_9__source) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rsrc_el9__diagnostic_common_diagnostics__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Rsrc_el9__diagnostic_common_diagnostics__rhel_9__source) | +| bin, ubuntu, amd64 | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__diagnostic_common_diagnostics__ubuntu_jammy_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_uJ64__diagnostic_common_diagnostics__ubuntu_jammy_amd64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__diagnostic_common_diagnostics__ubuntu_jammy_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_uJ64__diagnostic_common_diagnostics__ubuntu_jammy_amd64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_uN64__diagnostic_common_diagnostics__ubuntu_noble_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_uN64__diagnostic_common_diagnostics__ubuntu_noble_amd64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_uN64__diagnostic_common_diagnostics__ubuntu_noble_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_uN64__diagnostic_common_diagnostics__ubuntu_noble_amd64__binary) | +| bin, ubuntu, arm64 | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_ujv8_uJv8__diagnostic_common_diagnostics__ubuntu_jammy_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_ujv8_uJv8__diagnostic_common_diagnostics__ubuntu_jammy_arm64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_ujv8_uJv8__diagnostic_common_diagnostics__ubuntu_jammy_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_ujv8_uJv8__diagnostic_common_diagnostics__ubuntu_jammy_arm64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_unv8_uNv8__diagnostic_common_diagnostics__ubuntu_noble_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_unv8_uNv8__diagnostic_common_diagnostics__ubuntu_noble_arm64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_unv8_uNv8__diagnostic_common_diagnostics__ubuntu_noble_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_unv8_uNv8__diagnostic_common_diagnostics__ubuntu_noble_arm64__binary) | +| bin, rhel | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_rhel_el864__diagnostic_common_diagnostics__rhel_8_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_rhel_el864__diagnostic_common_diagnostics__rhel_8_x86_64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_rhel_el964__diagnostic_common_diagnostics__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_rhel_el964__diagnostic_common_diagnostics__rhel_9_x86_64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_rhel_el964__diagnostic_common_diagnostics__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_rhel_el964__diagnostic_common_diagnostics__rhel_9_x86_64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_rhel_el964__diagnostic_common_diagnostics__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_rhel_el964__diagnostic_common_diagnostics__rhel_9_x86_64__binary) | + + +### diagnostic_updater + +| | H | I | J | R | +| ------- | - | - | - | - | +| src, ubuntu | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hsrc_uJ__diagnostic_updater__ubuntu_jammy__source&style=ball-32x32)](https://build.ros2.org/job/Hsrc_uJ__diagnostic_updater__ubuntu_jammy__source) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Isrc_uJ__diagnostic_updater__ubuntu_jammy__source&style=ball-32x32)](https://build.ros2.org/job/Isrc_uJ__diagnostic_updater__ubuntu_jammy__source) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jsrc_uN__diagnostic_updater__ubuntu_noble__source&style=ball-32x32)](https://build.ros2.org/job/Jsrc_uN__diagnostic_updater__ubuntu_noble__source) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rsrc_uN__diagnostic_updater__ubuntu_noble__source&style=ball-32x32)](https://build.ros2.org/job/Rsrc_uN__diagnostic_updater__ubuntu_noble__source) | +| src, rhel | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hsrc_el8__diagnostic_updater__rhel_8__source&style=ball-32x32)](https://build.ros2.org/job/Hsrc_el8__diagnostic_updater__rhel_8__source) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Isrc_el9__diagnostic_updater__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Isrc_el9__diagnostic_updater__rhel_9__source) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jsrc_el9__diagnostic_updater__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Jsrc_el9__diagnostic_updater__rhel_9__source) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rsrc_el9__diagnostic_updater__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Rsrc_el9__diagnostic_updater__rhel_9__source) | +| bin, ubuntu, amd64 | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__diagnostic_updater__ubuntu_jammy_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_uJ64__diagnostic_updater__ubuntu_jammy_amd64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__diagnostic_updater__ubuntu_jammy_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_uJ64__diagnostic_updater__ubuntu_jammy_amd64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_uN64__diagnostic_updater__ubuntu_noble_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_uN64__diagnostic_updater__ubuntu_noble_amd64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_uN64__diagnostic_updater__ubuntu_noble_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_uN64__diagnostic_updater__ubuntu_noble_amd64__binary) | +| bin, ubuntu, arm64 | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_ujv8_uJv8__diagnostic_updater__ubuntu_jammy_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_ujv8_uJv8__diagnostic_updater__ubuntu_jammy_arm64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_ujv8_uJv8__diagnostic_updater__ubuntu_jammy_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_ujv8_uJv8__diagnostic_updater__ubuntu_jammy_arm64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_unv8_uNv8__diagnostic_updater__ubuntu_noble_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_unv8_uNv8__diagnostic_updater__ubuntu_noble_arm64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_unv8_uNv8__diagnostic_updater__ubuntu_noble_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_unv8_uNv8__diagnostic_updater__ubuntu_noble_arm64__binary) | +| bin, rhel | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_rhel_el864__diagnostic_updater__rhel_8_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_rhel_el864__diagnostic_updater__rhel_8_x86_64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_rhel_el964__diagnostic_updater__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_rhel_el964__diagnostic_updater__rhel_9_x86_64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_rhel_el964__diagnostic_updater__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_rhel_el964__diagnostic_updater__rhel_9_x86_64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_rhel_el964__diagnostic_updater__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_rhel_el964__diagnostic_updater__rhel_9_x86_64__binary) | + +### self_test + +| | H | I | J | R | +| ------- | - | - | - | - | +| src, ubuntu | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hsrc_uJ__self_test__ubuntu_jammy__source&style=ball-32x32)](https://build.ros2.org/job/Hsrc_uJ__self_test__ubuntu_jammy__source) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Isrc_uJ__self_test__ubuntu_jammy__source&style=ball-32x32)](https://build.ros2.org/job/Isrc_uJ__self_test__ubuntu_jammy__source) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jsrc_uN__self_test__ubuntu_noble__source&style=ball-32x32)](https://build.ros2.org/job/Jsrc_uN__self_test__ubuntu_noble__source) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rsrc_uN__self_test__ubuntu_noble__source&style=ball-32x32)](https://build.ros2.org/job/Rsrc_uN__self_test__ubuntu_noble__source) | +| src, rhel | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hsrc_el8__self_test__rhel_8__source&style=ball-32x32)](https://build.ros2.org/job/Hsrc_el8__self_test__rhel_8__source) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Isrc_el9__self_test__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Isrc_el9__self_test__rhel_9__source) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jsrc_el9__self_test__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Jsrc_el9__self_test__rhel_9__source) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rsrc_el9__self_test__rhel_9__source&style=ball-32x32)](https://build.ros2.org/job/Rsrc_el9__self_test__rhel_9__source) | +| bin, ubuntu, amd64 | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__self_test__ubuntu_jammy_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_uJ64__self_test__ubuntu_jammy_amd64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__self_test__ubuntu_jammy_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_uJ64__self_test__ubuntu_jammy_amd64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_uN64__self_test__ubuntu_noble_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_uN64__self_test__ubuntu_noble_amd64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_uN64__self_test__ubuntu_noble_amd64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_uN64__self_test__ubuntu_noble_amd64__binary) | +| bin, ubuntu, arm64 | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_ujv8_uJv8__self_test__ubuntu_jammy_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_ujv8_uJv8__self_test__ubuntu_jammy_arm64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_ujv8_uJv8__self_test__ubuntu_jammy_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_ujv8_uJv8__self_test__ubuntu_jammy_arm64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_unv8_uNv8__self_test__ubuntu_noble_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_unv8_uNv8__self_test__ubuntu_noble_arm64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_unv8_uNv8__self_test__ubuntu_noble_arm64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_unv8_uNv8__self_test__ubuntu_noble_arm64__binary) | +| bin, rhel | [![Humble](https://build.ros2.org/buildStatus/icon?job=Hbin_rhel_el864__self_test__rhel_8_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Hbin_rhel_el864__self_test__rhel_8_x86_64__binary) | [![Iron](https://build.ros2.org/buildStatus/icon?job=Ibin_rhel_el964__self_test__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Ibin_rhel_el964__self_test__rhel_9_x86_64__binary) | [![Jazzy](https://build.ros2.org/buildStatus/icon?job=Jbin_rhel_el964__self_test__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Jbin_rhel_el964__self_test__rhel_9_x86_64__binary) | [![Rolling](https://build.ros2.org/buildStatus/icon?job=Rbin_rhel_el964__self_test__rhel_9_x86_64__binary&style=ball-32x32)](https://build.ros2.org/job/Rbin_rhel_el964__self_test__rhel_9_x86_64__binary) | + + # License The source code is released under a [BSD 3-Clause license](LICENSE). From c1e03f9289f0f4dff5c6180edbd947e0d2637c28 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:30:15 +0100 Subject: [PATCH 20/34] Skipping flaky tests (#413) (#414) * skipping flaky ntp test Signed-off-by: Christian Henkel * Also skipping tests in diagnostic_aggregator Signed-off-by: Christian Henkel * also the test_critical_pub skipped Signed-off-by: Christian Henkel * oh no .. Signed-off-by: Christian Henkel --------- Signed-off-by: Christian Henkel (cherry picked from commit beb9935496abe179f21bcb597229fc78f40a9a9e) # Conflicts: # diagnostic_updater/CMakeLists.txt --- diagnostic_aggregator/CMakeLists.txt | 20 +++++++++++--------- diagnostic_updater/CMakeLists.txt | 21 +++++++++------------ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/diagnostic_aggregator/CMakeLists.txt b/diagnostic_aggregator/CMakeLists.txt index a72335e6f..63e0a7daf 100644 --- a/diagnostic_aggregator/CMakeLists.txt +++ b/diagnostic_aggregator/CMakeLists.txt @@ -151,15 +151,17 @@ if(BUILD_TESTING) ) endforeach() - add_launch_test( - test/test_critical_pub.py - TIMEOUT 30 - ) - - ament_add_pytest_test(test_discard_behavior - "${CMAKE_CURRENT_SOURCE_DIR}/test/test_discard_behavior.py" - TIMEOUT 60 - ) + # SKIPPING FLAKY TEST + # add_launch_test( + # test/test_critical_pub.py + # TIMEOUT 30 + # ) + + # SKIPPING FLAKY TEST + # ament_add_pytest_test(test_discard_behavior + # "${CMAKE_CURRENT_SOURCE_DIR}/test/test_discard_behavior.py" + # TIMEOUT 60 + # ) endif() install( diff --git a/diagnostic_updater/CMakeLists.txt b/diagnostic_updater/CMakeLists.txt index cd723d2e6..c4f1b3768 100644 --- a/diagnostic_updater/CMakeLists.txt +++ b/diagnostic_updater/CMakeLists.txt @@ -84,22 +84,19 @@ if(BUILD_TESTING) "rclcpp_lifecycle" "std_msgs" ) - ament_add_gtest(status_msg_test test/status_msg_test.cpp) - target_include_directories(status_msg_test - PUBLIC - $ - $ - ) - ament_target_dependencies( - status_msg_test - "diagnostic_msgs" - "rclcpp" - ) + # SKIPPING FLAKY TEST + # ament_add_gtest(status_msg_test test/status_msg_test.cpp) + # target_include_directories(status_msg_test + # PUBLIC + # $ + # $ + # ) + # target_link_libraries(status_msg_test ${PROJECT_NAME}) find_package(ament_cmake_pytest REQUIRED) ament_add_pytest_test(diagnostic_updater_test.py "test/diagnostic_updater_test.py") ament_add_pytest_test(test_DiagnosticStatusWrapper.py "test/test_diagnostic_status_wrapper.py") - ament_add_pytest_test(status_msg_test.py "test/status_msg_test.py") + # ament_add_pytest_test(status_msg_test.py "test/status_msg_test.py") endif() ament_python_install_package(${PROJECT_NAME}) From b78e24a5fc9c3397b6649d7bb1ed5ccf53592d33 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:46:47 +0100 Subject: [PATCH 21/34] Add missing rclpy dependency to common_diagnostics to fix rosdoc2 output (#402) (#406) (cherry picked from commit 095894604064ad48b75585673b7fd65a1d4bdb2a) Co-authored-by: R Kent James --- diagnostic_common_diagnostics/package.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/diagnostic_common_diagnostics/package.xml b/diagnostic_common_diagnostics/package.xml index a713e4d67..0bc7dab75 100644 --- a/diagnostic_common_diagnostics/package.xml +++ b/diagnostic_common_diagnostics/package.xml @@ -22,6 +22,7 @@ lm-sensors python3-ntplib python3-psutil + rclpy ament_lint_auto # License From 1b28ceca9b615459e765ed03e0c478dcf481d43d Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:05:07 +0100 Subject: [PATCH 29/34] [ros2-humble] Grafana Integration (#425) (#444) * Grafana Integration (#425) * First working version of remote_logging * Added more error handling, and skipping values when new line is present in stat * Changed default telegraf url to reflect the change to influxdb_v2_listener * Made node composable and changed name to influx to better reflect use cases * Added README * Escaping string values fixed * Build fix on farm * Adde curl dependency * Removed Curl Dependency as build does not work on farm * Curl Dependency * Added unit tests for influx_line_protocol * Only depend on gtest when testing * Apply suggestions from code review * Added diagnostic_remote_logging * Added BSD clause and added untested function * Apply suggestions from code review * Implemented linter feedback * removed build warning * Fixed linting erorrs that were fixed by auto formatting --------- Co-authored-by: Daan Wijffels Co-authored-by: Thiever Base Co-authored-by: Christian Henkel <6976069+ct2034@users.noreply.github.com> (cherry picked from commit 6e54e7fb2aa7f7dca32e1b1d23551a4335815a62) * formatting fixes Signed-off-by: Christian Henkel --------- Signed-off-by: Christian Henkel Co-authored-by: Daan Wijffels --- .github/workflows/lint.yaml | 1 + .github/workflows/test.yaml | 1 + diagnostic_remote_logging/CMakeLists.txt | 52 +++++ .../CMakeLists.txt.uncrustify | 52 +++++ .../CMakeLists.txt.uncrustify.uncrustify | 52 +++++ diagnostic_remote_logging/README.md | 81 ++++++++ .../README.md.uncrustify | 0 .../influx_line_protocol.hpp | 172 +++++++++++++++++ .../diagnostic_remote_logging/influxdb.hpp | 71 +++++++ diagnostic_remote_logging/package.xml | 22 +++ diagnostic_remote_logging/src/influxdb.cpp | 174 +++++++++++++++++ .../test/influx_line_protocol.cpp | 178 ++++++++++++++++++ 12 files changed, 856 insertions(+) create mode 100644 diagnostic_remote_logging/CMakeLists.txt create mode 100644 diagnostic_remote_logging/CMakeLists.txt.uncrustify create mode 100644 diagnostic_remote_logging/CMakeLists.txt.uncrustify.uncrustify create mode 100644 diagnostic_remote_logging/README.md create mode 100644 diagnostic_remote_logging/README.md.uncrustify create mode 100644 diagnostic_remote_logging/include/diagnostic_remote_logging/influx_line_protocol.hpp create mode 100644 diagnostic_remote_logging/include/diagnostic_remote_logging/influxdb.hpp create mode 100644 diagnostic_remote_logging/package.xml create mode 100644 diagnostic_remote_logging/src/influxdb.cpp create mode 100644 diagnostic_remote_logging/test/influx_line_protocol.cpp diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ecf8e7e5f..a62c49a81 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -39,6 +39,7 @@ jobs: package-name: | diagnostic_aggregator diagnostic_common_diagnostics + diagnostic_remote_logging diagnostic_updater self_test diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index da47273ff..2e29e6b73 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,6 +17,7 @@ jobs: package: [ diagnostic_aggregator, diagnostic_common_diagnostics, + diagnostic_remote_logging, diagnostic_updater, self_test, ] diff --git a/diagnostic_remote_logging/CMakeLists.txt b/diagnostic_remote_logging/CMakeLists.txt new file mode 100644 index 000000000..a33edea05 --- /dev/null +++ b/diagnostic_remote_logging/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.8) + +project(diagnostic_remote_logging) +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +set(dependencies + ament_cmake + rclcpp + rclcpp_components + diagnostic_msgs + CURL +) + +foreach(dep ${dependencies}) + find_package(${dep} REQUIRED) +endforeach(dep) + +include_directories( + src/ + include/ +) +add_library(influx_component SHARED src/influxdb.cpp) + +ament_target_dependencies(influx_component ${dependencies}) + +ament_export_dependencies(influx_component ${dependencies}) + +target_compile_features(influx_component PUBLIC c_std_99 cxx_std_17) + +rclcpp_components_register_node( + influx_component + PLUGIN "InfluxDB" + EXECUTABLE influx +) +ament_export_targets(export_influx_component) +install(TARGETS influx_component + EXPORT export_influx_component + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +if(BUILD_TESTING) + find_package(ament_cmake_gtest REQUIRED) + ament_add_gtest(unit_tests test/influx_line_protocol.cpp) + target_include_directories(unit_tests PRIVATE include) + target_link_libraries(unit_tests influx_component) + ament_target_dependencies(unit_tests ${dependencies} ament_cmake_gtest) +endif() +ament_package() diff --git a/diagnostic_remote_logging/CMakeLists.txt.uncrustify b/diagnostic_remote_logging/CMakeLists.txt.uncrustify new file mode 100644 index 000000000..39ed0e439 --- /dev/null +++ b/diagnostic_remote_logging/CMakeLists.txt.uncrustify @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.8) + +project(diagnostic_remote_logging) +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall - Wextra - Wpedantic) + endif() + + set(dependencies + ament_cmake + rclcpp + rclcpp_components + diagnostic_msgs + CURL + ) + + foreach(dep $ {dependencies}) + find_package($ {dep} REQUIRED) + endforeach(dep) + + include_directories( + src / + include / + ) + add_library(influx_component SHARED src / influxdb.cpp) + + ament_target_dependencies(influx_component $ {dependencies}) + + ament_export_dependencies(influx_component $ {dependencies}) + + target_compile_features(influx_component PUBLIC c_std_99 cxx_std_17) + + rclcpp_components_register_node( + influx_component + PLUGIN "InfluxDB" + EXECUTABLE influx + ) + ament_export_targets(export_influx_component) + install(TARGETS influx_component + EXPORT export_influx_component + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + ) + + if(BUILD_TESTING) + find_package(ament_cmake_gtest REQUIRED) + ament_add_gtest(unit_tests test / influx_line_protocol.cpp) + target_include_directories(unit_tests PRIVATE include) + target_link_libraries(unit_tests influx_component) + ament_target_dependencies(unit_tests $ {dependencies} ament_cmake_gtest) + endif() + ament_package() diff --git a/diagnostic_remote_logging/CMakeLists.txt.uncrustify.uncrustify b/diagnostic_remote_logging/CMakeLists.txt.uncrustify.uncrustify new file mode 100644 index 000000000..39ed0e439 --- /dev/null +++ b/diagnostic_remote_logging/CMakeLists.txt.uncrustify.uncrustify @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.8) + +project(diagnostic_remote_logging) +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall - Wextra - Wpedantic) + endif() + + set(dependencies + ament_cmake + rclcpp + rclcpp_components + diagnostic_msgs + CURL + ) + + foreach(dep $ {dependencies}) + find_package($ {dep} REQUIRED) + endforeach(dep) + + include_directories( + src / + include / + ) + add_library(influx_component SHARED src / influxdb.cpp) + + ament_target_dependencies(influx_component $ {dependencies}) + + ament_export_dependencies(influx_component $ {dependencies}) + + target_compile_features(influx_component PUBLIC c_std_99 cxx_std_17) + + rclcpp_components_register_node( + influx_component + PLUGIN "InfluxDB" + EXECUTABLE influx + ) + ament_export_targets(export_influx_component) + install(TARGETS influx_component + EXPORT export_influx_component + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + ) + + if(BUILD_TESTING) + find_package(ament_cmake_gtest REQUIRED) + ament_add_gtest(unit_tests test / influx_line_protocol.cpp) + target_include_directories(unit_tests PRIVATE include) + target_link_libraries(unit_tests influx_component) + ament_target_dependencies(unit_tests $ {dependencies} ament_cmake_gtest) + endif() + ament_package() diff --git a/diagnostic_remote_logging/README.md b/diagnostic_remote_logging/README.md new file mode 100644 index 000000000..bf7b96267 --- /dev/null +++ b/diagnostic_remote_logging/README.md @@ -0,0 +1,81 @@ +General information about this repository, including legal information and known issues/limitations, are given in [README.md](../README.md) in the repository root. + +# The diagnostic_remote_logging package + +This package provides the `influx` node, which listens to diagnostic messages and integrates with InfluxDB v2 for monitoring and visualization. Specifically, it subscribes to the [`diagnostic_msgs/DiagnosticArray`](https://index.ros.org/p/diagnostic_msgs) messages on the `/diagnostics_agg` topic and the [`diagnostic_msgs/DiagnosticStatus`](https://index.ros.org/p/diagnostic_msgs) messages on the `/diagnostics_toplevel_state` topic. The node processes these messages, sending their statistics and levels to an [`InfluxDB`](http://influxdb.com) database, enabling use with tools like [`Grafana`](https://grafana.com). + +As of now we only support InfluxDB v2, for support with older versions please use a proxy like [`Telegraf`](https://www.influxdata.com/time-series-platform/telegraf/). See section [Telegraf](#using-a-telegraf-proxy) for an example on how to setup. + +## Node Configuration + +You can send data to [`InfluxDB`](http://influxdb.com) in two ways: directly to the database or via a proxy like [`Telegraf`](https://www.influxdata.com/time-series-platform/telegraf/). While both methods are valid, using a proxy is generally recommended due to the following benefits: + +- **Efficient Data Transmission**: Telegraf aggregates multiple measurements and sends them in a single request, reducing bandwidth usage and minimizing database load. +- **Enhanced Reliability**: Provides buffering in case of connection issues, ensuring no data is lost. +- **Comprehensive Metric Collection**: Telegraf can send additional system metrics (e.g., RAM, CPU, network usage) with minimal configuration. +- **Data Filtering and Transformation**: Supports preprocessing, such as filtering or transforming data, before sending it to InfluxDB. + +To use either method, ensure you have a running instance of InfluxDB. The simplest way to set this up is through [`InfluxDB Cloud`](https://cloud2.influxdata.com/signup). + +### Parameters + +The `influx` node supports several parameters. Below is an example configuration: + +```yaml +/influx: + ros__parameters: + connection: + url: http://localhost:8086/api/v2/write + token: + bucket: + organization: + send: + agg: true + top_level_state: true +``` + +- `send.agg`: Enables or disables subscription to the `/diagnostics_agg` topic. +- `send.top_level_state`: Enables or disables subscription to the `/diagnostics_toplevel_state` topic. + +#### InfluxDB Configuration + +Set the following parameters in your configuration to match your InfluxDB instance: + +- `connection.url`: The URL of your InfluxDB write API endpoint. +- `connection.token`: Your InfluxDB authentication token. +- `connection.bucket`: The target bucket in InfluxDB. +- `connection.organization`: The name of your InfluxDB organization. + +### Starting the node + +Afterward all configurations are set run the node with the following command: + +```bash +ros2 run diagnostic_remote_logging influx --ros-args --params-file +``` + +## Using a Telegraf Proxy + +To configure Telegraf as a proxy for InfluxDB: + +1. Ensure Telegraf is set up to send data to your InfluxDB instance via its configuration file (`/etc/telegraf/telegraf.conf`). Check [this link](https://docs.influxdata.com/influxdb/cloud/write-data/no-code/use-telegraf/manual-config/) for an example. +2. Add the following to the telegraf configuration file to enable the InfluxDB v2 listener: + + ```toml + [[inputs.influxdb_v2_listener]] + service_address = ":8086" + ``` + +3. Update the `influx` node configuration to point to the appropriate URL. For example, if Telegraf is running on the same host as the `influx` node, the default `http://localhost:8086/api/v2/write` should work. + +4. Leave the following parameters empty in the `influx` node configuration when using Telegraf as a proxy: + + - `connection.token` + - `connection.bucket` + - `connection.organization` + +5. Afterwards run the node with the following command: + + ```bash + ros2 run diagnostic_remote_logging influx --ros-args --params-file + ``` diff --git a/diagnostic_remote_logging/README.md.uncrustify b/diagnostic_remote_logging/README.md.uncrustify new file mode 100644 index 000000000..e69de29bb diff --git a/diagnostic_remote_logging/include/diagnostic_remote_logging/influx_line_protocol.hpp b/diagnostic_remote_logging/include/diagnostic_remote_logging/influx_line_protocol.hpp new file mode 100644 index 000000000..3b4325411 --- /dev/null +++ b/diagnostic_remote_logging/include/diagnostic_remote_logging/influx_line_protocol.hpp @@ -0,0 +1,172 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2025, Daan Wijffels + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +/** + * \author Daan Wijffels + */ + +#ifndef DIAGNOSTIC_REMOTE_LOGGING__INFLUX_LINE_PROTOCOL_HPP_ +#define DIAGNOSTIC_REMOTE_LOGGING__INFLUX_LINE_PROTOCOL_HPP_ + +#include +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "diagnostic_msgs/msg/diagnostic_array.hpp" + +std::string toInfluxTimestamp(const rclcpp::Time & time) +{ + uint64_t seconds = static_cast(time.seconds()); + uint64_t nanoseconds = static_cast(time.nanoseconds()) % 1000000000; + + // Convert to strings + std::string secStr = std::to_string(seconds); + std::string nanosecStr = std::to_string(nanoseconds); + + // Zero-pad nanoseconds to 9 digits + nanosecStr = std::string(9 - nanosecStr.length(), '0') + nanosecStr; + + return secStr + nanosecStr; +} + +std::string escapeSpace(const std::string & input) +{ + std::string result; + for (char c : input) { + if (c == ' ') { + result += '\\'; // Add a backslash before the space + } + result += c; // Add the original character + } + return result; +} + +bool is_number(const std::string & s) +{ + std::istringstream iss(s); + double d; + return iss >> std::noskipws >> d && iss.eof(); +} + +std::string formatValues(const std::vector & values) +{ + std::string formatted; + for (const auto & kv : values) { + if (kv.value.find("\n") != std::string::npos) { + // If the value contains a newline, skip it + // InfluxDB uses this to separate measurements + continue; + } + + formatted += escapeSpace(kv.key) + "="; + + if (is_number(kv.value)) { + formatted += kv.value; + } else { + formatted += "\"" + kv.value + "\""; + } + formatted += ","; + } + if (!formatted.empty()) { + formatted.pop_back(); // Remove the last comma + } + return formatted; +} + +std::pair splitHardwareID(const std::string & input) +{ + size_t first_slash_pos = input.find('/'); + + // If no slash is found, treat the entire input as the node_name + if (first_slash_pos == std::string::npos) { + return {"none", input}; + } + + size_t second_slash_pos = input.find('/', first_slash_pos + 1); + + // If the second slash is found, extract the "ns" and "node" parts + if (second_slash_pos != std::string::npos) { + std::string ns = input.substr(first_slash_pos + 1, second_slash_pos - first_slash_pos - 1); + std::string node = input.substr(second_slash_pos + 1); + return {ns, node}; + } + + // If no second slash is found, everything after the first slash is the node + std::string node = input.substr(first_slash_pos + 1); + return {"none", node}; // ns is empty, node is the remaining string +} + +void statusToInfluxLineProtocol( + std::string & output, + const diagnostic_msgs::msg::DiagnosticStatus & status, + const std::string & timestamp_str) +{ + // hardware_id is empty for analyzer groups, so skip them + if (status.hardware_id.empty()) { + return; + } + + auto [ns, identifier] = splitHardwareID(status.hardware_id); + output += escapeSpace(identifier) + ",ns=" + escapeSpace(ns) + + " level=" + std::to_string(status.level) + ",message=\"" + status.message + "\""; + auto formatted_key_values = formatValues(status.values); + if (!formatted_key_values.empty()) { + output += "," + formatted_key_values; + } + output += " " + timestamp_str + "\n"; +} + +std::string diagnosticStatusToInfluxLineProtocol( + const diagnostic_msgs::msg::DiagnosticStatus::SharedPtr & msg, const rclcpp::Time & time) +{ + std::string output = + msg->name + " level=" + std::to_string(msg->level) + " " + toInfluxTimestamp(time) + "\n"; + return output; +} + +std::string diagnosticArrayToInfluxLineProtocol( + const diagnostic_msgs::msg::DiagnosticArray::SharedPtr & diag_msg) +{ + std::string output; + std::string timestamp = toInfluxTimestamp(diag_msg->header.stamp); + + for (auto & status : diag_msg->status) { + statusToInfluxLineProtocol(output, status, timestamp); + } + + return output; +} + +#endif // DIAGNOSTIC_REMOTE_LOGGING__INFLUX_LINE_PROTOCOL_HPP_ diff --git a/diagnostic_remote_logging/include/diagnostic_remote_logging/influxdb.hpp b/diagnostic_remote_logging/include/diagnostic_remote_logging/influxdb.hpp new file mode 100644 index 000000000..cedc4e079 --- /dev/null +++ b/diagnostic_remote_logging/include/diagnostic_remote_logging/influxdb.hpp @@ -0,0 +1,71 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2025, Daan Wijffels + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +/** + * \author Daan Wijffels + */ + +#ifndef DIAGNOSTIC_REMOTE_LOGGING__INFLUXDB_HPP_ +#define DIAGNOSTIC_REMOTE_LOGGING__INFLUXDB_HPP_ + +#include +#include + +#include "diagnostic_remote_logging/influx_line_protocol.hpp" + +#include "rclcpp/rclcpp.hpp" +#include "diagnostic_msgs/msg/diagnostic_array.hpp" + +class InfluxDB : public rclcpp::Node +{ +public: + explicit InfluxDB(const rclcpp::NodeOptions & opt); + ~InfluxDB(); + +private: + rclcpp::Subscription::SharedPtr diag_sub_; + rclcpp::Subscription::SharedPtr top_level_sub_; + + std::string post_url_, influx_token_; + CURL * curl_; + + void setupConnection(const std::string & telegraf_url); + + void diagnosticsCallback(const diagnostic_msgs::msg::DiagnosticArray::SharedPtr msg); + void topLevelCallback(const diagnostic_msgs::msg::DiagnosticStatus::SharedPtr msg); + + bool sendToInfluxDB(const std::string & data); +}; + +#endif // DIAGNOSTIC_REMOTE_LOGGING__INFLUXDB_HPP_ diff --git a/diagnostic_remote_logging/package.xml b/diagnostic_remote_logging/package.xml new file mode 100644 index 000000000..65bd41c08 --- /dev/null +++ b/diagnostic_remote_logging/package.xml @@ -0,0 +1,22 @@ + + + + diagnostic_remote_logging + 4.3.1 + diagnostic_remote_logging + Daan Wijffels + BSD-3-Clause + + ament_cmake + + diagnostic_msgs + rclcpp_components + curl + + ament_lint_auto + ament_lint_common + ament_cmake_gtest + + ament_cmake + + diff --git a/diagnostic_remote_logging/src/influxdb.cpp b/diagnostic_remote_logging/src/influxdb.cpp new file mode 100644 index 000000000..90d078cb7 --- /dev/null +++ b/diagnostic_remote_logging/src/influxdb.cpp @@ -0,0 +1,174 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2025, Daan Wijffels + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +/** + * \author Daan Wijffels + */ + +#include "diagnostic_remote_logging/influxdb.hpp" + +InfluxDB::InfluxDB(const rclcpp::NodeOptions & opt) +: Node("influxdb", opt) +{ + post_url_ = + this->declare_parameter("connection.url", "http://localhost:8086/api/v2/write"); + + if (post_url_.empty()) { + throw std::runtime_error("Parameter connection.url must be set"); + } + + std::string organization = declare_parameter("connection.organization", ""); + std::string bucket = declare_parameter("connection.bucket", ""); + influx_token_ = declare_parameter("connection.token", ""); + + // Check if any of the parameters is set + if (!organization.empty() || !bucket.empty() || !influx_token_.empty()) { + // Ensure all parameters are set + if (organization.empty() || bucket.empty() || influx_token_.empty()) { + throw std::runtime_error( + "All parameters (connection.organization, connection.bucket, connection.token) " + "must be set, or when using a proxy like Telegraf none have to be set."); + } + + // Construct the Telegraf URL + post_url_ += "?org=" + organization; + post_url_ += "&bucket=" + bucket; + } + + setupConnection(post_url_); + + if (declare_parameter("send.agg", true)) { + diag_sub_ = this->create_subscription( + "/diagnostics_agg", rclcpp::SensorDataQoS(), + std::bind(&InfluxDB::diagnosticsCallback, this, std::placeholders::_1)); + } + + if (declare_parameter("send.top_level_state", true)) { + top_level_sub_ = this->create_subscription( + "/diagnostics_toplevel_state", rclcpp::SensorDataQoS(), + std::bind(&InfluxDB::topLevelCallback, this, std::placeholders::_1)); + } +} + +void InfluxDB::diagnosticsCallback(const diagnostic_msgs::msg::DiagnosticArray::SharedPtr msg) +{ + std::string output = diagnosticArrayToInfluxLineProtocol(msg); + + if (!sendToInfluxDB(output)) { + RCLCPP_ERROR(this->get_logger(), "Failed to send /diagnostics_agg to telegraf"); + } + + RCLCPP_DEBUG(this->get_logger(), "%s", output.c_str()); +} + +void InfluxDB::topLevelCallback(const diagnostic_msgs::msg::DiagnosticStatus::SharedPtr msg) +{ + std::string output = diagnosticStatusToInfluxLineProtocol(msg, this->get_clock()->now()); + + if (!sendToInfluxDB(output)) { + RCLCPP_ERROR(this->get_logger(), "Failed to send /diagnostics_toplevel_state to telegraf"); + } + + RCLCPP_DEBUG(this->get_logger(), "%s", output.c_str()); +} + +void InfluxDB::setupConnection(const std::string & url) +{ + curl_global_init(CURL_GLOBAL_ALL); + curl_ = curl_easy_init(); + if (!curl_) { + throw std::runtime_error("Failed to initialize curl"); + } + + struct curl_slist * headers = nullptr; + + if (!influx_token_.empty()) { + headers = curl_slist_append(headers, ("Authorization: Token " + influx_token_).c_str()); + } + + headers = curl_slist_append(headers, "Content-Type: text/plain; charset=utf-8"); + headers = curl_slist_append(headers, "Accept: application/json"); + + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl_, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // For keep-alive + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, 10L); + curl_easy_setopt(curl_, CURLOPT_TCP_KEEPALIVE, 1L); + curl_easy_setopt(curl_, CURLOPT_POST, 1L); +} + +bool InfluxDB::sendToInfluxDB(const std::string & data) +{ + if (!curl_) { + RCLCPP_ERROR(this->get_logger(), "cURL not initialized."); + return false; + } + + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str()); + + // Perform the request + CURLcode res = curl_easy_perform(curl_); + + // Check for errors + if (res != CURLE_OK) { + RCLCPP_ERROR(this->get_logger(), "cURL error: %s", curl_easy_strerror(res)); + return false; + } + CURLcode response_code; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &response_code); + + if (response_code != 204) { + RCLCPP_ERROR( + this->get_logger(), "Error (%d) when sending to telegraf:\n%s", response_code, + data.c_str()); + return false; + } + + return true; +} + +InfluxDB::~InfluxDB() +{ + if (curl_) { + curl_easy_cleanup(curl_); + } + curl_global_cleanup(); +} + +#include "rclcpp_components/register_node_macro.hpp" + +// Register the component with class_loader. +// This acts as a sort of entry point, allowing the component to be discoverable when its library +// is being loaded into a running process. +RCLCPP_COMPONENTS_REGISTER_NODE(InfluxDB) diff --git a/diagnostic_remote_logging/test/influx_line_protocol.cpp b/diagnostic_remote_logging/test/influx_line_protocol.cpp new file mode 100644 index 000000000..deb0c8852 --- /dev/null +++ b/diagnostic_remote_logging/test/influx_line_protocol.cpp @@ -0,0 +1,178 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2025, Daan Wijffels + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +/** + * \author Daan Wijffels + */ + +#include "diagnostic_remote_logging/influx_line_protocol.hpp" + +#include + +#include "diagnostic_msgs/msg/diagnostic_array.hpp" +#include "diagnostic_msgs/msg/diagnostic_status.hpp" +#include "diagnostic_msgs/msg/key_value.hpp" +#include + +diagnostic_msgs::msg::KeyValue createKeyValue(const std::string & key, const std::string & value) +{ + diagnostic_msgs::msg::KeyValue output; + output.key = key; + output.value = value; + return output; +} + +// Test toInfluxTimestamp +TEST(InfluxTimestampTests, CorrectConversion) +{ + rclcpp::Time time(1672531200, 123456789); + std::string expected = "1672531200123456789"; + EXPECT_EQ(toInfluxTimestamp(time), expected); +} + +// Test escapeSpace +TEST(EscapeSpaceTests, HandlesSpaces) +{ + EXPECT_EQ(escapeSpace("test string"), "test\\ string"); + EXPECT_EQ(escapeSpace("no_space"), "no_space"); + EXPECT_EQ(escapeSpace("multiple spaces here"), "multiple\\ spaces\\ here"); +} + +// Test is_number +TEST(IsNumberTests, ValidatesNumbers) +{ + EXPECT_TRUE(is_number("123")); + EXPECT_TRUE(is_number("123.456")); + EXPECT_TRUE(is_number("-123.456")); + EXPECT_FALSE(is_number("123abc")); + EXPECT_FALSE(is_number("abc123")); + EXPECT_FALSE(is_number("")); +} + +// Test formatValues +TEST(FormatValuesTests, FormatsKeyValuePairs) +{ + std::vector values; + values.push_back(createKeyValue("key1", "value")); + values.push_back(createKeyValue("key2", "42")); + values.push_back(createKeyValue("key3", "-3.14")); + values.push_back(createKeyValue("key with spaces", "value with spaces")); + + std::string expected = + "key1=\"value\",key2=42,key3=-3.14,key\\ with\\ " + "spaces=\"value with spaces\""; + EXPECT_EQ(formatValues(values), expected); +} + +// Test splitHardwareID +TEST(SplitHardwareIDTests, SplitsCorrectly) +{ + EXPECT_EQ( + splitHardwareID("node_name"), std::make_pair( + std::string("none"), std::string( + "node_" + "name"))); + EXPECT_EQ( + splitHardwareID("/ns/node_name"), std::make_pair( + std::string("ns"), std::string( + "node_" + "nam" + "e"))); + EXPECT_EQ( + splitHardwareID("/ns/prefix/node_name"), + std::make_pair( + std::string("ns"), std::string( + "prefix/" + "node_name"))); +} + +// Test statusToInfluxLineProtocol +TEST(StatusToInfluxLineProtocolTests, FormatsCorrectly) +{ + diagnostic_msgs::msg::DiagnosticStatus status; + status.hardware_id = "/ns/node_name"; + status.level = 2; + status.message = "Test message"; + status.values.push_back(createKeyValue("key1", "value1")); + status.values.push_back(createKeyValue("key2", "42")); + + std::string expected = + "node_name,ns=ns level=2,message=\"Test message\",key1=\"value1\",key2=42" + " 1672531200123456789\n"; + std::string output; + statusToInfluxLineProtocol(output, status, "1672531200123456789"); + + EXPECT_EQ(output, expected); +} + +// Test diagnosticArrayToInfluxLineProtocol +TEST(DiagnosticArrayToInfluxLineProtocolTests, HandlesMultipleStatuses) +{ + auto diag_msg = std::make_shared(); + diag_msg->header.stamp = rclcpp::Time(1672531200, 123456789); + + diagnostic_msgs::msg::DiagnosticStatus status1; + status1.hardware_id = "/ns1/node1"; + status1.level = 1; + status1.message = "First status"; + status1.values.push_back(createKeyValue("keyA", "valueA")); + + diagnostic_msgs::msg::DiagnosticStatus status2; + status2.hardware_id = "node2"; + status2.level = 2; + status2.message = "Second status"; + status2.values.push_back(createKeyValue("keyB", "42")); + + diag_msg->status = {status1, status2}; + + std::string expected = + "node1,ns=ns1 level=1,message=\"First " + "status\",keyA=\"valueA\" 1672531200123456789\n" + "node2,ns=none level=2,message=\"Second " + "status\",keyB=42 1672531200123456789\n"; + + EXPECT_EQ(diagnosticArrayToInfluxLineProtocol(diag_msg), expected); +} + +// Test diagnosticStatusToInfluxLineProtocol +TEST(DiagnosticStatusToInfluxLineProtocol, HandlesSingleStatus) +{ + auto status = std::make_shared(); + status->level = 1; + status->name = "toplevel_state"; + auto time = rclcpp::Time(1672531200, 123456789); + + std::string expected = "toplevel_state level=1 1672531200123456789\n"; + EXPECT_EQ(diagnosticStatusToInfluxLineProtocol(status, time), expected); +} From fd95a4cddb015c4dc484093695a677f39aa7ca21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Fr=C3=B6hlich?= Date: Wed, 12 Mar 2025 15:53:46 +0100 Subject: [PATCH 30/34] Don't use `match` (#442) --- .../diagnostic_common_diagnostics/hd_monitor.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py index 3b577f1cb..29c4cdcb2 100755 --- a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py +++ b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py @@ -101,15 +101,14 @@ def callback_config(self, params: List[rclpy.Parameter]): see the class documentation for declared parameters. """ for param in params: - match param.name: - case 'path': - self._path = str( - Path(param.value).expanduser().resolve(strict=True) - ) - case 'free_percent_low': - self._free_percent_low = param.value - case 'free_percent_crit': - self._free_percent_crit = param.value + if param.name == 'path': + self._path = str( + Path(param.value).expanduser().resolve(strict=True) + ) + elif param.name == 'free_percent_low': + self._free_percent_low = param.value + elif param.name == 'free_percent_crit': + self._free_percent_crit = param.value return SetParametersResult(successful=True) From 80a3b796d70e844c85cdad150c0345c49e4f55ee Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Wed, 12 Mar 2025 18:05:18 +0100 Subject: [PATCH 31/34] cleanup (#450) (#451) Signed-off-by: Christian Henkel (cherry picked from commit ee4dd2401774a5a1765add9b4c21f22010987893) --- .../CMakeLists.txt.uncrustify | 52 ------------------- .../CMakeLists.txt.uncrustify.uncrustify | 52 ------------------- .../README.md.uncrustify | 0 3 files changed, 104 deletions(-) delete mode 100644 diagnostic_remote_logging/CMakeLists.txt.uncrustify delete mode 100644 diagnostic_remote_logging/CMakeLists.txt.uncrustify.uncrustify delete mode 100644 diagnostic_remote_logging/README.md.uncrustify diff --git a/diagnostic_remote_logging/CMakeLists.txt.uncrustify b/diagnostic_remote_logging/CMakeLists.txt.uncrustify deleted file mode 100644 index 39ed0e439..000000000 --- a/diagnostic_remote_logging/CMakeLists.txt.uncrustify +++ /dev/null @@ -1,52 +0,0 @@ -cmake_minimum_required(VERSION 3.8) - -project(diagnostic_remote_logging) -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall - Wextra - Wpedantic) - endif() - - set(dependencies - ament_cmake - rclcpp - rclcpp_components - diagnostic_msgs - CURL - ) - - foreach(dep $ {dependencies}) - find_package($ {dep} REQUIRED) - endforeach(dep) - - include_directories( - src / - include / - ) - add_library(influx_component SHARED src / influxdb.cpp) - - ament_target_dependencies(influx_component $ {dependencies}) - - ament_export_dependencies(influx_component $ {dependencies}) - - target_compile_features(influx_component PUBLIC c_std_99 cxx_std_17) - - rclcpp_components_register_node( - influx_component - PLUGIN "InfluxDB" - EXECUTABLE influx - ) - ament_export_targets(export_influx_component) - install(TARGETS influx_component - EXPORT export_influx_component - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin - ) - - if(BUILD_TESTING) - find_package(ament_cmake_gtest REQUIRED) - ament_add_gtest(unit_tests test / influx_line_protocol.cpp) - target_include_directories(unit_tests PRIVATE include) - target_link_libraries(unit_tests influx_component) - ament_target_dependencies(unit_tests $ {dependencies} ament_cmake_gtest) - endif() - ament_package() diff --git a/diagnostic_remote_logging/CMakeLists.txt.uncrustify.uncrustify b/diagnostic_remote_logging/CMakeLists.txt.uncrustify.uncrustify deleted file mode 100644 index 39ed0e439..000000000 --- a/diagnostic_remote_logging/CMakeLists.txt.uncrustify.uncrustify +++ /dev/null @@ -1,52 +0,0 @@ -cmake_minimum_required(VERSION 3.8) - -project(diagnostic_remote_logging) -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall - Wextra - Wpedantic) - endif() - - set(dependencies - ament_cmake - rclcpp - rclcpp_components - diagnostic_msgs - CURL - ) - - foreach(dep $ {dependencies}) - find_package($ {dep} REQUIRED) - endforeach(dep) - - include_directories( - src / - include / - ) - add_library(influx_component SHARED src / influxdb.cpp) - - ament_target_dependencies(influx_component $ {dependencies}) - - ament_export_dependencies(influx_component $ {dependencies}) - - target_compile_features(influx_component PUBLIC c_std_99 cxx_std_17) - - rclcpp_components_register_node( - influx_component - PLUGIN "InfluxDB" - EXECUTABLE influx - ) - ament_export_targets(export_influx_component) - install(TARGETS influx_component - EXPORT export_influx_component - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin - ) - - if(BUILD_TESTING) - find_package(ament_cmake_gtest REQUIRED) - ament_add_gtest(unit_tests test / influx_line_protocol.cpp) - target_include_directories(unit_tests PRIVATE include) - target_link_libraries(unit_tests influx_component) - ament_target_dependencies(unit_tests $ {dependencies} ament_cmake_gtest) - endif() - ament_package() diff --git a/diagnostic_remote_logging/README.md.uncrustify b/diagnostic_remote_logging/README.md.uncrustify deleted file mode 100644 index e69de29bb..000000000 From 590682f8dbe2d31b56a8cdbf312d1688ba90a9d6 Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Wed, 12 Mar 2025 18:34:11 +0100 Subject: [PATCH 32/34] Fix reported hd_monitor available value (#443) (#454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use percentage for HD usage instead of ratio * Update readme * Use global vars as default values * Fix test * Add ParameterDescriptor --------- Signed-off-by: Christian Henkel Co-authored-by: Christian Henkel (cherry picked from commit 207f0c0f1f8e9bd1842a07d90459aa9a2b670c23) Co-authored-by: Christoph Fröhlich --- diagnostic_common_diagnostics/README.md | 10 +++++++- .../hd_monitor.py | 25 +++++++++++-------- .../systemtest/test_hd_monitor_launchtest.py | 2 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/diagnostic_common_diagnostics/README.md b/diagnostic_common_diagnostics/README.md index fdece7f82..40857d353 100644 --- a/diagnostic_common_diagnostics/README.md +++ b/diagnostic_common_diagnostics/README.md @@ -67,7 +67,7 @@ Computer name in diagnostics output (ex: 'c1') Disable self test. ## hd_monitor.py -Runs 'shutil.disk_usage' to check if there is enough space left on a given device. +Runs 'shutil.disk_usage' to check if there is enough space left on a given device. With default parameters, the following thresholds are used: * Above 5% of free space left, an `OK` status will be published. * Between 5% and 1%, a `WARN` status will be published, * Below 1%, an `ERROR` status will be published. @@ -82,6 +82,14 @@ The diagnostics information. (default: home directory "~") Path in which to check remaining space. +#### free_percent_low +(default: 5%) +Warning threshold. + +#### free_percent_crit +(default: 1%) +Error threshold. + ## ram_monitor.py The `ram_monitor` module allows users to monitor the RAM usage of their system in real-time. It publishes the usage percentage in a diagnostic message. diff --git a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py index 29c4cdcb2..14b968a71 100755 --- a/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py +++ b/diagnostic_common_diagnostics/diagnostic_common_diagnostics/hd_monitor.py @@ -44,13 +44,13 @@ from diagnostic_msgs.msg import DiagnosticStatus, KeyValue from diagnostic_updater import Updater -from rcl_interfaces.msg import SetParametersResult +from rcl_interfaces.msg import ParameterDescriptor, SetParametersResult import rclpy from rclpy.node import Node -FREE_PERCENT_LOW = 0.05 -FREE_PERCENT_CRIT = 0.01 +FREE_PERCENT_LOW = 5 +FREE_PERCENT_CRIT = 1 DICT_STATUS = { DiagnosticStatus.OK: 'OK', DiagnosticStatus.WARN: 'Warning', @@ -82,13 +82,18 @@ def __init__(self): super().__init__(f'hd_monitor_{cleaned_hostname}') self._path = '~' - self._free_percent_low = 0.05 - self._free_percent_crit = 0.01 + self._free_percent_low = FREE_PERCENT_LOW + self._free_percent_crit = FREE_PERCENT_CRIT self.add_on_set_parameters_callback(self.callback_config) - self.declare_parameter('path', self._path) - self.declare_parameter('free_percent_low', self._free_percent_low) - self.declare_parameter('free_percent_crit', self._free_percent_crit) + self.declare_parameter('path', self._path, ParameterDescriptor( + description='Path in which to check remaining space.')) + self.declare_parameter( + 'free_percent_low', self._free_percent_low, ParameterDescriptor( + description='Warning threshold.', type=int())) + self.declare_parameter( + 'free_percent_crit', self._free_percent_crit, ParameterDescriptor( + description='Error threshold.', type=int())) self._updater = Updater(self) self._updater.setHardwareID(hostname) @@ -121,7 +126,7 @@ def check_disk_usage(self, diag: DiagnosticStatus) -> DiagnosticStatus: diag.level = DiagnosticStatus.OK total, _, free = disk_usage(self._path) - percent = free / total + percent = free / total * 100.0 if percent > self._free_percent_low: diag.level = DiagnosticStatus.OK @@ -136,7 +141,7 @@ def check_disk_usage(self, diag: DiagnosticStatus) -> DiagnosticStatus: KeyValue(key='Name', value=self._path), KeyValue(key='Status', value=DICT_STATUS[diag.level]), KeyValue(key='Total (Go)', value=str(total_go)), - KeyValue(key='Available (%)', value=str(round(percent, 2))), + KeyValue(key='Available (%)', value=str(round(percent, 1))), ] ) diff --git a/diagnostic_common_diagnostics/test/systemtest/test_hd_monitor_launchtest.py b/diagnostic_common_diagnostics/test/systemtest/test_hd_monitor_launchtest.py index 9086c5d75..792301985 100644 --- a/diagnostic_common_diagnostics/test/systemtest/test_hd_monitor_launchtest.py +++ b/diagnostic_common_diagnostics/test/systemtest/test_hd_monitor_launchtest.py @@ -59,7 +59,7 @@ def generate_test_description(): executable='hd_monitor.py', name='hd_monitor', output='screen', - parameters=[{'free_percent_low': 0.20, 'free_percent_crit': 0.05}], + parameters=[{'free_percent_low': 10, 'free_percent_crit': 5}], ), launch_testing.actions.ReadyToTest(), ] From 4ab7e6683a33edb748f0ec4e867387826c817679 Mon Sep 17 00:00:00 2001 From: redvinaa Date: Tue, 1 Apr 2025 14:05:55 +0000 Subject: [PATCH 33/34] Propagate clock instead of creating default --- .../diagnostic_aggregator/analyzer.hpp | 3 +-- .../generic_analyzer.hpp | 1 + .../generic_analyzer_base.hpp | 4 ++- .../diagnostic_aggregator/other_analyzer.hpp | 27 +++---------------- .../diagnostic_aggregator/status_item.hpp | 5 +++- diagnostic_aggregator/src/aggregator.cpp | 8 +++--- diagnostic_aggregator/src/analyzer_group.cpp | 7 ++--- .../src/generic_analyzer.cpp | 8 +++--- diagnostic_aggregator/src/status_item.cpp | 14 +++++++--- 9 files changed, 36 insertions(+), 41 deletions(-) diff --git a/diagnostic_aggregator/include/diagnostic_aggregator/analyzer.hpp b/diagnostic_aggregator/include/diagnostic_aggregator/analyzer.hpp index e64d79a91..187f35159 100644 --- a/diagnostic_aggregator/include/diagnostic_aggregator/analyzer.hpp +++ b/diagnostic_aggregator/include/diagnostic_aggregator/analyzer.hpp @@ -94,8 +94,7 @@ class Analyzer /*! *\brief Default constructor, called by pluginlib. */ - Analyzer() - : clock_(std::make_shared()) {} + Analyzer() {} virtual ~Analyzer() {} diff --git a/diagnostic_aggregator/include/diagnostic_aggregator/generic_analyzer.hpp b/diagnostic_aggregator/include/diagnostic_aggregator/generic_analyzer.hpp index 7496780b0..4899f52ea 100644 --- a/diagnostic_aggregator/include/diagnostic_aggregator/generic_analyzer.hpp +++ b/diagnostic_aggregator/include/diagnostic_aggregator/generic_analyzer.hpp @@ -229,6 +229,7 @@ class GenericAnalyzer : public GenericAnalyzerBase virtual bool match(const std::string & name); private: + rclcpp::Node::SharedPtr node_; std::vector chaff_; /**< Removed from the start of node names. */ std::vector expected_; std::vector startswith_; diff --git a/diagnostic_aggregator/include/diagnostic_aggregator/generic_analyzer_base.hpp b/diagnostic_aggregator/include/diagnostic_aggregator/generic_analyzer_base.hpp index d7cb8a485..9f1193341 100644 --- a/diagnostic_aggregator/include/diagnostic_aggregator/generic_analyzer_base.hpp +++ b/diagnostic_aggregator/include/diagnostic_aggregator/generic_analyzer_base.hpp @@ -101,7 +101,8 @@ class GenericAnalyzerBase : public Analyzer * Must be initialized in order to prepend the path to all outgoing status messages. */ bool init( - const std::string & path, const std::string & breadcrumb, double timeout = -1.0, + const std::string & path, const std::string & breadcrumb, + const rclcpp::Node::SharedPtr node, double timeout = -1.0, int num_items_expected = -1, bool discard_stale = false) { num_items_expected_ = num_items_expected; @@ -109,6 +110,7 @@ class GenericAnalyzerBase : public Analyzer path_ = path + "/" + nice_name_; discard_stale_ = discard_stale; breadcrumb_ = breadcrumb; + clock_ = node->get_clock(); if (discard_stale_ && timeout <= 0) { RCLCPP_WARN( diff --git a/diagnostic_aggregator/include/diagnostic_aggregator/other_analyzer.hpp b/diagnostic_aggregator/include/diagnostic_aggregator/other_analyzer.hpp index 67d0f1b0a..d7d421ae5 100644 --- a/diagnostic_aggregator/include/diagnostic_aggregator/other_analyzer.hpp +++ b/diagnostic_aggregator/include/diagnostic_aggregator/other_analyzer.hpp @@ -85,34 +85,15 @@ class OtherAnalyzer : public GenericAnalyzerBase *\param path Base path of Aggregator *\param breadcrumb Prefix for parameter getter. */ - bool init(const std::string & path, const std::string & breadcrumb = "") + bool init( + const std::string & path, const std::string & breadcrumb, + const rclcpp::Node::SharedPtr node) { (void)breadcrumb; nice_name_ = "Other"; path_ = path; - return GenericAnalyzerBase::init(path_, "", 5.0, -1, true); - } - - /* - *\brief OtherAnalyzer cannot be initialized with a NodeHandle - * - *\return False, since NodeHandle initialization isn't valid - */ - bool init( - const std::string & base_path, const std::string & breadcrumb, - const rclcpp::Node::SharedPtr node) - { - (void)base_path; - (void)breadcrumb; - (void)node; - - RCLCPP_ERROR( - rclcpp::get_logger( - "generic_analyzer_base"), - R"(OtherAnalyzer was attempted to initialize with a NodeHandle. - This analyzer cannot be used as a plugin.)"); - return false; + return GenericAnalyzerBase::init(path_, "", node, 5.0, -1, true); } /* diff --git a/diagnostic_aggregator/include/diagnostic_aggregator/status_item.hpp b/diagnostic_aggregator/include/diagnostic_aggregator/status_item.hpp index e11dad5d6..3c24590b2 100644 --- a/diagnostic_aggregator/include/diagnostic_aggregator/status_item.hpp +++ b/diagnostic_aggregator/include/diagnostic_aggregator/status_item.hpp @@ -187,13 +187,16 @@ class StatusItem *\brief Constructed from const DiagnosticStatus* */ DIAGNOSTIC_AGGREGATOR_PUBLIC - explicit StatusItem(const diagnostic_msgs::msg::DiagnosticStatus * status); + StatusItem( + const diagnostic_msgs::msg::DiagnosticStatus * status, + rclcpp::Clock::SharedPtr clock); /*! *\brief Constructed from string of item name */ DIAGNOSTIC_AGGREGATOR_PUBLIC StatusItem( + rclcpp::Clock::SharedPtr clock, const std::string item_name, const std::string message = "Missing", const DiagnosticLevel level = Level_Stale); diff --git a/diagnostic_aggregator/src/aggregator.cpp b/diagnostic_aggregator/src/aggregator.cpp index 6b60eee32..9f0079318 100644 --- a/diagnostic_aggregator/src/aggregator.cpp +++ b/diagnostic_aggregator/src/aggregator.cpp @@ -83,8 +83,8 @@ Aggregator::Aggregator(rclcpp::NodeOptions options) n_->create_publisher("/diagnostics_toplevel_state", 1); int publish_rate_ms = 1000 / pub_rate_; - publish_timer_ = n_->create_wall_timer( - std::chrono::milliseconds(publish_rate_ms), + publish_timer_ = rclcpp::create_timer( + n_, clock_, std::chrono::milliseconds(publish_rate_ms), std::bind(&Aggregator::publishData, this)); param_sub_ = n_->create_subscription( @@ -144,7 +144,7 @@ void Aggregator::initAnalyzers() // Last analyzer handles remaining data other_analyzer_ = std::make_unique(other_as_errors); - other_analyzer_->init(base_path_); // This always returns true + other_analyzer_->init(base_path_, "", n_); // This always returns true } } @@ -181,7 +181,7 @@ void Aggregator::diagCallback(const DiagnosticArray::SharedPtr diag_msg) std::lock_guard lock(mutex_); for (auto j = 0u; j < diag_msg->status.size(); ++j) { analyzed = false; - auto item = std::make_shared(&diag_msg->status[j]); + auto item = std::make_shared(&diag_msg->status[j], n_->get_clock()); if (analyzer_group_->match(item->getName())) { analyzed = analyzer_group_->analyze(item); diff --git a/diagnostic_aggregator/src/analyzer_group.cpp b/diagnostic_aggregator/src/analyzer_group.cpp index 0873ac4c5..ee62aa91a 100644 --- a/diagnostic_aggregator/src/analyzer_group.cpp +++ b/diagnostic_aggregator/src/analyzer_group.cpp @@ -66,6 +66,7 @@ bool AnalyzerGroup::init( path_ = path; breadcrumb_ = breadcrumb; nice_name_ = path; + clock_ = n->get_clock(); std::map parameters; if (!n->get_parameters(breadcrumb_, parameters)) { @@ -128,7 +129,7 @@ bool AnalyzerGroup::init( RCLCPP_ERROR( logger_, "Failed to load analyzer %s, type %s. Caught exception: %s", ns.c_str(), an_type.c_str(), e.what()); - auto item = std::make_shared(ns, "Pluginlib exception loading analyzer"); + auto item = std::make_shared(n->get_clock(), ns, "Pluginlib exception loading analyzer"); aux_items_.push_back(item); init_ok = false; continue; @@ -139,7 +140,7 @@ bool AnalyzerGroup::init( logger_, "Pluginlib returned a null analyzer for %s, namespace %s.", an_type.c_str(), n->get_namespace()); std::shared_ptr item( - new StatusItem(ns, "Pluginlib return NULL Analyzer for " + an_type)); + new StatusItem(n->get_clock(), ns, "Pluginlib return NULL Analyzer for " + an_type)); aux_items_.push_back(item); init_ok = false; continue; @@ -158,7 +159,7 @@ bool AnalyzerGroup::init( RCLCPP_ERROR( logger_, "Unable to initialize analyzer NS: %s, type: %s", n->get_namespace(), an_type.c_str()); - std::shared_ptr item(new StatusItem(ns, "Analyzer init failed")); + std::shared_ptr item(new StatusItem(n->get_clock(), ns, "Analyzer init failed")); aux_items_.push_back(item); init_ok = false; continue; diff --git a/diagnostic_aggregator/src/generic_analyzer.cpp b/diagnostic_aggregator/src/generic_analyzer.cpp index 6fa8efddd..85766431e 100644 --- a/diagnostic_aggregator/src/generic_analyzer.cpp +++ b/diagnostic_aggregator/src/generic_analyzer.cpp @@ -56,9 +56,11 @@ GenericAnalyzer::GenericAnalyzer() {} bool GenericAnalyzer::init( const std::string & path, const std::string & breadcrumb, const rclcpp::Node::SharedPtr n) { + node_ = n; path_ = path; breadcrumb_ = breadcrumb; nice_name_ = breadcrumb; + clock_ = n->get_clock(); RCLCPP_DEBUG( rclcpp::get_logger("GenericAnalyzer"), "GenericAnalyzer, breadcrumb: %s", breadcrumb_.c_str()); @@ -114,7 +116,7 @@ bool GenericAnalyzer::init( rclcpp::get_logger("GenericAnalyzer"), "GenericAnalyzer '%s' found expected: %s", nice_name_.c_str(), pvalue.value_to_string().c_str()); for (auto exp : pvalue.as_string_array()) { - auto item = std::make_shared(exp); + auto item = std::make_shared(n->get_clock(), exp); this->addItem(exp, item); } } else if (pname.compare("regex") == 0) { @@ -178,7 +180,7 @@ bool GenericAnalyzer::init( my_path = "/" + my_path; } - return GenericAnalyzerBase::init(path_, breadcrumb_, timeout, num_items_expected, discard_stale); + return GenericAnalyzerBase::init(path_, breadcrumb_, node_, timeout, num_items_expected, discard_stale); } GenericAnalyzer::~GenericAnalyzer() {} @@ -282,7 +284,7 @@ vector> GenericAnalyzer: // Add missing names to header ... for (unsigned int i = 0; i < expected_names_missing.size(); ++i) { - std::shared_ptr item(new StatusItem(expected_names_missing[i])); + std::shared_ptr item(new StatusItem(node_->get_clock(), expected_names_missing[i])); processed.push_back(item->toStatusMsg(path_, true)); } diff --git a/diagnostic_aggregator/src/status_item.cpp b/diagnostic_aggregator/src/status_item.cpp index 23921301c..5a33844c8 100644 --- a/diagnostic_aggregator/src/status_item.cpp +++ b/diagnostic_aggregator/src/status_item.cpp @@ -45,8 +45,10 @@ using std::string; using rclcpp::get_logger; -StatusItem::StatusItem(const diagnostic_msgs::msg::DiagnosticStatus * status) -: clock_(new rclcpp::Clock()) +StatusItem::StatusItem( + const diagnostic_msgs::msg::DiagnosticStatus * status, + rclcpp::Clock::SharedPtr clock) +: clock_(clock) { level_ = valToLevel(status->level); name_ = status->name; @@ -59,8 +61,12 @@ StatusItem::StatusItem(const diagnostic_msgs::msg::DiagnosticStatus * status) update_time_ = clock_->now(); } -StatusItem::StatusItem(const string item_name, const string message, const DiagnosticLevel level) -: clock_(new rclcpp::Clock()) +StatusItem::StatusItem( + rclcpp::Clock::SharedPtr clock, + const string item_name, + const string message, + const DiagnosticLevel level) +: clock_(clock) { RCLCPP_DEBUG(rclcpp::get_logger("StatusItem"), "StatusItem constructor from string"); name_ = item_name; From a5e2a3d784cba7a7417794dbcb3c346a31739e18 Mon Sep 17 00:00:00 2001 From: redvinaa Date: Fri, 4 Apr 2025 10:52:21 +0200 Subject: [PATCH 34/34] Lint --- diagnostic_aggregator/src/analyzer_group.cpp | 6 ++++-- diagnostic_aggregator/src/generic_analyzer.cpp | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/diagnostic_aggregator/src/analyzer_group.cpp b/diagnostic_aggregator/src/analyzer_group.cpp index ee62aa91a..9a564c93f 100644 --- a/diagnostic_aggregator/src/analyzer_group.cpp +++ b/diagnostic_aggregator/src/analyzer_group.cpp @@ -129,7 +129,8 @@ bool AnalyzerGroup::init( RCLCPP_ERROR( logger_, "Failed to load analyzer %s, type %s. Caught exception: %s", ns.c_str(), an_type.c_str(), e.what()); - auto item = std::make_shared(n->get_clock(), ns, "Pluginlib exception loading analyzer"); + auto item = std::make_shared( + n->get_clock(), ns, "Pluginlib exception loading analyzer"); aux_items_.push_back(item); init_ok = false; continue; @@ -159,7 +160,8 @@ bool AnalyzerGroup::init( RCLCPP_ERROR( logger_, "Unable to initialize analyzer NS: %s, type: %s", n->get_namespace(), an_type.c_str()); - std::shared_ptr item(new StatusItem(n->get_clock(), ns, "Analyzer init failed")); + std::shared_ptr item( + new StatusItem(n->get_clock(), ns, "Analyzer init failed")); aux_items_.push_back(item); init_ok = false; continue; diff --git a/diagnostic_aggregator/src/generic_analyzer.cpp b/diagnostic_aggregator/src/generic_analyzer.cpp index 85766431e..8eef914e6 100644 --- a/diagnostic_aggregator/src/generic_analyzer.cpp +++ b/diagnostic_aggregator/src/generic_analyzer.cpp @@ -180,7 +180,8 @@ bool GenericAnalyzer::init( my_path = "/" + my_path; } - return GenericAnalyzerBase::init(path_, breadcrumb_, node_, timeout, num_items_expected, discard_stale); + return GenericAnalyzerBase::init( + path_, breadcrumb_, node_, timeout, num_items_expected, discard_stale); } GenericAnalyzer::~GenericAnalyzer() {}