GPS Power State tidy-up (#4161)

* Refactor GPSPowerState enum
Identifies a case where the GPS hardware is awake, but an update is not yet desired

* Change terminology

* Clear old lock-time prediction on triple press

* Use exponential smoothing to predict lock time

* Rename averageLockTime to predictedLockTime

* Attempt: Send PMREQ with duration 0 on MCU deep-sleep

* Attempt 2: Send PMREQ with duration 0 on MCU deep-sleep

* Revert "Attempt 2: Send PMREQ with duration 0 on MCU deep-sleep"

This reverts commit 8b697cd2a4.

* Revert "Attempt: Send PMREQ with duration 0 on MCU deep-sleep"

This reverts commit 9d29ec7603.

* Remove unused notifyGPSSleep Observable
Handled with notifyDeepSleep, and enable() / disable()

* WIP: simplify GPS power management
An initial attempt only.

* Honor #3e9e0fd

* No-op when moving between GPS_IDLE and GPS_ACTIVE

* Ensure U-blox GPS is awake to receive indefinite sleep command

* Longer pause when waking U-blox to send sleep command

* Actually implement soft and hard sleep..

* Dynamically estimate the threshold for GPS_HARDSLEEP

* Fallback to GPS_HARDSLEEP, if GPS_SOFTSLEEP unsupported

* Move "excessive search time" behavior to scheduler class

* Minor logging adjustments

* Promote log to warning

* Gratuitous buffer clearing on boot

* Fix inverted standby pin logic
Specifically the standby pin for L76B, L76K and clones
Discovered during T-Echo testing: totally broken function, probe method failing.

* Remove redundant pin init
Now handled by setPowerState

* Replace max() with if statements
Avoid those platform specific implementations..

* Trunk formatting
New round of settings.json changes keep catching me out, have to remember to re-enable my "clang-format" for windows workaround.

* Remove some asserts from setPowerState
Original aim was to prevent sending a 0 second PMREQ to U-blox hardware as part of a timed sleep (GPS_HARDSLEEP, GPS_SOFTSLEEP). I'm not sure this is super important, and it feels tidier to just allow the 0 second sleeptime here, rather than fudge the sleeptime further up.

* Fix an error determining whether GPS_SOFTSLEEP is supported

* Clarify a log entry

* Set PIN_STANDBY for MCU deep-sleep
Required to reach TTGO's advertised 0.25mA sleep current for T-Echo. Without this change: ~6mA.
This commit is contained in:
todd-herbert 2024-07-11 15:26:43 +12:00 committed by GitHub
parent 11bca437fd
commit 33831cd41c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 436 additions and 296 deletions

View File

@ -9,6 +9,7 @@
#include "main.h" // pmu_found #include "main.h" // pmu_found
#include "sleep.h" #include "sleep.h"
#include "GPSUpdateScheduling.h"
#include "cas.h" #include "cas.h"
#include "ubx.h" #include "ubx.h"
@ -22,19 +23,6 @@
#define GPS_RESET_MODE HIGH #define GPS_RESET_MODE HIGH
#endif #endif
// How many minutes of sleep make it worthwhile to power-off the GPS
// Shorter than this, and GPS will only enter standby
// Affected by lock-time, and config.position.gps_update_interval
#ifndef GPS_STANDBY_THRESHOLD_MINUTES
#define GPS_STANDBY_THRESHOLD_MINUTES 15
#endif
// How many seconds of sleep make it worthwhile for the GPS to use powered-on standby
// Shorter than this, and we'll just wait instead
#ifndef GPS_IDLE_THRESHOLD_SECONDS
#define GPS_IDLE_THRESHOLD_SECONDS 10
#endif
#if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) #if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
HardwareSerial *GPS::_serial_gps = &Serial1; HardwareSerial *GPS::_serial_gps = &Serial1;
#else #else
@ -43,6 +31,8 @@ HardwareSerial *GPS::_serial_gps = NULL;
GPS *gps = nullptr; GPS *gps = nullptr;
GPSUpdateScheduling scheduling;
/// Multiple GPS instances might use the same serial port (in sequence), but we can /// Multiple GPS instances might use the same serial port (in sequence), but we can
/// only init that port once. /// only init that port once.
static bool didSerialInit; static bool didSerialInit;
@ -52,6 +42,25 @@ uint8_t uBloxProtocolVersion;
#define GPS_SOL_EXPIRY_MS 5000 // in millis. give 1 second time to combine different sentences. NMEA Frequency isn't higher anyway #define GPS_SOL_EXPIRY_MS 5000 // in millis. give 1 second time to combine different sentences. NMEA Frequency isn't higher anyway
#define NMEA_MSG_GXGSA "GNGSA" // GSA message (GPGSA, GNGSA etc) #define NMEA_MSG_GXGSA "GNGSA" // GSA message (GPGSA, GNGSA etc)
// For logging
const char *getGPSPowerStateString(GPSPowerState state)
{
switch (state) {
case GPS_ACTIVE:
return "ACTIVE";
case GPS_IDLE:
return "IDLE";
case GPS_SOFTSLEEP:
return "SOFTSLEEP";
case GPS_HARDSLEEP:
return "HARDSLEEP";
case GPS_OFF:
return "OFF";
default:
assert(false); // Unhandled enum value..
}
}
void GPS::UBXChecksum(uint8_t *message, size_t length) void GPS::UBXChecksum(uint8_t *message, size_t length)
{ {
uint8_t CK_A = 0, CK_B = 0; uint8_t CK_A = 0, CK_B = 0;
@ -767,7 +776,6 @@ bool GPS::setup()
} }
notifyDeepSleepObserver.observe(&notifyDeepSleep); notifyDeepSleepObserver.observe(&notifyDeepSleep);
notifyGPSSleepObserver.observe(&notifyGPSSleep);
return true; return true;
} }
@ -776,62 +784,116 @@ GPS::~GPS()
{ {
// we really should unregister our sleep observer // we really should unregister our sleep observer
notifyDeepSleepObserver.unobserve(&notifyDeepSleep); notifyDeepSleepObserver.unobserve(&notifyDeepSleep);
notifyGPSSleepObserver.observe(&notifyGPSSleep);
} }
const char *GPS::powerStateToString() // Put the GPS hardware into a specified state
void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
{ {
switch (powerState) { // Update the stored GPSPowerstate, and create local copies
case GPS_OFF: GPSPowerState oldState = powerState;
return "OFF"; powerState = newState;
case GPS_IDLE: LOG_INFO("GPS power state moving from %s to %s\n", getGPSPowerStateString(oldState), getGPSPowerStateString(newState));
return "IDLE";
case GPS_STANDBY: switch (newState) {
return "STANDBY";
case GPS_ACTIVE: case GPS_ACTIVE:
return "ACTIVE"; case GPS_IDLE:
default: if (oldState == GPS_ACTIVE || oldState == GPS_IDLE) // If hardware already awake, no changes needed
return "UNKNOWN"; break;
if (oldState != GPS_ACTIVE && oldState != GPS_IDLE) // If hardware just waking now, clear buffer
clearBuffer();
powerMon->setState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing)
writePinEN(true); // Power (EN pin): on
setPowerPMU(true); // Power (PMU): on
writePinStandby(false); // Standby (pin): awake (not standby)
setPowerUBLOX(true); // Standby (UBLOX): awake
break;
case GPS_SOFTSLEEP:
powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing)
writePinEN(true); // Power (EN pin): on
setPowerPMU(true); // Power (PMU): on
writePinStandby(true); // Standby (pin): asleep (not awake)
setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed
break;
case GPS_HARDSLEEP:
powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing)
writePinEN(false); // Power (EN pin): off
setPowerPMU(false); // Power (PMU): off
writePinStandby(true); // Standby (pin): asleep (not awake)
setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed
break;
case GPS_OFF:
assert(sleepTime == 0); // This is an indefinite sleep
powerMon->clearState(meshtastic_PowerMon_State_GPS_Active); // Report change for power monitoring (during testing)
writePinEN(false); // Power (EN pin): off
setPowerPMU(false); // Power (PMU): off
writePinStandby(true); // Standby (pin): asleep
setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely
break;
} }
} }
void GPS::setGPSPower(bool on, bool standbyOnly, uint32_t sleepTime) // Set power with EN pin, if relevant
void GPS::writePinEN(bool on)
{ {
// Record the current powerState // Abort: if conflict with Canned Messages when using Wisblock(?)
if (on) if (HW_VENDOR == meshtastic_HardwareModel_RAK4631 && (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1))
powerState = GPS_ACTIVE;
else if (!enabled) // User has disabled with triple press
powerState = GPS_OFF;
else if (sleepTime <= GPS_IDLE_THRESHOLD_SECONDS * 1000UL)
powerState = GPS_IDLE;
else if (standbyOnly)
powerState = GPS_STANDBY;
else
powerState = GPS_OFF;
LOG_DEBUG("GPS::powerState=%s\n", powerStateToString());
// If the next update is due *really soon*, don't actually power off or enter standby. Just wait it out.
if (!on && powerState == GPS_IDLE)
return; return;
if (on) { // Abort: if pin unset
powerMon->setState(meshtastic_PowerMon_State_GPS_Active); if (!en_gpio)
clearBuffer(); // drop any old data waiting in the buffer before re-enabling
if (en_gpio)
digitalWrite(en_gpio, on ? GPS_EN_ACTIVE : !GPS_EN_ACTIVE); // turn this on if defined, every time
} else {
powerMon->clearState(meshtastic_PowerMon_State_GPS_Active);
}
isInPowersave = !on;
if (!standbyOnly && en_gpio != 0 &&
!(HW_VENDOR == meshtastic_HardwareModel_RAK4631 && (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1))) {
LOG_DEBUG("GPS powerdown using GPS_EN_ACTIVE\n");
digitalWrite(en_gpio, on ? GPS_EN_ACTIVE : !GPS_EN_ACTIVE);
return; return;
}
#ifdef HAS_PMU // We only have PMUs on the T-Beam, and that board has a tiny battery to save GPS ephemera, so treat as a standby. // Determine new value for the pin
if (pmu_found && PMU) { bool val = GPS_EN_ACTIVE ? on : !on;
// Write and log
pinMode(en_gpio, OUTPUT);
digitalWrite(en_gpio, val);
#ifdef GPS_EXTRAVERBOSE
LOG_DEBUG("Pin EN %s\n", val == HIGH ? "HIGH" : "LOW");
#endif
}
// Set the value of the STANDBY pin, if relevant
// true for standby state, false for awake
void GPS::writePinStandby(bool standby)
{
#ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones
// Determine the new value for the pin
// Normally: active HIGH for awake
#if PIN_GPS_STANDBY_INVERTED
bool val = standby;
#else
bool val = !standby;
#endif
// Write and log
pinMode(PIN_GPS_STANDBY, OUTPUT);
digitalWrite(PIN_GPS_STANDBY, val);
#ifdef GPS_EXTRAVERBOSE
LOG_DEBUG("Pin STANDBY %s\n", val == HIGH ? "HIGH" : "LOW");
#endif
#endif
}
// Enable / Disable GPS with PMU, if present
void GPS::setPowerPMU(bool on)
{
// We only have PMUs on the T-Beam, and that board has a tiny battery to save GPS ephemera,
// so treat as a standby.
#ifdef HAS_PMU
// Abort: if no PMU
if (!pmu_found)
return;
// Abort: if PMU not initialized
if (!PMU)
return;
uint8_t model = PMU->getChipModel(); uint8_t model = PMU->getChipModel();
if (model == XPOWERS_AXP2101) { if (model == XPOWERS_AXP2101) {
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
@ -845,55 +907,62 @@ void GPS::setGPSPower(bool on, bool standbyOnly, uint32_t sleepTime)
// t-beam v1.1 GNSS power channel // t-beam v1.1 GNSS power channel
on ? PMU->enablePowerOutput(XPOWERS_LDO3) : PMU->disablePowerOutput(XPOWERS_LDO3); on ? PMU->enablePowerOutput(XPOWERS_LDO3) : PMU->disablePowerOutput(XPOWERS_LDO3);
} }
return;
} #ifdef GPS_EXTRAVERBOSE
LOG_DEBUG("PMU %s\n", on ? "on" : "off");
#endif #endif
#ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones #endif
}
// Set UBLOX power, if relevant
void GPS::setPowerUBLOX(bool on, uint32_t sleepMs)
{
// Abort: if not UBLOX hardware
if (gnssModel != GNSS_MODEL_UBLOX)
return;
// If waking
if (on) { if (on) {
LOG_INFO("Waking GPS\n");
pinMode(PIN_GPS_STANDBY, OUTPUT);
// Some PCB's use an inverse logic due to a transistor driver
// Example for this is the Pico-Waveshare Lora+GPS HAT
#ifdef PIN_GPS_STANDBY_INVERTED
digitalWrite(PIN_GPS_STANDBY, 0);
#else
digitalWrite(PIN_GPS_STANDBY, 1);
#endif
return;
} else {
LOG_INFO("GPS entering sleep\n");
// notifyGPSSleep.notifyObservers(NULL);
pinMode(PIN_GPS_STANDBY, OUTPUT);
#ifdef PIN_GPS_STANDBY_INVERTED
digitalWrite(PIN_GPS_STANDBY, 1);
#else
digitalWrite(PIN_GPS_STANDBY, 0);
#endif
return;
}
#endif
if (!on) {
if (gnssModel == GNSS_MODEL_UBLOX) {
uint8_t msglen;
LOG_DEBUG("Sleep Time: %i\n", sleepTime);
if (strncmp(info.hwVersion, "000A0000", 8) != 0) {
for (int i = 0; i < 4; i++) {
gps->_message_PMREQ[0 + i] = sleepTime >> (i * 8); // Encode the sleep time in millis into the packet
}
msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ), gps->_message_PMREQ);
} else {
for (int i = 0; i < 4; i++) {
gps->_message_PMREQ_10[4 + i] = sleepTime >> (i * 8); // Encode the sleep time in millis into the packet
}
msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ_10), gps->_message_PMREQ_10);
}
gps->_serial_gps->write(gps->UBXscratch, msglen);
}
} else {
if (gnssModel == GNSS_MODEL_UBLOX) {
gps->_serial_gps->write(0xFF); gps->_serial_gps->write(0xFF);
clearBuffer(); // This often returns old data, so drop it clearBuffer(); // This often returns old data, so drop it
#ifdef GPS_EXTRAVERBOSE
LOG_DEBUG("UBLOX: wake\n");
#endif
} }
// If putting to sleep
else {
uint8_t msglen;
// If we're being asked to sleep indefinitely, make *sure* we're awake first, to process the new sleep command
if (sleepMs == 0) {
setPowerUBLOX(true);
delay(500);
}
// Determine hardware version
if (strncmp(info.hwVersion, "000A0000", 8) != 0) {
// Encode the sleep time in millis into the packet
for (int i = 0; i < 4; i++)
gps->_message_PMREQ[0 + i] = sleepMs >> (i * 8);
// Record the message length
msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ), gps->_message_PMREQ);
} else {
// Encode the sleep time in millis into the packet
for (int i = 0; i < 4; i++)
gps->_message_PMREQ_10[4 + i] = sleepMs >> (i * 8);
// Record the message length
msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ_10), gps->_message_PMREQ_10);
}
// Send the UBX packet
gps->_serial_gps->write(gps->UBXscratch, msglen);
#ifdef GPS_EXTRAVERBOSE
LOG_DEBUG("UBLOX: sleep for %dmS\n", sleepMs);
#endif
} }
} }
@ -906,106 +975,52 @@ void GPS::setConnected()
} }
} }
/** // We want a GPS lock. Wake the hardware
* Switch the GPS into a mode where we are actively looking for a lock, or alternatively switch GPS into a low power mode void GPS::up()
*
* calls sleep/wake
*/
void GPS::setAwake(bool wantAwake)
{ {
scheduling.informSearching();
setPowerState(GPS_ACTIVE);
}
// If user has disabled GPS, make sure it is off, not just in standby or idle // We've got a GPS lock. Enter a low power state, potentially.
if (!wantAwake && !enabled && powerState != GPS_OFF) { void GPS::down()
setGPSPower(false, false, 0); {
return; scheduling.informGotLock();
} uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs();
uint32_t sleepTime = scheduling.msUntilNextSearch();
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval);
// If GPS power state needs to change LOG_DEBUG("%us until next search\n", sleepTime / 1000);
if ((wantAwake && powerState != GPS_ACTIVE) || (!wantAwake && powerState == GPS_ACTIVE)) {
LOG_DEBUG("WANT GPS=%d\n", wantAwake);
// Calculate how long it takes to get a GPS lock // If update interval less than 10 seconds, no attempt to sleep
if (wantAwake) { if (updateInterval <= 10 * 1000UL)
// Record the time we start looking for a lock setPowerState(GPS_IDLE);
lastWakeStartMsec = millis();
} else {
// Record by how much we missed our ideal target postion.gps_update_interval (for logging only)
// Need to calculate this before we update lastSleepStartMsec, to make the new prediction
int32_t lateByMsec = (int32_t)(millis() - lastSleepStartMsec) - (int32_t)getSleepTime();
// Record the time we finish looking for a lock
lastSleepStartMsec = millis();
// How long did it take to get GPS lock this time?
uint32_t lockTime = lastSleepStartMsec - lastWakeStartMsec;
// Update the lock-time prediction
// Used pre-emptively, attempting to hit target of gps.position_update_interval
switch (GPSCycles) {
case 0:
LOG_DEBUG("Initial GPS lock took %ds\n", lockTime / 1000);
break;
case 1:
predictedLockTime = lockTime; // Avoid slow ramp-up - start with a real value
LOG_DEBUG("GPS Lock took %ds\n", lockTime / 1000);
break;
default:
// Predict lock-time using exponential smoothing: respond slowly to changes
predictedLockTime = (lockTime * 0.2) + (predictedLockTime * 0.8); // Latest lock time has 20% weight on prediction
LOG_INFO("GPS Lock took %ds. %s by %ds. Next lock predicted to take %ds.\n", lockTime / 1000,
(lateByMsec > 0) ? "Late" : "Early", abs(lateByMsec) / 1000, predictedLockTime / 1000);
}
GPSCycles++;
}
// How long to wait before attempting next GPS update
// Aims to hit position.gps_update_interval by using the lock-time prediction
uint32_t compensatedSleepTime = (getSleepTime() > predictedLockTime) ? (getSleepTime() - predictedLockTime) : 0;
// If long interval between updates: power off between updates
if (compensatedSleepTime > GPS_STANDBY_THRESHOLD_MINUTES * MS_IN_MINUTE) {
setGPSPower(wantAwake, false, getSleepTime() - predictedLockTime);
}
// If waking relatively frequently: don't power off. Would use more energy trying to reacquire lock each time
// We'll either use a "powered-on" standby, or just wait it out, depending on how soon the next update is due
// Will decide which inside setGPSPower method
else { else {
#ifdef GPS_UC6580 // Check whether the GPS hardware is capable of GPS_SOFTSLEEP
setGPSPower(wantAwake, false, compensatedSleepTime); // If not, fallback to GPS_HARDSLEEP instead
#else bool softsleepSupported = false;
setGPSPower(wantAwake, true, compensatedSleepTime); if (gnssModel == GNSS_MODEL_UBLOX) // U-blox is supported via PMREQ
softsleepSupported = true;
#ifdef PIN_GPS_STANDBY // L76B, L76K and clones have a standby pin
softsleepSupported = true;
#endif #endif
// How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than GPS_SOFTSLEEP?
// Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M and M10050
// https://www.desmos.com/calculator/6gvjghoumr
// This is not particularly accurate, but probably an impromevement over a single, fixed threshold
uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22));
LOG_DEBUG("gps_update_interval >= %us needed to justify hardsleep\n", hardsleepThreshold / 1000);
// If update interval too short: softsleep (if supported by hardware)
if (softsleepSupported && updateInterval < hardsleepThreshold)
setPowerState(GPS_SOFTSLEEP, sleepTime);
// If update interval long enough (or softsleep unsupported): hardsleep instead
else
setPowerState(GPS_HARDSLEEP, sleepTime);
} }
}
}
/** Get how long we should stay looking for each acquisition in msecs
*/
uint32_t GPS::getWakeTime() const
{
uint32_t t = config.position.position_broadcast_secs;
if (t == UINT32_MAX)
return t; // already maxint
return Default::getConfiguredOrDefaultMs(t, default_broadcast_interval_secs);
}
/** Get how long we should sleep between aqusition attempts in msecs
*/
uint32_t GPS::getSleepTime() const
{
uint32_t t = config.position.gps_update_interval;
// We'll not need the GPS thread to wake up again after first acq. with fixed position.
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED || config.position.fixed_position)
t = UINT32_MAX; // Sleep forever now
if (t == UINT32_MAX)
return t; // already maxint
return Default::getConfiguredOrDefaultMs(t, default_gps_update_interval);
} }
void GPS::publishUpdate() void GPS::publishUpdate()
@ -1056,13 +1071,13 @@ int32_t GPS::runOnce()
return disable(); return disable();
} }
if (whileIdle()) { if (whileActive()) {
// if we have received valid NMEA claim we are connected // if we have received valid NMEA claim we are connected
setConnected(); setConnected();
} else { } else {
if ((config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) && (gnssModel == GNSS_MODEL_UBLOX)) { if ((config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) && (gnssModel == GNSS_MODEL_UBLOX)) {
// reset the GPS on next bootup // reset the GPS on next bootup
if (devicestate.did_gps_reset && (millis() - lastWakeStartMsec > 60000) && !hasFlow()) { if (devicestate.did_gps_reset && scheduling.elapsedSearchMs() > 60 * 1000UL && !hasFlow()) {
LOG_DEBUG("GPS is not communicating, trying factory reset on next bootup.\n"); LOG_DEBUG("GPS is not communicating, trying factory reset on next bootup.\n");
devicestate.did_gps_reset = false; devicestate.did_gps_reset = false;
nodeDB->saveDeviceStateToDisk(); nodeDB->saveDeviceStateToDisk();
@ -1077,20 +1092,10 @@ int32_t GPS::runOnce()
// gps->factoryReset(); // gps->factoryReset();
} }
// If we are overdue for an update, turn on the GPS and at least publish the current status // If we're due for an update, wake the GPS
uint32_t now = millis(); if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue())
uint32_t timeAsleep = now - lastSleepStartMsec; up();
auto sleepTime = getSleepTime();
if (powerState != GPS_ACTIVE && (sleepTime != UINT32_MAX) &&
((timeAsleep > sleepTime) || (isInPowersave && timeAsleep > (sleepTime - predictedLockTime)))) {
// We now want to be awake - so wake up the GPS
setAwake(true);
}
// While we are awake
if (powerState == GPS_ACTIVE) {
// LOG_DEBUG("looking for location\n");
// If we've already set time from the GPS, no need to ask the GPS // If we've already set time from the GPS, no need to ask the GPS
bool gotTime = (getRTCQuality() >= RTCQualityGPS); bool gotTime = (getRTCQuality() >= RTCQualityGPS);
if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time
@ -1105,9 +1110,9 @@ int32_t GPS::runOnce()
shouldPublish = true; shouldPublish = true;
} }
now = millis(); bool tooLong = scheduling.searchedTooLong();
auto wakeTime = getWakeTime(); if (tooLong)
bool tooLong = wakeTime != UINT32_MAX && (now - lastWakeStartMsec) > wakeTime; LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time.\n");
// Once we get a location we no longer desperately want an update // Once we get a location we no longer desperately want an update
// LOG_DEBUG("gotLoc %d, tooLong %d, gotTime %d\n", gotLoc, tooLong, gotTime); // LOG_DEBUG("gotLoc %d, tooLong %d, gotTime %d\n", gotLoc, tooLong, gotTime);
@ -1116,16 +1121,15 @@ int32_t GPS::runOnce()
if (tooLong) { if (tooLong) {
// we didn't get a location during this ack window, therefore declare loss of lock // we didn't get a location during this ack window, therefore declare loss of lock
if (hasValidLocation) { if (hasValidLocation) {
LOG_DEBUG("hasValidLocation FALLING EDGE (last read: %d)\n", gotLoc); LOG_DEBUG("hasValidLocation FALLING EDGE\n");
} }
p = meshtastic_Position_init_default; p = meshtastic_Position_init_default;
hasValidLocation = false; hasValidLocation = false;
} }
setAwake(false); down();
shouldPublish = true; // publish our update for this just finished acquisition window shouldPublish = true; // publish our update for this just finished acquisition window
} }
}
// If state has changed do a publish // If state has changed do a publish
publishUpdate(); publishUpdate();
@ -1150,9 +1154,7 @@ void GPS::clearBuffer()
int GPS::prepareDeepSleep(void *unused) int GPS::prepareDeepSleep(void *unused)
{ {
LOG_INFO("GPS deep sleep!\n"); LOG_INFO("GPS deep sleep!\n");
disable();
setAwake(false);
return 0; return 0;
} }
@ -1348,12 +1350,6 @@ GPS *GPS::createGps()
new_gps->tx_gpio = _tx_gpio; new_gps->tx_gpio = _tx_gpio;
new_gps->en_gpio = _en_gpio; new_gps->en_gpio = _en_gpio;
if (_en_gpio != 0) {
LOG_DEBUG("Setting %d to output.\n", _en_gpio);
pinMode(_en_gpio, OUTPUT);
digitalWrite(_en_gpio, !GPS_EN_ACTIVE);
}
#ifdef PIN_GPS_PPS #ifdef PIN_GPS_PPS
// pulse per second // pulse per second
pinMode(PIN_GPS_PPS, INPUT); pinMode(PIN_GPS_PPS, INPUT);
@ -1368,7 +1364,8 @@ GPS *GPS::createGps()
LOG_DEBUG("Using " NMEA_MSG_GXGSA " for 3DFIX and PDOP\n"); LOG_DEBUG("Using " NMEA_MSG_GXGSA " for 3DFIX and PDOP\n");
#endif #endif
new_gps->setGPSPower(true, false, 0); // Make sure the GPS is awake before performing any init.
new_gps->up();
#ifdef PIN_GPS_RESET #ifdef PIN_GPS_RESET
pinMode(PIN_GPS_RESET, OUTPUT); pinMode(PIN_GPS_RESET, OUTPUT);
@ -1376,7 +1373,6 @@ GPS *GPS::createGps()
delay(10); delay(10);
digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE);
#endif #endif
new_gps->setAwake(true); // Wake GPS power before doing any init
if (_serial_gps) { if (_serial_gps) {
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
@ -1662,13 +1658,13 @@ bool GPS::hasFlow()
return reader.passedChecksum() > 0; return reader.passedChecksum() > 0;
} }
bool GPS::whileIdle() bool GPS::whileActive()
{ {
unsigned int charsInBuf = 0; unsigned int charsInBuf = 0;
bool isValid = false; bool isValid = false;
if (powerState != GPS_ACTIVE) { if (powerState != GPS_ACTIVE) {
clearBuffer(); clearBuffer();
return (powerState == GPS_ACTIVE); return false;
} }
#ifdef SERIAL_BUFFER_SIZE #ifdef SERIAL_BUFFER_SIZE
if (_serial_gps->available() >= SERIAL_BUFFER_SIZE - 1) { if (_serial_gps->available() >= SERIAL_BUFFER_SIZE - 1) {
@ -1699,20 +1695,21 @@ bool GPS::whileIdle()
} }
void GPS::enable() void GPS::enable()
{ {
// Clear the old lock-time prediction // Clear the old scheduling info (reset the lock-time prediction)
GPSCycles = 0; scheduling.reset();
predictedLockTime = 0;
enabled = true; enabled = true;
setInterval(GPS_THREAD_INTERVAL); setInterval(GPS_THREAD_INTERVAL);
setAwake(true);
scheduling.informSearching();
setPowerState(GPS_ACTIVE);
} }
int32_t GPS::disable() int32_t GPS::disable()
{ {
enabled = false; enabled = false;
setInterval(INT32_MAX); setInterval(INT32_MAX);
setAwake(false); setPowerState(GPS_OFF);
return INT32_MAX; return INT32_MAX;
} }
@ -1721,11 +1718,11 @@ void GPS::toggleGpsMode()
{ {
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
LOG_DEBUG("Flag set to false for gps power. GpsMode: DISABLED\n"); LOG_INFO("User toggled GpsMode. Now DISABLED.\n");
disable(); disable();
} else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) { } else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) {
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
LOG_DEBUG("Flag set to true to restore power. GpsMode: ENABLED\n"); LOG_INFO("User toggled GpsMode. Now ENABLED\n");
enable(); enable();
} }
} }

