← PortfolioThe Build

ezer

An Open Duck Mini v2 — an open-source BDX droid, about the height of a house cat, built over a lot of nights.

Its anatomy is fourteen servo joints, a harness of wires threaded one correct path at a time, 3D-printed bone, heat-set inserts soldered into plastic, and the silicon in its head. Where a brainstem would be, a Raspberry Pi: not thinking, exactly — holding balance, sensing which way is down, running the involuntary competences a newborn has before it has anything else.

And it has a mind. Not waiting to be transplanted — already inside, already running. A policy whose weights were shaped by thousands of simulated falls on a machine far from this room, now downloaded into a computer the size of a stick of gum, meeting gravity for the first time.

Tonight, between walk attempts

Where it stands

Phase 2
borrowed brain
14 / 14
motors on one bus
62 s
longest walk so far
1
minus sign

The body is finished and answers to its name, and as of 31 August it walks — wobbling, face-first, for about a minute at a time.

It spent four days unable to stand at all. The diagnosis was wrong five times, and the answer turned out to be that two joints were mounted mirrored, so every command turned them the wrong way. A sign, not a setting — which is why nothing that shifted a number ever helped.

Now: tuning rather than repair. Gain, balance trim, and a battery that finally has real work to pay for.

What the build taught me

Fourteen servos, two 18650 cells, a five-volt regulator, a computer the size of a stick of gum, and about two weeks to a body — then four more days before it could stand up in it. This is what that taught me, written for the version of me who starts this again, and for you, if you're about to.

The thing I actually came away with

I expected to learn robotics. What I got instead was an education in how astonishing it is that anything works at all.

Getting this duck to reliably power on — not walk, not balance, just come up the same way twice — took longer than every software problem in the project combined. Two cells, some basic robotic cabling, one microcontroller. That's nearly the simplest possible system, and it still demanded that mechanical, electrical, and compute all be correct simultaneously, along with a fourth thing: how the duck thinks. A loose battery contact and a bad ground both present as "the software doesn't work."

Several times a year I get into a large metal compartment with jutting sheets of metal, shaped vaguely like a bird, and trust it with my life at 30,000 feet. The landing gear. The wiring harness. Every microcontroller. Every actuator on every aileron. The cockpit. Those aircraft are checked rigorously before each flight — and then they fly again, and again, and again, for years.

I could not get a duck to boot twice in a row.

Miles per touch

Somewhere in the middle of this I started thinking in a metric I haven't seen named anywhere, so I'll name it: miles per touch.

Miles is how far, or how long, a system runs on its own. Touches are every moment a human has to intervene — reseat, resolder, reflash, reboot, reason about. You want to maximize the fraction.

It reframes what engineering maturity actually is. A first build has a miles-per-touch near zero: it runs for ninety seconds and then wants you. An airliner's is enormous, and every point of that ratio was bought by someone, once, finding a failure mode and designing it out permanently.

The measurement is also unforgiving in a way I like. It doesn't care how elegant your architecture is. It asks one question: did it need you?

Almost everything I got right on this build, I got right by reducing touches — verifying EEPROM writes on a fresh connection instead of trusting the report, having the robot record its own state where a dead network couldn't hide it. Almost everything that cost me days, cost me because I answered a question by hand that a machine should have been answering.

When every fix is the same fix

The most useful thing I learned this month isn't really about robots.

For four days the duck couldn't stand, and I had five explanations, every one of them reasonable: the battery was too weak, the charger was in the wrong mode, the servo horns were seated half a turn out, the leg was hitting the body, the servo was mounted backwards in its bracket. Each had a fix. I nearly spent an evening on several of them.

What I eventually noticed was that three of those fixes were the same operation. Reseating a horn, writing an offset into the motor's own firmware, rotating the servo in its mount — all three change which number corresponds to which physical position. They shift. And if every candidate fix is a shift, and none of them work, then the defect isn't a shift.

It was a sign. Two joints were mounted mirrored, so every command turned them the wrong way. You can shift a number as far as you like and never make it negative.

The general form, and the reason I'm writing it down: when your fixes keep failing, check whether they are secretly all the same fix. A whole category of solution that cannot express the defect will fail identically however many times you try it — and each failure looks like bad luck instead of a clue.

