physicalai-studio-plugin 0.1.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.
- physicalai_studio_plugin-0.1.0/.dockerignore +3 -0
- physicalai_studio_plugin-0.1.0/.gitignore +212 -0
- physicalai_studio_plugin-0.1.0/.pre-commit-config.yaml +77 -0
- physicalai_studio_plugin-0.1.0/PKG-INFO +7 -0
- physicalai_studio_plugin-0.1.0/README.md +352 -0
- physicalai_studio_plugin-0.1.0/pyproject.toml +67 -0
- physicalai_studio_plugin-0.1.0/src/physicalai_studio_plugin/__init__.py +28 -0
- physicalai_studio_plugin-0.1.0/src/physicalai_studio_plugin/assets.py +20 -0
- physicalai_studio_plugin-0.1.0/src/physicalai_studio_plugin/catalog.py +71 -0
- physicalai_studio_plugin-0.1.0/src/physicalai_studio_plugin/factory.py +19 -0
- physicalai_studio_plugin-0.1.0/src/physicalai_studio_plugin/probe.py +56 -0
- physicalai_studio_plugin-0.1.0/src/physicalai_studio_plugin/schemas.py +12 -0
- physicalai_studio_plugin-0.1.0/tests/__init__.py +0 -0
- physicalai_studio_plugin-0.1.0/tests/test_contracts.py +188 -0
- physicalai_studio_plugin-0.1.0/tests/test_imports.py +30 -0
- physicalai_studio_plugin-0.1.0/uv.lock +1154 -0
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
lib64/
|
|
18
|
+
parts/
|
|
19
|
+
sdist/
|
|
20
|
+
var/
|
|
21
|
+
wheels/
|
|
22
|
+
share/python-wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
MANIFEST
|
|
27
|
+
|
|
28
|
+
# PyInstaller
|
|
29
|
+
# Usually these files are written by a python script from a template
|
|
30
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Installer logs
|
|
35
|
+
pip-log.txt
|
|
36
|
+
pip-delete-this-directory.txt
|
|
37
|
+
|
|
38
|
+
# Unit test / coverage reports
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
.coverage
|
|
43
|
+
.coverage.*
|
|
44
|
+
.cache
|
|
45
|
+
nosetests.xml
|
|
46
|
+
coverage.xml
|
|
47
|
+
*.cover
|
|
48
|
+
*.py.cover
|
|
49
|
+
.hypothesis/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
cover/
|
|
52
|
+
|
|
53
|
+
# Translations
|
|
54
|
+
*.mo
|
|
55
|
+
*.pot
|
|
56
|
+
|
|
57
|
+
# Django stuff:
|
|
58
|
+
*.log
|
|
59
|
+
local_settings.py
|
|
60
|
+
db.sqlite3
|
|
61
|
+
db.sqlite3-journal
|
|
62
|
+
|
|
63
|
+
# Flask stuff:
|
|
64
|
+
instance/
|
|
65
|
+
.webassets-cache
|
|
66
|
+
|
|
67
|
+
# Scrapy stuff:
|
|
68
|
+
.scrapy
|
|
69
|
+
|
|
70
|
+
# Sphinx documentation
|
|
71
|
+
docs/_build/
|
|
72
|
+
|
|
73
|
+
# PyBuilder
|
|
74
|
+
.pybuilder/
|
|
75
|
+
target/
|
|
76
|
+
|
|
77
|
+
# Jupyter Notebook
|
|
78
|
+
.ipynb_checkpoints
|
|
79
|
+
|
|
80
|
+
# IPython
|
|
81
|
+
profile_default/
|
|
82
|
+
ipython_config.py
|
|
83
|
+
|
|
84
|
+
# pyenv
|
|
85
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
86
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
87
|
+
# .python-version
|
|
88
|
+
|
|
89
|
+
# pipenv
|
|
90
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
91
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
92
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
93
|
+
# install all needed dependencies.
|
|
94
|
+
#Pipfile.lock
|
|
95
|
+
|
|
96
|
+
# UV
|
|
97
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
98
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
99
|
+
# commonly ignored for libraries.
|
|
100
|
+
#uv.lock
|
|
101
|
+
|
|
102
|
+
# poetry
|
|
103
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
104
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
105
|
+
# commonly ignored for libraries.
|
|
106
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
107
|
+
#poetry.lock
|
|
108
|
+
#poetry.toml
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
113
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
114
|
+
#pdm.lock
|
|
115
|
+
#pdm.toml
|
|
116
|
+
.pdm-python
|
|
117
|
+
.pdm-build/
|
|
118
|
+
|
|
119
|
+
# pixi
|
|
120
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
121
|
+
#pixi.lock
|
|
122
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
123
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
124
|
+
.pixi
|
|
125
|
+
|
|
126
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
127
|
+
__pypackages__/
|
|
128
|
+
|
|
129
|
+
# Celery stuff
|
|
130
|
+
celerybeat-schedule
|
|
131
|
+
celerybeat.pid
|
|
132
|
+
|
|
133
|
+
# SageMath parsed files
|
|
134
|
+
*.sage.py
|
|
135
|
+
|
|
136
|
+
# Environments
|
|
137
|
+
.env
|
|
138
|
+
.envrc
|
|
139
|
+
.venv
|
|
140
|
+
env/
|
|
141
|
+
venv/
|
|
142
|
+
ENV/
|
|
143
|
+
env.bak/
|
|
144
|
+
venv.bak/
|
|
145
|
+
|
|
146
|
+
# Spyder project settings
|
|
147
|
+
.spyderproject
|
|
148
|
+
.spyproject
|
|
149
|
+
|
|
150
|
+
# Rope project settings
|
|
151
|
+
.ropeproject
|
|
152
|
+
|
|
153
|
+
# mkdocs documentation
|
|
154
|
+
/site
|
|
155
|
+
|
|
156
|
+
# mypy
|
|
157
|
+
.mypy_cache/
|
|
158
|
+
.dmypy.json
|
|
159
|
+
dmypy.json
|
|
160
|
+
|
|
161
|
+
# Pyre type checker
|
|
162
|
+
.pyre/
|
|
163
|
+
|
|
164
|
+
# pytype static type analyzer
|
|
165
|
+
.pytype/
|
|
166
|
+
|
|
167
|
+
# Cython debug symbols
|
|
168
|
+
cython_debug/
|
|
169
|
+
|
|
170
|
+
# PyCharm
|
|
171
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
172
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
173
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
174
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
175
|
+
#.idea/
|
|
176
|
+
|
|
177
|
+
# Abstra
|
|
178
|
+
# Abstra is an AI-powered process automation framework.
|
|
179
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
180
|
+
# Learn more at https://abstra.io/docs
|
|
181
|
+
.abstra/
|
|
182
|
+
|
|
183
|
+
# Visual Studio Code
|
|
184
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
185
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
186
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
187
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
188
|
+
.vscode/
|
|
189
|
+
.idea
|
|
190
|
+
|
|
191
|
+
# Ruff stuff:
|
|
192
|
+
.ruff_cache/
|
|
193
|
+
|
|
194
|
+
# PyPI configuration file
|
|
195
|
+
.pypirc
|
|
196
|
+
|
|
197
|
+
# Cursor
|
|
198
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
199
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
200
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
201
|
+
.cursorignore
|
|
202
|
+
.cursorindexingignore
|
|
203
|
+
|
|
204
|
+
# Marimo
|
|
205
|
+
marimo/_static/
|
|
206
|
+
marimo/_lsp/
|
|
207
|
+
__marimo__/
|
|
208
|
+
|
|
209
|
+
# Custom
|
|
210
|
+
tmp*
|
|
211
|
+
lightning_logs
|
|
212
|
+
.DS_Store
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Copyright (C) 2026 Intel Corporation
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
# Plugin SDK-specific pre-commit configuration for prek workspace mode
|
|
5
|
+
# This config applies only to files in application/plugin/
|
|
6
|
+
#
|
|
7
|
+
# Kept in sync with application/backend/.pre-commit-config.yaml and
|
|
8
|
+
# application/trainer/.pre-commit-config.yaml (the canonical setup for
|
|
9
|
+
# Python apps in this repo). The backend's Alembic migration check is not
|
|
10
|
+
# included because this workspace does not own a database schema.
|
|
11
|
+
|
|
12
|
+
repos:
|
|
13
|
+
# ========================================================================== #
|
|
14
|
+
# PYTHON CHECKS #
|
|
15
|
+
# ========================================================================== #
|
|
16
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
17
|
+
rev: v5.0.0
|
|
18
|
+
hooks:
|
|
19
|
+
- id: debug-statements
|
|
20
|
+
- id: check-ast
|
|
21
|
+
|
|
22
|
+
# ========================================================================== #
|
|
23
|
+
# PYTHON LINTING & FORMATTING #
|
|
24
|
+
# ========================================================================== #
|
|
25
|
+
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
|
26
|
+
rev: "v0.11.11"
|
|
27
|
+
hooks:
|
|
28
|
+
# Run the linter with fixes (uses [tool.ruff] from pyproject.toml)
|
|
29
|
+
- id: ruff
|
|
30
|
+
args: ["--config=pyproject.toml", "--fix"]
|
|
31
|
+
# Run the formatter (uses [tool.ruff] from pyproject.toml)
|
|
32
|
+
- id: ruff-format
|
|
33
|
+
args: ["--config=pyproject.toml"]
|
|
34
|
+
|
|
35
|
+
# ========================================================================== #
|
|
36
|
+
# PYTHON TYPE CHECKING #
|
|
37
|
+
# ========================================================================== #
|
|
38
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
39
|
+
rev: "v1.15.0"
|
|
40
|
+
hooks:
|
|
41
|
+
- id: mypy
|
|
42
|
+
# Uses [tool.mypy] from pyproject.toml
|
|
43
|
+
args: ["--config-file=pyproject.toml"]
|
|
44
|
+
additional_dependencies:
|
|
45
|
+
- types-PyYAML
|
|
46
|
+
- types-setuptools
|
|
47
|
+
- types-aiofiles
|
|
48
|
+
|
|
49
|
+
- repo: https://github.com/facebook/pyrefly-pre-commit
|
|
50
|
+
rev: 1.1.1
|
|
51
|
+
hooks:
|
|
52
|
+
- id: pyrefly-check
|
|
53
|
+
name: Pyrefly (type checking)
|
|
54
|
+
entry: uv run pyrefly check
|
|
55
|
+
language: system
|
|
56
|
+
pass_filenames: false
|
|
57
|
+
|
|
58
|
+
# ========================================================================== #
|
|
59
|
+
# SECURITY - SECRET DETECTION #
|
|
60
|
+
# ========================================================================== #
|
|
61
|
+
# gitleaks detects secrets (API keys, tokens, credentials) in code.
|
|
62
|
+
- repo: https://github.com/gitleaks/gitleaks
|
|
63
|
+
rev: v8.30.1
|
|
64
|
+
hooks:
|
|
65
|
+
- id: gitleaks
|
|
66
|
+
|
|
67
|
+
# ========================================================================== #
|
|
68
|
+
# DEPENDENCY LOCKFILE CHECKS #
|
|
69
|
+
# ========================================================================== #
|
|
70
|
+
- repo: local
|
|
71
|
+
hooks:
|
|
72
|
+
- id: check-uv-lock
|
|
73
|
+
name: Check uv.lock is up to date
|
|
74
|
+
entry: uv lock --check
|
|
75
|
+
language: system
|
|
76
|
+
pass_filenames: false
|
|
77
|
+
files: ^(pyproject\.toml|uv\.lock)$
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Physical AI Studio Plugin
|
|
2
|
+
|
|
3
|
+
Types, protocols, and utilities for building robot catalog plugins for **Physical AI Studio**.
|
|
4
|
+
|
|
5
|
+
External robot types register themselves with Studio through an [entry-point](#entry-point-registration) mechanism, so they can be discovered, configured, and driven without modifying Studio's internal code.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv add physicalai-studio-plugin
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.12+. Dependencies are `pydantic>=2.12` and `physicalai`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
A minimal plugin has this structure:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
physicalai-my-robot-plugin/
|
|
25
|
+
├── pyproject.toml
|
|
26
|
+
├── README.md
|
|
27
|
+
└── src/
|
|
28
|
+
└── physicalai_my_robot_plugin/
|
|
29
|
+
├── __init__.py
|
|
30
|
+
└── studio_catalog.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `pyproject.toml`
|
|
34
|
+
|
|
35
|
+
```toml
|
|
36
|
+
[project]
|
|
37
|
+
name = "physicalai-my-robot-plugin"
|
|
38
|
+
version = "0.1.0"
|
|
39
|
+
requires-python = ">=3.12"
|
|
40
|
+
dependencies = [
|
|
41
|
+
"physicalai",
|
|
42
|
+
"physicalai-studio-plugin",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.entry-points."physicalai.studio.catalog_plugins"]
|
|
46
|
+
my-robot = "physicalai_my_robot_plugin.studio_catalog:register_physicalai_studio_plugin"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["hatchling"]
|
|
50
|
+
build-backend = "hatchling.build"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `studio_catalog.py`
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
from collections.abc import Awaitable, Callable
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
from typing import TYPE_CHECKING, Any
|
|
61
|
+
|
|
62
|
+
from physicalai.robot.interface import Robot as PhysicalAIRobot
|
|
63
|
+
from physicalai_studio_plugin import (
|
|
64
|
+
CatalogRobotFactory,
|
|
65
|
+
PortScanner,
|
|
66
|
+
RobotAdapterOptions,
|
|
67
|
+
RobotAsset,
|
|
68
|
+
RobotCatalogDefinition,
|
|
69
|
+
RobotProbe,
|
|
70
|
+
SerialPortInfo,
|
|
71
|
+
)
|
|
72
|
+
from pydantic import BaseModel, Field
|
|
73
|
+
|
|
74
|
+
if TYPE_CHECKING:
|
|
75
|
+
from physicalai_studio_plugin import CatalogRobot
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MyRobotPayload(BaseModel):
|
|
79
|
+
connection_string: str = ""
|
|
80
|
+
serial_number: str = Field(...)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _build_my_robot(
|
|
84
|
+
robot: CatalogRobot[MyRobotPayload],
|
|
85
|
+
factory: CatalogRobotFactory,
|
|
86
|
+
) -> PhysicalAIRobot:
|
|
87
|
+
port = await factory.find_port(
|
|
88
|
+
SerialPortInfo(
|
|
89
|
+
connection_string=robot.payload.connection_string or None,
|
|
90
|
+
serial_number=robot.payload.serial_number or None,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
if port is None:
|
|
94
|
+
msg = f"Robot not found: {robot.payload.serial_number}"
|
|
95
|
+
raise RuntimeError(msg)
|
|
96
|
+
# ... create and return your PhysicalAIRobot implementation ...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class MyRobotProbe:
|
|
100
|
+
"""Structurally implements RobotProbe[MyRobotPayload]."""
|
|
101
|
+
|
|
102
|
+
async def discover(self, manager: PortScanner) -> list[SerialPortInfo]:
|
|
103
|
+
await manager.find_robots()
|
|
104
|
+
return manager.robots
|
|
105
|
+
|
|
106
|
+
async def identify(
|
|
107
|
+
self, payload: MyRobotPayload, manager: PortScanner | None, joint: str | None = None
|
|
108
|
+
) -> None:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
async def is_online(
|
|
112
|
+
self, payload: MyRobotPayload, manager: PortScanner | None = None
|
|
113
|
+
) -> bool:
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _definitions() -> list[RobotCatalogDefinition[MyRobotPayload]]:
|
|
118
|
+
return [
|
|
119
|
+
RobotCatalogDefinition[MyRobotPayload](
|
|
120
|
+
type="MyRobot_Follower",
|
|
121
|
+
display_name="My Robot Follower",
|
|
122
|
+
role="follower",
|
|
123
|
+
robot_builder=_build_my_robot,
|
|
124
|
+
robot_payload=MyRobotPayload,
|
|
125
|
+
asset=RobotAsset(
|
|
126
|
+
urdf_relative_path=Path("my_robot/model.urdf"),
|
|
127
|
+
packages={"my_robot": Path("my_robot")},
|
|
128
|
+
joint_map={"gripper.pos": ["gripper"]},
|
|
129
|
+
root_resolver=lambda: Path("/path/to/urdf"),
|
|
130
|
+
),
|
|
131
|
+
adapter_options=RobotAdapterOptions(include_velocities=True),
|
|
132
|
+
probe=MyRobotProbe(),
|
|
133
|
+
),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def register_physicalai_studio_plugin(registry: Any) -> None:
|
|
138
|
+
for definition in _definitions():
|
|
139
|
+
registry.register_robot(definition)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## API Reference
|
|
145
|
+
|
|
146
|
+
### `RobotCatalogDefinition`
|
|
147
|
+
|
|
148
|
+
The primary data class that describes a robot type to Studio. Generic over the payload model — use ``RobotCatalogDefinition[MyRobotPayload]`` to link the payload, probe, and robot builder types together.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
@dataclass
|
|
152
|
+
class RobotCatalogDefinition(Generic[_PayloadT]):
|
|
153
|
+
type: str # Unique identifier, e.g. "MyRobot_Follower"
|
|
154
|
+
display_name: str # Human-readable name
|
|
155
|
+
role: Literal["follower", "leader"]
|
|
156
|
+
robot_builder: BuildRobotCallable | None = None
|
|
157
|
+
robot_payload: type[_PayloadT] | None = None
|
|
158
|
+
asset: RobotAsset | None = None
|
|
159
|
+
adapter_options: RobotAdapterOptions = field(default_factory=RobotAdapterOptions)
|
|
160
|
+
probe: RobotProbe[_PayloadT] | None = None
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Field | Description |
|
|
164
|
+
|-------|-------------|
|
|
165
|
+
| `type` | Stable identifier used in DB storage and API paths. Must be unique across all plugins. Convention: PascalCase with underscores, e.g. `"MyRobot_Follower"`. |
|
|
166
|
+
| `display_name` | Human-readable name shown in the Studio UI. |
|
|
167
|
+
| `role` | Either `"follower"` (executes actions) or `"leader"` (provides demonstrations). |
|
|
168
|
+
| `robot_builder` | Async callable that receives a robot payload and factory, returns a `PhysicalAIRobot` instance. |
|
|
169
|
+
| `robot_payload` | A Pydantic `BaseModel` subclass defining the configuration fields for this robot type (e.g. `serial_number`, `connection_string`). |
|
|
170
|
+
| `asset` | URDF and package maps for 3D visualization. |
|
|
171
|
+
| `adapter_options` | Controls velocity/effort forwarding behavior. |
|
|
172
|
+
| `probe` | Optional [`RobotProbe[_PayloadT]`](#robotprobe) typed to the same payload model. |
|
|
173
|
+
|
|
174
|
+
### `RobotAdapterOptions`
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class RobotAdapterOptions:
|
|
179
|
+
include_velocities: bool = False
|
|
180
|
+
goal_time_scale: float = 1.0
|
|
181
|
+
external_effort_gain: float | None = 0.1
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `RobotAsset`
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
@dataclass(frozen=True)
|
|
188
|
+
class RobotAsset:
|
|
189
|
+
urdf_relative_path: Path
|
|
190
|
+
packages: dict[str, Path]
|
|
191
|
+
joint_map: dict[str, list[str]]
|
|
192
|
+
root_resolver: Callable[[], Path] | None = None
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| Field | Description |
|
|
196
|
+
|-------|-------------|
|
|
197
|
+
| `urdf_relative_path` | Path to the URDF file, relative to the packages root. |
|
|
198
|
+
| `packages` | Maps ROS package names to their filesystem paths, e.g. `{"my_robot": Path("my_robot")}`. |
|
|
199
|
+
| `joint_map` | Maps Studio's observation key names (e.g. `"gripper.pos"`) to URDF joint name(s). |
|
|
200
|
+
| `root_resolver` | Callable that returns the root directory for URDF lookup. Used by Studio to resolve URDF paths for the API. |
|
|
201
|
+
|
|
202
|
+
### `RobotProbe`
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
@runtime_checkable
|
|
206
|
+
class RobotProbe(Protocol[_PayloadT]):
|
|
207
|
+
async def discover(self, manager: PortScanner) -> list[SerialPortInfo]: ...
|
|
208
|
+
async def identify(
|
|
209
|
+
self, payload: _PayloadT, manager: PortScanner | None, joint: str | None = None
|
|
210
|
+
) -> None: ...
|
|
211
|
+
async def is_online(
|
|
212
|
+
self, payload: _PayloadT, manager: PortScanner | None = None
|
|
213
|
+
) -> bool: ...
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Generic protocol over your robot's payload model. Implement it structurally — your class receives the typed payload directly instead of a raw dict. The ``_PayloadT`` type parameter is automatically inferred from ``identify`` / ``is_online`` signatures; you do not need to explicitly inherit from ``RobotProbe``.
|
|
217
|
+
|
|
218
|
+
### `PortScanner`
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
class PortScanner(Protocol):
|
|
222
|
+
async def find_robots(self) -> None: ...
|
|
223
|
+
@property
|
|
224
|
+
def robots(self) -> list[SerialPortInfo]: ...
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Duck-type protocol for serial/network port scanners. Call `find_robots()` to refresh the device list, then read `robots` for the current results.
|
|
228
|
+
|
|
229
|
+
### `CatalogRobotFactory`
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
class CatalogRobotFactory(Protocol):
|
|
233
|
+
async def find_port(self, port_info: SerialPortInfo) -> str | None: ...
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Factory protocol passed to your `robot_builder` callable. Use `find_port(SerialPortInfo(...))` to resolve a connection by serial number and/or configured connection string. Calibration data is now embedded in the robot payload model and does not require a factory method.
|
|
237
|
+
|
|
238
|
+
### `SerialPortInfo`
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
class SerialPortInfo(BaseModel):
|
|
242
|
+
connection_string: str | None
|
|
243
|
+
serial_number: str | None
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Describes a discovered serial or network connection.
|
|
247
|
+
|
|
248
|
+
### `PayloadContainer` / `CatalogRobot`
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
class PayloadContainer(Protocol[_PayloadT]):
|
|
252
|
+
payload: _PayloadT
|
|
253
|
+
|
|
254
|
+
class CatalogRobot(PayloadContainer[_PayloadT], Protocol[_PayloadT]):
|
|
255
|
+
type: str
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Protocols for the robot descriptor passed to `robot_builder`. The `payload` is an instance of your `robot_payload` model.
|
|
259
|
+
|
|
260
|
+
### `BuildRobotCallable`
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
BuildRobotCallable = Callable[[_RobotT, _FactoryT], Awaitable[PhysicalAIRobot]]
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Type alias for the `robot_builder` callable signature.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Entry Point Registration
|
|
271
|
+
|
|
272
|
+
Studio discovers plugins via Python [entry points](https://packaging.python.org/en/latest/specifications/entry-points/) in the group `physicalai.studio.catalog_plugins`.
|
|
273
|
+
|
|
274
|
+
In your `pyproject.toml`:
|
|
275
|
+
|
|
276
|
+
```toml
|
|
277
|
+
[project.entry-points."physicalai.studio.catalog_plugins"]
|
|
278
|
+
my-robot = "physicalai_my_robot_plugin.studio_catalog:register_physicalai_studio_plugin"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The callable must accept a single argument — the registry — and call `registry.register_robot(definition)` for each robot type:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
def register_physicalai_studio_plugin(registry: Any) -> None:
|
|
285
|
+
for definition in _definitions():
|
|
286
|
+
registry.register_robot(definition)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Studio calls all discovered entry points at startup. Duplicate `type` values raise a `ValueError`.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Robot Builder Pattern
|
|
294
|
+
|
|
295
|
+
The `robot_builder` is an async function that receives a robot descriptor and a factory, performs connection setup, and returns a `PhysicalAIRobot`:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from physicalai_studio_plugin import CatalogRobot
|
|
299
|
+
|
|
300
|
+
async def _build_my_robot(
|
|
301
|
+
robot: CatalogRobot[MyRobotPayload],
|
|
302
|
+
factory: CatalogRobotFactory,
|
|
303
|
+
) -> PhysicalAIRobot:
|
|
304
|
+
# `robot.payload` is already a validated MyRobotPayload instance.
|
|
305
|
+
# 1. Resolve the connection
|
|
306
|
+
port = await factory.find_port(
|
|
307
|
+
SerialPortInfo(
|
|
308
|
+
connection_string=robot.payload.connection_string or None,
|
|
309
|
+
serial_number=robot.payload.serial_number or None,
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
if port is None:
|
|
313
|
+
msg = f"Robot not found: {robot.payload.serial_number}"
|
|
314
|
+
raise RuntimeError(msg)
|
|
315
|
+
|
|
316
|
+
# 2. Return the driver
|
|
317
|
+
return MyRobotDriver(port=port, ...)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Testing
|
|
323
|
+
|
|
324
|
+
Create a minimal test file alongside your plugin:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from __future__ import annotations
|
|
328
|
+
|
|
329
|
+
from physicalai_studio_plugin import RobotCatalogDefinition, SerialPortInfo
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _fake_registry():
|
|
333
|
+
class _FakeRegistry:
|
|
334
|
+
def __init__(self):
|
|
335
|
+
self.definitions: list[RobotCatalogDefinition] = []
|
|
336
|
+
|
|
337
|
+
def register_robot(self, definition: RobotCatalogDefinition) -> None:
|
|
338
|
+
self.definitions.append(definition)
|
|
339
|
+
|
|
340
|
+
return _FakeRegistry()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_plugin_registration():
|
|
344
|
+
from physicalai_my_robot_plugin.studio_catalog import (
|
|
345
|
+
register_physicalai_studio_plugin,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
registry = _fake_registry()
|
|
349
|
+
register_physicalai_studio_plugin(registry)
|
|
350
|
+
assert len(registry.definitions) == 1
|
|
351
|
+
assert registry.definitions[0].type == "MyRobot_Follower"
|
|
352
|
+
```
|