From 300f8489b27c6a88aade085d8552e595bcd1e100 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 13 Jan 2026 09:39:22 -0800 Subject: [PATCH 01/33] Add pre-commit for linting locally Signed-off-by: Blake McHale --- .pre-commit-config.yaml | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fdd8649 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +exclude: ^third_party/ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/google/yapf + rev: v0.43.0 + hooks: + - id: yapf +- repo: https://github.com/scop/pre-commit-shfmt + rev: v3.12.0-2 + hooks: + - id: shfmt +- repo: https://github.com/sbrunner/hooks + rev: 1.6.1 + hooks: + - id: copyright-required + exclude: '(\.ini|\.json|\.service|__init__\.py|\.md|\.gitkeep|\.conf|LICENSE|\.toml|\.template|\.style\..*|\.gitattributes|\.gitignore|\.editorconfig|\.bash-completion|\.install|\.links|changelog|debian/source/format|.codespellrc|copyright)$' +- repo: local + hooks: + - id: ament-uncrustify + name: ament-uncrustify + entry: uncrustify -c ament_code_style.cfg --replace --no-backup + language: system + types_or: [c, c++] +- repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell From 5ba1541910f605a1647a80a5e359875bdc3d2d75 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 13 Jan 2026 17:16:38 -0800 Subject: [PATCH 02/33] Beginning of linting journey Signed-off-by: Blake McHale --- .github/workflows/debian-packages.yml | 4 +- .github/workflows/ros-tests.yml | 4 +- .gitignore | 2 +- .pre-commit-config.yaml | 12 +- Contributing.md | 2 +- README.md | 2 +- greenwave_monitor/examples/README.md | 2 +- greenwave_monitor/examples/example.launch.py | 35 +++-- .../greenwave_monitor/ncurses_frontend.py | 107 +++++++------ .../greenwave_monitor/test_utils.py | 127 ++++++++------- .../greenwave_monitor/ui_adaptor.py | 44 +++--- greenwave_monitor/launch/hz.launch.py | 2 +- .../launch/test_publishers.launch.py | 35 +++-- greenwave_monitor/scripts/ncurses_dashboard | 130 ++++++++-------- .../test/test_greenwave_monitor.py | 122 ++++++++------- .../test/test_ncurses_frontend_argparse.py | 6 +- .../test/test_topic_monitoring_integration.py | 146 ++++++++++-------- greenwave_monitor_interfaces/CMakeLists.txt | 4 +- .../srv/ManageTopic.srv | 4 +- .../srv/SetExpectedFrequency.srv | 4 +- scripts/build_debian_packages.sh | 124 ++++++++------- scripts/docker-test.sh | 60 +++---- 22 files changed, 519 insertions(+), 459 deletions(-) diff --git a/.github/workflows/debian-packages.yml b/.github/workflows/debian-packages.yml index 77e948c..da25bd7 100644 --- a/.github/workflows/debian-packages.yml +++ b/.github/workflows/debian-packages.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -318,4 +318,4 @@ jobs: sleep 2 kill -9 $(cat /tmp/gwm.pid) 2>/dev/null || true ros2 daemon stop || true - shell: bash \ No newline at end of file + shell: bash diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index b2e4d5e..6ddca4b 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -138,4 +138,4 @@ jobs: with: name: test-results-${{ matrix.ros_distro }} path: build/*/test_results/**/*.xml - retention-days: 7 \ No newline at end of file + retention-days: 7 diff --git a/.gitignore b/.gitignore index 5b49528..803c0d1 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,4 @@ _deps # Temporary files *.tmp -*.temp \ No newline at end of file +*.temp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fdd8649..862ebe7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,13 +33,11 @@ repos: hooks: - id: copyright-required exclude: '(\.ini|\.json|\.service|__init__\.py|\.md|\.gitkeep|\.conf|LICENSE|\.toml|\.template|\.style\..*|\.gitattributes|\.gitignore|\.editorconfig|\.bash-completion|\.install|\.links|changelog|debian/source/format|.codespellrc|copyright)$' -- repo: local +- repo: https://github.com/pocc/pre-commit-hooks + rev: v1.3.5 hooks: - - id: ament-uncrustify - name: ament-uncrustify - entry: uncrustify -c ament_code_style.cfg --replace --no-backup - language: system - types_or: [c, c++] + - id: uncrustify + args: [-c, ament_code_style.cfg] - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: diff --git a/Contributing.md b/Contributing.md index dde649a..0c46b20 100644 --- a/Contributing.md +++ b/Contributing.md @@ -1,4 +1,4 @@ -We welcome PRs for new features or bugfixes, CI will automatically run automated tests on new PRs. You can also use the scripts/docker-test.sh to debug tests for a particular distribution locally. +We welcome PRs for new features or bugfixes, CI will automatically run automated tests on new PRs. You can also use the scripts/docker-test.sh to debug tests for a particular distribution locally. We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. diff --git a/README.md b/README.md index c926a89..b4011f2 100644 --- a/README.md +++ b/README.md @@ -109,4 +109,4 @@ If you want to use it as a command line tool, you can do so with the following l ```bash ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' -``` \ No newline at end of file +``` diff --git a/greenwave_monitor/examples/README.md b/greenwave_monitor/examples/README.md index cfa4518..15651c2 100644 --- a/greenwave_monitor/examples/README.md +++ b/greenwave_monitor/examples/README.md @@ -31,4 +31,4 @@ Node( ), ``` -To see the output with the r2s_gw dashboard, run `ros2 run r2s_gw r2s_gw` in a separate terminal. \ No newline at end of file +To see the output with the r2s_gw dashboard, run `ros2 run r2s_gw r2s_gw` in a separate terminal. diff --git a/greenwave_monitor/examples/example.launch.py b/greenwave_monitor/examples/example.launch.py index d479205..7772574 100644 --- a/greenwave_monitor/examples/example.launch.py +++ b/greenwave_monitor/examples/example.launch.py @@ -30,33 +30,39 @@ def generate_launch_description(): executable='minimal_publisher_node', name='minimal_publisher1', output='log', - parameters=[ - {'topic': 'imu_topic', 'frequency_hz': 100.0} - ], + parameters=[{ + 'topic': 'imu_topic', + 'frequency_hz': 100.0 + }], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher2', output='log', - parameters=[ - {'topic': 'image_topic', 'message_type': 'image', 'frequency_hz': 30.0} - ], + parameters=[{ + 'topic': 'image_topic', + 'message_type': 'image', + 'frequency_hz': 30.0 + }], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher3', output='log', - parameters=[ - {'topic': 'string_topic', 'message_type': 'string', 'frequency_hz': 1000.0} - ], + parameters=[{ + 'topic': 'string_topic', + 'message_type': 'string', + 'frequency_hz': 1000.0 + }], ), Node( package='greenwave_monitor', executable='greenwave_monitor', name='greenwave_monitor', output='log', +<<<<<<< HEAD # Example of inline parameter settings # parameters=[{ # 'gw_monitored_topics': ['/string_topic'], @@ -67,9 +73,14 @@ def generate_launch_description(): # }], # Example of using a config file parameters=[config_file], +======= + parameters=[{ + 'topics': ['/imu_topic', '/image_topic', '/string_topic'] + }], +>>>>>>> 3f640d3 (Beginning of linting journey) ), LogInfo( - msg='Run `ros2 run r2s_gw r2s_gw` in another terminal to see the demo output ' - 'with the r2s dashboard.' - ), + msg= + 'Run `ros2 run r2s_gw r2s_gw` in another terminal to see the demo output ' + 'with the r2s dashboard.'), ]) diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index a50d231..06c9636 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 - """ Ncurses-based frontend for Greenwave Monitor. @@ -66,7 +65,8 @@ def __init__(self, hide_unmonitored: bool = False): self.ui_adaptor = GreenwaveUiAdaptor(self) # Timer to periodically update the topic list - self.topic_update_timer = self.create_timer(5.0, self.update_topic_list) + self.topic_update_timer = self.create_timer(5.0, + self.update_topic_list) # Initial topic list update self.update_topic_list() @@ -80,10 +80,14 @@ def update_topic_list(self): topic_names_and_types = [ (topic_name, topic_type) for topic_name, topic_type in topic_names_and_types - if (not topic_name.endswith('/nitros')) and ('/nitros/' not in topic_name) + if (not topic_name.endswith('/nitros')) and ( + '/nitros/' not in topic_name) ] - topic_set = {topic_name for topic_name, topic_type in topic_names_and_types} + topic_set = { + topic_name + for topic_name, topic_type in topic_names_and_types + } with self.topics_lock: # Update topics @@ -172,7 +176,8 @@ def curses_main(stdscr, node): key = -1 # Only redraw if we got input or enough time has passed - should_redraw = (key != -1) or (current_time - last_redraw >= redraw_interval) + should_redraw = (key != -1) or (current_time - last_redraw + >= redraw_interval) if not should_redraw: time.sleep(0.01) @@ -189,9 +194,9 @@ def curses_main(stdscr, node): STATUS_WIDTH = 18 BUTTON_WIDTH = 10 - total_width_needed = ( - MAX_NAME_WIDTH + 2 * FRAME_RATE_WIDTH + REALTIME_DELAY_WIDTH + - STATUS_WIDTH + BUTTON_WIDTH + 5) + total_width_needed = (MAX_NAME_WIDTH + 2 * FRAME_RATE_WIDTH + + REALTIME_DELAY_WIDTH + STATUS_WIDTH + + BUTTON_WIDTH + 5) if total_width_needed > width: scaling_factor = width / total_width_needed MAX_NAME_WIDTH = int(MAX_NAME_WIDTH * scaling_factor) @@ -200,12 +205,13 @@ def curses_main(stdscr, node): STATUS_WIDTH = int(STATUS_WIDTH * scaling_factor) # Draw header - header = (f'{"Topic Name":<{MAX_NAME_WIDTH}} {"Status":<{STATUS_WIDTH}} ' - f'{"Pub Rate (Hz)":<{FRAME_RATE_WIDTH}} ' - f'{"Latency (ms)":<{REALTIME_DELAY_WIDTH}} {"Expected Hz":<12}') + header = ( + f'{"Topic Name":<{MAX_NAME_WIDTH}} {"Status":<{STATUS_WIDTH}} ' + f'{"Pub Rate (Hz)":<{FRAME_RATE_WIDTH}} ' + f'{"Latency (ms)":<{REALTIME_DELAY_WIDTH}} {"Expected Hz":<12}') separator_width = min( - width - BUTTON_WIDTH - 2, - MAX_NAME_WIDTH + FRAME_RATE_WIDTH + REALTIME_DELAY_WIDTH + STATUS_WIDTH + 12 + 4) + width - BUTTON_WIDTH - 2, MAX_NAME_WIDTH + FRAME_RATE_WIDTH + + REALTIME_DELAY_WIDTH + STATUS_WIDTH + 12 + 4) separator = '-' * separator_width try: @@ -221,13 +227,15 @@ def curses_main(stdscr, node): input_mode = None input_buffer = '' elif key == 10 or key == 13: # Enter - if input_mode == 'frequency' and 0 <= selected_row < len(node.visible_topics): + if input_mode == 'frequency' and 0 <= selected_row < len( + node.visible_topics): topic_name = node.visible_topics[selected_row] try: parts = input_buffer.strip().split() if len(parts) >= 1: hz = float(parts[0]) - tolerance = float(parts[1]) if len(parts) > 1 else 5.0 + tolerance = float( + parts[1]) if len(parts) > 1 else 5.0 success, msg = node.ui_adaptor.set_expected_frequency( topic_name, hz, tolerance) status_message = f'Set frequency for {topic_name}: {hz}Hz' @@ -252,7 +260,8 @@ def curses_main(stdscr, node): start_idx = selected_row elif key == curses.KEY_DOWN: if len(node.visible_topics) > 0: - selected_row = min(len(node.visible_topics) - 1, selected_row + 1) + selected_row = min( + len(node.visible_topics) - 1, selected_row + 1) if selected_row >= start_idx + (height - 5): start_idx = min( len(node.visible_topics) - (height - 5), @@ -264,9 +273,12 @@ def curses_main(stdscr, node): elif key == curses.KEY_NPAGE: # Page Down visible_height = height - 5 if len(node.visible_topics) > 0: - start_idx = min(len(node.visible_topics) - visible_height, - start_idx + visible_height) - selected_row = min(len(node.visible_topics) - 1, selected_row + visible_height) + start_idx = min( + len(node.visible_topics) - visible_height, + start_idx + visible_height) + selected_row = min( + len(node.visible_topics) - 1, + selected_row + visible_height) elif key == ord('q') or key == ord('Q'): node.running = False break @@ -307,11 +319,14 @@ def curses_main(stdscr, node): start_idx = 0 else: selected_row = min(selected_row, len(visible_topics) - 1) - start_idx = min(start_idx, max(0, len(visible_topics) - (height - 5))) + start_idx = min(start_idx, + max(0, + len(visible_topics) - (height - 5))) start_idx = max(0, start_idx) visible_height = height - 5 - visible_topics_slice = visible_topics[start_idx:start_idx + visible_height] + visible_topics_slice = visible_topics[start_idx:start_idx + + visible_height] # Draw visible topics for idx, topic_name in enumerate(visible_topics_slice): @@ -330,10 +345,12 @@ def curses_main(stdscr, node): is_monitored = True status_display = diag.status # Use actual diagnostic status frame_rate_node = (diag.pub_rate.ljust(FRAME_RATE_WIDTH) - if diag.pub_rate != '-' else 'N/A'.ljust(FRAME_RATE_WIDTH)) + if diag.pub_rate != '-' else + 'N/A'.ljust(FRAME_RATE_WIDTH)) current_delay_from_realtime_ms = ( diag.latency.ljust(REALTIME_DELAY_WIDTH) - if diag.latency != '-' else 'N/A'.ljust(REALTIME_DELAY_WIDTH)) + if diag.latency != '-' else + 'N/A'.ljust(REALTIME_DELAY_WIDTH)) # Get expected frequency expected_hz, tolerance = node.ui_adaptor.get_expected_frequency(topic_name) @@ -351,7 +368,8 @@ def curses_main(stdscr, node): elif status_display == 'ERROR': color_pair = curses.color_pair(COLOR_ERROR) else: - color_pair = curses.color_pair(COLOR_OK) # Default green for monitored + color_pair = curses.color_pair( + COLOR_OK) # Default green for monitored else: color_pair = curses.color_pair(COLOR_UNMONITORED) @@ -359,7 +377,7 @@ def curses_main(stdscr, node): # Format topic name with truncation if len(topic_name) > MAX_NAME_WIDTH: - name_display = topic_name[:MAX_NAME_WIDTH-3] + '...' + name_display = topic_name[:MAX_NAME_WIDTH - 3] + '...' else: name_display = topic_name.ljust(MAX_NAME_WIDTH) @@ -378,8 +396,9 @@ def curses_main(stdscr, node): button_x = width - len(button_text) - 1 if is_selected: - button_color = (curses.color_pair(COLOR_ERROR) if is_monitored - else curses.color_pair(COLOR_BUTTON_ADD)) + button_color = (curses.color_pair(COLOR_ERROR) + if is_monitored else + curses.color_pair(COLOR_BUTTON_ADD)) else: button_color = curses.color_pair(COLOR_WARN) stdscr.addstr(idx + 2, button_x, button_text, button_color) @@ -400,7 +419,7 @@ def curses_main(stdscr, node): # Status message if current_time < status_timeout: try: - stdscr.addstr(height - 3, 0, status_message[:width-1], + stdscr.addstr(height - 3, 0, status_message[:width - 1], curses.color_pair(COLOR_STATUS_MSG)) except curses.error: pass @@ -408,7 +427,7 @@ def curses_main(stdscr, node): # Show node status message if current_time < node.status_timeout: try: - stdscr.addstr(height - 3, 0, node.status_message[:width-1], + stdscr.addstr(height - 3, 0, node.status_message[:width - 1], curses.color_pair(COLOR_STATUS_MSG)) except curses.error: pass @@ -417,7 +436,7 @@ def curses_main(stdscr, node): if input_mode: try: prompt = f'Set frequency: {input_buffer}' - stdscr.addstr(height - 3, 0, prompt[:width-1], + stdscr.addstr(height - 3, 0, prompt[:width - 1], curses.color_pair(COLOR_STATUS_MSG)) # Position cursor after the input cursor_x = len(prompt) @@ -430,10 +449,12 @@ def curses_main(stdscr, node): curses.curs_set(0) # Hide cursor when not in input mode # Footer - num_shown = min(start_idx + len(visible_topics_slice), len(visible_topics)) + num_shown = min(start_idx + len(visible_topics_slice), + len(visible_topics)) if input_mode: - status_line = ("Format: Hz [tolerance%] - Examples: '30' (30Hz±5% default) " - "or '30 10' (30Hz±10%) - ESC=cancel, Enter=confirm") + status_line = ( + "Format: Hz [tolerance%] - Examples: '30' (30Hz±5% default) " + "or '30 10' (30Hz±10%) - ESC=cancel, Enter=confirm") else: if node.hide_unmonitored: mode_text = 'monitored only' @@ -457,13 +478,10 @@ def curses_main(stdscr, node): def parse_args(args=None): """Parse command-line arguments.""" parser = argparse.ArgumentParser( - description='Ncurses-based frontend for Greenwave Monitor' - ) - parser.add_argument( - '--hide-unmonitored', - action='store_true', - help='Hide unmonitored topics on initialization' - ) + description='Ncurses-based frontend for Greenwave Monitor') + parser.add_argument('--hide-unmonitored', + action='store_true', + help='Hide unmonitored topics on initialization') return parser.parse_known_args(args) @@ -471,7 +489,8 @@ def main(args=None): """Entry point for the ncurses frontend application.""" parsed_args, ros_args = parse_args(args) rclpy.init(args=ros_args) - node = GreenwaveNcursesFrontend(hide_unmonitored=parsed_args.hide_unmonitored) + node = GreenwaveNcursesFrontend( + hide_unmonitored=parsed_args.hide_unmonitored) thread = None def signal_handler(signum, frame): @@ -483,7 +502,9 @@ def signal_handler(signum, frame): signal.signal(signal.SIGTERM, signal_handler) try: - thread = threading.Thread(target=rclpy.spin, args=(node,), daemon=False) + thread = threading.Thread(target=rclpy.spin, + args=(node, ), + daemon=False) thread.start() curses.wrapper(curses_main, node) except KeyboardInterrupt: diff --git a/greenwave_monitor/greenwave_monitor/test_utils.py b/greenwave_monitor/greenwave_monitor/test_utils.py index 49f5e0d..0c0d031 100644 --- a/greenwave_monitor/greenwave_monitor/test_utils.py +++ b/greenwave_monitor/greenwave_monitor/test_utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import rclpy from rclpy.node import Node - # Test configurations for various message types and frequencies # (message_type, expected_frequency, tolerance_hz) # NOTE: Tolerances and frequencies are set conservatively for reliable operation @@ -48,20 +47,20 @@ MONITOR_NODE_NAMESPACE = 'test_namespace' -def create_minimal_publisher( - topic: str, frequency_hz: float, message_type: str, id_suffix: str = ''): +def create_minimal_publisher(topic: str, + frequency_hz: float, + message_type: str, + id_suffix: str = ''): """Create a minimal publisher node with the given parameters.""" - return launch_ros.actions.Node( - package='greenwave_monitor', - executable='minimal_publisher_node', - name=f'minimal_publisher_node{id_suffix}', - parameters=[{ - 'topic': topic, - 'frequency_hz': frequency_hz, - 'message_type': message_type - }], - output='screen' - ) + return launch_ros.actions.Node(package='greenwave_monitor', + executable='minimal_publisher_node', + name=f'minimal_publisher_node{id_suffix}', + parameters=[{ + 'topic': topic, + 'frequency_hz': frequency_hz, + 'message_type': message_type + }], + output='screen') def create_monitor_node(namespace: str = MONITOR_NODE_NAMESPACE, @@ -86,19 +85,21 @@ def wait_for_service_connection(node: Node, timeout_sec: float = 3.0, service_name: str = 'service') -> bool: """Wait for a service to become available.""" - service_available = service_client.wait_for_service(timeout_sec=timeout_sec) + service_available = service_client.wait_for_service( + timeout_sec=timeout_sec) if not service_available: node.get_logger().error( - f'Service "{service_name}" not available within {timeout_sec} seconds') + f'Service "{service_name}" not available within {timeout_sec} seconds' + ) return service_available -def call_manage_topic_service(node: Node, - service_client, - add: bool, - topic: str, - timeout_sec: float = 8.0 - ) -> Optional[ManageTopic.Response]: +def call_manage_topic_service( + node: Node, + service_client, + add: bool, + topic: str, + timeout_sec: float = 8.0) -> Optional[ManageTopic.Response]: """Call the manage_topic service with given parameters.""" request = ManageTopic.Request() request.add_topic = add @@ -114,15 +115,15 @@ def call_manage_topic_service(node: Node, return future.result() -def call_set_frequency_service(node: Node, - service_client, - topic_name: str, - expected_hz: float = 0.0, - tolerance_percent: float = 0.0, - clear: bool = False, - add_if_missing: bool = True, - timeout_sec: float = 8.0 - ) -> Optional[SetExpectedFrequency.Response]: +def call_set_frequency_service( + node: Node, + service_client, + topic_name: str, + expected_hz: float = 0.0, + tolerance_percent: float = 0.0, + clear: bool = False, + add_if_missing: bool = True, + timeout_sec: float = 8.0) -> Optional[SetExpectedFrequency.Response]: """Call the set_expected_frequency service with given parameters.""" request = SetExpectedFrequency.Request() request.topic_name = topic_name @@ -141,10 +142,11 @@ def call_set_frequency_service(node: Node, return future.result() -def collect_diagnostics_for_topic(node: Node, - topic_name: str, - expected_count: int = 5, - timeout_sec: float = 10.0) -> List[DiagnosticStatus]: +def collect_diagnostics_for_topic( + node: Node, + topic_name: str, + expected_count: int = 5, + timeout_sec: float = 10.0) -> List[DiagnosticStatus]: """Collect diagnostic messages for a specific topic.""" received_diagnostics = [] @@ -153,12 +155,8 @@ def diagnostics_callback(msg): if topic_name == status.name: received_diagnostics.append(status) - subscription = node.create_subscription( - DiagnosticArray, - '/diagnostics', - diagnostics_callback, - 10 - ) + subscription = node.create_subscription(DiagnosticArray, '/diagnostics', + diagnostics_callback, 10) end_time = time.time() + timeout_sec while time.time() < end_time: @@ -173,10 +171,9 @@ def diagnostics_callback(msg): def find_best_diagnostic( - diagnostics: List[DiagnosticStatus], - expected_frequency: float, - message_type: str - ) -> Tuple[Optional[DiagnosticStatus], Optional[Tuple[float, float, float]]]: + diagnostics: List[DiagnosticStatus], expected_frequency: float, + message_type: str +) -> Tuple[Optional[DiagnosticStatus], Optional[Tuple[float, float, float]]]: """Find the diagnostic message with frequency closest to expected.""" best_status = None best_values = None @@ -200,7 +197,8 @@ def find_best_diagnostic( try: node_val = float(node_str) if node_str is not None else None msg_val = float(msg_str) if msg_str is not None else None - latency_val = float(latency_str) if latency_str is not None else None + latency_val = float( + latency_str) if latency_str is not None else None except (ValueError, TypeError): continue @@ -222,8 +220,7 @@ def find_best_diagnostic( def verify_diagnostic_values(status: DiagnosticStatus, values: Tuple[float, float, float], - expected_frequency: float, - message_type: str, + expected_frequency: float, message_type: str, tolerance_hz: float) -> List[str]: """Verify diagnostic values and return list of assertion errors.""" errors = [] @@ -235,21 +232,22 @@ def verify_diagnostic_values(status: DiagnosticStatus, if reported_frequency_msg == -1.0: errors.append("Did not find 'frame_rate_msg' in diagnostic") if reported_latency_ms == -1.0: - errors.append("Did not find 'current_delay_from_realtime_ms' in diagnostic") + errors.append( + "Did not find 'current_delay_from_realtime_ms' in diagnostic") # Check frequency tolerances if abs(reported_frequency_node - expected_frequency) > tolerance_hz: - errors.append( - f'Node frequency {reported_frequency_node} not within ' - f'{tolerance_hz} Hz of expected {expected_frequency}') + errors.append(f'Node frequency {reported_frequency_node} not within ' + f'{tolerance_hz} Hz of expected {expected_frequency}') if message_type == 'string': if reported_frequency_msg != 0.0: - errors.append(f'String message frequency should be 0.0, got {reported_frequency_msg}') - if not math.isnan(reported_latency_ms): errors.append( - f'String latency should be {math.nan}, ' - f'got {reported_latency_ms}') + f'String message frequency should be 0.0, got {reported_frequency_msg}' + ) + if not math.isnan(reported_latency_ms): + errors.append(f'String latency should be {math.nan}, ' + f'got {reported_latency_ms}') else: if abs(reported_frequency_msg - expected_frequency) > tolerance_hz: errors.append( @@ -257,22 +255,21 @@ def verify_diagnostic_values(status: DiagnosticStatus, f'{tolerance_hz} Hz of expected {expected_frequency}') # Relaxed to 50ms for slow/loaded CI systems (was 10ms) if reported_latency_ms > 50: - errors.append( - f'Latency should be <= 50 ms for non-string types, ' - f'got {reported_latency_ms}') + errors.append(f'Latency should be <= 50 ms for non-string types, ' + f'got {reported_latency_ms}') return errors -def create_service_clients(node: Node, namespace: str = MONITOR_NODE_NAMESPACE, +def create_service_clients(node: Node, + namespace: str = MONITOR_NODE_NAMESPACE, node_name: str = MONITOR_NODE_NAME): """Create service clients for the monitor node.""" manage_topic_client = node.create_client( - ManageTopic, f'/{namespace}/{node_name}/manage_topic' - ) + ManageTopic, f'/{namespace}/{node_name}/manage_topic') set_frequency_client = node.create_client( - SetExpectedFrequency, f'/{namespace}/{node_name}/set_expected_frequency' - ) + SetExpectedFrequency, + f'/{namespace}/{node_name}/set_expected_frequency') return manage_topic_client, set_frequency_client diff --git a/greenwave_monitor/greenwave_monitor/ui_adaptor.py b/greenwave_monitor/greenwave_monitor/ui_adaptor.py index 30d84ba..7d839d7 100644 --- a/greenwave_monitor/greenwave_monitor/ui_adaptor.py +++ b/greenwave_monitor/greenwave_monitor/ui_adaptor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 - """ Greenwave monitor diagnostics helpers for UI frontends. @@ -105,7 +104,9 @@ class GreenwaveUiAdaptor: """ - def __init__(self, node: Node, monitor_node_name: str = 'greenwave_monitor'): + def __init__(self, + node: Node, + monitor_node_name: str = 'greenwave_monitor'): """Initialize the UI adaptor for subscribing to diagnostics and managing topics.""" self.node = node self.monitor_node_name = monitor_node_name @@ -119,26 +120,19 @@ def __init__(self, node: Node, monitor_node_name: str = 'greenwave_monitor'): def _setup_ros_components(self): """Initialize ROS2 subscriptions, clients, and timers.""" self.subscription = self.node.create_subscription( - DiagnosticArray, - '/diagnostics', - self._on_diagnostics, - 100 - ) + DiagnosticArray, '/diagnostics', self._on_diagnostics, 100) manage_service_name = f'{self.monitor_node_name}/manage_topic' set_freq_service_name = f'{self.monitor_node_name}/set_expected_frequency' - self.node.get_logger().info(f'Connecting to monitor service: {manage_service_name}') + self.node.get_logger().info( + f'Connecting to monitor service: {manage_service_name}') self.manage_topic_client = self.node.create_client( - ManageTopic, - manage_service_name - ) + ManageTopic, manage_service_name) self.set_expected_frequency_client = self.node.create_client( - SetExpectedFrequency, - set_freq_service_name - ) + SetExpectedFrequency, set_freq_service_name) def _extract_topic_name(self, diagnostic_name: str) -> str: """ @@ -198,7 +192,9 @@ def toggle_topic_monitoring(self, topic_name: str): try: # Use asynchronous service call to prevent deadlock future = self.manage_topic_client.call_async(request) - rclpy.spin_until_future_complete(self.node, future, timeout_sec=3.0) + rclpy.spin_until_future_complete(self.node, + future, + timeout_sec=3.0) if future.result() is None: action = 'start' if request.add_topic else 'stop' @@ -228,10 +224,10 @@ def set_expected_frequency(self, topic_name: str, expected_hz: float = 0.0, tolerance_percent: float = 0.0, - clear: bool = False - ) -> tuple[bool, str]: + clear: bool = False) -> tuple[bool, str]: """Set or clear the expected frequency for a topic.""" - if not self.set_expected_frequency_client.wait_for_service(timeout_sec=1.0): + if not self.set_expected_frequency_client.wait_for_service( + timeout_sec=1.0): return False, 'Could not connect to set_expected_frequency service.' request = SetExpectedFrequency.Request() @@ -244,7 +240,9 @@ def set_expected_frequency(self, # Use asynchronous service call to prevent deadlock try: future = self.set_expected_frequency_client.call_async(request) - rclpy.spin_until_future_complete(self.node, future, timeout_sec=3.0) + rclpy.spin_until_future_complete(self.node, + future, + timeout_sec=3.0) if future.result() is None: action = 'clear' if clear else 'set' @@ -257,14 +255,16 @@ def set_expected_frequency(self, if not response.success: action = 'clear' if clear else 'set' self.node.get_logger().error( - f'Failed to {action} expected frequency: {response.message}') + f'Failed to {action} expected frequency: {response.message}' + ) return False, response.message else: with self.data_lock: if clear: self.expected_frequencies.pop(topic_name, None) else: - self.expected_frequencies[topic_name] = (expected_hz, tolerance_percent) + self.expected_frequencies[topic_name] = ( + expected_hz, tolerance_percent) return True, response.message except Exception as e: action = 'clear' if clear else 'set' diff --git a/greenwave_monitor/launch/hz.launch.py b/greenwave_monitor/launch/hz.launch.py index bb679df..534d040 100644 --- a/greenwave_monitor/launch/hz.launch.py +++ b/greenwave_monitor/launch/hz.launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/launch/test_publishers.launch.py b/greenwave_monitor/launch/test_publishers.launch.py index e1907c9..955b337 100644 --- a/greenwave_monitor/launch/test_publishers.launch.py +++ b/greenwave_monitor/launch/test_publishers.launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,10 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Launch file with minimal publishers for testing the greenwave monitor.""" - from launch import LaunchDescription from launch_ros.actions import Node @@ -26,35 +24,42 @@ def generate_launch_description(): executable='minimal_publisher_node', name='minimal_publisher1', output='log', - parameters=[ - {'topic': 'imu_topic', 'frequency_hz': 100.0} - ], + parameters=[{ + 'topic': 'imu_topic', + 'frequency_hz': 100.0 + }], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher2', output='log', - parameters=[ - {'topic': 'image_topic', 'message_type': 'image', 'frequency_hz': 30.0} - ], + parameters=[{ + 'topic': 'image_topic', + 'message_type': 'image', + 'frequency_hz': 30.0 + }], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher3', output='log', - parameters=[ - {'topic': 'string_topic', 'message_type': 'string', 'frequency_hz': 1000.0} - ], + parameters=[{ + 'topic': 'string_topic', + 'message_type': 'string', + 'frequency_hz': 1000.0 + }], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher4', output='log', - parameters=[ - {'topic': 'slow_string_topic', 'message_type': 'string', 'frequency_hz': 0.5} - ], + parameters=[{ + 'topic': 'slow_string_topic', + 'message_type': 'string', + 'frequency_hz': 0.5 + }], ), ]) diff --git a/greenwave_monitor/scripts/ncurses_dashboard b/greenwave_monitor/scripts/ncurses_dashboard index 807a777..b75d2d1 100755 --- a/greenwave_monitor/scripts/ncurses_dashboard +++ b/greenwave_monitor/scripts/ncurses_dashboard @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,77 +27,77 @@ HIDE_UNMONITORED=false MONITOR_ARGS=() show_help() { - echo "Usage: $0 [OPTIONS] [MONITOR_ARGS...]" - echo "" - echo "Launch Greenwave Monitor with ncurses TUI dashboard" - echo "" - echo "OPTIONS:" - echo " --demo, --test Launch demo publisher nodes for testing" - echo " --log-dir DIR Enable logging to specified directory" - echo " --hide-unmonitored Hide unmonitored topics on initialization" - echo " --help, -h Show this help message" - echo "" - echo "MONITOR_ARGS are passed directly to the greenwave_monitor node" - echo "" - echo "Controls in ncurses interface:" - echo " enter/space = toggle topic monitoring" - echo " f = Set expected frequency for selected topic (format: hz tolerance%)" - echo " c = Clear frequency settings for selected topic" - echo " h = Toggle hiding unmonitored topics" - echo " ↑/↓ = Navigate topics" - echo " q = Quit" + echo "Usage: $0 [OPTIONS] [MONITOR_ARGS...]" + echo "" + echo "Launch Greenwave Monitor with ncurses TUI dashboard" + echo "" + echo "OPTIONS:" + echo " --demo, --test Launch demo publisher nodes for testing" + echo " --log-dir DIR Enable logging to specified directory" + echo " --hide-unmonitored Hide unmonitored topics on initialization" + echo " --help, -h Show this help message" + echo "" + echo "MONITOR_ARGS are passed directly to the greenwave_monitor node" + echo "" + echo "Controls in ncurses interface:" + echo " enter/space = toggle topic monitoring" + echo " f = Set expected frequency for selected topic (format: hz tolerance%)" + echo " c = Clear frequency settings for selected topic" + echo " h = Toggle hiding unmonitored topics" + echo " ↑/↓ = Navigate topics" + echo " q = Quit" } while [[ $# -gt 0 ]]; do - case $1 in - --demo|--test) - DEMO_MODE=true - shift - ;; - --log-dir) - LOG_DIR="$2" - shift 2 - ;; - --hide-unmonitored) - HIDE_UNMONITORED=true - shift - ;; - --help|-h) - show_help - exit 0 - ;; - *) - MONITOR_ARGS+=("$1") - shift - ;; - esac + case $1 in + --demo | --test) + DEMO_MODE=true + shift + ;; + --log-dir) + LOG_DIR="$2" + shift 2 + ;; + --hide-unmonitored) + HIDE_UNMONITORED=true + shift + ;; + --help | -h) + show_help + exit 0 + ;; + *) + MONITOR_ARGS+=("$1") + shift + ;; + esac done # Handle logging configuration LOG_FILE="/dev/null" if [ -n "$LOG_DIR" ]; then - # Create logs directory if it doesn't exist - mkdir -p "${LOG_DIR}" + # Create logs directory if it doesn't exist + mkdir -p "${LOG_DIR}" - # Create a timestamped log file - TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") - LOG_FILE="${LOG_DIR}/monitor_ncurses_${TIMESTAMP}.log" + # Create a timestamped log file + TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") + LOG_FILE="${LOG_DIR}/monitor_ncurses_${TIMESTAMP}.log" fi # Function to clean up background processes on exit cleanup() { - echo "Shutting down..." - if [ -n "$MONITOR_PID" ]; then - echo "Terminating monitor node (PID: $MONITOR_PID)..." - if [ "$LOG_FILE" != "/dev/null" ]; then - echo "Monitor log available at: ${LOG_FILE}" - fi - # Kill background process and all its descendants - pkill -TERM -P $MONITOR_PID 2>/dev/null - kill -TERM $MONITOR_PID 2>/dev/null - fi - exit 0 + echo "Shutting down..." + if [ -n "$MONITOR_PID" ]; then + echo "Terminating monitor node (PID: $MONITOR_PID)..." + if [ "$LOG_FILE" != "/dev/null" ]; then + echo "Monitor log available at: ${LOG_FILE}" + fi + # Kill background process and all its descendants + pkill -TERM -P $MONITOR_PID 2>/dev/null + kill -TERM $MONITOR_PID 2>/dev/null + fi + exit 0 } # Set up trap to catch signals @@ -105,13 +105,13 @@ trap cleanup SIGINT SIGTERM EXIT # Launch demo nodes if requested if [ "$DEMO_MODE" = "true" ]; then - echo "Starting demo mode with test publisher nodes..." - ros2 launch greenwave_monitor example.launch.py &> "${LOG_FILE}" & - MONITOR_PID=$! + echo "Starting demo mode with test publisher nodes..." + ros2 launch greenwave_monitor example.launch.py &>"${LOG_FILE}" & + MONITOR_PID=$! else - echo "Starting Greenwave Monitor..." - ros2 run greenwave_monitor greenwave_monitor "${MONITOR_ARGS[@]}" &> "${LOG_FILE}" & - MONITOR_PID=$! + echo "Starting Greenwave Monitor..." + ros2 run greenwave_monitor greenwave_monitor "${MONITOR_ARGS[@]}" &>"${LOG_FILE}" & + MONITOR_PID=$! fi # Wait briefly to allow the monitor node to initialize @@ -124,7 +124,7 @@ echo "Controls: a=Add Topic, r=Remove, f=Set Frequency, c=Clear Freq, q=Quit" # NOTE: add proper argument parsing to the ncurses frontend if more than one argument is added here FRONTEND_ARGS=() if [ "$HIDE_UNMONITORED" = "true" ]; then - FRONTEND_ARGS+=("--hide-unmonitored") + FRONTEND_ARGS+=("--hide-unmonitored") fi python3 -m greenwave_monitor.ncurses_frontend "${FRONTEND_ARGS[@]}" diff --git a/greenwave_monitor/test/test_greenwave_monitor.py b/greenwave_monitor/test/test_greenwave_monitor.py index 82b58d4..11095b9 100644 --- a/greenwave_monitor/test/test_greenwave_monitor.py +++ b/greenwave_monitor/test/test_greenwave_monitor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,19 +24,11 @@ import unittest from greenwave_monitor.test_utils import ( - call_manage_topic_service, - collect_diagnostics_for_topic, - create_minimal_publisher, - create_monitor_node, - create_service_clients, - find_best_diagnostic, - MANAGE_TOPIC_TEST_CONFIG, - MONITOR_NODE_NAME, - MONITOR_NODE_NAMESPACE, - TEST_CONFIGURATIONS, - verify_diagnostic_values, - wait_for_service_connection -) + call_manage_topic_service, collect_diagnostics_for_topic, + create_minimal_publisher, create_monitor_node, create_service_clients, + find_best_diagnostic, MANAGE_TOPIC_TEST_CONFIG, MONITOR_NODE_NAME, + MONITOR_NODE_NAMESPACE, TEST_CONFIGURATIONS, verify_diagnostic_values, + wait_for_service_connection) import launch import launch_testing from launch_testing import post_shutdown_test @@ -97,7 +89,8 @@ def create_test_yaml_config(): @pytest.mark.launch_test -@launch_testing.parametrize('message_type, expected_frequency, tolerance_hz', TEST_CONFIGURATIONS) +@launch_testing.parametrize('message_type, expected_frequency, tolerance_hz', + TEST_CONFIGURATIONS) def generate_test_description(message_type, expected_frequency, tolerance_hz): """Generate launch description for greenwave monitor tests.""" # Create temporary YAML config for testing parameter loading @@ -140,8 +133,8 @@ def generate_test_description(message_type, expected_frequency, tolerance_hz): ros2_monitor_node, *publishers, # Unpack all publishers into the launch description launch_testing.actions.ReadyToTest() - ]), context - ) + ]), + context) @post_shutdown_test() @@ -185,8 +178,7 @@ def check_node_launches_successfully(self): # Create a service client to check if the node is ready # Service discovery is more reliable than node discovery in launch_testing manage_client, set_freq_client = create_service_clients( - self.test_node, MONITOR_NODE_NAMESPACE, MONITOR_NODE_NAME - ) + self.test_node, MONITOR_NODE_NAMESPACE, MONITOR_NODE_NAME) service_available = wait_for_service_connection( self.test_node, manage_client, timeout_sec=10.0, service_name=f'/{MONITOR_NODE_NAMESPACE}/{MONITOR_NODE_NAME}/manage_topic' @@ -197,12 +189,14 @@ def check_node_launches_successfully(self): 'not available within timeout') return manage_client - def verify_diagnostics(self, topic_name, expected_frequency, message_type, tolerance_hz): + def verify_diagnostics(self, topic_name, expected_frequency, message_type, + tolerance_hz): """Verify diagnostics for a given topic.""" # Collect diagnostic messages using shared utility - received_diagnostics = collect_diagnostics_for_topic( - self.test_node, topic_name, expected_count=5, timeout_sec=10.0 - ) + received_diagnostics = collect_diagnostics_for_topic(self.test_node, + topic_name, + expected_count=5, + timeout_sec=10.0) # We expect the monitor to be publishing diagnostics self.assertGreaterEqual(len(received_diagnostics), 5, @@ -210,22 +204,23 @@ def verify_diagnostics(self, topic_name, expected_frequency, message_type, toler # Find the best diagnostic message using shared utility best_status, best_values = find_best_diagnostic( - received_diagnostics, expected_frequency, message_type - ) + received_diagnostics, expected_frequency, message_type) - self.assertIsNotNone(best_status, 'Did not find a diagnostic with all required values') + self.assertIsNotNone( + best_status, 'Did not find a diagnostic with all required values') self.assertEqual(topic_name, best_status.name) # Verify diagnostic values using shared utility - errors = verify_diagnostic_values( - best_status, best_values, expected_frequency, message_type, tolerance_hz - ) + errors = verify_diagnostic_values(best_status, best_values, + expected_frequency, message_type, + tolerance_hz) # Assert no errors occurred if errors: self.fail(f"Diagnostic verification failed: {'; '.join(errors)}") - def test_frequency_monitoring(self, expected_frequency, message_type, tolerance_hz): + def test_frequency_monitoring(self, expected_frequency, message_type, + tolerance_hz): """Test that the monitor node correctly tracks different frequencies.""" # This test runs for all configurations to verify frequency monitoring self.check_node_launches_successfully() @@ -233,46 +228,58 @@ def test_frequency_monitoring(self, expected_frequency, message_type, tolerance_ def call_manage_topic(self, add, topic, service_client): """Service call helper.""" - response = call_manage_topic_service( - self.test_node, service_client, add, topic, timeout_sec=8.0 - ) + response = call_manage_topic_service(self.test_node, + service_client, + add, + topic, + timeout_sec=8.0) self.assertIsNotNone(response, 'Service call failed or timed out') return response - def test_manage_one_topic(self, expected_frequency, message_type, tolerance_hz): + def test_manage_one_topic(self, expected_frequency, message_type, + tolerance_hz): """Test that add_topic() and remove_topic() work correctly for one topic.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running manage topic tests once') service_client = self.check_node_launches_successfully() # 1. Remove an existing topic – should succeed on first attempt. - response = self.call_manage_topic( - add=False, topic=TEST_TOPIC, service_client=service_client) + response = self.call_manage_topic(add=False, + topic=TEST_TOPIC, + service_client=service_client) self.assertTrue(response.success) # 2. Removing the same topic again should fail because it no longer exists. - response = self.call_manage_topic( - add=False, topic=TEST_TOPIC, service_client=service_client) + response = self.call_manage_topic(add=False, + topic=TEST_TOPIC, + service_client=service_client) self.assertFalse(response.success) # 3. Add the topic back – should succeed now. - response = self.call_manage_topic( - add=True, topic=TEST_TOPIC, service_client=service_client) + response = self.call_manage_topic(add=True, + topic=TEST_TOPIC, + service_client=service_client) self.assertTrue(response.success) # Verify diagnostics after adding the topic back - self.verify_diagnostics(TEST_TOPIC, expected_frequency, message_type, tolerance_hz) + self.verify_diagnostics(TEST_TOPIC, expected_frequency, message_type, + tolerance_hz) # 4. Adding the same topic again should fail because it's already monitored. - response = self.call_manage_topic( - add=True, topic=TEST_TOPIC, service_client=service_client) + response = self.call_manage_topic(add=True, + topic=TEST_TOPIC, + service_client=service_client) self.assertFalse(response.success) - def test_manage_multiple_topics(self, expected_frequency, message_type, tolerance_hz): + def test_manage_multiple_topics(self, expected_frequency, message_type, + tolerance_hz): """Test that add_topic() and remove_topic() work correctly for multiple topics.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: - self.skipTest('Only running manage topic tests once for 30 hz images') + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + self.skipTest( + 'Only running manage topic tests once for 30 hz images') service_client = self.check_node_launches_successfully() @@ -287,24 +294,29 @@ def test_manage_multiple_topics(self, expected_frequency, message_type, toleranc self.assertFalse(response.success) # 1. Add first topic – should succeed. - response = self.call_manage_topic( - add=True, topic=TEST_TOPIC1, service_client=service_client) + response = self.call_manage_topic(add=True, + topic=TEST_TOPIC1, + service_client=service_client) self.assertTrue(response.success) # Verify diagnostics after adding the first topic - self.verify_diagnostics(TEST_TOPIC1, expected_frequency, message_type, tolerance_hz) + self.verify_diagnostics(TEST_TOPIC1, expected_frequency, message_type, + tolerance_hz) # 2. Add second topic – should succeed. - response = self.call_manage_topic( - add=True, topic=TEST_TOPIC2, service_client=service_client) + response = self.call_manage_topic(add=True, + topic=TEST_TOPIC2, + service_client=service_client) self.assertTrue(response.success) # Verify diagnostics after adding the second topic - self.verify_diagnostics(TEST_TOPIC2, expected_frequency, message_type, tolerance_hz) + self.verify_diagnostics(TEST_TOPIC2, expected_frequency, message_type, + tolerance_hz) # 3. Remove first topic – should succeed. - response = self.call_manage_topic( - add=False, topic=TEST_TOPIC1, service_client=service_client) + response = self.call_manage_topic(add=False, + topic=TEST_TOPIC1, + service_client=service_client) self.assertTrue(response.success) def test_yaml_parameter_loading(self, expected_frequency, message_type, tolerance_hz): diff --git a/greenwave_monitor/test/test_ncurses_frontend_argparse.py b/greenwave_monitor/test/test_ncurses_frontend_argparse.py index a12f8f3..e3c5ee2 100644 --- a/greenwave_monitor/test/test_ncurses_frontend_argparse.py +++ b/greenwave_monitor/test/test_ncurses_frontend_argparse.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 - """Tests for ncurses frontend argument parsing.""" from greenwave_monitor.ncurses_frontend import parse_args @@ -38,8 +37,7 @@ def test_hide_unmonitored_long_flag(self): def test_ros_args_passthrough(self): """Test that ROS arguments are passed through.""" parsed_args, ros_args = parse_args( - ['--hide-unmonitored', '--ros-args', '-r', '__node:=my_node'] - ) + ['--hide-unmonitored', '--ros-args', '-r', '__node:=my_node']) assert parsed_args.hide_unmonitored is True assert '--ros-args' in ros_args assert '-r' in ros_args diff --git a/greenwave_monitor/test/test_topic_monitoring_integration.py b/greenwave_monitor/test/test_topic_monitoring_integration.py index 3641c64..16d10d4 100644 --- a/greenwave_monitor/test/test_topic_monitoring_integration.py +++ b/greenwave_monitor/test/test_topic_monitoring_integration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,13 +25,8 @@ from diagnostic_msgs.msg import DiagnosticStatus from greenwave_monitor.test_utils import ( - create_minimal_publisher, - create_monitor_node, - MANAGE_TOPIC_TEST_CONFIG, - MONITOR_NODE_NAME, - MONITOR_NODE_NAMESPACE, - TEST_CONFIGURATIONS -) + create_minimal_publisher, create_monitor_node, MANAGE_TOPIC_TEST_CONFIG, + MONITOR_NODE_NAME, MONITOR_NODE_NAMESPACE, TEST_CONFIGURATIONS) from greenwave_monitor.ui_adaptor import GreenwaveUiAdaptor, UiDiagnosticData from greenwave_monitor_interfaces.srv import ManageTopic import launch @@ -83,7 +78,8 @@ def create_test_yaml_config(): @pytest.mark.launch_test -@launch_testing.parametrize('message_type, expected_frequency, tolerance_hz', TEST_CONFIGURATIONS) +@launch_testing.parametrize('message_type, expected_frequency, tolerance_hz', + TEST_CONFIGURATIONS) def generate_test_description(message_type, expected_frequency, tolerance_hz): """Generate launch description for topic monitoring tests.""" # Create temporary YAML config for testing parameter loading @@ -98,10 +94,13 @@ def generate_test_description(message_type, expected_frequency, tolerance_hz): # Create publishers for testing publishers = [ # Main test topic publisher with parametrized frequency - create_minimal_publisher('/test_topic', expected_frequency, message_type), + create_minimal_publisher('/test_topic', expected_frequency, + message_type), # Additional publishers for topic management tests - create_minimal_publisher('/test_topic1', expected_frequency, message_type, '1'), - create_minimal_publisher('/test_topic2', expected_frequency, message_type, '2'), + create_minimal_publisher('/test_topic1', expected_frequency, + message_type, '1'), + create_minimal_publisher('/test_topic2', expected_frequency, + message_type, '2'), # Publisher for service discovery tests create_minimal_publisher('/discovery_test_topic', 50.0, 'imu', '_discovery'), # Publisher for YAML config test @@ -120,13 +119,9 @@ def generate_test_description(message_type, expected_frequency, tolerance_hz): 'tolerance_hz': tolerance_hz, } - return ( - launch.LaunchDescription([ - ros2_monitor_node, - *publishers, - launch_testing.actions.ReadyToTest() - ]), context - ) + return (launch.LaunchDescription( + [ros2_monitor_node, *publishers, + launch_testing.actions.ReadyToTest()]), context) @post_shutdown_test() @@ -137,7 +132,8 @@ class TestTopicMonitoringPostShutdown(unittest.TestCase): def setUpClass(cls): """Initialize ROS2 and create test node.""" rclpy.init() - cls.test_node = Node('shutdown_test_node', namespace=MONITOR_NODE_NAMESPACE) + cls.test_node = Node('shutdown_test_node', + namespace=MONITOR_NODE_NAMESPACE) @classmethod def tearDownClass(cls): @@ -159,7 +155,8 @@ class TestTopicMonitoringIntegration(unittest.TestCase): def setUpClass(cls): """Initialize ROS2 and create test node.""" rclpy.init() - cls.test_node = Node('topic_monitoring_test_node', namespace=MONITOR_NODE_NAMESPACE) + cls.test_node = Node('topic_monitoring_test_node', + namespace=MONITOR_NODE_NAMESPACE) @classmethod def tearDownClass(cls): @@ -171,9 +168,7 @@ def setUp(self): """Set up for each test.""" # Create a fresh GreenwaveUiAdaptor instance for each test with proper namespace self.diagnostics_monitor = GreenwaveUiAdaptor( - self.test_node, - monitor_node_name=MONITOR_NODE_NAME - ) + self.test_node, monitor_node_name=MONITOR_NODE_NAME) # Allow time for service discovery in test environment (reduced from 2.0s) time.sleep(1.0) @@ -184,22 +179,26 @@ def tearDown(self): if hasattr(self, 'diagnostics_monitor'): # Clean up ROS components try: - self.test_node.destroy_subscription(self.diagnostics_monitor.subscription) - self.test_node.destroy_client(self.diagnostics_monitor.manage_topic_client) + self.test_node.destroy_subscription( + self.diagnostics_monitor.subscription) + self.test_node.destroy_client( + self.diagnostics_monitor.manage_topic_client) self.test_node.destroy_client( self.diagnostics_monitor.set_expected_frequency_client) except Exception: pass # Ignore cleanup errors - def test_service_discovery_default_namespace( - self, expected_frequency, message_type, tolerance_hz): + def test_service_discovery_default_namespace(self, expected_frequency, + message_type, tolerance_hz): """Test service discovery with default namespace.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running service discovery tests once') # The monitor should discover the services automatically self.assertIsNotNone(self.diagnostics_monitor.manage_topic_client) - self.assertIsNotNone(self.diagnostics_monitor.set_expected_frequency_client) + self.assertIsNotNone( + self.diagnostics_monitor.set_expected_frequency_client) # Verify services are available manage_available = self.diagnostics_monitor.manage_topic_client.wait_for_service( @@ -208,12 +207,16 @@ def test_service_discovery_default_namespace( self.diagnostics_monitor.set_expected_frequency_client .wait_for_service(timeout_sec=10.0)) - self.assertTrue(manage_available, 'ManageTopic service should be available') - self.assertTrue(set_freq_available, 'SetExpectedFrequency service should be available') + self.assertTrue(manage_available, + 'ManageTopic service should be available') + self.assertTrue(set_freq_available, + 'SetExpectedFrequency service should be available') - def test_diagnostic_data_conversion(self, expected_frequency, message_type, tolerance_hz): + def test_diagnostic_data_conversion(self, expected_frequency, message_type, + tolerance_hz): """Test conversion from DiagnosticStatus to UiDiagnosticData.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running diagnostic conversion tests once') # Create a mock DiagnosticStatus @@ -242,7 +245,8 @@ def test_diagnostic_data_conversion(self, expected_frequency, message_type, tole def test_diagnostic_data_conversion_different_levels( self, expected_frequency, message_type, tolerance_hz): """Test diagnostic status level conversion.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running diagnostic conversion tests once') status_levels = [ @@ -262,10 +266,11 @@ def test_diagnostic_data_conversion_different_levels( ui_data = UiDiagnosticData.from_status(status) self.assertEqual(ui_data.status, expected_str) - def test_toggle_topic_monitoring_add_remove( - self, expected_frequency, message_type, tolerance_hz): + def test_toggle_topic_monitoring_add_remove(self, expected_frequency, + message_type, tolerance_hz): """Test adding and removing topics from monitoring.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running topic toggle tests once') test_topic = '/test_topic1' @@ -283,15 +288,17 @@ def test_toggle_topic_monitoring_add_remove( while time.time() - start_time < max_wait_time: rclpy.spin_once(self.test_node, timeout_sec=0.1) - topic_data = self.diagnostics_monitor.get_topic_diagnostics(test_topic) + topic_data = self.diagnostics_monitor.get_topic_diagnostics( + test_topic) if topic_data.status != '-': break time.sleep(0.1) # Topic should now have diagnostic data self.assertIsNotNone(topic_data) - self.assertNotEqual(topic_data.status, '-', - f'Should have received diagnostic data after {max_wait_time}s') + self.assertNotEqual( + topic_data.status, '-', + f'Should have received diagnostic data after {max_wait_time}s') # Remove topic self.diagnostics_monitor.toggle_topic_monitoring(test_topic) @@ -299,10 +306,11 @@ def test_toggle_topic_monitoring_add_remove( # Topic should be removed from diagnostics self.assertNotIn(test_topic, self.diagnostics_monitor.ui_diagnostics) - def test_set_expected_frequency_operations( - self, expected_frequency, message_type, tolerance_hz): + def test_set_expected_frequency_operations(self, expected_frequency, + message_type, tolerance_hz): """Test setting and clearing expected frequencies.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running frequency setting tests once') test_topic = '/test_topic2' @@ -310,32 +318,35 @@ def test_set_expected_frequency_operations( test_tolerance = 15.0 # Initially no expected frequency - freq, tolerance = self.diagnostics_monitor.get_expected_frequency(test_topic) + freq, tolerance = self.diagnostics_monitor.get_expected_frequency( + test_topic) self.assertEqual((freq, tolerance), (0.0, 0.0)) # Set expected frequency success, message = self.diagnostics_monitor.set_expected_frequency( - test_topic, test_freq, test_tolerance - ) + test_topic, test_freq, test_tolerance) self.assertTrue(success, f'Failed to set frequency: {message}') # Check that frequency was stored locally - freq, tolerance = self.diagnostics_monitor.get_expected_frequency(test_topic) + freq, tolerance = self.diagnostics_monitor.get_expected_frequency( + test_topic) self.assertEqual((freq, tolerance), (test_freq, test_tolerance)) # Clear expected frequency success, message = self.diagnostics_monitor.set_expected_frequency( - test_topic, clear=True - ) + test_topic, clear=True) self.assertTrue(success, f'Failed to clear frequency: {message}') # Should be back to defaults - freq, tolerance = self.diagnostics_monitor.get_expected_frequency(test_topic) + freq, tolerance = self.diagnostics_monitor.get_expected_frequency( + test_topic) self.assertEqual((freq, tolerance), (0.0, 0.0)) - def test_diagnostic_data_thread_safety(self, expected_frequency, message_type, tolerance_hz): + def test_diagnostic_data_thread_safety(self, expected_frequency, + message_type, tolerance_hz): """Test thread safety of diagnostic data updates.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running thread safety tests once') test_topic = '/test_topic' @@ -347,7 +358,8 @@ def update_thread(): try: for _ in range(50): # Simulate concurrent diagnostic updates - data = self.diagnostics_monitor.get_topic_diagnostics(test_topic) + data = self.diagnostics_monitor.get_topic_diagnostics( + test_topic) if data.status != '-': update_count += 1 time.sleep(0.01) @@ -377,16 +389,20 @@ def spin_thread(): # Should not have encountered any thread safety issues self.assertFalse(error_occurred, 'Thread safety error occurred') - self.assertGreater(update_count, 0, 'Should have received some diagnostic updates') + self.assertGreater(update_count, 0, + 'Should have received some diagnostic updates') - def test_get_topic_diagnostics_nonexistent_topic( - self, expected_frequency, message_type, tolerance_hz): + def test_get_topic_diagnostics_nonexistent_topic(self, expected_frequency, + message_type, + tolerance_hz): """Test getting diagnostics for non-existent topic.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running diagnostic retrieval tests once') # Request diagnostics for non-monitored topic - data = self.diagnostics_monitor.get_topic_diagnostics('/nonexistent_topic') + data = self.diagnostics_monitor.get_topic_diagnostics( + '/nonexistent_topic') # Should return default values expected_default = UiDiagnosticData() @@ -395,9 +411,11 @@ def test_get_topic_diagnostics_nonexistent_topic( self.assertEqual(data.latency, expected_default.latency) self.assertEqual(data.status, expected_default.status) - def test_diagnostics_callback_processing(self, expected_frequency, message_type, tolerance_hz): + def test_diagnostics_callback_processing(self, expected_frequency, + message_type, tolerance_hz): """Test that diagnostic callbacks are processed correctly.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running callback processing tests once') test_topic = '/test_topic' @@ -512,11 +530,13 @@ def test_yaml_sets_parameters_at_startup( def test_service_timeout_handling(self, expected_frequency, message_type, tolerance_hz): """Test service call timeout handling.""" - if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, + tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running timeout handling tests once') # Create a client to a non-existent service - fake_client = self.test_node.create_client(ManageTopic, '/nonexistent_service') + fake_client = self.test_node.create_client(ManageTopic, + '/nonexistent_service') # Replace the real client temporarily original_client = self.diagnostics_monitor.manage_topic_client diff --git a/greenwave_monitor_interfaces/CMakeLists.txt b/greenwave_monitor_interfaces/CMakeLists.txt index a3c2b46..68b3691 100644 --- a/greenwave_monitor_interfaces/CMakeLists.txt +++ b/greenwave_monitor_interfaces/CMakeLists.txt @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,4 +30,4 @@ rosidl_generate_interfaces(${PROJECT_NAME} "srv/SetExpectedFrequency.srv" ) -ament_package() \ No newline at end of file +ament_package() diff --git a/greenwave_monitor_interfaces/srv/ManageTopic.srv b/greenwave_monitor_interfaces/srv/ManageTopic.srv index ae8abb1..8a7b637 100644 --- a/greenwave_monitor_interfaces/srv/ManageTopic.srv +++ b/greenwave_monitor_interfaces/srv/ManageTopic.srv @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,4 +21,4 @@ bool add_topic # true to add, false to remove --- # Response bool success -string message \ No newline at end of file +string message diff --git a/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv b/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv index cf61ff4..e4bfba6 100644 --- a/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv +++ b/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,4 +24,4 @@ bool add_topic_if_missing # add topic to monitoring if not already --- # Response bool success -string message \ No newline at end of file +string message diff --git a/scripts/build_debian_packages.sh b/scripts/build_debian_packages.sh index 8e7f8be..4f011af 100755 --- a/scripts/build_debian_packages.sh +++ b/scripts/build_debian_packages.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,55 +49,53 @@ UBUNTU_DISTRO="${2:-$DEFAULT_UBUNTU_DISTRO}" # Validate ROS distro case "$ROS_DISTRO" in - humble|iron|jazzy|kilted|rolling) - ;; - *) - echo "Error: Unsupported ROS distro: $ROS_DISTRO" - echo "Supported distros: humble, iron, jazzy, kilted, rolling" - exit 1 - ;; +humble | iron | jazzy | kilted | rolling) ;; +*) + echo "Error: Unsupported ROS distro: $ROS_DISTRO" + echo "Supported distros: humble, iron, jazzy, kilted, rolling" + exit 1 + ;; esac # Validate Ubuntu distro case "$UBUNTU_DISTRO" in - jammy|noble) - ;; - *) - echo "Error: Unsupported Ubuntu distro: $UBUNTU_DISTRO" - echo "Supported distros: jammy, noble" - exit 1 - ;; +jammy | noble) ;; +*) + echo "Error: Unsupported Ubuntu distro: $UBUNTU_DISTRO" + echo "Supported distros: jammy, noble" + exit 1 + ;; esac echo "Building Debian packages for ROS $ROS_DISTRO on Ubuntu $UBUNTU_DISTRO" # Check if running in a container (recommended) or warn user if [ ! -f "/.dockerenv" ] && [ ! -f "/run/.containerenv" ]; then - echo "WARNING: Not running in a container. This script is designed to run in a clean Ubuntu container." - echo "" - echo "Recommended: Run in Docker with:" - echo " docker run -it --rm -v \$(pwd):/workspace -w /workspace ubuntu:$UBUNTU_DISTRO ./scripts/build_debian_packages.sh $ROS_DISTRO $UBUNTU_DISTRO" - echo "" - echo "Press Ctrl+C to cancel, or Enter to continue anyway (not recommended)..." - read -r + echo "WARNING: Not running in a container. This script is designed to run in a clean Ubuntu container." + echo "" + echo "Recommended: Run in Docker with:" + echo " docker run -it --rm -v \$(pwd):/workspace -w /workspace ubuntu:$UBUNTU_DISTRO ./scripts/build_debian_packages.sh $ROS_DISTRO $UBUNTU_DISTRO" + echo "" + echo "Press Ctrl+C to cancel, or Enter to continue anyway (not recommended)..." + read -r fi # Setup ROS repository if not already configured echo "Setting up ROS repository..." export DEBIAN_FRONTEND=noninteractive export TZ=Etc/UTC -ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ >/etc/timezone apt-get update -qq apt-get install -y curl gnupg lsb-release if [ ! -f "/etc/apt/sources.list.d/ros2.list" ]; then - echo "Adding ROS 2 apt repository..." - curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" > /etc/apt/sources.list.d/ros2.list - apt-get update -qq + echo "Adding ROS 2 apt repository..." + curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" >/etc/apt/sources.list.d/ros2.list + apt-get update -qq else - echo "ROS 2 repository already configured" + echo "ROS 2 repository already configured" fi # Install dependencies @@ -106,7 +104,7 @@ echo "Installing build dependencies..." # Check if we need --break-system-packages for pip USE_BREAK_SYSTEM_PACKAGES="" if [[ "$ROS_DISTRO" == "jazzy" || "$ROS_DISTRO" == "kilted" || "$ROS_DISTRO" == "rolling" ]]; then - USE_BREAK_SYSTEM_PACKAGES="--break-system-packages" + USE_BREAK_SYSTEM_PACKAGES="--break-system-packages" fi # Install system dependencies @@ -114,13 +112,13 @@ apt-get install -y build-essential python3-pip python3-bloom python3-rosdep git # Install Python dependencies if [ -f "requirements.txt" ]; then - if [ -n "$USE_BREAK_SYSTEM_PACKAGES" ]; then - pip3 install $USE_BREAK_SYSTEM_PACKAGES -I pygments -r requirements.txt - python3 -m pip install -U $USE_BREAK_SYSTEM_PACKAGES bloom - else - pip3 install -r requirements.txt - python3 -m pip install -U bloom - fi + if [ -n "$USE_BREAK_SYSTEM_PACKAGES" ]; then + pip3 install $USE_BREAK_SYSTEM_PACKAGES -I pygments -r requirements.txt + python3 -m pip install -U $USE_BREAK_SYSTEM_PACKAGES bloom + else + pip3 install -r requirements.txt + python3 -m pip install -U bloom + fi fi # Initialize rosdep and install all build dependencies @@ -131,9 +129,9 @@ rosdep install --from-paths . --rosdistro "$ROS_DISTRO" --ignore-src -r -y # Source ROS environment (now installed via rosdep) if [ ! -f "/opt/ros/$ROS_DISTRO/setup.bash" ]; then - echo "Error: ROS $ROS_DISTRO not found after rosdep install" - echo "This should have been installed by rosdep. Check package.xml dependencies." - exit 1 + echo "Error: ROS $ROS_DISTRO not found after rosdep install" + echo "This should have been installed by rosdep. Check package.xml dependencies." + exit 1 fi source /opt/ros/$ROS_DISTRO/setup.bash @@ -168,34 +166,34 @@ mkdir -p "$DEBIAN_DIR" # Function to build a Debian package build_debian_package() { - local package_name=$1 - local package_dir=$2 + local package_name=$1 + local package_dir=$2 - echo "==================================" - echo "Generating Debian package for $package_name..." - echo "==================================" + echo "==================================" + echo "Generating Debian package for $package_name..." + echo "==================================" - cd "$package_dir" + cd "$package_dir" - # Generate debian files and build package - bloom-generate rosdebian --ros-distro "$ROS_DISTRO" - apt-get build-dep . -y || sudo apt-get build-dep . -y - fakeroot debian/rules binary + # Generate debian files and build package + bloom-generate rosdebian --ros-distro "$ROS_DISTRO" + apt-get build-dep . -y || sudo apt-get build-dep . -y + fakeroot debian/rules binary - # Move package to output directory - cp ../ros-$ROS_DISTRO-${package_name//_/-}_*.deb "../$DEBIAN_DIR/" + # Move package to output directory + cp ../ros-$ROS_DISTRO-${package_name//_/-}_*.deb "../$DEBIAN_DIR/" - cd .. + cd .. - echo "Successfully built $package_name" + echo "Successfully built $package_name" } # Function to install a package locally install_package() { - local package_pattern=$1 - echo "Installing $package_pattern..." - apt-get update || sudo apt-get update - apt-get install -y ./$DEBIAN_DIR/$package_pattern || sudo apt-get install -y ./$DEBIAN_DIR/$package_pattern + local package_pattern=$1 + echo "Installing $package_pattern..." + apt-get update || sudo apt-get update + apt-get install -y ./$DEBIAN_DIR/$package_pattern || sudo apt-get install -y ./$DEBIAN_DIR/$package_pattern } # Build packages in dependency order @@ -203,18 +201,18 @@ echo "Starting Debian package generation..." # 1. Build greenwave_monitor_interfaces if [ -d "greenwave_monitor_interfaces" ]; then - build_debian_package "greenwave_monitor_interfaces" "greenwave_monitor_interfaces" - install_package "ros-$ROS_DISTRO-greenwave-monitor-interfaces_*.deb" + build_debian_package "greenwave_monitor_interfaces" "greenwave_monitor_interfaces" + install_package "ros-$ROS_DISTRO-greenwave-monitor-interfaces_*.deb" else - echo "Warning: greenwave_monitor_interfaces directory not found, skipping" + echo "Warning: greenwave_monitor_interfaces directory not found, skipping" fi # 2. Build greenwave_monitor if [ -d "greenwave_monitor" ]; then - build_debian_package "greenwave_monitor" "greenwave_monitor" - install_package "ros-$ROS_DISTRO-greenwave-monitor_*.deb" + build_debian_package "greenwave_monitor" "greenwave_monitor" + install_package "ros-$ROS_DISTRO-greenwave-monitor_*.deb" else - echo "Warning: greenwave_monitor directory not found, skipping" + echo "Warning: greenwave_monitor directory not found, skipping" fi # Note: r2s_gw is now a separate repository and not included in debian packages diff --git a/scripts/docker-test.sh b/scripts/docker-test.sh index 16a4ebb..dc5ec0c 100755 --- a/scripts/docker-test.sh +++ b/scripts/docker-test.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,26 +27,26 @@ DISTRO=${1:-humble} # Image mapping based on ROS distro case $DISTRO in - humble) - IMAGE="ros:humble-ros-base-jammy" - ;; - iron) - IMAGE="ros:iron-ros-base-jammy" - ;; - jazzy) - IMAGE="ros:jazzy-ros-base-noble" - ;; - kilted) - IMAGE="ros:kilted-ros-base-noble" - ;; - rolling) - IMAGE="ros:rolling-ros-base-noble" - ;; - *) - echo "Unsupported ROS 2 distribution: $DISTRO" - echo "Supported: humble, iron, jazzy, kilted, rolling" - exit 1 - ;; +humble) + IMAGE="ros:humble-ros-base-jammy" + ;; +iron) + IMAGE="ros:iron-ros-base-jammy" + ;; +jazzy) + IMAGE="ros:jazzy-ros-base-noble" + ;; +kilted) + IMAGE="ros:kilted-ros-base-noble" + ;; +rolling) + IMAGE="ros:rolling-ros-base-noble" + ;; +*) + echo "Unsupported ROS 2 distribution: $DISTRO" + echo "Supported: humble, iron, jazzy, kilted, rolling" + exit 1 + ;; esac echo "Starting Docker container for ROS 2 $DISTRO..." @@ -57,14 +57,14 @@ WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../" # Run container with interactive shell, mounting current directory docker run -it --rm \ - --name greenwave-test-${DISTRO} \ - -e ROS_LOCALHOST_ONLY=1 \ - -e ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST \ - -e ROS_DISTRO=${DISTRO} \ - -v ${WORKSPACE_DIR}:/workspace/src/greenwave_monitor \ - -w /workspace \ - ${IMAGE} \ - bash -c " + --name greenwave-test-${DISTRO} \ + -e ROS_LOCALHOST_ONLY=1 \ + -e ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST \ + -e ROS_DISTRO=${DISTRO} \ + -v ${WORKSPACE_DIR}:/workspace/src/greenwave_monitor \ + -w /workspace \ + ${IMAGE} \ + bash -c " # Source ROS setup source /opt/ros/${DISTRO}/setup.bash @@ -100,4 +100,4 @@ docker run -it --rm \ # Start interactive shell bash - " \ No newline at end of file + " From dfdf8234bbf322c7feac27ad64aa18aaa46603e0 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 13 Jan 2026 23:13:53 -0800 Subject: [PATCH 03/33] More config fixes Signed-off-by: Blake McHale --- .pre-commit-config.yaml | 12 +++++++----- greenwave_monitor_interfaces/package.xml | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 862ebe7..2a24436 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -exclude: ^third_party/ +exclude: ^(build|install)/ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -32,12 +32,14 @@ repos: rev: 1.6.1 hooks: - id: copyright-required - exclude: '(\.ini|\.json|\.service|__init__\.py|\.md|\.gitkeep|\.conf|LICENSE|\.toml|\.template|\.style\..*|\.gitattributes|\.gitignore|\.editorconfig|\.bash-completion|\.install|\.links|changelog|debian/source/format|.codespellrc|copyright)$' -- repo: https://github.com/pocc/pre-commit-hooks - rev: v1.3.5 + exclude: '(\.ini|\.json|\.service|__init__\.py|\.md|\.gitkeep|\.conf|LICENSE|\.toml|\.template|\.style\..*|\.gitattributes|\.gitignore|\.editorconfig|\.bash-completion|\.install|\.links|changelog|debian/source/format|.codespellrc|copyright|ament_code_style\.cfg|test_pep257\.py|test_flake8\.py|test_copyright\.py)$' +- repo: local hooks: - id: uncrustify - args: [-c, ament_code_style.cfg] + name: uncrustify + entry: uncrustify -c ament_code_style.cfg --replace --no-backup + language: system + types_or: [c, c++] - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: diff --git a/greenwave_monitor_interfaces/package.xml b/greenwave_monitor_interfaces/package.xml index 8e9e6ee..011eb9b 100644 --- a/greenwave_monitor_interfaces/package.xml +++ b/greenwave_monitor_interfaces/package.xml @@ -1,4 +1,23 @@ + + + greenwave_monitor_interfaces From 624776f68f6d64881245c55d14fe42a5caa61392 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Wed, 14 Jan 2026 08:35:13 -0800 Subject: [PATCH 04/33] Match flake8 to ament_flake8 Signed-off-by: Blake McHale --- .pre-commit-config.yaml | 26 ++++++++++++------- greenwave_monitor/examples/example.launch.py | 3 +-- .../greenwave_monitor/ncurses_frontend.py | 3 ++- .../greenwave_monitor/test_utils.py | 6 ++--- .../greenwave_monitor/ui_adaptor.py | 16 +++++++----- .../test/test_greenwave_monitor.py | 9 ++++--- .../test/test_topic_monitoring_integration.py | 6 +++-- 7 files changed, 41 insertions(+), 28 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a24436..a8c6e00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,16 +14,22 @@ # limitations under the License. exclude: ^(build|install)/ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/google/yapf - rev: v0.43.0 - hooks: - - id: yapf +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/hhatto/autopep8 + rev: v2.3.1 + hooks: + - id: autopep8 + args: [-i, -a, -a] +- repo: https://github.com/pycqa/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + args: [--max-line-length=99, '--extend-ignore=B902,C816,D100,D101,D102,D103,D104,D105,D106,D107,D203,D212,D404,I202'] - repo: https://github.com/scop/pre-commit-shfmt rev: v3.12.0-2 hooks: diff --git a/greenwave_monitor/examples/example.launch.py b/greenwave_monitor/examples/example.launch.py index 7772574..cf149c4 100644 --- a/greenwave_monitor/examples/example.launch.py +++ b/greenwave_monitor/examples/example.launch.py @@ -80,7 +80,6 @@ def generate_launch_description(): >>>>>>> 3f640d3 (Beginning of linting journey) ), LogInfo( - msg= - 'Run `ros2 run r2s_gw r2s_gw` in another terminal to see the demo output ' + msg='Run `ros2 run r2s_gw r2s_gw` in another terminal to see the demo output ' 'with the r2s dashboard.'), ]) diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index 06c9636..8981a4e 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -104,7 +104,8 @@ def update_visible_topics(self): all_topic_names = list(self.all_topics) if self.hide_unmonitored and self.ui_adaptor: - # Filter to only show topics that have diagnostic data (are being monitored) + # Filter to only show topics that have diagnostic data (are being + # monitored) filtered_topics = [] for topic_name in all_topic_names: diag = self.ui_adaptor.get_topic_diagnostics(topic_name) diff --git a/greenwave_monitor/greenwave_monitor/test_utils.py b/greenwave_monitor/greenwave_monitor/test_utils.py index 0c0d031..dc9386a 100644 --- a/greenwave_monitor/greenwave_monitor/test_utils.py +++ b/greenwave_monitor/greenwave_monitor/test_utils.py @@ -89,8 +89,7 @@ def wait_for_service_connection(node: Node, timeout_sec=timeout_sec) if not service_available: node.get_logger().error( - f'Service "{service_name}" not available within {timeout_sec} seconds' - ) + f'Service "{service_name}" not available within {timeout_sec} seconds') return service_available @@ -243,8 +242,7 @@ def verify_diagnostic_values(status: DiagnosticStatus, if message_type == 'string': if reported_frequency_msg != 0.0: errors.append( - f'String message frequency should be 0.0, got {reported_frequency_msg}' - ) + f'String message frequency should be 0.0, got {reported_frequency_msg}') if not math.isnan(reported_latency_ms): errors.append(f'String latency should be {math.nan}, ' f'got {reported_latency_ms}') diff --git a/greenwave_monitor/greenwave_monitor/ui_adaptor.py b/greenwave_monitor/greenwave_monitor/ui_adaptor.py index 7d839d7..474c606 100644 --- a/greenwave_monitor/greenwave_monitor/ui_adaptor.py +++ b/greenwave_monitor/greenwave_monitor/ui_adaptor.py @@ -123,7 +123,8 @@ def _setup_ros_components(self): DiagnosticArray, '/diagnostics', self._on_diagnostics, 100) manage_service_name = f'{self.monitor_node_name}/manage_topic' - set_freq_service_name = f'{self.monitor_node_name}/set_expected_frequency' + set_freq_service_name = f'{ + self.monitor_node_name}/set_expected_frequency' self.node.get_logger().info( f'Connecting to monitor service: {manage_service_name}') @@ -144,12 +145,14 @@ def _extract_topic_name(self, diagnostic_name: str) -> str: This is a temporary hack until NITROS migrates to greenwave_diagnostics.hpp. """ - # If the name starts with '/', it's already just a topic name (Greenwave format) + # If the name starts with '/', it's already just a topic name + # (Greenwave format) if diagnostic_name.startswith('/'): return diagnostic_name # NITROS format: node_name + namespace + "/" + topic_name - # Node names cannot contain '/', so the first '/' marks where namespace+topic begins + # Node names cannot contain '/', so the first '/' marks where + # namespace+topic begins idx = diagnostic_name.find('/') if idx >= 0: return diagnostic_name[idx:] @@ -164,7 +167,8 @@ def _on_diagnostics(self, msg: DiagnosticArray): for status in msg.status: ui_data = UiDiagnosticData.from_status(status) ui_data.last_update = time.time() - # Normalize the topic name to handle both NITROS and Greenwave formats + # Normalize the topic name to handle both NITROS and Greenwave + # formats topic_name = self._extract_topic_name(status.name) self.ui_diagnostics[topic_name] = ui_data try: @@ -255,8 +259,8 @@ def set_expected_frequency(self, if not response.success: action = 'clear' if clear else 'set' self.node.get_logger().error( - f'Failed to {action} expected frequency: {response.message}' - ) + f'Failed to {action} expected frequency: { + response.message}') return False, response.message else: with self.data_lock: diff --git a/greenwave_monitor/test/test_greenwave_monitor.py b/greenwave_monitor/test/test_greenwave_monitor.py index 11095b9..011270f 100644 --- a/greenwave_monitor/test/test_greenwave_monitor.py +++ b/greenwave_monitor/test/test_greenwave_monitor.py @@ -176,7 +176,8 @@ def tearDownClass(cls): def check_node_launches_successfully(self): """Test that the node launches without errors.""" # Create a service client to check if the node is ready - # Service discovery is more reliable than node discovery in launch_testing + # Service discovery is more reliable than node discovery in + # launch_testing manage_client, set_freq_client = create_service_clients( self.test_node, MONITOR_NODE_NAMESPACE, MONITOR_NODE_NAME) service_available = wait_for_service_connection( @@ -251,7 +252,8 @@ def test_manage_one_topic(self, expected_frequency, message_type, service_client=service_client) self.assertTrue(response.success) - # 2. Removing the same topic again should fail because it no longer exists. + # 2. Removing the same topic again should fail because it no longer + # exists. response = self.call_manage_topic(add=False, topic=TEST_TOPIC, service_client=service_client) @@ -267,7 +269,8 @@ def test_manage_one_topic(self, expected_frequency, message_type, self.verify_diagnostics(TEST_TOPIC, expected_frequency, message_type, tolerance_hz) - # 4. Adding the same topic again should fail because it's already monitored. + # 4. Adding the same topic again should fail because it's already + # monitored. response = self.call_manage_topic(add=True, topic=TEST_TOPIC, service_client=service_client) diff --git a/greenwave_monitor/test/test_topic_monitoring_integration.py b/greenwave_monitor/test/test_topic_monitoring_integration.py index 16d10d4..d328555 100644 --- a/greenwave_monitor/test/test_topic_monitoring_integration.py +++ b/greenwave_monitor/test/test_topic_monitoring_integration.py @@ -166,11 +166,13 @@ def tearDownClass(cls): def setUp(self): """Set up for each test.""" - # Create a fresh GreenwaveUiAdaptor instance for each test with proper namespace + # Create a fresh GreenwaveUiAdaptor instance for each test with proper + # namespace self.diagnostics_monitor = GreenwaveUiAdaptor( self.test_node, monitor_node_name=MONITOR_NODE_NAME) - # Allow time for service discovery in test environment (reduced from 2.0s) + # Allow time for service discovery in test environment (reduced from + # 2.0s) time.sleep(1.0) def tearDown(self): From e42b1d66cf0f16229ec551f3e651f6f066698d24 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Wed, 14 Jan 2026 08:50:27 -0800 Subject: [PATCH 05/33] Run lint again Signed-off-by: Blake McHale --- greenwave_monitor/examples/example.launch.py | 32 ++-- .../greenwave_monitor/ncurses_frontend.py | 100 ++++++------ .../greenwave_monitor/test_utils.py | 97 +++++++----- .../greenwave_monitor/ui_adaptor.py | 35 +++-- .../launch/test_publishers.launch.py | 33 ++-- .../test/test_greenwave_monitor.py | 145 +++++++++++------- .../test/test_ncurses_frontend_argparse.py | 4 +- .../test/test_topic_monitoring_integration.py | 121 +++++++++------ 8 files changed, 322 insertions(+), 245 deletions(-) diff --git a/greenwave_monitor/examples/example.launch.py b/greenwave_monitor/examples/example.launch.py index cf149c4..d479205 100644 --- a/greenwave_monitor/examples/example.launch.py +++ b/greenwave_monitor/examples/example.launch.py @@ -30,39 +30,33 @@ def generate_launch_description(): executable='minimal_publisher_node', name='minimal_publisher1', output='log', - parameters=[{ - 'topic': 'imu_topic', - 'frequency_hz': 100.0 - }], + parameters=[ + {'topic': 'imu_topic', 'frequency_hz': 100.0} + ], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher2', output='log', - parameters=[{ - 'topic': 'image_topic', - 'message_type': 'image', - 'frequency_hz': 30.0 - }], + parameters=[ + {'topic': 'image_topic', 'message_type': 'image', 'frequency_hz': 30.0} + ], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher3', output='log', - parameters=[{ - 'topic': 'string_topic', - 'message_type': 'string', - 'frequency_hz': 1000.0 - }], + parameters=[ + {'topic': 'string_topic', 'message_type': 'string', 'frequency_hz': 1000.0} + ], ), Node( package='greenwave_monitor', executable='greenwave_monitor', name='greenwave_monitor', output='log', -<<<<<<< HEAD # Example of inline parameter settings # parameters=[{ # 'gw_monitored_topics': ['/string_topic'], @@ -73,13 +67,9 @@ def generate_launch_description(): # }], # Example of using a config file parameters=[config_file], -======= - parameters=[{ - 'topics': ['/imu_topic', '/image_topic', '/string_topic'] - }], ->>>>>>> 3f640d3 (Beginning of linting journey) ), LogInfo( msg='Run `ros2 run r2s_gw r2s_gw` in another terminal to see the demo output ' - 'with the r2s dashboard.'), + 'with the r2s dashboard.' + ), ]) diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index 8981a4e..d74cbf0 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -16,6 +16,7 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 + """ Ncurses-based frontend for Greenwave Monitor. @@ -65,8 +66,8 @@ def __init__(self, hide_unmonitored: bool = False): self.ui_adaptor = GreenwaveUiAdaptor(self) # Timer to periodically update the topic list - self.topic_update_timer = self.create_timer(5.0, - self.update_topic_list) + self.topic_update_timer = self.create_timer( + 5.0, self.update_topic_list) # Initial topic list update self.update_topic_list() @@ -80,14 +81,12 @@ def update_topic_list(self): topic_names_and_types = [ (topic_name, topic_type) for topic_name, topic_type in topic_names_and_types - if (not topic_name.endswith('/nitros')) and ( - '/nitros/' not in topic_name) + if (not topic_name.endswith('/nitros')) and ('/nitros/' not in topic_name) ] topic_set = { - topic_name - for topic_name, topic_type in topic_names_and_types - } + topic_name for topic_name, + topic_type in topic_names_and_types} with self.topics_lock: # Update topics @@ -177,8 +176,11 @@ def curses_main(stdscr, node): key = -1 # Only redraw if we got input or enough time has passed - should_redraw = (key != -1) or (current_time - last_redraw - >= redraw_interval) + should_redraw = ( + key != - + 1) or ( + current_time - + last_redraw >= redraw_interval) if not should_redraw: time.sleep(0.01) @@ -195,9 +197,9 @@ def curses_main(stdscr, node): STATUS_WIDTH = 18 BUTTON_WIDTH = 10 - total_width_needed = (MAX_NAME_WIDTH + 2 * FRAME_RATE_WIDTH + - REALTIME_DELAY_WIDTH + STATUS_WIDTH + - BUTTON_WIDTH + 5) + total_width_needed = ( + MAX_NAME_WIDTH + 2 * FRAME_RATE_WIDTH + REALTIME_DELAY_WIDTH + + STATUS_WIDTH + BUTTON_WIDTH + 5) if total_width_needed > width: scaling_factor = width / total_width_needed MAX_NAME_WIDTH = int(MAX_NAME_WIDTH * scaling_factor) @@ -207,12 +209,22 @@ def curses_main(stdscr, node): # Draw header header = ( - f'{"Topic Name":<{MAX_NAME_WIDTH}} {"Status":<{STATUS_WIDTH}} ' - f'{"Pub Rate (Hz)":<{FRAME_RATE_WIDTH}} ' - f'{"Latency (ms)":<{REALTIME_DELAY_WIDTH}} {"Expected Hz":<12}') + f'{ + "Topic Name":<{MAX_NAME_WIDTH}} { + "Status":<{STATUS_WIDTH}} ' f'{ + "Pub Rate (Hz)":<{FRAME_RATE_WIDTH}} ' f'{ + "Latency (ms)":<{REALTIME_DELAY_WIDTH}} { + "Expected Hz":<12}') separator_width = min( - width - BUTTON_WIDTH - 2, MAX_NAME_WIDTH + FRAME_RATE_WIDTH + - REALTIME_DELAY_WIDTH + STATUS_WIDTH + 12 + 4) + width - + BUTTON_WIDTH - + 2, + MAX_NAME_WIDTH + + FRAME_RATE_WIDTH + + REALTIME_DELAY_WIDTH + + STATUS_WIDTH + + 12 + + 4) separator = '-' * separator_width try: @@ -274,12 +286,10 @@ def curses_main(stdscr, node): elif key == curses.KEY_NPAGE: # Page Down visible_height = height - 5 if len(node.visible_topics) > 0: - start_idx = min( - len(node.visible_topics) - visible_height, - start_idx + visible_height) + start_idx = min(len(node.visible_topics) - visible_height, + start_idx + visible_height) selected_row = min( - len(node.visible_topics) - 1, - selected_row + visible_height) + len(node.visible_topics) - 1, selected_row + visible_height) elif key == ord('q') or key == ord('Q'): node.running = False break @@ -320,14 +330,12 @@ def curses_main(stdscr, node): start_idx = 0 else: selected_row = min(selected_row, len(visible_topics) - 1) - start_idx = min(start_idx, - max(0, - len(visible_topics) - (height - 5))) + start_idx = min(start_idx, max( + 0, len(visible_topics) - (height - 5))) start_idx = max(0, start_idx) visible_height = height - 5 - visible_topics_slice = visible_topics[start_idx:start_idx + - visible_height] + visible_topics_slice = visible_topics[start_idx:start_idx + visible_height] # Draw visible topics for idx, topic_name in enumerate(visible_topics_slice): @@ -346,12 +354,10 @@ def curses_main(stdscr, node): is_monitored = True status_display = diag.status # Use actual diagnostic status frame_rate_node = (diag.pub_rate.ljust(FRAME_RATE_WIDTH) - if diag.pub_rate != '-' else - 'N/A'.ljust(FRAME_RATE_WIDTH)) + if diag.pub_rate != '-' else 'N/A'.ljust(FRAME_RATE_WIDTH)) current_delay_from_realtime_ms = ( diag.latency.ljust(REALTIME_DELAY_WIDTH) - if diag.latency != '-' else - 'N/A'.ljust(REALTIME_DELAY_WIDTH)) + if diag.latency != '-' else 'N/A'.ljust(REALTIME_DELAY_WIDTH)) # Get expected frequency expected_hz, tolerance = node.ui_adaptor.get_expected_frequency(topic_name) @@ -369,8 +375,8 @@ def curses_main(stdscr, node): elif status_display == 'ERROR': color_pair = curses.color_pair(COLOR_ERROR) else: - color_pair = curses.color_pair( - COLOR_OK) # Default green for monitored + # Default green for monitored + color_pair = curses.color_pair(COLOR_OK) else: color_pair = curses.color_pair(COLOR_UNMONITORED) @@ -397,9 +403,8 @@ def curses_main(stdscr, node): button_x = width - len(button_text) - 1 if is_selected: - button_color = (curses.color_pair(COLOR_ERROR) - if is_monitored else - curses.color_pair(COLOR_BUTTON_ADD)) + button_color = (curses.color_pair(COLOR_ERROR) if is_monitored + else curses.color_pair(COLOR_BUTTON_ADD)) else: button_color = curses.color_pair(COLOR_WARN) stdscr.addstr(idx + 2, button_x, button_text, button_color) @@ -450,8 +455,10 @@ def curses_main(stdscr, node): curses.curs_set(0) # Hide cursor when not in input mode # Footer - num_shown = min(start_idx + len(visible_topics_slice), - len(visible_topics)) + num_shown = min( + start_idx + + len(visible_topics_slice), + len(visible_topics)) if input_mode: status_line = ( "Format: Hz [tolerance%] - Examples: '30' (30Hz±5% default) " @@ -479,10 +486,13 @@ def curses_main(stdscr, node): def parse_args(args=None): """Parse command-line arguments.""" parser = argparse.ArgumentParser( - description='Ncurses-based frontend for Greenwave Monitor') - parser.add_argument('--hide-unmonitored', - action='store_true', - help='Hide unmonitored topics on initialization') + description='Ncurses-based frontend for Greenwave Monitor' + ) + parser.add_argument( + '--hide-unmonitored', + action='store_true', + help='Hide unmonitored topics on initialization' + ) return parser.parse_known_args(args) @@ -503,9 +513,9 @@ def signal_handler(signum, frame): signal.signal(signal.SIGTERM, signal_handler) try: - thread = threading.Thread(target=rclpy.spin, - args=(node, ), - daemon=False) + thread = threading.Thread( + target=rclpy.spin, args=( + node,), daemon=False) thread.start() curses.wrapper(curses_main, node) except KeyboardInterrupt: diff --git a/greenwave_monitor/greenwave_monitor/test_utils.py b/greenwave_monitor/greenwave_monitor/test_utils.py index dc9386a..aa05dd7 100644 --- a/greenwave_monitor/greenwave_monitor/test_utils.py +++ b/greenwave_monitor/greenwave_monitor/test_utils.py @@ -27,6 +27,7 @@ import rclpy from rclpy.node import Node + # Test configurations for various message types and frequencies # (message_type, expected_frequency, tolerance_hz) # NOTE: Tolerances and frequencies are set conservatively for reliable operation @@ -47,20 +48,23 @@ MONITOR_NODE_NAMESPACE = 'test_namespace' -def create_minimal_publisher(topic: str, - frequency_hz: float, - message_type: str, - id_suffix: str = ''): +def create_minimal_publisher( + topic: str, + frequency_hz: float, + message_type: str, + id_suffix: str = ''): """Create a minimal publisher node with the given parameters.""" - return launch_ros.actions.Node(package='greenwave_monitor', - executable='minimal_publisher_node', - name=f'minimal_publisher_node{id_suffix}', - parameters=[{ - 'topic': topic, - 'frequency_hz': frequency_hz, - 'message_type': message_type - }], - output='screen') + return launch_ros.actions.Node( + package='greenwave_monitor', + executable='minimal_publisher_node', + name=f'minimal_publisher_node{id_suffix}', + parameters=[{ + 'topic': topic, + 'frequency_hz': frequency_hz, + 'message_type': message_type + }], + output='screen' + ) def create_monitor_node(namespace: str = MONITOR_NODE_NAMESPACE, @@ -93,12 +97,12 @@ def wait_for_service_connection(node: Node, return service_available -def call_manage_topic_service( - node: Node, - service_client, - add: bool, - topic: str, - timeout_sec: float = 8.0) -> Optional[ManageTopic.Response]: +def call_manage_topic_service(node: Node, + service_client, + add: bool, + topic: str, + timeout_sec: float = 8.0 + ) -> Optional[ManageTopic.Response]: """Call the manage_topic service with given parameters.""" request = ManageTopic.Request() request.add_topic = add @@ -114,15 +118,15 @@ def call_manage_topic_service( return future.result() -def call_set_frequency_service( - node: Node, - service_client, - topic_name: str, - expected_hz: float = 0.0, - tolerance_percent: float = 0.0, - clear: bool = False, - add_if_missing: bool = True, - timeout_sec: float = 8.0) -> Optional[SetExpectedFrequency.Response]: +def call_set_frequency_service(node: Node, + service_client, + topic_name: str, + expected_hz: float = 0.0, + tolerance_percent: float = 0.0, + clear: bool = False, + add_if_missing: bool = True, + timeout_sec: float = 8.0 + ) -> Optional[SetExpectedFrequency.Response]: """Call the set_expected_frequency service with given parameters.""" request = SetExpectedFrequency.Request() request.topic_name = topic_name @@ -154,8 +158,12 @@ def diagnostics_callback(msg): if topic_name == status.name: received_diagnostics.append(status) - subscription = node.create_subscription(DiagnosticArray, '/diagnostics', - diagnostics_callback, 10) + subscription = node.create_subscription( + DiagnosticArray, + '/diagnostics', + diagnostics_callback, + 10 + ) end_time = time.time() + timeout_sec while time.time() < end_time: @@ -170,8 +178,9 @@ def diagnostics_callback(msg): def find_best_diagnostic( - diagnostics: List[DiagnosticStatus], expected_frequency: float, - message_type: str + diagnostics: List[DiagnosticStatus], + expected_frequency: float, + message_type: str ) -> Tuple[Optional[DiagnosticStatus], Optional[Tuple[float, float, float]]]: """Find the diagnostic message with frequency closest to expected.""" best_status = None @@ -219,7 +228,8 @@ def find_best_diagnostic( def verify_diagnostic_values(status: DiagnosticStatus, values: Tuple[float, float, float], - expected_frequency: float, message_type: str, + expected_frequency: float, + message_type: str, tolerance_hz: float) -> List[str]: """Verify diagnostic values and return list of assertion errors.""" errors = [] @@ -236,16 +246,18 @@ def verify_diagnostic_values(status: DiagnosticStatus, # Check frequency tolerances if abs(reported_frequency_node - expected_frequency) > tolerance_hz: - errors.append(f'Node frequency {reported_frequency_node} not within ' - f'{tolerance_hz} Hz of expected {expected_frequency}') + errors.append( + f'Node frequency {reported_frequency_node} not within ' + f'{tolerance_hz} Hz of expected {expected_frequency}') if message_type == 'string': if reported_frequency_msg != 0.0: errors.append( f'String message frequency should be 0.0, got {reported_frequency_msg}') if not math.isnan(reported_latency_ms): - errors.append(f'String latency should be {math.nan}, ' - f'got {reported_latency_ms}') + errors.append( + f'String latency should be {math.nan}, ' + f'got {reported_latency_ms}') else: if abs(reported_frequency_msg - expected_frequency) > tolerance_hz: errors.append( @@ -253,18 +265,19 @@ def verify_diagnostic_values(status: DiagnosticStatus, f'{tolerance_hz} Hz of expected {expected_frequency}') # Relaxed to 50ms for slow/loaded CI systems (was 10ms) if reported_latency_ms > 50: - errors.append(f'Latency should be <= 50 ms for non-string types, ' - f'got {reported_latency_ms}') + errors.append( + f'Latency should be <= 50 ms for non-string types, ' + f'got {reported_latency_ms}') return errors -def create_service_clients(node: Node, - namespace: str = MONITOR_NODE_NAMESPACE, +def create_service_clients(node: Node, namespace: str = MONITOR_NODE_NAMESPACE, node_name: str = MONITOR_NODE_NAME): """Create service clients for the monitor node.""" manage_topic_client = node.create_client( - ManageTopic, f'/{namespace}/{node_name}/manage_topic') + ManageTopic, f'/{namespace}/{node_name}/manage_topic' + ) set_frequency_client = node.create_client( SetExpectedFrequency, diff --git a/greenwave_monitor/greenwave_monitor/ui_adaptor.py b/greenwave_monitor/greenwave_monitor/ui_adaptor.py index 474c606..373b2e5 100644 --- a/greenwave_monitor/greenwave_monitor/ui_adaptor.py +++ b/greenwave_monitor/greenwave_monitor/ui_adaptor.py @@ -16,6 +16,7 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 + """ Greenwave monitor diagnostics helpers for UI frontends. @@ -104,9 +105,10 @@ class GreenwaveUiAdaptor: """ - def __init__(self, - node: Node, - monitor_node_name: str = 'greenwave_monitor'): + def __init__( + self, + node: Node, + monitor_node_name: str = 'greenwave_monitor'): """Initialize the UI adaptor for subscribing to diagnostics and managing topics.""" self.node = node self.monitor_node_name = monitor_node_name @@ -120,7 +122,11 @@ def __init__(self, def _setup_ros_components(self): """Initialize ROS2 subscriptions, clients, and timers.""" self.subscription = self.node.create_subscription( - DiagnosticArray, '/diagnostics', self._on_diagnostics, 100) + DiagnosticArray, + '/diagnostics', + self._on_diagnostics, + 100 + ) manage_service_name = f'{self.monitor_node_name}/manage_topic' set_freq_service_name = f'{ @@ -130,10 +136,14 @@ def _setup_ros_components(self): f'Connecting to monitor service: {manage_service_name}') self.manage_topic_client = self.node.create_client( - ManageTopic, manage_service_name) + ManageTopic, + manage_service_name + ) self.set_expected_frequency_client = self.node.create_client( - SetExpectedFrequency, set_freq_service_name) + SetExpectedFrequency, + set_freq_service_name + ) def _extract_topic_name(self, diagnostic_name: str) -> str: """ @@ -196,9 +206,8 @@ def toggle_topic_monitoring(self, topic_name: str): try: # Use asynchronous service call to prevent deadlock future = self.manage_topic_client.call_async(request) - rclpy.spin_until_future_complete(self.node, - future, - timeout_sec=3.0) + rclpy.spin_until_future_complete( + self.node, future, timeout_sec=3.0) if future.result() is None: action = 'start' if request.add_topic else 'stop' @@ -228,7 +237,8 @@ def set_expected_frequency(self, topic_name: str, expected_hz: float = 0.0, tolerance_percent: float = 0.0, - clear: bool = False) -> tuple[bool, str]: + clear: bool = False + ) -> tuple[bool, str]: """Set or clear the expected frequency for a topic.""" if not self.set_expected_frequency_client.wait_for_service( timeout_sec=1.0): @@ -244,9 +254,8 @@ def set_expected_frequency(self, # Use asynchronous service call to prevent deadlock try: future = self.set_expected_frequency_client.call_async(request) - rclpy.spin_until_future_complete(self.node, - future, - timeout_sec=3.0) + rclpy.spin_until_future_complete( + self.node, future, timeout_sec=3.0) if future.result() is None: action = 'clear' if clear else 'set' diff --git a/greenwave_monitor/launch/test_publishers.launch.py b/greenwave_monitor/launch/test_publishers.launch.py index 955b337..728e5a0 100644 --- a/greenwave_monitor/launch/test_publishers.launch.py +++ b/greenwave_monitor/launch/test_publishers.launch.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + """Launch file with minimal publishers for testing the greenwave monitor.""" + from launch import LaunchDescription from launch_ros.actions import Node @@ -24,42 +26,35 @@ def generate_launch_description(): executable='minimal_publisher_node', name='minimal_publisher1', output='log', - parameters=[{ - 'topic': 'imu_topic', - 'frequency_hz': 100.0 - }], + parameters=[ + {'topic': 'imu_topic', 'frequency_hz': 100.0} + ], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher2', output='log', - parameters=[{ - 'topic': 'image_topic', - 'message_type': 'image', - 'frequency_hz': 30.0 - }], + parameters=[ + {'topic': 'image_topic', 'message_type': 'image', 'frequency_hz': 30.0} + ], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher3', output='log', - parameters=[{ - 'topic': 'string_topic', - 'message_type': 'string', - 'frequency_hz': 1000.0 - }], + parameters=[ + {'topic': 'string_topic', 'message_type': 'string', 'frequency_hz': 1000.0} + ], ), Node( package='greenwave_monitor', executable='minimal_publisher_node', name='minimal_publisher4', output='log', - parameters=[{ - 'topic': 'slow_string_topic', - 'message_type': 'string', - 'frequency_hz': 0.5 - }], + parameters=[ + {'topic': 'slow_string_topic', 'message_type': 'string', 'frequency_hz': 0.5} + ], ), ]) diff --git a/greenwave_monitor/test/test_greenwave_monitor.py b/greenwave_monitor/test/test_greenwave_monitor.py index 011270f..92995ef 100644 --- a/greenwave_monitor/test/test_greenwave_monitor.py +++ b/greenwave_monitor/test/test_greenwave_monitor.py @@ -24,11 +24,19 @@ import unittest from greenwave_monitor.test_utils import ( - call_manage_topic_service, collect_diagnostics_for_topic, - create_minimal_publisher, create_monitor_node, create_service_clients, - find_best_diagnostic, MANAGE_TOPIC_TEST_CONFIG, MONITOR_NODE_NAME, - MONITOR_NODE_NAMESPACE, TEST_CONFIGURATIONS, verify_diagnostic_values, - wait_for_service_connection) + call_manage_topic_service, + collect_diagnostics_for_topic, + create_minimal_publisher, + create_monitor_node, + create_service_clients, + find_best_diagnostic, + MANAGE_TOPIC_TEST_CONFIG, + MONITOR_NODE_NAME, + MONITOR_NODE_NAMESPACE, + TEST_CONFIGURATIONS, + verify_diagnostic_values, + wait_for_service_connection +) import launch import launch_testing from launch_testing import post_shutdown_test @@ -89,8 +97,9 @@ def create_test_yaml_config(): @pytest.mark.launch_test -@launch_testing.parametrize('message_type, expected_frequency, tolerance_hz', - TEST_CONFIGURATIONS) +@launch_testing.parametrize( + 'message_type, expected_frequency, tolerance_hz', + TEST_CONFIGURATIONS) def generate_test_description(message_type, expected_frequency, tolerance_hz): """Generate launch description for greenwave monitor tests.""" # Create temporary YAML config for testing parameter loading @@ -133,8 +142,8 @@ def generate_test_description(message_type, expected_frequency, tolerance_hz): ros2_monitor_node, *publishers, # Unpack all publishers into the launch description launch_testing.actions.ReadyToTest() - ]), - context) + ]), context + ) @post_shutdown_test() @@ -179,7 +188,8 @@ def check_node_launches_successfully(self): # Service discovery is more reliable than node discovery in # launch_testing manage_client, set_freq_client = create_service_clients( - self.test_node, MONITOR_NODE_NAMESPACE, MONITOR_NODE_NAME) + self.test_node, MONITOR_NODE_NAMESPACE, MONITOR_NODE_NAME + ) service_available = wait_for_service_connection( self.test_node, manage_client, timeout_sec=10.0, service_name=f'/{MONITOR_NODE_NAMESPACE}/{MONITOR_NODE_NAME}/manage_topic' @@ -190,14 +200,17 @@ def check_node_launches_successfully(self): 'not available within timeout') return manage_client - def verify_diagnostics(self, topic_name, expected_frequency, message_type, - tolerance_hz): + def verify_diagnostics( + self, + topic_name, + expected_frequency, + message_type, + tolerance_hz): """Verify diagnostics for a given topic.""" # Collect diagnostic messages using shared utility - received_diagnostics = collect_diagnostics_for_topic(self.test_node, - topic_name, - expected_count=5, - timeout_sec=10.0) + received_diagnostics = collect_diagnostics_for_topic( + self.test_node, topic_name, expected_count=5, timeout_sec=10.0 + ) # We expect the monitor to be publishing diagnostics self.assertGreaterEqual(len(received_diagnostics), 5, @@ -205,23 +218,31 @@ def verify_diagnostics(self, topic_name, expected_frequency, message_type, # Find the best diagnostic message using shared utility best_status, best_values = find_best_diagnostic( - received_diagnostics, expected_frequency, message_type) + received_diagnostics, expected_frequency, message_type + ) self.assertIsNotNone( - best_status, 'Did not find a diagnostic with all required values') + best_status, + 'Did not find a diagnostic with all required values') self.assertEqual(topic_name, best_status.name) # Verify diagnostic values using shared utility - errors = verify_diagnostic_values(best_status, best_values, - expected_frequency, message_type, - tolerance_hz) + errors = verify_diagnostic_values( + best_status, + best_values, + expected_frequency, + message_type, + tolerance_hz) # Assert no errors occurred if errors: self.fail(f"Diagnostic verification failed: {'; '.join(errors)}") - def test_frequency_monitoring(self, expected_frequency, message_type, - tolerance_hz): + def test_frequency_monitoring( + self, + expected_frequency, + message_type, + tolerance_hz): """Test that the monitor node correctly tracks different frequencies.""" # This test runs for all configurations to verify frequency monitoring self.check_node_launches_successfully() @@ -229,16 +250,17 @@ def test_frequency_monitoring(self, expected_frequency, message_type, def call_manage_topic(self, add, topic, service_client): """Service call helper.""" - response = call_manage_topic_service(self.test_node, - service_client, - add, - topic, - timeout_sec=8.0) + response = call_manage_topic_service( + self.test_node, service_client, add, topic, timeout_sec=8.0 + ) self.assertIsNotNone(response, 'Service call failed or timed out') return response - def test_manage_one_topic(self, expected_frequency, message_type, - tolerance_hz): + def test_manage_one_topic( + self, + expected_frequency, + message_type, + tolerance_hz): """Test that add_topic() and remove_topic() work correctly for one topic.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -247,37 +269,39 @@ def test_manage_one_topic(self, expected_frequency, message_type, service_client = self.check_node_launches_successfully() # 1. Remove an existing topic – should succeed on first attempt. - response = self.call_manage_topic(add=False, - topic=TEST_TOPIC, - service_client=service_client) + response = self.call_manage_topic( + add=False, topic=TEST_TOPIC, service_client=service_client) self.assertTrue(response.success) # 2. Removing the same topic again should fail because it no longer # exists. - response = self.call_manage_topic(add=False, - topic=TEST_TOPIC, - service_client=service_client) + response = self.call_manage_topic( + add=False, topic=TEST_TOPIC, service_client=service_client) self.assertFalse(response.success) # 3. Add the topic back – should succeed now. - response = self.call_manage_topic(add=True, - topic=TEST_TOPIC, - service_client=service_client) + response = self.call_manage_topic( + add=True, topic=TEST_TOPIC, service_client=service_client) self.assertTrue(response.success) # Verify diagnostics after adding the topic back - self.verify_diagnostics(TEST_TOPIC, expected_frequency, message_type, - tolerance_hz) + self.verify_diagnostics( + TEST_TOPIC, + expected_frequency, + message_type, + tolerance_hz) # 4. Adding the same topic again should fail because it's already # monitored. - response = self.call_manage_topic(add=True, - topic=TEST_TOPIC, - service_client=service_client) + response = self.call_manage_topic( + add=True, topic=TEST_TOPIC, service_client=service_client) self.assertFalse(response.success) - def test_manage_multiple_topics(self, expected_frequency, message_type, - tolerance_hz): + def test_manage_multiple_topics( + self, + expected_frequency, + message_type, + tolerance_hz): """Test that add_topic() and remove_topic() work correctly for multiple topics.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -297,29 +321,32 @@ def test_manage_multiple_topics(self, expected_frequency, message_type, self.assertFalse(response.success) # 1. Add first topic – should succeed. - response = self.call_manage_topic(add=True, - topic=TEST_TOPIC1, - service_client=service_client) + response = self.call_manage_topic( + add=True, topic=TEST_TOPIC1, service_client=service_client) self.assertTrue(response.success) # Verify diagnostics after adding the first topic - self.verify_diagnostics(TEST_TOPIC1, expected_frequency, message_type, - tolerance_hz) + self.verify_diagnostics( + TEST_TOPIC1, + expected_frequency, + message_type, + tolerance_hz) # 2. Add second topic – should succeed. - response = self.call_manage_topic(add=True, - topic=TEST_TOPIC2, - service_client=service_client) + response = self.call_manage_topic( + add=True, topic=TEST_TOPIC2, service_client=service_client) self.assertTrue(response.success) # Verify diagnostics after adding the second topic - self.verify_diagnostics(TEST_TOPIC2, expected_frequency, message_type, - tolerance_hz) + self.verify_diagnostics( + TEST_TOPIC2, + expected_frequency, + message_type, + tolerance_hz) # 3. Remove first topic – should succeed. - response = self.call_manage_topic(add=False, - topic=TEST_TOPIC1, - service_client=service_client) + response = self.call_manage_topic( + add=False, topic=TEST_TOPIC1, service_client=service_client) self.assertTrue(response.success) def test_yaml_parameter_loading(self, expected_frequency, message_type, tolerance_hz): diff --git a/greenwave_monitor/test/test_ncurses_frontend_argparse.py b/greenwave_monitor/test/test_ncurses_frontend_argparse.py index e3c5ee2..db6e500 100644 --- a/greenwave_monitor/test/test_ncurses_frontend_argparse.py +++ b/greenwave_monitor/test/test_ncurses_frontend_argparse.py @@ -16,6 +16,7 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 + """Tests for ncurses frontend argument parsing.""" from greenwave_monitor.ncurses_frontend import parse_args @@ -37,7 +38,8 @@ def test_hide_unmonitored_long_flag(self): def test_ros_args_passthrough(self): """Test that ROS arguments are passed through.""" parsed_args, ros_args = parse_args( - ['--hide-unmonitored', '--ros-args', '-r', '__node:=my_node']) + ['--hide-unmonitored', '--ros-args', '-r', '__node:=my_node'] + ) assert parsed_args.hide_unmonitored is True assert '--ros-args' in ros_args assert '-r' in ros_args diff --git a/greenwave_monitor/test/test_topic_monitoring_integration.py b/greenwave_monitor/test/test_topic_monitoring_integration.py index d328555..e635f7c 100644 --- a/greenwave_monitor/test/test_topic_monitoring_integration.py +++ b/greenwave_monitor/test/test_topic_monitoring_integration.py @@ -25,8 +25,13 @@ from diagnostic_msgs.msg import DiagnosticStatus from greenwave_monitor.test_utils import ( - create_minimal_publisher, create_monitor_node, MANAGE_TOPIC_TEST_CONFIG, - MONITOR_NODE_NAME, MONITOR_NODE_NAMESPACE, TEST_CONFIGURATIONS) + create_minimal_publisher, + create_monitor_node, + MANAGE_TOPIC_TEST_CONFIG, + MONITOR_NODE_NAME, + MONITOR_NODE_NAMESPACE, + TEST_CONFIGURATIONS +) from greenwave_monitor.ui_adaptor import GreenwaveUiAdaptor, UiDiagnosticData from greenwave_monitor_interfaces.srv import ManageTopic import launch @@ -78,8 +83,9 @@ def create_test_yaml_config(): @pytest.mark.launch_test -@launch_testing.parametrize('message_type, expected_frequency, tolerance_hz', - TEST_CONFIGURATIONS) +@launch_testing.parametrize( + 'message_type, expected_frequency, tolerance_hz', + TEST_CONFIGURATIONS) def generate_test_description(message_type, expected_frequency, tolerance_hz): """Generate launch description for topic monitoring tests.""" # Create temporary YAML config for testing parameter loading @@ -94,13 +100,21 @@ def generate_test_description(message_type, expected_frequency, tolerance_hz): # Create publishers for testing publishers = [ # Main test topic publisher with parametrized frequency - create_minimal_publisher('/test_topic', expected_frequency, - message_type), + create_minimal_publisher( + '/test_topic', + expected_frequency, + message_type), # Additional publishers for topic management tests - create_minimal_publisher('/test_topic1', expected_frequency, - message_type, '1'), - create_minimal_publisher('/test_topic2', expected_frequency, - message_type, '2'), + create_minimal_publisher( + '/test_topic1', + expected_frequency, + message_type, + '1'), + create_minimal_publisher( + '/test_topic2', + expected_frequency, + message_type, + '2'), # Publisher for service discovery tests create_minimal_publisher('/discovery_test_topic', 50.0, 'imu', '_discovery'), # Publisher for YAML config test @@ -119,9 +133,13 @@ def generate_test_description(message_type, expected_frequency, tolerance_hz): 'tolerance_hz': tolerance_hz, } - return (launch.LaunchDescription( - [ros2_monitor_node, *publishers, - launch_testing.actions.ReadyToTest()]), context) + return ( + launch.LaunchDescription([ + ros2_monitor_node, + *publishers, + launch_testing.actions.ReadyToTest() + ]), context + ) @post_shutdown_test() @@ -132,8 +150,9 @@ class TestTopicMonitoringPostShutdown(unittest.TestCase): def setUpClass(cls): """Initialize ROS2 and create test node.""" rclpy.init() - cls.test_node = Node('shutdown_test_node', - namespace=MONITOR_NODE_NAMESPACE) + cls.test_node = Node( + 'shutdown_test_node', + namespace=MONITOR_NODE_NAMESPACE) @classmethod def tearDownClass(cls): @@ -155,8 +174,9 @@ class TestTopicMonitoringIntegration(unittest.TestCase): def setUpClass(cls): """Initialize ROS2 and create test node.""" rclpy.init() - cls.test_node = Node('topic_monitoring_test_node', - namespace=MONITOR_NODE_NAMESPACE) + cls.test_node = Node( + 'topic_monitoring_test_node', + namespace=MONITOR_NODE_NAMESPACE) @classmethod def tearDownClass(cls): @@ -169,7 +189,9 @@ def setUp(self): # Create a fresh GreenwaveUiAdaptor instance for each test with proper # namespace self.diagnostics_monitor = GreenwaveUiAdaptor( - self.test_node, monitor_node_name=MONITOR_NODE_NAME) + self.test_node, + monitor_node_name=MONITOR_NODE_NAME + ) # Allow time for service discovery in test environment (reduced from # 2.0s) @@ -190,8 +212,8 @@ def tearDown(self): except Exception: pass # Ignore cleanup errors - def test_service_discovery_default_namespace(self, expected_frequency, - message_type, tolerance_hz): + def test_service_discovery_default_namespace( + self, expected_frequency, message_type, tolerance_hz): """Test service discovery with default namespace.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -209,13 +231,18 @@ def test_service_discovery_default_namespace(self, expected_frequency, self.diagnostics_monitor.set_expected_frequency_client .wait_for_service(timeout_sec=10.0)) - self.assertTrue(manage_available, - 'ManageTopic service should be available') - self.assertTrue(set_freq_available, - 'SetExpectedFrequency service should be available') - - def test_diagnostic_data_conversion(self, expected_frequency, message_type, - tolerance_hz): + self.assertTrue( + manage_available, + 'ManageTopic service should be available') + self.assertTrue( + set_freq_available, + 'SetExpectedFrequency service should be available') + + def test_diagnostic_data_conversion( + self, + expected_frequency, + message_type, + tolerance_hz): """Test conversion from DiagnosticStatus to UiDiagnosticData.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -268,8 +295,8 @@ def test_diagnostic_data_conversion_different_levels( ui_data = UiDiagnosticData.from_status(status) self.assertEqual(ui_data.status, expected_str) - def test_toggle_topic_monitoring_add_remove(self, expected_frequency, - message_type, tolerance_hz): + def test_toggle_topic_monitoring_add_remove( + self, expected_frequency, message_type, tolerance_hz): """Test adding and removing topics from monitoring.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -299,7 +326,8 @@ def test_toggle_topic_monitoring_add_remove(self, expected_frequency, # Topic should now have diagnostic data self.assertIsNotNone(topic_data) self.assertNotEqual( - topic_data.status, '-', + topic_data.status, + '-', f'Should have received diagnostic data after {max_wait_time}s') # Remove topic @@ -308,8 +336,8 @@ def test_toggle_topic_monitoring_add_remove(self, expected_frequency, # Topic should be removed from diagnostics self.assertNotIn(test_topic, self.diagnostics_monitor.ui_diagnostics) - def test_set_expected_frequency_operations(self, expected_frequency, - message_type, tolerance_hz): + def test_set_expected_frequency_operations( + self, expected_frequency, message_type, tolerance_hz): """Test setting and clearing expected frequencies.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -326,7 +354,8 @@ def test_set_expected_frequency_operations(self, expected_frequency, # Set expected frequency success, message = self.diagnostics_monitor.set_expected_frequency( - test_topic, test_freq, test_tolerance) + test_topic, test_freq, test_tolerance + ) self.assertTrue(success, f'Failed to set frequency: {message}') # Check that frequency was stored locally @@ -336,7 +365,8 @@ def test_set_expected_frequency_operations(self, expected_frequency, # Clear expected frequency success, message = self.diagnostics_monitor.set_expected_frequency( - test_topic, clear=True) + test_topic, clear=True + ) self.assertTrue(success, f'Failed to clear frequency: {message}') # Should be back to defaults @@ -344,8 +374,8 @@ def test_set_expected_frequency_operations(self, expected_frequency, test_topic) self.assertEqual((freq, tolerance), (0.0, 0.0)) - def test_diagnostic_data_thread_safety(self, expected_frequency, - message_type, tolerance_hz): + def test_diagnostic_data_thread_safety( + self, expected_frequency, message_type, tolerance_hz): """Test thread safety of diagnostic data updates.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -391,12 +421,13 @@ def spin_thread(): # Should not have encountered any thread safety issues self.assertFalse(error_occurred, 'Thread safety error occurred') - self.assertGreater(update_count, 0, - 'Should have received some diagnostic updates') + self.assertGreater( + update_count, + 0, + 'Should have received some diagnostic updates') - def test_get_topic_diagnostics_nonexistent_topic(self, expected_frequency, - message_type, - tolerance_hz): + def test_get_topic_diagnostics_nonexistent_topic( + self, expected_frequency, message_type, tolerance_hz): """Test getting diagnostics for non-existent topic.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -413,8 +444,8 @@ def test_get_topic_diagnostics_nonexistent_topic(self, expected_frequency, self.assertEqual(data.latency, expected_default.latency) self.assertEqual(data.status, expected_default.status) - def test_diagnostics_callback_processing(self, expected_frequency, - message_type, tolerance_hz): + def test_diagnostics_callback_processing( + self, expected_frequency, message_type, tolerance_hz): """Test that diagnostic callbacks are processed correctly.""" if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: @@ -537,8 +568,8 @@ def test_service_timeout_handling(self, expected_frequency, message_type, tolera self.skipTest('Only running timeout handling tests once') # Create a client to a non-existent service - fake_client = self.test_node.create_client(ManageTopic, - '/nonexistent_service') + fake_client = self.test_node.create_client( + ManageTopic, '/nonexistent_service') # Replace the real client temporarily original_client = self.diagnostics_monitor.manage_topic_client From 1fa80bf3c0c1f60ce98a795e741603478c6bd83d Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Wed, 14 Jan 2026 09:18:47 -0800 Subject: [PATCH 06/33] Only support python 3.10 features Signed-off-by: Blake McHale --- .pre-commit-config.yaml | 5 +++++ .../greenwave_monitor/ncurses_frontend.py | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8c6e00..2375170 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,11 @@ repos: hooks: - id: flake8 args: [--max-line-length=99, '--extend-ignore=B902,C816,D100,D101,D102,D103,D104,D105,D106,D107,D203,D212,D404,I202'] +- repo: https://github.com/netromdk/vermin + rev: v1.6.0 + hooks: + - id: vermin + args: [-t=3.10-, --violations] - repo: https://github.com/scop/pre-commit-shfmt rev: v3.12.0-2 hooks: diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index d74cbf0..0fadd99 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -209,12 +209,12 @@ def curses_main(stdscr, node): # Draw header header = ( - f'{ - "Topic Name":<{MAX_NAME_WIDTH}} { - "Status":<{STATUS_WIDTH}} ' f'{ - "Pub Rate (Hz)":<{FRAME_RATE_WIDTH}} ' f'{ - "Latency (ms)":<{REALTIME_DELAY_WIDTH}} { - "Expected Hz":<12}') + f'{"Topic Name":<{MAX_NAME_WIDTH}} ' + f'{"Status":<{STATUS_WIDTH}} ' + f'{"Pub Rate (Hz)":<{FRAME_RATE_WIDTH}} ' + f'{"Latency (ms)":<{REALTIME_DELAY_WIDTH}} ' + f'{"Expected Hz":<12}' + ) separator_width = min( width - BUTTON_WIDTH - From 900a432178ca95325e52e791ab2df86f7c701c7b Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Wed, 14 Jan 2026 09:19:15 -0800 Subject: [PATCH 07/33] Only support python 3.10 features Signed-off-by: Blake McHale --- .pre-commit-config.yaml | 2 +- greenwave_monitor/greenwave_monitor/ui_adaptor.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2375170..e4f603c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: rev: v2.3.1 hooks: - id: autopep8 - args: [-i, -a, -a] + args: [-i] - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: diff --git a/greenwave_monitor/greenwave_monitor/ui_adaptor.py b/greenwave_monitor/greenwave_monitor/ui_adaptor.py index 373b2e5..a348b08 100644 --- a/greenwave_monitor/greenwave_monitor/ui_adaptor.py +++ b/greenwave_monitor/greenwave_monitor/ui_adaptor.py @@ -129,8 +129,7 @@ def _setup_ros_components(self): ) manage_service_name = f'{self.monitor_node_name}/manage_topic' - set_freq_service_name = f'{ - self.monitor_node_name}/set_expected_frequency' + set_freq_service_name = f'{self.monitor_node_name}/set_expected_frequency' self.node.get_logger().info( f'Connecting to monitor service: {manage_service_name}') From 5c9e1ccec0bba1fa19728c3110ae2840f88d71b5 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 15:39:01 -0800 Subject: [PATCH 08/33] Fix line length issue Signed-off-by: Blake McHale --- .pre-commit-config.yaml | 2 +- greenwave_monitor/CMakeLists.txt | 2 +- greenwave_monitor/include/greenwave_diagnostics.hpp | 2 +- greenwave_monitor/include/greenwave_monitor.hpp | 2 +- greenwave_monitor/include/minimal_publisher_node.hpp | 2 +- greenwave_monitor/src/greenwave_monitor.cpp | 2 +- greenwave_monitor/src/greenwave_monitor_main.cpp | 2 +- greenwave_monitor/src/minimal_publisher_node.cpp | 2 +- greenwave_monitor/test/test_greenwave_diagnostics.cpp | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4f603c..5fc483f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: rev: v2.3.1 hooks: - id: autopep8 - args: [-i] + args: [--max-line-length=99, -i] - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: diff --git a/greenwave_monitor/CMakeLists.txt b/greenwave_monitor/CMakeLists.txt index e28c566..9135e8d 100644 --- a/greenwave_monitor/CMakeLists.txt +++ b/greenwave_monitor/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/include/greenwave_diagnostics.hpp b/greenwave_monitor/include/greenwave_diagnostics.hpp index 42ccafe..b291d73 100644 --- a/greenwave_monitor/include/greenwave_diagnostics.hpp +++ b/greenwave_monitor/include/greenwave_diagnostics.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/include/greenwave_monitor.hpp b/greenwave_monitor/include/greenwave_monitor.hpp index fdb1e0a..d6368c9 100644 --- a/greenwave_monitor/include/greenwave_monitor.hpp +++ b/greenwave_monitor/include/greenwave_monitor.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/include/minimal_publisher_node.hpp b/greenwave_monitor/include/minimal_publisher_node.hpp index f1fcbb3..8691946 100644 --- a/greenwave_monitor/include/minimal_publisher_node.hpp +++ b/greenwave_monitor/include/minimal_publisher_node.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/src/greenwave_monitor.cpp b/greenwave_monitor/src/greenwave_monitor.cpp index 9d59ff4..3736b51 100644 --- a/greenwave_monitor/src/greenwave_monitor.cpp +++ b/greenwave_monitor/src/greenwave_monitor.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/src/greenwave_monitor_main.cpp b/greenwave_monitor/src/greenwave_monitor_main.cpp index 95fc005..edf9aa1 100644 --- a/greenwave_monitor/src/greenwave_monitor_main.cpp +++ b/greenwave_monitor/src/greenwave_monitor_main.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/src/minimal_publisher_node.cpp b/greenwave_monitor/src/minimal_publisher_node.cpp index 0258b06..88b695b 100644 --- a/greenwave_monitor/src/minimal_publisher_node.cpp +++ b/greenwave_monitor/src/minimal_publisher_node.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/test/test_greenwave_diagnostics.cpp b/greenwave_monitor/test/test_greenwave_diagnostics.cpp index 6b158ae..c201049 100644 --- a/greenwave_monitor/test/test_greenwave_diagnostics.cpp +++ b/greenwave_monitor/test/test_greenwave_diagnostics.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 5898ef0bdb0defcbff5597d5c627a257fcdc40ff Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 15:44:34 -0800 Subject: [PATCH 09/33] Reset to main Signed-off-by: Blake McHale --- .github/workflows/debian-packages.yml | 4 +- .github/workflows/ros-tests.yml | 4 +- .gitignore | 2 +- Contributing.md | 2 +- README.md | 2 +- greenwave_monitor/CMakeLists.txt | 2 +- greenwave_monitor/examples/README.md | 2 +- .../greenwave_monitor/ncurses_frontend.py | 82 ++++------- .../greenwave_monitor/test_utils.py | 34 ++--- .../greenwave_monitor/ui_adaptor.py | 34 ++--- .../include/greenwave_diagnostics.hpp | 2 +- .../include/greenwave_monitor.hpp | 2 +- .../include/minimal_publisher_node.hpp | 2 +- greenwave_monitor/launch/hz.launch.py | 2 +- .../launch/test_publishers.launch.py | 2 +- greenwave_monitor/scripts/ncurses_dashboard | 130 +++++++++--------- greenwave_monitor/src/greenwave_monitor.cpp | 2 +- .../src/greenwave_monitor_main.cpp | 2 +- .../src/minimal_publisher_node.cpp | 2 +- .../test/test_greenwave_diagnostics.cpp | 2 +- .../test/test_greenwave_monitor.py | 80 +++-------- .../test/test_ncurses_frontend_argparse.py | 2 +- .../test/test_topic_monitoring_integration.py | 127 +++++------------ greenwave_monitor_interfaces/CMakeLists.txt | 4 +- greenwave_monitor_interfaces/package.xml | 19 --- .../srv/ManageTopic.srv | 4 +- .../srv/SetExpectedFrequency.srv | 4 +- scripts/build_debian_packages.sh | 124 +++++++++-------- scripts/docker-test.sh | 60 ++++---- 29 files changed, 288 insertions(+), 452 deletions(-) diff --git a/.github/workflows/debian-packages.yml b/.github/workflows/debian-packages.yml index da25bd7..77e948c 100644 --- a/.github/workflows/debian-packages.yml +++ b/.github/workflows/debian-packages.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -318,4 +318,4 @@ jobs: sleep 2 kill -9 $(cat /tmp/gwm.pid) 2>/dev/null || true ros2 daemon stop || true - shell: bash + shell: bash \ No newline at end of file diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 6ddca4b..b2e4d5e 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -138,4 +138,4 @@ jobs: with: name: test-results-${{ matrix.ros_distro }} path: build/*/test_results/**/*.xml - retention-days: 7 + retention-days: 7 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 803c0d1..5b49528 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,4 @@ _deps # Temporary files *.tmp -*.temp +*.temp \ No newline at end of file diff --git a/Contributing.md b/Contributing.md index 0c46b20..dde649a 100644 --- a/Contributing.md +++ b/Contributing.md @@ -1,4 +1,4 @@ -We welcome PRs for new features or bugfixes, CI will automatically run automated tests on new PRs. You can also use the scripts/docker-test.sh to debug tests for a particular distribution locally. +We welcome PRs for new features or bugfixes, CI will automatically run automated tests on new PRs. You can also use the scripts/docker-test.sh to debug tests for a particular distribution locally. We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. diff --git a/README.md b/README.md index b4011f2..c926a89 100644 --- a/README.md +++ b/README.md @@ -109,4 +109,4 @@ If you want to use it as a command line tool, you can do so with the following l ```bash ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' -``` +``` \ No newline at end of file diff --git a/greenwave_monitor/CMakeLists.txt b/greenwave_monitor/CMakeLists.txt index 9135e8d..e28c566 100644 --- a/greenwave_monitor/CMakeLists.txt +++ b/greenwave_monitor/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/examples/README.md b/greenwave_monitor/examples/README.md index 15651c2..cfa4518 100644 --- a/greenwave_monitor/examples/README.md +++ b/greenwave_monitor/examples/README.md @@ -31,4 +31,4 @@ Node( ), ``` -To see the output with the r2s_gw dashboard, run `ros2 run r2s_gw r2s_gw` in a separate terminal. +To see the output with the r2s_gw dashboard, run `ros2 run r2s_gw r2s_gw` in a separate terminal. \ No newline at end of file diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index 0fadd99..a50d231 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -66,8 +66,7 @@ def __init__(self, hide_unmonitored: bool = False): self.ui_adaptor = GreenwaveUiAdaptor(self) # Timer to periodically update the topic list - self.topic_update_timer = self.create_timer( - 5.0, self.update_topic_list) + self.topic_update_timer = self.create_timer(5.0, self.update_topic_list) # Initial topic list update self.update_topic_list() @@ -84,9 +83,7 @@ def update_topic_list(self): if (not topic_name.endswith('/nitros')) and ('/nitros/' not in topic_name) ] - topic_set = { - topic_name for topic_name, - topic_type in topic_names_and_types} + topic_set = {topic_name for topic_name, topic_type in topic_names_and_types} with self.topics_lock: # Update topics @@ -103,8 +100,7 @@ def update_visible_topics(self): all_topic_names = list(self.all_topics) if self.hide_unmonitored and self.ui_adaptor: - # Filter to only show topics that have diagnostic data (are being - # monitored) + # Filter to only show topics that have diagnostic data (are being monitored) filtered_topics = [] for topic_name in all_topic_names: diag = self.ui_adaptor.get_topic_diagnostics(topic_name) @@ -176,11 +172,7 @@ def curses_main(stdscr, node): key = -1 # Only redraw if we got input or enough time has passed - should_redraw = ( - key != - - 1) or ( - current_time - - last_redraw >= redraw_interval) + should_redraw = (key != -1) or (current_time - last_redraw >= redraw_interval) if not should_redraw: time.sleep(0.01) @@ -208,23 +200,12 @@ def curses_main(stdscr, node): STATUS_WIDTH = int(STATUS_WIDTH * scaling_factor) # Draw header - header = ( - f'{"Topic Name":<{MAX_NAME_WIDTH}} ' - f'{"Status":<{STATUS_WIDTH}} ' - f'{"Pub Rate (Hz)":<{FRAME_RATE_WIDTH}} ' - f'{"Latency (ms)":<{REALTIME_DELAY_WIDTH}} ' - f'{"Expected Hz":<12}' - ) + header = (f'{"Topic Name":<{MAX_NAME_WIDTH}} {"Status":<{STATUS_WIDTH}} ' + f'{"Pub Rate (Hz)":<{FRAME_RATE_WIDTH}} ' + f'{"Latency (ms)":<{REALTIME_DELAY_WIDTH}} {"Expected Hz":<12}') separator_width = min( - width - - BUTTON_WIDTH - - 2, - MAX_NAME_WIDTH + - FRAME_RATE_WIDTH + - REALTIME_DELAY_WIDTH + - STATUS_WIDTH + - 12 + - 4) + width - BUTTON_WIDTH - 2, + MAX_NAME_WIDTH + FRAME_RATE_WIDTH + REALTIME_DELAY_WIDTH + STATUS_WIDTH + 12 + 4) separator = '-' * separator_width try: @@ -240,15 +221,13 @@ def curses_main(stdscr, node): input_mode = None input_buffer = '' elif key == 10 or key == 13: # Enter - if input_mode == 'frequency' and 0 <= selected_row < len( - node.visible_topics): + if input_mode == 'frequency' and 0 <= selected_row < len(node.visible_topics): topic_name = node.visible_topics[selected_row] try: parts = input_buffer.strip().split() if len(parts) >= 1: hz = float(parts[0]) - tolerance = float( - parts[1]) if len(parts) > 1 else 5.0 + tolerance = float(parts[1]) if len(parts) > 1 else 5.0 success, msg = node.ui_adaptor.set_expected_frequency( topic_name, hz, tolerance) status_message = f'Set frequency for {topic_name}: {hz}Hz' @@ -273,8 +252,7 @@ def curses_main(stdscr, node): start_idx = selected_row elif key == curses.KEY_DOWN: if len(node.visible_topics) > 0: - selected_row = min( - len(node.visible_topics) - 1, selected_row + 1) + selected_row = min(len(node.visible_topics) - 1, selected_row + 1) if selected_row >= start_idx + (height - 5): start_idx = min( len(node.visible_topics) - (height - 5), @@ -288,8 +266,7 @@ def curses_main(stdscr, node): if len(node.visible_topics) > 0: start_idx = min(len(node.visible_topics) - visible_height, start_idx + visible_height) - selected_row = min( - len(node.visible_topics) - 1, selected_row + visible_height) + selected_row = min(len(node.visible_topics) - 1, selected_row + visible_height) elif key == ord('q') or key == ord('Q'): node.running = False break @@ -330,8 +307,7 @@ def curses_main(stdscr, node): start_idx = 0 else: selected_row = min(selected_row, len(visible_topics) - 1) - start_idx = min(start_idx, max( - 0, len(visible_topics) - (height - 5))) + start_idx = min(start_idx, max(0, len(visible_topics) - (height - 5))) start_idx = max(0, start_idx) visible_height = height - 5 @@ -375,8 +351,7 @@ def curses_main(stdscr, node): elif status_display == 'ERROR': color_pair = curses.color_pair(COLOR_ERROR) else: - # Default green for monitored - color_pair = curses.color_pair(COLOR_OK) + color_pair = curses.color_pair(COLOR_OK) # Default green for monitored else: color_pair = curses.color_pair(COLOR_UNMONITORED) @@ -384,7 +359,7 @@ def curses_main(stdscr, node): # Format topic name with truncation if len(topic_name) > MAX_NAME_WIDTH: - name_display = topic_name[:MAX_NAME_WIDTH - 3] + '...' + name_display = topic_name[:MAX_NAME_WIDTH-3] + '...' else: name_display = topic_name.ljust(MAX_NAME_WIDTH) @@ -425,7 +400,7 @@ def curses_main(stdscr, node): # Status message if current_time < status_timeout: try: - stdscr.addstr(height - 3, 0, status_message[:width - 1], + stdscr.addstr(height - 3, 0, status_message[:width-1], curses.color_pair(COLOR_STATUS_MSG)) except curses.error: pass @@ -433,7 +408,7 @@ def curses_main(stdscr, node): # Show node status message if current_time < node.status_timeout: try: - stdscr.addstr(height - 3, 0, node.status_message[:width - 1], + stdscr.addstr(height - 3, 0, node.status_message[:width-1], curses.color_pair(COLOR_STATUS_MSG)) except curses.error: pass @@ -442,7 +417,7 @@ def curses_main(stdscr, node): if input_mode: try: prompt = f'Set frequency: {input_buffer}' - stdscr.addstr(height - 3, 0, prompt[:width - 1], + stdscr.addstr(height - 3, 0, prompt[:width-1], curses.color_pair(COLOR_STATUS_MSG)) # Position cursor after the input cursor_x = len(prompt) @@ -455,14 +430,10 @@ def curses_main(stdscr, node): curses.curs_set(0) # Hide cursor when not in input mode # Footer - num_shown = min( - start_idx + - len(visible_topics_slice), - len(visible_topics)) + num_shown = min(start_idx + len(visible_topics_slice), len(visible_topics)) if input_mode: - status_line = ( - "Format: Hz [tolerance%] - Examples: '30' (30Hz±5% default) " - "or '30 10' (30Hz±10%) - ESC=cancel, Enter=confirm") + status_line = ("Format: Hz [tolerance%] - Examples: '30' (30Hz±5% default) " + "or '30 10' (30Hz±10%) - ESC=cancel, Enter=confirm") else: if node.hide_unmonitored: mode_text = 'monitored only' @@ -500,8 +471,7 @@ def main(args=None): """Entry point for the ncurses frontend application.""" parsed_args, ros_args = parse_args(args) rclpy.init(args=ros_args) - node = GreenwaveNcursesFrontend( - hide_unmonitored=parsed_args.hide_unmonitored) + node = GreenwaveNcursesFrontend(hide_unmonitored=parsed_args.hide_unmonitored) thread = None def signal_handler(signum, frame): @@ -513,9 +483,7 @@ def signal_handler(signum, frame): signal.signal(signal.SIGTERM, signal_handler) try: - thread = threading.Thread( - target=rclpy.spin, args=( - node,), daemon=False) + thread = threading.Thread(target=rclpy.spin, args=(node,), daemon=False) thread.start() curses.wrapper(curses_main, node) except KeyboardInterrupt: diff --git a/greenwave_monitor/greenwave_monitor/test_utils.py b/greenwave_monitor/greenwave_monitor/test_utils.py index aa05dd7..49f5e0d 100644 --- a/greenwave_monitor/greenwave_monitor/test_utils.py +++ b/greenwave_monitor/greenwave_monitor/test_utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,10 +49,7 @@ def create_minimal_publisher( - topic: str, - frequency_hz: float, - message_type: str, - id_suffix: str = ''): + topic: str, frequency_hz: float, message_type: str, id_suffix: str = ''): """Create a minimal publisher node with the given parameters.""" return launch_ros.actions.Node( package='greenwave_monitor', @@ -89,8 +86,7 @@ def wait_for_service_connection(node: Node, timeout_sec: float = 3.0, service_name: str = 'service') -> bool: """Wait for a service to become available.""" - service_available = service_client.wait_for_service( - timeout_sec=timeout_sec) + service_available = service_client.wait_for_service(timeout_sec=timeout_sec) if not service_available: node.get_logger().error( f'Service "{service_name}" not available within {timeout_sec} seconds') @@ -145,11 +141,10 @@ def call_set_frequency_service(node: Node, return future.result() -def collect_diagnostics_for_topic( - node: Node, - topic_name: str, - expected_count: int = 5, - timeout_sec: float = 10.0) -> List[DiagnosticStatus]: +def collect_diagnostics_for_topic(node: Node, + topic_name: str, + expected_count: int = 5, + timeout_sec: float = 10.0) -> List[DiagnosticStatus]: """Collect diagnostic messages for a specific topic.""" received_diagnostics = [] @@ -181,7 +176,7 @@ def find_best_diagnostic( diagnostics: List[DiagnosticStatus], expected_frequency: float, message_type: str -) -> Tuple[Optional[DiagnosticStatus], Optional[Tuple[float, float, float]]]: + ) -> Tuple[Optional[DiagnosticStatus], Optional[Tuple[float, float, float]]]: """Find the diagnostic message with frequency closest to expected.""" best_status = None best_values = None @@ -205,8 +200,7 @@ def find_best_diagnostic( try: node_val = float(node_str) if node_str is not None else None msg_val = float(msg_str) if msg_str is not None else None - latency_val = float( - latency_str) if latency_str is not None else None + latency_val = float(latency_str) if latency_str is not None else None except (ValueError, TypeError): continue @@ -241,8 +235,7 @@ def verify_diagnostic_values(status: DiagnosticStatus, if reported_frequency_msg == -1.0: errors.append("Did not find 'frame_rate_msg' in diagnostic") if reported_latency_ms == -1.0: - errors.append( - "Did not find 'current_delay_from_realtime_ms' in diagnostic") + errors.append("Did not find 'current_delay_from_realtime_ms' in diagnostic") # Check frequency tolerances if abs(reported_frequency_node - expected_frequency) > tolerance_hz: @@ -252,8 +245,7 @@ def verify_diagnostic_values(status: DiagnosticStatus, if message_type == 'string': if reported_frequency_msg != 0.0: - errors.append( - f'String message frequency should be 0.0, got {reported_frequency_msg}') + errors.append(f'String message frequency should be 0.0, got {reported_frequency_msg}') if not math.isnan(reported_latency_ms): errors.append( f'String latency should be {math.nan}, ' @@ -280,7 +272,7 @@ def create_service_clients(node: Node, namespace: str = MONITOR_NODE_NAMESPACE, ) set_frequency_client = node.create_client( - SetExpectedFrequency, - f'/{namespace}/{node_name}/set_expected_frequency') + SetExpectedFrequency, f'/{namespace}/{node_name}/set_expected_frequency' + ) return manage_topic_client, set_frequency_client diff --git a/greenwave_monitor/greenwave_monitor/ui_adaptor.py b/greenwave_monitor/greenwave_monitor/ui_adaptor.py index a348b08..30d84ba 100644 --- a/greenwave_monitor/greenwave_monitor/ui_adaptor.py +++ b/greenwave_monitor/greenwave_monitor/ui_adaptor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -105,10 +105,7 @@ class GreenwaveUiAdaptor: """ - def __init__( - self, - node: Node, - monitor_node_name: str = 'greenwave_monitor'): + def __init__(self, node: Node, monitor_node_name: str = 'greenwave_monitor'): """Initialize the UI adaptor for subscribing to diagnostics and managing topics.""" self.node = node self.monitor_node_name = monitor_node_name @@ -131,8 +128,7 @@ def _setup_ros_components(self): manage_service_name = f'{self.monitor_node_name}/manage_topic' set_freq_service_name = f'{self.monitor_node_name}/set_expected_frequency' - self.node.get_logger().info( - f'Connecting to monitor service: {manage_service_name}') + self.node.get_logger().info(f'Connecting to monitor service: {manage_service_name}') self.manage_topic_client = self.node.create_client( ManageTopic, @@ -154,14 +150,12 @@ def _extract_topic_name(self, diagnostic_name: str) -> str: This is a temporary hack until NITROS migrates to greenwave_diagnostics.hpp. """ - # If the name starts with '/', it's already just a topic name - # (Greenwave format) + # If the name starts with '/', it's already just a topic name (Greenwave format) if diagnostic_name.startswith('/'): return diagnostic_name # NITROS format: node_name + namespace + "/" + topic_name - # Node names cannot contain '/', so the first '/' marks where - # namespace+topic begins + # Node names cannot contain '/', so the first '/' marks where namespace+topic begins idx = diagnostic_name.find('/') if idx >= 0: return diagnostic_name[idx:] @@ -176,8 +170,7 @@ def _on_diagnostics(self, msg: DiagnosticArray): for status in msg.status: ui_data = UiDiagnosticData.from_status(status) ui_data.last_update = time.time() - # Normalize the topic name to handle both NITROS and Greenwave - # formats + # Normalize the topic name to handle both NITROS and Greenwave formats topic_name = self._extract_topic_name(status.name) self.ui_diagnostics[topic_name] = ui_data try: @@ -205,8 +198,7 @@ def toggle_topic_monitoring(self, topic_name: str): try: # Use asynchronous service call to prevent deadlock future = self.manage_topic_client.call_async(request) - rclpy.spin_until_future_complete( - self.node, future, timeout_sec=3.0) + rclpy.spin_until_future_complete(self.node, future, timeout_sec=3.0) if future.result() is None: action = 'start' if request.add_topic else 'stop' @@ -239,8 +231,7 @@ def set_expected_frequency(self, clear: bool = False ) -> tuple[bool, str]: """Set or clear the expected frequency for a topic.""" - if not self.set_expected_frequency_client.wait_for_service( - timeout_sec=1.0): + if not self.set_expected_frequency_client.wait_for_service(timeout_sec=1.0): return False, 'Could not connect to set_expected_frequency service.' request = SetExpectedFrequency.Request() @@ -253,8 +244,7 @@ def set_expected_frequency(self, # Use asynchronous service call to prevent deadlock try: future = self.set_expected_frequency_client.call_async(request) - rclpy.spin_until_future_complete( - self.node, future, timeout_sec=3.0) + rclpy.spin_until_future_complete(self.node, future, timeout_sec=3.0) if future.result() is None: action = 'clear' if clear else 'set' @@ -267,16 +257,14 @@ def set_expected_frequency(self, if not response.success: action = 'clear' if clear else 'set' self.node.get_logger().error( - f'Failed to {action} expected frequency: { - response.message}') + f'Failed to {action} expected frequency: {response.message}') return False, response.message else: with self.data_lock: if clear: self.expected_frequencies.pop(topic_name, None) else: - self.expected_frequencies[topic_name] = ( - expected_hz, tolerance_percent) + self.expected_frequencies[topic_name] = (expected_hz, tolerance_percent) return True, response.message except Exception as e: action = 'clear' if clear else 'set' diff --git a/greenwave_monitor/include/greenwave_diagnostics.hpp b/greenwave_monitor/include/greenwave_diagnostics.hpp index b291d73..42ccafe 100644 --- a/greenwave_monitor/include/greenwave_diagnostics.hpp +++ b/greenwave_monitor/include/greenwave_diagnostics.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/include/greenwave_monitor.hpp b/greenwave_monitor/include/greenwave_monitor.hpp index d6368c9..fdb1e0a 100644 --- a/greenwave_monitor/include/greenwave_monitor.hpp +++ b/greenwave_monitor/include/greenwave_monitor.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/include/minimal_publisher_node.hpp b/greenwave_monitor/include/minimal_publisher_node.hpp index 8691946..f1fcbb3 100644 --- a/greenwave_monitor/include/minimal_publisher_node.hpp +++ b/greenwave_monitor/include/minimal_publisher_node.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/launch/hz.launch.py b/greenwave_monitor/launch/hz.launch.py index 534d040..bb679df 100644 --- a/greenwave_monitor/launch/hz.launch.py +++ b/greenwave_monitor/launch/hz.launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/launch/test_publishers.launch.py b/greenwave_monitor/launch/test_publishers.launch.py index 728e5a0..e1907c9 100644 --- a/greenwave_monitor/launch/test_publishers.launch.py +++ b/greenwave_monitor/launch/test_publishers.launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/scripts/ncurses_dashboard b/greenwave_monitor/scripts/ncurses_dashboard index b75d2d1..807a777 100755 --- a/greenwave_monitor/scripts/ncurses_dashboard +++ b/greenwave_monitor/scripts/ncurses_dashboard @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,77 +27,77 @@ HIDE_UNMONITORED=false MONITOR_ARGS=() show_help() { - echo "Usage: $0 [OPTIONS] [MONITOR_ARGS...]" - echo "" - echo "Launch Greenwave Monitor with ncurses TUI dashboard" - echo "" - echo "OPTIONS:" - echo " --demo, --test Launch demo publisher nodes for testing" - echo " --log-dir DIR Enable logging to specified directory" - echo " --hide-unmonitored Hide unmonitored topics on initialization" - echo " --help, -h Show this help message" - echo "" - echo "MONITOR_ARGS are passed directly to the greenwave_monitor node" - echo "" - echo "Controls in ncurses interface:" - echo " enter/space = toggle topic monitoring" - echo " f = Set expected frequency for selected topic (format: hz tolerance%)" - echo " c = Clear frequency settings for selected topic" - echo " h = Toggle hiding unmonitored topics" - echo " ↑/↓ = Navigate topics" - echo " q = Quit" + echo "Usage: $0 [OPTIONS] [MONITOR_ARGS...]" + echo "" + echo "Launch Greenwave Monitor with ncurses TUI dashboard" + echo "" + echo "OPTIONS:" + echo " --demo, --test Launch demo publisher nodes for testing" + echo " --log-dir DIR Enable logging to specified directory" + echo " --hide-unmonitored Hide unmonitored topics on initialization" + echo " --help, -h Show this help message" + echo "" + echo "MONITOR_ARGS are passed directly to the greenwave_monitor node" + echo "" + echo "Controls in ncurses interface:" + echo " enter/space = toggle topic monitoring" + echo " f = Set expected frequency for selected topic (format: hz tolerance%)" + echo " c = Clear frequency settings for selected topic" + echo " h = Toggle hiding unmonitored topics" + echo " ↑/↓ = Navigate topics" + echo " q = Quit" } while [[ $# -gt 0 ]]; do - case $1 in - --demo | --test) - DEMO_MODE=true - shift - ;; - --log-dir) - LOG_DIR="$2" - shift 2 - ;; - --hide-unmonitored) - HIDE_UNMONITORED=true - shift - ;; - --help | -h) - show_help - exit 0 - ;; - *) - MONITOR_ARGS+=("$1") - shift - ;; - esac + case $1 in + --demo|--test) + DEMO_MODE=true + shift + ;; + --log-dir) + LOG_DIR="$2" + shift 2 + ;; + --hide-unmonitored) + HIDE_UNMONITORED=true + shift + ;; + --help|-h) + show_help + exit 0 + ;; + *) + MONITOR_ARGS+=("$1") + shift + ;; + esac done # Handle logging configuration LOG_FILE="/dev/null" if [ -n "$LOG_DIR" ]; then - # Create logs directory if it doesn't exist - mkdir -p "${LOG_DIR}" + # Create logs directory if it doesn't exist + mkdir -p "${LOG_DIR}" - # Create a timestamped log file - TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") - LOG_FILE="${LOG_DIR}/monitor_ncurses_${TIMESTAMP}.log" + # Create a timestamped log file + TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") + LOG_FILE="${LOG_DIR}/monitor_ncurses_${TIMESTAMP}.log" fi # Function to clean up background processes on exit cleanup() { - echo "Shutting down..." - if [ -n "$MONITOR_PID" ]; then - echo "Terminating monitor node (PID: $MONITOR_PID)..." - if [ "$LOG_FILE" != "/dev/null" ]; then - echo "Monitor log available at: ${LOG_FILE}" - fi - # Kill background process and all its descendants - pkill -TERM -P $MONITOR_PID 2>/dev/null - kill -TERM $MONITOR_PID 2>/dev/null - fi - exit 0 + echo "Shutting down..." + if [ -n "$MONITOR_PID" ]; then + echo "Terminating monitor node (PID: $MONITOR_PID)..." + if [ "$LOG_FILE" != "/dev/null" ]; then + echo "Monitor log available at: ${LOG_FILE}" + fi + # Kill background process and all its descendants + pkill -TERM -P $MONITOR_PID 2>/dev/null + kill -TERM $MONITOR_PID 2>/dev/null + fi + exit 0 } # Set up trap to catch signals @@ -105,13 +105,13 @@ trap cleanup SIGINT SIGTERM EXIT # Launch demo nodes if requested if [ "$DEMO_MODE" = "true" ]; then - echo "Starting demo mode with test publisher nodes..." - ros2 launch greenwave_monitor example.launch.py &>"${LOG_FILE}" & - MONITOR_PID=$! + echo "Starting demo mode with test publisher nodes..." + ros2 launch greenwave_monitor example.launch.py &> "${LOG_FILE}" & + MONITOR_PID=$! else - echo "Starting Greenwave Monitor..." - ros2 run greenwave_monitor greenwave_monitor "${MONITOR_ARGS[@]}" &>"${LOG_FILE}" & - MONITOR_PID=$! + echo "Starting Greenwave Monitor..." + ros2 run greenwave_monitor greenwave_monitor "${MONITOR_ARGS[@]}" &> "${LOG_FILE}" & + MONITOR_PID=$! fi # Wait briefly to allow the monitor node to initialize @@ -124,7 +124,7 @@ echo "Controls: a=Add Topic, r=Remove, f=Set Frequency, c=Clear Freq, q=Quit" # NOTE: add proper argument parsing to the ncurses frontend if more than one argument is added here FRONTEND_ARGS=() if [ "$HIDE_UNMONITORED" = "true" ]; then - FRONTEND_ARGS+=("--hide-unmonitored") + FRONTEND_ARGS+=("--hide-unmonitored") fi python3 -m greenwave_monitor.ncurses_frontend "${FRONTEND_ARGS[@]}" diff --git a/greenwave_monitor/src/greenwave_monitor.cpp b/greenwave_monitor/src/greenwave_monitor.cpp index 3736b51..9d59ff4 100644 --- a/greenwave_monitor/src/greenwave_monitor.cpp +++ b/greenwave_monitor/src/greenwave_monitor.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/src/greenwave_monitor_main.cpp b/greenwave_monitor/src/greenwave_monitor_main.cpp index edf9aa1..95fc005 100644 --- a/greenwave_monitor/src/greenwave_monitor_main.cpp +++ b/greenwave_monitor/src/greenwave_monitor_main.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/src/minimal_publisher_node.cpp b/greenwave_monitor/src/minimal_publisher_node.cpp index 88b695b..0258b06 100644 --- a/greenwave_monitor/src/minimal_publisher_node.cpp +++ b/greenwave_monitor/src/minimal_publisher_node.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/test/test_greenwave_diagnostics.cpp b/greenwave_monitor/test/test_greenwave_diagnostics.cpp index c201049..6b158ae 100644 --- a/greenwave_monitor/test/test_greenwave_diagnostics.cpp +++ b/greenwave_monitor/test/test_greenwave_diagnostics.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/test/test_greenwave_monitor.py b/greenwave_monitor/test/test_greenwave_monitor.py index 92995ef..82b58d4 100644 --- a/greenwave_monitor/test/test_greenwave_monitor.py +++ b/greenwave_monitor/test/test_greenwave_monitor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -97,9 +97,7 @@ def create_test_yaml_config(): @pytest.mark.launch_test -@launch_testing.parametrize( - 'message_type, expected_frequency, tolerance_hz', - TEST_CONFIGURATIONS) +@launch_testing.parametrize('message_type, expected_frequency, tolerance_hz', TEST_CONFIGURATIONS) def generate_test_description(message_type, expected_frequency, tolerance_hz): """Generate launch description for greenwave monitor tests.""" # Create temporary YAML config for testing parameter loading @@ -185,8 +183,7 @@ def tearDownClass(cls): def check_node_launches_successfully(self): """Test that the node launches without errors.""" # Create a service client to check if the node is ready - # Service discovery is more reliable than node discovery in - # launch_testing + # Service discovery is more reliable than node discovery in launch_testing manage_client, set_freq_client = create_service_clients( self.test_node, MONITOR_NODE_NAMESPACE, MONITOR_NODE_NAME ) @@ -200,12 +197,7 @@ def check_node_launches_successfully(self): 'not available within timeout') return manage_client - def verify_diagnostics( - self, - topic_name, - expected_frequency, - message_type, - tolerance_hz): + def verify_diagnostics(self, topic_name, expected_frequency, message_type, tolerance_hz): """Verify diagnostics for a given topic.""" # Collect diagnostic messages using shared utility received_diagnostics = collect_diagnostics_for_topic( @@ -221,28 +213,19 @@ def verify_diagnostics( received_diagnostics, expected_frequency, message_type ) - self.assertIsNotNone( - best_status, - 'Did not find a diagnostic with all required values') + self.assertIsNotNone(best_status, 'Did not find a diagnostic with all required values') self.assertEqual(topic_name, best_status.name) # Verify diagnostic values using shared utility errors = verify_diagnostic_values( - best_status, - best_values, - expected_frequency, - message_type, - tolerance_hz) + best_status, best_values, expected_frequency, message_type, tolerance_hz + ) # Assert no errors occurred if errors: self.fail(f"Diagnostic verification failed: {'; '.join(errors)}") - def test_frequency_monitoring( - self, - expected_frequency, - message_type, - tolerance_hz): + def test_frequency_monitoring(self, expected_frequency, message_type, tolerance_hz): """Test that the monitor node correctly tracks different frequencies.""" # This test runs for all configurations to verify frequency monitoring self.check_node_launches_successfully() @@ -256,14 +239,9 @@ def call_manage_topic(self, add, topic, service_client): self.assertIsNotNone(response, 'Service call failed or timed out') return response - def test_manage_one_topic( - self, - expected_frequency, - message_type, - tolerance_hz): + def test_manage_one_topic(self, expected_frequency, message_type, tolerance_hz): """Test that add_topic() and remove_topic() work correctly for one topic.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running manage topic tests once') service_client = self.check_node_launches_successfully() @@ -273,8 +251,7 @@ def test_manage_one_topic( add=False, topic=TEST_TOPIC, service_client=service_client) self.assertTrue(response.success) - # 2. Removing the same topic again should fail because it no longer - # exists. + # 2. Removing the same topic again should fail because it no longer exists. response = self.call_manage_topic( add=False, topic=TEST_TOPIC, service_client=service_client) self.assertFalse(response.success) @@ -285,28 +262,17 @@ def test_manage_one_topic( self.assertTrue(response.success) # Verify diagnostics after adding the topic back - self.verify_diagnostics( - TEST_TOPIC, - expected_frequency, - message_type, - tolerance_hz) - - # 4. Adding the same topic again should fail because it's already - # monitored. + self.verify_diagnostics(TEST_TOPIC, expected_frequency, message_type, tolerance_hz) + + # 4. Adding the same topic again should fail because it's already monitored. response = self.call_manage_topic( add=True, topic=TEST_TOPIC, service_client=service_client) self.assertFalse(response.success) - def test_manage_multiple_topics( - self, - expected_frequency, - message_type, - tolerance_hz): + def test_manage_multiple_topics(self, expected_frequency, message_type, tolerance_hz): """Test that add_topic() and remove_topic() work correctly for multiple topics.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: - self.skipTest( - 'Only running manage topic tests once for 30 hz images') + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + self.skipTest('Only running manage topic tests once for 30 hz images') service_client = self.check_node_launches_successfully() @@ -326,11 +292,7 @@ def test_manage_multiple_topics( self.assertTrue(response.success) # Verify diagnostics after adding the first topic - self.verify_diagnostics( - TEST_TOPIC1, - expected_frequency, - message_type, - tolerance_hz) + self.verify_diagnostics(TEST_TOPIC1, expected_frequency, message_type, tolerance_hz) # 2. Add second topic – should succeed. response = self.call_manage_topic( @@ -338,11 +300,7 @@ def test_manage_multiple_topics( self.assertTrue(response.success) # Verify diagnostics after adding the second topic - self.verify_diagnostics( - TEST_TOPIC2, - expected_frequency, - message_type, - tolerance_hz) + self.verify_diagnostics(TEST_TOPIC2, expected_frequency, message_type, tolerance_hz) # 3. Remove first topic – should succeed. response = self.call_manage_topic( diff --git a/greenwave_monitor/test/test_ncurses_frontend_argparse.py b/greenwave_monitor/test/test_ncurses_frontend_argparse.py index db6e500..a12f8f3 100644 --- a/greenwave_monitor/test/test_ncurses_frontend_argparse.py +++ b/greenwave_monitor/test/test_ncurses_frontend_argparse.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/test/test_topic_monitoring_integration.py b/greenwave_monitor/test/test_topic_monitoring_integration.py index e635f7c..3641c64 100644 --- a/greenwave_monitor/test/test_topic_monitoring_integration.py +++ b/greenwave_monitor/test/test_topic_monitoring_integration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -83,9 +83,7 @@ def create_test_yaml_config(): @pytest.mark.launch_test -@launch_testing.parametrize( - 'message_type, expected_frequency, tolerance_hz', - TEST_CONFIGURATIONS) +@launch_testing.parametrize('message_type, expected_frequency, tolerance_hz', TEST_CONFIGURATIONS) def generate_test_description(message_type, expected_frequency, tolerance_hz): """Generate launch description for topic monitoring tests.""" # Create temporary YAML config for testing parameter loading @@ -100,21 +98,10 @@ def generate_test_description(message_type, expected_frequency, tolerance_hz): # Create publishers for testing publishers = [ # Main test topic publisher with parametrized frequency - create_minimal_publisher( - '/test_topic', - expected_frequency, - message_type), + create_minimal_publisher('/test_topic', expected_frequency, message_type), # Additional publishers for topic management tests - create_minimal_publisher( - '/test_topic1', - expected_frequency, - message_type, - '1'), - create_minimal_publisher( - '/test_topic2', - expected_frequency, - message_type, - '2'), + create_minimal_publisher('/test_topic1', expected_frequency, message_type, '1'), + create_minimal_publisher('/test_topic2', expected_frequency, message_type, '2'), # Publisher for service discovery tests create_minimal_publisher('/discovery_test_topic', 50.0, 'imu', '_discovery'), # Publisher for YAML config test @@ -150,9 +137,7 @@ class TestTopicMonitoringPostShutdown(unittest.TestCase): def setUpClass(cls): """Initialize ROS2 and create test node.""" rclpy.init() - cls.test_node = Node( - 'shutdown_test_node', - namespace=MONITOR_NODE_NAMESPACE) + cls.test_node = Node('shutdown_test_node', namespace=MONITOR_NODE_NAMESPACE) @classmethod def tearDownClass(cls): @@ -174,9 +159,7 @@ class TestTopicMonitoringIntegration(unittest.TestCase): def setUpClass(cls): """Initialize ROS2 and create test node.""" rclpy.init() - cls.test_node = Node( - 'topic_monitoring_test_node', - namespace=MONITOR_NODE_NAMESPACE) + cls.test_node = Node('topic_monitoring_test_node', namespace=MONITOR_NODE_NAMESPACE) @classmethod def tearDownClass(cls): @@ -186,15 +169,13 @@ def tearDownClass(cls): def setUp(self): """Set up for each test.""" - # Create a fresh GreenwaveUiAdaptor instance for each test with proper - # namespace + # Create a fresh GreenwaveUiAdaptor instance for each test with proper namespace self.diagnostics_monitor = GreenwaveUiAdaptor( self.test_node, monitor_node_name=MONITOR_NODE_NAME ) - # Allow time for service discovery in test environment (reduced from - # 2.0s) + # Allow time for service discovery in test environment (reduced from 2.0s) time.sleep(1.0) def tearDown(self): @@ -203,10 +184,8 @@ def tearDown(self): if hasattr(self, 'diagnostics_monitor'): # Clean up ROS components try: - self.test_node.destroy_subscription( - self.diagnostics_monitor.subscription) - self.test_node.destroy_client( - self.diagnostics_monitor.manage_topic_client) + self.test_node.destroy_subscription(self.diagnostics_monitor.subscription) + self.test_node.destroy_client(self.diagnostics_monitor.manage_topic_client) self.test_node.destroy_client( self.diagnostics_monitor.set_expected_frequency_client) except Exception: @@ -215,14 +194,12 @@ def tearDown(self): def test_service_discovery_default_namespace( self, expected_frequency, message_type, tolerance_hz): """Test service discovery with default namespace.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running service discovery tests once') # The monitor should discover the services automatically self.assertIsNotNone(self.diagnostics_monitor.manage_topic_client) - self.assertIsNotNone( - self.diagnostics_monitor.set_expected_frequency_client) + self.assertIsNotNone(self.diagnostics_monitor.set_expected_frequency_client) # Verify services are available manage_available = self.diagnostics_monitor.manage_topic_client.wait_for_service( @@ -231,21 +208,12 @@ def test_service_discovery_default_namespace( self.diagnostics_monitor.set_expected_frequency_client .wait_for_service(timeout_sec=10.0)) - self.assertTrue( - manage_available, - 'ManageTopic service should be available') - self.assertTrue( - set_freq_available, - 'SetExpectedFrequency service should be available') - - def test_diagnostic_data_conversion( - self, - expected_frequency, - message_type, - tolerance_hz): + self.assertTrue(manage_available, 'ManageTopic service should be available') + self.assertTrue(set_freq_available, 'SetExpectedFrequency service should be available') + + def test_diagnostic_data_conversion(self, expected_frequency, message_type, tolerance_hz): """Test conversion from DiagnosticStatus to UiDiagnosticData.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running diagnostic conversion tests once') # Create a mock DiagnosticStatus @@ -274,8 +242,7 @@ def test_diagnostic_data_conversion( def test_diagnostic_data_conversion_different_levels( self, expected_frequency, message_type, tolerance_hz): """Test diagnostic status level conversion.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running diagnostic conversion tests once') status_levels = [ @@ -298,8 +265,7 @@ def test_diagnostic_data_conversion_different_levels( def test_toggle_topic_monitoring_add_remove( self, expected_frequency, message_type, tolerance_hz): """Test adding and removing topics from monitoring.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running topic toggle tests once') test_topic = '/test_topic1' @@ -317,18 +283,15 @@ def test_toggle_topic_monitoring_add_remove( while time.time() - start_time < max_wait_time: rclpy.spin_once(self.test_node, timeout_sec=0.1) - topic_data = self.diagnostics_monitor.get_topic_diagnostics( - test_topic) + topic_data = self.diagnostics_monitor.get_topic_diagnostics(test_topic) if topic_data.status != '-': break time.sleep(0.1) # Topic should now have diagnostic data self.assertIsNotNone(topic_data) - self.assertNotEqual( - topic_data.status, - '-', - f'Should have received diagnostic data after {max_wait_time}s') + self.assertNotEqual(topic_data.status, '-', + f'Should have received diagnostic data after {max_wait_time}s') # Remove topic self.diagnostics_monitor.toggle_topic_monitoring(test_topic) @@ -339,8 +302,7 @@ def test_toggle_topic_monitoring_add_remove( def test_set_expected_frequency_operations( self, expected_frequency, message_type, tolerance_hz): """Test setting and clearing expected frequencies.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running frequency setting tests once') test_topic = '/test_topic2' @@ -348,8 +310,7 @@ def test_set_expected_frequency_operations( test_tolerance = 15.0 # Initially no expected frequency - freq, tolerance = self.diagnostics_monitor.get_expected_frequency( - test_topic) + freq, tolerance = self.diagnostics_monitor.get_expected_frequency(test_topic) self.assertEqual((freq, tolerance), (0.0, 0.0)) # Set expected frequency @@ -359,8 +320,7 @@ def test_set_expected_frequency_operations( self.assertTrue(success, f'Failed to set frequency: {message}') # Check that frequency was stored locally - freq, tolerance = self.diagnostics_monitor.get_expected_frequency( - test_topic) + freq, tolerance = self.diagnostics_monitor.get_expected_frequency(test_topic) self.assertEqual((freq, tolerance), (test_freq, test_tolerance)) # Clear expected frequency @@ -370,15 +330,12 @@ def test_set_expected_frequency_operations( self.assertTrue(success, f'Failed to clear frequency: {message}') # Should be back to defaults - freq, tolerance = self.diagnostics_monitor.get_expected_frequency( - test_topic) + freq, tolerance = self.diagnostics_monitor.get_expected_frequency(test_topic) self.assertEqual((freq, tolerance), (0.0, 0.0)) - def test_diagnostic_data_thread_safety( - self, expected_frequency, message_type, tolerance_hz): + def test_diagnostic_data_thread_safety(self, expected_frequency, message_type, tolerance_hz): """Test thread safety of diagnostic data updates.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running thread safety tests once') test_topic = '/test_topic' @@ -390,8 +347,7 @@ def update_thread(): try: for _ in range(50): # Simulate concurrent diagnostic updates - data = self.diagnostics_monitor.get_topic_diagnostics( - test_topic) + data = self.diagnostics_monitor.get_topic_diagnostics(test_topic) if data.status != '-': update_count += 1 time.sleep(0.01) @@ -421,21 +377,16 @@ def spin_thread(): # Should not have encountered any thread safety issues self.assertFalse(error_occurred, 'Thread safety error occurred') - self.assertGreater( - update_count, - 0, - 'Should have received some diagnostic updates') + self.assertGreater(update_count, 0, 'Should have received some diagnostic updates') def test_get_topic_diagnostics_nonexistent_topic( self, expected_frequency, message_type, tolerance_hz): """Test getting diagnostics for non-existent topic.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running diagnostic retrieval tests once') # Request diagnostics for non-monitored topic - data = self.diagnostics_monitor.get_topic_diagnostics( - '/nonexistent_topic') + data = self.diagnostics_monitor.get_topic_diagnostics('/nonexistent_topic') # Should return default values expected_default = UiDiagnosticData() @@ -444,11 +395,9 @@ def test_get_topic_diagnostics_nonexistent_topic( self.assertEqual(data.latency, expected_default.latency) self.assertEqual(data.status, expected_default.status) - def test_diagnostics_callback_processing( - self, expected_frequency, message_type, tolerance_hz): + def test_diagnostics_callback_processing(self, expected_frequency, message_type, tolerance_hz): """Test that diagnostic callbacks are processed correctly.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running callback processing tests once') test_topic = '/test_topic' @@ -563,13 +512,11 @@ def test_yaml_sets_parameters_at_startup( def test_service_timeout_handling(self, expected_frequency, message_type, tolerance_hz): """Test service call timeout handling.""" - if (message_type, expected_frequency, - tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: + if (message_type, expected_frequency, tolerance_hz) != MANAGE_TOPIC_TEST_CONFIG: self.skipTest('Only running timeout handling tests once') # Create a client to a non-existent service - fake_client = self.test_node.create_client( - ManageTopic, '/nonexistent_service') + fake_client = self.test_node.create_client(ManageTopic, '/nonexistent_service') # Replace the real client temporarily original_client = self.diagnostics_monitor.manage_topic_client diff --git a/greenwave_monitor_interfaces/CMakeLists.txt b/greenwave_monitor_interfaces/CMakeLists.txt index 68b3691..a3c2b46 100644 --- a/greenwave_monitor_interfaces/CMakeLists.txt +++ b/greenwave_monitor_interfaces/CMakeLists.txt @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,4 +30,4 @@ rosidl_generate_interfaces(${PROJECT_NAME} "srv/SetExpectedFrequency.srv" ) -ament_package() +ament_package() \ No newline at end of file diff --git a/greenwave_monitor_interfaces/package.xml b/greenwave_monitor_interfaces/package.xml index 011eb9b..8e9e6ee 100644 --- a/greenwave_monitor_interfaces/package.xml +++ b/greenwave_monitor_interfaces/package.xml @@ -1,23 +1,4 @@ - - - greenwave_monitor_interfaces diff --git a/greenwave_monitor_interfaces/srv/ManageTopic.srv b/greenwave_monitor_interfaces/srv/ManageTopic.srv index 8a7b637..ae8abb1 100644 --- a/greenwave_monitor_interfaces/srv/ManageTopic.srv +++ b/greenwave_monitor_interfaces/srv/ManageTopic.srv @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,4 +21,4 @@ bool add_topic # true to add, false to remove --- # Response bool success -string message +string message \ No newline at end of file diff --git a/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv b/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv index e4bfba6..cf61ff4 100644 --- a/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv +++ b/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,4 +24,4 @@ bool add_topic_if_missing # add topic to monitoring if not already --- # Response bool success -string message +string message \ No newline at end of file diff --git a/scripts/build_debian_packages.sh b/scripts/build_debian_packages.sh index 4f011af..8e7f8be 100755 --- a/scripts/build_debian_packages.sh +++ b/scripts/build_debian_packages.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,53 +49,55 @@ UBUNTU_DISTRO="${2:-$DEFAULT_UBUNTU_DISTRO}" # Validate ROS distro case "$ROS_DISTRO" in -humble | iron | jazzy | kilted | rolling) ;; -*) - echo "Error: Unsupported ROS distro: $ROS_DISTRO" - echo "Supported distros: humble, iron, jazzy, kilted, rolling" - exit 1 - ;; + humble|iron|jazzy|kilted|rolling) + ;; + *) + echo "Error: Unsupported ROS distro: $ROS_DISTRO" + echo "Supported distros: humble, iron, jazzy, kilted, rolling" + exit 1 + ;; esac # Validate Ubuntu distro case "$UBUNTU_DISTRO" in -jammy | noble) ;; -*) - echo "Error: Unsupported Ubuntu distro: $UBUNTU_DISTRO" - echo "Supported distros: jammy, noble" - exit 1 - ;; + jammy|noble) + ;; + *) + echo "Error: Unsupported Ubuntu distro: $UBUNTU_DISTRO" + echo "Supported distros: jammy, noble" + exit 1 + ;; esac echo "Building Debian packages for ROS $ROS_DISTRO on Ubuntu $UBUNTU_DISTRO" # Check if running in a container (recommended) or warn user if [ ! -f "/.dockerenv" ] && [ ! -f "/run/.containerenv" ]; then - echo "WARNING: Not running in a container. This script is designed to run in a clean Ubuntu container." - echo "" - echo "Recommended: Run in Docker with:" - echo " docker run -it --rm -v \$(pwd):/workspace -w /workspace ubuntu:$UBUNTU_DISTRO ./scripts/build_debian_packages.sh $ROS_DISTRO $UBUNTU_DISTRO" - echo "" - echo "Press Ctrl+C to cancel, or Enter to continue anyway (not recommended)..." - read -r + echo "WARNING: Not running in a container. This script is designed to run in a clean Ubuntu container." + echo "" + echo "Recommended: Run in Docker with:" + echo " docker run -it --rm -v \$(pwd):/workspace -w /workspace ubuntu:$UBUNTU_DISTRO ./scripts/build_debian_packages.sh $ROS_DISTRO $UBUNTU_DISTRO" + echo "" + echo "Press Ctrl+C to cancel, or Enter to continue anyway (not recommended)..." + read -r fi # Setup ROS repository if not already configured echo "Setting up ROS repository..." export DEBIAN_FRONTEND=noninteractive export TZ=Etc/UTC -ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ >/etc/timezone +ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone apt-get update -qq apt-get install -y curl gnupg lsb-release if [ ! -f "/etc/apt/sources.list.d/ros2.list" ]; then - echo "Adding ROS 2 apt repository..." - curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" >/etc/apt/sources.list.d/ros2.list - apt-get update -qq + echo "Adding ROS 2 apt repository..." + curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" > /etc/apt/sources.list.d/ros2.list + apt-get update -qq else - echo "ROS 2 repository already configured" + echo "ROS 2 repository already configured" fi # Install dependencies @@ -104,7 +106,7 @@ echo "Installing build dependencies..." # Check if we need --break-system-packages for pip USE_BREAK_SYSTEM_PACKAGES="" if [[ "$ROS_DISTRO" == "jazzy" || "$ROS_DISTRO" == "kilted" || "$ROS_DISTRO" == "rolling" ]]; then - USE_BREAK_SYSTEM_PACKAGES="--break-system-packages" + USE_BREAK_SYSTEM_PACKAGES="--break-system-packages" fi # Install system dependencies @@ -112,13 +114,13 @@ apt-get install -y build-essential python3-pip python3-bloom python3-rosdep git # Install Python dependencies if [ -f "requirements.txt" ]; then - if [ -n "$USE_BREAK_SYSTEM_PACKAGES" ]; then - pip3 install $USE_BREAK_SYSTEM_PACKAGES -I pygments -r requirements.txt - python3 -m pip install -U $USE_BREAK_SYSTEM_PACKAGES bloom - else - pip3 install -r requirements.txt - python3 -m pip install -U bloom - fi + if [ -n "$USE_BREAK_SYSTEM_PACKAGES" ]; then + pip3 install $USE_BREAK_SYSTEM_PACKAGES -I pygments -r requirements.txt + python3 -m pip install -U $USE_BREAK_SYSTEM_PACKAGES bloom + else + pip3 install -r requirements.txt + python3 -m pip install -U bloom + fi fi # Initialize rosdep and install all build dependencies @@ -129,9 +131,9 @@ rosdep install --from-paths . --rosdistro "$ROS_DISTRO" --ignore-src -r -y # Source ROS environment (now installed via rosdep) if [ ! -f "/opt/ros/$ROS_DISTRO/setup.bash" ]; then - echo "Error: ROS $ROS_DISTRO not found after rosdep install" - echo "This should have been installed by rosdep. Check package.xml dependencies." - exit 1 + echo "Error: ROS $ROS_DISTRO not found after rosdep install" + echo "This should have been installed by rosdep. Check package.xml dependencies." + exit 1 fi source /opt/ros/$ROS_DISTRO/setup.bash @@ -166,34 +168,34 @@ mkdir -p "$DEBIAN_DIR" # Function to build a Debian package build_debian_package() { - local package_name=$1 - local package_dir=$2 + local package_name=$1 + local package_dir=$2 - echo "==================================" - echo "Generating Debian package for $package_name..." - echo "==================================" + echo "==================================" + echo "Generating Debian package for $package_name..." + echo "==================================" - cd "$package_dir" + cd "$package_dir" - # Generate debian files and build package - bloom-generate rosdebian --ros-distro "$ROS_DISTRO" - apt-get build-dep . -y || sudo apt-get build-dep . -y - fakeroot debian/rules binary + # Generate debian files and build package + bloom-generate rosdebian --ros-distro "$ROS_DISTRO" + apt-get build-dep . -y || sudo apt-get build-dep . -y + fakeroot debian/rules binary - # Move package to output directory - cp ../ros-$ROS_DISTRO-${package_name//_/-}_*.deb "../$DEBIAN_DIR/" + # Move package to output directory + cp ../ros-$ROS_DISTRO-${package_name//_/-}_*.deb "../$DEBIAN_DIR/" - cd .. + cd .. - echo "Successfully built $package_name" + echo "Successfully built $package_name" } # Function to install a package locally install_package() { - local package_pattern=$1 - echo "Installing $package_pattern..." - apt-get update || sudo apt-get update - apt-get install -y ./$DEBIAN_DIR/$package_pattern || sudo apt-get install -y ./$DEBIAN_DIR/$package_pattern + local package_pattern=$1 + echo "Installing $package_pattern..." + apt-get update || sudo apt-get update + apt-get install -y ./$DEBIAN_DIR/$package_pattern || sudo apt-get install -y ./$DEBIAN_DIR/$package_pattern } # Build packages in dependency order @@ -201,18 +203,18 @@ echo "Starting Debian package generation..." # 1. Build greenwave_monitor_interfaces if [ -d "greenwave_monitor_interfaces" ]; then - build_debian_package "greenwave_monitor_interfaces" "greenwave_monitor_interfaces" - install_package "ros-$ROS_DISTRO-greenwave-monitor-interfaces_*.deb" + build_debian_package "greenwave_monitor_interfaces" "greenwave_monitor_interfaces" + install_package "ros-$ROS_DISTRO-greenwave-monitor-interfaces_*.deb" else - echo "Warning: greenwave_monitor_interfaces directory not found, skipping" + echo "Warning: greenwave_monitor_interfaces directory not found, skipping" fi # 2. Build greenwave_monitor if [ -d "greenwave_monitor" ]; then - build_debian_package "greenwave_monitor" "greenwave_monitor" - install_package "ros-$ROS_DISTRO-greenwave-monitor_*.deb" + build_debian_package "greenwave_monitor" "greenwave_monitor" + install_package "ros-$ROS_DISTRO-greenwave-monitor_*.deb" else - echo "Warning: greenwave_monitor directory not found, skipping" + echo "Warning: greenwave_monitor directory not found, skipping" fi # Note: r2s_gw is now a separate repository and not included in debian packages diff --git a/scripts/docker-test.sh b/scripts/docker-test.sh index dc5ec0c..16a4ebb 100755 --- a/scripts/docker-test.sh +++ b/scripts/docker-test.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,26 +27,26 @@ DISTRO=${1:-humble} # Image mapping based on ROS distro case $DISTRO in -humble) - IMAGE="ros:humble-ros-base-jammy" - ;; -iron) - IMAGE="ros:iron-ros-base-jammy" - ;; -jazzy) - IMAGE="ros:jazzy-ros-base-noble" - ;; -kilted) - IMAGE="ros:kilted-ros-base-noble" - ;; -rolling) - IMAGE="ros:rolling-ros-base-noble" - ;; -*) - echo "Unsupported ROS 2 distribution: $DISTRO" - echo "Supported: humble, iron, jazzy, kilted, rolling" - exit 1 - ;; + humble) + IMAGE="ros:humble-ros-base-jammy" + ;; + iron) + IMAGE="ros:iron-ros-base-jammy" + ;; + jazzy) + IMAGE="ros:jazzy-ros-base-noble" + ;; + kilted) + IMAGE="ros:kilted-ros-base-noble" + ;; + rolling) + IMAGE="ros:rolling-ros-base-noble" + ;; + *) + echo "Unsupported ROS 2 distribution: $DISTRO" + echo "Supported: humble, iron, jazzy, kilted, rolling" + exit 1 + ;; esac echo "Starting Docker container for ROS 2 $DISTRO..." @@ -57,14 +57,14 @@ WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../" # Run container with interactive shell, mounting current directory docker run -it --rm \ - --name greenwave-test-${DISTRO} \ - -e ROS_LOCALHOST_ONLY=1 \ - -e ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST \ - -e ROS_DISTRO=${DISTRO} \ - -v ${WORKSPACE_DIR}:/workspace/src/greenwave_monitor \ - -w /workspace \ - ${IMAGE} \ - bash -c " + --name greenwave-test-${DISTRO} \ + -e ROS_LOCALHOST_ONLY=1 \ + -e ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST \ + -e ROS_DISTRO=${DISTRO} \ + -v ${WORKSPACE_DIR}:/workspace/src/greenwave_monitor \ + -w /workspace \ + ${IMAGE} \ + bash -c " # Source ROS setup source /opt/ros/${DISTRO}/setup.bash @@ -100,4 +100,4 @@ docker run -it --rm \ # Start interactive shell bash - " + " \ No newline at end of file From 0489236627467277c57feec095d25f642da73428 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 15:46:41 -0800 Subject: [PATCH 10/33] Fix lints Signed-off-by: Blake McHale --- .github/workflows/debian-packages.yml | 4 +- .github/workflows/ros-tests.yml | 4 +- .gitignore | 2 +- Contributing.md | 2 +- README.md | 2 +- greenwave_monitor/CMakeLists.txt | 2 +- greenwave_monitor/examples/README.md | 2 +- .../greenwave_monitor/ncurses_frontend.py | 2 +- .../greenwave_monitor/test_utils.py | 4 +- .../greenwave_monitor/ui_adaptor.py | 2 +- .../include/greenwave_diagnostics.hpp | 2 +- .../include/greenwave_monitor.hpp | 2 +- .../include/minimal_publisher_node.hpp | 2 +- greenwave_monitor/launch/hz.launch.py | 2 +- .../launch/test_publishers.launch.py | 2 +- greenwave_monitor/scripts/ncurses_dashboard | 130 +++++++++--------- greenwave_monitor/src/greenwave_monitor.cpp | 2 +- .../src/greenwave_monitor_main.cpp | 2 +- .../src/minimal_publisher_node.cpp | 2 +- .../test/test_greenwave_diagnostics.cpp | 2 +- .../test/test_greenwave_monitor.py | 2 +- .../test/test_ncurses_frontend_argparse.py | 2 +- .../test/test_topic_monitoring_integration.py | 2 +- greenwave_monitor_interfaces/CMakeLists.txt | 4 +- greenwave_monitor_interfaces/package.xml | 19 +++ .../srv/ManageTopic.srv | 4 +- .../srv/SetExpectedFrequency.srv | 4 +- scripts/build_debian_packages.sh | 124 ++++++++--------- scripts/docker-test.sh | 60 ++++---- 29 files changed, 206 insertions(+), 189 deletions(-) diff --git a/.github/workflows/debian-packages.yml b/.github/workflows/debian-packages.yml index 77e948c..da25bd7 100644 --- a/.github/workflows/debian-packages.yml +++ b/.github/workflows/debian-packages.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -318,4 +318,4 @@ jobs: sleep 2 kill -9 $(cat /tmp/gwm.pid) 2>/dev/null || true ros2 daemon stop || true - shell: bash \ No newline at end of file + shell: bash diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index b2e4d5e..6ddca4b 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -138,4 +138,4 @@ jobs: with: name: test-results-${{ matrix.ros_distro }} path: build/*/test_results/**/*.xml - retention-days: 7 \ No newline at end of file + retention-days: 7 diff --git a/.gitignore b/.gitignore index 5b49528..803c0d1 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,4 @@ _deps # Temporary files *.tmp -*.temp \ No newline at end of file +*.temp diff --git a/Contributing.md b/Contributing.md index dde649a..0c46b20 100644 --- a/Contributing.md +++ b/Contributing.md @@ -1,4 +1,4 @@ -We welcome PRs for new features or bugfixes, CI will automatically run automated tests on new PRs. You can also use the scripts/docker-test.sh to debug tests for a particular distribution locally. +We welcome PRs for new features or bugfixes, CI will automatically run automated tests on new PRs. You can also use the scripts/docker-test.sh to debug tests for a particular distribution locally. We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. diff --git a/README.md b/README.md index c926a89..b4011f2 100644 --- a/README.md +++ b/README.md @@ -109,4 +109,4 @@ If you want to use it as a command line tool, you can do so with the following l ```bash ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' -``` \ No newline at end of file +``` diff --git a/greenwave_monitor/CMakeLists.txt b/greenwave_monitor/CMakeLists.txt index e28c566..9135e8d 100644 --- a/greenwave_monitor/CMakeLists.txt +++ b/greenwave_monitor/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/examples/README.md b/greenwave_monitor/examples/README.md index cfa4518..15651c2 100644 --- a/greenwave_monitor/examples/README.md +++ b/greenwave_monitor/examples/README.md @@ -31,4 +31,4 @@ Node( ), ``` -To see the output with the r2s_gw dashboard, run `ros2 run r2s_gw r2s_gw` in a separate terminal. \ No newline at end of file +To see the output with the r2s_gw dashboard, run `ros2 run r2s_gw r2s_gw` in a separate terminal. diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index a50d231..0f3359f 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/greenwave_monitor/test_utils.py b/greenwave_monitor/greenwave_monitor/test_utils.py index 49f5e0d..e952720 100644 --- a/greenwave_monitor/greenwave_monitor/test_utils.py +++ b/greenwave_monitor/greenwave_monitor/test_utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -176,7 +176,7 @@ def find_best_diagnostic( diagnostics: List[DiagnosticStatus], expected_frequency: float, message_type: str - ) -> Tuple[Optional[DiagnosticStatus], Optional[Tuple[float, float, float]]]: +) -> Tuple[Optional[DiagnosticStatus], Optional[Tuple[float, float, float]]]: """Find the diagnostic message with frequency closest to expected.""" best_status = None best_values = None diff --git a/greenwave_monitor/greenwave_monitor/ui_adaptor.py b/greenwave_monitor/greenwave_monitor/ui_adaptor.py index 30d84ba..015962b 100644 --- a/greenwave_monitor/greenwave_monitor/ui_adaptor.py +++ b/greenwave_monitor/greenwave_monitor/ui_adaptor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/include/greenwave_diagnostics.hpp b/greenwave_monitor/include/greenwave_diagnostics.hpp index 42ccafe..b291d73 100644 --- a/greenwave_monitor/include/greenwave_diagnostics.hpp +++ b/greenwave_monitor/include/greenwave_diagnostics.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/include/greenwave_monitor.hpp b/greenwave_monitor/include/greenwave_monitor.hpp index fdb1e0a..d6368c9 100644 --- a/greenwave_monitor/include/greenwave_monitor.hpp +++ b/greenwave_monitor/include/greenwave_monitor.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/include/minimal_publisher_node.hpp b/greenwave_monitor/include/minimal_publisher_node.hpp index f1fcbb3..8691946 100644 --- a/greenwave_monitor/include/minimal_publisher_node.hpp +++ b/greenwave_monitor/include/minimal_publisher_node.hpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/launch/hz.launch.py b/greenwave_monitor/launch/hz.launch.py index bb679df..534d040 100644 --- a/greenwave_monitor/launch/hz.launch.py +++ b/greenwave_monitor/launch/hz.launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/launch/test_publishers.launch.py b/greenwave_monitor/launch/test_publishers.launch.py index e1907c9..728e5a0 100644 --- a/greenwave_monitor/launch/test_publishers.launch.py +++ b/greenwave_monitor/launch/test_publishers.launch.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/scripts/ncurses_dashboard b/greenwave_monitor/scripts/ncurses_dashboard index 807a777..b75d2d1 100755 --- a/greenwave_monitor/scripts/ncurses_dashboard +++ b/greenwave_monitor/scripts/ncurses_dashboard @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,77 +27,77 @@ HIDE_UNMONITORED=false MONITOR_ARGS=() show_help() { - echo "Usage: $0 [OPTIONS] [MONITOR_ARGS...]" - echo "" - echo "Launch Greenwave Monitor with ncurses TUI dashboard" - echo "" - echo "OPTIONS:" - echo " --demo, --test Launch demo publisher nodes for testing" - echo " --log-dir DIR Enable logging to specified directory" - echo " --hide-unmonitored Hide unmonitored topics on initialization" - echo " --help, -h Show this help message" - echo "" - echo "MONITOR_ARGS are passed directly to the greenwave_monitor node" - echo "" - echo "Controls in ncurses interface:" - echo " enter/space = toggle topic monitoring" - echo " f = Set expected frequency for selected topic (format: hz tolerance%)" - echo " c = Clear frequency settings for selected topic" - echo " h = Toggle hiding unmonitored topics" - echo " ↑/↓ = Navigate topics" - echo " q = Quit" + echo "Usage: $0 [OPTIONS] [MONITOR_ARGS...]" + echo "" + echo "Launch Greenwave Monitor with ncurses TUI dashboard" + echo "" + echo "OPTIONS:" + echo " --demo, --test Launch demo publisher nodes for testing" + echo " --log-dir DIR Enable logging to specified directory" + echo " --hide-unmonitored Hide unmonitored topics on initialization" + echo " --help, -h Show this help message" + echo "" + echo "MONITOR_ARGS are passed directly to the greenwave_monitor node" + echo "" + echo "Controls in ncurses interface:" + echo " enter/space = toggle topic monitoring" + echo " f = Set expected frequency for selected topic (format: hz tolerance%)" + echo " c = Clear frequency settings for selected topic" + echo " h = Toggle hiding unmonitored topics" + echo " ↑/↓ = Navigate topics" + echo " q = Quit" } while [[ $# -gt 0 ]]; do - case $1 in - --demo|--test) - DEMO_MODE=true - shift - ;; - --log-dir) - LOG_DIR="$2" - shift 2 - ;; - --hide-unmonitored) - HIDE_UNMONITORED=true - shift - ;; - --help|-h) - show_help - exit 0 - ;; - *) - MONITOR_ARGS+=("$1") - shift - ;; - esac + case $1 in + --demo | --test) + DEMO_MODE=true + shift + ;; + --log-dir) + LOG_DIR="$2" + shift 2 + ;; + --hide-unmonitored) + HIDE_UNMONITORED=true + shift + ;; + --help | -h) + show_help + exit 0 + ;; + *) + MONITOR_ARGS+=("$1") + shift + ;; + esac done # Handle logging configuration LOG_FILE="/dev/null" if [ -n "$LOG_DIR" ]; then - # Create logs directory if it doesn't exist - mkdir -p "${LOG_DIR}" + # Create logs directory if it doesn't exist + mkdir -p "${LOG_DIR}" - # Create a timestamped log file - TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") - LOG_FILE="${LOG_DIR}/monitor_ncurses_${TIMESTAMP}.log" + # Create a timestamped log file + TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") + LOG_FILE="${LOG_DIR}/monitor_ncurses_${TIMESTAMP}.log" fi # Function to clean up background processes on exit cleanup() { - echo "Shutting down..." - if [ -n "$MONITOR_PID" ]; then - echo "Terminating monitor node (PID: $MONITOR_PID)..." - if [ "$LOG_FILE" != "/dev/null" ]; then - echo "Monitor log available at: ${LOG_FILE}" - fi - # Kill background process and all its descendants - pkill -TERM -P $MONITOR_PID 2>/dev/null - kill -TERM $MONITOR_PID 2>/dev/null - fi - exit 0 + echo "Shutting down..." + if [ -n "$MONITOR_PID" ]; then + echo "Terminating monitor node (PID: $MONITOR_PID)..." + if [ "$LOG_FILE" != "/dev/null" ]; then + echo "Monitor log available at: ${LOG_FILE}" + fi + # Kill background process and all its descendants + pkill -TERM -P $MONITOR_PID 2>/dev/null + kill -TERM $MONITOR_PID 2>/dev/null + fi + exit 0 } # Set up trap to catch signals @@ -105,13 +105,13 @@ trap cleanup SIGINT SIGTERM EXIT # Launch demo nodes if requested if [ "$DEMO_MODE" = "true" ]; then - echo "Starting demo mode with test publisher nodes..." - ros2 launch greenwave_monitor example.launch.py &> "${LOG_FILE}" & - MONITOR_PID=$! + echo "Starting demo mode with test publisher nodes..." + ros2 launch greenwave_monitor example.launch.py &>"${LOG_FILE}" & + MONITOR_PID=$! else - echo "Starting Greenwave Monitor..." - ros2 run greenwave_monitor greenwave_monitor "${MONITOR_ARGS[@]}" &> "${LOG_FILE}" & - MONITOR_PID=$! + echo "Starting Greenwave Monitor..." + ros2 run greenwave_monitor greenwave_monitor "${MONITOR_ARGS[@]}" &>"${LOG_FILE}" & + MONITOR_PID=$! fi # Wait briefly to allow the monitor node to initialize @@ -124,7 +124,7 @@ echo "Controls: a=Add Topic, r=Remove, f=Set Frequency, c=Clear Freq, q=Quit" # NOTE: add proper argument parsing to the ncurses frontend if more than one argument is added here FRONTEND_ARGS=() if [ "$HIDE_UNMONITORED" = "true" ]; then - FRONTEND_ARGS+=("--hide-unmonitored") + FRONTEND_ARGS+=("--hide-unmonitored") fi python3 -m greenwave_monitor.ncurses_frontend "${FRONTEND_ARGS[@]}" diff --git a/greenwave_monitor/src/greenwave_monitor.cpp b/greenwave_monitor/src/greenwave_monitor.cpp index 9d59ff4..3736b51 100644 --- a/greenwave_monitor/src/greenwave_monitor.cpp +++ b/greenwave_monitor/src/greenwave_monitor.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/src/greenwave_monitor_main.cpp b/greenwave_monitor/src/greenwave_monitor_main.cpp index 95fc005..edf9aa1 100644 --- a/greenwave_monitor/src/greenwave_monitor_main.cpp +++ b/greenwave_monitor/src/greenwave_monitor_main.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/src/minimal_publisher_node.cpp b/greenwave_monitor/src/minimal_publisher_node.cpp index 0258b06..88b695b 100644 --- a/greenwave_monitor/src/minimal_publisher_node.cpp +++ b/greenwave_monitor/src/minimal_publisher_node.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/test/test_greenwave_diagnostics.cpp b/greenwave_monitor/test/test_greenwave_diagnostics.cpp index 6b158ae..c201049 100644 --- a/greenwave_monitor/test/test_greenwave_diagnostics.cpp +++ b/greenwave_monitor/test/test_greenwave_diagnostics.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/test/test_greenwave_monitor.py b/greenwave_monitor/test/test_greenwave_monitor.py index 82b58d4..4fd10c4 100644 --- a/greenwave_monitor/test/test_greenwave_monitor.py +++ b/greenwave_monitor/test/test_greenwave_monitor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/test/test_ncurses_frontend_argparse.py b/greenwave_monitor/test/test_ncurses_frontend_argparse.py index a12f8f3..db6e500 100644 --- a/greenwave_monitor/test/test_ncurses_frontend_argparse.py +++ b/greenwave_monitor/test/test_ncurses_frontend_argparse.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor/test/test_topic_monitoring_integration.py b/greenwave_monitor/test/test_topic_monitoring_integration.py index 3641c64..5f93769 100644 --- a/greenwave_monitor/test/test_topic_monitoring_integration.py +++ b/greenwave_monitor/test/test_topic_monitoring_integration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/greenwave_monitor_interfaces/CMakeLists.txt b/greenwave_monitor_interfaces/CMakeLists.txt index a3c2b46..68b3691 100644 --- a/greenwave_monitor_interfaces/CMakeLists.txt +++ b/greenwave_monitor_interfaces/CMakeLists.txt @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,4 +30,4 @@ rosidl_generate_interfaces(${PROJECT_NAME} "srv/SetExpectedFrequency.srv" ) -ament_package() \ No newline at end of file +ament_package() diff --git a/greenwave_monitor_interfaces/package.xml b/greenwave_monitor_interfaces/package.xml index 8e9e6ee..011eb9b 100644 --- a/greenwave_monitor_interfaces/package.xml +++ b/greenwave_monitor_interfaces/package.xml @@ -1,4 +1,23 @@ + + + greenwave_monitor_interfaces diff --git a/greenwave_monitor_interfaces/srv/ManageTopic.srv b/greenwave_monitor_interfaces/srv/ManageTopic.srv index ae8abb1..8a7b637 100644 --- a/greenwave_monitor_interfaces/srv/ManageTopic.srv +++ b/greenwave_monitor_interfaces/srv/ManageTopic.srv @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,4 +21,4 @@ bool add_topic # true to add, false to remove --- # Response bool success -string message \ No newline at end of file +string message diff --git a/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv b/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv index cf61ff4..e4bfba6 100644 --- a/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv +++ b/greenwave_monitor_interfaces/srv/SetExpectedFrequency.srv @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,4 +24,4 @@ bool add_topic_if_missing # add topic to monitoring if not already --- # Response bool success -string message \ No newline at end of file +string message diff --git a/scripts/build_debian_packages.sh b/scripts/build_debian_packages.sh index 8e7f8be..4f011af 100755 --- a/scripts/build_debian_packages.sh +++ b/scripts/build_debian_packages.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,55 +49,53 @@ UBUNTU_DISTRO="${2:-$DEFAULT_UBUNTU_DISTRO}" # Validate ROS distro case "$ROS_DISTRO" in - humble|iron|jazzy|kilted|rolling) - ;; - *) - echo "Error: Unsupported ROS distro: $ROS_DISTRO" - echo "Supported distros: humble, iron, jazzy, kilted, rolling" - exit 1 - ;; +humble | iron | jazzy | kilted | rolling) ;; +*) + echo "Error: Unsupported ROS distro: $ROS_DISTRO" + echo "Supported distros: humble, iron, jazzy, kilted, rolling" + exit 1 + ;; esac # Validate Ubuntu distro case "$UBUNTU_DISTRO" in - jammy|noble) - ;; - *) - echo "Error: Unsupported Ubuntu distro: $UBUNTU_DISTRO" - echo "Supported distros: jammy, noble" - exit 1 - ;; +jammy | noble) ;; +*) + echo "Error: Unsupported Ubuntu distro: $UBUNTU_DISTRO" + echo "Supported distros: jammy, noble" + exit 1 + ;; esac echo "Building Debian packages for ROS $ROS_DISTRO on Ubuntu $UBUNTU_DISTRO" # Check if running in a container (recommended) or warn user if [ ! -f "/.dockerenv" ] && [ ! -f "/run/.containerenv" ]; then - echo "WARNING: Not running in a container. This script is designed to run in a clean Ubuntu container." - echo "" - echo "Recommended: Run in Docker with:" - echo " docker run -it --rm -v \$(pwd):/workspace -w /workspace ubuntu:$UBUNTU_DISTRO ./scripts/build_debian_packages.sh $ROS_DISTRO $UBUNTU_DISTRO" - echo "" - echo "Press Ctrl+C to cancel, or Enter to continue anyway (not recommended)..." - read -r + echo "WARNING: Not running in a container. This script is designed to run in a clean Ubuntu container." + echo "" + echo "Recommended: Run in Docker with:" + echo " docker run -it --rm -v \$(pwd):/workspace -w /workspace ubuntu:$UBUNTU_DISTRO ./scripts/build_debian_packages.sh $ROS_DISTRO $UBUNTU_DISTRO" + echo "" + echo "Press Ctrl+C to cancel, or Enter to continue anyway (not recommended)..." + read -r fi # Setup ROS repository if not already configured echo "Setting up ROS repository..." export DEBIAN_FRONTEND=noninteractive export TZ=Etc/UTC -ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ >/etc/timezone apt-get update -qq apt-get install -y curl gnupg lsb-release if [ ! -f "/etc/apt/sources.list.d/ros2.list" ]; then - echo "Adding ROS 2 apt repository..." - curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" > /etc/apt/sources.list.d/ros2.list - apt-get update -qq + echo "Adding ROS 2 apt repository..." + curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" >/etc/apt/sources.list.d/ros2.list + apt-get update -qq else - echo "ROS 2 repository already configured" + echo "ROS 2 repository already configured" fi # Install dependencies @@ -106,7 +104,7 @@ echo "Installing build dependencies..." # Check if we need --break-system-packages for pip USE_BREAK_SYSTEM_PACKAGES="" if [[ "$ROS_DISTRO" == "jazzy" || "$ROS_DISTRO" == "kilted" || "$ROS_DISTRO" == "rolling" ]]; then - USE_BREAK_SYSTEM_PACKAGES="--break-system-packages" + USE_BREAK_SYSTEM_PACKAGES="--break-system-packages" fi # Install system dependencies @@ -114,13 +112,13 @@ apt-get install -y build-essential python3-pip python3-bloom python3-rosdep git # Install Python dependencies if [ -f "requirements.txt" ]; then - if [ -n "$USE_BREAK_SYSTEM_PACKAGES" ]; then - pip3 install $USE_BREAK_SYSTEM_PACKAGES -I pygments -r requirements.txt - python3 -m pip install -U $USE_BREAK_SYSTEM_PACKAGES bloom - else - pip3 install -r requirements.txt - python3 -m pip install -U bloom - fi + if [ -n "$USE_BREAK_SYSTEM_PACKAGES" ]; then + pip3 install $USE_BREAK_SYSTEM_PACKAGES -I pygments -r requirements.txt + python3 -m pip install -U $USE_BREAK_SYSTEM_PACKAGES bloom + else + pip3 install -r requirements.txt + python3 -m pip install -U bloom + fi fi # Initialize rosdep and install all build dependencies @@ -131,9 +129,9 @@ rosdep install --from-paths . --rosdistro "$ROS_DISTRO" --ignore-src -r -y # Source ROS environment (now installed via rosdep) if [ ! -f "/opt/ros/$ROS_DISTRO/setup.bash" ]; then - echo "Error: ROS $ROS_DISTRO not found after rosdep install" - echo "This should have been installed by rosdep. Check package.xml dependencies." - exit 1 + echo "Error: ROS $ROS_DISTRO not found after rosdep install" + echo "This should have been installed by rosdep. Check package.xml dependencies." + exit 1 fi source /opt/ros/$ROS_DISTRO/setup.bash @@ -168,34 +166,34 @@ mkdir -p "$DEBIAN_DIR" # Function to build a Debian package build_debian_package() { - local package_name=$1 - local package_dir=$2 + local package_name=$1 + local package_dir=$2 - echo "==================================" - echo "Generating Debian package for $package_name..." - echo "==================================" + echo "==================================" + echo "Generating Debian package for $package_name..." + echo "==================================" - cd "$package_dir" + cd "$package_dir" - # Generate debian files and build package - bloom-generate rosdebian --ros-distro "$ROS_DISTRO" - apt-get build-dep . -y || sudo apt-get build-dep . -y - fakeroot debian/rules binary + # Generate debian files and build package + bloom-generate rosdebian --ros-distro "$ROS_DISTRO" + apt-get build-dep . -y || sudo apt-get build-dep . -y + fakeroot debian/rules binary - # Move package to output directory - cp ../ros-$ROS_DISTRO-${package_name//_/-}_*.deb "../$DEBIAN_DIR/" + # Move package to output directory + cp ../ros-$ROS_DISTRO-${package_name//_/-}_*.deb "../$DEBIAN_DIR/" - cd .. + cd .. - echo "Successfully built $package_name" + echo "Successfully built $package_name" } # Function to install a package locally install_package() { - local package_pattern=$1 - echo "Installing $package_pattern..." - apt-get update || sudo apt-get update - apt-get install -y ./$DEBIAN_DIR/$package_pattern || sudo apt-get install -y ./$DEBIAN_DIR/$package_pattern + local package_pattern=$1 + echo "Installing $package_pattern..." + apt-get update || sudo apt-get update + apt-get install -y ./$DEBIAN_DIR/$package_pattern || sudo apt-get install -y ./$DEBIAN_DIR/$package_pattern } # Build packages in dependency order @@ -203,18 +201,18 @@ echo "Starting Debian package generation..." # 1. Build greenwave_monitor_interfaces if [ -d "greenwave_monitor_interfaces" ]; then - build_debian_package "greenwave_monitor_interfaces" "greenwave_monitor_interfaces" - install_package "ros-$ROS_DISTRO-greenwave-monitor-interfaces_*.deb" + build_debian_package "greenwave_monitor_interfaces" "greenwave_monitor_interfaces" + install_package "ros-$ROS_DISTRO-greenwave-monitor-interfaces_*.deb" else - echo "Warning: greenwave_monitor_interfaces directory not found, skipping" + echo "Warning: greenwave_monitor_interfaces directory not found, skipping" fi # 2. Build greenwave_monitor if [ -d "greenwave_monitor" ]; then - build_debian_package "greenwave_monitor" "greenwave_monitor" - install_package "ros-$ROS_DISTRO-greenwave-monitor_*.deb" + build_debian_package "greenwave_monitor" "greenwave_monitor" + install_package "ros-$ROS_DISTRO-greenwave-monitor_*.deb" else - echo "Warning: greenwave_monitor directory not found, skipping" + echo "Warning: greenwave_monitor directory not found, skipping" fi # Note: r2s_gw is now a separate repository and not included in debian packages diff --git a/scripts/docker-test.sh b/scripts/docker-test.sh index 16a4ebb..dc5ec0c 100755 --- a/scripts/docker-test.sh +++ b/scripts/docker-test.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,26 +27,26 @@ DISTRO=${1:-humble} # Image mapping based on ROS distro case $DISTRO in - humble) - IMAGE="ros:humble-ros-base-jammy" - ;; - iron) - IMAGE="ros:iron-ros-base-jammy" - ;; - jazzy) - IMAGE="ros:jazzy-ros-base-noble" - ;; - kilted) - IMAGE="ros:kilted-ros-base-noble" - ;; - rolling) - IMAGE="ros:rolling-ros-base-noble" - ;; - *) - echo "Unsupported ROS 2 distribution: $DISTRO" - echo "Supported: humble, iron, jazzy, kilted, rolling" - exit 1 - ;; +humble) + IMAGE="ros:humble-ros-base-jammy" + ;; +iron) + IMAGE="ros:iron-ros-base-jammy" + ;; +jazzy) + IMAGE="ros:jazzy-ros-base-noble" + ;; +kilted) + IMAGE="ros:kilted-ros-base-noble" + ;; +rolling) + IMAGE="ros:rolling-ros-base-noble" + ;; +*) + echo "Unsupported ROS 2 distribution: $DISTRO" + echo "Supported: humble, iron, jazzy, kilted, rolling" + exit 1 + ;; esac echo "Starting Docker container for ROS 2 $DISTRO..." @@ -57,14 +57,14 @@ WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../" # Run container with interactive shell, mounting current directory docker run -it --rm \ - --name greenwave-test-${DISTRO} \ - -e ROS_LOCALHOST_ONLY=1 \ - -e ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST \ - -e ROS_DISTRO=${DISTRO} \ - -v ${WORKSPACE_DIR}:/workspace/src/greenwave_monitor \ - -w /workspace \ - ${IMAGE} \ - bash -c " + --name greenwave-test-${DISTRO} \ + -e ROS_LOCALHOST_ONLY=1 \ + -e ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST \ + -e ROS_DISTRO=${DISTRO} \ + -v ${WORKSPACE_DIR}:/workspace/src/greenwave_monitor \ + -w /workspace \ + ${IMAGE} \ + bash -c " # Source ROS setup source /opt/ros/${DISTRO}/setup.bash @@ -100,4 +100,4 @@ docker run -it --rm \ # Start interactive shell bash - " \ No newline at end of file + " From 1ff3421df6b95693b4200857a728c9948832b82c Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 16:15:12 -0800 Subject: [PATCH 11/33] Update Contributing.md to reference installation steps Signed-off-by: Blake McHale --- .pre-commit-config.yaml | 5 ----- Contributing.md | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fc483f..5a683b8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,11 +30,6 @@ repos: hooks: - id: flake8 args: [--max-line-length=99, '--extend-ignore=B902,C816,D100,D101,D102,D103,D104,D105,D106,D107,D203,D212,D404,I202'] -- repo: https://github.com/netromdk/vermin - rev: v1.6.0 - hooks: - - id: vermin - args: [-t=3.10-, --violations] - repo: https://github.com/scop/pre-commit-shfmt rev: v3.12.0-2 hooks: diff --git a/Contributing.md b/Contributing.md index 0c46b20..0360f9a 100644 --- a/Contributing.md +++ b/Contributing.md @@ -1,5 +1,9 @@ +# Contributing + We welcome PRs for new features or bugfixes, CI will automatically run automated tests on new PRs. You can also use the scripts/docker-test.sh to debug tests for a particular distribution locally. +## Sign Off + We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. Any contribution which contains commits that are not Signed-Off can not be accepted. @@ -52,3 +56,13 @@ By making a contribution to this project, I certify that: maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` + +## Linting + +If you would like to run the linter locally you can setup a pre-commit hook: + +``` +sudo apt install uncrustify pipx +pipx install pre-commit +pre-commit run --all-files # will also run on every commit now +``` From eb663608423bf0666641e5c4614a696e21b7295f Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 16:30:23 -0800 Subject: [PATCH 12/33] Add lint tests as first step since it should fail fast Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 6ddca4b..273d104 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -52,8 +52,20 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: + fetch-depth: 0 # fetch all history to run pre-commit copyright check hooks path: greenwave_monitor + - name: Run pre-commit hooks (linting) + run: | + apt update && apt install -y pipx + pipx install pre-commit + pipx ensurepath + source ~/.bashrc + pre-commit run --all-files --show-diff-on-failure + shell: bash + env: + DEBIAN_FRONTEND: noninteractive + - name: Clone r2s_gw repository run: | # Install git if not already available @@ -121,9 +133,9 @@ jobs: source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash set -e - timeout 10s bash -lc "script -qfec 'ros2 run greenwave_monitor ncurses_dashboard' /dev/null <<< \$'q'" || true - timeout 10s bash -lc "script -qfec 'ros2 run r2s_gw r2s_gw_dashboard' /dev/null <<< \$'q'" || true - timeout 5s ros2 launch greenwave_monitor hz.launch.py topics:='["/topic1"]' || true + timeout 10s bash -lc "script -qfec 'ros2 run greenwave_monitor ncurses_dashboard' /dev/null <<< \$'q'" + timeout 10s bash -lc "script -qfec 'ros2 run r2s_gw r2s_gw_dashboard' /dev/null <<< \$'q'" + timeout 5s ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' shell: bash - name: Run tests From da401d0f5d8c11fb8342bdcbb60ebc13ef84ea4c Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 16:31:53 -0800 Subject: [PATCH 13/33] Tests lint in CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 273d104..f5e171b 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -57,7 +57,7 @@ jobs: - name: Run pre-commit hooks (linting) run: | - apt update && apt install -y pipx + apt update && apt install -y pipx git pipx install pre-commit pipx ensurepath source ~/.bashrc From b09b7b9549edee7351081419e946d25630b6431e Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 16:51:24 -0800 Subject: [PATCH 14/33] Check sign off pre-commit Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 2 +- .pre-commit-config.yaml | 13 ++++++++++++- Contributing.md | 6 ++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index f5e171b..5804f15 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 # fetch all history to run pre-commit copyright check hooks path: greenwave_monitor - - name: Run pre-commit hooks (linting) + - name: Run pre-commit hooks (linting, sign-off check, copyright check, etc.) run: | apt update && apt install -y pipx git pipx install pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a683b8..cea8a75 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,11 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace +# - repo: https://github.com/KAUTH/pre-commit-git-checks +# rev: 6bceeb7fb9f24270acff72b46979e9298bd60823 +# hooks: +# - id: git-signoff +# stages: [commit-msg] - repo: https://github.com/hhatto/autopep8 rev: v2.3.1 hooks: @@ -38,7 +43,7 @@ repos: rev: 1.6.1 hooks: - id: copyright-required - exclude: '(\.ini|\.json|\.service|__init__\.py|\.md|\.gitkeep|\.conf|LICENSE|\.toml|\.template|\.style\..*|\.gitattributes|\.gitignore|\.editorconfig|\.bash-completion|\.install|\.links|changelog|debian/source/format|.codespellrc|copyright|ament_code_style\.cfg|test_pep257\.py|test_flake8\.py|test_copyright\.py)$' + exclude: '(^\.git/|(\.ini|\.json|\.service|__init__\.py|\.md|\.gitkeep|\.conf|LICENSE|\.toml|\.template|\.style\..*|\.gitattributes|\.gitignore|\.editorconfig|\.bash-completion|\.install|\.links|changelog|debian/source/format|.codespellrc|copyright|ament_code_style\.cfg|test_pep257\.py|test_flake8\.py|test_copyright\.py)$)' - repo: local hooks: - id: uncrustify @@ -46,6 +51,12 @@ repos: entry: uncrustify -c ament_code_style.cfg --replace --no-backup language: system types_or: [c, c++] + - id: signed-off-by + name: Check commit is signed off + entry: "bash -c 'grep -q \"^Signed-off-by: \" \"$1\"' --" + language: system + stages: [commit-msg] + pass_filenames: true - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: diff --git a/Contributing.md b/Contributing.md index 0360f9a..10ff885 100644 --- a/Contributing.md +++ b/Contributing.md @@ -57,12 +57,14 @@ By making a contribution to this project, I certify that: this project or the open source license(s) involved. ``` -## Linting +## Pre-commit hooks (linting, sign-off check, copyright check, etc.) -If you would like to run the linter locally you can setup a pre-commit hook: +If you would like the linter and other checks to run on every commit use [pre-commit](https://pre-commit.com/): ``` sudo apt install uncrustify pipx pipx install pre-commit pre-commit run --all-files # will also run on every commit now ``` + +Every commit now a series of checks will be run to ensure the changes are meeting this repositories requirements. From a016e5c1d9d9a217733f73a62de820d15027330d Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 16:52:27 -0800 Subject: [PATCH 15/33] Check commit Signed-off-by: Blake McHale --- .pre-commit-config.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cea8a75..aa2eafb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,6 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace -# - repo: https://github.com/KAUTH/pre-commit-git-checks -# rev: 6bceeb7fb9f24270acff72b46979e9298bd60823 -# hooks: -# - id: git-signoff -# stages: [commit-msg] - repo: https://github.com/hhatto/autopep8 rev: v2.3.1 hooks: @@ -52,7 +47,7 @@ repos: language: system types_or: [c, c++] - id: signed-off-by - name: Check commit is signed off + name: check commit is signed off entry: "bash -c 'grep -q \"^Signed-off-by: \" \"$1\"' --" language: system stages: [commit-msg] From d676849e7d1fd1729e4e3ccba87c9bfe90bc3139 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 16:57:18 -0800 Subject: [PATCH 16/33] Update md Signed-off-by: Blake McHale --- Contributing.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 10ff885..bc91ce6 100644 --- a/Contributing.md +++ b/Contributing.md @@ -64,7 +64,8 @@ If you would like the linter and other checks to run on every commit use [pre-co ``` sudo apt install uncrustify pipx pipx install pre-commit -pre-commit run --all-files # will also run on every commit now +pre-commit install --hook-type pre-commit --hook-type commit-msg +pre-commit run --all-files # try it out, this will run every commit now ``` Every commit now a series of checks will be run to ensure the changes are meeting this repositories requirements. From e819d0774bdcfa8da03c3187a53534b0b42a811a Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 17:00:54 -0800 Subject: [PATCH 17/33] Use proper working directory Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 5804f15..e152a1e 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -63,6 +63,7 @@ jobs: source ~/.bashrc pre-commit run --all-files --show-diff-on-failure shell: bash + working-directory: greenwave_monitor env: DEBIAN_FRONTEND: noninteractive From 2f4721db779bcadea6cbc62a798ab67456f4b172 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 17:04:25 -0800 Subject: [PATCH 18/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index e152a1e..d7198bd 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -58,6 +58,7 @@ jobs: - name: Run pre-commit hooks (linting, sign-off check, copyright check, etc.) run: | apt update && apt install -y pipx git + git config --global --add safe.directory "$GITHUB_WORKSPACE/greenwave_monitor" pipx install pre-commit pipx ensurepath source ~/.bashrc From c85bec48721a9804c1594a65a24e45e7bcf9fdb5 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 17:05:45 -0800 Subject: [PATCH 19/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index d7198bd..3558011 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -62,6 +62,7 @@ jobs: pipx install pre-commit pipx ensurepath source ~/.bashrc + pwd pre-commit run --all-files --show-diff-on-failure shell: bash working-directory: greenwave_monitor From fb8bd6cee6b512397171515a8027d8d0028ef6b4 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 17:11:31 -0800 Subject: [PATCH 20/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 3558011..793fb2f 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -27,6 +27,24 @@ env: ROS_AUTOMATIC_DISCOVERY_RANGE: LOCALHOST jobs: + pre-commit: + name: Pre-commit checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pre-commit/action@v3.0.1 + - name: Check commits are signed off + uses: gsactions/commit-message-checker@v2 + with: + pattern: 'Signed-off-by: .+' + error: 'All commits must include a Signed-off-by line. Use "git commit -s" to sign off.' + excludeDescription: true + excludeTitle: true + checkAllCommitMessages: true + accessToken: ${{ secrets.GITHUB_TOKEN }} + test: name: Test ROS2 ${{ matrix.ros_distro }} runs-on: ubuntu-latest @@ -52,23 +70,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # fetch all history to run pre-commit copyright check hooks path: greenwave_monitor - - name: Run pre-commit hooks (linting, sign-off check, copyright check, etc.) - run: | - apt update && apt install -y pipx git - git config --global --add safe.directory "$GITHUB_WORKSPACE/greenwave_monitor" - pipx install pre-commit - pipx ensurepath - source ~/.bashrc - pwd - pre-commit run --all-files --show-diff-on-failure - shell: bash - working-directory: greenwave_monitor - env: - DEBIAN_FRONTEND: noninteractive - - name: Clone r2s_gw repository run: | # Install git if not already available From 0b771b3797266f4d5ab25b5295a95137796d7fab Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 17:12:36 -0800 Subject: [PATCH 21/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 793fb2f..b5de919 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -35,15 +35,6 @@ jobs: with: fetch-depth: 0 - uses: pre-commit/action@v3.0.1 - - name: Check commits are signed off - uses: gsactions/commit-message-checker@v2 - with: - pattern: 'Signed-off-by: .+' - error: 'All commits must include a Signed-off-by line. Use "git commit -s" to sign off.' - excludeDescription: true - excludeTitle: true - checkAllCommitMessages: true - accessToken: ${{ secrets.GITHUB_TOKEN }} test: name: Test ROS2 ${{ matrix.ros_distro }} From 444730bf7ab3abf3e1bf7c23ac06dfa054f17077 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 17:14:39 -0800 Subject: [PATCH 22/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index b5de919..775ecdf 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -34,6 +34,8 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y uncrustify git - uses: pre-commit/action@v3.0.1 test: From 1cfb152a09025d3dd6e0dafaec0a45f07f022393 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 17:15:21 -0800 Subject: [PATCH 23/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 775ecdf..27a1287 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -28,7 +28,7 @@ env: jobs: pre-commit: - name: Pre-commit checks + name: Pre-commit checks (linting, copyright check, etc.) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 37f15a0c3e2dc555f2be2111728c564c2aecb77d Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 17:17:45 -0800 Subject: [PATCH 24/33] Test CI Signed-off-by: Blake McHale --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index bc91ce6..753a1f5 100644 --- a/Contributing.md +++ b/Contributing.md @@ -68,4 +68,4 @@ pre-commit install --hook-type pre-commit --hook-type commit-msg pre-commit run --all-files # try it out, this will run every commit now ``` -Every commit now a series of checks will be run to ensure the changes are meeting this repositories requirements. +On every commit now a series of checks will be run to ensure the changes are meeting this repositories requirements. From 38425d725f23cf76ed97a95ac9b6b0820f9666cf Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 18:43:47 -0800 Subject: [PATCH 25/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 27a1287..f0cb465 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -132,8 +132,8 @@ jobs: source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash set -e - timeout 10s bash -lc "script -qfec 'ros2 run greenwave_monitor ncurses_dashboard' /dev/null <<< \$'q'" - timeout 10s bash -lc "script -qfec 'ros2 run r2s_gw r2s_gw_dashboard' /dev/null <<< \$'q'" + timeout 10s bash -c "source install/setup.bash && script -qfec 'ros2 run greenwave_monitor ncurses_dashboard' /dev/null <<< \$'q'" + timeout 10s bash -c "source install/setup.bash && script -qfec 'ros2 run r2s_gw r2s_gw_dashboard' /dev/null <<< \$'q'" timeout 5s ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' shell: bash From 2fac9da2d7e61ecf9197b526560fc2203067a89a Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 18:48:36 -0800 Subject: [PATCH 26/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index f0cb465..7ef3569 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -132,8 +132,8 @@ jobs: source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash set -e - timeout 10s bash -c "source install/setup.bash && script -qfec 'ros2 run greenwave_monitor ncurses_dashboard' /dev/null <<< \$'q'" - timeout 10s bash -c "source install/setup.bash && script -qfec 'ros2 run r2s_gw r2s_gw_dashboard' /dev/null <<< \$'q'" + timeout 10s bash -c "source /opt/ros/${{ matrix.ros_distro }}/setup.bash && source install/setup.bash && script -qfec 'ros2 run greenwave_monitor ncurses_dashboard' /dev/null <<< \$'q'" + timeout 10s bash -c "source /opt/ros/${{ matrix.ros_distro }}/setup.bash && source install/setup.bash && script -qfec 'ros2 run r2s_gw r2s_gw_dashboard' /dev/null <<< \$'q'" timeout 5s ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' shell: bash From 1d0122059b21a525f7ce33b2c72520d65904503a Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 18:53:29 -0800 Subject: [PATCH 27/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 7ef3569..71c7496 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -132,8 +132,8 @@ jobs: source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash set -e - timeout 10s bash -c "source /opt/ros/${{ matrix.ros_distro }}/setup.bash && source install/setup.bash && script -qfec 'ros2 run greenwave_monitor ncurses_dashboard' /dev/null <<< \$'q'" - timeout 10s bash -c "source /opt/ros/${{ matrix.ros_distro }}/setup.bash && source install/setup.bash && script -qfec 'ros2 run r2s_gw r2s_gw_dashboard' /dev/null <<< \$'q'" + timeout 10s script -qfec "bash -c 'source /opt/ros/${{ matrix.ros_distro }}/setup.bash && source install/setup.bash && ros2 run greenwave_monitor ncurses_dashboard'" /dev/null <<< $'q' + timeout 10s script -qfec "bash -c 'source /opt/ros/${{ matrix.ros_distro }}/setup.bash && source install/setup.bash && ros2 run r2s_gw r2s_gw_dashboard'" /dev/null <<< $'q' timeout 5s ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' shell: bash From 8714eb08840eaabff830db925cdb86a54ed811c9 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 18:57:24 -0800 Subject: [PATCH 28/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 71c7496..ceb2670 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -132,8 +132,9 @@ jobs: source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash set -e - timeout 10s script -qfec "bash -c 'source /opt/ros/${{ matrix.ros_distro }}/setup.bash && source install/setup.bash && ros2 run greenwave_monitor ncurses_dashboard'" /dev/null <<< $'q' - timeout 10s script -qfec "bash -c 'source /opt/ros/${{ matrix.ros_distro }}/setup.bash && source install/setup.bash && ros2 run r2s_gw r2s_gw_dashboard'" /dev/null <<< $'q' + export -p > /tmp/ros_env.sh + timeout 10s script -qfec "/bin/bash -c 'source /tmp/ros_env.sh && ros2 run greenwave_monitor ncurses_dashboard'" /dev/null <<< $'q' + timeout 10s script -qfec "/bin/bash -c 'source /tmp/ros_env.sh && ros2 run r2s_gw r2s_gw_dashboard'" /dev/null <<< $'q' timeout 5s ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' shell: bash From 387403b7bda32bb0e2d300a0cbcf440f4e35450b Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 19:04:22 -0800 Subject: [PATCH 29/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index ceb2670..bc392b7 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -131,6 +131,7 @@ jobs: run: | source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash + ros2 --help set -e export -p > /tmp/ros_env.sh timeout 10s script -qfec "/bin/bash -c 'source /tmp/ros_env.sh && ros2 run greenwave_monitor ncurses_dashboard'" /dev/null <<< $'q' From fd0a0aa4bcd47cc3ffd20c5befdaef2c941c97a8 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 19:09:45 -0800 Subject: [PATCH 30/33] Test CI Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index bc392b7..df689fa 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -94,7 +94,7 @@ jobs: curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" > /etc/apt/sources.list.d/ros2.list apt-get update -qq - apt-get install -y python3-rosdep python3-colcon-common-extensions build-essential cmake git python3-pip + apt-get install -y python3-rosdep python3-colcon-common-extensions build-essential cmake git python3-pip ros-${{ matrix.ros_distro }}-ros-base shell: bash - name: Initialize rosdep and install dependencies @@ -131,11 +131,13 @@ jobs: run: | source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash - ros2 --help set -e - export -p > /tmp/ros_env.sh - timeout 10s script -qfec "/bin/bash -c 'source /tmp/ros_env.sh && ros2 run greenwave_monitor ncurses_dashboard'" /dev/null <<< $'q' - timeout 10s script -qfec "/bin/bash -c 'source /tmp/ros_env.sh && ros2 run r2s_gw r2s_gw_dashboard'" /dev/null <<< $'q' + # Create wrapper scripts with absolute paths baked in + printf '#!/bin/bash\nsource /opt/ros/${{ matrix.ros_distro }}/setup.bash\nsource %s/install/setup.bash\nros2 run greenwave_monitor ncurses_dashboard\n' "$(pwd)" > /tmp/run_gw_dashboard.sh + printf '#!/bin/bash\nsource /opt/ros/${{ matrix.ros_distro }}/setup.bash\nsource %s/install/setup.bash\nros2 run r2s_gw r2s_gw_dashboard\n' "$(pwd)" > /tmp/run_r2s_dashboard.sh + chmod +x /tmp/run_gw_dashboard.sh /tmp/run_r2s_dashboard.sh + timeout 10s script -qfec /tmp/run_gw_dashboard.sh /dev/null <<< $'q' + timeout 10s script -qfec /tmp/run_r2s_dashboard.sh /dev/null <<< $'q' timeout 5s ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' shell: bash From 7c23de990e1864af7a9e0fe819e03d561ee00e8c Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 19:39:02 -0800 Subject: [PATCH 31/33] Don't fix smoke tests Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index df689fa..d93e7c7 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -132,13 +132,9 @@ jobs: source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash set -e - # Create wrapper scripts with absolute paths baked in - printf '#!/bin/bash\nsource /opt/ros/${{ matrix.ros_distro }}/setup.bash\nsource %s/install/setup.bash\nros2 run greenwave_monitor ncurses_dashboard\n' "$(pwd)" > /tmp/run_gw_dashboard.sh - printf '#!/bin/bash\nsource /opt/ros/${{ matrix.ros_distro }}/setup.bash\nsource %s/install/setup.bash\nros2 run r2s_gw r2s_gw_dashboard\n' "$(pwd)" > /tmp/run_r2s_dashboard.sh - chmod +x /tmp/run_gw_dashboard.sh /tmp/run_r2s_dashboard.sh - timeout 10s script -qfec /tmp/run_gw_dashboard.sh /dev/null <<< $'q' - timeout 10s script -qfec /tmp/run_r2s_dashboard.sh /dev/null <<< $'q' - timeout 5s ros2 launch greenwave_monitor hz.launch.py gw_monitored_topics:='["/topic1", "/topic2"]' + timeout 10s bash -lc "script -qfec 'ros2 run greenwave_monitor ncurses_dashboard' /dev/null <<< \$'q'" || true + timeout 10s bash -lc "script -qfec 'ros2 run r2s_gw r2s_gw_dashboard' /dev/null <<< \$'q'" || true + timeout 5s ros2 launch greenwave_monitor hz.launch.py topics:='["/topic1"]' || true shell: bash - name: Run tests From 9841804a09b55ade0d51bfed8d3f80fca80599c3 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 19:43:13 -0800 Subject: [PATCH 32/33] Add uninstall for pre-commit command Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 2 +- Contributing.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index d93e7c7..1ef1b07 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/Contributing.md b/Contributing.md index 753a1f5..fb9f42f 100644 --- a/Contributing.md +++ b/Contributing.md @@ -69,3 +69,9 @@ pre-commit run --all-files # try it out, this will run every commit now ``` On every commit now a series of checks will be run to ensure the changes are meeting this repositories requirements. + +Uninstall pre-commit with: + +``` +pre-commit uninstall --hook-type pre-commit --hook-type commit-msg +``` From f2c75c40a824eaf85367eed3bdd511cabc696a2b Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Tue, 27 Jan 2026 20:14:00 -0800 Subject: [PATCH 33/33] Remove ros base Signed-off-by: Blake McHale --- .github/workflows/ros-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ros-tests.yml b/.github/workflows/ros-tests.yml index 1ef1b07..ed11dcd 100644 --- a/.github/workflows/ros-tests.yml +++ b/.github/workflows/ros-tests.yml @@ -94,7 +94,7 @@ jobs: curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" > /etc/apt/sources.list.d/ros2.list apt-get update -qq - apt-get install -y python3-rosdep python3-colcon-common-extensions build-essential cmake git python3-pip ros-${{ matrix.ros_distro }}-ros-base + apt-get install -y python3-rosdep python3-colcon-common-extensions build-essential cmake git python3-pip shell: bash - name: Initialize rosdep and install dependencies