GPS & NMEA
Parse GPS NMEA sentences with PySerial and pynmea2. Read coordinates, speed, and satellite data from serial GPS receivers in Python.
Read and parse NMEA 0183 sentences from a serial GPS receiver using PySerial and the pynmea2 library.
Read GPS Data with pynmea2
Install pynmea2 (pip install pynmea2), then read position fixes from a serial GPS module:
import serial
import pynmea2
ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)
while True:
line = ser.readline().decode('ascii', errors='ignore').strip()
if not line.startswith('$'):
continue
try:
msg = pynmea2.parse(line)
except pynmea2.ParseError:
continue
if isinstance(msg, pynmea2.GGA):
print(f"Lat: {msg.latitude:.6f}, Lon: {msg.longitude:.6f}, Alt: {msg.altitude}m")
print(f"Fix quality: {msg.gps_qual}, Satellites: {msg.num_sats}")
elif isinstance(msg, pynmea2.RMC):
print(f"Speed: {msg.spd_over_grnd} knots, Course: {msg.true_course}")
print(f"Date/Time: {msg.datetime}")Log GPS test results automatically
TofuPilot records test results from your PySerial scripts, tracks pass/fail rates, and generates compliance reports. Free to start.
NMEA Sentence Reference
Common sentence types from GPS receivers:
| Sentence | Name | Key Fields |
|---|---|---|
| GGA | Fix Data | Lat, lon, altitude, fix quality, satellite count, HDOP |
| RMC | Recommended Minimum | Lat, lon, speed, course, date/time, magnetic variation |
| GSA | DOP and Active Satellites | Fix type (2D/3D), PDOP, HDOP, VDOP, PRN list |
| GSV | Satellites in View | Satellite count, PRN, elevation, azimuth, SNR |
| VTG | Track and Ground Speed | True course, magnetic course, speed (knots and km/h) |
NMEA sentence format:
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
| | | | | | | | | | checksum
| | | | | | | | | geoid height
| | | | | | | | altitude
| | | | | | | HDOP
| | | | | | satellites used
| | | | | fix quality
| | | | longitude
| | latitude
| UTC time
talker + sentence typeManual NMEA Parsing
If you can't install pynmea2, parse NMEA manually. Coordinates use DDMM.MMMM format.
def nmea_checksum_valid(sentence):
if '*' not in sentence:
return False
body, checksum = sentence[1:].split('*')
calc = 0
for ch in body:
calc ^= ord(ch)
return f"{calc:02X}" == checksum.upper()
def nmea_to_decimal(coord, direction):
"""Convert NMEA DDMM.MMMM to decimal degrees."""
dot = coord.index('.')
degrees = int(coord[:dot - 2])
minutes = float(coord[dot - 2:])
dd = degrees + minutes / 60.0
if direction in ('S', 'W'):
dd = -dd
return dd
def parse_gga(sentence):
"""Parse a GGA sentence and return a dict."""
if not nmea_checksum_valid(sentence):
return None
fields = sentence.split(',')
if len(fields) < 15 or fields[6] == '0':
return None
return {
'latitude': nmea_to_decimal(fields[2], fields[3]),
'longitude': nmea_to_decimal(fields[4], fields[5]),
'altitude': float(fields[9]) if fields[9] else None,
'satellites': int(fields[7]) if fields[7] else 0,
'hdop': float(fields[8]) if fields[8] else None,
}
# Test with a sample sentence
sample = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47"
result = parse_gga(sample)
if result:
print(f"Lat: {result['latitude']:.6f}, Lon: {result['longitude']:.6f}")Log GPS Data to CSV
import serial
import pynmea2
import csv
from datetime import datetime
def log_gps(port, csv_path="gps_log.csv", duration=60):
ser = serial.Serial(port, 9600, timeout=1)
with open(csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "latitude", "longitude", "altitude", "speed_knots", "satellites"])
import time
start = time.time()
while time.time() - start < duration:
line = ser.readline().decode('ascii', errors='ignore').strip()
if not line.startswith('$'):
continue
try:
msg = pynmea2.parse(line)
except pynmea2.ParseError:
continue
if isinstance(msg, pynmea2.GGA) and msg.gps_qual > 0:
writer.writerow([
datetime.utcnow().isoformat(),
f"{msg.latitude:.6f}",
f"{msg.longitude:.6f}",
msg.altitude or "",
"",
msg.num_sats,
])
elif isinstance(msg, pynmea2.RMC) and msg.status == 'A':
writer.writerow([
datetime.utcnow().isoformat(),
f"{msg.latitude:.6f}",
f"{msg.longitude:.6f}",
"",
msg.spd_over_grnd or "",
"",
])
ser.close()
print(f"GPS data saved to {csv_path}")
log_gps("/dev/ttyUSB0", duration=300)Common GPS Module Settings
| Module | Default Baud | Update Rate | Notes |
|---|---|---|---|
| u-blox NEO-6M/7M/M8 | 9600 | 1 Hz | Configurable via UBX protocol |
| Adafruit Ultimate GPS | 9600 | 1 Hz | Up to 10 Hz with PMTK command |
| BN-880 (GPS + compass) | 9600 | 1 Hz | Dual GNSS (GPS + GLONASS) |
| SIM28/SIM68 | 9600 | 1 Hz | Low power |
Most GPS modules output at 9600 baud by default. If you get garbled data, check your baud rate. Some modules need an antenna with clear sky view before they produce a valid fix (cold start can take 30+ seconds).