points_history.h (4757B)
1 #pragma once 2 #include <stdint.h> 3 #include <Preferences.h> 4 #include "config.h" 5 6 // ───────────────────────────────────────────────────────────────────────── 7 // Points history — timestamped samples of the cumulative points score, for 8 // the line graph on the POINTS view (x = wall-clock time over a fixed 5-day 9 // window, y = points). 10 // 11 // Each sample carries its wall-clock epoch (from the soft-clock) so points can 12 // be placed at their true time on the event axis. The buffer is persisted to 13 // NVS and restored on boot, so the graph survives reboots. Because the board 14 // has no RTC (clock.h doesn't count power-off time), the first sample after a 15 // reboot is flagged `seg_start` — the renderer breaks the line there to mark 16 // "rebooted here" (we can show the break, but not the true off-duration). If 17 // the clock was never synced, samples carry epoch 0 and the renderer falls 18 // back to uniform spacing. 19 // 20 // Sampled at most once per POINTS_HISTORY_SAMPLE_MS (hourly), so LEN samples 21 // span the 5-day axis. 22 // ───────────────────────────────────────────────────────────────────────── 23 24 struct PointSample { 25 uint32_t epoch; // wall-clock seconds (0 = clock unsynced when sampled) 26 uint16_t points; // cumulative score at that time 27 uint8_t seg_start; // 1 if a reboot/discontinuity precedes this sample 28 }; 29 30 class PointsHistory { 31 public: 32 // Append the current score at most once per sample interval. `epoch` is 33 // g_clock.now() (0 if unsynced); `now_ms` is millis(). Returns true iff a new 34 // sample was actually stored (so the caller can persist only when it changed). 35 bool sample(uint16_t points, uint32_t epoch, uint32_t now_ms) { 36 if (_last_ms != 0 && now_ms - _last_ms < POINTS_HISTORY_SAMPLE_MS) return false; 37 _last_ms = now_ms; 38 _buf[_head] = { epoch, points, (uint8_t)(_pending_break ? 1 : 0) }; 39 _pending_break = false; 40 _head = (_head + 1) % POINTS_HISTORY_LEN; 41 if (_count < POINTS_HISTORY_LEN) _count++; 42 return true; 43 } 44 45 // Copy samples oldest→newest into `out` (capacity `max`); returns the count. 46 int snapshot(PointSample* out, int max) const { 47 if (max <= 0) return 0; 48 int n = _count, skip = 0; 49 if (n > max) { skip = n - max; n = max; } 50 int start = (_head - _count + skip + POINTS_HISTORY_LEN) % POINTS_HISTORY_LEN; 51 for (int i = 0; i < n; i++) 52 out[i] = _buf[(start + i) % POINTS_HISTORY_LEN]; 53 return n; 54 } 55 56 // Persist / restore the whole ring to NVS (mirrors PointsLedger). Restoring a 57 // non-empty buffer arms a break before the next sample — that's the reboot. 58 void save(Preferences& p) const { 59 p.putBytes("phist", _buf, sizeof(_buf)); 60 p.putUInt("phist_h", (uint32_t)_head); 61 p.putUInt("phist_c", (uint32_t)_count); 62 } 63 void load(Preferences& p) { 64 if (p.getBytes("phist", _buf, sizeof(_buf)) != sizeof(_buf)) return; 65 _head = (int)p.getUInt("phist_h", 0) % POINTS_HISTORY_LEN; 66 _count = (int)p.getUInt("phist_c", 0); 67 if (_count > POINTS_HISTORY_LEN) _count = POINTS_HISTORY_LEN; 68 _pending_break = (_count > 0); // a reboot just happened → break the line 69 } 70 71 private: 72 PointSample _buf[POINTS_HISTORY_LEN] = {}; 73 int _head = 0; // next write index 74 int _count = 0; // valid samples (≤ LEN) 75 uint32_t _last_ms = 0; // last accepted sample time (millis) 76 bool _pending_break = false; 77 }; 78 79 // Fine-grained recent ring for the "last 1 hour" POINTS view. 1-minute cadence, 80 // in-RAM only (volatile recent detail — resets on reboot, never persisted, no 81 // reboot-break bookkeeping). POINTS_RECENT_LEN × POINTS_RECENT_SAMPLE_MS = 1 h. 82 class PointsRecent { 83 public: 84 bool sample(uint16_t points, uint32_t epoch, uint32_t now_ms) { 85 if (_last_ms != 0 && now_ms - _last_ms < POINTS_RECENT_SAMPLE_MS) return false; 86 _last_ms = now_ms; 87 _buf[_head] = { epoch, points, 0 }; 88 _head = (_head + 1) % POINTS_RECENT_LEN; 89 if (_count < POINTS_RECENT_LEN) _count++; 90 return true; 91 } 92 int snapshot(PointSample* out, int max) const { 93 if (max <= 0) return 0; 94 int n = _count, skip = 0; 95 if (n > max) { skip = n - max; n = max; } 96 int start = (_head - _count + skip + POINTS_RECENT_LEN) % POINTS_RECENT_LEN; 97 for (int i = 0; i < n; i++) out[i] = _buf[(start + i) % POINTS_RECENT_LEN]; 98 return n; 99 } 100 private: 101 PointSample _buf[POINTS_RECENT_LEN] = {}; 102 int _head = 0; 103 int _count = 0; 104 uint32_t _last_ms = 0; 105 };