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/__init__.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""One Cartesian installation calibration and atomic local storage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from numbers import Real
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .. import geometry as geo
|
|
16
|
+
from ..semantics import HOME_POSE7
|
|
17
|
+
from ..motion import pose_reached
|
|
18
|
+
|
|
19
|
+
DEFAULT_PROFILE_PATH = "~/.config/roboapi/calibration.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _pose(value, name):
|
|
23
|
+
if (not isinstance(value, (list, tuple, np.ndarray)) or np.shape(value) != (7,)
|
|
24
|
+
or any(isinstance(x, (bool, np.bool_)) or not isinstance(x, Real)
|
|
25
|
+
or not math.isfinite(x) for x in value)):
|
|
26
|
+
raise ValueError(f"{name} must contain 7 finite values")
|
|
27
|
+
geo.pose7_to_matrix(value)
|
|
28
|
+
return [float(x) for x in value]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _plane(value):
|
|
32
|
+
if not isinstance(value, (list, tuple, np.ndarray)) or np.shape(value) != (3, 7):
|
|
33
|
+
raise ValueError("reference plane requires three Cartesian poses: A, B and C")
|
|
34
|
+
return [_pose(pose, name) for pose, name in zip(value, ("A", "B", "C"))]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def fit_home_plane(home, plane):
|
|
38
|
+
"""Align Home, the level plane's upward normal and A-to-B heading.
|
|
39
|
+
|
|
40
|
+
Home is above the reference plane. All samples use the same tool orientation and
|
|
41
|
+
fixed contact point, so contact-point offsets cancel in the flange-position differences.
|
|
42
|
+
"""
|
|
43
|
+
home = np.asarray(_pose(home, "Home"))
|
|
44
|
+
a, b, c = np.asarray(_plane(plane))[:, :3]
|
|
45
|
+
ab, ac = b - a, c - a
|
|
46
|
+
ab_length, ac_length = np.linalg.norm(ab), np.linalg.norm(ac)
|
|
47
|
+
if ab_length <= 1e-6:
|
|
48
|
+
raise ValueError("Place B away from A toward task +X.")
|
|
49
|
+
normal = np.cross(ab, ac)
|
|
50
|
+
if ac_length <= 1e-6 or np.linalg.norm(normal) <= 1e-6 * ab_length * ac_length:
|
|
51
|
+
raise ValueError("Place C on the reference surface, away from line AB.")
|
|
52
|
+
z_axis = normal / np.linalg.norm(normal)
|
|
53
|
+
height = np.dot(home[:3] - a, z_axis)
|
|
54
|
+
if abs(height) <= 1e-6:
|
|
55
|
+
raise ValueError("Home must be above the reference surface.")
|
|
56
|
+
if height < 0:
|
|
57
|
+
z_axis = -z_axis
|
|
58
|
+
x_axis = ab - np.dot(ab, z_axis) * z_axis
|
|
59
|
+
x_axis /= np.linalg.norm(x_axis)
|
|
60
|
+
task_from_base = np.eye(4)
|
|
61
|
+
task_from_base[:3, :3] = np.column_stack((x_axis, np.cross(z_axis, x_axis), z_axis)).T
|
|
62
|
+
task_from_base[:3, 3] = np.asarray(HOME_POSE7[:3]) - task_from_base[:3, :3] @ home[:3]
|
|
63
|
+
native_from_tool = np.eye(4)
|
|
64
|
+
native_from_tool[:3, :3] = (
|
|
65
|
+
(task_from_base[:3, :3] @ geo.pose7_to_matrix(home)[:3, :3]).T
|
|
66
|
+
@ geo.pose7_to_matrix(HOME_POSE7)[:3, :3]
|
|
67
|
+
)
|
|
68
|
+
return geo.FrameAdapter(geo.matrix_to_pose7(task_from_base), geo.matrix_to_pose7(native_from_tool))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class CalibrationProfile:
|
|
73
|
+
identity: str
|
|
74
|
+
device: dict
|
|
75
|
+
home_native_pose7: list[float]
|
|
76
|
+
plane_native_poses7: list[list[float]]
|
|
77
|
+
check_native_pose7: list[float] | None = None
|
|
78
|
+
|
|
79
|
+
def __post_init__(self):
|
|
80
|
+
if not isinstance(self.identity, str) or not self.identity:
|
|
81
|
+
raise ValueError("calibration installation identity is required")
|
|
82
|
+
if not isinstance(self.device, dict) or not self.device:
|
|
83
|
+
raise ValueError("calibration device information is required")
|
|
84
|
+
# Device-specific validity is checked by the read-only reader.
|
|
85
|
+
json.dumps(self.device, allow_nan=False)
|
|
86
|
+
self.plane_native_poses7 = _plane(self.plane_native_poses7)
|
|
87
|
+
for name in ("home_native_pose7", "check_native_pose7"):
|
|
88
|
+
value = getattr(self, name)
|
|
89
|
+
if value is not None:
|
|
90
|
+
setattr(self, name, _pose(value, name))
|
|
91
|
+
elif name != "check_native_pose7":
|
|
92
|
+
raise ValueError(f"{name} is required")
|
|
93
|
+
|
|
94
|
+
def adapter(self):
|
|
95
|
+
return fit_home_plane(self.home_native_pose7, self.plane_native_poses7)
|
|
96
|
+
|
|
97
|
+
def validate(self, identity):
|
|
98
|
+
self.__post_init__()
|
|
99
|
+
if self.identity != identity:
|
|
100
|
+
raise ValueError("calibration does not match this installation")
|
|
101
|
+
self.adapter()
|
|
102
|
+
if self.check_native_pose7 is None:
|
|
103
|
+
raise ValueError("an independent return to Home must verify calibration")
|
|
104
|
+
if not pose_reached(self.home_native_pose7, self.check_native_pose7):
|
|
105
|
+
raise ValueError("Home return did not complete")
|
|
106
|
+
|
|
107
|
+
def to_dict(self):
|
|
108
|
+
return asdict(self)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def save_profile(profile, path=DEFAULT_PROFILE_PATH):
|
|
112
|
+
"""Replace the current local calibration atomically, with owner-only permissions."""
|
|
113
|
+
profile = CalibrationProfile(**profile.to_dict())
|
|
114
|
+
profile.validate(profile.identity)
|
|
115
|
+
target = Path(path).expanduser()
|
|
116
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
fd, temporary = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
|
|
118
|
+
try:
|
|
119
|
+
os.fchmod(fd, 0o600)
|
|
120
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
121
|
+
json.dump(profile.to_dict(), stream, indent=2, sort_keys=True, allow_nan=False)
|
|
122
|
+
stream.write("\n")
|
|
123
|
+
stream.flush()
|
|
124
|
+
os.fsync(stream.fileno())
|
|
125
|
+
os.replace(temporary, target)
|
|
126
|
+
finally:
|
|
127
|
+
if os.path.exists(temporary):
|
|
128
|
+
os.unlink(temporary)
|
|
129
|
+
return target
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_profile(path=DEFAULT_PROFILE_PATH):
|
|
133
|
+
raw = json.loads(Path(path).expanduser().read_text(encoding="utf-8"))
|
|
134
|
+
if not isinstance(raw, dict):
|
|
135
|
+
raise ValueError("calibration must be a JSON object")
|
|
136
|
+
try:
|
|
137
|
+
return CalibrationProfile(**raw)
|
|
138
|
+
except TypeError as exc:
|
|
139
|
+
raise ValueError(f"invalid calibration: {exc}") from exc
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Home and level-plane teaching with one automatic return to Home."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import replace
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from .. import geometry as geo
|
|
15
|
+
from ..semantics import ACTION_DT
|
|
16
|
+
from ..operations import exclusive
|
|
17
|
+
from ..errors import ControlNotReady
|
|
18
|
+
from ..motion import dispatch, pose_reached, wait_for_pose
|
|
19
|
+
from .profile import CalibrationProfile, load_profile, save_profile
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Calibration:
|
|
25
|
+
def __init__(self, runtime):
|
|
26
|
+
self.runtime = runtime
|
|
27
|
+
self.emb = emb = runtime.emb
|
|
28
|
+
self.operations = runtime.operations
|
|
29
|
+
self.mode = lambda: runtime.mode
|
|
30
|
+
self.cfg = emb.cfg.calibration
|
|
31
|
+
arm = emb.cfg.arm
|
|
32
|
+
identity = [arm.type, getattr(arm, "host", "") or getattr(arm, "ip", ""),
|
|
33
|
+
self.cfg.tool_id, emb.cfg.gripper.type, emb.cfg.gripper.port,
|
|
34
|
+
emb.cfg.camera.type, emb.cfg.camera.device]
|
|
35
|
+
self.identity = hashlib.sha256(json.dumps(identity).encode()).hexdigest()
|
|
36
|
+
self.profile = self.draft = None
|
|
37
|
+
self._motion_connection = False
|
|
38
|
+
self.guiding = emb.new_guiding_session()
|
|
39
|
+
self.guiding_axis = None
|
|
40
|
+
self._last_sample_timestamp = None
|
|
41
|
+
self.home_pose = None
|
|
42
|
+
self.plane_poses = []
|
|
43
|
+
self.device = None
|
|
44
|
+
self.phase = "acquire"
|
|
45
|
+
self.error = self.notice = None
|
|
46
|
+
try:
|
|
47
|
+
profile = load_profile(self.cfg.profile_file)
|
|
48
|
+
self._validate_profile(profile)
|
|
49
|
+
self._install(profile)
|
|
50
|
+
except FileNotFoundError:
|
|
51
|
+
pass
|
|
52
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
53
|
+
self.error = f"Calibration is invalid. Calibrate again. {exc}"
|
|
54
|
+
|
|
55
|
+
def _validate_profile(self, profile):
|
|
56
|
+
profile.validate(self.identity)
|
|
57
|
+
self.emb.new_feedback_reader().validate_device(profile.device)
|
|
58
|
+
|
|
59
|
+
def _install(self, profile):
|
|
60
|
+
self.emb.arm.set_adapter(profile.adapter())
|
|
61
|
+
self.emb.calibrated = True
|
|
62
|
+
self.profile = profile
|
|
63
|
+
self.phase = "complete"
|
|
64
|
+
|
|
65
|
+
def status(self):
|
|
66
|
+
profile = self.draft if self.phase == "preview" else self.profile if self.phase == "complete" else None
|
|
67
|
+
preview = {"axes_in_native_base": profile.adapter().base_from_task[:3, :3].T.tolist()} if profile else None
|
|
68
|
+
return {"phase": self.phase, "busy": self.operations.busy,
|
|
69
|
+
"teaching_method": "guiding" if self.emb.cfg.arm.type == "franka" else "pendant",
|
|
70
|
+
"guiding_axis": self.guiding_axis,
|
|
71
|
+
"connected": bool(getattr(self.emb.feedback, "live", False)),
|
|
72
|
+
"control_connected": self.emb.connected or self._motion_connection,
|
|
73
|
+
"control_enabled": self.emb.control_ready and self.emb.connected,
|
|
74
|
+
"complete": self.emb.calibrated, "error": self.error, "notice": self.notice,
|
|
75
|
+
"tool_load_ready": ((self.guiding.tool_load_ready or self.emb.arm.native.tool_load_ready)
|
|
76
|
+
if self.guiding else None),
|
|
77
|
+
"preview": preview,
|
|
78
|
+
"profile_file": str(Path(self.cfg.profile_file).expanduser().resolve()),
|
|
79
|
+
"hardware_released": not self.emb.connected and not self._motion_connection
|
|
80
|
+
and not (self.guiding and self.guiding.active)}
|
|
81
|
+
|
|
82
|
+
def interrupt(self):
|
|
83
|
+
self.operations.abort.set()
|
|
84
|
+
|
|
85
|
+
def close(self):
|
|
86
|
+
self.emb.close_feedback()
|
|
87
|
+
if self.guiding is not None:
|
|
88
|
+
self.guiding.close()
|
|
89
|
+
if self._motion_connection:
|
|
90
|
+
self.emb.arm.native.disconnect()
|
|
91
|
+
self._motion_connection = False
|
|
92
|
+
|
|
93
|
+
def _open_reader(self):
|
|
94
|
+
try:
|
|
95
|
+
if self.guiding is not None:
|
|
96
|
+
self.guiding.start(self.guiding_axis)
|
|
97
|
+
with self.emb.prepare_feedback() as reader:
|
|
98
|
+
state = self._snapshot(reader)
|
|
99
|
+
self._last_sample_timestamp = state["timestamp"]
|
|
100
|
+
except Exception:
|
|
101
|
+
if self.guiding is not None:
|
|
102
|
+
self.guiding.close()
|
|
103
|
+
raise
|
|
104
|
+
return state
|
|
105
|
+
|
|
106
|
+
def _snapshot(self, reader=None):
|
|
107
|
+
reader = self.emb.feedback if reader is None else reader
|
|
108
|
+
if reader is None or not reader.live:
|
|
109
|
+
raise ValueError("Read-only feedback is unavailable. Start again.")
|
|
110
|
+
state = reader.snapshot()
|
|
111
|
+
reader.validate_device(state["device"])
|
|
112
|
+
if self.device is not None and state["device"] != self.device:
|
|
113
|
+
raise ValueError("Robot settings changed. Exit and calibrate again.")
|
|
114
|
+
return state
|
|
115
|
+
|
|
116
|
+
def _sample(self):
|
|
117
|
+
if self.guiding is not None:
|
|
118
|
+
self.guiding.check_stopped()
|
|
119
|
+
deadline = time.monotonic() + 0.6
|
|
120
|
+
while True:
|
|
121
|
+
self._check_stop()
|
|
122
|
+
state = self._snapshot()
|
|
123
|
+
self._check_stop()
|
|
124
|
+
if state.get("has_errors"):
|
|
125
|
+
raise ControlNotReady("Clear the robot error, then capture again.")
|
|
126
|
+
if self._last_sample_timestamp is None or state["timestamp"] > self._last_sample_timestamp:
|
|
127
|
+
pose = np.asarray(state["pose"], dtype=float)
|
|
128
|
+
geo.pose7_to_matrix(pose)
|
|
129
|
+
self._last_sample_timestamp = state["timestamp"]
|
|
130
|
+
return pose.tolist()
|
|
131
|
+
if time.monotonic() >= deadline:
|
|
132
|
+
raise ValueError("No fresh feedback. Check the connection and capture again.")
|
|
133
|
+
time.sleep(0.02)
|
|
134
|
+
|
|
135
|
+
def _require_phase(self, phase):
|
|
136
|
+
if self.phase != phase:
|
|
137
|
+
raise ValueError("Follow the current teaching step.")
|
|
138
|
+
|
|
139
|
+
def _check_stop(self):
|
|
140
|
+
if self.operations.abort.is_set():
|
|
141
|
+
raise ValueError("Calibration stopped.")
|
|
142
|
+
|
|
143
|
+
def _return_to(self, target, name):
|
|
144
|
+
"""Borrow position control for a captured pose, then restore read-only sampling."""
|
|
145
|
+
self.emb.arm.check_motion_ready()
|
|
146
|
+
if self.guiding is not None:
|
|
147
|
+
self.guiding.check_stopped()
|
|
148
|
+
self.close()
|
|
149
|
+
native, cfg = self.emb.arm.native, self.emb.cfg
|
|
150
|
+
original_cfg = native.cfg if cfg.arm.type == "ur" else None
|
|
151
|
+
if getattr(original_cfg, "zero_ft_on_start", False):
|
|
152
|
+
# C is a contact sample. Preserve the sensor zero until ordinary control at Home.
|
|
153
|
+
native.cfg = replace(original_cfg, zero_ft_on_start=False)
|
|
154
|
+
self._motion_connection = True
|
|
155
|
+
completed = False
|
|
156
|
+
try:
|
|
157
|
+
self._check_stop()
|
|
158
|
+
native.connect()
|
|
159
|
+
self._check_stop()
|
|
160
|
+
if cfg.arm.type == "ur":
|
|
161
|
+
device = {**self.device, "tcp_offset": list(native.get_tcp_offset())}
|
|
162
|
+
self.emb.new_feedback_reader().validate_device(device)
|
|
163
|
+
if not np.allclose(device["tcp_offset"], self.device["tcp_offset"], atol=1e-6, rtol=0):
|
|
164
|
+
raise ValueError("The control connection changed the TCP. Calibrate again.")
|
|
165
|
+
plan = geo.plan_path(
|
|
166
|
+
native.get_eef_pose(), [target], 0.0, [0.0], [0.0],
|
|
167
|
+
speed=cfg.arm.cartesian_speed, angular_speed=cfg.arm.cartesian_angular_speed,
|
|
168
|
+
gripper_travel_s=0.0, dt=ACTION_DT,
|
|
169
|
+
)
|
|
170
|
+
def interrupted():
|
|
171
|
+
self._check_stop()
|
|
172
|
+
if not native.live:
|
|
173
|
+
raise ValueError(native.last_error or "Arm control is unavailable.")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
dispatch(native, plan.poses, should_stop=interrupted, position_only=True)
|
|
177
|
+
wait_for_pose(native, target, interrupted, name=name)
|
|
178
|
+
completed = True
|
|
179
|
+
finally:
|
|
180
|
+
try:
|
|
181
|
+
native.disconnect()
|
|
182
|
+
finally:
|
|
183
|
+
if original_cfg is not None:
|
|
184
|
+
native.cfg = original_cfg
|
|
185
|
+
self._motion_connection = False
|
|
186
|
+
try:
|
|
187
|
+
self._open_reader()
|
|
188
|
+
except Exception:
|
|
189
|
+
if completed:
|
|
190
|
+
raise
|
|
191
|
+
logger.warning("Read-only feedback could not resume after a failed return", exc_info=True)
|
|
192
|
+
pose = self._sample()
|
|
193
|
+
if not pose_reached(target, pose):
|
|
194
|
+
raise ValueError(f"{name} was not reached. Check the robot and retry Return {name}.")
|
|
195
|
+
return pose
|
|
196
|
+
|
|
197
|
+
def _return_home(self):
|
|
198
|
+
self._require_phase("return_home")
|
|
199
|
+
pose = self._return_to(self.home_pose, "Home")
|
|
200
|
+
self.draft = replace(self.draft, check_native_pose7=pose)
|
|
201
|
+
self.phase = "preview"
|
|
202
|
+
|
|
203
|
+
@exclusive
|
|
204
|
+
def action(self, action, data=None):
|
|
205
|
+
try:
|
|
206
|
+
return self._action(action, {} if data is None else data)
|
|
207
|
+
except ControlNotReady as exc:
|
|
208
|
+
self.notice = str(exc)
|
|
209
|
+
raise
|
|
210
|
+
except (ValueError, OSError, RuntimeError) as exc:
|
|
211
|
+
self.error = str(exc)
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
def _action(self, action, data):
|
|
215
|
+
if not isinstance(data, dict):
|
|
216
|
+
raise ValueError("request must be an object")
|
|
217
|
+
if self.mode() != "idle":
|
|
218
|
+
raise ValueError("Stop the current action first.")
|
|
219
|
+
if action not in {"acquire", "confirm_home", "confirm_plane_a", "confirm_plane_b", "confirm_plane_c", "return_home", "return_a",
|
|
220
|
+
"save", "undo", "enable_control", "set_guiding_axis"}:
|
|
221
|
+
raise ValueError("unknown calibration action")
|
|
222
|
+
self.operations.reset_abort()
|
|
223
|
+
self.error = self.notice = None
|
|
224
|
+
if action == "acquire":
|
|
225
|
+
if self.phase not in ("acquire", "complete"):
|
|
226
|
+
raise ValueError("Exit the current calibration first.")
|
|
227
|
+
if data.get("confirmed") is not True:
|
|
228
|
+
raise ValueError("Confirm level-plane teaching and the automatic return to Home after C.")
|
|
229
|
+
self.close()
|
|
230
|
+
self.emb.disconnect_control()
|
|
231
|
+
self._check_stop()
|
|
232
|
+
self.device = None
|
|
233
|
+
self.home_pose = self.draft = None
|
|
234
|
+
self.plane_poses = []
|
|
235
|
+
self.guiding_axis = None
|
|
236
|
+
try:
|
|
237
|
+
self.device = self._open_reader()["device"]
|
|
238
|
+
self._check_stop()
|
|
239
|
+
except Exception:
|
|
240
|
+
self.close()
|
|
241
|
+
raise
|
|
242
|
+
self.emb.control_ready = False
|
|
243
|
+
self.phase = "teach_home"
|
|
244
|
+
elif action == "undo":
|
|
245
|
+
previous = {
|
|
246
|
+
"plane_a": ("teach_home", 0),
|
|
247
|
+
"plane_b": ("plane_a", 0),
|
|
248
|
+
"plane_c": ("plane_b", 1),
|
|
249
|
+
"return_home": ("plane_c", 2),
|
|
250
|
+
"preview": ("plane_c", 2),
|
|
251
|
+
}.get(self.phase)
|
|
252
|
+
if previous is None:
|
|
253
|
+
raise ValueError("There is no recorded step to undo.")
|
|
254
|
+
phase, count = previous
|
|
255
|
+
if phase == "teach_home":
|
|
256
|
+
if self.guiding is not None:
|
|
257
|
+
self.guiding.set_axis(None)
|
|
258
|
+
self.guiding_axis = None
|
|
259
|
+
self.home_pose = None
|
|
260
|
+
self.plane_poses = self.plane_poses[:count]
|
|
261
|
+
self.draft = None
|
|
262
|
+
self.phase = phase
|
|
263
|
+
elif action == "enable_control":
|
|
264
|
+
self._require_phase("complete")
|
|
265
|
+
self._validate_profile(self.profile)
|
|
266
|
+
if data.get("confirmed") is not True or data.get("initialization_confirmed") is not True:
|
|
267
|
+
raise ValueError("Confirm that the area is safe and hardware/gripper initialization is allowed.")
|
|
268
|
+
self.close()
|
|
269
|
+
try:
|
|
270
|
+
with self.emb.prepare_feedback() as reader:
|
|
271
|
+
if reader.snapshot()["device"] != self.profile.device:
|
|
272
|
+
raise ValueError("Robot settings do not match calibration. Calibrate again.")
|
|
273
|
+
self.emb.close_feedback()
|
|
274
|
+
if self.operations.abort.is_set():
|
|
275
|
+
raise ValueError("Connection was stopped.")
|
|
276
|
+
self.runtime.connect()
|
|
277
|
+
if self.emb.cfg.arm.type == "ur":
|
|
278
|
+
offset = list(self.emb.arm.native.get_tcp_offset())
|
|
279
|
+
self.emb.new_feedback_reader().validate_device({
|
|
280
|
+
"serial": self.profile.device["serial"],
|
|
281
|
+
"tcp_offset": offset,
|
|
282
|
+
})
|
|
283
|
+
if not np.allclose(offset, self.profile.device["tcp_offset"], atol=1e-6, rtol=0):
|
|
284
|
+
raise ValueError("The control connection changed the TCP. Calibrate again.")
|
|
285
|
+
if not self.emb.alive or self.operations.abort.is_set():
|
|
286
|
+
raise ValueError("The devices are not ready or the connection was stopped.")
|
|
287
|
+
self.emb.control_ready = True
|
|
288
|
+
except Exception:
|
|
289
|
+
self.emb.control_ready = False
|
|
290
|
+
self.close()
|
|
291
|
+
self.runtime.disconnect()
|
|
292
|
+
self.emb.connect_preview()
|
|
293
|
+
raise
|
|
294
|
+
elif action == "confirm_home":
|
|
295
|
+
self._require_phase("teach_home")
|
|
296
|
+
pose = self._sample()
|
|
297
|
+
if self.guiding is not None:
|
|
298
|
+
self.guiding.set_axis("z")
|
|
299
|
+
self.guiding_axis = "z"
|
|
300
|
+
self.home_pose = pose
|
|
301
|
+
self.phase = "plane_a"
|
|
302
|
+
elif action == "set_guiding_axis":
|
|
303
|
+
if self.guiding is None or self.phase not in {"plane_a", "plane_b", "plane_c"}:
|
|
304
|
+
raise ValueError("Single-axis guiding is available during Franka plane teaching.")
|
|
305
|
+
axis = data.get("axis")
|
|
306
|
+
if axis not in ("x", "y", "z"):
|
|
307
|
+
raise ValueError("Guiding axis must be x, y or z")
|
|
308
|
+
self.guiding.set_axis(axis)
|
|
309
|
+
self.guiding_axis = axis
|
|
310
|
+
elif action == "confirm_plane_a":
|
|
311
|
+
self._require_phase("plane_a")
|
|
312
|
+
self.plane_poses = [self._sample()]
|
|
313
|
+
self.phase = "plane_b"
|
|
314
|
+
elif action == "confirm_plane_b":
|
|
315
|
+
self._require_phase("plane_b")
|
|
316
|
+
pose = self._sample()
|
|
317
|
+
if np.linalg.norm(np.asarray(pose[:3]) - self.plane_poses[0][:3]) <= 1e-6:
|
|
318
|
+
raise ValueError("Place B away from A toward task +X.")
|
|
319
|
+
self.plane_poses.append(pose)
|
|
320
|
+
self.phase = "plane_c"
|
|
321
|
+
elif action == "confirm_plane_c":
|
|
322
|
+
self._require_phase("plane_c")
|
|
323
|
+
plane = [*self.plane_poses, self._sample()]
|
|
324
|
+
candidate = CalibrationProfile(self.identity, self.device, self.home_pose, plane)
|
|
325
|
+
candidate.adapter()
|
|
326
|
+
self.plane_poses = plane
|
|
327
|
+
self.draft = candidate
|
|
328
|
+
self.phase = "return_home"
|
|
329
|
+
self._return_home()
|
|
330
|
+
elif action == "return_home":
|
|
331
|
+
self._return_home()
|
|
332
|
+
elif action == "return_a":
|
|
333
|
+
if self.phase not in {"plane_b", "plane_c"}:
|
|
334
|
+
raise ValueError("Capture A before using Return A during plane teaching.")
|
|
335
|
+
self._return_to(self.plane_poses[0], "A")
|
|
336
|
+
elif action == "save":
|
|
337
|
+
self._require_phase("preview")
|
|
338
|
+
if data.get("confirmed") is not True:
|
|
339
|
+
raise ValueError("Confirm saving the calibration.")
|
|
340
|
+
self._snapshot()
|
|
341
|
+
self._validate_profile(self.draft)
|
|
342
|
+
if self.operations.abort.is_set():
|
|
343
|
+
raise ValueError("Saving was stopped.")
|
|
344
|
+
save_profile(self.draft, self.cfg.profile_file)
|
|
345
|
+
self._install(self.draft)
|
|
346
|
+
if self.guiding is not None:
|
|
347
|
+
self.guiding.close()
|
|
348
|
+
return self.status()
|
|
349
|
+
|
|
350
|
+
|