Skip to content

Commit ab243d0

Browse files
committed
Re-organise files. Add as_tGPS.py.
1 parent 80c1860 commit ab243d0

9 files changed

+349
-1018
lines changed

gps/README.md

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ sentences on startup. An optional read-write driver is provided for
1414
MTK3329/MTK3339 chips as used on the above board. This enables the device
1515
configuration to be altered.
1616

17+
A further driver, for the Pyboard and other boards based on STM processors,
18+
provides for using the GPS device for precision timing. The chip's RTC may be
19+
precisely set and calibrated using the PPS signal from the GPS chip.
20+
1721
###### [Main README](../README.md)
1822

1923
## 1.1 Overview
@@ -26,7 +30,8 @@ to access data such as position, altitude, course, speed, time and date.
2630
### 1.1.1 Wiring
2731

2832
These notes are for the Adafruit Ultimate GPS Breakout. It may be run from 3.3V
29-
or 5V. If running the Pyboard from USB it may be wired as follows:
33+
or 5V. If running the Pyboard from USB, GPS Vin may be wired to Pyboard V+. If
34+
the Pyboard is run from a voltage >5V the Pyboard 3V3 pin should be used.
3035

3136
| GPS | Pyboard | Optional |
3237
|:---:|:----------:|:--------:|
@@ -37,9 +42,9 @@ or 5V. If running the Pyboard from USB it may be wired as follows:
3742
| Rx | X1 (U4 tx) | |
3843

3944
This is based on UART 4 as used in the test programs; any UART may be used. The
40-
X1-Rx connection is only necessary if using the read/write driver to alter the
41-
GPS device operation. The PPS connection is required only if using the device
42-
for precise timing (`as_GPS_time.py`). Any pin may be used.
45+
UART Tx-GPS Rx connection is only necessary if using the read/write driver. The
46+
PPS connection is required only if using the device for precise timing
47+
(`as_tGPS.py`). Any pin may be used.
4348

4449
## 1.2 Basic Usage
4550

@@ -81,7 +86,8 @@ The following are relevant to the default read-only driver.
8186
`uasyncio`.
8287

