1212import threading
1313import time
1414import uuid
15+ import datetime
1516from typing import Optional
1617
1718import rclpy
2021from rclpy .callback_groups import ReentrantCallbackGroup
2122from std_msgs .msg import String , Bool
2223from geometry_msgs .msg import Twist
24+ from coffee_voice_agent_msgs .msg import TtsEvent , AgentState , EmotionState , ConversationItem
2325
2426try :
2527 import websockets
@@ -55,26 +57,33 @@ def __init__(self):
5557
5658 # ROS2 Publishers (Voice Agent → ROS2)
5759 self .state_pub = self .create_publisher (
58- String ,
60+ AgentState ,
5961 'voice_agent/state' ,
6062 10 ,
6163 callback_group = self .callback_group
6264 )
6365
6466 self .conversation_pub = self .create_publisher (
65- String ,
67+ ConversationItem ,
6668 'voice_agent/conversation' ,
6769 10 ,
6870 callback_group = self .callback_group
6971 )
7072
7173 self .emotion_pub = self .create_publisher (
72- String ,
74+ EmotionState ,
7375 'voice_agent/emotion' ,
7476 10 ,
7577 callback_group = self .callback_group
7678 )
7779
80+ self .tts_events_pub = self .create_publisher (
81+ TtsEvent ,
82+ 'voice_agent/tts_events' ,
83+ 10 ,
84+ callback_group = self .callback_group
85+ )
86+
7887 self .connected_pub = self .create_publisher (
7988 Bool ,
8089 'voice_agent/connected' ,
@@ -158,32 +167,58 @@ async def _handle_websocket_message(self, message: str):
158167
159168 if message_type == 'STATE_CHANGE' :
160169 # Publish agent state change
161- state_msg = String ()
162- state_msg .data = json .dumps ({
163- 'state' : data .get ('state' ),
164- 'timestamp' : data .get ('timestamp' ),
165- 'previous_state' : data .get ('previous_state' )
166- })
170+ state_msg = AgentState ()
171+ state_msg .current_state = data .get ('state' , 'unknown' )
172+ state_msg .previous_state = data .get ('previous_state' , 'unknown' )
173+ # Parse timestamp if provided, otherwise use current time
174+ timestamp_str = data .get ('timestamp' )
175+ if timestamp_str :
176+ # Convert ISO timestamp to ROS Time if needed
177+ try :
178+ dt = datetime .datetime .fromisoformat (timestamp_str .replace ('Z' , '+00:00' ))
179+ state_msg .timestamp .sec = int (dt .timestamp ())
180+ state_msg .timestamp .nanosec = int ((dt .timestamp () % 1 ) * 1e9 )
181+ except :
182+ # Fallback to current time
183+ state_msg .timestamp = self .get_clock ().now ().to_msg ()
184+ else :
185+ state_msg .timestamp = self .get_clock ().now ().to_msg ()
167186 self .state_pub .publish (state_msg )
168187
169188 elif message_type == 'CONVERSATION' :
170189 # Publish conversation transcript
171- conv_msg = String ()
172- conv_msg .data = json .dumps ({
173- 'role' : data .get ('role' ),
174- 'text' : data .get ('text' ),
175- 'timestamp' : data .get ('timestamp' )
176- })
190+ conv_msg = ConversationItem ()
191+ conv_msg .role = data .get ('role' , 'unknown' )
192+ conv_msg .text = data .get ('text' , '' )
193+ # Parse timestamp if provided, otherwise use current time
194+ timestamp_str = data .get ('timestamp' )
195+ if timestamp_str :
196+ try :
197+ dt = datetime .datetime .fromisoformat (timestamp_str .replace ('Z' , '+00:00' ))
198+ conv_msg .timestamp .sec = int (dt .timestamp ())
199+ conv_msg .timestamp .nanosec = int ((dt .timestamp () % 1 ) * 1e9 )
200+ except :
201+ conv_msg .timestamp = self .get_clock ().now ().to_msg ()
202+ else :
203+ conv_msg .timestamp = self .get_clock ().now ().to_msg ()
177204 self .conversation_pub .publish (conv_msg )
178205
179206 elif message_type == 'EMOTION' :
180207 # Publish emotion change
181- emotion_msg = String ()
182- emotion_msg .data = json .dumps ({
183- 'emotion' : data .get ('emotion' ),
184- 'previous_emotion' : data .get ('previous_emotion' ),
185- 'timestamp' : data .get ('timestamp' )
186- })
208+ emotion_msg = EmotionState ()
209+ emotion_msg .emotion = data .get ('emotion' , 'unknown' )
210+ emotion_msg .previous_emotion = data .get ('previous_emotion' , 'unknown' )
211+ # Parse timestamp if provided, otherwise use current time
212+ timestamp_str = data .get ('timestamp' )
213+ if timestamp_str :
214+ try :
215+ dt = datetime .datetime .fromisoformat (timestamp_str .replace ('Z' , '+00:00' ))
216+ emotion_msg .timestamp .sec = int (dt .timestamp ())
217+ emotion_msg .timestamp .nanosec = int ((dt .timestamp () % 1 ) * 1e9 )
218+ except :
219+ emotion_msg .timestamp = self .get_clock ().now ().to_msg ()
220+ else :
221+ emotion_msg .timestamp = self .get_clock ().now ().to_msg ()
187222 self .emotion_pub .publish (emotion_msg )
188223
189224 elif message_type == 'STATUS' :
@@ -205,9 +240,24 @@ async def _handle_websocket_message(self, message: str):
205240
206241 self .get_logger ().info (f"TTS { event } : emotion={ emotion } , source={ source } , text='{ text_preview } '" )
207242
208- # TODO: Publish to ROS2 topics when robot coordination is ready
209- # self.tts_started_pub.publish(...) for event == "started"
210- # self.tts_finished_pub.publish(...) for event == "finished"
243+ # Publish TTS event to ROS2 topic
244+ tts_msg = TtsEvent ()
245+ tts_msg .event = event
246+ tts_msg .emotion = emotion
247+ tts_msg .text = text_preview # Use truncated text for efficiency
248+ tts_msg .source = source
249+ # Parse timestamp if provided, otherwise use current time
250+ timestamp_str = event_data .get ('timestamp' )
251+ if timestamp_str :
252+ try :
253+ dt = datetime .datetime .fromisoformat (timestamp_str .replace ('Z' , '+00:00' ))
254+ tts_msg .timestamp .sec = int (dt .timestamp ())
255+ tts_msg .timestamp .nanosec = int ((dt .timestamp () % 1 ) * 1e9 )
256+ except :
257+ tts_msg .timestamp = self .get_clock ().now ().to_msg ()
258+ else :
259+ tts_msg .timestamp = self .get_clock ().now ().to_msg ()
260+ self .tts_events_pub .publish (tts_msg )
211261
212262 elif message_type == 'ACKNOWLEDGMENT' :
213263 # Handle acknowledgment messages from voice agent
0 commit comments