1+ #!/usr/bin/env python3
2+ """
3+ Coffee Voice Agent Monitor - RQT Plugin for Real-time Monitoring
4+
5+ This RQT plugin provides a comprehensive dashboard for monitoring the Coffee Voice Agent
6+ system including agent status, emotions, tool usage, conversation flow, and analytics.
7+ """
8+
9+ import os
10+ import rclpy
11+ from rclpy .node import Node
12+ from rclpy .executors import MultiThreadedExecutor
13+ import threading
14+ from datetime import datetime
15+
16+ from rqt_gui_py .plugin import Plugin
17+ from python_qt_binding import loadUi
18+ from python_qt_binding .QtWidgets import QWidget , QVBoxLayout , QHBoxLayout , QGridLayout
19+ from python_qt_binding .QtCore import Qt , QTimer , pyqtSignal , pyqtSlot
20+
21+ # ROS2 Messages
22+ from std_msgs .msg import String , Bool
23+ from coffee_voice_agent_msgs .msg import AgentStatus , ToolEvent
24+
25+ # Import custom widgets
26+ from .widgets .agent_status_widget import AgentStatusWidget
27+ from .widgets .emotion_display_widget import EmotionDisplayWidget
28+ from .widgets .conversation_widget import ConversationWidget
29+ from .widgets .tool_monitor_widget import ToolMonitorWidget
30+ from .widgets .analytics_widget import AnalyticsWidget
31+ from .widgets .controls_widget import ControlsWidget
32+
33+
34+ class VoiceAgentMonitorNode (Node ):
35+ """ROS2 Node for handling voice agent monitoring subscriptions"""
36+
37+ # PyQt signals for thread-safe UI updates
38+ agent_status_received = pyqtSignal (AgentStatus )
39+ tool_event_received = pyqtSignal (ToolEvent )
40+ user_speech_received = pyqtSignal (str )
41+ connection_status_received = pyqtSignal (bool )
42+
43+ def __init__ (self ):
44+ super ().__init__ ('voice_agent_monitor_node' )
45+
46+ # Topic subscriptions
47+ self .status_sub = self .create_subscription (
48+ AgentStatus ,
49+ 'voice_agent/status' ,
50+ self .status_callback ,
51+ 10
52+ )
53+
54+ self .tool_sub = self .create_subscription (
55+ ToolEvent ,
56+ 'voice_agent/tool_events' ,
57+ self .tool_callback ,
58+ 10
59+ )
60+
61+ self .speech_sub = self .create_subscription (
62+ String ,
63+ 'voice_agent/user_speech' ,
64+ self .speech_callback ,
65+ 10
66+ )
67+
68+ self .connection_sub = self .create_subscription (
69+ Bool ,
70+ 'voice_agent/connected' ,
71+ self .connection_callback ,
72+ 10
73+ )
74+
75+ # Publishers for sending commands
76+ self .virtual_request_pub = self .create_publisher (
77+ String ,
78+ 'voice_agent/virtual_requests' ,
79+ 10
80+ )
81+
82+ self .command_pub = self .create_publisher (
83+ String ,
84+ 'voice_agent/commands' ,
85+ 10
86+ )
87+
88+ self .get_logger ().info ("Voice Agent Monitor Node initialized" )
89+
90+ def status_callback (self , msg ):
91+ """Handle AgentStatus messages"""
92+ self .agent_status_received .emit (msg )
93+
94+ def tool_callback (self , msg ):
95+ """Handle ToolEvent messages"""
96+ self .tool_event_received .emit (msg )
97+
98+ def speech_callback (self , msg ):
99+ """Handle user speech messages"""
100+ self .user_speech_received .emit (msg .data )
101+
102+ def connection_callback (self , msg ):
103+ """Handle connection status messages"""
104+ self .connection_status_received .emit (msg .data )
105+
106+
107+ class VoiceAgentMonitor (Plugin ):
108+ """RQT Plugin for Coffee Voice Agent Monitoring Dashboard"""
109+
110+ def __init__ (self , context ):
111+ super (VoiceAgentMonitor , self ).__init__ (context )
112+
113+ # Give QObjects reasonable names
114+ self .setObjectName ('VoiceAgentMonitor' )
115+
116+ # Create main widget
117+ self ._widget = QWidget ()
118+
119+ # Set up the UI
120+ self ._setup_ui ()
121+
122+ # Initialize ROS2 node
123+ self ._init_ros ()
124+
125+ # Add widget to the user interface
126+ context .add_widget (self ._widget )
127+
128+ # Start update timer
129+ self ._setup_timers ()
130+
131+ def _setup_ui (self ):
132+ """Set up the main UI layout"""
133+ # Main layout - 3x3 grid for dashboard panels
134+ main_layout = QGridLayout ()
135+ self ._widget .setLayout (main_layout )
136+
137+ # Create dashboard widgets
138+ self .agent_status_widget = AgentStatusWidget ()
139+ self .emotion_widget = EmotionDisplayWidget ()
140+ self .conversation_widget = ConversationWidget ()
141+ self .tool_monitor_widget = ToolMonitorWidget ()
142+ self .analytics_widget = AnalyticsWidget ()
143+ self .controls_widget = ControlsWidget ()
144+
145+ # Arrange widgets in dashboard layout
146+ # Row 0: Agent Status | Conversation Flow | Analytics
147+ main_layout .addWidget (self .agent_status_widget , 0 , 0 )
148+ main_layout .addWidget (self .conversation_widget , 0 , 1 )
149+ main_layout .addWidget (self .analytics_widget , 0 , 2 )
150+
151+ # Row 1: Emotion Display | Tool Monitor | Controls
152+ main_layout .addWidget (self .emotion_widget , 1 , 0 )
153+ main_layout .addWidget (self .tool_monitor_widget , 1 , 1 )
154+ main_layout .addWidget (self .controls_widget , 1 , 2 )
155+
156+ # Set column stretch to make conversation widget wider
157+ main_layout .setColumnStretch (0 , 1 ) # Status/Emotion column
158+ main_layout .setColumnStretch (1 , 2 ) # Conversation/Tools column (wider)
159+ main_layout .setColumnStretch (2 , 1 ) # Analytics/Controls column
160+
161+ self ._widget .setWindowTitle ('Coffee Voice Agent Monitor' )
162+ self ._widget .resize (1200 , 800 )
163+
164+ def _init_ros (self ):
165+ """Initialize ROS2 node and connections"""
166+ # Initialize rclpy if not already done
167+ if not rclpy .ok ():
168+ rclpy .init ()
169+
170+ # Create ROS2 node
171+ self .ros_node = VoiceAgentMonitorNode ()
172+
173+ # Connect ROS signals to UI update methods
174+ self .ros_node .agent_status_received .connect (self ._update_agent_status )
175+ self .ros_node .tool_event_received .connect (self ._update_tool_event )
176+ self .ros_node .user_speech_received .connect (self ._update_user_speech )
177+ self .ros_node .connection_status_received .connect (self ._update_connection_status )
178+
179+ # Connect control widget signals to publishers
180+ self .controls_widget .virtual_request_signal .connect (self ._send_virtual_request )
181+ self .controls_widget .command_signal .connect (self ._send_command )
182+
183+ # Start ROS spinning in separate thread
184+ self .ros_executor = MultiThreadedExecutor ()
185+ self .ros_executor .add_node (self .ros_node )
186+ self .ros_thread = threading .Thread (target = self .ros_executor .spin , daemon = True )
187+ self .ros_thread .start ()
188+
189+ def _setup_timers (self ):
190+ """Set up update timers for the UI"""
191+ # Main update timer for real-time data
192+ self .update_timer = QTimer ()
193+ self .update_timer .timeout .connect (self ._periodic_update )
194+ self .update_timer .start (100 ) # 10 FPS updates
195+
196+ # Analytics update timer (slower)
197+ self .analytics_timer = QTimer ()
198+ self .analytics_timer .timeout .connect (self ._update_analytics )
199+ self .analytics_timer .start (1000 ) # 1 FPS for analytics
200+
201+ @pyqtSlot (AgentStatus )
202+ def _update_agent_status (self , status ):
203+ """Update UI with new agent status"""
204+ self .agent_status_widget .update_status (status )
205+ self .emotion_widget .update_emotion (status .emotion , status .previous_emotion )
206+ self .conversation_widget .update_agent_state (status )
207+
208+ @pyqtSlot (ToolEvent )
209+ def _update_tool_event (self , event ):
210+ """Update UI with new tool event"""
211+ self .tool_monitor_widget .add_tool_event (event )
212+ self .conversation_widget .add_tool_event (event )
213+
214+ @pyqtSlot (str )
215+ def _update_user_speech (self , speech ):
216+ """Update UI with user speech"""
217+ self .conversation_widget .add_user_speech (speech )
218+
219+ @pyqtSlot (bool )
220+ def _update_connection_status (self , connected ):
221+ """Update UI with connection status"""
222+ self .agent_status_widget .update_connection (connected )
223+
224+ @pyqtSlot (str )
225+ def _send_virtual_request (self , request_json ):
226+ """Send virtual request through ROS2"""
227+ if self .ros_node :
228+ msg = String ()
229+ msg .data = request_json
230+ self .ros_node .virtual_request_pub .publish (msg )
231+
232+ @pyqtSlot (str )
233+ def _send_command (self , command_json ):
234+ """Send command through ROS2"""
235+ if self .ros_node :
236+ msg = String ()
237+ msg .data = command_json
238+ self .ros_node .command_pub .publish (msg )
239+
240+ def _periodic_update (self ):
241+ """Periodic UI updates for real-time elements"""
242+ # Update conversation widget timestamps
243+ self .conversation_widget .update_timestamps ()
244+
245+ # Update tool monitor timing
246+ self .tool_monitor_widget .update_timing ()
247+
248+ # Update emotion transitions
249+ self .emotion_widget .update_animations ()
250+
251+ def _update_analytics (self ):
252+ """Update analytics data"""
253+ # Collect data from other widgets for analytics
254+ agent_data = self .agent_status_widget .get_analytics_data ()
255+ tool_data = self .tool_monitor_widget .get_analytics_data ()
256+ conversation_data = self .conversation_widget .get_analytics_data ()
257+
258+ self .analytics_widget .update_analytics (agent_data , tool_data , conversation_data )
259+
260+ def shutdown_plugin (self ):
261+ """Clean up when plugin shuts down"""
262+ if hasattr (self , 'update_timer' ):
263+ self .update_timer .stop ()
264+ if hasattr (self , 'analytics_timer' ):
265+ self .analytics_timer .stop ()
266+
267+ if hasattr (self , 'ros_executor' ):
268+ self .ros_executor .shutdown ()
269+
270+ if hasattr (self , 'ros_thread' ):
271+ self .ros_thread .join (timeout = 1.0 )
272+
273+
274+ def main ():
275+ """Standalone entry point for testing"""
276+ import sys
277+ from python_qt_binding .QtWidgets import QApplication
278+
279+ # Initialize ROS2
280+ rclpy .init ()
281+
282+ # Create Qt Application
283+ app = QApplication (sys .argv )
284+
285+ # Create standalone widget
286+ widget = QWidget ()
287+ monitor = VoiceAgentMonitor (None )
288+ layout = QVBoxLayout ()
289+ layout .addWidget (monitor ._widget )
290+ widget .setLayout (layout )
291+ widget .show ()
292+
293+ try :
294+ sys .exit (app .exec_ ())
295+ except KeyboardInterrupt :
296+ pass
297+ finally :
298+ rclpy .shutdown ()
299+
300+
301+ if __name__ == '__main__' :
302+ main ()
0 commit comments