The EMERGENCY button that did nothing
I pressed HOLD on a drone and the status line said:
[FALCON-1] HOLD → REJECTED
No reason. Just refused.
Two things were wrong here, and the second one is much worse than the first.
First problem: the console knew why, and didn't say
The rejection had a reason attached the whole time. The result object carries a
reason field, that field was filled in correctly, and it was written to the
flight recorder. The screen showed the status and threw the reason away.
So the operator sees a refusal with no cause. The only way to find out more is to go read the source code. On an operations screen that's close to useless — "it said no" isn't something you can act on.
The fix took about four lines. The status line now reads:
[FALCON-1] HOLD → rejected — MAVLink adapter does not map command 'set_mode'
That one line would have saved me the whole investigation, which is a good sign it was worth doing.
Second problem: the aircraft never could do it
Now the real bug. Here's the chain, one step at a time.
The HOLD button sends a "set mode" command — it tells the aircraft to hold its position.
Every asset in my system declares a list of what it can do: take off, land, return home, set mode. That list does two jobs. The interface uses it to decide which buttons to draw, and the adapter uses it to reject any command that isn't on it.
The autopilot adapter's list included set mode.
But the part of the adapter that turns a command into actual bytes on the wire had never implemented set mode. It sat on an explicit list of "commands this adapter does not map."
So the command passed the gate, because the adapter claimed it could do this. Then the translator refused it, because it couldn't. The adapter was promising something it had no way to deliver.
And here's the part that actually matters. HOLD is a convenience. But EMERGENCY sends the same kind of command. Which means that on every aircraft of that type, the button labelled EMERGENCY had never done anything. It drew itself. It highlighted when you hovered. It was styled in warning red. Press it in a real emergency and you'd get a refusal — and before today's other fix, a refusal with no explanation.
I sat with that one for a while.
Nothing had crashed. No test would have caught it. Both halves are correct on their own: the translator is right to refuse a command it can't map, and the list is just data. The bug lived in the gap between them, and nothing was looking at that gap.
What a declared capability actually means
The fix was small — delete one line from that adapter's list, and hide HOLD and EMERGENCY on any asset that doesn't declare it. But it's worth being precise about why it matters.
In this system, that list isn't documentation. It's a promise, and two different things believe it:
- The adapter's own gate. It lets a command through if the capability is declared. This is the last check before anything reaches the wire, and the whole idea is that a device decides what it's allowed to be asked to do.
- The interface. It builds its controls from that same list — a rover shows drive controls, a submersible shows depth, an aircraft shows takeoff. Adding a new vehicle type needs no special cases in the UI. That only works if the declarations are true.
Which gives me a rule I should have written down much earlier: a declared capability has to be a promise the adapter can keep. What a device claims must always be a subset of what it actually implements.
The good news is that's something a test can check — one test per adapter, asserting that everything it claims, it can really map. That's going in.
The general version, for anyone building something similar: if your interface draws itself from a list of capabilities, that list isn't a config file any more. It's safety-critical, and it deserves the same scrutiny as the code it controls.
While I was in there: disconnect now asks
Related, and found by pulling the same thread.
The console wouldn't let you disconnect an aircraft that was flying — the button was hidden in that state. But the fleet-wide "disconnect all" would happily reach those same airborne aircraft, one click, no confirmation. So the safe path was blocked and the dangerous path was wide open, right next to it. Either choice applied consistently would have been better than that.
To decide what to do, I had to know what disconnecting actually does. Akala ko (I thought) it was harmless — the aircraft just keeps flying and you stop watching it.
It's the opposite. Both major open-source autopilots have a link-loss failsafe, turned on by default. One of them watches for the ground station's heartbeat and triggers after about five seconds of silence. My console sends exactly that heartbeat, once a second, the entire time it's connected.
So disconnecting a flying aircraft isn't nothing. It stops the heartbeat, and a few seconds later the aircraft reacts to the silence — usually hold, then return home or land, depending on how it's set up. One of the two docs also says that when the link comes back, the vehicle stays in failsafe rather than picking up what it was doing before.
That's a real consequence, and the operator never asked for it. To me that's the definition of a command that needs confirming. The convention I've worked to on ground systems is that anything with a hazardous outcome gets confirmed in two steps, and that you tell the operator the specific condition that makes it hazardous — not a generic "are you sure?".
So disconnect now opens a dialog that says the actual thing:
HAWK-3 is AIRBORNE. Losing the ground-station link normally triggers the vehicle's own link-loss failsafe within a few seconds — typically hold, then return-to-launch or land, depending on how it is configured. Reconnecting may not return it to its current mode. This station stops sending its heartbeat and will no longer receive telemetry or be able to command it.
Cancel is focused by default, Escape cancels, and the confirm button says "Disconnect anyway" instead of "OK". I also brought back the in-flight disconnect — hiding it never removed the danger, it just removed the warning — and the fleet-wide version now tells you how many of the aircraft it's about to reach are currently flying.
So what should the button do?
Deleting a broken button is easy. Deciding what it should do turned out to be the real question, and the answer changed my mind about whether it should exist.
PX4 — the autopilot this adapter targets — has no "emergency mode". It has four different things you might mean by the word, and they are not close relatives:
- Land — descends where it is, regardless of what happens to be underneath.
- Return — flies home.
- Kill switch / force disarm — motor outputs stop and the aircraft falls. Reversible for about five seconds, then it disarms.
- Flight termination — controllers off, outputs to failsafe values, parachute if one is fitted. The documentation is blunt: there is no way to recover, and you should unplug the battery as soon as you can.
Two of those save the aircraft. Two destroy it.
Picture the actual moment. Something is going wrong, the operator's hand goes to the button marked EMERGENCY, and what happens next depends entirely on which of the four whoever wrote the adapter happened to pick. Expecting a landing and getting a falling aircraft is a crash. Expecting the motors to stop and instead watching a failing vehicle fly itself home over a road is worse.
The label is the bug. One word standing for four incompatible actions isn't a control, it's a coin flip. Ang pindutan na may apat na kahulugan ay walang kahulugan (a button with four meanings has none). And the console already has LAND and RTL as their own buttons — so an EMERGENCY that quietly means "land" is just a worse-named LAND.
So emergency stops being something you can command. It stays as something a
vehicle can report — "I am in a failsafe state" is real and worth putting on a
screen. But commanding it goes away.
If I want a true emergency stop later, and I probably do, it gets to be its own thing: its own command, its own capability so an aircraft that can't do it doesn't advertise that it can, and the confirmation dialog I just built — saying the quiet part out loud. The motors will stop and the aircraft will fall.
The part that made me wince
While working this out I checked the other adapter, the one for the small
consumer drone. That one does implement emergency: it maps to that drone's
own emergency command, which cuts the motors immediately.
So on that aircraft, the button has always worked. One click, mid-flight, motors off, no confirmation.
Meanwhile disconnecting the same drone — which doesn't touch the motors — now asks you to confirm, twice if it's flying.
I had built a console that guards the recoverable action and waves through the irreversible one. That goes on the list too.
Added later. All of this shipped the same week. Stopping the motors is its own command now, with its own capability. The small drone says it can do it, because it really can. The autopilot adapter says it can't, and gives a reason when you ask. The button is called STOP MOTORS instead of EMERGENCY, it only appears on aircraft that can actually do it, and it asks first — with the plain version: the aircraft will fall from where it is, use LAND or RTL if you want it down safely. There's also a test now that checks every capability an adapter claims against what it can really do, so this bug can't come back the same way.
The deeper fix: capabilities the adapter owns
Talking this through, someone made the point that landed hardest: isn't this what the adapter design is for? And yes — that is exactly what it is for. The pattern wasn't wrong. The granularity was.
set_mode is one command covering actions that have nothing to do with each
other in consequence: hover here, fly home, land, and on some aircraft cut the
motors. An adapter declaring "I can set modes" is telling the truth in general
while being unable to do the specific thing the operator reached for. The
declaration was too coarse to be either honest or dishonest.
So the fix isn't a patch in the interface. It's three changes at the seam where the knowledge actually lives:
The adapter stays the only thing that knows what a device can do. Not the
screen, not a special case in a component. If a control ever needs an
if (this is the little drone), the capability model is wrong and the model gets
fixed, not the component.
Capabilities get split until they match what an operator distinguishes. An emergency stop is not a mode change — it becomes its own capability, which the consumer drone declares because it genuinely has it, and the autopilot adapter does not because it genuinely doesn't. Different aircraft, different buttons, zero device knowledge on screen.
Capabilities should be discovered, not just declared. A static list in the adapter is a starting point, not the truth: real protocols advertise what they support, and two aircraft running the same autopilot aren't necessarily the same aircraft — firmware and airframe change what's possible. So the set an operator sees becomes declared, narrowed by what the device actually reports, narrowed again by what policy permits — with the rule that discovery may only ever narrow. A device claiming more than the adapter implements changes nothing.
And the end state, which is where this is going: one table mapping a capability to the control that represents it, with every surface — the console and the simulator's own dashboard — rendering by looking up what the asset declares. At that point it becomes structurally impossible to draw a control for something the aircraft can't do. Not caught by a test. Impossible.
It's more work than fixing the button. Mas mahirap, pero iyon ang punto (harder, but that's the point) — that cost is the entire multi-domain, plug-and-play claim: adding the fifth or the twentieth vehicle should cost an adapter and nothing else. A capability system that's really just a tidier interface isn't worth the ceremony.
One guardrail I'm writing down before I start, because I can feel the pull in both directions: a capability names something the device can do, not something the screen shows. Too coarse and it hides real differences — that's this bug. One capability per button and it's the interface wearing a different hat, with the device knowledge smuggled straight back in. "Stop the motors" is a capability. "Show a battery pill" is not.
What I'm taking from this
The dead button wasn't a coding error. Every piece was correct on its own. It was two descriptions of the same fact disagreeing — what the adapter says it can do, and what it can actually do — with nothing checking that they matched.
Those are the bugs that survive code review, kasi (because) reviewing either half on its own shows nothing wrong. The answer isn't to be more careful. It's to make the rule something a machine can check: one test, per adapter, asserting the claim matches the capability.
And the smaller lesson, the one that cost me twenty minutes today: when your software refuses to do something, tell the person why. You already know. It's sitting right there in the object you're holding.
Build log entry. Apache-2.0.
Thoughts?
Comments are threads on GitHub, so a GitHub account is needed to post. No account? Email me instead — I read everything.