From 8e6c64a18ec307136044b3dab41767190882a11b Mon Sep 17 00:00:00 2001 From: acidburns Date: Mon, 2 Feb 2026 21:42:51 +0100 Subject: [PATCH] Reduce sender power draw (RX windows + CPU/WiFi/ADC/pins) - Add LoRa idle/sleep/receive-window helpers and use short RX windows for ACK/time sync - Schedule sender time-sync windows (fast/slow) and track RX vs sleep time in debug - Lower sender power (80 MHz CPU, WiFi/BT off, reduced ADC sampling, unused pins pulldown) - Make SERIAL_DEBUG_MODE a build flag, add prod envs with debug off, and document changes --- README.md | 11 +++-- include/config.h | 8 +++- include/lora_transport.h | 2 + include/power_manager.h | 1 + platformio.ini | 34 ++++++++++++++++ src/lora_transport.cpp | 14 +++++++ src/main.cpp | 88 ++++++++++++++++++++++++++-------------- src/power_manager.cpp | 22 ++++++---- 8 files changed, 139 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 409ccff..f53a09f 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,7 @@ inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = { 0xF19C }; - 3.0 V = 0% - 4.2 V = 100% - Uses deep sleep between cycles (`SENDER_WAKE_INTERVAL_SEC`). +- Sender CPU is throttled to 80 MHz and LoRa RX is only enabled in short windows (ACK wait or time-sync). ## Web UI - AP SSID: `DD3-Bridge-` (prefix configurable) @@ -296,12 +297,14 @@ inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = { 0xF19C }; - When no RTC is present or enabled, the receiver keeps sending time sync every 60 seconds. ## Build Environments -- `lilygo-t3-v1-6-1`: production build +- `lilygo-t3-v1-6-1`: production build (debug on) - `lilygo-t3-v1-6-1-test`: test build with `ENABLE_TEST_MODE` -- `lilygo-t3-v1-6-1-868`: production build for 868 MHz modules +- `lilygo-t3-v1-6-1-868`: production build for 868 MHz modules (debug on) - `lilygo-t3-v1-6-1-868-test`: test build for 868 MHz modules - `lilygo-t3-v1-6-1-payload-test`: build with `PAYLOAD_CODEC_TEST` - `lilygo-t3-v1-6-1-868-payload-test`: 868 MHz build with `PAYLOAD_CODEC_TEST` +- `lilygo-t3-v1-6-1-prod`: production build with serial debug off +- `lilygo-t3-v1-6-1-868-prod`: 868 MHz production build with serial debug off ## Config Knobs Key timing settings in `include/config.h`: @@ -311,9 +314,11 @@ Key timing settings in `include/config.h`: - `BATCH_MAX_RETRIES` - `BATCH_QUEUE_DEPTH` - `BATCH_RETRY_POLICY` (keep or drop on retry exhaustion) - - `SERIAL_DEBUG_MODE` / `SERIAL_DEBUG_DUMP_JSON` + - `SERIAL_DEBUG_MODE_FLAG` (build flag) / `SERIAL_DEBUG_DUMP_JSON` - `LORA_SEND_BYPASS` (debug only) - `ENABLE_SD_LOGGING` / `PIN_SD_CS` + - `SENDER_TIMESYNC_WINDOW_MS` + - `SENDER_TIMESYNC_CHECK_SEC_FAST` / `SENDER_TIMESYNC_CHECK_SEC_SLOW` - `SD_HISTORY_MAX_DAYS` / `SD_HISTORY_MIN_RES_MIN` - `SD_HISTORY_MAX_BINS` / `SD_HISTORY_TIME_BUDGET_MS` - `WEB_AUTH_REQUIRE_STA` / `WEB_AUTH_REQUIRE_AP` / `WEB_AUTH_DEFAULT_USER` / `WEB_AUTH_DEFAULT_PASS` diff --git a/include/config.h b/include/config.h index 6c50496..78fd5ca 100644 --- a/include/config.h +++ b/include/config.h @@ -60,6 +60,9 @@ constexpr uint32_t SENDER_WAKE_INTERVAL_SEC = 30; constexpr uint32_t TIME_SYNC_INTERVAL_SEC = 60; constexpr uint32_t TIME_SYNC_SLOW_INTERVAL_SEC = 3600; constexpr uint32_t TIME_SYNC_FAST_WINDOW_MS = 10UL * 60UL * 1000UL; +constexpr uint32_t SENDER_TIMESYNC_WINDOW_MS = 300; +constexpr uint32_t SENDER_TIMESYNC_CHECK_SEC_FAST = 60; +constexpr uint32_t SENDER_TIMESYNC_CHECK_SEC_SLOW = 3600; constexpr bool ENABLE_DS3231 = true; constexpr uint32_t OLED_PAGE_INTERVAL_MS = 4000; constexpr uint32_t OLED_AUTO_OFF_MS = 10UL * 60UL * 1000UL; @@ -73,7 +76,10 @@ constexpr uint8_t BATCH_QUEUE_DEPTH = 10; constexpr BatchRetryPolicy BATCH_RETRY_POLICY = BatchRetryPolicy::Keep; constexpr uint32_t WATCHDOG_TIMEOUT_SEC = 120; constexpr bool ENABLE_HA_DISCOVERY = true; -constexpr bool SERIAL_DEBUG_MODE = true; +#ifndef SERIAL_DEBUG_MODE_FLAG +#define SERIAL_DEBUG_MODE_FLAG 0 +#endif +constexpr bool SERIAL_DEBUG_MODE = SERIAL_DEBUG_MODE_FLAG != 0; constexpr bool SERIAL_DEBUG_DUMP_JSON = false; constexpr bool LORA_SEND_BYPASS = false; constexpr bool ENABLE_SD_LOGGING = true; diff --git a/include/lora_transport.h b/include/lora_transport.h index bb445fb..ad3e932 100644 --- a/include/lora_transport.h +++ b/include/lora_transport.h @@ -19,5 +19,7 @@ struct LoraPacket { void lora_init(); bool lora_send(const LoraPacket &pkt); bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms); +void lora_idle(); void lora_sleep(); +bool lora_receive_window(LoraPacket &pkt, uint32_t timeout_ms); uint32_t lora_airtime_ms(size_t packet_len); diff --git a/include/power_manager.h b/include/power_manager.h index 110c377..0a4aeea 100644 --- a/include/power_manager.h +++ b/include/power_manager.h @@ -5,6 +5,7 @@ void power_sender_init(); void power_receiver_init(); +void power_configure_unused_pins_sender(); void read_battery(MeterData &data); uint8_t battery_percent_from_voltage(float voltage_v); void light_sleep_ms(uint32_t ms); diff --git a/platformio.ini b/platformio.ini index aaf3db0..99e3165 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,6 +18,8 @@ lib_deps = adafruit/Adafruit SSD1306@^2.5.9 adafruit/Adafruit GFX Library@^1.11.9 knolleary/PubSubClient@^2.8 +build_flags = + -DSERIAL_DEBUG_MODE_FLAG=1 [env:lilygo-t3-v1-6-1-test] platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip @@ -30,6 +32,7 @@ lib_deps = adafruit/Adafruit GFX Library@^1.11.9 knolleary/PubSubClient@^2.8 build_flags = + -DSERIAL_DEBUG_MODE_FLAG=1 -DENABLE_TEST_MODE [env:lilygo-t3-v1-6-1-868] @@ -43,6 +46,7 @@ lib_deps = adafruit/Adafruit GFX Library@^1.11.9 knolleary/PubSubClient@^2.8 build_flags = + -DSERIAL_DEBUG_MODE_FLAG=1 -DLORA_FREQUENCY_HZ=868E6 [env:lilygo-t3-v1-6-1-868-test] @@ -56,6 +60,7 @@ lib_deps = adafruit/Adafruit GFX Library@^1.11.9 knolleary/PubSubClient@^2.8 build_flags = + -DSERIAL_DEBUG_MODE_FLAG=1 -DENABLE_TEST_MODE -DLORA_FREQUENCY_HZ=868E6 @@ -70,6 +75,7 @@ lib_deps = adafruit/Adafruit GFX Library@^1.11.9 knolleary/PubSubClient@^2.8 build_flags = + -DSERIAL_DEBUG_MODE_FLAG=1 -DPAYLOAD_CODEC_TEST [env:lilygo-t3-v1-6-1-868-payload-test] @@ -83,5 +89,33 @@ lib_deps = adafruit/Adafruit GFX Library@^1.11.9 knolleary/PubSubClient@^2.8 build_flags = + -DSERIAL_DEBUG_MODE_FLAG=1 -DPAYLOAD_CODEC_TEST -DLORA_FREQUENCY_HZ=868E6 + +[env:lilygo-t3-v1-6-1-prod] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip +board = ttgo-lora32-v1 +framework = arduino +lib_deps = + sandeepmistry/LoRa@^0.8.0 + bblanchon/ArduinoJson@^6.21.5 + adafruit/Adafruit SSD1306@^2.5.9 + adafruit/Adafruit GFX Library@^1.11.9 + knolleary/PubSubClient@^2.8 +build_flags = + -DSERIAL_DEBUG_MODE_FLAG=0 + +[env:lilygo-t3-v1-6-1-868-prod] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip +board = ttgo-lora32-v1 +framework = arduino +lib_deps = + sandeepmistry/LoRa@^0.8.0 + bblanchon/ArduinoJson@^6.21.5 + adafruit/Adafruit SSD1306@^2.5.9 + adafruit/Adafruit GFX Library@^1.11.9 + knolleary/PubSubClient@^2.8 +build_flags = + -DSERIAL_DEBUG_MODE_FLAG=0 + -DLORA_FREQUENCY_HZ=868E6 diff --git a/src/lora_transport.cpp b/src/lora_transport.cpp index 97032f6..e19cb09 100644 --- a/src/lora_transport.cpp +++ b/src/lora_transport.cpp @@ -110,10 +110,24 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) { } } +void lora_idle() { + LoRa.idle(); +} + void lora_sleep() { LoRa.sleep(); } +bool lora_receive_window(LoraPacket &pkt, uint32_t timeout_ms) { + if (timeout_ms == 0) { + return false; + } + LoRa.receive(); + bool got = lora_receive(pkt, timeout_ms); + LoRa.sleep(); + return got; +} + uint32_t lora_airtime_ms(size_t packet_len) { if (packet_len == 0) { return 0; diff --git a/src/main.cpp b/src/main.cpp index e173807..882d42e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -88,6 +88,10 @@ static uint8_t g_inflight_count = 0; static uint16_t g_inflight_batch_id = 0; static bool g_inflight_active = false; static uint32_t g_last_debug_log_ms = 0; +static uint32_t g_sender_last_timesync_check_ms = 0; +static uint32_t g_sender_rx_window_ms = 0; +static uint32_t g_sender_sleep_ms = 0; +static uint32_t g_sender_power_log_ms = 0; static void watchdog_kick(); @@ -144,6 +148,22 @@ static void update_battery_cache() { g_last_battery_ms = millis(); } +static bool sender_timesync_window_due() { + uint32_t interval_sec = SENDER_TIMESYNC_CHECK_SEC_FAST; + if (time_is_synced() && time_rtc_present()) { + interval_sec = SENDER_TIMESYNC_CHECK_SEC_SLOW; + } + if (g_sender_last_timesync_check_ms == 0) { + g_sender_last_timesync_check_ms = millis() - interval_sec * 1000UL; + } + uint32_t now_ms = millis(); + if (now_ms - g_sender_last_timesync_check_ms >= interval_sec * 1000UL) { + g_sender_last_timesync_check_ms = now_ms; + return true; + } + return false; +} + static bool batch_queue_drop_oldest() { if (g_batch_count == 0) { return false; @@ -620,6 +640,7 @@ void setup() { if (g_role == DeviceRole::Sender) { power_sender_init(); + power_configure_unused_pins_sender(); meter_init(); g_last_sample_ms = millis() - METER_SAMPLE_INTERVAL_MS; g_last_send_ms = millis(); @@ -712,37 +733,18 @@ static void sender_loop() { } if (g_batch_ack_pending) { - uint32_t end_ms = millis() + 400; - while (millis() < end_ms) { - LoraPacket ack_pkt = {}; - if (!lora_receive(ack_pkt, 0) || ack_pkt.protocol_version != PROTOCOL_VERSION) { - delay(5); - continue; - } - if (ack_pkt.payload_type == PayloadType::Ack && ack_pkt.payload_len >= 6 && ack_pkt.role == DeviceRole::Receiver) { - uint16_t ack_id = read_u16_le(ack_pkt.payload); - uint16_t ack_sender = read_u16_le(&ack_pkt.payload[2]); - uint16_t ack_receiver = read_u16_le(&ack_pkt.payload[4]); - if (ack_sender == g_short_id && ack_receiver == ack_pkt.device_id_short && - g_batch_ack_pending && ack_id == g_last_sent_batch_id) { - g_last_acked_batch_id = ack_id; - serial_debug_printf("ack: ok batch_id=%u", ack_id); - finish_inflight_batch(); - break; - } - } + LoraPacket ack_pkt = {}; + uint32_t rx_start = millis(); + bool got_ack = lora_receive_window(ack_pkt, 400); + uint32_t rx_elapsed = millis() - rx_start; + if (SERIAL_DEBUG_MODE) { + g_sender_rx_window_ms += rx_elapsed; } - } - - LoraPacket rx = {}; - if (lora_receive(rx, 0) && rx.protocol_version == PROTOCOL_VERSION) { - if (rx.payload_type == PayloadType::TimeSync) { - time_handle_timesync_payload(rx.payload, rx.payload_len); - } else if (rx.payload_type == PayloadType::Ack && rx.payload_len >= 6 && rx.role == DeviceRole::Receiver) { - uint16_t ack_id = read_u16_le(rx.payload); - uint16_t ack_sender = read_u16_le(&rx.payload[2]); - uint16_t ack_receiver = read_u16_le(&rx.payload[4]); - if (ack_sender == g_short_id && ack_receiver == rx.device_id_short && + if (got_ack && ack_pkt.payload_type == PayloadType::Ack && ack_pkt.payload_len >= 6 && ack_pkt.role == DeviceRole::Receiver) { + uint16_t ack_id = read_u16_le(ack_pkt.payload); + uint16_t ack_sender = read_u16_le(&ack_pkt.payload[2]); + uint16_t ack_receiver = read_u16_le(&ack_pkt.payload[4]); + if (ack_sender == g_short_id && ack_receiver == ack_pkt.device_id_short && g_batch_ack_pending && ack_id == g_last_sent_batch_id) { g_last_acked_batch_id = ack_id; serial_debug_printf("ack: ok batch_id=%u", ack_id); @@ -751,6 +753,23 @@ static void sender_loop() { } } + bool timesync_due = (!g_batch_ack_pending && sender_timesync_window_due()); + if (timesync_due) { + LoraPacket rx = {}; + uint32_t rx_start = millis(); + bool got = lora_receive_window(rx, SENDER_TIMESYNC_WINDOW_MS); + uint32_t rx_elapsed = millis() - rx_start; + if (SERIAL_DEBUG_MODE) { + g_sender_rx_window_ms += rx_elapsed; + } + if (got && rx.payload_type == PayloadType::TimeSync) { + time_handle_timesync_payload(rx.payload, rx.payload_len); + } + } + if (!g_batch_ack_pending) { + lora_sleep(); + } + if (g_batch_ack_pending && (now_ms - g_last_batch_send_ms >= g_batch_ack_timeout_ms)) { if (g_batch_retry_count < BATCH_MAX_RETRIES) { g_batch_retry_count++; @@ -780,6 +799,15 @@ static void sender_loop() { uint32_t next_due = next_sample_due < next_send_due ? next_sample_due : next_send_due; if (!g_batch_ack_pending && next_due > now_ms) { watchdog_kick(); + if (SERIAL_DEBUG_MODE) { + g_sender_sleep_ms += (next_due - now_ms); + if (now_ms - g_sender_power_log_ms >= 10000) { + g_sender_power_log_ms = now_ms; + serial_debug_printf("power: rx_ms=%lu sleep_ms=%lu", static_cast(g_sender_rx_window_ms), + static_cast(g_sender_sleep_ms)); + } + } + lora_sleep(); light_sleep_ms(next_due - now_ms); } } diff --git a/src/power_manager.cpp b/src/power_manager.cpp index 50b74e0..ba9f7a8 100644 --- a/src/power_manager.cpp +++ b/src/power_manager.cpp @@ -10,7 +10,10 @@ static constexpr float BATTERY_CAL = 1.0f; static constexpr float ADC_REF_V = 3.3f; void power_sender_init() { + setCpuFrequencyMhz(80); + WiFi.mode(WIFI_OFF); esp_wifi_stop(); + esp_wifi_deinit(); btStop(); analogReadResolution(12); pinMode(PIN_BAT_ADC, INPUT); @@ -22,14 +25,19 @@ void power_receiver_init() { pinMode(PIN_BAT_ADC, INPUT); } -void read_battery(MeterData &data) { - const int samples = 8; - uint32_t sum = 0; - for (int i = 0; i < samples; ++i) { - sum += analogRead(PIN_BAT_ADC); - delay(5); +void power_configure_unused_pins_sender() { + // Board-specific: only touch pins that are known unused and safe on TTGO LoRa32 v1.6.1 + const uint8_t pins[] = {32, 33}; + for (uint8_t pin : pins) { + pinMode(pin, INPUT_PULLDOWN); } - float avg = static_cast(sum) / samples; +} + +void read_battery(MeterData &data) { + uint32_t sum = 0; + sum += analogRead(PIN_BAT_ADC); + sum += analogRead(PIN_BAT_ADC); + float avg = static_cast(sum) / 2.0f; float v = (avg / 4095.0f) * ADC_REF_V * BATTERY_DIVIDER * BATTERY_CAL; data.battery_voltage_v = v;