Indexer Spec¶
This is the developer-facing reference for the OnSpeed indexer — the visual gauge the M5 secondary display (and the in-cockpit tablet via the /indexer web tab) draws. It defines what every visual element shows, what gates each color change, and how each anchor on the gauge moves with the lever.
It is not a pilot-facing document. For "what do the lights mean in flight," see What the Tones Mean. For the audio side that the chevron + audio low tone share, see Audio Tone Spec.
The canonical implementations are:
software/OnSpeed-M5-Display/src/main.cpp::drawAOA— the renderersoftware/Libraries/onspeed_core/src/aoa/DisplayPctAnchors.{h,cpp}— the anchor producersoftware/Libraries/onspeed_core/src/proto/DisplaySerial.h— the wire format
0. Design rule (Vac)¶
L/Dmax pips are aerodynamic references.
Fast tone is an operational limit cue.
They must remain independent.
— Vac, L/Dmax Cue and Fast-Tone Logic, §8
Vac's 'fast tone' = OnSpeed's 'low tone'
Vac's term fast tone is the cue that fires when you're flying fast for this configuration — i.e., AOA is below the OnSpeed band. In OnSpeed firmware terminology this is the low tone (the 400 Hz pulse, gated by fLDMAXAOA). The OnSpeed high tone (1600 Hz, gated by fONSPEEDSLOWAOA) is the slow-side warning, not the fast-side cue. So Vac's "fast tone is an operational limit cue" applies to the L/Dmax / low-tone gate documented throughout this spec.
This rule is load-bearing. The indexer carries two cues that are visually near each other but semantically unrelated:
- Operational cue: the bottom green chevron, gated on the audio low-tone threshold. Snaps per detent. If a tone is on, the chevron is green.
- Aerodynamic cue: the L/Dmax pip dots on the index bar's edges. Slides smoothly with the lever. Reflects the aerodynamic intuition that L/Dmax body angle slides toward the OnSpeed band as flaps deploy.
The two coincide visually only at the cleanest detent. They diverge during deployment, by design.
1. Visual elements¶
The indexer occupies a vertical strip on the M5 panel (and the tablet canvas). Five elements:
| Element | Position | Purpose |
|---|---|---|
| Top chevrons | Top of strip | Stall warning. Yellow approaching stall, red imminent, flashing red past stall warn. |
| Bottom chevron | Bottom of strip | Operational cue. Green when the audio low tone is playing. |
| Donut top arc | Just above midline | OnSpeed cue (top half). Green when AOA is in the upper half of the OnSpeed band. |
| Donut bottom arc | Just below midline | OnSpeed cue (bottom half). Green when AOA is in the lower half of the OnSpeed band. |
| Donut center dot | Center of strip | OnSpeed cue (centered). Green when AOA is in the middle 50% of the OnSpeed band. |
| White index bar | Slides on strip | Current AOA in percent-lift space. |
| L/Dmax pip dots | Two small dots on strip edges | Aerodynamic cue. Slides smoothly clean → fullflap. |
The strip's vertical extent maps to percent-lift via a piecewise-linear function (see §4). All gates are in percent-lift units.
2. Wire fields that drive the indexer¶
The Gen3 firmware emits one #1 frame every 50 ms. Each visual element reads one or more percent fields:
| Wire field | Snap or slide | Drives |
|---|---|---|
percentLift |
live AOA reading | white index bar position |
tonesOnPctLift |
snap per active detent | bottom chevron lower gate |
onSpeedFastPctLift |
snap per active detent | donut bottom arc lower edge, donut center bounds, screen Y-mapping |
onSpeedSlowPctLift |
snap per active detent | donut top arc upper edge, screen Y-mapping, top chevron color thresholds |
stallWarnPctLift |
snap per active detent | top chevron flash threshold, screen Y-mapping |
pipPctLift |
slide clean → fullflap | L/Dmax pip dot positions |
flapsDeg |
per-bracket lerp | flap-position widget angle (separate from indexer) |
percentLift is the live AOA reading as a float in whole-percent units (0.0..99.9), computed from g_Sensors.AOA and the active detent's calibration via ComputePercentLift. The four anchor fields (tonesOnPctLift, onSpeedFastPctLift, onSpeedSlowPctLift, stallWarnPctLift, pipPctLift) stay integer-percent in [0, 99] — they only move on detent or config-save events, so sub-percent buys nothing on those. Computed by ComputeDisplayPctAnchors from the configured flap vector and the raw lever-pot ADC. Comparisons between percentLift (float) and an anchor (int) promote the int to float; behavior is exact for our range since integer values up to 2²⁴ are representable in float32 without rounding.
3. Gates and color logic¶
3.1 Top chevrons¶
Two stylized triangles forming an up-arrow near the top.
chevMid = onSpeedSlowPctLift + (stallWarnPctLift − onSpeedSlowPctLift) / 2
if percentLift > onSpeedSlowPctLift and percentLift ≤ chevMid → YELLOW
if percentLift > chevMid and percentLift ≤ stallWarnPctLift → RED
if percentLift > stallWarnPctLift and not flashing → RED
if percentLift > stallWarnPctLift and flashing → DARKGREY (flashing red blink)
otherwise → DARKGREY
Source: main.cpp::drawAOA, lines ~870–920.
3.2 Bottom chevron — operational cue¶
Two stylized triangles forming a down-arrow near the bottom.
Source: main.cpp::drawAOA, lines ~921–970.
This gate matches the audio low-tone gate exactly. When the audio is pulsing the low tone (a "you're flying fast for this flap configuration" cue), the bottom chevron is green. When the audio goes silent or transitions to solid (in the donut band), the chevron goes dark or stays in the prior color.
The audio path computes the same condition independently, against g_Config.aFlaps[g_Flaps.iIndex].fLDMAXAOA and fONSPEEDFASTAOA. tonesOnPctLift on the wire is ComputePercentLift(active.fLDMAXAOA, active, true) — the percent representation of the audio threshold for the active detent. Both fire from the same source of truth; they snap together at every detent transition.
3.3 Donut¶
Three concentric elements.
OnspeedRange = onSpeedSlowPctLift − onSpeedFastPctLift
bottom arc = GREEN if percentLift ≥ onSpeedFastPctLift and percentLift ≤ onSpeedSlowPctLift − 0.25 × OnspeedRange
top arc = GREEN if percentLift ≥ onSpeedFastPctLift + 0.25 × OnspeedRange and percentLift ≤ onSpeedSlowPctLift
center dot = GREEN if percentLift ≥ onSpeedFastPctLift + 0.25 × OnspeedRange and percentLift ≤ onSpeedSlowPctLift − 0.25 × OnspeedRange
The three regions overlap: at the geometric center of the band, all three are lit simultaneously. The center dot lights across the inner 50% of the band; the arcs light across an extra 25% on each side.
Source: main.cpp::drawAOA, lines ~975–1000.
3.4 White index bar¶
Horizontal bar across the full strip width.
Where mapPct2Display (see §4) maps percent-lift to a pixel Y coordinate using the snapped band edges as anchor points.
Source: main.cpp::drawAOA, lines ~1002–1004.
3.5 L/Dmax pip dots — aerodynamic cue¶
Two small white dots, one on each side of the strip.
pipPctLift slides smoothly across the entire pot range (see §5). At the cleanest detent, pipPctLift == tonesOnPctLift and the pip lines up with the bottom chevron's edge. At the most-deployed detent, pipPctLift equals the geometric center of that detent's OnSpeed band and the pip sits inside the donut.
Source: main.cpp::drawAOA, lines ~1010–1014.
4. Screen-Y mapping (mapPct2Display)¶
The strip's pixel Y coordinate is a piecewise-linear function of percent-lift, anchored on the snapped band edges:
| Percent-lift range | Pixel Y range |
|---|---|
≤ 0 |
192 (display bottom) |
(0, onSpeedFastPctLift] |
192 → 115 (linear) |
(onSpeedFastPctLift, onSpeedSlowPctLift] |
115 → 78 (donut band, fixed screen-Y) |
(onSpeedSlowPctLift, 99] |
78 → 1 (linear) |
> 99 |
1 (display top) |
The upper ramp tops out at percent_lift = 99 — the lift-envelope ceiling — independent of the active detent's stall-warn percent. Stall-warn drives the chevron flash-red color logic in drawAOA (§3.1); it does not gate Y here. Floats in (99.0, 99.9] (the float clamp range above 99) saturate at y=1 just like the integer 99 — the bar visibly pinning at "off the chart" is the documented saturation cue.
The donut band is anchored at fixed pixel Y so the donut never moves on screen, regardless of which detent is active. The L/Dmax pip and the white index bar both float according to their percent values — when those values fall within the donut band, they appear inside it.
Source: main.cpp::mapPct2Display, line ~1510.
5. Pip computation (pipPctLift)¶
The pip percent is a single linear interpolation between two endpoints:
with
and
That is, the full-flap pip target is the bottom-half-of-donut anchor — one quarter of the way from the fast (lower-percent) edge into the OnSpeed band. Lands the chevron in the lower donut at L/Dmax instead of climbing into the upper donut to meet a band-center pip (per PR #376).
cleanest is the lowest-degree configured flap entry (g_Config.aFlaps[0] after parse-time sort). mostDeployed is the highest-degree entry (g_Config.aFlaps[entryCount-1]). Intermediate detents are ignored for the pip — a typical 3-detent config (clean / 16° / 33°) lerps from clean to 33° as the lever moves, passing through the 16° detent's calibrated L/Dmax percent only by coincidence (the 16° detent's L/Dmax does not anchor the pip).
Source: DisplayPctAnchors.cpp::ComputeDisplayPctAnchors.
Why ignore intermediate detents¶
Per Vac, this is intentional. The pip is a visual aerodynamic reference, not a per-detent calibration anchor. The aerodynamic intuition — "L/Dmax slides toward OnSpeed as flaps deploy" — is faithfully represented by a smooth two-endpoint lerp; making the pip visit each calibrated detent's L/Dmax mid-deployment introduces a subtle stutter the design specifically avoids.
The operational cue (the bottom chevron, gated on tonesOnPctLift) handles per-detent calibration accurately, because it must match the audio.
6. Snap-vs-slide table¶
| Field | Behavior | Why |
|---|---|---|
tonesOnPctLift |
snap per active detent | matches audio low-tone gate; chevron snaps with audio |
onSpeedFastPctLift |
snap per active detent | donut lower edge; matches audio's onspeed-fast threshold |
onSpeedSlowPctLift |
snap per active detent | donut upper edge; matches audio's onspeed-slow threshold |
stallWarnPctLift |
snap per active detent | top chevron flash; matches audio's stall-warn threshold |
pipPctLift |
slide clean → fullflap (single lerp, ignores intermediate detents) | aerodynamic reference; smooth visual |
flapsDeg |
slide per-bracket (lerp adjacent detents' iDegrees) | mechanical lever angle; visits every detent |
7. Continuity invariants¶
Pinned by test/test_display_pct_anchors/:
pipPctLiftis continuous inrawAdceverywhere. No discontinuities at detent boundaries — single lerp covers the entire pot range. Adjacent ADC samples produce pip values within 1 percent of each other.tonesOnPctLiftsnaps atiIndexadvances. SamerawAdcwith differentactiveIndexvalues produces differenttonesOnPctLiftvalues, by exactly the difference between the two detents' calibrated L/Dmax percents. The chevron edge snaps with the audio.pipPctLiftis independent ofactiveIndex. The pip depends only onrawAdcand the configured flap vector — not on which detent is "active." A detent transition does not change the pip.- Pip and tones-on coincide at the cleanest detent. When
rawAdc == cleanest.iPotPositionandiIndex == 0,pipPctLift == tonesOnPctLift. This is the visual "they line up in clean" property. - Pip and tones-on diverge at full flaps. When
rawAdc == mostDeployed.iPotPositionandiIndex == entryCount-1,pipPctLift > tonesOnPctLiftfor any sane calibration where L/Dmax is below the OnSpeed band.
8. Worked example (RV-10, sam@frogrocketai.com calibration)¶
Three detents from ~/Downloads/onspeed2_latest.cfg:
| Flap | Pot ADC | fLDMAXAOA (°) |
fONSPEEDFASTAOA (°) |
fONSPEEDSLOWAOA (°) |
fAlpha0 (°) |
fAlphaStall (°) |
|---|---|---|---|---|---|---|
| 0° (clean) | 1462 | 3.24 | 3.98 | 5.26 | −3.72 | 10.31 |
| 16° | 897 | 1.11 | 2.44 | 3.88 | −6.22 | 9.57 |
| 33° (full) | 2 | −2.24 | 2.19 | 4.09 | −9.21 | 11.57 |
Computed percent-lift values (from the actual ComputePercentLift /
ComputeDisplayPctAnchors implementation, which truncates fractions
toward zero per the saturation convention):
| Flap | L/Dmax pct | Fast pct | Slow pct | StallWarn pct |
|---|---|---|---|---|
| 0° (clean) | 49 | 54 | 64 | 85 |
| 16° | 46 | 54 | 63 | 84 |
| 33° (full) | 33 | 54 | 64 | 82 |
The pip's full-flap target is the round-half-away mean of 33°'s
Fast and Slow: lround((54 + 64) / 2.0) = 59.
Lever positions and what the indexer shows. Active-detent index
follows the standard midpoint rule (Flaps::Update):
| Lever pot | Active detent | tonesOnPctLift |
pipPctLift |
flapsDeg | Coincide? |
|---|---|---|---|---|---|
| 1462 (clean) | 0° | 49 | 49 | 0 | yes (pip and chevron edge line up) |
| 1100 (mid clean→16°) | 16° | 46 | 51 | 10 | no |
| 897 (16° detent) | 16° | 46 | 53 | 16 | no (chevron at 46, pip at 53) |
| 450 (mid 16°→33°) | 16° | 46 | 56 | 24 | no |
| 2 (33° full) | 33° | 33 | 59 | 33 | no (chevron at 33, pip at center of donut) |
Notice that at the 16° detent, the chevron edge sits at the
calibrated 46% (from the 16° detent's fLDMAXAOA), but the pip is at
53% (from the two-endpoint lerp). The pip ignores the 16° detent's
calibration.
The lerp's denominator is the full pot span (1462 − 2 = 1460); at the
1100 row, t = (1462 − 1100) / 1460 = 0.248, so
pip = lround(49 + 0.248 × (59 − 49)) = lround(51.48) = 51. Same
formula for every row.
9. Bench verification¶
Reflash bench device with v4.22 firmware on both Gen3 main and M5. Confirm:
- Clean, AOA below L/Dmax: silent, chevron grey, pip and chevron edge coincide on screen.
- Clean, AOA between L/Dmax and OnSpeedFast: low tone pulsing, chevron green, pip and chevron edge still coincide.
- Mid-deployment (lever between 0° and 16° detents): chevron snaps from the clean L/Dmax to the 16° detent's L/Dmax at the midpoint pot value (in lockstep with
iIndexadvancing inFlaps::Update); pip slides smoothly with no jump. - Full flaps (33°), AOA between active L/Dmax and OnSpeedFast: low tone pulsing, chevron green; pip sits up at 59% (inside the donut band on screen), well above the chevron-lit band.
10. Wire-format change history¶
- v4.21 (PR #320): introduced percent anchors;
tonesOnPctLift,onSpeedFastPctLift,onSpeedSlowPctLift,stallWarnPctLiftall snap per active detent. - v4.21+ (PR #327): made
tonesOnPctLiftinterpolate across adjacent detent brackets so the L/Dmax pip slid smoothly. This broke chevron–audio alignment mid-deployment because the chevron is gated on the same field as the audio threshold — making the field interpolated meant the chevron lit at a percent the audio did not match. - v4.22 (PR #336): split the pip out into a new wire field,
pipPctLift.tonesOnPctLiftreverts to the v4.21 snap-per-detent semantics. Frame size grows from 74 to 76 bytes. Chevron and audio fire from the same threshold again. - v4.23 (PR #386):
percentLiftwidens from%02u(integer percent, 0..99) to%03u(tenths of a percent, 0..999) so the index bar advances at sub-pixel temporal smoothness off the 20 Hz frame cadence. The four band-edge anchors stay at integer-percent.lateralGflips to body-frame (positive = right) to match the IMU, SD log, and WebSocket JSON; slip-ball renderers negate locally. Frame size grows from 76 to 77 bytes.
See Display Serial Protocol for the byte-level wire format.