Skip to content

Build a radar-style G meter as the G-Only primary display #136

Open

Description

@PurpleSentinel

Objective

A radar-style graphical G meter that becomes the primary display when G-Only mode
(#132) is selected: a polar plot showing current G, a fading trace of the last few
seconds, and the session maximum held as a marker.

What exists already

More than expected, and it should be reused rather than rewritten:

  • ui::ImuMeterController already produces everything the display needs — current planar
    G, a trail ring buffer, and peaks for acceleration, braking, left, right and total,
    including the position at which total peaked. Host-tested.
  • board::ImuSample is already 6-axis: three accelerometer and three gyroscope axes.
  • ui::GmeterScreen exists from Build G-meter and IMU status views #84.

What is missing

  • The existing screen is a cartesian scatter, not a radar. It places 6 px square
    markers and a peak marker on a flat panel with a text summary beside it. There is no
    polar grid, no G rings, no axis labelling, and no fade along the trace.
  • kImuTrailCapacity is 24 points. At a realistic sample-to-display rate that is
    roughly a second, not "the last few seconds". The capacity needs sizing against the
    chosen update rate rather than left at its current value.
  • No sample source: see Bring up QMI8658, RTC, microSD, and board power diagnostics #12 for the QMI8658 driver. The meter is fed by fixtures only.

Scope

  • Radar rendering: concentric G rings with labelled magnitudes, cardinal axes marked
    for braking, acceleration and left/right cornering
  • Current-G indicator distinct from the trace
  • Trace of the last few seconds, oldest fading, with the capacity sized from the
    chosen sample rate and stated in the code rather than implied
  • Session maximum shown as a held marker at the position it occurred
  • Numeric peaks retained alongside the graphic; a driver reviewing afterwards wants
    the number, not only the picture
  • Full-panel layout when G-Only is the active mode, with no timer or lap furniture
  • Legible in daylight at 311 PPI; sizes chosen in millimetres, per Driver-facing typography is too small to read on the physical panel #127
  • Measure redraw cost. A trace redrawn every frame on a 600x450 panel through a
    96,000-byte stripe buffer is the most demanding rendering the device does.

Exiting G-Only

G-Only has no on-screen controls, so leaving it uses the standard gated route: press
and hold
opens the menu, then Mode, then choose another mode. This is the same gesture
as everywhere else and needs no special case, but it must be verified that the hold is
reachable when the radar occupies the whole panel — a child object that consumes presses
would block it, which is how the ready dashboard's buttons behaved before #133.

Acceptance criteria

  • G-Only renders the radar full-panel with no timer furniture
  • The trace covers a stated number of seconds and fades with age
  • The session maximum is held and visible, and matches the numeric peak
  • Press-and-hold reliably opens the menu from the radar, confirmed on hardware
  • Redraw cost is measured and recorded
  • Readable in daylight on the physical panel

Notes

Activity

  1. added
    enhancementNew feature or request
    priority:highRequired for the current milestone or a correctness/reliability risk
    area:session-uiSession workflow and driver-facing UI
    on Aug 18, 2026
  2. PurpleSentinel commented on Oct 16, 2026

    @PurpleSentinel
    ContributorAuthor

    Status check: the radar is built, but one acceptance item this issue specifically called out is untouched

    Done. GRadarScreen (g_radar_screen.cpp) is the full-panel primary display for G-Only — home_screen() returns radar_root_ for DeviceMode::g_only, with no timer or lap furniture:

    • Concentric G rings with labelled magnitudes — kRingG = {0.5, 1.0, kRadarFullScaleG}
    • Cardinal axes marked, kAxisText
    • Current-G indicator distinct from the trace
    • Trace drawn oldest-first so opacity fades with age (g_radar_screen.cpp:144, floored at 30 so the tail does not vanish)
    • Session maximum held as a marker at the position it occurred, via peaks.total_position
    • Numeric peaks retained alongside the graphic: ACCEL, BRAKE, LEFT, RIGHT, MAX, with -- rather than 0.00 when the IMU is absent, because zero is a legitimate measurement

    Not done — the trail capacity. This issue said:

    kImuTrailCapacity is 24 points. At a realistic sample-to-display rate that is roughly a second, not "the last few seconds". The capacity needs sizing against the chosen update rate rather than left at its current value.

    It is still 24 (imu_meter.hpp:16), and the update rate has since been chosen and can now be measured rather than estimated: service_imu() is called from behind the display cadence gate at kDisplayServicePeriodUs = 50'000, so the meter samples at 20 Hz and 24 points is 1.2 seconds of trace. The prediction in the issue was right, and the number it was waiting on now exists.

    For "the last few seconds" at 20 Hz: 3 s needs 60 points, 5 s needs 100. PlanarAcceleration is 2 floats plus 2 bools, so 100 points is a few hundred bytes of the controller — cheap in memory. The cost is in rendering, which is the other open item.

    Not verified — redraw cost. The final box is unticked and I have no measurement for it:

    Measure redraw cost. A trace redrawn every frame on a 600x450 panel through a 96,000-byte stripe buffer is the most demanding rendering the device does.

    Each trail point is an LVGL object whose opacity is restyled every update. At 24 points that is affordable; at 100 it needs measuring before being assumed, and this is exactly the case the issue flagged. Raising the capacity without that measurement is how the display cadence quietly becomes the bottleneck.

    Remaining scope

    All bench work; no circuit or road test required.

  3. PurpleSentinel commented on Oct 16, 2026

    @PurpleSentinel
    ContributorAuthor

    Correction: the trace increase was merged on a bad measurement and has been reverted

    #211 raised the trace to three seconds on the strength of a render-cost measurement I took incorrectly. #212 reverts it. The radar work this issue describes remains delivered; both boxes I claimed to close are open again.

    The mistake. I measured while moving the device by hand, reasoning that a moving dot dirties more screen area than a parked one. Motion keeps GravityCalibration from completing, so resolve() returns invalid, the meter state never reaches ready, and GRadarScreen::update() takes the branch that hides every dot without positioning or restyling it. The figures were the cost of drawing nothing — and they scaled cleanly from 1,727 to 2,395 us, which is what made them convincing.

    Measured properly, device untouched, which is when the trace is actually drawn:

    24 points 60 points
    object work, mean 4,889–6,252 us 13,658–18,303 us
    object work, worst 8,386 us 20,677 us
    share of the 50 ms display period 12–16% 36–41%
    worst repaint 38,532–41,095 us 53,101–55,442 us
    worst repaint vs the 25 ms service period 154–164% 212–221%
    repaints per 10 s 147–152 166–170

    Three times the object work and 14 ms more on the worst repaint. Object cost grows faster than linearly in the dot count, which fits LVGL's invalid-area list saturating and joining rather than a fixed overhead plus a per-dot cost.

    What this issue is now blocked on

    The remaining scope stands, but the order has changed — the capacity cannot be raised first:

    • Make the trace cheaper to draw. At the current 24 points a repaint already costs ~40 ms against a 25 ms service period, 147–152 times per 10 s, on the task that also dispatches the GNSS poll. At 25 Hz a fix is due every 40 ms. Nothing visibly fails today and there is no margin either. Sixty individual LVGL objects, each invalidating its own area, is the wrong shape for this; a single canvas or a line-drawn trace would invalidate once
    • Then size kImuTrailCapacity from the cadence, with the arithmetic stated in the code
    • Re-measure and confirm the display cadence holds

    The instrumentation that produced these figures went out with the revert, deliberately, to put main back to a known state rather than keep the half of the change I still think is right. Re-landing it on its own — no capacity change — is worth doing first, since nothing above can be verified without it.

    Note to self, recorded because it cost a merge and a revert

    Measure the path that actually runs. An early-out that skips the expensive work will produce clean, plausible, well-scaling numbers for the wrong branch, and those are harder to doubt than obviously broken ones.

  4. PurpleSentinel commented on Oct 16, 2026

    @PurpleSentinel
    ContributorAuthor

    Blocked on #213, which carries the repaint measurement: a single repaint costs 38.5-41.1 ms against a 25 ms service period at the current 24 trail points, so the trace cannot grow until the drawing is cheaper.

    Drawing the trace as one object rather than 24 individually positioned dots is likely to resolve both: one invalidation instead of 24, and a capacity that stops being the thing that drives render cost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:session-uiSession workflow and driver-facing UIenhancementNew feature or requestpriority:highRequired for the current milestone or a correctness/reliability risk

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions