From b6e6730feb9c3b13d2f9abe52677407e1dc0b713 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 28 May 2026 16:00:10 +0100 Subject: [PATCH 01/11] RAK3401: re-enable GPS, fix stale PVT data, prevent radio rail-kill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related issues hit by RAK3401 + RAK19007 + RAK12500 (I²C ublox). 1. -D RAK_BOARD restored on [rak3401]. It was removed in 68363d9e (revert of #2401) with the symptom "RadioLib error -707", because adding RAK_BOARD enables rakGPSInit(), whose WB_IO4 fallback probe pulses pin 4 (= SX126X_RESET on RAK3401) LOW for 500 ms — hard-resetting the SX1262 after radio_init() has already configured it, and (via stop_gps + gpsResetPin = WB_IO4) leaving it held in reset afterwards. Without RAK_BOARD, RAK_WISBLOCK_GPS isn't auto-defined, the I²C ublox path is unreachable, and the firmware falls back to Serial1-NMEA detection — which silently fails on a serial-less RAK12500. Fixes 2 and 3 below make RAK_BOARD safe to re-enable. 2. RAK12500LocationProvider passed maxWait of 2/8 ms to getLatitude() etc. These are below the ublox I²C response window, so the polls routinely timed out and the SparkFun library returned stale/garbage PVT bytes. Observed: getGnssFixOk() returning true with a current latitude but a longitude from some previous fix. Bumped to 250 ms. Also switched getAltitude() (height above WGS84 ellipsoid) to getAltitudeMSL() for more intuitive altitude readings. 3. rakGPSInit() probes WB_IO2 first. On failure, gpsIsAwake() leaves the probed pin as INPUT. WB_IO2 also controls the 3V3_S switched peripheral rail on these RAK base boards, so a failed first probe left I²C peripherals (RTC, display, the GPS itself) unpowered before the WB_IO4/WB_IO5 fallbacks ran. The else-branch now restores WB_IO2 HIGH before falling through. On RAK3401, those fallback probes (WB_IO4 = pin 4 = SX126X_RESET, WB_IO5 = pin 9 = SX126X_BUSY) are also dangerous, see point 1. Adding -D FORCE_GPS_ALIVE in the RAK3401 base env skips stop_gps() after detection, so even if gpsResetPin ends up = WB_IO2 (kills the rail) or pin 4 (= radio reset), it can't be pulled LOW again after init. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../sensors/EnvironmentSensorManager.cpp | 46 ++++++++++++------- variants/rak3401/platformio.ini | 3 ++ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index b5e23b3fe8..51492304d9 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -168,16 +168,21 @@ class RAK12500LocationProvider : public LocationProvider { void begin() override { } void stop() override { } void loop() override { - if (ublox_GNSS.getGnssFixOk(8)) { + // The numeric arg is maxWait (ms) for the I²C poll, not a field index. + // The previous 2/8 ms values were below the ublox response window, so polls + // routinely timed out and returned stale/garbage PVT data (observed: random + // wrong-hemisphere longitudes despite getGnssFixOk() returning true). + // 250 ms matches the SparkFun library's typical response time. + if (ublox_GNSS.getGnssFixOk(250)) { _fix = true; - _lat = ublox_GNSS.getLatitude(2) / 10; - _lng = ublox_GNSS.getLongitude(2) / 10; - _alt = ublox_GNSS.getAltitude(2); - _sats = ublox_GNSS.getSIV(2); + _lat = ublox_GNSS.getLatitude(250) / 10; + _lng = ublox_GNSS.getLongitude(250) / 10; + _alt = ublox_GNSS.getAltitudeMSL(250); // height above mean sea level, not ellipsoid + _sats = ublox_GNSS.getSIV(250); } else { _fix = false; } - _epoch = ublox_GNSS.getUnixEpoch(2); + _epoch = ublox_GNSS.getUnixEpoch(250); } bool isEnabled() override { return true; } }; @@ -702,16 +707,25 @@ void EnvironmentSensorManager::rakGPSInit(){ //search for the correct IO standby pin depending on socket used if(gpsIsAwake(WB_IO2)){ } - else if(gpsIsAwake(WB_IO4)){ - } - else if(gpsIsAwake(WB_IO5)){ - } - else{ - MESH_DEBUG_PRINTLN("No GPS found"); - gps_active = false; - gps_detected = false; - Serial1.end(); - return; + else { + // WB_IO2 controls the 3V3_S switched peripheral rail on these RAK boards. + // gpsIsAwake() leaves the pin as INPUT on failure, which drops the rail and + // takes I²C peripherals (RTC, display, the GPS itself) down with it. + // Restore it before probing other sockets. + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, HIGH); + + if(gpsIsAwake(WB_IO4)){ + } + else if(gpsIsAwake(WB_IO5)){ + } + else{ + MESH_DEBUG_PRINTLN("No GPS found"); + gps_active = false; + gps_detected = false; + Serial1.end(); + return; + } } #ifndef FORCE_GPS_ALIVE // for use with repeaters, until GPS toggle is implimented diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 20a8a548b9..2040faa4df 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -6,6 +6,7 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 + -D RAK_BOARD -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper @@ -13,6 +14,8 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM + -D FORCE_GPS_ALIVE ; on RAK3401, gpsResetPin ends up = WB_IO2 (kills 3V3_S rail) or + ; pin 4 (= SX126X_RESET, holds radio in reset). stop_gps() must not run. build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak3401> + From c9dfd1bde3ea58e6b8cb166f5e8e85122103b148 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 28 May 2026 16:31:55 +0100 Subject: [PATCH 02/11] GPS: drop duplicate lat/lon debug print, throttle the remaining one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loop() function printed lat/lon, then computed altitude, then printed lat/lon/alt — making the first print pure noise. Every gps_update_interval_sec (default 1 s) the log got two near-identical lines, drowning out anything else when MESH_DEBUG=1 is on. Drop the first MESH_DEBUG_PRINTLN entirely (in both the RAK_WISBLOCK_GPS and the non-RAK branches). Throttle the surviving lat/lon/alt line to roughly every 10th update via a static skip counter, so a debug build gets a position sample every ~10 s instead of every second while still covering the typical GPS visibility window. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/helpers/sensors/EnvironmentSensorManager.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 51492304d9..b9d79712d1 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -815,6 +815,7 @@ void EnvironmentSensorManager::stop_gps() { void EnvironmentSensorManager::loop() { static long next_gps_update = 0; + static uint8_t gps_debug_skip = 0; // throttle: print roughly every 10th update tick #if ENV_INCLUDE_GPS if (gps_active) { @@ -827,17 +828,21 @@ void EnvironmentSensorManager::loop() { if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) { node_lat = ((double)_location->getLatitude())/1000000.; node_lon = ((double)_location->getLongitude())/1000000.; - MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); node_altitude = ((double)_location->getAltitude()) / 1000.0; - MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); + if (gps_debug_skip-- == 0) { + gps_debug_skip = 9; + MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); + } } #else if (_location->isValid()) { node_lat = ((double)_location->getLatitude())/1000000.; node_lon = ((double)_location->getLongitude())/1000000.; - MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); node_altitude = ((double)_location->getAltitude()) / 1000.0; - MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); + if (gps_debug_skip-- == 0) { + gps_debug_skip = 9; + MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); + } } #endif } From f2ad72e619468a9084542ee9797130980884f9a8 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 28 May 2026 20:17:01 +0100 Subject: [PATCH 03/11] GPS: require fixType==3 in addition to gnssFixOK before reporting coords MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ublox modules sometimes report gnssFixOK=true with bogus coordinates during cold start, before any actual fix has been acquired — e.g. factory-almanac defaults that look like real positions. Observed: an unfixed module reporting plausible-looking but wrong-hemisphere coordinates persistently, with _location->isValid() returning true. Tighten RAK12500LocationProvider::loop() to require BOTH gnssFixOK (DOP/accuracy masks satisfied) AND fixType == 3 (3D fix). fixType values: 0=no fix, 1=dead reckoning only, 2=2D, 3=3D, 4=GNSS+DR, 5=time only. Only the 3D fix is a position we want to publish. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/helpers/sensors/EnvironmentSensorManager.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index b9d79712d1..8db2788afd 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -173,7 +173,12 @@ class RAK12500LocationProvider : public LocationProvider { // routinely timed out and returned stale/garbage PVT data (observed: random // wrong-hemisphere longitudes despite getGnssFixOk() returning true). // 250 ms matches the SparkFun library's typical response time. - if (ublox_GNSS.getGnssFixOk(250)) { + // + // Require BOTH gnssFixOK (DOP/accuracy mask passed) AND fixType==3 (3D fix). + // Ublox reports gnssFixOK=true with bogus coords during cold start (e.g. an + // unfixed module on first boot reporting coordinates from factory almanac); + // requiring fixType==3 filters those out. + if (ublox_GNSS.getGnssFixOk(250) && ublox_GNSS.getFixType(250) == 3) { _fix = true; _lat = ublox_GNSS.getLatitude(250) / 10; _lng = ublox_GNSS.getLongitude(250) / 10; From a13098a583f6e7e8154040f0dab2216fb613a417 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Mon, 1 Jun 2026 11:02:42 +0100 Subject: [PATCH 04/11] =?UTF-8?q?GPS:=20throttle=20RAK12500=20I=C2=B2C=20p?= =?UTF-8?q?olling=20to=20~1=20Hz=20so=20it=20can't=20stall=20the=20main=20?= =?UTF-8?q?loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stale-PVT fix bumped every ublox getXxx() maxWait to 250 ms. Each getter does a synchronous I²C poll, and RAK12500LocationProvider::loop() runs on every main-loop iteration — so the loop spent most of its time blocked on redundant GPS polls (the GNSS solution only updates at the 1 Hz nav rate anyway). That stall made the AIN1 button feel sluggish and drop double-clicks ("prev"), which is what the separate interrupt-driven AIN1 button work was chasing — turns out unnecessary once this is fixed. Throttle the poll to once per nav epoch (~1 Hz) inside the provider, and gate all field reads on a single getPVT(250): if that poll times out we return early and keep the last values, so no getter fires its own ~1100 ms default-maxWait poll on a stale cache. The throttle lives in the provider (not the shared outer loop) because the serial/NMEA providers must keep getting loop() every iteration to avoid dropping bytes. Same proven polling logic that was getting fixes, just not hammered every iteration. Co-Authored-By: Claude Opus 4.8 --- .../sensors/EnvironmentSensorManager.cpp | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index d2d79a49b1..f7974bc07d 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -181,6 +181,7 @@ class RAK12500LocationProvider : public LocationProvider { int _sats = 0; long _epoch = 0; bool _fix = false; + unsigned long _next_poll = 0; public: long getLatitude() override { return _lat; } long getLongitude() override { return _lng; } @@ -193,26 +194,45 @@ class RAK12500LocationProvider : public LocationProvider { void begin() override { } void stop() override { } void loop() override { + // Throttle the I²C poll to ~1 Hz. The getXxx() calls below each issue a + // synchronous ublox poll that blocks up to its maxWait; the GNSS solution only + // updates at the 1 Hz navigation rate (setMeasurementRate(1000)), so polling on + // every main-loop iteration just burns the loop on redundant I²C round-trips — + // that stall is what made the AIN1 button sluggish and drop double-clicks. + // Polling once per nav epoch keeps the fix/time fresh while leaving the loop + // free for button sampling the rest of the time. + if (millis() < _next_poll) { + return; + } + _next_poll = millis() + 1000; + // The numeric arg is maxWait (ms) for the I²C poll, not a field index. // The previous 2/8 ms values were below the ublox response window, so polls // routinely timed out and returned stale/garbage PVT data (observed: random // wrong-hemisphere longitudes despite getGnssFixOk() returning true). - // 250 ms matches the SparkFun library's typical response time. - // + // 250 ms is a ceiling; a responsive module returns well before that. getPVT() + // does the single poll here, then every getter reads that same cached packet + // (passing no maxWait, so they never re-poll). Gating all reads on getPVT() + // succeeding matters: if the poll times out, a getter with a stale cache would + // otherwise fire its own poll at the library default (~1100 ms) and stall hard. + if (!ublox_GNSS.getPVT(250)) { + return; // no response this tick — keep last reported values + } + // Require BOTH gnssFixOK (DOP/accuracy mask passed) AND fixType==3 (3D fix). // Ublox reports gnssFixOK=true with bogus coords during cold start (e.g. an // unfixed module on first boot reporting coordinates from factory almanac); // requiring fixType==3 filters those out. - if (ublox_GNSS.getGnssFixOk(250) && ublox_GNSS.getFixType(250) == 3) { + if (ublox_GNSS.getGnssFixOk() && ublox_GNSS.getFixType() == 3) { _fix = true; - _lat = ublox_GNSS.getLatitude(250) / 10; - _lng = ublox_GNSS.getLongitude(250) / 10; - _alt = ublox_GNSS.getAltitudeMSL(250); // height above mean sea level, not ellipsoid - _sats = ublox_GNSS.getSIV(250); + _lat = ublox_GNSS.getLatitude() / 10; + _lng = ublox_GNSS.getLongitude() / 10; + _alt = ublox_GNSS.getAltitudeMSL(); // height above mean sea level, not ellipsoid + _sats = ublox_GNSS.getSIV(); } else { _fix = false; } - _epoch = ublox_GNSS.getUnixEpoch(250); + _epoch = ublox_GNSS.getUnixEpoch(); } bool isEnabled() override { return true; } }; From 9e8cbf6bca7864f393de8e4fc1b94b429e58ada4 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 11 Jun 2026 18:27:40 +0100 Subject: [PATCH 05/11] Enable RAK12501 Quectel GPS on RAK19007 Slot A --- variants/rak3401/variant.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 9882788608..a2e32ae29a 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -188,8 +188,8 @@ static const uint8_t AREF = PIN_AREF; // Power is on the controllable 3V3_S rail #define PIN_GPS_PPS (17) // Pulse per second input from the GPS -#define PIN_GPS_RX PIN_SERIAL1_RX -#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_TX PIN_SERIAL1_RX #define PIN_GPS_1PPS PIN_GPS_PPS #define GPS_BAUD_RATE 9600 From 14f9d08a254720289904bfaf96eb6cf6a8ac4da4 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 11 Jun 2026 19:30:11 +0100 Subject: [PATCH 06/11] treat PIN_GPS_EN=-1 as no GPS enable pin, define it on rak3401 gpsResetPin now initializes to -1 so start_gps/stop_gps pin writes are out-of-range no-ops when no enable pin exists, instead of driving pin 0 (P0.00 = LFXO crystal on nRF52). Co-Authored-By: Claude Fable 5 --- src/helpers/sensors/EnvironmentSensorManager.cpp | 6 ++++-- variants/rak3401/platformio.ini | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index f7974bc07d..ad9a39fd9d 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -165,7 +165,9 @@ static RAK12035_SoilMoisture RAK12035; #endif #ifdef RAK_WISBLOCK_GPS -static uint32_t gpsResetPin = 0; +// -1 = no enable pin; out-of-range values are no-ops in pinMode/digitalWrite, +// while 0 would be a real GPIO (P0.00 = LFXO crystal on nRF52) +static uint32_t gpsResetPin = -1; static bool i2cGPSFlag = false; static bool serialGPSFlag = false; #ifndef TELEM_RAK12500_ADDRESS @@ -876,7 +878,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ } else if (Serial1.available()) { MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on"); #ifdef PIN_GPS_EN - if(PIN_GPS_EN){ + if(PIN_GPS_EN >= 0){ gpsResetPin = PIN_GPS_EN; } #endif diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 2040faa4df..2f60b98dfb 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -16,6 +16,7 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM -D FORCE_GPS_ALIVE ; on RAK3401, gpsResetPin ends up = WB_IO2 (kills 3V3_S rail) or ; pin 4 (= SX126X_RESET, holds radio in reset). stop_gps() must not run. + -D PIN_GPS_EN=-1 ; -1 = no dedicated GPS enable pin (power is the shared 3V3_S rail), same as rak4631 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak3401> + From 13d34967958dac13c6ce5efad1bb2a0879522d35 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 11 Jun 2026 19:34:37 +0100 Subject: [PATCH 07/11] Note PIN_USER_BTN=9 incompatibility with Slot D peripherals P0.09 is WisBlock IO5 (Slot D). A module driving that line, e.g. the RAK12002 RTC whose RV-3028 CLKOUT is push-pull and driven low when disabled, reads as a held user button. The resulting long-press within 8s of boot enters CLI rescue, which stops servicing the BLE/USB companion interface. Co-Authored-By: Claude Fable 5 --- variants/rak4631/platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 2bbba31463..9077cf1cea 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -122,7 +122,7 @@ board_upload.maximum_size = 712704 build_flags = ${rak4631.build_flags} -I examples/companion_radio/ui-new - -D PIN_USER_BTN=9 + -D PIN_USER_BTN=9 ; P0.09 = IO5: incompatible with Slot D peripherals (e.g. RAK12002 CLKOUT holds it low) -D PIN_USER_BTN_ANA=31 -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 @@ -143,7 +143,7 @@ board_upload.maximum_size = 712704 build_flags = ${rak4631.build_flags} -I examples/companion_radio/ui-new - -D PIN_USER_BTN=9 + -D PIN_USER_BTN=9 ; P0.09 = IO5: incompatible with Slot D peripherals (e.g. RAK12002 CLKOUT holds it low) -D PIN_USER_BTN_ANA=31 -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 @@ -165,7 +165,7 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} - -D PIN_USER_BTN=9 + -D PIN_USER_BTN=9 ; P0.09 = IO5: incompatible with Slot D peripherals (e.g. RAK12002 CLKOUT holds it low) -D PIN_USER_BTN_ANA=31 -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 From 47d3753c9b64d5cad50918ecc44d9701b339209d Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Wed, 24 Jun 2026 15:03:56 +0100 Subject: [PATCH 08/11] rak4631: move gps pins to variant.h, force_gps_alive just like rak3401 --- variants/rak4631/platformio.ini | 3 +-- variants/rak4631/variant.h | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 9077cf1cea..4de3f2a245 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -12,8 +12,7 @@ build_flags = ${nrf52_base.build_flags} -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 - -D PIN_GPS_TX=PIN_SERIAL1_RX - -D PIN_GPS_RX=PIN_SERIAL1_TX + -D FORCE_GPS_ALIVE -D PIN_GPS_EN=-1 -D PIN_OLED_RESET=-1 -D USE_SX1262 diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index 38cc88685c..42ca3b0c53 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -182,6 +182,9 @@ extern "C" #define EXTERNAL_FLASH_DEVICES IS25LP080D #define EXTERNAL_FLASH_USE_QSPI +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_TX PIN_SERIAL1_RX + #define PIN_GPS_1PPS 17 //GPS PPS pin #define GPS_BAUD_RATE 9600 #define GPS_ADDRESS 0x42 //i2c address for GPS From ff82e66a494560b2e3cfca30826e8a78b09761ba Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Wed, 24 Jun 2026 17:04:05 +0100 Subject: [PATCH 09/11] Decouple GPS stop/start from the shared 3V3_S rail --- .../sensors/EnvironmentSensorManager.cpp | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index ad9a39fd9d..9aa7cf6b6a 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -897,8 +897,23 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ void EnvironmentSensorManager::start_gps() { gps_active = true; #ifdef RAK_WISBLOCK_GPS - pinMode(gpsResetPin, OUTPUT); - digitalWrite(gpsResetPin, HIGH); + #if defined(PIN_GPS_EN) && (PIN_GPS_EN >= 0) + pinMode(gpsResetPin, OUTPUT); + digitalWrite(gpsResetPin, HIGH); // dedicated GPS enable pin + #else + // No dedicated enable pin: gpsResetPin is the shared 3V3_S rail (WB_IO2), + // which also powers the RTC/boost and must stay HIGH. Wake the receiver in + // software instead of touching the rail. + if (i2cGPSFlag) { + // UBX-CFG-RST "controlled GNSS start": resume the receiver stopped below. + static const uint8_t ubxGnssStart[] = {0xB5,0x62,0x06,0x04,0x04,0x00,0x00,0x00,0x09,0x00,0x17,0x76}; + Wire.beginTransmission((uint8_t)TELEM_RAK12500_ADDRESS); + Wire.write(ubxGnssStart, sizeof(ubxGnssStart)); + Wire.endTransmission(); + } else if (serialGPSFlag) { + Serial1.write((uint8_t)0xFF); // wake MTK/Quectel from standby + } + #endif return; #endif @@ -913,8 +928,25 @@ void EnvironmentSensorManager::start_gps() { void EnvironmentSensorManager::stop_gps() { gps_active = false; #ifdef RAK_WISBLOCK_GPS - pinMode(gpsResetPin, OUTPUT); - digitalWrite(gpsResetPin, LOW); + #if defined(PIN_GPS_EN) && (PIN_GPS_EN >= 0) + pinMode(gpsResetPin, OUTPUT); + digitalWrite(gpsResetPin, LOW); // dedicated GPS enable pin + #else + // No dedicated enable pin: gpsResetPin is the shared 3V3_S rail (WB_IO2), + // which also powers the RTC/boost and must stay HIGH. Don't drop it; put + // the receiver into software low-power instead (rail/RTC stay up, warm-start). + if (i2cGPSFlag) { + // UBX-CFG-RST "controlled GNSS stop": stops RF/tracking but keeps the host + // interface alive, so it restarts reliably over I2C. (PMREQ backup only wakes + // on EXTINT/UART, not I2C -- confirmed dead on this hardware.) + static const uint8_t ubxGnssStop[] = {0xB5,0x62,0x06,0x04,0x04,0x00,0x00,0x00,0x08,0x00,0x16,0x74}; + Wire.beginTransmission((uint8_t)TELEM_RAK12500_ADDRESS); + Wire.write(ubxGnssStop, sizeof(ubxGnssStop)); + Wire.endTransmission(); + } else if (serialGPSFlag) { + Serial1.println("$PMTK161,0*28"); // Quectel/MTK standby + } + #endif return; #endif From 8f867bfd688e59a339f4f3ee4b7d0f1d7587a191 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Wed, 24 Jun 2026 18:16:29 +0100 Subject: [PATCH 10/11] Better GPS fix in ui-new --- examples/companion_radio/ui-new/UITask.cpp | 8 +++++++- src/helpers/sensors/EnvironmentSensorManager.cpp | 7 +++++-- src/helpers/sensors/LocationProvider.h | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ee12ca740d..1cadd4cc6a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -310,7 +310,13 @@ class HomeScreen : public UIScreen { y = y + 12; display.drawTextLeftAlign(0, y, "Can't access GPS"); } else { - strcpy(buf, nmea->isValid()?"fix":"no fix"); + if (!gps_state) { + strcpy(buf, "off"); + } else switch (nmea->getFixType()) { + case 3: strcpy(buf, "3D"); break; + case 2: strcpy(buf, "2D"); break; + default: strcpy(buf, "no fix"); break; + } display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; display.drawTextLeftAlign(0, y, "sat"); diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 9aa7cf6b6a..facfbccc36 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -183,6 +183,7 @@ class RAK12500LocationProvider : public LocationProvider { int _sats = 0; long _epoch = 0; bool _fix = false; + uint8_t _fixType = 0; unsigned long _next_poll = 0; public: long getLatitude() override { return _lat; } @@ -190,6 +191,7 @@ class RAK12500LocationProvider : public LocationProvider { long getAltitude() override { return _alt; } long satellitesCount() override { return _sats; } bool isValid() override { return _fix; } + uint8_t getFixType() override { return _fixType; } long getTimestamp() override { return _epoch; } void sendSentence(const char * sentence) override { } void reset() override { } @@ -225,12 +227,13 @@ class RAK12500LocationProvider : public LocationProvider { // Ublox reports gnssFixOK=true with bogus coords during cold start (e.g. an // unfixed module on first boot reporting coordinates from factory almanac); // requiring fixType==3 filters those out. - if (ublox_GNSS.getGnssFixOk() && ublox_GNSS.getFixType() == 3) { + _sats = ublox_GNSS.getSIV(); // valid without a fix -- shows acquisition progress + _fixType = ublox_GNSS.getFixType(); // 0/1=none, 2=2D, 3=3D -- for the UI + if (ublox_GNSS.getGnssFixOk() && _fixType == 3) { _fix = true; _lat = ublox_GNSS.getLatitude() / 10; _lng = ublox_GNSS.getLongitude() / 10; _alt = ublox_GNSS.getAltitudeMSL(); // height above mean sea level, not ellipsoid - _sats = ublox_GNSS.getSIV(); } else { _fix = false; } diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652ed..864ee629fb 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -15,6 +15,7 @@ class LocationProvider { virtual long getAltitude() = 0; virtual long satellitesCount() = 0; virtual bool isValid() = 0; + virtual uint8_t getFixType() { return isValid() ? 3 : 0; } // 0=none, 2=2D, 3=3D; providers may override virtual long getTimestamp() = 0; virtual void sendSentence(const char * sentence); virtual void reset() = 0; From d2e47487c4fbb87d39734c4b6b3b05c4856bfb87 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 25 Jun 2026 00:37:11 +0100 Subject: [PATCH 11/11] GPS screen: accurate fix state, no bogus coordinates Show off / no-GPS / no-fix / 2D / 3D with a live sat count, and "---" until there's a real position. Reject ublox cold-start junk and MicroNMEA's 999-deg no-fix sentinel (both -> 0 = no data). Position renders eagerly on-screen, telemetry still reports only an accuracy-OK 3D fix. --- examples/companion_radio/ui-new/UITask.cpp | 17 ++++++++--- examples/companion_radio/ui-tiny/UITask.cpp | 2 +- .../sensors/EnvironmentSensorManager.cpp | 28 +++++++++++++------ .../sensors/EnvironmentSensorManager.h | 4 ++- .../sensors/MicroNMEALocationProvider.h | 11 ++++++-- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 1cadd4cc6a..1c59f2ff14 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -308,7 +308,7 @@ class HomeScreen : public UIScreen { display.drawTextLeftAlign(0, y, buf); if (nmea == NULL) { y = y + 12; - display.drawTextLeftAlign(0, y, "Can't access GPS"); + display.drawTextLeftAlign(0, y, "No GPS detected"); } else { if (!gps_state) { strcpy(buf, "off"); @@ -323,13 +323,22 @@ class HomeScreen : public UIScreen { sprintf(buf, "%d", nmea->satellitesCount()); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; + bool has_pos = (nmea->getLatitude() != 0 || nmea->getLongitude() != 0); display.drawTextLeftAlign(0, y, "pos"); - sprintf(buf, "%.4f %.4f", - nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.); + if (has_pos) { + sprintf(buf, "%.4f %.4f", + nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.); + } else { + strcpy(buf, "---"); + } display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; display.drawTextLeftAlign(0, y, "alt"); - sprintf(buf, "%.2f", nmea->getAltitude()/1000.); + if (has_pos) { + sprintf(buf, "%.2f", nmea->getAltitude()/1000.); + } else { + strcpy(buf, "---"); + } display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; } diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 45a07a02ef..8e3ad9c2d4 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -267,7 +267,7 @@ class HomeScreen : public UIScreen { display.drawTextLeftAlign(0, y, buf); if (nmea == NULL) { // y = y + 8; - display.drawTextLeftAlign(0, y, "Can't access GPS"); + display.drawTextLeftAlign(0, y, "No GPS detected"); } else { if (!gps_state || !nmea->isValid()) { strcpy(buf, "no fix"); diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index facfbccc36..357c330fa9 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -223,18 +223,28 @@ class RAK12500LocationProvider : public LocationProvider { return; // no response this tick — keep last reported values } - // Require BOTH gnssFixOK (DOP/accuracy mask passed) AND fixType==3 (3D fix). - // Ublox reports gnssFixOK=true with bogus coords during cold start (e.g. an - // unfixed module on first boot reporting coordinates from factory almanac); - // requiring fixType==3 filters those out. + // Show the position EAGERLY: as soon as there's a real 2D/3D solution (SIV>0, + // fixType>=2) with valid coords, display it -- don't wait for gnssFixOk, which can + // take up to ~30 s to assert. Coords must be present (0/0 is the "no data" value) and + // in range: the module emits 0/0 and ~999deg junk while cold/re-tracking, so rejecting + // those keeps _lat/_lng at 0 (-> "---"). Telemetry is stricter: _fix (= isValid()) only + // goes true once gnssFixOk passes on a full 3D fix, and node_lat is gated on isValid() + // -- so the screen can show a settling fix while we withhold a loose one from the mesh. _sats = ublox_GNSS.getSIV(); // valid without a fix -- shows acquisition progress - _fixType = ublox_GNSS.getFixType(); // 0/1=none, 2=2D, 3=3D -- for the UI - if (ublox_GNSS.getGnssFixOk() && _fixType == 3) { - _fix = true; - _lat = ublox_GNSS.getLatitude() / 10; - _lng = ublox_GNSS.getLongitude() / 10; + long lat = ublox_GNSS.getLatitude() / 10; + long lng = ublox_GNSS.getLongitude() / 10; + uint8_t ft = (_sats > 0) ? ublox_GNSS.getFixType() : 0; + bool coords_ok = (lat != 0 || lng != 0) && + lat >= -90000000 && lat <= 90000000 && // +/-90 deg (1e6 scale) + lng >= -180000000 && lng <= 180000000; // +/-180 deg + if (ft >= 2 && coords_ok) { + _fixType = ft; // 2 or 3 -- shown on screen with the coords + _lat = lat; + _lng = lng; _alt = ublox_GNSS.getAltitudeMSL(); // height above mean sea level, not ellipsoid + _fix = (ft == 3 && ublox_GNSS.getGnssFixOk()); // isValid()/telemetry: accuracy-OK 3D only } else { + _fixType = 0; // no usable fix -> "no fix" + held/--- position _fix = false; } _epoch = ublox_GNSS.getUnixEpoch(); diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 29147c8967..53d4be66b4 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -37,7 +37,9 @@ class EnvironmentSensorManager : public SensorManager { public: #if ENV_INCLUDE_GPS EnvironmentSensorManager(LocationProvider &location): _location(&location){}; - LocationProvider* getLocationProvider() { return _location; } + // NULL when no GPS was detected, so the UI shows "No GPS detected" (CLI: "gps provider + // not found") instead of an unfixed-but-present GPS. All consumers already null-check. + LocationProvider* getLocationProvider() { return gps_detected ? _location : NULL; } #else EnvironmentSensorManager(){}; #endif diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index eec466d3aa..49c77483dd 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -112,15 +112,20 @@ public : } void syncTime() override { nmea.clear(); LocationProvider::syncTime(); } - long getLatitude() override { return nmea.getLatitude(); } - long getLongitude() override { return nmea.getLongitude(); } + // MicroNMEA reports lat/lng = 999000000 (999 deg) when there is no fix; map that to 0 + // (the codebase's no-data value) so the UI shows "---" and node_lat stays 0. + long getLatitude() override { long v = nmea.getLatitude(); return v == 999000000L ? 0 : v; } + long getLongitude() override { long v = nmea.getLongitude(); return v == 999000000L ? 0 : v; } long getAltitude() override { long alt = 0; nmea.getAltitude(alt); return alt; } long satellitesCount() override { return nmea.getNumSatellites(); } - bool isValid() override { return nmea.isValid(); } + // Require actual coordinates, not just the RMC 'A' status flag: some MTK/Quectel + // receivers assert 'A' for seconds before the position is computed -- and MicroNMEA + // reports lat==999000000 until then. Without this it'd show "fix" with no coords. + bool isValid() override { return nmea.isValid() && nmea.getLatitude() != 999000000L; } long getTimestamp() override { DateTime dt(nmea.getYear(), nmea.getMonth(),nmea.getDay(),nmea.getHour(),nmea.getMinute(),nmea.getSecond());