Skip to content

Commit 3a7c7c7

Browse files
committed
Prior to asynchronous parsing.
1 parent 2980c2a commit 3a7c7c7

8 files changed

+412
-69
lines changed

gps/README.md

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ on this excellent library [micropyGPS].
1414
based on the popular MTK3329/MTK3339 chips.
1515
* The above drivers are portable between [MicroPython] and Python 3.5 or above.
1616
* Timing drivers for [MicroPython] only extend the capabilities of the
17-
read-only and read-write drivers to provide precision μs class GPS timing. On
17+
read-only and read-write drivers to provide accurate sub-ms GPS timing. On
1818
STM-based hosts (e.g. the Pyboard) the RTC may be set from GPS and calibrated
1919
to achieve timepiece-level accuracy.
20+
* Can write `.kml` files for displaying journeys on Google Earth.
2021
* Drivers may be extended via subclassing, for example to support additional
2122
sentence types.
2223

@@ -28,8 +29,8 @@ driver as they emit NMEA sentences on startup.
2829

2930
NMEA sentence parsing is based on [micropyGPS] but with significant changes.
3031

31-
* As asynchronous drivers they require `uasyncio` on [MicroPython] or asyncio
32-
under Python 3.5+.
32+
* As asynchronous drivers they require `uasyncio` on [MicroPython] or
33+
`asyncio` under Python 3.5+.
3334
* Sentence parsing is adapted for asynchronous use.
3435
* Rollover of local time into the date value enables worldwide use.
3536
* RAM allocation is cut by various techniques to lessen heap fragmentation.
@@ -359,8 +360,8 @@ The following are counts since instantiation.
359360
* `local_time` (property) [hrs: int, mins: int, secs: int] Local time.
360361
* `date` (property) [day: int, month: int, year: int] e.g. [23, 3, 18]
361362
* `local_offset` Local time offset in hrs as specified to constructor.
362-
* `epoch_time` Integer. Time since the epoch. Epoch start depends on whether
363-
running under MicroPython or Python 3.5+.
363+
* `epoch_time` Integer. Time in seconds since the epoch. Epoch start depends
364+
on whether running under MicroPython (Y2K) or Python 3.5+ (1970 on Unix).
364365

365366
The `utc`, `date` and `local_time` properties updates on receipt of RMC
366367
messages. If a nonzero `local_offset` value is specified the `date` value will
@@ -532,6 +533,15 @@ cleared by power cycling the GPS.
532533

533534
### 3.3.1 Changing baudrate
534535

536+
I have experienced failures on a Pyboard V1.1 at baudrates higher than 19200.
537+
Under investigation. **TODO UPDATE THIS**
538+
539+
Further, there are problems (at least with my GPS firmware build
540+
['AXN_2.31_3339_13101700', '5632', 'PA6H', '1.0']) whereby setting baudrates
541+
only works for certain rates. 19200, 38400 and 115200 work. 4800 sets 115200.
542+
Importantly 9600 does nothing. This means that the only way to restore the
543+
default is to perform a `FULL_COLD_START`. The test programs do this.
544+
535545
If you change the GPS baudrate the UART should be re-initialised immediately
536546
after the `baudrate` coroutine terminates:
537547

@@ -605,7 +615,8 @@ and `GPS_RWTimer` for read/write access.
605615
* `as_GPS_utils.py` Additional formatted string methods for `AS_GPS`.
606616
* `as_rwGPS.py` Required if using the read/write variant.
607617
* `as_tGPS.py` The library. Provides `GPS_Timer` and `GPS_RWTimer` classes.
608-
* `as_GPS_time.py` Test scripts for above.
618+
* `as_GPS_time.py` Test scripts for read only driver.
619+
* `as_rwGPS_time.py` Test scripts for read/write driver.
609620

610621
### 4.1.1 Usage example
611622

@@ -687,20 +698,21 @@ Optional positional args:
687698

688699
## 4.3 Public methods
689700

690-
These return an accurate GPS time of day. As such they return as fast as
691-
possible. To achieve this they avoid allocation and dispense with error
692-
checking: these functions should not be called until a valid time/date message
693-
and PPS signal have occurred. Await the `ready` coroutine prior to first use.
694-
Subsequent calls may occur without restriction; see usage example above.
701+
The methods that return an accurate GPS time of day run as fast as possible. To
702+
achieve this they avoid allocation and dispense with error checking: these
703+
methods should not be called until a valid time/date message and PPS signal
704+
have occurred. Await the `ready` coroutine prior to first use. Subsequent calls
705+
may occur without restriction; see usage example above.
706+
707+
These methods use the MicroPython microsecond timer to interpolate between PPS
708+
pulses. They do not involve the RTC. Hence they should work on any MicroPython
709+
target supporting `machine.ticks_us`.
695710

