quackd-microduck 0.10.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.
- quackd_microduck-0.10.0/.gitignore +41 -0
- quackd_microduck-0.10.0/PKG-INFO +33 -0
- quackd_microduck-0.10.0/README.md +14 -0
- quackd_microduck-0.10.0/pyproject.toml +42 -0
- quackd_microduck-0.10.0/src/quackd_microduck/__init__.py +311 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/__init__.py +18 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/assets.py +327 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/gait.py +92 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/live.py +44 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/microduck.py +389 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/render.py +126 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/scene.py +175 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/upstream_api.py +333 -0
- quackd_microduck-0.10.0/src/quackd_microduck/sim3d/world.py +630 -0
- quackd_microduck-0.10.0/src/quackd_microduck/transports/__init__.py +8 -0
- quackd_microduck-0.10.0/src/quackd_microduck/transports/factory.py +60 -0
- quackd_microduck-0.10.0/src/quackd_microduck/transports/jsonrpc_unix.py +565 -0
- quackd_microduck-0.10.0/src/quackd_microduck/transports/mujoco.py +255 -0
- quackd_microduck-0.10.0/src/quackd_microduck/transports/websocket_stub.py +70 -0
- quackd_microduck-0.10.0/src/quackd_microduck/upstream_api.py +279 -0
- quackd_microduck-0.10.0/src/quackd_microduck/verbs.py +256 -0
- quackd_microduck-0.10.0/src/quackd_microduck/webrtc.py +306 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# secrets
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
|
|
6
|
+
# runs are artifacts, not source (the hero GIF lives in docs/assets on purpose)
|
|
7
|
+
runs/
|
|
8
|
+
|
|
9
|
+
# MuJoCo writes this into the working directory, with no way to redirect it, the first time
|
|
10
|
+
# the physics goes non-finite. `test_a_world_that_mujoco_has_reset_under_us_refuses_to_carry_on`
|
|
11
|
+
# makes that happen on purpose, so every test run produces one.
|
|
12
|
+
MUJOCO_LOG.TXT
|
|
13
|
+
|
|
14
|
+
# python
|
|
15
|
+
__pycache__/
|
|
16
|
+
*.py[cod]
|
|
17
|
+
*.egg-info/
|
|
18
|
+
build/
|
|
19
|
+
dist/
|
|
20
|
+
.venv/
|
|
21
|
+
.venv*/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.coverage
|
|
26
|
+
htmlcov/
|
|
27
|
+
|
|
28
|
+
# editors / os
|
|
29
|
+
.vscode/
|
|
30
|
+
.idea/
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
|
|
34
|
+
# upstream assets, never vendored (docs/licenses.md). A real microduck:mujoco run leaves
|
|
35
|
+
# upstream's CC BY-SA-NC model in ~/.quackd/cache, and a debugging copy next to the checkout
|
|
36
|
+
# is one `git add -A` from being permanent in a public history.
|
|
37
|
+
*.onnx
|
|
38
|
+
*.pt
|
|
39
|
+
*.stl
|
|
40
|
+
robot_walk.xml
|
|
41
|
+
*.stackdump
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: quackd-microduck
|
|
3
|
+
Version: 0.10.0
|
|
4
|
+
Summary: The Microduck adapter for quackd, with its simulators. Install it as quackd[microduck].
|
|
5
|
+
Project-URL: Homepage, https://github.com/rokbenko/quackd
|
|
6
|
+
Project-URL: Documentation, https://github.com/rokbenko/quackd/blob/main/docs/adapter-status.md
|
|
7
|
+
Author: Rok Benko
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: quackd<0.11,>=0.10
|
|
11
|
+
Provides-Extra: camera
|
|
12
|
+
Requires-Dist: aiortc>=1.9; extra == 'camera'
|
|
13
|
+
Requires-Dist: av>=12; extra == 'camera'
|
|
14
|
+
Requires-Dist: websockets>=13; extra == 'camera'
|
|
15
|
+
Provides-Extra: mujoco
|
|
16
|
+
Requires-Dist: mujoco<4,>=3.12; extra == 'mujoco'
|
|
17
|
+
Requires-Dist: onnxruntime>=1.20; extra == 'mujoco'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# quackd-microduck
|
|
21
|
+
|
|
22
|
+
The [Microduck](https://github.com/pollen-robotics/microduck) adapter for
|
|
23
|
+
[quackd](https://github.com/rokbenko/quackd), and the two simulators that come with it: the
|
|
24
|
+
2D cartoon arena and the MuJoCo physics backend. Install it through quackd:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv pip install "quackd[microduck]" # the cartoon, the mock and the real duck
|
|
28
|
+
uv pip install "quackd[mujoco]" # and the physics simulator
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
No Microduck has run quackd. Every name the `jsonrpc` backend relies on is read from upstream
|
|
32
|
+
source at a pinned commit and listed in `upstream_api.py`. What it does and what it refuses:
|
|
33
|
+
[docs/adapter-status.md](https://github.com/rokbenko/quackd/blob/main/docs/adapter-status.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# quackd-microduck
|
|
2
|
+
|
|
3
|
+
The [Microduck](https://github.com/pollen-robotics/microduck) adapter for
|
|
4
|
+
[quackd](https://github.com/rokbenko/quackd), and the two simulators that come with it: the
|
|
5
|
+
2D cartoon arena and the MuJoCo physics backend. Install it through quackd:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv pip install "quackd[microduck]" # the cartoon, the mock and the real duck
|
|
9
|
+
uv pip install "quackd[mujoco]" # and the physics simulator
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
No Microduck has run quackd. Every name the `jsonrpc` backend relies on is read from upstream
|
|
13
|
+
source at a pinned commit and listed in `upstream_api.py`. What it does and what it refuses:
|
|
14
|
+
[docs/adapter-status.md](https://github.com/rokbenko/quackd/blob/main/docs/adapter-status.md).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "quackd-microduck"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "The Microduck adapter for quackd, with its simulators. Install it as quackd[microduck]."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Rok Benko" }]
|
|
13
|
+
dependencies = ["quackd>=0.10,<0.11"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
# The physics simulator: MuJoCo on the CPU, no GPU, both imported only inside connect().
|
|
17
|
+
# onnxruntime is what runs the robot's own walking policy; the meshes and that policy are
|
|
18
|
+
# fetched at run time and never shipped here (docs/licenses.md).
|
|
19
|
+
mujoco = ["mujoco>=3.12,<4", "onnxruntime>=1.20"]
|
|
20
|
+
# The duck's camera reaches clients only as an H.264 WebRTC track from mediad, so a picture
|
|
21
|
+
# means being a WebRTC peer. Heavy, and needed by nobody who is not pointing quackd at a
|
|
22
|
+
# real Microduck.
|
|
23
|
+
camera = ["aiortc>=1.9", "av>=12", "websockets>=13"]
|
|
24
|
+
|
|
25
|
+
[project.entry-points."quackd.adapters"]
|
|
26
|
+
microduck = "quackd_microduck"
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/rokbenko/quackd"
|
|
30
|
+
Documentation = "https://github.com/rokbenko/quackd/blob/main/docs/adapter-status.md"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.version]
|
|
33
|
+
path = "src/quackd_microduck/__init__.py"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/quackd_microduck"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["src", "README.md", "pyproject.toml"]
|
|
40
|
+
|
|
41
|
+
[tool.uv.sources]
|
|
42
|
+
quackd = { workspace = true }
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""The Microduck adapter: every Microduck transport, wrapped, plus the manifest.
|
|
2
|
+
|
|
3
|
+
`quackd/transport/*` is untouched and becomes the Microduck backend layer. This adapter
|
|
4
|
+
delegates every call to one of those transports and adds what 0.4 needs: a manifest, the
|
|
5
|
+
named preconditions the manifest references, and the Microduck's own verbs. Wrapping
|
|
6
|
+
rather than moving is how "zero behaviour change" is made mechanically true (ADR-0017).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import AsyncIterator, Callable, Sequence
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
16
|
+
from quackd.adapters.base import RestResult, one_camera_url, refuse_rest_pose
|
|
17
|
+
from quackd.adapters.manifest import (
|
|
18
|
+
Datasheet,
|
|
19
|
+
Figure,
|
|
20
|
+
Frame,
|
|
21
|
+
Health,
|
|
22
|
+
RobotManifest,
|
|
23
|
+
SafetyAuthority,
|
|
24
|
+
Sensor,
|
|
25
|
+
verb_spec,
|
|
26
|
+
)
|
|
27
|
+
from quackd.transport.base import Ack, DuckState, DuckTransport, HeartbeatError, Intent
|
|
28
|
+
from quackd.verbs.core import CORE
|
|
29
|
+
from quackd.verbs.registry import Precondition, Verb
|
|
30
|
+
from quackd_microduck.verbs import MICRODUCK_VERBS, microduck_conditions
|
|
31
|
+
|
|
32
|
+
__version__ = "0.10.0"
|
|
33
|
+
"""Kept in step with quackd's own version by scripts/set_version.py. It lives here
|
|
34
|
+
rather than being read from the core, because this file is all an adapter's sdist
|
|
35
|
+
contains."""
|
|
36
|
+
|
|
37
|
+
BACKENDS = ("sim2d", "mujoco", "mock", "jsonrpc", "websocket")
|
|
38
|
+
|
|
39
|
+
# The 0.3 descriptions of the renamed verbs, so an old duck's tool schemas are byte-identical.
|
|
40
|
+
_MOVE_DESCRIPTION = (
|
|
41
|
+
"Walk with a velocity for a duration. Use small values; the robot is 25 cm tall."
|
|
42
|
+
)
|
|
43
|
+
_GO_TO_DESCRIPTION = (
|
|
44
|
+
"Walk toward a detected target and stop at a distance. Closes the loop on the camera itself."
|
|
45
|
+
)
|
|
46
|
+
_SEARCH_SCAN_DESCRIPTION = "Rotate in steps, looking for a target. Returns where it was seen."
|
|
47
|
+
_APPROACH_AND_DESCRIPTION = "walk_to a target, then run another verb (kick, grab)."
|
|
48
|
+
BLURB = "a small biped duck robot (25 cm, 800 g)"
|
|
49
|
+
|
|
50
|
+
DATASHEET = Datasheet(
|
|
51
|
+
mass_kg=Figure(value=0.8, confidence="official", source="the Pollen Robotics README"),
|
|
52
|
+
height_m=Figure(value=0.25, confidence="official", source="the Pollen Robotics README"),
|
|
53
|
+
dof=Figure(
|
|
54
|
+
value=15,
|
|
55
|
+
confidence="official",
|
|
56
|
+
source="the Pollen Robotics README",
|
|
57
|
+
note="XL330 class servos, which is an estimate",
|
|
58
|
+
),
|
|
59
|
+
manipulator="beak",
|
|
60
|
+
tethered=False,
|
|
61
|
+
terrain="indoor_flat",
|
|
62
|
+
not_rated=["stairs", "steps", "slopes"],
|
|
63
|
+
cannot=[
|
|
64
|
+
"carry, hold or push anything: the beak scoops at the floor and nothing else",
|
|
65
|
+
"climb or descend a step",
|
|
66
|
+
"hold a heading for long without a landmark: the IMU has no magnetometer, so heading "
|
|
67
|
+
"drifts",
|
|
68
|
+
],
|
|
69
|
+
notes=[
|
|
70
|
+
"The speed caps are a software clamp in robotd, not a measured hardware maximum",
|
|
71
|
+
"The time-of-flight sensor is an 8 by 8 grid good from 0.12 to 3.54 m",
|
|
72
|
+
"It falls, and it can get itself upright again",
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def microduck_manifest(
|
|
78
|
+
backend: str, robot_id: str = "microduck", *, camera: bool = True
|
|
79
|
+
) -> RobotManifest:
|
|
80
|
+
"""The Microduck as data. Static: no connection needed (validate, announce, doctor).
|
|
81
|
+
|
|
82
|
+
`camera` is the one thing a description of the robot cannot settle on its own. Upstream has
|
|
83
|
+
no socket-level camera method at all — the camera reaches clients through `mediad`'s WebRTC
|
|
84
|
+
track — so on the jsonrpc backend a frame exists only when `--camera-url` names something
|
|
85
|
+
serving one. Advertising `observe` either way promises the pilot vision it may not have.
|
|
86
|
+
"""
|
|
87
|
+
core = [
|
|
88
|
+
verb_spec(CORE["report_state"], core=True),
|
|
89
|
+
verb_spec(CORE["stop"], core=True),
|
|
90
|
+
verb_spec(MICRODUCK_VERBS["say"], core=True),
|
|
91
|
+
verb_spec(CORE["move"], core=True, description=_MOVE_DESCRIPTION),
|
|
92
|
+
]
|
|
93
|
+
if camera:
|
|
94
|
+
core += [
|
|
95
|
+
verb_spec(CORE["observe"], core=True),
|
|
96
|
+
verb_spec(CORE["go_to"], core=True, description=_GO_TO_DESCRIPTION),
|
|
97
|
+
verb_spec(CORE["search_scan"], core=True, description=_SEARCH_SCAN_DESCRIPTION),
|
|
98
|
+
verb_spec(CORE["approach_and"], core=True, description=_APPROACH_AND_DESCRIPTION),
|
|
99
|
+
]
|
|
100
|
+
extensions = [verb_spec(v, core=False) for n, v in MICRODUCK_VERBS.items() if n != "say"]
|
|
101
|
+
sensors: list[Sensor] = ["battery", "odometry", "imu", "tof"]
|
|
102
|
+
if camera:
|
|
103
|
+
sensors.insert(0, "camera")
|
|
104
|
+
return RobotManifest(
|
|
105
|
+
id=robot_id,
|
|
106
|
+
vendor="pollen-robotics",
|
|
107
|
+
model="microduck",
|
|
108
|
+
embodiment="biped",
|
|
109
|
+
mobility="legged",
|
|
110
|
+
intents=["twist", "skill", "gaze", "sound", "pose"],
|
|
111
|
+
sensors=sensors,
|
|
112
|
+
verbs=core + extensions,
|
|
113
|
+
# exactly the 0.3 attachments: walk/kick/grab need standing, sit/stand/gaze not fallen
|
|
114
|
+
preconditions={
|
|
115
|
+
"move": ["standing"],
|
|
116
|
+
"kick": ["standing"],
|
|
117
|
+
"grab": ["standing"],
|
|
118
|
+
"sit": ["not_fallen"],
|
|
119
|
+
"stand": ["not_fallen"],
|
|
120
|
+
"gaze": ["not_fallen"],
|
|
121
|
+
},
|
|
122
|
+
safety_authority=SafetyAuthority(native="robotd_deadman", deadman=True, heartbeat_hz=2.0),
|
|
123
|
+
frame=Frame(reference="body"),
|
|
124
|
+
limits={"max_vx": 0.3, "max_vy": 0.2, "max_wz": 1.5},
|
|
125
|
+
backend=backend,
|
|
126
|
+
blurb=BLURB,
|
|
127
|
+
datasheet=DATASHEET,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class MicroduckAdapter:
|
|
132
|
+
"""A `RobotAdapter` over any of the Microduck transports."""
|
|
133
|
+
|
|
134
|
+
name = "microduck"
|
|
135
|
+
|
|
136
|
+
def __init__(self, transport: DuckTransport, *, robot_id: str = "microduck") -> None:
|
|
137
|
+
self.transport = transport
|
|
138
|
+
self.backend = transport.name
|
|
139
|
+
self.robot_id = robot_id
|
|
140
|
+
self.manifest: RobotManifest | None = None
|
|
141
|
+
|
|
142
|
+
# ── protocol ────────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
async def connect(self) -> RobotManifest:
|
|
145
|
+
await self.transport.connect()
|
|
146
|
+
# sim and mock always have a camera. A real duck has one only if a frame actually
|
|
147
|
+
# arrived: a URL that was typed but never answered is not a camera, and advertising
|
|
148
|
+
# `observe` on the strength of one is how a pilot gets told it can see.
|
|
149
|
+
if hasattr(self.transport, "camera_working"):
|
|
150
|
+
camera = bool(self.transport.camera_working)
|
|
151
|
+
else:
|
|
152
|
+
camera = getattr(self.transport, "camera_url", True) is not None
|
|
153
|
+
self.manifest = microduck_manifest(self.backend, self.robot_id, camera=camera)
|
|
154
|
+
return self.manifest
|
|
155
|
+
|
|
156
|
+
async def disconnect(self) -> None:
|
|
157
|
+
await self.transport.close()
|
|
158
|
+
|
|
159
|
+
async def close(self) -> None:
|
|
160
|
+
await self.disconnect()
|
|
161
|
+
|
|
162
|
+
async def get_state(self) -> DuckState:
|
|
163
|
+
return await self.transport.get_state()
|
|
164
|
+
|
|
165
|
+
async def get_frame(self) -> Image.Image | None:
|
|
166
|
+
return await self.transport.get_frame()
|
|
167
|
+
|
|
168
|
+
async def send_intent(self, intent: Intent) -> Ack:
|
|
169
|
+
return await self.transport.send_intent(intent)
|
|
170
|
+
|
|
171
|
+
async def health(self) -> Health:
|
|
172
|
+
try:
|
|
173
|
+
await self.transport.heartbeat()
|
|
174
|
+
except HeartbeatError as e:
|
|
175
|
+
return Health(ok=False, reason=str(e))
|
|
176
|
+
state = await self.transport.get_state()
|
|
177
|
+
return Health(
|
|
178
|
+
ok=True,
|
|
179
|
+
battery_percent=state.battery_percent,
|
|
180
|
+
extras={"policy": state.policy, "posture": state.posture},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
async def heartbeat(self) -> None:
|
|
184
|
+
await self.transport.heartbeat()
|
|
185
|
+
|
|
186
|
+
async def stop(self) -> None:
|
|
187
|
+
await self.transport.stop()
|
|
188
|
+
|
|
189
|
+
async def go_to_rest(self) -> RestResult:
|
|
190
|
+
"""A rest pose is a joint-angle map for an arm, and this body takes postures through
|
|
191
|
+
its own verbs (`sit`, `stand`), so there is nothing here to drive it to."""
|
|
192
|
+
return RestResult.none()
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def stop_error(self) -> str | None:
|
|
196
|
+
"""`jsonrpc` has recorded this since 0.6, but nothing could read it: `stop` in
|
|
197
|
+
`verbs/core.py` looks for it on the object it was handed, which is this adapter."""
|
|
198
|
+
return getattr(self.transport, "stop_error", None)
|
|
199
|
+
|
|
200
|
+
def subscribe(self, topic: str) -> AsyncIterator[dict[str, Any]]:
|
|
201
|
+
return self.transport.subscribe(topic)
|
|
202
|
+
|
|
203
|
+
def now(self) -> float:
|
|
204
|
+
return self.transport.now()
|
|
205
|
+
|
|
206
|
+
async def sleep(self, seconds: float) -> None:
|
|
207
|
+
await self.transport.sleep(seconds)
|
|
208
|
+
|
|
209
|
+
def preconditions(self) -> dict[str, Precondition]:
|
|
210
|
+
return microduck_conditions()
|
|
211
|
+
|
|
212
|
+
def implementations(self) -> dict[str, Verb]:
|
|
213
|
+
return dict(MICRODUCK_VERBS)
|
|
214
|
+
|
|
215
|
+
# ── sim-only passthroughs the flock and the recorder use today ──────────────────
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def world(self) -> Any:
|
|
219
|
+
return self.transport.world # type: ignore[attr-defined]
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def clock(self) -> Any:
|
|
223
|
+
return self.transport.clock # type: ignore[attr-defined]
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def duck_index(self) -> int:
|
|
227
|
+
return int(self.transport.duck_index) # type: ignore[attr-defined]
|
|
228
|
+
|
|
229
|
+
def add_tick_hook(self, hook: Callable[[Any], None]) -> None:
|
|
230
|
+
self.transport.add_tick_hook(hook) # type: ignore[attr-defined]
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def post_sleep(self) -> Callable[[], None] | None:
|
|
234
|
+
return getattr(self.transport, "post_sleep", None)
|
|
235
|
+
|
|
236
|
+
@post_sleep.setter
|
|
237
|
+
def post_sleep(self, hook: Callable[[], None] | None) -> None:
|
|
238
|
+
self.transport.post_sleep = hook # type: ignore[attr-defined]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ── what the factory calls (the same four names on every adapter package) ───────────────
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def describe(backend: str, robot_id: str | None = None) -> RobotManifest:
|
|
245
|
+
return microduck_manifest(backend, robot_id or "microduck")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def implementations() -> dict[str, Verb]:
|
|
249
|
+
return dict(MICRODUCK_VERBS)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def conditions() -> dict[str, Precondition]:
|
|
253
|
+
return microduck_conditions()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def make(
|
|
257
|
+
backend: str,
|
|
258
|
+
*,
|
|
259
|
+
robot_id: str | None = None,
|
|
260
|
+
seed: int | None = None,
|
|
261
|
+
address: str | None = None,
|
|
262
|
+
live: bool = False,
|
|
263
|
+
camera_url: str | Sequence[str] | None = None,
|
|
264
|
+
token: str | None = None,
|
|
265
|
+
rest_pose: dict[str, float] | None = None,
|
|
266
|
+
) -> MicroduckAdapter:
|
|
267
|
+
refuse_rest_pose("microduck", rest_pose)
|
|
268
|
+
url = one_camera_url(camera_url, spec=f"microduck:{backend}")
|
|
269
|
+
# `token` is accepted and unused, deliberately: the factory calls every adapter's `make`
|
|
270
|
+
# with the same four keywords, and this robot has nothing to authenticate to. `robotd`'s
|
|
271
|
+
# socket has no auth at all — access is filesystem permissions on /run/robotd.sock — and
|
|
272
|
+
# mediad's own note is that a pairing PIN which is 000000 on every robot "authenticates
|
|
273
|
+
# nobody". Reach both over ssh rather than trusting the network.
|
|
274
|
+
from quackd_microduck.transports.factory import make_transport
|
|
275
|
+
|
|
276
|
+
transport = make_transport(backend, seed=seed, address=address, live=live, camera_url=url)
|
|
277
|
+
return MicroduckAdapter(transport, robot_id=robot_id or "microduck")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
__all__ = [
|
|
281
|
+
"BACKENDS",
|
|
282
|
+
"MICRODUCK_VERBS",
|
|
283
|
+
"MicroduckAdapter",
|
|
284
|
+
"conditions",
|
|
285
|
+
"describe",
|
|
286
|
+
"implementations",
|
|
287
|
+
"make",
|
|
288
|
+
"microduck_conditions",
|
|
289
|
+
"microduck_manifest",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# What this adapter reads from upstream, for `quackd doctor`. Two rows: the robot's own IPC
|
|
294
|
+
# protocol, and the model and walking policy the physics backend fetches at run time and never
|
|
295
|
+
# ships. Declared here rather than in a table in the core (ADR-0022).
|
|
296
|
+
def _upstream_rows() -> tuple[tuple[str, object, str, str], ...]:
|
|
297
|
+
from quackd_microduck import upstream_api as robotd
|
|
298
|
+
from quackd_microduck.sim3d import upstream_api as rl
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
("microduck", robotd, "docs/adapter-status.md", "a robotd (the jsonrpc backend)"),
|
|
302
|
+
(
|
|
303
|
+
"microduck_rl",
|
|
304
|
+
rl,
|
|
305
|
+
"docs/adr/0030-mujoco-physics-backend.md",
|
|
306
|
+
"a robot: the model and the policies are fetched at run time and never shipped",
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
UPSTREAMS = _upstream_rows()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The physics simulator: a MuJoCo arena the same shape as the cartoon's.
|
|
2
|
+
|
|
3
|
+
`sim2d` exists to test the agent loop, and says so; it will never tell you whether a gait
|
|
4
|
+
works, because it has no joints. This package is where a robot's own controllers run for
|
|
5
|
+
real: a rigid body world, a camera that renders what the head would see, and, once a body
|
|
6
|
+
brings one, the walking policy the robot ships with. The arena, the ball, the kick cone, the
|
|
7
|
+
deadman and the seeded spawn order are the cartoon's, deliberately, so a `.duck` that asks
|
|
8
|
+
for those runs unchanged against either and a seed puts the duck and the ball in the same
|
|
9
|
+
place in both. The one thing not carried over is the cartoon's person marker: nobody stands
|
|
10
|
+
in this arena, so a `.duck` about a person — `follow-me` — is a 2D task.
|
|
11
|
+
|
|
12
|
+
Nearly everything here imports `mujoco`, which is an optional extra (`quackd[mujoco]`):
|
|
13
|
+
nothing on the default path imports this package, and the transport that uses it imports it
|
|
14
|
+
inside `connect()` so `--robot microduck:mujoco` fails with the extra's name rather than a
|
|
15
|
+
stack. The exception is `gait.py`, which is deliberately pure arithmetic over floats, so the
|
|
16
|
+
rule that decides whether a duck moves or only reports moving is tested on every runner
|
|
17
|
+
rather than only where the extra and a filled asset cache happen to meet.
|
|
18
|
+
"""
|