From 0a7b9caef44ea067a00a7f3258d6095299053744 Mon Sep 17 00:00:00 2001 From: ladyada Date: Wed, 16 Sep 2026 12:45:09 -0400 Subject: [PATCH] Expose exact received positions through the GPS API Co-authored-by: Limor Fried --- README.md | 42 ++++++ extras/tests/gps_position/gps_position.ino | 148 +++++++++++++++++++++ src/Adafruit_GPS.cpp | 26 ++++ src/Adafruit_GPS.h | 3 +- 4 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 extras/tests/gps_position/gps_position.ino diff --git a/README.md b/README.md index b94203d..755989d 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,48 @@ may not yet include the latest merged fixes. | I2C | Your GPS exposes SDA and SCL, as the PA1010D does. | Connect SDA, SCL, power, and ground. Examples use address `0x10`; on an Uno or Metro Mini, SDA is A4 and SCL is A5. | | SPI | Your GPS specifically supports SPI. | Follow its wiring requirements; the echo example uses CS on D10 and reset on D9. | +## Exact received positions + +`GPS.lastPosition()` returns the latest received GGA, RMC, or GLL sentence as +an independent `gnss_position_t`. Its coordinate components retain all nine +fractional-minute digits without a global float-type override. Use +`Adafruit_GNSS::formatCoordinate()` to print them as decimal degrees on any board, +including AVR. Preserving digits does not increase the receiver's accuracy. + +For a sketch that polls `GPS.read()` in `loop()`, after initializing `GPS`: + +```cpp +GPS.read(); +if (GPS.newNMEAreceived()) { + gnss_position_t position = GPS.lastPosition(); + GPS.lastNMEA(); // Acknowledge this line; lastPosition() leaves the flag alone. + if (position.validation.status == GNSS_SENTENCE_VALID && + position.fixStatus == NMEA_NUMBER_VALID && position.fix && + position.latitude.status == NMEA_NUMBER_VALID && + position.longitude.status == NMEA_NUMBER_VALID) { + char coordinate[GNSS_COORDINATE_TEXT_SIZE]; + Adafruit_GNSS::formatCoordinate(coordinate, sizeof(coordinate), position.latitude); + Serial.print(coordinate); + Serial.print(F(", ")); + Adafruit_GNSS::formatCoordinate(coordinate, sizeof(coordinate), position.longitude); + Serial.println(coordinate); + } +} +``` + +The result describes one sentence, without merging previous fixes. Empty fields +stay empty, and replies or other sentence types return `GNSS_SENTENCE_UNSUPPORTED`. +Before any complete line, or for a line with an invalid frame/checksum, the result +is `GNSS_SENTENCE_INVALID_FRAME`. Partial or oversized input leaves the previous +complete line available. If an interrupt calls `GPS.read()`, synchronize it with +`lastPosition()` so the receive buffer cannot change during decoding. + +This getter does not update `GPS.fix`, `GPS.latitudeDegrees`, or other legacy +fields; continue using `GPS.parse(GPS.lastNMEA())` when those fields are needed. +Likewise, parsing a separate caller-supplied string does not change this getter's +received sentence. Decode such a string with `Adafruit_NMEA::validate()` followed +by `Adafruit_GNSS::parsePosition()` instead. + ## Example guide Every example is linked below. An **echo** sketch shows the GPS's raw text; diff --git a/extras/tests/gps_position/gps_position.ino b/extras/tests/gps_position/gps_position.ino new file mode 100644 index 0000000..b9b6410 --- /dev/null +++ b/extras/tests/gps_position/gps_position.ino @@ -0,0 +1,148 @@ +#include + +class TestStream : public Stream { +public: + const char *input = ""; + int available() { return *input != 0; } + int read() { return *input ? *input++ : -1; } + int peek() { return *input ? *input : -1; } + void flush() {} + size_t write(uint8_t) { return 1; } +}; + +TestStream firstStream, secondStream; +Adafruit_GPS firstGPS((Stream *)&firstStream), secondGPS((Stream *)&secondStream); +bool testsPassed = false; +bool receiveBody(TestStream &stream, Adafruit_GPS &gps, const char *body); +bool noPosition(const gnss_position_t &position, gnss_sentence_status_t status); + +void setup() { + Serial.begin(115200); + Serial.println(F("Adafruit GPS exact received position regression")); + const Adafruit_GPS &view = firstGPS; + if (!noPosition(view.lastPosition(), GNSS_SENTENCE_INVALID_FRAME) || + firstGPS.newNMEAreceived()) { + Serial.println(F("FAIL: getter before reception")); + return; + } + firstGPS.fix = false; + firstGPS.latitude_fixed = 42; + firstGPS.year = 99; + if (!receiveBody(firstStream, firstGPS, + "GNRMC,123519.123,A,8959.123456789,S,17959.123456789,W,1,2,290224")) { + Serial.println(F("FAIL: receive first exact position")); + return; + } + gnss_position_t saved = view.lastPosition(); + char coordinate[GNSS_COORDINATE_TEXT_SIZE]; + if (saved.validation.status != GNSS_SENTENCE_VALID || !saved.fix || + saved.latitude.fractionalMinutes != 123456789 || saved.date.day != 29 || + saved.time.millisecond != 123 || !firstGPS.newNMEAreceived() || + firstGPS.fix || firstGPS.latitude_fixed != 42 || firstGPS.year != 99 || + !Adafruit_GNSS::formatCoordinate(coordinate, sizeof(coordinate), saved.longitude) || + strcmp(coordinate, "-179.98539094648")) { + Serial.println(F("FAIL: exact result, acknowledgement flag, or legacy state changed")); + return; + } + if (!firstGPS.parse(firstGPS.lastNMEA()) || firstGPS.newNMEAreceived() || + !firstGPS.fix || firstGPS.latitude_fixed != saved.latitude.degreesE7 || + view.lastPosition().longitude.fractionalMinutes != 123456789) { + Serial.println(F("FAIL: legacy parse and exact getter coexistence")); + return; + } + Serial.println(F("PASS: exact output on float builds without changing receive or legacy state")); + + firstStream.input = "$unfinished"; + while (firstStream.available()) firstGPS.read(); + if (firstGPS.newNMEAreceived() || + view.lastPosition().latitude.fractionalMinutes != 123456789 || + !receiveBody(firstStream, firstGPS, + "GNRMC,123520,A,8959.123456889,S,17959.123456889,W,1,2,290224")) { + Serial.println(F("FAIL: partial sentence or recovery")); + return; + } + gnss_position_t next = view.lastPosition(); + if (next.latitude.degreesE7 != saved.latitude.degreesE7 || + next.latitude.fractionalMinutes != 123456889 || + saved.latitude.fractionalMinutes != 123456789 || + !Adafruit_GNSS::formatCoordinate(coordinate, sizeof(coordinate), next.longitude) || + strcmp(coordinate, "-179.98539094815")) { + Serial.println(F("FAIL: submillimeter change or owned result")); + return; + } + Serial.println(F("PASS: new sentences retain changes smaller than E7 and preserve copied results")); + + if (!receiveBody(secondStream, secondGPS, + "GBGGA,010203,4807.038,N,01131.000,E,4,08,0.9,545.4,M,46.9") || + secondGPS.lastPosition().fixQuality != 4 || + secondGPS.lastPosition().latitude.degreesE7 != 481173000 || + view.lastPosition().latitude.fractionalMinutes != 123456889) { + Serial.println(F("FAIL: independent receivers or shared talker routing")); + return; + } + char separate[120] = "$GPGLL,4807.038,N,01131.000,E,123519,A"; + firstGPS.addChecksum(separate); + if (!firstGPS.parse(separate) || + view.lastPosition().latitude.fractionalMinutes != 123456889 || + !firstGPS.newNMEAreceived()) { + Serial.println(F("FAIL: parsing a supplied string changed the received snapshot")); + return; + } + Serial.println(F("PASS: supplied-buffer parsing and separate receivers remain independent")); + + firstGPS.lastNMEA(); + if (!receiveBody(firstStream, firstGPS, "GNRMC,,V,,,,,,,") || + view.lastPosition().validation.status != GNSS_SENTENCE_VALID || + view.lastPosition().latitude.status != NMEA_NUMBER_EMPTY || + view.lastPosition().fix || !firstGPS.fix) { + Serial.println(F("FAIL: empty/no-fix line reused a cached position")); + return; + } + firstGPS.lastNMEA(); + if (!receiveBody(firstStream, firstGPS, "PAIR001,062,0") || + !noPosition(view.lastPosition(), GNSS_SENTENCE_UNSUPPORTED) || + !firstGPS.newNMEAreceived() || strncmp(firstGPS.lastNMEA(), "$PAIR001,", 9)) { + Serial.println(F("FAIL: command reply replaced or acknowledged by getter")); + return; + } + firstStream.input = "$GPGLL,4807.038,N,01131.000,E,123519,A*00\n"; + while (firstStream.available()) firstGPS.read(); + if (!firstGPS.newNMEAreceived() || + !noPosition(view.lastPosition(), GNSS_SENTENCE_INVALID_FRAME)) { + Serial.println(F("FAIL: bad checksum accepted or acknowledged")); + return; + } + firstGPS.common_init(); + if (!noPosition(view.lastPosition(), GNSS_SENTENCE_INVALID_FRAME) || + firstGPS.newNMEAreceived() || secondGPS.lastPosition().fixQuality != 4 || + saved.latitude.fractionalMinutes != 123456789) { + Serial.println(F("FAIL: reset, receiver isolation, or result lifetime")); + return; + } + Serial.println(F("PASS: no-fix, replies, bad checksum, and reset never reuse stale positions")); + testsPassed = true; +} + +void loop() { + if (testsPassed) Serial.println(F("PASS: exact received position regression")); + else Serial.println(F("FAIL: exact received position regression")); +} + +bool receiveBody(TestStream &stream, Adafruit_GPS &gps, const char *body) { + char line[120]; + if (!Adafruit_NMEA::buildCommand(line, sizeof(line), body, strlen(body))) return false; + stream.input = line; + while (stream.available()) gps.read(); + stream.input = ""; + return gps.newNMEAreceived(); +} + +bool noPosition(const gnss_position_t &position, gnss_sentence_status_t status) { + return position.validation.status == status && + position.latitude.status == NMEA_NUMBER_MISSING && + position.longitude.status == NMEA_NUMBER_MISSING && + position.time.status == NMEA_NUMBER_MISSING && + position.date.status == NMEA_NUMBER_MISSING && + position.fixStatus == NMEA_NUMBER_MISSING && + position.fixQualityStatus == NMEA_NUMBER_MISSING && !position.fix; +} diff --git a/src/Adafruit_GPS.cpp b/src/Adafruit_GPS.cpp index 397ce42..4595b18 100644 --- a/src/Adafruit_GPS.cpp +++ b/src/Adafruit_GPS.cpp @@ -405,6 +405,32 @@ char *Adafruit_GPS::lastNMEA(void) { return (char *)lastline; } +/**************************************************************************/ +/*! + @brief Decode the latest received sentence with exact coordinate precision. + @return Independent GGA/RMC/GLL position data and per-field status. + + Call after read() completes a line. This does not acknowledge the receive + flag, update legacy GPS fields, or merge data from earlier sentences. Use + lastNMEA() to acknowledge the line and parse() to update legacy fields. + Parsing a caller-supplied string does not replace the received sentence. + + Check validation and each field's status; a valid fix flag does not imply + populated coordinates. Exact components can be passed to + Adafruit_GNSS::formatCoordinate() even when nmea_float_t is float. + No stored fix or extra receive buffer is added. The result owns its values + and survives subsequent input. Calls must be synchronized with read(), + including when read() runs in an interrupt. + + Before reception or after common_init(), returns INVALID_FRAME. Invalid + complete lines return INVALID_FRAME; valid non-position sentences return + UNSUPPORTED. Partial or oversized input preserves the last complete line. +*/ +/**************************************************************************/ +gnss_position_t Adafruit_GPS::lastPosition() const { + return receiver.lastPosition(); +} + /**************************************************************************/ /*! * @brief Wait for a specified sentence from the device diff --git a/src/Adafruit_GPS.h b/src/Adafruit_GPS.h index 92dbdfc..5d75ad9 100644 --- a/src/Adafruit_GPS.h +++ b/src/Adafruit_GPS.h @@ -106,6 +106,7 @@ public: bool newNMEAreceived(); void pause(bool b); char *lastNMEA(void); + gnss_position_t lastPosition() const; bool waitForSentence(const char *wait, uint8_t max = MAXWAITSENTENCE, bool usingInterrupts = false, uint32_t timeout = 10000); bool LOCUS_StartLogger(void); @@ -293,7 +294,7 @@ private: volatile char line1[MAXLINELENGTH]; ///< We double buffer: read one line in ///< and leave one for the main program volatile char line2[MAXLINELENGTH]; ///< Second buffer - Adafruit_NMEA receiver{line1, line2, MAXLINELENGTH}; ///< Bounded byte framing + Adafruit_GNSS receiver{line1, line2, MAXLINELENGTH}; ///< Shared GNSS receiver const char *volatile lastline = (const char *)line2; ///< Completed line published by read() volatile bool recvdflag; ///< Received flag