feat(power): 1Hz chunked light-sleep; meter backoff; log throttling

- Replace delay() with light_sleep_chunked_ms() in sender idle path
  (100ms chunks preserve UART FIFO safety at 9600 baud)
- Add ENABLE_LIGHT_SLEEP_IDLE build flag (default: on, fallback: =0)
- Meter reader task: exponential backoff on consecutive poll failures
  (METER_FAIL_BACKOFF_BASE_MS..MAX_MS) to reduce idle Core-0 wakeups
- Configurable SENDER_DIAG_LOG_INTERVAL_MS (5s debug / 30s prod)
- Configurable METER_FRAME_TIMEOUT_CFG_MS, SENDER_CPU_MHZ
- New PlatformIO envs: lowpower, 868-lowpower, lowpower-debug
- Add docs/POWER_OPTIMIZATION.md with measurement plan and Go/No-Go
This commit is contained in:
2026-03-16 16:32:49 +01:00
parent 99aae76404
commit b9591ce9bb
7 changed files with 489 additions and 20 deletions

293
docs/POWER_OPTIMIZATION.md Normal file
View File

@@ -0,0 +1,293 @@
# Energie-Optimierung: DD3 LoRa Bridge Sender
## Kurzreport
### Ziel
- **1 Hz Messauflösung** beibehalten (`METER_SAMPLE_INTERVAL_MS = 1000`)
- **30 s Batch-Senden** beibehalten (`METER_SEND_INTERVAL_MS = 30000`)
- **≥ 20 % Reduktion** des durchschnittlichen Stromverbrauchs
- **0 Datenverlust**, identische Batch-Semantik
### Kernmaßnahmen & Priorisierung
| # | Maßnahme | Einsparung (geschätzt) | Risiko | Priorität |
|---|----------|------------------------|--------|-----------|
| 1 | Chunked Light-Sleep zwischen 1 Hz Samples | 2535 % avg. Strom | niedrig | **P0** |
| 2 | Meter-Reader Exponential-Backoff | 25 % (weniger Core-0-Wakeups) | sehr niedrig | P1 |
| 3 | Log-Drosselung (konfigurierbar) | 13 % (weniger UART TX) | keins | P1 |
| 4 | CPU-Frequenz konfigurierbar (80→40 MHz) | 510 % (optional) | SPI-Timing prüfen | P2 |
| 5 | OLED Auto-Off (bereits implementiert) | ~5 mA wenn aus | keins | ✅ bereits aktiv |
| 6 | WiFi/BT deaktiviert (Sender) | ~80 mA gespart | keins | ✅ bereits aktiv |
| 7 | LoRa Sleep zwischen Batches | ~10 mA gespart | keins | ✅ bereits aktiv |
### Zusammenfassung
Der **größte Hebel** (P0) ist der Wechsel von `delay(idle_ms)` zu
`light_sleep_chunked_ms()` in der Sender-Hauptschleife. Im Normalzustand (Zeit
synchronisiert, 1 Hz Sampling) verbringt die CPU ca. 950 ms/s im Idle. Bisher
wurde `delay()` verwendet (CPU aktiv bei 80 MHz ≈ 2530 mA), jetzt wird in
100 ms-Chunks Light-Sleep eingesetzt (≈ 0,81,5 mA). Das allein senkt den
mittleren Strom um ~25 mA, bei einem Gesamtverbrauch von ~3540 mA ca. **35 %**.
---
## Technischer Anhang
### 1. Chunked Light-Sleep (P0)
**Problem:** Im Sender-Loop wurde nach dem Sampling-Tick `delay(idle_ms)`
aufgerufen, um den Meter-Reader-Task auf Core 0 weiterlaufen zu lassen. Die CPU
blieb dabei komplett aktiv.
**Lösung:** `light_sleep_chunked_ms(total_ms, chunk_ms)` aufgeteilt in max.
100 ms Chunks, damit die UART-Hardware-FIFO (128 Byte @ 9600 Baud ≈ 133 ms
Sicherheitspuffer) nicht überläuft.
**Mechanismus:**
1. Main-Task (Core 1) ruft `esp_light_sleep_start()` auf → beide Cores schlafen
2. Timer-Wakeup nach max. 100 ms
3. FreeRTOS-Scheduler läuft → Meter-Reader-Task (Core 0, Prio 2) draint FIFO
4. Main-Task setzt fort → nächster Chunk oder Sampling-Tick
**Betroffene Dateien:**
```
include/config.h # Neue Konstanten: LIGHT_SLEEP_IDLE, LIGHT_SLEEP_CHUNK_MS
include/power_manager.h # Neue Funktion: light_sleep_chunked_ms()
src/power_manager.cpp # Implementierung light_sleep_chunked_ms()
src/sender_state_machine.cpp # Idle-Pfad: delay() → light_sleep_chunked_ms()
```
**Patch power_manager.cpp:**
```cpp
void light_sleep_chunked_ms(uint32_t total_ms, uint32_t chunk_ms) {
if (total_ms == 0) return;
if (chunk_ms == 0) chunk_ms = total_ms;
uint32_t start = millis();
for (;;) {
uint32_t elapsed = millis() - start;
if (elapsed >= total_ms) break;
uint32_t remaining = total_ms - elapsed;
uint32_t this_chunk = remaining > chunk_ms ? chunk_ms : remaining;
if (this_chunk < 10) {
delay(this_chunk); // Light-sleep overhead nicht lohnend
break;
}
light_sleep_ms(this_chunk);
// Nach Wakeup läuft der FreeRTOS-Scheduler automatisch:
// meter_reader_task (Prio 2 > Main-Prio 1) draint UART-FIFO
}
}
```
**Patch sender_state_machine.cpp (Idle-Pfad):**
```cpp
lora_sleep();
if (LIGHT_SLEEP_IDLE) {
// Chunked light-sleep: wake every LIGHT_SLEEP_CHUNK_MS so the
// meter_reader_task (Core 0, prio 2) can drain the 128-byte UART HW FIFO
// before it overflows (~133 ms at 9600 baud). Saves ~25 mA vs delay().
light_sleep_chunked_ms(idle_ms, LIGHT_SLEEP_CHUNK_MS);
} else if (g_time_acquired) {
delay(idle_ms); // Fallback
} else {
light_sleep_ms(idle_ms);
}
```
**Fallback-Flag:** `ENABLE_LIGHT_SLEEP_IDLE=0` deaktiviert Light-Sleep komplett
→ identisches Verhalten wie vorher.
---
### 2. Meter-Reader Exponential-Backoff (P1)
**Problem:** Der Meter-Reader-Task pollt alle 5 ms via `vTaskDelay(5)` auch
wenn der Meter nicht angeschlossen ist oder dauerhaft Fehler liefert. Bei nicht
angeschlossenem Meter bedeutet das ~200 Wakeups/s auf Core 0 ohne Nutzen.
**Lösung:** Exponential-Backoff auf `METER_FAIL_BACKOFF_BASE_MS` (10 ms) bis
`METER_FAIL_BACKOFF_MAX_MS` (500 ms) bei konsekutiven Fehlschlägen. Bei
erfolgreichem Frame-Empfang sofortige Reset auf 5 ms (= normalem Polling).
```cpp
// In meter_reader_task_entry():
uint32_t backoff_ms = METER_FAIL_BACKOFF_BASE_MS << consecutive_fails;
if (backoff_ms > METER_FAIL_BACKOFF_MAX_MS) backoff_ms = METER_FAIL_BACKOFF_MAX_MS;
vTaskDelay(pdMS_TO_TICKS(backoff_ms));
```
**Risiko:** Keines normaler 1 Hz Betrieb mit angeschlossenem Meter liefert
dauerhaft Frames → `consecutive_fails = 0` → Backoff bleibt bei 10 ms.
---
### 3. Log-Drosselung (P1)
**Problem:** Diagnose-Logs wurden alle 5 s gesendet, Power-Logs alle 10 s.
Jeder `Serial.printf()` kostet ~1 ms CPU + UART-TX-Energie.
**Lösung:** Konfigurierbares `SENDER_DIAG_LOG_INTERVAL_MS` 5 s im Debug-Modus,
30 s im Nicht-Debug-Modus. Production-Build (`SERIAL_DEBUG_MODE_FLAG=0`) hat
alle Logs vollständig eliminiert (bestehendes Verhalten, jetzt explizit).
---
### 4. CPU-Frequenz (P2, optional)
`SENDER_CPU_MHZ` ist jetzt konfigurierbar (Default: 80 MHz). 40 MHz wäre
möglich, spart ~5 mA, erfordert aber Validierung der SPI-Timing für
LoRa-Modul (SX1276). **Empfehlung:** Erst mit 80 MHz validieren, dann 40 MHz
testen.
**Hinweis:** Kein separater Build-Flag hinzugefügt; bei Bedarf:
`-DSENDER_CPU_MHZ=40` in `build_flags`.
---
### 5. Frame-Timeout (konfigurierbar)
`METER_FRAME_TIMEOUT_CFG_MS` (Default: 3000 ms) ist jetzt in `config.h` statt
hart kodiert in `meter_driver.cpp`. Erlaubt Tuning ohne Quellcode-Änderung.
---
## Build-Varianten
| Environment | Beschreibung |
|-------------|-------------|
| `lilygo-t3-v1-6-1` | Standard-Build, Debug ein, Light-Sleep **ein** (Default) |
| `lilygo-t3-v1-6-1-prod` | Production, Debug aus, Light-Sleep **ein** |
| `lilygo-t3-v1-6-1-lowpower` | Low-Power, Debug aus, Light-Sleep ein |
| `lilygo-t3-v1-6-1-868-lowpower` | Low-Power @ 868 MHz |
| `lilygo-t3-v1-6-1-lowpower-debug` | Low-Power + Debug + Meter-Diag |
**Light-Sleep deaktivieren** (Fallback): `-DENABLE_LIGHT_SLEEP_IDLE=0`
---
## Messprotokoll/Testplan
### Equipment
- USB-Multimeter (z. B. FNIRSI FNB58) oder INA219 Breakout am Batterie-Anschluss
- Sender-Board (TTGO LoRa32 v1.6.1) mit angeschlossenem Smart-Meter
- Receiver-Board für ACK
### Messprozedur (30 min Run)
1. **Baseline (ohne Light-Sleep):**
```
pio run -e lilygo-t3-v1-6-1 -t upload -- -DENABLE_LIGHT_SLEEP_IDLE=0
```
- 30 min laufen lassen, Durchschnittsstrom messen
- Serielle Ausgabe loggen: `pio device monitor -b 115200 > baseline.log`
2. **Light-Sleep (aktiviert):**
```
pio run -e lilygo-t3-v1-6-1-lowpower-debug -t upload
```
- 30 min laufen lassen, Durchschnittsstrom messen
- Serielle Ausgabe loggen: `pio device monitor -b 115200 > lowpower.log`
3. **Auswertung:**
- Mittlerer Strom: `avg(I_baseline)` vs `avg(I_lowpower)`
- 1 Hz Jitter: `grep "diag:" lowpower.log` → Sample-Timestamps prüfen
- Sample-Verluste: Batch-Logs auswerten (`valid_count`, `invalid_count`)
- Batch-Semantik: ACK-Erfolgsrate vergleichen
### Akzeptanzkriterien
| Kriterium | Schwellwert |
|-----------|------------|
| Durchschnittlicher Strom | ≥ 20 % Reduktion vs Baseline |
| Verlorene Samples | 0 in 30 min |
| 1 Hz Jitter | < 50 ms |
| Batch-Semantik | Identische ACK-Erfolgsrate (±2 %) |
| Fehlerrate | ≤ 2/h über 4 h |
| OLED-Funktion | Button weckt Display, Auto-Off funktioniert |
| Watchdog | Kein Reset in 4 h |
### Go/No-Go
- **Go:** Alle Kriterien erfüllt → Merge in `main`
- **No-Go bei Jitter > 100 ms:** `LIGHT_SLEEP_CHUNK_MS` auf 50 ms reduzieren,
erneut messen
- **No-Go bei Sample-Verlust:** `ENABLE_LIGHT_SLEEP_IDLE=0` als Fallback,
UART-FIFO-Puffergröße prüfen
---
## Strombudget-Schätzung (Sender, 1 Hz Sampling + 30 s Batch)
### Baseline (delay-basiert)
| Phase | Dauer/30s | Strom (mA) | Anteil |
|-------|-----------|------------|--------|
| Sampling (30× ~20 ms) | 600 ms | 30 | 2 % |
| Encoding + TX (~1.5 s) | 1500 ms | 120 | 5 % |
| ACK RX Window (~3 s) | 3000 ms | 25 | 10 % |
| Idle/delay (~25 s) | 24900 ms | 28 | 83 % |
| **Durchschnitt** | | **~32 mA** | |
### Optimiert (Light-Sleep)
| Phase | Dauer/30s | Strom (mA) | Anteil |
|-------|-----------|------------|--------|
| Sampling (30× ~20 ms) | 600 ms | 30 | 2 % |
| Encoding + TX (~1.5 s) | 1500 ms | 120 | 5 % |
| ACK RX Window (~3 s) | 3000 ms | 25 | 10 % |
| Light-Sleep (~25 s) | 24900 ms | 1.2 | 83 % |
| **Durchschnitt** | | **~10 mA** | |
**Geschätzte Einsparung: ~70 % (32→10 mA)**
> Reale Werte hängen vom Board (Quiescent-Strom des Reglers, LED), OLED-Status
> und LoRa-Spreading-Factor ab. Konservativ ≥ 20 % erreichbar.
---
## PR-Plan
### Branch
```
feat/power-light-sleep-idle
```
### Commits
```
feat(power): 1Hz RTC wake + chunked light-sleep; meter backoff; log throttling
- Replace delay() with light_sleep_chunked_ms() in sender idle path
- Add ENABLE_LIGHT_SLEEP_IDLE config flag (default: on)
- Meter reader task: exponential backoff on consecutive poll failures
- Configurable SENDER_DIAG_LOG_INTERVAL_MS, METER_FRAME_TIMEOUT_CFG_MS
- Configurable SENDER_CPU_MHZ (default: 80)
- New PlatformIO environments: lowpower, 868-lowpower, lowpower-debug
```
---
## Offene Risiken / Nebenwirkungen
1. **UART FIFO Overflow bei > 9600 Baud:** Falls künftig eine höhere Baudrate
verwendet wird, muss `LIGHT_SLEEP_CHUNK_MS` proportional reduziert werden
(Formel: `128 / (baud / 10) * 1000`).
2. **ESP32 Light-Sleep + LoRa-Interrupt:** Wenn der LoRa-Transceiver (SX1276)
DIO0-Interrupts während Light-Sleep generiert, werden diese nach dem Wakeup
verarbeitet. Im Sender-Modus (TX-only zwischen Batches) kein Problem, da
`lora_sleep()` vor dem Light-Sleep aufgerufen wird.
3. **Watchdog:** `WATCHDOG_TIMEOUT_SEC = 120 s` ist mehr als ausreichend für
den maximalen Light-Sleep-Chunk von 100 ms. Kein Risiko.
4. **FreeRTOS Tick-Drift:** Nach Light-Sleep wird der Tick-Counter nachgeführt.
`millis()` bleibt konsistent. Kein Einfluss auf 1 Hz Timing.
5. **Meter-Backoff bei normalem Betrieb:** Der Backoff greift nur bei
`meter_poll_frame() == false` (kein verfügbarer Frame). Bei normalem Betrieb
mit 1 Hz Frames kehrt der Backoff sofort auf `METER_FAIL_BACKOFF_BASE_MS`
zurück. Kein Einfluss auf Sampling-Latenz.