# 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 <https://www.adafruit.com/product/746>`_
|
|
* Adafruit `Ultimate GPS FeatherWing <https://www.adafruit.com/product/3133>`_
|
|
|
|
**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))
|