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
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
// Dedicated reader task pumps UART continuously; keep timeout short so parser can
|
||||
// recover quickly from broken frames.
|
||||
static constexpr uint32_t METER_FRAME_TIMEOUT_MS = 3000;
|
||||
static constexpr uint32_t METER_FRAME_TIMEOUT_MS = METER_FRAME_TIMEOUT_CFG_MS;
|
||||
static constexpr size_t METER_FRAME_MAX = 512;
|
||||
|
||||
enum class MeterRxState : uint8_t {
|
||||
|
||||
@@ -9,7 +9,7 @@ static constexpr float BATTERY_DIVIDER = 2.0f;
|
||||
static constexpr float ADC_REF_V = 3.3f;
|
||||
|
||||
void power_sender_init() {
|
||||
setCpuFrequencyMhz(80);
|
||||
setCpuFrequencyMhz(SENDER_CPU_MHZ);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
esp_wifi_stop();
|
||||
esp_wifi_deinit();
|
||||
@@ -117,6 +117,33 @@ void light_sleep_ms(uint32_t ms) {
|
||||
esp_light_sleep_start();
|
||||
}
|
||||
|
||||
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) {
|
||||
// Light-sleep overhead (~1 ms save/restore) not worthwhile for tiny slices.
|
||||
delay(this_chunk);
|
||||
break;
|
||||
}
|
||||
light_sleep_ms(this_chunk);
|
||||
// After wake the FreeRTOS scheduler runs higher-priority tasks (e.g. the
|
||||
// meter_reader_task on Core 0) before returning here, so the UART HW FIFO
|
||||
// is drained automatically between chunks.
|
||||
}
|
||||
}
|
||||
|
||||
void go_to_deep_sleep(uint32_t seconds) {
|
||||
esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(seconds) * 1000000ULL);
|
||||
esp_deep_sleep_start();
|
||||
|
||||
@@ -203,7 +203,7 @@ static void sender_log_diagnostics(uint32_t now_ms) {
|
||||
if (!SERIAL_DEBUG_MODE) {
|
||||
return;
|
||||
}
|
||||
if (now_ms - g_last_debug_log_ms < 5000) {
|
||||
if (now_ms - g_last_debug_log_ms < SENDER_DIAG_LOG_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
g_last_debug_log_ms = now_ms;
|
||||
@@ -246,6 +246,18 @@ static void sender_log_diagnostics(uint32_t now_ms) {
|
||||
static_cast<unsigned long>(meter_age_ms),
|
||||
static_cast<unsigned long>(g_sender_rx_window_ms),
|
||||
static_cast<unsigned long>(g_sender_sleep_ms));
|
||||
|
||||
#ifdef DEBUG_METER_DIAG
|
||||
serial_debug_printf(
|
||||
"meter_diag: err_m=%lu err_d=%lu err_tx=%lu build_att=%lu build_ok=%lu build_fail=%lu stale_s=%lu",
|
||||
static_cast<unsigned long>(g_sender_faults.meter_read_fail),
|
||||
static_cast<unsigned long>(g_sender_faults.decode_fail),
|
||||
static_cast<unsigned long>(g_sender_faults.lora_tx_fail),
|
||||
static_cast<unsigned long>(g_build_attempts),
|
||||
static_cast<unsigned long>(g_build_valid),
|
||||
static_cast<unsigned long>(g_build_invalid),
|
||||
static_cast<unsigned long>(g_meter_stale_seconds));
|
||||
#endif
|
||||
}
|
||||
|
||||
static void invalidate_inflight_encode_cache() {
|
||||
@@ -398,6 +410,7 @@ static void meter_queue_push_latest(const MeterSampleEvent &event) {
|
||||
|
||||
static void meter_reader_task_entry(void *arg) {
|
||||
(void)arg;
|
||||
uint32_t consecutive_fails = 0;
|
||||
for (;;) {
|
||||
#ifdef ENABLE_TEST_MODE
|
||||
MeterData test_sample = {};
|
||||
@@ -410,16 +423,33 @@ static void meter_reader_task_entry(void *arg) {
|
||||
event.data = test_sample;
|
||||
event.rx_ms = now_ms;
|
||||
meter_queue_push_latest(event);
|
||||
consecutive_fails = 0;
|
||||
continue;
|
||||
#endif
|
||||
|
||||
const char *frame = nullptr;
|
||||
size_t frame_len = 0;
|
||||
if (!meter_poll_frame(frame, frame_len)) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
// Exponential backoff: 5→10→20→…→METER_FAIL_BACKOFF_MAX_MS on consecutive
|
||||
// poll misses. Reduces CPU wake-ups when the meter is unresponsive.
|
||||
uint32_t backoff_ms = METER_FAIL_BACKOFF_BASE_MS;
|
||||
if (consecutive_fails < 16) {
|
||||
backoff_ms = METER_FAIL_BACKOFF_BASE_MS << consecutive_fails;
|
||||
}
|
||||
if (backoff_ms < 5) {
|
||||
backoff_ms = 5;
|
||||
}
|
||||
if (backoff_ms > METER_FAIL_BACKOFF_MAX_MS) {
|
||||
backoff_ms = METER_FAIL_BACKOFF_MAX_MS;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(backoff_ms));
|
||||
if (consecutive_fails < UINT32_MAX) {
|
||||
consecutive_fails++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
consecutive_fails = 0;
|
||||
MeterData parsed = {};
|
||||
if (parse_meter_frame_sample(frame, frame_len, parsed)) {
|
||||
MeterSampleEvent event = {};
|
||||
@@ -1194,6 +1224,42 @@ static void sender_loop() {
|
||||
|
||||
if (g_time_acquired) {
|
||||
sender_reset_fault_stats_on_hour_boundary();
|
||||
|
||||
// Evaluate meter staleness once per loop iteration, not per catch-up tick.
|
||||
// This prevents LoRa TX blocking (seconds) from inflating the fault counter
|
||||
// by N missed ticks when the same stale-data condition persists throughout.
|
||||
uint32_t meter_age_ms = g_last_meter_valid ? (now_ms - g_last_meter_rx_ms) : UINT32_MAX;
|
||||
bool has_snapshot = g_last_meter_valid;
|
||||
bool meter_ok = has_snapshot && meter_age_ms <= METER_SAMPLE_MAX_AGE_MS;
|
||||
bool meter_fault_noted = false;
|
||||
|
||||
// Count one time-jump fault per event, outside the catch-up loop.
|
||||
if (g_meter_time_jump_pending) {
|
||||
g_meter_time_jump_pending = false;
|
||||
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead);
|
||||
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
|
||||
meter_fault_noted = true;
|
||||
}
|
||||
|
||||
// Count one stale-meter fault per contiguous stale period, not per tick.
|
||||
if (!meter_ok && !meter_fault_noted) {
|
||||
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead);
|
||||
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_METER_DIAG
|
||||
{
|
||||
uint32_t pending_ticks = (now_ms - g_last_sample_ms) / METER_SAMPLE_INTERVAL_MS;
|
||||
if (pending_ticks > 1) {
|
||||
serial_debug_printf("meter_diag: catchup ticks=%lu age_ms=%lu ok=%u snap=%u",
|
||||
static_cast<unsigned long>(pending_ticks),
|
||||
static_cast<unsigned long>(meter_age_ms),
|
||||
meter_ok ? 1U : 0U,
|
||||
has_snapshot ? 1U : 0U);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
while (now_ms - g_last_sample_ms >= METER_SAMPLE_INTERVAL_MS) {
|
||||
g_last_sample_ms += METER_SAMPLE_INTERVAL_MS;
|
||||
MeterData data = {};
|
||||
@@ -1206,10 +1272,6 @@ static void sender_loop() {
|
||||
data.phase_power_w[2] = NAN;
|
||||
|
||||
g_build_attempts++;
|
||||
uint32_t meter_age_ms = g_last_meter_valid ? (now_ms - g_last_meter_rx_ms) : UINT32_MAX;
|
||||
// Reuse recent good samples to bridge short parser gaps without accepting stale data forever.
|
||||
bool has_snapshot = g_last_meter_valid;
|
||||
bool meter_ok = has_snapshot && meter_age_ms <= METER_SAMPLE_MAX_AGE_MS;
|
||||
if (has_snapshot) {
|
||||
data.meter_seconds = g_last_meter_data.meter_seconds;
|
||||
data.meter_seconds_valid = g_last_meter_data.meter_seconds_valid;
|
||||
@@ -1222,15 +1284,6 @@ static void sender_loop() {
|
||||
} else {
|
||||
g_meter_stale_seconds = g_last_meter_valid ? (meter_age_ms / 1000) : (g_meter_stale_seconds + 1);
|
||||
}
|
||||
if (!meter_ok) {
|
||||
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead);
|
||||
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
|
||||
}
|
||||
if (g_meter_time_jump_pending) {
|
||||
g_meter_time_jump_pending = false;
|
||||
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead);
|
||||
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
|
||||
}
|
||||
if (g_build_count == 0 && battery_sample_due(now_ms)) {
|
||||
update_battery_cache();
|
||||
}
|
||||
@@ -1458,15 +1511,20 @@ static void sender_loop() {
|
||||
uint32_t idle_ms = next_due - now_ms;
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
g_sender_sleep_ms += idle_ms;
|
||||
if (now_ms - g_sender_power_log_ms >= 10000) {
|
||||
if (now_ms - g_sender_power_log_ms >= SENDER_DIAG_LOG_INTERVAL_MS) {
|
||||
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();
|
||||
if (g_time_acquired) {
|
||||
// Keep the meter reader task running while metering is active.
|
||||
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) {
|
||||
// Fallback: keep meter reader task alive with an active wait.
|
||||
delay(idle_ms);
|
||||
} else {
|
||||
light_sleep_ms(idle_ms);
|
||||
|
||||
Reference in New Issue
Block a user