onlyn00bs-badge

OnlyN00bs: a DEF CON 34 friend-finder badge. ESP32 firmware, Web Bluetooth setup app, printable case
git clone https://git.virtualshack.io/onlyn00bs-badge.git
Log | Files | Refs | README | LICENSE

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 };