dropbear 0.1.0a1__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.
- dropbear/__init__.py +41 -0
- dropbear/arm.py +431 -0
- dropbear/assets/__init__.py +0 -0
- dropbear/assets/camera_reference.jpg +0 -0
- dropbear/assets/camera_reference.svg +82 -0
- dropbear/bench.py +236 -0
- dropbear/calibrate.py +52 -0
- dropbear/cli.py +320 -0
- dropbear/config.py +116 -0
- dropbear/constants.py +78 -0
- dropbear/control.py +233 -0
- dropbear/demo.py +66 -0
- dropbear/doctor.py +385 -0
- dropbear/errors.py +316 -0
- dropbear/franka.py +105 -0
- dropbear/hardware/__init__.py +0 -0
- dropbear/hardware/cameras.py +41 -0
- dropbear/hardware/fake.py +64 -0
- dropbear/hardware/lerobot_so101.py +122 -0
- dropbear/hardware/ports.py +39 -0
- dropbear/hardware/protocols.py +28 -0
- dropbear/libero.py +67 -0
- dropbear/motion/__init__.py +0 -0
- dropbear/motion/ensemble.py +91 -0
- dropbear/motion/loop.py +258 -0
- dropbear/motion/safety.py +112 -0
- dropbear/optional.py +15 -0
- dropbear/policy/__init__.py +50 -0
- dropbear/policy/config.py +176 -0
- dropbear/policy/lifecycle.py +552 -0
- dropbear/policy/remote.py +1202 -0
- dropbear/policy/run_loop.py +1187 -0
- dropbear/policy/runtime/__init__.py +1 -0
- dropbear/policy/runtime/action_buffer.py +251 -0
- dropbear/policy/runtime/calibration.py +489 -0
- dropbear/policy/runtime/contract.py +126 -0
- dropbear/policy/runtime/control.py +393 -0
- dropbear/policy/runtime/rtc.py +774 -0
- dropbear/policy/types.py +169 -0
- dropbear/quickstart.py +397 -0
- dropbear/region_probe.py +187 -0
- dropbear/sessions.py +242 -0
- dropbear/sessions_cmd.py +130 -0
- dropbear/sim/__init__.py +0 -0
- dropbear/sim/artifacts.py +88 -0
- dropbear/sim/metrics.py +336 -0
- dropbear/sim/runtime.py +401 -0
- dropbear/sim/sim_arm.py +1392 -0
- dropbear/sim/timing.py +100 -0
- dropbear/so101.py +79 -0
- dropbear/state.py +93 -0
- dropbear/status.py +168 -0
- dropbear/terminal.py +351 -0
- dropbear/transport_selection.py +141 -0
- dropbear/wire.py +179 -0
- dropbear-0.1.0a1.dist-info/METADATA +223 -0
- dropbear-0.1.0a1.dist-info/RECORD +60 -0
- dropbear-0.1.0a1.dist-info/WHEEL +4 -0
- dropbear-0.1.0a1.dist-info/entry_points.txt +2 -0
- dropbear-0.1.0a1.dist-info/licenses/LICENSE +176 -0
dropbear/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Dropbear - fast serverless inference for your robots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("dropbear")
|
|
10
|
+
except PackageNotFoundError: # Direct source import outside an installed environment.
|
|
11
|
+
__version__ = "0+unknown"
|
|
12
|
+
|
|
13
|
+
from dropbear import franka, libero, so101
|
|
14
|
+
from dropbear.policy import RegionPreference, RunHooks, RunStrategy, aconnect, connect
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from dropbear.arm import Arm
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def connect_so101(*, region: RegionPreference = "nearest") -> Arm:
|
|
21
|
+
"""Connect to the legacy physical SO-101 control path.
|
|
22
|
+
|
|
23
|
+
Prefer dropbear.connect(model="molmoact2-so101", ...) for new cloud-policy work.
|
|
24
|
+
"""
|
|
25
|
+
from dropbear.arm import connect_so101 as _connect
|
|
26
|
+
|
|
27
|
+
return _connect(region=region)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"RunHooks",
|
|
32
|
+
"RunStrategy",
|
|
33
|
+
"RegionPreference",
|
|
34
|
+
"aconnect",
|
|
35
|
+
"connect",
|
|
36
|
+
"connect_so101",
|
|
37
|
+
"franka",
|
|
38
|
+
"libero",
|
|
39
|
+
"so101",
|
|
40
|
+
"__version__",
|
|
41
|
+
]
|
dropbear/arm.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""connect_so101() and the Arm handle (spec §8).
|
|
2
|
+
|
|
3
|
+
Zero args: ~/.dropbear/config.toml supplies everything. Owns session acquisition
|
|
4
|
+
(reusing quickstart's pre-warm), UDP preflight, pinned QUIC connect, the LeRobot
|
|
5
|
+
hardware adapter, the 30 Hz safety loop, heartbeats, and reconnect-on-freeze.
|
|
6
|
+
Dependency-injection and tuning knobs live behind _connect_so101_internal().
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import inspect
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from contextlib import suppress
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from dropbear import errors, wire
|
|
22
|
+
from dropbear.config import Config, load_config
|
|
23
|
+
from dropbear.constants import FIRST_ACTION_TIMEOUT_S, MODEL_SO101
|
|
24
|
+
from dropbear.control import ControlPlaneClient, Session
|
|
25
|
+
from dropbear.motion.loop import ControlLoop, LoopResult
|
|
26
|
+
from dropbear.motion.safety import EStop, SpacebarEStop, install_sigint_estop
|
|
27
|
+
from dropbear.policy.lifecycle import _LoopRunner
|
|
28
|
+
from dropbear.policy.types import RegionPreference
|
|
29
|
+
from dropbear.sessions import Heartbeat, acquire_session, stop_session
|
|
30
|
+
from dropbear.state import load_state
|
|
31
|
+
from dropbear.transport_selection import connect_selected_transport
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _default_transport_connect(addr: str, port: int, *, cert_fingerprint: str,
|
|
35
|
+
session_id: str, session_token: str):
|
|
36
|
+
from dropbear_transport import QuicClient
|
|
37
|
+
|
|
38
|
+
return QuicClient.connect(addr, port, cert_fingerprint=cert_fingerprint,
|
|
39
|
+
session_id=session_id, session_token=session_token)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _default_confirm_retry() -> bool:
|
|
43
|
+
answer = input("AWS is out of GPU capacity. Auto-retry with backoff? [Y/n] ")
|
|
44
|
+
return answer.strip().lower() != "n"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _default_doctor_runner() -> None:
|
|
48
|
+
from dropbear.doctor import assert_motion_ready
|
|
49
|
+
|
|
50
|
+
assert_motion_ready()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def connect_so101(
|
|
54
|
+
*,
|
|
55
|
+
region: RegionPreference = "nearest",
|
|
56
|
+
) -> Arm:
|
|
57
|
+
"""Connect to the physical SO-101 compatibility control loop."""
|
|
58
|
+
return _connect_so101_internal(region=region)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _connect_so101_internal(
|
|
62
|
+
*,
|
|
63
|
+
client_factory: Callable[..., Any] = ControlPlaneClient,
|
|
64
|
+
transport_connect: Callable[..., Any] = _default_transport_connect,
|
|
65
|
+
robot_factory: Callable[[], Any] | None = None,
|
|
66
|
+
preflight_fn: Callable[..., Any] | None = None,
|
|
67
|
+
allow_fallback: bool = True,
|
|
68
|
+
relay_connect: Callable[..., Any] | None = None,
|
|
69
|
+
doctor_runner: Callable[[], None] = _default_doctor_runner,
|
|
70
|
+
on_line: Callable[[str], None] = print,
|
|
71
|
+
confirm_retry: Callable[[], bool] = _default_confirm_retry,
|
|
72
|
+
duration_default_s: float = 60.0,
|
|
73
|
+
first_action_timeout_s: float = FIRST_ACTION_TIMEOUT_S,
|
|
74
|
+
region: RegionPreference = "nearest",
|
|
75
|
+
idle_timeout: int | None = None,
|
|
76
|
+
) -> Arm:
|
|
77
|
+
"""Internal SO-101 connector with dependency-injection hooks for tests/tools."""
|
|
78
|
+
config = load_config()
|
|
79
|
+
if not config.api_key:
|
|
80
|
+
raise errors.catalog("not_configured")
|
|
81
|
+
if robot_factory is None:
|
|
82
|
+
robot_factory = _make_default_robot_factory(config)
|
|
83
|
+
if preflight_fn is None:
|
|
84
|
+
from dropbear_transport import diagnose_udp as preflight_fn
|
|
85
|
+
preflight_fn = _compatible_diagnose_udp(preflight_fn)
|
|
86
|
+
|
|
87
|
+
runner = _LoopRunner()
|
|
88
|
+
try:
|
|
89
|
+
return runner.run(_connect_async(
|
|
90
|
+
runner=runner, config=config, client_factory=client_factory,
|
|
91
|
+
transport_connect=transport_connect, robot_factory=robot_factory,
|
|
92
|
+
preflight_fn=preflight_fn, allow_fallback=allow_fallback,
|
|
93
|
+
relay_connect=relay_connect, doctor_runner=doctor_runner, on_line=on_line,
|
|
94
|
+
confirm_retry=confirm_retry, duration_default_s=duration_default_s,
|
|
95
|
+
first_action_timeout_s=first_action_timeout_s,
|
|
96
|
+
region=region, idle_timeout=idle_timeout,
|
|
97
|
+
))
|
|
98
|
+
except BaseException:
|
|
99
|
+
runner.shutdown()
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _make_default_robot_factory(config: Config) -> Callable[[], Any]:
|
|
104
|
+
def factory():
|
|
105
|
+
from dropbear.hardware.lerobot_so101 import create_so101
|
|
106
|
+
from dropbear.optional import require_so101_extra
|
|
107
|
+
|
|
108
|
+
require_so101_extra()
|
|
109
|
+
if config.robot_port is None:
|
|
110
|
+
raise errors.catalog("robot_port_not_found", port="(none configured)")
|
|
111
|
+
if config.camera_index is None or config.wrist_camera_index is None:
|
|
112
|
+
raise errors.catalog("camera_pair_not_configured")
|
|
113
|
+
return create_so101(port=config.robot_port,
|
|
114
|
+
side_camera_index=config.camera_index,
|
|
115
|
+
wrist_camera_index=config.wrist_camera_index,
|
|
116
|
+
calibration_path=config.calibration_path)
|
|
117
|
+
|
|
118
|
+
return factory
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def _connect_async(*, runner, config, client_factory, transport_connect,
|
|
122
|
+
robot_factory, preflight_fn, allow_fallback,
|
|
123
|
+
relay_connect, doctor_runner, on_line, confirm_retry,
|
|
124
|
+
duration_default_s, first_action_timeout_s,
|
|
125
|
+
region="nearest", idle_timeout=None) -> Arm:
|
|
126
|
+
client = None
|
|
127
|
+
session: Session | None = None
|
|
128
|
+
transport = None
|
|
129
|
+
robot = None
|
|
130
|
+
robot_connect_attempted = False
|
|
131
|
+
heartbeat: Heartbeat | None = None
|
|
132
|
+
try:
|
|
133
|
+
client = client_factory(config.control_plane_url, config.api_key)
|
|
134
|
+
session = await acquire_session(client, MODEL_SO101, on_progress=on_line,
|
|
135
|
+
region=region, idle_timeout=idle_timeout,
|
|
136
|
+
confirm_retry=confirm_retry)
|
|
137
|
+
|
|
138
|
+
async def quic_connect(ready_session: Session):
|
|
139
|
+
return await _pin_connect(transport_connect, ready_session)
|
|
140
|
+
|
|
141
|
+
async def relay_connect_for_session(ready_session: Session):
|
|
142
|
+
connect = relay_connect or getattr(client, "connect_relay", None)
|
|
143
|
+
if connect is None:
|
|
144
|
+
raise errors.catalog(
|
|
145
|
+
"relay_unavailable",
|
|
146
|
+
retry_command="dropbear run pick-place",
|
|
147
|
+
)
|
|
148
|
+
return await connect(ready_session)
|
|
149
|
+
|
|
150
|
+
transport = await connect_selected_transport(
|
|
151
|
+
session,
|
|
152
|
+
diagnose_udp_fn=preflight_fn,
|
|
153
|
+
quic_connect=quic_connect,
|
|
154
|
+
relay_connect=relay_connect_for_session,
|
|
155
|
+
allow_fallback=allow_fallback,
|
|
156
|
+
on_line=on_line,
|
|
157
|
+
report_transport_fn=getattr(client, "report_transport", None),
|
|
158
|
+
)
|
|
159
|
+
robot = robot_factory()
|
|
160
|
+
robot_connect_attempted = True
|
|
161
|
+
await asyncio.to_thread(robot.connect)
|
|
162
|
+
# Pre-warm the JPEG encoder: wire.encode_jpeg lazily imports cv2, whose
|
|
163
|
+
# first call costs ~200-700 ms cold. Python's import lock holds the GIL
|
|
164
|
+
# through the dlopen, so asyncio.to_thread cannot keep that one-time cost
|
|
165
|
+
# off the event loop — it would starve the 30 Hz executor on the first
|
|
166
|
+
# do(). Pay it here, before any timed loop exists.
|
|
167
|
+
await asyncio.to_thread(wire.encode_jpeg, np.zeros((8, 8, 3), dtype=np.uint8))
|
|
168
|
+
heartbeat = Heartbeat(client, session.session_id)
|
|
169
|
+
heartbeat.start()
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
status = await client.status()
|
|
173
|
+
active = int(status.get(MODEL_SO101, {}).get("active_sessions", 0))
|
|
174
|
+
if active >= 2:
|
|
175
|
+
on_line(f"{active - 1} other session(s) are sharing this GPU — "
|
|
176
|
+
"motion will be slower than usual")
|
|
177
|
+
except (errors.DropbearError, httpx.HTTPError):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
return Arm(runner=runner, client=client, session=session, transport=transport,
|
|
181
|
+
transport_connect=transport_connect, diagnose_udp_fn=preflight_fn,
|
|
182
|
+
allow_fallback=allow_fallback, relay_connect=relay_connect,
|
|
183
|
+
robot=robot, heartbeat=heartbeat, on_line=on_line,
|
|
184
|
+
doctor_runner=doctor_runner, duration_default_s=duration_default_s,
|
|
185
|
+
first_action_timeout_s=first_action_timeout_s)
|
|
186
|
+
except BaseException:
|
|
187
|
+
await _cleanup_connect_failure(
|
|
188
|
+
client=client,
|
|
189
|
+
session=session,
|
|
190
|
+
transport=transport,
|
|
191
|
+
robot=robot,
|
|
192
|
+
robot_connect_attempted=robot_connect_attempted,
|
|
193
|
+
heartbeat=heartbeat,
|
|
194
|
+
)
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _compatible_diagnose_udp(preflight_fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
199
|
+
async def diagnose(addr: str, port: int, **kwargs):
|
|
200
|
+
call_kwargs = _accepted_preflight_kwargs(preflight_fn, kwargs)
|
|
201
|
+
result = preflight_fn(addr, port, **call_kwargs)
|
|
202
|
+
if inspect.isawaitable(result):
|
|
203
|
+
result = await result
|
|
204
|
+
return _as_udp_reachability_report(result)
|
|
205
|
+
|
|
206
|
+
return diagnose
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _accepted_preflight_kwargs(preflight_fn: Callable[..., Any],
|
|
210
|
+
kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
211
|
+
try:
|
|
212
|
+
params = inspect.signature(preflight_fn).parameters
|
|
213
|
+
except (TypeError, ValueError):
|
|
214
|
+
return kwargs
|
|
215
|
+
if any(param.kind == inspect.Parameter.VAR_KEYWORD for param in params.values()):
|
|
216
|
+
return kwargs
|
|
217
|
+
return {key: value for key, value in kwargs.items() if key in params}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _as_udp_reachability_report(result: Any) -> Any:
|
|
221
|
+
if hasattr(result, "ok") and hasattr(result, "worker"):
|
|
222
|
+
return result
|
|
223
|
+
if not hasattr(result, "status"):
|
|
224
|
+
return result
|
|
225
|
+
from dropbear_transport import PreflightResult, UdpReachabilityReport
|
|
226
|
+
|
|
227
|
+
status = result.status
|
|
228
|
+
remediation = getattr(result, "remediation", "")
|
|
229
|
+
reason = "ok"
|
|
230
|
+
if status == "udp_blocked":
|
|
231
|
+
reason = "udp_refused"
|
|
232
|
+
elif status != "ok":
|
|
233
|
+
reason = "data_plane_unreachable"
|
|
234
|
+
return UdpReachabilityReport(
|
|
235
|
+
worker=PreflightResult(status=status, remediation=remediation),
|
|
236
|
+
diagnostic=None,
|
|
237
|
+
reason=reason,
|
|
238
|
+
remediation="" if status == "ok" else remediation,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
async def _cleanup_connect_failure(*, client, session, transport, robot,
|
|
243
|
+
robot_connect_attempted: bool,
|
|
244
|
+
heartbeat: Heartbeat | None) -> None:
|
|
245
|
+
if heartbeat is not None:
|
|
246
|
+
with suppress(Exception):
|
|
247
|
+
await heartbeat.stop()
|
|
248
|
+
if transport is not None:
|
|
249
|
+
with suppress(Exception):
|
|
250
|
+
await transport.close()
|
|
251
|
+
if robot is not None and robot_connect_attempted:
|
|
252
|
+
with suppress(Exception):
|
|
253
|
+
await asyncio.to_thread(robot.disconnect)
|
|
254
|
+
if client is not None and session is not None:
|
|
255
|
+
with suppress(Exception):
|
|
256
|
+
await stop_session(client, session.session_id, clear_prewarm_state=True)
|
|
257
|
+
if client is not None:
|
|
258
|
+
with suppress(Exception):
|
|
259
|
+
await client.close()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def _pin_connect(transport_connect, session: Session):
|
|
263
|
+
try:
|
|
264
|
+
return await transport_connect(
|
|
265
|
+
session.worker_addr, session.port,
|
|
266
|
+
cert_fingerprint=session.cert_fingerprint,
|
|
267
|
+
session_id=session.session_id,
|
|
268
|
+
session_token=session.session_token,
|
|
269
|
+
)
|
|
270
|
+
except errors.DropbearError:
|
|
271
|
+
raise
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
text = str(exc).lower()
|
|
274
|
+
if "fingerprint" in text or "pin" in text:
|
|
275
|
+
raise errors.catalog("cert_fingerprint_mismatch") from exc
|
|
276
|
+
raise
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class Arm:
|
|
280
|
+
"""Live handle: arm.do(instruction), arm.estop(), arm.close(), context manager."""
|
|
281
|
+
|
|
282
|
+
def __init__(self, *, runner, client, session, transport, transport_connect,
|
|
283
|
+
diagnose_udp_fn, allow_fallback, relay_connect, robot, heartbeat,
|
|
284
|
+
on_line, doctor_runner, duration_default_s, first_action_timeout_s) -> None:
|
|
285
|
+
self._runner = runner
|
|
286
|
+
self._client = client
|
|
287
|
+
self._session = session
|
|
288
|
+
self._transport = transport
|
|
289
|
+
self._transport_connect = transport_connect
|
|
290
|
+
self._diagnose_udp_fn = diagnose_udp_fn
|
|
291
|
+
self._allow_fallback = allow_fallback
|
|
292
|
+
self._relay_connect = relay_connect
|
|
293
|
+
self._robot = robot
|
|
294
|
+
self._heartbeat = heartbeat
|
|
295
|
+
self._on_line = on_line
|
|
296
|
+
self._doctor_runner = doctor_runner
|
|
297
|
+
self._duration_default_s = duration_default_s
|
|
298
|
+
self._first_action_timeout_s = first_action_timeout_s
|
|
299
|
+
self._active_estop: EStop | None = None
|
|
300
|
+
self._closed = False
|
|
301
|
+
|
|
302
|
+
def do(self, instruction: str, duration_s: float | None = None) -> LoopResult:
|
|
303
|
+
config = load_config()
|
|
304
|
+
if config.motion_enabled:
|
|
305
|
+
self._ensure_doctor()
|
|
306
|
+
estop = EStop()
|
|
307
|
+
self._active_estop = estop
|
|
308
|
+
# signal handlers may only be installed from the main thread
|
|
309
|
+
if threading.current_thread() is threading.main_thread():
|
|
310
|
+
restore = install_sigint_estop(estop)
|
|
311
|
+
else:
|
|
312
|
+
def restore() -> None:
|
|
313
|
+
return None
|
|
314
|
+
keys = SpacebarEStop(estop)
|
|
315
|
+
keys.start()
|
|
316
|
+
mode = "LIVE" if config.motion_enabled else "DRY RUN (no motion until you enable it)"
|
|
317
|
+
self._on_line(f"{mode} — press SPACE or Ctrl-C to e-stop.")
|
|
318
|
+
try:
|
|
319
|
+
return self._runner.run(self._do_async(
|
|
320
|
+
instruction, duration_s or self._duration_default_s, estop,
|
|
321
|
+
config.motion_enabled,
|
|
322
|
+
))
|
|
323
|
+
finally:
|
|
324
|
+
keys.stop()
|
|
325
|
+
restore()
|
|
326
|
+
self._active_estop = None
|
|
327
|
+
|
|
328
|
+
def _ensure_doctor(self) -> None:
|
|
329
|
+
if load_state().doctor_last_pass is None:
|
|
330
|
+
self._on_line("First motion run — running checks (dropbear doctor)…")
|
|
331
|
+
self._doctor_runner() # raises DropbearError on failure
|
|
332
|
+
|
|
333
|
+
async def _do_async(self, instruction: str, duration_s: float, estop: EStop,
|
|
334
|
+
motion_enabled: bool) -> LoopResult:
|
|
335
|
+
reconnects = 0
|
|
336
|
+
while True:
|
|
337
|
+
loop = ControlLoop(
|
|
338
|
+
transport=self._transport, robot=self._robot,
|
|
339
|
+
session_id=self._session.session_id, instruction=instruction,
|
|
340
|
+
motion_enabled=motion_enabled, estop=estop, duration_s=duration_s,
|
|
341
|
+
on_line=self._on_line,
|
|
342
|
+
first_action_timeout_s=self._first_action_timeout_s,
|
|
343
|
+
)
|
|
344
|
+
result = await loop.run()
|
|
345
|
+
if not result.froze:
|
|
346
|
+
return result
|
|
347
|
+
self._on_line(errors.catalog("connection_lost").render())
|
|
348
|
+
if reconnects >= 1:
|
|
349
|
+
raise errors.catalog("connection_lost")
|
|
350
|
+
reconnects += 1
|
|
351
|
+
prefer_relay = (
|
|
352
|
+
self._allow_fallback
|
|
353
|
+
and getattr(self._transport, "mode", None) == "quic"
|
|
354
|
+
and result.freeze_reason in {"transport_lost", "first_action_timeout"}
|
|
355
|
+
)
|
|
356
|
+
await self._reconnect(prefer_relay=prefer_relay)
|
|
357
|
+
|
|
358
|
+
async def _reconnect(self, *, prefer_relay: bool = False) -> None:
|
|
359
|
+
try:
|
|
360
|
+
await self._transport.close()
|
|
361
|
+
except Exception: # noqa: BLE001 — best-effort close of a dead transport
|
|
362
|
+
pass
|
|
363
|
+
refreshed = await self._client.get_session(self._session.session_id)
|
|
364
|
+
if refreshed.status not in ("ready", "active"):
|
|
365
|
+
raise errors.catalog("session_expired")
|
|
366
|
+
if refreshed.session_token:
|
|
367
|
+
self._session = refreshed
|
|
368
|
+
|
|
369
|
+
async def quic_connect(ready_session: Session):
|
|
370
|
+
return await _pin_connect(self._transport_connect, ready_session)
|
|
371
|
+
|
|
372
|
+
async def relay_connect_for_session(ready_session: Session):
|
|
373
|
+
connect = self._relay_connect or getattr(self._client, "connect_relay", None)
|
|
374
|
+
if connect is None:
|
|
375
|
+
raise errors.catalog(
|
|
376
|
+
"relay_unavailable",
|
|
377
|
+
retry_command="dropbear run pick-place",
|
|
378
|
+
)
|
|
379
|
+
return await connect(ready_session)
|
|
380
|
+
|
|
381
|
+
if prefer_relay:
|
|
382
|
+
self._on_line("QUIC/UDP stopped delivering actions; using HTTPS fallback.")
|
|
383
|
+
self._on_line("Motion may feel less responsive while using fallback.")
|
|
384
|
+
self._transport = await relay_connect_for_session(self._session)
|
|
385
|
+
self._transport.mode = getattr(self._transport, "mode", "relay")
|
|
386
|
+
self._transport.fallback_reason = "quic_runtime_lost"
|
|
387
|
+
self._transport.primary_preflight_status = None
|
|
388
|
+
self._transport.diagnostic_preflight_status = None
|
|
389
|
+
else:
|
|
390
|
+
self._transport = await connect_selected_transport(
|
|
391
|
+
self._session,
|
|
392
|
+
diagnose_udp_fn=self._diagnose_udp_fn,
|
|
393
|
+
quic_connect=quic_connect,
|
|
394
|
+
relay_connect=relay_connect_for_session,
|
|
395
|
+
allow_fallback=self._allow_fallback,
|
|
396
|
+
on_line=self._on_line,
|
|
397
|
+
report_transport_fn=getattr(self._client, "report_transport", None),
|
|
398
|
+
)
|
|
399
|
+
self._on_line("Reconnected.")
|
|
400
|
+
|
|
401
|
+
def estop(self) -> None:
|
|
402
|
+
if self._active_estop is not None:
|
|
403
|
+
self._active_estop.trigger("api")
|
|
404
|
+
|
|
405
|
+
def close(self) -> None:
|
|
406
|
+
if self._closed:
|
|
407
|
+
return
|
|
408
|
+
self._closed = True
|
|
409
|
+
try:
|
|
410
|
+
self._runner.run_cleanup(self._close_async())
|
|
411
|
+
finally:
|
|
412
|
+
self._runner.shutdown()
|
|
413
|
+
|
|
414
|
+
async def _close_async(self) -> None:
|
|
415
|
+
with suppress(Exception):
|
|
416
|
+
await self._heartbeat.stop()
|
|
417
|
+
with suppress(Exception):
|
|
418
|
+
await self._transport.close()
|
|
419
|
+
with suppress(Exception):
|
|
420
|
+
await stop_session(self._client, self._session.session_id,
|
|
421
|
+
clear_prewarm_state=True)
|
|
422
|
+
with suppress(Exception):
|
|
423
|
+
await self._client.close()
|
|
424
|
+
with suppress(Exception):
|
|
425
|
+
await asyncio.to_thread(self._robot.disconnect)
|
|
426
|
+
|
|
427
|
+
def __enter__(self) -> Arm:
|
|
428
|
+
return self
|
|
429
|
+
|
|
430
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
431
|
+
self.close()
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<svg viewBox="0 0 960 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Dropbear golden-path camera setup reference">
|
|
2
|
+
<title>Dropbear camera setup: third-person side webcam for the SO-101 golden path</title>
|
|
3
|
+
<rect width="960" height="540" fill="#FAF7F2"/>
|
|
4
|
+
<text x="32" y="44" font-family="sans-serif" font-size="24" font-weight="700" fill="#1F2933">Camera setup — third-person side view</text>
|
|
5
|
+
<text x="32" y="68" font-family="sans-serif" font-size="14" fill="#52606D">Match this layout, then check your live preview against the framing guide on the right.</text>
|
|
6
|
+
|
|
7
|
+
<!-- ============ LEFT PANEL: top view geometry ============ -->
|
|
8
|
+
<text x="32" y="108" font-family="sans-serif" font-size="15" font-weight="700" fill="#1F2933">Top view</text>
|
|
9
|
+
<rect x="32" y="120" width="420" height="270" rx="8" fill="#FFFFFF" stroke="#CBD2D9"/>
|
|
10
|
+
<!-- table -->
|
|
11
|
+
<rect x="72" y="150" width="340" height="150" rx="4" fill="#F1EDE6" stroke="#9AA5B1"/>
|
|
12
|
+
<text x="242" y="316" font-family="sans-serif" font-size="12" fill="#52606D" text-anchor="middle">table</text>
|
|
13
|
+
<!-- arm base -->
|
|
14
|
+
<rect x="340" y="195" width="44" height="60" rx="4" fill="#3E4C59"/>
|
|
15
|
+
<text x="362" y="186" font-family="sans-serif" font-size="12" font-weight="700" fill="#1F2933" text-anchor="middle">SO-101 base</text>
|
|
16
|
+
<!-- workspace -->
|
|
17
|
+
<rect x="120" y="185" width="170" height="80" rx="4" fill="none" stroke="#7B8794" stroke-dasharray="6 4"/>
|
|
18
|
+
<text x="205" y="230" font-family="sans-serif" font-size="12" fill="#52606D" text-anchor="middle">Workspace (props)</text>
|
|
19
|
+
<!-- reach arrow arm->workspace -->
|
|
20
|
+
<line x1="340" y1="225" x2="296" y2="225" stroke="#7B8794" stroke-width="2"/>
|
|
21
|
+
<text x="318" y="216" font-family="sans-serif" font-size="11" fill="#52606D" text-anchor="middle">~25 cm</text>
|
|
22
|
+
<!-- webcam -->
|
|
23
|
+
<g>
|
|
24
|
+
<rect x="190" y="430" width="48" height="26" rx="6" fill="#3E4C59"/>
|
|
25
|
+
<circle cx="214" cy="443" r="7" fill="#FAF7F2"/>
|
|
26
|
+
<circle cx="214" cy="443" r="3.5" fill="#1F2933"/>
|
|
27
|
+
<text x="214" y="478" font-family="sans-serif" font-size="13" font-weight="700" fill="#1F2933" text-anchor="middle">Webcam</text>
|
|
28
|
+
</g>
|
|
29
|
+
<!-- distance webcam->table, side-on angle -->
|
|
30
|
+
<line x1="214" y1="430" x2="214" y2="300" stroke="#7B8794" stroke-width="2" stroke-dasharray="4 4"/>
|
|
31
|
+
<text x="226" y="372" font-family="sans-serif" font-size="12" fill="#52606D">60–80 cm</text>
|
|
32
|
+
<text x="226" y="390" font-family="sans-serif" font-size="12" fill="#52606D">side-on (90° to arm)</text>
|
|
33
|
+
|
|
34
|
+
<!-- ============ MIDDLE PANEL: side view ============ -->
|
|
35
|
+
<text x="492" y="108" font-family="sans-serif" font-size="15" font-weight="700" fill="#1F2933">Side view</text>
|
|
36
|
+
<rect x="492" y="120" width="200" height="270" rx="8" fill="#FFFFFF" stroke="#CBD2D9"/>
|
|
37
|
+
<!-- table line -->
|
|
38
|
+
<line x1="512" y1="330" x2="672" y2="330" stroke="#9AA5B1" stroke-width="3"/>
|
|
39
|
+
<!-- arm silhouette -->
|
|
40
|
+
<rect x="618" y="296" width="22" height="34" fill="#3E4C59"/>
|
|
41
|
+
<line x1="629" y1="296" x2="600" y2="250" stroke="#3E4C59" stroke-width="7" stroke-linecap="round"/>
|
|
42
|
+
<line x1="600" y1="250" x2="566" y2="276" stroke="#3E4C59" stroke-width="6" stroke-linecap="round"/>
|
|
43
|
+
<!-- props -->
|
|
44
|
+
<rect x="540" y="318" width="12" height="12" fill="#7B8794"/>
|
|
45
|
+
<!-- camera at lens height -->
|
|
46
|
+
<rect x="500" y="266" width="26" height="16" rx="4" fill="#3E4C59"/>
|
|
47
|
+
<line x1="526" y1="274" x2="556" y2="296" stroke="#7B8794" stroke-width="1.5" stroke-dasharray="3 3"/>
|
|
48
|
+
<!-- lens height above table -->
|
|
49
|
+
<line x1="513" y1="282" x2="513" y2="330" stroke="#7B8794" stroke-width="2" stroke-dasharray="4 4"/>
|
|
50
|
+
<text x="521" y="312" font-family="sans-serif" font-size="11" fill="#52606D">20–30 cm</text>
|
|
51
|
+
<text x="592" y="368" font-family="sans-serif" font-size="11" fill="#52606D" text-anchor="middle">lens just above prop height, slight downward tilt</text>
|
|
52
|
+
|
|
53
|
+
<!-- ============ RIGHT PANEL: framing guide ============ -->
|
|
54
|
+
<text x="732" y="108" font-family="sans-serif" font-size="15" font-weight="700" fill="#1F2933">Your frame should look like this</text>
|
|
55
|
+
<rect x="732" y="120" width="196" height="146" rx="8" fill="#FFFFFF" stroke="#CBD2D9"/>
|
|
56
|
+
<rect x="740" y="128" width="180" height="130" fill="#F1EDE6"/>
|
|
57
|
+
<!-- rule-of-thirds guides -->
|
|
58
|
+
<line x1="800" y1="128" x2="800" y2="258" stroke="#CBD2D9" stroke-width="1"/>
|
|
59
|
+
<line x1="860" y1="128" x2="860" y2="258" stroke="#CBD2D9" stroke-width="1"/>
|
|
60
|
+
<!-- arm in the right third -->
|
|
61
|
+
<rect x="872" y="206" width="16" height="32" fill="#3E4C59"/>
|
|
62
|
+
<line x1="880" y1="206" x2="852" y2="172" stroke="#3E4C59" stroke-width="5" stroke-linecap="round"/>
|
|
63
|
+
<line x1="852" y1="172" x2="824" y2="192" stroke="#3E4C59" stroke-width="4" stroke-linecap="round"/>
|
|
64
|
+
<!-- props middle third -->
|
|
65
|
+
<rect x="806" y="226" width="10" height="10" fill="#7B8794"/>
|
|
66
|
+
<rect x="826" y="228" width="10" height="8" fill="#9AA5B1"/>
|
|
67
|
+
<!-- table edge -->
|
|
68
|
+
<line x1="740" y1="238" x2="920" y2="238" stroke="#9AA5B1" stroke-width="2"/>
|
|
69
|
+
<text x="830" y="282" font-family="sans-serif" font-size="11" fill="#52606D" text-anchor="middle">whole arm + whole workspace visible,</text>
|
|
70
|
+
<text x="830" y="297" font-family="sans-serif" font-size="11" fill="#52606D" text-anchor="middle">nothing important on the frame edge</text>
|
|
71
|
+
|
|
72
|
+
<!-- ============ CHECKLIST ============ -->
|
|
73
|
+
<text x="732" y="336" font-family="sans-serif" font-size="14" font-weight="700" fill="#1F2933">Checklist</text>
|
|
74
|
+
<text x="732" y="358" font-family="sans-serif" font-size="12" fill="#3E4C59">• even, diffuse light — no backlight</text>
|
|
75
|
+
<text x="732" y="377" font-family="sans-serif" font-size="12" fill="#3E4C59">• plain background, no clutter</text>
|
|
76
|
+
<text x="732" y="396" font-family="sans-serif" font-size="12" fill="#3E4C59">• camera on a stable mount</text>
|
|
77
|
+
<text x="732" y="415" font-family="sans-serif" font-size="12" fill="#3E4C59">• 1080p, 16:9, landscape</text>
|
|
78
|
+
<text x="732" y="441" font-family="sans-serif" font-size="13" font-weight="700" fill="#B7410E">• Lock white balance + exposure</text>
|
|
79
|
+
<text x="744" y="459" font-family="sans-serif" font-size="11" fill="#B7410E">auto-WB drift mid-task confuses the model</text>
|
|
80
|
+
|
|
81
|
+
<text x="32" y="512" font-family="sans-serif" font-size="11" fill="#7B8794">dropbear/assets/camera_reference.svg — ships with the dropbear SDK; full recipe at /docs/golden-path</text>
|
|
82
|
+
</svg>
|