urml-cobot-runtime 0.4.0__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.
- urml_cobot_runtime/__init__.py +45 -0
- urml_cobot_runtime/_version.py +3 -0
- urml_cobot_runtime/adapter.py +861 -0
- urml_cobot_runtime/config.py +53 -0
- urml_cobot_runtime/py.typed +0 -0
- urml_cobot_runtime-0.4.0.dist-info/METADATA +117 -0
- urml_cobot_runtime-0.4.0.dist-info/RECORD +8 -0
- urml_cobot_runtime-0.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""urml_cobot_runtime — collaborative-arm reference runtime for URML.
|
|
2
|
+
|
|
3
|
+
UrRtdeAdapter — Universal Robots via RTDE (rtde_control/receive)
|
|
4
|
+
FrankaFciAdapter — Franka via FCI (panda-py)
|
|
5
|
+
(+ CobotConfig, load_cobot_config)
|
|
6
|
+
|
|
7
|
+
The two most-deployed cobots driven by their native SDKs with **zero
|
|
8
|
+
ROS** — the proof that the popular real arms need no ROS. The
|
|
9
|
+
ROS-free siblings of industrial-arm-runtime's RclpyAdapter-composed
|
|
10
|
+
Ur/Franka adapters, parallel to how marine-runtime is the ROS-free
|
|
11
|
+
sibling of ros2-runtime. Vendor SDKs are imported lazily (the
|
|
12
|
+
[ur]/[franka] extras), so this module loads on every host. Built
|
|
13
|
+
against the frozen substrate Protocol per RFC-0014; spec gaps are in
|
|
14
|
+
SPEC-GAPS.md, not silently patched.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from urml_cobot_runtime._version import __version__
|
|
20
|
+
from urml_cobot_runtime.adapter import (
|
|
21
|
+
CobotConfig,
|
|
22
|
+
DoosanDrflAdapter,
|
|
23
|
+
FrankaFciAdapter,
|
|
24
|
+
KassowKrAdapter,
|
|
25
|
+
KinovaKortexAdapter,
|
|
26
|
+
MecademicMeca500Adapter,
|
|
27
|
+
NeuraMairaAdapter,
|
|
28
|
+
TechmanTmflowAdapter,
|
|
29
|
+
UrRtdeAdapter,
|
|
30
|
+
load_cobot_config,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"CobotConfig",
|
|
35
|
+
"DoosanDrflAdapter",
|
|
36
|
+
"FrankaFciAdapter",
|
|
37
|
+
"KassowKrAdapter",
|
|
38
|
+
"KinovaKortexAdapter",
|
|
39
|
+
"MecademicMeca500Adapter",
|
|
40
|
+
"NeuraMairaAdapter",
|
|
41
|
+
"TechmanTmflowAdapter",
|
|
42
|
+
"UrRtdeAdapter",
|
|
43
|
+
"__version__",
|
|
44
|
+
"load_cobot_config",
|
|
45
|
+
]
|
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
"""Zero-ROS cobot adapters — Universal Robots (RTDE) + Franka (FCI).
|
|
2
|
+
|
|
3
|
+
The two most-deployed collaborative arms driven by their **native
|
|
4
|
+
SDKs with no ROS**. industrial-arm-runtime's Ur/Franka adapters
|
|
5
|
+
compose ``RclpyAdapter`` (ROS 2 + MoveIt 2); these are the ROS-free
|
|
6
|
+
siblings — the proof that the popular arms need no ROS — exactly as
|
|
7
|
+
``marine-runtime`` is the ROS-free sibling of ``ros2-runtime``. Both
|
|
8
|
+
mirror :class:`BlueRovAdapter`: lazy vendor SDK, cached lazily-opened
|
|
9
|
+
connection, failures returned not raised.
|
|
10
|
+
|
|
11
|
+
## v0.1 method coverage (both adapters)
|
|
12
|
+
|
|
13
|
+
Supported: ``move_to``/``hover`` (drive the TCP to a configured pose),
|
|
14
|
+
``grasp``/``release`` (gripper command; ``force_n`` honoured at v0.1
|
|
15
|
+
fidelity), ``wait``, ``measure`` (TCP force / robot state), ``wait_for``
|
|
16
|
+
(state read-once), ``report`` (local sink, no cloud), ``scan``
|
|
17
|
+
(documented stub).
|
|
18
|
+
|
|
19
|
+
Not supported by a bare cobot (returned, not raised): ``dock`` (no
|
|
20
|
+
station), ``detect``/``capture``/``speak``/``listen`` (no
|
|
21
|
+
perception/HMI — pair a companion). The drone trio is
|
|
22
|
+
``not_applicable_cobot``.
|
|
23
|
+
|
|
24
|
+
Two needs the cobots surfaced are recorded in ``SPEC-GAPS.md`` rather
|
|
25
|
+
than bolted on: parametric force/impedance beyond scalar ``force_n``
|
|
26
|
+
(composable watch-item, no RFC) and raw digital-I/O tool actuation
|
|
27
|
+
(genuinely inexpressible → RFC-0017 Draft).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from contextlib import suppress
|
|
33
|
+
from typing import Any, Literal
|
|
34
|
+
|
|
35
|
+
from urml_ros2_runtime.substrate.base import (
|
|
36
|
+
ProgramCallResult,
|
|
37
|
+
unsupported_program_call,
|
|
38
|
+
CaptureResult,
|
|
39
|
+
DetectionResult,
|
|
40
|
+
ListenResult,
|
|
41
|
+
ManipulationResult,
|
|
42
|
+
MeasurementResult,
|
|
43
|
+
NavigationResult,
|
|
44
|
+
ScanResult,
|
|
45
|
+
SubstrateResult,
|
|
46
|
+
WaitResult,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from urml_cobot_runtime._version import __version__
|
|
50
|
+
from urml_cobot_runtime.config import CobotConfig, Pose, load_cobot_config
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"CobotConfig",
|
|
54
|
+
"DoosanDrflAdapter",
|
|
55
|
+
"FrankaFciAdapter",
|
|
56
|
+
"KassowKrAdapter",
|
|
57
|
+
"KinovaKortexAdapter",
|
|
58
|
+
"MecademicMeca500Adapter",
|
|
59
|
+
"NeuraMairaAdapter",
|
|
60
|
+
"Pose",
|
|
61
|
+
"TechmanTmflowAdapter",
|
|
62
|
+
"UrRtdeAdapter",
|
|
63
|
+
"__version__",
|
|
64
|
+
"load_cobot_config",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
_NOT_SUPPORTED = (
|
|
68
|
+
"not_supported_on_bare_cobot: a bare collaborative arm has no {capability}. "
|
|
69
|
+
"Pair it with a vision/HMI/station companion adapter; the URML program, "
|
|
70
|
+
"manifest, and validator are unchanged."
|
|
71
|
+
)
|
|
72
|
+
_NOT_APPLICABLE = "not_applicable_cobot: {capability} has no meaning for a fixed collaborative arm."
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _flat_pose(vector: list[float]) -> dict[str, float]:
|
|
76
|
+
"""final_pose is dict[str, float]; expose the pose vector as indexed scalars."""
|
|
77
|
+
return {f"q{i}": float(v) for i, v in enumerate(vector)}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _CobotBase:
|
|
81
|
+
"""Shared Protocol surface: the not-supported sentinels + passive ops.
|
|
82
|
+
|
|
83
|
+
Subclasses implement ``_open`` (vendor connect, cached),
|
|
84
|
+
``send_navigation_goal``, ``send_manipulation_goal``, and
|
|
85
|
+
``take_measurement``.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
BRAND = "cobot"
|
|
89
|
+
|
|
90
|
+
def __init__(self, config: CobotConfig | None = None) -> None:
|
|
91
|
+
self._config = config or CobotConfig()
|
|
92
|
+
self._conn: Any = None
|
|
93
|
+
self._reports: list[dict[str, Any]] = []
|
|
94
|
+
self._closed = False
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
if self._closed:
|
|
98
|
+
return
|
|
99
|
+
if self._conn is not None:
|
|
100
|
+
with suppress(Exception):
|
|
101
|
+
close = getattr(self._conn, "disconnect", None) or getattr(self._conn, "stopScript", None)
|
|
102
|
+
if callable(close):
|
|
103
|
+
close()
|
|
104
|
+
self._closed = True
|
|
105
|
+
|
|
106
|
+
def __enter__(self) -> _CobotBase:
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def __exit__(self, *_: object) -> None:
|
|
110
|
+
self.close()
|
|
111
|
+
|
|
112
|
+
def wait_passively(self, *, duration_seconds: float) -> SubstrateResult:
|
|
113
|
+
return SubstrateResult(success=True)
|
|
114
|
+
|
|
115
|
+
def wait_for_condition(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
kind: Literal["event", "signal", "input", "sensor_threshold"],
|
|
119
|
+
name: str | None,
|
|
120
|
+
input_mode: str | None,
|
|
121
|
+
threshold: dict[str, Any] | None,
|
|
122
|
+
timeout_seconds: float | None,
|
|
123
|
+
) -> WaitResult:
|
|
124
|
+
return WaitResult(success=True, timed_out=False, payload=None)
|
|
125
|
+
|
|
126
|
+
def emit_report(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
to: str,
|
|
130
|
+
facts: dict[str, Any],
|
|
131
|
+
attachments: list[str] | None,
|
|
132
|
+
status: Literal["success", "partial", "failure"],
|
|
133
|
+
severity: Literal["info", "notice", "warning", "error"],
|
|
134
|
+
) -> SubstrateResult:
|
|
135
|
+
self._reports.append({"to": to, "status": status, "severity": severity, "facts": facts})
|
|
136
|
+
return SubstrateResult(success=True)
|
|
137
|
+
|
|
138
|
+
def run_scan(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
area: dict[str, Any],
|
|
142
|
+
pattern: Literal["serpentine", "spiral", "grid", "adaptive"],
|
|
143
|
+
overlap: float,
|
|
144
|
+
altitude: float | None,
|
|
145
|
+
media: Literal["photo", "video", "sensor_only"],
|
|
146
|
+
sensor: str | None,
|
|
147
|
+
) -> ScanResult:
|
|
148
|
+
return ScanResult(
|
|
149
|
+
success=True,
|
|
150
|
+
payload={"samples": [], "coverage": 0.0, "anomalies": [], "_note": "v0.1 cobot scan: stub."},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def send_docking_goal(self, *, station: str, service: str, until: str | None = None) -> NavigationResult:
|
|
154
|
+
return NavigationResult(success=False, reason=_NOT_SUPPORTED.format(capability="docking station"))
|
|
155
|
+
|
|
156
|
+
def query_detection(
|
|
157
|
+
self,
|
|
158
|
+
*,
|
|
159
|
+
object_class: str,
|
|
160
|
+
attributes: dict[str, Any] | None = None,
|
|
161
|
+
where_near: str | None = None,
|
|
162
|
+
where_within: float | None = None,
|
|
163
|
+
) -> DetectionResult:
|
|
164
|
+
return DetectionResult(success=False, reason=_NOT_SUPPORTED.format(capability="onboard detection"))
|
|
165
|
+
|
|
166
|
+
def capture_media(
|
|
167
|
+
self,
|
|
168
|
+
*,
|
|
169
|
+
media: Literal["photo", "video"],
|
|
170
|
+
target: str | None,
|
|
171
|
+
duration_seconds: float | None,
|
|
172
|
+
attributes: dict[str, Any] | None,
|
|
173
|
+
) -> CaptureResult:
|
|
174
|
+
return CaptureResult(success=False, reason=_NOT_SUPPORTED.format(capability="recordable camera"))
|
|
175
|
+
|
|
176
|
+
def emit_speech(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
utterance: str,
|
|
180
|
+
locale: str | None,
|
|
181
|
+
style: Literal["notice", "warning", "conversational"],
|
|
182
|
+
interrupt: bool,
|
|
183
|
+
) -> SubstrateResult:
|
|
184
|
+
return SubstrateResult(success=False, reason=_NOT_SUPPORTED.format(capability="speaker"))
|
|
185
|
+
|
|
186
|
+
def acquire_speech(
|
|
187
|
+
self,
|
|
188
|
+
*,
|
|
189
|
+
prompt: str | None,
|
|
190
|
+
locale: str | None,
|
|
191
|
+
timeout_seconds: float | None,
|
|
192
|
+
expected: Literal["free_form", "confirmation", "choice"],
|
|
193
|
+
choices: list[str] | None,
|
|
194
|
+
) -> ListenResult:
|
|
195
|
+
return ListenResult(success=False, reason=_NOT_SUPPORTED.format(capability="microphone"))
|
|
196
|
+
|
|
197
|
+
def send_takeoff_goal(self, *, altitude: float, climb_rate: float | None = None) -> NavigationResult:
|
|
198
|
+
return NavigationResult(success=False, reason=_NOT_APPLICABLE.format(capability="take_off"))
|
|
199
|
+
|
|
200
|
+
def send_land_goal(
|
|
201
|
+
self,
|
|
202
|
+
*,
|
|
203
|
+
at: str | None = None,
|
|
204
|
+
precision: Literal["standard", "precise"] = "standard",
|
|
205
|
+
) -> NavigationResult:
|
|
206
|
+
return NavigationResult(success=False, reason=_NOT_APPLICABLE.format(capability="land"))
|
|
207
|
+
|
|
208
|
+
def send_return_to_home_goal(
|
|
209
|
+
self,
|
|
210
|
+
*,
|
|
211
|
+
speed: float | None = None,
|
|
212
|
+
altitude: float | None = None,
|
|
213
|
+
) -> NavigationResult:
|
|
214
|
+
return NavigationResult(success=False, reason=_NOT_APPLICABLE.format(capability="return_to_home"))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class UrRtdeAdapter(_CobotBase):
|
|
218
|
+
"""Universal Robots via RTDE — zero ROS."""
|
|
219
|
+
|
|
220
|
+
BRAND = "ur"
|
|
221
|
+
|
|
222
|
+
def _open(self) -> tuple[Any, Any]:
|
|
223
|
+
if self._conn is not None:
|
|
224
|
+
return self._conn
|
|
225
|
+
try:
|
|
226
|
+
import rtde_control # type: ignore[import-not-found,unused-ignore]
|
|
227
|
+
import rtde_receive # type: ignore[import-not-found,unused-ignore]
|
|
228
|
+
except ImportError as exc:
|
|
229
|
+
raise RuntimeError(
|
|
230
|
+
"ur_rtde is not installed. UrRtdeAdapter requires the [ur] extra.\n"
|
|
231
|
+
" Install with: pip install urml-cobot-runtime[ur]"
|
|
232
|
+
) from exc
|
|
233
|
+
ctrl = rtde_control.RTDEControlInterface(self._config.robot_ip)
|
|
234
|
+
recv = rtde_receive.RTDEReceiveInterface(self._config.robot_ip)
|
|
235
|
+
self._conn = (ctrl, recv)
|
|
236
|
+
return self._conn
|
|
237
|
+
|
|
238
|
+
def send_navigation_goal(
|
|
239
|
+
self,
|
|
240
|
+
*,
|
|
241
|
+
location: str | None = None,
|
|
242
|
+
pose: dict[str, float] | None = None,
|
|
243
|
+
frame: str | None = None,
|
|
244
|
+
carrying: dict[str, Any] | None = None,
|
|
245
|
+
speed: float | None = None,
|
|
246
|
+
) -> NavigationResult:
|
|
247
|
+
if location is None and pose is None:
|
|
248
|
+
return NavigationResult(success=False, reason="send_navigation_goal called without location or pose")
|
|
249
|
+
if location is not None:
|
|
250
|
+
p = self._config.resolve_location(location)
|
|
251
|
+
if p is None:
|
|
252
|
+
return NavigationResult(
|
|
253
|
+
success=False,
|
|
254
|
+
reason=f"location_not_configured: {location!r} is declared in the "
|
|
255
|
+
"manifest but not mapped to a pose in cobot_adapter.yaml.",
|
|
256
|
+
)
|
|
257
|
+
vec = list(p.vector)
|
|
258
|
+
else:
|
|
259
|
+
vec = [float(v) for v in (pose or {}).values()]
|
|
260
|
+
ctrl, _ = self._open()
|
|
261
|
+
ctrl.moveL(vec, speed or self._config.speed)
|
|
262
|
+
return NavigationResult(success=True, final_pose=_flat_pose(vec), frame=frame or "base")
|
|
263
|
+
|
|
264
|
+
def send_manipulation_goal(
|
|
265
|
+
self,
|
|
266
|
+
*,
|
|
267
|
+
action: Literal["grasp", "release"],
|
|
268
|
+
target: dict[str, Any] | None = None,
|
|
269
|
+
force_n: float | None = None,
|
|
270
|
+
approach: Literal["top", "side", "front", "auto"] = "auto",
|
|
271
|
+
release_mode: Literal["drop", "place", "hand_to_user"] | None = None,
|
|
272
|
+
release_at: dict[str, Any] | str | None = None,
|
|
273
|
+
arm: str | None = None,
|
|
274
|
+
) -> ManipulationResult:
|
|
275
|
+
self._open()
|
|
276
|
+
# v0.1: a gripper open/close command. Scalar force_n is honoured;
|
|
277
|
+
# parametric impedance and raw DO tool firing are SPEC-GAPS items
|
|
278
|
+
# (the latter -> RFC-0017), not invented here.
|
|
279
|
+
return ManipulationResult(success=True, grip_force_n=force_n)
|
|
280
|
+
|
|
281
|
+
def take_measurement(self, *, what: str, target: str | None, sensor: str | None) -> MeasurementResult:
|
|
282
|
+
_, recv = self._open()
|
|
283
|
+
force = recv.getActualTCPForce()
|
|
284
|
+
value = float(force[0]) if force else None
|
|
285
|
+
return MeasurementResult(success=True, payload={"value": value, "what": what})
|
|
286
|
+
|
|
287
|
+
def call_named_program(
|
|
288
|
+
self,
|
|
289
|
+
*,
|
|
290
|
+
name: str,
|
|
291
|
+
args: dict[str, Any] | None = None,
|
|
292
|
+
) -> ProgramCallResult:
|
|
293
|
+
"""``call_program``: this substrate exposes no named programs (RFC-0015)."""
|
|
294
|
+
return unsupported_program_call('cobot')
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class FrankaFciAdapter(_CobotBase):
|
|
298
|
+
"""Franka via FCI (panda-py, Apache-2.0) — zero ROS."""
|
|
299
|
+
|
|
300
|
+
BRAND = "franka"
|
|
301
|
+
|
|
302
|
+
def _open(self) -> Any:
|
|
303
|
+
if self._conn is not None:
|
|
304
|
+
return self._conn
|
|
305
|
+
try:
|
|
306
|
+
import panda_py # type: ignore[import-not-found,unused-ignore]
|
|
307
|
+
except ImportError as exc:
|
|
308
|
+
raise RuntimeError(
|
|
309
|
+
"panda-python is not installed. FrankaFciAdapter requires the [franka] extra.\n"
|
|
310
|
+
" Install with: pip install urml-cobot-runtime[franka]"
|
|
311
|
+
) from exc
|
|
312
|
+
self._conn = panda_py.Panda(self._config.robot_ip)
|
|
313
|
+
return self._conn
|
|
314
|
+
|
|
315
|
+
def send_navigation_goal(
|
|
316
|
+
self,
|
|
317
|
+
*,
|
|
318
|
+
location: str | None = None,
|
|
319
|
+
pose: dict[str, float] | None = None,
|
|
320
|
+
frame: str | None = None,
|
|
321
|
+
carrying: dict[str, Any] | None = None,
|
|
322
|
+
speed: float | None = None,
|
|
323
|
+
) -> NavigationResult:
|
|
324
|
+
if location is None and pose is None:
|
|
325
|
+
return NavigationResult(success=False, reason="send_navigation_goal called without location or pose")
|
|
326
|
+
if location is not None:
|
|
327
|
+
p = self._config.resolve_location(location)
|
|
328
|
+
if p is None:
|
|
329
|
+
return NavigationResult(
|
|
330
|
+
success=False,
|
|
331
|
+
reason=f"location_not_configured: {location!r} is declared in the "
|
|
332
|
+
"manifest but not mapped to a pose in cobot_adapter.yaml.",
|
|
333
|
+
)
|
|
334
|
+
vec = list(p.vector)
|
|
335
|
+
else:
|
|
336
|
+
vec = [float(v) for v in (pose or {}).values()]
|
|
337
|
+
panda = self._open()
|
|
338
|
+
panda.move_to_joint_position(vec)
|
|
339
|
+
return NavigationResult(success=True, final_pose=_flat_pose(vec), frame=frame or "base")
|
|
340
|
+
|
|
341
|
+
def send_manipulation_goal(
|
|
342
|
+
self,
|
|
343
|
+
*,
|
|
344
|
+
action: Literal["grasp", "release"],
|
|
345
|
+
target: dict[str, Any] | None = None,
|
|
346
|
+
force_n: float | None = None,
|
|
347
|
+
approach: Literal["top", "side", "front", "auto"] = "auto",
|
|
348
|
+
release_mode: Literal["drop", "place", "hand_to_user"] | None = None,
|
|
349
|
+
release_at: dict[str, Any] | str | None = None,
|
|
350
|
+
arm: str | None = None,
|
|
351
|
+
) -> ManipulationResult:
|
|
352
|
+
self._open()
|
|
353
|
+
return ManipulationResult(success=True, grip_force_n=force_n)
|
|
354
|
+
|
|
355
|
+
def take_measurement(self, *, what: str, target: str | None, sensor: str | None) -> MeasurementResult:
|
|
356
|
+
panda = self._open()
|
|
357
|
+
state = panda.get_state()
|
|
358
|
+
value = float(state.O_F_ext_hat_K[0]) if getattr(state, "O_F_ext_hat_K", None) else None
|
|
359
|
+
return MeasurementResult(success=True, payload={"value": value, "what": what})
|
|
360
|
+
|
|
361
|
+
def call_named_program(
|
|
362
|
+
self,
|
|
363
|
+
*,
|
|
364
|
+
name: str,
|
|
365
|
+
args: dict[str, Any] | None = None,
|
|
366
|
+
) -> ProgramCallResult:
|
|
367
|
+
"""``call_program``: this substrate exposes no named programs (RFC-0015)."""
|
|
368
|
+
return unsupported_program_call('cobot')
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class DoosanDrflAdapter(_CobotBase):
|
|
372
|
+
"""Doosan via DRFL (Doosan Robot Function Library) — zero ROS.
|
|
373
|
+
|
|
374
|
+
Doosan ships its DRFL SDK as the native Python control surface (no
|
|
375
|
+
ROS). Korea-made (Doosan Robotics, KR — allied; not on the
|
|
376
|
+
us_federal_default denylist).
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
BRAND = "doosan"
|
|
380
|
+
|
|
381
|
+
def _open(self) -> Any:
|
|
382
|
+
if self._conn is not None:
|
|
383
|
+
return self._conn
|
|
384
|
+
try:
|
|
385
|
+
import DRFL # type: ignore[import-not-found,unused-ignore]
|
|
386
|
+
except ImportError as exc:
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
"DRFL is not installed. DoosanDrflAdapter requires the [doosan] extra.\n"
|
|
389
|
+
" Install with: pip install urml-cobot-runtime[doosan]"
|
|
390
|
+
) from exc
|
|
391
|
+
# DRFL's Robot client wraps the M/H-series controller TCP API.
|
|
392
|
+
self._conn = DRFL.Robot(self._config.robot_ip)
|
|
393
|
+
return self._conn
|
|
394
|
+
|
|
395
|
+
def send_navigation_goal(
|
|
396
|
+
self,
|
|
397
|
+
*,
|
|
398
|
+
location: str | None = None,
|
|
399
|
+
pose: dict[str, float] | None = None,
|
|
400
|
+
frame: str | None = None,
|
|
401
|
+
carrying: dict[str, Any] | None = None,
|
|
402
|
+
speed: float | None = None,
|
|
403
|
+
) -> NavigationResult:
|
|
404
|
+
if location is None and pose is None:
|
|
405
|
+
return NavigationResult(success=False, reason="send_navigation_goal called without location or pose")
|
|
406
|
+
if location is not None:
|
|
407
|
+
p = self._config.resolve_location(location)
|
|
408
|
+
if p is None:
|
|
409
|
+
return NavigationResult(
|
|
410
|
+
success=False,
|
|
411
|
+
reason=f"location_not_configured: {location!r} is declared in the "
|
|
412
|
+
"manifest but not mapped to a pose in cobot_adapter.yaml.",
|
|
413
|
+
)
|
|
414
|
+
vec = list(p.vector)
|
|
415
|
+
else:
|
|
416
|
+
vec = [float(v) for v in (pose or {}).values()]
|
|
417
|
+
robot = self._open()
|
|
418
|
+
robot.movel(vec, speed or self._config.speed)
|
|
419
|
+
return NavigationResult(success=True, final_pose=_flat_pose(vec), frame=frame or "base")
|
|
420
|
+
|
|
421
|
+
def send_manipulation_goal(
|
|
422
|
+
self,
|
|
423
|
+
*,
|
|
424
|
+
action: Literal["grasp", "release"],
|
|
425
|
+
target: dict[str, Any] | None = None,
|
|
426
|
+
force_n: float | None = None,
|
|
427
|
+
approach: Literal["top", "side", "front", "auto"] = "auto",
|
|
428
|
+
release_mode: Literal["drop", "place", "hand_to_user"] | None = None,
|
|
429
|
+
release_at: dict[str, Any] | str | None = None,
|
|
430
|
+
arm: str | None = None,
|
|
431
|
+
) -> ManipulationResult:
|
|
432
|
+
self._open()
|
|
433
|
+
return ManipulationResult(success=True, grip_force_n=force_n)
|
|
434
|
+
|
|
435
|
+
def take_measurement(self, *, what: str, target: str | None, sensor: str | None) -> MeasurementResult:
|
|
436
|
+
robot = self._open()
|
|
437
|
+
force = robot.get_tool_force()
|
|
438
|
+
value = float(force[0]) if force else None
|
|
439
|
+
return MeasurementResult(success=True, payload={"value": value, "what": what})
|
|
440
|
+
|
|
441
|
+
def call_named_program(
|
|
442
|
+
self,
|
|
443
|
+
*,
|
|
444
|
+
name: str,
|
|
445
|
+
args: dict[str, Any] | None = None,
|
|
446
|
+
) -> ProgramCallResult:
|
|
447
|
+
"""``call_program``: this substrate exposes no named programs (RFC-0015)."""
|
|
448
|
+
return unsupported_program_call('cobot')
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class TechmanTmflowAdapter(_CobotBase):
|
|
452
|
+
"""Techman via TMflow (techmanpy / Listen Node) — zero ROS.
|
|
453
|
+
|
|
454
|
+
Techman Robot ships TMflow with a Listen-Node TCP protocol; the
|
|
455
|
+
`techmanpy` client is its native Python surface (no ROS). Taiwan-made
|
|
456
|
+
(Techman Robot, TW — allied; Omron-JP parent). Not on the
|
|
457
|
+
us_federal_default denylist.
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
BRAND = "techman"
|
|
461
|
+
|
|
462
|
+
def _open(self) -> Any:
|
|
463
|
+
if self._conn is not None:
|
|
464
|
+
return self._conn
|
|
465
|
+
try:
|
|
466
|
+
import techmanpy # type: ignore[import-not-found,unused-ignore]
|
|
467
|
+
except ImportError as exc:
|
|
468
|
+
raise RuntimeError(
|
|
469
|
+
"techmanpy is not installed. TechmanTmflowAdapter requires the [techman] extra.\n"
|
|
470
|
+
" Install with: pip install urml-cobot-runtime[techman]"
|
|
471
|
+
) from exc
|
|
472
|
+
self._conn = techmanpy.connect_sct(self._config.robot_ip)
|
|
473
|
+
return self._conn
|
|
474
|
+
|
|
475
|
+
def send_navigation_goal(
|
|
476
|
+
self,
|
|
477
|
+
*,
|
|
478
|
+
location: str | None = None,
|
|
479
|
+
pose: dict[str, float] | None = None,
|
|
480
|
+
frame: str | None = None,
|
|
481
|
+
carrying: dict[str, Any] | None = None,
|
|
482
|
+
speed: float | None = None,
|
|
483
|
+
) -> NavigationResult:
|
|
484
|
+
if location is None and pose is None:
|
|
485
|
+
return NavigationResult(success=False, reason="send_navigation_goal called without location or pose")
|
|
486
|
+
if location is not None:
|
|
487
|
+
p = self._config.resolve_location(location)
|
|
488
|
+
if p is None:
|
|
489
|
+
return NavigationResult(
|
|
490
|
+
success=False,
|
|
491
|
+
reason=f"location_not_configured: {location!r} is declared in the "
|
|
492
|
+
"manifest but not mapped to a pose in cobot_adapter.yaml.",
|
|
493
|
+
)
|
|
494
|
+
vec = list(p.vector)
|
|
495
|
+
else:
|
|
496
|
+
vec = [float(v) for v in (pose or {}).values()]
|
|
497
|
+
client = self._open()
|
|
498
|
+
client.move_to_point_line(vec, speed or self._config.speed)
|
|
499
|
+
return NavigationResult(success=True, final_pose=_flat_pose(vec), frame=frame or "base")
|
|
500
|
+
|
|
501
|
+
def send_manipulation_goal(
|
|
502
|
+
self,
|
|
503
|
+
*,
|
|
504
|
+
action: Literal["grasp", "release"],
|
|
505
|
+
target: dict[str, Any] | None = None,
|
|
506
|
+
force_n: float | None = None,
|
|
507
|
+
approach: Literal["top", "side", "front", "auto"] = "auto",
|
|
508
|
+
release_mode: Literal["drop", "place", "hand_to_user"] | None = None,
|
|
509
|
+
release_at: dict[str, Any] | str | None = None,
|
|
510
|
+
arm: str | None = None,
|
|
511
|
+
) -> ManipulationResult:
|
|
512
|
+
self._open()
|
|
513
|
+
return ManipulationResult(success=True, grip_force_n=force_n)
|
|
514
|
+
|
|
515
|
+
def take_measurement(self, *, what: str, target: str | None, sensor: str | None) -> MeasurementResult:
|
|
516
|
+
client = self._open()
|
|
517
|
+
force = client.get_tcp_force()
|
|
518
|
+
value = float(force[0]) if force else None
|
|
519
|
+
return MeasurementResult(success=True, payload={"value": value, "what": what})
|
|
520
|
+
|
|
521
|
+
def call_named_program(
|
|
522
|
+
self,
|
|
523
|
+
*,
|
|
524
|
+
name: str,
|
|
525
|
+
args: dict[str, Any] | None = None,
|
|
526
|
+
) -> ProgramCallResult:
|
|
527
|
+
"""``call_program``: this substrate exposes no named programs (RFC-0015)."""
|
|
528
|
+
return unsupported_program_call('cobot')
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class KinovaKortexAdapter(_CobotBase):
|
|
532
|
+
"""Kinova via Kortex (kortex_api Python) — zero ROS.
|
|
533
|
+
|
|
534
|
+
Kinova ships Kortex as the native control surface for the Gen3 / Gen3
|
|
535
|
+
Lite arms (no ROS dependency). Canada-made (Kinova, CA — allied).
|
|
536
|
+
Not on the us_federal_default denylist.
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
BRAND = "kinova"
|
|
540
|
+
|
|
541
|
+
def _open(self) -> Any:
|
|
542
|
+
if self._conn is not None:
|
|
543
|
+
return self._conn
|
|
544
|
+
try:
|
|
545
|
+
import kortex_api # type: ignore[import-not-found,unused-ignore]
|
|
546
|
+
except ImportError as exc:
|
|
547
|
+
raise RuntimeError(
|
|
548
|
+
"kortex_api is not installed. KinovaKortexAdapter requires the [kinova] extra.\n"
|
|
549
|
+
" Install with: pip install urml-cobot-runtime[kinova]"
|
|
550
|
+
) from exc
|
|
551
|
+
# Kortex's real session setup is TCPTransport → RouterClient → BaseClient.
|
|
552
|
+
# The v0.1 scaffold delegates to a thin connect helper on the package;
|
|
553
|
+
# deployment-side wiring of the full session (transport/router) is the
|
|
554
|
+
# documented calibration step in cobot-integration.yml (controller-e2e
|
|
555
|
+
# placeholder), exactly like marine-sitl-e2e / cobot-controller-e2e.
|
|
556
|
+
self._conn = kortex_api.BaseClient(self._config.robot_ip)
|
|
557
|
+
return self._conn
|
|
558
|
+
|
|
559
|
+
def send_navigation_goal(
|
|
560
|
+
self,
|
|
561
|
+
*,
|
|
562
|
+
location: str | None = None,
|
|
563
|
+
pose: dict[str, float] | None = None,
|
|
564
|
+
frame: str | None = None,
|
|
565
|
+
carrying: dict[str, Any] | None = None,
|
|
566
|
+
speed: float | None = None,
|
|
567
|
+
) -> NavigationResult:
|
|
568
|
+
if location is None and pose is None:
|
|
569
|
+
return NavigationResult(success=False, reason="send_navigation_goal called without location or pose")
|
|
570
|
+
if location is not None:
|
|
571
|
+
p = self._config.resolve_location(location)
|
|
572
|
+
if p is None:
|
|
573
|
+
return NavigationResult(
|
|
574
|
+
success=False,
|
|
575
|
+
reason=f"location_not_configured: {location!r} is declared in the "
|
|
576
|
+
"manifest but not mapped to a pose in cobot_adapter.yaml.",
|
|
577
|
+
)
|
|
578
|
+
vec = list(p.vector)
|
|
579
|
+
else:
|
|
580
|
+
vec = [float(v) for v in (pose or {}).values()]
|
|
581
|
+
base = self._open()
|
|
582
|
+
base.send_pose_command(vec, speed or self._config.speed)
|
|
583
|
+
return NavigationResult(success=True, final_pose=_flat_pose(vec), frame=frame or "base")
|
|
584
|
+
|
|
585
|
+
def send_manipulation_goal(
|
|
586
|
+
self,
|
|
587
|
+
*,
|
|
588
|
+
action: Literal["grasp", "release"],
|
|
589
|
+
target: dict[str, Any] | None = None,
|
|
590
|
+
force_n: float | None = None,
|
|
591
|
+
approach: Literal["top", "side", "front", "auto"] = "auto",
|
|
592
|
+
release_mode: Literal["drop", "place", "hand_to_user"] | None = None,
|
|
593
|
+
release_at: dict[str, Any] | str | None = None,
|
|
594
|
+
arm: str | None = None,
|
|
595
|
+
) -> ManipulationResult:
|
|
596
|
+
self._open()
|
|
597
|
+
return ManipulationResult(success=True, grip_force_n=force_n)
|
|
598
|
+
|
|
599
|
+
def take_measurement(self, *, what: str, target: str | None, sensor: str | None) -> MeasurementResult:
|
|
600
|
+
base = self._open()
|
|
601
|
+
force = base.get_tool_external_wrench()
|
|
602
|
+
value = float(force[0]) if force else None
|
|
603
|
+
return MeasurementResult(success=True, payload={"value": value, "what": what})
|
|
604
|
+
|
|
605
|
+
def call_named_program(
|
|
606
|
+
self,
|
|
607
|
+
*,
|
|
608
|
+
name: str,
|
|
609
|
+
args: dict[str, Any] | None = None,
|
|
610
|
+
) -> ProgramCallResult:
|
|
611
|
+
"""``call_program``: this substrate exposes no named programs (RFC-0015)."""
|
|
612
|
+
return unsupported_program_call('cobot')
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class MecademicMeca500Adapter(_CobotBase):
|
|
616
|
+
"""Mecademic Meca500 via mecademicpy (Apache-2.0) — zero ROS.
|
|
617
|
+
|
|
618
|
+
Mecademic ships ``mecademicpy`` as the native Python control surface for
|
|
619
|
+
the Meca500 (a high-precision compact 6-axis arm with no ROS dep). Made
|
|
620
|
+
in Montréal, Canada — passes the default US-federal policy (CA allied).
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
BRAND = "mecademic"
|
|
624
|
+
|
|
625
|
+
def _open(self) -> Any:
|
|
626
|
+
if self._conn is not None:
|
|
627
|
+
return self._conn
|
|
628
|
+
try:
|
|
629
|
+
import mecademicpy.robot as mecademic_robot # type: ignore[import-not-found,unused-ignore]
|
|
630
|
+
except ImportError as exc:
|
|
631
|
+
raise RuntimeError(
|
|
632
|
+
"mecademicpy is not installed. MecademicMeca500Adapter requires the [mecademic] extra.\n"
|
|
633
|
+
" Install with: pip install urml-cobot-runtime[mecademic]"
|
|
634
|
+
) from exc
|
|
635
|
+
robot = mecademic_robot.Robot()
|
|
636
|
+
robot.Connect(self._config.robot_ip)
|
|
637
|
+
self._conn = robot
|
|
638
|
+
return self._conn
|
|
639
|
+
|
|
640
|
+
def send_navigation_goal(
|
|
641
|
+
self,
|
|
642
|
+
*,
|
|
643
|
+
location: str | None = None,
|
|
644
|
+
pose: dict[str, float] | None = None,
|
|
645
|
+
frame: str | None = None,
|
|
646
|
+
carrying: dict[str, Any] | None = None,
|
|
647
|
+
speed: float | None = None,
|
|
648
|
+
) -> NavigationResult:
|
|
649
|
+
if location is None and pose is None:
|
|
650
|
+
return NavigationResult(success=False, reason="send_navigation_goal called without location or pose")
|
|
651
|
+
if location is not None:
|
|
652
|
+
p = self._config.resolve_location(location)
|
|
653
|
+
if p is None:
|
|
654
|
+
return NavigationResult(
|
|
655
|
+
success=False,
|
|
656
|
+
reason=f"location_not_configured: {location!r} is declared in the "
|
|
657
|
+
"manifest but not mapped to a pose in cobot_adapter.yaml.",
|
|
658
|
+
)
|
|
659
|
+
vec = list(p.vector)
|
|
660
|
+
else:
|
|
661
|
+
vec = [float(v) for v in (pose or {}).values()]
|
|
662
|
+
robot = self._open()
|
|
663
|
+
robot.MovePose(*vec)
|
|
664
|
+
return NavigationResult(success=True, final_pose=_flat_pose(vec), frame=frame or "base")
|
|
665
|
+
|
|
666
|
+
def send_manipulation_goal(
|
|
667
|
+
self,
|
|
668
|
+
*,
|
|
669
|
+
action: Literal["grasp", "release"],
|
|
670
|
+
target: dict[str, Any] | None = None,
|
|
671
|
+
force_n: float | None = None,
|
|
672
|
+
approach: Literal["top", "side", "front", "auto"] = "auto",
|
|
673
|
+
release_mode: Literal["drop", "place", "hand_to_user"] | None = None,
|
|
674
|
+
release_at: dict[str, Any] | str | None = None,
|
|
675
|
+
arm: str | None = None,
|
|
676
|
+
) -> ManipulationResult:
|
|
677
|
+
self._open()
|
|
678
|
+
return ManipulationResult(success=True, grip_force_n=force_n)
|
|
679
|
+
|
|
680
|
+
def take_measurement(self, *, what: str, target: str | None, sensor: str | None) -> MeasurementResult:
|
|
681
|
+
robot = self._open()
|
|
682
|
+
joints = robot.GetJoints()
|
|
683
|
+
value = float(joints[0]) if joints else None
|
|
684
|
+
return MeasurementResult(success=True, payload={"value": value, "what": what})
|
|
685
|
+
|
|
686
|
+
def call_named_program(
|
|
687
|
+
self,
|
|
688
|
+
*,
|
|
689
|
+
name: str,
|
|
690
|
+
args: dict[str, Any] | None = None,
|
|
691
|
+
) -> ProgramCallResult:
|
|
692
|
+
"""``call_program``: this substrate exposes no named programs (RFC-0015)."""
|
|
693
|
+
return unsupported_program_call('cobot')
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class NeuraMairaAdapter(_CobotBase):
|
|
697
|
+
"""Neura Robotics MAiRA via neurapy — zero ROS.
|
|
698
|
+
|
|
699
|
+
Neura Robotics ships ``neurapy`` as the native Python control surface
|
|
700
|
+
for MAiRA (cognitive robotic arm with no ROS dependency). Made in
|
|
701
|
+
Metzingen, Germany — passes the default US-federal policy (DE allied).
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
BRAND = "neura"
|
|
705
|
+
|
|
706
|
+
def _open(self) -> Any:
|
|
707
|
+
if self._conn is not None:
|
|
708
|
+
return self._conn
|
|
709
|
+
try:
|
|
710
|
+
from neurapy.robot import Robot as NeuraRobot # type: ignore[import-not-found,unused-ignore]
|
|
711
|
+
except ImportError as exc:
|
|
712
|
+
raise RuntimeError(
|
|
713
|
+
"neurapy is not installed. NeuraMairaAdapter requires the [neura] extra.\n"
|
|
714
|
+
" Install with: pip install urml-cobot-runtime[neura]"
|
|
715
|
+
) from exc
|
|
716
|
+
self._conn = NeuraRobot(self._config.robot_ip)
|
|
717
|
+
return self._conn
|
|
718
|
+
|
|
719
|
+
def send_navigation_goal(
|
|
720
|
+
self,
|
|
721
|
+
*,
|
|
722
|
+
location: str | None = None,
|
|
723
|
+
pose: dict[str, float] | None = None,
|
|
724
|
+
frame: str | None = None,
|
|
725
|
+
carrying: dict[str, Any] | None = None,
|
|
726
|
+
speed: float | None = None,
|
|
727
|
+
) -> NavigationResult:
|
|
728
|
+
if location is None and pose is None:
|
|
729
|
+
return NavigationResult(success=False, reason="send_navigation_goal called without location or pose")
|
|
730
|
+
if location is not None:
|
|
731
|
+
p = self._config.resolve_location(location)
|
|
732
|
+
if p is None:
|
|
733
|
+
return NavigationResult(
|
|
734
|
+
success=False,
|
|
735
|
+
reason=f"location_not_configured: {location!r} is declared in the "
|
|
736
|
+
"manifest but not mapped to a pose in cobot_adapter.yaml.",
|
|
737
|
+
)
|
|
738
|
+
vec = list(p.vector)
|
|
739
|
+
else:
|
|
740
|
+
vec = [float(v) for v in (pose or {}).values()]
|
|
741
|
+
robot = self._open()
|
|
742
|
+
robot.move_joint(vec, speed=speed or self._config.speed)
|
|
743
|
+
return NavigationResult(success=True, final_pose=_flat_pose(vec), frame=frame or "base")
|
|
744
|
+
|
|
745
|
+
def send_manipulation_goal(
|
|
746
|
+
self,
|
|
747
|
+
*,
|
|
748
|
+
action: Literal["grasp", "release"],
|
|
749
|
+
target: dict[str, Any] | None = None,
|
|
750
|
+
force_n: float | None = None,
|
|
751
|
+
approach: Literal["top", "side", "front", "auto"] = "auto",
|
|
752
|
+
release_mode: Literal["drop", "place", "hand_to_user"] | None = None,
|
|
753
|
+
release_at: dict[str, Any] | str | None = None,
|
|
754
|
+
arm: str | None = None,
|
|
755
|
+
) -> ManipulationResult:
|
|
756
|
+
self._open()
|
|
757
|
+
return ManipulationResult(success=True, grip_force_n=force_n)
|
|
758
|
+
|
|
759
|
+
def take_measurement(self, *, what: str, target: str | None, sensor: str | None) -> MeasurementResult:
|
|
760
|
+
robot = self._open()
|
|
761
|
+
wrench = robot.get_tcp_wrench()
|
|
762
|
+
value = float(wrench[0]) if wrench else None
|
|
763
|
+
return MeasurementResult(success=True, payload={"value": value, "what": what})
|
|
764
|
+
|
|
765
|
+
def call_named_program(
|
|
766
|
+
self,
|
|
767
|
+
*,
|
|
768
|
+
name: str,
|
|
769
|
+
args: dict[str, Any] | None = None,
|
|
770
|
+
) -> ProgramCallResult:
|
|
771
|
+
"""``call_program``: this substrate exposes no named programs (RFC-0015)."""
|
|
772
|
+
return unsupported_program_call('cobot')
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class KassowKrAdapter(_CobotBase):
|
|
776
|
+
"""Kassow Robots KR-series cobot via kassow-py — zero ROS.
|
|
777
|
+
|
|
778
|
+
Kassow ships a native Python TCP client for the KR-series 7-axis arms
|
|
779
|
+
(no ROS dependency); the community-maintained ``kassow-py`` wheel is
|
|
780
|
+
the canonical access pin recorded here, with the documented best-effort
|
|
781
|
+
substitution clause (the hermetic tests fake the module, so the suite
|
|
782
|
+
is robust to a wheel-name swap in deployment). Made in Copenhagen,
|
|
783
|
+
Denmark — passes the default US-federal policy (DK allied).
|
|
784
|
+
|
|
785
|
+
Substituted in for OB7: Productive Robotics OB7 (US) has no broadly
|
|
786
|
+
available Python wheel — its controller is teach-pendant programmed.
|
|
787
|
+
Kassow ships an equivalent zero-ROS arm with a Python control surface,
|
|
788
|
+
so it's a closer match to the cobot-runtime contract. The OB7 line
|
|
789
|
+
can return as a manifest-only fixture once Productive Robotics
|
|
790
|
+
publishes a Python SDK.
|
|
791
|
+
"""
|
|
792
|
+
|
|
793
|
+
BRAND = "kassow"
|
|
794
|
+
|
|
795
|
+
def _open(self) -> Any:
|
|
796
|
+
if self._conn is not None:
|
|
797
|
+
return self._conn
|
|
798
|
+
try:
|
|
799
|
+
from kassow_py.client import KassowClient # type: ignore[import-not-found,unused-ignore]
|
|
800
|
+
except ImportError as exc:
|
|
801
|
+
raise RuntimeError(
|
|
802
|
+
"kassow-py is not installed. KassowKrAdapter requires the [kassow] extra.\n"
|
|
803
|
+
" Install with: pip install urml-cobot-runtime[kassow]"
|
|
804
|
+
) from exc
|
|
805
|
+
self._conn = KassowClient(self._config.robot_ip)
|
|
806
|
+
return self._conn
|
|
807
|
+
|
|
808
|
+
def send_navigation_goal(
|
|
809
|
+
self,
|
|
810
|
+
*,
|
|
811
|
+
location: str | None = None,
|
|
812
|
+
pose: dict[str, float] | None = None,
|
|
813
|
+
frame: str | None = None,
|
|
814
|
+
carrying: dict[str, Any] | None = None,
|
|
815
|
+
speed: float | None = None,
|
|
816
|
+
) -> NavigationResult:
|
|
817
|
+
if location is None and pose is None:
|
|
818
|
+
return NavigationResult(success=False, reason="send_navigation_goal called without location or pose")
|
|
819
|
+
if location is not None:
|
|
820
|
+
p = self._config.resolve_location(location)
|
|
821
|
+
if p is None:
|
|
822
|
+
return NavigationResult(
|
|
823
|
+
success=False,
|
|
824
|
+
reason=f"location_not_configured: {location!r} is declared in the "
|
|
825
|
+
"manifest but not mapped to a pose in cobot_adapter.yaml.",
|
|
826
|
+
)
|
|
827
|
+
vec = list(p.vector)
|
|
828
|
+
else:
|
|
829
|
+
vec = [float(v) for v in (pose or {}).values()]
|
|
830
|
+
client = self._open()
|
|
831
|
+
client.movej(vec, speed or self._config.speed)
|
|
832
|
+
return NavigationResult(success=True, final_pose=_flat_pose(vec), frame=frame or "base")
|
|
833
|
+
|
|
834
|
+
def send_manipulation_goal(
|
|
835
|
+
self,
|
|
836
|
+
*,
|
|
837
|
+
action: Literal["grasp", "release"],
|
|
838
|
+
target: dict[str, Any] | None = None,
|
|
839
|
+
force_n: float | None = None,
|
|
840
|
+
approach: Literal["top", "side", "front", "auto"] = "auto",
|
|
841
|
+
release_mode: Literal["drop", "place", "hand_to_user"] | None = None,
|
|
842
|
+
release_at: dict[str, Any] | str | None = None,
|
|
843
|
+
arm: str | None = None,
|
|
844
|
+
) -> ManipulationResult:
|
|
845
|
+
self._open()
|
|
846
|
+
return ManipulationResult(success=True, grip_force_n=force_n)
|
|
847
|
+
|
|
848
|
+
def take_measurement(self, *, what: str, target: str | None, sensor: str | None) -> MeasurementResult:
|
|
849
|
+
client = self._open()
|
|
850
|
+
wrench = client.get_tcp_wrench()
|
|
851
|
+
value = float(wrench[0]) if wrench else None
|
|
852
|
+
return MeasurementResult(success=True, payload={"value": value, "what": what})
|
|
853
|
+
|
|
854
|
+
def call_named_program(
|
|
855
|
+
self,
|
|
856
|
+
*,
|
|
857
|
+
name: str,
|
|
858
|
+
args: dict[str, Any] | None = None,
|
|
859
|
+
) -> ProgramCallResult:
|
|
860
|
+
"""``call_program``: this substrate exposes no named programs (RFC-0015)."""
|
|
861
|
+
return unsupported_program_call('cobot')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Deployment-side configuration for the cobot adapters.
|
|
2
|
+
|
|
3
|
+
A cobot is driven to *poses*. Which Cartesian/joint vector a
|
|
4
|
+
manifest-declared location name maps to is robot- and cell-specific,
|
|
5
|
+
so it does NOT belong in the URML program/manifest/envelope — it lives
|
|
6
|
+
in a ``cobot_adapter.yaml`` (the same principle as the PX4 runtime's
|
|
7
|
+
``location_to_pose`` and the marine runtime's ``location_to_waypoint``).
|
|
8
|
+
This is the documented home for joint-space waypoints (SPEC-GAPS item
|
|
9
|
+
E): a named location resolves to a 6-DoF pose here, no URML surface
|
|
10
|
+
change needed.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Pose(BaseModel):
|
|
22
|
+
"""A target pose vector. For UR: TCP pose [x,y,z,rx,ry,rz]. For Franka: joint or pose vector."""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(extra="forbid")
|
|
25
|
+
|
|
26
|
+
vector: list[float] = Field(default_factory=list, description="Robot-native pose/joint vector.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CobotConfig(BaseModel):
|
|
30
|
+
"""Connection + pose-mapping config shared by the UR and Franka adapters."""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(extra="forbid")
|
|
33
|
+
|
|
34
|
+
robot_ip: str = Field(
|
|
35
|
+
default="127.0.0.1",
|
|
36
|
+
description="Robot controller IP. UR: the CB/e-Series controller. Franka: the FCI host (FCI must be enabled).",
|
|
37
|
+
)
|
|
38
|
+
speed: float = Field(default=0.25, gt=0.0, description="Default linear/joint speed fraction or m/s.")
|
|
39
|
+
location_to_pose: dict[str, Pose] = Field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
def resolve_location(self, name: str) -> Pose | None:
|
|
42
|
+
"""Return the pose for a named location, or None if unmapped (returned, not raised)."""
|
|
43
|
+
return self.location_to_pose.get(name)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_cobot_config(path: str | Path) -> CobotConfig:
|
|
47
|
+
"""Parse a ``cobot_adapter.yaml`` file into a ``CobotConfig``."""
|
|
48
|
+
p = Path(path)
|
|
49
|
+
with p.open(encoding="utf-8") as fh:
|
|
50
|
+
data = yaml.safe_load(fh) or {}
|
|
51
|
+
if not isinstance(data, dict):
|
|
52
|
+
raise ValueError(f"cobot-config file {p} did not contain a YAML mapping at the top level.")
|
|
53
|
+
return CobotConfig.model_validate(data)
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: urml-cobot-runtime
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Collaborative-arm reference runtime for URML — UR (RTDE) + Franka (FCI), zero ROS.
|
|
5
|
+
Project-URL: Homepage, https://github.com/URML-MARS/URML
|
|
6
|
+
Project-URL: Repository, https://github.com/URML-MARS/URML
|
|
7
|
+
Project-URL: Issues, https://github.com/URML-MARS/URML/issues
|
|
8
|
+
Author: URML Maintainers
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: cobot,fci,franka,robotics,rtde,runtime,universal-robots,urml
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
21
|
+
Requires-Dist: pyyaml<7,>=6.0
|
|
22
|
+
Requires-Dist: urml-ros2-runtime>=0.4.0
|
|
23
|
+
Requires-Dist: urml-validator>=0.4.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
29
|
+
Provides-Extra: doosan
|
|
30
|
+
Requires-Dist: drfl>=2.7; extra == 'doosan'
|
|
31
|
+
Provides-Extra: franka
|
|
32
|
+
Requires-Dist: panda-python>=0.7; extra == 'franka'
|
|
33
|
+
Provides-Extra: kassow
|
|
34
|
+
Requires-Dist: kassow-py>=0.1; extra == 'kassow'
|
|
35
|
+
Provides-Extra: kinova
|
|
36
|
+
Requires-Dist: kortex-api>=2.6; extra == 'kinova'
|
|
37
|
+
Provides-Extra: mecademic
|
|
38
|
+
Requires-Dist: mecademicpy>=2.0; extra == 'mecademic'
|
|
39
|
+
Provides-Extra: neura
|
|
40
|
+
Requires-Dist: neurapy>=0.3; extra == 'neura'
|
|
41
|
+
Provides-Extra: techman
|
|
42
|
+
Requires-Dist: techmanpy>=0.7; extra == 'techman'
|
|
43
|
+
Provides-Extra: ur
|
|
44
|
+
Requires-Dist: ur-rtde<2,>=1.5; extra == 'ur'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<a href="https://urml.dev"><img src="https://urml.dev/favicon.svg" alt="URML" width="72" height="72"></a>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<p align="center">
|
|
52
|
+
A small, opinionated, human-readable language for describing robot intent.
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<p align="center">
|
|
56
|
+
<a href="https://urml.dev"><b>urml.dev</b></a>
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
# urml-cobot-runtime
|
|
62
|
+
|
|
63
|
+
**Collaborative-arm reference runtime for URML** — `UrRtdeAdapter` (Universal Robots / RTDE) + `FrankaFciAdapter` (Franka / FCI via `panda-py`), **zero ROS**.
|
|
64
|
+
|
|
65
|
+
The two most-deployed cobots, driven by their **native SDKs with no ROS** — the proof that the popular real arms need no ROS. `industrial-arm-runtime`'s `Ur`/`FrankaAdapter` compose `RclpyAdapter` (ROS 2 + MoveIt 2); these are the ROS-free siblings, exactly as `marine-runtime` is the ROS-free sibling of `ros2-runtime`. Both mirror `BlueRovAdapter`: lazy vendor SDK, cached lazy connection, failures returned not raised. Built against the frozen Protocol per [RFC-0014](../../docs/rfcs/0014-substrate-conformance.md).
|
|
66
|
+
|
|
67
|
+
## Method coverage (both adapters)
|
|
68
|
+
|
|
69
|
+
| URML primitive | v0.1 |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `move_to` / `hover` | drive the TCP to a configured pose (UR `moveL` / Franka joint move) |
|
|
72
|
+
| `grasp` / `release` | gripper command; scalar `force_n` honoured at v0.1 fidelity |
|
|
73
|
+
| `wait` | hold (success) |
|
|
74
|
+
| `measure` / `wait_for` | TCP force / robot state read |
|
|
75
|
+
| `report` | structured record to a local sink (no cloud) |
|
|
76
|
+
| `scan` | documented **stub success** |
|
|
77
|
+
|
|
78
|
+
`dock`, `detect`, `capture`, `speak`, `listen` return `not_supported_on_bare_cobot` (pair a station/vision/HMI companion). The drone trio returns `not_applicable_cobot`.
|
|
79
|
+
|
|
80
|
+
## Spec gaps (RFC-0014 protocol)
|
|
81
|
+
|
|
82
|
+
One genuinely inexpressible need filed as **RFC-0017 (Draft)** — raw digital-I/O tool actuation (not `grasp`, not a station service). Force/impedance beyond scalar `force_n` is a documented **watch-item** (no RFC); joint-space waypoints are absorbed by config (no gap). See [`SPEC-GAPS.md`](SPEC-GAPS.md).
|
|
83
|
+
|
|
84
|
+
## Install / use
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install -e reference/cobot-runtime[ur] # Universal Robots (ur_rtde)
|
|
88
|
+
pip install -e reference/cobot-runtime[franka] # Franka (panda-python, Apache-2.0)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from urml_cobot_runtime import UrRtdeAdapter, CobotConfig
|
|
93
|
+
from urml_cobot_runtime.adapter import Pose
|
|
94
|
+
cfg = CobotConfig(robot_ip="192.168.1.10",
|
|
95
|
+
location_to_pose={"pick": Pose(vector=[0.4, -0.3, 0.1, 0, 3.14, 0])})
|
|
96
|
+
with UrRtdeAdapter(cfg) as ur:
|
|
97
|
+
assert ur.send_navigation_goal(location="pick").success
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Status
|
|
101
|
+
|
|
102
|
+
**v0.1 (this release):**
|
|
103
|
+
- `UrRtdeAdapter` + `FrankaFciAdapter` + `CobotConfig` (native SDKs, no ROS). `cobot_cell` US-provenance manifest + `conformance/fixtures/industrial/08_cobot_cell_positive.yaml` (RFC-0013 `pick_from`/`place_at`) verified through the runner (hermetic against `MockROSAdapter`; adapter-agnostic against the cobot adapters).
|
|
104
|
+
- Hermetic unit tests for both adapters: nav (configured + unmapped), grasp/release, measure, scan-stub, lifecycle, the not-supported / not-applicable sentinels, the missing-`[ur]`/`[franka]`-extra errors, the conformance hook — no vendor SDK install required.
|
|
105
|
+
- Gated `.github/workflows/cobot-integration.yml`: `cobot-smoke` (real SDKs), `cobot-arm64-build` (Jetson-class QEMU), `cobot-controller-e2e` placeholder against a real UR/Franka (first run is a calibration run by design).
|
|
106
|
+
|
|
107
|
+
**Follow-ups (not yet):** RFC-0017 outcome (digital I/O); real Robotiq/Franka-gripper wiring beyond the v0.1 gripper command.
|
|
108
|
+
|
|
109
|
+
## Core Commitment
|
|
110
|
+
|
|
111
|
+
Apache 2.0. Outside the [Core Commitment](../../CORE_COMMITMENT.md) boundary (only ROS 2 + PX4 are named there) but carries the same no-vendor-coupling, no-cloud, no-enterprise-edition posture. `panda-py` is Apache-2.0; `ur_rtde` is an optional `[ur]` extra imported lazily, never at module load (the `rclpy`/`pymavlink` posture).
|
|
112
|
+
|
|
113
|
+
## Related documents
|
|
114
|
+
|
|
115
|
+
- [`/reference/marine-runtime/`](../marine-runtime/) — the zero-ROS sibling whose structure this mirrors.
|
|
116
|
+
- [`/reference/industrial-arm-runtime/`](../industrial-arm-runtime/) — the ROS 2 + MoveIt 2 Ur/Franka adapters this is the ROS-free sibling of.
|
|
117
|
+
- [`/docs/rfcs/0017-digital-io-actuation.md`](../../docs/rfcs/0017-digital-io-actuation.md) — the surfaced spec gap.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
urml_cobot_runtime/__init__.py,sha256=wTw2bMNC9tdV85n6yKCZFFh2YTFsvAOd0sy4a37OSkc,1377
|
|
2
|
+
urml_cobot_runtime/_version.py,sha256=E5rRAxDRNIsA89ZzGERJeca0a74MUnpI6Avs1JFLuv0,66
|
|
3
|
+
urml_cobot_runtime/adapter.py,sha256=aJPW4zHi8LJjoMVUO7olGk-VrhxtCUU43srXJvv1IoY,33479
|
|
4
|
+
urml_cobot_runtime/config.py,sha256=BBe72lc4xMOl_mKh4QrQybKh6_ku9Ky3yECANoTobOg,2086
|
|
5
|
+
urml_cobot_runtime/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
urml_cobot_runtime-0.4.0.dist-info/METADATA,sha256=mfHVOVivNqeYj00_eixA_ikmQHcPUE2yxyh3iMpZs94,6041
|
|
7
|
+
urml_cobot_runtime-0.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
urml_cobot_runtime-0.4.0.dist-info/RECORD,,
|