armnet-runtime 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/PKG-INFO +2 -2
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/pyproject.toml +2 -2
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/__init__.py +6 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/context.py +72 -2
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/leaderboard.py +1 -1
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/lerobot/__init__.py +8 -1
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/lerobot/import_swap.py +20 -2
- armnet_runtime-0.3.2/src/armnet_runtime/lerobot/remote_yam.py +319 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/variation.py +61 -80
- armnet_runtime-0.3.2/tests/test_embodiment.py +166 -0
- armnet_runtime-0.3.2/tests/test_remote_yam.py +626 -0
- armnet_runtime-0.3.2/tests/test_scene_calibration_context.py +30 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_variation.py +71 -41
- armnet_runtime-0.3.1/tests/test_embodiment.py +0 -47
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/.gitignore +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/LICENSE +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/README.md +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/proto/rerun.proto +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/scripts/gen_proto.sh +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/_rerun_pb2.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/cli.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/decorator.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/env.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/lerobot/_remote_protocol.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/lerobot/remote_arx5.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/lerobot/remote_camera.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/lerobot/remote_feetech.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/markers.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/rerun.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/src/armnet_runtime/volume_sync.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_cell_instrument.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_completion.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_context_arms.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_edge_goal_clamp.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_leaderboard.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_persistent_connection.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_proxy_method_cache.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_rail_recovery.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_rail_reset_restore.py +0 -0
- {armnet_runtime-0.3.1 → armnet_runtime-0.3.2}/tests/test_robot_telemetry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: armnet-runtime
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: armnet runtime SDK — used by code running inside customer containers on a cell.
|
|
5
5
|
Project-URL: Homepage, https://armnet.dev
|
|
6
6
|
Project-URL: Documentation, https://docs.armnet.dev/packages/runtime/
|
|
@@ -8,7 +8,7 @@ Author: Ville Kuosmanen
|
|
|
8
8
|
License-Expression: Apache-2.0
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Python: <3.13,>=3.12
|
|
11
|
-
Requires-Dist: armnet-core>=0.3.
|
|
11
|
+
Requires-Dist: armnet-core>=0.3.2
|
|
12
12
|
Requires-Dist: protobuf<7,>=5
|
|
13
13
|
Requires-Dist: pydantic<3,>=2.6
|
|
14
14
|
Provides-Extra: dev
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
name = "armnet-runtime"
|
|
7
7
|
# 0.2.0 follows armnet-core: ctx.cell now carries an environment and hands
|
|
8
8
|
# reset/scoring to it, and CompletionStatus moved to armnet-core.
|
|
9
|
-
version = "0.3.
|
|
9
|
+
version = "0.3.2"
|
|
10
10
|
description = "armnet runtime SDK \u2014 used by code running inside customer containers on a cell."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.12,<3.13"
|
|
@@ -15,7 +15,7 @@ license-files = ["LICENSE"]
|
|
|
15
15
|
authors = [{ name = "Ville Kuosmanen" }]
|
|
16
16
|
|
|
17
17
|
dependencies = [
|
|
18
|
-
"armnet-core>=0.3.
|
|
18
|
+
"armnet-core>=0.3.2",
|
|
19
19
|
"pydantic>=2.6,<3",
|
|
20
20
|
# Wire format for Rerun visualization packets (armnet_runtime.rerun).
|
|
21
21
|
"protobuf>=5,<7",
|
|
@@ -37,6 +37,7 @@ from armnet_core import (
|
|
|
37
37
|
|
|
38
38
|
from armnet_runtime.context import (
|
|
39
39
|
BIMANUAL_SO101_EMBODIMENT,
|
|
40
|
+
BIMANUAL_YAM_EMBODIMENT,
|
|
40
41
|
SO101_EMBODIMENT,
|
|
41
42
|
Cell,
|
|
42
43
|
CompletionStatus,
|
|
@@ -45,6 +46,8 @@ from armnet_runtime.context import (
|
|
|
45
46
|
RuntimeCameraMount,
|
|
46
47
|
RuntimeLightbox,
|
|
47
48
|
RuntimeRail,
|
|
49
|
+
require_eval_embodiment,
|
|
50
|
+
require_bimanual_yam_embodiment,
|
|
48
51
|
require_so101_embodiment,
|
|
49
52
|
)
|
|
50
53
|
from armnet_runtime.decorator import MainRegistrationError, main
|
|
@@ -57,6 +60,7 @@ from armnet_runtime.variation import (
|
|
|
57
60
|
|
|
58
61
|
__all__ = [
|
|
59
62
|
"BIMANUAL_SO101_EMBODIMENT",
|
|
63
|
+
"BIMANUAL_YAM_EMBODIMENT",
|
|
60
64
|
"SO101_EMBODIMENT",
|
|
61
65
|
"Cell",
|
|
62
66
|
"CompletionStatus",
|
|
@@ -65,6 +69,8 @@ __all__ = [
|
|
|
65
69
|
"NothingToVary",
|
|
66
70
|
"Scenario",
|
|
67
71
|
"sample_scenario",
|
|
72
|
+
"require_eval_embodiment",
|
|
73
|
+
"require_bimanual_yam_embodiment",
|
|
68
74
|
"require_so101_embodiment",
|
|
69
75
|
"Embodiment",
|
|
70
76
|
"Job",
|
|
@@ -22,6 +22,7 @@ from typing import Any, Iterable, Mapping, Optional
|
|
|
22
22
|
from urllib.parse import urlparse
|
|
23
23
|
|
|
24
24
|
from armnet_core import (
|
|
25
|
+
BIMANUAL_YAM_EMBODIMENT,
|
|
25
26
|
CellDeviceConfig,
|
|
26
27
|
CompletionStatus,
|
|
27
28
|
Embodiment,
|
|
@@ -29,6 +30,7 @@ from armnet_core import (
|
|
|
29
30
|
InstrumentedCell,
|
|
30
31
|
Reading,
|
|
31
32
|
RobotCellConfig,
|
|
33
|
+
SceneCalibration,
|
|
32
34
|
Task,
|
|
33
35
|
environment_for,
|
|
34
36
|
)
|
|
@@ -322,6 +324,9 @@ class Cell:
|
|
|
322
324
|
immovable.
|
|
323
325
|
"""
|
|
324
326
|
|
|
327
|
+
scene_calibration: Optional[SceneCalibration] = None
|
|
328
|
+
"""Manipulation-area snapshot selected when this cell process started."""
|
|
329
|
+
|
|
325
330
|
device_config: Optional[CellDeviceConfig] = None
|
|
326
331
|
"""What this cell may vary and within what bounds.
|
|
327
332
|
|
|
@@ -896,8 +901,17 @@ class Cell:
|
|
|
896
901
|
if not confirm:
|
|
897
902
|
return
|
|
898
903
|
|
|
899
|
-
|
|
900
|
-
|
|
904
|
+
self.confirm_scene_reset()
|
|
905
|
+
|
|
906
|
+
def confirm_scene_reset(self) -> None:
|
|
907
|
+
"""Wait for operator scene staging without moving the robot first.
|
|
908
|
+
|
|
909
|
+
Most callers should use :meth:`reset`, which returns the robot to rest
|
|
910
|
+
before requesting confirmation. A runtime that deliberately starts
|
|
911
|
+
from a non-rest pose can call ``reset(confirm=False)``, move to that
|
|
912
|
+
pose, then call this method so the operator stages objects around the
|
|
913
|
+
robot's actual rollout start configuration.
|
|
914
|
+
"""
|
|
901
915
|
request = {"op": "reset", "request": {"kind": "manual"}}
|
|
902
916
|
operator_endpoint = self.operator_call_endpoint or self.local_control_endpoint
|
|
903
917
|
if operator_endpoint:
|
|
@@ -1595,6 +1609,13 @@ class Context:
|
|
|
1595
1609
|
SO101_EMBODIMENT = "lerobot/so-101"
|
|
1596
1610
|
BIMANUAL_SO101_EMBODIMENT = "lerobot/bimanual_so101"
|
|
1597
1611
|
|
|
1612
|
+
_EVAL_EMBODIMENTS = (
|
|
1613
|
+
SO101_EMBODIMENT,
|
|
1614
|
+
BIMANUAL_SO101_EMBODIMENT,
|
|
1615
|
+
BIMANUAL_YAM_EMBODIMENT,
|
|
1616
|
+
)
|
|
1617
|
+
_BIMANUAL_EVAL_EMBODIMENTS = (BIMANUAL_SO101_EMBODIMENT, BIMANUAL_YAM_EMBODIMENT)
|
|
1618
|
+
|
|
1598
1619
|
|
|
1599
1620
|
def require_so101_embodiment(ctx: "Context", runtime_name: str) -> bool:
|
|
1600
1621
|
"""Validate the job's embodiment is a (single or bimanual) SO-101.
|
|
@@ -1624,6 +1645,52 @@ def require_so101_embodiment(ctx: "Context", runtime_name: str) -> bool:
|
|
|
1624
1645
|
return expect_bimanual
|
|
1625
1646
|
|
|
1626
1647
|
|
|
1648
|
+
def require_eval_embodiment(ctx: "Context", runtime_name: str) -> None:
|
|
1649
|
+
"""Validate the job's embodiment is one LeRobot eval can drive.
|
|
1650
|
+
|
|
1651
|
+
Accepts single SO-101, bimanual SO-101, and bimanual YAM. Both bimanual
|
|
1652
|
+
types must land on a cell wired with left and right arms.
|
|
1653
|
+
|
|
1654
|
+
Raises :class:`NotImplementedError` for any other embodiment, and
|
|
1655
|
+
:class:`RuntimeError` if the embodiment's arm count disagrees with the
|
|
1656
|
+
cell's actual wiring.
|
|
1657
|
+
"""
|
|
1658
|
+
if ctx.embodiment not in _EVAL_EMBODIMENTS:
|
|
1659
|
+
supported = ", ".join(repr(name) for name in _EVAL_EMBODIMENTS)
|
|
1660
|
+
raise NotImplementedError(
|
|
1661
|
+
f"{runtime_name} supports {supported}, got {ctx.embodiment!r}"
|
|
1662
|
+
)
|
|
1663
|
+
expect_bimanual = ctx.embodiment in _BIMANUAL_EVAL_EMBODIMENTS
|
|
1664
|
+
if expect_bimanual != ctx.cell.is_bimanual:
|
|
1665
|
+
raise RuntimeError(
|
|
1666
|
+
f"embodiment {ctx.embodiment!r} expects "
|
|
1667
|
+
f"{'two arms (left+right)' if expect_bimanual else 'a single arm'}, "
|
|
1668
|
+
f"but the cell exposes arms={sorted(ctx.cell.arms)}"
|
|
1669
|
+
)
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
def require_bimanual_yam_embodiment(ctx: "Context", runtime_name: str) -> None:
|
|
1673
|
+
"""Validate the job targets the bimanual YAM cell exactly.
|
|
1674
|
+
|
|
1675
|
+
Requires ``ctx.embodiment`` to be :data:`BIMANUAL_YAM_EMBODIMENT` and the
|
|
1676
|
+
cell to expose exactly ``left`` and ``right`` arms — no other embodiment or
|
|
1677
|
+
arm layout is accepted.
|
|
1678
|
+
|
|
1679
|
+
Raises :class:`NotImplementedError` for any other embodiment, and
|
|
1680
|
+
:class:`RuntimeError` if the cell's arms are not exactly left and right.
|
|
1681
|
+
"""
|
|
1682
|
+
if ctx.embodiment != BIMANUAL_YAM_EMBODIMENT:
|
|
1683
|
+
raise NotImplementedError(
|
|
1684
|
+
f"{runtime_name} supports {BIMANUAL_YAM_EMBODIMENT!r}, "
|
|
1685
|
+
f"got {ctx.embodiment!r}"
|
|
1686
|
+
)
|
|
1687
|
+
if set(ctx.cell.arms) != {"left", "right"}:
|
|
1688
|
+
raise RuntimeError(
|
|
1689
|
+
f"embodiment {ctx.embodiment!r} expects arms left and right exactly, "
|
|
1690
|
+
f"but the cell exposes arms={sorted(ctx.cell.arms)}"
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
|
|
1627
1694
|
def build_context() -> Context:
|
|
1628
1695
|
"""Read the cell-injected env vars and construct a :class:`Context`.
|
|
1629
1696
|
|
|
@@ -1698,6 +1765,9 @@ def build_context() -> Context:
|
|
|
1698
1765
|
rails=_build_runtime_rails(cell_config, args),
|
|
1699
1766
|
lightbox=_build_runtime_lightbox(cell_config),
|
|
1700
1767
|
camera_mounts=_build_runtime_camera_mounts(cell_config),
|
|
1768
|
+
scene_calibration=(
|
|
1769
|
+
cell_config.scene_calibration if cell_config else None
|
|
1770
|
+
),
|
|
1701
1771
|
device_config=(
|
|
1702
1772
|
CellDeviceConfig.from_cell_config(cell_config) if cell_config else None
|
|
1703
1773
|
),
|
|
@@ -31,7 +31,7 @@ from typing import Optional
|
|
|
31
31
|
logger = logging.getLogger(__name__)
|
|
32
32
|
|
|
33
33
|
# Shared with demo_space/leaderboard.py — the demo reads from the same dataset.
|
|
34
|
-
LEADERBOARD_REPO = "
|
|
34
|
+
LEADERBOARD_REPO = "armnet/armnet-demo-leaderboard"
|
|
35
35
|
RUNS_FILE = "runs.jsonl"
|
|
36
36
|
|
|
37
37
|
|
|
@@ -4,5 +4,12 @@ from armnet_runtime.lerobot.import_swap import install_lerobot_remote_mode
|
|
|
4
4
|
from armnet_runtime.lerobot.remote_arx5 import RemoteARX5Arm
|
|
5
5
|
from armnet_runtime.lerobot.remote_camera import RemoteOpenCVCamera
|
|
6
6
|
from armnet_runtime.lerobot.remote_feetech import RemoteFeetechMotorsBus
|
|
7
|
+
from armnet_runtime.lerobot.remote_yam import RemoteYAMFollower
|
|
7
8
|
|
|
8
|
-
__all__ = [
|
|
9
|
+
__all__ = [
|
|
10
|
+
"RemoteARX5Arm",
|
|
11
|
+
"RemoteFeetechMotorsBus",
|
|
12
|
+
"RemoteOpenCVCamera",
|
|
13
|
+
"RemoteYAMFollower",
|
|
14
|
+
"install_lerobot_remote_mode",
|
|
15
|
+
]
|
|
@@ -10,6 +10,7 @@ from armnet_core import env as env_keys
|
|
|
10
10
|
from armnet_runtime.lerobot.remote_camera import RemoteOpenCVCamera
|
|
11
11
|
from armnet_runtime.lerobot.remote_arx5 import RemoteARX5Arm
|
|
12
12
|
from armnet_runtime.lerobot.remote_feetech import RemoteFeetechMotorsBus
|
|
13
|
+
from armnet_runtime.lerobot.remote_yam import RemoteYAMFollower
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
@@ -25,6 +26,10 @@ _ARX5_MODULE_CANDIDATES = [
|
|
|
25
26
|
"arx_common",
|
|
26
27
|
"arx_common.arm",
|
|
27
28
|
]
|
|
29
|
+
_YAM_MODULE_CANDIDATES = [
|
|
30
|
+
"lerobot_robot_yam",
|
|
31
|
+
"lerobot_robot_yam.yam_follower",
|
|
32
|
+
]
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
_feetech_is_remote = False
|
|
@@ -88,13 +93,26 @@ def install_lerobot_remote_mode() -> None:
|
|
|
88
93
|
setattr(module, "ARX5Arm", RemoteARX5Arm)
|
|
89
94
|
patched.append(f"{module_name}.ARX5Arm")
|
|
90
95
|
|
|
96
|
+
for module_name in _YAM_MODULE_CANDIDATES:
|
|
97
|
+
try:
|
|
98
|
+
module = importlib.import_module(module_name)
|
|
99
|
+
except ImportError:
|
|
100
|
+
continue
|
|
101
|
+
for attr in ("YAMFollower", "YAMFollowerRobot"):
|
|
102
|
+
if hasattr(module, attr):
|
|
103
|
+
setattr(module, attr, RemoteYAMFollower)
|
|
104
|
+
patched.append(f"{module_name}.{attr}")
|
|
105
|
+
|
|
91
106
|
if patched:
|
|
92
107
|
global _feetech_is_remote
|
|
93
108
|
_feetech_is_remote = any(attr.endswith("FeetechMotorsBus") for attr in patched)
|
|
94
109
|
logger.info("installed armnet LeRobot remote mode: patched %s", patched)
|
|
95
110
|
else:
|
|
96
111
|
logger.warning(
|
|
97
|
-
"LeRobot is installed, but no known Feetech bus, OpenCV camera,
|
|
112
|
+
"LeRobot is installed, but no known Feetech bus, OpenCV camera, ARX5 arm, or YAM follower class was found to patch; "
|
|
98
113
|
"remote robot access may fail. Checked: %s",
|
|
99
|
-
_FEETECH_MODULE_CANDIDATES
|
|
114
|
+
_FEETECH_MODULE_CANDIDATES
|
|
115
|
+
+ _OPENCV_CAMERA_MODULE_CANDIDATES
|
|
116
|
+
+ _ARX5_MODULE_CANDIDATES
|
|
117
|
+
+ _YAM_MODULE_CANDIDATES,
|
|
100
118
|
)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Remote YAM follower used inside customer containers.
|
|
2
|
+
|
|
3
|
+
Device-specific code only. Wire-protocol plumbing lives in
|
|
4
|
+
:mod:`armnet_runtime.lerobot._remote_protocol`. This module does not import
|
|
5
|
+
LeRobot or yam_common at module import time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import parse_qs, urlparse
|
|
14
|
+
|
|
15
|
+
from armnet_runtime.lerobot._remote_protocol import (
|
|
16
|
+
IS_CONNECTED_TTL_S,
|
|
17
|
+
PersistentConnection,
|
|
18
|
+
RemoteAttributeError,
|
|
19
|
+
RemoteContextManager,
|
|
20
|
+
check_response,
|
|
21
|
+
decode,
|
|
22
|
+
jsonable,
|
|
23
|
+
)
|
|
24
|
+
from armnet_runtime.lerobot.remote_camera import RemoteOpenCVCamera
|
|
25
|
+
|
|
26
|
+
__all__ = ["RemoteYAMFollower"]
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
ACTION_KEYS = (
|
|
31
|
+
"shoulder_pan.pos",
|
|
32
|
+
"shoulder_lift.pos",
|
|
33
|
+
"elbow_flex.pos",
|
|
34
|
+
"wrist_flex.pos",
|
|
35
|
+
"wrist_roll.pos",
|
|
36
|
+
"wrist_yaw.pos",
|
|
37
|
+
"gripper.pos",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RemoteYAMFollower:
|
|
42
|
+
"""LeRobot YAM follower replacement that keeps cameras on separate proxies."""
|
|
43
|
+
|
|
44
|
+
name = "yam_follower"
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: Any, *args: Any, **kwargs: Any) -> None:
|
|
47
|
+
port = getattr(config, "port", None) or kwargs.get("port")
|
|
48
|
+
if not isinstance(port, str):
|
|
49
|
+
raise RuntimeError("RemoteYAMFollower requires config.port with ?arm=left or ?arm=right")
|
|
50
|
+
object.__setattr__(self, "_config", config)
|
|
51
|
+
object.__setattr__(self, "_socket_path", port)
|
|
52
|
+
object.__setattr__(self, "_arm_name", _arm_from_port(port))
|
|
53
|
+
object.__setattr__(self, "_object_id", None)
|
|
54
|
+
object.__setattr__(self, "_method_cache", set())
|
|
55
|
+
object.__setattr__(self, "_missing_cache", set())
|
|
56
|
+
object.__setattr__(self, "_connected", None)
|
|
57
|
+
object.__setattr__(self, "_connected_at", 0.0)
|
|
58
|
+
object.__setattr__(self, "_conn", PersistentConnection(self._socket_path))
|
|
59
|
+
object.__setattr__(self, "cameras", {})
|
|
60
|
+
object.__setattr__(
|
|
61
|
+
self,
|
|
62
|
+
"_object_id",
|
|
63
|
+
self._request(
|
|
64
|
+
{
|
|
65
|
+
"op": "create",
|
|
66
|
+
"class": "YAMFollower",
|
|
67
|
+
"arm": self._arm_name,
|
|
68
|
+
}
|
|
69
|
+
)["object_id"],
|
|
70
|
+
)
|
|
71
|
+
try:
|
|
72
|
+
cameras: dict[str, RemoteOpenCVCamera] = {}
|
|
73
|
+
for name, cam_cfg in (getattr(config, "cameras", None) or {}).items():
|
|
74
|
+
cameras[str(name)] = RemoteOpenCVCamera(cam_cfg)
|
|
75
|
+
object.__setattr__(self, "cameras", cameras)
|
|
76
|
+
except Exception:
|
|
77
|
+
try:
|
|
78
|
+
self.disconnect()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
object.__setattr__(self, "_object_id", None)
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_calibrated(self) -> bool:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def observation_features(self) -> dict[str, Any]:
|
|
90
|
+
features: dict[str, Any] = {key: float for key in ACTION_KEYS}
|
|
91
|
+
for name, cam_cfg in (getattr(self._config, "cameras", None) or {}).items():
|
|
92
|
+
height = getattr(cam_cfg, "height", None)
|
|
93
|
+
width = getattr(cam_cfg, "width", None)
|
|
94
|
+
if isinstance(cam_cfg, dict):
|
|
95
|
+
height = cam_cfg.get("height", height)
|
|
96
|
+
width = cam_cfg.get("width", width)
|
|
97
|
+
features[str(name)] = (height, width, 3)
|
|
98
|
+
return features
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def action_features(self) -> dict[str, type]:
|
|
102
|
+
return {key: float for key in ACTION_KEYS}
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def is_connected(self) -> bool:
|
|
106
|
+
arm = self._arm_connected()
|
|
107
|
+
cameras_ok = all(getattr(cam, "is_connected", True) for cam in self.cameras.values())
|
|
108
|
+
return bool(arm) and cameras_ok
|
|
109
|
+
|
|
110
|
+
def connect(self, calibrate: bool = True) -> None: # noqa: ARG002
|
|
111
|
+
self._method_proxy("connect")()
|
|
112
|
+
try:
|
|
113
|
+
for cam in self.cameras.values():
|
|
114
|
+
cam.connect()
|
|
115
|
+
except Exception:
|
|
116
|
+
for cam in self.cameras.values():
|
|
117
|
+
try:
|
|
118
|
+
cam.disconnect()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
try:
|
|
122
|
+
self._method_proxy("emergency_cleanup")()
|
|
123
|
+
except Exception:
|
|
124
|
+
try:
|
|
125
|
+
self._method_proxy("disconnect")()
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
object.__setattr__(self, "_connected", False)
|
|
129
|
+
raise
|
|
130
|
+
self._set_connected(True)
|
|
131
|
+
|
|
132
|
+
def disconnect(self) -> None:
|
|
133
|
+
for cam in self.cameras.values():
|
|
134
|
+
try:
|
|
135
|
+
cam.disconnect()
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
try:
|
|
139
|
+
self._method_proxy("disconnect")()
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
self._set_connected(False)
|
|
143
|
+
|
|
144
|
+
def get_observation(self) -> dict[str, Any]:
|
|
145
|
+
obs = dict(self._method_proxy("get_observation")() or {})
|
|
146
|
+
for name, cam in self.cameras.items():
|
|
147
|
+
obs[name] = cam.async_read()
|
|
148
|
+
return obs
|
|
149
|
+
|
|
150
|
+
def send_action(self, action: Any) -> Any:
|
|
151
|
+
return self._method_proxy("send_action")(action)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def robot_type(self) -> str:
|
|
155
|
+
return str(getattr(self._config, "robot_type", None) or self.name)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def config(self) -> Any:
|
|
159
|
+
return self._config
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def id(self) -> Any:
|
|
163
|
+
return getattr(self._config, "id", None)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def calibration_dir(self) -> Any:
|
|
167
|
+
return getattr(self._config, "calibration_dir", None)
|
|
168
|
+
|
|
169
|
+
def calibrate(self) -> None:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def configure(self) -> None:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def close(self) -> None:
|
|
176
|
+
self.disconnect()
|
|
177
|
+
|
|
178
|
+
def zero_torque_mode(self) -> None:
|
|
179
|
+
return self._method_proxy("zero_torque_mode")()
|
|
180
|
+
|
|
181
|
+
def get_observations(self) -> Any:
|
|
182
|
+
return self._method_proxy("get_observations")()
|
|
183
|
+
|
|
184
|
+
def get_joint_pos(self) -> Any:
|
|
185
|
+
return self._method_proxy("get_joint_pos")()
|
|
186
|
+
|
|
187
|
+
def command_joint_pos(self, joint_pos: Any) -> Any:
|
|
188
|
+
return self._method_proxy("command_joint_pos")(joint_pos)
|
|
189
|
+
|
|
190
|
+
def __enter__(self) -> "RemoteYAMFollower":
|
|
191
|
+
self.connect()
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def __exit__(self, exc_type, exc, tb) -> bool: # noqa: ANN001
|
|
195
|
+
self.disconnect()
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def _set_connected(self, value: bool) -> None:
|
|
199
|
+
object.__setattr__(self, "_connected", value)
|
|
200
|
+
object.__setattr__(self, "_connected_at", time.monotonic())
|
|
201
|
+
|
|
202
|
+
def _arm_connected(self) -> bool:
|
|
203
|
+
if (
|
|
204
|
+
self._connected is not None
|
|
205
|
+
and (time.monotonic() - self._connected_at) < IS_CONNECTED_TTL_S
|
|
206
|
+
):
|
|
207
|
+
return bool(self._connected)
|
|
208
|
+
try:
|
|
209
|
+
response = self._request(
|
|
210
|
+
{"op": "getattr", "object_id": self._object_id, "name": "is_connected"}
|
|
211
|
+
)
|
|
212
|
+
value = bool(decode(response.get("value")))
|
|
213
|
+
self._set_connected(value)
|
|
214
|
+
return value
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
self._mark_disconnected_if_stale(exc)
|
|
217
|
+
if _is_unknown_object_error(exc):
|
|
218
|
+
return False
|
|
219
|
+
return bool(self._connected)
|
|
220
|
+
|
|
221
|
+
def __del__(self) -> None:
|
|
222
|
+
try:
|
|
223
|
+
object_id = object.__getattribute__(self, "_object_id")
|
|
224
|
+
conn = object.__getattribute__(self, "_conn")
|
|
225
|
+
except AttributeError:
|
|
226
|
+
return
|
|
227
|
+
if object_id:
|
|
228
|
+
try:
|
|
229
|
+
self.disconnect()
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
if conn is not None:
|
|
233
|
+
try:
|
|
234
|
+
conn.close()
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
def __getattr__(self, name: str) -> Any:
|
|
239
|
+
if name.startswith("_"):
|
|
240
|
+
raise AttributeError(name)
|
|
241
|
+
if name in self._method_cache:
|
|
242
|
+
return self._method_proxy(name)
|
|
243
|
+
if name in self._missing_cache:
|
|
244
|
+
raise AttributeError(name)
|
|
245
|
+
try:
|
|
246
|
+
response = self._request(
|
|
247
|
+
{"op": "getattr", "object_id": self._object_id, "name": name}
|
|
248
|
+
)
|
|
249
|
+
if response.get("callable"):
|
|
250
|
+
self._method_cache.add(name)
|
|
251
|
+
return self._method_proxy(name)
|
|
252
|
+
value = decode(response["value"])
|
|
253
|
+
if name == "is_connected":
|
|
254
|
+
self._set_connected(bool(value))
|
|
255
|
+
return value
|
|
256
|
+
except RemoteAttributeError:
|
|
257
|
+
self._missing_cache.add(name)
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
261
|
+
if name.startswith("_") or name == "cameras":
|
|
262
|
+
object.__setattr__(self, name, value)
|
|
263
|
+
return
|
|
264
|
+
self._request(
|
|
265
|
+
{
|
|
266
|
+
"op": "setattr",
|
|
267
|
+
"object_id": self._object_id,
|
|
268
|
+
"name": name,
|
|
269
|
+
"value": jsonable(value),
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _method_proxy(self, name: str):
|
|
274
|
+
def method(*args: Any, **kwargs: Any) -> Any:
|
|
275
|
+
response = self._request(
|
|
276
|
+
{
|
|
277
|
+
"op": "call",
|
|
278
|
+
"object_id": self._object_id,
|
|
279
|
+
"method": name,
|
|
280
|
+
"args": jsonable(args),
|
|
281
|
+
"kwargs": jsonable(kwargs),
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
if name == "connect":
|
|
285
|
+
self._set_connected(True)
|
|
286
|
+
elif name in {"disconnect", "emergency_cleanup", "close"}:
|
|
287
|
+
self._set_connected(False)
|
|
288
|
+
if response.get("context_manager"):
|
|
289
|
+
return RemoteContextManager(
|
|
290
|
+
conn=self._conn,
|
|
291
|
+
object_id=response["object_id"],
|
|
292
|
+
)
|
|
293
|
+
return decode(response.get("value"))
|
|
294
|
+
|
|
295
|
+
return method
|
|
296
|
+
|
|
297
|
+
def _request(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
298
|
+
logger.debug("YAM request op=%r path=%r", payload.get("op"), self._socket_path)
|
|
299
|
+
try:
|
|
300
|
+
return check_response(self._conn.request(payload), error_message="remote YAM call failed")
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
self._mark_disconnected_if_stale(exc)
|
|
303
|
+
raise
|
|
304
|
+
|
|
305
|
+
def _mark_disconnected_if_stale(self, exc: BaseException) -> None:
|
|
306
|
+
if _is_unknown_object_error(exc):
|
|
307
|
+
self._set_connected(False)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _is_unknown_object_error(exc: BaseException) -> bool:
|
|
311
|
+
return "unknown connector object" in str(exc).lower()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _arm_from_port(port: str) -> str:
|
|
315
|
+
parsed = urlparse(port)
|
|
316
|
+
arms = parse_qs(parsed.query).get("arm")
|
|
317
|
+
if not arms:
|
|
318
|
+
raise ValueError("RemoteYAMFollower requires ?arm=left or ?arm=right on config.port")
|
|
319
|
+
return str(arms[0])
|