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
15 changes: 15 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -665,6 +666,20 @@ Both the Windows and Mac versions of the client app are fully unlocked and are f
- MeshCore Advantages by MCarper: <https://github.com/mikecarper/meshfirmware/blob/main/MeshCoreAdvantages.md>
- MeshCore vs Meshtastic by austinmesh.org: <https://www.austinmesh.org/learn/meshcore-vs-meshtastic>

### 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).


---

Expand Down
4 changes: 4 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) { }
Expand Down
67 changes: 66 additions & 1 deletion examples/companion_radio/ui-new/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/companion_radio/ui-new/UITask.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
90 changes: 88 additions & 2 deletions src/helpers/sensors/EnvironmentSensorManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -916,20 +951,71 @@ 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");
#endif
}
#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) {

Expand Down
21 changes: 21 additions & 0 deletions src/helpers/sensors/EnvironmentSensorManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
#include <helpers/SensorManager.h>
#include <helpers/sensors/LocationProvider.h>

// 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;
Expand All @@ -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();
Expand All @@ -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
Expand Down