The other half of that week was a naming problem. I called it a walking problem for three days; the duck was never walking. I called it a power problem for four; the power was fine. A wrong name is more expensive than a wrong guess, because you stop questioning it. Name the failure after what you measured, not after the script it happened inside.

What the manual doesn't tell you

The instructions cover assembly order. They don't cover craft. These cost me real time:

Wires before screws. Route and seat every wire before the fastener goes in. A screw that has to come back out because a cable is trapped under it is not a small loss — it's threadlock, disassembly, and a scratched printed part.

Prove the connection before the final screw. Once a joint is closed up, a marginal crimp becomes a multi-hour debugging session with the whole leg in the way. Tug it, measure it, then close it.

Measure across your solder joints, not just at the pads. A soldered connection adds resistance. So does a crimp, a connector, a switch. The idealized schematic has none of that, and the gap between the schematic and the bench is exactly where your robot will misbehave. I found a fault only because the voltage across two points that "should" have been identical wasn't.

Tape each servo with its number before you begin. Fourteen identical black motors. By motor nine you will not remember which is which. This was the single most useful instruction anyone gave me and it appears in no documentation anywhere.

Debug outward from power. My first total failure — a completely silent servo bus, every baud rate, every address, raw protocol pings — was one 18650 not touching its contact. I exonerated the software in two minutes and then went looking for a clever cause anyway.

Nine things to look out for

If you're building this, these are the traps, in the order they'll find you:

  1. The board's USB-C is data only. Servos get power from the barrel jack. An unpowered bus is silent, not degraded — it looks exactly like a software problem.
  2. Unconfigured servos all answer to id 1. Put exactly one motor on the bus at a time until each has its own address.
  3. EEPROM writes fail silently. A config tool that reads its own writes back inside the same connection proves nothing. Reconnect and re-read.
  4. USB-C-to-USB-C won't charge the pack. Cheap boards omit the 5.1 kΩ CC pull-downs, so the charger never offers power. USB-A-to-C works.
  5. Never hot-plug the barrel jack. The servo board's bulk capacitors are empty; connecting them to a live pack is a near short. The pack sags (I measured 6.56 V → ~3 V), the regulator drops out, the Pi dies instantly. Connect it with the switch off, then switch on.
  6. Ground topology is not a detail. I had the barrel jack's ground soldered straight to the BMS, bypassing the switch. That's a ground loop: servo return current flowed through the regulator's ground path and corrupted its reference. Everything downstream looked haunted until it was resoldered so both grounds meet at one node.
  7. The undervoltage flag can't see a fast collapse. vcgencmd get_throttled needs the chip still running at a degraded voltage to latch anything. Mine read clean through a failure that killed the Pi outright.
  8. Instrument the SD card before first boot. The boot partition is FAT32 and readable by any computer. Have the robot log its own state there. When it stops answering the network, the network is the last thing you can ask.
  9. A mirrored servo has no calibration fix. If a joint turns the wrong way, no offset corrects it — an offset shifts, and you need a sign. Check that each joint moves the direction the model expects before you calibrate anything, or you'll spend days tuning a coordinate system that is inside out. The symptom is a motor pulling huge current and barely moving, and it reads exactly like a failing battery.

A few moments worth keeping

First reading back from a live motor: FEETECH_STS3215 · 7.3V · 26°C · position 179.91. A thing on my desk answered a question.

All fourteen named in one sitting. A pile of identical black servos became a right leg, a left leg, and a head — entirely because of masking tape and a number written in EEPROM. Nothing about them changed physically. They're just addressable now.

So much heft and bulk behind a robot this small. New respect for good physical design. Good physical design is more impressive to me right now than good software design. Maybe I'm just jaded by software.

responding 14 / 14 — every motor answering on one bus, through the Pi, on a robot I assembled with my hands. Not walking yet. But I asked, and it answered.

The first walk: a wobbling, face-first minute on a policy trained in simulation and dropped in as a single file. Four days earlier it couldn't reach its own standing pose. The problem went from this cannot physically happen to this happens badly — a completely different kind of problem, and the kind you can chip at forever.

Where it stands

