clock.h (3815B)
1 #pragma once 2 #include <Arduino.h> 3 #include <Preferences.h> 4 #include "config.h" 5 6 // ───────────────────────────────────────────────────────────────────────── 7 // Soft real-time clock. 8 // 9 // The FireBeetle 2 ESP32-E badge board has no RTC, so we seed wall-clock time from the 10 // phone over BLE during provisioning, then free-run off millis(). To survive 11 // reboots without re-syncing, we checkpoint the current epoch to NVS every 12 // CLOCK_PERSIST_MS and resume from there on boot (downtime isn't counted — 13 // fine for a badge that runs continuously through a con). 14 // 15 // If never synced, now() returns 0 and synced() is false → the report labels 16 // timestamps as relative rather than wall-clock. 17 // 18 // A brownout reset means we were dark for an unknown span, so the resumed epoch is 19 // skewed by that downtime. setup() then calls mark_stale(): synced() goes false 20 // (report falls back to relative) and stale() drives a "re-sync" nudge, until a 21 // fresh BLE sync (OPTIONS → Provisioning Mode → hosted page) restores trust. The stale 22 // flag is persisted, so a later clean power-cycle can't silently un-stale it. 23 // ───────────────────────────────────────────────────────────────────────── 24 class SoftClock { 25 public: 26 void begin() { 27 _p.begin("clock", false); 28 _base_epoch = _p.getUInt("epoch", 0); 29 _stale = _p.getUChar("stale", 0) != 0; 30 _base_ms = millis(); 31 _last_persist = millis(); 32 } 33 34 // "synced" = we hold a wall-clock we TRUST. False if never seeded, OR if marked 35 // stale (e.g. resumed-from-NVS after a brownout, when the badge was dark for an 36 // unknown span) — callers then fall back to relative time. A BLE re-sync restores it. 37 bool synced() const { return _base_epoch != 0 && !_stale; } 38 39 // Epoch is present but no longer trustworthy (downtime-skewed). Drives the nudge. 40 bool stale() const { return _base_epoch != 0 && _stale; } 41 42 uint32_t now() const { 43 if (!_base_epoch) return 0; 44 return _base_epoch + (millis() - _base_ms) / 1000; 45 } 46 47 // Called from the BLE time characteristic — a fresh sync restores trust. 48 void set(uint32_t epoch) { 49 _base_epoch = epoch; 50 _base_ms = millis(); 51 _stale = false; 52 _p.putUInt("epoch", epoch); 53 _p.putUChar("stale", 0); 54 } 55 56 // Mark the persisted epoch untrusted. Persisted so it survives further reboots 57 // until a re-sync clears it — a clean power-cycle after a brownout must not 58 // silently "un-stale" a clock that's still skewed by the dark period. 59 void mark_stale() { 60 if (!_base_epoch || _stale) return; 61 _stale = true; 62 _p.putUChar("stale", 1); 63 } 64 65 // Factory reset: wipe the persisted epoch + stale flag. The clock lives in its OWN NVS 66 // namespace ("clock"), so main.cpp's prefs.clear() on the "badge" namespace does NOT 67 // reach it — without this a "wiped" badge still carries the previous owner's wall clock. 68 // Caller is expected to restart immediately; the in-RAM state is reset too regardless. 69 void factory_clear() { 70 _p.clear(); 71 _base_epoch = 0; 72 _base_ms = millis(); 73 _stale = false; 74 } 75 76 // Call often; checkpoints to NVS at most once per CLOCK_PERSIST_MS. 77 void tick() { 78 if (!_base_epoch) return; 79 if (millis() - _last_persist >= CLOCK_PERSIST_MS) { 80 _last_persist = millis(); 81 _p.putUInt("epoch", now()); 82 } 83 } 84 85 private: 86 Preferences _p; 87 uint32_t _base_epoch = 0; 88 uint32_t _base_ms = 0; 89 uint32_t _last_persist = 0; 90 bool _stale = false; // epoch present but downtime-skewed → untrusted 91 };