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: Standard method of operation is to parse only a particular string specified
67 by the -s flag (e.g. -s GGA ). It would be nicer to be able to specify multiple -s
68 flags or a comma delimited list of strings to parsed and have all of them parsed.
69 The potential problem is how to tell which is which when you're done?
70 '''
71
72 import os
73 import sys
74 import datetime
75 import re
76 import string
77 import decimal as dec
78 import pdb
79 from operator import xor
80 import exceptions
81
83 '''
84 A GPSString is any string that contains a complete NMEA string someplace
85 within it. The string must start with the leading $ and end with the *hh
86 where hh is the checksum.
87 '''
88 GPS_IDs = { \
89 'GGA' : 1, \
90 'ZDA' : 2, \
91 'RMC' : 3, \
92 'GST' : 4, \
93 'GSV' : 5, \
94 'VTG' : 6, \
95 'HDT' : 7 ,\
96 'PASHR' : 8}
97
98
100 '''
101 Initializes the class with any string containing a single NMEA data string.
102
103 @param msg: The ASCII string containing the NMEA data string.
104
105 '''
106 self.msg = msg
107 'The message containing the gps string.'
108 self.debug = False
109 'A flag for writing debugging information'
110
112 '''
113 This method identifies the string within gps_string, returning an ID index
114 which is required to parse the string.
115
116 Currently the following message types are supported:
117 GGA, ZDA, RMC, GST, GSV, VTG, HDT, PASHR
118 '''
119 'A dictionary of supported strings and their numeric indentifiers'
120
121 for key in self.GPS_IDs.keys():
122 if re.search( key, self.msg):
123 self.id = self.GPS_IDs[key]
124 return self.GPS_IDs[key]
125
126 raise NotImplementedError, ("This string is not recognized: " + self.msg)
127
129 '''
130 This method pareses a GPSString, defining a set of attributes for the class
131 with the parsing results. How each string is parsed is dependent on the
132 type of string. In any case, the fields returned are defined in
133 "self.fieldnames".
134
135 Before parsing the string's checksum if verified. The GPSString.FailedChecksum
136 exception is raised if the checksum fails. [NOTE: The checksum verification
137 may cause problems for some gps systems which do not calculate the checksum
138 on the proper portion of the string. The NMEA standard specifies calculation
139 on the portions of the string __between__ the leading "$" and "*", but
140 not to include either. ]
141
142 A few general rules are in order. Time stamps are converted to datetime
143 objects. Several GPS strings contain only time fields with no year, month,
144 or day. If gps_string.date is defined with a datetime.date object (i.e.
145 mygpsstring.date = datetime.date(2008,12,1) ) prior to calling the
146 parse() method, the final datetime object will combine the pre-set
147 date with the gps parsed time value. If gps_string.date is not defined
148 the returned datetime object returned from the parse() method will
149 reflect the gps time as a datetime.time() object.
150
151 Latitude and Longitude are converted to decimal degrees with negative
152 values for the Southern and Western hemispheres. They are reported to 8
153 decimal places which equates to just over 1 mm precision.
154
155 Some fields are not parsed because they do not typically change. The
156 units fields of meters for geoid separation in the GGA string is a classic
157 example.
158
159 '''
160
161 ' Verify Checksum'
162 if not self.checksum(True):
163 raise self.FailedChecksum, ("Checksum: " + self.checksum())
164
165
166 if self.id == 1:
167 'GGA'
168 exp = '(?P<match>\$..GGA.*)\*(?P<chksum>..)'
169 m = re.search(exp,self.msg)
170 if m:
171 gps_extract = m.group('match')
172 fields = gps_extract.split(',')
173
174 'Handle GGA Fields'
175 self.handlegpstime(fields[1])
176 self.handle_lat( fields[2], fields[3] )
177 self.handle_lon ( fields[4], fields[5] )
178 self.quality = dec.Decimal( fields[6] )
179 self.svs = dec.Decimal( fields[7] )
180 self.hdop = dec.Decimal( fields[8])
181 self.antennaheightMSL = dec.Decimal(fields[9])
182 try:
183 self.geoid = dec.Decimal(fields[11])
184 except dec.InvalidOperation:
185 if self.debug:
186 print "The field GEOID Height may not be present."
187 print fields[11]
188 self.geoid = dec.Decimal('NaN')
189 try:
190 self.dgpsage = dec.Decimal(fields[13])
191 except dec.InvalidOperation:
192 if self.debug:
193 print "The field DGPS Age may not be present."
194 print fields[13]
195 self.dgpspage = dec.Decimal('NaN')
196 try:
197 self.stationid = dec.Decimal(fields[14] )
198 except dec.InvalidOperation:
199 if self.debug:
200 print "The field DGPS Station ID may not be present."
201 print fields[14]
202 self.stationid = dec.Decimal('NaN')
203 elif self.id == 2:
204 'ZDA'
205 exp = '(?P<match>\$..ZDA.*)\*(?P<chksum>..)'
206 m = re.search(exp,self.msg)
207 if m:
208 gps_extract = m.group('match')
209 fields = gps_extract.split(',')
210 'Handle ZDA Fields'
211 self.datetime = datetime.date(int( fields[4]), \
212 int(fields[3]), \
213 int(fields[2]))
214 self.handlegpstime(fields[1])
215 try:
216 self.tzoffsethours = dec.Decimal( fields[5] )
217 except dec.InvalidOperation:
218 if self.debug:
219 print "Thef ield Local TZ Offset Hours may not be present."
220 print fields[5]
221 self.tzoffsethours = dec.Decimal('NaN')
222
223 try:
224 self.tzoffsetminutes = dec.Decimal( fields[6] )
225 except dec.InvalidOperation:
226 if self.debug:
227 print "The field Local TZ Offset Minutes may not be present."
228 print fields[6]
229 self.tzoffsetminutes = dec.Decimal('NaN')
230
231
232 elif self.id == 3:
233 'RMC'
234 exp = '(?P<match>\$..RMC.*)\*(?P<chksum>..)'
235 m = re.search(exp,self.msg)
236 if m:
237 gps_extract = m.group('match')
238 fields = gps_extract.split(',')
239 'Handle RMC Fields'
240 'Getting the date first ensure handlegpstime will return a full'
241 'datetime object'
242 self.datetime.date(int(fields[9][4:6]+2000),
243 int(fields[9][2:4]),
244 int(fields[10][0:2]))
245 self.handlgpstime(fields[1])
246 if fields[2] == 'A':
247 self.fixstatus = 1
248 else:
249 self.fixstatus = 0
250 self.handlegpslat(fields[3], fields[4])
251 self.handlegpslon(fields[5], fields[6])
252 self.knots = fields[7]
253 self.cog = fields[8]
254
255 elif self.id == 4:
256 'GST'
257 exp = '(?P<match>\$..GST.*)\*(?P<chksum>..)'
258 m = re.search(exp,self.msg)
259 if m:
260 gps_extract = m.group('match')
261 fields = gps_extract.split(',')
262 'Handle GST Fields'
263 self.handlegpstime(fields[1])
264 self.residualrms = dec.Decimal(fields[2])
265 self.semimajor = dec.Decimal(fields[3])
266 self.semiminor = dec.Decimal(fields[4])
267 self.orientation = dec.Decimal(fields[5])
268 self.lat1sigma = dec.Decimal(fields[6])
269 self.lon1sigma = dec.Decimal(fields[7])
270 self.height1sigma = dec.Decimal(fields[8])
271
272 elif self.id == 5:
273 'GSV'
274 exp = '(?P<match>\$..GSV.*)\*(?P<chksum>..)'
275 m = re.search(exp,self.msg)
276 if m:
277 gps_extract = m.group('match')
278 fields = gps_extract.split(',')
279 'Handle GSV Fields'
280 self.messages = dec.Decimal( fields[1] )
281 self.messagenum = dec.Decimal ( fields[2] )
282 self.visibleSVs = dec.Decimal ( fields[3] )
283 self.PRN = []
284 self.elevation = []
285 self.azimuth = []
286 self.snr = []
287 if self.debug:
288 print fields
289 for idx in range(4,fields.__len__() - 1, 4):
290 self.PRN.append(dec.Decimal(fields[idx]))
291 self.elevation.append(dec.Decimal(fields[idx + 1]))
292 try:
293 self.azimuth.append(dec.Decimal(fields[idx + 2]))
294 except dec.InvalidOperation:
295 self.azimuth.append(dec.Decimal('NaN'))
296 print "The field Satellite Azimuth may be missing."
297 print fields[idx + 3]
298 try:
299 self.snr.append(dec.Decimal(fields[idx + 3]))
300 except dec.InvalidOperation:
301
302 self.snr.append(dec.Decimal('NaN'))
303
304 elif self.id == 6:
305 'VTG'
306 exp = '(?P<match>\$..VTG.*)\*(?P<chksum>..)'
307 m = re.search(exp,self.msg)
308 if m:
309 gps_extract = m.group('match')
310 fields = gps_extract.split(',')
311 'Handle VTG Fields'
312 self.cog = dec.Decimal(fields[1])
313 self.knots = dec.Decimal(fields[5])
314 self.kmph = dec.Decimal(fields[7])
315
316
317 elif self.id == 7:
318 'HDT'
319 exp = '(?P<match>\$..HDT.*)\*(?P<chksum>..)'
320 m = re.search(exp,self.msg)
321 if m:
322 gps_extract = m.group('match')
323 fields = gps_extract.split(',')
324 'Handle HDT Fields'
325 self.heading = fields[1]
326 elif self.id == 8:
327 'PASHR'
328 exp = '(?P<match>\$PASHR.*)\*(?P<chksum>..)'
329 m = re.search(exp,self.msg)
330 if m:
331 gps_extract = m.group('match')
332 fields = gps_extract.split(',')
333 'Handle PASHR Fields'
334 self.handlegpstime(fields[1])
335 self.heading = dec.Decimal(fields[2])
336 self.roll = dec.Decimal(fields[4])
337 self.pitch = dec.Decimal(fields[5])
338 self.heave = dec.Decimal(fields[6])
339 self.rollaccuracy = dec.Decimal(fields[7])
340 self.pitchaccuracy = dec.Decimal(fields[8])
341 self.headingaccuracy = dec.Decimal(fields[9])
342 self.headingalgorithm = dec.Decimal(fields[10])
343 self.imustatus = dec.Decimal(fields[11])
344
345
346 keys = self.__dict__.keys()
347 keys.remove('debug')
348 keys.remove('msg')
349 keys.remove('id')
350 self.fields = {}
351 for item in keys:
352 self.fields[item] = self.__getattribute__(item)
353
355 '''
356 An internal method to convert gps time strings to datetime.time objects
357 (default) or datetime.datetime objects when GPSString.date is pre-defined
358 with a datetime.date object.
359
360 @param timestr: A NMEA time string of the form HHMMSS.SSS .
361
362 Since many strings do not contain the date,
363 defining the 'date' attribute of GPSString allows one to manually set
364 the date.
365 '''
366 tmptime = timestr
367 hour = dec.Decimal(tmptime[0:2])
368 try:
369 minute = dec.Decimal(tmptime[2:4])
370 except dec.InvalidOperation:
371 print timestr
372 print tmptime[2:4]
373 print self.msg
374 sys.exit()
375
376 seconds = int(dec.Decimal(tmptime[4:tmptime.__len__()]))
377 microseconds = int( (dec.Decimal(tmptime[4:tmptime.__len__()]) - \
378 dec.Decimal(seconds) ) * 1000000 )
379
380 timeval = datetime.time(hour, minute, seconds, microseconds)
381
382 try:
383 self.datetime = datetime.datetime.combine(self.date, timeval)
384 except:
385 self.datetime = timeval
386
388 '''
389 Converts latitude strings of arbitrary precision to decimal degrees to
390 10 decimal places of precision (about .000001 meters).
391
392 @param lattmp: The NMEA latitude string. (DDMM.MMMM)
393 @param lathem: The NMEA latitude hemisphere ('N'/'S')
394 '''
395 self.latitude = '%.10f' % (dec.Decimal(lattmp[0:2]) +
396 dec.Decimal(lattmp[3:lattmp.__len__()]) / 60)
397 self.latitude = dec.Decimal(self.latitude)
398 if lathem == 'S':
399 self.latitude = - self.latitude
400
402 '''
403 Converts longitude strings of arbitrary precision to decimal degrees to
404 10 decimal places of precision (about .000001 meters at the equator).
405
406 @param lontmp: The NMEA longitude string. (DDDMM.MMMM)
407 @param lonhem: The NMEA longitude hemisphere ('E'/'W')
408 '''
409 self.longitude = '%.10f' % (dec.Decimal(lontmp[0:3]) +
410 dec.Decimal(lontmp[4:lontmp.__len__()]) / 60)
411 self.longitude = dec.Decimal(self.longitude)
412 if lonhem == 'W':
413 self.longitude = - self.longitude
414
416 '''
417 Strips and ISO 8601 time stamp from the GPSString and returns a datetime
418 object.
419
420 For many scientific applications GPS strings are logged with the
421 logging computer's date-time stamp. When these time stamps are in
422 ISO 8601 format, this method will extract and parse them, returning a
423 datetime object.
424 '''
425 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+)')
426 m = re.search(iso_exp,self.msg)
427 if m:
428 year = int( m.group('year'))
429 month = int( m.group('month'))
430 day = int( m.group('day'))
431 hour = int(m.group('hour'))
432 minute = int(m.group('minute'))
433 seconds = int(float(m.group('seconds')))
434 microseconds = int( ( float(m.group('seconds')) - seconds ) * 1000000)
435 dts = datetime.datetime(year, month, day, hour, minute, seconds, microseconds)
436 return dts
437
439 '''
440 Converts a datetime stamp in the form of a datetime object to a
441 tab-delimited vector of numeric values.
442
443 @param dts: A datetime object.
444
445 For many scientific applications one often desires to import data into MATLAB
446 or Octave. A simple way to do that is to produce a tab-delimited date time
447 stamp of the form:
448
449 YYYY MH DD HR MN SS
450
451 whiich can be converted to MATLABs internal representation of time with
452 datevec(). This method returns such as time stamp from a datetime object.
453 '''
454 return "\t".join(map(str,( dts.year, dts.month, dts.day, dts.hour, dts.minute, float(dts.second) + float(dts.microsecond) / 1000000 )))
455
457 '''
458 A function to calculate and return a checksum of a NMEA string.
459
460 @param verify: When specified as True, checksum returns True/False
461 rather than the acutal checksum value.
462
463 '''
464 exp = '(?P<match>\$.*)\*(?P<chksum>..)'
465 m = re.search(exp,self.msg)
466 if m:
467 data = m.group('match')
468 tmp = map(ord, data[1:])
469 checksum = hex(reduce(xor, tmp))
470 if verify:
471 return checksum[2:4].upper() == m.group('chksum')
472 else:
473 return checksum[2:4].upper()
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
490 '''
491 A class creating the exception FailedChecksum, which is derived from the
492 standard exception.Warning. This exception is raised when a GPS string's
493 checksum is incorrect indicating corruption to the data.
494 '''
495 pass
496
497
498
499
500
501 if __name__ == '__main__':
502
503 gpstmp = GPSString('')
504 supportedstrings = gpstmp.GPS_IDs.keys()
505 supportedstrings.sort()
506 supportedstrings = ' '.join(supportedstrings)
507
508 from optparse import OptionParser
509 optionparser = OptionParser(usage="%prog [options]",
510 version="%prog "+__version__+' ('+__date__+')')
511 optionparser.add_option('-f','--filename', dest='filename',action='store',
512 help='specify the filename')
513 optionparser.add_option('-s','--stringtype',dest='stringtype',action='store',
514 type='string', help='specify which string to parse by specifying the three-letter identifier (Currently supported strings: '+ supportedstrings + ' )')
515
516 (options,args) = optionparser.parse_args()
517
518 filename = options.filename
519 stringtype = options.stringtype
520
521 for line in file(filename,'r'):
522 gps = GPSString(line)
523 try:
524 id = gps.identify()
525 except NotImplementedError:
526 sys.stderr.write('Unrecognized NMEA string.\n')
527 continue
528
529 'Only handle string specified'
530 if id != gps.GPS_IDs[stringtype]:
531 continue
532
533 'This will die silently if there is not pc timestamp.'
534 PCtime = gps.stripisotime()
535
536 if id == 1:
537 try:
538 gps.date = PCtime.date()
539 except:
540 gps.date = datetime.datetime.utcnow().date()
541
542 try:
543 gps.parse()
544 except gps.FailedChecksum:
545 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
546 " :: " + gps.msg + '\n')
547 continue
548
549 fieldstoprint = [gps.datetimevec(PCtime),
550 gps.datetimevec(gps.datetime),
551 gps.latitude,
552 gps.longitude,
553 gps.quality,
554 gps.svs,
555 gps.hdop,
556 gps.antennaheightMSL,
557 gps.geoid]
558 print "\t".join(map(str,fieldstoprint)).expandtabs()
559
560 if id == 2:
561 try:
562 gps.date = PCtime.date()
563 except:
564 gps.date = datetime.datetime.utcnow().date()
565
566 try:
567 gps.parse()
568 except gps.FailedChecksum:
569 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
570 " :: " + gps.msg + '\n')
571
572 continue
573
574 fieldstoprint = [gps.datetimevec(PCtime),
575 gps.datetimevec(gps.datetime)]
576
577 print "\t".join(map(str,fieldstoprint)).expandtabs()
578
579 if id == 3:
580 try:
581 gps.date = PCtime.date()
582 except:
583 gps.date = datetime.datetime.utcnow().date()
584 try:
585 gps.parse()
586 except self.FailedChecksum:
587 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
588 " :: " + gps.msg + '\n')
589 continue
590
591 fieldstoprint = [gps.datetimevec(PCtime),
592 gps.datetimevec(gps.datetime),
593 gps.fixstatus,
594 gps.latitude,
595 gps.longitude,
596 gps.knots,
597 gps.cog]
598 print "\t".join(map(str,fieldstoprint)).expandtabs()
599
600 if id == 4:
601 try:
602 gps.date = PCtime.date()
603 except:
604 gps.date = datetime.datetime.utcnow().date()
605
606 try:
607 gps.parse()
608 except self.FailedChecksum:
609 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
610 " :: " + gps.msg + '\n')
611 continue
612
613 fieldstoprint = [gps.datetimevec(PCtime),
614 gps.datetimevec(gps.datetime),
615 gps.residualrms,
616 gps.semimajor,
617 gps.semiminor,
618 gps.orientation,
619 gps.lat1sigma,
620 gps.lon1sigma,
621 gps.height1sigma]
622 print "\t".join(map(str,fieldstoprint)).expandtabs()
623
624 if id == 5:
625 try:
626 gps.date = PCtime.date()
627 except:
628 gps.date = datetime.datetime.utcnow().date()
629
630 try:
631 gps.parse()
632 except self.FailedChecksum:
633 sys.stderr.write( "Failed Checksum: " + gps.checksum() +
634 " :: " + gps.msg + '\n')
635 continue
636
637 fieldstoprint = ([gps.datetimevec(PCtime)] +
638 map(str,gps.PRN) +
639 map(str,gps.elevation) +
640 map(str,gps.azimuth) +
641 map(str,gps.snr) )
642 print "\t".join(map(str,fieldstoprint)).expandtabs()
643
644
645 if id == 6:
646 try:
647 gps.parse()
648 except self.FailedChecksum:
649 sys.stderr.write( "Failed Checksum:" + gps.checksum() +
650 "::" + gps.msg + '\n')
651 continue
652
653
654
655 if id == 8:
656 try:
657 gps.parse()
658 except self.FailedChecksum:
659 sys.stderr.write( "Failed Checksum:" + gps.checksum() +
660 "::" + gps.msg + '\n')
661 continue
662