Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions src/Power.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(); }
Comment thread
thebentern marked this conversation as resolved.

/**
* return true if there is a battery installed in this unit
Expand Down
31 changes: 31 additions & 0 deletions src/gps/GPS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,10 @@ bool GPS::setup()
}

notifyDeepSleepObserver.observe(&notifyDeepSleep);
#ifdef ARCH_ESP32
notifyLightSleepObserver.observe(&notifyLightSleep);
notifyLightSleepEndObserver.observe(&notifyLightSleepEnd);
#endif

return true;
}
Expand All @@ -827,6 +831,10 @@ GPS::~GPS()
{
// we really should unregister our sleep observer
notifyDeepSleepObserver.unobserve(&notifyDeepSleep);
#ifdef ARCH_ESP32
notifyLightSleepObserver.unobserve(&notifyLightSleep);
notifyLightSleepEndObserver.unobserve(&notifyLightSleepEnd);
#endif
}

// Put the GPS hardware into a specified state
Expand Down Expand Up @@ -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";

Expand Down
13 changes: 13 additions & 0 deletions src/gps/GPS.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ class GPS : private concurrency::OSThread

CallbackObserver<GPS, void *> notifyDeepSleepObserver = CallbackObserver<GPS, void *>(this, &GPS::prepareDeepSleep);

#ifdef ARCH_ESP32
GPSPowerState preLightSleepState = GPS_OFF;
CallbackObserver<GPS, void *> notifyLightSleepObserver = CallbackObserver<GPS, void *>(this, &GPS::prepareLightSleep);
CallbackObserver<GPS, esp_sleep_wakeup_cause_t> notifyLightSleepEndObserver =
CallbackObserver<GPS, esp_sleep_wakeup_cause_t>(this, &GPS::endLightSleep);
#endif

/** If !NULL we will use this serial port to construct our GPS */
#if defined(ARCH_RP2040)
static SerialUART *_serial_gps;
Expand Down Expand Up @@ -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);
Expand Down
29 changes: 28 additions & 1 deletion src/sleep.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment thread
thebentern marked this conversation as resolved.
#else
assert(0); // fallback for archs without a clean restart primitive
#endif
break;
}
}
Expand Down Expand Up @@ -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);
}
Expand Down
Loading