From 15015b54d9652aadd6f9922fc0f0cd713541bc5a Mon Sep 17 00:00:00 2001 From: James Carr Date: Thu, 11 Feb 2021 20:43:49 +0000 Subject: [PATCH 01/17] Enable the GSV parsing with _parse_gpgsv --- adafruit_gps.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index 14d0b42..378dc86 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -131,6 +131,8 @@ def update(self): self._parse_gprmc(args) elif data_type in (b"GPGGA", b"GNGGA"): # GGA, 3d location fix self._parse_gpgga(args) + elif data_type in (b"GLGSV", b"GPGSV", b"GNGSV"): # GSV, Satelittes in view + self._parse_gpgsv(args) return True def send_command(self, command, add_checksum=True): @@ -443,7 +445,7 @@ def _parse_gpgsv(self, args): sat_tup = data[3:] satdict = {} - for i in range(len(sat_tup) / 4): + for i in range(len(sat_tup) // 4): j = i * 4 key = "gps{}".format(i + (4 * (self.mess_num - 1))) satnum = _parse_int(sat_tup[0 + j]) # Satellite number @@ -460,13 +462,8 @@ def _parse_gpgsv(self, args): try: if self.satellites < self.satellites_prev: - for i in self.sats: - try: - if int(i[-2]) >= self.satellites: - del self.sats[i] - except ValueError: - if int(i[-1]) >= self.satellites: - del self.sats[i] + for i in range(self.satellites, self.satellites_prev): + del self.sats[f"gps{i}"] except TypeError: pass self.satellites_prev = self.satellites From 002f8508426ccee5decec163680ab610c9593ab4 Mon Sep 17 00:00:00 2001 From: James Carr Date: Thu, 11 Feb 2021 20:48:50 +0000 Subject: [PATCH 02/17] Spelling. --- adafruit_gps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index 378dc86..5e1a267 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -131,7 +131,7 @@ def update(self): self._parse_gprmc(args) elif data_type in (b"GPGGA", b"GNGGA"): # GGA, 3d location fix self._parse_gpgga(args) - elif data_type in (b"GLGSV", b"GPGSV", b"GNGSV"): # GSV, Satelittes in view + elif data_type in (b"GLGSV", b"GPGSV", b"GNGSV"): # GSV, Satellites in view self._parse_gpgsv(args) return True From c4c999a908e04ccb3b261a608ffca40952eb8f1d Mon Sep 17 00:00:00 2001 From: James Carr Date: Thu, 11 Feb 2021 21:50:38 +0000 Subject: [PATCH 03/17] Enable the GSA parsing with _parse_gpgsa --- adafruit_gps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adafruit_gps.py b/adafruit_gps.py index 5e1a267..5d5b5cc 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -133,6 +133,8 @@ def update(self): self._parse_gpgga(args) elif data_type in (b"GLGSV", b"GPGSV", b"GNGSV"): # GSV, Satellites in view self._parse_gpgsv(args) + elif data_type in (b"GLGSA", b"GPGSA", b"GNGSA"): # GSA, GPS DOP and active satellites + self._parse_gpgsa(args) return True def send_command(self, command, add_checksum=True): From 6dcfca0a2532f5cd6d43d2572f7cc903194929c1 Mon Sep 17 00:00:00 2001 From: James Carr Date: Mon, 22 Feb 2021 22:57:15 +0000 Subject: [PATCH 04/17] Rename the _parse_[sentence] methods so that they don't have gp as part of their name. eg. _parse_gpgll -> _parse_gll --- adafruit_gps.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index c16f847..141d74e 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -137,15 +137,15 @@ def update(self): return True if sentence_type == b"GLL": # Geographic position - Latitude/Longitude - self._parse_gpgll(args) + self._parse_gll(args) elif sentence_type == b"RMC": # Minimum location info - self._parse_gprmc(args) + self._parse_rmc(args) elif sentence_type == b"GGA": # 3D location fix - self._parse_gpgga(args) + self._parse_gga(args) elif sentence_type == b"GSV": # Satellites in view - self._parse_gpgsv(talker, args) + self._parse_gsv(talker, args) elif sentence_type == b"GSA": # GPS DOP and active satellites - self._parse_gpgsa(talker, args) + self._parse_gsa(talker, args) return True def send_command(self, command, add_checksum=True): @@ -264,7 +264,7 @@ def _parse_talker(data_type): return (data_type[:2], data_type[2:]) - def _parse_gpgll(self, args): + def _parse_gll(self, args): data = args.split(",") if data is None or data[0] is None or (data[0] == ""): return # Unexpected number of params. @@ -297,7 +297,7 @@ def _parse_gpgll(self, args): # Parse data active or void self.isactivedata = _parse_str(data[5]) - def _parse_gprmc(self, args): + def _parse_rmc(self, args): # Parse the arguments (everything after data type) for NMEA GPRMC # minimum location fix sentence. data = args.split(",") @@ -376,7 +376,7 @@ def _parse_gprmc(self, args): (year, month, day, 0, 0, 0, 0, 0, -1) ) - def _parse_gpgga(self, args): + def _parse_gga(self, args): # Parse the arguments (everything after data type) for NMEA GPGGA # 3D location fix sentence. data = args.split(",") @@ -425,7 +425,7 @@ def _parse_gpgga(self, args): self.altitude_m = _parse_float(data[8]) self.height_geoid = _parse_float(data[10]) - def _parse_gpgsa(self, talker, args): + def _parse_gsa(self, talker, args): talker = talker.decode("ascii") data = args.split(",") if data is None or (data[0] == ""): @@ -447,7 +447,7 @@ def _parse_gpgsa(self, talker, args): # Parse VDOP, vertical dilution of precision self.vdop = _parse_float(data[-1]) - def _parse_gpgsv(self, talker, args): + def _parse_gsv(self, talker, args): # Parse the arguments (everything after data type) for NMEA GPGGA # pylint: disable=too-many-branches # 3D location fix sentence. From 7a59234e2e2dc2e4ff93e7af0d48a5549276464e Mon Sep 17 00:00:00 2001 From: James Carr Date: Tue, 23 Feb 2021 21:49:35 +0000 Subject: [PATCH 05/17] Refactor the sentence parsing (Fixes #55) Refactor the date and time parsing into a method Add an instance variable self.mode_indicator Add an instance variable self.magnetic_variation Sentence Parsing: 1) Read the sentence from I2C/UART (No change) 2) Validate the CRC, else fail (No change) 3) Call _parse_[SENTENCE_TYPE](), if the sentence was from a GNSS, else return True 4) The data length is compared against it's expected length, else fail 5) These then call _parse_data(PSEUDO_SENTENCE_TYPE,DATA) 6) The data is converted into the expected parameter types, else fail PSEUDO_SENTENCE_TYPE is usually SENTENCE_TYPE, but can be modified for a SENTENCE_TYPE with variable data length. The update() method returns True if: The sentence passes CRC AND Not from A GNSS (No parsing happens) OR From a GNSS, with a handled sentence type which passes parsing OR From a GNSS, with an unhandled sentence type (No parsing happens) Else False --- adafruit_gps.py | 636 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 428 insertions(+), 208 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index 141d74e..82d4b96 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -34,6 +34,140 @@ _GPSI2C_DEFAULT_ADDRESS = const(0x10) +_SENTENCE_PARAMS = { + "GLL": ("DEGREES", "CHAR", "DEGREES", "CHAR", "FLOAT", "CHAR", "CHARN"), + "RMC": ( + "FLOAT", + "CHAR", + "FLOAT", + "CHAR", + "FLOAT", + "CHAR", + "FLOAT", + "FLOAT", + "INT", + "DEGREESN", + "CHARN", + "CHARN", + ), + "GGA": ( + "FLOAT", + "DEGREES", + "CHAR", + "DEGREES", + "CHAR", + "INT", + "INT", + "FLOAT", + "FLOAT", + "STR", + "FLOAT", + "STR", + "INTN", + "STRN", + ), + "GSA": ( + "CHAR", + "INT", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "FLOAT", + "FLOAT", + "FLOAT", + ), + "GSA_4_11": ( # NMEA 4.11 - Nov 2018 + "CHAR", + "INT", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "INTN", + "FLOAT", + "FLOAT", + "FLOAT", + "STRN", + ), + "GSV7": ( + "INT", + "INT", + "INT", + "INT", + "INT", + "INT", + "INTN", + ), + "GSV11": ( + "INT", + "INT", + "INT", + "INT", + "INT", + "INT", + "INTN", + "INT", + "INT", + "INT", + "INTN", + ), + "GSV15": ( + "INT", + "INT", + "INT", + "INT", + "INT", + "INT", + "INTN", + "INT", + "INT", + "INT", + "INTN", + "INT", + "INT", + "INT", + "INTN", + ), + "GSV19": ( + "INT", + "INT", + "INT", + "INT", + "INT", + "INT", + "INTN", + "INT", + "INT", + "INT", + "INTN", + "INT", + "INT", + "INT", + "INTN", + "INT", + "INT", + "INT", + "INTN", + ), +} + + # Internal helper parsing functions. # These handle input that might be none or null and return none instead of # throwing errors. @@ -66,6 +200,87 @@ def _parse_str(nmea_data): return str(nmea_data) +def _parse_talker(data_type): + # Split the data_type into talker and sentence_type + if data_type[0] == b"P": # Proprietary codes + return (data_type[:1], data_type[1:]) + + return (data_type[:2], data_type[2:]) + + +def _parse_data(sentence_type, data): + """Parse sentence data for the specified sentence type and return a list of parameters in the correct format, or return None.""" + + if sentence_type not in _SENTENCE_PARAMS: + # The sentence_type is unknown + return None + + param_types = _SENTENCE_PARAMS[sentence_type] + + if len(param_types) != len(data): + # The expected number does not match the number of data items + print( + f"DEBUG:GPS: {sentence_type} len(param_types):{len(param_types)} " + f"is not equal to len(data):{len(data)}" + ) + return None + + params = [] + try: + for i in range(len(data)): + pti = param_types[i] + di = data[i] + if pti == "CHAR": + # A single character + if len(di) != 1: + return None + params.append(di) + elif pti == "CHARN": + # A single character or Nothing + if di is None or len(di) == 0: + params.append(None) + else: + params.append(di) + elif pti == "DEGREES": + # A number parseable as degrees + params.append(_parse_degrees(di)) + elif pti == "DEGREESN": + # A number parseable as degrees or Nothing + if di is None or len(di) == 0: + params.append(None) + else: + params.append(_parse_degrees(di)) + elif pti == "FLOAT": + # A floating point number + params.append(_parse_float(di)) + elif pti == "INT": + # An integer + params.append(_parse_int(di)) + elif pti == "INTN": + # An integer or Nothing + if di is None or len(di) == 0: + params.append(None) + else: + params.append(_parse_int(di)) + elif pti == "STR": + # A string + params.append(di) + elif pti == "STRN": + # A string or Nothing + if di is None or len(di) == 0: + params.append(None) + else: + params.append(di) + else: + raise TypeError(f"Unexpected parameter type '{pti}'") + except ValueError: + # Something didn't parse, abort + return None + + # Return the parsed data + return params + + # lint warning about too many attributes disabled # pylint: disable-msg=R0902 @@ -81,8 +296,8 @@ def __init__(self, uart, debug=False): self.timestamp_utc = None self.latitude = None self.longitude = None - self.fix_quality = None - self.fix_quality_3d = None + self.fix_quality = 0 + self.fix_quality_3d = 0 self.satellites = None self.satellites_prev = None self.horizontal_dilution = None @@ -103,6 +318,8 @@ def __init__(self, uart, debug=False): self.total_mess_num = None self.mess_num = None self._raw_sentence = None + self.mode_indicator = None + self.magnetic_variation = None self.debug = debug def update(self): @@ -122,7 +339,7 @@ def update(self): print(sentence) data_type, args = sentence data_type = bytes(data_type.upper(), "ascii") - (talker, sentence_type) = GPS._parse_talker(data_type) + (talker, sentence_type) = _parse_talker(data_type) # Check for all currently known GNSS talkers # GA - Galileo @@ -134,19 +351,21 @@ def update(self): # GN - GNSS / More than one of the above if talker not in (b"GA", b"GB", b"GI", b"GL", b"GP", b"GQ", b"GN"): # It's not a known GNSS source of data + # Assume it's a valid packet anyway return True if sentence_type == b"GLL": # Geographic position - Latitude/Longitude - self._parse_gll(args) + return self._parse_gll(args) elif sentence_type == b"RMC": # Minimum location info - self._parse_rmc(args) + return self._parse_rmc(args) elif sentence_type == b"GGA": # 3D location fix - self._parse_gga(args) + return self._parse_gga(args) elif sentence_type == b"GSV": # Satellites in view - self._parse_gsv(talker, args) + return self._parse_gsv(talker, args) elif sentence_type == b"GSA": # GPS DOP and active satellites - self._parse_gsa(talker, args) - return True + return self._parse_gsa(talker, args) + else: + return True def send_command(self, command, add_checksum=True): """Send a command string to the GPS. If add_checksum is True (the @@ -256,239 +475,240 @@ def _parse_sentence(self): data_type = sentence[1:delimiter] return (data_type, sentence[delimiter + 1 :]) - @staticmethod - def _parse_talker(data_type): - # Split the data_type into talker and sentence_type - if data_type[0] == b"P": # Proprietary codes - return (data_type[:1], data_type[1:]) - - return (data_type[:2], data_type[2:]) + def _update_timestamp_utc(self, time_utc, date=None): + hours = time_utc // 10000 + mins = (time_utc // 100) % 100 + secs = time_utc % 100 + if date is None: + if self.timestamp_utc is None: + day, month, year = 0, 0, 0 + else: + day = self.timestamp_utc.tm_mday + month = self.timestamp_utc.tm_mon + year = self.timestamp_utc.tm_year + else: + day = date // 10000 + month = (date // 100) % 100 + year = 2000 + date % 100 + + self.timestamp_utc = time.struct_time( + (year, month, day, hours, mins, secs, 0, 0, -1) + ) def _parse_gll(self, args): - data = args.split(",") - if data is None or data[0] is None or (data[0] == ""): - return # Unexpected number of params. + # GLL - Geographic Position - Latitude/Longitude - # Parse latitude and longitude. - self.latitude = _parse_degrees(data[0]) - if self.latitude is not None and data[1] is not None and data[1].lower() == "s": + data = args.split(",") + if data is None or len(data) != 7: + return False # Unexpected number of params. + data = _parse_data("GLL", data) + if data is None: + return False # Params didn't parse + + # Latitude + self.latitude = data[0] + if data[1].lower() == "s": self.latitude *= -1.0 - self.longitude = _parse_degrees(data[2]) - if ( - self.longitude is not None - and data[3] is not None - and data[3].lower() == "w" - ): + + # Longitude + self.longitude = data[2] + if data[3].lower() == "w": self.longitude *= -1.0 - time_utc = int(_parse_int(float(data[4]))) - 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( - (0, 0, 0, hours, mins, secs, 0, 0, -1) - ) - else: - self.timestamp_utc = time.struct_time( - (0, 0, 0, hours, mins, secs, 0, 0, -1) - ) - # Parse data active or void - self.isactivedata = _parse_str(data[5]) + + # UTC time of position + self._update_timestamp_utc(int(data[4])) + + # Status Valid(A) or Invalid(V) + self.isactivedata = data[5] + + # Parse FAA mode indicator + self.mode_indicator = data[6] + + return True def _parse_rmc(self, args): - # Parse the arguments (everything after data type) for NMEA GPRMC - # minimum location fix sentence. + # RMC - Recommended Minimum Navigation Information + data = args.split(",") - if data is None or len(data) < 11 or data[0] is None or (data[0] == ""): - 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": + if data is None or len(data) != 12: + return False # Unexpected number of params. + data = _parse_data("RMC", data) + if data is None: + return False # Params didn't parse + + # UTC time of position and date + self._update_timestamp_utc(int(data[0]), data[8]) + + # Status Valid(A) or Invalid(V) + self.isactivedata = data[1] + if data[1].lower() == "a": + if self.fix_quality == 0: + self.fix_quality = 1 + else: + self.fix_quality = 0 + + # Latitude + self.latitude = data[2] + if 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 year 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) - ) + + # Longitude + self.latitude = data[4] + if data[5].lower() == "w": + self.latitude *= -1.0 + + # Speed over ground, knots + self.speed_knots = data[6] + + # Track made good, degrees true + self.track_angle_deg = data[7] + + # Magnetic variation + if data[9] is None or data[10] is None: + self.magnatic_variation = None + else: + self.magnetic_variation = data[9] + if data[10].lower() == "w": + self.magnetic_variation *= -1.0 + + # Parse FAA mode indicator + self.mode_indicator = data[11] + + return True def _parse_gga(self, args): - # Parse the arguments (everything after data type) for NMEA GPGGA - # 3D location fix sentence. + # GGA - Global Positioning System Fix Data + data = args.split(",") - if data is None or len(data) != 14 or (data[0] == ""): - 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": + if data is None or len(data) != 14: + return False # Unexpected number of params. + data = _parse_data("GGA", data) + if data is None: + return False # Params didn't parse + + # UTC time of position + self._update_timestamp_utc(int(data[0])) + + # Latitude + self.latitude = data[1] + if 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]) + + # Longitude + self.latitude = data[3] + if data[4].lower() == "w": + self.latitude *= -1.0 + + # GPS quality indicator + # 0 - fix not available, + # 1 - GPS fix, + # 2 - Differential GPS fix (values above 2 are 2.3 features) + # 3 - PPS fix + # 4 - Real Time Kinematic + # 5 - Float RTK + # 6 - estimated (dead reckoning) + # 7 - Manual input mode + # 8 - Simulation mode + self.fix_quality = data[5] + + # Number of satellites in use, 0 - 12 + self.satellites = data[6] + + # Horizontal dilution of precision + self.horizontal_dilution = data[7] + + # Antenna altitude relative to mean sea level self.altitude_m = _parse_float(data[8]) + # data[9] - antenna altitude unit, always 'M' ??? + + # Geoidal separation relative to WGS 84 self.height_geoid = _parse_float(data[10]) + # data[11] - geoidal separation unit, always 'M' ??? + + # data[12] - Age of differential GPS data, can be null + # data[13] - Differential reference station ID, can be null + + return True def _parse_gsa(self, talker, args): - talker = talker.decode("ascii") + # GSA - GPS DOP and active satellites + data = args.split(",") - if data is None or (data[0] == ""): - return # Unexpected number of params + if data is None or len(data) not in (17, 18): + return False # Unexpected number of params. + if len(data) == 17: + data = _parse_data("GSA", data) + else: + data = _parse_data("GSA_4_11", data) + if data is None: + return False # Params didn't parse + + talker = talker.decode("ascii") + + # Selection mode: 'M' - manual, 'A' - automatic + self.sel_mode = data[0] + + # Mode: 1 - no fix, 2 - 2D fix, 3 - 3D fix + self.fix_quality_3d = data[1] - # Parse selection mode - self.sel_mode = _parse_str(data[0]) - # Parse 3d fix - self.fix_quality_3d = _parse_int(data[1]) satlist = list(filter(None, data[2:-4])) self.sat_prns = [] for sat in satlist: - self.sat_prns.append("{}{}".format(talker, _parse_int(sat))) + self.sat_prns.append("{}{}".format(talker, sat)) - # Parse PDOP, dilution of precision - self.pdop = _parse_float(data[-3]) - # Parse HDOP, horizontal dilution of precision - self.hdop = _parse_float(data[-2]) - # Parse VDOP, vertical dilution of precision - self.vdop = _parse_float(data[-1]) + # PDOP, dilution of precision + self.pdop = _parse_float(data[14]) + + # HDOP, horizontal dilution of precision + self.hdop = _parse_float(data[15]) + + # VDOP, vertical dilution of precision + self.vdop = _parse_float(data[16]) + + # data[17] - System ID + + return True def _parse_gsv(self, talker, args): - # Parse the arguments (everything after data type) for NMEA GPGGA - # pylint: disable=too-many-branches - # 3D location fix sentence. - talker = talker.decode("ascii") + # GSV - Satellites in view + data = args.split(",") - if data is None or (data[0] == ""): - return # Unexpected number of params. + if data is None or len(data) not in (7, 11, 15, 19): + return False # Unexpected number of params. + data = _parse_data( + {7: "GSV7", 11: "GSV11", 15: "GSV15", 19: "GSV19"}[len(data)], data + ) + if data is None: + return False # Params didn't parse - # Parse number of messages - self.total_mess_num = _parse_int(data[0]) # Total number of messages - # Parse message number - self.mess_num = _parse_int(data[1]) # Message number - # Parse number of satellites in view - self.satellites = _parse_int(data[2]) # Number of satellites + talker = talker.decode("ascii") - if len(data) < 3: - return + # Number of messages + self.total_mess_num = data[0] + # Message number + self.mess_num = data[1] + # Number of satellites in view + self.satellites = data[2] sat_tup = data[3:] satlist = [] timestamp = time.monotonic() for i in range(len(sat_tup) // 4): - try: - j = i * 4 - value = ( - # Satellite number - "{}{}".format(talker, _parse_int(sat_tup[0 + j])), - # Elevation in degrees - _parse_int(sat_tup[1 + j]), - # Azimuth in degrees - _parse_int(sat_tup[2 + j]), - # signal-to-noise ratio in dB - _parse_int(sat_tup[3 + j]), - # Timestamp - timestamp, - ) - satlist.append(value) - except ValueError: - # Something wasn't an int - pass + j = i * 4 + value = ( + # Satellite number + "{}{}".format(talker, sat_tup[0 + j]), + # Elevation in degrees + sat_tup[1 + j], + # Azimuth in degrees + sat_tup[2 + j], + # signal-to-noise ratio in dB + sat_tup[3 + j], + # Timestamp + timestamp, + ) + satlist.append(value) if self._sats is None: self._sats = [] From 632dc4a1b17f42881f93cfa56c464dae2a624397 Mon Sep 17 00:00:00 2001 From: James Carr Date: Tue, 23 Feb 2021 22:48:32 +0000 Subject: [PATCH 06/17] Use longitude for longitude, not latitude. D'Oh! --- adafruit_gps.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index 82d4b96..15beb26 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -553,9 +553,9 @@ def _parse_rmc(self, args): self.latitude *= -1.0 # Longitude - self.latitude = data[4] + self.longitude = data[4] if data[5].lower() == "w": - self.latitude *= -1.0 + self.longitude *= -1.0 # Speed over ground, knots self.speed_knots = data[6] @@ -595,9 +595,9 @@ def _parse_gga(self, args): self.latitude *= -1.0 # Longitude - self.latitude = data[3] + self.longitude = data[3] if data[4].lower() == "w": - self.latitude *= -1.0 + self.longitude *= -1.0 # GPS quality indicator # 0 - fix not available, From 91f584689acc42d37714194d4a88694bb779ed35 Mon Sep 17 00:00:00 2001 From: James Carr Date: Tue, 23 Feb 2021 23:01:02 +0000 Subject: [PATCH 07/17] Correct some of the RMC parameter types. --- adafruit_gps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index 15beb26..fc4e504 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -39,9 +39,9 @@ "RMC": ( "FLOAT", "CHAR", - "FLOAT", + "DEGREES", "CHAR", - "FLOAT", + "DEGREES", "CHAR", "FLOAT", "FLOAT", From 2cc44731e4fd7c7f379411b69d489be387057d8b Mon Sep 17 00:00:00 2001 From: James Carr Date: Wed, 24 Feb 2021 23:10:46 +0000 Subject: [PATCH 08/17] Pass the debug value into _parse_data --- adafruit_gps.py | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index fc4e504..a5bf602 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -208,21 +208,26 @@ def _parse_talker(data_type): return (data_type[:2], data_type[2:]) -def _parse_data(sentence_type, data): - """Parse sentence data for the specified sentence type and return a list of parameters in the correct format, or return None.""" +def _parse_data(sentence_type, data, debug): + """Parse sentence data for the specified sentence type and + return a list of parameters in the correct format, or return None. + """ if sentence_type not in _SENTENCE_PARAMS: # The sentence_type is unknown + if debug: + print(f"DEBUG:GPS: Unknown sentence type:{sentence_type}") return None param_types = _SENTENCE_PARAMS[sentence_type] if len(param_types) != len(data): # The expected number does not match the number of data items - print( - f"DEBUG:GPS: {sentence_type} len(param_types):{len(param_types)} " - f"is not equal to len(data):{len(data)}" - ) + if debug: + print( + f"DEBUG:GPS: {sentence_type} len(param_types):{len(param_types)} " + f"is not equal to len(data):{len(data)}" + ) return None params = [] @@ -239,6 +244,8 @@ def _parse_data(sentence_type, data): # A single character or Nothing if di is None or len(di) == 0: params.append(None) + elif len(di) != 1: + return None else: params.append(di) elif pti == "DEGREES": @@ -318,8 +325,8 @@ def __init__(self, uart, debug=False): self.total_mess_num = None self.mess_num = None self._raw_sentence = None - self.mode_indicator = None - self.magnetic_variation = None + self._mode_indicator = None + self._magnetic_variation = None self.debug = debug def update(self): @@ -501,7 +508,7 @@ def _parse_gll(self, args): data = args.split(",") if data is None or len(data) != 7: return False # Unexpected number of params. - data = _parse_data("GLL", data) + data = _parse_data("GLL", data, self.debug) if data is None: return False # Params didn't parse @@ -522,7 +529,7 @@ def _parse_gll(self, args): self.isactivedata = data[5] # Parse FAA mode indicator - self.mode_indicator = data[6] + self._mode_indicator = data[6] return True @@ -532,7 +539,7 @@ def _parse_rmc(self, args): data = args.split(",") if data is None or len(data) != 12: return False # Unexpected number of params. - data = _parse_data("RMC", data) + data = _parse_data("RMC", data, self.debug) if data is None: return False # Params didn't parse @@ -567,12 +574,12 @@ def _parse_rmc(self, args): if data[9] is None or data[10] is None: self.magnatic_variation = None else: - self.magnetic_variation = data[9] + self._magnetic_variation = data[9] if data[10].lower() == "w": - self.magnetic_variation *= -1.0 + self._magnetic_variation *= -1.0 # Parse FAA mode indicator - self.mode_indicator = data[11] + self._mode_indicator = data[11] return True @@ -582,7 +589,7 @@ def _parse_gga(self, args): data = args.split(",") if data is None or len(data) != 14: return False # Unexpected number of params. - data = _parse_data("GGA", data) + data = _parse_data("GGA", data, self.debug) if data is None: return False # Params didn't parse @@ -637,9 +644,9 @@ def _parse_gsa(self, talker, args): if data is None or len(data) not in (17, 18): return False # Unexpected number of params. if len(data) == 17: - data = _parse_data("GSA", data) + data = _parse_data("GSA", data, self.debug) else: - data = _parse_data("GSA_4_11", data) + data = _parse_data("GSA_4_11", data, self.debug) if data is None: return False # Params didn't parse @@ -676,7 +683,9 @@ def _parse_gsv(self, talker, args): if data is None or len(data) not in (7, 11, 15, 19): return False # Unexpected number of params. data = _parse_data( - {7: "GSV7", 11: "GSV11", 15: "GSV15", 19: "GSV19"}[len(data)], data + {7: "GSV7", 11: "GSV11", 15: "GSV15", 19: "GSV19"}[len(data)], + data, + self.debug, ) if data is None: return False # Params didn't parse From 2f79c44ef67efa3db1b2905b579db61d82cfea93 Mon Sep 17 00:00:00 2001 From: James Carr Date: Wed, 24 Feb 2021 23:13:17 +0000 Subject: [PATCH 09/17] Typo. Thank you, PyLint --- adafruit_gps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index a5bf602..6f4a410 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -572,7 +572,7 @@ def _parse_rmc(self, args): # Magnetic variation if data[9] is None or data[10] is None: - self.magnatic_variation = None + self._magnetic_variation = None else: self._magnetic_variation = data[9] if data[10].lower() == "w": From 92efe69f54ae48b762f95115e589e3a7daf09ace Mon Sep 17 00:00:00 2001 From: James Carr Date: Wed, 24 Feb 2021 23:18:22 +0000 Subject: [PATCH 10/17] Missing return statement. Thank you, PyLint. --- adafruit_gps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/adafruit_gps.py b/adafruit_gps.py index 6f4a410..f6609c8 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -678,6 +678,7 @@ def _parse_gsa(self, talker, args): def _parse_gsv(self, talker, args): # GSV - Satellites in view + # pylint: disable=too-many-branches data = args.split(",") if data is None or len(data) not in (7, 11, 15, 19): @@ -747,6 +748,8 @@ def _parse_gsv(self, talker, args): self.satellites_prev = self.satellites + return True + class GPS_GtopI2C(GPS): """GTop-compatible I2C GPS parsing module. Can parse simple NMEA data From 037be6ebf3da0d84586f38c4506e74cc7cf3bf04 Mon Sep 17 00:00:00 2001 From: James Carr Date: Wed, 24 Feb 2021 23:34:15 +0000 Subject: [PATCH 11/17] Black/PyLint pass --- adafruit_gps.py | 52 +++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index f6609c8..862e456 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -212,6 +212,7 @@ def _parse_data(sentence_type, data, debug): """Parse sentence data for the specified sentence type and return a list of parameters in the correct format, or return None. """ + # pylint: disable=too-many-branches if sentence_type not in _SENTENCE_PARAMS: # The sentence_type is unknown @@ -232,54 +233,53 @@ def _parse_data(sentence_type, data, debug): params = [] try: - for i in range(len(data)): + for i, dti in enumerate(data): pti = param_types[i] - di = data[i] if pti == "CHAR": # A single character - if len(di) != 1: + if len(dti) != 1: return None - params.append(di) + params.append(dti) elif pti == "CHARN": # A single character or Nothing - if di is None or len(di) == 0: + if dti is None or len(dti) == 0: params.append(None) - elif len(di) != 1: + elif len(dti) != 1: return None else: - params.append(di) + params.append(dti) elif pti == "DEGREES": # A number parseable as degrees - params.append(_parse_degrees(di)) + params.append(_parse_degrees(dti)) elif pti == "DEGREESN": # A number parseable as degrees or Nothing - if di is None or len(di) == 0: + if dti is None or len(dti) == 0: params.append(None) else: - params.append(_parse_degrees(di)) + params.append(_parse_degrees(dti)) elif pti == "FLOAT": # A floating point number - params.append(_parse_float(di)) + params.append(_parse_float(dti)) elif pti == "INT": # An integer - params.append(_parse_int(di)) + params.append(_parse_int(dti)) elif pti == "INTN": # An integer or Nothing - if di is None or len(di) == 0: + if dti is None or len(dti) == 0: params.append(None) else: - params.append(_parse_int(di)) + params.append(_parse_int(dti)) elif pti == "STR": # A string - params.append(di) + params.append(dti) elif pti == "STRN": # A string or Nothing - if di is None or len(di) == 0: + if dti is None or len(dti) == 0: params.append(None) else: - params.append(di) + params.append(dti) else: - raise TypeError(f"Unexpected parameter type '{pti}'") + raise TypeError(f"GPS: Unexpected parameter type '{pti}'") except ValueError: # Something didn't parse, abort return None @@ -336,6 +336,7 @@ def update(self): """ # Grab a sentence and check its data type to call the appropriate # parsing function. + try: sentence = self._parse_sentence() except UnicodeError: @@ -361,18 +362,19 @@ def update(self): # Assume it's a valid packet anyway return True + result = True if sentence_type == b"GLL": # Geographic position - Latitude/Longitude - return self._parse_gll(args) + result = self._parse_gll(args) elif sentence_type == b"RMC": # Minimum location info - return self._parse_rmc(args) + result = self._parse_rmc(args) elif sentence_type == b"GGA": # 3D location fix - return self._parse_gga(args) + result = self._parse_gga(args) elif sentence_type == b"GSV": # Satellites in view - return self._parse_gsv(talker, args) + result = self._parse_gsv(talker, args) elif sentence_type == b"GSA": # GPS DOP and active satellites - return self._parse_gsa(talker, args) - else: - return True + result = self._parse_gsa(talker, args) + + return result def send_command(self, command, add_checksum=True): """Send a command string to the GPS. If add_checksum is True (the From 23b2df8960d54e8089cc488c505edb99ce6b851d Mon Sep 17 00:00:00 2001 From: James Carr Date: Thu, 25 Feb 2021 12:08:49 +0000 Subject: [PATCH 12/17] Modify the time_source example so that it doesn't trigger a PyLint duplicate warning. --- examples/gps_time_source.py | 53 +++++++++++++------------------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/examples/gps_time_source.py b/examples/gps_time_source.py index ef25214..14df00a 100644 --- a/examples/gps_time_source.py +++ b/examples/gps_time_source.py @@ -11,11 +11,11 @@ import rtc import adafruit_gps -uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) -# i2c = busio.I2C(board.SCL, board.SDA) +# uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) +i2c = busio.I2C(board.SCL, board.SDA) -gps = adafruit_gps.GPS(uart, debug=False) -# gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface +# gps = adafruit_gps.GPS(uart, debug=False) +gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") gps.send_command(b"PMTK220,1000") @@ -24,6 +24,18 @@ rtc.set_time_source(gps) the_rtc = rtc.RTC() + +def _format_datetime(datetime): + return "{:02}/{:02}/{} {:02}:{:02}:{:02}".format( + datetime.tm_mon, + datetime.tm_mday, + datetime.tm_year, + datetime.tm_hour, + datetime.tm_min, + datetime.tm_sec, + ) + + last_print = time.monotonic() while True: @@ -36,39 +48,12 @@ print("No time data from GPS yet") continue # Time & date from GPS informations - print( - "Fix timestamp: {:02}/{:02}/{} {:02}:{:02}:{:02}".format( - gps.timestamp_utc.tm_mon, # Grab parts of the time from the - gps.timestamp_utc.tm_mday, # struct_time object that holds - gps.timestamp_utc.tm_year, # the fix time. Note you might - gps.timestamp_utc.tm_hour, # not get all data like year, day, - gps.timestamp_utc.tm_min, # month! - gps.timestamp_utc.tm_sec, - ) - ) + print("Fix timestamp: {}".format(_format_datetime(gps.timestamp_utc))) # Time & date from internal RTC - print( - "RTC timestamp: {:02}/{:02}/{} {:02}:{:02}:{:02}".format( - the_rtc.datetime.tm_mon, - the_rtc.datetime.tm_mday, - the_rtc.datetime.tm_year, - the_rtc.datetime.tm_hour, - the_rtc.datetime.tm_min, - the_rtc.datetime.tm_sec, - ) - ) + print("RTC timestamp: {}".format(_format_datetime(the_rtc.datetime))) # Time & date from time.localtime() function local_time = time.localtime() - print( - "Local time: {:02}/{:02}/{} {:02}:{:02}:{:02}".format( - local_time.tm_mon, - local_time.tm_mday, - local_time.tm_year, - local_time.tm_hour, - local_time.tm_min, - local_time.tm_sec, - ) - ) + print("Local time: {}".format(_format_datetime(local_time))) From 07d483899a671ab525320d1960d21e65207d7e02 Mon Sep 17 00:00:00 2001 From: James Carr Date: Thu, 25 Feb 2021 12:25:33 +0000 Subject: [PATCH 13/17] Add PyLint disable=duplicate-code to time_source. --- examples/gps_time_source.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/gps_time_source.py b/examples/gps_time_source.py index 14df00a..db86637 100644 --- a/examples/gps_time_source.py +++ b/examples/gps_time_source.py @@ -5,6 +5,8 @@ # The GPS timestamps are available without a fix and keep the track of # time while there is powersource (ie coin cell battery) +# pylint: disable=duplicate-code + import time import board import busio From ad6930a0a6d51b99ad2e7768eee0f3feb7b5d978 Mon Sep 17 00:00:00 2001 From: James Carr Date: Thu, 25 Feb 2021 12:37:43 +0000 Subject: [PATCH 14/17] Add PyLint disable=duplicate-code to all examples. --- examples/gps_datalogging.py | 3 +++ examples/gps_echotest.py | 3 +++ examples/gps_satellitefix.py | 2 ++ examples/gps_simpletest.py | 3 +++ 4 files changed, 11 insertions(+) diff --git a/examples/gps_datalogging.py b/examples/gps_datalogging.py index 1eee0d8..7b77d11 100644 --- a/examples/gps_datalogging.py +++ b/examples/gps_datalogging.py @@ -10,6 +10,9 @@ # MUST carefully follow the steps in this guide to enable writes to the # internal filesystem: # https://learn.adafruit.com/adafruit-ultimate-gps-featherwing/circuitpython-library + +# pylint: disable=duplicate-code + import board import busio import adafruit_gps diff --git a/examples/gps_echotest.py b/examples/gps_echotest.py index baae47f..da21fb9 100644 --- a/examples/gps_echotest.py +++ b/examples/gps_echotest.py @@ -4,6 +4,9 @@ # Simple GPS module demonstration. # Will print NMEA sentences received from the GPS, great for testing connection # Uses the GPS to send some commands, then reads directly from the GPS + +# pylint: disable=duplicate-code + import time import board import busio diff --git a/examples/gps_satellitefix.py b/examples/gps_satellitefix.py index 17ef3c0..58fdd36 100644 --- a/examples/gps_satellitefix.py +++ b/examples/gps_satellitefix.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2021 lesamouraipourpre # SPDX-License-Identifier: MIT +# pylint: disable=duplicate-code + import time import board diff --git a/examples/gps_simpletest.py b/examples/gps_simpletest.py index fb93283..731e69b 100644 --- a/examples/gps_simpletest.py +++ b/examples/gps_simpletest.py @@ -4,6 +4,9 @@ # Simple GPS module demonstration. # Will wait for a fix and print a message every second with the current location # and other details. + +# pylint: disable=duplicate-code + import time import board import busio From e0d9f3d3f791f507b8ca75d0b30ba3d5c7ee9520 Mon Sep 17 00:00:00 2001 From: James Carr Date: Mon, 1 Mar 2021 08:36:50 +0000 Subject: [PATCH 15/17] Replace _SENTENCE_PARAMS dictionary with a tuple of compacted strings to reduce memory usage. --- adafruit_gps.py | 261 ++++++++++++++---------------------------------- 1 file changed, 74 insertions(+), 187 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index 862e456..6a2d6c3 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -34,138 +34,38 @@ _GPSI2C_DEFAULT_ADDRESS = const(0x10) -_SENTENCE_PARAMS = { - "GLL": ("DEGREES", "CHAR", "DEGREES", "CHAR", "FLOAT", "CHAR", "CHARN"), - "RMC": ( - "FLOAT", - "CHAR", - "DEGREES", - "CHAR", - "DEGREES", - "CHAR", - "FLOAT", - "FLOAT", - "INT", - "DEGREESN", - "CHARN", - "CHARN", - ), - "GGA": ( - "FLOAT", - "DEGREES", - "CHAR", - "DEGREES", - "CHAR", - "INT", - "INT", - "FLOAT", - "FLOAT", - "STR", - "FLOAT", - "STR", - "INTN", - "STRN", - ), - "GSA": ( - "CHAR", - "INT", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "FLOAT", - "FLOAT", - "FLOAT", - ), - "GSA_4_11": ( # NMEA 4.11 - Nov 2018 - "CHAR", - "INT", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "INTN", - "FLOAT", - "FLOAT", - "FLOAT", - "STRN", - ), - "GSV7": ( - "INT", - "INT", - "INT", - "INT", - "INT", - "INT", - "INTN", - ), - "GSV11": ( - "INT", - "INT", - "INT", - "INT", - "INT", - "INT", - "INTN", - "INT", - "INT", - "INT", - "INTN", - ), - "GSV15": ( - "INT", - "INT", - "INT", - "INT", - "INT", - "INT", - "INTN", - "INT", - "INT", - "INT", - "INTN", - "INT", - "INT", - "INT", - "INTN", - ), - "GSV19": ( - "INT", - "INT", - "INT", - "INT", - "INT", - "INT", - "INTN", - "INT", - "INT", - "INT", - "INTN", - "INT", - "INT", - "INT", - "INTN", - "INT", - "INT", - "INT", - "INTN", - ), -} +_GLL = 0 +_RMC = 1 +_GGA = 2 +_GSA = 3 +_GSA_4_11 = 4 +_GSV7 = 5 +_GSV11 = 6 +_GSV15 = 7 +_GSV19 = 8 +_ST_MIN = _GLL +_ST_MAX = _GSV19 + +_SENTENCE_PARAMS = ( + # 0 - _GLL + "dcdcfcC", + # 1 - _RMC + "fcdcdcffiDCC", + # 2 - _GGA + "fdcdciiffsfsIS", + # 3 - _GSA + "ciIIIIIIIIIIIIfff", + # 4 - _GSA_4_11 + "ciIIIIIIIIIIIIfffS", + # 5 - _GSV7 + "iiiiiiI", + # 6 - _GSV11 + "iiiiiiIiiiI", + # 7 - _GSV15 + "iiiiiiIiiiIiiiI", + # 8 - _GSV19 + "iiiiiiIiiiIiiiIiiiI", +) # Internal helper parsing functions. @@ -200,6 +100,13 @@ def _parse_str(nmea_data): return str(nmea_data) +def _read_degrees(data, index, neg): + x = data[index] + if data[index + 1].lower() == neg: + x *= -1.0 + return x + + def _parse_talker(data_type): # Split the data_type into talker and sentence_type if data_type[0] == b"P": # Proprietary codes @@ -208,73 +115,68 @@ def _parse_talker(data_type): return (data_type[:2], data_type[2:]) -def _parse_data(sentence_type, data, debug): +def _parse_data(sentence_type, data): """Parse sentence data for the specified sentence type and return a list of parameters in the correct format, or return None. """ # pylint: disable=too-many-branches - if sentence_type not in _SENTENCE_PARAMS: + if not _ST_MIN <= sentence_type <= _ST_MAX: # The sentence_type is unknown - if debug: - print(f"DEBUG:GPS: Unknown sentence type:{sentence_type}") return None param_types = _SENTENCE_PARAMS[sentence_type] if len(param_types) != len(data): # The expected number does not match the number of data items - if debug: - print( - f"DEBUG:GPS: {sentence_type} len(param_types):{len(param_types)} " - f"is not equal to len(data):{len(data)}" - ) return None params = [] try: for i, dti in enumerate(data): pti = param_types[i] - if pti == "CHAR": + len_dti = len(dti) + nothing = dti is None or len_dti == 0 + if pti == "c": # A single character - if len(dti) != 1: + if len_dti != 1: return None params.append(dti) - elif pti == "CHARN": + elif pti == "C": # A single character or Nothing - if dti is None or len(dti) == 0: + if nothing: params.append(None) - elif len(dti) != 1: + elif len_dti != 1: return None else: params.append(dti) - elif pti == "DEGREES": + elif pti == "d": # A number parseable as degrees params.append(_parse_degrees(dti)) - elif pti == "DEGREESN": + elif pti == "D": # A number parseable as degrees or Nothing - if dti is None or len(dti) == 0: + if nothing: params.append(None) else: params.append(_parse_degrees(dti)) - elif pti == "FLOAT": + elif pti == "f": # A floating point number params.append(_parse_float(dti)) - elif pti == "INT": + elif pti == "i": # An integer params.append(_parse_int(dti)) - elif pti == "INTN": + elif pti == "I": # An integer or Nothing - if dti is None or len(dti) == 0: + if nothing: params.append(None) else: params.append(_parse_int(dti)) - elif pti == "STR": + elif pti == "s": # A string params.append(dti) - elif pti == "STRN": + elif pti == "S": # A string or Nothing - if dti is None or len(dti) == 0: + if nothing: params.append(None) else: params.append(dti) @@ -510,19 +412,15 @@ def _parse_gll(self, args): data = args.split(",") if data is None or len(data) != 7: return False # Unexpected number of params. - data = _parse_data("GLL", data, self.debug) + data = _parse_data(_GLL, data) if data is None: return False # Params didn't parse # Latitude - self.latitude = data[0] - if data[1].lower() == "s": - self.latitude *= -1.0 + self.latitude = _read_degrees(data, 0, "s") # Longitude - self.longitude = data[2] - if data[3].lower() == "w": - self.longitude *= -1.0 + self.longitude = _read_degrees(data, 2, "w") # UTC time of position self._update_timestamp_utc(int(data[4])) @@ -541,7 +439,7 @@ def _parse_rmc(self, args): data = args.split(",") if data is None or len(data) != 12: return False # Unexpected number of params. - data = _parse_data("RMC", data, self.debug) + data = _parse_data(_RMC, data) if data is None: return False # Params didn't parse @@ -557,14 +455,10 @@ def _parse_rmc(self, args): self.fix_quality = 0 # Latitude - self.latitude = data[2] - if data[3].lower() == "s": - self.latitude *= -1.0 + self.latitude = _read_degrees(data, 2, "s") # Longitude - self.longitude = data[4] - if data[5].lower() == "w": - self.longitude *= -1.0 + self.longitude = _read_degrees(data, 4, "w") # Speed over ground, knots self.speed_knots = data[6] @@ -576,9 +470,7 @@ def _parse_rmc(self, args): if data[9] is None or data[10] is None: self._magnetic_variation = None else: - self._magnetic_variation = data[9] - if data[10].lower() == "w": - self._magnetic_variation *= -1.0 + self._magnetic_variation = _read_degrees(data, 9, "w") # Parse FAA mode indicator self._mode_indicator = data[11] @@ -591,7 +483,7 @@ def _parse_gga(self, args): data = args.split(",") if data is None or len(data) != 14: return False # Unexpected number of params. - data = _parse_data("GGA", data, self.debug) + data = _parse_data(_GGA, data) if data is None: return False # Params didn't parse @@ -599,14 +491,10 @@ def _parse_gga(self, args): self._update_timestamp_utc(int(data[0])) # Latitude - self.latitude = data[1] - if data[2].lower() == "s": - self.latitude *= -1.0 + self.latitude = _read_degrees(data, 1, "s") # Longitude - self.longitude = data[3] - if data[4].lower() == "w": - self.longitude *= -1.0 + self.longitude = _read_degrees(data, 3, "w") # GPS quality indicator # 0 - fix not available, @@ -646,9 +534,9 @@ def _parse_gsa(self, talker, args): if data is None or len(data) not in (17, 18): return False # Unexpected number of params. if len(data) == 17: - data = _parse_data("GSA", data, self.debug) + data = _parse_data(_GSA, data) else: - data = _parse_data("GSA_4_11", data, self.debug) + data = _parse_data(_GSA_4_11, data) if data is None: return False # Params didn't parse @@ -686,9 +574,8 @@ def _parse_gsv(self, talker, args): if data is None or len(data) not in (7, 11, 15, 19): return False # Unexpected number of params. data = _parse_data( - {7: "GSV7", 11: "GSV11", 15: "GSV15", 19: "GSV19"}[len(data)], + {7: _GSV7, 11: _GSV11, 15: _GSV15, 19: _GSV19}[len(data)], data, - self.debug, ) if data is None: return False # Params didn't parse @@ -780,7 +667,7 @@ def read(self, num_bytes=1): # 'stuffed' newlines and then append to our result array for byteification i2c.readinto(self._charbuff) char = self._charbuff[0] - if (char == ord("\n")) and (self._lastbyte != ord("\r")): + if (char == 0x0A) and (self._lastbyte != 0x0D): continue # skip duplicate \n's! result.append(char) self._lastbyte = char # keep track of the last character approved @@ -804,14 +691,14 @@ def readline(self): timeout = time.monotonic() + self._timeout while timeout > time.monotonic(): # check if our internal buffer has a '\n' termination already - if self._internalbuffer and (self._internalbuffer[-1] == ord("\n")): + if self._internalbuffer and (self._internalbuffer[-1] == 0x0A): break char = self.read(1) if not char: continue self._internalbuffer.append(char[0]) # print(bytearray(self._internalbuffer)) - if self._internalbuffer and self._internalbuffer[-1] == ord("\n"): + if self._internalbuffer and self._internalbuffer[-1] == 0x0A: ret = bytearray(self._internalbuffer) self._internalbuffer = [] # reset the buffer to empty return ret From 6da1add986633d4ad3cdac1c4ecf4fe55759e401 Mon Sep 17 00:00:00 2001 From: James Carr Date: Mon, 1 Mar 2021 08:43:48 +0000 Subject: [PATCH 16/17] Move the args.split() call up a level to reduce memory usage. --- adafruit_gps.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/adafruit_gps.py b/adafruit_gps.py index 6a2d6c3..2aca02a 100644 --- a/adafruit_gps.py +++ b/adafruit_gps.py @@ -265,6 +265,7 @@ def update(self): return True result = True + args = args.split(",") if sentence_type == b"GLL": # Geographic position - Latitude/Longitude result = self._parse_gll(args) elif sentence_type == b"RMC": # Minimum location info @@ -406,10 +407,9 @@ def _update_timestamp_utc(self, time_utc, date=None): (year, month, day, hours, mins, secs, 0, 0, -1) ) - def _parse_gll(self, args): + def _parse_gll(self, data): # GLL - Geographic Position - Latitude/Longitude - data = args.split(",") if data is None or len(data) != 7: return False # Unexpected number of params. data = _parse_data(_GLL, data) @@ -433,10 +433,9 @@ def _parse_gll(self, args): return True - def _parse_rmc(self, args): + def _parse_rmc(self, data): # RMC - Recommended Minimum Navigation Information - data = args.split(",") if data is None or len(data) != 12: return False # Unexpected number of params. data = _parse_data(_RMC, data) @@ -477,10 +476,9 @@ def _parse_rmc(self, args): return True - def _parse_gga(self, args): + def _parse_gga(self, data): # GGA - Global Positioning System Fix Data - data = args.split(",") if data is None or len(data) != 14: return False # Unexpected number of params. data = _parse_data(_GGA, data) @@ -527,10 +525,9 @@ def _parse_gga(self, args): return True - def _parse_gsa(self, talker, args): + def _parse_gsa(self, talker, data): # GSA - GPS DOP and active satellites - data = args.split(",") if data is None or len(data) not in (17, 18): return False # Unexpected number of params. if len(data) == 17: @@ -566,11 +563,10 @@ def _parse_gsa(self, talker, args): return True - def _parse_gsv(self, talker, args): + def _parse_gsv(self, talker, data): # GSV - Satellites in view # pylint: disable=too-many-branches - data = args.split(",") if data is None or len(data) not in (7, 11, 15, 19): return False # Unexpected number of params. data = _parse_data( From 15415ca9ad376910eb5670d3648cbb4f5d53db80 Mon Sep 17 00:00:00 2001 From: James Carr Date: Tue, 2 Mar 2021 07:20:19 +0000 Subject: [PATCH 17/17] Revert changes to get examples to pass pylint. This should be fixed when the pre-commit gets updated. --- examples/gps_datalogging.py | 3 --- examples/gps_echotest.py | 3 --- examples/gps_satellitefix.py | 2 -- examples/gps_simpletest.py | 3 --- examples/gps_time_source.py | 10 ++++------ 5 files changed, 4 insertions(+), 17 deletions(-) diff --git a/examples/gps_datalogging.py b/examples/gps_datalogging.py index 7b77d11..1eee0d8 100644 --- a/examples/gps_datalogging.py +++ b/examples/gps_datalogging.py @@ -10,9 +10,6 @@ # MUST carefully follow the steps in this guide to enable writes to the # internal filesystem: # https://learn.adafruit.com/adafruit-ultimate-gps-featherwing/circuitpython-library - -# pylint: disable=duplicate-code - import board import busio import adafruit_gps diff --git a/examples/gps_echotest.py b/examples/gps_echotest.py index da21fb9..baae47f 100644 --- a/examples/gps_echotest.py +++ b/examples/gps_echotest.py @@ -4,9 +4,6 @@ # Simple GPS module demonstration. # Will print NMEA sentences received from the GPS, great for testing connection # Uses the GPS to send some commands, then reads directly from the GPS - -# pylint: disable=duplicate-code - import time import board import busio diff --git a/examples/gps_satellitefix.py b/examples/gps_satellitefix.py index 58fdd36..17ef3c0 100644 --- a/examples/gps_satellitefix.py +++ b/examples/gps_satellitefix.py @@ -1,8 +1,6 @@ # SPDX-FileCopyrightText: 2021 lesamouraipourpre # SPDX-License-Identifier: MIT -# pylint: disable=duplicate-code - import time import board diff --git a/examples/gps_simpletest.py b/examples/gps_simpletest.py index 731e69b..fb93283 100644 --- a/examples/gps_simpletest.py +++ b/examples/gps_simpletest.py @@ -4,9 +4,6 @@ # Simple GPS module demonstration. # Will wait for a fix and print a message every second with the current location # and other details. - -# pylint: disable=duplicate-code - import time import board import busio diff --git a/examples/gps_time_source.py b/examples/gps_time_source.py index db86637..0572b1a 100644 --- a/examples/gps_time_source.py +++ b/examples/gps_time_source.py @@ -5,19 +5,17 @@ # The GPS timestamps are available without a fix and keep the track of # time while there is powersource (ie coin cell battery) -# pylint: disable=duplicate-code - import time import board import busio import rtc import adafruit_gps -# uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) -i2c = busio.I2C(board.SCL, board.SDA) +uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) +# i2c = busio.I2C(board.SCL, board.SDA) -# gps = adafruit_gps.GPS(uart, debug=False) -gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface +gps = adafruit_gps.GPS(uart, debug=False) +# gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") gps.send_command(b"PMTK220,1000")