diff --git a/src/Power.cpp b/src/Power.cpp index bb9f554be40..fa41c694023 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -848,6 +848,17 @@ void Power::shutdown() #endif } +// Persisted across SDS / deep-sleep on ESP32 so a flat-cell device that has +// already chosen to deep-sleep doesn't wake on the timer, see still-low +// voltage, and burn boot energy on a doomed full init only to deep-sleep +// again. On non-ESP32 the SDS path doesn't reset this anyway, so a plain +// static gives the same effective semantics. +#ifdef ARCH_ESP32 +RTC_DATA_ATTR static bool low_battery_latched = false; +#else +static bool low_battery_latched = false; +#endif + /// Reads power status to powerStatus singleton. // // TODO(girts): move this and other axp stuff to power.h/power.cpp. @@ -968,11 +979,41 @@ void Power::readPowerStatus() // if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { - if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) { + // Hysteresis around the low-battery latch: + // - The latch survives SDS via RTC memory so a still-flat cell doesn't + // wake-thrash on the timer. + // - To clear the latch, voltage has to recover above OCV[min] + + // LOW_BATT_HYSTERESIS_MV (per cell) for the same K reads we used + // to set it. + // - If still low when we wake, immediately re-trigger SDS rather than + // spending battery on a doomed boot. + // OCV[] is per-cell; getBattVoltage() returns pack voltage. Scale by + // NUM_CELLS to keep the comparison consistent with the per-cell + // table on multi-cell variants (e.g. chatter2, station-g1). + constexpr int LOW_BATT_HYSTERESIS_MV_PER_CELL = 100; + const int v = batteryLevel->getBattVoltage(); + const int lowThreshold = OCV[NUM_OCV_POINTS - 1] * NUM_CELLS; + const int recoveryThreshold = lowThreshold + LOW_BATT_HYSTERESIS_MV_PER_CELL * NUM_CELLS; + if (low_battery_latched) { + if (v > recoveryThreshold) { + low_voltage_counter++; + if (low_voltage_counter > 10) { + LOG_INFO("Battery recovered above hysteresis threshold, clear latch"); + low_battery_latched = false; + low_voltage_counter = 0; + } + } else { + low_voltage_counter = 0; + LOG_INFO("Latched low-battery still flat at %dmV, re-enter deep sleep", v); + powerFSM.trigger(EVENT_LOW_BATTERY); + } + } else if (v < lowThreshold) { low_voltage_counter++; LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter); if (low_voltage_counter > 10) { LOG_INFO("Low voltage detected, trigger deep sleep"); + low_battery_latched = true; + low_voltage_counter = 0; powerFSM.trigger(EVENT_LOW_BATTERY); } } else { @@ -1668,9 +1709,14 @@ class LipoCharger : public HasBatteryLevel } /** - * The raw voltage of the battery in millivolts, or NAN if unknown + * The raw voltage of the battery in millivolts, or 0 if unknown. + * Use the BQ25896 charger ADC for both voltage and presence detection so + * the two readings can never disagree. The BQ27220 fuel gauge can report + * 0 / stale on a freshly-attached cell before its initial learn cycle + * finishes, which would make hasBattery=false but voltage=4.0V — the + * upstream PowerFSM then thinks the device is externally powered. */ - virtual uint16_t getBattVoltage() override { return bq->getVoltage(); } + virtual uint16_t getBattVoltage() override { return PPM->getBattVoltage(); } /** * return true if there is a battery installed in this unit diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index e581eb0ef98..72fd752efc4 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -819,6 +819,10 @@ bool GPS::setup() } notifyDeepSleepObserver.observe(¬ifyDeepSleep); +#ifdef ARCH_ESP32 + notifyLightSleepObserver.observe(¬ifyLightSleep); + notifyLightSleepEndObserver.observe(¬ifyLightSleepEnd); +#endif return true; } @@ -827,6 +831,10 @@ GPS::~GPS() { // we really should unregister our sleep observer notifyDeepSleepObserver.unobserve(¬ifyDeepSleep); +#ifdef ARCH_ESP32 + notifyLightSleepObserver.unobserve(¬ifyLightSleep); + notifyLightSleepEndObserver.unobserve(¬ifyLightSleepEnd); +#endif } // Put the GPS hardware into a specified state @@ -1246,6 +1254,29 @@ int GPS::prepareDeepSleep(void *unused) return 0; } +#ifdef ARCH_ESP32 +// Light-sleep is short and the GPS state machine survives across it; soft-sleep +// the receiver instead of disabling so we don't pay re-init / re-acquire cost +// on every cycle. Without this the GPS keeps drawing ~25 mA the whole time the +// MCU is asleep — the dominant drain on most non-router devices. +int GPS::prepareLightSleep(void *unused) +{ + preLightSleepState = powerState; + if (powerState == GPS_ACTIVE) { + setPowerState(GPS_SOFTSLEEP); + } + return 0; +} + +int GPS::endLightSleep(esp_sleep_wakeup_cause_t cause) +{ + if (preLightSleepState == GPS_ACTIVE && powerState != GPS_ACTIVE) { + setPowerState(GPS_ACTIVE); + } + return 0; +} +#endif + static const char *PROBE_MESSAGE = "Trying %s (%s)..."; static const char *DETECTED_MESSAGE = "%s detected"; diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 8d63ce82fee..2778ae79d4c 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -198,6 +198,13 @@ class GPS : private concurrency::OSThread CallbackObserver notifyDeepSleepObserver = CallbackObserver(this, &GPS::prepareDeepSleep); +#ifdef ARCH_ESP32 + GPSPowerState preLightSleepState = GPS_OFF; + CallbackObserver notifyLightSleepObserver = CallbackObserver(this, &GPS::prepareLightSleep); + CallbackObserver notifyLightSleepEndObserver = + CallbackObserver(this, &GPS::endLightSleep); +#endif + /** If !NULL we will use this serial port to construct our GPS */ #if defined(ARCH_RP2040) static SerialUART *_serial_gps; @@ -226,6 +233,12 @@ class GPS : private concurrency::OSThread /// always returns 0 to indicate okay to sleep int prepareDeepSleep(void *unused); +#ifdef ARCH_ESP32 + /// Drop the GPS into a low-power state for the duration of CPU light-sleep, and restore on wake. + int prepareLightSleep(void *unused); + int endLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + /** Set power with EN pin, if relevant */ void writePinEN(bool on); diff --git a/src/sleep.cpp b/src/sleep.cpp index 792781f6d0d..eb1fa784fba 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -195,7 +195,26 @@ static void waitEnterSleep(bool skipPreflight = false) if (!Throttle::isWithinTimespanMs(now, THIRTY_SECONDS_MS)) { // If we wait too long just report an error and go to sleep RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_SLEEP_ENTER_WAIT); - assert(0); // FIXME - for now we just restart, need to fix bug #167 + // FIXME issue #167: a misbehaving observer can veto sleep forever. + // Restart cleanly rather than panicking — same effective recovery, + // no core-dump pollution, and the firmware can come back up with a + // working state machine instead of triggering the panic handler. + LOG_ERROR("Preflight sleep wait exceeded 30s, restarting"); + // Notify reboot observers (e.g. InkHUD) so they can persist / + // shut down cleanly, matching Power::reboot's contract. + notifyReboot.notifyObservers(NULL); + console->flush(); +#if defined(ARCH_ESP32) + ESP.restart(); +#elif defined(ARCH_NRF52) + NVIC_SystemReset(); +#elif defined(ARCH_RP2040) + rp2040.reboot(); +#elif defined(ARCH_STM32WL) + HAL_NVIC_SystemReset(); +#else + assert(0); // fallback for archs without a clean restart primitive +#endif break; } } @@ -370,6 +389,14 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN pinMode(I2C_SCL, ANALOG); #endif +#ifdef ARCH_ESP32 + // gpio_hold_en alone only retains pin state during light sleep on ESP32; + // for the holds to apply during deep sleep the global enable must be + // armed once before esp_deep_sleep_start. Without this, BUTTON_PIN and + // LORA_CS float in DSLP and can leak current or trigger spurious activity. + gpio_deep_sleep_hold_en(); +#endif + console->flush(); cpuDeepSleep(msecToWake); }