From 8cdf9c05cc7b602b53d77a7fc5cf9e32cc48ae41 Mon Sep 17 00:00:00 2001 From: PurpleSentinel <199351535+PurpleSentinel@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:40:55 +0100 Subject: [PATCH] fix(ui): report the receiver as it is now, not as it was at boot With 16 satellites and a 24.9 Hz fix stream the dashboard still showed what was true at 2.8 seconds, and Race mode said "lap times pending GNSS" throughout. ready_snapshot read the health live and correctly. refresh_ready was the problem: it is reached from boot, a settings save and a track change, and from nowhere else. The receiver acquires its first fix after the boot sequence has already drawn the screen - I (2810) timing: runtime started, fixes now reach the engine <- drawn here I (2990) gnss: fix accepted (sats 16, ...) <- fix after - so the dashboard drew a receiver that was not yet working and never corrected itself. Storage and IMU readiness had the same fault and merely happened to be ready by then. Refreshed from the display cadence now, gated twice: only while the dashboard is the active screen, and only when a fingerprint of the drawn state has changed. An idle dashboard costs a struct comparison. The gating is not tidiness - a repaint costs about 40 ms against a 25 ms service period (#213), so an unconditional refresh at 20 Hz would have spent the device's most expensive operation on producing an identical screen, and made #213 worse. The Race note was the fixed string "RACE - lap times pending GNSS". Honest while nothing consumed fixes, and since the timing runtime was wired both wrong and the most alarming thing the screen could say while everything worked. It now distinguishes no circuit armed, waiting for a fix, and lap times live, and is re-derived whenever the fingerprint moves. logging_available and session_active were hardcoded to false in the same snapshot. Logging is available once there is somewhere to write, and whether a session is running is something the router already tracks. Verified on hardware: the dashboard reads LAP TIMING READY, which prints only when gnss_health is good and the track state is selected, so both are being read live rather than carried from boot. Closes #217 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 15 +++++++ firmware/main/screen_router.cpp | 77 ++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 102931a..12ce06f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -288,6 +288,21 @@ The project follows Semantic Versioning once the first firmware release is tagge ### Fixed +- the ready dashboard **reports the receiver as it is now, not as it was at boot**. + `ready_snapshot` read the health live and correctly, but `refresh_ready` was only + reached from boot, a settings save and a track change, and the receiver acquires its + first fix after the boot sequence has already drawn the screen. With 16 satellites and a + 24.9 Hz fix stream the dashboard still showed what was true at 2.8 seconds. It now + refreshes from the display cadence, gated on the dashboard being the active screen and + on a fingerprint of the drawn state having changed - an idle dashboard costs a struct + comparison, because a repaint costs about 40 ms against a 25 ms service period (#213) +- Race mode no longer claims **`lap times pending GNSS`** whatever the receiver is doing. + Honest while nothing consumed fixes; since the timing runtime was wired it was both + wrong and the most alarming thing the screen could say while everything worked. The note + now distinguishes no circuit armed, waiting for a fix, and lap times live +- `logging_available` and `session_active` on the dashboard are read rather than + hardcoded to `false` + - the **GPS session trigger now starts a session**. Choosing it armed the device and showed `WAITING FOR THE LINE`, and nothing could ever start the session: only a manual START and IMU launch detection reached `begin_session`. The mechanism existed - `gate_automation` diff --git a/firmware/main/screen_router.cpp b/firmware/main/screen_router.cpp index 90fe0f2..e380d1c 100644 --- a/firmware/main/screen_router.cpp +++ b/firmware/main/screen_router.cpp @@ -596,6 +596,7 @@ class ScreenRouter { } last_display_service_us_ = now_us; service_imu(); + service_ready_dashboard(); service_roller(); if (capturing_) { // Drawn at the display's cadence; the controller above is fed every tick. @@ -1589,9 +1590,22 @@ class ScreenRouter { trackday_->set_mode_note("TRACK DAY - lap times available in Review"); break; case ui::DeviceMode::race: - // Race wants lap times and a delta alongside the countdown. Neither exists - // without GNSS, so it borrows the countdown and says so. - trackday_->set_mode_note("RACE - lap times pending GNSS"); + // Race wants lap times and a delta alongside the countdown, and both need a + // circuit armed and a receiver producing fixes. This used to be the fixed + // string "lap times pending GNSS", which was honest while nothing consumed + // fixes and became the most alarming thing the screen could say once laps + // worked. Each of the three states gets its own words, so a driver can tell + // which of them they are in. + if (!track_armed_) { + trackday_->set_mode_note("RACE - no circuit armed for lap times"); + } + else if (gnss_pipeline_.health(esp_timer_get_time()) != + domain::GnssHealth::good) { + trackday_->set_mode_note("RACE - waiting for a fix"); + } + else { + trackday_->set_mode_note("RACE - lap times live"); + } break; case ui::DeviceMode::g_only: trackday_->set_mode_note("G-ONLY - session timer"); @@ -1700,8 +1714,10 @@ class ScreenRouter { snapshot.storage = storage::mounted() ? ui::Readiness::ready : ui::Readiness::unavailable; snapshot.imu = imu::running() ? ui::Readiness::ready : ui::Readiness::unavailable; - snapshot.logging_available = false; - snapshot.session_active = false; + // Both were hardcoded. Logging is available once there is somewhere to write, and + // whether a session is running is something the router already tracks. + snapshot.logging_available = storage::mounted(); + snapshot.session_active = session_was_active_; if (track_armed_) { std::strncpy(snapshot.selected_track.data(), active_track_.name.data(), @@ -1725,6 +1741,53 @@ class ScreenRouter { return snapshot; } + // Everything on the dashboard that changes without anybody touching the device. + // Compared rather than redrawn: a repaint costs about 40 ms against a 25 ms service + // period (#213), so repainting a dashboard that has not changed would spend the + // device's most expensive operation on producing an identical screen. + struct ReadyFingerprint { + domain::GnssHealth gnss_health{domain::GnssHealth::unavailable}; + ui::Readiness storage{ui::Readiness::unavailable}; + ui::Readiness imu{ui::Readiness::unavailable}; + ui::ReadyTrackState track_state{ui::ReadyTrackState::none}; + bool logging_available{false}; + bool session_active{false}; + + [[nodiscard]] bool operator==(const ReadyFingerprint& other) const noexcept + { + return gnss_health == other.gnss_health && storage == other.storage && + imu == other.imu && track_state == other.track_state && + logging_available == other.logging_available && + session_active == other.session_active; + } + }; + + [[nodiscard]] ReadyFingerprint ready_fingerprint() const noexcept + { + const auto snapshot = ready_snapshot(); + return {snapshot.gnss_health, snapshot.storage, snapshot.imu, + snapshot.track_state, snapshot.logging_available, snapshot.session_active}; + } + + // The receiver acquires its first fix after the boot sequence has already drawn the + // dashboard, so without this the screen reports a receiver that was not yet working + // and never corrects itself. refresh_ready is otherwise only reached from boot, a + // settings save and a track change - none of which a driver waiting for a fix does. + void service_ready_dashboard() noexcept + { + if (ready_ == nullptr || lv_screen_active() != screen_for(ui::Destination::ready)) { + return; + } + const auto fingerprint = ready_fingerprint(); + if (fingerprint == drawn_ready_fingerprint_) { + return; + } + drawn_ready_fingerprint_ = fingerprint; + refresh_ready(); + // The Race note is derived from the same state, so it goes stale the same way. + describe_mode(); + } + void refresh_ready() noexcept { if (ready_ != nullptr) { @@ -2512,6 +2575,10 @@ class ScreenRouter { // Everything that is not the GNSS poll runs at this period rather than the tick's. static constexpr std::int64_t kDisplayServicePeriodUs = 50'000; std::int64_t last_display_service_us_{0}; + // Deliberately default-constructed rather than seeded from the first reading, so the + // first service tick after boot always draws once and corrects whatever the boot + // sequence put on screen before the receiver had a fix. + ReadyFingerprint drawn_ready_fingerprint_{}; gnss::GnssTransport* gnss_transport_{nullptr}; gnss::GnssPipeline gnss_pipeline_{}; gnss::GnssConfigurationReport gnss_configuration_{};