mirror of
https://github.com/adafruit/Adafruit_GPS.git
synced 2026-10-26 22:10:17 +00:00
Connect the GNSS receiver base to shared NMEA framing
Co-authored-by: Limor Fried <ladyada@users.noreply.github.com>
This commit is contained in:
1 parent
bf8d03e793
commit
1f5936353d
4 files changed
+240
-6
No files matched your search
@@ -46,6 +46,13 @@ fix and GGA-quality status, empty/missing fields, exact coordinates through text
|
||||
output, and rejection of malformed sentences without partial measurements.
|
||||
Returned values remain independent of the input buffer and other receivers.
|
||||
|
||||
The GNSS receiver regression feeds interleaved bytes into two receiver subclasses,
|
||||
checks independent timestamps and exact results, and exercises partial lines,
|
||||
overflow recovery, invalid checksums, reset, and mixed proprietary replies.
|
||||
`Adafruit_GNSS(firstBuffer, secondBuffer, capacity)` inherits `feed()` and
|
||||
`lastSentence()` from the NMEA core; call `lastPosition()` after a completed line.
|
||||
It returns that line's position, without caching or merging a previous fix.
|
||||
|
||||
Physical reception tests live separately in `extras/hw_tests/gnss_receive`.
|
||||
Build and run them explicitly on the documented Nano or HILBERT fixture; the
|
||||
host test runner does not operate attached boards.
|
||||
@@ -0,0 +1,151 @@
|
||||
#include "Adafruit_GNSS.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <type_traits>
|
||||
|
||||
class Receiver : public Adafruit_GNSS {
|
||||
public:
|
||||
using Adafruit_GNSS::Adafruit_GNSS;
|
||||
};
|
||||
|
||||
static size_t build(char *line, size_t capacity, const char *body) {
|
||||
size_t length =
|
||||
Adafruit_NMEA::buildCommand(line, capacity, body, strlen(body));
|
||||
assert(length);
|
||||
return length;
|
||||
}
|
||||
|
||||
static nmea_frame_status_t feed(Adafruit_GNSS &receiver, const char *line,
|
||||
size_t length, uint32_t start) {
|
||||
nmea_frame_status_t status = NMEA_FRAME_INCOMPLETE;
|
||||
for (size_t i = 0; i < length; i++)
|
||||
status = receiver.feed(line[i], start + (uint32_t)i);
|
||||
return status;
|
||||
}
|
||||
|
||||
static void noPosition(const gnss_position_t &position,
|
||||
gnss_sentence_status_t status) {
|
||||
assert(position.validation.status == status);
|
||||
assert(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);
|
||||
assert(!position.latitude.fractionalMinutes &&
|
||||
!position.longitude.degreesE7 && !position.fix &&
|
||||
!position.fixQuality);
|
||||
}
|
||||
|
||||
int main() {
|
||||
static_assert(std::is_base_of<Adafruit_NMEA, Adafruit_GNSS>::value,
|
||||
"GNSS shares the NMEA receiver");
|
||||
static_assert(sizeof(Adafruit_GNSS) == sizeof(Adafruit_NMEA),
|
||||
"GNSS must not add persistent receive or fix storage");
|
||||
static_assert(!std::is_copy_constructible<Adafruit_GNSS>::value &&
|
||||
!std::is_copy_assignable<Adafruit_GNSS>::value,
|
||||
"Receivers must not share writable buffers through copies");
|
||||
char first[128], second[128], third[128], fourth[128];
|
||||
Receiver one(first, second, sizeof(first)), two(third, fourth, sizeof(third));
|
||||
noPosition(one.lastPosition(), GNSS_SENTENCE_INVALID_FRAME);
|
||||
Adafruit_GNSS disabled;
|
||||
assert(disabled.feed('$', 1) == NMEA_FRAME_BAD_FORMAT);
|
||||
noPosition(disabled.lastPosition(), GNSS_SENTENCE_INVALID_FRAME);
|
||||
|
||||
char rmc[128], gga[128];
|
||||
size_t rmcLength =
|
||||
build(rmc, sizeof(rmc),
|
||||
"GNRMC,123519.123,A,8959.123456789,S,17959.123456789,W,1,2,290224");
|
||||
size_t ggaLength =
|
||||
build(gga, sizeof(gga),
|
||||
"GPGGA,010203,4807.038,N,01131.000,E,4,08,0.9,545.4,M,46.9");
|
||||
uint32_t start = UINT32_MAX - 10;
|
||||
for (size_t i = 0; i < rmcLength || i < ggaLength; i++) {
|
||||
if (i < rmcLength)
|
||||
one.feed(rmc[i], start + (uint32_t)i);
|
||||
if (i < ggaLength)
|
||||
two.feed(gga[i], 500 + (uint32_t)i);
|
||||
}
|
||||
gnss_position_t saved = one.lastPosition();
|
||||
assert(saved.validation.status == GNSS_SENTENCE_VALID && saved.fix &&
|
||||
saved.latitude.fractionalMinutes == 123456789 && saved.date.day == 29);
|
||||
gnss_position_t other = two.lastPosition();
|
||||
assert(other.validation.status == GNSS_SENTENCE_VALID &&
|
||||
other.fixQuality == 4 && other.latitude.degreesE7 == 481173000 &&
|
||||
other.time.hour == 1);
|
||||
assert(one.sentenceStartedAt() == start &&
|
||||
one.sentenceReceivedAt() == start + (uint32_t)rmcLength - 1 &&
|
||||
two.sentenceStartedAt() == 500 &&
|
||||
two.sentenceReceivedAt() == 500 + ggaLength - 1);
|
||||
assert(one.lastPosition().longitude.fractionalMinutes == 123456789);
|
||||
assert(!memcmp(one.lastText().data, rmc, rmcLength));
|
||||
char coordinate[GNSS_COORDINATE_TEXT_SIZE];
|
||||
assert(Adafruit_GNSS::formatCoordinate(coordinate, sizeof(coordinate),
|
||||
saved.longitude));
|
||||
assert(!strcmp(coordinate, "-179.98539094648"));
|
||||
puts("PASS: subclass reception, interleaved receivers, exact output, and "
|
||||
"wraparound times");
|
||||
|
||||
assert(one.feed('$', 1000) == NMEA_FRAME_INCOMPLETE);
|
||||
assert(one.feed('G', 1001) == NMEA_FRAME_INCOMPLETE);
|
||||
assert(one.lastPosition().latitude.fractionalMinutes == 123456789);
|
||||
unsigned overflows = 0;
|
||||
for (size_t i = 0; i < sizeof(first) + 5; i++)
|
||||
overflows += one.feed('A', 1002 + i) == NMEA_FRAME_OVERFLOW;
|
||||
assert(overflows == 1 && one.sentenceStartedAt() == start);
|
||||
assert(one.lastPosition().longitude.fractionalMinutes == 123456789);
|
||||
assert(feed(one, gga, ggaLength, 2000) == NMEA_FRAME_VALID);
|
||||
assert(one.lastPosition().fixQuality == 4);
|
||||
assert(saved.longitude.fractionalMinutes == 123456789 &&
|
||||
saved.date.day == 29);
|
||||
puts("PASS: partial/oversized input preserves the last line and recovery "
|
||||
"replaces it");
|
||||
|
||||
char line[128];
|
||||
const char *unsupported[] = {"PQTMVERNO", "PAIR001,062,0", "PGGLL,,,,,,A",
|
||||
"GPGSV,1,1,00", "GPRMCX,1", "GLL,1",
|
||||
"gPGLL,,,,,,A", "1PGLL,,,,,,A"};
|
||||
for (const char *body : unsupported) {
|
||||
size_t length = build(line, sizeof(line), body);
|
||||
assert(feed(one, line, length, 3000) == NMEA_FRAME_VALID);
|
||||
noPosition(one.lastPosition(), GNSS_SENTENCE_UNSUPPORTED);
|
||||
assert(one.lastSentence().status == NMEA_FRAME_VALID &&
|
||||
!memcmp(one.lastText().data, line, length));
|
||||
}
|
||||
const char *talkers[] = {"GP", "GN", "GL", "GA", "GB", "GQ", "GI", "II"};
|
||||
for (const char *talker : talkers) {
|
||||
char body[64];
|
||||
snprintf(body, sizeof(body), "%sGLL,4807.038,N,01131.000,E,123519,A",
|
||||
talker);
|
||||
size_t length = build(line, sizeof(line), body);
|
||||
nmea_sentence_t sentence = Adafruit_NMEA::validate(line, length);
|
||||
assert(Adafruit_GNSS::parsePosition(sentence).validation.status ==
|
||||
GNSS_SENTENCE_VALID);
|
||||
line[0] = '!'; // The start marker is outside the checksum.
|
||||
sentence = Adafruit_NMEA::validate(line, length);
|
||||
assert(sentence.status == NMEA_FRAME_VALID);
|
||||
noPosition(Adafruit_GNSS::parsePosition(sentence),
|
||||
GNSS_SENTENCE_UNSUPPORTED);
|
||||
}
|
||||
puts("PASS: standard talkers route to positions; replies remain available to "
|
||||
"subclasses");
|
||||
|
||||
size_t length =
|
||||
build(line, sizeof(line), "GPGLL,4807.038,N,01131.000,E,123519,Q");
|
||||
assert(feed(one, line, length, 4000) == NMEA_FRAME_VALID);
|
||||
noPosition(one.lastPosition(), GNSS_SENTENCE_INVALID_FIELD);
|
||||
assert(one.lastPosition().validation.field == 6);
|
||||
line[length - 4] = line[length - 4] == '0' ? '1' : '0';
|
||||
assert(feed(one, line, length, 5000) == NMEA_FRAME_BAD_CHECKSUM);
|
||||
noPosition(one.lastPosition(), GNSS_SENTENCE_INVALID_FRAME);
|
||||
assert(one.lastSentence().status == NMEA_FRAME_BAD_CHECKSUM);
|
||||
one.reset();
|
||||
noPosition(one.lastPosition(), GNSS_SENTENCE_INVALID_FRAME);
|
||||
assert(one.sentenceStartedAt() == 0 && one.sentenceReceivedAt() == 0);
|
||||
assert(two.lastPosition().fixQuality == 4 && two.sentenceStartedAt() == 500);
|
||||
assert(saved.latitude.fractionalMinutes == 123456789);
|
||||
puts("PASS: malformed navigation, checksum failure, reset, and owned result "
|
||||
"lifetime");
|
||||
return 0;
|
||||
}
|
||||
@@ -12,6 +12,76 @@
|
||||
static nmea_number_status_t parseSixDigits(nmea_span_t field, uint8_t *pairs);
|
||||
static bool validUnsignedByte(nmea_span_t field);
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Create a GNSS receiver using the shared bounded NMEA framer.
|
||||
@param firstBuffer First caller-owned writable receive buffer.
|
||||
@param secondBuffer Second buffer, disjoint from the first.
|
||||
@param capacity Bytes per buffer, including NUL space.
|
||||
|
||||
Storage must outlive the receiver. The default or invalid storage disables
|
||||
feed() as described by Adafruit_NMEA; static decoding remains available.
|
||||
Supply incoming bytes and their timestamps with the inherited feed().
|
||||
No position cache, transport, command handling, or allocation is added.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
Adafruit_GNSS::Adafruit_GNSS(volatile char *firstBuffer,
|
||||
volatile char *secondBuffer, size_t capacity)
|
||||
: Adafruit_NMEA(firstBuffer, secondBuffer, capacity) {}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Decode the latest complete line into an independent position result.
|
||||
@return Exact GGA/RMC/GLL measurements, or validation diagnostics.
|
||||
|
||||
Call after feed() reports a complete line, and synchronize with feed() if
|
||||
using interrupts. Each call validates and decodes the current line; it does
|
||||
not consume it. Before a line or after reset(), the result is INVALID_FRAME.
|
||||
Invalid completed lines replace earlier positions; proprietary replies and
|
||||
other sentences return UNSUPPORTED and remain accessible via lastSentence().
|
||||
Partial/overflowing input preserves the previous complete line, just as the
|
||||
framer does. Use sentenceStartedAt() and sentenceReceivedAt() for this line.
|
||||
Returned values own their data and survive later input or reset().
|
||||
*/
|
||||
/**************************************************************************/
|
||||
gnss_position_t Adafruit_GNSS::lastPosition() const {
|
||||
return parsePosition(lastSentence());
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Route a validated standard NMEA sentence to the position decoder.
|
||||
@param sentence Unmodified view returned by validate() or lastSentence().
|
||||
@return Independent values; INVALID_FRAME for non-VALID input, UNSUPPORTED
|
||||
for a valid frame outside the supported standard position sentences.
|
||||
|
||||
Keep the borrowed storage readable and unchanged throughout the call.
|
||||
Trust the supplied frame status; use validate() first for arbitrary text.
|
||||
A standard position address has two uppercase talker letters followed by
|
||||
GGA, RMC, or GLL. Any such talker is accepted except the proprietary 'P'
|
||||
prefix. Encapsulated '!' messages and proprietary addresses are not routed.
|
||||
No receiver-specific address whitelist or command handling is imposed on
|
||||
the underlying framer. Invalid/unsupported input returns no measurements.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
gnss_position_t Adafruit_GNSS::parsePosition(const nmea_sentence_t &sentence) {
|
||||
nmea_span_t type = {NULL, 0}, fields = {NULL, 0};
|
||||
if (sentence.status == NMEA_FRAME_VALID && sentence.text.data &&
|
||||
sentence.text.length && sentence.text.data[0] == '$' &&
|
||||
sentence.address.data && sentence.address.length == 5) {
|
||||
const char *address = sentence.address.data;
|
||||
if (address[0] >= 'A' && address[0] <= 'Z' && address[0] != 'P' &&
|
||||
address[1] >= 'A' && address[1] <= 'Z') {
|
||||
type = {address + 2, 3};
|
||||
fields = sentence.fields;
|
||||
}
|
||||
}
|
||||
gnss_position_t result = parsePosition(type, fields);
|
||||
if (sentence.status != NMEA_FRAME_VALID)
|
||||
result.validation.status = GNSS_SENTENCE_INVALID_FRAME;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Decode a latitude or longitude field and its hemisphere.
|
||||
|
||||
+12
-6
@@ -50,13 +50,14 @@ typedef enum : uint8_t {
|
||||
GNSS_SENTENCE_VALID, ///< Supported fields are valid or empty.
|
||||
GNSS_SENTENCE_UNSUPPORTED, ///< This validator does not handle the type.
|
||||
GNSS_SENTENCE_MISSING_FIELDS, ///< A required field position is absent.
|
||||
GNSS_SENTENCE_INVALID_FIELD ///< A populated field has invalid content.
|
||||
GNSS_SENTENCE_INVALID_FIELD, ///< A populated field has invalid content.
|
||||
GNSS_SENTENCE_INVALID_FRAME ///< No complete frame with valid checksum.
|
||||
} gnss_sentence_status_t;
|
||||
|
||||
/// Validation result; coordinate-pair errors identify the coordinate field.
|
||||
typedef struct {
|
||||
gnss_sentence_status_t status; ///< Result of navigation field validation.
|
||||
uint8_t field; ///< One-based error field, or zero for valid/unsupported.
|
||||
uint8_t field; ///< One-based error field, or zero without a field error.
|
||||
} gnss_validation_t;
|
||||
|
||||
/** One sentence's position data, independent of earlier receiver state.
|
||||
@@ -75,17 +76,22 @@ typedef struct {
|
||||
uint8_t fixQuality; ///< GGA quality, including RTK values; check its status.
|
||||
} gnss_position_t;
|
||||
|
||||
/** Stateless standard GNSS decoding shared by receiver implementations.
|
||||
* Inputs are borrowed spans. Decoding neither allocates memory nor performs
|
||||
* transport I/O, and does not alter the supplied text or receiver state.
|
||||
/** Standard GNSS receiver and decoding shared by receiver implementations.
|
||||
* Inherits bounded framing with caller-owned buffers and per-instance times.
|
||||
* Position decoding is stateless: no fix is cached or merged across lines.
|
||||
* Static decoding does not require receive storage. No heap or transport I/O.
|
||||
*/
|
||||
class Adafruit_GNSS {
|
||||
class Adafruit_GNSS : public Adafruit_NMEA {
|
||||
public:
|
||||
Adafruit_GNSS(volatile char *firstBuffer = NULL,
|
||||
volatile char *secondBuffer = NULL, size_t capacity = 0);
|
||||
gnss_position_t lastPosition() const;
|
||||
static gnss_coordinate_t parseCoordinate(nmea_span_t coordinate,
|
||||
nmea_span_t hemisphere);
|
||||
static size_t formatCoordinate(char *output, size_t capacity,
|
||||
const gnss_coordinate_t &coordinate);
|
||||
static gnss_position_t parsePosition(nmea_span_t type, nmea_span_t fields);
|
||||
static gnss_position_t parsePosition(const nmea_sentence_t &sentence);
|
||||
static gnss_time_t parseTime(nmea_span_t field);
|
||||
static gnss_date_t parseDate(nmea_span_t field);
|
||||
static gnss_validation_t validateNavigation(nmea_span_t type,
|
||||
|
||||
Reference in new issue
Block a user