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,196 @@
|
|
|
1
|
+
"""Read measured flange poses from Desk without taking control or enabling FCI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import ssl
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from ... import geometry as geo
|
|
15
|
+
from ...errors import ControlNotReady
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def connect_socket(host, token, path):
|
|
19
|
+
from websockets.sync.client import connect
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
return connect("wss://" + host + path,
|
|
23
|
+
ssl=ssl._create_unverified_context(), proxy=None,
|
|
24
|
+
additional_headers={"authorization": token},
|
|
25
|
+
open_timeout=5, close_timeout=2, max_size=65536, max_queue=4)
|
|
26
|
+
except Exception as exc:
|
|
27
|
+
raise RuntimeError(f"Franka Desk WebSocket {path} failed: {exc}") from exc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_robot_status(cfg, admin):
|
|
31
|
+
"""Read one robot status without using FR3-only routes on Panda."""
|
|
32
|
+
if cfg.robot_model == "panda":
|
|
33
|
+
with connect_socket(cfg.host, admin.token, "/desk/api/robot/status") as socket:
|
|
34
|
+
return json.loads(socket.recv(timeout=5))
|
|
35
|
+
return admin.send_api_request(
|
|
36
|
+
"/admin/api/system-status", method="GET", response_encoding="json",
|
|
37
|
+
)["robot"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def has_robot_errors(errors):
|
|
41
|
+
if isinstance(errors, dict) and all(type(value) is bool for value in errors.values()):
|
|
42
|
+
return any(errors.values())
|
|
43
|
+
if isinstance(errors, list) and all(isinstance(value, str) for value in errors):
|
|
44
|
+
return bool(errors)
|
|
45
|
+
raise ValueError("Unsupported Desk robot-error feedback")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def transform(values):
|
|
49
|
+
values = np.asarray(values, dtype=float)
|
|
50
|
+
if values.shape != (16,):
|
|
51
|
+
raise ValueError("Invalid Franka transform")
|
|
52
|
+
matrix = values.reshape(4, 4, order="F")
|
|
53
|
+
geo.matrix_to_pose7(matrix)
|
|
54
|
+
rotation = matrix[:3, :3]
|
|
55
|
+
if not np.allclose(rotation.T @ rotation, np.eye(3), atol=1e-6) or not np.isclose(np.linalg.det(rotation), 1.0):
|
|
56
|
+
raise ValueError("Invalid Franka rotation")
|
|
57
|
+
return matrix
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_state(configuration, status, serial, *, robot_model="fr3"):
|
|
61
|
+
try:
|
|
62
|
+
robot = status if robot_model == "panda" else status["robot"]
|
|
63
|
+
ee = robot["endEffectorConfiguration"]["transformation"]
|
|
64
|
+
pose = configuration["cartesianPose"]
|
|
65
|
+
errors = robot["robotErrors"]
|
|
66
|
+
except (KeyError, TypeError) as exc:
|
|
67
|
+
raise ValueError("Unsupported Desk feedback format. Check the robot system version.") from exc
|
|
68
|
+
flange = transform(pose) @ np.linalg.inv(transform(ee))
|
|
69
|
+
return {"timestamp": time.monotonic(), "pose": geo.matrix_to_pose7(flange).tolist(),
|
|
70
|
+
"has_errors": has_robot_errors(errors),
|
|
71
|
+
"device": {"serial": serial}}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FrankaFeedbackReader:
|
|
75
|
+
def __init__(self, cfg):
|
|
76
|
+
self.cfg = cfg
|
|
77
|
+
self._admin = self._socket = self._status_socket = self._thread = None
|
|
78
|
+
self._lock = threading.Lock()
|
|
79
|
+
self._state = None
|
|
80
|
+
self._error = None
|
|
81
|
+
self._stop = threading.Event()
|
|
82
|
+
|
|
83
|
+
def _connect_socket(self, path):
|
|
84
|
+
# Robot traffic stays on the robot LAN, independent of desktop proxy settings.
|
|
85
|
+
return connect_socket(self.cfg.host, self._admin.token, path)
|
|
86
|
+
|
|
87
|
+
def connect(self):
|
|
88
|
+
import franky
|
|
89
|
+
|
|
90
|
+
self.disconnect()
|
|
91
|
+
self._error = None
|
|
92
|
+
self._stop.clear()
|
|
93
|
+
password = Path(self.cfg.desk_password_file).expanduser().read_text().strip()
|
|
94
|
+
self._admin = franky.DeskWebSession(self.cfg.host, self.cfg.desk_user, password)
|
|
95
|
+
try:
|
|
96
|
+
self._admin.open(timeout=10)
|
|
97
|
+
self._serial = self._read_serial()
|
|
98
|
+
status_path = ("/desk/api/robot/status" if self.cfg.robot_model == "panda"
|
|
99
|
+
else "/admin/api/system-status")
|
|
100
|
+
self._status_socket = self._connect_socket(status_path)
|
|
101
|
+
self._status = json.loads(self._status_socket.recv(timeout=5))
|
|
102
|
+
self._socket = self._connect_socket("/desk/api/robot/configuration")
|
|
103
|
+
self._thread = threading.Thread(target=self._read, name="franka-feedback", daemon=True)
|
|
104
|
+
self._thread.start()
|
|
105
|
+
deadline = time.monotonic() + 5
|
|
106
|
+
while not self.live:
|
|
107
|
+
if self._error is not None:
|
|
108
|
+
raise self._error
|
|
109
|
+
if time.monotonic() >= deadline:
|
|
110
|
+
raise ValueError("No live Franka feedback. Check Desk access.")
|
|
111
|
+
time.sleep(0.02)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
self.disconnect()
|
|
114
|
+
if isinstance(exc, (ControlNotReady, RuntimeError, ValueError, OSError)):
|
|
115
|
+
raise
|
|
116
|
+
raise RuntimeError(f"Franka Desk feedback failed: {exc}") from exc
|
|
117
|
+
|
|
118
|
+
def _read_serial(self):
|
|
119
|
+
try:
|
|
120
|
+
with self._connect_socket("/admin/api/robot/nameplate") as socket:
|
|
121
|
+
nameplate = json.loads(socket.recv(timeout=5))
|
|
122
|
+
if nameplate.get("status") != "Connected":
|
|
123
|
+
raise ControlNotReady("Franka arm is not connected to Desk")
|
|
124
|
+
serial = nameplate.get("robotSerial")
|
|
125
|
+
except RuntimeError as exc:
|
|
126
|
+
response = getattr(exc.__cause__, "response", None)
|
|
127
|
+
if getattr(response, "status_code", None) != 404:
|
|
128
|
+
raise
|
|
129
|
+
# System 5.6 exposes the serial separately, without the newer nameplate feed.
|
|
130
|
+
serial = self._admin.send_api_request(
|
|
131
|
+
"/admin/api/robot/serial", method="GET", response_encoding="text",
|
|
132
|
+
)
|
|
133
|
+
try:
|
|
134
|
+
serial = json.loads(serial)
|
|
135
|
+
except ValueError:
|
|
136
|
+
pass
|
|
137
|
+
if not isinstance(serial, str) or not serial.strip():
|
|
138
|
+
raise ValueError("Desk returned an invalid arm serial number")
|
|
139
|
+
return serial.strip()
|
|
140
|
+
|
|
141
|
+
def _read(self):
|
|
142
|
+
try:
|
|
143
|
+
while not self._stop.is_set():
|
|
144
|
+
configuration = json.loads(self._socket.recv(timeout=1))
|
|
145
|
+
# Desk sends status changes separately from the periodic pose stream.
|
|
146
|
+
while True:
|
|
147
|
+
try:
|
|
148
|
+
self._status = json.loads(self._status_socket.recv(timeout=0))
|
|
149
|
+
except TimeoutError:
|
|
150
|
+
break
|
|
151
|
+
state = parse_state(configuration, self._status, self._serial,
|
|
152
|
+
robot_model=self.cfg.robot_model)
|
|
153
|
+
self.validate_device(state["device"])
|
|
154
|
+
with self._lock:
|
|
155
|
+
self._state = state
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
if not self._stop.is_set():
|
|
158
|
+
self._error = exc
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def live(self):
|
|
162
|
+
with self._lock:
|
|
163
|
+
return (not self._stop.is_set() and self._state is not None and self._error is None
|
|
164
|
+
and time.monotonic() - self._state["timestamp"] <= 0.5)
|
|
165
|
+
|
|
166
|
+
def snapshot(self):
|
|
167
|
+
with self._lock:
|
|
168
|
+
if self._state is None or self._error or time.monotonic() - self._state["timestamp"] > 0.5:
|
|
169
|
+
raise ValueError(str(self._error) if self._error else "Franka feedback is stale.")
|
|
170
|
+
return deepcopy(self._state)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def last_error(self):
|
|
174
|
+
if self._stop.is_set():
|
|
175
|
+
return None
|
|
176
|
+
return str(self._error) if self._error else (
|
|
177
|
+
"Franka read-only feedback is stale" if self._state is not None and not self.live else None)
|
|
178
|
+
|
|
179
|
+
def validate_device(self, device):
|
|
180
|
+
if set(device) != {"serial"} or not isinstance(device["serial"], str) or not device["serial"]:
|
|
181
|
+
raise ValueError("Invalid Franka calibration device information")
|
|
182
|
+
|
|
183
|
+
def disconnect(self):
|
|
184
|
+
self._stop.set()
|
|
185
|
+
for connection in (self._socket, self._status_socket):
|
|
186
|
+
if connection is not None:
|
|
187
|
+
connection.close()
|
|
188
|
+
if self._thread is not None:
|
|
189
|
+
self._thread.join(timeout=3)
|
|
190
|
+
if self._thread.is_alive():
|
|
191
|
+
raise RuntimeError("Franka feedback reader did not stop")
|
|
192
|
+
if self._admin is not None:
|
|
193
|
+
self._admin.close()
|
|
194
|
+
self._admin = self._socket = self._status_socket = self._thread = None
|
|
195
|
+
with self._lock:
|
|
196
|
+
self._state = None
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Native Programming-mode guiding; FCI is reserved for robot motion."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from ...errors import ControlNotReady
|
|
8
|
+
from .desk import _DeskLease, apply_tool_configuration, create_desk, ensure_programming
|
|
9
|
+
from .feedback import has_robot_errors, read_robot_status
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FrankaGuiding:
|
|
13
|
+
def __init__(self, cfg):
|
|
14
|
+
self.cfg = cfg
|
|
15
|
+
self.lease = self.admin = None
|
|
16
|
+
self.axis = None
|
|
17
|
+
self.tool_load_ready = False
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def active(self):
|
|
21
|
+
return self.lease is not None and self.lease.acquired
|
|
22
|
+
|
|
23
|
+
def start(self, axis=None):
|
|
24
|
+
import franky
|
|
25
|
+
|
|
26
|
+
if self.lease is not None:
|
|
27
|
+
raise RuntimeError("Franka guiding is already open")
|
|
28
|
+
self.tool_load_ready = False
|
|
29
|
+
password = Path(self.cfg.desk_password_file).expanduser().read_text().strip()
|
|
30
|
+
desk = create_desk(self.cfg, password)
|
|
31
|
+
self.lease = _DeskLease(desk)
|
|
32
|
+
try:
|
|
33
|
+
self.lease.acquire(activate=False)
|
|
34
|
+
self.admin = franky.DeskWebSession(self.cfg.host, self.cfg.desk_user, password)
|
|
35
|
+
self.admin.open(timeout=10)
|
|
36
|
+
self.check_stopped()
|
|
37
|
+
apply_tool_configuration(self.cfg, desk)
|
|
38
|
+
self.tool_load_ready = True
|
|
39
|
+
self.set_axis(axis)
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
self.close()
|
|
42
|
+
if isinstance(exc, (ControlNotReady, RuntimeError, ValueError, OSError)):
|
|
43
|
+
raise
|
|
44
|
+
raise RuntimeError(f"Franka guiding preparation failed: {exc}") from exc
|
|
45
|
+
|
|
46
|
+
def _api(self, path, **kwargs):
|
|
47
|
+
try:
|
|
48
|
+
return self.admin.send_api_request(path, **kwargs)
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
raise RuntimeError(f"Franka Desk request failed: {exc}") from exc
|
|
51
|
+
|
|
52
|
+
def check_stopped(self):
|
|
53
|
+
if not self.active or not self.lease.desk.has_control:
|
|
54
|
+
raise ControlNotReady("Desk control changed. Start calibration again.")
|
|
55
|
+
if self.cfg.robot_model == "panda":
|
|
56
|
+
robot = read_robot_status(self.cfg, self.admin)
|
|
57
|
+
else:
|
|
58
|
+
state = self._api("/admin/api/system-status", method="GET", response_encoding="json")
|
|
59
|
+
if state["safety"].get("safeInputState", {}).get("guidingEnableButton") == "Active":
|
|
60
|
+
raise ControlNotReady("Release the arm buttons, then retry.")
|
|
61
|
+
robot = state["robot"]
|
|
62
|
+
if has_robot_errors(robot["robotErrors"]):
|
|
63
|
+
raise ControlNotReady("Clear the robot error in Desk, then retry.")
|
|
64
|
+
ensure_programming(self.lease.desk)
|
|
65
|
+
|
|
66
|
+
def _request(self, path, field, value):
|
|
67
|
+
return self._api(
|
|
68
|
+
"/desk/api/robot/guiding/" + path, method="PUT",
|
|
69
|
+
headers={"X-Control-Token": self.lease.desk.control_token,
|
|
70
|
+
"Content-Type": "application/x-www-form-urlencoded"},
|
|
71
|
+
content={field: json.dumps(value)}, content_encoding="x-www-form-urlencoded",
|
|
72
|
+
response_encoding="text",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def set_axis(self, axis):
|
|
76
|
+
if axis not in (None, "x", "y", "z"):
|
|
77
|
+
raise ValueError("Guiding axis must be x, y or z")
|
|
78
|
+
self.check_stopped()
|
|
79
|
+
if axis is not None:
|
|
80
|
+
config = {"translation_" + key: key == axis for key in "xyz"}
|
|
81
|
+
config.update({"rotation_" + key: False for key in "xyz"})
|
|
82
|
+
config["elbow"] = False
|
|
83
|
+
self._request("configuration", "configuration", config)
|
|
84
|
+
self._request("mode", "mode", "free" if axis is None else "user")
|
|
85
|
+
expected = "free" if axis is None else "user"
|
|
86
|
+
deadline = time.monotonic() + 3
|
|
87
|
+
while True:
|
|
88
|
+
mode = self._api("/desk/api/robot/guiding/mode", method="GET", response_encoding="json")
|
|
89
|
+
actual = (self._api("/desk/api/robot/guiding/configuration", method="GET", response_encoding="json")
|
|
90
|
+
if axis is not None else None)
|
|
91
|
+
if mode["availableModes"][mode["currentMode"]] == expected and (axis is None or actual == config):
|
|
92
|
+
break
|
|
93
|
+
if time.monotonic() >= deadline:
|
|
94
|
+
raise RuntimeError("Desk did not apply the guiding constraint")
|
|
95
|
+
time.sleep(0.05)
|
|
96
|
+
self.axis = axis
|
|
97
|
+
|
|
98
|
+
def close(self):
|
|
99
|
+
if self.lease is not None:
|
|
100
|
+
self.lease.release()
|
|
101
|
+
self.lease = None
|
|
102
|
+
if self.admin is not None:
|
|
103
|
+
self.admin.close()
|
|
104
|
+
self.admin = None
|