Skip to content

Commit 4306467

Browse files
authored
Use UTC for timestamps. Fixes #100 (#151)
use UTC for timestamps. Fixes #100
1 parent 988c297 commit 4306467

File tree

7 files changed

+139
-22
lines changed

7 files changed

+139
-22
lines changed

examples/nmea2gpx.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'''
2+
Convert a NMEA ascii log file into a GPX file
3+
'''
4+
5+
import argparse
6+
import datetime
7+
import logging
8+
import pathlib
9+
import re
10+
import xml.dom.minidom
11+
12+
log = logging.getLogger(__name__)
13+
14+
try:
15+
import pynmea2
16+
except ImportError:
17+
import sys
18+
import pathlib
19+
p = pathlib.Path(__file__).parent.parent
20+
sys.path.append(str(p))
21+
log.info(sys.path)
22+
import pynmea2
23+
24+
25+
def main():
26+
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
27+
parser.add_argument('nmea_file')
28+
29+
args = parser.parse_args()
30+
nmea_file = pathlib.Path(args.nmea_file)
31+
32+
if m := re.match(r'^(\d{2})(\d{2})(\d{2})', nmea_file.name):
33+
date = datetime.date(year=2000 + int(m.group(1)), month=int(m.group(2)), day=int(m.group(3)))
34+
log.debug('date parsed from filename: %r', date)
35+
else:
36+
date = None
37+
38+
author = 'https://github.com/Knio/pynmea2'
39+
doc = xml.dom.minidom.Document()
40+
doc.appendChild(root := doc.createElement('gpx'))
41+
root.setAttribute('xmlns', "http://www.topografix.com/GPX/1/1")
42+
root.setAttribute('version', "1.1")
43+
root.setAttribute('creator', author)
44+
root.setAttribute('xmlns', "http://www.topografix.com/GPX/1/1")
45+
root.setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance")
46+
root.setAttribute('xsi:schemaLocation', "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd")
47+
48+
root.appendChild(meta := doc.createElement('metadata'))
49+
root.appendChild(trk := doc.createElement('trk'))
50+
meta.appendChild(meta_name := doc.createElement('name'))
51+
meta.appendChild(meta_author := doc.createElement('author'))
52+
trk.appendChild(trk_name := doc.createElement('name'))
53+
trk.appendChild(trkseg := doc.createElement('trkseg'))
54+
meta_name.appendChild(doc.createTextNode(nmea_file.name))
55+
trk_name. appendChild(doc.createTextNode(nmea_file.name))
56+
meta_author.appendChild(author_link := doc.createElement('link'))
57+
author_link.setAttribute('href', author)
58+
author_link.appendChild(author_text := doc.createElement('text'))
59+
author_link.appendChild(author_type := doc.createElement('type'))
60+
author_text.appendChild(doc.createTextNode('Pynmea2'))
61+
author_type.appendChild(doc.createTextNode('text/html'))
62+
63+
for line in open(args.nmea_file):
64+
try:
65+
msg = pynmea2.parse(line)
66+
except Exception as e:
67+
log.warning('Couldn\'t parse line: %r', e)
68+
continue
69+
70+
if not (hasattr(msg, 'latitude') and hasattr(msg, 'longitude')):
71+
continue
72+
73+
# if not hasattr(msg, 'altitude'):
74+
# continue
75+
76+
trkseg.appendChild(trkpt := doc.createElement('trkpt'))
77+
78+
trkpt.setAttribute('lat', f'{msg.latitude:.6f}')
79+
trkpt.setAttribute('lon', f'{msg.longitude:.6f}')
80+
if hasattr(msg, 'altitude'):
81+
trkpt.appendChild(ele := doc.createElement('ele'))
82+
ele.appendChild(doc.createTextNode(f'{msg.altitude:.3f}'))
83+
84+
# TODO try msg.datetime
85+
86+
if date:
87+
trkpt.appendChild(time := doc.createElement('time'))
88+
dt = datetime.datetime.combine(date, msg.timestamp)
89+
dts = dt.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
90+
time.appendChild(doc.createTextNode(dts))
91+
92+
xml_data = doc.toprettyxml(
93+
indent=' ',
94+
newl='\n',
95+
encoding='utf8',
96+
).decode('utf8')
97+
print(xml_data)
98+
99+
100+
101+
if __name__ == '__main__':
102+
logging.basicConfig(level=logging.DEBUG)
103+
main()

pynmea2/nmea_utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
import datetime
33
import re
44

5+
6+
# python 2.7 backport
7+
if not hasattr(datetime, 'timezone'):
8+
class UTC(datetime.tzinfo):
9+
def utcoffset(self, dt):
10+
return datetime.timedelta(0)
11+
class timezone(object):
12+
utc = UTC()
13+
datetime.timezone = timezone
14+
15+
516
def valid(s):
617
return s == 'A'
718