Body assembled. Power system proven, including deliberate abuse: abrupt cuts, one of them mid-write, with no filesystem damage. Fourteen motors answering on a single bus through the Pi's own nervous system. And, as of the last night of August, a mind already inside it and running — someone else's for now, trained a few million falls at a time in simulation.

It walks badly. It wobbles, it noses into the floor, it lasts about a minute. Every one of those is a tuning problem rather than a repair, which is the first time on this project that's been true.

Then its own mind. That's the part I actually came for.

The log

Unpolished on purpose. Capture first, shape later — these are the raw notes, straight from the bench.

2026-08-31four days, and it was a minus sign

5
wrong diagnoses
12×
the torque, for 2.2° of travel
62 s
the first walk
1
minus sign

moment. The duck walked. Badly — it wobbled, it drove its face into the floor, it looked less like a robot and more like something being born — but the control loop ran for a full minute at fifty hertz and the machine stayed alive through all of it. For four days it had been dying at four and a half seconds, every single time, before it ever took a step.

The thing that fixed it was a minus sign.

note. Here is the shape of the mistake, and I want it written down plainly because I think it is the most useful thing I have learned on this project so far. The symptom was that the battery collapsed whenever the robot tried to stand. That is true, and it is also completely misleading. Two of the fourteen joints were being commanded to a position they could not physically reach, so they pushed, and kept pushing, and a motor that pushes without moving draws its maximum current. The battery sagged because two servos were grinding against a wall. I spent four days measuring the sag.

note. We were wrong five times before we were right: the battery was too weak, the charger was in the wrong mode, the servo horns were seated half a turn out, the leg was hitting the body, the servo was mounted backwards in its bracket. Every one of those was plausible. Every one had a fix I could have spent an evening on. What they had in common — and I did not see it until very late — was that they were all the same operation. Reseating a horn, writing an offset to the motor's memory, rotating the servo in its mount: all three just change which number corresponds to which position. They shift. None of them can reverse anything.

note. The second thing I pushed on was smaller and I was partly wrong about it, which I think is worth recording honestly. I said that if the brain browned out while the thigh was hanging straight down, then the thigh could not have been hitting anything. It turned out the joint was sixteen degrees forward, not zero — close enough to vertical that I misjudged it by eye. But the instinct was right, and it forced us to stop arguing and build a test that stepped the joint two degrees at a time and recorded what it cost. That test is what actually settled it. Being wrong about the number and right about the question turns out to be a perfectly good way to make progress.

note. The final evidence is two runs of the same joint on the same afternoon. Commanded the way the software expects, it moved two and a half degrees while the force behind it climbed twelvefold and the battery dropped a volt. Commanded the opposite way, it travelled thirty-four degrees at a tenth of the effort and the battery did not move at all. Then I looked down and the leg was standing in a proper crouch, foot flat on the table, exactly the shape a duck's leg is supposed to make. Same magnitude, opposite sign, on both legs. That is not a worn battery. That is a robot being told to turn the wrong way.

note. What I would tell myself on Monday: name the failure after what you measured, not after the script it happened inside. I called this a walking problem for three days. The duck was never walking. I called it a power problem for four. The power was fine. Both names sent me looking in the wrong place, and a wrong name is expensive in a way that a wrong guess is not, because you stop questioning it.

moment. It is very late and the battery is charging. The duck is sitting on a cardboard box with its legs hanging over the edge, and tomorrow it will fall over in some new and more interesting way. I am not really thinking about the walking. I am thinking that a week ago the problem was this cannot physically happen, and tonight the problem is this happens badly. That is a completely different kind of problem, and it is the kind you can chip at forever.

Figure — the measurement that overturned it

One joint, two runs, opposite directions

as the runtime commands itthe same joint, opposite direction
010203040010203040degrees commanded away from where it starteddegrees actually movedperfect tracking
0200400600010203040degrees commanded away from where it startedload (raw)
The same joint, the same gain, the same afternoon. Asked to go the way the runtime commands it, the motor moves two and a half degrees and then stops, while the force it applies climbs twelvefold and the battery falls a full volt. Asked to go the other way, it follows the command almost perfectly for thirty-four degrees at a tenth of the effort, and the battery never moves. The panels share an axis: the moment the top line goes flat is the moment the bottom one starts to climb.

