50
50
# Internal helper parsing functions.
51
51
# These handle input that might be none or null and return none instead of
52
52
# throwing errors.
53
+
54
+
53
55
def _parse_degrees (nmea_data ):
54
56
# Parse a NMEA lat/long data pair 'dddmm.mmmm' into a pure degrees value.
55
57
# Where ddd is the degrees, mm.mmmm is the minutes.
@@ -60,21 +62,31 @@ def _parse_degrees(nmea_data):
60
62
minutes = raw % 100
61
63
return deg + minutes / 60
62
64
65
+
63
66
def _parse_int (nmea_data ):
64
67
if nmea_data is None or nmea_data == '' :
65
68
return None
66
69
return int (nmea_data )
67
70
71
+
68
72
def _parse_float (nmea_data ):
69
73
if nmea_data is None or nmea_data == '' :
70
74
return None
71
75
return float (nmea_data )
72
76
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
+
73
83
# lint warning about too many attributes disabled
74
84
#pylint: disable-msg=R0902
85
+
86
+
75
87
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.
78
90
"""
79
91
def __init__ (self , uart , debug = False ):
80
92
self ._uart = uart
@@ -83,13 +95,26 @@ def __init__(self, uart, debug=False):
83
95
self .latitude = None
84
96
self .longitude = None
85
97
self .fix_quality = None
98
+ self .fix_quality_3d = None
86
99
self .satellites = None
100
+ self .satellites_prev = None
87
101
self .horizontal_dilution = None
88
102
self .altitude_m = None
89
103
self .height_geoid = None
90
- self .velocity_knots = None
91
104
self .speed_knots = None
105
+ self .speed_kmh = None
92
106
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
93
118
self .debug = debug
94
119
95
120
def update (self ):
@@ -109,10 +134,19 @@ def update(self):
109
134
print (sentence )
110
135
data_type , args = sentence
111
136
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
115
141
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 )
116
150
return True
117
151
118
152
def send_command (self , command , add_checksum = True ):
@@ -136,6 +170,13 @@ def has_fix(self):
136
170
"""True if a current fix for location information is available."""
137
171
return self .fix_quality is not None and self .fix_quality >= 1
138
172
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
+
139
180
@property
140
181
def datetime (self ):
141
182
"""Return struct_time object to feed rtc.set_time_source() function"""
@@ -147,8 +188,11 @@ def _parse_sentence(self):
147
188
# This needs to be refactored when it can be tested.
148
189
149
190
# 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
+ """
150
193
if self._uart.in_waiting < 64:
151
194
return None
195
+ """
152
196
153
197
sentence = self ._uart .readline ()
154
198
if sentence is None or sentence == b'' or len (sentence ) < 1 :
@@ -170,20 +214,27 @@ def _parse_sentence(self):
170
214
sentence = sentence [:- 3 ]
171
215
# Parse out the type of sentence (first string after $ up to comma)
172
216
# 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 :
175
219
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 :])
178
222
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 ):
182
224
data = args .split (',' )
183
- if data is None or len ( data ) != 14 :
225
+ if data is None or data [ 0 ] is None :
184
226
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 ])))
187
238
if time_utc is not None :
188
239
hours = time_utc // 10000
189
240
mins = (time_utc // 100 ) % 100
@@ -196,21 +247,8 @@ def _parse_gpgga(self, args):
196
247
else :
197
248
self .timestamp_utc = time .struct_time ((0 , 0 , 0 , hours , mins ,
198
249
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 ])
214
252
215
253
def _parse_gprmc (self , args ):
216
254
# Parse the arguments (everything after data type) for NMEA GPRMC
@@ -253,7 +291,7 @@ def _parse_gprmc(self, args):
253
291
if data [8 ] is not None and len (data [8 ]) == 6 :
254
292
day = int (data [8 ][0 :2 ])
255
293
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.
257
295
# This is a problem with the NMEA
258
296
# spec and not this code.
259
297
if self .timestamp_utc is not None :
@@ -270,3 +308,121 @@ def _parse_gprmc(self, args):
270
308
# Time hasn't been set so create it.
271
309
self .timestamp_utc = time .struct_time ((year , month , day , 0 , 0 ,
272
310
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