Skip to content

Commit 1960c04

Browse files
authored
Merge pull request #27 from dherrada/format-adding
Added support for GPGLL, GPVTG, GPGSA, and GPGSV NMEA sentences
2 parents 33bd331 + aa27861 commit 1960c04

File tree

2 files changed

+189
-33
lines changed

2 files changed

+189
-33
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ bundles
99
*.DS_Store
1010
.eggs
1111
dist
12-
**/*.egg-info
12+
**/*.egg-info

adafruit_gps.py

Lines changed: 188 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
# Internal helper parsing functions.
5151
# These handle input that might be none or null and return none instead of
5252
# throwing errors.
53+
54+
5355
def _parse_degrees(nmea_data):
5456
# Parse a NMEA lat/long data pair 'dddmm.mmmm' into a pure degrees value.
5557
# Where ddd is the degrees, mm.mmmm is the minutes.
@@ -60,21 +62,31 @@ def _parse_degrees(nmea_data):
6062
minutes = raw % 100
6163
return deg + minutes/60
6264

65+
6366
def _parse_int(nmea_data):
6467
if nmea_data is None or nmea_data == '':
6568
return None
6669
return int(nmea_data)
6770

71+
6872
def _parse_float(nmea_data):
6973
if nmea_data is None or nmea_data == '':
7074
return None
7175
return float(nmea_data)
7276

