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/cli.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Command line interface for the Sim2Bot Python package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import sys
|
|
11
|
+
import webbrowser
|
|
12
|
+
|
|
13
|
+
from websockets.exceptions import ConnectionClosed
|
|
14
|
+
from websockets.sync.client import connect as ws_connect
|
|
15
|
+
|
|
16
|
+
from .bridge import (
|
|
17
|
+
DEFAULT_CONTROL_URL,
|
|
18
|
+
DEFAULT_TCP_PORT,
|
|
19
|
+
DEFAULT_UDP_PORT,
|
|
20
|
+
bridge_info,
|
|
21
|
+
health_url,
|
|
22
|
+
is_bridge_running,
|
|
23
|
+
video_url,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# The hosted app is the right default for an installed package. Sim2Bot's own
|
|
27
|
+
# developers point this at their dev server with SIM2BOT_APP_URL.
|
|
28
|
+
DEFAULT_APP_URL = os.environ.get("SIM2BOT_APP_URL", "https://app.sim2bot.com")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main(argv: list[str] | None = None) -> int:
|
|
32
|
+
parser = build_parser()
|
|
33
|
+
args = parser.parse_args(argv)
|
|
34
|
+
if not args.command:
|
|
35
|
+
parser.print_help()
|
|
36
|
+
return 0
|
|
37
|
+
return int(args.func(args) or 0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
41
|
+
parser = argparse.ArgumentParser(prog="sim2bot", description="Sim2Bot SDK tools")
|
|
42
|
+
sub = parser.add_subparsers(dest="command")
|
|
43
|
+
|
|
44
|
+
bridge = sub.add_parser("bridge", help="run the local Sim2Bot bridge")
|
|
45
|
+
bridge.add_argument("--host", default=os.environ.get("BRIDGE_HOST", "127.0.0.1"))
|
|
46
|
+
bridge.add_argument("--port", type=int, default=int(os.environ.get("BRIDGE_PORT", "8765")))
|
|
47
|
+
bridge.add_argument(
|
|
48
|
+
"--tcp-port", type=int, default=int(os.environ.get("BRIDGE_TCP_PORT", DEFAULT_TCP_PORT))
|
|
49
|
+
)
|
|
50
|
+
bridge.add_argument(
|
|
51
|
+
"--udp-port", type=int, default=int(os.environ.get("BRIDGE_UDP_PORT", DEFAULT_UDP_PORT))
|
|
52
|
+
)
|
|
53
|
+
bridge.add_argument("--log-level", default="info")
|
|
54
|
+
bridge.set_defaults(func=run_bridge)
|
|
55
|
+
|
|
56
|
+
doctor = sub.add_parser("doctor", help="check bridge, browser, and SDK connectivity")
|
|
57
|
+
doctor.add_argument("--url", default=DEFAULT_CONTROL_URL)
|
|
58
|
+
doctor.add_argument("--room", default=os.environ.get("SIM2BOT_BRIDGE_ROOM"))
|
|
59
|
+
doctor.add_argument("--tcp-port", type=int, default=DEFAULT_TCP_PORT)
|
|
60
|
+
doctor.add_argument("--udp-port", type=int, default=DEFAULT_UDP_PORT)
|
|
61
|
+
doctor.add_argument("--timeout", type=float, default=2.0)
|
|
62
|
+
doctor.set_defaults(func=run_doctor)
|
|
63
|
+
|
|
64
|
+
list_robots = sub.add_parser("list-robots", help="list robots announced by the browser sim")
|
|
65
|
+
list_robots.add_argument("--url", default=DEFAULT_CONTROL_URL)
|
|
66
|
+
list_robots.add_argument("--room", default=os.environ.get("SIM2BOT_BRIDGE_ROOM"))
|
|
67
|
+
list_robots.add_argument("--timeout", type=float, default=30.0)
|
|
68
|
+
list_robots.add_argument("--json", action="store_true", help="print machine-readable JSON")
|
|
69
|
+
list_robots.set_defaults(func=run_list_robots)
|
|
70
|
+
|
|
71
|
+
open_cmd = sub.add_parser("open", help="open the Sim2Bot web app")
|
|
72
|
+
open_cmd.add_argument("--url", default=DEFAULT_APP_URL)
|
|
73
|
+
open_cmd.set_defaults(func=run_open)
|
|
74
|
+
|
|
75
|
+
return parser
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run_bridge(args: argparse.Namespace) -> int:
|
|
79
|
+
os.environ["BRIDGE_TCP_PORT"] = str(args.tcp_port)
|
|
80
|
+
os.environ["BRIDGE_UDP_PORT"] = str(args.udp_port)
|
|
81
|
+
os.environ["BRIDGE_RAW_HOST"] = args.host
|
|
82
|
+
|
|
83
|
+
import uvicorn
|
|
84
|
+
|
|
85
|
+
url = f"ws://{_display_host(args.host)}:{args.port}/ws"
|
|
86
|
+
print("Sim2Bot bridge running")
|
|
87
|
+
print(f"Control WS: {url}")
|
|
88
|
+
print(f"Video WS: {video_url(url)}")
|
|
89
|
+
print(f"TCP: {_display_host(args.host)}:{args.tcp_port}")
|
|
90
|
+
print(f"UDP: {_display_host(args.host)}:{args.udp_port}")
|
|
91
|
+
if args.host not in {"127.0.0.1", "localhost", "::1"} and not (
|
|
92
|
+
os.environ.get("SIM2BOT_BRIDGE_TOKEN")
|
|
93
|
+
or os.environ.get("BRIDGE_AUTH_TOKEN")
|
|
94
|
+
or os.environ.get("SIM2BOT_API_KEY")
|
|
95
|
+
):
|
|
96
|
+
print("LAN clients need SIM2BOT_BRIDGE_TOKEN or BRIDGE_AUTH_TOKEN.")
|
|
97
|
+
print("Waiting for browser simulator...")
|
|
98
|
+
uvicorn.run(
|
|
99
|
+
"sim2bot.bridge_server:app",
|
|
100
|
+
host=args.host,
|
|
101
|
+
port=args.port,
|
|
102
|
+
log_level=args.log_level,
|
|
103
|
+
)
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_doctor(args: argparse.Namespace) -> int:
|
|
108
|
+
info = bridge_info(args.url, tcp_port=args.tcp_port, udp_port=args.udp_port)
|
|
109
|
+
print("Sim2Bot doctor")
|
|
110
|
+
print("")
|
|
111
|
+
print(f"SDK: {_version()}")
|
|
112
|
+
print(f"Control: {info.url}")
|
|
113
|
+
print(f"Video: {info.video_url}")
|
|
114
|
+
print(f"Health: {info.health_url}")
|
|
115
|
+
|
|
116
|
+
if not is_bridge_running(args.url, timeout=args.timeout):
|
|
117
|
+
print("Bridge: not running")
|
|
118
|
+
print("")
|
|
119
|
+
print("Start it with `sim2bot bridge`, or use `Robot(auto_bridge=True)`.")
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
print("Bridge: running")
|
|
123
|
+
_check_scene(args)
|
|
124
|
+
_check_video(args)
|
|
125
|
+
_check_tcp(info, args.timeout, args.room)
|
|
126
|
+
_check_udp(info, args.timeout, args.room)
|
|
127
|
+
_check_cv2()
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def run_list_robots(args: argparse.Namespace) -> int:
|
|
132
|
+
from .client import Robot
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
with Robot(
|
|
136
|
+
url=args.url,
|
|
137
|
+
auto_bridge=True,
|
|
138
|
+
wait_for_sim=True,
|
|
139
|
+
wait_for_sim_timeout=args.timeout,
|
|
140
|
+
room=args.room,
|
|
141
|
+
) as robot:
|
|
142
|
+
robots = robot.describe()
|
|
143
|
+
except TimeoutError:
|
|
144
|
+
print("No robots found.")
|
|
145
|
+
print("Open the Sim2Bot web app, then Tools -> Bridge -> Connect.")
|
|
146
|
+
return 1
|
|
147
|
+
|
|
148
|
+
if args.json:
|
|
149
|
+
print(json.dumps([_robot_info_dict(robot) for robot in robots], indent=2))
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
print(f"Robots: {len(robots)}")
|
|
153
|
+
for robot in robots:
|
|
154
|
+
_print_robot_info(robot)
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def run_open(args: argparse.Namespace) -> int:
|
|
159
|
+
print(f"Opening {args.url}")
|
|
160
|
+
ok = webbrowser.open(args.url)
|
|
161
|
+
if not ok:
|
|
162
|
+
print("Could not open a browser automatically.")
|
|
163
|
+
return 1
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _check_scene(args: argparse.Namespace) -> None:
|
|
168
|
+
try:
|
|
169
|
+
with ws_connect(args.url, open_timeout=args.timeout) as ws:
|
|
170
|
+
ws.send(json.dumps(_with_room({"type": "hello", "role": "controller"}, args.room)))
|
|
171
|
+
ws.send(json.dumps(_with_room({"type": "describe"}, args.room)))
|
|
172
|
+
scene = _recv_type(ws, "scene", args.timeout)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
print(f"Browser: control WS failed ({exc})")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
robots = scene.get("robots", [])
|
|
178
|
+
cameras = scene.get("cameras", [])
|
|
179
|
+
if robots:
|
|
180
|
+
print(f"Browser: connected ({len(robots)} robot(s))")
|
|
181
|
+
for robot in robots:
|
|
182
|
+
print(
|
|
183
|
+
" "
|
|
184
|
+
f"[{robot.get('index', 0)}] {robot.get('name', '')}, "
|
|
185
|
+
f"id={robot.get('id') or '(none)'}, "
|
|
186
|
+
f"dof={robot.get('dof', '?')}, "
|
|
187
|
+
f"gripper={'yes' if robot.get('hasGripper') else 'no'}"
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
print("Browser: no simulator scene announced yet")
|
|
191
|
+
print(f"Cameras: {len(cameras)} available")
|
|
192
|
+
for camera in cameras:
|
|
193
|
+
print(f" {camera.get('id')} ({camera.get('label', camera.get('kind', 'camera'))})")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _check_video(args: argparse.Namespace) -> None:
|
|
197
|
+
try:
|
|
198
|
+
with ws_connect(video_url(args.url), open_timeout=args.timeout, max_size=None) as ws:
|
|
199
|
+
ws.send(json.dumps(_with_room({"type": "hello", "role": "controller-video"}, args.room)))
|
|
200
|
+
print("Video: reachable")
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
print(f"Video: failed ({exc})")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _check_tcp(info, timeout: float, room: str | None = None) -> None:
|
|
206
|
+
try:
|
|
207
|
+
with socket.create_connection((info.host, info.tcp_port), timeout=timeout) as sock:
|
|
208
|
+
sock.settimeout(timeout)
|
|
209
|
+
sock.sendall(json.dumps(_with_room({"type": "describe"}, room)).encode() + b"\n")
|
|
210
|
+
sock.recv(65535)
|
|
211
|
+
print("TCP: reachable")
|
|
212
|
+
except OSError as exc:
|
|
213
|
+
print(f"TCP: failed ({exc})")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _check_udp(info, timeout: float, room: str | None = None) -> None:
|
|
217
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
218
|
+
sock.settimeout(timeout)
|
|
219
|
+
try:
|
|
220
|
+
sock.sendto(json.dumps(_with_room({"type": "describe"}, room)).encode(), (info.host, info.udp_port))
|
|
221
|
+
sock.recvfrom(65535)
|
|
222
|
+
print("UDP: reachable")
|
|
223
|
+
except OSError as exc:
|
|
224
|
+
print(f"UDP: failed ({exc})")
|
|
225
|
+
finally:
|
|
226
|
+
sock.close()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _check_cv2() -> None:
|
|
230
|
+
try:
|
|
231
|
+
import cv2 # type: ignore # noqa: F401
|
|
232
|
+
import numpy # type: ignore # noqa: F401
|
|
233
|
+
except ImportError:
|
|
234
|
+
print("OpenCV: not installed (install sim2bot[cv2] for frame.image())")
|
|
235
|
+
return
|
|
236
|
+
print("OpenCV: installed")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _recv_type(ws, want_type: str, timeout: float) -> dict:
|
|
240
|
+
while True:
|
|
241
|
+
try:
|
|
242
|
+
raw = ws.recv(timeout=timeout)
|
|
243
|
+
except (TimeoutError, ConnectionClosed) as exc:
|
|
244
|
+
raise RuntimeError(f"no {want_type!r} response") from exc
|
|
245
|
+
message = json.loads(raw)
|
|
246
|
+
if message.get("type") == want_type:
|
|
247
|
+
return message
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _with_room(message: dict, room: str | None) -> dict:
|
|
251
|
+
return {**message, "room": room} if room else message
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _display_host(host: str) -> str:
|
|
255
|
+
return "localhost" if host in {"0.0.0.0", "::"} else host
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _print_robot_info(robot) -> None:
|
|
259
|
+
print(f"[{robot.index}] {robot.name}")
|
|
260
|
+
print(f" id: {robot.id or '(not announced)'}")
|
|
261
|
+
print(f" dof: {robot.dof}")
|
|
262
|
+
print(f" joints: {robot.joint_names or '(not announced)'}")
|
|
263
|
+
print(f" home: {robot.home or '(not announced)'}")
|
|
264
|
+
print(f" gripper: {robot.has_gripper}")
|
|
265
|
+
print(f" locomotion: {robot.locomotion}")
|
|
266
|
+
print(f" base_dof: {robot.base_dof}")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _robot_info_dict(robot) -> dict:
|
|
270
|
+
return {
|
|
271
|
+
"index": robot.index,
|
|
272
|
+
"id": robot.id,
|
|
273
|
+
"name": robot.name,
|
|
274
|
+
"dof": robot.dof,
|
|
275
|
+
"joint_names": robot.joint_names,
|
|
276
|
+
"joint_limits": robot.joint_limits,
|
|
277
|
+
"home": robot.home,
|
|
278
|
+
"has_gripper": robot.has_gripper,
|
|
279
|
+
"locomotion": robot.locomotion,
|
|
280
|
+
"base_dof": robot.base_dof,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _version() -> str:
|
|
285
|
+
try:
|
|
286
|
+
return importlib.metadata.version("sim2bot")
|
|
287
|
+
except importlib.metadata.PackageNotFoundError:
|
|
288
|
+
return "editable/local"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
raise SystemExit(main(sys.argv[1:]))
|