Merge pull request 'feat/rtc-pcf8523' (#3) from feat/rtc-pcf8523 into main
Some checks failed
/ build (push) Failing after 26s

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-05-03 16:28:05 +02:00
9 changed files with 1053 additions and 43 deletions

View File

@@ -674,6 +674,88 @@ body {
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
}
.leaderboard-entry.latest {
border: 2px solid #00ff88;
animation: latest-pulse 1.6s ease-in-out infinite;
position: relative;
z-index: 1;
}
.leaderboard-entry.latest .name {
color: #ffffff;
font-weight: 800;
text-shadow: 0 0 8px rgba(0, 255, 136, 0.7);
}
.leaderboard-entry.latest .time {
color: #ffffff;
animation: latest-time-flash 1.6s ease-in-out infinite;
}
.latest-badge {
display: inline-block;
background: #00ff88;
color: #0d1733;
font-weight: 900;
font-size: clamp(0.6rem, 1vw, 0.85rem);
letter-spacing: 1px;
padding: 3px 8px;
border-radius: 5px;
flex-shrink: 0;
text-transform: uppercase;
animation: latest-badge-pulse 1.6s ease-in-out infinite;
}
@keyframes latest-pulse {
0%,
100% {
background: linear-gradient(
135deg,
rgba(0, 255, 136, 0.28) 0%,
rgba(0, 200, 110, 0.18) 100%
);
box-shadow: 0 0 8px rgba(0, 255, 136, 0.35),
inset 0 0 6px rgba(0, 255, 136, 0.18);
border-color: #00ff88;
}
50% {
background: linear-gradient(
135deg,
rgba(0, 255, 136, 0.5) 0%,
rgba(0, 230, 120, 0.32) 100%
);
box-shadow: 0 0 16px rgba(0, 255, 136, 0.6),
0 0 32px rgba(0, 255, 136, 0.3),
inset 0 0 10px rgba(255, 255, 255, 0.25);
border-color: #ffffff;
}
}
@keyframes latest-badge-pulse {
0%,
100% {
background: #00ff88;
color: #0d1733;
box-shadow: 0 0 5px rgba(0, 255, 136, 0.5);
}
50% {
background: #ffffff;
color: #006a3a;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.7),
0 0 16px rgba(0, 255, 136, 0.55);
}
}
@keyframes latest-time-flash {
0%,
100% {
text-shadow: 0 0 6px rgba(0, 255, 136, 0.55);
}
50% {
text-shadow: 0 0 8px #ffffff, 0 0 14px rgba(0, 255, 136, 0.7);
}
}
.leaderboard-entry .rank {
color: #ffd700;
font-weight: bold;

View File

@@ -168,23 +168,12 @@
document.getElementById(indicatorId).classList.remove("active");
}
}
// Start-/Stopp-Event → Status kann jetzt wechseln (ready↔running).
// Bei Stop-Events den Display-Wert lokal einfrieren, damit der
// Timer nicht bis zum nächsten Sync weiterzählt und dann sichtbar
// zurückspringt. Anschließend sofort syncen, damit der Scheduler
// zwischen schneller (50 ms) und langsamer (500 ms) Taktung wechselt.
if (data.active === true) {
const now = Date.now();
if (data.button === "stop1" && status1 === "running") {
timer1 += (now - lastSync) / 1000;
status1 = "finished";
} else if (data.button === "stop2" && status2 === "running") {
timer2 += (now - lastSync) / 1000;
status2 = "finished";
}
kickDisplayScheduler();
syncFromBackend();
}
// Hinweis: Heartbeats und echte Tastendrücke kommen im WebSocket
// identisch als {button, mac, active: true} an. Eine optimistische
// Status-Übernahme (z. B. running→finished bei stop1) führte daher
// zu kurzem „Geschafft!"-Aufblitzen während des Laufs, sobald der
// Stop-Button einen periodischen Heartbeat sendete. Der Status
// kommt jetzt ausschließlich über syncFromBackend (1 s-Polling).
}
try {
@@ -495,9 +484,17 @@
return `${hh}:${mm}:${ss}`;
}
function createEntryElement(entry) {
function createEntryElement(entry, isLatest) {
const div = document.createElement("div");
div.className = "leaderboard-entry";
if (isLatest) div.classList.add("latest");
if (isLatest) {
const badge = document.createElement("span");
badge.className = "latest-badge";
badge.textContent = "NEU";
div.appendChild(badge);
}
const nameSpan = document.createElement("span");
nameSpan.className = "name";
@@ -527,7 +524,9 @@
container.appendChild(empty);
return;
}
entries.forEach((e) => container.appendChild(createEntryElement(e)));
entries.forEach((e, i) =>
container.appendChild(createEntryElement(e, i === 0))
);
}
function updateLeaderboardDisplay() {

View File

@@ -0,0 +1,607 @@
# 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](../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_deps` blocks of all six envs)
- [ ] **Step 1: Add `adafruit/RTClib@^2.1.4` to every `lib_deps` block**
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:
```ini
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**
```bash
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:
```cpp
// 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**
```bash
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` (the `setupRTC()` function from Task 2)
- [ ] **Step 1: Extend `setupRTC()` to seed the system clock from RTC if available**
Replace the body of `setupRTC()` with:
```cpp
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**
```bash
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:
```cpp
// 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 invoke `appendTimeStatus`**
The existing function uses the deprecated `StaticJsonDocument<200>`. ArduinoJson v7 prefers plain `JsonDocument`. Replace lines 2346 of `src/timesync.h` (the entire `getCurrentTimeJSON()` body) with:
```cpp
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 `onSystemTimeSet` from `setSystemTime()`**
Replace lines 7284 of `src/timesync.h` (the `setSystemTime` function) with:
```cpp
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**
```bash
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 below `setupRTC`)
- [ ] **Step 1: Append the persistence function and the weak-hook override**
Add the following at the end of `src/rtcsync.h`:
```cpp
// 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**
```bash
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()` from `timesync.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`:
```cpp
// 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**
```bash
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`:
```cpp
// 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**
```bash
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`:
```cpp
// 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**
```bash
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) and `src/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:
```cpp
#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()` before `setupWifi()`**
In `setup()`, insert `setupRTC();` directly before the existing `setupWifi();` call (currently line 53). The relevant block becomes:
```cpp
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();`:
```cpp
setupWifi(); // WiFi initialisieren
if (WiFi.status() == WL_CONNECTED) {
syncFromNTP();
lastStaConnected = true; // Edge bereits "konsumiert"
}
setupOTA(&server);
```
- [ ] **Step 4: Call `loopRTC()` at the start of `loop()`**
In `loop()` (currently lines 6581), make `loopRTC()` the first call after `checkAutoReset()`:
```cpp
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**
```bash
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**
1. Ensure no FeatherWing is plugged in.
2. `pio run -e esp32thing -t upload`
3. `pio device monitor -b 115200`
4. Expected serial output during boot:
```
[RTC] PCF8523 nicht gefunden — RTC-Funktionen deaktiviert
```
followed by the rest of the boot sequence reaching the WiFi/MQTT init **without crash or reboot loop**.
5. Open `http://<device-ip>/api/time` in the browser. Expected JSON contains:
```json
"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)**
1. Plug in the Adalogger FeatherWing.
2. Disconnect the device from any STA network (factory-reset WiFi or boot in AP-only).
3. Boot, observe serial:
```
[RTC] PCF8523 initialisiert
[RTC] Systemzeit aus RTC gesetzt: <epoch> (UTC)
```
(If the RTC was never set, the line says "unplausibel" instead — acceptable for a fresh chip.)
4. 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**
1. Configure WiFi-STA credentials so the device joins the home network.
2. 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)
```
3. `curl http://<device-ip>/api/time` — expected `rtc_synced_from_ntp: true`, `last_ntp_sync_ago_s` close to 0, `rtc_time_utc` matches `timestamp`.
- [ ] **Step D — Browser-time persistence**
1. With device booted (Step C), use the existing "set browser time" button in the settings UI.
2. Power-cycle the device with WiFi disabled (pull antenna or block STA).
3. 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 23; weak-hook decoupling → Tasks 45; 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_EPOCH` all used consistently across tasks.
- **Bounds check:** `RTC_MIN_EPOCH = 1735689600` (2025-01-01) and `RTC_MAX_EPOCH = 4102444800` (2100-01-01) — match the spec's "Year 20252099" range.

View 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 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:
```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