@@ -18,7 +29,8 @@ def timestamp(s):
1829
hour=int(s[0:2]),
1930
minute=int(s[2:4]),
2031
second=int(s[4:6]),
21-
microsecond=ms)
32+
microsecond=ms,
33+
tzinfo=datetime.timezone.utc)
2234
return t
2335

2436

pynmea2/types/talker.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ class XTE(TalkerSentence):
507507
)
508508

509509

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

528528
@property
529-
def datetime(self):
529+
def localdatetime(self):
530530
d = datetime.datetime.combine(self.datestamp, self.timestamp)
531-
return d.replace(tzinfo=self.tzinfo)
531+
return d.astimezone(self.tzinfo)
532532

533533

534534

test/test_ash.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_ashratt():
1919
assert type(msg) == pynmea2.ash.ASHRATT
2020
assert msg.data == ['R', '130533.620', '0.311', 'T', '-80.467', '-1.395', '0.25', '0.066', '0.067', '0.215', '2', '3']
2121
assert msg.manufacturer == 'ASH'
22-
assert msg.timestamp == datetime.time(13, 5, 33, 620000)
22+
assert msg.timestamp == datetime.time(13, 5, 33, 620000, tzinfo=datetime.timezone.utc)
2323
assert msg.true_heading == 0.311
2424
assert msg.is_true_heading == 'T'
2525
assert msg.roll == -80.467

test/test_nor.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def test_norbt0():
1111
assert msg.sentence_type == 'NORBT0'
1212
assert msg.beam == 1
1313
assert msg.datestamp == datetime.date(2021, 7, 4)
14-
assert msg.timestamp == datetime.time(13, 13, 35, 334100)
14+
assert msg.timestamp == datetime.time(13, 13, 35, 334100, tzinfo=datetime.timezone.utc)
1515
assert msg.dt1 == 23.961
1616
assert msg.dt2 == -48.122
1717
assert msg.bv == -32.76800
@@ -164,7 +164,7 @@ def test_nors1():
164164
assert msg.manufacturer == 'NOR'
165165
assert msg.sentence_type == 'NORS1'
166166
assert msg.datestamp == datetime.date(2009, 11, 16)
167-
assert msg.timestamp == datetime.time(13, 24, 55)
167+
assert msg.timestamp == datetime.time(13, 24, 55, tzinfo=datetime.timezone.utc)
168168
assert msg.ec == 0
169169
assert msg.sc == '34000034'
170170
assert msg.battery_voltage == 23.9
@@ -203,7 +203,7 @@ def test_norc1():
203203
assert type(msg) == pynmea2.nor.NORC1
204204
assert msg.manufacturer == 'NOR'
205205
assert msg.sentence_type == 'NORC1'
206-
assert msg.datetime == datetime.datetime(2009, 11, 16, 13, 24, 55)
206+
assert msg.datetime == datetime.datetime(2009, 11, 16, 13, 24, 55, tzinfo=datetime.timezone.utc)
207207
assert msg.cn == 3
208208
assert msg.cp == 11.0
209209
assert msg.vx == 0.332
@@ -242,7 +242,7 @@ def test_norh4():
242242
assert msg.manufacturer == 'NOR'
243243
assert msg.sentence_type == 'NORH4'
244244
assert msg.datestamp == datetime.date(2009, 11, 16)
245-
assert msg.timestamp == datetime.time(14, 34, 59)
245+
assert msg.timestamp == datetime.time(14, 34, 59, tzinfo=datetime.timezone.utc)
246246
assert msg.ec == 0
247247
assert msg.sc == '204C0002'
248248
assert msg.render() == data

test/test_proprietary.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def test_ubx00():
138138
assert type(msg) == pynmea2.ubx.UBX00
139139
assert msg.identifier() == 'PUBX'
140140
assert msg.ubx_type == '00'
141-
assert msg.timestamp == datetime.time(7, 44, 40)
141+
assert msg.timestamp == datetime.time(7, 44, 40, tzinfo=datetime.timezone.utc)
142142
assert msg.latitude == 47.06236716666667
143143
assert msg.lat_dir == 'N'
144144
assert msg.render() == data
@@ -157,7 +157,7 @@ def test_ubx04():
157157
msg = pynmea2.parse(data)
158158
assert type(msg) == pynmea2.ubx.UBX04
159159
assert msg.date == datetime.date(2014, 10, 13)
160-
assert msg.time == datetime.time(7, 38, 24)
160+
assert msg.time == datetime.time(7, 38, 24, tzinfo=datetime.timezone.utc)
161161
assert msg.clk_bias == 495176
162162
assert msg.render() == data
163163

