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.
@@ -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,3 @@
1
+ """Package version. Bumped per release."""
2
+
3
+ __version__ = "0.4.0"
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any