Skip to content

Added support for GPGLL, GPVTG, GPGSA, and GPGSV NMEA sentences #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
May 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
213c36b
Added support for GPGSV. Haven't tested yet
evaherrada May 9, 2019
a4f0525
Modified version of simpletest to allow me to test different formats
evaherrada May 9, 2019
ec5ddbe
Have to commit this before I switch branch
evaherrada May 9, 2019
e2d8b83
Commented out some code that made self.has_fix a variable
evaherrada May 9, 2019
358d7d5
Removed tester.py
evaherrada May 9, 2019
e52d879
Added all functions. They still need testing
evaherrada May 10, 2019
91e1ddf
Merge pull request #3 from adafruit/master
evaherrada May 10, 2019
975fdd8
Merge pull request #4 from dherrada/master
evaherrada May 10, 2019
f5376ae
Pretty much where I was last commit lol
evaherrada May 11, 2019
9d4beb8
Added function to parse GPGLL data. It FINALLY works
evaherrada May 11, 2019
b5c5752
Reverted simpletest
evaherrada May 11, 2019
5e26243
Added explanation for commenting out block that checked if in_waiting…
evaherrada May 11, 2019
74c50a5
Reverted .gitignore to earlier version
evaherrada May 11, 2019
77325a6
Changed delineator to delimiter
evaherrada May 11, 2019
a6fd9c2
Fixed _parse_gpgsa
evaherrada May 13, 2019
9cd3257
self.sats is now defined in __init__
evaherrada May 13, 2019
c67fb4c
Finished all functions for parsing sentences. It finally works
evaherrada May 14, 2019
50161af
Made (mostly) pep8 compliant
evaherrada May 14, 2019
724eec8
Removed redundant code
evaherrada May 14, 2019
882646d
Fixed error where list of GPSs could not decrease in size, also remov…
evaherrada May 14, 2019
41ca55e
Trying to get Travis to pass
evaherrada May 14, 2019
aa27861
Removed trailing white-space
evaherrada May 14, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ bundles
*.DS_Store
.eggs
dist
**/*.egg-info
**/*.egg-info
220 changes: 188 additions & 32 deletions adafruit_gps.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
# Internal helper parsing functions.
# These handle input that might be none or null and return none instead of
# throwing errors.


def _parse_degrees(nmea_data):
# Parse a NMEA lat/long data pair 'dddmm.mmmm' into a pure degrees value.
# Where ddd is the degrees, mm.mmmm is the minutes.
Expand All @@ -60,21 +62,31 @@ def _parse_degrees(nmea_data):
minutes = raw % 100
return deg + minutes/60


def _parse_int(nmea_data):
if nmea_data is None or nmea_data == '':
return None
return int(nmea_data)


def _parse_float(nmea_data):
if nmea_data is None or nmea_data == '':
return None
return float(nmea_data)


def _parse_str(nmea_data):
if nmea_data is None or nmea_data == '':
return None
return str(nmea_data)

# lint warning about too many attributes disabled
#pylint: disable-msg=R0902


