Pollen Microduck¶
A 25 cm, ~800 g walking biped. Fifteen servos, a camera, a time-of-flight depth
sensor, two IMUs and an articulated beak. It does not walk by inverse
kinematics — motion comes from RL policies trained with PPO in MuJoCo,
exported to ONNX, and executed by the on-board robotd daemon at 50 Hz.
OpenCastor drives it as an intent client over robotd's JSON-RPC 2.0 NDJSON
socket — the same contract used by robotctl, the gamepad daemon and Pollen's
own phone app.
robotd owns the motor bus
robotd enforces its own limits, fall detection and battery cutoff. OpenCastor sends intents and never raw motor writes. Do not bypass the driver to write the servo bus while robotd is running.
Setup¶
That finds the duck, verifies it answers, and writes a ready config.
castor duck --deep # also sweep the ARP neighbour table
castor duck --host 192.168.1.42 # skip discovery
castor duck --start # configure, then run it
castor duck find # just list candidates
castor duck health # live loop rate + battery
castor duck test # stand up and walk (asks first)
Run it off-board¶
The duck has 1 GB of RAM and a 50 Hz control loop to protect, so the
recommended topology is OpenCastor on another machine, with the driver
opening an ssh -L 7788:/run/robotd.sock forward. On-board works — set
transport: unix — but expect contention with robotd, mediad and the policy
across four Cortex-A55 cores.
- Find its address with
duckctl ipover Bluetooth, or your router's DHCP table. mDNS on the stock image is unreliable — don't count on it. ssh-copy-idyour key.- Put the login user in the
robotgroup so it can open the robotd socket:sudo usermod -aG robot "$USER".
Bring-up¶
robot.init torques the servos on and ramps to standing over about two
seconds. The driver does not call it automatically (auto_init: false):
the duck deliberately does not move when a process starts.
stop() and relax() are not the same
stop() holds it standing. relax() cuts torque and it will drop —
seat it first.
Hardware¶
| Compute | Rockchip RK3566 — quad Cortex-A55, 1 GB RAM, 32 GB storage, NPU |
| OS | Armbian (Radxa Zero 3 profile) |
| Actuation | 15 Dynamixel-protocol servos on /dev/ttyS2 @ 1 Mbps (14 policy-driven) |
| Sensing | Camera, ToF depth, two IMUs |
| Power | Removable NP-F550, ~1 h. Empty floor 6.6 V |
Choreography¶
Pollen ships walking, standing, two kicks, a ground pick, a sit toggle, a
forward roll and seven sounds. Each is a one-shot that holds the whole robot
while it runs — robot.do takes exactly one skill, and a refusal names the
move already in possession.
There is no sequencing, no branching on what the duck can see, and no way to say "walk to the ball, line up, knock it toward the couch, then celebrate." Every one of those verbs exists. The sentence does not.
That sentence is what OpenCastor supplies. Thirteen primitives carry the two facts a planner cannot work without — how long a move occupies the duck, and whether it holds the robot exclusively — and thirteen routines compose them:
| Routine | What it does |
|---|---|
approach |
Walk forward a distance in metres, at a careful pace |
back_off |
Walk backward a distance in metres |
turn_by |
Turn in place by an angle in degrees. Positive is left |
scan |
Look left, then right, then back to centre |
nod / shake |
Yes, and no |
greet |
Look up at someone, say hello, and nod |
fetch |
Go to something, pick it up in the beak, and bring it back |
nudge |
Line up on something on the floor and knock it forward with a kick |
patrol |
Walk a square, looking around at each corner |
celebrate |
A quack, a roll and a whoop. Needs clear space ahead |
dance |
A little dance — lean, sway, quack |
settle |
Wind down: sit, say goodnight, and go quiet |
fetch is one word that becomes ten primitives: look down, walk, stop, pick,
turn, stop, walk, stop, open the beak, quack. Every routine is written as a
plan a user could have typed, so nothing is hidden in code you could not have
asked for yourself.
A plan is a proposal
The choreographer computes no permission. Every motion goes out through
the driver and the SafetyLayer, the duck's own limits apply on top and come
back in robot.state.limited_by, and steps abort on a fall or a flat
battery. robotd remains the only thing that decides what a servo does.
Config¶
The packaged profile is pollen/microduck (preset #16). The shipped envelope:
drivers:
- id: duck
protocol: microduck
transport: ssh # or unix, to run on-board
socket: /run/robotd.sock
local_port: 7788
max_vx: 0.2 # m/s forward
max_vy: 0.1 # m/s strafe
max_vyaw: 1.0 # rad/s yaw
intent_hz: 20
command_ttl_s: 1.5
auto_init: false # the duck does not move on process start
safety:
estop_on_startup: true
max_linear_speed_mps: 0.2
max_angular_speed_radps: 1.0
local_safety_wins: true
watchdog:
timeout_s: 2
local_safety_wins is the important line: the duck's own limits are not
advisory, and anything OpenCastor asks for outside them comes back reported
rather than obeyed.
The same duck, in Swift¶
The brain also exists as a standalone Swift package, DuckKit, so a phone can run the real trained policy with no robot in the room — which makes an AR ghost duck the trained network walking rather than an animation of walking.
It has zero dependencies: a hand-written ONNX reader and an ELU multilayer
perceptron in Foundation and arithmetic, which is what lets the real
alpha_walking.onnx run under swift test on a Raspberry Pi and produce the
same floats an iPhone will. The joint order, home pose, action scaling and
filter coefficients are the same ported numbers the driver uses, and the
kinematic chain is the upstream MuJoCo model vendored as a fixture — so the
tables cannot drift from upstream without a test going red. The forward pass is
proved against onnxruntime's own output to 1e-4.
A second product, DuckEvidence, adds swift-crypto for the things that sign.
Provenance¶
The Microduck is made by Pollen Robotics and ships to customers around Christmas 2026. Contract facts — the observation layout, the joint tables and the control tuning — come from that repository, which is Apache-2.0. OpenCastor is not affiliated with Pollen Robotics.