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.
Files changed (94) hide show
  1. roboapi/__init__.py +2 -0
  2. roboapi/calibration/__init__.py +6 -0
  3. roboapi/calibration/profile.py +139 -0
  4. roboapi/calibration/session.py +350 -0
  5. roboapi/cli.py +215 -0
  6. roboapi/config/__init__.py +22 -0
  7. roboapi/config/loader.py +119 -0
  8. roboapi/config/models.py +191 -0
  9. roboapi/embodiment.py +251 -0
  10. roboapi/errors.py +37 -0
  11. roboapi/geometry.py +337 -0
  12. roboapi/hardware/__init__.py +5 -0
  13. roboapi/hardware/base.py +113 -0
  14. roboapi/hardware/camera.py +167 -0
  15. roboapi/hardware/franka/__init__.py +5 -0
  16. roboapi/hardware/franka/arm.py +476 -0
  17. roboapi/hardware/franka/desk.py +555 -0
  18. roboapi/hardware/franka/feedback.py +196 -0
  19. roboapi/hardware/franka/guiding.py +104 -0
  20. roboapi/hardware/franka/ik.py +543 -0
  21. roboapi/hardware/franka/limits.py +110 -0
  22. roboapi/hardware/franka/settings.py +116 -0
  23. roboapi/hardware/robotiq.py +350 -0
  24. roboapi/hardware/ur/__init__.py +16 -0
  25. roboapi/hardware/ur/build.py +92 -0
  26. roboapi/hardware/ur/compliant.py +128 -0
  27. roboapi/hardware/ur/dashboard.py +103 -0
  28. roboapi/hardware/ur/feedback.py +159 -0
  29. roboapi/hardware/ur/ipc.py +206 -0
  30. roboapi/hardware/ur/native/CMakeLists.txt +37 -0
  31. roboapi/hardware/ur/native/dependencies.lock.json +13 -0
  32. roboapi/hardware/ur/native/include/ur_cpp/cartesian_speedl.hpp +170 -0
  33. roboapi/hardware/ur/native/include/ur_cpp/control_loop.hpp +126 -0
  34. roboapi/hardware/ur/native/include/ur_cpp/geom.hpp +72 -0
  35. roboapi/hardware/ur/native/include/ur_cpp/motion_primitives.hpp +350 -0
  36. roboapi/hardware/ur/native/include/ur_cpp/robot_io.hpp +26 -0
  37. roboapi/hardware/ur/native/include/ur_cpp/streaming_pose.hpp +369 -0
  38. roboapi/hardware/ur/native/src/control_loop.cpp +512 -0
  39. roboapi/hardware/ur/native/src/robot_io_urcl.cpp +143 -0
  40. roboapi/hardware/ur/native/tests/cartesian_speedl_unit.cpp +113 -0
  41. roboapi/hardware/ur/native/tests/control_modes_unit.cpp +137 -0
  42. roboapi/hardware/ur/native/tests/streaming_pose_unit.cpp +348 -0
  43. roboapi/hardware/ur/native/tests/velocity_governor_unit.cpp +65 -0
  44. roboapi/hardware/ur/native/worker.cpp +147 -0
  45. roboapi/hardware/ur/position.py +253 -0
  46. roboapi/hardware/ur/settings.py +95 -0
  47. roboapi/images.py +16 -0
  48. roboapi/initialize.py +137 -0
  49. roboapi/mcp.py +497 -0
  50. roboapi/motion.py +125 -0
  51. roboapi/naming.py +7 -0
  52. roboapi/operations.py +45 -0
  53. roboapi/policy/__init__.py +7 -0
  54. roboapi/policy/client.py +277 -0
  55. roboapi/policy/protocol.py +166 -0
  56. roboapi/policy/settings.py +79 -0
  57. roboapi/product-names.json +12 -0
  58. roboapi/records.py +149 -0
  59. roboapi/runtime.py +911 -0
  60. roboapi/semantics.py +6 -0
  61. roboapi/tasks.py +337 -0
  62. roboapi/web/__init__.py +5 -0
  63. roboapi/web/presets.py +44 -0
  64. roboapi/web/server.py +419 -0
  65. roboapi/web/static/assets/Bundler-LICENSE.txt +48 -0
  66. roboapi/web/static/assets/CalibrationScene-BO_spRoL.js +4220 -0
  67. roboapi/web/static/assets/Inter-OFL.txt +93 -0
  68. roboapi/web/static/assets/Inter-Variable-c8O0ljhh.ttf +0 -0
  69. roboapi/web/static/assets/Lucide-LICENSE.txt +43 -0
  70. roboapi/web/static/assets/Meshoptimizer-LICENSE.txt +23 -0
  71. roboapi/web/static/assets/MiSans-Regular-ES4DrbaF.woff2 +0 -0
  72. roboapi/web/static/assets/MiSans-Semibold-B34HIk9C.woff2 +0 -0
  73. roboapi/web/static/assets/PokeBrand-NOTICE.txt +20 -0
  74. roboapi/web/static/assets/React-LICENSE.txt +23 -0
  75. roboapi/web/static/assets/Three-LICENSE.txt +23 -0
  76. roboapi/web/static/assets/home-reference-CTpf8-oV.png +0 -0
  77. roboapi/web/static/assets/index-CGLhpUm0.js +28 -0
  78. roboapi/web/static/assets/index-QXqV-fDC.css +1 -0
  79. roboapi/web/static/assets/robotiq-ace-pro-2-EJuoOXTy.glb +0 -0
  80. roboapi/web/static/index.html +15 -0
  81. roboapi-0.4.2.dist-info/METADATA +205 -0
  82. roboapi-0.4.2.dist-info/RECORD +94 -0
  83. roboapi-0.4.2.dist-info/WHEEL +5 -0
  84. roboapi-0.4.2.dist-info/entry_points.txt +2 -0
  85. roboapi-0.4.2.dist-info/licenses/LICENSE +202 -0
  86. roboapi-0.4.2.dist-info/licenses/NOTICE +16 -0
  87. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Bundler-LICENSE.txt +48 -0
  88. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Inter-OFL.txt +93 -0
  89. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Lucide-LICENSE.txt +43 -0
  90. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Meshoptimizer-LICENSE.txt +23 -0
  91. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/PokeBrand-NOTICE.txt +20 -0
  92. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/React-LICENSE.txt +23 -0
  93. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Three-LICENSE.txt +23 -0
  94. roboapi-0.4.2.dist-info/top_level.txt +1 -0
@@ -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.
@@ -0,0 +1,5 @@
1
+ """Franka hardware adapter."""
2
+
3
+ from .arm import FrankaArm
4
+
5
+ __all__ = ["FrankaArm"]