Skip to content

Commit 7b3028f

Browse files
committed
Subclass mod complete.
1 parent 62bc0f0 commit 7b3028f

File tree

4 files changed

+131
-95
lines changed

4 files changed

+131
-95
lines changed

gps/README.md

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
** WARNING: Under development and subject to change **
1+
**NOTE: Under development. API may be subject to change**
22

33
# 1. as_GPS
44

55
This is an asynchronous device driver for GPS devices which communicate with
66
the driver via a UART. GPS NMEA sentence parsing is based on this excellent
7-
library [micropyGPS].
7+
library [micropyGPS]. It was adapted for asynchronous use; also to reduce RAM
8+
use and frequency of allocations and to correctly process local time values.
89

910
The driver is designed to be extended by subclassing, for example to support
1011
additional sentence types. It is compatible with Python 3.5 or later and also
1112
with [MicroPython]. Testing was performed using a [pyboard] with the Adafruit
1213
[Ultimate GPS Breakout] board.
1314

1415
Most GPS devices will work with the read-only driver as they emit NMEA
15-
sentences on startup. An optional read-write driver is provided for
16+
sentences on startup. The read-only driver is designed for use on resource
17+
constrained hosts. An optional read-write subclass is provided for
1618
MTK3329/MTK3339 chips as used on the above board. This enables the device
1719
configuration to be altered.
1820

19-
A further driver, for the Pyboard and other boards based on STM processors,
20-
provides for using the GPS device for precision timing. The chip's RTC may be
21+
Further subclasses, for the Pyboard and other boards based on STM processors,
22+
provide for using the GPS device for precision timing. The chip's RTC may be
2123
precisely set and calibrated using the PPS signal from the GPS chip.
2224

2325
###### [Main README](../README.md)
@@ -78,10 +80,13 @@ The following are relevant to the default read-only driver.
7880

7981
* `as_GPS.py` The library. Supports the `AS_GPS` class for read-only access to
8082
GPS hardware.
83+
* `as_GPS_utils.py` Additional formatted string methods for `AS_GPS`.
8184
* `ast_pb.py` Test/demo program: assumes a MicroPython hardware device with
8285
GPS connected to UART 4.
8386
* `log_kml.py` A simple demo which logs a route travelled to a .kml file which
8487
may be displayed on Google Earth.
88+
89+
Special tests:
8590
* `astests.py` Test with synthetic data. Run on CPython 3.x or MicroPython.
8691
* `astests_pyb.py` Test with synthetic data on UART. GPS hardware replaced by
8792
a loopback on UART 4. Requires CPython 3.5 or later or MicroPython and
@@ -95,11 +100,12 @@ Additional files relevant to the read/write driver are listed
95100

96101
### 1.4.1 Micropython
97102

98-
To install on "bare metal" hardware such as the Pyboard copy the file
99-
`as_GPS.py` onto the device's filesystem and ensure that `uasyncio` is
100-
installed. The code has been tested on the Pyboard with `uasyncio` V2 and the
101-
Adafruit [Ultimate GPS Breakout] module. If memory errors are encountered on
102-
resource constrained devices install as a [frozen module].
103+
To install on "bare metal" hardware such as the Pyboard copy the files
104+
`as_GPS.py` and `as_GPS_utils.py` onto the device's filesystem and ensure that
105+
`uasyncio` is installed. The code was tested on the Pyboard with `uasyncio` V2
106+
and the Adafruit [Ultimate GPS Breakout] module. If memory errors are
107+
encountered on resource constrained devices install each file as a
108+
[frozen module].
103109