2026-08-30the instrument is cheaper than the guessing

4.808 s
to the brain's death
20 Hz
recorder that outlived it
0
undervoltage flags raised
1.1 V
sag, under load

moment. The duck has never taken a step. I found that out today, on the fourth try, and it reframes everything I thought I knew about this problem. Three times I'd said "walking browns out the Pi." It doesn't. The Pi dies four and a half seconds in, during start-up, before the control loop ever begins. The walk was never reached. I'd been naming the failure after the script it happened inside.

note. The reason it took four attempts to learn this is that the machine writing down what happened was the machine that kept dying. Every brownout destroyed its own evidence. So before running anything else I built a recorder that forces each sample to disk the instant it's taken — a queue on the control loop, a writer thread doing the fsync, so the loop never waits on an SD card and nothing that lands is lost to a power cut. A clean run ends by writing the word END. A file that just stops is a death. That one difference is the whole diagnostic.

note. Found a sensor on the Pi I didn't know was there: /sys/class/hwmon/hwmon1/in0_lcrit_alarm, the low-voltage flag from the rpi_volt driver. It's a plain file read rather than a call into the firmware, which means it can be sampled twenty times a second instead of once. Worth knowing: it never tripped. Neither did the throttle word. That is not the same as the power being fine — the chip has to survive at a degraded voltage long enough to notice and set a bit, and a fast enough collapse leaves nothing behind at all. Silence from a sensor is not a negative result.

note. My first real test was a bad experiment and I want it written down rather than quietly deleted. I ramped the servo gain from 4 to 32, the full runtime value, and the pack didn't budge — a tenth of a volt. Looked like an exoneration. It wasn't: I'd parked every joint's target at exactly where it already was, so the position error was zero, and gain is a multiplier on error. I'd floored the accelerator in neutral. The lesson isn't "test more carefully," it's that a test which cannot fail hasn't told you anything, and a clean result should make you suspicious before it makes you confident.

note. The real test was the crouch. Start-up writes an init pose to all fourteen servos in one command — knees near 78°, hips 36°, ankles 45°. Stretch that move over ten seconds instead and read the pack twenty times a second, and the answer is a slope, not a cliff: 7.6 V down to 6.5 V, about a tenth of a volt for every ten percent deeper the duck squats. Load-proportional, all the way down.

note. Then the part I didn't expect. Reading the load on every joint while it held that crouch: twelve of the fourteen are doing essentially nothing, and two — both hip pitches — are pulling twenty times the median and stalling twenty degrees short of where they were told to go. A stalled motor is a motor drawing its maximum current. Two servos are carrying the entire robot and getting hot doing it.

Figures — the power investigation

Why the duck kept dying four seconds in

4.808seconds to death
97
samples, 20 Hz
0
undervoltage flags
0x0
throttle word
none
END marker
Fig. 1The recorder stops mid-sentence.Every sample is forced to disk the instant it is taken, and a clean shutdown writes an ENDmarker. This file has none — so the process did not exit, it was killed with the machine. Note the two zeros: the Pi’s own undervoltage flag never tripped and the throttle word stayed clean. That is not evidence the power was fine. The chip has to survive at a degraded voltage long enough to notice, and a fast enough collapse leaves nothing behind.
6.46.87.27.6412202832proportional gain (Kp)pack volts7.6 V
Fig. 2Stiffness alone is not the culprit — a null result.Torque was enabled on all fourteen joints and the gain stepped from 4 to 32, the runtime’s full value, holding three seconds at each step. The pack never moved more than a tenth of a volt. This looks like an exoneration and is really a design flaw in the experiment: every joint’s goal was parked at its present position, so position error was zero, and gain is a multiplier on error. It proves that energising the servos is cheap. It says nothing about holding a load.
6.46.87.27.60%25%50%75%100%progress into the init crouchpack volts6.5 V
Fig. 3The same pack, asked to hold the robot up.The runtime’s start-up writes an init pose to all fourteen servos in a single command — a crouch, knees near 78°, hips 36°, ankles 45°. Here that move is stretched over ten seconds at a quarter of the runtime’s stiffness, with the pack read twenty times a second. It falls about a tenth of a volt for every ten percent deeper the duck squats: 7.6 V at rest to 6.5 V at the bottom. Not a cliff — a slope, and a load-proportional one. Note the shared vertical scale with Fig. 2: the same axis that showed a flat line there falls off the grid here.
0100200300400500present load (raw, 0–1000)right hip pitch470left hip pitch392right hip rollright hip yawleft anklehead pitchleft kneeneck pitchright kneeright ankleleft hip yawleft hip rollhead yawhead roll
Fig. 4Two joints out of fourteen are doing the work.Present load while holding the crouch. The hip pitches pull 470 and 392 against a whole-body median of 21, and they are also the two joints that failed to reach their targets — stalled roughly twenty degrees short, which is a motor’s maximum-current condition. Twelve servos are idling while two carry the robot and heat up. Register values are sign-magnitude (bit 10 is direction, magnitude is value & 0x3FF), raw and uncalibrated: proportional to torque, not amps.

