An open, buildable bipedal robot duck. Assembly drawings, exploded views, CAD-importable assemblies, a printable parts list, a bill of materials and a complete electronics teardown, all recovered from publicly released simulation files and source code.
Macrodork is a 25 cm, 737 g bipedal robot duck driven by 15 Dynamixel XL330 servos (14 under policy control) that learns to walk with reinforcement learning. Its geometry, kinematics and electronics follow an upstream commercial robot whose simulation model and runtime are open source. What this repository adds is everything needed to actually build one, which upstream never published: assembly relationships, part quantities, the fastener system, a bill of materials, the reconstructed IMU-board protocol and sourcing lists.
Provenance. Which upstream material this is derived from, what is and is not published upstream, and the licence terms that follow: Upstream Provenance and NOTICE.md. Macrodork is an independent project, not affiliated with or endorsed by the upstream vendor.
Two public artifacts turn out to be enough:
- The upstream simulation model ships the full MJCF and 47 STL meshes. The MJCF contains the complete kinematic tree - which part mounts to which, relative positions accurate to 0.1 mm, joint axes, travel limits, masses and inertia tensors.
- The upstream runtime is open source, and a runtime that drives real hardware must hard-code device paths, IΒ²C addresses, register offsets, baud rates and protocols. The code is the datasheet.
That information is enough to recover the assembly. This repository is what falls out of reading both.
β Independently verified. On 2026-08-31, @tspy published a hardware teardown on X (169 likes) that matches this repository's conclusions exactly - including the critical one: the main board is a Radxa Zero 3W. Two independent paths, one answer. See Community Intelligence.
Someone is actually building this. The head shell, trunk shell, leg structure and feet are printed, and the M2 screws go into the leg parts - the conclusion in Fastener Reconstruction holds on physical hardware.
The rest of this repository is analysis on paper, recovered from the public MJCF and source. The Build Log records the hands-on side - print settings, assembly problems, and whether the derived numbers hold up on real parts.
This repository states repeatedly that simulation STLs are not manufacturing files. The build log is the test of that claim. The result gets recorded either way.
β Build Log
Seven drawings under assembly-drawings/:
| File | Contents |
|---|---|
01_front 02_side 03_back 04_three_quarter |
Front / side / rear / isometric, assembled, natural colors |
05_exploded_side |
15 parts exploded along the kinematic chain, labeled with names and masses |
06_exploded_three_quarter |
Isometric - shows the left/right leg mirroring clearly |
07_color_coded_assembled |
Assembled state in the same color coding as the exploded views, for cross-reference |
Trunk 199 g
ββ L hip yawβroll 23 g β L hip roll 6 g β L thigh 48 g β L shin 22 g β L ankle+foot 30 g
ββ Neck base 37 g β Neck pitch 6 g β Head yaw/roll 49 g β Head assembly + beak 189 g
ββ R hip yawβroll 23 g β R hip roll 6 g β R thigh 48 g β R shin 22 g β R ankle+foot 30 g
Total 737.2 g Envelope 144 Γ 141 Γ 264 mm
The trunk and the head weigh almost the same (199 g vs 189 g) - the head is a quarter of the whole robot and the center of mass sits high, which explains why its walking policy is hard to train.
Five DoF per leg, four for neck and head - 14 under policy control. The robot actually carries 15 Dynamixel XL330: the 15th drives the beak / lower jaw through a passive linkage and never enters the action space.
| Joint | Travel |
|---|---|
hip_yaw |
β25Β° β¦ +30Β° |
hip_roll |
Β±22Β° |
hip_pitch / knee / ankle / head_pitch |
Β±90Β° |
neck_pitch |
β90Β° β¦ +60Β° |
head_yaw |
Β±170Β° |
head_roll |
Β±25Β° |
What to buy and how many - BOM.md
15 servos, 14 bearings, ~325 fasteners, 2 boards to fabricate. Quantities are counted from geom references in the upstream MJCF (38 mesh types / 75 instances), not estimated. Includes a per-board parts list with LCSC numbers.
β οΈ Two corrections in there that stop you buying the wrong thing: the battery is an NP-F550, not an F970, and the XL330 is run over-voltage.
Every individual STL, split into print-these and buy-these, with Macrodork part names -
print/
| Directory | Count |
|---|---|
print/printed-parts/ |
30 types / 41 pieces of structural parts |
print/standard-parts-not-printed/ |
8 bought-part models (for fit checking) |
print/variant-roller-skate/ |
5 types / 15 pieces for the roller-skate variant |
The STL files themselves are not stored in this repository. scripts/build_print_tree.py
generates the tree from the upstream mesh release at a pinned commit, renaming each mesh to its
Macrodork name (see "Reproducing This" below). The name mapping, quantity table and printing
notes are in print/README.md; sourcing is in the
Mechanical Sourcing List.
cad/ holds STL files with world transforms already applied - import them and the
robot is assembled. (The 47 upstream STLs are each in their own part coordinate frame;
importing those directly piles every part at the origin.)
00_macrodork_full_assembly.stl- whole robot, single file, 796,792 triangles01β¦15- the 15 rigid bodies, filenames are part namesparts_manifest.json- which upstream source meshes make up each body
Units are millimeters. Opens in FreeCAD, Fusion 360, SolidWorks, Blender, or any slicer.
No CAD installed? tools/stl_viewer.html is a zero-install WebGL viewer - open it in a
browser and drop an STL in.
Physical layout of the five modules. Dashed grey = physical region, solid = module, dashed red = mounted outside the shell.
Orange is the servo bus (top-down), red is battery power (bottom-up).
The one thing people get wrong: the compute board, the HAT and the camera are all in the head - the camera sits ~13 mm from the board centre with no joint between them,
so the MIPI ribbon never crosses the neck. What does cross it is the servo bus and the power line.
Full diagram set with commentary β Β· Download PDF (7 diagrams, A3)
One 1 Mbps TTL serial bus does everything.
Radxa Zero 3W (RK3566) Β· Armbian
βββ UART2 1 Mbps TTL half-duplex ββ 15Γ XL330 + imu_to_dxl (ID 200)
βββ I2C3 400 kHz (pins 3/5) ββββββ AIC3104@0x18 Β· ToF@0x29 Β· BMI088 (unused)
βββ I2S3 12.288 MHz ββββββββββββββ audio
βββ MIPI CSI βββββββββββββββββββββββ IMX219 (I2C@0x10, rotated 90Β°)
βββ Bluetooth ββββββββββββββββββββββ gamepad / phone app
βββ Wi-Fi ββββββββββββββββββββββββββ WebRTC
βββ USB-C ββββββββββββββββββββββββββ power + maskrom
| Main board | Radxa Zero 3W - an off-the-shelf module, not a custom carrier |
| SoC | RK3566, quad Cortex-A55, Mali-G52, 0.8 TOPS NPU. Officially 1 GB RAM / 32 GB eMMC; for a replica we recommend 2G/16G, see Electronics Sourcing List |
| Servo bus | Single-wire half-duplex TTL - not RS-232, not RS-485. Dynamixel Protocol V2 @ 1 Mbps on /dev/ttyS2 |
| Custom board 1 | imu_to_dxl v2 - an LSM6DSV16X that speaks Dynamixel: bus ID 200, register 124, a 12-byte block read in the same sync_read as the servos |
| Custom board 2 | RPI Robot HAT - TLV320AIC3104 @ 0x18, a dormant BMI088, a Stemma header for the ToF. Published upstream (elec_RPI_Robot_HAT) |
| Battery | Sony NP-F550, 2S Li-ion. No fuel gauge, no ADC - pack voltage is read from what the servos report as their own supply |
| Sensors | LSM6DSV16X IMU Β· VL53L5CX/L8CX 8Γ8 ToF Β· IMX219 (Pi Camera v2) |
The imu_to_dxl design is the elegant part: the IMU is not on IΒ²C. It presents itself as a
Dynamixel slave, so orientation arrives in the same bus transaction as the joint states -
no second bus, no host-side sensor fusion (the LSM6DSV16X's on-chip SFLP block emits a game
rotation quaternion and estimates its own gyro bias).
Full detail: Hardware Teardown Β· Hardware Spec Sheet
The whole robot is reproducible; the sticking point is cost, not technology. Current status:
| Status | |
|---|---|
| Part geometry | β 47 STLs |
| Assembly relationships | β 0.1 mm accurate, drawings produced |
| Joint axes / travel | β all 14 |
| Mass / inertia | β all 15 bodies |
| Servo model | β Dynamixel XL330 Γ 15 β docs/actuator-selection.md |
| Bearings | β Γ22Γ16Γ4 and Γ15Γ10Γ3 |
| HAT board | β
Published upstream - KiCad + Gerbers + BOM, order directly β elec_RPI_Robot_HAT |
imu_to_dxl board |
|
| Main board | β Radxa Zero 3W, off the shelf (previously misjudged as a custom carrier) |
| Fastener list | β Reverse-engineered from STL hole features β docs/fastener-reconstruction.md |
| Battery / sensors | β NP-F550 2S, IMX219, VL53L8CX, LSM6DSV16X |
| Cable routing | β Nothing published |
| Control software | β Use the same Radxa Zero 3W and the upstream Rust runtime runs as-is (Apache-2.0); porting is only needed if you change the main board |
| Upstream ONNX policies | β Usable as-is (9 of them) if the hardware stays identical; retraining is only needed if you change the body or electronics |
π° Building one probably costs more than buying the upstream robot - but how much more depends entirely on your channel. Fifteen XL330s run about $359 at ROBOTIS international, $412 at ROBOTIS US, and β¬603β629 in Europe inc-VAT - anywhere from slightly under the upstream retail price to well above it. Add the compute module, battery, two boards to fabricate and filament, and it is certainly more. Full costing: BOM.md.
Skip 100% replication (the imu_to_dxl board and editable mechanical CAD are not published) and go
"copy the mechanics, build your own electronics":
| Approach | |
|---|---|
| Mechanics | Use the STLs and drawings here - geometry copies exactly |
| Servos | XL330 Γ 15, off the shelf |
| Main board | Radxa Zero 3W, off-the-shelf module, same as the original |
| IMU board | Roll your own imu_to_dxl: LSM6DSV16X + a small MCU + half-duplex transceiver. The protocol is fully documented here |
| HAT | Order the upstream Gerbers (4-layer). If you don't need audio recording you can skip it entirely, but the half-duplex direction circuit then needs its own adapter board - see Electronics Sourcing List (section "The Two PCBs") |
| Software | Same main board β the upstream Rust runtime runs unmodified (Apache-2.0) |
| Policies | The nine upstream ONNX policies work; retrain with the upstream RL environment if you change hardware |
The conclusion has been revised from "the mechanics are copyable, the electronics are a wall" to "the whole robot is reproducible" - the main board is an off-the-shelf module, and the custom boards' function and protocol have been fully recovered from source. See docs/hardware-teardown.md.
- Armbian runs a login console on UART2.
serial-getty@ttyS2holds the port -systemctl maskit. Upstream found this withfuser -v /dev/ttyS2. - i2c3 collides with the FUSB302. Using the hardware IΒ²C on header pins 3/5 costs you USB-C PD negotiation (plain 5 V charging still works).
- The NPU ships disabled in Armbian - flash the overlay and reboot to run RKNN models.
| Document | Contents |
|---|---|
| Fastener Reconstruction | Hole-feature scan across 47 STLs β M2 screw system and purchase quantities |
| Actuator Selection | XL330 parameters, BAM M6 config, five calibrated PD sets, backlash modeling; why closed-loop steppers do not work here, what swapping to an STS3215 actually costs (737 g vs 2107 g, measured), and a cross-comparison of same-class servos including a deep assessment of the Unitree S288 |
| Hardware Primer | Board by board - what each of the five modules does, how signals flow within one tick, what changes on the Feetech route, and a closing section on five checks to run before you replicate |
| Hardware Spec Sheet | One-page reference - block diagram, part numbers, bus parameters, build list, pitfalls |
| Hardware Teardown | Full derivation with evidence citations |
| Electronics Sourcing List | Taobao links with verified availability - main board / camera / ToF / power / both PCBs / cabling and the debug adapter, with selection reasoning and pitfalls (2026-09-04 snapshot) |
| Mechanical Sourcing List | Taobao links - bearings / M2 fasteners / heat-set inserts and the insertion tip / thread locker / filament |
| Community Intelligence | X / GitHub signals, independent verification, noise and scam warnings |
| Upstream Provenance | What upstream published, what it did not, baseline commits |
| Upstream Ecosystem | Upstream repos, simulators, policies, datasets and community projects, annotated |
| Progress | Status, decisions, open work |
Headline result: the whole robot is an M2 screw system (Γ2.2 through-holes Γ77 + Γ4.4 counterbores Γ28 + Γ1.6 tap-drill holes Γ20), about 146 through-holes across the structural parts; bearings Γ22Γ16Γ4 and Γ15Γ10Γ3.
# 1. Fetch upstream (nothing from upstream is stored in this repository)
bash scripts/fetch_upstream.sh
# 2. Build the printable-parts tree (pinned upstream commit, Macrodork part names)
python scripts/build_print_tree.py
# 3. Regenerate the drawings
python scripts/render_assembly.py upstream/microduck_rl assembly-drawings
# 4. Re-export the CAD assemblies
python scripts/export_assembly_stl.py upstream/microduck_rl cad
# 5. Re-scan hole features
python scripts/analyze_holes.py upstream/microduck_rl/src/mjlab_microduck/robot/microduck/assetsRequires mujoco, numpy, pillow, scipy. Rendering needs a working OpenGL context.
Scripts and tools are Apache-2.0; drawings, CAD assemblies and documentation are CC BY-NC-SA 4.0. Attribution, upstream sources and the compliance statement: NOTICE.md.

