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.
Files changed (60) hide show
  1. luckyrobots/__init__.py +30 -12
  2. luckyrobots/client.py +997 -0
  3. luckyrobots/config/robots.yaml +231 -71
  4. luckyrobots/engine/__init__.py +23 -0
  5. luckyrobots/{utils → engine}/check_updates.py +108 -48
  6. luckyrobots/{utils → engine}/download.py +61 -39
  7. luckyrobots/engine/manager.py +427 -0
  8. luckyrobots/grpc/__init__.py +6 -0
  9. luckyrobots/grpc/generated/__init__.py +18 -0
  10. luckyrobots/grpc/generated/agent_pb2.py +69 -0
  11. luckyrobots/grpc/generated/agent_pb2_grpc.py +283 -0
  12. luckyrobots/grpc/generated/camera_pb2.py +47 -0
  13. luckyrobots/grpc/generated/camera_pb2_grpc.py +144 -0
  14. luckyrobots/grpc/generated/common_pb2.py +43 -0
  15. luckyrobots/grpc/generated/common_pb2_grpc.py +24 -0
  16. luckyrobots/grpc/generated/hazel_rpc_pb2.py +43 -0
  17. luckyrobots/grpc/generated/hazel_rpc_pb2_grpc.py +24 -0
  18. luckyrobots/grpc/generated/media_pb2.py +39 -0
  19. luckyrobots/grpc/generated/media_pb2_grpc.py +24 -0
  20. luckyrobots/grpc/generated/mujoco_pb2.py +51 -0
  21. luckyrobots/grpc/generated/mujoco_pb2_grpc.py +230 -0
  22. luckyrobots/grpc/generated/scene_pb2.py +66 -0
  23. luckyrobots/grpc/generated/scene_pb2_grpc.py +317 -0
  24. luckyrobots/grpc/generated/telemetry_pb2.py +47 -0
  25. luckyrobots/grpc/generated/telemetry_pb2_grpc.py +143 -0
  26. luckyrobots/grpc/generated/viewport_pb2.py +50 -0
  27. luckyrobots/grpc/generated/viewport_pb2_grpc.py +144 -0
  28. luckyrobots/grpc/proto/agent.proto +213 -0
  29. luckyrobots/grpc/proto/camera.proto +41 -0
  30. luckyrobots/grpc/proto/common.proto +36 -0
  31. luckyrobots/grpc/proto/hazel_rpc.proto +32 -0
  32. luckyrobots/grpc/proto/media.proto +26 -0
  33. luckyrobots/grpc/proto/mujoco.proto +64 -0
  34. luckyrobots/grpc/proto/scene.proto +104 -0
  35. luckyrobots/grpc/proto/telemetry.proto +43 -0
  36. luckyrobots/grpc/proto/viewport.proto +45 -0
  37. luckyrobots/luckyrobots.py +252 -0
  38. luckyrobots/models/__init__.py +15 -0
  39. luckyrobots/models/camera.py +97 -0
  40. luckyrobots/models/observation.py +135 -0
  41. luckyrobots/models/randomization.py +77 -0
  42. luckyrobots/{utils/helpers.py → utils.py} +75 -40
  43. luckyrobots-0.1.72.dist-info/METADATA +262 -0
  44. luckyrobots-0.1.72.dist-info/RECORD +47 -0
  45. {luckyrobots-0.1.66.dist-info → luckyrobots-0.1.72.dist-info}/WHEEL +1 -1
  46. luckyrobots/core/luckyrobots.py +0 -624
  47. luckyrobots/core/manager.py +0 -236
  48. luckyrobots/core/models.py +0 -68
  49. luckyrobots/core/node.py +0 -273
  50. luckyrobots/message/__init__.py +0 -18
  51. luckyrobots/message/pubsub.py +0 -145
  52. luckyrobots/message/srv/client.py +0 -81
  53. luckyrobots/message/srv/service.py +0 -135
  54. luckyrobots/message/srv/types.py +0 -83
  55. luckyrobots/message/transporter.py +0 -427
  56. luckyrobots/utils/event_loop.py +0 -94
  57. luckyrobots/utils/sim_manager.py +0 -406
  58. luckyrobots-0.1.66.dist-info/METADATA +0 -253
  59. luckyrobots-0.1.66.dist-info/RECORD +0 -24
  60. {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()
@@ -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")