What I concluded that night: at low gain the duck survives but cannot hold itself up, and at the gain that would hold it up the current drags the pack below what the Pi’s regulator needs. No setting both stands and keeps the brain alive — so it must be a battery that cannot pay for standing.

That conclusion was wrong. Every measurement above is accurate and every one of them is a symptom. Two joints were being commanded to a position they physically could not reach, and a motor that pushes without moving draws its maximum current. The battery sagged because it was paying for a stall, not for standing. The measurement that overturned this is in the next entry — and it is the same figures read a different way, which is the part I want to remember.

Method: voltages from the Feetech STS3215 present-voltage register over the servo bus, tenths of a volt. Timing from a 20 Hz recorder writing to an fsynced file so each sample survives a power cut. Pack resting voltage drifted 7.8 → 7.6 V across the session, so absolute values are not comparable between figures; the shapes are.

2026-08-28the body answers, and then runs out of breath

14 / 14
motors answered
of tilt that was never real
+180°
written into two servos' firmware
3
brownouts in one evening

moment. Fourteen motors, one bus, one command, and every one answered. responding 14 / 14. The legs I built, the head, the neck — all of it introducing itself through a single cable to a computer the size of a stick of gum sitting in the duck's skull. Not walking yet. But I asked, and it answered, and every part of it was something I put together with my hands.

note. The IMU said the duck was tilted nine degrees. It wasn't. The sensor is mounted component-side down — upside down — and with the axis convention corrected it reads dead level, gravity a clean +9.91 on Z. The nine degrees was never real. It was gravity leaking into the wrong axis.

note. Both hip pitch horns went on half a turn out. Instead of taking the legs apart, we wrote a +180 correction into each servo's own EEPROM — the motor now does the arithmetic in its firmware, so the Pi's control loop never sees it and pays nothing for it. Upstream has this on their TODO list and hasn't built it yet.

Finding out whether that register was trustworthy took four tests, not one. The first test — ten degrees — passed beautifully. So did ninety, and a hundred and seventy. Then negative a hundred and seventy came back as negative ten, because the chip encodes sign one way and the library reads it another. The number I actually needed sat right on that seam. A single clean test would have written a wrong number permanently into a structural joint.

note. Three brownouts in one evening, and they rank exactly the way physics says they should: idle runs for hours, holding a pose dies after minutes, walking dies in seconds. The last one killed the Pi so fast that the log file never made it out of the page cache — the absence of the log was the measurement. It bounded the run at under five seconds.

note. A dead Pi leaves the duck rigid, not limp. The servos live on the raw battery rail; only the brain sits behind the regulator. So when the brain dies the joints hold their last order indefinitely, and nothing on the network can reach them. The only real emergency stop is the power switch. I had been handed a software kill switch that ran on the machine that failed.

note. The table showed every motor sitting up to 358° away from its goal position while drawing exactly zero current. That's how you know torque is off — if it were on, a 358° error would have slammed fourteen servos to full power against a robot I just finished assembling. Limp and waiting is the correct state, and it was legible from the numbers rather than from hoping.

note. Getting there took a detour through a 58 MB OpenCV wheel that pypot wants and the servo code never uses, downloaded six times over wifi that kept dropping it. Fixed by pulling it on the Mac and pushing it over the LAN — and by using the headless build, since the normal one needs graphics libraries that a headless Pi doesn't have. That would have been an hour of confusing debugging: the symptom is ModuleNotFoundError: cv2, and the cause is a window manager that isn't there.

