This repository offers a suite of asynchronous device drivers for GPS devices which communicate with the host via a UART. GPS NMEA-0183 sentence parsing is based on this excellent library micropyGPS.
The code requires asyncio V3. Some modules can run under CPython: doing so requires Python V3.8 or later.
The main modules have been tested on Pyboards and RP2 (Pico and Pico W). Since the interface is a standard UART it is expected that the modules will work on other hosts. Some modules use GPS for precision timing: the accuracy of these may be reduced on some platforms.
- Contents
1.1 Driver characteristics
1.2 Comparison with micropyGPS
1.3 Overview - Installation
2.1 Wiring
2.2 Library installation
2.3 Dependency - Basic Usage
3.1 Demo programs - The AS_GPS Class read-only driver Base class: a general purpose driver.
4.1 Constructor
4.1.1 The fix callback Optional callback-based interface.
4.2 Public Methods
4.2.1 Location
4.2.2 Course
4.2.3 Time and date
4.3 Public coroutines
4.3.1 Data validity
4.3.2 Satellite data
4.4 Public bound variables and properties
4.4.1 Position and course
4.4.2 Statistics and status
4.4.3 Date and time
4.4.4 Satellite data
4.5 Subclass hooks
4.6 Public class variable - The GPS class read-write driver Subclass supports changing GPS hardware modes.
5.1 Test script
5.2 Usage example
5.3 The GPS class constructor
5.4 Public coroutines
5.4.1 Changing baudrate
5.5 Public bound variables
5.6 The parse method developer note - Using GPS for accurate timing
6.1 Test scripts
6.2 Usage example
6.3 GPS_Timer and GPS_RWTimer classes
6.4 Public methods
6.5 Public coroutines
6.6 Absolute accuracy
6.7 Demo program as_GPS_time.py - Supported sentences
- Developer notes For those wanting to modify the modules.
8.1 Subclassing
8.2 Special test programs - Notes on timing
9.1 Absolute accuracy - Files List of files in the repo.
10.1 Basic files
10.2 Files for read write operation 10.3 Files for timing applications
10.4 Special test programs
- Asynchronous: UART messaging is handled as a background task allowing the application to perform other tasks such as handling user interaction.
- The read-only driver is suitable for resource constrained devices and will work with most GPS devices using a UART for communication.
- Can write
.kml
files for displaying journeys on Google Earth. - The read-write driver enables altering the configuration of GPS devices based on the popular MTK3329/MTK3339 chips.
- The above drivers are portable between MicroPython and Python 3.8 or above.
- Timing drivers for MicroPython only extend the capabilities of the read-only and read-write drivers to provide accurate sub-ms GPS timing. On STM-based hosts (e.g. the Pyboard) the RTC may be set from GPS and calibrated to achieve timepiece-level accuracy.
- Drivers may be extended via subclassing, for example to support additional sentence types.
Testing was performed using a Pyboard with the Adafruit Ultimate GPS Breakout board. Most GPS devices will work with the read-only driver as they emit NMEA-0183 sentences on startup.
NMEA-0183 sentence parsing is based on micropyGPS but with significant changes.
- As asynchronous drivers they require
asyncio
on MicroPython or under Python 3.8+. - Sentence parsing is adapted for asynchronous use.
- Rollover of local time into the date value enables worldwide use.
- RAM allocation is cut by various techniques to lessen heap fragmentation. This improves application reliability on RAM constrained devices.
- Some functionality is devolved to a utility module, reducing RAM usage where these functions are unused.
- The read/write driver is a subclass of the read-only driver.
- Timing drivers are added offering time measurement with μs resolution and high absolute accuracy. These are implemented by subclassing these drivers.
- Hooks are provided for user-designed subclassing, for example to parse additional message types.
The AS_GPS
object runs a coroutine which receives NMEA-0183 sentences from
the UART and parses them as they arrive. Valid sentences cause local bound
variables to be updated. These can be accessed at any time with minimal latency
to access data such as position, altitude, course, speed, time and date.
These notes are for the Adafruit Ultimate GPS Breakout. It may be run from 3.3V or 5V. If running the Pyboard from USB, GPS Vin may be wired to Pyboard V+. If the Pyboard is run from a voltage >5V the Pyboard 3V3 pin should be used. Testing on Pico and Pico W used the 3.3V output to power the GPS module.
GPS | Pyboard | RP2 | Optional | Use case |
---|---|---|---|---|
Vin | V+ or 3V3 | 3V3 | ||
Gnd | Gnd | Gnd | ||
PPS | X3 | 2 | Y | Precision timing applications. |
Tx | X2 (U4 rx) | 1 | ||
Rx | X1 (U4 tx) | 0 | Y | Changing GPS module parameters. |
Pyboard connections are based on UART 4 as used in the test programs; any UART may be used. RP2 connections assume UART 0.
The UART Tx-GPS Rx connection is only necessary if using the read/write driver.
The PPS connection is required only if using the timing driver as_tGPS.py
. Any
pin may be used.
On the Pyboard D the 3.3V output is switched. Enable it with the following
(typically in main.py
):
import time
machine.Pin.board.EN_3V3.value(1)
time.sleep(1)
The library is implemented as a Python package. To install copy the following directory and its contents to the target hardware:
as_drivers/as_GPS
The following directory is required for certain Pyboard-specific test scripts:
threadsafe
See section 10.3.
On platforms with an underlying OS such as the Raspberry Pi ensure that the directory is on the Python path and that the Python version is 3.8 or later. Code samples will need adaptation for the serial port.
The library requires asyncio
V3 on MicroPython and asyncio
on CPython.
In the example below a UART is instantiated and an AS_GPS
instance created.
A callback is specified which will run each time a valid fix is acquired.
The test runs for 60 seconds once data has been received.
Pyboard:
import asyncio
import as_drivers.as_GPS as as_GPS
from machine import UART
def callback(gps, *_): # Runs for each valid fix
print(gps.latitude(), gps.longitude(), gps.altitude)
uart = UART(4, 9600)
sreader = asyncio.StreamReader(uart) # Create a StreamReader
gps = as_GPS.AS_GPS(sreader, fix_cb=callback) # Instantiate GPS
async def test():
print('waiting for GPS data')
await gps.data_received(position=True, altitude=True)
await asyncio.sleep(60) # Run for one minute
asyncio.run(test())
RP2:
import asyncio
import as_drivers.as_GPS as as_GPS
from machine import UART, Pin
def callback(gps, *_): # Runs for each valid fix
print(gps.latitude(), gps.longitude(), gps.altitude)
uart = UART(0, 9600, tx=Pin(0), rx=Pin(1), timeout=5000, timeout_char=5000)
sreader = asyncio.StreamReader(uart) # Create a StreamReader
gps = as_GPS.AS_GPS(sreader, fix_cb=callback) # Instantiate GPS
async def test():
print('waiting for GPS data')
await gps.data_received(position=True, altitude=True)
await asyncio.sleep(60) # Run for one minute
asyncio.run(test())
This example achieves the same thing without using a callback:
import asyncio
import as_drivers.as_GPS as as_GPS
from machine import UART
uart = UART(4, 9600)
sreader = asyncio.StreamReader(uart) # Create a StreamReader
gps = as_GPS.AS_GPS(sreader) # Instantiate GPS
async def test():
print('waiting for GPS data')
await gps.data_received(position=True, altitude=True)
for _ in range(10):
print(gps.latitude(), gps.longitude(), gps.altitude)
await asyncio.sleep(2)
asyncio.run(test())
This assumes a Pyboard 1.x or Pyboard D with GPS connected to UART 4 and prints received data:
import as_drivers.as_gps.ast_pb
A simple demo which logs a route travelled to a .kml file which may be
displayed on Google Earth. Logging stops when the user switch is pressed.
Data is logged to /sd/log.kml
at 10s intervals.
import as_drivers.as_gps.log_kml
Method calls and access to bound variables are nonblocking and return the most current data. This is updated transparently by a coroutine. In situations where updates cannot be achieved, for example in buildings or tunnels, values will be out of date. The action to take (if any) is application dependent.
Three mechanisms exist for responding to outages.
- Check the
time_since_fix
method section 2.2.3. - Pass a
fix_cb
callback to the constructor (see below). - Cause a coroutine to pause until an update is received: see section 4.3.1. This ensures current data.
Mandatory positional arg:
sreader
This is aStreamReader
instance associated with the UART. Optional positional args:local_offset
Local timezone offset in hours realtive to UTC (GMT). May be an integer or float.fix_cb
An optional callback. This runs after a valid message of a chosen type has been received and processed.cb_mask
A bitmask determining which sentences will trigger the callback. DefaultRMC
: the callback will occur on RMC messages only (see below).fix_cb_args
A tuple of args for the callback (default()
).
Notes:
local_offset
will alter the date when time passes the 00.00.00 boundary.
If sreader
is None
a special test mode is engaged (see astests.py
).
This receives the following positional args:
- The GPS instance.
- An integer defining the message type which triggered the callback.
- Any args provided in
msg_cb_args
.
Message types are defined by the following constants in as_GPS.py
: RMC
,
GLL
, VTG
, GGA
, GSA
and GSV
.
The cb_mask
constructor argument may be the logical or
of any of these
constants. In this example the callback will occur after successful processing
of RMC and VTG messages:
gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG)
These are grouped below by the type of data returned.
-
latitude
Optional argcoord_format=as_GPS.DD
. Returns the most recent latitude.
Ifcoord_format
isas_GPS.DM
returns a tuple(degs, mins, hemi)
.
Ifas_GPS.DD
is passed returns(degs, hemi)
where degs is a float.
Ifas_GPS.DMS
is passed returns(degs, mins, secs, hemi)
.
hemi
is 'N' or 'S'. -
longitude
Optional argcoord_format=as_GPS.DD
. Returns the most recent longitude.
Ifcoord_format
isas_GPS.DM
returns a tuple(degs, mins, hemi)
.
Ifas_GPS.DD
is passed returns(degs, hemi)
where degs is a float.
Ifas_GPS.DMS
is passed returns(degs, mins, secs, hemi)
.
hemi
is 'E' or 'W'. -
latitude_string
Optional argcoord_format=as_GPS.DM
. Returns the most recent latitude in human-readable format. Formats areas_GPS.DM
,as_GPS.DD
,as_GPS.DMS
oras_GPS.KML
.
Ifcoord_format
isas_GPS.DM
it returns degrees, minutes and hemisphere ('N' or 'S').as_GPS.DD
returns degrees and hemisphere.
as_GPS.DMS
returns degrees, minutes, seconds and hemisphere.
as_GPS.KML
returns decimal degrees, +ve in northern hemisphere and -ve in southern, intended for logging to Google Earth compatible kml files. -
longitude_string
Optional argcoord_format=as_GPS.DM
. Returns the most recent longitude in human-readable format. Formats areas_GPS.DM
,as_GPS.DD
,as_GPS.DMS
oras_GPS.KML
.
Ifcoord_format
isas_GPS.DM
it returns degrees, minutes and hemisphere ('E' or 'W').as_GPS.DD
returns degrees and hemisphere.
as_GPS.DMS
returns degrees, minutes, seconds and hemisphere.
as_GPS.KML
returns decimal degrees, +ve in eastern hemisphere and -ve in western, intended for logging to Google Earth compatible kml files.
-
speed
Optional argunit=as_GPS.KPH
. Returns the current speed in the specified units. Options:as_GPS.KPH
,as_GPS.MPH
,as_GPS.KNOT
. -
speed_string
Optional argunit=as_GPS.KPH
. Returns the current speed in the specified units. Optionsas_GPS.KPH
,as_GPS.MPH
,as_GPS.KNOT
. -
compass_direction
No args. Returns current course as a string e.g. 'ESE' or 'NW'. Note that this requires the fileas_GPS_utils.py
.
-
time_since_fix
No args. Returns time in milliseconds since last valid fix. -
time_string
Optional arglocal=True
. Returns the current time in form 'hh:mm:ss.sss'. Iflocal
isFalse
returns UTC time. -
date_string
Optional argformatting=MDY
. Returns the date as a string. Formatting options:
as_GPS.MDY
returns 'MM/DD/YY'.
as_GPS.DMY
returns 'DD/MM/YY'.
as_GPS.LONG
returns a string of form 'January 1st, 2014'. Note that this requires the fileas_GPS_utils.py
.
On startup after a cold start it may take time before valid data is received. During and shortly after an outage messages will be absent. To avoid reading stale data, reception of messages can be checked before accessing data.
data_received
Boolean args:position
,course
,date
,altitude
. All defaultFalse
. The coroutine will pause until at least one valid message of each specified types has been received. This example will pause until new position and altitude messages have been received:
while True:
await my_gps.data_received(position=True, altitude=True)
# Access these data values now
No option is provided for satellite data: this functionality is provided by the
get_satellite_data
coroutine.
Satellite data requires multiple sentences from the GPS and therefore requires a coroutine which will pause execution until a complete set of data has been acquired.
get_satellite_data
No args. Waits for a set of GSV (satellites in view) sentences and returns a dictionary. Typical usage in a user coroutine:
d = await my_gps.get_satellite_data()
print(d.keys()) # List of satellite PRNs
print(d.values()) # [(elev, az, snr), (elev, az, snr)...]
Dictionary values are (elevation, azimuth, snr) where elevation and azimuth are in degrees and snr (a measure of signal strength) is in dB in range 0-99. Higher is better.
Note that if the GPS module does not support producing GSV sentences this coroutine will pause forever. It can also pause for arbitrary periods if satellite reception is blocked, such as in a building.
These are updated whenever a sentence of the relevant type has been correctly
received from the GPS unit. For crucial navigation data the time_since_fix
method may be used to determine how current these values are.
The sentence type which updates a value is shown in brackets e.g. (GGA).
course
Track angle in degrees. (VTG).altitude
Metres above mean sea level. (GGA).geoid_height
Height of geoid (mean sea level) in metres above WGS84 ellipsoid. (GGA).magvar
Magnetic variation. Degrees. -ve == West. Current firmware does not produce this data: it will always read zero.
The following are counts since instantiation.
crc_fails
Usually 0 but can occur on baudrate change.clean_sentences
Number of sentences received without major failures.parsed_sentences
Sentences successfully parsed.unsupported_sentences
This is incremented if a sentence is received which has a valid format and checksum, but is not supported by the class. This value will also increment if these are supported in a subclass. See section 8.
utc
(property) [hrs: int, mins: int, secs: int] UTC time e.g. [23, 3, 58]. Note the integer seconds value. The MTK3339 chip provides a float but its value is always an integer. To achieve accurate subsecond timing see section 6.local_time
(property) [hrs: int, mins: int, secs: int] Local time.date
(property) [day: int, month: int, year: int] e.g. [23, 3, 18]local_offset
Local time offset in hrs as specified to constructor.epoch_time
Integer. Time in seconds since the epoch. Epoch start depends on whether running under MicroPython (Y2K) or Python 3.8+ (1970 on Unix).
The utc
, date
and local_time
properties updates on receipt of RMC
messages. If a nonzero local_offset
value is specified the date
value will
update when local time passes midnight (local time and date are computed from
epoch_time
).
satellites_in_view
No. of satellites in view. (GSV).satellites_in_use
No. of satellites in use. (GGA).satellites_used
List of satellite PRN's. (GSA).pdop
Dilution of precision (GSA).hdop
Horizontal dilution of precsion (GSA).vdop
Vertical dilution of precision (GSA).
Dilution of Precision (DOP) values close to 1.0 indicate excellent quality position data. Increasing values indicate decreasing precision.
The following public methods are null. They are intended for optional overriding in subclasses. Or monkey patching if you like that sort of thing.
reparse
Called after a supported sentence has been parsed.parse
Called when an unsupported sentence has been received.
If the received string is invalid (e.g. bad character or incorrect checksum) these will not be called.
Both receive as arguments a list of strings, each being a segment of the comma
separated sentence. The '$' character in the first arg and the '*' character
and subsequent characters are stripped from the last. Thus if the string
b'$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n'
was received reparse
would see
['GPGGA','123519','4807.038','N','01131.000','E','1','08','0.9','545.4','M','46.9','M','','']
FULL_CHECK
DefaultTrue
. If setFalse
disables CRC checking and other basic checks on received sentences. If GPS is linked directly to the target (rather than via long cables) these checks are arguably not neccessary.
This is a subclass of AS_GPS
and supports all its public methods, coroutines
and bound variables. It provides support for sending PMTK command packets to
GPS modules based on the MTK3329/MTK3339 chip. These include:
- Adafruit Ultimate GPS Breakout
- Digilent PmodGPS
- Sparkfun GPS Receiver LS20031
- 43oh MTK3339 GPS Launchpad Boosterpack
A subset of the PMTK packet types is supported but this may be extended by subclassing.
This assumes a Pyboard 1.x or Pyboard D with GPS on UART 4. To run issue:
import as_drivers.as_gps.ast_pbrw
The test script will pause until a fix has been achieved. After that changes
are made for about 1 minute reporting data at the REPL and on the LEDs. On
completion (or ctrl-c
) a factory reset is performed to restore the default
baudrate.
LED's:
- Red: Toggles each time a GPS update occurs.
- Green: ON if GPS data is being received, OFF if no data received for >10s.
- Yellow: Toggles each 4s if navigation updates are being received.
This reduces to 2s the interval at which the GPS sends messages:
import asyncio
from as_drivers.as_GPS.as_rwGPS import GPS
# Pyboard
#from machine import UART
#uart = UART(4, 9600)
# RP2
from machine import UART, Pin
uart = UART(0, 9600, tx=Pin(0), rx=Pin(1), timeout=5000, timeout_char=5000)
#
sreader = asyncio.StreamReader(uart) # Create a StreamReader
swriter = asyncio.StreamWriter(uart, {})
gps = GPS(sreader, swriter) # Instantiate GPS
async def test():
print('waiting for GPS data')
await gps.data_received(position=True, altitude=True)
await gps.update_interval(2000) # Reduce message rate
for _ in range(10):
print(gps.latitude(), gps.longitude(), gps.altitude)
await asyncio.sleep(2)
asyncio.run(test())
This takes two mandatory positional args:
sreader
This is aStreamReader
instance associated with the UART.swriter
This is aStreamWriter
instance associated with the UART.
Optional positional args:
local_offset
Local timezone offset in hours realtive to UTC (GMT).fix_cb
An optional callback which runs each time a valid fix is received.cb_mask
A bitmask determining which sentences will trigger the callback. DefaultRMC
: the callback will occur on RMC messages only (see below).fix_cb_args
A tuple of args for the callback.msg_cb
Optional callback. This will run if any handled message is received and also for unhandledPMTK
messages.msg_cb_args
A tuple of args for the above callback.
If implemented the message callback will receive the following positional args:
- The GPS instance.
- A list of text strings from the message.
- Any args provided in
msg_cb_args
.
In the case of handled messages the list of text strings has length 2. The
first is 'version', 'enabled' or 'antenna' followed by the value of the
relevant bound variable e.g. ['antenna', 3]
.
For unhandled messages text strings are as received, processed as per section 4.5.
The args presented to the fix callback are as described in section 4.1.
baudrate
Arg: baudrate. Must be 4800, 9600, 14400, 19200, 38400, 57600 or 115200. See below.update_interval
Arg: interval in ms. Default 1000. Must be between 100 and 10000. If the rate is to be increased see notes on timing.enable
Determine the frequency with which each sentence type is sent. A value of 0 disables a sentence, a value of 1 causes it to be sent with each received position fix. A value of N causes it to be sent once every N fixes.
It takes 7 keyword-only integer args, one for each supported sentence. These, with default values, are:
gll=0
,rmc=1
,vtg=1
,gga=1
,gsa=1
,gsv=5
,chan=0
. The last represents GPS channel status. These values are the factory defaults.command
Arg: a command from the following set:as_rwGPS.HOT_START
Use all available data in the chip's NV Store.as_rwGPS.WARM_START
Don't use Ephemeris at re-start.as_rwGPS.COLD_START
Don't use Time, Position, Almanacs and Ephemeris data at re-start.as_rwGPS.FULL_COLD_START
A 'cold_start', but additionally clear system/user configurations at re-start. That is, reset the receiver to the factory status.as_rwGPS.STANDBY
Put into standby mode. Sending any command resumes operation.as_rwGPS.DEFAULT_SENTENCES
Sets all sentence frequencies to factory default values as listed underenable
.as_rwGPS.VERSION
Causes the GPS to report its firmware version. This will appear as theversion
bound variable when the report is received.as_rwGPS.ENABLE
Causes the GPS to report the enabled status of the various message types as set by theenable
coroutine. This will appear as theenable
bound variable when the report is received.as_rwGPS.ANTENNA
Causes the GPS to send antenna status messages. The status value will appear in theantenna
bound variable each time a report is received.as_rwGPS.NO_ANTENNA
Turns off antenna messages.
Antenna issues In my testing the antenna functions have issues which
hopefully will be fixed in later firmware versions. The NO_ANTENNA
message
has no effect. And, while issuing the ANTENNA
message works, it affects the
response of the unit to subsequent commands. If possible issue it after all
other commands have been sent. I have also observed issues which can only be
cleared by power cycling the GPS.
I have experienced failures on a Pyboard V1.1 at baudrates higher than 19200. This may be a problem with my GPS hardware (see below).
Further, there are problems (at least with my GPS firmware build) where setting
baudrates only works for certain rates. This is clearly an issue with the GPS
unit; rates of 19200, 38400, 57600 and 115200 work. Setting 4800 sets 115200.
Importantly 9600 does nothing. Hence the only way to restore the default is to
perform a FULL_COLD_START
. The test programs do this.
If you change the GPS baudrate the UART should be re-initialised immediately
after the baudrate
coroutine terminates:
async def change_status(gps, uart):
await gps.baudrate(19200)
uart.init(19200)
At risk of stating the obvious to seasoned programmers, say your application
changes the GPS unit's baudrate. If interrupted (with a bug or ctrl-c
) the
GPS will still be running at the new baudrate. The application may need to be
designed to reflect this: see ast_pbrw.py
which uses try-finally
to reset
the baudrate in the event that the program terminates due to an exception or
otherwise.
Particular care needs to be used if a backup battery is employed as the GPS will then remember its baudrate over a power cycle.
See also notes on timing.
These are updated when a response to a command is received. The time taken for this to occur depends on the GPS unit. One solution is to implement a message callback. Alternatively await a coroutine which periodically (in intervals measured in seconds) polls the value, returning it when it changes.
version
InitiallyNone
. A list of version strings.enabled
InitiallyNone
. A dictionary of frequencies indexed by message type (seeenable
coroutine above).antenna
Initially 0. Values:
0 No report received.
1 Antenna fault.
2 Internal antenna.
3 External antenna.
The null parse
method in the base class is overridden. It intercepts the
single response to VERSION
and ENABLE
commands and updates the above bound
variables. The ANTENNA
command causes repeated messages to be sent. These
update the antenna
bound variable. These "handled" messages call the message
callback with the GPS
instance followed by a list of sentence segments
followed by any args specified in the constructor.
Other PMTK
messages are passed to the optional message callback as described
in section 5.3.
Many GPS chips (e.g. MTK3339) provide a PPS signal which is a pulse occurring at 1s intervals whose leading edge is a highly accurate UTC time reference.
This driver uses this pulse to provide accurate subsecond UTC and local time values. The driver requires MicroPython because PPS needs a pin interrupt.
On STM platforms such as the Pyboard it may be used to set and to calibrate the realtime clock (RTC). This functionality is not currently portable to other chips.
See Absolute accuracy for a discussion of the absolute accuracy provided by this module (believed to be on the order of +-70μs).
Two classes are provided: GPS_Timer
for read-only access to the GPS device
and GPS_RWTimer
for read/write access.
as_GPS_time.py
Test scripts for read only driver.as_rwGPS_time.py
Test scripts for read/write driver.
On import, these will list available tests. Example usage:
import as_drivers.as_GPS.as_GPS_time as test
test.usec()
Pyboard:
import asyncio
import pyb
import as_drivers.as_GPS.as_tGPS as as_tGPS
async def test():
fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}'
red = pyb.LED(1)
green = pyb.LED(2)
uart = pyb.UART(4, 9600, read_buf_len=200)
sreader = asyncio.StreamReader(uart)
pps_pin = pyb.Pin('X3', pyb.Pin.IN)
gps_tim = as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1,
fix_cb=lambda *_: red.toggle(),
pps_cb=lambda *_: green.toggle())
print('Waiting for signal.')
await gps_tim.ready() # Wait for GPS to get a signal
await gps_tim.set_rtc() # Set RTC from GPS
while True:
await asyncio.sleep(1)
# In a precision app, get the time list without allocation:
t = gps_tim.get_t_split()
print(fstr.format(gps_tim.get_ms(), t[0], t[1], t[2], t[3]))
asyncio.run(test())
RP2 (note set_rtc function is Pyboard specific)
import asyncio
from machine import UART, Pin
import as_drivers.as_GPS.as_tGPS as as_tGPS
async def test():
fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}'
uart = UART(0, 9600, tx=Pin(0), rx=Pin(1), rxbuf=200, timeout=5000, timeout_char=5000)
sreader = asyncio.StreamReader(uart)
pps_pin = Pin(2, Pin.IN)
gps_tim = as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1,
fix_cb=lambda *_: print("fix"),
pps_cb=lambda *_: print("pps"))
print('Waiting for signal.')
await gps_tim.ready() # Wait for GPS to get a signal
while True:
await asyncio.sleep(1)
# In a precision app, get the time list without allocation:
t = gps_tim.get_t_split()
print(fstr.format(gps_tim.get_ms(), t[0], t[1], t[2], t[3]))
asyncio.run(test())
These classes inherit from AS_GPS
and GPS
respectively, with read-only and
read/write access to the GPS hardware. All public methods and bound variables of
the base classes are supported. Additional functionality is detailed below.
Mandatory positional args:
sreader
TheStreamReader
instance associated with the UART.pps_pin
An initialised inputPin
instance for the PPS signal.
Optional positional args:
local_offset
See base class for details of these args.fix_cb
cb_mask
fix_cb_args
pps_cb
Callback runs when a PPS interrupt occurs. The callback runs in an interrupt context so it should return quickly and cannot allocate RAM. Default is a null method. See below for callback args.pps_cb_args
Default()
. A tuple of args for the callback. The callback receives theGPS_Timer
instance as the first arg, followed by any args in the tuple.
This takes three mandatory positional args:
sreader
TheStreamReader
instance associated with the UART.swriter
TheStreamWriter
instance associated with the UART.pps_pin
An initialised inputPin
instance for the PPS signal.
Optional positional args:
local_offset
See base class for details of these args.fix_cb
cb_mask
fix_cb_args
msg_cb
msg_cb_args
pps_cb
Callback runs when a PPS interrupt occurs. The callback runs in an interrupt context so it should return quickly and cannot allocate RAM. Default is a null method. See below for callback args.pps_cb_args
Default()
. A tuple of args for the callback. The callback receives theGPS_RWTimer
instance as the first arg, followed by any args in the tuple.
The methods that return an accurate GPS time of day run as fast as possible. To
achieve this they avoid allocation and dispense with error checking: these
methods should not be called until a valid time/date message and PPS signal
have occurred. Await the ready
coroutine prior to first use. Subsequent calls
may occur without restriction; see usage example above.
These methods use the MicroPython microsecond timer to interpolate between PPS
pulses. They do not involve the RTC. Hence they should work on any MicroPython
target supporting machine.ticks_us
.
get_ms
No args. Returns an integer: the period past midnight in ms.get_t_split
No args. Returns time of day in a list of form[hrs: int, mins: int, secs: int, μs: int]
.close
No args. Shuts down the PPS pin interrupt handler. Usage is optional but in test situations avoids the ISR continuing to run after termination.
See Absolute accuracy for a discussion of the accuracy of these methods.
All MicroPython targets:
ready
No args. Pauses until a valid time/date message and PPS signal have occurred.
STM hosts only:
set_rtc
No args. Sets the RTC to GPS time. Coroutine pauses for up to 1s as it waits for a PPS pulse.delta
No args. Returns no. of μs RTC leads GPS. Coro pauses for up to 1s.calibrate
Arg: integer, no. of minutes to run default 5. Calibrates the RTC and returns the calibration factor for it.
The calibrate
coroutine sets the RTC (with any existing calibration removed)
and measures its drift with respect to the GPS time. This measurement becomes
more precise as time passes. It calculates a calibration value at 10s intervals
and prints progress information. When the calculated calibration factor is
repeatable within one digit (or the spcified time has elapsed) it terminates.
Typical run times are on the order of two miutes.
Achieving an accurate calibration factor takes time but does enable the Pyboard RTC to achieve timepiece quality results. Note that calibration is lost on power down: solutions are either to use an RTC backup battery or to store the calibration factor in a file (or in code) and re-apply it on startup.
Crystal oscillator frequency has a small temperature dependence; consequently the optimum calibration factor has a similar dependence. For best results allow the hardware to reach working temperature before calibrating.
The claimed absolute accuracy of the leading edge of the PPS signal is +-10ns.
In practice this is dwarfed by errors including latency in the MicroPython VM.
Nevertheless the get_ms
method can be expected to provide 1 digit (+-1ms)
accuracy and the get_t_split
method should provide accuracy on the order of
-5μs +65μs (standard deviation). This is based on a Pyboard running at 168MHz.
The reasoning behind this is discussed in
section 9.
Run by issuing
import as_drivers.as_GPS.as_GPS_time as test
test.time() # e.g.
This comprises the following test functions. Reset the chip with ctrl-d between runs.
time(minutes=1)
Print out GPS time values.calibrate(minutes=5)
Determine the calibration factor of the Pyboard RTC. Set it and calibrate it.drift(minutes=5)
Monitor the drift between RTC time and GPS time. At the end of the run, print the error in μs/hr and minutes/year.usec(minutes=1)
Measure the accuracy ofutime.ticks_us()
against the PPS signal. Print basic statistics at the end of the run. Provides an estimate of some limits to the absolute accuracy of theget_t_split
method as discussed above.
- GPRMC GP indicates NMEA sentence (US GPS system).
- GLRMC GL indicates GLONASS (Russian system).
- GNRMC GN GNSS (Global Navigation Satellite System).
- GPGLL
- GLGLL
- GPGGA
- GLGGA
- GNGGA
- GPVTG
- GLVTG
- GNVTG
- GPGSA
- GLGSA
- GPGSV
- GLGSV
These notes are for those wishing to adapt these drivers.
If support for further sentence types is required the AS_GPS
class may be
subclassed. If a correctly formed sentence with a valid checksum is received,
but is not supported, the parse
method is called. By default this is a
lambda
which ignores args and returns True
.
A subclass may override parse
to parse such sentences. An example this may be
found in the as_rwGPS.py
module.
The parse
method receives an arg segs
being a list of strings. These are
the parts of the sentence which were delimited by commas. See
section 4.5 for details.
The parse
method should return True
if the sentence was successfully
parsed, otherwise False
.
Where a sentence is successfully parsed by the driver, a null reparse
method
is called. It receives the same string list as parse
. It may be overridden in
a subclass, possibly to extract further information from the sentence.
These tests allow NMEA parsing to be verified in the absence of GPS hardware:
astests_pyb.py
Test with synthetic data on UART. GPS hardware replaced by a loopback on UART 4. Requires a Pyboard.astests.py
Test with synthetic data. Run on a PC under CPython 3.8+ or MicroPython. Run from thev3
directory at the REPL as follows:
from as_drivers.as_GPS.astests import run_tests
run_tests()
or at the command line:
$ micropython -m as_drivers.as_GPS.astests
At the default 1s update rate the GPS hardware emits a PPS pulse followed by a set of messages. It then remains silent until the next PPS. At the default baudrate of 9600 the UART continued receiving data for 400ms when a set of GPSV messages came in. This time could be longer depending on data. So if an update rate higher than the default 1 second is to be used, either the baudrate should be increased or satellite information messages should be disabled.
The accuracy of the timing drivers may be degraded if a PPS pulse arrives while the UART is still receiving. The update rate should be chosen to avoid this.
The PPS signal on the MTK3339 occurs only when a fix has been achieved. The leading edge occurs on a 1s boundary with high absolute accuracy. It therefore follows that the RMC message carrying the time/date of that second arrives after the leading edge (because of processing and UART latency). It is also the case that on a one-second boundary minutes, hours and the date may roll over.
Further, the local_time offset can affect the date. These drivers aim to handle
these factors. They do this by storing the epoch time (as an integer number of
seconds) as the fundamental time reference. This is updated by the RMC message.
The utc
, date
and localtime
properties convert this to usable values with
the latter two using the local_offset
value to ensure correct results.
Without an atomic clock synchronised to a Tier 1 NTP server, absolute accuracy (Einstein notwithstanding :-)) is hard to prove. However if the manufacturer's claim of the accuracy of the PPS signal is accepted, the errors contributed by MicroPython can be estimated.
The driver interpolates between PPS pulses using utime.ticks_us()
to provide
μs precision. The leading edge of PPS triggers an interrupt which records the
arrival time of PPS in the acquired
bound variable. The ISR also records, to
1 second precision, an accurate datetime derived from the previous RMC message.
The time can therefore be estimated by taking the datetime and adding the
elapsed time since the time stored in the acquired
bound variable. This is
subject to the following errors:
Sources of fixed lag:
- Latency in the function used to retrieve the time.
- Mean value of the interrupt latency.
Sources of variable error:
- Variations in interrupt latency (small on Pyboard).
- Inaccuracy in the
ticks_us
timer (significant over a 1 second interval).
With correct usage when the PPS interrupt occurs the UART will not be receiving
data (this can substantially affect ISR latency variability). Consequently, on
the Pyboard, variations in interrupt latency are small. Using an osciloscope a
normal latency of 15μs was measured with the time
test in as_GPS_time.py
running. The maximum observed was 17μs.
The test program as_GPS_time.py
has a test usecs
which aims to assess the
sources of variable error. Over a period it repeatedly uses ticks_us
to
measure the time between PPS pulses. Given that the actual time is effectively
constant the measurement is of error relative to the expected value of 1s. At
the end of the measurement period the test calculates some simple statistics on
the results. On targets other than a 168MHz Pyboard this may be run to estimate
overheads.
The timing method get_t_split
measures the time when it is called, which it
records as quickly as possible. Assuming this has a similar latency to the ISR
there is likely to be a 30μs lag coupled with ~+-35μs (SD) jitter largely
caused by inaccuracy of ticks_us
over a 1 second period. Note that I have
halved the jitter time on the basis that the timing method is called
asynchronously to PPS: the interval will centre on 0.5s. The assumption is that
inaccuracy in the ticks_us
timer measured in μs is proportional to the
duration over which it is measured.
If space on the filesystem is limited, unneccessary files may be deleted. Many applications will not need the read/write or timing files.
as_GPS.py
The library. Supports theAS_GPS
class for read-only access to GPS hardware.as_GPS_utils.py
Additional formatted string methods forAS_GPS
. On RAM-constrained devices this may be omitted in which case thedate_string
andcompass_direction
methods will be unavailable.
Demos. Written for Pyboard but readily portable.
ast_pb.py
Test/demo program: assumes a Pyboard with GPS connected to UART 4.log_kml.py
A simple demo which logs a route travelled to a .kml file which may be displayed on Google Earth.
as_rwGPS.py
Supports theGPS
class. This subclass ofAS_GPS
enables writing PMTK packets.
Demo. Written for Pyboard but readily portable.
ast_pbrw.py
as_tGPS.py
The library. ProvidesGPS_Timer
andGPS_RWTimer
classes. Cross platform.
Note that the following are Pyboard specific and require the threadsafe
directory to be copied to the target.
as_GPS_time.py
Test scripts for read only driver (Pyboard).as_rwGPS_time.py
Test scripts for read/write driver (Pyboard).
These tests allow NMEA parsing to be verified in the absence of GPS hardware. For those modifying or extending the sentence parsing:
astests.py
Test with synthetic data. Run on PC under CPython 3.8+ or MicroPython.astests_pyb.py
Test with synthetic data on UART. GPS hardware replaced by a loopback on UART 4. Requires a Pyboard.