sim2bot 0.1.0__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.
- sim2bot/__init__.py +38 -0
- sim2bot/bridge.py +232 -0
- sim2bot/bridge_server.py +488 -0
- sim2bot/cli.py +292 -0
- sim2bot/client.py +1543 -0
- sim2bot-0.1.0.dist-info/METADATA +226 -0
- sim2bot-0.1.0.dist-info/RECORD +11 -0
- sim2bot-0.1.0.dist-info/WHEEL +5 -0
- sim2bot-0.1.0.dist-info/entry_points.txt +2 -0
- sim2bot-0.1.0.dist-info/licenses/LICENSE +21 -0
- sim2bot-0.1.0.dist-info/top_level.txt +1 -0
sim2bot/bridge_server.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""Packaged Sim2Bot local bridge server.
|
|
2
|
+
|
|
3
|
+
This is the same FastAPI relay used by the development bridge, but kept inside
|
|
4
|
+
the Python package so `sim2bot bridge` and `Robot(auto_bridge=True)` can start it
|
|
5
|
+
without relying on the repository layout.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import ipaddress
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import secrets
|
|
15
|
+
import string
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
18
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
19
|
+
|
|
20
|
+
app = FastAPI(title="Sim2Bot Simulated Robot Bridge")
|
|
21
|
+
|
|
22
|
+
simulators: set[WebSocket] = set()
|
|
23
|
+
controllers: set[WebSocket] = set()
|
|
24
|
+
websocket_rooms: dict[WebSocket, str] = {}
|
|
25
|
+
latest_simulator_by_room: dict[str, WebSocket] = {}
|
|
26
|
+
|
|
27
|
+
tcp_clients: dict[asyncio.StreamWriter, str] = {}
|
|
28
|
+
udp_clients: dict[tuple[str, int], str] = {}
|
|
29
|
+
udp_transport: asyncio.DatagramTransport | None = None
|
|
30
|
+
|
|
31
|
+
COMMAND_TYPES = {
|
|
32
|
+
"joint_position",
|
|
33
|
+
"joint_velocity",
|
|
34
|
+
"tcp_pose",
|
|
35
|
+
"gripper",
|
|
36
|
+
"room_opening",
|
|
37
|
+
"base_velocity",
|
|
38
|
+
"camera_subscribe",
|
|
39
|
+
"camera_unsubscribe",
|
|
40
|
+
"marker",
|
|
41
|
+
"marker_delete",
|
|
42
|
+
"marker_clear",
|
|
43
|
+
"reset",
|
|
44
|
+
"stop",
|
|
45
|
+
"joint_trajectory",
|
|
46
|
+
"joint_trajectory_stop",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
DEFAULT_ROOM = "default"
|
|
50
|
+
MAX_ROOM_LENGTH = 80
|
|
51
|
+
ROOM_CHARS = set(string.ascii_letters + string.digits + "._-")
|
|
52
|
+
|
|
53
|
+
scene_by_room: dict[str, dict[str, list]] = {}
|
|
54
|
+
|
|
55
|
+
video_simulators: set[WebSocket] = set()
|
|
56
|
+
video_controllers: set[WebSocket] = set()
|
|
57
|
+
video_rooms: dict[WebSocket, str] = {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def normalize_room(value: object | None) -> str:
|
|
61
|
+
if not isinstance(value, str):
|
|
62
|
+
return DEFAULT_ROOM
|
|
63
|
+
room = value.strip()
|
|
64
|
+
if not room or len(room) > MAX_ROOM_LENGTH:
|
|
65
|
+
return DEFAULT_ROOM
|
|
66
|
+
if any(ch not in ROOM_CHARS for ch in room):
|
|
67
|
+
return DEFAULT_ROOM
|
|
68
|
+
return room
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def message_room(message: dict, fallback: str = DEFAULT_ROOM) -> str:
|
|
72
|
+
return normalize_room(message.get("room") or fallback)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def websocket_room(websocket: WebSocket, fallback: str = DEFAULT_ROOM) -> str:
|
|
76
|
+
return normalize_room(websocket.query_params.get("room") or fallback)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def scene_message(room: str = DEFAULT_ROOM) -> dict:
|
|
80
|
+
scene = scene_by_room.get(room, {"robots": [], "cameras": [], "devices": []})
|
|
81
|
+
return {
|
|
82
|
+
"type": "scene",
|
|
83
|
+
"room": room,
|
|
84
|
+
"robots": scene["robots"],
|
|
85
|
+
"cameras": scene["cameras"],
|
|
86
|
+
"devices": scene.get("devices", []),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def tcp_port() -> int:
|
|
91
|
+
return int(os.environ.get("BRIDGE_TCP_PORT", "8770"))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def udp_port() -> int:
|
|
95
|
+
return int(os.environ.get("BRIDGE_UDP_PORT", "8771"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def raw_host() -> str:
|
|
99
|
+
return os.environ.get("BRIDGE_RAW_HOST") or os.environ.get("BRIDGE_HOST") or "127.0.0.1"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.get("/health")
|
|
103
|
+
async def health() -> dict[str, str]:
|
|
104
|
+
return {"status": "ok"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def bridge_token() -> str | None:
|
|
108
|
+
return (
|
|
109
|
+
os.environ.get("SIM2BOT_BRIDGE_TOKEN")
|
|
110
|
+
or os.environ.get("BRIDGE_AUTH_TOKEN")
|
|
111
|
+
or os.environ.get("SIM2BOT_API_KEY")
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_loopback_host(host: str | None) -> bool:
|
|
116
|
+
if not host:
|
|
117
|
+
return False
|
|
118
|
+
if host == "localhost":
|
|
119
|
+
return True
|
|
120
|
+
try:
|
|
121
|
+
return ipaddress.ip_address(host).is_loopback
|
|
122
|
+
except ValueError:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def is_loopback_client(websocket: WebSocket) -> bool:
|
|
127
|
+
return is_loopback_host(websocket.client.host if websocket.client else None)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def allowed_origin(websocket: WebSocket) -> bool:
|
|
131
|
+
origin = websocket.headers.get("origin")
|
|
132
|
+
if not origin:
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
allowed = {
|
|
136
|
+
item.strip()
|
|
137
|
+
for item in os.environ.get("BRIDGE_ALLOWED_ORIGINS", "").split(",")
|
|
138
|
+
if item.strip()
|
|
139
|
+
}
|
|
140
|
+
if origin in allowed:
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
parsed = urlparse(origin)
|
|
144
|
+
return is_loopback_host(parsed.hostname)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def websocket_token(websocket: WebSocket) -> str | None:
|
|
148
|
+
for key in ("token", "api_key", "authToken"):
|
|
149
|
+
value = websocket.query_params.get(key)
|
|
150
|
+
if value:
|
|
151
|
+
return value
|
|
152
|
+
auth = websocket.headers.get("authorization", "")
|
|
153
|
+
if auth.lower().startswith("bearer "):
|
|
154
|
+
return auth[7:].strip()
|
|
155
|
+
return websocket.headers.get("x-sim2bot-token")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def token_matches(value: str | None) -> bool:
|
|
159
|
+
expected = bridge_token()
|
|
160
|
+
return bool(expected and value and secrets.compare_digest(value, expected))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def authorize_websocket(websocket: WebSocket) -> bool:
|
|
164
|
+
if not allowed_origin(websocket):
|
|
165
|
+
await websocket.close(code=1008, reason="Origin is not allowed")
|
|
166
|
+
return False
|
|
167
|
+
if is_loopback_client(websocket):
|
|
168
|
+
return True
|
|
169
|
+
if token_matches(websocket_token(websocket)):
|
|
170
|
+
return True
|
|
171
|
+
await websocket.close(code=1008, reason="Remote bridge access requires a token")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def sanitize_message(message: dict) -> dict:
|
|
176
|
+
if not any(key in message for key in ("authToken", "apiKey", "api_key", "token")):
|
|
177
|
+
return message
|
|
178
|
+
cleaned = dict(message)
|
|
179
|
+
for key in ("authToken", "apiKey", "api_key", "token"):
|
|
180
|
+
cleaned.pop(key, None)
|
|
181
|
+
return cleaned
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def message_token(message: dict) -> str | None:
|
|
185
|
+
for key in ("authToken", "apiKey", "api_key", "token"):
|
|
186
|
+
value = message.get(key)
|
|
187
|
+
if isinstance(value, str) and value:
|
|
188
|
+
return value
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def raw_client_authorized(host: str | None, message: dict) -> bool:
|
|
193
|
+
return is_loopback_host(host) or token_matches(message_token(message))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def broadcast(targets, message: dict) -> None:
|
|
197
|
+
dead: list[WebSocket] = []
|
|
198
|
+
for ws in list(targets):
|
|
199
|
+
try:
|
|
200
|
+
await ws.send_json(message)
|
|
201
|
+
except Exception:
|
|
202
|
+
dead.append(ws)
|
|
203
|
+
for ws in dead:
|
|
204
|
+
targets.discard(ws)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def sockets_in_room(sockets: set[WebSocket], rooms: dict[WebSocket, str], room: str) -> set[WebSocket]:
|
|
208
|
+
return {ws for ws in sockets if rooms.get(ws, DEFAULT_ROOM) == room}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def simulator_targets(room: str) -> set[WebSocket]:
|
|
212
|
+
latest = latest_simulator_by_room.get(room)
|
|
213
|
+
if latest in simulators and websocket_rooms.get(latest, DEFAULT_ROOM) == room:
|
|
214
|
+
return {latest}
|
|
215
|
+
return sockets_in_room(simulators, websocket_rooms, room)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def controller_targets(room: str) -> set[WebSocket]:
|
|
219
|
+
return sockets_in_room(controllers, websocket_rooms, room)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def register_websocket(websocket: WebSocket, role: str, room: str) -> None:
|
|
223
|
+
old_room = websocket_rooms.get(websocket)
|
|
224
|
+
simulators.discard(websocket)
|
|
225
|
+
controllers.discard(websocket)
|
|
226
|
+
websocket_rooms[websocket] = room
|
|
227
|
+
if role == "simulator":
|
|
228
|
+
simulators.add(websocket)
|
|
229
|
+
latest_simulator_by_room[room] = websocket
|
|
230
|
+
else:
|
|
231
|
+
controllers.add(websocket)
|
|
232
|
+
if old_room and old_room != room and latest_simulator_by_room.get(old_room) is websocket:
|
|
233
|
+
promote_latest_simulator(old_room)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def promote_latest_simulator(room: str) -> None:
|
|
237
|
+
for ws in list(simulators):
|
|
238
|
+
if websocket_rooms.get(ws, DEFAULT_ROOM) == room:
|
|
239
|
+
latest_simulator_by_room[room] = ws
|
|
240
|
+
return
|
|
241
|
+
latest_simulator_by_room.pop(room, None)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def forward_to_simulators(message: dict, room: str | None = None) -> None:
|
|
245
|
+
if message.get("type") in COMMAND_TYPES:
|
|
246
|
+
asyncio.create_task(broadcast(simulator_targets(room or message_room(message)), message))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def forward_telemetry_to_raw(message: dict, room: str) -> None:
|
|
250
|
+
line = (json.dumps(message) + "\n").encode()
|
|
251
|
+
for writer, client_room in list(tcp_clients.items()):
|
|
252
|
+
if client_room != room:
|
|
253
|
+
continue
|
|
254
|
+
try:
|
|
255
|
+
writer.write(line)
|
|
256
|
+
except Exception:
|
|
257
|
+
tcp_clients.pop(writer, None)
|
|
258
|
+
if udp_transport is not None:
|
|
259
|
+
datagram = json.dumps(message).encode()
|
|
260
|
+
for addr, client_room in list(udp_clients.items()):
|
|
261
|
+
if client_room != room:
|
|
262
|
+
continue
|
|
263
|
+
try:
|
|
264
|
+
udp_transport.sendto(datagram, addr)
|
|
265
|
+
except Exception:
|
|
266
|
+
udp_clients.pop(addr, None)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def broadcast_status(room: str = DEFAULT_ROOM) -> None:
|
|
270
|
+
await broadcast(
|
|
271
|
+
sockets_in_room(simulators | controllers, websocket_rooms, room),
|
|
272
|
+
{
|
|
273
|
+
"type": "bridge_status",
|
|
274
|
+
"room": room,
|
|
275
|
+
"simulators": len(sockets_in_room(simulators, websocket_rooms, room)),
|
|
276
|
+
"controllers": (
|
|
277
|
+
len(sockets_in_room(controllers, websocket_rooms, room))
|
|
278
|
+
+ sum(1 for client_room in tcp_clients.values() if client_room == room)
|
|
279
|
+
+ sum(1 for client_room in udp_clients.values() if client_room == room)
|
|
280
|
+
),
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.websocket("/ws")
|
|
286
|
+
async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
287
|
+
if not await authorize_websocket(websocket):
|
|
288
|
+
return
|
|
289
|
+
await websocket.accept()
|
|
290
|
+
role: str | None = None
|
|
291
|
+
room = websocket_room(websocket)
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
while True:
|
|
295
|
+
message = await websocket.receive_json()
|
|
296
|
+
if not isinstance(message, dict):
|
|
297
|
+
continue
|
|
298
|
+
message = sanitize_message(message)
|
|
299
|
+
msg_type = message.get("type")
|
|
300
|
+
|
|
301
|
+
if msg_type == "hello":
|
|
302
|
+
role = "simulator" if message.get("role") == "simulator" else "controller"
|
|
303
|
+
old_room = room
|
|
304
|
+
room = message_room(message, room)
|
|
305
|
+
register_websocket(websocket, role, room)
|
|
306
|
+
if role == "simulator" and message.get("robots"):
|
|
307
|
+
scene_by_room[room] = {
|
|
308
|
+
"robots": message.get("robots") or [],
|
|
309
|
+
"cameras": message.get("cameras") or [],
|
|
310
|
+
"devices": message.get("devices") or [],
|
|
311
|
+
}
|
|
312
|
+
if old_room != room:
|
|
313
|
+
await broadcast_status(old_room)
|
|
314
|
+
await broadcast_status(room)
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
if msg_type == "describe":
|
|
318
|
+
await websocket.send_json(scene_message(message_room(message, room)))
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
if role is None:
|
|
322
|
+
role = "simulator" if msg_type == "telemetry" else "controller"
|
|
323
|
+
room = message_room(message, room)
|
|
324
|
+
register_websocket(websocket, role, room)
|
|
325
|
+
await broadcast_status(room)
|
|
326
|
+
|
|
327
|
+
if role == "controller" and msg_type in COMMAND_TYPES:
|
|
328
|
+
await broadcast(simulator_targets(message_room(message, room)), message)
|
|
329
|
+
elif role == "simulator" and msg_type == "telemetry":
|
|
330
|
+
await broadcast(controller_targets(room), message)
|
|
331
|
+
forward_telemetry_to_raw(message, room)
|
|
332
|
+
except WebSocketDisconnect:
|
|
333
|
+
pass
|
|
334
|
+
finally:
|
|
335
|
+
room = websocket_rooms.pop(websocket, room)
|
|
336
|
+
simulators.discard(websocket)
|
|
337
|
+
controllers.discard(websocket)
|
|
338
|
+
if latest_simulator_by_room.get(room) is websocket:
|
|
339
|
+
promote_latest_simulator(room)
|
|
340
|
+
if not sockets_in_room(simulators, websocket_rooms, room):
|
|
341
|
+
scene_by_room.pop(room, None)
|
|
342
|
+
await broadcast_status(room)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@app.websocket("/video")
|
|
346
|
+
async def video_endpoint(websocket: WebSocket) -> None:
|
|
347
|
+
if not await authorize_websocket(websocket):
|
|
348
|
+
return
|
|
349
|
+
await websocket.accept()
|
|
350
|
+
role: str | None = None
|
|
351
|
+
room = websocket_room(websocket)
|
|
352
|
+
try:
|
|
353
|
+
while True:
|
|
354
|
+
message = await websocket.receive()
|
|
355
|
+
if message["type"] == "websocket.disconnect":
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
text = message.get("text")
|
|
359
|
+
if text is not None:
|
|
360
|
+
try:
|
|
361
|
+
data = json.loads(text)
|
|
362
|
+
except ValueError:
|
|
363
|
+
continue
|
|
364
|
+
if isinstance(data, dict) and data.get("type") == "hello":
|
|
365
|
+
room = message_room(data, room)
|
|
366
|
+
role = (
|
|
367
|
+
"simulator-video"
|
|
368
|
+
if data.get("role") == "simulator-video"
|
|
369
|
+
else "controller-video"
|
|
370
|
+
)
|
|
371
|
+
(video_simulators if role == "simulator-video" else video_controllers).add(
|
|
372
|
+
websocket
|
|
373
|
+
)
|
|
374
|
+
video_rooms[websocket] = room
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
frame = message.get("bytes")
|
|
378
|
+
if frame is None:
|
|
379
|
+
continue
|
|
380
|
+
if role is None:
|
|
381
|
+
role = "simulator-video"
|
|
382
|
+
video_simulators.add(websocket)
|
|
383
|
+
video_rooms[websocket] = room
|
|
384
|
+
if role == "simulator-video":
|
|
385
|
+
dead: list[WebSocket] = []
|
|
386
|
+
for ws in list(video_controllers):
|
|
387
|
+
if video_rooms.get(ws, DEFAULT_ROOM) != room:
|
|
388
|
+
continue
|
|
389
|
+
try:
|
|
390
|
+
await ws.send_bytes(frame)
|
|
391
|
+
except Exception:
|
|
392
|
+
dead.append(ws)
|
|
393
|
+
for ws in dead:
|
|
394
|
+
video_controllers.discard(ws)
|
|
395
|
+
except WebSocketDisconnect:
|
|
396
|
+
pass
|
|
397
|
+
finally:
|
|
398
|
+
video_rooms.pop(websocket, None)
|
|
399
|
+
video_simulators.discard(websocket)
|
|
400
|
+
video_controllers.discard(websocket)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
async def handle_tcp_client(
|
|
404
|
+
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
405
|
+
) -> None:
|
|
406
|
+
peer = writer.get_extra_info("peername")
|
|
407
|
+
peer_host = peer[0] if isinstance(peer, tuple) and peer else None
|
|
408
|
+
authorized = is_loopback_host(peer_host)
|
|
409
|
+
tcp_clients[writer] = DEFAULT_ROOM
|
|
410
|
+
await broadcast_status(DEFAULT_ROOM)
|
|
411
|
+
try:
|
|
412
|
+
while True:
|
|
413
|
+
line = await reader.readline()
|
|
414
|
+
if not line:
|
|
415
|
+
break
|
|
416
|
+
try:
|
|
417
|
+
message = json.loads(line.decode())
|
|
418
|
+
except (ValueError, UnicodeDecodeError):
|
|
419
|
+
continue
|
|
420
|
+
if isinstance(message, dict):
|
|
421
|
+
old_room = tcp_clients.get(writer, DEFAULT_ROOM)
|
|
422
|
+
room = message_room(message, old_room)
|
|
423
|
+
if room != old_room:
|
|
424
|
+
tcp_clients[writer] = room
|
|
425
|
+
await broadcast_status(old_room)
|
|
426
|
+
await broadcast_status(room)
|
|
427
|
+
if not authorized:
|
|
428
|
+
authorized = token_matches(message_token(message))
|
|
429
|
+
if not authorized:
|
|
430
|
+
writer.write(b'{"type":"error","error":"unauthorized"}\n')
|
|
431
|
+
await writer.drain()
|
|
432
|
+
break
|
|
433
|
+
message = sanitize_message(message)
|
|
434
|
+
if message.get("type") == "describe":
|
|
435
|
+
writer.write((json.dumps(scene_message(room)) + "\n").encode())
|
|
436
|
+
else:
|
|
437
|
+
forward_to_simulators(message, room)
|
|
438
|
+
except (ConnectionError, asyncio.IncompleteReadError):
|
|
439
|
+
pass
|
|
440
|
+
finally:
|
|
441
|
+
room = tcp_clients.pop(writer, DEFAULT_ROOM)
|
|
442
|
+
try:
|
|
443
|
+
writer.close()
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
446
|
+
await broadcast_status(room)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class CommandUDPProtocol(asyncio.DatagramProtocol):
|
|
450
|
+
def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
|
|
451
|
+
try:
|
|
452
|
+
message = json.loads(data.decode())
|
|
453
|
+
except (ValueError, UnicodeDecodeError):
|
|
454
|
+
return
|
|
455
|
+
if not isinstance(message, dict):
|
|
456
|
+
return
|
|
457
|
+
if not raw_client_authorized(addr[0], message):
|
|
458
|
+
return
|
|
459
|
+
room = message_room(message, udp_clients.get(addr, DEFAULT_ROOM))
|
|
460
|
+
message = sanitize_message(message)
|
|
461
|
+
udp_clients[addr] = room
|
|
462
|
+
if message.get("type") == "describe":
|
|
463
|
+
if udp_transport is not None:
|
|
464
|
+
udp_transport.sendto(json.dumps(scene_message(room)).encode(), addr)
|
|
465
|
+
return
|
|
466
|
+
forward_to_simulators(message, room)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@app.on_event("startup")
|
|
470
|
+
async def start_raw_endpoints() -> None:
|
|
471
|
+
global udp_transport
|
|
472
|
+
loop = asyncio.get_running_loop()
|
|
473
|
+
|
|
474
|
+
tcp_server = await asyncio.start_server(handle_tcp_client, raw_host(), tcp_port())
|
|
475
|
+
app.state.tcp_server = tcp_server
|
|
476
|
+
|
|
477
|
+
udp_transport, _ = await loop.create_datagram_endpoint(
|
|
478
|
+
CommandUDPProtocol, local_addr=(raw_host(), udp_port())
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@app.on_event("shutdown")
|
|
483
|
+
async def stop_raw_endpoints() -> None:
|
|
484
|
+
server = getattr(app.state, "tcp_server", None)
|
|
485
|
+
if server is not None:
|
|
486
|
+
server.close()
|
|
487
|
+
if udp_transport is not None:
|
|
488
|
+
udp_transport.close()
|