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
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"""Franka Desk sessions, supported controller protocols and end-effector configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import quote
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .settings import STANDARD_TOOL
|
|
16
|
+
from ...errors import ControlNotReady
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeskLeaseError(RuntimeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ensure_programming(desk):
|
|
26
|
+
"""Prepare native guiding or tool configuration using an owned Desk session."""
|
|
27
|
+
if desk.get_fci_status() == "Active":
|
|
28
|
+
desk.disable_fci()
|
|
29
|
+
if getattr(desk.operating_mode, "value", None) != "Programming":
|
|
30
|
+
if desk.api == "v1":
|
|
31
|
+
desk.send_control_api_request(
|
|
32
|
+
"/api/system/operating-mode:change",
|
|
33
|
+
content={"desiredOperatingMode": "Programming"},
|
|
34
|
+
)
|
|
35
|
+
else:
|
|
36
|
+
desk.set_mode_programming()
|
|
37
|
+
deadline = time.monotonic() + 10
|
|
38
|
+
while (getattr(desk.operating_mode, "value", None) != "Programming"
|
|
39
|
+
or desk.get_fci_status() == "Active"):
|
|
40
|
+
if time.monotonic() >= deadline:
|
|
41
|
+
raise ControlNotReady("Desk did not enter Programming. Check the robot state and retry.")
|
|
42
|
+
time.sleep(0.05)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _DeskLease:
|
|
46
|
+
def __init__(
|
|
47
|
+
self, desk, *, timeout: float = 15.0, poll_period: float = 0.5,
|
|
48
|
+
release_attempts: int = 3, api: str | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.desk = desk
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
self.poll_period = poll_period
|
|
53
|
+
self.release_attempts = release_attempts
|
|
54
|
+
api = api or desk.api
|
|
55
|
+
if api not in ("v1", "panda4", "fr3-legacy"):
|
|
56
|
+
raise ValueError("Unsupported Desk API")
|
|
57
|
+
self.api = api
|
|
58
|
+
self.opened = False
|
|
59
|
+
self.acquired = False
|
|
60
|
+
self.fault: str | None = None
|
|
61
|
+
self._stop = threading.Event()
|
|
62
|
+
self._thread: threading.Thread | None = None
|
|
63
|
+
|
|
64
|
+
def _owner(self) -> str | None:
|
|
65
|
+
if self.api == "fr3-legacy":
|
|
66
|
+
active = self.desk._control_status()["activeToken"]
|
|
67
|
+
return None if active is None else str(active.get("ownedBy") or "unknown owner")
|
|
68
|
+
path = "/admin/api/control-token" if self.api == "panda4" else "/api/system/control-token"
|
|
69
|
+
response = self.desk.send_api_request(path, method="GET", response_encoding="json")
|
|
70
|
+
if not isinstance(response, dict):
|
|
71
|
+
raise DeskLeaseError("invalid Desk control-token response")
|
|
72
|
+
if self.api == "panda4":
|
|
73
|
+
if "activeToken" not in response:
|
|
74
|
+
raise DeskLeaseError("missing Panda control-token status")
|
|
75
|
+
active = response["activeToken"]
|
|
76
|
+
if active is None:
|
|
77
|
+
return None
|
|
78
|
+
if not isinstance(active, dict) or "id" not in active:
|
|
79
|
+
raise DeskLeaseError("invalid Panda control-token owner")
|
|
80
|
+
return str(active.get("ownedBy") or "unknown owner")
|
|
81
|
+
# The v1 API returns {} when control is available.
|
|
82
|
+
if not response:
|
|
83
|
+
return None
|
|
84
|
+
if "owner" not in response:
|
|
85
|
+
raise DeskLeaseError("missing Desk control-token owner")
|
|
86
|
+
owner = response.get("owner")
|
|
87
|
+
return str(owner) if owner else None
|
|
88
|
+
|
|
89
|
+
def acquire(self, *, prepare=None, activate=True) -> None:
|
|
90
|
+
try:
|
|
91
|
+
self.desk.open(timeout=self.timeout)
|
|
92
|
+
self.opened = True
|
|
93
|
+
owner = self._owner()
|
|
94
|
+
if owner is not None:
|
|
95
|
+
raise ControlNotReady(f"Release Desk control held by {owner!r}, then retry.")
|
|
96
|
+
self.desk.take_control(wait_timeout=self.timeout, force=False)
|
|
97
|
+
self.acquired = True
|
|
98
|
+
if prepare is not None:
|
|
99
|
+
prepare(self.desk)
|
|
100
|
+
if not activate:
|
|
101
|
+
return
|
|
102
|
+
self.desk.set_mode_execution()
|
|
103
|
+
if "UNLOCKED" not in str(self.desk.brake_state).upper():
|
|
104
|
+
self.desk.unlock_brakes()
|
|
105
|
+
self.desk.enable_fci()
|
|
106
|
+
deadline = time.monotonic() + self.timeout
|
|
107
|
+
while self.desk.get_fci_status() != "Active" and time.monotonic() < deadline:
|
|
108
|
+
time.sleep(0.05)
|
|
109
|
+
self.verify()
|
|
110
|
+
self._stop.clear()
|
|
111
|
+
self._thread = threading.Thread(target=self._monitor, name="franka-desk", daemon=True)
|
|
112
|
+
self._thread.start()
|
|
113
|
+
except Exception:
|
|
114
|
+
with suppress(Exception):
|
|
115
|
+
self.release()
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
def verify(self) -> None:
|
|
119
|
+
if not self.acquired or not bool(self.desk.has_control):
|
|
120
|
+
raise DeskLeaseError("Desk lease lost control ownership")
|
|
121
|
+
status = str(self.desk.get_fci_status())
|
|
122
|
+
if status != "Active":
|
|
123
|
+
raise DeskLeaseError(f"FCI left Active state while leased: {status}")
|
|
124
|
+
|
|
125
|
+
def check(self) -> None:
|
|
126
|
+
if self.fault is not None:
|
|
127
|
+
raise DeskLeaseError(self.fault)
|
|
128
|
+
|
|
129
|
+
def _monitor(self) -> None:
|
|
130
|
+
while not self._stop.wait(self.poll_period):
|
|
131
|
+
try:
|
|
132
|
+
self.verify()
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
self.fault = str(exc)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
def release(self) -> None:
|
|
138
|
+
self._stop.set()
|
|
139
|
+
thread, self._thread = self._thread, None
|
|
140
|
+
if thread is not None:
|
|
141
|
+
thread.join(timeout=self.timeout + 1.0)
|
|
142
|
+
if thread.is_alive():
|
|
143
|
+
raise DeskLeaseError("Desk monitor did not stop; token was not released")
|
|
144
|
+
errors = []
|
|
145
|
+
if self.acquired:
|
|
146
|
+
for attempt in range(1, self.release_attempts + 1):
|
|
147
|
+
try:
|
|
148
|
+
if not self.opened or not bool(getattr(self.desk, "is_open", False)):
|
|
149
|
+
self.desk.open(timeout=self.timeout)
|
|
150
|
+
self.opened = True
|
|
151
|
+
if self._owner() is None:
|
|
152
|
+
self.acquired = False
|
|
153
|
+
break
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
errors.append(f"attempt {attempt} owner check: {type(exc).__name__}: {exc}")
|
|
156
|
+
try:
|
|
157
|
+
self.desk.release_control()
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
errors.append(f"attempt {attempt} release: {type(exc).__name__}: {exc}")
|
|
160
|
+
try:
|
|
161
|
+
if self._owner() is None:
|
|
162
|
+
self.acquired = False
|
|
163
|
+
break
|
|
164
|
+
except Exception as owner_exc:
|
|
165
|
+
errors.append(
|
|
166
|
+
f"attempt {attempt} recheck: {type(owner_exc).__name__}: {owner_exc}"
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
self.acquired = False
|
|
170
|
+
try:
|
|
171
|
+
owner = self._owner()
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
logger.warning("Desk owner verification after release failed: %s", exc)
|
|
174
|
+
else:
|
|
175
|
+
if owner is not None:
|
|
176
|
+
errors.append(f"Desk control remained owned by {owner!r} after release")
|
|
177
|
+
break
|
|
178
|
+
if attempt < self.release_attempts:
|
|
179
|
+
time.sleep(0.2)
|
|
180
|
+
if self.opened and not self.acquired:
|
|
181
|
+
self.desk.close()
|
|
182
|
+
self.opened = False
|
|
183
|
+
if self.acquired:
|
|
184
|
+
raise DeskLeaseError(
|
|
185
|
+
"Desk token release failed: " + ("; ".join(errors) or "owner remained set")
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class _LegacyOwnership:
|
|
190
|
+
def __init__(self, *args, **kwargs):
|
|
191
|
+
super().__init__(*args, **kwargs)
|
|
192
|
+
self._pending_control: tuple[str, str] | None = None
|
|
193
|
+
|
|
194
|
+
def _clear_pending_control(self):
|
|
195
|
+
if self._pending_control is None:
|
|
196
|
+
return
|
|
197
|
+
token_id, token = self._pending_control
|
|
198
|
+
for attempt in range(3):
|
|
199
|
+
try:
|
|
200
|
+
status = self._control_status()
|
|
201
|
+
active, pending = status["activeToken"], status.get("tokenRequest")
|
|
202
|
+
if active is not None and str(active["id"]) == token_id:
|
|
203
|
+
self.send_api_request("/admin/api/control-token", method="DELETE", content={"token": token})
|
|
204
|
+
elif pending is not None and str(pending["id"]) == token_id:
|
|
205
|
+
self.send_api_request(
|
|
206
|
+
"/admin/api/control-token/request/" + token_id,
|
|
207
|
+
method="DELETE", content={"token": token},
|
|
208
|
+
)
|
|
209
|
+
self._pending_control = None
|
|
210
|
+
return
|
|
211
|
+
except Exception:
|
|
212
|
+
if attempt == 2:
|
|
213
|
+
raise
|
|
214
|
+
time.sleep(0.05)
|
|
215
|
+
|
|
216
|
+
def close(self):
|
|
217
|
+
# Keep the request credential and session if cleanup fails, so close can be retried.
|
|
218
|
+
self._clear_pending_control()
|
|
219
|
+
super().close()
|
|
220
|
+
|
|
221
|
+
def _get_has_control(self):
|
|
222
|
+
active = self._control_status()["activeToken"]
|
|
223
|
+
return active is not None and str(active["id"]) == self.control_token_id
|
|
224
|
+
|
|
225
|
+
def _take_control(self, wait_timeout=30.0, force=False):
|
|
226
|
+
if force:
|
|
227
|
+
raise self._sdk.DeskError("Forced Desk control takeover is not supported")
|
|
228
|
+
status = self._control_status()
|
|
229
|
+
if status["activeToken"] is not None or status.get("tokenRequest") is not None:
|
|
230
|
+
raise self._sdk.DeskError("Desk control is already owned or requested by another session")
|
|
231
|
+
response = self.send_api_request(
|
|
232
|
+
"/admin/api/control-token/request", content={"requestedBy": self.username}, response_encoding="json",
|
|
233
|
+
)
|
|
234
|
+
if not isinstance(response, dict) or "id" not in response or not isinstance(response.get("token"), str) or not response["token"]:
|
|
235
|
+
raise self._sdk.DeskError("Invalid Desk control request response; inspect Desk ownership")
|
|
236
|
+
token_id, token = str(response["id"]), response["token"]
|
|
237
|
+
if not token_id.isdecimal():
|
|
238
|
+
raise self._sdk.DeskError("Invalid Desk control request identity; inspect Desk ownership")
|
|
239
|
+
# franky adopts the token only after _take_control returns successfully.
|
|
240
|
+
self._pending_control = token_id, token
|
|
241
|
+
deadline = time.monotonic() + wait_timeout
|
|
242
|
+
try:
|
|
243
|
+
while True:
|
|
244
|
+
active = self._control_status()["activeToken"]
|
|
245
|
+
if active is not None and str(active["id"]) == token_id:
|
|
246
|
+
return token_id, token
|
|
247
|
+
if time.monotonic() >= deadline:
|
|
248
|
+
raise self._sdk.TakeControlTimeoutError("Desk did not grant this control request")
|
|
249
|
+
time.sleep(min(0.05, max(0.0, deadline - time.monotonic())))
|
|
250
|
+
except BaseException as error:
|
|
251
|
+
try:
|
|
252
|
+
self._clear_pending_control()
|
|
253
|
+
except Exception:
|
|
254
|
+
raise self._sdk.DeskError("Desk control cleanup is still pending; retry closing the session") from error
|
|
255
|
+
raise
|
|
256
|
+
|
|
257
|
+
def get_fci_status(self):
|
|
258
|
+
return "Active" if self._control_status()["fciActive"] else "Inactive"
|
|
259
|
+
|
|
260
|
+
def enable_fci(self):
|
|
261
|
+
self.send_control_api_request("/admin/api/control-token/fci", content={"token": self.control_token})
|
|
262
|
+
|
|
263
|
+
def disable_fci(self):
|
|
264
|
+
self.send_control_api_request(
|
|
265
|
+
"/admin/api/control-token/fci", method="DELETE", content={"token": self.control_token},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class _System4Endpoints(_LegacyOwnership):
|
|
270
|
+
api = "panda4"
|
|
271
|
+
|
|
272
|
+
def _control_status(self):
|
|
273
|
+
status = self.send_api_request("/admin/api/control-token", method="GET", response_encoding="json")
|
|
274
|
+
if not isinstance(status, dict) or "activeToken" not in status or "fciActive" not in status:
|
|
275
|
+
raise self._sdk.DeskError("Invalid Panda control-token status")
|
|
276
|
+
for field in ("activeToken", "tokenRequest"):
|
|
277
|
+
token = status.get(field)
|
|
278
|
+
if token is not None and (not isinstance(token, dict) or "id" not in token):
|
|
279
|
+
raise self._sdk.DeskError("Invalid Panda control-token identity")
|
|
280
|
+
if not isinstance(status["fciActive"], bool):
|
|
281
|
+
raise self._sdk.DeskError("Invalid Panda FCI status")
|
|
282
|
+
return status
|
|
283
|
+
|
|
284
|
+
def _get_system_status(self):
|
|
285
|
+
# System 4.x can label JSON responses as text; do not infer the parser from MIME.
|
|
286
|
+
return self.send_api_request("/desk/api/system/status", method="GET", response_encoding="json")
|
|
287
|
+
|
|
288
|
+
def _get_brake_state(self):
|
|
289
|
+
brakes = self._get_system_status().get("brakesOpen")
|
|
290
|
+
if not isinstance(brakes, list) or len(brakes) != 7 or any(type(b) is not bool for b in brakes):
|
|
291
|
+
raise self._sdk.DeskError("Invalid Panda brake state")
|
|
292
|
+
if all(brakes):
|
|
293
|
+
return self._sdk.BrakeState.UNLOCKED
|
|
294
|
+
if not any(brakes):
|
|
295
|
+
return self._sdk.BrakeState.LOCKED
|
|
296
|
+
raise self._sdk.DeskError("Panda joints have inconsistent brake states")
|
|
297
|
+
|
|
298
|
+
def _get_operating_mode(self):
|
|
299
|
+
mode = self._get_system_status().get("operationalMode")
|
|
300
|
+
if mode == "EXEC":
|
|
301
|
+
return self._sdk.OperatingMode.EXECUTION
|
|
302
|
+
if mode in ("SS1", "SS2"):
|
|
303
|
+
return self._sdk.OperatingMode.PROGRAMMING
|
|
304
|
+
raise self._sdk.DeskError("Unrecognized Panda operational mode")
|
|
305
|
+
|
|
306
|
+
def set_mode_execution(self):
|
|
307
|
+
# On 4.x activating FCI selects EXEC, after brakes are opened. There is no
|
|
308
|
+
# separate operating-mode route. Check the physical stop without changing it.
|
|
309
|
+
status = self._get_system_status()
|
|
310
|
+
if status.get("x3Enabled") is not True or status.get("operationalMode") not in ("SS1", "SS2", "EXEC"):
|
|
311
|
+
raise self._sdk.DeskError("Panda physical stop/mode prevents execution; inspect on site")
|
|
312
|
+
|
|
313
|
+
def unlock_brakes(self):
|
|
314
|
+
self.send_control_api_request("/desk/api/robot/open-brakes", content={"force": False})
|
|
315
|
+
|
|
316
|
+
def lock_brakes(self):
|
|
317
|
+
self.send_control_api_request("/desk/api/robot/close-brakes")
|
|
318
|
+
|
|
319
|
+
def set_mode_programming(self):
|
|
320
|
+
# Releasing a token may disable FCI while leaving the controller in EXEC.
|
|
321
|
+
if self.get_fci_status() == "Active" or self.operating_mode == self._sdk.OperatingMode.EXECUTION:
|
|
322
|
+
self.disable_fci()
|
|
323
|
+
|
|
324
|
+
def execute_self_test(self):
|
|
325
|
+
raise self._sdk.DeskError("Panda system 4.x self-tests must be handled through Desk on site")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class _LegacyFR3Endpoints(_LegacyOwnership):
|
|
329
|
+
"""Pre-v1 FR3 web protocol; retain the shared token request/cleanup lifecycle."""
|
|
330
|
+
|
|
331
|
+
api = "fr3-legacy"
|
|
332
|
+
|
|
333
|
+
def _get_system_status(self):
|
|
334
|
+
return self.send_api_request("/admin/api/system-status", method="GET", response_encoding="json")
|
|
335
|
+
|
|
336
|
+
def _control_status(self):
|
|
337
|
+
status = self._get_system_status().get("controlToken")
|
|
338
|
+
if (not isinstance(status, dict) or "activeToken" not in status
|
|
339
|
+
or not isinstance(status.get("fciActive"), bool)):
|
|
340
|
+
raise self._sdk.DeskError("Invalid legacy FR3 control-token status")
|
|
341
|
+
for field in ("activeToken", "tokenRequest"):
|
|
342
|
+
token = status.get(field)
|
|
343
|
+
if token is not None and (not isinstance(token, dict) or "id" not in token):
|
|
344
|
+
raise self._sdk.DeskError("Invalid legacy FR3 control-token identity")
|
|
345
|
+
return status
|
|
346
|
+
|
|
347
|
+
def _get_operating_mode(self):
|
|
348
|
+
return self._sdk.DeskWebSession._get_operating_mode(self)
|
|
349
|
+
|
|
350
|
+
def _get_brake_state(self):
|
|
351
|
+
return self._sdk.DeskWebSession._get_brake_state(self)
|
|
352
|
+
|
|
353
|
+
def set_mode_execution(self):
|
|
354
|
+
self._sdk.DeskWebSession.set_mode_execution(self)
|
|
355
|
+
|
|
356
|
+
def set_mode_programming(self):
|
|
357
|
+
if self.get_fci_status() == "Active":
|
|
358
|
+
self.disable_fci()
|
|
359
|
+
self._sdk.DeskWebSession.set_mode_programming(self)
|
|
360
|
+
|
|
361
|
+
def enable_fci(self):
|
|
362
|
+
try:
|
|
363
|
+
super().enable_fci()
|
|
364
|
+
except self._sdk.FrankaAPIError as exc:
|
|
365
|
+
if exc.http_code != 404:
|
|
366
|
+
raise
|
|
367
|
+
# Older clients expose these two activation routes. Only a missing
|
|
368
|
+
# endpoint selects the SDK's form-encoded route; other failures propagate.
|
|
369
|
+
self._sdk.DeskWebSession.enable_fci(self)
|
|
370
|
+
|
|
371
|
+
def unlock_brakes(self):
|
|
372
|
+
self._sdk.DeskWebSession.unlock_brakes(self)
|
|
373
|
+
|
|
374
|
+
def lock_brakes(self):
|
|
375
|
+
self._sdk.DeskWebSession.lock_brakes(self)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _desk_type(api):
|
|
379
|
+
import franky
|
|
380
|
+
|
|
381
|
+
if api == "v1":
|
|
382
|
+
return franky.Desk
|
|
383
|
+
if api not in ("panda4", "fr3-legacy"):
|
|
384
|
+
raise ValueError("Unsupported Desk API")
|
|
385
|
+
import franky.desk as sdk
|
|
386
|
+
|
|
387
|
+
endpoints = _System4Endpoints if api == "panda4" else _LegacyFR3Endpoints
|
|
388
|
+
|
|
389
|
+
class LegacySession(endpoints, sdk.DeskWebSession):
|
|
390
|
+
_sdk = sdk
|
|
391
|
+
|
|
392
|
+
return LegacySession
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def create_desk(cfg, password):
|
|
396
|
+
import httpx
|
|
397
|
+
|
|
398
|
+
if cfg.robot_model == "panda":
|
|
399
|
+
api = "panda4"
|
|
400
|
+
else:
|
|
401
|
+
# Probe a read-only endpoint. Authentication and network failures must not
|
|
402
|
+
# silently select another protocol, and probing never takes robot control.
|
|
403
|
+
response = httpx.get(
|
|
404
|
+
"https://" + cfg.host + "/api/system",
|
|
405
|
+
auth=(cfg.desk_user, password), verify=False, trust_env=False, timeout=10,
|
|
406
|
+
)
|
|
407
|
+
if response.status_code == 404:
|
|
408
|
+
api = "fr3-legacy"
|
|
409
|
+
else:
|
|
410
|
+
response.raise_for_status()
|
|
411
|
+
api = "v1"
|
|
412
|
+
desk = _desk_type(api)(cfg.host, cfg.desk_user, password)
|
|
413
|
+
desk.api = api
|
|
414
|
+
return desk
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
_PROFILES = "/admin/api/end-effector/profiles"
|
|
418
|
+
_INERTIA_ENTRIES = (("x11", 0, 0), ("x12", 0, 1), ("x13", 0, 2),
|
|
419
|
+
("x22", 1, 1), ("x23", 1, 2), ("x33", 2, 2))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def tool_inertial():
|
|
423
|
+
"""Encode the standard assembly as fresh Desk inertial fields."""
|
|
424
|
+
inertia = STANDARD_TOOL.inertia_kg_m2
|
|
425
|
+
return {
|
|
426
|
+
"mass": STANDARD_TOOL.mass_kg,
|
|
427
|
+
"centerOfMass": dict(zip("xyz", STANDARD_TOOL.com_flange_m)),
|
|
428
|
+
"inertia": {key: inertia[i][j] for key, i, j in _INERTIA_ENTRIES},
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _same_load(actual, expected):
|
|
433
|
+
try:
|
|
434
|
+
a = [actual["mass"], *[actual["centerOfMass"][k] for k in "xyz"],
|
|
435
|
+
*[actual["inertia"][k] for k in expected["inertia"]]]
|
|
436
|
+
b = [expected["mass"], *expected["centerOfMass"].values(), *expected["inertia"].values()]
|
|
437
|
+
return np.all(np.isfinite(a)) and np.allclose(a, b, rtol=1e-6, atol=1e-8)
|
|
438
|
+
except (KeyError, TypeError, ValueError):
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def tool_tcp():
|
|
443
|
+
"""Desk TCP coincides with the flange in both position and orientation."""
|
|
444
|
+
return {"translation": dict.fromkeys("xyz", 0.0),
|
|
445
|
+
"rotation": dict.fromkeys(("roll", "pitch", "yaw"), 0.0)}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _same_tcp(actual):
|
|
449
|
+
try:
|
|
450
|
+
values = [actual[group][axis] for group, axes in
|
|
451
|
+
(("translation", "xyz"), ("rotation", ("roll", "pitch", "yaw")))
|
|
452
|
+
for axis in axes]
|
|
453
|
+
return np.all(np.isfinite(values)) and np.allclose(values, 0.0, rtol=0, atol=1e-8)
|
|
454
|
+
except (KeyError, TypeError, ValueError):
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def apply_tool_configuration(cfg, control_desk):
|
|
459
|
+
"""Apply the standard inertial data and flange-centre TCP before FCI starts."""
|
|
460
|
+
import franky
|
|
461
|
+
from .feedback import read_robot_status
|
|
462
|
+
|
|
463
|
+
expected = tool_inertial()
|
|
464
|
+
password = Path(cfg.desk_password_file).expanduser().read_text().strip()
|
|
465
|
+
# Profiles use the web admin login, including when control uses the v1 API.
|
|
466
|
+
admin = franky.DeskWebSession(cfg.host, cfg.desk_user, password)
|
|
467
|
+
headers = {"Content-Type": "application/json", "X-Control-Token": control_desk.control_token}
|
|
468
|
+
try:
|
|
469
|
+
admin.open(timeout=15.0)
|
|
470
|
+
if control_desk.api in ("fr3-legacy", "panda4"):
|
|
471
|
+
try:
|
|
472
|
+
actual = read_robot_status(cfg, admin)["endEffectorConfiguration"]
|
|
473
|
+
expected_flat = tool_configuration_data()
|
|
474
|
+
matches = all(
|
|
475
|
+
np.shape(actual[key]) == np.shape(expected_flat[key])
|
|
476
|
+
and np.all(np.isfinite(actual[key]))
|
|
477
|
+
and np.allclose(actual[key], expected_flat[key], rtol=1e-6, atol=1e-8)
|
|
478
|
+
for key in expected_flat
|
|
479
|
+
)
|
|
480
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
481
|
+
raise ValueError("Unsupported Desk end-effector feedback") from exc
|
|
482
|
+
if not matches:
|
|
483
|
+
raise ControlNotReady(
|
|
484
|
+
"Desk end-effector load or TCP does not match the standard configuration. Run "
|
|
485
|
+
"'roboapi export-franka-tool', import endeffector-config.json in "
|
|
486
|
+
"Desk's end-effector settings, then retry."
|
|
487
|
+
)
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
def request(path, method="GET", content=None):
|
|
491
|
+
return admin.send_api_request(path, method=method, content=content,
|
|
492
|
+
headers=headers, response_encoding="json")
|
|
493
|
+
|
|
494
|
+
active = request(_PROFILES + "/active")
|
|
495
|
+
if not isinstance(active, dict) or not all(k in active for k in ("id", "name", "deviceId", "tcp", "inertial")):
|
|
496
|
+
raise ValueError("Desk returned an invalid active end-effector profile.")
|
|
497
|
+
body = {"name": active["name"], "deviceId": active["deviceId"],
|
|
498
|
+
"tcp": tool_tcp(), "inertial": expected}
|
|
499
|
+
profile_id = active["id"]
|
|
500
|
+
needs_update = (profile_id == "ee-no-profile" or not _same_load(active["inertial"], expected)
|
|
501
|
+
or not _same_tcp(active["tcp"]))
|
|
502
|
+
if needs_update:
|
|
503
|
+
ensure_programming(control_desk)
|
|
504
|
+
if profile_id == "ee-no-profile":
|
|
505
|
+
body.update(name="roboapi", deviceId="ee-no-gripper")
|
|
506
|
+
profiles = request(_PROFILES)
|
|
507
|
+
if not isinstance(profiles, list) or not all(isinstance(p, dict) for p in profiles):
|
|
508
|
+
raise ValueError("Desk returned an invalid end-effector profile list.")
|
|
509
|
+
matches = [p for p in profiles if p.get("name") == "roboapi"]
|
|
510
|
+
if len(matches) > 1:
|
|
511
|
+
raise ValueError("Desk has multiple roboapi end-effector profiles.")
|
|
512
|
+
if matches:
|
|
513
|
+
profile_id = matches[0]["id"]
|
|
514
|
+
else:
|
|
515
|
+
try:
|
|
516
|
+
created = request(_PROFILES, "POST", body)
|
|
517
|
+
except Exception as exc:
|
|
518
|
+
# franky's transport treats HTTP 201 as an error, although creation succeeded.
|
|
519
|
+
if getattr(exc, "http_code", None) != 201:
|
|
520
|
+
raise
|
|
521
|
+
created = json.loads(exc.message)
|
|
522
|
+
profile_id = created["id"]
|
|
523
|
+
if not isinstance(profile_id, str) or not profile_id:
|
|
524
|
+
raise ValueError("Desk returned an invalid end-effector profile ID.")
|
|
525
|
+
if needs_update:
|
|
526
|
+
request(_PROFILES + "/" + quote(profile_id, safe=""), "PUT", body)
|
|
527
|
+
request(_PROFILES + "/active", "PUT", profile_id)
|
|
528
|
+
applied = request(_PROFILES + "/active")
|
|
529
|
+
if (not isinstance(applied, dict) or applied.get("id") != profile_id
|
|
530
|
+
or not _same_tcp(applied.get("tcp"))
|
|
531
|
+
or applied.get("deviceId") != body["deviceId"]
|
|
532
|
+
or not _same_load(applied.get("inertial"), expected)):
|
|
533
|
+
raise ValueError("Desk did not apply the standard end-effector load and flange-centre TCP.")
|
|
534
|
+
finally:
|
|
535
|
+
admin.close()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def tool_configuration_data():
|
|
539
|
+
"""Legacy Desk import data for the standard load and identity flange-to-TCP transform."""
|
|
540
|
+
return {
|
|
541
|
+
"mass": STANDARD_TOOL.mass_kg,
|
|
542
|
+
"centerOfMass": list(STANDARD_TOOL.com_flange_m),
|
|
543
|
+
"inertia": np.asarray(STANDARD_TOOL.inertia_kg_m2).ravel(order="F").tolist(),
|
|
544
|
+
"transformation": np.eye(4).ravel(order="F").tolist(),
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def export_tool_configuration(output):
|
|
549
|
+
"""Export the complete standard end-effector configuration without connecting to Desk."""
|
|
550
|
+
data = tool_configuration_data()
|
|
551
|
+
target = Path(output).expanduser()
|
|
552
|
+
with target.open("x", encoding="utf-8") as stream:
|
|
553
|
+
json.dump(data, stream, indent=2, allow_nan=False)
|
|
554
|
+
stream.write("\n")
|
|
555
|
+
return target
|