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,476 @@
1
+ """Franka: native base/flange poses -> feedback-leashed IK -> joint impedance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import collections
6
+ import logging
7
+ import os
8
+ import threading
9
+ import time
10
+ from contextlib import contextmanager
11
+ from dataclasses import dataclass, replace
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+
16
+ from ... import geometry as geo
17
+ from ...errors import ControlNotReady
18
+ from .desk import DeskLeaseError, _DeskLease, apply_tool_configuration, create_desk
19
+ from .ik import solve_feedback_leashed_ik
20
+ from .limits import load_limits
21
+ from .settings import COLLISION_FORCE, COLLISION_TORQUE, IKConfig
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ NUM_JOINTS = 7
26
+
27
+
28
+ @contextmanager
29
+ def _realtime_control_start(realtime_config: str, *, inherit: bool = True):
30
+ """Let each new native control thread inherit FIFO, then restore its caller.
31
+
32
+ Tracking and stop motions create native threads through move().
33
+ Elevate only during asynchronous startup; joining and Python work run under
34
+ the caller's original scheduler. Robot construction can itself elevate its
35
+ caller, so it uses this scope only to restore that original scheduler.
36
+ """
37
+ if realtime_config != "enforce":
38
+ yield
39
+ return
40
+ policy = os.sched_getscheduler(0)
41
+ param = os.sched_getparam(0)
42
+ priority = os.sched_get_priority_max(os.SCHED_FIFO)
43
+ try:
44
+ if inherit:
45
+ os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(priority))
46
+ if (os.sched_getscheduler(0) != os.SCHED_FIFO
47
+ or os.sched_getparam(0).sched_priority != priority):
48
+ raise RuntimeError("Franka control startup requires SCHED_FIFO at maximum priority")
49
+ yield
50
+ finally:
51
+ os.sched_setscheduler(0, policy, param)
52
+ if (os.sched_getscheduler(0) != policy
53
+ or os.sched_getparam(0).sched_priority != param.sched_priority):
54
+ raise RuntimeError("Failed to restore Franka control caller's scheduling")
55
+
56
+
57
+ class _JointSession:
58
+ """The small part of franky's JointImpedanceTracker needed here."""
59
+
60
+ def __init__(self, robot, franky, cfg) -> None:
61
+ self.robot = robot
62
+ self.franky = franky
63
+ self._realtime_config = cfg.realtime_config
64
+ gains = cfg.joint_impedance
65
+ self.motion = franky.JointImpedanceTrackingMotion(
66
+ stiffness=np.asarray(gains.stiffness),
67
+ damping=np.asarray(gains.damping),
68
+ cartesian_stiffness=np.asarray(gains.cartesian_stiffness),
69
+ cartesian_damping=np.asarray(gains.cartesian_damping),
70
+ max_delta_tau=gains.max_delta_tau,
71
+ gains_time_constant=gains.gains_time_constant,
72
+ )
73
+
74
+ def start(self, q: np.ndarray) -> None:
75
+ self.set_target(q, np.zeros(NUM_JOINTS))
76
+ with _realtime_control_start(self._realtime_config):
77
+ self.robot.move(
78
+ self.motion,
79
+ asynchronous=True,
80
+ limit_rate=True,
81
+ cutoff_frequency=100.0,
82
+ )
83
+
84
+ def set_target(self, q: np.ndarray, dq: np.ndarray) -> None:
85
+ self.motion.set_reference(self.franky.JointReference(q=q, dq=dq))
86
+
87
+ def stop(self) -> None:
88
+ if bool(self.robot.is_in_control):
89
+ motion = self.franky.TorqueStopMotion()
90
+ with _realtime_control_start(self._realtime_config):
91
+ self.robot.move(motion, asynchronous=True)
92
+ try:
93
+ self.robot.join_motion()
94
+ except Exception as exc:
95
+ # Defensive parity with franky's tracker wrapper: self-preemption means the
96
+ # torque loop did stop; every other ControlException remains actionable.
97
+ if not isinstance(exc, getattr(self.franky, "ControlException", ())) or (
98
+ "Move command preempted!" not in str(exc)
99
+ ):
100
+ raise
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class FrankaSnapshot:
105
+ sample_time: float
106
+ received_at: float
107
+ q: np.ndarray
108
+ dq: np.ndarray
109
+ flange_pose_wxyz: np.ndarray
110
+ jacobian: np.ndarray
111
+
112
+
113
+ class FrankaArm:
114
+ """Owns Desk, one Franka connection, and one 50 Hz Hybrid worker."""
115
+
116
+ def __init__(self, cfg) -> None:
117
+ self.cfg = cfg
118
+ self._franky = None
119
+ self._robot = None
120
+ self._model = None
121
+ self._lease: _DeskLease | None = None
122
+ self._session: _JointSession | None = None
123
+ self._thread: threading.Thread | None = None
124
+ self._stop = threading.Event()
125
+ self._started = threading.Event()
126
+ self._fault: str | None = None
127
+ self.tool_load_ready = False
128
+ self._target: tuple[np.ndarray, np.ndarray] | None = None
129
+ self._snapshot: FrankaSnapshot | None = None
130
+ self._snapshot_lock = threading.Lock()
131
+ self._trace = collections.deque(maxlen=cfg.trace_capacity)
132
+ self._trace_lock = threading.Lock()
133
+ control_period = 1.0 / float(cfg.hybrid_hz)
134
+ self._limits = load_limits(cfg.robot_model, None)
135
+ self._ik = IKConfig(
136
+ nominal_dt=control_period,
137
+ max_rotation_step=float(cfg.cartesian_angular_speed) * control_period,
138
+ posture_gain=0.20,
139
+ posture_max_velocity=0.10,
140
+ )
141
+ self._configure_limits()
142
+
143
+ def _configure_limits(self) -> None:
144
+ self._lower = np.asarray(self._limits.joint_lower)
145
+ self._upper = np.asarray(self._limits.joint_upper)
146
+ self._ik = replace(
147
+ self._ik,
148
+ max_joint_velocity=self._limits.joint_velocity,
149
+ max_joint_acceleration=self._limits.joint_acceleration,
150
+ max_translation_velocity=self._limits.cartesian_velocity[0],
151
+ max_rotation_velocity=self._limits.cartesian_velocity[1],
152
+ )
153
+
154
+ @property
155
+ def live(self) -> bool:
156
+ return (
157
+ self._robot is not None
158
+ and self._fault is None
159
+ and self._thread is not None
160
+ and self._thread.is_alive()
161
+ )
162
+
163
+ @property
164
+ def last_error(self) -> str | None:
165
+ return self._fault
166
+
167
+ def connect(self) -> None:
168
+ self._connect(recover_from_errors=False)
169
+
170
+ def _acquire_desk(self) -> None:
171
+ if self._robot is not None:
172
+ raise RuntimeError("Franka is already connected")
173
+ self.tool_load_ready = False
174
+ if self._lease is not None:
175
+ # A previous failed connect may have left an authenticated lease awaiting
176
+ # release. Never overwrite that handle with a new one.
177
+ self._lease.release()
178
+ self._lease = None
179
+ import franky
180
+
181
+ password_path = Path(self.cfg.desk_password_file).expanduser()
182
+ password = password_path.read_text().strip()
183
+ if not password:
184
+ raise RuntimeError(f"Franka Desk password file is empty: {password_path}")
185
+ self._franky = franky
186
+ self._fault = None
187
+ self._lease = _DeskLease(create_desk(self.cfg, password))
188
+
189
+ def prepare(desk):
190
+ try:
191
+ apply_tool_configuration(self.cfg, desk)
192
+ except ControlNotReady:
193
+ raise
194
+ except Exception as exc:
195
+ raise RuntimeError(f"Franka end-effector initialization failed: {exc}") from exc
196
+ self.tool_load_ready = True
197
+
198
+ try:
199
+ self._lease.acquire(prepare=prepare)
200
+ except ControlNotReady:
201
+ raise
202
+ except Exception as exc:
203
+ raise RuntimeError(f"Franka Desk preparation failed: {exc}") from exc
204
+
205
+ def _connect(self, *, recover_from_errors: bool) -> None:
206
+ if self._robot is not None:
207
+ raise RuntimeError("Franka is already connected")
208
+ try:
209
+ self._acquire_desk()
210
+ franky = self._franky
211
+ realtime = (
212
+ franky.RealtimeConfig.Enforce
213
+ if self.cfg.realtime_config == "enforce"
214
+ else franky.RealtimeConfig.Ignore
215
+ )
216
+ with _realtime_control_start(self.cfg.realtime_config, inherit=False):
217
+ self._robot = franky.Robot(self.cfg.host, realtime_config=realtime)
218
+ self._limits = load_limits(
219
+ self.cfg.robot_model, getattr(self._robot, "model_urdf", None),
220
+ )
221
+ self._configure_limits()
222
+ if recover_from_errors:
223
+ self._robot.recover_from_errors()
224
+ # This synchronous SDK call must succeed before any control worker starts.
225
+ self._robot.set_collision_behavior(COLLISION_TORQUE, COLLISION_FORCE)
226
+ logger.info(
227
+ "Franka collision thresholds applied: joints=%s Nm; Cartesian=%s (N,N,N,Nm,Nm,Nm)",
228
+ COLLISION_TORQUE, COLLISION_FORCE,
229
+ )
230
+ self._model = self._robot.model
231
+ snapshot = self._read_snapshot()
232
+ self._ik = replace(self._ik, posture_reference=tuple(snapshot.q))
233
+ self._cache_snapshot(snapshot)
234
+ self._start_worker()
235
+ except Exception:
236
+ try:
237
+ self._teardown()
238
+ except Exception as cleanup_exc:
239
+ logger.error("Franka connect cleanup failed; preserving original error: %s", cleanup_exc)
240
+ raise
241
+
242
+ def disconnect(self) -> None:
243
+ self._teardown()
244
+
245
+ def _teardown(self) -> None:
246
+ self._stop_worker()
247
+ if self._robot is not None and bool(self._robot.is_in_control):
248
+ self._latch_fault("Franka control session is still active; Desk token was not released")
249
+ raise RuntimeError(self._fault)
250
+ self._model = None
251
+ self._robot = None
252
+ lease = self._lease
253
+ if lease is not None:
254
+ lease.release()
255
+ self._lease = None
256
+
257
+ def _start_worker(self) -> None:
258
+ self._target = None
259
+ self._stop.clear()
260
+ self._started.clear()
261
+ self._thread = threading.Thread(target=self._worker, name="franka-hybrid", daemon=True)
262
+ self._thread.start()
263
+ if not self._started.wait(2.0):
264
+ raise RuntimeError("Franka Hybrid worker did not start")
265
+ if self._fault is not None:
266
+ raise RuntimeError(self._fault)
267
+
268
+ def _stop_worker(self) -> None:
269
+ self._stop.set()
270
+ thread = self._thread
271
+ if thread is not None:
272
+ thread.join(timeout=4.0)
273
+ if thread.is_alive():
274
+ self._latch_fault("Franka Hybrid worker did not stop")
275
+ raise RuntimeError(self._fault)
276
+ self._thread = None
277
+ self._target = None
278
+
279
+ def _latch_fault(self, message: str) -> None:
280
+ if self._fault is None:
281
+ self._fault = message
282
+
283
+ def wait_for_fault(self, timeout: float = 4.0) -> str | None:
284
+ """Collect a late native exception without delaying fault detection or polling."""
285
+ thread = self._thread
286
+ if self._fault is not None and thread is not None and thread is not threading.current_thread():
287
+ thread.join(timeout=timeout)
288
+ if thread.is_alive():
289
+ logger.warning("Franka fault cleanup still pending after %.1f s: %s", timeout, self._fault)
290
+ return self._fault
291
+
292
+ def _read_snapshot(self) -> FrankaSnapshot:
293
+ state = self._robot.state
294
+ now = time.monotonic()
295
+ flange = self._model.pose(self._franky.Frame.Flange, state)
296
+ flange_matrix = np.asarray(flange.matrix, dtype=np.float64)
297
+ return FrankaSnapshot(
298
+ sample_time=float(state.time.to_sec()),
299
+ received_at=now,
300
+ q=np.asarray(state.q, dtype=np.float64).copy(),
301
+ dq=np.asarray(state.dq, dtype=np.float64).copy(),
302
+ flange_pose_wxyz=geo.matrix_to_pose7(flange_matrix),
303
+ jacobian=np.asarray(
304
+ self._model.zero_jacobian(self._franky.Frame.Flange, state), dtype=np.float64
305
+ ).copy(),
306
+ )
307
+
308
+ def _check_hardware(self) -> None:
309
+ if bool(self._robot.has_errors):
310
+ raise RuntimeError("Franka reflex or robot error is active")
311
+ if not bool(self._robot.is_in_control):
312
+ raise RuntimeError("joint impedance tracker terminated unexpectedly")
313
+
314
+ def _check_freshness(
315
+ self, snapshot: FrankaSnapshot, last_sample: float | None, last_received: float
316
+ ) -> tuple[float, float, float]:
317
+ """Track repeated samples for diagnostics; native SDK faults end control."""
318
+ if last_sample is not None and snapshot.sample_time < last_sample - 1e-9:
319
+ raise RuntimeError("Franka state clock moved backwards")
320
+ if last_sample is None or snapshot.sample_time > last_sample + 1e-9:
321
+ last_sample = snapshot.sample_time
322
+ last_received = snapshot.received_at
323
+ sample_age = snapshot.received_at - last_received
324
+ return last_sample, last_received, sample_age
325
+
326
+ def _cache_snapshot(self, snapshot: FrankaSnapshot) -> None:
327
+ with self._snapshot_lock:
328
+ self._snapshot = snapshot
329
+
330
+ def _worker(self) -> None:
331
+ period = 1.0 / float(self.cfg.hybrid_hz)
332
+ previous_dq = np.zeros(NUM_JOINTS)
333
+ last_sample = None
334
+ sample_received_at = time.monotonic()
335
+ deadline = time.monotonic()
336
+ try:
337
+ self._lease.check()
338
+ snapshot = self._read_snapshot()
339
+ # A worker always starts from a measured, discontinuity-free reference. Keep
340
+ # that reference fixed until a Cartesian target replaces it. Updating the joint
341
+ # reference from every subsequent measurement would turn the nominal idle hold
342
+ # into unintended free guiding: an external push would move both the arm and its
343
+ # reference, leaving no restoring error.
344
+ idle_hold_q = snapshot.q.copy()
345
+ idle_hold_pose = snapshot.flange_pose_wxyz.copy()
346
+ self._session = _JointSession(self._robot, self._franky, self.cfg)
347
+ self._session.start(idle_hold_q)
348
+ self._started.set()
349
+ while not self._stop.is_set():
350
+ deadline += period
351
+ remaining = deadline - time.monotonic()
352
+ if remaining > 0:
353
+ self._stop.wait(remaining)
354
+ if self._stop.is_set():
355
+ break
356
+ self._lease.check()
357
+ self._check_hardware()
358
+ snapshot = self._read_snapshot()
359
+ last_sample, sample_received_at, sample_age = self._check_freshness(
360
+ snapshot, last_sample, sample_received_at
361
+ )
362
+ self._cache_snapshot(snapshot)
363
+
364
+ target = self._target
365
+ hold_reason = None
366
+ if target is None:
367
+ target_pose = idle_hold_pose
368
+ qd, dqd = idle_hold_q, np.zeros(NUM_JOINTS)
369
+ pose_error = np.zeros(6)
370
+ margin = float(np.min(np.minimum(snapshot.q - self._lower, self._upper - snapshot.q)))
371
+ task_scale, velocity_limited, posture_limited = 0.0, False, False
372
+ hold_reason = "no_target"
373
+ else:
374
+ target_pose, target_twist = target
375
+ solved = solve_feedback_leashed_ik(
376
+ q=snapshot.q,
377
+ current_pose7=geo.obs_pose(snapshot.flange_pose_wxyz),
378
+ target_pose7=geo.obs_pose(target_pose),
379
+ target_twist=target_twist,
380
+ jacobian=snapshot.jacobian,
381
+ lower_limits=self._lower,
382
+ upper_limits=self._upper,
383
+ dt=period,
384
+ config=self._ik,
385
+ previous_dq=previous_dq,
386
+ joint_velocity_bounds=self._limits.velocity_bounds(snapshot.q),
387
+ )
388
+ qd, dqd = solved.qd, solved.dq
389
+ pose_error = solved.pose_error
390
+ margin = solved.minimum_joint_margin
391
+ task_scale = solved.task_scale
392
+ velocity_limited = solved.joint_velocity_limited
393
+ posture_limited = solved.posture_limited
394
+ self._session.set_target(qd, dqd)
395
+ previous_dq = np.asarray(dqd).copy()
396
+ self._append_trace(
397
+ snapshot, target_pose, qd, dqd, pose_error, margin, task_scale,
398
+ velocity_limited, posture_limited, hold_reason, sample_age,
399
+ )
400
+ except Exception as exc:
401
+ logger.exception("Franka Hybrid worker failed")
402
+ self._latch_fault(str(exc))
403
+ self._started.set()
404
+ finally:
405
+ session, self._session = self._session, None
406
+ if session is not None:
407
+ try:
408
+ session.stop()
409
+ except Exception as exc:
410
+ logger.exception("Franka joint impedance tracker failed during shutdown")
411
+ # The SDK may only expose its original control exception at join,
412
+ # after its cached flags have only reported a generic worker fault.
413
+ message = f"{type(exc).__name__}: {exc}"
414
+ if self._fault is not None:
415
+ message += f"\nEarlier worker fault: {self._fault}"
416
+ self._fault = message
417
+
418
+ def _append_trace(
419
+ self, snapshot, target, qd, dqd, pose_error, margin, scale,
420
+ velocity_limited, posture_limited, hold_reason, sample_age,
421
+ ) -> None:
422
+ item = {
423
+ "t_monotonic": snapshot.received_at,
424
+ "sample_ms": round(sample_age * 1e3, 3),
425
+ "target_pose": np.asarray(target).tolist(),
426
+ "measured_pose": snapshot.flange_pose_wxyz.tolist(),
427
+ "native_frame": "franka_base",
428
+ "q": snapshot.q.tolist(),
429
+ "dq": snapshot.dq.tolist(),
430
+ "qd": np.asarray(qd).tolist(),
431
+ "dqd": np.asarray(dqd).tolist(),
432
+ "pose_error": np.asarray(pose_error).tolist(),
433
+ "minimum_joint_margin": float(margin),
434
+ "task_scale": float(scale),
435
+ "joint_velocity_limited": bool(velocity_limited),
436
+ "posture_limited": bool(posture_limited),
437
+ "ik_hold_reason": hold_reason,
438
+ }
439
+ with self._trace_lock:
440
+ self._trace.append(item)
441
+
442
+ def drain_trace(self) -> list[dict]:
443
+ with self._trace_lock:
444
+ trace = list(self._trace)
445
+ self._trace.clear()
446
+ return trace
447
+
448
+ def get_joints(self) -> np.ndarray:
449
+ with self._snapshot_lock:
450
+ if self._snapshot is None:
451
+ raise RuntimeError("Franka is not connected")
452
+ return self._snapshot.q.copy()
453
+
454
+ def get_eef_pose(self) -> np.ndarray:
455
+ with self._snapshot_lock:
456
+ if self._snapshot is None:
457
+ raise RuntimeError("Franka is not connected")
458
+ return self._snapshot.flange_pose_wxyz.copy()
459
+
460
+ def servo_pose(self, pose7: np.ndarray, twist: np.ndarray | None = None, *, position_only=False) -> None:
461
+ del position_only # The shared Cartesian targets use joint impedance without added admittance.
462
+ pose = np.asarray(pose7, dtype=np.float64)
463
+ velocity = np.zeros(6) if twist is None else np.asarray(twist, dtype=np.float64)
464
+ if pose.shape != (7,) or not np.all(np.isfinite(pose)):
465
+ raise ValueError("servo_pose expects 7 finite pose values")
466
+ if velocity.shape != (6,) or not np.all(np.isfinite(velocity)):
467
+ raise ValueError("servo_pose expects 6 finite twist values")
468
+ if not self.live:
469
+ raise RuntimeError(f"not_alive: {self.last_error}. Call recover().")
470
+ pose = np.concatenate([pose[:3], geo.normalize_quat(pose[3:])])
471
+ self._target = (pose, velocity.copy())
472
+
473
+ def recover(self) -> None:
474
+ """A hardware fault is cleared only by this explicit reconnect."""
475
+ self._teardown()
476
+ self._connect(recover_from_errors=True)