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,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