diff --git a/docs/faq.md b/docs/faq.md index ade59bf0090..24f701862ae 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -65,6 +65,7 @@ A list of frequently-asked questions and answers for MeshCore - [5.14. Q: Are there projects built around MeshCore?](#514-q-are-there-projects-built-around-meshcore) - [5.15. Q: Are there client applications for Windows or Mac?](#515-q-are-there-client-applications-for-windows-or-mac) - [5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?](#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems) + - [5.17. Q: Can the GPS be powered down between fixes to save battery?](#517-q-can-the-gps-be-powered-down-between-fixes-to-save-battery) - [6. Troubleshooting](#6-troubleshooting) - [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago) - [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed) @@ -665,6 +666,20 @@ Both the Windows and Mac versions of the client app are fully unlocked and are f - MeshCore Advantages by MCarper: - MeshCore vs Meshtastic by austinmesh.org: +### 5.17. Q: Can the GPS be powered down between fixes to save battery? +**A:** Yes, on companion firmware for boards whose GPS has a power/enable pin. Set the custom variable `gps_duty` to the number of seconds to wait between fixes (`0` = always on, the default; max `86400`). While GPS is on, the companion powers the GPS up, waits for a fix, keeps it for 10 seconds (so the clock can sync), stores the position, powers the GPS down, and waits `gps_duty` seconds before the next fix. GPS stays "on" in the app the whole time, and adverts and telemetry use the last stored position. + +On devices with a screen, long-pressing on the GPS page steps through **off → on → every 1m → 5m → 15m → 1h → off**. The page shows the interval (`gps on 5m`), and `gps sleep 5m` while the GPS is powered down. + +``` +meshcli -s /dev/ttyUSB0 set gps_duty 300 +``` + +Notes: +- If the GPS has no backup battery, every wake is a cold start (often 30-60 s outdoors). The GPS stays powered until it gets a fix, so with no sky view it stays on. +- On boards where the firmware can't switch the GPS off (no enable pin or switched power), `gps_duty` saves no power. It only limits how often the position is updated. +- Build flags: `GPS_DUTY_SETTLE_SECS` (default 10) and `GPS_DUTY_MAX_ON_SECS` (give up after N seconds without a fix, default 0 = wait for a fix). + --- diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6112b2dbb17..b548481e6c1 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -993,6 +993,7 @@ void MyMesh::begin() { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours + _prefs.gps_duty = constrain(_prefs.gps_duty, 0, 86400); _active_ble_pin = _prefs.ble_pin; @@ -1883,6 +1884,9 @@ void MyMesh::handleCmdFrame(size_t len) { uint32_t interval_seconds = atoi(np); _prefs.gps_interval = constrain(interval_seconds, 0, 86400); savePrefs(); + } else if (strcmp(sp, "gps_duty") == 0) { + _prefs.gps_duty = atol(np); // range checked by sensors + savePrefs(); } #endif writeOKFrame(); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 944790df922..5c6b12e9f74 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -209,6 +209,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { sprintf(interval_str, "%u", _prefs.gps_interval); sensors.setSettingValue("gps_interval", interval_str); } +#if ENV_GPS_DUTY_CYCLE + sensors.configureGPSDuty(_prefs.gps_duty); +#endif } #endif diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 3cceed56668..a85bf507975 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -35,6 +35,7 @@ class NodePrefs : public ConfigSerializer { // persisted to file uint8_t vibe_quiet = 0; uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled) uint32_t gps_interval = 0; // GPS read interval in seconds + uint32_t gps_duty = 0; // GPS power cycle in seconds (0 = always on) uint8_t autoadd_config = 0; // bitmask for auto-add contacts config uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted) uint8_t radio_fem_rxgain = 0; // external LoRa FEM RX gain (LNA) @@ -130,6 +131,7 @@ class NodePrefs : public ConfigSerializer { // persisted to file def("en", _parent->gps_enabled); // boolean def("int", _parent->gps_interval); // interval in seconds def("adv_loc", _parent->advert_loc_policy); + def("duty", _parent->gps_duty); } public: GPSPrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index aced739dc45..e0e4b7dbb09 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -33,6 +33,13 @@ #include "icons.h" +// GPS duty in its largest exact unit ("30s", "5m", "1h"), shared by the GPS page and its alerts +static void formatGPSDuty(char* dest, uint32_t secs) { + if (secs % 3600 == 0) sprintf(dest, "%luh", (unsigned long)(secs / 3600)); + else if (secs % 60 == 0) sprintf(dest, "%lum", (unsigned long)(secs / 60)); + else sprintf(dest, "%lus", (unsigned long)secs); +} + class SplashScreen : public UIScreen { UITask* _task; unsigned long dismiss_after; @@ -404,6 +411,12 @@ class HomeScreen : public UIScreen { #else strcpy(buf, gps_state ? "gps on" : "gps off"); #endif + uint32_t duty = _task->getGPSDuty(); + if (gps_state && duty > 0) { + if (nmea != NULL && !nmea->isEnabled()) strcpy(buf, "gps sleep"); // powered down between fixes + strcat(buf, " "); + formatGPSDuty(strchr(buf, 0), duty); + } display.setColor(UIColor::primary_txt); display.drawTextLeftAlign(0, y, buf); if (nmea == NULL) { @@ -599,7 +612,7 @@ class HomeScreen : public UIScreen { } #if ENV_INCLUDE_GPS == 1 if (c == KEY_ENTER && _page == HomePage::GPS) { - _task->toggleGPS(); + _task->cycleGPSMode(); return true; } #endif @@ -1159,6 +1172,58 @@ bool UITask::getGPSState() { return false; } +uint32_t UITask::getGPSDuty() { + const char* v = _sensors != NULL ? _sensors->getSettingByKey("gps_duty") : NULL; + return v ? strtoul(v, NULL, 10) : 0; +} + +// Steps through off -> always on -> power-cycled every 1m / 5m / 15m / 60m -> off +void UITask::cycleGPSMode() { + static const uint32_t presets[] = { 0, 60, 300, 900, 3600 }; // duty secs while on, 0 = always on + const int n = sizeof(presets) / sizeof(presets[0]); + if (_sensors == NULL || _sensors->getSettingByKey("gps") == NULL) return; + + bool on = getGPSState(); + if (_sensors->getSettingByKey("gps_duty") == NULL) { // no duty cycling on this board + toggleGPS(); + return; + } + uint32_t duty = getGPSDuty(); + int next; // index into presets, or n = off + if (!on) { + next = 0; + } else { + int cur = 0; // a custom duty from the app continues as if "always on" + for (int i = 0; i < n; i++) if (presets[i] == duty) cur = i; + next = cur + 1; + } + + char tmp[12]; + if (next >= n) { + _sensors->setSettingValue("gps", "0"); + _node_prefs->gps_enabled = 0; + showAlert("GPS: Disabled", 800); + } else { + sprintf(tmp, "%lu", (unsigned long)presets[next]); + _sensors->setSettingValue("gps_duty", tmp); + _node_prefs->gps_duty = presets[next]; + if (!on) { // only start when off; start_gps() is not idempotent + _sensors->setSettingValue("gps", "1"); + _node_prefs->gps_enabled = 1; + } + if (presets[next] == 0) { + showAlert("GPS: Always on", 800); + } else { + strcpy(tmp, "GPS: "); + formatGPSDuty(&tmp[5], presets[next]); // presets fit tmp ("GPS: 15m") + showAlert(tmp, 800); + } + } + notify(UIEventType::ack); + the_mesh.savePrefs(); + _next_refresh = 0; +} + void UITask::toggleGPS() { if (_sensors != NULL) { // toggle GPS on/off diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 4d44bfe1a6e..976d3eafad0 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -114,6 +114,8 @@ class UITask : public AbstractUITask { void toggleBuzzer(); bool getGPSState(); void toggleGPS(); + void cycleGPSMode(); + uint32_t getGPSDuty(); // MyMesh::Listener void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 0dc892c3a52..220abe1343b 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -704,6 +704,9 @@ int EnvironmentSensorManager::getNumSettings() const { #if ENV_INCLUDE_GPS if (gps_detected) settings++; // only show GPS setting if GPS is detected #endif + #if ENV_GPS_DUTY_CYCLE + if (gps_detected && gps_duty_enabled) settings++; // gps_duty + #endif return settings; } @@ -714,6 +717,11 @@ const char* EnvironmentSensorManager::getSettingName(int i) const { return "gps"; } #endif + #if ENV_GPS_DUTY_CYCLE + if (gps_detected && gps_duty_enabled && i == settings++) { + return "gps_duty"; + } + #endif return NULL; } @@ -724,6 +732,12 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const { return gps_active ? "1" : "0"; } #endif + #if ENV_GPS_DUTY_CYCLE + if (gps_detected && gps_duty_enabled && i == settings++) { + snprintf(gps_duty_val, sizeof(gps_duty_val), "%lu", (unsigned long)gps_duty_secs); + return gps_duty_val; + } + #endif return NULL; } @@ -743,6 +757,15 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val return true; } #endif + #if ENV_GPS_DUTY_CYCLE + if (gps_detected && gps_duty_enabled && strcmp(name, "gps_duty") == 0) { + long secs = atol(value); + if (secs < 0 || secs > 86400) return false; + gps_duty_secs = secs; + if (gps_duty_secs == 0 && gps_active && !gps_powered) power_on_gps(); // back to always-on + return true; + } + #endif return false; // not supported } @@ -783,6 +806,8 @@ void EnvironmentSensorManager::initBasicGPS() { MESH_DEBUG_PRINTLN("GPS detected"); #ifdef PERSISTANT_GPS gps_active = true; + gps_powered = true; // left on by begin() above + gps_powered_at = millis(); return; #endif } else { @@ -897,14 +922,24 @@ void EnvironmentSensorManager::start_gps() { return; #endif - _location->begin(); - _location->reset(); + power_on_gps(); #ifndef PIN_GPS_EN MESH_DEBUG_PRINTLN("Start GPS is N/A on this board. Actual GPS state unchanged"); #endif } +void EnvironmentSensorManager::power_on_gps() { + _location->begin(); + _location->reset(); + if (gps_duty_secs > 0) { + _location->syncTime(); // also clears the parser, so duty cycling waits for a fresh fix + } + gps_powered = true; + gps_powered_at = millis(); + gps_fix_since = 0; +} + void EnvironmentSensorManager::stop_gps() { gps_active = false; #ifdef RAK_WISBLOCK_GPS @@ -916,6 +951,7 @@ void EnvironmentSensorManager::stop_gps() { #endif _location->stop(); + gps_powered = false; #ifndef PIN_GPS_EN MESH_DEBUG_PRINTLN("Stop GPS is N/A on this board. Actual GPS state unchanged"); @@ -923,13 +959,63 @@ void EnvironmentSensorManager::stop_gps() { } #endif // ENV_INCLUDE_GPS +#if ENV_GPS_DUTY_CYCLE +#ifndef GPS_DUTY_SETTLE_SECS + #define GPS_DUTY_SETTLE_SECS 10 // keep the fix this long before powering down (lets the RTC sync) +#endif +#ifndef GPS_DUTY_MAX_ON_SECS + #define GPS_DUTY_MAX_ON_SECS 0 // give up after this long without a fix, 0 = wait for a fix +#endif + +void EnvironmentSensorManager::dutyCycleGPS() { + if (!gps_powered) { + if ((long)(millis() - gps_next_wake) >= 0) { + MESH_DEBUG_PRINTLN("GPS duty cycle: waking"); + power_on_gps(); + } + return; + } + + bool give_up = GPS_DUTY_MAX_ON_SECS > 0 && (millis() - gps_powered_at) >= GPS_DUTY_MAX_ON_SECS * 1000UL; + if (_location->isValid()) { + if (gps_fix_since == 0) gps_fix_since = millis(); + if ((millis() - gps_fix_since) < GPS_DUTY_SETTLE_SECS * 1000UL && !give_up) return; + node_lat = ((double)_location->getLatitude())/1000000.; + node_lon = ((double)_location->getLongitude())/1000000.; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("GPS duty cycle: fix lat %f lon %f, sleeping", node_lat, node_lon); + } else { + gps_fix_since = 0; + if (!give_up) return; + MESH_DEBUG_PRINTLN("GPS duty cycle: no fix, sleeping"); + } + _location->stop(); + gps_powered = false; + gps_next_wake = millis() + gps_duty_secs * 1000UL; +} + +void EnvironmentSensorManager::configureGPSDuty(uint32_t secs) { + gps_duty_enabled = true; + char buf[12]; + snprintf(buf, sizeof(buf), "%lu", (unsigned long)secs); + setSettingValue("gps_duty", buf); +} +#endif + #if ENV_INCLUDE_GPS || defined(ENV_INCLUDE_BME680_BSEC) void EnvironmentSensorManager::loop() { #if ENV_INCLUDE_GPS static unsigned long next_gps_update = 0; if (gps_active) { + #if ENV_GPS_DUTY_CYCLE + // While powered down the parser still holds the last fix. Running it would let the provider's + // periodic RTC sync set the clock back to that fix's time. + if (gps_duty_secs == 0 || gps_powered) _location->loop(); + if (gps_duty_secs > 0) dutyCycleGPS(); + #else _location->loop(); + #endif } if ((long)(millis() - next_gps_update) > 0) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 17706cbc492..ac94846a901 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -4,6 +4,11 @@ #include #include +// Not for RAK WisBlock GPS (same condition as RAK_WISBLOCK_GPS in the .cpp), which has its own power handling +#if ENV_INCLUDE_GPS && !(defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)) + #define ENV_GPS_DUTY_CYCLE 1 +#endif + class EnvironmentSensorManager : public SensorManager { protected: static const int MAX_ACTIVE_SENSORS = 16; @@ -25,6 +30,18 @@ class EnvironmentSensorManager : public SensorManager { #if ENV_INCLUDE_GPS LocationProvider* _location; + // Duty cycling: with gps_duty_secs > 0, power the GPS down after each fix and wake it + // gps_duty_secs later. gps_active (the user setting) stays true while powered down. + // Only exposed as a setting once the app calls configureGPSDuty(), since it must persist it. + uint32_t gps_duty_secs = 0; + bool gps_duty_enabled = false; + mutable char gps_duty_val[12]; + bool gps_powered = false; + unsigned long gps_next_wake = 0; + unsigned long gps_powered_at = 0; + unsigned long gps_fix_since = 0; + void power_on_gps(); + void dutyCycleGPS(); void start_gps(); void stop_gps(); void initBasicGPS(); @@ -39,6 +56,10 @@ class EnvironmentSensorManager : public SensorManager { EnvironmentSensorManager(LocationProvider &location): _location(&location){}; LocationProvider* getLocationProvider() { return _location; } bool isGPSDetected() const override { return gps_detected; } + #if ENV_GPS_DUTY_CYCLE + // Call after begin(). Lists gps_duty and applies the saved value (invalid = always on). + void configureGPSDuty(uint32_t secs); + #endif #else EnvironmentSensorManager(){}; #endif