From b2b88cd7019c9074a8752d5133d08d2963f50e6d Mon Sep 17 00:00:00 2001 From: tomas-fryza Date: Tue, 18 Feb 2025 15:46:33 +0100 Subject: [PATCH] basic gps scripts added --- examples/11-gps/main.py | 97 +++++++------- examples/adafruit_gps.py | 246 ++++++++++++++++++++++++++++++++++++ examples/micropython-gps.py | 91 +++++++++++++ 3 files changed, 387 insertions(+), 47 deletions(-) create mode 100644 examples/adafruit_gps.py create mode 100644 examples/micropython-gps.py diff --git a/examples/11-gps/main.py b/examples/11-gps/main.py index 98fbc80..3f43baa 100644 --- a/examples/11-gps/main.py +++ b/examples/11-gps/main.py @@ -1,22 +1,14 @@ # https://RandomNerdTutorials.com/micropython-esp32-neo-6m-gps/ # https://microcontrollerslab.com/neo-6m-gps-module-esp32-micropython/ +# https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm?tocpath=Commands%20%2526%20Logs%7CLogs%7CGNSS%20Logs%7C_____59 + from machine import Pin, UART import utime, time -gpsModule = UART(2, baudrate=9600)#, tx=17, rx=16) +gpsModule = UART(2, baudrate=9600) # tx=17 (D10), rx=16 (D11) print(gpsModule) -""" -while True: - if gpsModule.any(): - line = gpsModule.readline() # Read a complete line from the UART - if line: - line = line.decode('utf-8') - print(line.strip()) - sleep(3) -""" - buff = bytearray(255) TIMEOUT = False @@ -30,31 +22,34 @@ def getGPS(gpsModule): global FIX_STATUS, TIMEOUT, latitude, longitude, satellites, GPStime - - timeout = time.time() + 8 + + timeout = time.time() + 8 + while True: gpsModule.readline() buff = str(gpsModule.readline()) parts = buff.split(',') - + print(parts[0], len(parts)) + if (parts[0] == "b'$GPGGA" and len(parts) == 15): - if(parts[1] and parts[2] and parts[3] and parts[4] and parts[5] and parts[6] and parts[7]): - print(buff) - - latitude = convertToDegree(parts[2]) - if (parts[3] == 'S'): - latitude = -latitude - longitude = convertToDegree(parts[4]) - if (parts[5] == 'W'): - longitude = -longitude - satellites = parts[7] - GPStime = parts[1][0:2] + ":" + parts[1][2:4] + ":" + parts[1][4:6] - FIX_STATUS = True - break - + # if(parts[1] and parts[2] and parts[3] and parts[4] and parts[5] and parts[6] and parts[7]): + # print(buff) + + # latitude = convertToDegree(parts[2]) + # if (parts[3] == 'S'): + # latitude = -latitude + # longitude = convertToDegree(parts[4]) + # if (parts[5] == 'W'): + # longitude = -longitude + # satellites = parts[7] + GPStime = parts[1][0:2] + ":" + parts[1][2:4] + ":" + parts[1][4:] + FIX_STATUS = True + print(GPStime) + # break + if (time.time() > timeout): TIMEOUT = True - break + # break utime.sleep_ms(500) @@ -63,24 +58,32 @@ def convertToDegree(RawDegrees): RawAsFloat = float(RawDegrees) firstdigits = int(RawAsFloat/100) nexttwodigits = RawAsFloat - float(firstdigits*100) - + Converted = float(firstdigits + nexttwodigits/60.0) Converted = '{0:.6f}'.format(Converted) return str(Converted) - - -while True: - print() - getGPS(gpsModule) - - if(FIX_STATUS == True): - print("Latitude: "+latitude) - print("Longitude: "+longitude) - print("Satellites: " +satellites) - print("Time: "+GPStime) - - FIX_STATUS = False - - if(TIMEOUT == True): - # print("No GPS data is found.") - TIMEOUT = False + + +print("\r\nPress `Ctrl+C` to stop") + +try: + while True: + getGPS(gpsModule) + + if(FIX_STATUS == True): + print("Latitude: "+latitude) + print("Longitude: "+longitude) + print("Satellites: " +satellites) + print("Time: "+GPStime) + + FIX_STATUS = False + + if(TIMEOUT == True): + print("No GPS data is found.") + TIMEOUT = False + +except KeyboardInterrupt: + # This part runs when Ctrl+C is pressed + print("Program stopped. Exiting...") + + # Optional cleanup code diff --git a/examples/adafruit_gps.py b/examples/adafruit_gps.py new file mode 100644 index 0000000..03b3ab6 --- /dev/null +++ b/examples/adafruit_gps.py @@ -0,0 +1,246 @@ +# 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, Alexandre Marquet. + +Implementation Notes +-------------------- + +**Hardware:** + +* Adafruit `Ultimate GPS Breakout `_ +* Adafruit `Ultimate GPS FeatherWing `_ + +**Software and Dependencies:** + +* MicroPython: + https://github.com/micropython/micropython + +""" +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/alexmrqt/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.decode()) + deg = raw // 100 + minutes = raw % 100 + return deg + minutes/60 + +def _parse_int(nmea_data): + if nmea_data is None or nmea_data == b'': + return None + return int(nmea_data) + +def _parse_float(nmea_data): + if nmea_data is None or nmea_data == b'': + return None + return float(nmea_data.decode()) + +# 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. + + :param uart: The `UART` object to use. + """ + 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 == b'GPGGA': # GGA, 3d location fix + self._parse_gpgga(args) + elif data_type == b'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 = sentence.strip() + # Look for a checksum and validate it if present. + if len(sentence) > 7 and sentence[-3] == ord('*'): + # Get included checksum, then calculate it and compare. + expected = int(sentence[-2:], 16) + actual = 0 + for i in range(1, len(sentence)-3): + actual ^= 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(b',') + 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(b',') + 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 = ( + self.timestamp_utc[0], self.timestamp_utc[1], + self.timestamp_utc[2], hours, mins, secs, 0, 0) + else: + self.timestamp_utc = (0, 0, 0, hours, mins, secs, 0, 0) + # 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() == b'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() == b'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(b',') + if data is None or len(data) < 11 or data[0] is None: + 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 = ( + self.timestamp_utc[0], self.timestamp_utc[1], + self.timestamp_utc[2], hours, mins, secs, 0, 0) + else: + self.timestamp_utc = (0, 0, 0, hours, mins, secs, 0, 0) + # Parse status (active/fixed or void). + status = data[1] + self.fix_quality = 0 + if status is not None and status.lower() == b'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() == b'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() == b'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. + self.timestamp_utc = (year, month, day, + self.timestamp_utc[3], + self.timestamp_utc[4], + self.timestamp_utc[5], + 0, + 0) + else: + # Time hasn't been set so create it. + self.timestamp_utc = (year, month, day, 0, 0, 0, 0, 0) \ No newline at end of file diff --git a/examples/micropython-gps.py b/examples/micropython-gps.py new file mode 100644 index 0000000..3451e17 --- /dev/null +++ b/examples/micropython-gps.py @@ -0,0 +1,91 @@ + +# https://github.com/alexmrqt/micropython-gps/tree/master + +# Simple GPS module demonstration. +# Will wait for a fix and print a message every second with the current location +# and other details. +from machine import UART +import utime as time + +import adafruit_gps + +# Create a GPS module instance. +uart = UART(2, baudrate=9600) #, timeout_chars=3000) #, pins=('P8','P2')) +print(uart) + +# Create a GPS module instance. +gps = adafruit_gps.GPS(uart) + +# Initialize the GPS module by changing what data it sends and at what rate. +# These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and +# PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust +# the GPS module behavior: +# https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf + +# Turn on the basic GGA and RMC info (what you typically want) +gps.send_command('PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') +# Turn on just minimum info (RMC only, location): +# gps.send_command('PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') +# Turn off everything: +# gps.send_command('PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') +# Turn on everything (not all of it is parsed!) +# gps.send_command('PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0') + +# Set update rate to once a second (1hz) which is what you typically want. +gps.send_command('PMTK220,1000') +# Or decrease to once every two seconds by doubling the millisecond value. +# Be sure to also increase your UART timeout above! +# gps.send_command('PMTK220,2000') +# You can also speed up the rate, but don't go too fast or else you can lose +# data during parsing. This would be twice a second (2hz, 500ms delay): +# gps.send_command('PMTK220,500') + +# Main loop runs forever printing the location, etc. every second. +last_print = time.ticks_ms() + +while True: + # Make sure to call gps.update() every loop iteration and at least twice + # as fast as data comes from the GPS unit (usually every second). + # This returns a bool that's true if it parsed new data (you can ignore it + # though if you don't care and instead look at the has_fix property). + status = gps.update() + + # Every second print out current location details if there's a fix. + current = time.ticks_ms() + + if (time.ticks_diff(current, last_print) >= 1000) and status: + last_print = current + print(last_print, status) + + if not gps.has_fix: + # Try again if we don't have a fix yet. + print('Waiting for fix...') + continue + + # We have a fix! (gps.has_fix is true) + # Print out details about the fix like location, date, etc. + print('=' * 40) # Print a separator line. + print('Fix timestamp: {}/{}/{} {:02}:{:02}:{:02}'.format( + gps.timestamp_utc[1], # Grab parts of the time from the + gps.timestamp_utc[2], # struct_time object that holds + gps.timestamp_utc[0], # the fix time. Note you might + gps.timestamp_utc[3], # not get all data like year, day, + gps.timestamp_utc[4], # month! + gps.timestamp_utc[5])) + print('Latitude: {} degrees'.format(gps.latitude)) + print('Longitude: {} degrees'.format(gps.longitude)) + print('Fix quality: {}'.format(gps.fix_quality)) + # Some attributes beyond latitude, longitude and timestamp are optional + # and might not be present. Check if they're None before trying to use! + if gps.satellites is not None: + print('# satellites: {}'.format(gps.satellites)) + if gps.altitude_m is not None: + print('Altitude: {} meters'.format(gps.altitude_m)) + if gps.track_angle_deg is not None: + print('Speed: {} knots'.format(gps.speed_knots)) + if gps.track_angle_deg is not None: + print('Track angle: {} degrees'.format(gps.track_angle_deg)) + if gps.horizontal_dilution is not None: + print('Horizontal dilution: {}'.format(gps.horizontal_dilution)) + if gps.height_geoid is not None: + print('Height geo ID: {} meters'.format(gps.height_geoid))