View File

@@ -19,7 +19,7 @@ lib_compat_mode = strict
[env:wemos_d1_mini32]
board = wemos_d1_mini32
monitor_speed = 115200
build_flags =
build_flags =
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
-DBATTERY_PIN=16
@@ -27,19 +27,20 @@ board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
targets = uploadfs
board_build.psram = disabled
lib_deps =
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
[env:esp32thing_OTA]
board = esp32thing
monitor_speed = 115200
upload_protocol = espota
upload_port = 192.168.1.96
build_flags =
build_flags =
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
-DBATTERY_PIN=36
@@ -47,19 +48,20 @@ board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
targets = uploadfs
board_build.psram = disabled
lib_deps =
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
[env:esp32thing]
board = esp32thing_plus
monitor_speed = 115200
build_flags =
build_flags =
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
-DBATTERY_PIN=36
@@ -67,54 +69,57 @@ board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
targets = uploadfs
board_build.psram = disabled
lib_deps =
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
[env:esp32thing_CI]
platform = espressif32
board = esp32dev
framework = arduino
build_flags =
build_flags =
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
-DBATTERY_PIN=36
board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
lib_deps =
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
[env:um_feathers3]
board = um_feathers3
monitor_speed = 115200
board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
board_upload.wait_for_upload_port = false
build_flags =
board_upload.wait_for_upload_port = false
build_flags =
-D ARDUINO_USB_CDC_ON_BOOT=1
-D BATTERY_PIN=35
-D ARDUINO_USB_MODE=1
lib_deps =
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
[env:um_feathers3_debug]
board = um_feathers3
board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
board_upload.wait_for_upload_port = false
build_flags =
board_upload.wait_for_upload_port = false
build_flags =
-D ARDUINO_USB_CDC_ON_BOOT=1
-D BATTERY_PIN=35
-D ARDUINO_USB_MODE=0
@@ -126,10 +131,11 @@ upload_port = COM5
monitor_speed = 115200
monitor_port = COM7
lib_deps =
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

