Ten tasks covering dependency setup, header creation, weak-hook decoupling from timesync.h, master.cpp wiring, and hardware verification on real device. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
RTC PCF8523 Fallback Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a new header src/rtcsync.h that uses an Adafruit Adalogger FeatherWing (PCF8523) as persistent time storage and as a fallback time source when NTP is unavailable, while remaining fully optional (soft-fail without hardware).
Architecture: Header-only module following the existing AquaMaster pattern. Decoupled from timesync.h via GCC weak symbols, so devices without RTC compile and run unchanged. Time is stored in the RTC as UTC (POSIX epoch). NTP remains the authoritative source; RTC is consulted only at boot before WiFi comes up. Manual time sets (e.g. browser-time button) are persisted to RTC with a plausibility check.
Tech Stack: ESP32 / Arduino-Framework / PlatformIO, adafruit/RTClib, ArduinoJson@^7.4.1, existing timesync.h API.
Spec: docs/superpowers/specs/2026-05-03-rtc-pcf8523-fallback-design.md
Verification model: This project has no unit-test suite (test/ is empty). "Tests" in this plan mean: (1) build success across all PlatformIO envs, (2) serial-log behavior on real hardware, (3) HTTP-API checks via curl/browser. Each task ends with a verification step appropriate for the change.
File Structure
| File | Role |
|---|---|
src/rtcsync.h |
NEW — header-only RTC module (globals + setupRTC, loopRTC, syncFromNTP, persistSystemTimeToRTC, weak-hook overrides) |
src/timesync.h |
Add two weak-symbol declarations + invocations (no behavioral change without rtcsync.h) |
src/master.cpp |
Include rtcsync.h; call setupRTC() before setupWifi(); call loopRTC() at start of loop() |
platformio.ini |
Add adafruit/RTClib to lib_deps of every env |
Task 1: Add RTClib dependency to all PlatformIO envs
Files:
-
Modify:
platformio.ini(lines 30, 50, 70, 87, 105, 129 —lib_depsblocks of all six envs) -
Step 1: Add
adafruit/RTClib@^2.1.4to everylib_depsblock
In platformio.ini, append the line adafruit/RTClib@^2.1.4 (tab-indented like the other entries) to each of the six lib_deps sections. Concretely, each block should become:
lib_deps =
bblanchon/ArduinoJson@^7.4.1
esp32async/ESPAsyncWebServer@^3.7.7
esp32async/AsyncTCP@^3.4.2
mlesniew/PicoMQTT@^1.3.0
adafruit/Adafruit PN532@^1.3.4
adafruit/RTClib@^2.1.4
Apply this edit to envs: wemos_d1_mini32, esp32thing_OTA, esp32thing, esp32thing_CI, um_feathers3, um_feathers3_debug.
- Step 2: Verify CI env builds with new dependency
Run: pio run -e esp32thing_CI
Expected: Library is downloaded (look for Library Manager: Installing adafruit/RTClib), build succeeds with SUCCESS line. No code change yet, so binary size should be essentially unchanged.
- Step 3: Commit
git add platformio.ini
git commit -m "build: add RTClib dependency for PCF8523 RTC support"
Task 2: Create src/rtcsync.h skeleton with RTC detection (soft-fail)
Files:
-
Create:
src/rtcsync.h -
Step 1: Create the skeleton file
Create src/rtcsync.h with the following content. This is the smallest possible vertical slice — only detection, no time-handling yet:
// PCF8523-RTC-Modul mit NTP-Sync und Fallback.
// Header-only nach Projekt-Pattern: Globale Objekte werden hier definiert.
// Dieser Header darf NUR in master.cpp inkludiert werden.
#pragma once
#include <Arduino.h>
#include <RTClib.h>
#include <Wire.h>
// Globale RTC-Instanz und Status-Flags
RTC_PCF8523 rtc;
bool rtcAvailable = false;
bool ntpEverSynced = false;
time_t lastNtpSyncEpoch = 0;
bool lastStaConnected = false;
// I2C-Init und PCF8523-Detektion. Soft-Fail: setzt nur rtcAvailable.
void setupRTC() {
Wire.begin(); // idempotent — falls bereits durch andere Module aufgerufen
if (!rtc.begin()) {
Serial.println("[RTC] PCF8523 nicht gefunden — RTC-Funktionen deaktiviert");
rtcAvailable = false;
return;
}
if (!rtc.initialized() || rtc.lostPower()) {
Serial.println("[RTC] PCF8523 erkannt, aber Akku-Backup ungültig (lostPower)");
// Wir betrachten den Chip trotzdem als verfügbar — NTP wird ihn gleich neu setzen.
}
rtc.start(); // STOP-Bit löschen, falls gesetzt
rtcAvailable = true;
Serial.println("[RTC] PCF8523 initialisiert");
}
- Step 2: Build CI env to verify the header compiles standalone
The header is not yet included anywhere, so this only checks that the includes resolve. Run: pio run -e esp32thing_CI
Expected: SUCCESS, no warnings about rtcsync.h.
- Step 3: Commit
git add src/rtcsync.h
git commit -m "feat(rtc): add rtcsync.h skeleton with PCF8523 detection"
Task 3: Add RTC → system-time fallback inside setupRTC()
Files:
-
Modify:
src/rtcsync.h(thesetupRTC()function from Task 2) -
Step 1: Extend
setupRTC()to seed the system clock from RTC if available
Replace the body of setupRTC() with:
void setupRTC() {
Wire.begin();
if (!rtc.begin()) {
Serial.println("[RTC] PCF8523 nicht gefunden — RTC-Funktionen deaktiviert");
rtcAvailable = false;
return;
}
if (!rtc.initialized() || rtc.lostPower()) {
Serial.println("[RTC] PCF8523 erkannt, aber Akku-Backup ungültig (lostPower)");
}
rtc.start();
rtcAvailable = true;
Serial.println("[RTC] PCF8523 initialisiert");
// Systemzeit aus RTC vorbelegen — wird ggf. später durch NTP übersteuert.
// RTC speichert UTC, settimeofday erwartet UTC. Kein Offset nötig.
DateTime nowRtc = rtc.now();
uint32_t rtcEpoch = nowRtc.unixtime();
// Plausibilität: RTC sollte mindestens 2025 zeigen, sonst ist sie ungestellt.
if (rtcEpoch >= 1735689600UL) {
struct timeval tv;
tv.tv_sec = (time_t)rtcEpoch;
tv.tv_usec = 0;
settimeofday(&tv, NULL);
Serial.printf("[RTC] Systemzeit aus RTC gesetzt: %lu (UTC)\n",
(unsigned long)rtcEpoch);
} else {
Serial.printf("[RTC] RTC-Zeit unplausibel (%lu) — nicht übernommen\n",
(unsigned long)rtcEpoch);
}
}
- Step 2: Build CI env
Run: pio run -e esp32thing_CI
Expected: SUCCESS. The header still isn't wired into master.cpp, so no runtime effect yet.
- Step 3: Commit
git add src/rtcsync.h
git commit -m "feat(rtc): seed system time from RTC at boot"
Task 4: Add weak-symbol hooks to timesync.h
These hooks let rtcsync.h extend timesync.h without timesync.h knowing about it. Without rtcsync.h linked in, the weak default is a null pointer and the call sites short-circuit.
Files:
-
Modify:
src/timesync.h:23-46(getCurrentTimeJSON) -
Modify:
src/timesync.h:72-84(setSystemTime) -
Step 1: Add weak hook declarations near the top of
timesync.h
Insert these declarations directly after the existing bool isValidDateTime(...) line is declared (i.e. after the prototypes block, before getCurrentTimeJSON's implementation around line 22). If no clean location exists, place them right after the #include <time.h> line:
// Weak hooks — falls rtcsync.h kompiliert/gelinkt wird, überschreibt es diese.
// Ohne rtcsync.h sind beide Symbole nullptr und werden nicht aufgerufen.
extern "C" void onSystemTimeSet(time_t t) __attribute__((weak));
extern "C" void appendTimeStatus(JsonDocument &doc) __attribute__((weak));
- Step 2: Convert
getCurrentTimeJSON()doc type and invokeappendTimeStatus
The existing function uses the deprecated StaticJsonDocument<200>. ArduinoJson v7 prefers plain JsonDocument. Replace lines 23–46 of src/timesync.h (the entire getCurrentTimeJSON() body) with:
String getCurrentTimeJSON() {
gettimeofday(&tv, &tz);
now = tv.tv_sec;
JsonDocument doc;
doc["timestamp"] = (long)now;
doc["success"] = true;
// Zusätzliche Zeitinformationen
gmtime_r(&now, &timeinfo);
char timeStr[64];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
doc["formatted"] = String(timeStr);
doc["year"] = timeinfo.tm_year + 1900;
doc["month"] = timeinfo.tm_mon + 1;
doc["day"] = timeinfo.tm_mday;
doc["hour"] = timeinfo.tm_hour;
doc["minute"] = timeinfo.tm_min;
doc["second"] = timeinfo.tm_sec;
// Optionale RTC/Sync-Status-Felder, falls rtcsync.h gelinkt ist
if (appendTimeStatus) {
appendTimeStatus(doc);
}
String response;
serializeJson(doc, response);
return response;
}
- Step 3: Invoke
onSystemTimeSetfromsetSystemTime()
Replace lines 72–84 of src/timesync.h (the setSystemTime function) with:
bool setSystemTime(long timestamp) {
struct timeval tv;
tv.tv_sec = timestamp;
tv.tv_usec = 0;
if (settimeofday(&tv, NULL) == 0) {
Serial.println("Zeit erfolgreich gesetzt: " + String(timestamp));
if (onSystemTimeSet) {
onSystemTimeSet((time_t)timestamp);
}
return true;
} else {
Serial.println("Fehler beim Setzen der Zeit");
return false;
}
}
- Step 4: Build CI env to verify backwards compatibility
rtcsync.h is still not included in master.cpp, so both weak symbols must resolve to nullptr and the if (...) guards must skip them.
Run: pio run -e esp32thing_CI
Expected: SUCCESS. No new warnings. Behavior is identical to before this commit (the hooks are no-ops).
- Step 5: Commit
git add src/timesync.h
git commit -m "feat(timesync): add weak hooks for RTC integration"
Task 5: Implement persistSystemTimeToRTC() and the onSystemTimeSet override
Files:
-
Modify:
src/rtcsync.h(append belowsetupRTC) -
Step 1: Append the persistence function and the weak-hook override
Add the following at the end of src/rtcsync.h:
// Plausibilitäts-Grenzen: 2025-01-01 .. 2100-01-01 (UTC).
// Verhindert, dass kaputte Timestamps (0, negativ, 1970) die RTC korrumpieren.
static constexpr uint32_t RTC_MIN_EPOCH = 1735689600UL; // 2025-01-01 00:00:00 UTC
static constexpr uint32_t RTC_MAX_EPOCH = 4102444800UL; // 2100-01-01 00:00:00 UTC
void persistSystemTimeToRTC(time_t t) {
if (!rtcAvailable) return;
if ((uint32_t)t < RTC_MIN_EPOCH || (uint32_t)t >= RTC_MAX_EPOCH) {
Serial.printf("[RTC] persist abgelehnt — Timestamp unplausibel: %ld\n",
(long)t);
return;
}
rtc.adjust(DateTime((uint32_t)t));
Serial.printf("[RTC] in RTC geschrieben: %ld (UTC)\n", (long)t);
}
// Weak-Hook-Override aus timesync.h — wird automatisch aufgerufen,
// sobald irgendwo setSystemTime() Erfolg meldet.
extern "C" void onSystemTimeSet(time_t t) {
persistSystemTimeToRTC(t);
}
- Step 2: Build CI env
Run: pio run -e esp32thing_CI
Expected: SUCCESS. Still no behavioral change because rtcsync.h is not yet included by master.cpp.
- Step 3: Commit
git add src/rtcsync.h
git commit -m "feat(rtc): persist system time to RTC with plausibility check"
Task 6: Implement syncFromNTP() wrapper
Files:
-
Modify:
src/rtcsync.h(append at end) -
Step 1: Add a thin wrapper around
syncTimeWithNTP()fromtimesync.h
syncTimeWithNTP() in timesync.h:48 already does the NTP heavy lifting (configTime + 5 s polling loop) and prints success/failure. We just need to detect success after the fact (system clock is now > 2025) and persist + bookkeep.
Append to src/rtcsync.h:
// Versucht NTP-Sync via timesync.h. Bei Erfolg: schreibt UTC in RTC,
// setzt ntpEverSynced=true, aktualisiert lastNtpSyncEpoch.
// Returns true bei Erfolg.
bool syncFromNTP() {
// Snapshot vor dem Sync — wenn die Zeit hinterher >= 2025 und neuer als vorher,
// werten wir den Sync als erfolgreich.
time_t before = time(NULL);
syncTimeWithNTP(); // benutzt Defaults aus timesync.h: pool.ntp.org, +1h, kein DST
time_t after = time(NULL);
bool ok = ((uint32_t)after >= RTC_MIN_EPOCH) && (after >= before);
if (!ok) {
Serial.println("[RTC] NTP-Sync fehlgeschlagen — RTC unverändert");
return false;
}
// setSystemTime() wird intern von syncTimeWithNTP NICHT aufgerufen,
// also den Weak-Hook-Pfad hier umgehen und direkt schreiben.
persistSystemTimeToRTC(after);
ntpEverSynced = true;
lastNtpSyncEpoch = after;
return true;
}
- Step 2: Build CI env
Run: pio run -e esp32thing_CI
Expected: SUCCESS.
- Step 3: Commit
git add src/rtcsync.h
git commit -m "feat(rtc): add syncFromNTP wrapper that persists to RTC"
Task 7: Implement loopRTC() (STA reconnect edge + 24 h periodic)
Files:
-
Modify:
src/rtcsync.h(append at end) -
Step 1: Add the loop-tick function
Append to src/rtcsync.h:
// Aus loop() aufgerufen. Triggert syncFromNTP():
// - bei steigender Flanke von WiFi.isConnected() (STA-Reconnect)
// - alle 24h, sobald STA verbunden ist
// Reine Vergleichsoperationen, NTP-Roundtrip selbst ist blockierend (~ms..5s).
void loopRTC() {
bool sta = (WiFi.status() == WL_CONNECTED);
// Edge: false -> true (frischer STA-Connect)
if (sta && !lastStaConnected) {
Serial.println("[RTC] STA-Reconnect erkannt — NTP-Sync");
syncFromNTP();
}
lastStaConnected = sta;
// 24h-Periodik (nur wenn STA online)
if (sta && ntpEverSynced) {
time_t nowEpoch = time(NULL);
if (nowEpoch - lastNtpSyncEpoch >= 86400) {
Serial.println("[RTC] 24h-Periodik — NTP-Sync");
syncFromNTP();
}
}
}
WiFi.h ist bereits indirekt durch andere Header (wificlass.h → WiFi.h) verfügbar; falls der Build hier ein Symbol nicht findet, oben im Header #include <WiFi.h> ergänzen.
- Step 2: Build CI env
Run: pio run -e esp32thing_CI
Expected: SUCCESS. Bei 'WiFi' was not declared: #include <WiFi.h> ganz oben in rtcsync.h ergänzen und neu bauen.
- Step 3: Commit
git add src/rtcsync.h
git commit -m "feat(rtc): add loopRTC with STA-reconnect and 24h re-sync"
Task 8: Implement appendTimeStatus() override
Files:
-
Modify:
src/rtcsync.h(append at end) -
Step 1: Add the JSON-extension override
Append to src/rtcsync.h:
// Weak-Hook-Override aus timesync.h — erweitert /api/time um RTC-Status.
extern "C" void appendTimeStatus(JsonDocument &doc) {
doc["rtc_available"] = rtcAvailable;
doc["rtc_synced_from_ntp"] = ntpEverSynced;
doc["last_ntp_sync_ago_s"] =
ntpEverSynced ? (long)(time(NULL) - lastNtpSyncEpoch) : (long)-1;
if (rtcAvailable) {
doc["rtc_time_utc"] = (long)rtc.now().unixtime();
} else {
doc["rtc_time_utc"] = (long)0;
}
}
- Step 2: Build CI env
Run: pio run -e esp32thing_CI
Expected: SUCCESS.
- Step 3: Commit
git add src/rtcsync.h
git commit -m "feat(rtc): expose RTC status fields via /api/time"
Task 9: Wire rtcsync.h into master.cpp
This is the activation step. Until now, rtcsync.h exists but is unused. After this commit, devices with the FeatherWing get RTC behavior; devices without it print one warning line and run normally.
Files:
-
Modify:
src/master.cpp:3-25(include block) andsrc/master.cpp:31-81(setup/loop) -
Step 1: Add the include
In src/master.cpp, add #include <rtcsync.h> directly after the existing #include <timesync.h> line (currently line 23). The new include block tail should look like:
#include <rfid.h>
#include <timesync.h>
#include <rtcsync.h>
#include <webserverrouter.h>
#include <wificlass.h>
Reihenfolge: rtcsync.h MUSS nach timesync.h kommen — die Weak-Hook-Overrides in rtcsync.h brauchen die Deklarationen aus timesync.h.
- Step 2: Call
setupRTC()beforesetupWifi()
In setup(), insert setupRTC(); directly before the existing setupWifi(); call (currently line 53). The relevant block becomes:
loadWifiSettings();
loadLocationSettings();
setupRTC(); // RTC zuerst, damit Systemzeit vor WiFi plausibel ist
setupWifi(); // WiFi initialisieren
setupOTA(&server);
- Step 3: Add an explicit NTP-sync attempt after WiFi is up
After setupWifi(), the STA may already be connected. The first loopRTC() call would catch the edge eventually, but doing one explicit boot-time sync makes the boot log cleaner. Insert directly after setupWifi();:
setupWifi(); // WiFi initialisieren
if (WiFi.status() == WL_CONNECTED) {
syncFromNTP();
lastStaConnected = true; // Edge bereits "konsumiert"
}
setupOTA(&server);
- Step 4: Call
loopRTC()at the start ofloop()
In loop() (currently lines 65–81), make loopRTC() the first call after checkAutoReset():
void loop() {
checkAutoReset();
loopRTC();
// MQTT hat höchste Priorität (wird zuerst verarbeitet)
loopMqttServer();
...
- Step 5: Build all PlatformIO envs
Run them sequentially (or in one go if your machine handles it):
pio run -e esp32thing_CI
pio run -e esp32thing
pio run -e wemos_d1_mini32
pio run -e um_feathers3
Expected: every env reports SUCCESS. Note the increase in flash usage (RTClib is small, ~5 KB).
- Step 6: Commit
git add src/master.cpp
git commit -m "feat(rtc): wire rtcsync into setup/loop"
Task 10: Hardware verification on real device
This task is mandatory — automated tests cannot validate the I²C interaction or boot ordering. If no FeatherWing is currently attached, do at least Sub-step A (no-hardware soft-fail check).
Files: none (manual)
- Step A — Without RTC attached: confirm soft-fail path
- Ensure no FeatherWing is plugged in.
pio run -e esp32thing -t uploadpio device monitor -b 115200- Expected serial output during boot:
followed by the rest of the boot sequence reaching the WiFi/MQTT init without crash or reboot loop.
[RTC] PCF8523 nicht gefunden — RTC-Funktionen deaktiviert - Open
http://<device-ip>/api/timein the browser. Expected JSON contains:"rtc_available": false, "rtc_synced_from_ntp": <true if STA is online, else false>, "rtc_time_utc": 0
- Step B — With RTC attached, no NTP (AP-only mode)
- Plug in the Adalogger FeatherWing.
- Disconnect the device from any STA network (factory-reset WiFi or boot in AP-only).
- Boot, observe serial:
(If the RTC was never set, the line says "unplausibel" instead — acceptable for a fresh chip.)
[RTC] PCF8523 initialisiert [RTC] Systemzeit aus RTC gesetzt: <epoch> (UTC) - Connect to the AP and load
http://192.168.10.1/api/time. Expected:rtc_available: true, formatted time matches the RTC's last known value.
- Step C — With RTC attached, with NTP
- Configure WiFi-STA credentials so the device joins the home network.
- Boot, observe serial:
[RTC] PCF8523 initialisiert [RTC] Systemzeit aus RTC gesetzt: ... ... (WiFi connect) Warte auf NTP-Zeit (max 5s)... NTP-Zeit synchronisiert! [RTC] in RTC geschrieben: <epoch> (UTC) curl http://<device-ip>/api/time— expectedrtc_synced_from_ntp: true,last_ntp_sync_ago_sclose to 0,rtc_time_utcmatchestimestamp.
- Step D — Browser-time persistence
- With device booted (Step C), use the existing "set browser time" button in the settings UI.
- Power-cycle the device with WiFi disabled (pull antenna or block STA).
- Boot. Expected: serial shows the time from the browser-set, not 1970.
- Step E — Document outcome
If all steps pass: the task is done. If any step fails: do not mark this task complete. Open a follow-up note describing what failed and which task's assumptions were wrong.
Self-Review (already performed)
- Spec coverage: Each spec section maps to a task — Architecture/library → Task 1; new header + soft-fail → Tasks 2–3; weak-hook decoupling → Tasks 4–5; NTP-sync wrapper → Task 6; loop integration → Task 7; API extension → Task 8; setup/loop wiring → Task 9; hardware verification → Task 10.
- Placeholder scan: No TBD/TODO; all code blocks are concrete and copy-pasteable.
- Type/name consistency:
appendTimeStatus,onSystemTimeSet,persistSystemTimeToRTC,syncFromNTP,setupRTC,loopRTC,rtcAvailable,ntpEverSynced,lastNtpSyncEpoch,lastStaConnected,RTC_MIN_EPOCH,RTC_MAX_EPOCHall used consistently across tasks. - Bounds check:
RTC_MIN_EPOCH = 1735689600(2025-01-01) andRTC_MAX_EPOCH = 4102444800(2100-01-01) — match the spec's "Year 2025–2099" range.