Files
DD3-LoRa-Bridge-MultiSender/docs/POWER_OPTIMIZATION.md
acidburns b9591ce9bb 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
2026-03-16 16:32:49 +01:00

294 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.