A number is not a measurement
For weeks my console displayed altitude as 394. It was correct. It was also, strictly speaking, meaningless — because a number on an operations screen is only a measurement once you know three things about it:
- Its unit. 394 feet or 394 metres? Those differ by a factor of three, and both are plausible altitudes for a small drone.
- Its reference frame. Measured from sea level, or from the ground under the aircraft? Over a 200-metre hill those are two very different aircraft.
- Its age. Is that where it is, or where it was before the radio went quiet?
I'd been carrying all three as tribal knowledge — things I knew because I wrote the code. Which is fine right up until someone else is the one flying. Today I put them on the screen.
The plan going in
Two small items off the backlog: an operator-selectable metric/imperial toggle, and a hover tooltip showing an asset's status without having to select it. A tidy afternoon of user-facing polish.
What landed
One conversion module, and only one. Every unit conversion in the console
now goes through a single file. Before today, one component had * 3.28084
inlined — a metres-to-feet conversion hardcoded at the point of display. That is
how a codebase ends up with three different rounding rules and no way to add a
toggle. The rule I'm holding to, written into the file's header so the next
person inherits the reasoning:
Everything inside the system is metric and geodetic — metres, metres per second, degrees on the World Geodetic System 1984 (WGS84) datum. Device drivers convert at the edge where they speak to hardware. Display is the only other place a unit is allowed to change.
That rule is not aesthetics. If a display unit leaks inward, it eventually reaches the flight recorder, and then a recording can only be replayed by someone who knows which units the operator had selected that day. The recording has to mean the same thing forever.
The reference frame is on the screen now. Altitude on an asset card reads
394 ft MSL — above mean sea level, the frame the telemetry actually carries.
The takeoff height input reads AGL — above ground level, because takeoff
height is measured from where the aircraft is standing. Same word, "altitude,"
two different meanings, and the console used to print both as a bare number. For
weeks the simulator and the display disagreed by exactly the terrain elevation
and it read like a synchronisation bug. It wasn't. It was a missing label.
Hover status. Pointing at an aircraft — on the map or in the layers list — now pops a compact card: link state, battery, mode, armed, altitude, and last update, counting up in seconds. No clicking, no selecting, no leaving the picture.
The last line matters more than the rest. It's the piece that answers question three, and it inherits the rule from last time: when the link goes quiet, every live value in the card blanks to a dash and only the age keeps ticking. The card tells you what it doesn't know.
Plan vs. reality
A test caught me widening a safety limit. The takeoff height input is bounded — small uncrewed aircraft are generally held to about 400 metres above the ground, and the input refuses to go higher. Adding the imperial option meant adding a second set of bounds, and I wrote the obvious ones: 3 to 1312 feet.
Then I wrote a test asserting that the imperial bounds sit inside the metric ones, and it failed on the floor. Three feet is 0.914 metres. My metric floor was 1 metre. Switching the display to imperial let an operator command a takeoff below the limit the metric setting enforced.
The magnitude is trivial — 9 centimetres, on a floor, not a ceiling. The mechanism is not. A unit conversion had quietly become a policy change. Do that on the ceiling instead of the floor and you've got a display preference editing a regulatory limit. Nakakatakot (that's scary), and I only found it because I bothered to write the assertion. The bounds now round inward on conversion, and the property — switching units can never widen the envelope — is a test rather than a comment.
That is the whole argument for putting conversions in one tested module. Spread across six components, that bug has six places to hide and no place to assert.
The tooltip owns its own clock. The card's "8s ago" has to keep counting when nothing is arriving — the absence of telemetry generates no events to re-render on. I expected to thread a timestamp down through the view tree. Instead the card runs its own once-a-second timer, which exists only while the pointer is over an asset. Its lifetime is already scoped to the hover, and nothing else on screen re-renders for it.
I built the same thing twice and then stopped. Two map surfaces needed identical hover plumbing. I wired the first, started copy-pasting into the second, and pulled it into a shared piece instead. The map component reports that a marker was hovered and where it sits; it knows nothing about status content. That boundary is why the map package didn't grow a dependency on battery percentages today.
What I learned
A displayed value carries an implicit claim, and each of the three questions is a different way for that claim to be wrong. Wrong unit: off by a constant factor — usually obvious, occasionally fatal. Wrong reference frame: off by the terrain — never obvious, because the number looks entirely reasonable. Wrong age: not off at all, just no longer true, which is the one an operator can't detect by staring at it.
The console now answers all three. It took a metre of extra text on screen.
What's next
Move link-loss detection out of the display and into the vehicle link itself, so a mission engine or a fail-secure policy — return home, hold, surface — sees the silence too, not just the operator.
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.