Skip to content

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.

Edit page