From eaffcb044f40aeafaf820c6bf2494b7ce5669e3d Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Fri, 27 Feb 2026 10:48:47 -0800 Subject: [PATCH 1/4] Fix ncurses frontend Signed-off-by: Blake McHale --- .../greenwave_monitor/ncurses_frontend.py | 24 +++++++----- .../greenwave_monitor/ui_adaptor.py | 38 ++++++++++--------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index 0f3359f..7c6cf3d 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -113,13 +113,11 @@ def update_visible_topics(self): self.visible_topics.sort() - def toggle_topic_monitoring(self, topic_name: str): + def toggle_topic_monitoring(self, topic_name: str) -> tuple[bool, str]: """Toggle monitoring for a topic.""" if self.ui_adaptor: - self.ui_adaptor.toggle_topic_monitoring(topic_name) - self.show_status(f'Toggling monitoring for {topic_name}') - else: - self.show_status('UI adaptor not available') + return self.ui_adaptor.toggle_topic_monitoring(topic_name) + return False, 'UI adaptor not available' def show_status(self, message: str): """Show a status message for 3 seconds.""" @@ -141,6 +139,7 @@ def curses_main(stdscr, node): last_redraw = 0 status_message = '' status_timeout = 0 + status_is_error = False input_mode = None input_buffer = '' @@ -231,12 +230,15 @@ def curses_main(stdscr, node): success, msg = node.ui_adaptor.set_expected_frequency( topic_name, hz, tolerance) status_message = f'Set frequency for {topic_name}: {hz}Hz' + status_is_error = not success if not success: status_message = f'Error: {msg}' else: status_message = 'Invalid input format' + status_is_error = True except ValueError: status_message = 'Invalid frequency values' + status_is_error = True status_timeout = current_time + 3.0 input_mode = None input_buffer = '' @@ -273,8 +275,9 @@ def curses_main(stdscr, node): elif key == ord('\n') or key == ord(' '): if 0 <= selected_row < len(node.visible_topics): topic_name = node.visible_topics[selected_row] - node.toggle_topic_monitoring(topic_name) - status_message = f'Toggled monitoring for {topic_name}' + success, msg = node.toggle_topic_monitoring(topic_name) + status_message = msg if not success else f'Toggled monitoring for {topic_name}' + status_is_error = not success status_timeout = current_time + 3.0 elif key == ord('f') or key == ord('F'): if 0 <= selected_row < len(node.visible_topics): @@ -286,6 +289,7 @@ def curses_main(stdscr, node): success, msg = node.ui_adaptor.set_expected_frequency( topic_name, clear=True) status_message = f'Cleared frequency for {topic_name}' + status_is_error = not success if not success: status_message = f'Error: {msg}' status_timeout = current_time + 3.0 @@ -295,6 +299,7 @@ def curses_main(stdscr, node): node.update_visible_topics() mode_text = 'monitored only' if node.hide_unmonitored else 'all topics' status_message = f'Showing {mode_text}' + status_is_error = False status_timeout = current_time + 3.0 # Get data safely @@ -400,8 +405,9 @@ def curses_main(stdscr, node): # Status message if current_time < status_timeout: try: - stdscr.addstr(height - 3, 0, status_message[:width-1], - curses.color_pair(COLOR_STATUS_MSG)) + color = curses.color_pair(COLOR_ERROR) if status_is_error \ + else curses.color_pair(COLOR_STATUS_MSG) + stdscr.addstr(height - 3, 0, status_message[:width-1], color) except curses.error: pass diff --git a/greenwave_monitor/greenwave_monitor/ui_adaptor.py b/greenwave_monitor/greenwave_monitor/ui_adaptor.py index 015962b..c3a6df8 100644 --- a/greenwave_monitor/greenwave_monitor/ui_adaptor.py +++ b/greenwave_monitor/greenwave_monitor/ui_adaptor.py @@ -184,10 +184,10 @@ def _on_diagnostics(self, msg: DiagnosticArray): # Skip updating expected_frequencies if values aren't numeric self.expected_frequencies.pop(topic_name, None) - def toggle_topic_monitoring(self, topic_name: str): + def toggle_topic_monitoring(self, topic_name: str) -> tuple[bool, str]: """Toggle monitoring for a topic.""" if not self.manage_topic_client.wait_for_service(timeout_sec=1.0): - return + return False, 'Could not connect to manage_topic service.' request = ManageTopic.Request() request.topic_name = topic_name @@ -195,34 +195,36 @@ def toggle_topic_monitoring(self, topic_name: str): with self.data_lock: request.add_topic = topic_name not in self.ui_diagnostics + action = 'start' if request.add_topic else 'stop' + 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) if future.result() is None: - action = 'start' if request.add_topic else 'stop' - self.node.get_logger().error( - f'Failed to {action} monitoring: Service call timed out') - return + error_msg = f'Failed to {action} monitoring: Service call timed out' + self.node.get_logger().debug(error_msg) + return False, error_msg response = future.result() - with self.data_lock: - if not response.success: - action = 'start' if request.add_topic else 'stop' - self.node.get_logger().error( - f'Failed to {action} monitoring: {response.message}') - return + if not response.success: + error_msg = f'Failed to {action} monitoring: {response.message}' + self.node.get_logger().debug(error_msg) + return False, error_msg + with self.data_lock: if not request.add_topic and topic_name in self.ui_diagnostics: del self.ui_diagnostics[topic_name] if topic_name in self.expected_frequencies: del self.expected_frequencies[topic_name] + return True, f'Successfully {"started" if request.add_topic else "stopped"} monitoring' + except Exception as e: - action = 'start' if request.add_topic else 'stop' - self.node.get_logger().error(f'Failed to {action} monitoring: {e}') + error_msg = f'Failed to {action} monitoring: {e}' + self.node.get_logger().debug(error_msg) + return False, error_msg def set_expected_frequency(self, topic_name: str, @@ -249,14 +251,14 @@ def set_expected_frequency(self, if future.result() is None: action = 'clear' if clear else 'set' error_msg = f'Failed to {action} expected frequency: Service call timed out' - self.node.get_logger().error(error_msg) + self.node.get_logger().debug(error_msg) return False, error_msg response = future.result() if not response.success: action = 'clear' if clear else 'set' - self.node.get_logger().error( + self.node.get_logger().debug( f'Failed to {action} expected frequency: {response.message}') return False, response.message else: @@ -269,7 +271,7 @@ def set_expected_frequency(self, except Exception as e: action = 'clear' if clear else 'set' error_msg = f'Failed to {action} expected frequency: {e}' - self.node.get_logger().error(error_msg) + self.node.get_logger().debug(error_msg) return False, error_msg def get_topic_diagnostics(self, topic_name: str) -> UiDiagnosticData: From 4e4e5079530f78cc457b60c08df0eed458e31be3 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Fri, 27 Feb 2026 11:45:34 -0800 Subject: [PATCH 2/4] Silence stdout for ROS logs in ncurses Signed-off-by: Blake McHale --- .../greenwave_monitor/ncurses_frontend.py | 1 + greenwave_monitor/greenwave_monitor/ui_adaptor.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index 7c6cf3d..d5615e5 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -476,6 +476,7 @@ def parse_args(args=None): def main(args=None): """Entry point for the ncurses frontend application.""" parsed_args, ros_args = parse_args(args) + ros_args.extend(['--ros-args', '--disable-stdout-logs']) rclpy.init(args=ros_args) node = GreenwaveNcursesFrontend(hide_unmonitored=parsed_args.hide_unmonitored) thread = None diff --git a/greenwave_monitor/greenwave_monitor/ui_adaptor.py b/greenwave_monitor/greenwave_monitor/ui_adaptor.py index c3a6df8..3e0b822 100644 --- a/greenwave_monitor/greenwave_monitor/ui_adaptor.py +++ b/greenwave_monitor/greenwave_monitor/ui_adaptor.py @@ -203,14 +203,14 @@ def toggle_topic_monitoring(self, topic_name: str) -> tuple[bool, str]: if future.result() is None: error_msg = f'Failed to {action} monitoring: Service call timed out' - self.node.get_logger().debug(error_msg) + self.node.get_logger().error(error_msg) return False, error_msg response = future.result() if not response.success: error_msg = f'Failed to {action} monitoring: {response.message}' - self.node.get_logger().debug(error_msg) + self.node.get_logger().error(error_msg) return False, error_msg with self.data_lock: @@ -223,7 +223,7 @@ def toggle_topic_monitoring(self, topic_name: str) -> tuple[bool, str]: except Exception as e: error_msg = f'Failed to {action} monitoring: {e}' - self.node.get_logger().debug(error_msg) + self.node.get_logger().error(error_msg) return False, error_msg def set_expected_frequency(self, @@ -251,14 +251,14 @@ def set_expected_frequency(self, if future.result() is None: action = 'clear' if clear else 'set' error_msg = f'Failed to {action} expected frequency: Service call timed out' - self.node.get_logger().debug(error_msg) + self.node.get_logger().error(error_msg) return False, error_msg response = future.result() if not response.success: action = 'clear' if clear else 'set' - self.node.get_logger().debug( + self.node.get_logger().error( f'Failed to {action} expected frequency: {response.message}') return False, response.message else: @@ -271,7 +271,7 @@ def set_expected_frequency(self, except Exception as e: action = 'clear' if clear else 'set' error_msg = f'Failed to {action} expected frequency: {e}' - self.node.get_logger().debug(error_msg) + self.node.get_logger().error(error_msg) return False, error_msg def get_topic_diagnostics(self, topic_name: str) -> UiDiagnosticData: From 3fa661f4c67c6adec8ca9794ceaa5601aaf0c4a3 Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Fri, 27 Feb 2026 11:51:07 -0800 Subject: [PATCH 3/4] Fix display after setting expected freq Signed-off-by: Blake McHale --- greenwave_monitor/greenwave_monitor/ncurses_frontend.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py index d5615e5..b51295c 100644 --- a/greenwave_monitor/greenwave_monitor/ncurses_frontend.py +++ b/greenwave_monitor/greenwave_monitor/ncurses_frontend.py @@ -283,6 +283,8 @@ def curses_main(stdscr, node): if 0 <= selected_row < len(node.visible_topics): input_mode = 'frequency' input_buffer = '' + status_timeout = 0 + status_is_error = False elif key == ord('c') or key == ord('C'): if 0 <= selected_row < len(node.visible_topics): topic_name = node.visible_topics[selected_row] From fe896e874cee36fa92511c328f44805e14b4524b Mon Sep 17 00:00:00 2001 From: Blake McHale Date: Fri, 27 Feb 2026 13:16:51 -0800 Subject: [PATCH 4/4] retrigger checks Signed-off-by: Blake McHale