696711
* `get_ms` No args. Returns an integer: the period past midnight in ms.
697712
* `get_t_split` No args. Returns time of day in a list of form
698713
`[hrs: int, mins: int, secs: int, μs: int]`.
699-
700-
These methods use the MicroPython microsecond timer to interpolate between PPS
701-
pulses. They do not involve the RTC. Hence they should work on any MicroPython
702-
target supporting `machine.ticks_us`. These methods are currently based on the
703-
default 1s update rate. If this is increased they may return incorrect values.
714+
* `close` No args. Shuts down the PPS pin interrupt handler. Usage is optional
715+
but in test situations avoids the ISR continuing to run after termination.
704716

705717
See [Absolute accuracy](./README.md#45-absolute-accuracy) for a discussion of
706718
the accuracy of these methods.

gps/as_GPS.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
except ImportError:
2222
const = lambda x : x
2323

24+
from math import modf
25+
2426
# Angle formats
2527
DD = const(1)
2628
DMS = const(2)
@@ -50,6 +52,8 @@
5052

5153

5254
class AS_GPS(object):
55+
# Can omit time consuming checks: CRC 6ms Bad char and line length 9ms
56+
FULL_CHECK = True
5357
_SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence)
5458
_NO_FIX = 1
5559

@@ -71,7 +75,7 @@ def _week_day(year, month, day, offset = [0, 31, 59, 90, 120, 151, 181, 212, 243
7175
day_of_week = day_of_week if day_of_week else 7
7276
return day_of_week
7377

74-
# 8-bit xor of characters between "$" and "*"
78+
# 8-bit xor of characters between "$" and "*". Takes 6ms on Pyboard!
7579
@staticmethod
7680
def _crc_check(res, ascii_crc):
7781
try:
@@ -137,6 +141,8 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC
137141
# precise timing use PPS signal and as_tGPS library.
138142
self.local_offset = local_offset # hrs
139143
self.epoch_time = 0 # Integer secs since epoch (Y2K under MicroPython)
144+
# Add ms if supplied by device. Only used by timing drivers.
145+
self.msecs = 0
140146

141147
# Position/Motion
142148
self._latitude = [0, 0.0, 'N'] # (°, mins, N/S)
@@ -151,6 +157,7 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC
151157
self._last_sv_sentence = 0 # for GSV parsing
152158
self._total_sv_sentences = 0
153159
self._satellite_data = dict() # for get_satellite_data()
160+
self._update_ms = 1000 # Update rate for timing drivers. Default 1 sec.
154161

155162
# GPS Info
156163
self.satellites_in_view = 0
@@ -182,20 +189,22 @@ async def _run(self):
182189
# Update takes a line of text
183190
def _update(self, line):
184191
line = line.rstrip()
185-
try:
186-
next(c for c in line if ord(c) < 10 or ord(c) > 126)
187-
return None # Bad character received
188-
except StopIteration:
189-
pass # All good
192+
if self.FULL_CHECK: # 9ms on Pyboard
193+
try:
194+
next(c for c in line if ord(c) < 10 or ord(c) > 126)
195+
return None # Bad character received
196+
except StopIteration:
197+
pass # All good
190198

191-
if len(line) > self._SENTENCE_LIMIT or not '*' in line:
192-
return None # Too long or malformed
199+
if len(line) > self._SENTENCE_LIMIT or not '*' in line:
200+
return None # Too long or malformed
193201

194202
a = line.split(',')
195203
segs = a[:-1] + a[-1].split('*')
196-
if not self._crc_check(line, segs[-1]):
197-
self.crc_fails += 1 # Update statistics
198-
return None
204+
if self.FULL_CHECK: # 6ms on Pyboard
205+
if not self._crc_check(line, segs[-1]):
206+
self.crc_fails += 1 # Update statistics
207+
return None
199208
self.clean_sentences += 1 # Sentence is good but unparsed.
200209
segs[0] = segs[0][1:] # discard $
201210
segs = segs[:-1] # and checksum
@@ -261,11 +270,28 @@ def _set_date_time(self, utc_string, date_string):
261270
return False
262271
try:
263272
hrs = int(utc_string[0:2]) # h
273+
if hrs > 24 or hrs < 0:
274+
return False
264275
mins = int(utc_string[2:4]) # mins
265-
secs = int(utc_string[4:6]) # secs from chip is a float but FP is always 0
276+
if mins > 60 or mins < 0:
277+
return False
278+
# Secs from MTK3339 chip is a float but others may return only 2 chars
279+
# for integer secs. If a float keep epoch as integer seconds and store
280+
# the fractional part as integer ms (ms since midnight fits 32 bits).
281+
fss, fsecs = modf(float(utc_string[4:]))
282+
secs = int(fsecs)
283+
if secs > 60 or secs < 0:
284+
return False
285+
self.msecs = int(fss * 1000)
266286
d = int(date_string[0:2]) # day
287+
if d > 31 or d < 1:
288+
return False
267289
m = int(date_string[2:4]) # month
290+
if m > 12 or m < 1:
291+
return False
268292
y = int(date_string[4:6]) + 2000 # year
293+
if y < 2018 or y > 2030:
294+
return False
269295
except ValueError: # Bad date or time strings
270296
return False
271297
wday = self._week_day(y, m, d)
@@ -284,16 +310,18 @@ def _set_date_time(self, utc_string, date_string):
284310
# The ._received dict entry is initially set False and is set only if the data
285311
# was successfully updated. Valid because parsers are synchronous methods.
286312

313+
# Chip sends rubbish RMC messages before first PPS pulse, but these have data
314+
# valid False
287315
def _gprmc(self, gps_segments): # Parse RMC sentence
288316
self._valid &= ~RMC
289-
# UTC Timestamp and date.
290-
if not self._set_date_time(gps_segments[1], gps_segments[9]):
291-
return False
292-
293317
# Check Receiver Data Valid Flag
294318
if gps_segments[2] != 'A':
295319
return True # Correctly parsed
296320

321+
# UTC Timestamp and date.
322+
if not self._set_date_time(gps_segments[1], gps_segments[9]):
323+
return False
324+
297325
# Data from Receiver is Valid/Has Fix. Longitude / Latitude
298326
if not self._fix(gps_segments, 3, 5):
299327
return False

gps/as_GPS_time.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# as_GPS_time.py Test scripts for as_tGPS
1+
# as_GPS_time.py Test scripts for as_tGPS.py read-only driver.
22
# Using GPS for precision timing and for calibrating Pyboard RTC
3+
34
# This is STM-specific: requires pyb module.
45
# Requires asyn.py from this repo.
56

@@ -37,6 +38,7 @@ async def setup():
3738

3839
# Test terminator: task sets the passed event after the passed time.
3940
async def killer(end_event, minutes):
41+
print('Will run for {} minutes.'.format(minutes))
4042
await asyncio.sleep(minutes * 60)
4143
end_event.set()
4244

@@ -59,11 +61,14 @@ async def drift_test(terminate, gps_tim):
5961
await asyncio.sleep(10)
6062
return dt - dstart
6163

62-
async def do_drift(terminate, minutes):
64+
async def do_drift(minutes):
6365
print('Setting up GPS.')
6466
gps_tim = await setup()
6567
print('Waiting for time data.')
6668
await gps_tim.ready()
69+
terminate = asyn.Event()
70+
loop = asyncio.get_event_loop()
71+
loop.create_task(killer(terminate, minutes))
6772
print('Setting RTC.')
6873
await gps_tim.set_rtc()
6974
print('Measuring drift.')
@@ -73,32 +78,31 @@ async def do_drift(terminate, minutes):
7378
print('Rate of change {}μs/hr {}secs/year'.format(ush, spa))
7479

7580
def drift(minutes=5):
76-
terminate = asyn.Event()
7781
loop = asyncio.get_event_loop()
78-
loop.create_task(killer(terminate, minutes))
79-
loop.run_until_complete(do_drift(terminate, minutes))
82+
loop.run_until_complete(do_drift(minutes))
8083

8184
# ******** Time printing demo ********
8285
# Every 10s print the difference between GPS time and RTC time
83-
async def do_time(terminate):
86+
async def do_time(minutes):
8487
fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}'
8588
print('Setting up GPS.')
8689
gps_tim = await setup()
8790
print('Waiting for time data.')
8891
await gps_tim.ready()
8992
print('Setting RTC.')
9093
await gps_tim.set_rtc()
94+
terminate = asyn.Event()
95+
loop = asyncio.get_event_loop()
96+
loop.create_task(killer(terminate, minutes))
9197
while not terminate.is_set():
9298
await asyncio.sleep(1)
9399
# In a precision app, get the time list without allocation:
94100
t = gps_tim.get_t_split()
95101
print(fstr.format(gps_tim.get_ms(), t[0], t[1], t[2], t[3]))
96102

97103
def time(minutes=1):
98-
terminate = asyn.Event()
99104
loop = asyncio.get_event_loop()
100-
loop.create_task(killer(terminate, minutes))
101-
loop.run_until_complete(do_time(terminate))
105+
loop.run_until_complete(do_time(minutes))
102106

103107
# ******** Measure accracy of μs clock ********
104108
# Callback occurs in interrupt context
@@ -122,7 +126,7 @@ async def us_setup(tick):
122126
fix_cb=lambda *_: red.toggle(),
123127
pps_cb=us_cb, pps_cb_args=(tick, blue))
124128

