Microduck¶
A Microduck walks on intents; no per-joint write, and the same walking policy as in simulation.
At the end of this page a Microduck walks on intents from Robot("microduck", mode="real"), and you know why it has no per-joint write and that its walking policy is the one you run in simulation.
This needs a duck on the network running robotd. No port is needed for the common case:
from strands_robots import Robot
duck = Robot("microduck", mode="real") # finds robotd via $MICRODUCK_SOCKET, $MICRODUCK_HOST, /run/robotd.sock
duck = Robot("microduck", mode="real", port="ssh://radxa@duck.local") # explicit ssh forward of the socket
print(duck.connect_eagerly())
duck.send_action({"vx": 0.1, "vyaw": 0.0}) # a twist intent, m/s and rad/s
How the socket is found¶
| source | meaning |
|---|---|
$MICRODUCK_SOCKET |
a local unix socket path, one you forwarded yourself |
$MICRODUCK_HOST |
[user@]host; the driver runs ssh -N -L and forwards robotd's socket. User defaults to radxa; $DUCK_BOARD_USER is honoured as duckctl does |
/run/robotd.sock |
the answer when this code runs on the duck |
port="ssh://[user@]host" |
the same forward, named explicitly |
Intents, not joints¶
robotd owns the 50 Hz control loop and runs the walking and skill policy on board. Its JSON-RPC 2.0 surface (duck-ipc-proto, NDJSON over the socket) has no per-joint write. The whole robot.* surface is intent-level, so mode="real" is delegate-only by the robot's design:
| intent | wire | kind |
|---|---|---|
robot.move (twist), robot.head, robot.pose, robot.mouth |
notification, no reply | continuous |
robot.do (skills), robot.enable, robot.relax, robot.init, robot.stop |
request, reply awaited | discrete |
robot.state |
request | read |
send_action accepts vx, vy, vyaw (twist), neck_pitch, head_pitch, head_yaw, head_roll (radians), z, roll, pitch, active (standing pose), open (mouth, 0 to 1) and skill (one of SKILLS, sent as robot.do). run_policy and start_task refuse and name the intent path; the wire has no method for 14 joint targets.
read_state publishes 14 joints, the locomotion set the policy speaks. robotd's own JOINT_NAMES is 15 wide with mouth at index 9; the driver drops it, and the mouth travels through robot.mouth.
Sim to real¶
The on-robot policy is the same alpha_walking.onnx the [microduck] extra runs in MuJoCo (byte-compatible, difference 0.0), so a sim rollout with equal observations predicts the hardware. Weights come from the Hub at pollen-robotics/microduck-policies:
pip install 'strands-robots[microduck,sim-mujoco]' # onnxruntime + huggingface_hub for the policy, MuJoCo for the twin
sim = Robot("microduck")
sim.run_policy(robot_name="microduck", policy_provider="microduck", policy_config={"onnx_path": "alpha_walking.onnx"}) # self-configures from the ONNX metadata
Camera¶
The driver reads the duck's camera through robotd and returns one frame as an image the agent can see; cv2 and numpy load on that call only.
Safety¶
The duck is velocity-commanded: it walks until the twist is zeroed or robot.stop is sent. stop() cancels any held move and sends robot.stop; cleanup() closes the socket and ssh forward. Whether robotd times out a twist when intents stop is the daemon's call, not this driver's.