104110
For the [read/write driver](./README.md#3-the-gps-class-read/write-driver) the
105111
file `as_rwGPS.py` must also be installed. For the
@@ -108,9 +114,9 @@ should also be copied across.
108114

109115
### 1.4.2 Python 3.5 or later
110116

111-
On platforms with an underlying OS such as the Raspberry Pi ensure that
112-
`as_GPS.py` (and optionally `as_rwGPS.py`) is on the Python path and that the
113-
Python version is 3.5 or later.
117+
On platforms with an underlying OS such as the Raspberry Pi ensure that the
118+
required driver files are on the Python path and that the Python version is 3.5
119+
or later.
114120

115121
# 2. The AS_GPS Class read-only driver
116122

@@ -124,7 +130,7 @@ Three mechanisms exist for responding to outages.
124130
* Check the `time_since_fix` method [section 2.2.3](./README.md#223-time-and-date).
125131
* Pass a `fix_cb` callback to the constructor (see below).
126132
* Cause a coroutine to pause until an update is received: see
127-
[section 3.2](./README.md#231-data-validity). This ensures current data.
133+
[section 2.3.1](./README.md#231-data-validity). This ensures current data.
128134

129135
## 2.1 Constructor
130136

@@ -138,7 +144,9 @@ Optional positional args:
138144
Default `RMC`: the callback will occur on RMC messages only (see below).
139145
* `fix_cb_args` A tuple of args for the callback (default `()`).
140146

141-
Note:
147+
Notes:
148+
`local_offset` correctly alters the date where time passes the 00.00.00
149+
boundary.
142150
If `sreader` is `None` a special test mode is engaged (see `astests.py`).
143151

144152
### 2.1.1 The fix callback
@@ -206,20 +214,21 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG)
206214
the specified units. Options `as_GPS.KPH`, `as_GPS.MPH`, `as_GPS.KNOT`.
207215

208216
* `compass_direction` No args. Returns current course as a string e.g. 'ESE'
209-
or 'NW'
217+
or 'NW'. Note that this requires the file `as_GPS_utils.py`.
210218

211219
### 2.2.3 Time and date
212220

213221
* `time_since_fix` No args. Returns time in milliseconds since last valid fix.
214222

223+
* `time_string` Arg `local` default `True`. Returns the current time in form
224+
'hh:mm:ss.sss'. If `local` is `False` returns UTC time.
225+
215226
* `date_string` Optional arg `formatting=MDY`. Returns the date as
216227
a string. Formatting options:
217228
`as_GPS.MDY` returns 'MM/DD/YY'.
218229
`as_GPS.DMY` returns 'DD/MM/YY'.
219230
`as_GPS.LONG` returns a string of form 'January 1st, 2014'.
220-
221-
* `time_string` Arg `local` default `True`. Returns the current time in form
222-
'hh:mm:ss.sss'. If `local` is `False` returns UTC time.
231+
Note that this requires the file `as_GPS_utils.py`.
223232

224233
## 2.3 Public coroutines
225234

@@ -280,7 +289,7 @@ The sentence type which updates a value is shown in brackets e.g. (GGA).
280289
* `geoid_height` Height of geoid (mean sea level) in metres above WGS84
281290
ellipsoid. (GGA).
282291
* `magvar` Magnetic variation. Degrees. -ve == West. Current firmware does not
283-
produce this data and it will always read zero.
292+
produce this data: it will always read zero.
284293

285294
### 2.4.2 Statistics and status
286295

@@ -490,18 +499,47 @@ Other `PMTK` messages are passed to the optional message callback as described
490499

491500
Many GPS chips (e.g. MTK3339) provide a PPS signal which is a pulse occurring
492501
at 1s intervals whose leading edge is a highly accurate time reference. It may
493-
be used to set and to calibrate the Pyboard realtime clock (RTC).
502+
be used to set and to calibrate the Pyboard realtime clock (RTC). Note that
503+
these drivers are for STM based targets only (at least until the `machine`
504+
library supports an `RTC` class).
494505

495506
## 4.1 Files
496507

497-
* `as_tGPS.py` The library. Supports the `GPS_Timer` class.
508+
* `as_tGPS.py` The library. Provides `GPS_Timer` and `GPS_RWTimer` classes.
498509
* `as_GPS_time.py` Test scripts for above.
499510

500-
## 4.2 GPS_Timer class Constructor
511+
## 4.2 GPS_Timer and GPS_RWTimer classes
512+
513+
These classes inherit from `AS_GPS` and `GPS` respectively, with read-only and
514+
read/write access to the GPS hardware. All public methods and bound variables of
515+
the base classes are supported. Additional functionality is detailed below.
516+
517+
### 4.2.1 GPS_Timer class Constructor
501518

502-
This takes the following arguments:
503-
* `gps` An instance of the `AS_GPS` (read-only) or `GPS` (read/write) classes.
519+
Mandatory positional args:
520+
* `sreader` The `StreamReader` instance associated with the UART.
504521
* `pps_pin` An initialised input `Pin` instance for the PPS signal.
522+
Optional positional args:
523+
* `local_offset` See `AS_GPS` details for these args.
524+
* `fix_cb`
525+
* `cb_mask`
526+
* `fix_cb_args`
527+
* `led` Default `None`. If an `LED` instance is passed, this will toggle each
528+
time a PPS interrupt is handled.
529+
530+
### 4.2.2 GPS_RWTimer class Constructor
531+
532+
This takes three mandatory positional args:
533+
* `sreader` The `StreamReader` instance associated with the UART.
534+
* `swriter` The `StreamWriter` instance associated with the UART.
535+
* `pps_pin` An initialised input `Pin` instance for the PPS signal.
536+
Optional positional args:
537+
* `local_offset` See `GPS` details.
538+
* `fix_cb`
539+
* `cb_mask`
540+
* `fix_cb_args`
541+
* `msg_cb`
542+
* `msg_cb_args`
505543
* `led` Default `None`. If an `LED` instance is passed, this will toggle each
506544
time a PPS interrupt is handled.
507545

@@ -538,6 +576,10 @@ RTC to achieve timepiece quality results. Note that calibration is lost on
538576
power down: solutions are either to use an RTC backup battery or to store the
539577
calibration factor in a file (or in code) and re-apply it on startup.
540578

579+
Crystal oscillator frequency (and hence calibration factor) is temperature
580+
dependent. For the most accurate possible results allow the Pyboard to reach
581+
working temperature before calibrating.
582+
541583
# 5. Supported Sentences
542584

543585
* GPRMC GP indicates NMEA sentence
@@ -580,11 +622,14 @@ messages came in. This time could be longer depending on data. So if an update
580622
rate higher than the default 1 second is to be used, either the baudrate must
581623
be increased or the satellite information messages should be disabled.
582624

583-
The PPS signal (not used by this driver) on the MTK3339 occurs only when a fix
584-
has been achieved. The leading edge always occurs before a set of messages are
585-
output. So, if the leading edge is to be used for precise timing, 1s should be
586-
added to the `timestamp` value (beware of possible rollover into minutes and
587-
hours).
625+
The PPS signal on the MTK3339 occurs only when a fix has been achieved. The
626+
leading edge occurs on a 1s boundary with high absolute accuracy. It therefore
627+
follows that the RMC message carrying the time/date of that second arrives
628+
after the leading edge (because of processing and UART latency). It is also
629+
the case that on a second boundary minutes, hours and the date may roll over.
630+
631+
Further, the local_time offset can affect the date. These drivers aim to handle
632+
these factors.
588633

589634
[MicroPython]:https://micropython.org/
590635
[frozen module]:https://learn.adafruit.com/micropython-basics-loading-modules/frozen-modules

gps/as_GPS.py

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@
5252
class AS_GPS(object):
5353
_SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence)
5454
_NO_FIX = 1
55-
_DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W',
56-
'WNW', 'NW', 'NNW')
57-
_MONTHS = ('January', 'February', 'March', 'April', 'May',
58-
'June', 'July', 'August', 'September', 'October',
59-
'November', 'December')
6055

6156
# Return day of week from date. Pyboard RTC format: 1-7 for Monday through Sunday.
6257
# https://stackoverflow.com/questions/9847213/how-do-i-get-the-day-of-week-given-a-date-in-python?noredirect=1&lq=1
@@ -141,7 +136,6 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC
141136
# part is always zero. So treat seconds value as an integer. For
142137
# precise timing use PPS signal and as_tGPS library.
143138
self.local_offset = local_offset # hrs
144-
self._rtcbuf = [0]*8 # Buffer for RTC setting
145139
self.epoch_time = 0 # Integer secs since epoch (Y2K under MicroPython)
146140

147141
# Position/Motion
@@ -257,6 +251,9 @@ def _fix(self, gps_segments, idx_lat, idx_long):
257251
self._fix_time = self._get_time()
258252
return True
259253

254+
def _dtset(self, _): # For subclass
255+
pass
256+
260257
# A local offset may exist so check for date rollover. Local offsets can
261258
# include fractions of an hour but not seconds (AFAIK).
262259
def _set_date_time(self, utc_string, date_string):
@@ -274,16 +271,7 @@ def _set_date_time(self, utc_string, date_string):
274271
wday = self._week_day(y, m, d)
275272
t = self._mktime((y, m, d, hrs, mins, int(secs), wday - 1, 0, 0))
276273
self.epoch_time = t # This is the fundamental datetime reference.
277-
# Need local time for setting Pyboard RTC in interrupt context
278-
t += int(3600 * self.local_offset)
279-
y, m, d, hrs, mins, secs, *_ = self._localtime(t)
280-
self._rtcbuf[0] = y
281-
self._rtcbuf[1] = m
282-
self._rtcbuf[2] = d
283-
self._rtcbuf[3] = wday
284-
self._rtcbuf[4] = hrs
285-
self._rtcbuf[5] = mins
286-
self._rtcbuf[6] = secs
274+
self._dtset(wday) # Subclass may override
287275
return True
288276

289277
########################################
@@ -581,13 +569,8 @@ def time_since_fix(self): # ms since last valid fix
581569
return self._time_diff(self._get_time(), self._fix_time)
582570

583571
def compass_direction(self): # Return cardinal point as string.
584-
# Calculate the offset for a rotated compass
585-
if self.course >= 348.75:
586-
offset_course = 360 - self.course
587-
else:
588-
offset_course = self.course + 11.25
589-
# Each compass point is separated by 22.5°, divide to find lookup value
590-
return self._DIRECTIONS[int(offset_course // 22.5)]
572+
from as_GPS_utils import compass_direction
573+
return compass_direction(self)
591574

592575
def latitude_string(self, coord_format=DM):
593576
if coord_format == DD:
@@ -633,7 +616,7 @@ def date(self):
633616

634617
@property
635618
def utc(self):
636-
t = self.epoch_time + int(3600 * self.local_offset)
619+
t = self.epoch_time
637620
_, _, _, hrs, mins, secs, *_ = self._localtime(t)
638621
return hrs, mins, secs
639622

@@ -642,26 +625,5 @@ def time_string(self, local=True):
642625
return '{:02d}:{:02d}:{:02d}'.format(hrs, mins, secs)
643626

644627
def date_string(self, formatting=MDY):
645-
day, month, year = self.date
646-
# Long Format January 1st, 2014
647-
if formatting == LONG:
648-
dform = '{:s} {:2d}{:s}, 20{:2d}'
649-
# Retrieve Month string from private set
650-
month = self._MONTHS[month - 1]
651-
# Determine Date Suffix
652-
if day in (1, 21, 31):
653-
suffix = 'st'
654-
elif day in (2, 22):
655-
suffix = 'nd'
656-
elif day == 3:
657-
suffix = 'rd'
658-
else:
659-
suffix = 'th'
660-
return dform.format(month, day, suffix, year)
661-
662-
dform = '{:02d}/{:02d}/{:02d}'
663-
if formatting == DMY:
664-
return dform.format(day, month, year)
665-
elif formatting == MDY: # Default date format
666-
return dform.format(month, day, year)
667-
raise ValueError('Unknown date format.')
628+
from as_GPS_utils import date_string
629+
return date_string(self, formatting)

gps/as_GPS_time.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import uasyncio as asyncio
99
import pyb
10-
import as_GPS
1110
import as_tGPS
1211

1312
print('Available tests:')
@@ -21,9 +20,9 @@ async def setup():
2120
blue = pyb.LED(4)
2221
uart = pyb.UART(4, 9600, read_buf_len=200)
2322
sreader = asyncio.StreamReader(uart)
24-
gps = as_GPS.AS_GPS(sreader, local_offset=1, fix_cb=lambda *_: red.toggle())
2523
pps_pin = pyb.Pin('X3', pyb.Pin.IN)
26-
return as_tGPS.GPS_Timer(gps, pps_pin, blue)
24+
return as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1,
25+
fix_cb=lambda *_: red.toggle(), led=blue)
2726

2827
running = True
2928

@@ -36,7 +35,7 @@ async def drift_test(gps_tim):
3635
dstart = await gps_tim.delta()
3736
while running:
3837
dt = await gps_tim.delta()
39-
print('{} Delta {}μs'.format(gps_tim.gps.time_string(), dt))
38+
print('{} Delta {}μs'.format(gps_tim.time_string(), dt))
4039
await asyncio.sleep(10)
4140
return dt - dstart
4241

0 commit comments

Comments
 (0)