physicalai-trossen-plugin 0.2.0__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.
Files changed (41) hide show
  1. physicalai_trossen_plugin-0.2.0/.gitignore +213 -0
  2. physicalai_trossen_plugin-0.2.0/PKG-INFO +72 -0
  3. physicalai_trossen_plugin-0.2.0/README.md +52 -0
  4. physicalai_trossen_plugin-0.2.0/pyproject.toml +55 -0
  5. physicalai_trossen_plugin-0.2.0/src/physicalai_trossen_plugin/__init__.py +50 -0
  6. physicalai_trossen_plugin-0.2.0/src/physicalai_trossen_plugin/_urdf.py +23 -0
  7. physicalai_trossen_plugin-0.2.0/src/physicalai_trossen_plugin/bimanual_widowxai.py +200 -0
  8. physicalai_trossen_plugin-0.2.0/src/physicalai_trossen_plugin/constants.py +22 -0
  9. physicalai_trossen_plugin-0.2.0/src/physicalai_trossen_plugin/studio_catalog.py +317 -0
  10. physicalai_trossen_plugin-0.2.0/src/physicalai_trossen_plugin/widowxai.py +314 -0
  11. physicalai_trossen_plugin-0.2.0/urdf/LICENSE +29 -0
  12. physicalai_trossen_plugin-0.2.0/urdf/README.md +1 -0
  13. physicalai_trossen_plugin-0.2.0/urdf/meshes/maple_tabletop.png +0 -0
  14. physicalai_trossen_plugin-0.2.0/urdf/meshes/peripherals/d405.stl +0 -0
  15. physicalai_trossen_plugin-0.2.0/urdf/meshes/stationary_ai/cam_high_mount_link.stl +0 -0
  16. physicalai_trossen_plugin-0.2.0/urdf/meshes/stationary_ai/cam_low_mount_link.stl +0 -0
  17. physicalai_trossen_plugin-0.2.0/urdf/meshes/stationary_ai/frame_link.stl +0 -0
  18. physicalai_trossen_plugin-0.2.0/urdf/meshes/stationary_ai/tabletop_link.stl +0 -0
  19. physicalai_trossen_plugin-0.2.0/urdf/meshes/trossen_black.png +0 -0
  20. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/base_link.stl +0 -0
  21. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/camera_mount_d405.stl +0 -0
  22. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/carriage_left.stl +0 -0
  23. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/carriage_right.stl +0 -0
  24. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/gripper_left.stl +0 -0
  25. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/gripper_right.stl +0 -0
  26. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/handle_left.stl +0 -0
  27. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/handle_right.stl +0 -0
  28. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/leader_finger_left_index.stl +0 -0
  29. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/leader_finger_left_thumb.stl +0 -0
  30. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/leader_finger_right_index.stl +0 -0
  31. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/leader_finger_right_thumb.stl +0 -0
  32. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/link_1.stl +0 -0
  33. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/link_2.stl +0 -0
  34. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/link_3.stl +0 -0
  35. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/link_4.stl +0 -0
  36. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/link_5.stl +0 -0
  37. physicalai_trossen_plugin-0.2.0/urdf/meshes/wxai/link_6.stl +0 -0
  38. physicalai_trossen_plugin-0.2.0/urdf/urdf/generated/stationary_ai.urdf +1321 -0
  39. physicalai_trossen_plugin-0.2.0/urdf/urdf/generated/stationary_ai_leader.urdf +900 -0
  40. physicalai_trossen_plugin-0.2.0/urdf/urdf/generated/wxai/wxai_follower.urdf +405 -0
  41. physicalai_trossen_plugin-0.2.0/urdf/urdf/generated/wxai/wxai_leader_left.urdf +318 -0
