Files
AquaMasterMQTT/docs/superpowers/specs/2026-05-03-rtc-pcf8523-fallback-design.md
Carsten Graf 96fcb74c80 docs: add design spec for PCF8523 RTC fallback
New header rtcsync.h providing persistent time storage and offline
fallback when NTP is unavailable. Soft-fails when hardware is absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 14:44:40 +02:00

7.2 KiB
Raw Blame History

RTC-Fallback (PCF8523) — Design

Datum: 2026-05-03 Status: Entwurf Scope: Neuer Header src/rtcsync.h für Adafruit Adalogger FeatherWing (PCF8523) als persistente Zeitquelle mit NTP-Sync und Fallback bei fehlendem Internet.

Ziel

Der AquaMaster soll nach jedem Boot eine plausible Uhrzeit haben, auch ohne WiFi-STA-Verbindung. NTP bleibt die primäre Quelle; die RTC dient als persistenter Speicher (überlebt Power-Off) und als Fallback im AP-only-Betrieb.

Hardware ist optional — Geräte ohne FeatherWing müssen unverändert funktionieren.

Nicht-Ziele

  • Keine Drift-Kompensation in Software (PCF8523-Trim-Register werden nicht angefasst)
  • Kein RTC-Alarm-/Interrupt-Handling
  • Keine Persistenz anderer Daten als der Uhrzeit (SD-Slot des FeatherWing wird ignoriert)
  • Kein Migrations-Pfad für andere RTC-Chips (DS3231 etc.) — explizit auf PCF8523 zugeschnitten

Architektur

Neuer Header: src/rtcsync.h

Header-only nach bestehendem Pattern, wird nur in master.cpp inkludiert. Definiert eigene globale Objekte:

  • RTC_PCF8523 rtc
  • bool rtcAvailable — wurde Chip beim Boot via I²C gefunden
  • bool ntpEverSynced — gab es seit Boot mind. einen erfolgreichen NTP-Sync
  • time_t lastNtpSyncEpoch — Zeitpunkt des letzten NTP-Erfolgs (UTC, für 24h-Timer und Status-Anzeige)
  • bool lastStaConnected — Edge-Detection für STA-Reconnect

Library

adafruit/RTClib (zieht adafruit/Adafruit BusIO transitiv mit) — ergänzt in platformio.ini unter lib_deps aller relevanten Envs.

Schnittstelle (Header-Funktionen)

Funktion Zweck
void setupRTC() I²C-Init, PCF8523 detektieren (rtc.begin() + rtc.initialized()-Check), bei Erfolg RTC-Zeit als initialen Fallback in Systemzeit übernehmen
bool syncFromNTP() Ruft existierendes syncTimeWithNTP() aus timesync.h auf; bei Erfolg → schreibt UTC in RTC, setzt ntpEverSynced=true, aktualisiert lastNtpSyncEpoch. Return: ob NTP erfolgreich war.
void loopRTC() Aus loop() aufgerufen: erkennt STA-Reconnect-Edge (Übergang lastStaConnected: false → true) und 24h-Periodik → ruft syncFromNTP(). Reine Vergleichsoperationen, nicht-blockierend bis NTP tatsächlich getriggert wird.
void persistSystemTimeToRTC(time_t t) Schreibt Systemzeit in RTC, nur wenn rtcAvailable && t >= 1735689600 && t < 4102444800 (Jahr 20252099). Wird via Weak-Hook von setSystemTime() aufgerufen.
void appendTimeStatus(JsonDocument &doc) Hängt Felder rtc_available, rtc_synced_from_ntp, last_ntp_sync_ago_s, rtc_time_utc an die Antwort von /api/time an (Implementierung des Weak-Hooks aus timesync.h).

Kopplung mit timesync.h via Weak-Symbol

timesync.h darf rtcsync.h nicht kennen (Geräte ohne RTC sollen ohne Code-Änderung funktionieren). Lösung:

// in timesync.h, am Ende von setSystemTime() vor return true:
extern "C" void onSystemTimeSet(time_t t) __attribute__((weak));
if (onSystemTimeSet) onSystemTimeSet(timestamp);

// in rtcsync.h:
extern "C" void onSystemTimeSet(time_t t) {
    persistSystemTimeToRTC(t);
}

Wenn rtcsync.h aus master.cpp weggelassen wird, ist onSystemTimeSet unaufgelöst → Weak-Default ist Nullpointer → if-Check verhindert Aufruf. Geräte ohne RTC kompilieren und laufen unverändert.

Analog für getCurrentTimeJSON(): Weak-Hook void appendTimeStatus(JsonDocument&) der von rtcsync.h überschrieben wird.

Boot-Flow (master.cpp::setup())

