diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ee12ca740d..1c59f2ff14 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -308,22 +308,37 @@ 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 { - 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"); 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 73842d9eeb..357c330fa9 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 @@ -181,28 +183,71 @@ 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; } long getLongitude() override { return _lng; } 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 { } void begin() override { } void stop() override { } void loop() override { - if (ublox_GNSS.getGnssFixOk(8)) { - _fix = true; - _lat = ublox_GNSS.getLatitude(2) / 10; - _lng = ublox_GNSS.getLongitude(2) / 10; - _alt = ublox_GNSS.getAltitude(2); - _sats = ublox_GNSS.getSIV(2); + // 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 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 + } + + // 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 + 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(2); + _epoch = ublox_GNSS.getUnixEpoch(); } bool isEnabled() override { return true; } }; @@ -787,16 +832,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 @@ -837,7 +891,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 @@ -856,8 +910,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 @@ -872,8 +941,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 @@ -890,6 +976,7 @@ void EnvironmentSensorManager::loop() { #if ENV_INCLUDE_GPS static long next_gps_update = 0; + static uint8_t gps_debug_skip = 0; // throttle: print roughly every 10th update tick if (gps_active) { _location->loop(); } @@ -900,17 +987,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 } 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/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; 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()); diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 20a8a548b9..2f60b98dfb 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,9 @@ 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. + -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> + 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 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 2bbba31463..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 @@ -122,7 +121,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 +142,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 +164,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 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