diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index f31dd5e55..db8a38fe2 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -47,7 +47,7 @@ #define FIRMWARE_ROLE "sensor" -#define MAX_SEARCH_RESULTS 8 +#define MAX_SEARCH_RESULTS 24 #define MAX_CONCURRENT_ALERTS 4 class SensorMesh : public mesh::Mesh, public CommonCLICallbacks, diff --git a/platformio.ini b/platformio.ini index 6f6ceca47..8ea76d472 100644 --- a/platformio.ini +++ b/platformio.ini @@ -426,6 +426,8 @@ lib_deps = [env:native] platform = native test_framework = googletest +; Exercise the real Arduino GPS parser against the native Stream mock. +lib_compat_mode = off build_flags = -std=c++17 -I src -I test/mocks @@ -463,6 +465,7 @@ build_src_filter = lib_deps = google/googletest @ 1.17.0 bblanchon/ArduinoJson @ 7.4.3 + stevemarple/MicroNMEA @ 2.0.6 [env:native_kiss_modem] platform = native diff --git a/src/Utils.h b/src/Utils.h index 06368e824..66734f29a 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -50,7 +50,7 @@ public: /** * \brief checks the MAC (in leading bytes of 'src'), then if valid, decrypts remaining bytes in src. - * \returns zero if MAC is invalid, otherwise the length of decrypted bytes in 'dest' + * \returns zero if MAC is invalid or ciphertext is not block-aligned, otherwise the length of decrypted bytes in 'dest' */ static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 9cd6d48ad..05faffea7 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -16,7 +16,7 @@ static_assert(MAX_CORRELATED_CLI_TEXT_LEN <= MAX_TEXT_LEN, #include "ContactInfo.h" -#define MAX_SEARCH_RESULTS 8 +#define MAX_SEARCH_RESULTS 24 #define MSG_SEND_FAILED 0 #define MSG_SEND_SENT_FLOOD 1 diff --git a/src/helpers/sensors/AirohaSleep.h b/src/helpers/sensors/AirohaSleep.h new file mode 100644 index 000000000..eade11ce8 --- /dev/null +++ b/src/helpers/sensors/AirohaSleep.h @@ -0,0 +1,24 @@ +#pragma once +#include +#ifdef GPS_NMEA_DEBUG +#include +#endif + +// Board must keep the GPS RTC backup pin powered and can cut GPS power once we return. +// Returns true if the command was acked within 3 attempts, otherwise returns false. +static inline bool airohaEnterSleep(LocationProvider* nmea) { + for (uint8_t attempt = 0; attempt < 3; attempt++) { + nmea->drain(); + nmea->sendSentence("$PAIR650,0"); + if (nmea->waitFor("$PAIR001,650,0", 50)) { // wait for the command received signal + #ifdef GPS_NMEA_DEBUG + if (mesh::isUsbLoggingEnabled()) { + mesh::usbLoggingPort().printf("Airoha RTC Backup sleep command accepted by GPS after %u attempts\r\n", attempt + 1); + } + #endif + nmea->waitFor("$PAIR650,0", 50); // give the GPS 50ms grace to signal it is ready for sleep + return true; + } + } + return false; +} diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index b79a7c18d..c4a8dca6d 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -73,6 +73,8 @@ public: virtual bool isValid() = 0; virtual long getTimestamp() = 0; virtual void sendSentence(const char * sentence); + virtual bool waitFor(const char* prefix, uint32_t timeout_ms) { return false; } + virtual void drain() { } virtual void reset() = 0; virtual void begin() = 0; virtual void stop() = 0; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index 239670047..6374f5f6e 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -198,10 +198,34 @@ public : return timestamp <= 0x7FFFFFFFUL ? static_cast(timestamp) : 0; } + void drain() override { while (_gps_serial->available()) _gps_serial->read(); } + + const char* getSentence() const { return nmea.getSentence(); } + void sendSentence(const char *sentence) override { nmea.sendSentence(*_gps_serial, sentence); } + bool waitFor(const char* prefix, uint32_t timeout_ms) override { + size_t plen = strlen(prefix); + uint32_t timeout = millis() + timeout_ms; + while ((int32_t)(millis() - timeout) < 0) { + if (_gps_serial->available()) { + char c = _gps_serial->read(); + #ifdef GPS_NMEA_DEBUG + if (mesh::isUsbLoggingEnabled()) mesh::usbLoggingPort().print(c); + #endif + // MicroNMEA also returns true for a complete sentence with a + // bad checksum. A damaged ACK must not confirm GPS sleep. + if (nmea.process(c) && MicroNMEA::testChecksum(nmea.getSentence()) + && strncmp(nmea.getSentence(), prefix, plen) == 0) return true; + } else { + yield(); + } + } + return false; + } + void loop() override { size_t processed = 0; diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h index 9c9ddcc3c..fd10d8c97 100644 --- a/test/mocks/Arduino.h +++ b/test/mocks/Arduino.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/test/test_airoha_sleep/RTClib.h b/test/test_airoha_sleep/RTClib.h new file mode 100644 index 000000000..d8f786d27 --- /dev/null +++ b/test/test_airoha_sleep/RTClib.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +// Command/ACK tests do not use RTC conversion. Keep the provider header +// compilable without the hardware I2C dependencies of the Arduino RTC library. +class DateTime { +public: + DateTime(uint16_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t) {} + uint32_t unixtime() const { return 0; } +}; diff --git a/test/test_airoha_sleep/test_airoha_sleep.cpp b/test/test_airoha_sleep/test_airoha_sleep.cpp new file mode 100644 index 000000000..d05d40012 --- /dev/null +++ b/test/test_airoha_sleep/test_airoha_sleep.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include + +void yield() { ++g_mock_millis; } + +#include +#include + +// The production provider overrides this default virtual command sender. +void LocationProvider::sendSentence(const char*) {} + +class GpsStream : public Stream { +public: + std::deque incoming; + std::vector commands; + std::string pending_command; + unsigned reply_on_attempt = 0; + bool send_ready = true; + + void receive(const char* text) { + while (*text) incoming.push_back(static_cast(*text++)); + } + int available() override { return static_cast(incoming.size()); } + int read() override { + if (incoming.empty()) return -1; + uint8_t c = incoming.front(); + incoming.pop_front(); + return c; + } + size_t write(uint8_t c) override { + pending_command += static_cast(c); + if (c == '\n') { + commands.push_back(pending_command); + pending_command.clear(); + if (reply_on_attempt != 0 && commands.size() == reply_on_attempt) { + receive("$PAIR001,650,0*38\r\n"); + if (send_ready) receive("$PAIR650,0*25\r\n"); + } + } + return 1; + } +}; + +TEST(AirohaSleep, RejectsCorruptedAcknowledgment) { + resetArduinoMock(); + GpsStream uart; + MicroNMEALocationProvider gps(uart, nullptr, -1, -1); + uart.receive("$PAIR001,650,0*00\r\n"); + EXPECT_FALSE(gps.waitFor("$PAIR001,650,0", 50)); + EXPECT_EQ(50U, millis()); +} + +TEST(AirohaSleep, ValidAcknowledgmentAfterCorruptedSentenceIsAccepted) { + resetArduinoMock(); + GpsStream uart; + MicroNMEALocationProvider gps(uart, nullptr, -1, -1); + uart.receive("$PAIR001,650,0*00\r\n$PAIR001,650,0*38\r\n"); + EXPECT_TRUE(gps.waitFor("$PAIR001,650,0", 50)); +} + +TEST(AirohaSleep, RetriesAndDrainsStaleAcknowledgments) { + resetArduinoMock(); + GpsStream uart; + uart.reply_on_attempt = 3; + uart.receive("$PAIR001,650,0*38\r\n"); // Must not acknowledge a new command. + MicroNMEALocationProvider gps(uart, nullptr, -1, -1); + ASSERT_TRUE(airohaEnterSleep(&gps)); + ASSERT_EQ(3U, uart.commands.size()); + for (const auto& command : uart.commands) EXPECT_EQ("$PAIR650,0*25\r\n", command); + EXPECT_EQ(100U, millis()); +} + +TEST(AirohaSleep, MissingReceiverIsBoundedToThreeAttempts) { + resetArduinoMock(); + GpsStream uart; + uart.receive("$PAIR001,650,0*38\r\n"); + MicroNMEALocationProvider gps(uart, nullptr, -1, -1); + EXPECT_FALSE(airohaEnterSleep(&gps)); + EXPECT_EQ(3U, uart.commands.size()); + EXPECT_EQ(150U, millis()); +} + +TEST(AirohaSleep, AcceptedCommandAllowsBoundedReadyGrace) { + resetArduinoMock(); + GpsStream uart; + uart.reply_on_attempt = 1; + uart.send_ready = false; + MicroNMEALocationProvider gps(uart, nullptr, -1, -1); + EXPECT_TRUE(airohaEnterSleep(&gps)); + EXPECT_EQ(1U, uart.commands.size()); + EXPECT_EQ(50U, millis()); +} + +TEST(AirohaSleep, TimeoutWorksAcrossMillisRollover) { + resetArduinoMock(); + g_mock_millis = UINT32_MAX - 10; + GpsStream uart; + MicroNMEALocationProvider gps(uart, nullptr, -1, -1); + EXPECT_FALSE(gps.waitFor("$PAIR001,650,0", 50)); + EXPECT_EQ(39U, millis()); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index 5f3281baa..b6b9678e9 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -94,6 +94,32 @@ lib_deps = ${LilyGo_T-Echo-Lite.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:LilyGo_T-Echo-Lite_kiss_modem] +extends = LilyGo_T-Echo-Lite +build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} + +<../examples/kiss_modem/> +[env:LilyGo_T-Echo-Lite_companion_radio_usb] +extends = LilyGo_T-Echo-Lite +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${LilyGo_T-Echo-Lite.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D UI_RECENT_LIST_SIZE=9 + -D UI_SENSORS_PAGE=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 +; -D QSPIFLASH=1 +build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T-Echo-Lite.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:LilyGo_T-Echo-Lite_non_shell_companion_radio_ble] extends = LilyGo_T-Echo-Lite upload_protocol = nrfutil @@ -183,8 +209,3 @@ build_src_filter = ${rak4631_hw.build_src_filter} lib_deps = ${LilyGo_T-Echo-Lite.lib_deps} densaugeo/base64 @ ~1.4.0 - -[env:LilyGo_T-Echo-Lite_kiss_modem] -extends = LilyGo_T-Echo-Lite -build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} - +<../examples/kiss_modem/> diff --git a/variants/meshtracker_x1/target.cpp b/variants/meshtracker_x1/target.cpp index 9b3c654e4..d2a14d690 100644 --- a/variants/meshtracker_x1/target.cpp +++ b/variants/meshtracker_x1/target.cpp @@ -1,6 +1,7 @@ #include #include "target.h" #include +#include MeshTrackerX1Board board; @@ -35,28 +36,20 @@ void MeshTrackerX1SensorManager::armGpsPowerSavingCycle() { void MeshTrackerX1SensorManager::start_gps() { if (gps_active) return; gps_active = true; - // this init sequence comes from seeed examples and deals with all gps pins - pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, HIGH); delay(10); - pinMode(GPS_VRTC_EN, OUTPUT); - digitalWrite(GPS_VRTC_EN, HIGH); - delay(10); - - pinMode(GPS_RESET, OUTPUT); - digitalWrite(GPS_RESET, HIGH); - delay(10); - digitalWrite(GPS_RESET, LOW); - - pinMode(GPS_SLEEP_INT, OUTPUT); - digitalWrite(GPS_SLEEP_INT, HIGH); - pinMode(GPS_RTC_INT, OUTPUT); + // Resume from RTC backup without resetting retained ephemeris data. + digitalWrite(GPS_RTC_INT, HIGH); + delay(5); digitalWrite(GPS_RTC_INT, LOW); - _nmea->begin(); armGpsPowerSavingCycle(); } void MeshTrackerX1SensorManager::sleep_gps() { + stop_gps(); +} + +void MeshTrackerX1SensorManager::stop_gps() { if (!gps_active) return; gps_active = false; if (powersaving_enabled && _nmea->getGPSPowerSaving()) { @@ -64,22 +57,10 @@ void MeshTrackerX1SensorManager::sleep_gps() { _nmea->setNextGPSOff(0); _nmea->setNextWake(); } - _nmea->stop(); - digitalWrite(GPS_VRTC_EN, HIGH); // keep RTC alive for faster fix on wake - digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, LOW); - digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_VRTC_EN, HIGH); // keep RTC powered for faster fix on wake digitalWrite(GPS_RTC_INT, LOW); -} - -void MeshTrackerX1SensorManager::stop_gps() { - gps_active = false; - _nmea->stop(); - digitalWrite(GPS_VRTC_EN, LOW); + airohaEnterSleep(_nmea); digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, LOW); - digitalWrite(GPS_SLEEP_INT, HIGH); - digitalWrite(GPS_RTC_INT, LOW); } bool MeshTrackerX1SensorManager::begin() { @@ -90,6 +71,14 @@ bool MeshTrackerX1SensorManager::begin() { pinMode(GPS_RESET, OUTPUT); pinMode(GPS_SLEEP_INT, OUTPUT); pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); + digitalWrite(GPS_EN, HIGH); + digitalWrite(GPS_RTC_INT, LOW); + digitalWrite(GPS_RESET, HIGH); + delay(10); + digitalWrite(GPS_RESET, LOW); + // Preserve the existing default-off behavior after initial setup. + gps_active = true; stop_gps(); // init SPA06-003 barometer diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index bc61eaf76..9001a4ffd 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -4,6 +4,7 @@ #include #include #include +#include T1000eBoard board; @@ -103,46 +104,17 @@ void T1000SensorManager::armGpsPowerSavingCycle() { void T1000SensorManager::start_gps() { if (gps_active) return; gps_active = true; - //_nmea->begin(); - // this init sequence should be better - // comes from seeed examples and deals with all gps pins - pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, HIGH); delay(10); - pinMode(GPS_VRTC_EN, OUTPUT); - digitalWrite(GPS_VRTC_EN, HIGH); - delay(10); - - pinMode(GPS_RESET, OUTPUT); - digitalWrite(GPS_RESET, HIGH); - delay(10); - digitalWrite(GPS_RESET, LOW); - - pinMode(GPS_SLEEP_INT, OUTPUT); - digitalWrite(GPS_SLEEP_INT, HIGH); - pinMode(GPS_RTC_INT, OUTPUT); + // Resume from RTC backup without resetting retained ephemeris data. + digitalWrite(GPS_RTC_INT, HIGH); + delay(5); digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, INPUT_PULLUP); armGpsPowerSavingCycle(); } void T1000SensorManager::sleep_gps() { - if (!gps_active) return; - gps_active = false; - if (powersaving_enabled && _nmea->getGPSPowerSaving()) { - _nmea->stopTimeSync(); - _nmea->setNextGPSOff(0); - _nmea->setNextWake(); - } - - digitalWrite(GPS_VRTC_EN, HIGH); - digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, HIGH); - digitalWrite(GPS_SLEEP_INT, HIGH); - digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, OUTPUT); - digitalWrite(GPS_RESETB, LOW); - //_nmea->stop(); + stop_gps(); } void T1000SensorManager::stop_gps() { @@ -154,14 +126,10 @@ void T1000SensorManager::stop_gps() { _nmea->setNextWake(); } - digitalWrite(GPS_VRTC_EN, LOW); - digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, HIGH); - digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_VRTC_EN, HIGH); // keep RTC powered for faster fix on wake digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, OUTPUT); - digitalWrite(GPS_RESETB, LOW); - //_nmea->stop(); + airohaEnterSleep(_nmea); // save ephemeris before cutting GPS VCC + digitalWrite(GPS_EN, LOW); } @@ -173,7 +141,15 @@ bool T1000SensorManager::begin() { pinMode(GPS_RESET, OUTPUT); pinMode(GPS_SLEEP_INT, OUTPUT); pinMode(GPS_RTC_INT, OUTPUT); - pinMode(GPS_RESETB, OUTPUT); + pinMode(GPS_RESETB, INPUT_PULLUP); + digitalWrite(GPS_VRTC_EN, HIGH); + digitalWrite(GPS_EN, HIGH); + digitalWrite(GPS_SLEEP_INT, LOW); + digitalWrite(GPS_RTC_INT, LOW); + digitalWrite(GPS_RESET, HIGH); + delay(10); + digitalWrite(GPS_RESET, LOW); + // Preserve the existing default-off behavior after initial setup. gps_active = true; stop_gps(); return true; @@ -217,7 +193,6 @@ void T1000SensorManager::loop() { node_lon = ((double)_nmea->getLongitude())/1000000.; node_altitude = ((double)_nmea->getAltitude()) / 1000.0; processGpsTelemetryFix(node_lat, node_lon, node_altitude, now); - //Serial.printf("lat %f lon %f\r\n", _lat, _lon); } next_gps_update = now + getGpsUpdateIntervalMillis(); } diff --git a/variants/t1000-e/variant.cpp b/variants/t1000-e/variant.cpp index ed21fd68c..fc30a6197 100644 --- a/variants/t1000-e/variant.cpp +++ b/variants/t1000-e/variant.cpp @@ -71,7 +71,7 @@ void initVariant() pinMode(LUX_SENSOR, INPUT); pinMode(EXT_CHRG_DETECT, INPUT); pinMode(EXT_PWR_DETECT, INPUT); - pinMode(GPS_RESETB, INPUT); + pinMode(GPS_RESETB, INPUT_PULLUP); pinMode(PIN_BUTTON1, INPUT); pinMode(PIN_3V3_EN, OUTPUT); @@ -89,10 +89,10 @@ void initVariant() digitalWrite(PIN_3V3_ACC_EN, LOW); digitalWrite(BUZZER_EN, LOW); digitalWrite(SENSOR_EN, LOW); - digitalWrite(GPS_EN, LOW); + digitalWrite(GPS_EN, HIGH); digitalWrite(GPS_RESET, LOW); - digitalWrite(GPS_VRTC_EN, LOW); - digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_VRTC_EN, HIGH); + digitalWrite(GPS_SLEEP_INT, LOW); digitalWrite(GPS_RTC_INT, LOW); digitalWrite(LED_PIN, LOW); }