From 4bb721e0606249cddb1cdf785bc528c761f29dd4 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:28:49 +1100 Subject: [PATCH] implementing low battery brownout protection to prevent contacts file corruption caused by low voltage reboot loop; board goes to sleep at 2800mv --- examples/companion_radio/DataStore.cpp | 32 ++++++++++++++++++++++ examples/companion_radio/ui-new/UITask.cpp | 11 +++++--- examples/companion_radio/ui-new/UITask.h | 1 + variants/lilygo_tdeck_pro/TDeckBoard.cpp | 19 +++++++++++++ variants/lilygo_tdeck_pro/platformio.ini | 2 ++ 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index d499bc78..3048b010 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -303,6 +303,38 @@ void DataStore::loadContacts(DataStoreHost* host) { File file = openRead(fs, "/contacts3"); if (file) { + // --- Truncation guard --- + // If the file is smaller than one full contact record (152 bytes), + // it was truncated by a crash/brown-out. Discard it and try the + // .tmp backup if available. + size_t fsize = file.size(); + if (fsize > 0 && fsize < 152) { + Serial.printf("DataStore: contacts3 truncated (%d bytes < 152), discarding\n", (int)fsize); + file.close(); + fs->remove("/contacts3"); + if (fs->exists("/contacts3.tmp")) { + File tmp = openRead(fs, "/contacts3.tmp"); + if (tmp && tmp.size() >= 152) { + Serial.println("DataStore: recovering from .tmp after truncation"); + tmp.close(); + fs->rename("/contacts3.tmp", "/contacts3"); + file = openRead(fs, "/contacts3"); + if (!file) return; // give up + } else { + if (tmp) tmp.close(); + Serial.println("DataStore: no valid contacts backup — starting fresh"); + return; + } + } else { + Serial.println("DataStore: no .tmp backup — starting fresh"); + return; + } + } else if (fsize == 0) { + // Empty file — nothing to load + file.close(); + return; + } + bool full = false; while (!full) { ContactInfo c; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index c6d2bc44..4e1360d6 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1289,10 +1289,11 @@ if (curr) curr->poll(); if (millis() > next_batt_chck) { uint16_t milliVolts = getBattMilliVolts(); if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS) { + _low_batt_count++; + if (_low_batt_count >= 3) { // 3 consecutive low readings (~24s) to avoid transient sags - // show low battery shutdown alert - // we should only do this for eink displays, which will persist after power loss - #if defined(THINKNODE_M1) || defined(LILYGO_TECHO) + // show low battery shutdown alert on e-ink (persists after power loss) + #if defined(THINKNODE_M1) || defined(LILYGO_TECHO) || defined(LilyGo_TDeck_Pro) if (_display != NULL) { _display->startFrame(); _display->setTextSize(2); @@ -1304,7 +1305,9 @@ if (curr) curr->poll(); #endif shutdown(); - + } + } else { + _low_batt_count = 0; } next_batt_chck = millis() + 8000; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 6a34a79f..d97e6738 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -54,6 +54,7 @@ class UITask : public AbstractUITask { unsigned long _alert_expiry; int _msgcount; unsigned long ui_started_at, next_batt_chck; + uint8_t _low_batt_count = 0; // Consecutive low-voltage readings for debounce int next_backlight_btn_check = 0; #ifdef PIN_STATUS_LED int led_state = 0; diff --git a/variants/lilygo_tdeck_pro/TDeckBoard.cpp b/variants/lilygo_tdeck_pro/TDeckBoard.cpp index d5398dbf..97da0c3e 100644 --- a/variants/lilygo_tdeck_pro/TDeckBoard.cpp +++ b/variants/lilygo_tdeck_pro/TDeckBoard.cpp @@ -78,6 +78,25 @@ void TDeckBoard::begin() { MESH_DEBUG_PRINTLN("TDeckBoard::begin() - Battery voltage: %d mV", voltage); configureFuelGauge(); #endif + + // --- Early low-voltage protection --- + // If we boot below the shutdown threshold, go straight to deep sleep + // WITHOUT touching the filesystem. This breaks the brown-out reboot + // loop that corrupts contacts when battery is deeply depleted (~2.5V). + #if HAS_BQ27220 && defined(AUTO_SHUTDOWN_MILLIVOLTS) + { + uint16_t bootMv = getBattMilliVolts(); + if (bootMv > 0 && bootMv < AUTO_SHUTDOWN_MILLIVOLTS) { + Serial.printf("CRITICAL: Boot voltage %dmV < %dmV — sleeping immediately\n", + bootMv, AUTO_SHUTDOWN_MILLIVOLTS); + // Don't mount SD, don't load contacts, don't pass Go. + // Only wake on user button press (presumably after plugging in charger). + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + esp_sleep_enable_ext1_wakeup(1ULL << PIN_USER_BTN, ESP_EXT1_WAKEUP_ANY_HIGH); + esp_deep_sleep_start(); // CPU halts here + } + } + #endif MESH_DEBUG_PRINTLN("TDeckBoard::begin() - complete"); } diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index fa5a6071..18b51b69 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -62,6 +62,8 @@ build_flags = -D EINK_MOSI=33 -D EINK_BL=45 -D EINK_NOT_HIBERNATE=1 + -D HAS_BQ27220=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=2800 -D EINK_LIMIT_FASTREFRESH=10 -D EINK_LIMIT_GHOSTING_PX=2000 -D DISPLAY_ROTATION=0