Python library for parsing and generating NMEA 0183 protocol messages used in GPS and marine navigation systems
77
Utility functions and mixin classes provide coordinate conversion, timestamp parsing, validation helpers, and extended functionality for NMEA sentence objects.
def timestamp(s: str) -> datetime.time:
"""
Convert HHMMSS[.ss] ASCII string to datetime.time object.
Args:
s: Time string in format HHMMSS or HHMMSS.ss
Returns:
datetime.time object with UTC timezone
Example:
timestamp('184353.07') -> datetime.time(18, 43, 53, 70000, tzinfo=datetime.timezone.utc)
"""
def datestamp(s: str) -> datetime.date:
"""
Convert DDMMYY ASCII string to datetime.date object.
Args:
s: Date string in format DDMMYY
Returns:
datetime.date object
Example:
datestamp('120598') -> datetime.date(1998, 5, 12)
"""def dm_to_sd(dm: str) -> float:
"""
Convert degrees/minutes format to signed decimal degrees.
Args:
dm: Coordinate in DDDMM.MMMM format (e.g., '12319.943281')
Returns:
Decimal degrees as float
Raises:
ValueError: If format is invalid
Example:
dm_to_sd('12319.943281') -> 123.33238801666667
"""def valid(s: str) -> bool:
"""
Check if status flag equals 'A' (active/valid).
Args:
s: Status character
Returns:
True if s == 'A', False otherwise
"""import pynmea2
from pynmea2 import timestamp, datestamp, dm_to_sd, valid
# Parse timestamps
time_obj = timestamp('184353.07')
print(f"Time: {time_obj}") # 18:43:53.070000+00:00
print(f"Hour: {time_obj.hour}") # 18
print(f"Microseconds: {time_obj.microsecond}")# 70000
# Parse dates
date_obj = datestamp('120598')
print(f"Date: {date_obj}") # 1998-05-12
# Convert coordinates
decimal_deg = dm_to_sd('12319.943281')
print(f"Decimal degrees: {decimal_deg}") # 123.33238801666667
# Validate status
print(f"Valid status A: {valid('A')}") # True
print(f"Valid status V: {valid('V')}") # Falseclass LatLonFix:
"""
Mixin adding latitude/longitude properties as signed decimal degrees.
Requires sentence to have lat, lat_dir, lon, lon_dir fields.
"""
@property
def latitude(self) -> float:
"""
Latitude in signed decimal degrees.
Returns:
Positive for North, negative for South
"""
@property
def longitude(self) -> float:
"""
Longitude in signed decimal degrees.
Returns:
Positive for East, negative for West
"""
@property
def latitude_minutes(self) -> float:
"""Minutes component of latitude."""
@property
def longitude_minutes(self) -> float:
"""Minutes component of longitude."""
@property
def latitude_seconds(self) -> float:
"""Seconds component of latitude."""
@property
def longitude_seconds(self) -> float:
"""Seconds component of longitude."""class DatetimeFix:
"""
Mixin adding datetime property combining date and time fields.
Requires sentence to have datestamp and timestamp fields.
"""
@property
def datetime(self) -> datetime.datetime:
"""Combined date and time as datetime object."""class ValidStatusFix:
"""
Mixin adding is_valid property checking status == 'A'.
Requires sentence to have status field.
"""
@property
def is_valid(self) -> bool:
"""True if status field equals 'A'."""
class ValidGGAFix:
"""
Mixin for GGA sentence validity checking.
Requires sentence to have gps_qual field.
"""
@property
def is_valid(self) -> bool:
"""True if GPS quality indicates valid fix (1-5)."""
class ValidGSAFix:
"""
Mixin for GSA sentence validity checking.
Requires sentence to have mode_fix_type field.
"""
@property
def is_valid(self) -> bool:
"""True if fix type is 2D or 3D (2 or 3)."""
class ValidRMCStatusFix(ValidStatusFix):
"""
Extended validity checking for RMC sentences.
Checks status, mode_indicator, and nav_status fields.
"""
@property
def is_valid(self) -> bool:
"""True if all status indicators are valid."""import pynmea2
# LatLonFix example
msg = pynmea2.parse("$GPGGA,184353.07,1929.045,S,02410.506,E,1,04,2.6,100.00,M,-33.9,M,,0000*6D")
# Decimal degrees (most useful)
print(f"Position: {msg.latitude:.6f}, {msg.longitude:.6f}") # -19.484083, 24.175100
# Component access
print(f"Lat minutes: {msg.latitude_minutes:.3f}") # 29.045
print(f"Lon minutes: {msg.longitude_minutes:.3f}") # 10.506
print(f"Lat seconds: {msg.latitude_seconds:.1f}") # 2.7
print(f"Lon seconds: {msg.longitude_seconds:.1f}") # 30.4
# Format coordinates in different ways
abs_lat, abs_lon = abs(msg.latitude), abs(msg.longitude)
print(f"DMS: {abs_lat:.0f}°{msg.latitude_minutes:.3f}'{msg.lat_dir} {abs_lon:.0f}°{msg.longitude_minutes:.3f}'{msg.lon_dir}")
# DatetimeFix example
msg = pynmea2.parse("$GPRMC,184353.07,A,1929.045,S,02410.506,E,0.13,309.62,120598,,A*70")
print(f"Date: {msg.datestamp}") # 1998-05-12
print(f"Time: {msg.timestamp}") # 18:43:53.070000+00:00
print(f"Combined: {msg.datetime}") # 1998-05-12 18:43:53.070000+00:00
# Validation examples
gga_msg = pynmea2.parse("$GPGGA,184353.07,1929.045,S,02410.506,E,1,04,2.6,100.00,M,-33.9,M,,0000*6D")
print(f"GGA valid: {gga_msg.is_valid}") # True (GPS quality = 1)
rmc_msg = pynmea2.parse("$GPRMC,184353.07,A,1929.045,S,02410.506,E,0.13,309.62,120598,,A*70")
print(f"RMC valid: {rmc_msg.is_valid}") # True (status = A)
gsa_msg = pynmea2.parse("$GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39")
print(f"GSA valid: {gsa_msg.is_valid}") # True (3D fix)class TZInfo(datetime.tzinfo):
"""Custom timezone info class for local time zones."""
def __init__(self, hh: int, mm: int):
"""
Initialize with hour and minute offset from UTC.
Args:
hh: Hour offset from UTC
mm: Minute offset from UTC
"""
def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta:
"""Return offset from UTC."""
def tzname(self, dt: datetime.datetime) -> str:
"""Return timezone name."""
def dst(self, dt: datetime.datetime) -> datetime.timedelta:
"""Return DST offset (always 0)."""import pynmea2
from pynmea2 import TZInfo
# Create timezone info for UTC+5:30 (India Standard Time)
ist = TZInfo(5, 30)
msg = pynmea2.parse("$GPZDA,184353.07,12,05,1998,05,30*4F")
utc_time = msg.datetime
# Convert to local time
local_time = utc_time.replace(tzinfo=pynmea2.datetime.timezone.utc).astimezone(ist)
print(f"UTC: {utc_time}") # 1998-05-12 18:43:53.070000
print(f"Local: {local_time}") # 1998-05-13 00:13:53.070000+05:30class SeaTalk:
"""
Mixin adding SeaTalk protocol functionality.
Based on Thomas Knauf's SeaTalk documentation.
Requires sentence to have cmd field.
"""
byte_to_command: Dict[str, str] # Mapping of command bytes to descriptions
@property
def command_name(self) -> str:
"""Get human-readable command name from cmd field."""import pynmea2
# Parse SeaTalk sentence (ALK sentence type)
msg = pynmea2.parse("$STALK,84,96,82,00,00,00,08,02,00*77")
print(f"Command: {msg.cmd}") # 84
print(f"Command name: {msg.command_name}") # Compass heading Autopilot course and Rudder positionInstall with Tessl CLI
npx tessl i tessl/pypi-pynmea2docs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10