Fix long-uptime clock display bugs (colon stalls, stray PM/seconds dot)#330
Fix long-uptime clock display bugs (colon stalls, stray PM/seconds dot)#330Bullpuph123 wants to merge 3 commits into
Conversation
- GlobalTime: drift-free rollover-safe 1s scheduler (advance by whole periods instead of resetting to millis()), so displayed seconds are never skipped due to accumulated sampling drift - ClockWidget: colon now blinks on a dedicated 500ms millis()-based timer instead of seconds parity; AM/PM only redrawn on change to stop the per-second TTF font reload cycle that fragmented the heap over days; middle/status orb ownership of colon/ticks/AM-PM made explicit with bounds guards on orb indexes and seconds values - ScreenManager: deselect all CS lines before selecting the target screen and validate the screen index, so draws can never land on an adjacent orb - Add host-side simulation (firmware/test/host_sim_long_uptime.py) covering millis() rollover, blocked loops and 24h blink drift Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After hours of uptime the colon degraded to 1s on / 1s off: when the NTP server stops responding, NTPClient::forceUpdate() blocks ~1010ms and never advances _lastUpdate on failure, so NTPClient::update() re-attempts the blocking call on every invocation. With GlobalTime ticking once per second, the whole render loop degraded to ~1 iteration/second, and the colon (one toggle max per draw) blinked at half rate. A reboot picked a responsive server and masked the bug. - GlobalTime: gate NTP attempts to once per 61s (5s until first sync), tracked regardless of success, and log failed attempts; timekeeping continues via millis() between syncs - ClockWidget: colon catch-up now toggles by period parity so loop stalls cannot invert the blink phase - Host sim: new check reproduces the old failure (median toggle ~1010ms) and verifies zero phase errors with the fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Follow-up fix pushed (b8becdf): during on-hardware soak testing, the colon blink degraded from 500ms to 1s on / 1s off after several hours of uptime — a new reproduction of the original "works after reboot, degrades over time" pattern, this time with a different root cause. Root cause: when the NTP server stops responding (pool rotation, router NAT timeout, etc.), Changes:
Verified on hardware: builds clean, flashed, normal boot with WiFi/timezone intact, stable 500ms blink. |
…ISRs - Gate getTimeZoneOffsetFromAPI() attempts (30s until first success, 5min after a failed refresh): it is a blocking HTTP call and timezonedb rate-limits to ~1 req/s, so retrying every 1s tick stalled the render loop and guaranteed rate-limit failures at boot and DST changeover. - Initialize the loop index in WidgetSet::updateAll() (was reading an uninitialized int8_t - undefined behavior on every boot). - Add IRAM_ATTR to the button ISR wrappers and Button::isrButtonChange() so a button edge during a flash write cannot crash with "Cache disabled but cached memory region accessed". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Follow-up commit 0c4fbf1 fixes three more interaction bugs found while reviewing how the event loops interact: 1. Timezone API retry storm (same class as the NTP bug above) 2. Uninitialized loop index in 3. Button ISRs not placed in IRAM Build verified (RAM 16.2%, Flash 77.2%) and the host-side timing simulation in |
|
I updated the target branch to be |
|
Thanks for the support and for retargeting to dev! As requested, I've split this into three focused PRs, each re-implemented directly on top of dev (rather than resolving conflicts here, since dev's ConfigManager/WidgetTimer architecture diverged quite a bit from main):
One note: the uninitialized loop index fix in WidgetSet::updateAll from this PR was dropped — it's already fixed on dev. Closing this in favor of the three above. |
Summary
m_updateTimer = millis()after a> 1000mscheck, so its period drifted a few ms per cycle relative to the NTP epoch's exact 1000ms grid (samemillis()clock). Each time the drift crossed a second boundary, a second was skipped — and since the colon toggled on seconds parity, it froze for 2+s at irregular intervals. Now advances by whole periods (rollover-safe, drift-free) so seconds are never skipped.millis()-based timer (stable 1s cycle) instead of seconds parity; AM/PM is only redrawn when it changes, eliminating the per-second TTF font unload/reload cycle (~86k FreeType heap alloc cycles/day) that fragmented the heap over multi-day uptimes; middle/status orb ownership of colon/seconds tick/AM-PM is explicit with bounds guards on orb indexes and seconds values;update()uses rollover-safe unsigned elapsed-time math and avoids per-loop String allocations.selectScreen()deselects all CS lines before asserting the target and validates the index, so two panels can never be briefly selected during a transition (stray pixels latched into adjacent orbs).firmware/test/host_sim_long_uptime.py) that replicates the exact uint32 arithmetic: the old scheduler skips 53 seconds in 3 simulated hours; the new logic shows 0 skips including across the 49.7-daymillis()rollover, holds a 500ms colon cadence with 2ms cumulative drift over 24h, and keeps all seconds-dot draws bounded to the middle orb.Test plan
python firmware/test/host_sim_long_uptime.py(all checks pass, incl. rollover boundary and blocked-loop catch-up)pio run -e esp32doit-devkit-v1builds clean (no new warnings)🤖 Generated with Claude Code