15 KiB
Data Model
Rather than holding onto individual fields, the concept of a fix is used to group data members of the GPS acquisition into a C structure (a struct
type called gps_fix
). This also facilitates merging pieces received at different times (i.e., in separate sentences) into a single easy-to-use structure.
The main NMEAGPS gps;
object you declare in your sketch parses received characters, gradually assembling a fix
. Most programs will call gps.read()
to obtain the completed fix structure (see Usage below).
Given a variable declaration of type gps_fix
:
gps_fix fix;
...this fix
variable (or any other variable of type gps_fix
) contains the following members:
fix.status
, a status codeenum
values STATUS_NONE, STATUS_EST, STATUS_TIME_ONLY, STATUS_STD or STATUS_DGPS
- a location structure (i.e., latitude and longitude), accessed with
fix.latitudeL()
andfix.longitudeL()
for the higher-precision integer degrees, scaled by 10,000,000 (10 significant digits)fix.latitude()
andfix.longitude()
for the lower-precision floating-point degrees (~7 significant digits)- NOTE: these lat/lon values are
- positive for North or East degrees and negative for South or West degrees.
- stored in a 'fix.location' structure, like a 2D coordinate. The
location_t
class provides additional methods for distance, bearing and offset calculations, as described here.
fix.latitudeDMS
andfix.latitudeDMS
are structures (see DMS.h) that each containfix.longitudeDMS.degrees
in integer degreesfix.latitudeDMS.degrees
, in integer minutesfix.longitudeDMS.seconds_whole
, in integer secondsfix.latitudeDMS.seconds_frac
, in integer thousandths of a secondfix.latitudeDMS.secondsF()
, in floating-point seconds- hemisphere indicator, accessed with
fix.longitudeDMS.hemisphere
(enum values NORTH_H, SOUTH_H, EAST_H or WEST_H)fix.longitudeDMS.EW()
(char valuesE
orW
)fix.latitudeDMS.NS()
(char valuesN
orS
)
- NOTE: An integer degree value (scaled by 107 can be used to set the DMS structure by using
fix.latitudeDMS.From( otherLatitude );
- an altitude (above ellipsoid, aka Mean Sea Level), accessed with
fix.altitude_cm()
, in integer centimetersfix.altitude()
, in floating-point metersfix.alt.whole
, in integer metersfix.alt.frac
, in integer centimeters, to be added to the whole part
- a speed, accessed with
fix.speed_kph()
, in floating-point kilometers per hourfix.speed_mph()
, in floating-point miles per hourfix.speed()
, in floating-point knots (nautical miles per hour)fix.speed_mkn()
, in integer knots, scaled by 1000fix.spd.whole
, in integer knotsfix.spd.frac
, in integer thousandths of a knot, to be added to the whole part
- a heading, accessed with
fix.heading_cd()
, in integer hundredths of a degreefix.heading()
, in floating-point degrees
- velocity components in the North, East and Down directions, accessed with
fix.velocity_north
, in integer cm/sfix.velocity_east
, in integer cm/sfix.velocity_down
, in integer cm/s
fix.hdop
,fix.vdop
andfix.pdop
, in integer thousandths of the DOP.- Dilution of Precision is a unitless measure of the current satellite constellation geometry WRT how 'good' it is for determining a position. This is independent of signal strength and many other factors that may be internal to the receiver. It cannot be used to determine position accuracy in meters. Instead, use the LAT/LON/ALT error in cm members, which are populated by GST sentences.
- latitude, longitude and altitude error, accessed with
fix.lat_err_cm
,fix.lon_err_cm
andfix.alt_err_cm
, in integer centimetersfix.lat_err()
,fix.lon_err()
andfix.alt_err()
, in floating-point meters
- speed, heading and time errors, accessed with
fix.spd_err_mmps
, in integer mm/sfix.hdg_errE5
, in integer degrees * 100000fix.time_err_ns
, in integer nanoseconds
or withfix.spd_err()
in floating-point m/sfix.hdg_err()
in floating-point degreesfix.time_err()
in floating-point seconds
- geoid height above ellipsoid (see here for description), accessed with
fix.geoidHeight_cm
, in integer centimetersfix.geoidHeight()
, in floating-point metersfix.geoidHt.whole
, in integer metersfix.geoidHt.frac
, in integer centimeters to be added to the whole part
fix.satellites
, a satellite count- a date/time structure (see Time.h), accessed with
fix.dateTime.year
,fix.dateTime.month
,fix.dateTime.date
, the day-of-month,fix.dateTime.hours
,fix.dateTime.minutes
,fix.dateTime.seconds
, andfix.dateTime.day
, the day-of-the-week. This member is expensive to calculate, so it is uninitialized until you call theset_day()
method. If you need the day-of-the-week, be sure to callset_day
whenever theyear
,month
ordate
members are changed. In general, callfix.dateTime.set_day()
wheneverfix
is assigned (e.g.,fix = gps.read()
).
Time
operations allow converting to and from total seconds offset from a de facto starting time (e.g., an epoch date/time "origin"). There are constants in Time.h for NTP, POSIX and Y2K epochs. Simply change thestatic
memberss_epoch_year
ands_epoch_weekday
in Time.h, and all date/time operations will be based on that epoch. This does not affect GPS times, but it will allow you to easily convert a GPS time to/from an NTP or POSIX time value (seconds).
The NMEAtimezone.ino example program shows how to convert the GPS time (UTC) into a local time. Basically, aTime
structure is converted to seconds (from the epoch start), then the time zone offset in seconds is added, and then the offset seconds are converted back to a time structure, with corrected day, month, year, hours and minutes members.
fix.dateTime_cs
, in integer hundredths of a secondfix.dateTime_ms()
, in millisecondsfix.dateTime_us()
, in microseconds
- a collection of boolean
valid
flags for each of the above members, accessed withfix.valid.status
fix.valid.date
for year, month, day-of-monthfix.valid.time
for hours, minutes, seconds and centisecondsfix.valid.location
for latitude and longitudefix.valid.altitude
fix.valid.speed
fix.valid.heading
fix.valid.hdop
,fix.valid.vdop
andfix.valid.pdop
fix.valid.lat_err
,fix.valid.lon_err
andfix.valid.alt_err
fix.valid.geoidHeight
Validity
Because the GPS device may not have a fix, each member of a gps_fix
can be marked as valid or invalid. That is, the GPS device may not know the lat/long yet. To check whether the fix member has been received, test the corresponding valid
flag (described above). For example, to check if lat/long data has been received:
if (my_fix.valid.location) {
Serial.print( my_fix.latitude() );
Serial.print( ',' );
Serial.println( my_fix.longitude() );
}
You should also know that, even though you have enabled a particular member (see GPSfix_cfg.h), it may not have a value until the related NMEA sentence sets it. And if you have not enabled that sentence for parsing in NMEAGPS_cfg.h
, it will never be valid.
Other GPS-related information
There is additional information that is not related to a fix. Instead, it contains information about parsing or a Global Navigation Satellite System. GNSS's currently include GPS (US), GLONASS (Russia), Beidou (China) and Galileo (EU). The main NMEAGPS gps
object you declare in your sketch contains:
gps.UTCsecondStart()
, the Arduinomicros()
value when the current UTC second startedgps.UTCms()
, the number of milliseconds since the last received UTC time, calculated frommicros()
andgps.UTCsecondStart
.gps.UTCus()
, the number of microseconds since the last received UTC time, calculated frommicros()
andgps.UTCsecondStart
.gps.nmeaMessage
, the latest received message type. This is an ephemeral value, because multiple sentences are merged into onefix
structure. If you only check this after a complete fix is received, you will only see the LAST_SENTENCE_IN_INTERVAL.- enum values NMEA_GLL, NMEA_GSA, NMEA_GST, NMEA_GSV, NMEA_RMC, NMEA_VTG or NMEA_ZDA
gps.satellies[]
, an array of satellite-specific information, where each element containsgps.satellies[i].id
, satellite IDgps.satellies[i].elevation
, satellite elevation in 0-90 integer degreesgps.satellies[i].azimuth
, satellite azimuth in 0-359 integer degreesgps.satellies[i].snr
, satellite signal-to-noise ratio in 0-99 integer dBHzgps.satellies[i].tracked
, satellite being tracked flag, a boolean
gps.sat_count
, the number of elements in thegps.satellites[]
arraygps.talker_id[]
, talker ID, a two-character array (not NUL-terminated)gps.mfr_id[]
, manufacturer ID, a three-character array (not NUL-terminated)- an internal fix structure,
gps.fix()
. Most sketches should not usegps.fix()
directly!
Usage
First, declare an instance of NMEAGPS
:
NMEAGPS gps;
Next, tell the gps
object to handle any available characters on the serial port:
void loop()
{
while (gps.available( gps_port )) {
The gps
object will check if there are any characters available, and if so, read them from the port and parse them into its internal fix. Many characters will have to be read before the current fix is complete, so gps.available
will return false
until the fix is complete; the body of while
loop will be skipped many times, and the rest of loop()
will be executed.
When a fix is finally completed, gps.available
will return true
. Now your sketch can "read" the completed fix structure from the gps
object:
void loop()
{
while (gps.available( gps_port )) {
gps_fix fix = gps.read();
The local fix
variable now contains all the GPS fields that were parsed from the gps_port
. You can access them as described above:
void loop()
{
while (gps.available( gps_port )) {
gps_fix fix = gps.read();
if (fix.valid.time) {
...
Note that the fix
variable is local to that while
loop; it cannot be accessed elsewhere in your sketch. If you need to access the fix information elsewhere, you must declare a global fix variable:
gps_fix currentFix;
void loop()
{
while (gps.available( gps_port )) {
currentFix = gps.read();
if (currentFix.valid.time) {
...
Any part of your sketch can use the information in currentFix
.
Please note that the fix structure is much smaller than the raw character data (sentences). A fix is nominally 1/4 the size of one sentence (~30 bytes vs ~120 bytes). If two sentences are sent during each update interval, a fix could be 1/8 the size required for buffering two sentences.
In this fix-oriented program structure, the methods gps.available
and gps.read
are manipulating entire gps_fix
structures. Multiple characters and sentences are used internally to fill out a single fix: members are "merged" from sentences into one fix structure (described here).
That program structure is very similar to the typical serial port reading loop:
void loop()
{
while (serial.available()) {
char c = serial.read();
... do something with the character ...;
}
However, the fix-oriented methods operate on complete fixes, not individual characters, fields or sentences.
Note: If you find that you need to filter or merge data with a finer level of control, you may need to use a different Merging option, Coherency, or the more-advanced Character-Oriented methods.
Examples
Some examples of accessing fix values:
gps_fix fix_copy = gps.read();
int32_t lat_10e7 = fix_copy.lat; // scaled integer value of latitude
float lat = fix_copy.latitude(); // float value of latitude
Serial.print( fix_copy.latDMS.degrees );
Serial.print( ' ' );
Serial.print( fix_copy.latDMS.minutes );
Serial.print( F("' " );
Serial.print( fix_copy.latDMS.seconds );
if (fix_copy.dateTime.month == 4) // test for the cruelest month
cry();
// Count how satellites are being received for each GNSS
for (uint8_t i=0; i < gps.sat_count; i++) {
if (gps.satellites[i].tracked) {
if (gps.satellites[i] . id <= 32)
GPS_satellites++;
if (gps.satellites[i] . id <= 64)
SBAS_satellites++;
if (gps.satellites[i] . id <= 96)
GLONASS_satellites++;
}
}
And some examples of accessing valid flags in a fix
structure:
if (fix_copy.valid.location)
// we have a lat/long!
if (fix_copy.valid.time)
// the copy has hours, minutes and seconds
Here's an example for accessing the altitude
if (fix_copy.valid.altitude) {
z2 = fix_copy.altitude_cm();
vz = (z2 - z1) / dt;
z1 = z2;
// Note: if you only care about meters, you could also do this:
// z = fix_copy.alt.whole;
}
You can also check a collection of flags before performing a calculation involving multiple members:
if (fix_copy.valid.altitude && fix_copy.valid.date && fix_copy.valid.time) {
dt = (clock_t) fix_copy.dateTime - (clock_t) fix_copy.dateTime;
dz = fix_copy.alt.whole - last_alt; // meters
vz = dz / dt; // meters per second vertical velocity
}
Bonus: The compiler will optimize this into a single bit mask operation.
The example printing utility file, Streamers.cpp shows how to access each fix member and print its value.
Options
Except for status
, each of these gps_fix
members is conditionally compiled; any, all, or no members can be selected for parsing, storing and merging. This allows you to configuring NeoGPS to use the minimum amount of RAM for the particular members of interest. See Configurations for how to edit GPSfix_cfg.h and NMEAGPS_cfg.h, respectively.
Precision
Integers are used for all members, retaining full precision of the original data.
gps_fix fix = gps.read();
if (fix.valid.location) {
// 32-bit ints have 10 significant digits, so you can detect very
// small changes in position:
d_lat = fix_copy.lat - last_lat;
}
Optional floating-point accessors are provided for many members.
if (fix_copy.valid.location) {
float lat = fix_copy.latitude();
// floats only have about 6 significant digits, so this
// computation is useless for detecting small movements:
foobar = (lat - target_lat);