Skip to content

Commit

Permalink
Use UTC for timestamps. Fixes #100 (#151)
Browse files Browse the repository at this point in the history
use UTC for timestamps. Fixes #100
  • Loading branch information
Knio authored Oct 23, 2022
1 parent 988c297 commit 4306467
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 22 deletions.
103 changes: 103 additions & 0 deletions examples/nmea2gpx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'''
Convert a NMEA ascii log file into a GPX file
'''

import argparse
import datetime
import logging
import pathlib
import re
import xml.dom.minidom

log = logging.getLogger(__name__)

try:
import pynmea2
except ImportError:
import sys
import pathlib
p = pathlib.Path(__file__).parent.parent
sys.path.append(str(p))
log.info(sys.path)
import pynmea2


def main():
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('nmea_file')

args = parser.parse_args()
nmea_file = pathlib.Path(args.nmea_file)

if m := re.match(r'^(\d{2})(\d{2})(\d{2})', nmea_file.name):
date = datetime.date(year=2000 + int(m.group(1)), month=int(m.group(2)), day=int(m.group(3)))
log.debug('date parsed from filename: %r', date)
else:
date = None

author = 'https://github.com/Knio/pynmea2'
doc = xml.dom.minidom.Document()
doc.appendChild(root := doc.createElement('gpx'))
root.setAttribute('xmlns', "http://www.topografix.com/GPX/1/1")
root.setAttribute('version', "1.1")
root.setAttribute('creator', author)
root.setAttribute('xmlns', "http://www.topografix.com/GPX/1/1")
root.setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance")
root.setAttribute('xsi:schemaLocation', "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd")

root.appendChild(meta := doc.createElement('metadata'))
root.appendChild(trk := doc.createElement('trk'))
meta.appendChild(meta_name := doc.createElement('name'))
meta.appendChild(meta_author := doc.createElement('author'))
trk.appendChild(trk_name := doc.createElement('name'))
trk.appendChild(trkseg := doc.createElement('trkseg'))
meta_name.appendChild(doc.createTextNode(nmea_file.name))
trk_name. appendChild(doc.createTextNode(nmea_file.name))
meta_author.appendChild(author_link := doc.createElement('link'))
author_link.setAttribute('href', author)
author_link.appendChild(author_text := doc.createElement('text'))
author_link.appendChild(author_type := doc.createElement('type'))
author_text.appendChild(doc.createTextNode('Pynmea2'))
author_type.appendChild(doc.createTextNode('text/html'))

for line in open(args.nmea_file):
try:
msg = pynmea2.parse(line)
except Exception as e:
log.warning('Couldn\'t parse line: %r', e)
continue

if not (hasattr(msg, 'latitude') and hasattr(msg, 'longitude')):
continue

# if not hasattr(msg, 'altitude'):
# continue

trkseg.appendChild(trkpt := doc.createElement('trkpt'))

trkpt.setAttribute('lat', f'{msg.latitude:.6f}')
trkpt.setAttribute('lon', f'{msg.longitude:.6f}')
if hasattr(msg, 'altitude'):
trkpt.appendChild(ele := doc.createElement('ele'))
ele.appendChild(doc.createTextNode(f'{msg.altitude:.3f}'))

# TODO try msg.datetime

if date:
trkpt.appendChild(time := doc.createElement('time'))
dt = datetime.datetime.combine(date, msg.timestamp)
dts = dt.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
time.appendChild(doc.createTextNode(dts))

xml_data = doc.toprettyxml(
indent=' ',
newl='\n',
encoding='utf8',
).decode('utf8')
print(xml_data)



if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
main()
14 changes: 13 additions & 1 deletion pynmea2/nmea_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
import datetime
import re


# python 2.7 backport
if not hasattr(datetime, 'timezone'):
class UTC(datetime.tzinfo):
def utcoffset(self, dt):
return datetime.timedelta(0)
class timezone(object):
utc = UTC()
datetime.timezone = timezone


def valid(s):
return s == 'A'

Expand All @@ -18,7 +29,8 @@ def timestamp(s):
hour=int(s[0:2]),
minute=int(s[2:4]),
second=int(s[4:6]),
microsecond=ms)
microsecond=ms,
tzinfo=datetime.timezone.utc)
return t


Expand Down
6 changes: 3 additions & 3 deletions pynmea2/types/talker.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ class XTE(TalkerSentence):
)