View File

@@ -425,8 +425,12 @@ void setupBackendRoutes(AsyncWebServer &server) {
// Erweiterte Leaderboard API (für Leaderboard-Seite - 10 Einträge)
server.on(
"/api/leaderboard-full", HTTP_GET, [](AsyncWebServerRequest *request) {
// Sortiere nach Zeit (beste zuerst)
std::sort(localTimes.begin(), localTimes.end(),
// Sortiere eine Kopie nach Zeit (beste zuerst). Niemals die globale
// localTimes-Liste sortieren - sonst geht die chronologische
// Reihenfolge verloren, die /api/leaderboard für "letzte Zeiten"
// braucht (und die per saveBestTimes auch persistiert wird).
std::vector<LocalTime> sortedTimes(localTimes);
std::sort(sortedTimes.begin(), sortedTimes.end(),
[](const LocalTime &a, const LocalTime &b) {
return a.timeMs < b.timeMs;
});
@@ -436,7 +440,7 @@ void setupBackendRoutes(AsyncWebServer &server) {
// Nimm die besten 10
int count = 0;
for (const auto &time : localTimes) {
for (const auto &time : sortedTimes) {
if (count >= 10)
break;

View File

@@ -21,6 +21,7 @@
#include <preferencemanager.h>
#include <rfid.h>
#include <timesync.h>
#include <rtcsync.h>
#include <webserverrouter.h>
#include <wificlass.h>
@@ -50,7 +51,18 @@ void setup() {
loadWifiSettings();
loadLocationSettings();
setupRTC(); // RTC zuerst, damit Systemzeit vor WiFi plausibel ist
setupWifi(); // WiFi initialisieren
// wificlass.h hat intern bereits syncTimeWithNTP() versucht.
// Falls die Systemzeit jetzt plausibel ist, in RTC persistieren und Bookkeeping setzen —
// ohne einen zweiten NTP-Roundtrip zu provozieren.
if (WiFi.status() == WL_CONNECTED && time(NULL) >= 1735689600L) {
time_t nowEpoch = time(NULL);
persistSystemTimeToRTC(nowEpoch);
ntpEverSynced = true;
lastNtpSyncEpoch = nowEpoch;
lastStaConnected = true; // Edge bereits "konsumiert"
}
setupOTA(&server);
setupRoutes();
@@ -64,6 +76,7 @@ void setup() {
void loop() {
checkAutoReset();
loopRTC();
// MQTT hat höchste Priorität (wird zuerst verarbeitet)
loopMqttServer();

133
src/rtcsync.h Normal file
View File

@@ -0,0 +1,133 @@
// 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;
time_t cachedRtcEpoch = 0; // letzter rtc.now()-Wert, gelesen aus loopRTC (I2C-Race-Guard)
// I2C-Init, PCF8523-Detektion, und Systemzeit-Fallback aus RTC.
// Soft-Fail: bei nicht gefundener Hardware bleibt rtcAvailable=false.
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);
cachedRtcEpoch = (time_t)rtcEpoch;
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);
}
}
// 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.
void onSystemTimeSet(time_t t) {
persistSystemTimeToRTC(t);
}
// Versucht NTP-Sync via timesync.h. Bei Erfolg: schreibt UTC in RTC,
// setzt ntpEverSynced=true, aktualisiert lastNtpSyncEpoch.
// Returns true bei Erfolg.
bool syncFromNTP() {
if (!syncTimeWithNTP()) {
Serial.println("[RTC] NTP-Sync fehlgeschlagen — RTC unverändert");
return false;
}
time_t after = time(NULL);
if ((uint32_t)after < RTC_MIN_EPOCH) {
Serial.println("[RTC] NTP-Sync lieferte unplausible Zeit — RTC unverändert");
return false;
}
persistSystemTimeToRTC(after);
ntpEverSynced = true;
lastNtpSyncEpoch = after;
return true;
}
// 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() {
// RTC-Lesen passiert NUR aus der main-loop (verhindert I2C-Race mit PN532).
// appendTimeStatus() liest danach nur den gecachten Wert.
if (rtcAvailable) {
cachedRtcEpoch = (time_t)rtc.now().unixtime();
}
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);
// Defensive: Clock-Jump rückwärts (z.B. via Browser-Time auf altes Datum)
// würde das Delta negativ machen. Auf "jetzt" zurücksetzen.
if (nowEpoch < lastNtpSyncEpoch) lastNtpSyncEpoch = nowEpoch;
if (nowEpoch - lastNtpSyncEpoch >= 86400) {
Serial.println("[RTC] 24h-Periodik — NTP-Sync");
syncFromNTP();
}
}
}
// Weak-Hook-Override aus timesync.h — erweitert /api/time um RTC-Status.
void appendTimeStatus(JsonDocument &doc) {
doc["rtc_available"] = rtcAvailable;
doc["rtc_synced_from_ntp"] = ntpEverSynced;
long ago = ntpEverSynced ? (long)(time(NULL) - lastNtpSyncEpoch) : (long)-1;
if (ago < 0 && ntpEverSynced) ago = 0; // Clock-Jump rückwärts → 0 statt negativ
doc["last_ntp_sync_ago_s"] = ago;
doc["rtc_time_utc"] = rtcAvailable ? (long)cachedRtcEpoch : (long)0;
}

View File

@@ -6,6 +6,11 @@
#include <sys/time.h>
#include <time.h>
// Weak hooks — falls rtcsync.h kompiliert/gelinkt wird, überschreibt es diese.
// Ohne rtcsync.h sind beide Symbole nullptr und werden nicht aufgerufen.
void onSystemTimeSet(time_t t) __attribute__((weak));
void appendTimeStatus(JsonDocument &doc) __attribute__((weak));
// Globale Zeitvariablen
struct timeval tv;
struct timezone tz;
@@ -24,7 +29,7 @@ String getCurrentTimeJSON() {
gettimeofday(&tv, &tz);
now = tv.tv_sec;
StaticJsonDocument<200> doc;
JsonDocument doc;
doc["timestamp"] = (long)now;
doc["success"] = true;
@@ -33,19 +38,24 @@ String getCurrentTimeJSON() {
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["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;
}
void syncTimeWithNTP(const char *ntpServer = "pool.ntp.org",
bool syncTimeWithNTP(const char *ntpServer = "pool.ntp.org",
long gmtOffset_sec = 3600, int daylightOffset_sec = 0) {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("Warte auf NTP-Zeit (max 5s)...");
@@ -66,6 +76,7 @@ void syncTimeWithNTP(const char *ntpServer = "pool.ntp.org",
} else {
Serial.println("\nNTP-Sync fehlgeschlagen (Timeout nach 5s)");
}
return synced;
}
// Hilfsfunktion: Setzt die Systemzeit auf den angegebenen Zeitstempel.
@@ -76,6 +87,9 @@ bool setSystemTime(long timestamp) {
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");