PySerial
PySerialDocs

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:

SentenceNameKey Fields
GGAFix DataLat, lon, altitude, fix quality, satellite count, HDOP
RMCRecommended MinimumLat, lon, speed, course, date/time, magnetic variation
GSADOP and Active SatellitesFix type (2D/3D), PDOP, HDOP, VDOP, PRN list
GSVSatellites in ViewSatellite count, PRN, elevation, azimuth, SNR
VTGTrack and Ground SpeedTrue 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 type

Manual 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

ModuleDefault BaudUpdate RateNotes
u-blox NEO-6M/7M/M896001 HzConfigurable via UBX protocol
Adafruit Ultimate GPS96001 HzUp to 10 Hz with PMTK command
BN-880 (GPS + compass)96001 HzDual GNSS (GPS + GLONASS)
SIM28/SIM6896001 HzLow 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).