luckyrobots 0.1.66__py3-none-any.whl → 0.1.72__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- luckyrobots/__init__.py +30 -12
- luckyrobots/client.py +997 -0
- luckyrobots/config/robots.yaml +231 -71
- luckyrobots/engine/__init__.py +23 -0
- luckyrobots/{utils → engine}/check_updates.py +108 -48
- luckyrobots/{utils → engine}/download.py +61 -39
- luckyrobots/engine/manager.py +427 -0
- luckyrobots/grpc/__init__.py +6 -0
- luckyrobots/grpc/generated/__init__.py +18 -0
- luckyrobots/grpc/generated/agent_pb2.py +69 -0
- luckyrobots/grpc/generated/agent_pb2_grpc.py +283 -0
- luckyrobots/grpc/generated/camera_pb2.py +47 -0
- luckyrobots/grpc/generated/camera_pb2_grpc.py +144 -0
- luckyrobots/grpc/generated/common_pb2.py +43 -0
- luckyrobots/grpc/generated/common_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/hazel_rpc_pb2.py +43 -0
- luckyrobots/grpc/generated/hazel_rpc_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/media_pb2.py +39 -0
- luckyrobots/grpc/generated/media_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/mujoco_pb2.py +51 -0
- luckyrobots/grpc/generated/mujoco_pb2_grpc.py +230 -0
- luckyrobots/grpc/generated/scene_pb2.py +66 -0
- luckyrobots/grpc/generated/scene_pb2_grpc.py +317 -0
- luckyrobots/grpc/generated/telemetry_pb2.py +47 -0
- luckyrobots/grpc/generated/telemetry_pb2_grpc.py +143 -0
- luckyrobots/grpc/generated/viewport_pb2.py +50 -0
- luckyrobots/grpc/generated/viewport_pb2_grpc.py +144 -0
- luckyrobots/grpc/proto/agent.proto +213 -0
- luckyrobots/grpc/proto/camera.proto +41 -0
- luckyrobots/grpc/proto/common.proto +36 -0
- luckyrobots/grpc/proto/hazel_rpc.proto +32 -0
- luckyrobots/grpc/proto/media.proto +26 -0
- luckyrobots/grpc/proto/mujoco.proto +64 -0
- luckyrobots/grpc/proto/scene.proto +104 -0
- luckyrobots/grpc/proto/telemetry.proto +43 -0
- luckyrobots/grpc/proto/viewport.proto +45 -0
- luckyrobots/luckyrobots.py +252 -0
- luckyrobots/models/__init__.py +15 -0
- luckyrobots/models/camera.py +97 -0
- luckyrobots/models/observation.py +135 -0
- luckyrobots/models/randomization.py +77 -0
- luckyrobots/{utils/helpers.py → utils.py} +75 -40
- luckyrobots-0.1.72.dist-info/METADATA +262 -0
- luckyrobots-0.1.72.dist-info/RECORD +47 -0
- {luckyrobots-0.1.66.dist-info → luckyrobots-0.1.72.dist-info}/WHEEL +1 -1
- luckyrobots/core/luckyrobots.py +0 -624
- luckyrobots/core/manager.py +0 -236
- luckyrobots/core/models.py +0 -68
- luckyrobots/core/node.py +0 -273
- luckyrobots/message/__init__.py +0 -18
- luckyrobots/message/pubsub.py +0 -145
- luckyrobots/message/srv/client.py +0 -81
- luckyrobots/message/srv/service.py +0 -135
- luckyrobots/message/srv/types.py +0 -83
- luckyrobots/message/transporter.py +0 -427
- luckyrobots/utils/event_loop.py +0 -94
- luckyrobots/utils/sim_manager.py +0 -406
- luckyrobots-0.1.66.dist-info/METADATA +0 -253
- luckyrobots-0.1.66.dist-info/RECORD +0 -24
- {luckyrobots-0.1.66.dist-info → luckyrobots-0.1.72.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
WebSocket-based transport layer for the LuckyRobots messaging system.
|
|
3
|
-
|
|
4
|
-
This module provides the WebSocketTransport class which handles message
|
|
5
|
-
serialization and communication over WebSockets between distributed nodes.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import msgpack
|
|
9
|
-
import asyncio
|
|
10
|
-
import json
|
|
11
|
-
import logging
|
|
12
|
-
import time
|
|
13
|
-
import uuid
|
|
14
|
-
import websockets
|
|
15
|
-
from enum import Enum
|
|
16
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
17
|
-
|
|
18
|
-
from pydantic import BaseModel, ValidationError
|
|
19
|
-
|
|
20
|
-
from ..utils.event_loop import run_coroutine, get_event_loop
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
logging.basicConfig(
|
|
24
|
-
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
25
|
-
)
|
|
26
|
-
logger = logging.getLogger("transporter")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class MessageType(str, Enum):
|
|
30
|
-
PUBLISH = "publish"
|
|
31
|
-
SUBSCRIBE = "subscribe"
|
|
32
|
-
UNSUBSCRIBE = "unsubscribe"
|
|
33
|
-
SERVICE_REQUEST = "service_request"
|
|
34
|
-
SERVICE_RESPONSE = "service_response"
|
|
35
|
-
SERVICE_REGISTER = "service_register"
|
|
36
|
-
SERVICE_UNREGISTER = "service_unregister"
|
|
37
|
-
NODE_ANNOUNCE = "node_announce"
|
|
38
|
-
NODE_SHUTDOWN = "node_shutdown"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class TransportMessage(BaseModel):
|
|
42
|
-
msg_type: MessageType
|
|
43
|
-
node_name: str
|
|
44
|
-
uuid: str
|
|
45
|
-
topic_or_service: str
|
|
46
|
-
data: Optional[dict] = None
|
|
47
|
-
message_id: Optional[str] = None
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class Transporter:
|
|
51
|
-
def __init__(
|
|
52
|
-
self,
|
|
53
|
-
node_name: str,
|
|
54
|
-
uuid: str,
|
|
55
|
-
host: str = "localhost",
|
|
56
|
-
port: int = 3000,
|
|
57
|
-
reconnect_interval: float = 5.0,
|
|
58
|
-
):
|
|
59
|
-
self.node_name = node_name
|
|
60
|
-
self.uuid = uuid
|
|
61
|
-
self.server_uri = f"ws://{host}:{port}/nodes"
|
|
62
|
-
self.reconnect_interval = reconnect_interval
|
|
63
|
-
|
|
64
|
-
# Websocket connections
|
|
65
|
-
self._connection = None
|
|
66
|
-
self._connected = asyncio.Event()
|
|
67
|
-
self._connection_task = None
|
|
68
|
-
self._should_run = True
|
|
69
|
-
|
|
70
|
-
# Message handlers
|
|
71
|
-
self._topic_handlers: Dict[str, List[Callable[[Any], None]]] = {}
|
|
72
|
-
self._service_handlers: Dict[str, Callable[[Any], Any]] = {}
|
|
73
|
-
self._response_futures: Dict[str, asyncio.Future] = {}
|
|
74
|
-
|
|
75
|
-
# Start connection task using the shared event loop
|
|
76
|
-
self._connection_task = run_coroutine(self._maintain_connection())
|
|
77
|
-
|
|
78
|
-
async def _maintain_connection(self):
|
|
79
|
-
"""Maintain a WebSocket connection to the server, reconnecting as needed"""
|
|
80
|
-
while self._should_run:
|
|
81
|
-
try:
|
|
82
|
-
async with websockets.connect(self.server_uri) as websocket:
|
|
83
|
-
self._connection = websocket
|
|
84
|
-
self._connected.set()
|
|
85
|
-
logger.info(f"Node {self.node_name} connected to WebSocket server")
|
|
86
|
-
|
|
87
|
-
# Announce this node
|
|
88
|
-
await self._announce_node()
|
|
89
|
-
|
|
90
|
-
# Re-subscribe to all topics
|
|
91
|
-
await self._resubscribe()
|
|
92
|
-
|
|
93
|
-
# Re-register all services
|
|
94
|
-
await self._reregister_services()
|
|
95
|
-
|
|
96
|
-
# Handle messages
|
|
97
|
-
await self._handle_messages()
|
|
98
|
-
except (websockets.ConnectionClosed, ConnectionRefusedError) as e:
|
|
99
|
-
self._connected.clear()
|
|
100
|
-
self._connection = None
|
|
101
|
-
logger.warning(
|
|
102
|
-
f"WebSocket connection lost: {e}. Reconnecting in {self.reconnect_interval} seconds"
|
|
103
|
-
)
|
|
104
|
-
await asyncio.sleep(self.reconnect_interval)
|
|
105
|
-
except Exception as e:
|
|
106
|
-
logger.error(f"Unexpected error in WebSocket connection: {e}")
|
|
107
|
-
self._connected.clear()
|
|
108
|
-
self._connection = None
|
|
109
|
-
await asyncio.sleep(self.reconnect_interval)
|
|
110
|
-
|
|
111
|
-
async def _announce_node(self):
|
|
112
|
-
"""Announce this node to the network"""
|
|
113
|
-
message = TransportMessage(
|
|
114
|
-
msg_type=MessageType.NODE_ANNOUNCE,
|
|
115
|
-
node_name=self.node_name,
|
|
116
|
-
uuid=self.uuid,
|
|
117
|
-
topic_or_service="",
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
await self._send_message(message)
|
|
121
|
-
|
|
122
|
-
async def _resubscribe(self):
|
|
123
|
-
"""Re-subscribe to all topics after reconnection"""
|
|
124
|
-
for topic in self._topic_handlers:
|
|
125
|
-
message = TransportMessage(
|
|
126
|
-
msg_type=MessageType.SUBSCRIBE,
|
|
127
|
-
node_name=self.node_name,
|
|
128
|
-
uuid=self.uuid,
|
|
129
|
-
topic_or_service=topic,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
await self._send_message(message)
|
|
133
|
-
|
|
134
|
-
async def _reregister_services(self):
|
|
135
|
-
"""Re-register all services after reconnection"""
|
|
136
|
-
for service in self._service_handlers:
|
|
137
|
-
message = TransportMessage(
|
|
138
|
-
msg_type=MessageType.SERVICE_REGISTER,
|
|
139
|
-
node_name=self.node_name,
|
|
140
|
-
uuid=self.uuid,
|
|
141
|
-
topic_or_service=service,
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
await self._send_message(message)
|
|
145
|
-
|
|
146
|
-
async def _handle_messages(self):
|
|
147
|
-
"""Handle incoming messages from the WebSocket connection"""
|
|
148
|
-
while self._connection and self._should_run:
|
|
149
|
-
try:
|
|
150
|
-
try:
|
|
151
|
-
message_text = await self._connection.recv()
|
|
152
|
-
# Parse the message
|
|
153
|
-
message_data = msgpack.unpackb(message_text)
|
|
154
|
-
message = TransportMessage(**message_data)
|
|
155
|
-
|
|
156
|
-
# Process directly since we're already in an async context
|
|
157
|
-
await self._process_message(message)
|
|
158
|
-
except (json.JSONDecodeError, ValidationError) as e:
|
|
159
|
-
logger.error(f"Error parsing message: {e}, message: {message_text}")
|
|
160
|
-
except websockets.ConnectionClosed:
|
|
161
|
-
break
|
|
162
|
-
except Exception as e:
|
|
163
|
-
logger.error(f"Error handling message: {e}")
|
|
164
|
-
|
|
165
|
-
async def _process_message(self, message: TransportMessage):
|
|
166
|
-
if message.msg_type == MessageType.PUBLISH:
|
|
167
|
-
# Handle published messages
|
|
168
|
-
if message.topic_or_service in self._topic_handlers:
|
|
169
|
-
for handler in self._topic_handlers[message.topic_or_service]:
|
|
170
|
-
try:
|
|
171
|
-
handler(message.data)
|
|
172
|
-
except Exception as e:
|
|
173
|
-
logger.error(f"Error in message handler: {e}")
|
|
174
|
-
|
|
175
|
-
elif message.msg_type == MessageType.SERVICE_REQUEST:
|
|
176
|
-
# Handle service requests
|
|
177
|
-
if message.topic_or_service in self._service_handlers:
|
|
178
|
-
handler = self._service_handlers[message.topic_or_service]
|
|
179
|
-
try:
|
|
180
|
-
# Process the request
|
|
181
|
-
result = await self._run_service_handler(handler, message.data)
|
|
182
|
-
|
|
183
|
-
# Send the response
|
|
184
|
-
response = TransportMessage(
|
|
185
|
-
msg_type=MessageType.SERVICE_RESPONSE,
|
|
186
|
-
node_name=self.node_name,
|
|
187
|
-
uuid=self.uuid,
|
|
188
|
-
topic_or_service=message.topic_or_service,
|
|
189
|
-
message_id=message.message_id,
|
|
190
|
-
data=result,
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
await self._send_message(response)
|
|
194
|
-
except Exception as e:
|
|
195
|
-
logger.error(f"Error handling service request: {e}")
|
|
196
|
-
# Send error response
|
|
197
|
-
error_response = TransportMessage(
|
|
198
|
-
msg_type=MessageType.SERVICE_RESPONSE,
|
|
199
|
-
node_name=self.node_name,
|
|
200
|
-
uuid=self.uuid,
|
|
201
|
-
topic_or_service=message.topic_or_service,
|
|
202
|
-
message_id=message.message_id,
|
|
203
|
-
data={"error": str(e), "success": False},
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
await self._send_message(error_response)
|
|
207
|
-
|
|
208
|
-
elif message.msg_type == MessageType.SERVICE_RESPONSE:
|
|
209
|
-
# Handle service responses
|
|
210
|
-
if message.message_id in self._response_futures:
|
|
211
|
-
future = self._response_futures[message.message_id]
|
|
212
|
-
if not future.done():
|
|
213
|
-
future.set_result(message.data)
|
|
214
|
-
del self._response_futures[message.message_id]
|
|
215
|
-
|
|
216
|
-
async def _run_service_handler(self, handler: Callable, request_data: dict) -> dict:
|
|
217
|
-
try:
|
|
218
|
-
# If the handler is asynchronous, await it
|
|
219
|
-
if asyncio.iscoroutinefunction(handler):
|
|
220
|
-
# This part is correct, but we need to ensure the result is properly awaited
|
|
221
|
-
result = await handler(request_data)
|
|
222
|
-
else:
|
|
223
|
-
# Run non-async handler in a thread pool
|
|
224
|
-
loop = get_event_loop()
|
|
225
|
-
result = await loop.run_in_executor(None, handler, request_data)
|
|
226
|
-
|
|
227
|
-
# Check if the result itself is a coroutine (sometimes happens with wrapped functions)
|
|
228
|
-
if asyncio.iscoroutine(result):
|
|
229
|
-
result = await result
|
|
230
|
-
|
|
231
|
-
# Ensure result is JSON serializable
|
|
232
|
-
if hasattr(result, "dict"):
|
|
233
|
-
return result.dict()
|
|
234
|
-
elif isinstance(result, dict):
|
|
235
|
-
return result
|
|
236
|
-
else:
|
|
237
|
-
# Try to convert to dict if possible
|
|
238
|
-
try:
|
|
239
|
-
return dict(result)
|
|
240
|
-
except (TypeError, ValueError):
|
|
241
|
-
return {"value": result, "success": True}
|
|
242
|
-
except Exception as e:
|
|
243
|
-
logger.error(f"Error in service handler: {e}")
|
|
244
|
-
raise
|
|
245
|
-
|
|
246
|
-
async def _send_message(self, message: TransportMessage):
|
|
247
|
-
if not self._connection:
|
|
248
|
-
await self._connected.wait()
|
|
249
|
-
|
|
250
|
-
try:
|
|
251
|
-
await self._connection.send(msgpack.dumps(message.dict()))
|
|
252
|
-
except websockets.ConnectionClosed:
|
|
253
|
-
logger.warning("Could not send message, connection closed")
|
|
254
|
-
self._connected.clear()
|
|
255
|
-
except Exception as e:
|
|
256
|
-
logger.error(f"Error sending message: {e}")
|
|
257
|
-
|
|
258
|
-
def publish(self, topic: str, message: Any):
|
|
259
|
-
# Ensure message is serializable
|
|
260
|
-
if hasattr(message, "dict"):
|
|
261
|
-
data = message.dict()
|
|
262
|
-
elif hasattr(message, "to_dict"):
|
|
263
|
-
data = message.to_dict()
|
|
264
|
-
elif isinstance(message, dict):
|
|
265
|
-
data = message
|
|
266
|
-
else:
|
|
267
|
-
data = {"value": message}
|
|
268
|
-
|
|
269
|
-
transport_message = TransportMessage(
|
|
270
|
-
msg_type=MessageType.PUBLISH,
|
|
271
|
-
node_name=self.node_name,
|
|
272
|
-
uuid=self.uuid,
|
|
273
|
-
topic_or_service=topic,
|
|
274
|
-
data=data,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
run_coroutine(self._send_message(transport_message))
|
|
278
|
-
|
|
279
|
-
def subscribe(self, topic: str, callback: Callable[[Any], None]):
|
|
280
|
-
# Add the callback to the topic handlers
|
|
281
|
-
if topic not in self._topic_handlers:
|
|
282
|
-
self._topic_handlers[topic] = []
|
|
283
|
-
self._topic_handlers[topic].append(callback)
|
|
284
|
-
|
|
285
|
-
# Send a subscribe message
|
|
286
|
-
transport_message = TransportMessage(
|
|
287
|
-
msg_type=MessageType.SUBSCRIBE,
|
|
288
|
-
node_name=self.node_name,
|
|
289
|
-
uuid=self.uuid,
|
|
290
|
-
topic_or_service=topic,
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
run_coroutine(self._send_message(transport_message))
|
|
294
|
-
|
|
295
|
-
def unsubscribe(self, topic: str, callback: Callable[[Any], None]):
|
|
296
|
-
# Remove the callback from the topic handlers
|
|
297
|
-
if topic not in self._topic_handlers:
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
if callback in self._topic_handlers[topic]:
|
|
301
|
-
self._topic_handlers[topic].remove(callback)
|
|
302
|
-
|
|
303
|
-
# If no more handlers, send an unsubscribe message
|
|
304
|
-
if not self._topic_handlers[topic]:
|
|
305
|
-
transport_message = TransportMessage(
|
|
306
|
-
msg_type=MessageType.UNSUBSCRIBE,
|
|
307
|
-
node_name=self.node_name,
|
|
308
|
-
uuid=self.uuid,
|
|
309
|
-
topic_or_service=topic,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
run_coroutine(self._send_message(transport_message))
|
|
313
|
-
|
|
314
|
-
# Remove the empty list
|
|
315
|
-
del self._topic_handlers[topic]
|
|
316
|
-
|
|
317
|
-
def register_service(self, service_name: str, handler: Callable[[Any], Any]):
|
|
318
|
-
# Add the handler to the service handlers
|
|
319
|
-
self._service_handlers[service_name] = handler
|
|
320
|
-
|
|
321
|
-
# Send a service register message
|
|
322
|
-
transport_message = TransportMessage(
|
|
323
|
-
msg_type=MessageType.SERVICE_REGISTER,
|
|
324
|
-
node_name=self.node_name,
|
|
325
|
-
uuid=self.uuid,
|
|
326
|
-
topic_or_service=service_name,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
run_coroutine(self._send_message(transport_message))
|
|
330
|
-
|
|
331
|
-
def unregister_service(self, service_name: str):
|
|
332
|
-
# Remove the handler from the service handlers
|
|
333
|
-
if service_name in self._service_handlers:
|
|
334
|
-
del self._service_handlers[service_name]
|
|
335
|
-
|
|
336
|
-
# Send a service unregister message
|
|
337
|
-
transport_message = TransportMessage(
|
|
338
|
-
msg_type=MessageType.SERVICE_UNREGISTER,
|
|
339
|
-
node_name=self.node_name,
|
|
340
|
-
uuid=self.uuid,
|
|
341
|
-
topic_or_service=service_name,
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
run_coroutine(self._send_message(transport_message))
|
|
345
|
-
|
|
346
|
-
async def call_service(
|
|
347
|
-
self, service_name: str, request: Any, timeout: float = 30.0
|
|
348
|
-
) -> Any:
|
|
349
|
-
# Ensure request is serializable
|
|
350
|
-
if hasattr(request, "dict"):
|
|
351
|
-
data = request.dict()
|
|
352
|
-
elif hasattr(request, "to_dict"):
|
|
353
|
-
data = request.to_dict()
|
|
354
|
-
elif isinstance(request, dict):
|
|
355
|
-
data = request
|
|
356
|
-
else:
|
|
357
|
-
data = {"value": request}
|
|
358
|
-
|
|
359
|
-
# Generate a unique message ID
|
|
360
|
-
message_id = (
|
|
361
|
-
f"{self.node_name}_{service_name}_{time.perf_counter()}_{uuid.uuid4().hex}"
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
# Create a future for the response within the same event loop
|
|
365
|
-
shared_loop = get_event_loop()
|
|
366
|
-
future = shared_loop.create_future()
|
|
367
|
-
self._response_futures[message_id] = future
|
|
368
|
-
|
|
369
|
-
# Create the service request message
|
|
370
|
-
transport_message = TransportMessage(
|
|
371
|
-
msg_type=MessageType.SERVICE_REQUEST,
|
|
372
|
-
node_name=self.node_name,
|
|
373
|
-
uuid=self.uuid,
|
|
374
|
-
topic_or_service=service_name,
|
|
375
|
-
message_id=message_id,
|
|
376
|
-
data=data,
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
# Send the message directly within the same event loop
|
|
380
|
-
try:
|
|
381
|
-
await self._send_message(transport_message)
|
|
382
|
-
except Exception as e:
|
|
383
|
-
# Clean up and propagate the error
|
|
384
|
-
if message_id in self._response_futures:
|
|
385
|
-
del self._response_futures[message_id]
|
|
386
|
-
raise Exception(f"Failed to send service request: {e}")
|
|
387
|
-
|
|
388
|
-
try:
|
|
389
|
-
# Wait for the response with timeout
|
|
390
|
-
return await asyncio.wait_for(future, timeout)
|
|
391
|
-
except asyncio.TimeoutError:
|
|
392
|
-
# Remove the future if it times out
|
|
393
|
-
if message_id in self._response_futures:
|
|
394
|
-
del self._response_futures[message_id]
|
|
395
|
-
raise TimeoutError(
|
|
396
|
-
f"Service call to {service_name} timed out after {timeout} seconds"
|
|
397
|
-
)
|
|
398
|
-
except Exception as e:
|
|
399
|
-
# Remove the future on any error
|
|
400
|
-
if message_id in self._response_futures:
|
|
401
|
-
del self._response_futures[message_id]
|
|
402
|
-
raise Exception(f"Service call error: {e}")
|
|
403
|
-
|
|
404
|
-
def shutdown(self):
|
|
405
|
-
"""Shutdown the transport layer"""
|
|
406
|
-
self._should_run = False
|
|
407
|
-
|
|
408
|
-
if self._connection:
|
|
409
|
-
transport_message = TransportMessage(
|
|
410
|
-
msg_type=MessageType.NODE_SHUTDOWN,
|
|
411
|
-
node_name=self.node_name,
|
|
412
|
-
uuid=self.uuid,
|
|
413
|
-
topic_or_service="",
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
try:
|
|
417
|
-
loop = get_event_loop()
|
|
418
|
-
if loop and loop.is_running():
|
|
419
|
-
future = asyncio.run_coroutine_threadsafe(
|
|
420
|
-
self._send_message(transport_message), loop
|
|
421
|
-
)
|
|
422
|
-
future.result(timeout=2.0) # Short timeout
|
|
423
|
-
except Exception:
|
|
424
|
-
pass # Ignore errors during shutdown
|
|
425
|
-
|
|
426
|
-
self._connection = None
|
|
427
|
-
self._response_futures.clear()
|
luckyrobots/utils/event_loop.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import threading
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
|
-
logger = logging.getLogger("event_loop")
|
|
6
|
-
|
|
7
|
-
# Global variables to store the event loop and thread
|
|
8
|
-
_event_loop = None
|
|
9
|
-
_event_loop_thread = None
|
|
10
|
-
_ready_event = threading.Event()
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def initialize_event_loop():
|
|
14
|
-
"""Initialize the event loop"""
|
|
15
|
-
global _event_loop, _event_loop_thread
|
|
16
|
-
|
|
17
|
-
# If already initialized, return the existing loop
|
|
18
|
-
if _event_loop is not None and _ready_event.is_set():
|
|
19
|
-
return _event_loop
|
|
20
|
-
|
|
21
|
-
def run_event_loop():
|
|
22
|
-
"""Run the event loop"""
|
|
23
|
-
logger.info("Event loop thread started")
|
|
24
|
-
loop = asyncio.new_event_loop()
|
|
25
|
-
asyncio.set_event_loop(loop)
|
|
26
|
-
|
|
27
|
-
global _event_loop
|
|
28
|
-
_event_loop = loop
|
|
29
|
-
|
|
30
|
-
logger.info("Event loop created, setting ready event")
|
|
31
|
-
_ready_event.set()
|
|
32
|
-
|
|
33
|
-
logger.info("Shared event loop running")
|
|
34
|
-
try:
|
|
35
|
-
loop.run_forever()
|
|
36
|
-
finally:
|
|
37
|
-
logger.info("Shared event loop shutting down")
|
|
38
|
-
loop.close()
|
|
39
|
-
|
|
40
|
-
# Start the event loop in a background thread
|
|
41
|
-
_event_loop_thread = threading.Thread(target=run_event_loop, daemon=True)
|
|
42
|
-
_event_loop_thread.start()
|
|
43
|
-
|
|
44
|
-
# Wait with a longer timeout
|
|
45
|
-
logger.info("Waiting for event loop to be ready")
|
|
46
|
-
success = _ready_event.wait(timeout=10.0)
|
|
47
|
-
|
|
48
|
-
if not success:
|
|
49
|
-
logger.error("Failed to initialize shared event loop")
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
return _event_loop
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def get_event_loop():
|
|
56
|
-
if _event_loop is None or not _ready_event.is_set():
|
|
57
|
-
return initialize_event_loop()
|
|
58
|
-
return _event_loop
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def run_coroutine(coro, timeout=None):
|
|
62
|
-
loop = get_event_loop()
|
|
63
|
-
if loop is None:
|
|
64
|
-
raise RuntimeError("Event loop is not initialized")
|
|
65
|
-
|
|
66
|
-
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
67
|
-
if timeout is not None:
|
|
68
|
-
try:
|
|
69
|
-
return future.result(timeout)
|
|
70
|
-
except asyncio.TimeoutError:
|
|
71
|
-
logger.error(f"Coroutine timed out after {timeout} seconds")
|
|
72
|
-
raise
|
|
73
|
-
return future # Return the future object when no timeout is specified
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def shutdown_event_loop():
|
|
77
|
-
global _event_loop, _event_loop_thread
|
|
78
|
-
|
|
79
|
-
if _event_loop is not None and _ready_event.is_set():
|
|
80
|
-
logger.info("Shutting down event loop")
|
|
81
|
-
try:
|
|
82
|
-
_event_loop.call_soon_threadsafe(_event_loop.stop)
|
|
83
|
-
except RuntimeError:
|
|
84
|
-
# Event loop already closed
|
|
85
|
-
pass
|
|
86
|
-
|
|
87
|
-
if _event_loop_thread is not None:
|
|
88
|
-
_event_loop_thread.join(timeout=5.0)
|
|
89
|
-
|
|
90
|
-
_event_loop = None
|
|
91
|
-
_event_loop_thread = None
|
|
92
|
-
_ready_event.clear()
|
|
93
|
-
|
|
94
|
-
logger.info("Event loop shutdown complete")
|