← All churts

Half a Kilo Down, Thirty Grams Up

Half a Kilo Down, Thirty Grams Up

I’ve started building a drone that delivers small packages between friends.

The shape of it: your friend needs a Raspberry Pi and you have a spare. They open an app and drop a pin on their back yard. You put the Pi in the drone. It flies over, hovers, lowers it on a line into the spot they picked, lets go, winds the line back up, and comes home. It never lands at the other end.

The project is called AirDropt.

Let me be upfront about novelty, since it’s the first thing worth establishing: there isn’t much. ArduPilot ships DO_WINCH as a mission command. Orchestrating a mission from a Raspberry Pi over MAVLink is the standard pattern, documented to death. Pin-a-location delivery is a commercial industry with well-funded companies in it. If you’re here for a new technique, this isn’t it.

What I think is worth writing down is the reasoning — particularly one load case I had completely backwards, and which changed the hardware I bought.

The payload is 500 grams. The winch lifts 30.

The obvious way to size a winch is: the package weighs 500 g, so pick an actuator that lifts 500 g with margin. I sketched a spool, found the torque equation, and started looking at geared motors, because a 9 g hobby servo lifting half a kilo is not obviously reasonable.

Then I actually wrote out the phases:

PhaseLoadWhat the actuator does
Lowering500 gBrakes a descending mass — gravity does the work
ReleaseNothing
Retracting~30 gLifts an empty catch

The winch lowers the package and retracts nothing. By the time it’s winding back in, the package is on the ground and the only thing on the line is the release mechanism. The sustained lifting case — the one that sets your torque requirement and cooks your servo — doesn’t exist in the nominal mission.

That inverts the sizing problem. A FEETECH FS90R is a 9 g continuous-rotation servo rated around 1.5 kg·cm. On a 25 mm spool that’s roughly a kilo of lift, which sounds like ample margin until you remember that published servo torque is stall torque, and continuous safe duty is somewhere around 30–50% of it. Sized against a 500 g lift, you’re right at the ceiling. Sized against a 30 g lift, you’re nowhere near it.

There’s still a case that demands the full 500 g: failed release. If the catch doesn’t open, the aircraft has to haul the package back up the whole five metres. That’s the contingency, and it’s the one I’ll bench-test deliberately — hang the full weight, retract all five metres, and see whether the servo completes it or overheats. That test decides the actuator, not the nominal one.

The general lesson, which I keep relearning: size for the load case that actually occurs, then enumerate the ones that occur when something breaks. Sizing for the case that intuitively feels heaviest sent me shopping for geared motors I don’t need.

Making imprecision harmless

The release mechanism went the same way.

The intuitive design is a servo-driven claw at the end of the line: lower it, command it open, retract. That’s what my first sketch had, and it maps neatly onto ArduPilot’s DO_GRIPPER command. It also means running power and signal down five metres of tether, spooling wire that fatigues where it bends, and hanging another 14 grams on the end of a pendulum I already have to worry about.

The alternative is a catch held closed by line tension and sprung open by slack. The package touches down, tension drops, the catch opens. No electronics below the aircraft at all.

That costs something real: there’s no commanded release and no confirmation. The mechanism fires when the physics says so, not when I say so. But it buys something I didn’t expect, which is that it makes the winch’s measurement problem disappear.

A continuous-rotation servo has no position feedback. You don’t know how much line is out. You dead-reckon it: distance equals rate times time, and your accuracy is however well you calibrated the rate. On a 25 mm spool at roughly 14 cm/s, expect ±10–15% over five metres — call it half a metre of uncertainty.

Half a metre sounds bad until you notice the error is asymmetric in your favour. Paying out too much line is harmless — the package lands, slack develops, the catch opens. Paying out too little means it dangles. So the algorithm isn’t “lower exactly five metres,” it’s:

read AGL from the rangefinder
pay out that distance plus 20%
let slack do the rest

Precision was never the requirement. Sufficiency was. Once the mechanism tolerates overshoot, an open-loop servo and a stopwatch are genuinely enough, and the encoder I’d been planning becomes a nice-to-have.

Retraction is the direction that needs care, since over-retracting can jam the catch into the spool. That gets bounded in software and backed by a mechanical stop.

ArduPilot had already written my algorithm

I went into this expecting to write the dead-reckoning myself, most likely as a Lua script on the flight controller, because ArduPilot’s winch support was built around the Daiwa unit, which has an encoder. An open-loop servo seemed like the unsupported case.

