From b6e6730feb9c3b13d2f9abe52677407e1dc0b713 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 28 May 2026 16:00:10 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] =?UTF-8?q?GPS:=20throttle=20RAK12500=20I=C2=B2C=20pol?= =?UTF-8?q?ling=20to=20~1=20Hz=20so=20it=20can't=20stall=20the=20main=20lo?= =?UTF-8?q?op?= 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; } };