roboapi 0.4.2__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.
- roboapi/__init__.py +2 -0
- roboapi/calibration/__init__.py +6 -0
- roboapi/calibration/profile.py +139 -0
- roboapi/calibration/session.py +350 -0
- roboapi/cli.py +215 -0
- roboapi/config/__init__.py +22 -0
- roboapi/config/loader.py +119 -0
- roboapi/config/models.py +191 -0
- roboapi/embodiment.py +251 -0
- roboapi/errors.py +37 -0
- roboapi/geometry.py +337 -0
- roboapi/hardware/__init__.py +5 -0
- roboapi/hardware/base.py +113 -0
- roboapi/hardware/camera.py +167 -0
- roboapi/hardware/franka/__init__.py +5 -0
- roboapi/hardware/franka/arm.py +476 -0
- roboapi/hardware/franka/desk.py +555 -0
- roboapi/hardware/franka/feedback.py +196 -0
- roboapi/hardware/franka/guiding.py +104 -0
- roboapi/hardware/franka/ik.py +543 -0
- roboapi/hardware/franka/limits.py +110 -0
- roboapi/hardware/franka/settings.py +116 -0
- roboapi/hardware/robotiq.py +350 -0
- roboapi/hardware/ur/__init__.py +16 -0
- roboapi/hardware/ur/build.py +92 -0
- roboapi/hardware/ur/compliant.py +128 -0
- roboapi/hardware/ur/dashboard.py +103 -0
- roboapi/hardware/ur/feedback.py +159 -0
- roboapi/hardware/ur/ipc.py +206 -0
- roboapi/hardware/ur/native/CMakeLists.txt +37 -0
- roboapi/hardware/ur/native/dependencies.lock.json +13 -0
- roboapi/hardware/ur/native/include/ur_cpp/cartesian_speedl.hpp +170 -0
- roboapi/hardware/ur/native/include/ur_cpp/control_loop.hpp +126 -0
- roboapi/hardware/ur/native/include/ur_cpp/geom.hpp +72 -0
- roboapi/hardware/ur/native/include/ur_cpp/motion_primitives.hpp +350 -0
- roboapi/hardware/ur/native/include/ur_cpp/robot_io.hpp +26 -0
- roboapi/hardware/ur/native/include/ur_cpp/streaming_pose.hpp +369 -0
- roboapi/hardware/ur/native/src/control_loop.cpp +512 -0
- roboapi/hardware/ur/native/src/robot_io_urcl.cpp +143 -0
- roboapi/hardware/ur/native/tests/cartesian_speedl_unit.cpp +113 -0
- roboapi/hardware/ur/native/tests/control_modes_unit.cpp +137 -0
- roboapi/hardware/ur/native/tests/streaming_pose_unit.cpp +348 -0
- roboapi/hardware/ur/native/tests/velocity_governor_unit.cpp +65 -0
- roboapi/hardware/ur/native/worker.cpp +147 -0
- roboapi/hardware/ur/position.py +253 -0
- roboapi/hardware/ur/settings.py +95 -0
- roboapi/images.py +16 -0
- roboapi/initialize.py +137 -0
- roboapi/mcp.py +497 -0
- roboapi/motion.py +125 -0
- roboapi/naming.py +7 -0
- roboapi/operations.py +45 -0
- roboapi/policy/__init__.py +7 -0
- roboapi/policy/client.py +277 -0
- roboapi/policy/protocol.py +166 -0
- roboapi/policy/settings.py +79 -0
- roboapi/product-names.json +12 -0
- roboapi/records.py +149 -0
- roboapi/runtime.py +911 -0
- roboapi/semantics.py +6 -0
- roboapi/tasks.py +337 -0
- roboapi/web/__init__.py +5 -0
- roboapi/web/presets.py +44 -0
- roboapi/web/server.py +419 -0
- roboapi/web/static/assets/Bundler-LICENSE.txt +48 -0
- roboapi/web/static/assets/CalibrationScene-BO_spRoL.js +4220 -0
- roboapi/web/static/assets/Inter-OFL.txt +93 -0
- roboapi/web/static/assets/Inter-Variable-c8O0ljhh.ttf +0 -0
- roboapi/web/static/assets/Lucide-LICENSE.txt +43 -0
- roboapi/web/static/assets/Meshoptimizer-LICENSE.txt +23 -0
- roboapi/web/static/assets/MiSans-Regular-ES4DrbaF.woff2 +0 -0
- roboapi/web/static/assets/MiSans-Semibold-B34HIk9C.woff2 +0 -0
- roboapi/web/static/assets/PokeBrand-NOTICE.txt +20 -0
- roboapi/web/static/assets/React-LICENSE.txt +23 -0
- roboapi/web/static/assets/Three-LICENSE.txt +23 -0
- roboapi/web/static/assets/home-reference-CTpf8-oV.png +0 -0
- roboapi/web/static/assets/index-CGLhpUm0.js +28 -0
- roboapi/web/static/assets/index-QXqV-fDC.css +1 -0
- roboapi/web/static/assets/robotiq-ace-pro-2-EJuoOXTy.glb +0 -0
- roboapi/web/static/index.html +15 -0
- roboapi-0.4.2.dist-info/METADATA +205 -0
- roboapi-0.4.2.dist-info/RECORD +94 -0
- roboapi-0.4.2.dist-info/WHEEL +5 -0
- roboapi-0.4.2.dist-info/entry_points.txt +2 -0
- roboapi-0.4.2.dist-info/licenses/LICENSE +202 -0
- roboapi-0.4.2.dist-info/licenses/NOTICE +16 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Bundler-LICENSE.txt +48 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Inter-OFL.txt +93 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Lucide-LICENSE.txt +43 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Meshoptimizer-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/PokeBrand-NOTICE.txt +20 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/React-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Three-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/top_level.txt +1 -0
roboapi/hardware/base.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Native device interfaces and the shared Cartesian coordinate boundary.
|
|
2
|
+
|
|
3
|
+
Arms stream Cartesian targets through non-blocking ``servo_pose`` calls. Each driver
|
|
4
|
+
owns its control loop; the runtime owns paths, timing and returning to Home.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ..geometry import FrameAdapter
|
|
14
|
+
from ..semantics import TRAINING_FRAME
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class Arm(Protocol):
|
|
19
|
+
@property
|
|
20
|
+
def live(self) -> bool:
|
|
21
|
+
"""False means: do not command this arm. ``last_error`` says why."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def last_error(self) -> str | None: ...
|
|
25
|
+
|
|
26
|
+
def connect(self) -> None: ...
|
|
27
|
+
|
|
28
|
+
def disconnect(self) -> None: ...
|
|
29
|
+
|
|
30
|
+
def get_joints(self) -> np.ndarray:
|
|
31
|
+
"""Joint positions in rad. 6 for UR, 7 for Franka -- callers must not assume a length."""
|
|
32
|
+
|
|
33
|
+
def get_eef_pose(self) -> np.ndarray:
|
|
34
|
+
"""pose7d ``[x, y, z, qw, qx, qy, qz]``."""
|
|
35
|
+
|
|
36
|
+
def servo_pose(self, pose7: np.ndarray, twist: np.ndarray | None = None, *, position_only=False) -> None:
|
|
37
|
+
"""Update a Cartesian target, optionally without added force-driven offsets."""
|
|
38
|
+
|
|
39
|
+
def recover(self) -> None:
|
|
40
|
+
"""Restore device control after a fault. The runtime then returns to Home."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@runtime_checkable
|
|
44
|
+
class Gripper(Protocol):
|
|
45
|
+
@property
|
|
46
|
+
def live(self) -> bool: ...
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def last_error(self) -> str | None: ...
|
|
50
|
+
|
|
51
|
+
def connect(self) -> None: ...
|
|
52
|
+
|
|
53
|
+
def disconnect(self) -> None: ...
|
|
54
|
+
|
|
55
|
+
def get_ratio(self) -> float: ...
|
|
56
|
+
|
|
57
|
+
def set_ratio(self, ratio: float) -> None: ...
|
|
58
|
+
|
|
59
|
+
def move_blocking(self, ratio: float, should_stop=None) -> None: ...
|
|
60
|
+
|
|
61
|
+
def recover(self) -> None: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FramedArm:
|
|
65
|
+
"""One bidirectional frame boundary for every native arm implementation.
|
|
66
|
+
|
|
67
|
+
Changing calibration is an idle-only operation owned by the runtime. This
|
|
68
|
+
adapter never starts a thread or sends motion when a profile is installed.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, native: Arm, adapter: FrameAdapter) -> None:
|
|
72
|
+
self.native = native
|
|
73
|
+
self.adapter = adapter
|
|
74
|
+
|
|
75
|
+
def __getattr__(self, name):
|
|
76
|
+
return getattr(self.native, name)
|
|
77
|
+
|
|
78
|
+
def set_adapter(self, adapter: FrameAdapter) -> None:
|
|
79
|
+
self.adapter = adapter
|
|
80
|
+
|
|
81
|
+
def get_eef_pose(self) -> np.ndarray:
|
|
82
|
+
return self.adapter.to_task(self.native.get_eef_pose())
|
|
83
|
+
|
|
84
|
+
def check_motion_ready(self) -> None:
|
|
85
|
+
"""Check driver-specific prerequisites before an explicit robot operation."""
|
|
86
|
+
check = getattr(self.native, "check_motion_ready", None)
|
|
87
|
+
if check is not None:
|
|
88
|
+
check()
|
|
89
|
+
|
|
90
|
+
def servo_pose(self, pose7: np.ndarray, twist: np.ndarray | None = None, *, position_only=False) -> None:
|
|
91
|
+
adapter = self.adapter
|
|
92
|
+
self.native.servo_pose(
|
|
93
|
+
adapter.to_native(pose7),
|
|
94
|
+
None if twist is None else adapter.twist_to_native(twist),
|
|
95
|
+
position_only=position_only,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def drain_trace(self) -> list[dict]:
|
|
99
|
+
drain = getattr(self.native, "drain_trace", None)
|
|
100
|
+
if drain is None:
|
|
101
|
+
return []
|
|
102
|
+
adapter = self.adapter
|
|
103
|
+
trace = drain()
|
|
104
|
+
for item in trace:
|
|
105
|
+
for key in ("target_pose", "measured_pose"):
|
|
106
|
+
item[key] = adapter.to_task(item[key]).tolist()
|
|
107
|
+
# Pose errors are geometric vectors at the same control point, not
|
|
108
|
+
# spatial screw coordinates: only rotate their linear/angular axes.
|
|
109
|
+
error = np.asarray(item["pose_error"]).reshape(2, 3)
|
|
110
|
+
item["pose_error"] = (error @ adapter.task_from_base[:3, :3].T).ravel().tolist()
|
|
111
|
+
item["eef_frame"] = TRAINING_FRAME
|
|
112
|
+
return trace
|
|
113
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Fixed-mode wrist capture with one owner, fresh frames and hotplug recovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class V4L2Camera:
|
|
12
|
+
"""V4L2 MJPG at 1280×720 and 30 FPS; one reader thread, latest frame only."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, device: str) -> None:
|
|
15
|
+
self._device = device
|
|
16
|
+
self._cap = None
|
|
17
|
+
self._lock = threading.Lock()
|
|
18
|
+
self._lifecycle = threading.Lock()
|
|
19
|
+
self._frame: np.ndarray | None = None
|
|
20
|
+
self._stamp = 0.0
|
|
21
|
+
self._read_started_at = 0.0
|
|
22
|
+
self._stop = threading.Event()
|
|
23
|
+
self._thread: threading.Thread | None = None
|
|
24
|
+
self._error: str | None = None
|
|
25
|
+
self._first_attempt = threading.Event()
|
|
26
|
+
self.failure_count = 0
|
|
27
|
+
self.on_event = None
|
|
28
|
+
self.retry_s = 1.0
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def live(self) -> bool:
|
|
32
|
+
with self._lock:
|
|
33
|
+
return not self._stop.is_set() and self._frame is not None and self._error is None
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def last_error(self) -> str | None:
|
|
37
|
+
return self._error
|
|
38
|
+
|
|
39
|
+
def connect(self) -> None:
|
|
40
|
+
with self._lifecycle:
|
|
41
|
+
if self._thread is not None and self._thread.is_alive():
|
|
42
|
+
if self._stop.is_set():
|
|
43
|
+
raise RuntimeError("camera is still stopping")
|
|
44
|
+
else:
|
|
45
|
+
self._stop.clear()
|
|
46
|
+
self._first_attempt.clear()
|
|
47
|
+
self._thread = threading.Thread(target=self._run, name="camera", daemon=True)
|
|
48
|
+
self._thread.start()
|
|
49
|
+
# An absent camera leaves preview available while the owner retries in the background.
|
|
50
|
+
self._first_attempt.wait(5.0)
|
|
51
|
+
|
|
52
|
+
def _open(self):
|
|
53
|
+
import cv2
|
|
54
|
+
|
|
55
|
+
cap = cv2.VideoCapture(self._device, cv2.CAP_V4L2)
|
|
56
|
+
try:
|
|
57
|
+
if not cap.isOpened():
|
|
58
|
+
raise RuntimeError(f"cannot open camera {self._device}")
|
|
59
|
+
mode = (
|
|
60
|
+
(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG")),
|
|
61
|
+
(cv2.CAP_PROP_FRAME_WIDTH, 1280),
|
|
62
|
+
(cv2.CAP_PROP_FRAME_HEIGHT, 720),
|
|
63
|
+
(cv2.CAP_PROP_FPS, 30),
|
|
64
|
+
)
|
|
65
|
+
for prop, value in mode:
|
|
66
|
+
cap.set(prop, value)
|
|
67
|
+
actual = [cap.get(prop) for prop, _ in mode]
|
|
68
|
+
if not np.allclose(actual, [value for _, value in mode], rtol=0, atol=1e-3):
|
|
69
|
+
fourcc, width, height, fps = actual
|
|
70
|
+
code = int(fourcc) if np.isfinite(fourcc) else 0
|
|
71
|
+
codec = "".join(chr((code >> (8 * i)) & 0xff) for i in range(4)).strip("\x00") or "unknown"
|
|
72
|
+
raise RuntimeError(
|
|
73
|
+
f"camera {self._device} requires MJPG 1280x720 at 30 FPS; "
|
|
74
|
+
f"negotiated {codec} {width:g}x{height:g} at {fps:g} FPS"
|
|
75
|
+
)
|
|
76
|
+
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
77
|
+
return cap
|
|
78
|
+
except Exception:
|
|
79
|
+
cap.release()
|
|
80
|
+
raise
|
|
81
|
+
|
|
82
|
+
def _run(self) -> None:
|
|
83
|
+
import cv2
|
|
84
|
+
|
|
85
|
+
failed = False
|
|
86
|
+
while not self._stop.is_set():
|
|
87
|
+
cap = None
|
|
88
|
+
try:
|
|
89
|
+
cap = self._open()
|
|
90
|
+
self._cap = cap
|
|
91
|
+
while not self._stop.is_set():
|
|
92
|
+
started_at = time.monotonic()
|
|
93
|
+
ok, bgr = cap.read()
|
|
94
|
+
if not ok:
|
|
95
|
+
raise RuntimeError("camera frame read failed")
|
|
96
|
+
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
|
97
|
+
with self._lock:
|
|
98
|
+
if self._stop.is_set():
|
|
99
|
+
break
|
|
100
|
+
self._frame = rgb
|
|
101
|
+
self._stamp = time.monotonic()
|
|
102
|
+
self._read_started_at = started_at
|
|
103
|
+
self._error = None
|
|
104
|
+
if failed and self.on_event:
|
|
105
|
+
self.on_event("Camera reconnected")
|
|
106
|
+
failed = False
|
|
107
|
+
self._first_attempt.set()
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
if self._stop.is_set():
|
|
110
|
+
break
|
|
111
|
+
with self._lock:
|
|
112
|
+
self._frame = None
|
|
113
|
+
self._stamp = self._read_started_at = 0.0
|
|
114
|
+
self._error = str(exc)
|
|
115
|
+
self.failure_count += 1
|
|
116
|
+
if not failed and not self._stop.is_set() and self.on_event:
|
|
117
|
+
self.on_event(str(exc), "error")
|
|
118
|
+
failed = True
|
|
119
|
+
self._first_attempt.set()
|
|
120
|
+
finally:
|
|
121
|
+
if cap is not None:
|
|
122
|
+
cap.release()
|
|
123
|
+
self._cap = None
|
|
124
|
+
self._stop.wait(self.retry_s)
|
|
125
|
+
|
|
126
|
+
def read(self) -> tuple[np.ndarray, float]:
|
|
127
|
+
with self._lock:
|
|
128
|
+
if self._frame is None:
|
|
129
|
+
raise RuntimeError(f"no frame yet: {self._error or 'camera not started'}")
|
|
130
|
+
return self._frame, time.monotonic() - self._stamp
|
|
131
|
+
|
|
132
|
+
def read_after(self, after: float | None, timeout_s: float, should_stop=None):
|
|
133
|
+
"""Wait for a read begun after the motion boundary, skipping any in-flight read.
|
|
134
|
+
|
|
135
|
+
These are host receipt/read timestamps, not sensor exposure timestamps. V4L2's
|
|
136
|
+
device/USB latency still needs capture timestamps for exact sensor/state alignment.
|
|
137
|
+
"""
|
|
138
|
+
deadline = time.monotonic() + timeout_s
|
|
139
|
+
while True:
|
|
140
|
+
if should_stop is not None and should_stop():
|
|
141
|
+
return None
|
|
142
|
+
if after is None:
|
|
143
|
+
return self.read()
|
|
144
|
+
with self._lock:
|
|
145
|
+
if self._error:
|
|
146
|
+
raise RuntimeError(self._error)
|
|
147
|
+
if self._frame is not None and self._read_started_at > after:
|
|
148
|
+
return self._frame, time.monotonic() - self._stamp
|
|
149
|
+
remaining = deadline - time.monotonic()
|
|
150
|
+
if remaining <= 0:
|
|
151
|
+
raise TimeoutError("camera produced no new frame after the motion boundary")
|
|
152
|
+
if self._stop.is_set():
|
|
153
|
+
raise RuntimeError("camera disconnected while waiting for a new frame")
|
|
154
|
+
time.sleep(min(0.005, remaining))
|
|
155
|
+
|
|
156
|
+
def disconnect(self) -> None:
|
|
157
|
+
with self._lifecycle:
|
|
158
|
+
self._stop.set()
|
|
159
|
+
with self._lock:
|
|
160
|
+
self._frame = None
|
|
161
|
+
self._stamp = self._read_started_at = 0.0
|
|
162
|
+
self._error = None
|
|
163
|
+
if self._thread is not None:
|
|
164
|
+
self._thread.join(timeout=2.0)
|
|
165
|
+
if not self._thread.is_alive():
|
|
166
|
+
self._thread = None
|
|
167
|
+
# Only the reader releases capture. A blocked read must not gain a second owner.
|