# The MIT License (MIT) # # Copyright (c) 2017 Tony DiCola for Adafruit Industries # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ `adafruit_gps` ==================================================== GPS parsing module. Can parse simple NMEA data sentences from serial GPS modules to read latitude, longitude, and more. * Author(s): Tony DiCola Implementation Notes -------------------- **Hardware:** * Adafruit `Ultimate GPS Breakout `_ * Adafruit `Ultimate GPS FeatherWing `_ **Software and Dependencies:** * Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: https://github.com/adafruit/circuitpython/releases """ import time __version__ = "3.0.2" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_GPS.git" # 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. if nmea_data is None or len(nmea_data) < 3: return None raw = float(nmea_data) deg = raw // 100 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) # 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. """ def __init__(self, uart): self._uart = uart # Initialize null starting values for GPS attributes. self.timestamp_utc = None self.latitude = None self.longitude = None self.fix_quality = None self.satellites = None self.horizontal_dilution = None self.altitude_m = None self.height_geoid = None self.velocity_knots = None self.speed_knots = None self.track_angle_deg = None def update(self): """Check for updated data from the GPS module and process it accordingly. Returns True if new data was processed, and False if nothing new was received. """ # Grab a sentence and check its data type to call the appropriate # parsing function. sentence = self._parse_sentence() if sentence is None: return False data_type, args = sentence data_type = data_type.upper() if data_type == 'GPGGA': # GGA, 3d location fix self._parse_gpgga(args) elif data_type == 'GPRMC': # RMC, minimum location info self._parse_gprmc(args) return True def send_command(self, command, add_checksum=True): """Send a command string to the GPS. If add_checksum is True (the default) a NMEA checksum will automatically be computed and added. Note you should NOT add the leading $ and trailing * to the command as they will automatically be added! """ self._uart.write('$') self._uart.write(command) if add_checksum: checksum = 0 for char in command: checksum ^= ord(char) self._uart.write('*') self._uart.write('{:02x}'.format(checksum).upper()) self._uart.write('\r\n') @property 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 def _parse_sentence(self): # Parse any NMEA sentence that is available. sentence = self._uart.readline() if sentence is None or sentence == b'' or len(sentence) < 1: return None sentence = str(sentence, 'ascii').strip() # Look for a checksum and validate it if present. if len(sentence) > 7 and sentence[-3] == '*': # Get included checksum, then calculate it and compare. expected = int(sentence[-2:], 16) actual = 0 for i in range(1, len(sentence)-3): actual ^= ord(sentence[i]) if actual != expected: return None # Failed to validate checksum. # Remove checksum once validated. 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: return None # Invalid sentence, no comma after data type. data_type = sentence[1:delineator] return (data_type, sentence[delineator+1:]) 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_gprmc(self, args): # Parse the arguments (everything after data type) for NMEA GPRMC # minimum location fix sentence. data = args.split(',') if data is None or len(data) < 11: 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 status (active/fixed or void). status = data[1] self.fix_quality = 0 if status is not None and status.lower() == 'a': self.fix_quality = 1 # Parse latitude and longitude. self.latitude = _parse_degrees(data[2]) if self.latitude is not None and \ data[3] is not None and data[3].lower() == 's': self.latitude *= -1.0 self.longitude = _parse_degrees(data[4]) if self.longitude is not None and \ data[5] is not None and data[5].lower() == 'w': self.longitude *= -1.0 # Parse out speed and other simple numeric values. self.speed_knots = _parse_float(data[6]) self.track_angle_deg = _parse_float(data[7]) # Parse date. 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. # This is a problem with the NMEA # spec and not this code. if self.timestamp_utc is not None: # Replace the timestamp with an updated one. # (struct_time is immutable and can't be changed in place) self.timestamp_utc = time.struct_time((year, month, day, self.timestamp_utc.tm_hour, self.timestamp_utc.tm_min, self.timestamp_utc.tm_sec, 0, 0, -1)) else: # Time hasn't been set so create it. self.timestamp_utc = time.struct_time((year, month, day, 0, 0, 0, 0, 0, -1))