class ZDA(TalkerSentence):
class ZDA(TalkerSentence, DatetimeFix):
fields = (
("Timestamp", "timestamp", timestamp), # hhmmss.ss = UTC
("Day", "day", int), # 01 to 31
Expand All @@ -526,9 +526,9 @@ def tzinfo(self):
return TZInfo(self.local_zone, self.local_zone_minutes)

@property
def datetime(self):
def localdatetime(self):
d = datetime.datetime.combine(self.datestamp, self.timestamp)
return d.replace(tzinfo=self.tzinfo)
return d.astimezone(self.tzinfo)



Expand Down
2 changes: 1 addition & 1 deletion test/test_ash.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_ashratt():
assert type(msg) == pynmea2.ash.ASHRATT
assert msg.data == ['R', '130533.620', '0.311', 'T', '-80.467', '-1.395', '0.25', '0.066', '0.067', '0.215', '2', '3']
assert msg.manufacturer == 'ASH'
assert msg.timestamp == datetime.time(13, 5, 33, 620000)
assert msg.timestamp == datetime.time(13, 5, 33, 620000, tzinfo=datetime.timezone.utc)
assert msg.true_heading == 0.311
assert msg.is_true_heading == 'T'
assert msg.roll == -80.467
Expand Down
8 changes: 4 additions & 4 deletions test/test_nor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_norbt0():
assert msg.sentence_type == 'NORBT0'
assert msg.beam == 1
assert msg.datestamp == datetime.date(2021, 7, 4)
assert msg.timestamp == datetime.time(13, 13, 35, 334100)
assert msg.timestamp == datetime.time(13, 13, 35, 334100, tzinfo=datetime.timezone.utc)
assert msg.dt1 == 23.961
assert msg.dt2 == -48.122
assert msg.bv == -32.76800
Expand Down Expand Up @@ -164,7 +164,7 @@ def test_nors1():
assert msg.manufacturer == 'NOR'
assert msg.sentence_type == 'NORS1'
assert msg.datestamp == datetime.date(2009, 11, 16)
assert msg.timestamp == datetime.time(13, 24, 55)
assert msg.timestamp == datetime.time(13, 24, 55, tzinfo=datetime.timezone.utc)
assert msg.ec == 0
assert msg.sc == '34000034'
assert msg.battery_voltage == 23.9
Expand Down Expand Up @@ -203,7 +203,7 @@ def test_norc1():
assert type(msg) == pynmea2.nor.NORC1
assert msg.manufacturer == 'NOR'
assert msg.sentence_type == 'NORC1'
assert msg.datetime == datetime.datetime(2009, 11, 16, 13, 24, 55)
assert msg.datetime == datetime.datetime(2009, 11, 16, 13, 24, 55, tzinfo=datetime.timezone.utc)
assert msg.cn == 3
assert msg.cp == 11.0
assert msg.vx == 0.332
Expand Down Expand Up @@ -242,7 +242,7 @@ def test_norh4():
assert msg.manufacturer == 'NOR'
assert msg.sentence_type == 'NORH4'
assert msg.datestamp == datetime.date(2009, 11, 16)
assert msg.timestamp == datetime.time(14, 34, 59)
assert msg.timestamp == datetime.time(14, 34, 59, tzinfo=datetime.timezone.utc)
assert msg.ec == 0
assert msg.sc == '204C0002'
assert msg.render() == data
Expand Down
8 changes: 4 additions & 4 deletions test/test_proprietary.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def test_ubx00():
assert type(msg) == pynmea2.ubx.UBX00
assert msg.identifier() == 'PUBX'
assert msg.ubx_type == '00'
assert msg.timestamp == datetime.time(7, 44, 40)
assert msg.timestamp == datetime.time(7, 44, 40, tzinfo=datetime.timezone.utc)
assert msg.latitude == 47.06236716666667
assert msg.lat_dir == 'N'
assert msg.render() == data
Expand All @@ -157,7 +157,7 @@ def test_ubx04():
msg = pynmea2.parse(data)
assert type(msg) == pynmea2.ubx.UBX04
assert msg.date == datetime.date(2014, 10, 13)
assert msg.time == datetime.time(7, 38, 24)
assert msg.time == datetime.time(7, 38, 24, tzinfo=datetime.timezone.utc)
assert msg.clk_bias == 495176
assert msg.render() == data

Expand Down Expand Up @@ -239,7 +239,7 @@ def test_KWDWPL():
data = "$PKWDWPL,053125,V,4531.7900,N,12253.4800,W,,,200320,,AC7FD-1,/-*10"
msg = pynmea2.parse(data)
assert msg.manufacturer == "KWD"
assert msg.timestamp == datetime.time(5, 31, 25)
assert msg.timestamp == datetime.time(5, 31, 25, tzinfo=datetime.timezone.utc)
assert msg.status == 'V'
assert msg.is_valid == False
assert msg.lat == '4531.7900'
Expand All @@ -249,7 +249,7 @@ def test_KWDWPL():
assert msg.sog == None
assert msg.cog == None
assert msg.datestamp == datetime.date(2020, 3, 20)
assert msg.datetime == datetime.datetime(2020, 3, 20, 5, 31, 25)
assert msg.datetime == datetime.datetime(2020, 3, 20, 5, 31, 25, tzinfo=datetime.timezone.utc)
assert msg.altitude == None
assert msg.wname == 'AC7FD-1'
assert msg.ts == '/-'
Expand Down
20 changes: 11 additions & 9 deletions test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_GGA():
assert isinstance(msg, pynmea2.GGA)

# Timestamp
assert msg.timestamp == datetime.time(18, 43, 53, 70000)
assert msg.timestamp == datetime.time(18, 43, 53, 70000, tzinfo=datetime.timezone.utc)
# Latitude
assert msg.lat == '1929.045'
# Latitude Direction
Expand Down Expand Up @@ -99,7 +99,7 @@ def test_GST():
data = "$GPGST,172814.0,0.006,0.023,0.020,273.6,0.023,0.020,0.031*6A"
msg = pynmea2.parse(data)
assert isinstance(msg, pynmea2.GST)
assert msg.timestamp == datetime.time(hour=17, minute=28, second=14)
assert msg.timestamp == datetime.time(hour=17, minute=28, second=14, tzinfo=datetime.timezone.utc)
assert msg.rms == 0.006
assert msg.std_dev_major == 0.023
assert msg.std_dev_minor == 0.020
Expand All @@ -114,11 +114,11 @@ def test_RMC():
data = '''$GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68'''
msg = pynmea2.parse(data)
assert isinstance(msg, pynmea2.RMC)
assert msg.timestamp == datetime.time(hour=22, minute=54, second=46)
assert msg.timestamp == datetime.time(hour=22, minute=54, second=46, tzinfo=datetime.timezone.utc)
assert msg.datestamp == datetime.date(1994, 11, 19)
assert msg.latitude == 49.274166666666666
assert msg.longitude == -123.18533333333333
assert msg.datetime == datetime.datetime(1994, 11, 19, 22, 54, 46)
assert msg.datetime == datetime.datetime(1994, 11, 19, 22, 54, 46, tzinfo=datetime.timezone.utc)
assert msg.is_valid == True
assert msg.render() == data

Expand All @@ -129,7 +129,7 @@ def test_RMC_valid():
only test validation against supplied values.
Supplied means that a `,` exists it does NOT mean that a value had to be
supplied in the space provided. See
supplied in the space provided. See
https://orolia.com/manuals/VSP/Content/NC_and_SS/Com/Topics/APPENDIX/NMEA_RMCmess.htm
Expand All @@ -140,7 +140,7 @@ def test_RMC_valid():
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,*33',
'$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,*24',
'$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,*72',

# RMC Timing Messages
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S*4C',
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,N*51',
Expand All @@ -151,7 +151,7 @@ def test_RMC_valid():
'$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,S*0D',
'$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,N*10',
'$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,*5E',

# RMC Nav Messags
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,S*33',
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,V*36',
Expand Down Expand Up @@ -204,14 +204,16 @@ def test_ZDA():
data = '''$GPZDA,010203.05,06,07,2008,-08,30'''
msg = pynmea2.parse(data)
assert isinstance(msg, pynmea2.ZDA)
assert msg.timestamp == datetime.time(hour=1, minute=2, second=3, microsecond=50000)
assert msg.timestamp == datetime.time(hour=1, minute=2, second=3, microsecond=50000, tzinfo=datetime.timezone.utc)
assert msg.day == 6
assert msg.month == 7
assert msg.year == 2008
assert msg.tzinfo.utcoffset(0) == datetime.timedelta(hours=-8, minutes=30)
assert msg.local_zone == -8
assert msg.local_zone_minutes == 30
assert msg.datestamp == datetime.date(2008, 7, 6)
assert msg.datetime == datetime.datetime(2008, 7, 6, 1, 2, 3, 50000, msg.tzinfo)
assert msg.datetime == datetime.datetime(2008, 7, 6, 1, 2, 3, 50000, tzinfo=datetime.timezone.utc)
assert msg.localdatetime == datetime.datetime(2008, 7, 5, 17, 32, 3, 50000, tzinfo=msg.tzinfo)

def test_VPW():
data = "$XXVPW,1.2,N,3.4,M"
Expand Down

0 comments on commit 4306467

Please sign in to comment.