View File

@ -39,10 +39,11 @@ typedef enum {
} GPS_RESPONSE; } GPS_RESPONSE;
enum GPSPowerState : uint8_t { enum GPSPowerState : uint8_t {
GPS_OFF = 0, // Physically powered off GPS_ACTIVE, // Awake and want a position
GPS_ACTIVE = 1, // Awake and want a position GPS_IDLE, // Awake, but not wanting another position yet
GPS_STANDBY = 2, // Physically powered on, but soft-sleeping GPS_SOFTSLEEP, // Physically powered on, but soft-sleeping
GPS_IDLE = 3, // Awake, but not wanting another position yet GPS_HARDSLEEP, // Physically powered off, but scheduled to wake
GPS_OFF // Powered off indefinitely
}; };
// Generate a string representation of DOP // Generate a string representation of DOP
@ -67,14 +68,11 @@ class GPS : private concurrency::OSThread
uint8_t fixType = 0; // fix type from GPGSA uint8_t fixType = 0; // fix type from GPGSA
#endif #endif
private: private:
uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0;
const int serialSpeeds[6] = {9600, 4800, 38400, 57600, 115200, 9600}; const int serialSpeeds[6] = {9600, 4800, 38400, 57600, 115200, 9600};
uint32_t rx_gpio = 0; uint32_t rx_gpio = 0;
uint32_t tx_gpio = 0; uint32_t tx_gpio = 0;
uint32_t en_gpio = 0; uint32_t en_gpio = 0;
uint32_t predictedLockTime = 0;
uint32_t GPSCycles = 0;
int speedSelect = 0; int speedSelect = 0;
int probeTries = 2; int probeTries = 2;
@ -99,7 +97,6 @@ class GPS : private concurrency::OSThread
uint8_t numSatellites = 0; uint8_t numSatellites = 0;
CallbackObserver<GPS, void *> notifyDeepSleepObserver = CallbackObserver<GPS, void *>(this, &GPS::prepareDeepSleep); CallbackObserver<GPS, void *> notifyDeepSleepObserver = CallbackObserver<GPS, void *>(this, &GPS::prepareDeepSleep);
CallbackObserver<GPS, void *> notifyGPSSleepObserver = CallbackObserver<GPS, void *>(this, &GPS::prepareDeepSleep);
public: public:
/** If !NULL we will use this serial port to construct our GPS */ /** If !NULL we will use this serial port to construct our GPS */
@ -175,7 +172,8 @@ class GPS : private concurrency::OSThread
// toggle between enabled/disabled // toggle between enabled/disabled
void toggleGpsMode(); void toggleGpsMode();
void setGPSPower(bool on, bool standbyOnly, uint32_t sleepTime); // Change the power state of the GPS - for power saving / shutdown
void setPowerState(GPSPowerState newState, uint32_t sleepMs = 0);
/// Returns true if we have acquired GPS lock. /// Returns true if we have acquired GPS lock.
virtual bool hasLock(); virtual bool hasLock();
@ -206,18 +204,18 @@ class GPS : private concurrency::OSThread
GPS_RESPONSE getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis); GPS_RESPONSE getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis);
/**
* Switch the GPS into a mode where we are actively looking for a lock, or alternatively switch GPS into a low power mode
*
* calls sleep/wake
*/
void setAwake(bool on);
virtual bool factoryReset(); virtual bool factoryReset();
// Creates an instance of the GPS class. // Creates an instance of the GPS class.
// Returns the new instance or null if the GPS is not present. // Returns the new instance or null if the GPS is not present.
static GPS *createGps(); static GPS *createGps();
// Wake the GPS hardware - ready for an update
void up();
// Let the GPS hardware save power between updates
void down();
protected: protected:
/** /**
* Perform any processing that should be done only while the GPS is awake and looking for a fix. * Perform any processing that should be done only while the GPS is awake and looking for a fix.
@ -240,7 +238,7 @@ class GPS : private concurrency::OSThread
* *
* Return true if we received a valid message from the GPS * Return true if we received a valid message from the GPS
*/ */
virtual bool whileIdle(); virtual bool whileActive();
/** /**
* Perform any processing that should be done only while the GPS is awake and looking for a fix. * Perform any processing that should be done only while the GPS is awake and looking for a fix.
@ -267,13 +265,21 @@ class GPS : private concurrency::OSThread
void UBXChecksum(uint8_t *message, size_t length); void UBXChecksum(uint8_t *message, size_t length);
void CASChecksum(uint8_t *message, size_t length); void CASChecksum(uint8_t *message, size_t length);
/** Get how long we should stay looking for each aquisition /** Set power with EN pin, if relevant
*/ */
uint32_t getWakeTime() const; void writePinEN(bool on);
/** Get how long we should sleep between aqusition attempts /** Set the value of the STANDBY pin, if relevant
*/ */
uint32_t getSleepTime() const; void writePinStandby(bool standby);
/** Set GPS power with PMU, if relevant
*/
void setPowerPMU(bool on);
/** Set UBLOX power, if relevant
*/
void setPowerUBLOX(bool on, uint32_t sleepMs = 0);
/** /**
* Tell users we have new GPS readings * Tell users we have new GPS readings

View File

@ -0,0 +1,118 @@
#include "GPSUpdateScheduling.h"
#include "Default.h"
// Mark the time when searching for GPS position begins
void GPSUpdateScheduling::informSearching()
{
searchStartedMs = millis();
}
// Mark the time when searching for GPS is complete,
// then update the predicted lock-time
void GPSUpdateScheduling::informGotLock()
{
searchEndedMs = millis();
LOG_DEBUG("Took %us to get lock\n", (searchEndedMs - searchStartedMs) / 1000);
updateLockTimePrediction();
}
// Clear old lock-time prediction data.
// When re-enabling GPS with user button.
void GPSUpdateScheduling::reset()
{
searchStartedMs = 0;
searchEndedMs = 0;
searchCount = 0;
predictedMsToGetLock = 0;
}
// How many milliseconds before we should next search for GPS position
// Used by GPS hardware directly, to enter timed hardware sleep
uint32_t GPSUpdateScheduling::msUntilNextSearch()
{
uint32_t now = millis();
// Target interval (seconds), between GPS updates
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval);
// Check how long until we should start searching, to hopefully hit our target interval
uint32_t dueAtMs = searchEndedMs + updateInterval;
uint32_t compensatedStart = dueAtMs - predictedMsToGetLock;
int32_t remainingMs = compensatedStart - now;
// If we should have already started (negative value), start ASAP
if (remainingMs < 0)
remainingMs = 0;
return (uint32_t)remainingMs;
}
// How long have we already been searching?
// Used to abort a search in progress, if it runs unnaceptably long
uint32_t GPSUpdateScheduling::elapsedSearchMs()
{
// If searching
if (searchStartedMs > searchEndedMs)
return millis() - searchStartedMs;
// If not searching - 0ms. We shouldn't really consume this value
else
return 0;
}
// Is it now time to begin searching for a GPS position?
bool GPSUpdateScheduling::isUpdateDue()
{
return (msUntilNextSearch() == 0);
}
// Have we been searching for a GPS position for too long?
bool GPSUpdateScheduling::searchedTooLong()
{
uint32_t maxSearchMs =
Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs);
// If broadcast interval set to max, no such thing as "too long"
if (maxSearchMs == UINT32_MAX)
return false;
// If we've been searching longer than our position broadcast interval: that's too long
else if (elapsedSearchMs() > maxSearchMs)
return true;
// Otherwise, not too long yet!
else
return false;
}
// Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation
void GPSUpdateScheduling::updateLockTimePrediction()
{
// How long did it take to get GPS lock this time?
// Duration between down() calls
int32_t lockTime = searchEndedMs - searchStartedMs;
if (lockTime < 0)
lockTime = 0;
// Ignore the first lock-time: likely to be long, will skew data
// Second locktime: likely stable. Use to intialize the smoothing filter
if (searchCount == 1)
predictedMsToGetLock = lockTime;
// Third locktime and after: predict using exponential smoothing. Respond slowly to changes
else if (searchCount > 1)
predictedMsToGetLock = (lockTime * weighting) + (predictedMsToGetLock * (1 - weighting));
searchCount++; // Only tracked so we can diregard initial lock-times
LOG_DEBUG("Predicting %us to get next lock\n", predictedMsToGetLock / 1000);
}
// How long do we expect to spend searching for a lock?
uint32_t GPSUpdateScheduling::predictedSearchDurationMs()
{
return GPSUpdateScheduling::predictedMsToGetLock;
}

View File

@ -0,0 +1,29 @@
#pragma once
#include "configuration.h"
// Encapsulates code responsible for the timing of GPS updates
class GPSUpdateScheduling
{
public:
// Marks the time of these events, for calculation use
void informSearching();
void informGotLock(); // Predicted lock-time is recalculated here
void reset(); // Reset the prediction - after GPS::disable() / GPS::enable()
bool isUpdateDue(); // Is it time to begin searching for a GPS position?
bool searchedTooLong(); // Have we been searching for too long?
uint32_t msUntilNextSearch(); // How long until we need to begin searching for a GPS? Info provided to GPS hardware for sleep
uint32_t elapsedSearchMs(); // How long have we been searching so far?
uint32_t predictedSearchDurationMs(); // How long do we expect to spend searching for a lock?
private:
void updateLockTimePrediction(); // Called from informGotLock
uint32_t searchStartedMs = 0;
uint32_t searchEndedMs = 0;
uint32_t searchCount = 0;
uint32_t predictedMsToGetLock = 0;
const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time".
};

View File

@ -37,10 +37,7 @@ Observable<void *> preflightSleep;
/// Called to tell observers we are now entering sleep and you should prepare. Must return 0 /// Called to tell observers we are now entering sleep and you should prepare. Must return 0
/// notifySleep will be called for light or deep sleep, notifyDeepSleep is only called for deep sleep /// notifySleep will be called for light or deep sleep, notifyDeepSleep is only called for deep sleep
/// notifyGPSSleep will be called when config.position.gps_enabled is set to 0 or from buttonthread when GPS_POWER_TOGGLE is
/// enabled.
Observable<void *> notifySleep, notifyDeepSleep; Observable<void *> notifySleep, notifyDeepSleep;
Observable<void *> notifyGPSSleep;
// deep sleep support // deep sleep support
RTC_DATA_ATTR int bootCount = 0; RTC_DATA_ATTR int bootCount = 0;
@ -240,11 +237,6 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false)
#ifdef PIN_POWER_EN #ifdef PIN_POWER_EN
pinMode(PIN_POWER_EN, INPUT); // power off peripherals pinMode(PIN_POWER_EN, INPUT); // power off peripherals
// pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN);
#endif
#if HAS_GPS
// Kill GPS power completely (even if previously we just had it in sleep mode)
if (gps)
gps->setGPSPower(false, false, 0);
#endif #endif
setLed(false); setLed(false);

View File

@ -41,8 +41,6 @@ extern Observable<void *> notifySleep;
/// Called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0 /// Called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0
extern Observable<void *> notifyDeepSleep; extern Observable<void *> notifyDeepSleep;
/// Called to tell GPS thread to enter deep sleep independently of LoRa/MCU sleep, prior to full poweroff. Must return 0
extern Observable<void *> notifyGPSSleep;
void enableModemSleep(); void enableModemSleep();
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
void enableLoraInterrupt(); void enableLoraInterrupt();