1
2
3 __version__ = '$Revision: 4799 $'.split()[1]
4 __date__ = '$Date: 2008-11-20 11:09:02 -0400 (Mon, 25 Sep 2006) $'.split()[1]
5 __author__ = 'Val Schmidt'
6 __doc__ = '''
7 GPSparser, a Python GPS NMEA String parsing module.
8
9 This module provides GPS NMEA string parsing through the GPSString
10 class. A GPSstring is an object whose I{msg} is any ASCII text string
11 containing a NMEA GPS string somewhere within it. NMEA strings
12 characteristicly start with the leading "$" and end with the *hh where
13 hh is the two character checksum. All of the following examples are fine:
14
15 - C{$GPGGA,154809.00,4305.52462642,N,07051.89568468,W,1,3,4.1,48.971,M,-32.985,M,,*54}
16 - C{RTK1_GPS DATA 2008-11-21T15:48:10.510017 $GPGGA,154809.00,4305.52462642,N,07051.89568468,W,1,3,4.1,48.971,M,-32.985,M,,*5}
17 - C{09/12/2003,04:01:46.666,$GPGGA,040145.871,7117.3458,N,15700.3788,W,1,06,1.4,014.6,M,-000.4,M,,*50}
18 - C{posnav 2008:226:18:31:34.1365 $INGGA,183133.898,7120.91996,N,15651.72629,W,1,11,0.8,-1.06,M,,,,*19}
19
20 GPSparser provides methods for extracting the NMEA string from the
21 larger set of ASCII characters, verifying its checksum and parsing the
22 string's fields. The fields may then be accessed as attributes of GPSString
23 (i.e. C{GPSString.latitude}). The complete set of attributes available
24 after a string has been parsed can be found in the attribute list
25 C{GPSString.fieldnames}. Some fields are not returned, such as those that
26 provide units which, by standard or convention, never seem to change.
27
28 GPSparser is designed for follow-on processing and plotting of values,
29 and therefore every effort is made to convert all fields to
30 meaningful, numeric values (objects of the Decimal Class). For example, Latitude and
31 Longitude are converted to decimal degrees to 10 digits past the
32 decimal point. Similarly, GPS fix type in RMC strings are indicated by an "A"
33 when the GPS has a fix and a "V" when it does not. These values are
34 reported by GPSParser as 1 and 0, respectively.
35
36 Time in GPS strings are converted to datetime objects (see the
37 datetime module). Many NMEA strings contain time-of-day, but not date
38 (GGA for example). GPSparser will, by default, convert these strings
39 to datetime.time objects. However if the GPSString.date attribute is
40 set with a datetime.date object prior to calling the parse() method, a
41 full datetime.datetime object will be returned combining the supplied
42 date and time parsed from the string. This is a handy way to produce
43 full date-time stamps for each string.
44
45 A common thing to overlook, however, when parsing a file of strings
46 from a parent script, in which the times rotate over a UTC day, one
47 must be sure to also rotate the value used to set the GPSString.date
48 attribute.
49
50 When the module is called as a script, it will parse a file for a
51 single string type, which must be specified on the command-line (see
52 C{gpsparser.py -h}). The fields are written in tab-delimited format to
53 standard-out. Date-time stamps are written as tab-delimited vectors
54 (C{YYYY MM DD HH MM SS}). This format makes reading parsed data files
55 into Octave or MATLAB trivial ( C{load('datafile')} ), with the notable
56 exception of GSV strings which have variable numbers of fields
57 depending on the number of satellites tracked.
58
59 @author: U{'''+__author__+'''<http://aliceandval.com/valswork>}
60 @organization: Center for Coastal and Ocean Mapping, University of New Hampshire
61 @version: ''' + __version__ +'''
62 @copyright: 2008
63 @status: under development
64 @license: GPL
65
66 @todo: Unit tests. There are none so far and there needs to be.
67
68 '''
69
70 import os
71 import sys
72 import datetime
73 import re
74 import string
75 import decimal as dec
76 import pdb
77 from operator import xor
78 import exceptions
79
81 '''
82 A GPSString is any string that contains a complete NMEA string someplace
83 within it. The string must start with the leading $ and end with the *hh
84 where hh is the checksum.
85 '''
86 GPS_IDs = { \
87 'GGA' : 1, \
88 'ZDA' : 2, \
89 'RMC' : 3, \
90 'GST' : 4, \
91 'GSV' : 5, \
92 'VTG' : 6, \
93 'HDT' : 7 ,\
94 'PASHR' : 8}
95
96
98 '''
99 Initializes the class with any string containing a single NMEA data string.
100
101 @param msg: The ASCII string containing the NMEA data string.
102
103 '''
104 self.msg = msg
105 'The message containing the gps string.'
106 self.debug = False
107 'A flag for writing debugging information'
108
110 '''
111 This method identifies the string within gps_string, returning an ID index
112 which is required to parse the string.
113
114 Currently the following message types are supported:
115 GGA, ZDA, RMC, GST, GSV, VTG, HDT, PASHR
116 '''
117 'A dictionary of supported strings and their numeric indentifiers'
118
119 for key in self.GPS_IDs.keys():
120 if re.search( key, self.msg):
121 self.id = self.GPS_IDs[key]
122 return self.GPS_IDs[key]
123
124 raise NotImplementedError, ("This string is not recognized: " + self.msg)
125
127 '''
128 This method pareses a GPSString, defining a set of attributes for the class
129 with the parsing results. How each string is parsed is dependent on the
130 type of string. In any case, the fields returned are defined in
131 "self.fieldnames".
132
133 Before parsing the string's checksum if verified. The GPSString.FailedChecksum
134 exception is raised if the checksum fails. [NOTE: The checksum verification
135 may cause problems for some gps systems which do not calculate the checksum
136 on the proper portion of the string. The NMEA standard specifies calculation
137 on the portions of the string __between__ the leading "$" and "*", but
138 not to include either. ]
139
140 A few general rules are in order. Time stamps are converted to datetime
141 objects. Several GPS strings contain only time fields with no year, month,
142 or day. If gps_string.date is defined with a datetime.date object (i.e.
143 mygpsstring.date = datetime.date(2008,12,1) ) prior to calling the
144 parse() method, the final datetime object will combine the pre-set
145 date with the gps parsed time value. If gps_string.date is not defined
146 the returned datetime object returned from the parse() method will
147 reflect the gps time as a datetime.time() object.
148
149 Latitude and Longitude are converted to decimal degrees with negative
150 values for the Southern and Western hemispheres. They are reported to 8
151 decimal places which equates to just over 1 mm precision.
152
153 Some fields are not parsed because they do not typically change. The
154 units fields of meters for geoid separation in the GGA string is a classic
155 example.
156
157 '''
158
159 ' Verify Checksum'
160 if not self.checksum(True):
161 raise self.FailedChecksum, ("Checksum: " + self.checksum())
162
163
164 if self.id == 1:
165 'GGA'
166 exp = '(?P<match>\$..GGA.*)\*(?P<chksum>..)'
167 m = re.search(exp,self.msg)
168 if m:
169 gps_extract = m.group('match')
170 fields = gps_extract.split(',')
171
172 'Handle GGA Fields'
173 self.handlegpstime(fields[1])
174 self.handle_lat( fields[2], fields[3] )
175 self.handle_lon ( fields[4], fields[5] )
176 self.quality = dec.Decimal( fields[6] )
177 self.svs = dec.Decimal( fields[7] )
178 self.hdop = dec.Decimal( fields[8])
179 self.antennaheightMSL = dec.Decimal(fields[9])
180 try:
181 self.geoid = dec.Decimal(fields[11])
182 except dec.InvalidOperation:
183 if self.debug:
184 print "The field GEOID Height may not be present."
185 print fields[11]
186 self.geoid = dec.Decimal('NaN')
187 try:
188 self.dgpsage = dec.Decimal(fields[13])
189 except dec.InvalidOperation:
190 if self.debug:
191 print "The field DGPS Age may not be present."
192 print fields[13]
193 self.dgpspage = dec.Decimal('NaN')
194 try:
195 self.stationid = dec.Decimal(fields[14] )
196 except dec.InvalidOperation:
197 if self.debug:
198 print "The field DGPS Station ID may not be present."
199 print fields[14]
200 self.stationid = dec.Decimal('NaN')
201 elif self.id == 2:
202 'ZDA'
203 exp = '(?P<match>\$..ZDA.*)\*(?P<chksum>..)'
204 m = re.search(exp,self.msg)
205 if m:
206 gps_extract = m.group('match')
207 fields = gps_extract.split(',')
208 'Handle ZDA Fields'
209 self.datetime = datetime.date(int( fields[4]), \
210 int(fields[3]), \
211 int(fields[2]))
212 self.handlegpstime(fields[1])
213 try:
214 self.tzoffsethours = dec.Decimal( fields[5] )
215 except dec.InvalidOperation:
216 if self.debug:
217 print "Thef ield Local TZ Offset Hours may not be present."
218 print fields[5]
219 self.tzoffsethours = dec.Decimal('NaN')
220
221 try:
222 self.tzoffsetminutes = dec.Decimal( fields[6] )
223 except dec.InvalidOperation:
224 if self.debug:
225 print "The field Local TZ Offset Minutes may not be present."
226 print fields[6]
227 self.tzoffsetminutes = dec.Decimal('NaN')
228
229
230 elif self.id == 3:
231 'RMC'
232 exp = '(?P<match>\$..RMC.*)\*(?P<chksum>..)'
233 m = re.search(exp,self.msg)
234 if m:
235 gps_extract = m.group('match')
236 fields = gps_extract.split(',')
237 'Handle RMC Fields'
238 'Getting the date first ensure handlegpstime will return a full'
239 'datetime object'
240 self.datetime.date(int(fields[9][4:6]+2000),
241 int(fields[9][2:4]),
242 int(fields[10][0:2]))
243 self.handlgpstime(fields[1])
244 if fields[2] == 'A':
245 self.fixstatus = 1
246 else:
247 self.fixstatus = 0
248 self.handlegpslat(fields[3], fields[4])
249 self.handlegpslon(fields[5], fields[6])
250 self.knots = fields[7]
251 self.cog = fields[8]
252
253 elif self.id == 4:
254 'GST'
255 exp = '(?P<match>\$..GST.*)\*(?P<chksum>..)'
256 m = re.search(exp,self.msg)
257 if m:
258 gps_extract = m.group('match')
259 fields = gps_extract.split(',')
260 'Handle GST Fields'
261 self.handlegpstime(fields[1])
262 self.residualrms = dec.Decimal(fields[2])
263 self.semimajor = dec.Decimal(fields[3])
264 self.semiminor = dec.Decimal(fields[4])
265 self.orientation = dec.Decimal(fields[5])
266 self.lat1sigma = dec.Decimal(fields[6])
267 self.lon1sigma = dec.Decimal(fields[7])
268 self.height1sigma = dec.Decimal(fields[8])
269
270 elif self.id == 5:
271 'GSV'
272 exp = '(?P<match>\$..GSV.*)\*(?P<chksum>..)'
273 m = re.search(exp,self.msg)
274 if m:
275 gps_extract = m.group('match')
276 fields = gps_extract.split(',')
277 'Handle GSV Fields'
278 self.messages = dec.Decimal( fields[1] )
279 self.messagenum = dec.Decimal ( fields[2] )
280 self.visibleSVs = dec.Decimal ( fields[3] )
281 self.PRN = []
282 self.elevation = []
283 self.azimuth = []
284 self.snr = []
285 if self.debug:
286 print fields
287 for idx in range(4,fields.__len__() - 1, 4):
288 self.PRN.append(dec.Decimal(fields[idx]))
289 self.elevation.append(dec.Decimal(fields[idx + 1]))
290 try:
291 self.azimuth.append(dec.Decimal(fields[idx + 2]))
292 except dec.InvalidOperation:
293 self.azimuth.append(dec.Decimal('NaN'))
294 print "The field Satellite Azimuth may be missing."
295 print fields[idx + 3]
296 try:
297 self.snr.append(dec.Decimal(fields[idx + 3]))
298 except dec.InvalidOperation:
299
300 self.snr.append(dec.Decimal('NaN'))
301
302 elif self.id == 6:
303 'VTG'
304 exp = '(?P<match>\$..VTG.*)\*(?P<chksum>..)'
305 m = re.search(exp,self.msg)
306 if m:
307 gps_extract = m.group('match')
308 fields = gps_extract.split(',')
309 'Handle VTG Fields'
310 self.cog = dec.Decimal(fields[1])
311 self.knots = dec.Decimal(fields[5])
312 self.kmph = dec.Decimal(fields[7])
313
314
315 elif self.id == 7:
316 'HDT'
317 exp = '(?P<match>\$..HDT.*)\*(?P<chksum>..)'
318 m = re.search(exp,self.msg)
319 if m:
320 gps_extract = m.group('match')
321 fields = gps_extract.split(',')
322 'Handle HDT Fields'
323 self.heading = fields[1]
324 elif self.id == 8:
325 'PASHR'
326 exp = '(?P<match>\$PASHR.*)\*(?P<chksum>..)'
327 m = re.search(exp,self.msg)
328 if m:
329 gps_extract = m.group('match')
330 fields = gps_extract.split(',')
331 'Handle PASHR Fields'
332 self.handlegpstime(fields[1])
333 self.heading = dec.Decimal(fields[2])
334 self.roll = dec.Decimal(fields[4])
335 self.pitch = dec.Decimal(fields[5])
336 self.heave = dec.Decimal(fields[6])
337 self.rollaccuracy = dec.Decimal(fields[7])
338 self.pitchaccuracy = dec.Decimal(fields[8])
339 self.headingaccuracy = dec.Decimal(fields[9])
340 self.headingalgorithm = dec.Decimal(fields[10])
341 self.imustatus = dec.Decimal(fields[11])
342
343
344 keys = self.__dict__.keys()
345 keys.remove('debug')
346 keys.remove('msg')
347 keys.remove('id')
348 self.fields = {}
349 for item in keys:
350 self.fields[item] = self.__getattribute__(item)
351
353 '''
354 An internal method to convert gps time strings to datetime.time objects
355 (default) or datetime.datetime objects when GPSString.date is pre-defined
356 with a datetime.date object.
357
358 @param timestr: A NMEA time string of the form HHMMSS.SSS .
359
360 Since many strings do not contain the date,
361 defining the 'date' attribute of GPSString allows one to manually set
362 the date.
363 '''
364 tmptime = timestr
365 hour = dec.Decimal(tmptime[0:2])
366 try:
367 minute = dec.Decimal(tmptime[2:4])
368 except dec.InvalidOperation:
369 print timestr
370 print tmptime[2:4]
371 print self.msg
372 sys.exit()
373
374 seconds = int(dec.Decimal(tmptime[4:tmptime.__len__()]))
375 microseconds = int( (dec.Decimal(tmptime[4:tmptime.__len__()]) - \
376 dec.Decimal(seconds) ) * 1000000 )
377
378 timeval = datetime.time(hour, minute, seconds, microseconds)
379
380 try:
381 self.datetime = datetime.datetime.combine(self.date, timeval)
382 except:
383 self.datetime = timeval
384
386 '''
387 Converts latitude strings of arbitrary precision to decimal degrees to
388 10 decimal places of precision (about .000001 meters).
389
390 @param lattmp: The NMEA latitude string. (DDMM.MMMM)
391 @param lathem: The NMEA latitude hemisphere ('N'/'S')
392 '''
393 self.latitude = '%.10f' % (dec.Decimal(lattmp[0:2]) +
394 dec.Decimal(lattmp[3:lattmp.__len__()]) / 60)
395 self.latitude = dec.Decimal(self.latitude)
396 if lathem == 'S':
397 self.latitude = - self.latitude
398
400 '''
401 Converts longitude strings of arbitrary precision to decimal degrees to
402 10 decimal places of precision (about .000001 meters at the equator).
403
404 @param lontmp: The NMEA longitude string. (DDDMM.MMMM)
405 @param lonhem: The NMEA longitude hemisphere ('E'/'W')
406 '''
407 self.longitude = '%.10f' % (dec.Decimal(lontmp[0:3]) +
408 dec.Decimal(lontmp[4:lontmp.__len__()]) / 60)
409 self.longitude = dec.Decimal(self.longitude)
410 if lonhem == 'W':
411 self.longitude = - self.longitude
412
414 '''
415 Strips and ISO 8601 time stamp from the GPSString and returns a datetime
416 object.
417
418 For many scientific applications GPS strings are logged with the
419 logging computer's date-time stamp. When these time stamps are in
420 ISO 8601 format, this method will extract and parse them, returning a
421 datetime object.
422 '''
423 iso_exp = re.compile('(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d)T(?P<hour>\d\\d):(?P<minute>\d\d):(?P<seconds>\d\d\.\d+)')
424 m = re.search(iso_exp,self.msg)
425 if m:
426 year = int( m.group('year'))
427 month = int( m.group('month'))
428 day = int( m.group('day'))
429 hour = int(m.group('hour'))
430 minute = int(m.group('minute'))
431 seconds = int(float(m.group('seconds')))
432 microseconds = int( ( float(m.group('seconds')) - seconds ) * 1000000)
433 dts = datetime.datetime(year, month, day, hour, minute, seconds, microseconds)
434 return dts
435
437 '''
438 Converts a datetime stamp in the form of a datetime object to a
439 tab-delimited vector of numeric values.
440
441 @param dts: A datetime object.
442
443 For many scientific applications one often desires to import data into MATLAB
444 or Octave. A simple way to do that is to produce a tab-delimited date time
445 stamp of the form:
446
447 YYYY MH DD HR MN SS
448
449 whiich can be converted to MATLABs internal representation of time with
450 datevec(). This method returns such as time stamp from a datetime object.
451 '''
452 return "\t".join(map(str,( dts.year, dts.month, dts.day, dts.hour, dts.minute, float(dts.second) + float(dts.microsecond) / 1000000 )))
453
455 '''
456 A function to calculate and return a checksum of a NMEA string.
457
458 @param verify: When specified as True, checksum returns True/False
459 rather than the acutal checksum value.
460
461 '''
462 exp = '(?P<match>\$.*)\*(?P<chksum>..)'
463 m = re.search(exp,self.msg)
464 if m:
465 data = m.group('match')
466 tmp = map(ord, data[1:])
467 checksum = hex(reduce(xor, tmp))
468 if verify:
469 return checksum[2:4].upper() == m.group('chksum')
470 else:
471 return checksum[2:4].upper()
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
488 '''
489 A class creating the exception FailedChecksum, which is derived from the
490 standard exception.Warning. This exception is raised when a GPS string's
491 checksum is incorrect indicating corruption to the data.
492 '''
493 pass
494
495
496
497
498
499 if __name__ == '__main__':
500
501 gpstmp = GPSString('')
502 supportedstrings = gpstmp.GPS_IDs.keys()
503 supportedstrings.sort()
504 supportedstrings = ' '.join(supportedstrings)
505
506 from optparse import OptionParser
507 optionparser = OptionParser(usage="%prog [options]",
508 version="%prog "+__version__+' ('+__date__+')')
509 optionparser.add_option('-f','--filename', dest='filename',action='store',
510 help='specify the filename')
511 optionparser.add_option('-s','--stringtype',dest='stringtype',action='store',
512 type='string', help='specify which string to parse by specifying the three-letter identifier (Currently supported strings: '+ supportedstrings + ' )')
513
514 (options,args) = optionparser.parse_args()
515
516 filename = options.filename
517 stringtype = options.stringtype
518
519 for line in file(filename,'r'):
520 gps = GPSString(line)
521 try:
522 id = gps.identify()
523 except NotImplementedError:
524 sys.stderr.write('Unrecognized NMEA string.\n')
525 continue
526
527 'Only handle string specified'
528 if id != gps.GPS_IDs[stringtype]:
529 continue
530
531 'This will die silently if there is not pc timestamp.'
532 PCtime = gps.stripisotime()
533
534 if id == 1:
535 try:
536 gps.date = PCtime.date()
537 except:
538 gps.date = datetime.datetime.utcnow().date()
539
540 try:
541 gps.parse()
542 except gps.FailedChecksum:
543 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
544 " :: " + gps.msg + '\n')
545 continue
546
547 fieldstoprint = [gps.datetimevec(PCtime),
548 gps.datetimevec(gps.datetime),
549 gps.latitude,
550 gps.longitude,
551 gps.quality,
552 gps.svs,
553 gps.hdop,
554 gps.antennaheightMSL,
555 gps.geoid]
556 print "\t".join(map(str,fieldstoprint)).expandtabs()
557
558 if id == 2:
559 try:
560 gps.date = PCtime.date()
561 except:
562 gps.date = datetime.datetime.utcnow().date()
563
564 try:
565 gps.parse()
566 except gps.FailedChecksum:
567 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
568 " :: " + gps.msg + '\n')
569
570 continue
571
572 fieldstoprint = [gps.datetimevec(PCtime),
573 gps.datetimevec(gps.datetime)]
574
575 print "\t".join(map(str,fieldstoprint)).expandtabs()
576
577 if id == 3:
578 try:
579 gps.date = PCtime.date()
580 except:
581 gps.date = datetime.datetime.utcnow().date()
582 try:
583 gps.parse()
584 except self.FailedChecksum:
585 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
586 " :: " + gps.msg + '\n')
587 continue
588
589 fieldstoprint = [gps.datetimevec(PCtime),
590 gps.datetimevec(gps.datetime),
591 gps.fixstatus,
592 gps.latitude,
593 gps.longitude,
594 gps.knots,
595 gps.cog]
596 print "\t".join(map(str,fieldstoprint)).expandtabs()
597
598 if id == 4:
599 try:
600 gps.date = PCtime.date()
601 except:
602 gps.date = datetime.datetime.utcnow().date()
603
604 try:
605 gps.parse()
606 except self.FailedChecksum:
607 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
608 " :: " + gps.msg + '\n')
609 continue
610
611 fieldstoprint = [gps.datetimevec(PCtime),
612 gps.datetimevec(gps.datetime),
613 gps.residualrms,
614 gps.semimajor,
615 gps.semiminor,
616 gps.orientation,
617 gps.lat1sigma,
618 gps.lon1sigma,
619 gps.height1sigma]
620 print "\t".join(map(str,fieldstoprint)).expandtabs()
621
622 if id == 5:
623 try:
624 gps.date = PCtime.date()
625 except:
626 gps.date = datetime.datetime.utcnow().date()
627
628 try:
629 gps.parse()
630 except self.FailedChecksum:
631 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
632 " :: " + gps.msg + '\n')
633 continue
634
635 fieldstoprint = ([gps.datetimevec(PCtime)] +
636 map(str,gps.PRN) +
637 map(str,gps.elevation) +
638 map(str,gps.azimuth) +
639 map(str,gps.snr) )
640 print "\t".join(map(str,fieldstoprint)).expandtabs()
641
642
643 if id == 6:
644 try:
645 gps.parse()
646 except self.FailedChecksum:
647 sys.stderr.write( "Failed Checksum:" + gps.checksum() +
648 "::" + gps.msg + '\n')
649 continue
650
651
652
653 if id == 8:
654 try:
655 gps.parse()
656 except self.FailedChecksum:
657 sys.stderr.write( "Failed Checksum:" + gps.checksum() +
658 "::" + gps.msg + '\n')
659 continue
660