125-
async def do_usec(terminate):
129+
async def do_usec(minutes):
126130
tick = asyn.Event()
127131
print('Setting up GPS.')
128132
gps_tim = await us_setup(tick)
@@ -133,6 +137,9 @@ async def do_usec(terminate):
133137
sd = 0
134138
nsamples = 0
135139
count = 0
140+
terminate = asyn.Event()
141+
loop = asyncio.get_event_loop()
142+
loop.create_task(killer(terminate, minutes))
136143
while not terminate.is_set():
137144
await tick
138145
usecs = tick.value()
@@ -151,7 +158,5 @@ async def do_usec(terminate):
151158
print('Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd))
152159

153160
def usec(minutes=1):
154-
terminate = asyn.Event()
155161
loop = asyncio.get_event_loop()
156-
loop.create_task(killer(terminate, minutes))
157-
loop.run_until_complete(do_usec(terminate))
162+
loop.run_until_complete(do_usec(minutes))

gps/as_GPS_utils.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# as_GPS_utils.py Extra functionality for as_GPS.py
2+
# Put in separate file to minimise size of as_GPS.py for resource constrained
3+
# systems.
4+
5+
# Copyright (c) 2018 Peter Hinch
6+
# Released under the MIT License (MIT) - see LICENSE file
7+
from as_GPS import MDY, DMY, LONG
8+
9+
_DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW',
10+
'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW')
11+
12+
def compass_direction(gps): # Return cardinal point as string.
13+
# Calculate the offset for a rotated compass
14+
if gps.course >= 348.75:
15+
offset_course = 360 - gps.course
16+
else:
17+
offset_course = gps.course + 11.25
18+
# Each compass point is separated by 22.5°, divide to find lookup value
19+
return _DIRECTIONS[int(offset_course // 22.5)]
20+
21+
_MONTHS = ('January', 'February', 'March', 'April', 'May',
22+
'June', 'July', 'August', 'September', 'October',
23+
'November', 'December')
24+
25+
def date_string(gps, formatting=MDY):
26+
day, month, year = gps.date
27+
# Long Format January 1st, 2014
28+
if formatting == LONG:
29+
dform = '{:s} {:2d}{:s}, 20{:2d}'
30+
# Retrieve Month string from private set
31+
month = _MONTHS[month - 1]
32+
# Determine Date Suffix
33+
if day in (1, 21, 31):
34+
suffix = 'st'
35+
elif day in (2, 22):
36+
suffix = 'nd'
37+
elif day in (3, 23):
38+
suffix = 'rd'
39+
else:
40+
suffix = 'th'
41+
return dform.format(month, day, suffix, year)
42+
43+
dform = '{:02d}/{:02d}/{:02d}'
44+
if formatting == DMY:
45+
return dform.format(day, month, year)
46+
elif formatting == MDY: # Default date format
47+
return dform.format(month, day, year)
48+
raise ValueError('Unknown date format.')

gps/as_rwGPS.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ async def update_interval(self, ms=1000):
7777
raise ValueError('Invalid update interval {:d}ms.'.format(ms))
7878
sentence = bytearray('$PMTK220,{:d}*00\r\n'.format(ms))
7979
await self._send(sentence)
80+
self._update_ms = ms # Save for timing driver
8081

8182
async def enable(self, *, gll=0, rmc=1, vtg=1, gga=1, gsa=1, gsv=5, chan=0):
8283
fstr = '$PMTK314,{:d},{:d},{:d},{:d},{:d},{:d},0,0,0,0,0,0,0,0,0,0,0,0,{:d}*00\r\n'

0 commit comments

Comments
 (0)