8388
Additional files relevant to the read/write driver are listed
84-
[here](./README.md#31-files).
89+
[here](./README.md#31-files). Files for the timing driver are listed
90+
[here](./README.md#41-files).
8591

8692
## 1.4 Installation
8793

@@ -94,7 +100,9 @@ Adafruit [Ultimate GPS Breakout] module. If memory errors are encountered on
94100
resource constrained devices install as a [frozen module].
95101

96102
For the [read/write driver](./README.md#3-the-gps-class-read/write-driver) the
97-
file `as_rwGPS.py` must also be installed.
103+
file `as_rwGPS.py` must also be installed. For the
104+
[timing driver](./README.md#4-using-gps-for-accurate-timing) `as_tGPS.py`
105+
should also be copied across.
98106

99107
### 1.4.2 Python 3.5 or later
100108

@@ -128,8 +136,7 @@ Optional positional args:
128136
Default `RMC`: the callback will occur on RMC messages only (see below).
129137
* `fix_cb_args` A tuple of args for the callback (default `()`).
130138

131-
Notes:
132-
`local_offset` does not affect the date value.
139+
Note:
133140
If `sreader` is `None` a special test mode is engaged (see `astests.py`).
134141

135142
### 2.1.1 The fix callback
@@ -284,13 +291,19 @@ The following are counts since instantiation.
284291

285292
### 2.4.3 Date and time
286293

287-
As received from most recent GPS message.
288-
289-
* `timestamp` [hrs, mins, secs] e.g. [12, 15, 3.0]. Values are integers except
290-
for secs which is a float (perhaps dependent on GPS hardware).
291-
* `date` [day, month, year] e.g. [23, 3, 18]
294+
* `utc` [hrs: int, mins: int, secs: float] UTC time e.g. [23, 3, 58.0]. Note
295+
that some GPS hardware may only provide integer seconds. The MTK3339 chip
296+
provides a float.
297+
* `local_time` [hrs: int, mins: int, secs: float] Local time.
298+
* `date` [day: int, month: int, year: int] e.g. [23, 3, 18]
292299
* `local_offset` Local time offset in hrs as specified to constructor.
293300

301+
The `utc` bound variable updates on receipt of RMC, GLL or GGA messages.
302+
303+
The `date` and `local_time` variables are updated when an RMC message is
304+
received. A local time offset will result in date changes where the time
305+
offset causes the local time to pass midnight.
306+
294307
### 2.4.4 Satellite data
295308

296309
* `satellites_in_view` No. of satellites in view. (GSV).
@@ -478,45 +491,47 @@ be used to set and to calibrate the Pyboard realtime clock (RTC).
478491

479492
## 4.1 Files
480493

481-
* `as_GPS_time.py` Supports the `GPS_Timer` class.
494+
* `as_tGPS.py` The library. Supports the `GPS_Timer` class.
495+
* `as_GPS_time.py` Test scripts for above.
482496

483497
## 4.2 GPS_Timer class Constructor
484498

485499
This takes the following arguments:
486500
* `gps` An instance of the `AS_GPS` (read-only) or `GPS` (read/write) classes.
487501
* `pps_pin` An initialised input `Pin` instance for the PPS signal.
502+
* `led` Default `None`. If an `LED` instance is passed, this will toggle each
503+
time a PPS interrupt is handled.
488504

489505
## 4.3 Public methods
490506

491-
With the exception of `delta` these return immediately. Times are derived from
492-
the GPS PPS signal. These functions should not be called until a valid
493-
time/date message and PPS signal have occurred: await the `ready` coroutine
494-
prior to first use. These functions do not check this for themselves to ensure
495-
fast return.
507+
These return immediately. Times are derived from the GPS PPS signal. These
508+
functions should not be called until a valid time/date message and PPS signal
509+
have occurred: await the `ready` coroutine prior to first use.
496510

497511
* `get_secs` No args. Returns a float: the period past midnight in seconds.
498512
* `get_t_split` No args. Returns time of day tuple of form
499513
(hrs: int, mins: int, secs: float).
500-
* `set_rtc` No args. Sets the Pyboard RTC to GPS time.
501-
* `delta` No args. Returns no. of μs RTC leads GPS. This method blocks for up
502-
to a second.
503514

504515
## 4.4 Public coroutines
505516

506517
* `ready` No args. Pauses until a valid time/date message and PPS signal have
507518
occurred.
519+
* `set_rtc` No args. Sets the Pyboard RTC to GPS time. Coro pauses for up to
520+
1s as it waits for a PPS pulse.
521+
* `delta` No args. Returns no. of μs RTC leads GPS. Coro pauses for up to 1s.
508522
* `calibrate` Arg: integer, no. of minutes to run default 5. Calibrates the
509-
Pyboard RTC and returns the calibration factor for it.
523+
Pyboard RTC and returns the calibration factor for it. This coroutine sets the
524+
RTC (with any existing calibration removed) and measures its drift with
525+
respect to the GPS time. This measurement becomes more precise as time passes.
526+
It calculates a calibration value at 10s intervals and prints progress
527+
information. When the calculated calibration factor is repeatable within 1
528+
digit (or the spcified time has elapsed) it terminates. Typical run times are
529+
on the order of two miutes.
510530

511531
Achieving an accurate calibration factor takes time but does enable the Pyboard
512532
RTC to achieve timepiece quality results. Note that calibration is lost on
513533
power down: solutions are either to use an RTC backup battery or to store the
514-
calibration factor in a file and re-apply it on startup.
515-
516-
The coroutine calculates the calibration factor at 10 second intervals and will
517-
return early if three consecutive identical calibration factors are calculated.
518-
Note that, because of the need for precise timing, this coroutine blocks at
519-
intervals for periods of up to one second.
534+
calibration factor in a file (or in code) and re-apply it on startup.
520535

521536
# 5. Supported Sentences
522537

gps/as_GPS.py

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
# Copyright (c) 2018 Peter Hinch
99
# Released under the MIT License (MIT) - see LICENSE file
1010

11+
# astests.py runs under CPython but not MicroPython because mktime is missing
12+
# from Unix build of utime
13+
1114
try:
1215
import uasyncio as asyncio
1316
except ImportError:
@@ -45,6 +48,7 @@
4548
DATE = const(RMC)
4649
COURSE = const(RMC | VTG)
4750

51+
4852
class AS_GPS(object):
4953
_SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence)
5054
_NO_FIX = 1
@@ -54,6 +58,25 @@ class AS_GPS(object):
5458
'June', 'July', 'August', 'September', 'October',
5559
'November', 'December')
5660

61+
# Return day of week from date. Pyboard RTC format: 1-7 for Monday through Sunday.
62+
# https://stackoverflow.com/questions/9847213/how-do-i-get-the-day-of-week-given-a-date-in-python?noredirect=1&lq=1
63+
# Adapted for Python 3 and Pyboard RTC format.
64+
@staticmethod
65+
def _week_day(year, month, day):
66+
offset = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
67+
aux = year - 1700 - (1 if month <= 2 else 0)
68+
# day_of_week for 1700/1/1 = 5, Friday
69+
day_of_week = 5
70+
# partial sum of days betweem current date and 1700/1/1
71+
day_of_week += (aux + (1 if month <= 2 else 0)) * 365
72+
# leap year correction
73+
day_of_week += aux // 4 - aux // 100 + (aux + 100) // 400
74+
# sum monthly and day offsets
75+
day_of_week += offset[month - 1] + (day - 1)
76+
day_of_week %= 7
77+
day_of_week = day_of_week if day_of_week else 7
78+
return day_of_week
79+
5780
# 8-bit xor of characters between "$" and "*"
5881
@staticmethod
5982
def _crc_check(res, ascii_crc):
@@ -74,17 +97,21 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC
7497
self.cb_mask = cb_mask
7598
self._fix_cb_args = fix_cb_args
7699

77-
# Import utime or time for fix time handling
100+
# CPython compatibility. Import utime or time for fix time handling.
78101
try:
79102
import utime
80103
self._get_time = utime.ticks_ms
81104
self._time_diff = utime.ticks_diff
105+
self._localtime = utime.localtime
106+
self._mktime = utime.mktime
82107
except ImportError:
83108
# Otherwise default to time module for non-embedded implementations
84109
# Should still support millisecond resolution.
85110
import time
86111
self._get_time = time.time
87112
self._time_diff = lambda start, end: 1000 * (start - end)
113+
self._localtime = time.localtime
114+
self._mktime = time.mktime
88115

89116
# Key: currently supported NMEA sentences. Value: parse method.
90117
self.supported_sentences = {'GPRMC': self._gprmc, 'GLRMC': self._gprmc,
@@ -112,8 +139,9 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC
112139
# Data From Sentences
113140
# Time. Ignore http://www.gpsinformation.org/dale/nmea.htm, hardware
114141
# returns a float.
115-
self.timestamp = [0, 0, 0.0] # [h, m, s]
116-
self.date = [0, 0, 0] # [d, m, y]
142+
self.utc = [0, 0, 0.0] # [h: int, m: int, s: float]
143+
self.local_time = [0, 0, 0.0] # [h: int, m: int, s: float]
144+
self.date = [0, 0, 0] # [dd: int, mm: int, yy: int]
117145
self.local_offset = local_offset # hrs
118146

119147
# Position/Motion
@@ -232,15 +260,46 @@ def _fix(self, gps_segments, idx_lat, idx_long):
232260

233261
# Set timestamp. If time/date not present retain last reading (if any).
234262
def _set_timestamp(self, utc_string):
235-
if utc_string: # Possible timestamp found
236-
try:
237-
self.timestamp[0] = int(utc_string[0:2]) + self.local_offset # h
238-
self.timestamp[1] = int(utc_string[2:4]) # mins
239-
self.timestamp[2] = float(utc_string[4:]) # secs from chip is a float
240-
return True
241-
except ValueError:
242-
pass
243-
return False
263+
if not utc_string:
264+
return False
265+
# Possible timestamp found
266+
try:
267+
self.utc[0] = int(utc_string[0:2]) # h
268+
self.utc[1] = int(utc_string[2:4]) # mins
269+
self.utc[2] = float(utc_string[4:]) # secs from chip is a float
270+
return True
271+
except ValueError:
272+
return False
273+
for idx in range(3):
274+
self.local_time[idx] = self.utc[idx]
275+
return True
276+
277+
# A local offset may exist so check for date rollover. Local offsets can
278+
# include fractions of an hour but not seconds (AFAIK).
279+
def _set_date(self, date_string):
280+
if not date_string:
281+
return False
282+
try:
283+
d = int(date_string[0:2]) # day
284+
m = int(date_string[2:4]) # month
285+
y = int(date_string[4:6]) + 2000 # year
286+
except ValueError: # Bad Date stamp value present
287+
return False
288+
hrs = self.utc[0]
289+
mins = self.utc[1]
290+
secs = self.utc[2]
291+
wday = self._week_day(y, m, d) - 1
292+
t = self._mktime((y, m, d, hrs, mins, int(secs), wday, 0, 0))
293+
t += int(3600 * self.local_offset)
294+
y, m, d, hrs, mins, *_ = self._localtime(t) # Preserve float seconds
295+
y -= 2000
296+
self.local_time[0] = hrs
297+
self.local_time[1] = mins
298+
self.local_time[2] = secs
299+
self.date[0] = d
300+
self.date[1] = m
301+
self.date[2] = y
302+
return True
244303

245304
########################################
246305
# Sentence Parsers
@@ -255,20 +314,9 @@ def _set_timestamp(self, utc_string):
255314
def _gprmc(self, gps_segments): # Parse RMC sentence
256315
self._valid &= ~RMC
257316
# UTC Timestamp.
258-
try:
259-
self._set_timestamp(gps_segments[1])
260-
except ValueError: # Bad Timestamp value present
261-
return False
262-
263-
# Date stamp
264-
try:
265-
date_string = gps_segments[9]
266-
if date_string: # Possible date stamp found
267-
self.date[0] = int(date_string[0:2]) # day
268-
self.date[1] = int(date_string[2:4]) # month
269-
self.date[2] = int(date_string[4:6]) # year 18 == 2018
270-
271-
except ValueError: # Bad Date stamp value present
317+
if not self._set_timestamp(gps_segments[1]):
318+
return False # Bad Timestamp value present
319+
if not self._set_date(gps_segments[9]):
272320
return False
273321

274322
# Check Receiver Data Valid Flag
@@ -599,8 +647,9 @@ def speed_string(self, unit=KPH):
599647
return sform.format(speed, 'knots')
600648
return sform.format(speed, 'km/h')
601649

602-
def time(self):
603-
return '{:02d}:{:02d}:{:2.3f}'.format(*self.timestamp)
650+
def time(self, local=True):
651+
t = self.local_time if local else self.utc
652+
return '{:02d}:{:02d}:{:2.3f}'.format(*t)
604653

605654
def date_string(self, formatting=MDY):
606655
day, month, year = self.date

0 commit comments

Comments
 (0)