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>
7.2 KiB
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 rtcbool rtcAvailable— wurde Chip beim Boot via I²C gefundenbool ntpEverSynced— gab es seit Boot mind. einen erfolgreichen NTP-Synctime_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 2025–2099). 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/timezeigtrtc_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 insetupRTC()einmal aufgerufen und ist idempotent. rtc.begin()-Verhalten: Returnt auf manchen Library-Versionentrueauch ohne Hardware. Zusätzlichrtc.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/timeim Frontend prüfen