Reihenfolge — setupRTC() kommt vor WiFi, damit Systemzeit so früh wie möglich plausibel ist:

SPIFFS
→ API-Setups
→ load*() aus Preferences
→ setupRTC()                        ← NEU
   └─ wenn rtcAvailable: settimeofday(rtc.now().unixtime())
→ WiFi (AP/STA)
→ wenn STA online: syncFromNTP()    ← überschreibt RTC mit NTP-Wert
→ ...rest unverändert

Im AP-only-Betrieb bleibt es bei der RTC-Zeit. Im STA-Modus wird sie sofort durch NTP übersteuert.

Loop-Flow (master.cpp::loop())

loopRTC() als erste Zeile in loop() — billig (zwei Vergleiche), nicht-blockierend bis tatsächlich ein Sync ausgelöst wird. Der NTP-Sync selbst ist blockierend (max. 5 s wie heute), passiert aber selten:

  • bei STA-Reconnect (Übergang false→true von WiFi.isConnected())
  • alle 24 h (time(NULL) - lastNtpSyncEpoch >= 86400)

Zeitzone

UTC in der RTC. Schreiben mit rtc.adjust(DateTime((uint32_t)t)), lesen mit rtc.now().unixtime(). Systemzeit ist POSIX-intern auch UTC; der TZ-Offset (configTime(3600, 0, ...)) wirkt nur auf localtime()-Konversion. Daher kein +/-3600 nötig.

Plausibilitäts-Check

persistSystemTimeToRTC() schreibt nur, wenn der Timestamp im Bereich [2025-01-01, 2099-01-01) liegt. Verhindert Bugs mit Timestamp=0 oder negativen Werten, ist aber lose genug, dass jede vom Browser gesetzte Zeit durchkommt (Browser-Sync-Workflow soll erhalten bleiben).

Konkret: untere Grenze 1735689600 (= 2025-01-01 UTC), obere Grenze 4102444800 (= 2100-01-01 UTC).

Soft-Fail-Verhalten

Wenn rtc.begin() fehlschlägt:

  • rtcAvailable = false, Warnung in Serial
  • Alle RTC-Schreib-/Lese-Operationen werden no-op (geprüft via rtcAvailable)
  • NTP-Sync funktioniert weiter wie bisher
  • /api/time zeigt rtc_available: false

Es gibt keine automatische Re-Detection. Wenn die RTC nachträglich angesteckt wird, ist ein Reboot nötig.

API-Erweiterung

/api/time (GET) bekommt zusätzliche Felder:

{
  "timestamp": 1746288000,
  "success": true,
  "formatted": "2026-05-03 14:00:00",
  ...,
  "rtc_available": true,
  "rtc_synced_from_ntp": true,
  "last_ntp_sync_ago_s": 1234,
  "rtc_time_utc": 1746288001
}

rtc_time_utc ist der direkt aus dem Chip gelesene Wert (zur Diagnose, falls Systemzeit und RTC divergieren).

Keine neuen Endpunkte. Bewusste Entscheidung gegen /api/rtc/set, weil der bestehende set browser time-Workflow über setSystemTime() jetzt automatisch in die RTC durchschlägt.

Geänderte Dateien

Datei Änderung
src/rtcsync.h Neu — komplette RTC-Logik
src/master.cpp Include rtcsync.h; setupRTC() in setup() (vor WiFi); loopRTC() in loop() (erste Zeile)
src/timesync.h Weak-Hook onSystemTimeSet() am Ende von setSystemTime(); Weak-Hook appendTimeStatus() in getCurrentTimeJSON()
platformio.ini adafruit/RTClib in lib_deps aller Envs ergänzen

Risiken & Trade-offs

  • PCF8523-Drift (~±20 ppm, ≈1.7 s/Tag): Akzeptabel, weil bei jedem STA-Reconnect und mind. alle 24 h nachsyncronisiert wird.
  • I²C-Bus geteilt mit PN532: Keine Adresskollision (PN532=0x24, PCF8523=0x68). Bus-Init muss vor beiden Devices passieren — Wire.begin() wird in setupRTC() einmal aufgerufen und ist idempotent.
  • rtc.begin()-Verhalten: Returnt auf manchen Library-Versionen true auch ohne Hardware. Zusätzlich rtc.initialized() und ein Test-Read prüfen.
  • Weak-Symbol-Pattern: Funktioniert mit GCC (ESP32-Toolchain ist GCC) — keine Portabilitätsbedenken in diesem Projekt.

Was explizit nicht getestet wird

Es gibt im Projekt keine Test-Suite (test/ ist leer). Verifikation erfolgt durch:

  • Build über alle Envs (pio run)
  • Manueller Test auf Hardware mit/ohne FeatherWing
  • Serial-Log-Checks für Boot-Flow
  • /api/time im Frontend prüfen