77+
78+
def _parse_str(nmea_data):
79+
if nmea_data is None or nmea_data == '':
80+
return None
81+
return str(nmea_data)
82+
7383
# lint warning about too many attributes disabled
7484
#pylint: disable-msg=R0902
85+
86+
7587
class GPS:
76-
"""GPS parsing module. Can parse simple NMEA data sentences from serial GPS
77-
modules to read latitude, longitude, and more.
88+
"""GPS parsing module. Can parse simple NMEA data sentences from serial
89+
GPS modules to read latitude, longitude, and more.
7890
"""
7991
def __init__(self, uart, debug=False):
8092
self._uart = uart
@@ -83,13 +95,26 @@ def __init__(self, uart, debug=False):
8395
self.latitude = None
8496
self.longitude = None
8597
self.fix_quality = None
98+
self.fix_quality_3d = None
8699
self.satellites = None
100+
self.satellites_prev = None
87101
self.horizontal_dilution = None
88102
self.altitude_m = None
89103
self.height_geoid = None
90-
self.velocity_knots = None
91104
self.speed_knots = None
105+
self.speed_kmh = None
92106
self.track_angle_deg = None
107+
self.total_mess_num = None
108+
self.mess_num = None
109+
self.sats = None
110+
self.isactivedata = None
111+
self.true_track = None
112+
self.mag_track = None
113+
self.sat_prns = None
114+
self.sel_mode = None
115+
self.pdop = None
116+
self.hdop = None
117+
self.vdop = None
93118
self.debug = debug
94119

95120
def update(self):
@@ -109,10 +134,19 @@ def update(self):
109134
print(sentence)
110135
data_type, args = sentence
111136
data_type = bytes(data_type.upper(), "ascii")
112-
if data_type == b'GPGGA': # GGA, 3d location fix
113-
self._parse_gpgga(args)
114-
elif data_type == b'GPRMC': # RMC, minimum location info
137+
# return sentence
138+
if data_type == b'GPGLL': # GLL, Geographic Position – Latitude/Longitude
139+
self._parse_gpgll(args)
140+
elif data_type == b'GPRMC': # RMC, minimum location info
115141
self._parse_gprmc(args)
142+
elif data_type == b'GPVTG': # VTG, Track Made Good and Ground Speed
143+
self._parse_gpvtg(args)
144+
elif data_type == b'GPGGA': # GGA, 3d location fix
145+
self._parse_gpgga(args)
146+
elif data_type == b'GPGSA': # GSA, GPS DOP and active satellites
147+
self._parse_gpgsa(args)
148+
elif data_type == b'GPGSV': # GSV, Satellites in view
149+
self._parse_gpgsv(args)
116150
return True
117151

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

173+
@property
174+
def has_3d_fix(self):
175+
"""Returns true if there is a 3d fix available.
176+
use has_fix to determine if a 2d fix is available,
177+
passing it the same data"""
178+
return self.fix_quality_3d is not None and self.fix_quality_3d >= 2
179+
139180
@property
140181
def datetime(self):
141182
"""Return struct_time object to feed rtc.set_time_source() function"""
@@ -147,8 +188,11 @@ def _parse_sentence(self):
147188
# This needs to be refactored when it can be tested.
148189

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

153197
sentence = self._uart.readline()
154198
if sentence is None or sentence == b'' or len(sentence) < 1:
@@ -170,20 +214,27 @@ def _parse_sentence(self):
170214
sentence = sentence[:-3]
171215
# Parse out the type of sentence (first string after $ up to comma)
172216
# and then grab the rest as data within the sentence.
173-
delineator = sentence.find(',')
174-
if delineator == -1:
217+
delimiter = sentence.find(',')
218+
if delimiter == -1:
175219
return None # Invalid sentence, no comma after data type.
176-
data_type = sentence[1:delineator]
177-
return (data_type, sentence[delineator+1:])
220+
data_type = sentence[1:delimiter]
221+
return (data_type, sentence[delimiter+1:])
178222

179-
def _parse_gpgga(self, args):
180-
# Parse the arguments (everything after data type) for NMEA GPGGA
181-
# 3D location fix sentence.
223+
def _parse_gpgll(self, args):
182224
data = args.split(',')
183-
if data is None or len(data) != 14:
225+
if data is None or data[0] is None:
184226
return # Unexpected number of params.
185-
# Parse fix time.
186-
time_utc = int(_parse_float(data[0]))
227+
228+
# Parse latitude and longitude.
229+
self.latitude = _parse_degrees(data[0])
230+
if self.latitude is not None and \
231+
data[1] is not None and data[1].lower() == 's':
232+
self.latitude *= -1.0
233+
self.longitude = _parse_degrees(data[2])
234+
if self.longitude is not None and \
235+
data[3] is not None and data[3].lower() == 'w':
236+
self.longitude *= -1.0
237+
time_utc = int(_parse_int(float(data[4])))
187238
if time_utc is not None:
188239
hours = time_utc // 10000
189240
mins = (time_utc // 100) % 100
@@ -196,21 +247,8 @@ def _parse_gpgga(self, args):
196247
else:
197248
self.timestamp_utc = time.struct_time((0, 0, 0, hours, mins,
198249
secs, 0, 0, -1))
199-
# Parse latitude and longitude.
200-
self.latitude = _parse_degrees(data[1])
201-
if self.latitude is not None and \
202-
data[2] is not None and data[2].lower() == 's':
203-
self.latitude *= -1.0
204-
self.longitude = _parse_degrees(data[3])
205-
if self.longitude is not None and \
206-
data[4] is not None and data[4].lower() == 'w':
207-
self.longitude *= -1.0
208-
# Parse out fix quality and other simple numeric values.
209-
self.fix_quality = _parse_int(data[5])
210-
self.satellites = _parse_int(data[6])
211-
self.horizontal_dilution = _parse_float(data[7])
212-
self.altitude_m = _parse_float(data[8])
213-
self.height_geoid = _parse_float(data[10])
250+
# Parse data active or void
251+
self.isactivedata = _parse_str(data[5])
214252

215253
def _parse_gprmc(self, args):
216254
# Parse the arguments (everything after data type) for NMEA GPRMC
@@ -253,7 +291,7 @@ def _parse_gprmc(self, args):
253291
if data[8] is not None and len(data[8]) == 6:
254292
day = int(data[8][0:2])
255293
month = int(data[8][2:4])
256-
year = 2000 + int(data[8][4:6]) # Y2k bug, 2 digit date assumption.
294+
year = 2000 + int(data[8][4:6]) # Y2k bug, 2 digit year assumption.
257295
# This is a problem with the NMEA
258296
# spec and not this code.
259297
if self.timestamp_utc is not None:
@@ -270,3 +308,121 @@ def _parse_gprmc(self, args):
270308
# Time hasn't been set so create it.
271309
self.timestamp_utc = time.struct_time((year, month, day, 0, 0,
272310
0, 0, 0, -1))
311+
312+
def _parse_gpvtg(self, args):
313+
data = args.split(',')
314+
315+
# Parse true track made good (degrees)
316+
self.true_track = _parse_float(data[0])
317+
318+
# Parse magnetic track made good
319+
self.mag_track = _parse_float(data[2])
320+
321+
# Parse speed
322+
self.speed_knots = _parse_float(data[4])
323+
self.speed_kmh = _parse_float(data[6])
324+
325+
def _parse_gpgga(self, args):
326+
# Parse the arguments (everything after data type) for NMEA GPGGA
327+
# 3D location fix sentence.
328+
data = args.split(',')
329+
if data is None or len(data) != 14:
330+
return # Unexpected number of params.
331+
# Parse fix time.
332+
time_utc = int(_parse_float(data[0]))
333+
if time_utc is not None:
334+
hours = time_utc // 10000
335+
mins = (time_utc // 100) % 100
336+
secs = time_utc % 100
337+
# Set or update time to a friendly python time struct.
338+
if self.timestamp_utc is not None:
339+
self.timestamp_utc = time.struct_time((
340+
self.timestamp_utc.tm_year, self.timestamp_utc.tm_mon,
341+
self.timestamp_utc.tm_mday, hours, mins, secs, 0, 0, -1))
342+
else:
343+
self.timestamp_utc = time.struct_time((0, 0, 0, hours, mins,
344+
secs, 0, 0, -1))
345+
# Parse latitude and longitude.
346+
self.latitude = _parse_degrees(data[1])
347+
if self.latitude is not None and \
348+
data[2] is not None and data[2].lower() == 's':
349+
self.latitude *= -1.0
350+
self.longitude = _parse_degrees(data[3])
351+
if self.longitude is not None and \
352+
data[4] is not None and data[4].lower() == 'w':
353+
self.longitude *= -1.0
354+
# Parse out fix quality and other simple numeric values.
355+
self.fix_quality = _parse_int(data[5])
356+
self.satellites = _parse_int(data[6])
357+
self.horizontal_dilution = _parse_float(data[7])
358+
self.altitude_m = _parse_float(data[8])
359+
self.height_geoid = _parse_float(data[10])
360+
361+
def _parse_gpgsa(self, args):
362+
data = args.split(',')
363+
if data is None:
364+
return # Unexpected number of params
365+
366+
# Parse selection mode
367+
self.sel_mode = _parse_str(data[0])
368+
# Parse 3d fix
369+
self.fix_quality_3d = _parse_int(data[1])
370+
satlist = list(filter(None, data[2:-4]))
371+
self.sat_prns = {}
372+
for i, sat in enumerate(satlist, 1):
373+
self.sat_prns["gps{}".format(i)] = _parse_int(sat)
374+
375+
# Parse PDOP, dilution of precision
376+
self.pdop = _parse_float(data[-3])
377+
# Parse HDOP, horizontal dilution of precision
378+
self.hdop = _parse_float(data[-2])
379+
# Parse VDOP, vertical dilution of precision
380+
self.vdop = _parse_float(data[-1])
381+
382+
def _parse_gpgsv(self, args):
383+
# Parse the arguments (everything after data type) for NMEA GPGGA
384+
# 3D location fix sentence.
385+
data = args.split(',')
386+
if data is None:
387+
return # Unexpected number of params.
388+
389+
# Parse number of messages
390+
self.total_mess_num = _parse_int(data[0]) # Total number of messages
391+
# Parse message number
392+
self.mess_num = _parse_int(data[1]) # Message number
393+
# Parse number of satellites in view
394+
self.satellites = _parse_int(data[2]) # Number of satellites
395+
396+
if len(data) < 3:
397+
return
398+
399+
sat_tup = data[3:]
400+
401+
satdict = {}
402+
for i in range(len(sat_tup)/4):
403+
j = i*4
404+
key = "gps{}".format(i+(4*(self.mess_num-1)))
405+
satnum = _parse_int(sat_tup[0+j]) # Satellite number
406+
satdeg = _parse_int(sat_tup[1+j]) # Elevation in degrees
407+
satazim = _parse_int(sat_tup[2+j]) # Azimuth in degrees
408+
satsnr = _parse_int(sat_tup[3+j]) # signal-to-noise ratio in dB
409+
value = (satnum, satdeg, satazim, satsnr)
410+
satdict[key] = value
411+
412+
if self.sats is None:
413+
self.sats = {}
414+
for satnum in satdict:
415+
self.sats[satnum] = satdict[satnum]
416+
417+
try:
418+
if self.satellites < self.satellites_prev:
419+
for i in self.sats:
420+
try:
421+
if int(i[-2]) >= self.satellites:
422+
del self.sats[i]
423+
except ValueError:
424+
if int(i[-1]) >= self.satellites:
425+
del self.sats[i]
426+
except TypeError:
427+
pass
428+
self.satellites_prev = self.satellites

0 commit comments

Comments
 (0)