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
luckyrobots/core/luckyrobots.py
DELETED
|
@@ -1,624 +0,0 @@
|
|
|
1
|
-
import cv2
|
|
2
|
-
import json
|
|
3
|
-
import msgpack
|
|
4
|
-
import asyncio
|
|
5
|
-
import logging
|
|
6
|
-
import secrets
|
|
7
|
-
import platform
|
|
8
|
-
import signal
|
|
9
|
-
import threading
|
|
10
|
-
import time
|
|
11
|
-
|
|
12
|
-
from typing import Dict
|
|
13
|
-
|
|
14
|
-
import uvicorn
|
|
15
|
-
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
16
|
-
from websocket import create_connection
|
|
17
|
-
|
|
18
|
-
from .manager import Manager
|
|
19
|
-
from ..message.transporter import MessageType, TransportMessage
|
|
20
|
-
from ..message.srv.types import Reset, Step
|
|
21
|
-
from ..utils.sim_manager import launch_luckyworld, stop_luckyworld
|
|
22
|
-
from ..core.models import ObservationModel
|
|
23
|
-
from .node import Node
|
|
24
|
-
from ..utils.event_loop import (
|
|
25
|
-
get_event_loop,
|
|
26
|
-
initialize_event_loop,
|
|
27
|
-
)
|
|
28
|
-
from ..utils.helpers import (
|
|
29
|
-
validate_params,
|
|
30
|
-
get_robot_config,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
logging.basicConfig(
|
|
34
|
-
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
35
|
-
)
|
|
36
|
-
logger = logging.getLogger("luckyrobots")
|
|
37
|
-
|
|
38
|
-
app = FastAPI()
|
|
39
|
-
manager = Manager()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class LuckyRobots(Node):
|
|
43
|
-
"""Main LuckyRobots node for managing robot communication and control"""
|
|
44
|
-
|
|
45
|
-
def __init__(self, host: str = "localhost", port: int = 3000) -> None:
|
|
46
|
-
self.host = host
|
|
47
|
-
self.port = port
|
|
48
|
-
self.robot_client = None
|
|
49
|
-
self.world_client = None
|
|
50
|
-
self._pending_resets = {}
|
|
51
|
-
self._pending_steps = {}
|
|
52
|
-
self._running = False
|
|
53
|
-
self._nodes: Dict[str, Node] = {}
|
|
54
|
-
self._shutdown_event = threading.Event()
|
|
55
|
-
|
|
56
|
-
initialize_event_loop()
|
|
57
|
-
|
|
58
|
-
self._start_websocket_server()
|
|
59
|
-
|
|
60
|
-
super().__init__("lucky_robots_manager", "", self.host, self.port)
|
|
61
|
-
app.lucky_robots = self
|
|
62
|
-
|
|
63
|
-
def _is_websocket_server_running(self) -> bool:
|
|
64
|
-
"""Check if the websocket server is already running"""
|
|
65
|
-
try:
|
|
66
|
-
ws_url = f"ws://{self.host}:{self.port}/nodes"
|
|
67
|
-
ws = create_connection(ws_url, timeout=1)
|
|
68
|
-
ws.close()
|
|
69
|
-
return True
|
|
70
|
-
except Exception as e:
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
def _start_websocket_server(self) -> None:
|
|
74
|
-
"""Start the websocket server in a separate thread using uvicorn"""
|
|
75
|
-
|
|
76
|
-
if self._is_websocket_server_running():
|
|
77
|
-
logger.warning(
|
|
78
|
-
f"WebSocket server already running on {self.host}:{self.port}"
|
|
79
|
-
)
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
def run_server():
|
|
83
|
-
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
|
84
|
-
config = uvicorn.Config(
|
|
85
|
-
app, host=self.host, port=self.port, log_level="warning"
|
|
86
|
-
)
|
|
87
|
-
self._server = uvicorn.Server(config)
|
|
88
|
-
self._server.run()
|
|
89
|
-
|
|
90
|
-
self._server_thread = threading.Thread(target=run_server, daemon=True)
|
|
91
|
-
self._server_thread.start()
|
|
92
|
-
|
|
93
|
-
logger.info(f"Starting WebSocket server on {self.host}:{self.port}")
|
|
94
|
-
|
|
95
|
-
# Wait for the server to start
|
|
96
|
-
timeout = 10.0
|
|
97
|
-
start_time = time.perf_counter()
|
|
98
|
-
|
|
99
|
-
while time.perf_counter() - start_time < timeout:
|
|
100
|
-
if self._is_websocket_server_running():
|
|
101
|
-
logger.info(f"WebSocket server ready on {self.host}:{self.port}")
|
|
102
|
-
return
|
|
103
|
-
time.sleep(0.1)
|
|
104
|
-
|
|
105
|
-
logger.error(f"WebSocket server failed to start within {timeout} seconds")
|
|
106
|
-
raise RuntimeError(
|
|
107
|
-
f"WebSocket server failed to start on {self.host}:{self.port}"
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
@staticmethod
|
|
111
|
-
def get_robot_config(robot: str = None) -> dict:
|
|
112
|
-
"""Get the configuration for the LuckyRobots node"""
|
|
113
|
-
return get_robot_config(robot)
|
|
114
|
-
|
|
115
|
-
def register_node(self, node: Node) -> None:
|
|
116
|
-
"""Register a node with the LuckyRobots node"""
|
|
117
|
-
self._nodes[node.full_name] = node
|
|
118
|
-
logger.info(f"Registered node: {node.full_name}")
|
|
119
|
-
|
|
120
|
-
async def _setup_async(self):
|
|
121
|
-
"""Setup the LuckyRobots node asynchronously"""
|
|
122
|
-
self.reset_service = await self.create_service(
|
|
123
|
-
Reset, "/reset", self.handle_reset
|
|
124
|
-
)
|
|
125
|
-
self.step_service = await self.create_service(Step, "/step", self.handle_step)
|
|
126
|
-
|
|
127
|
-
def _register_cleanup_handlers(self) -> None:
|
|
128
|
-
"""Register cleanup handlers for the LuckyRobots node to handle Ctrl+C"""
|
|
129
|
-
|
|
130
|
-
def sigint_handler(signum, frame):
|
|
131
|
-
logger.info("Ctrl+C pressed. Shutting down...")
|
|
132
|
-
self.shutdown()
|
|
133
|
-
|
|
134
|
-
signal.signal(signal.SIGINT, sigint_handler)
|
|
135
|
-
|
|
136
|
-
def start(
|
|
137
|
-
self,
|
|
138
|
-
scene: str,
|
|
139
|
-
robot: str,
|
|
140
|
-
task: str = None,
|
|
141
|
-
executable_path: str = None,
|
|
142
|
-
observation_type: str = "pixels_agent_pos",
|
|
143
|
-
headless: bool = False,
|
|
144
|
-
) -> None:
|
|
145
|
-
"""Start the LuckyRobots node"""
|
|
146
|
-
if self._running:
|
|
147
|
-
logger.warning("LuckyRobots is already running")
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
validate_params(scene, robot, task, observation_type)
|
|
151
|
-
self.process_cameras = "pixels" in observation_type
|
|
152
|
-
|
|
153
|
-
success = launch_luckyworld(
|
|
154
|
-
scene=scene,
|
|
155
|
-
robot=robot,
|
|
156
|
-
task=task,
|
|
157
|
-
executable_path=executable_path,
|
|
158
|
-
headless=headless,
|
|
159
|
-
)
|
|
160
|
-
if not success:
|
|
161
|
-
logger.error("Failed to launch LuckyWorld")
|
|
162
|
-
self.shutdown()
|
|
163
|
-
raise
|
|
164
|
-
|
|
165
|
-
self._register_cleanup_handlers()
|
|
166
|
-
|
|
167
|
-
# Start all registered nodes
|
|
168
|
-
failed_nodes = []
|
|
169
|
-
for node in self._nodes.values():
|
|
170
|
-
try:
|
|
171
|
-
node.start()
|
|
172
|
-
logger.info(f"Started node: {node.full_name}")
|
|
173
|
-
except Exception as e:
|
|
174
|
-
logger.error(f"Error starting node {node.full_name}: {e}")
|
|
175
|
-
failed_nodes.append(node.full_name)
|
|
176
|
-
|
|
177
|
-
if failed_nodes:
|
|
178
|
-
logger.error(f"Failed to start nodes: {', '.join(failed_nodes)}")
|
|
179
|
-
# Continue anyway - some nodes might be optional
|
|
180
|
-
|
|
181
|
-
super().start()
|
|
182
|
-
|
|
183
|
-
self._running = True
|
|
184
|
-
|
|
185
|
-
def _display_welcome_message(self) -> None:
|
|
186
|
-
"""Display the welcome message for the LuckyRobots node in the terminal"""
|
|
187
|
-
|
|
188
|
-
# Create the complete message in one go
|
|
189
|
-
stars = "*" * 60
|
|
190
|
-
|
|
191
|
-
welcome_lines = [
|
|
192
|
-
stars,
|
|
193
|
-
" ",
|
|
194
|
-
" ",
|
|
195
|
-
"▄▄▌ ▄• ▄▌ ▄▄· ▄ •▄ ▄· ▄▌▄▄▄ ▄▄▄▄· ▄▄▄▄▄.▄▄ · ",
|
|
196
|
-
"██• █▪██▌▐█ ▌▪█▌▄▌▪▐█▪██▌▀▄ █·▪ ▐█ ▀█▪▪ •██ ▐█ ▀. ",
|
|
197
|
-
"██▪ █▌▐█▌██ ▄▄▐▀▀▄·▐█▌▐█▪▐▀▀▄ ▄█▀▄ ▐█▀▀█▄ ▄█▀▄ ▐█.▪▄▀▀▀█▄",
|
|
198
|
-
"▐█▌▐▌▐█▄█▌▐███▌▐█.█▌ ▐█▀·.▐█•█▌▐█▌.▐▌██▄▪▐█▐█▌.▐▌ ▐█▌·▐█▄▪▐█",
|
|
199
|
-
".▀▀▀ ▀▀▀ ·▀▀▀ ·▀ ▀ ▀ • .▀ ▀ ▀█▄▀▪·▀▀▀▀ ▀█▄▀▪ ▀▀▀ ▀▀▀▀ ",
|
|
200
|
-
" ",
|
|
201
|
-
" ",
|
|
202
|
-
]
|
|
203
|
-
|
|
204
|
-
# Add macOS instructions if needed
|
|
205
|
-
if platform.system() == "Darwin":
|
|
206
|
-
welcome_lines.extend(
|
|
207
|
-
[
|
|
208
|
-
stars,
|
|
209
|
-
"For macOS users:",
|
|
210
|
-
"Please be patient. The application may take up to a minute to open on its first launch.",
|
|
211
|
-
"If the application doesn't appear, please follow these steps:",
|
|
212
|
-
"1. Open System Settings",
|
|
213
|
-
"2. Navigate to Privacy & Security",
|
|
214
|
-
"3. Scroll down and click 'Allow' next to the 'luckyrobots' app",
|
|
215
|
-
stars,
|
|
216
|
-
]
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
# Add final messages
|
|
220
|
-
welcome_lines.extend(
|
|
221
|
-
[
|
|
222
|
-
"Lucky Robots application started successfully.",
|
|
223
|
-
"To move the robot: Choose a level and tick the HTTP checkbox.",
|
|
224
|
-
"To receive camera feed: Choose a level and tick the Capture checkbox.",
|
|
225
|
-
stars,
|
|
226
|
-
"", # Empty line at the end
|
|
227
|
-
]
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
# Single print statement - cannot be interrupted!
|
|
231
|
-
print("\n".join(welcome_lines), flush=True)
|
|
232
|
-
|
|
233
|
-
def wait_for_world_client(self, timeout: float = 120.0) -> bool:
|
|
234
|
-
"""Wait for the world client to connect to the websocket server"""
|
|
235
|
-
start_time = time.perf_counter()
|
|
236
|
-
|
|
237
|
-
logger.info(f"Waiting for world client to connect (timeout: {timeout}s)")
|
|
238
|
-
while not self.world_client and time.perf_counter() - start_time < timeout:
|
|
239
|
-
time.sleep(0.5)
|
|
240
|
-
|
|
241
|
-
if self.world_client:
|
|
242
|
-
logger.info("World client connected successfully")
|
|
243
|
-
return True
|
|
244
|
-
else:
|
|
245
|
-
logger.error(f"No world client connected after {timeout} seconds")
|
|
246
|
-
self.shutdown()
|
|
247
|
-
raise RuntimeError(
|
|
248
|
-
f"World client connection timeout after {timeout} seconds"
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
async def handle_reset(self, request: Reset.Request) -> Reset.Response:
|
|
252
|
-
"""Handle the reset request by forwarding to the world client"""
|
|
253
|
-
if self.world_client is None:
|
|
254
|
-
logger.error("No world client connection available")
|
|
255
|
-
self.shutdown()
|
|
256
|
-
raise
|
|
257
|
-
|
|
258
|
-
request_id = secrets.token_hex(4)
|
|
259
|
-
shared_loop = get_event_loop()
|
|
260
|
-
response_future = shared_loop.create_future()
|
|
261
|
-
self._pending_resets[request_id] = response_future
|
|
262
|
-
|
|
263
|
-
seed = getattr(request, "seed", None)
|
|
264
|
-
options = getattr(request, "options", None)
|
|
265
|
-
request_data = {
|
|
266
|
-
"request_type": "reset",
|
|
267
|
-
"request_id": request_id,
|
|
268
|
-
"seed": seed,
|
|
269
|
-
"options": options,
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
try:
|
|
273
|
-
await self.world_client.send_text(json.dumps(request_data))
|
|
274
|
-
response_data = await asyncio.wait_for(response_future, timeout=30.0)
|
|
275
|
-
|
|
276
|
-
# Wait for reset animation to finish in Lucky World
|
|
277
|
-
time.sleep(1)
|
|
278
|
-
|
|
279
|
-
observation = ObservationModel(**response_data["Observation"])
|
|
280
|
-
if self.process_cameras:
|
|
281
|
-
observation.process_all_cameras()
|
|
282
|
-
|
|
283
|
-
return Reset.Response(
|
|
284
|
-
success=True,
|
|
285
|
-
message="Reset request processed",
|
|
286
|
-
request_type=response_data["RequestType"],
|
|
287
|
-
request_id=response_data["RequestID"],
|
|
288
|
-
time_stamp=response_data["TimeStamp"],
|
|
289
|
-
observation=observation,
|
|
290
|
-
info=response_data["Info"],
|
|
291
|
-
)
|
|
292
|
-
except Exception as e:
|
|
293
|
-
self._pending_resets.pop(request_id, None)
|
|
294
|
-
logger.error(f"Error processing reset request: {e}")
|
|
295
|
-
self.shutdown()
|
|
296
|
-
raise
|
|
297
|
-
|
|
298
|
-
async def handle_step(self, request: Step.Request) -> Step.Response:
|
|
299
|
-
"""Handle the step request by forwarding to the world client"""
|
|
300
|
-
if self.world_client is None:
|
|
301
|
-
logger.error("No world client connection available")
|
|
302
|
-
self.shutdown()
|
|
303
|
-
raise
|
|
304
|
-
|
|
305
|
-
request_id = secrets.token_hex(4)
|
|
306
|
-
shared_loop = get_event_loop()
|
|
307
|
-
response_future = shared_loop.create_future()
|
|
308
|
-
self._pending_steps[request_id] = response_future
|
|
309
|
-
|
|
310
|
-
self._pending_steps[request_id] = response_future
|
|
311
|
-
|
|
312
|
-
request_data = {
|
|
313
|
-
"request_type": "step",
|
|
314
|
-
"request_id": request_id,
|
|
315
|
-
"actuator_values": request.actuator_values,
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
try:
|
|
319
|
-
await self.world_client.send_text(json.dumps(request_data))
|
|
320
|
-
response_data = await asyncio.wait_for(response_future, timeout=30.0)
|
|
321
|
-
|
|
322
|
-
observation = ObservationModel(**response_data["Observation"])
|
|
323
|
-
if self.process_cameras:
|
|
324
|
-
observation.process_all_cameras()
|
|
325
|
-
|
|
326
|
-
return Step.Response(
|
|
327
|
-
success=True,
|
|
328
|
-
message="Step request processed",
|
|
329
|
-
request_type=response_data["RequestType"],
|
|
330
|
-
request_id=response_data["RequestID"],
|
|
331
|
-
time_stamp=response_data["TimeStamp"],
|
|
332
|
-
observation=observation,
|
|
333
|
-
info=response_data["Info"],
|
|
334
|
-
)
|
|
335
|
-
except Exception as e:
|
|
336
|
-
self._pending_steps.pop(request_id, None)
|
|
337
|
-
logger.error(f"Error processing step request: {e}")
|
|
338
|
-
self.shutdown()
|
|
339
|
-
raise
|
|
340
|
-
|
|
341
|
-
def spin(self) -> None:
|
|
342
|
-
"""Spin the LuckyRobots node to keep it running"""
|
|
343
|
-
if not self._running:
|
|
344
|
-
logger.warning("LuckyRobots is not running")
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
self._display_welcome_message()
|
|
348
|
-
logger.info("LuckyRobots spinning")
|
|
349
|
-
|
|
350
|
-
try:
|
|
351
|
-
self._shutdown_event.wait()
|
|
352
|
-
except KeyboardInterrupt:
|
|
353
|
-
logger.info("Keyboard interrupt received. Shutting down...")
|
|
354
|
-
self.shutdown()
|
|
355
|
-
|
|
356
|
-
logger.info("LuckyRobots stopped spinning")
|
|
357
|
-
|
|
358
|
-
def _stop_websocket_server(self) -> None:
|
|
359
|
-
"""Stop the WebSocket server if it's running"""
|
|
360
|
-
if hasattr(self, "_server") and self._server is not None:
|
|
361
|
-
try:
|
|
362
|
-
# Stop the uvicorn server
|
|
363
|
-
self._server.should_exit = True
|
|
364
|
-
|
|
365
|
-
# Wait for the server thread to terminate
|
|
366
|
-
if (
|
|
367
|
-
hasattr(self, "_server_thread")
|
|
368
|
-
and self._server_thread
|
|
369
|
-
and self._server_thread.is_alive()
|
|
370
|
-
):
|
|
371
|
-
self._server_thread.join(timeout=5.0)
|
|
372
|
-
|
|
373
|
-
if self._server_thread.is_alive():
|
|
374
|
-
logger.warning(
|
|
375
|
-
"WebSocket server thread did not terminate within timeout"
|
|
376
|
-
)
|
|
377
|
-
else:
|
|
378
|
-
logger.info("WebSocket server thread terminated successfully")
|
|
379
|
-
|
|
380
|
-
self._server = None
|
|
381
|
-
self._server_thread = None
|
|
382
|
-
|
|
383
|
-
except Exception as e:
|
|
384
|
-
logger.error(f"Error stopping WebSocket server: {e}")
|
|
385
|
-
else:
|
|
386
|
-
logger.info("No WebSocket server instance found")
|
|
387
|
-
|
|
388
|
-
def _cleanup_camera_windows(self) -> None:
|
|
389
|
-
"""Clean up all OpenCV windows and reset tracking"""
|
|
390
|
-
try:
|
|
391
|
-
# Only cleanup if we're in the main thread to avoid Qt warnings
|
|
392
|
-
if threading.current_thread() == threading.main_thread():
|
|
393
|
-
cv2.destroyAllWindows()
|
|
394
|
-
cv2.waitKey(1)
|
|
395
|
-
else:
|
|
396
|
-
# If not in main thread, just skip OpenCV cleanup
|
|
397
|
-
# The windows will close when the main thread exits anyway
|
|
398
|
-
pass
|
|
399
|
-
except Exception:
|
|
400
|
-
# Ignore any errors during cleanup
|
|
401
|
-
pass
|
|
402
|
-
|
|
403
|
-
def shutdown(self) -> None:
|
|
404
|
-
"""Shutdown the LuckyRobots node and clean up resources"""
|
|
405
|
-
if not self._running:
|
|
406
|
-
logger.info("LuckyRobots already shut down")
|
|
407
|
-
return
|
|
408
|
-
|
|
409
|
-
logger.info("Starting LuckyRobots shutdown sequence")
|
|
410
|
-
self._running = False
|
|
411
|
-
|
|
412
|
-
self._cancel_pending_operations()
|
|
413
|
-
self._shutdown_nodes()
|
|
414
|
-
self._stop_luckyworld()
|
|
415
|
-
self._shutdown_transport()
|
|
416
|
-
self._stop_websocket_server()
|
|
417
|
-
self._cleanup_resources()
|
|
418
|
-
self._shutdown_event.set()
|
|
419
|
-
|
|
420
|
-
logger.info("LuckyRobots shutdown complete")
|
|
421
|
-
|
|
422
|
-
exit(0)
|
|
423
|
-
|
|
424
|
-
def _cancel_pending_operations(self) -> None:
|
|
425
|
-
"""Cancel all pending reset and step operations"""
|
|
426
|
-
logger.info("Cancelling pending operations")
|
|
427
|
-
|
|
428
|
-
# Cancel pending resets
|
|
429
|
-
for _, future in self._pending_resets.items():
|
|
430
|
-
if not future.done():
|
|
431
|
-
future.cancel()
|
|
432
|
-
self._pending_resets.clear()
|
|
433
|
-
|
|
434
|
-
# Cancel pending steps
|
|
435
|
-
for _, future in self._pending_steps.items():
|
|
436
|
-
if not future.done():
|
|
437
|
-
future.cancel()
|
|
438
|
-
self._pending_steps.clear()
|
|
439
|
-
|
|
440
|
-
def _shutdown_nodes(self) -> None:
|
|
441
|
-
"""Shutdown all registered nodes with timeout"""
|
|
442
|
-
if not self._nodes:
|
|
443
|
-
return
|
|
444
|
-
|
|
445
|
-
logger.info(f"Shutting down {len(self._nodes)} registered nodes")
|
|
446
|
-
|
|
447
|
-
failed_nodes = []
|
|
448
|
-
for node_name, node in self._nodes.items():
|
|
449
|
-
try:
|
|
450
|
-
logger.debug(f"Shutting down node: {node_name}")
|
|
451
|
-
node.shutdown()
|
|
452
|
-
logger.debug(f"Node {node_name} shut down successfully")
|
|
453
|
-
except Exception as e:
|
|
454
|
-
logger.error(f"Error shutting down node {node_name}: {e}")
|
|
455
|
-
failed_nodes.append(node_name)
|
|
456
|
-
|
|
457
|
-
if failed_nodes:
|
|
458
|
-
logger.warning(f"Failed to shutdown nodes: {', '.join(failed_nodes)}")
|
|
459
|
-
else:
|
|
460
|
-
logger.info("All nodes shut down successfully")
|
|
461
|
-
|
|
462
|
-
self._nodes.clear()
|
|
463
|
-
|
|
464
|
-
def _stop_luckyworld(self) -> None:
|
|
465
|
-
"""Stop the LuckyWorld executable with error handling"""
|
|
466
|
-
try:
|
|
467
|
-
stop_luckyworld()
|
|
468
|
-
except Exception as e:
|
|
469
|
-
logger.error(f"Error stopping LuckyWorld executable: {e}")
|
|
470
|
-
# Don't raise - continue with shutdown even if LuckyWorld fails to stop
|
|
471
|
-
|
|
472
|
-
def _shutdown_transport(self) -> None:
|
|
473
|
-
"""Shutdown the transport layer with timeout"""
|
|
474
|
-
try:
|
|
475
|
-
logger.info("Shutting down transport layer...")
|
|
476
|
-
super().shutdown()
|
|
477
|
-
logger.info("Transport layer shut down")
|
|
478
|
-
except Exception as e:
|
|
479
|
-
logger.error(f"Error shutting down transport layer: {e}")
|
|
480
|
-
|
|
481
|
-
def _cleanup_resources(self) -> None:
|
|
482
|
-
"""Clean up all remaining resources"""
|
|
483
|
-
|
|
484
|
-
# Cleanup camera windows
|
|
485
|
-
self._cleanup_camera_windows()
|
|
486
|
-
|
|
487
|
-
# Close WebSocket connections
|
|
488
|
-
if hasattr(self, "world_client") and self.world_client:
|
|
489
|
-
try:
|
|
490
|
-
self.world_client.close()
|
|
491
|
-
self.world_client = None
|
|
492
|
-
except Exception as e:
|
|
493
|
-
logger.debug(f"Error closing world client: {e}")
|
|
494
|
-
|
|
495
|
-
# Clear any remaining state
|
|
496
|
-
self.robot_client = None
|
|
497
|
-
self.world_client = None
|
|
498
|
-
|
|
499
|
-
logger.info("Resource cleanup complete")
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
@app.websocket("/nodes")
|
|
503
|
-
async def nodes_endpoint(websocket: WebSocket) -> None:
|
|
504
|
-
"""WebSocket endpoint for node communication"""
|
|
505
|
-
await websocket.accept()
|
|
506
|
-
node_name = None
|
|
507
|
-
|
|
508
|
-
try:
|
|
509
|
-
# Wait for the first message, which should be NODE_ANNOUNCE
|
|
510
|
-
message = await websocket.receive_bytes()
|
|
511
|
-
message_data = msgpack.unpackb(message)
|
|
512
|
-
message = TransportMessage(**message_data)
|
|
513
|
-
|
|
514
|
-
if message.msg_type != MessageType.NODE_ANNOUNCE:
|
|
515
|
-
logger.warning(
|
|
516
|
-
f"First message from node should be NODE_ANNOUNCE, got {message.msg_type}"
|
|
517
|
-
)
|
|
518
|
-
await websocket.close(4000, "First message must be NODE_ANNOUNCE")
|
|
519
|
-
return
|
|
520
|
-
|
|
521
|
-
# Register the node
|
|
522
|
-
node_name = message.node_name
|
|
523
|
-
await manager.register_node(node_name, websocket)
|
|
524
|
-
|
|
525
|
-
# Message processing loop
|
|
526
|
-
while True:
|
|
527
|
-
try:
|
|
528
|
-
message = await websocket.receive_bytes()
|
|
529
|
-
message_data = msgpack.unpackb(message)
|
|
530
|
-
message = TransportMessage(**message_data)
|
|
531
|
-
|
|
532
|
-
# Process message based on type
|
|
533
|
-
handlers = {
|
|
534
|
-
MessageType.SUBSCRIBE: lambda: manager.subscribe(
|
|
535
|
-
node_name, message.topic_or_service
|
|
536
|
-
),
|
|
537
|
-
MessageType.UNSUBSCRIBE: lambda: manager.unsubscribe(
|
|
538
|
-
node_name, message.topic_or_service
|
|
539
|
-
),
|
|
540
|
-
MessageType.SERVICE_REGISTER: lambda: manager.register_service(
|
|
541
|
-
node_name, message.topic_or_service
|
|
542
|
-
),
|
|
543
|
-
MessageType.SERVICE_UNREGISTER: lambda: manager.unregister_service(
|
|
544
|
-
node_name, message.topic_or_service
|
|
545
|
-
),
|
|
546
|
-
MessageType.NODE_SHUTDOWN: lambda: None, # Will break the loop
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if message.msg_type in handlers:
|
|
550
|
-
if message.msg_type == MessageType.NODE_SHUTDOWN:
|
|
551
|
-
break
|
|
552
|
-
await handlers[message.msg_type]()
|
|
553
|
-
else:
|
|
554
|
-
await manager.route_message(message)
|
|
555
|
-
|
|
556
|
-
except msgpack.UnpackValueError:
|
|
557
|
-
logger.error(f"Received invalid msgpack from {node_name}")
|
|
558
|
-
except Exception as e:
|
|
559
|
-
logger.error(f"Error processing message from {node_name}: {e}")
|
|
560
|
-
|
|
561
|
-
except WebSocketDisconnect:
|
|
562
|
-
logger.info(f"Node {node_name} disconnected")
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
@app.websocket("/world")
|
|
566
|
-
async def world_endpoint(websocket: WebSocket) -> None:
|
|
567
|
-
"""WebSocket endpoint for world client communication"""
|
|
568
|
-
await websocket.accept()
|
|
569
|
-
|
|
570
|
-
if hasattr(app, "lucky_robots"):
|
|
571
|
-
app.lucky_robots.world_client = websocket
|
|
572
|
-
|
|
573
|
-
lucky_robots = app.lucky_robots
|
|
574
|
-
pending_resets = lucky_robots._pending_resets
|
|
575
|
-
pending_steps = lucky_robots._pending_steps
|
|
576
|
-
|
|
577
|
-
try:
|
|
578
|
-
while True:
|
|
579
|
-
try:
|
|
580
|
-
message_bytes = await websocket.receive_bytes()
|
|
581
|
-
message_data = msgpack.unpackb(message_bytes)
|
|
582
|
-
request_type = message_data.get("RequestType")
|
|
583
|
-
request_id = message_data.get("RequestID")
|
|
584
|
-
shared_loop = get_event_loop()
|
|
585
|
-
|
|
586
|
-
if request_type == "reset_response":
|
|
587
|
-
future = pending_resets.get(request_id)
|
|
588
|
-
shared_loop.call_soon_threadsafe(
|
|
589
|
-
lambda: future.set_result(message_data)
|
|
590
|
-
if future is not None and not future.done()
|
|
591
|
-
else None
|
|
592
|
-
)
|
|
593
|
-
shared_loop.call_soon_threadsafe(
|
|
594
|
-
lambda: pending_resets.pop(request_id, None)
|
|
595
|
-
)
|
|
596
|
-
elif request_type == "step_response":
|
|
597
|
-
future = pending_steps.get(request_id)
|
|
598
|
-
shared_loop.call_soon_threadsafe(
|
|
599
|
-
lambda: future.set_result(message_data)
|
|
600
|
-
if future is not None and not future.done()
|
|
601
|
-
else None
|
|
602
|
-
)
|
|
603
|
-
shared_loop.call_soon_threadsafe(
|
|
604
|
-
lambda: pending_resets.pop(request_id, None)
|
|
605
|
-
if request_type == "reset_response"
|
|
606
|
-
else pending_steps.pop(request_id, None)
|
|
607
|
-
)
|
|
608
|
-
else:
|
|
609
|
-
logger.warning(f"Unhandled message type: {request_type}")
|
|
610
|
-
|
|
611
|
-
except WebSocketDisconnect as e:
|
|
612
|
-
logger.info(f"WebSocket disconnected. Code: {e.code}")
|
|
613
|
-
break
|
|
614
|
-
except Exception as e:
|
|
615
|
-
logger.error(f"Message processing error: {type(e).__name__}: {e}")
|
|
616
|
-
break
|
|
617
|
-
|
|
618
|
-
except WebSocketDisconnect:
|
|
619
|
-
pass
|
|
620
|
-
except Exception as e:
|
|
621
|
-
logger.error(f"Critical error in world_endpoint: {type(e).__name__}: {e}")
|
|
622
|
-
finally:
|
|
623
|
-
if hasattr(app, "lucky_robots") and app.lucky_robots.world_client == websocket:
|
|
624
|
-
app.lucky_robots.world_client = None
|