@@ -239,7 +239,7 @@ def test_KWDWPL():
239239
data = "$PKWDWPL,053125,V,4531.7900,N,12253.4800,W,,,200320,,AC7FD-1,/-*10"
240240
msg = pynmea2.parse(data)
241241
assert msg.manufacturer == "KWD"
242-
assert msg.timestamp == datetime.time(5, 31, 25)
242+
assert msg.timestamp == datetime.time(5, 31, 25, tzinfo=datetime.timezone.utc)
243243
assert msg.status == 'V'
244244
assert msg.is_valid == False
245245
assert msg.lat == '4531.7900'
@@ -249,7 +249,7 @@ def test_KWDWPL():
249249
assert msg.sog == None
250250
assert msg.cog == None
251251
assert msg.datestamp == datetime.date(2020, 3, 20)
252-
assert msg.datetime == datetime.datetime(2020, 3, 20, 5, 31, 25)
252+
assert msg.datetime == datetime.datetime(2020, 3, 20, 5, 31, 25, tzinfo=datetime.timezone.utc)
253253
assert msg.altitude == None
254254
assert msg.wname == 'AC7FD-1'
255255
assert msg.ts == '/-'

test/test_types.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def test_GGA():
1313
assert isinstance(msg, pynmea2.GGA)
1414

1515
# Timestamp
16-
assert msg.timestamp == datetime.time(18, 43, 53, 70000)
16+
assert msg.timestamp == datetime.time(18, 43, 53, 70000, tzinfo=datetime.timezone.utc)
1717
# Latitude
1818
assert msg.lat == '1929.045'
1919
# Latitude Direction
@@ -99,7 +99,7 @@ def test_GST():
9999
data = "$GPGST,172814.0,0.006,0.023,0.020,273.6,0.023,0.020,0.031*6A"
100100
msg = pynmea2.parse(data)
101101
assert isinstance(msg, pynmea2.GST)
102-
assert msg.timestamp == datetime.time(hour=17, minute=28, second=14)
102+
assert msg.timestamp == datetime.time(hour=17, minute=28, second=14, tzinfo=datetime.timezone.utc)
103103
assert msg.rms == 0.006
104104
assert msg.std_dev_major == 0.023
105105
assert msg.std_dev_minor == 0.020
@@ -114,11 +114,11 @@ def test_RMC():
114114
data = '''$GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68'''
115115
msg = pynmea2.parse(data)
116116
assert isinstance(msg, pynmea2.RMC)
117-
assert msg.timestamp == datetime.time(hour=22, minute=54, second=46)
117+
assert msg.timestamp == datetime.time(hour=22, minute=54, second=46, tzinfo=datetime.timezone.utc)
118118
assert msg.datestamp == datetime.date(1994, 11, 19)
119119
assert msg.latitude == 49.274166666666666
120120
assert msg.longitude == -123.18533333333333
121-
assert msg.datetime == datetime.datetime(1994, 11, 19, 22, 54, 46)
121+
assert msg.datetime == datetime.datetime(1994, 11, 19, 22, 54, 46, tzinfo=datetime.timezone.utc)
122122
assert msg.is_valid == True
123123
assert msg.render() == data
124124

@@ -129,7 +129,7 @@ def test_RMC_valid():
129129
only test validation against supplied values.
130130
131131
Supplied means that a `,` exists it does NOT mean that a value had to be
132-
supplied in the space provided. See
132+
supplied in the space provided. See
133133
134134
https://orolia.com/manuals/VSP/Content/NC_and_SS/Com/Topics/APPENDIX/NMEA_RMCmess.htm
135135
@@ -140,7 +140,7 @@ def test_RMC_valid():
140140
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,*33',
141141
'$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,*24',
142142
'$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,*72',
143-
143+
144144
# RMC Timing Messages
145145
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S*4C',
146146
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,N*51',
@@ -151,7 +151,7 @@ def test_RMC_valid():
151151
'$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,S*0D',
152152
'$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,N*10',
153153
'$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,*5E',
154-
154+
155155
# RMC Nav Messags
156156
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,S*33',
157157
'$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,V*36',
@@ -204,14 +204,16 @@ def test_ZDA():
204204
data = '''$GPZDA,010203.05,06,07,2008,-08,30'''
205205
msg = pynmea2.parse(data)
206206
assert isinstance(msg, pynmea2.ZDA)
207-
assert msg.timestamp == datetime.time(hour=1, minute=2, second=3, microsecond=50000)
207+
assert msg.timestamp == datetime.time(hour=1, minute=2, second=3, microsecond=50000, tzinfo=datetime.timezone.utc)
208208
assert msg.day == 6
209209
assert msg.month == 7
210210
assert msg.year == 2008
211+
assert msg.tzinfo.utcoffset(0) == datetime.timedelta(hours=-8, minutes=30)
211212
assert msg.local_zone == -8
212213
assert msg.local_zone_minutes == 30
213214
assert msg.datestamp == datetime.date(2008, 7, 6)
214-
assert msg.datetime == datetime.datetime(2008, 7, 6, 1, 2, 3, 50000, msg.tzinfo)
215+
assert msg.datetime == datetime.datetime(2008, 7, 6, 1, 2, 3, 50000, tzinfo=datetime.timezone.utc)
216+
assert msg.localdatetime == datetime.datetime(2008, 7, 5, 17, 32, 3, 50000, tzinfo=msg.tzinfo)
215217

216218
def test_VPW():
217219
data = "$XXVPW,1.2,N,3.4,M"

0 commit comments

Comments
 (0)