diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 45f4b9d64..5957a8a93 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -175,6 +175,9 @@ jobs: - name: Verify ESP32 USB sleep and G3 button wake run: python3 -B test/test_esp32_usb_sleep.py + - name: Verify GPS status distinguishes user settings from receiver sleep + run: python3 -B test/test_gps_status.py + - name: Verify Companion ASCII defaults and USB session cleanup run: | python3 -B test/test_companion_usb_default.py diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 5df4cdf3b..3d60d6669 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -3197,9 +3197,9 @@ void setup() { sensors.begin(); #if ENV_INCLUDE_GPS == 1 - // Device power saving applies a 10-minute awake, 5-minute sleep GPS cycle. + // Device power saving applies a 10-minute awake, 30-minute sleep GPS cycle. if (sensors.getLocationProvider() != NULL) { - sensors.getLocationProvider()->setPowerSavingProfile(600, 300); + sensors.getLocationProvider()->setPowerSavingProfile(600, 1800); sensors.setPowerSavingEnabled( the_mesh.getNodePrefs()->powersaving_enabled != 0); } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 4d9605118..c45ecf124 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -81,6 +81,11 @@ void setup() { external_watchdog.begin(); #endif +#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM) + // Allow the USB console to settle before printing boot diagnostics. + delay(5000); +#endif + #ifdef DISPLAY_CLASS if (display.begin()) { display.turnOff(); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index c7712b680..dcafe3e2a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3057,18 +3057,21 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re const char* gps_setting = _sensors->getSettingByKey("gps"); bool active = gps_setting != NULL && strcmp(gps_setting, "1") == 0; - if (_prefs->powersaving_enabled && l->getGPSPowerSaving()) { // GPS Power Saving + if (!active) { + snprintf(reply, 160, "off, %s", enabled ? "powered" : "unpowered"); + } else if (_prefs->powersaving_enabled && l->getGPSPowerSaving()) { // GPS Power Saving unsigned long now = millis(); unsigned long next_off = l->getNextGPSOff(); unsigned long deadline = next_off != 0 ? next_off : l->getNextGPSOn(); long remaining_ms = deadline == 0 ? 0 : (long)(deadline - now); unsigned long mins = remaining_ms > 0 ? (unsigned long)remaining_ms / 60000UL : 0; if (next_off != 0) { - snprintf(reply, 160, "on (powersaving, sleep in %luh %lum), %s, %s, %d sats", - mins / 60UL, mins % 60UL, active ? "active" : "deactivated", + snprintf(reply, 160, "on, %s (powersaving, sleep in %luh %lum), %s, %d sats", + enabled ? "powered" : "unpowered", mins / 60UL, mins % 60UL, fix ? "fix" : "no fix", sats); } else { - snprintf(reply, 160, "off (powersaving, wake in %luh %lum)", mins / 60UL, mins % 60UL); + snprintf(reply, 160, "on, %s (powersaving, wake in %luh %lum)", + enabled ? "powered" : "unpowered", mins / 60UL, mins % 60UL); } // "last sync" from GPS @@ -3082,14 +3085,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); } } else { // Normal mode - if (enabled) { - sprintf(reply, "on, %s, %s, %d sats", - active?"active":"deactivated", - fix?"fix":"no fix", - sats); - } else { - strcpy(reply, "off"); - } + snprintf(reply, 160, "on, %s, %s, %d sats", + enabled ? "powered" : "unpowered", + fix ? "fix" : "no fix", sats); } } else { strcpy(reply, "Can't find GPS"); diff --git a/test/test_gps_status.py b/test/test_gps_status.py new file mode 100644 index 000000000..0bd733744 --- /dev/null +++ b/test/test_gps_status.py @@ -0,0 +1,96 @@ +"""Run the production GPS status branch against user and receiver states.""" +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +from test_replay_reset_integration import extract_braced + +ROOT = Path(__file__).resolve().parents[1] + + +class GpsStatusTest(unittest.TestCase): + def test_requested_state_is_distinct_from_sleeping_receiver(self): + compiler = shutil.which("g++") or shutil.which("clang++") + self.assertIsNotNone(compiler) + source = (ROOT / "src/helpers/CommonCLI.cpp").read_text() + branch = extract_braced(source, 'else if (memcmp(command, "gps", 3) == 0)') + branch = branch[branch.index("{"):].replace("#endif", "") + harness = r''' +#include +#include +#include +unsigned long now = 1000; +unsigned long millis() { return now; } +struct DateTime { + explicit DateTime(unsigned long) {} + int hour() { return 12; } int minute() { return 34; } + int day() { return 30; } int month() { return 9; } int year() { return 2026; } +}; +struct LocationProvider { + bool powered = false, fix = false, saving = false; + unsigned long off = 0, on = 0, sync = 0; + bool isEnabled() { return powered; } bool isValid() { return fix; } + int satellitesCount() { return 5; } + bool getGPSPowerSaving() { return saving; } + unsigned long getNextGPSOff() { return off; } + unsigned long getNextGPSOn() { return on; } + unsigned long getLastValidTimeSync() { return sync; } +}; +struct Sensors { + LocationProvider* provider; + const char* user = "0"; + LocationProvider* getLocationProvider() { return provider; } + const char* getSettingByKey(const char*) { return user; } +}; +struct Prefs { bool powersaving_enabled = true; }; +void status(Sensors* _sensors, Prefs* _prefs, char* reply) @BRANCH@ +int main() { + LocationProvider gps; + Sensors sensors{&gps}; Prefs prefs; + char reply[160]; + // Manual off must not be described as a periodic sleep, even with stale timers. + gps.saving = true; gps.on = now + 1800000; + status(&sensors, &prefs, reply); + assert(!strcmp(reply, "off, unpowered")); + // A shared power rail can keep the receiver powered after GPS is disabled. + gps.powered = true; + status(&sensors, &prefs, reply); + assert(!strcmp(reply, "off, powered")); + sensors.user = "1"; gps.powered = false; + status(&sensors, &prefs, reply); + assert(!strcmp(reply, "on, unpowered (powersaving, wake in 0h 30m), last sync: none")); + // Sleep can leave a shared rail powered; the wake timer is still meaningful. + gps.powered = true; + status(&sensors, &prefs, reply); + assert(!strcmp(reply, "on, powered (powersaving, wake in 0h 30m), last sync: none")); + gps.off = now + 600000; gps.powered = true; gps.fix = true; gps.sync = 1; + status(&sensors, &prefs, reply); + assert(!strcmp(reply, "on, powered (powersaving, sleep in 0h 10m), fix, 5 sats, last sync: 12:34 - 30/9/2026 UTC")); + // An overdue timer must not underflow into a huge remaining duration. + gps.off = now - 1; + status(&sensors, &prefs, reply); + assert(strstr(reply, "sleep in 0h 0m")); + prefs.powersaving_enabled = false; gps.powered = false; + status(&sensors, &prefs, reply); + assert(!strcmp(reply, "on, unpowered, fix, 5 sats")); + // No GPS setting is a valid disabled state; avoid a null strcmp. + sensors.user = nullptr; + status(&sensors, &prefs, reply); + assert(!strcmp(reply, "off, unpowered")); + sensors.provider = nullptr; + status(&sensors, &prefs, reply); + assert(!strcmp(reply, "Can't find GPS")); +} +'''.replace("@BRANCH@", branch) + with tempfile.TemporaryDirectory(prefix="meshcore-gps-status-") as directory: + path = Path(directory) + (path / "status.cpp").write_text(harness) + subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", + str(path / "status.cpp"), "-o", str(path / "status")], check=True) + subprocess.run([str(path / "status")], check=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 8f357199c..5b94964ea 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -144,6 +144,7 @@ build_flags = ${t1000-e.build_flags} -D MAX_NEIGHBOURS=50 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; -D POWERSAVING_DEBUG=1 build_src_filter = ${t1000-e.build_src_filter} +<../examples/simple_sensor> lib_deps = ${t1000-e.lib_deps}