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

10 KiB
Raw Blame History

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:

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):

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).

// 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.