It isn’t. WINCH_TYPE=1 selects a PWM backend whose core is this:

// update distance estimate assuming winch will move exactly as requested
line_length += config.rate_desired * dt;

That’s the algorithm. Someone already decided that assuming the winch does what it’s told is good enough, and shipped it.

Validated in simulation: commanded five metres, got 4.95 m in 35.7 seconds against 36 seconds predicted at 0.14 m/s. It held commanded rate, decelerated cleanly into the target under position control, and retracted symmetrically. Configuration, not code. No Lua, no firmware fork.

WINCH_RATE_MAX turns out to be the whole ballgame — it’s both the speed setting and the accuracy limit, because position is derived from it. A 20%-wrong rate means 20%-wrong reported line length. That number gets calibrated on a bench with the real servo, the real spool, and the real weight, and everything downstream inherits its error.

Two things that will cost someone else an evening:

  • WINCH_TYPE requires a reboot. It carries an enable flag, so the backend only instantiates at boot. Set it on a running vehicle and it silently does nothing.
  • WINCH_STATUS is in no default stream. You must request it explicitly or line length is never reported at all.

Three things about talking to an autopilot

The mission logic runs on a Raspberry Pi as a state machine over MAVLink: take off, fly out, descend, settle, lower, watch, retract, return. Bringing it up against the simulator produced three findings I’d have hit much more expensively on real hardware.

A companion has to request its own telemetry. Connect directly to the autopilot and you get heartbeats and very little else — ArduPilot streams on request, and normally it’s the ground station doing the asking. Skip that and you see gps_fix=0, sats=0, which looks exactly like a dead GPS rather than a missing subscription. I spent a cycle debugging hardware that was fine.

There can be exactly one message pump. My first version had the winch class calling recv_match() while the vehicle class was also draining the queue. They raced. Whichever read first consumed the message and the other silently saw nothing, so the winch looked dead while working perfectly. All message state now lands in one place and everything else reads from it.

Arming needs retries, not a flag check. There’s a pre-arm health bit, and I waited on it, and it lied — it read OK moments before the autopilot announced Arm: EKF attitude is bad. It flaps while the EKF converges. The fix was to retry and surface what the autopilot actually said, which walked through waiting for homeNeed Position EstimateEKF3 IMU1 is using GPS and armed on the eleventh attempt, about thirty seconds in. Those messages are the real diagnostic; a single attempt leaves you holding a bare MAV_RESULT_FAILED and no idea why.

The part I can’t test yet

Because the release is passive, nothing reports that it happened. The aircraft has to infer it, and the signal is that it suddenly got lighter — hover throttle drops when 500 g leaves the hook.

My first estimate for that drop was about 20%, reasoning from the weight change. That’s wrong, and it’s wrong in the direction that matters. Rotor thrust scales roughly with the square of rotor speed, and throttle tracks speed, so dropping to 77% of the weight means:

ω_after / ω_before ≈ √0.77 ≈ 0.88

Roughly a 12% throttle reduction, not 23%. A vehicle hovering at 65% settles around 57%. That’s a much narrower margin to detect against, and it’s why the detector baselines the actual hover throttle in flight rather than trusting any number I calculated.

And here’s the honest limitation: the simulator doesn’t model the aircraft getting lighter. Throttle stays flat through the whole drop, so the detector correctly reports no release, and the mission ends in a state I named RETURNED_WITH_PAYLOAD rather than claiming a delivery it can’t verify. Which is the behaviour I want — but it means the threshold is theoretical and the noise handling has never seen real data. Simulated hover throttle has a standard deviation of exactly zero. Real hover throttle wanders.

That one gets calibrated from flight logs: hover with the package, release it, measure what actually happens.

Where it is

Parts are shipping. The simulator flies the full mission end to end — takeoff, transit, descent, lower, watch, retract, return, land — in about 4.9 minutes, which lines up with the 4.5–5.5 I’d budgeted from hover-current estimates. Nothing has flown.

The rule I’ve held to throughout, and the one I’d defend hardest: software never sits between the pilot and the aircraft. The radio link goes transmitter → receiver → flight controller with nothing of mine in it. The Pi talks to the autopilot over a separate path and can request, monitor, and report — but a mode switch on the transmitter instantly overrides everything, and every wait loop in the mission checks whether that happened and stands down if it did. It never tries to take control back.

A crash in my code should mean a failed delivery. Never a crashed drone.

More as it flies.

← All churts