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
This commit is contained in:
2026-02-02 21:42:51 +01:00
parent a4d9be1903
commit 8e6c64a18e
8 changed files with 139 additions and 41 deletions

View File

@@ -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-<short_id>` (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`

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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<unsigned long>(g_sender_rx_window_ms),
static_cast<unsigned long>(g_sender_sleep_ms));
}
}
lora_sleep();
light_sleep_ms(next_due - now_ms);
}
}

View File

@@ -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<float>(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<float>(sum) / 2.0f;
float v = (avg / 4095.0f) * ADC_REF_V * BATTERY_DIVIDER * BATTERY_CAL;
data.battery_voltage_v = v;