Merge pull request #199 from ladyada-eagleclaw/gps-exact-received-position

Expose exact received positions through the GPS API
This commit is contained in:
Limor "Ladyada" Fried authored and GitHub committed 2026-09-16 12:55:12 -04:00
commit 6a569968e1
4 files changed
+218 -1

No files matched your search

+42
View File
@@ -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;
+148
View File
@@ -0,0 +1,148 @@
#include <Adafruit_GPS.h>
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;
}
+26
View File
@@ -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
+2 -1
View File
@@ -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