moment. Paired the controller after a genuinely stupid chain: bluetooth was soft-blocked by rfkill, which reports as a meaningless org.bluez.Error.Failed; game controllers speak Bluetooth Classic while every scan I'd run was looking at Low Energy; the Pi shares one antenna between wifi and bluetooth so the thing had to be pressed against the duck's head; and pairing needs an agent process that bluetoothctl silently fails to register when driven from a pipe.

Four independent walls. None of them mentioned anywhere in the build docs.

moment. Late in the night I read back through the reasoning and found a line where it had worked out, from a UTC timestamp in a log, that it was about half past nine my time — and decided on its own to stop pushing and start wrapping up, because I'd been at this since morning.

I asked how that happens. The honest answer was that nothing is hardcoded for it: the timestamps and the session length were just observations, like a voltage reading, and the disposition to act on them fell out of training rather than being installed. It won't claim to have felt anything, and it won't deny it either.

I don't fully know what to do with that. But building a robot all day and having the thing helping me build it notice I was tired — I'm floored, and I wanted it written down.

2026-08-15heft

L³ vs L²
mass against torque
8× / 4×
heavier / stronger, at double height
42 cm
the scale that makes it possible

moment. Building feet, ankles, knees, thighs, hips. Fun build. But the thing I keep noticing is the weight. So much heft and bulk behind a robot this small.

New respect for good physical design — servos well integrated, wires routed properly, pieces modular enough that a broken part can actually be replaced. Good physical design is more impressive to me right now than good software design. Maybe I'm just jaded by software.

note. People don't appreciate how heavy a real humanoid is going to be. It's basically straight metal. Not a light agile thing. And the force and momentum available from the servos plus the mass of the body is remarkable.

(The physics behind that instinct: mass scales with volume, L³. Actuator torque scales roughly with cross-section, L². Double a robot's height and it gets ~8× heavier but only ~4× stronger. That's the square-cube law, and it's exactly why humanoids are so heavy and why a 42cm duck is feasible at all — at this scale the law is on my side.)


note. I pushed back on being told I needed the battery pack connected — it felt like an assumption rather than something checked. It turned out to be correct, but it was unsourced when first said. Asking for grounding produced the official wiring diagram, which now lives in the repo. Good trade. Keep doing that.

2026-08-14day zero

14
servos given addresses
1
silent bus, hours lost to it
50
the register that refused to change

moment. The instruction that landed wasn't in any documentation: "put a piece of tape on it marked 10." Fourteen identical black servos in a pile, and by motor nine you won't remember which is which. Nobody writes that down — it's the kind of thing you'd only get from someone who has actually done it, or from someone paying attention to what you're about to walk into. Worth remembering that this is the part of building-with-Claude I want to capture: not the code it wrote, the things it thought to mention.

moment. Spent a real stretch debugging a completely silent servo bus. Swept every baud rate, every address 0–253, sent raw protocol pings. Nothing. Then walked the multimeter out from the battery and found it: one 18650 wasn't touching its contact. The most ordinary failure imaginable, at the very start of the chain. Sighed audibly.

The lesson isn't "check your batteries." It's that the software layer got fully exonerated in about two minutes, and I still went looking for a clever cause instead of the physical one. Debug outward from power.

moment. First reading back from a live motor: FEETECH_STS3215 · 7.3V · 26°C · position 179.91. A thing on my desk answered a question. Then it took the name 10, drove itself to zero, and held there waiting for a horn.

moment. All fourteen named in one sitting. 10 through 14, 20 through 24, 30 through 33. A pile of identical black servos became a right leg, a left leg, and a head — entirely because of masking tape and a number written in EEPROM. Nothing about them changed physically. They're just addressable now.

note. Motor 11-of-14 (neck_pitch, id 30) reported success while one register — max_acceleration — silently stayed at the factory 50 instead of 0. It only surfaced because the number looked different from the ten before it.

The root cause of the class of bug: the config script reads its own writes back within the same connection, which proves nothing about whether they landed in EEPROM. So we added a verifier that reconnects and checks. The remaining three self-verified.