# 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 | 25–35 % avg. Strom | niedrig | **P0** | | 2 | Meter-Reader Exponential-Backoff | 2–5 % (weniger Core-0-Wakeups) | sehr niedrig | P1 | | 3 | Log-Drosselung (konfigurierbar) | 1–3 % (weniger UART TX) | keins | P1 | | 4 | CPU-Frequenz konfigurierbar (80→40 MHz) | 5–10 % (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 ≈ 25–30 mA), jetzt wird in 100 ms-Chunks Light-Sleep eingesetzt (≈ 0,8–1,5 mA). Das allein senkt den mittleren Strom um ~25 mA, bei einem Gesamtverbrauch von ~35–40 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.