class GPS:
"""GPS parsing module. Can parse simple NMEA data sentences from serial GPS
modules to read latitude, longitude, and more.
"""GPS parsing module. Can parse simple NMEA data sentences from serial
GPS modules to read latitude, longitude, and more.
"""
def __init__(self, uart, debug=False):
self._uart = uart
Expand All @@ -83,13 +95,26 @@ def __init__(self, uart, debug=False):
self.latitude = None
self.longitude = None
self.fix_quality = None
self.fix_quality_3d = None
self.satellites = None
self.satellites_prev = None
self.horizontal_dilution = None
self.altitude_m = None
self.height_geoid = None
self.velocity_knots = None
self.speed_knots = None
self.speed_kmh = None
self.track_angle_deg = None
self.total_mess_num = None
self.mess_num = None
self.sats = None
self.isactivedata = None
self.true_track = None
self.mag_track = None
self.sat_prns = None
self.sel_mode = None
self.pdop = None
self.hdop = None
self.vdop = None
self.debug = debug

def update(self):
Expand All @@ -109,10 +134,19 @@ def update(self):
print(sentence)
data_type, args = sentence
data_type = bytes(data_type.upper(), "ascii")
if data_type == b'GPGGA': # GGA, 3d location fix
self._parse_gpgga(args)
elif data_type == b'GPRMC': # RMC, minimum location info
# return sentence
if data_type == b'GPGLL': # GLL, Geographic Position – Latitude/Longitude
self._parse_gpgll(args)
elif data_type == b'GPRMC': # RMC, minimum location info
self._parse_gprmc(args)
elif data_type == b'GPVTG': # VTG, Track Made Good and Ground Speed
self._parse_gpvtg(args)
elif data_type == b'GPGGA': # GGA, 3d location fix
self._parse_gpgga(args)
elif data_type == b'GPGSA': # GSA, GPS DOP and active satellites
self._parse_gpgsa(args)
elif data_type == b'GPGSV': # GSV, Satellites in view
self._parse_gpgsv(args)
return True

def send_command(self, command, add_checksum=True):
Expand All @@ -136,6 +170,13 @@ def has_fix(self):
"""True if a current fix for location information is available."""
return self.fix_quality is not None and self.fix_quality >= 1

@property
def has_3d_fix(self):
"""Returns true if there is a 3d fix available.
use has_fix to determine if a 2d fix is available,
passing it the same data"""
return self.fix_quality_3d is not None and self.fix_quality_3d >= 2

@property
def datetime(self):
"""Return struct_time object to feed rtc.set_time_source() function"""
Expand All @@ -147,8 +188,11 @@ def _parse_sentence(self):
# This needs to be refactored when it can be tested.

# Only continue if we have at least 64 bytes in the input buffer
# I've commented this out for now as not all sentences are 64 bytes long
"""
if self._uart.in_waiting < 64:
return None
"""

sentence = self._uart.readline()
if sentence is None or sentence == b'' or len(sentence) < 1:
Expand All @@ -170,20 +214,27 @@ def _parse_sentence(self):
sentence = sentence[:-3]
# Parse out the type of sentence (first string after $ up to comma)
# and then grab the rest as data within the sentence.
delineator = sentence.find(',')
if delineator == -1:
delimiter = sentence.find(',')
if delimiter == -1:
return None # Invalid sentence, no comma after data type.
data_type = sentence[1:delineator]
return (data_type, sentence[delineator+1:])
data_type = sentence[1:delimiter]
return (data_type, sentence[delimiter+1:])

def _parse_gpgga(self, args):
# Parse the arguments (everything after data type) for NMEA GPGGA
# 3D location fix sentence.
def _parse_gpgll(self, args):
data = args.split(',')
if data is None or len(data) != 14:
if data is None or data[0] is None:
return # Unexpected number of params.
# Parse fix time.
time_utc = int(_parse_float(data[0]))

# Parse latitude and longitude.
self.latitude = _parse_degrees(data[0])
if self.latitude is not None and \
data[1] is not None and data[1].lower() == 's':
self.latitude *= -1.0
self.longitude = _parse_degrees(data[2])
if self.longitude is not None and \
data[3] is not None and data[3].lower() == 'w':
self.longitude *= -1.0
time_utc = int(_parse_int(float(data[4])))
if time_utc is not None:
hours = time_utc // 10000
mins = (time_utc // 100) % 100
Expand All @@ -196,21 +247,8 @@ def _parse_gpgga(self, args):
else:
self.timestamp_utc = time.struct_time((0, 0, 0, hours, mins,
secs, 0, 0, -1))
# Parse latitude and longitude.
self.latitude = _parse_degrees(data[1])
if self.latitude is not None and \
data[2] is not None and data[2].lower() == 's':
self.latitude *= -1.0
self.longitude = _parse_degrees(data[3])
if self.longitude is not None and \
data[4] is not None and data[4].lower() == 'w':
self.longitude *= -1.0
# Parse out fix quality and other simple numeric values.
self.fix_quality = _parse_int(data[5])
self.satellites = _parse_int(data[6])
self.horizontal_dilution = _parse_float(data[7])
self.altitude_m = _parse_float(data[8])
self.height_geoid = _parse_float(data[10])
# Parse data active or void
self.isactivedata = _parse_str(data[5])

def _parse_gprmc(self, args):
# Parse the arguments (everything after data type) for NMEA GPRMC
Expand Down Expand Up @@ -253,7 +291,7 @@ def _parse_gprmc(self, args):
if data[8] is not None and len(data[8]) == 6:
day = int(data[8][0:2])
month = int(data[8][2:4])
year = 2000 + int(data[8][4:6]) # Y2k bug, 2 digit date assumption.
year = 2000 + int(data[8][4:6]) # Y2k bug, 2 digit year assumption.
# This is a problem with the NMEA
# spec and not this code.
if self.timestamp_utc is not None:
Expand All @@ -270,3 +308,121 @@ def _parse_gprmc(self, args):
# Time hasn't been set so create it.
self.timestamp_utc = time.struct_time((year, month, day, 0, 0,
0, 0, 0, -1))

def _parse_gpvtg(self, args):
data = args.split(',')

# Parse true track made good (degrees)
self.true_track = _parse_float(data[0])

# Parse magnetic track made good
self.mag_track = _parse_float(data[2])

# Parse speed
self.speed_knots = _parse_float(data[4])
self.speed_kmh = _parse_float(data[6])

def _parse_gpgga(self, args):
# Parse the arguments (everything after data type) for NMEA GPGGA
# 3D location fix sentence.
data = args.split(',')
if data is None or len(data) != 14:
return # Unexpected number of params.
# Parse fix time.
time_utc = int(_parse_float(data[0]))
if time_utc is not None:
hours = time_utc // 10000
mins = (time_utc // 100) % 100
secs = time_utc % 100
# Set or update time to a friendly python time struct.
if self.timestamp_utc is not None:
self.timestamp_utc = time.struct_time((
self.timestamp_utc.tm_year, self.timestamp_utc.tm_mon,
self.timestamp_utc.tm_mday, hours, mins, secs, 0, 0, -1))
else:
self.timestamp_utc = time.struct_time((0, 0, 0, hours, mins,
secs, 0, 0, -1))
# Parse latitude and longitude.
self.latitude = _parse_degrees(data[1])
if self.latitude is not None and \
data[2] is not None and data[2].lower() == 's':
self.latitude *= -1.0
self.longitude = _parse_degrees(data[3])
if self.longitude is not None and \
data[4] is not None and data[4].lower() == 'w':
self.longitude *= -1.0
# Parse out fix quality and other simple numeric values.
self.fix_quality = _parse_int(data[5])
self.satellites = _parse_int(data[6])
self.horizontal_dilution = _parse_float(data[7])
self.altitude_m = _parse_float(data[8])
self.height_geoid = _parse_float(data[10])

def _parse_gpgsa(self, args):
data = args.split(',')
if data is None:
return # Unexpected number of params

# Parse selection mode
self.sel_mode = _parse_str(data[0])
# Parse 3d fix
self.fix_quality_3d = _parse_int(data[1])
satlist = list(filter(None, data[2:-4]))
self.sat_prns = {}
for i, sat in enumerate(satlist, 1):
self.sat_prns["gps{}".format(i)] = _parse_int(sat)

# Parse PDOP, dilution of precision
self.pdop = _parse_float(data[-3])
# Parse HDOP, horizontal dilution of precision
self.hdop = _parse_float(data[-2])
# Parse VDOP, vertical dilution of precision
self.vdop = _parse_float(data[-1])

def _parse_gpgsv(self, args):
# Parse the arguments (everything after data type) for NMEA GPGGA
# 3D location fix sentence.
data = args.split(',')
if data is None:
return # Unexpected number of params.

# Parse number of messages
self.total_mess_num = _parse_int(data[0]) # Total number of messages
# Parse message number
self.mess_num = _parse_int(data[1]) # Message number
# Parse number of satellites in view
self.satellites = _parse_int(data[2]) # Number of satellites

if len(data) < 3:
return

sat_tup = data[3:]

satdict = {}
for i in range(len(sat_tup)/4):
j = i*4
key = "gps{}".format(i+(4*(self.mess_num-1)))
satnum = _parse_int(sat_tup[0+j]) # Satellite number
satdeg = _parse_int(sat_tup[1+j]) # Elevation in degrees
satazim = _parse_int(sat_tup[2+j]) # Azimuth in degrees
satsnr = _parse_int(sat_tup[3+j]) # signal-to-noise ratio in dB
value = (satnum, satdeg, satazim, satsnr)
satdict[key] = value

if self.sats is None:
self.sats = {}
for satnum in satdict:
self.sats[satnum] = satdict[satnum]

try:
if self.satellites < self.satellites_prev:
for i in self.sats:
try:
if int(i[-2]) >= self.satellites:
del self.sats[i]
except ValueError:
if int(i[-1]) >= self.satellites:
del self.sats[i]
except TypeError:
pass
self.satellites_prev = self.satellites