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>
This commit is contained in:
152
docs/superpowers/specs/2026-05-03-rtc-pcf8523-fallback-design.md
Normal file
152
docs/superpowers/specs/2026-05-03-rtc-pcf8523-fallback-design.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# 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 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:
|
||||
|
||||
```cpp
|
||||
// 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
Reference in New Issue
Block a user