Merge pull request 'feat/rtc-pcf8523' (#3) from feat/rtc-pcf8523 into main
Some checks failed
/ build (push) Failing after 26s
Some checks failed
/ build (push) Failing after 26s
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
607
docs/superpowers/plans/2026-05-03-rtc-pcf8523-fallback.md
Normal file
607
docs/superpowers/plans/2026-05-03-rtc-pcf8523-fallback.md
Normal 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 23–46 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 72–84 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 65–81), 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 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_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 2025–2099" range.
|
||||
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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
133
src/rtcsync.h
Normal 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;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user