@@ -0,0 +1,213 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+
161
+ # mkdocs documentation
162
+ /site
163
+
164
+ # mypy
165
+ .mypy_cache/
166
+ .dmypy.json
167
+ dmypy.json
168
+
169
+ # Pyre type checker
170
+ .pyre/
171
+
172
+ # pytype static type analyzer
173
+ .pytype/
174
+
175
+ # Cython debug symbols
176
+ cython_debug/
177
+
178
+ # PyCharm
179
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
180
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
181
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
182
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
183
+ .idea/
184
+
185
+ # Abstra
186
+ # Abstra is an AI-powered process automation framework.
187
+ # Ignore directories containing user credentials, local state, and settings.
188
+ # Learn more at https://abstra.io/docs
189
+ .abstra/
190
+
191
+ # Visual Studio Code
192
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
193
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
194
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
195
+ # you could uncomment the following to ignore the entire vscode folder
196
+ .vscode/
197
+
198
+ # Ruff stuff:
199
+ .ruff_cache/
200
+
201
+ # PyPI configuration file
202
+ .pypirc
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ # Streamlit
210
+ .streamlit/secrets.toml
211
+
212
+ # Misc
213
+ .DS_Store
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: physicalai-trossen-plugin
3
+ Version: 0.2.0
4
+ Summary: Trossen WidowX AI robot arm plugin for PhysicalAI
5
+ Project-URL: Homepage, https://github.com/openvinotoolkit/physicalai
6
+ Project-URL: Repository, https://github.com/openvinotoolkit/physicalai
7
+ Project-URL: Issues, https://github.com/openvinotoolkit/physicalai/issues
8
+ License-Expression: Apache-2.0
9
+ Requires-Python: <3.14,>=3.12
10
+ Requires-Dist: loguru>=0.7
11
+ Requires-Dist: numpy>=1.24
12
+ Requires-Dist: physicalai-studio-plugin>=0.3.0
13
+ Requires-Dist: physicalai>=0.1.1
14
+ Requires-Dist: pydantic>=2.0
15
+ Requires-Dist: trossen-arm>=1.9.0
16
+ Provides-Extra: tests
17
+ Requires-Dist: pytest; extra == 'tests'
18
+ Requires-Dist: pytest-anyio; extra == 'tests'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # PhysicalAI Trossen Plugin
22
+
23
+ Trossen WidowX AI robot drivers and Physical AI Studio catalog definitions for
24
+ [PhysicalAI](https://github.com/openvinotoolkit/physicalai).
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ uv add physicalai-trossen-plugin
30
+ ```
31
+
32
+ The plugin depends on `trossen-arm`. That SDK does not currently publish
33
+ wheels for Python 3.14+, so use Python 3.12 or 3.13 when controlling a robot.
34
+
35
+ ## Usage
36
+
37
+ ```python
38
+ from physicalai_trossen_plugin import WidowXAI
39
+
40
+ robot = WidowXAI(ip="192.168.1.100", role="follower")
41
+ ```
42
+
43
+ For two arms, construct `BimanualWidowXAI` from left and right `WidowXAI`
44
+ instances. Installing this distribution also registers the Trossen entries in
45
+ the Physical AI Studio robot catalog.
46
+
47
+ ## URDF assets
48
+
49
+ This package vendors the generated URDFs and mesh assets required by its
50
+ catalog entries from Trossen `trossen_arm_description` revision
51
+ `21d8b360c211c2ad8a065d8f462cbec0207626e7`. The source license is preserved
52
+ at [`urdf/LICENSE`](urdf/LICENSE).
53
+
54
+ ```python
55
+ from physicalai_trossen_plugin import get_urdf_path
56
+
57
+ urdf_root = get_urdf_path()
58
+ single_arm_follower = urdf_root / "urdf/generated/wxai/wxai_follower.urdf"
59
+ single_arm_leader = urdf_root / "urdf/generated/wxai/wxai_leader_left.urdf"
60
+ bimanual_follower = urdf_root / "urdf/generated/stationary_ai.urdf"
61
+ bimanual_leader = urdf_root / "urdf/generated/stationary_ai_leader.urdf"
62
+ ```
63
+
64
+ The single-arm leader catalog entry uses the left-handed leader model. The
65
+ bimanual leader model includes left- and right-handed leader arms.
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ uv sync
71
+ uv run pytest packages/physicalai-trossen-plugin/tests
72
+ ```
@@ -0,0 +1,52 @@
1
+ # PhysicalAI Trossen Plugin
2
+
3
+ Trossen WidowX AI robot drivers and Physical AI Studio catalog definitions for
4
+ [PhysicalAI](https://github.com/openvinotoolkit/physicalai).
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ uv add physicalai-trossen-plugin
10
+ ```
11
+
12
+ The plugin depends on `trossen-arm`. That SDK does not currently publish
13
+ wheels for Python 3.14+, so use Python 3.12 or 3.13 when controlling a robot.
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from physicalai_trossen_plugin import WidowXAI
19
+
20
+ robot = WidowXAI(ip="192.168.1.100", role="follower")
21
+ ```
22
+
23
+ For two arms, construct `BimanualWidowXAI` from left and right `WidowXAI`
24
+ instances. Installing this distribution also registers the Trossen entries in
25
+ the Physical AI Studio robot catalog.
26
+
27
+ ## URDF assets
28
+
29
+ This package vendors the generated URDFs and mesh assets required by its
30
+ catalog entries from Trossen `trossen_arm_description` revision
31
+ `21d8b360c211c2ad8a065d8f462cbec0207626e7`. The source license is preserved
32
+ at [`urdf/LICENSE`](urdf/LICENSE).
33
+
34
+ ```python
35
+ from physicalai_trossen_plugin import get_urdf_path
36
+
37
+ urdf_root = get_urdf_path()
38
+ single_arm_follower = urdf_root / "urdf/generated/wxai/wxai_follower.urdf"
39
+ single_arm_leader = urdf_root / "urdf/generated/wxai/wxai_leader_left.urdf"
40
+ bimanual_follower = urdf_root / "urdf/generated/stationary_ai.urdf"
41
+ bimanual_leader = urdf_root / "urdf/generated/stationary_ai_leader.urdf"
42
+ ```
43
+
44
+ The single-arm leader catalog entry uses the left-handed leader model. The
45
+ bimanual leader model includes left- and right-handed leader arms.
46
+
47
+ ## Development
48
+
49
+ ```bash
50
+ uv sync
51
+ uv run pytest packages/physicalai-trossen-plugin/tests
52
+ ```
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling<1.30", "hatch-vcs"] # cap <1.30: hatchling>=1.30 emits Metadata 2.5, rejected by PyPI/twine (pypi/warehouse#19083)
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "physicalai-trossen-plugin"
7
+ dynamic = ["version"]
8
+ description = "Trossen WidowX AI robot arm plugin for PhysicalAI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12,<3.14"
11
+ license = "Apache-2.0"
12
+ dependencies = [
13
+ "physicalai>=0.1.1",
14
+ "physicalai-studio-plugin>=0.3.0",
15
+ "numpy>=1.24",
16
+ "loguru>=0.7",
17
+ "pydantic>=2.0",
18
+ "trossen-arm>=1.9.0",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ tests = ["pytest", "pytest-anyio"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/openvinotoolkit/physicalai"
26
+ Repository = "https://github.com/openvinotoolkit/physicalai"
27
+ Issues = "https://github.com/openvinotoolkit/physicalai/issues"
28
+
29
+ [project.entry-points."physicalai.studio.catalog_plugins"]
30
+ trossen = "physicalai_trossen_plugin.studio_catalog:register_physicalai_studio_plugin"
31
+
32
+ [tool.hatch.version]
33
+ source = "vcs"
34
+ tag-pattern = '^physicalai-trossen-plugin-v(?P<version>\d+(?:\.\d+)*)$'
35
+ raw-options = { search_parent_directories = true, git_describe_command = "git describe --dirty --tags --long --match physicalai-trossen-plugin-v*" }
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/physicalai_trossen_plugin"]
39
+
40
+ [tool.hatch.build.targets.wheel.force-include]
41
+ "urdf" = "urdf"
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ include = [
45
+ "src/physicalai_trossen_plugin/**",
46
+ "urdf/**",
47
+ "README.md",
48
+ "pyproject.toml",
49
+ "LICENSE",
50
+ ]
51
+
52
+ [tool.pytest.ini_options]
53
+ addopts = ["--strict-markers", "--strict-config", "-ra"]
54
+ testpaths = "tests"
55
+ pythonpath = "src"
@@ -0,0 +1,50 @@
1
+ # Copyright (C) 2026 Intel Corporation
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # ruff: noqa: PLC0415
5
+
6
+ """Trossen WidowX AI robot integration for PhysicalAI."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ from physicalai_trossen_plugin._urdf import get_urdf_path as get_urdf_path
13
+
14
+ if TYPE_CHECKING:
15
+ from physicalai_trossen_plugin.bimanual_widowxai import BimanualWidowXAI as BimanualWidowXAI
16
+ from physicalai_trossen_plugin.bimanual_widowxai import BimanualWidowXAIObservation as BimanualWidowXAIObservation
17
+ from physicalai_trossen_plugin.widowxai import WidowXAI as WidowXAI
18
+ from physicalai_trossen_plugin.widowxai import WidowXAIObservation as WidowXAIObservation
19
+
20
+ __all__ = [
21
+ "BimanualWidowXAI",
22
+ "BimanualWidowXAIObservation",
23
+ "WidowXAI",
24
+ "WidowXAIObservation",
25
+ "get_urdf_path",
26
+ ]
27
+
28
+
29
+ def __getattr__(name: str) -> object:
30
+ """Lazily import robot drivers without loading the vendor SDK on package import.
31
+
32
+ Returns:
33
+ The requested robot driver or observation type.
34
+
35
+ Raises:
36
+ AttributeError: If ``name`` is not exported by this package.
37
+ """
38
+ if name in {"WidowXAI", "WidowXAIObservation"}:
39
+ from physicalai_trossen_plugin.widowxai import WidowXAI, WidowXAIObservation
40
+
41
+ return {"WidowXAI": WidowXAI, "WidowXAIObservation": WidowXAIObservation}[name]
42
+ if name in {"BimanualWidowXAI", "BimanualWidowXAIObservation"}:
43
+ from physicalai_trossen_plugin.bimanual_widowxai import BimanualWidowXAI, BimanualWidowXAIObservation
44
+
45
+ return {
46
+ "BimanualWidowXAI": BimanualWidowXAI,
47
+ "BimanualWidowXAIObservation": BimanualWidowXAIObservation,
48
+ }[name]
49
+ msg = f"module {__name__!r} has no attribute {name!r}"
50
+ raise AttributeError(msg)
@@ -0,0 +1,23 @@
1
+ # Copyright (C) 2026 Intel Corporation
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """URDF path utility for bundled Trossen robot descriptions."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import importlib.resources as ir # nosemgrep: python.lang.compatibility.python37.python37-compatibility-importlib2
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+
15
+ def get_urdf_path() -> Path:
16
+ """Return the directory containing bundled Trossen description assets."""
17
+ traversal = ir.files("physicalai_trossen_plugin")
18
+ with ir.as_file(traversal) as package_path:
19
+ candidates = (package_path.parent / "urdf", package_path.parent.parent / "urdf")
20
+ for candidate in candidates:
21
+ if candidate.exists():
22
+ return candidate
23
+ return candidates[1]
@@ -0,0 +1,200 @@
1
+ # Copyright (C) 2026 Intel Corporation
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Bimanual Trossen WidowX AI robot driver.
5
+
6
+ Composes two :class:`WidowXAI` arms (left + right) behind the
7
+ :class:`~physicalai.robot.Robot` protocol, prefixing all joint names with
8
+ ``left_`` / ``right_`` respectively.
9
+
10
+ Usage::
11
+
12
+ from physicalai_trossen_plugin import WidowXAI, BimanualWidowXAI
13
+
14
+ left = WidowXAI(ip="192.168.1.10", role="follower")
15
+ right = WidowXAI(ip="192.168.1.11", role="follower")
16
+ robot = BimanualWidowXAI(left=left, right=right)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from typing import TYPE_CHECKING
23
+
24
+ import numpy as np
25
+
26
+ from physicalai.config import export_config
27
+ from physicalai.robot import Robot
28
+
29
+ if TYPE_CHECKING:
30
+ from physicalai.capture.frame import Frame
31
+ from physicalai.robot.interface import RobotObservation
32
+ from physicalai_trossen_plugin.widowxai import WidowXAI
33
+
34
+
35
+ @dataclass
36
+ class BimanualWidowXAIObservation:
37
+ """Merged observation from a bimanual WidowX AI robot.
38
+
39
+ Attributes:
40
+ joint_positions: Array of shape ``(14,)`` — left (7) then right (7)
41
+ with non-gripper joints in degrees and grippers in native scalar
42
+ units.
43
+ timestamp: ``time.monotonic()`` at the moment of capture (left arm).
44
+ sensor_data: Merged velocities and (for follower) external efforts,
45
+ each of shape ``(14,)``.
46
+ images: Always ``None`` — no built-in camera support.
47
+ """
48
+
49
+ joint_positions: np.ndarray
50
+ timestamp: float
51
+ sensor_data: dict[str, np.ndarray] | None = None
52
+ images: dict[str, Frame] | None = None
53
+
54
+ @property
55
+ def state(self) -> np.ndarray:
56
+ """State vector: joint positions (14)."""
57
+ return self.joint_positions
58
+
59
+
60
+ @export_config(class_path="physicalai_trossen_plugin.BimanualWidowXAI")
61
+ class BimanualWidowXAI(Robot):
62
+ """Two-arm WidowX AI driver composing a left and right :class:`WidowXAI`.
63
+
64
+ Args:
65
+ left: Left arm driver.
66
+ right: Right arm driver.
67
+
68
+ Raises:
69
+ ValueError: If the two arms have different roles.
70
+ """
71
+
72
+ def __init__(self, left: WidowXAI, right: WidowXAI) -> None:
73
+ """Initialize BimanualWidowXAI with two single-arm drivers.
74
+
75
+ Raises:
76
+ ValueError: If left and right arms have different roles.
77
+ """
78
+ if left.role != right.role:
79
+ msg = f"Both arms must have the same role; got left={left.role!r}, right={right.role!r}."
80
+ raise ValueError(msg)
81
+ if type(left) is not type(right):
82
+ msg = (
83
+ f"Both arms must be the same driver type; got left={type(left).__name__}, right={type(right).__name__}."
84
+ )
85
+ raise ValueError(msg)
86
+
87
+ self._left = left
88
+ self._right = right
89
+
90
+ @property
91
+ def role(self) -> str:
92
+ """Robot role (``"leader"`` or ``"follower"``)."""
93
+ return self._left.role
94
+
95
+ @property
96
+ def device_ids(self) -> tuple[str, ...]:
97
+ """Sorted, deduplicated device ids of both constituent arms."""
98
+ return tuple(sorted(set(self._left.device_ids) | set(self._right.device_ids)))
99
+
100
+ @property
101
+ def joint_names(self) -> list[str]:
102
+ """Ordered joint names with ``left_`` / ``right_`` prefix."""
103
+ left_names = [f"left_{n}" for n in self._left.joint_names]
104
+ right_names = [f"right_{n}" for n in self._right.joint_names]
105
+ return left_names + right_names
106
+
107
+ def connect(self) -> None:
108
+ """Connect both arms."""
109
+ self._left.connect()
110
+ try:
111
+ self._right.connect()
112
+ except Exception:
113
+ self._left.disconnect()
114
+ raise
115
+
116
+ def disconnect(self) -> None:
117
+ """Disconnect both arms."""
118
+ self._left.disconnect()
119
+ self._right.disconnect()
120
+
121
+ def is_connected(self) -> bool:
122
+ """Return ``True`` when both arms are connected."""
123
+ return self._left.is_connected() and self._right.is_connected()
124
+
125
+ def get_observation(self) -> RobotObservation:
126
+ """Read and merge observations from both arms.
127
+
128
+ Returns:
129
+ Merged observation with left-then-right joint positions and
130
+ sensor data arrays of shape ``(14,)``.
131
+ """
132
+ left_obs = self._left.get_observation()
133
+ right_obs = self._right.get_observation()
134
+
135
+ positions = np.concatenate([left_obs.joint_positions, right_obs.joint_positions])
136
+
137
+ sensor_data: dict[str, np.ndarray] | None = None
138
+ if left_obs.sensor_data is not None and right_obs.sensor_data is not None:
139
+ sensor_data = {
140
+ key: np.concatenate([left_value, right_obs.sensor_data[key]])
141
+ for key, left_value in left_obs.sensor_data.items()
142
+ }
143
+
144
+ return BimanualWidowXAIObservation(
145
+ joint_positions=positions,
146
+ timestamp=left_obs.timestamp,
147
+ sensor_data=sensor_data,
148
+ )
149
+
150
+ def send_action(self, action: np.ndarray, *, goal_time: float = 0.1) -> None:
151
+ """Send joint position command (follower only).
152
+
153
+ Args:
154
+ action: Array of shape ``(N,)`` where N >= 14. Only the first 14
155
+ elements (left 7 + right 7 positions) are used; extra
156
+ dimensions (e.g. predicted velocities) are ignored.
157
+ goal_time: Minimum time (seconds) for the arms to reach the target.
158
+
159
+ Raises:
160
+ RuntimeError: If called on a leader robot.
161
+ ValueError: If action has fewer than 14 elements.
162
+ """
163
+ if self.role == "leader":
164
+ msg = "Cannot send actions to a leader robot."
165
+ raise RuntimeError(msg)
166
+
167
+ n = self._left.NUM_JOINTS
168
+ min_dims = 2 * n
169
+ if action.shape[0] < min_dims:
170
+ msg = f"Expected at least {min_dims} action dims, got {action.shape}"
171
+ raise ValueError(msg)
172
+
173
+ action = action[:min_dims]
174
+ self._left.send_action(action[:n], goal_time=goal_time)
175
+ self._right.send_action(action[n:], goal_time=goal_time)
176
+
177
+ def set_external_efforts(self, efforts: np.ndarray, gain: float = 1.0) -> None:
178
+ """Apply force feedback (leader only).
179
+
180
+ Args:
181
+ efforts: Array of shape ``(14,)`` — left (7) then right (7) effort
182
+ values.
183
+ gain: Scaling factor for efforts. Defaults to 1.0.
184
+
185
+ Raises:
186
+ RuntimeError: If called on a follower robot.
187
+ ValueError: If efforts shape is not ``(14,)``.
188
+ """
189
+ if self.role != "leader":
190
+ msg = "set_external_efforts is only available for leader robots."
191
+ raise RuntimeError(msg)
192
+
193
+ n = self._left.NUM_JOINTS
194
+ expected_shape = (2 * n,)
195
+ if efforts.shape != expected_shape:
196
+ msg = f"Expected efforts shape {expected_shape}, got {efforts.shape}"
197
+ raise ValueError(msg)
198
+
199
+ self._left.set_external_efforts(efforts[:n], gain)
200
+ self._right.set_external_efforts(efforts[n:], gain)
@@ -0,0 +1,22 @@
1
+ # Copyright (C) 2026 Intel Corporation
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Trossen WidowX AI hardware constants."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Final
9
+
10
+ WIDOWXAI_JOINT_ORDER: Final = (
11
+ "shoulder_pan",
12
+ "shoulder_lift",
13
+ "elbow_flex",
14
+ "wrist_flex",
15
+ "wrist_yaw",
16
+ "wrist_roll",
17
+ "gripper",
18
+ )
19
+
20
+ VALID_ROLES: Final = frozenset({"leader", "follower"})
21
+
22
+ HOME_POSITION: Final = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)