Skip to content

Commit 984432a

Browse files
authored
Add --hide-unmonitored flag to ncurses frontend (#20)
Add a --hide-unmonitored flag to the ncurses frontend. This makes it possible for the user to start greenwave monitor with only the topics that are being monitored visible. Tests were also added to verify the argument parsing is functional with the frontend. Signed-off-by: Blake McHale <bmchale@nvidia.com>
1 parent 0f35afb commit 984432a

File tree

4 files changed

+97
-14
lines changed

4 files changed

+97
-14
lines changed

greenwave_monitor/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ if(BUILD_TESTING)
110110
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
111111
)
112112

113+
# Add ncurses frontend tests
114+
ament_add_pytest_test(test_ncurses_frontend_argparse test/test_ncurses_frontend_argparse.py
115+
TIMEOUT 120
116+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
117+
)
118+
113119
# Add gtests
114120
ament_add_gtest(test_message_diagnostics test/test_message_diagnostics.cpp
115121
TIMEOUT 60

greenwave_monitor/greenwave_monitor/ncurses_frontend.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
with real-time diagnostics including publication rates, latency, and status.
2525
"""
2626

27+
import argparse
2728
import curses
2829
import signal
2930
import threading
@@ -40,7 +41,7 @@
4041
class GreenwaveNcursesFrontend(Node):
4142
"""Ncurses frontend for Greenwave Monitor."""
4243

43-
def __init__(self):
44+
def __init__(self, hide_unmonitored: bool = False):
4445
"""Initialize the ncurses frontend node."""
4546
super().__init__('greenwave_ncurses_frontend')
4647

@@ -59,7 +60,7 @@ def __init__(self):
5960
self.input_buffer = ''
6061
self.status_message = ''
6162
self.status_timeout = 0
62-
self.show_only_monitored = False
63+
self.hide_unmonitored = hide_unmonitored
6364

6465
# Initialize UI adaptor
6566
self.ui_adaptor = GreenwaveUiAdaptor(self)
@@ -98,7 +99,7 @@ def update_visible_topics(self):
9899
"""Update the visible topics list based on current filters."""
99100
all_topic_names = list(self.all_topics)
100101

101-
if self.show_only_monitored and self.ui_adaptor:
102+
if self.hide_unmonitored and self.ui_adaptor:
102103
# Filter to only show topics that have diagnostic data (are being monitored)
103104
filtered_topics = []
104105
for topic_name in all_topic_names:
@@ -289,10 +290,10 @@ def curses_main(stdscr, node):
289290
status_message = f'Error: {msg}'
290291
status_timeout = current_time + 3.0
291292
elif key == ord('h') or key == ord('H'):
292-
node.show_only_monitored = not node.show_only_monitored
293+
node.hide_unmonitored = not node.hide_unmonitored
293294
with node.topics_lock:
294295
node.update_visible_topics()
295-
mode_text = 'monitored only' if node.show_only_monitored else 'all topics'
296+
mode_text = 'monitored only' if node.hide_unmonitored else 'all topics'
296297
status_message = f'Showing {mode_text}'
297298
status_timeout = current_time + 3.0
298299

@@ -434,11 +435,16 @@ def curses_main(stdscr, node):
434435
status_line = ("Format: Hz [tolerance%] - Examples: '30' (30Hz±5% default) "
435436
"or '30 10' (30Hz±10%) - ESC=cancel, Enter=confirm")
436437
else:
437-
mode_text = 'monitored only' if node.show_only_monitored else 'all topics'
438+
if node.hide_unmonitored:
439+
mode_text = 'monitored only'
440+
mode_help_text = 'show unmonitored'
441+
else:
442+
mode_text = 'all topics'
443+
mode_help_text = 'hide unmonitored'
438444
status_line = (
439445
f'Showing {start_idx + 1} - {num_shown} of {len(visible_topics)} '
440446
f'topics ({mode_text}). Enter=toggle, f=set freq, c=clear freq, '
441-
f'h=hide unmonitored, q=quit')
447+
f'h={mode_help_text}, q=quit')
442448

443449
try:
444450
stdscr.addstr(height - 2, 0, status_line[:width - 1])
@@ -448,10 +454,24 @@ def curses_main(stdscr, node):
448454
stdscr.refresh()
449455

450456

457+
def parse_args(args=None):
458+
"""Parse command-line arguments."""
459+
parser = argparse.ArgumentParser(
460+
description='Ncurses-based frontend for Greenwave Monitor'
461+
)
462+
parser.add_argument(
463+
'--hide-unmonitored',
464+
action='store_true',
465+
help='Hide unmonitored topics on initialization'
466+
)
467+
return parser.parse_known_args(args)
468+
469+
451470
def main(args=None):
452471
"""Entry point for the ncurses frontend application."""
453-
rclpy.init(args=args)
454-
node = GreenwaveNcursesFrontend()
472+
parsed_args, ros_args = parse_args(args)
473+
rclpy.init(args=ros_args)
474+
node = GreenwaveNcursesFrontend(hide_unmonitored=parsed_args.hide_unmonitored)
455475
thread = None
456476

457477
def signal_handler(signum, frame):

greenwave_monitor/scripts/ncurses_dashboard

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
# Parse command line arguments
2424
DEMO_MODE=false
2525
LOG_DIR=""
26+
HIDE_UNMONITORED=false
2627
MONITOR_ARGS=()
2728

2829
show_help() {
@@ -33,15 +34,16 @@ show_help() {
3334
echo "OPTIONS:"
3435
echo " --demo, --test Launch demo publisher nodes for testing"
3536
echo " --log-dir DIR Enable logging to specified directory"
37+
echo " --hide-unmonitored Hide unmonitored topics on initialization"
3638
echo " --help, -h Show this help message"
3739
echo ""
3840
echo "MONITOR_ARGS are passed directly to the greenwave_monitor node"
3941
echo ""
4042
echo "Controls in ncurses interface:"
41-
echo " a = Add topic to monitoring"
42-
echo " r = Remove selected topic"
43-
echo " f = Set expected frequency (format: topic_name hz tolerance)"
43+
echo " enter/space = toggle topic monitoring"
44+
echo " f = Set expected frequency for selected topic (format: hz tolerance%)"
4445
echo " c = Clear frequency settings for selected topic"
46+
echo " h = Toggle hiding unmonitored topics"
4547
echo " ↑/↓ = Navigate topics"
4648
echo " q = Quit"
4749
}
@@ -56,6 +58,10 @@ while [[ $# -gt 0 ]]; do
5658
LOG_DIR="$2"
5759
shift 2
5860
;;
61+
--hide-unmonitored)
62+
HIDE_UNMONITORED=true
63+
shift
64+
;;
5965
--help|-h)
6066
show_help
6167
exit 0
@@ -115,7 +121,12 @@ echo "Monitor process started with PID: $MONITOR_PID"
115121
# Launch ncurses frontend in the foreground
116122
echo "Starting ncurses TUI..."
117123
echo "Controls: a=Add Topic, r=Remove, f=Set Frequency, c=Clear Freq, q=Quit"
118-
python3 -m greenwave_monitor.ncurses_frontend
124+
# NOTE: add proper argument parsing to the ncurses frontend if more than one argument is added here
125+
FRONTEND_ARGS=()
126+
if [ "$HIDE_UNMONITORED" = "true" ]; then
127+
FRONTEND_ARGS+=("--hide-unmonitored")
128+
fi
129+
python3 -m greenwave_monitor.ncurses_frontend "${FRONTEND_ARGS[@]}"
119130

120131
# Note: We don't need to explicitly exit here because the trap will handle cleanup
121-
# when the ncurses frontend exits
132+
# when the ncurses frontend exits
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env python3
2+
3+
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
4+
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# SPDX-License-Identifier: Apache-2.0
19+
20+
"""Tests for ncurses frontend argument parsing."""
21+
22+
from greenwave_monitor.ncurses_frontend import parse_args
23+
24+
25+
class TestParseArgs:
26+
"""Test argument parsing for ncurses frontend."""
27+
28+
def test_default_hide_unmonitored_false(self):
29+
"""Test that hide_unmonitored defaults to False."""
30+
parsed_args, _ = parse_args([])
31+
assert parsed_args.hide_unmonitored is False
32+
33+
def test_hide_unmonitored_long_flag(self):
34+
"""Test --hide-unmonitored flag enables hide_unmonitored."""
35+
parsed_args, _ = parse_args(['--hide-unmonitored'])
36+
assert parsed_args.hide_unmonitored is True
37+
38+
def test_ros_args_passthrough(self):
39+
"""Test that ROS arguments are passed through."""
40+
parsed_args, ros_args = parse_args(
41+
['--hide-unmonitored', '--ros-args', '-r', '__node:=my_node']
42+
)
43+
assert parsed_args.hide_unmonitored is True
44+
assert '--ros-args' in ros_args
45+
assert '-r' in ros_args
46+
assert '__node:=my_node' in ros_args

0 commit comments

Comments
 (0)