Sunday 30 April 2017

Raspberry Pi GPS

Raspberry Pi GPS

I covered a whole GPS solution using a Pi Zero in a previous post. This covered a lot of different components and the only one I haven't posted about is the GP-20U7 module for GPS.
This approach will also work for other GPS modules as GPSD supports lots of devices over serial UART and USB.
I've chosen the GP-20U7 basically because it's one of the cheaper options for GPS and it does a reasonable job. This GPS device provides information every second. GPS lock is a bit slow from cold start.
My post here will cover GPS usage using Python2.7 and not using other tools or languages.

Sadly the standard GPSD python library is just 2.7 but there are python 3 libraries ported outside the standard distro for the Pi.

GPS Installation

A couple of packages are needed to get GPS services up and running
> sudo apt-get install gpsd python-gps

There are some tools to see GPS on X and the command line.
Note that the clients will also install dependencies for X so I skip these for headless Pi Zero builds. If you would like to try them then install
> sudo apt-get install gpsd-clients

Edit /etc/defaults/gpsd. For the GP-20U7 module add info to the file
# Default settings for the gpsd init script and the hotplug wrapper.

# Start the gpsd daemon automatically at boot time
START_DAEMON="true"

# Use USB hotplugging to add new USB devices automatically to the daemon
USBAUTO="true"

# Devices gpsd should collect to at boot time.
# They need to be read/writeable, either by user gpsd or the group dialout.
DEVICES="/dev/ttyAMA0"

# Other options you want to pass to gpsd
GPSD_OPTIONS=""

Ensure the serial ports are enabled and the login shell is disabled.
Run raspi-config and access Advanced options
> sudo raspi-config

Select the Serial option.


When asked if you want the login shell accessible, answer No.
This removes references from the /etc/inittab and /boot/cmdline.txt.
The serial port should now be free for the GPS module to use.

Hardware Setup

Ensure there's no power to the hardware before plugging in the GPS module.

The image shows a Raspberry Pi Zero, but the 40 pin header is the same for Pi 2 and Pi 3 so the wiring is the same. 
Power to VCC is from the 3.3V pin. GND can connect to any group pin on the Raspberry Pi header. TX pin on the GPS module must connect to the RX pin on the Raspberry Pi.

The connector on the GP-20U7 is a JST female, and unless you have a male JST adapter the wires will need cutting to add whatever connector you need. I use 0.1 inch female crimped housing which can be found from various online vendors (linked to one vendor as an example).

Running GPSD

Executing the service requires enabling the service and socket. This will ensure the service runs on start-up. 
> sudo systemctl enable gpsd.service
> sudo systemctl enable gpsd.socket
> sudo systemctl start gpsd.service

The service will run and respond to requests for GPS data using the loopback interface (127.0.0.1) on the default port of 2947.
To change the interface and port the /etc/systemd/system/sockets.target.wants/gpsd.socket file can be updated with the required settings

Other Pi Issues

The year may be incorrectly displayed if the Pi clock hasn't been updated over NTP, set manually or maintained by a real time clock. I've not yet experimented with different clock settings for the GP-20U7 and it may or may not be an issue.

Example Code

All example code is for python 2.7.

None of the code is GP-20U7 specific. If you have a GPS module working with GPSD then this should work.

Each example is more advanced than the last. This should help you discover the appropriate level of code to start working from.

Python Example - 1

This example is about as simple as possible to test that GPSD is running as expected

import gps

gpsobj = gps.gps('127.0.0.1', 2947) ;
gpsobj.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)

try:
    for rpt in gpsobj:
        try:
            print rpt

        except StopIteration:
            # Attempt to restart
            gpsobj = gps.gps()
            gpsobj.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)

except:
    gpsobj.stream(gps.WATCH_DISABLE)

print ("\nTerminated GPS Example 1")

This spits out a report from the python gpsd library. There's a lot of information here and can take a bit of time to figure out what is what. There is detailed documentation for the gpsd_json data, which explains all of the output.

Note that gpsd requires a continuous loop to keep up and read data. Clients which do not read data before the stream buffer is full will be disconnected by the gpsd daemon. 

Python Example - 2

This example shows the extraction of TPV data, which is typically what you need for any GPS application as this record provides all the location, time and speed info.
In addition to this the SKY information is also read to provide some information on satellites, although this isn't essential.
All data fields cannot be assumed to be present and each needs to be checked.
The key field to check is the mode data field. Until this equals 2 or 3 the location isn't fixed.
When mode is 2 then the data is 2D information and although the location is fixed, there's no altitude data.
When mode is 3 then the data is 3D meaning all longitude, latitude and altitude is fixed.

import gps
import dateutil.parser

kmtomiles = 0.621371

info = {'mode':0,
        'gpstime':'',
        'error_time':0,
        'latitude':0,
        'longitude':0,
        'error_latitude':0,
        'error_longitude':0,
        'altitude':0,
        'error_altitude':0,
        'speedkm':0,
        'speedmiles':0,
        'error_speed':0,
        'climb':0,
        'error_climb':0,
        'satellites':0,
        'satellites_used':0}

gpsobj = gps.gps() ;
gpsobj.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)

# Print a header for values being displayed
print(str(info.keys()).strip('[]'))

try:
    while True:
        try:
            gpsdat = gpsobj.next()
            if gpsdat['class'] == 'TPV': # Read time-position-velocity data
                if hasattr(gpsdat, 'time'):
                    nix_time = dateutil.parser.parse(gpsdat.time)
                    info['gpstime'] = str(nix_time)
                if hasattr(gpsdat, 'ept'): info['error_time'] = float(gpsdat.ept)
                if hasattr(gpsdat, 'mode'): info['mode'] = int(gpsdat.mode)
                if hasattr(gpsdat, 'lat'): info['latitude'] = float(gpsdat.lat)
                if hasattr(gpsdat, 'lon'): info['longitude'] = float(gpsdat.lon)
                if hasattr(gpsdat, 'epy'): info['error_latitude'] = float(gpsdat.epy)
                if hasattr(gpsdat, 'epx'): info['error_longitude'] = float(gpsdat.epx)
                if hasattr(gpsdat, 'alt'): info['altitude'] = float(gpsdat.alt)
                if hasattr(gpsdat, 'epv'): info['error_altitude'] = float(gpsdat.epv)
                if hasattr(gpsdat, 'speed'):
                    info['speedkm'] = float(gpsdat.speed)
                    info['speedmiles'] = float(gpsdat.speed) * kmtomiles
                if hasattr(gpsdat, 'eps'): info['error_speed'] = float(gpsdat.eps)
                if hasattr(gpsdat, 'climb'): info['climb'] = float(gpsdat.climb)
                if hasattr(gpsdat, 'epc'): info['error_climb'] = float(gpsdat.epc)
            if hasattr(gpsdat, 'satellites'): # Read sky data
                satellites_used = 0
                for sat in gpsdat.satellites:
                    if hasattr(sat, 'used'):
                        if sat.used:
                            satellites_used += 1
                info['satellites'] = len(gpsdat.satellites)
                info['satellites_used'] = satellites_used
                
            print(str(info.values()).strip('[]'))
                
        except KeyError:
            pass # Ignore key errors
        except StopIteration:
        # Attempt to restart
            gpsobj = gps.gps()
            gpsobj.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)

except KeyboardInterrupt:
    gpsobj.stream(gps.WATCH_DISABLE)
    print ("\nTerminated GPS Example 2")

Python Example - 3

Let's get into something more advanced with a threaded GPS application. An application needs to constantly poll and consume GPSD data to keep up-to-date and prevent GPSD from closing the data stream. This is a bit problematic for applications that need to do other things like wait for user input. 
The solution is to run the GPS updates in the background and provide a utility to get position data when required.
This example provides a GPSWorker object which does all the hard work in another thread. At the end of the example you can see a simulated application which waits for user input.

Enter q to quit and any other input followed by Enter to print GPS data. This demonstrates an application that has to wait on other inputs and processing without concern about consuming GPS data.
The example here just extracts longitude, latitude and altitude, but any of the other data points can be read in an application. 

import gps
import dateutil.parser
from threading import Thread, Lock

class GPSWorker(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.info = {'mode':0,
                     'gpstime':'',
                     'error_time':0,
                     'latitude':0,
                     'longitude':0,
                     'error_latitude':0,
                     'error_longitude':0,
                     'altitude':0,
                     'error_altitude':0,
                     'speedkm':0,
                     'speedmiles':0,
                     'error_speed':0,
                     'climb':0,
                     'error_climb':0,
                     'satellites':0,
                     'satellites_used':0}

        self.gpsobj = gps.gps() ;
        self.__quit = False
        self.__lock = Lock()
        self.__started = False 

    def start(self):
        self.gpsobj.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
        if not self.__started:
            Thread.start(self)
        
    def stop(self):
        if self.is_alive():
            self.gpsobj.stream(gps.WATCH_DISABLE)

    def terminate(self):
        self.__quit = True

        # start the streaming again to unblock the wait
        self.gpsobj.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
        if self.is_alive():
            self.join()
            self.__started = False

    def get_location_info(self):
        self.__lock.acquire()
        ret = self.info.copy()
        self.__lock.release()
        return ret
            
    def run(self):
        # Print a header for values being displayed
        #print(str(info.keys()).strip('[]'))
        kmtomiles = 0.621371

        while not self.__quit:
            try:
                gpsdat = self.gpsobj.next()
                self.__lock.acquire()
                if gpsdat['class'] == 'TPV': # Read time-position-velocity data
                    if hasattr(gpsdat, 'time'):
                        nix_time = dateutil.parser.parse(gpsdat.time)
                        self.info['gpstime'] = str(nix_time)
                    if hasattr(gpsdat, 'ept'): self.info['error_time'] = float(gpsdat.ept)
                    if hasattr(gpsdat, 'mode'): self.info['mode'] = int(gpsdat.mode)
                    if hasattr(gpsdat, 'lat'): self.info['latitude'] = float(gpsdat.lat)
                    if hasattr(gpsdat, 'lon'): self.info['longitude'] = float(gpsdat.lon)
                    if hasattr(gpsdat, 'epy'): self.info['error_latitude'] = float(gpsdat.epy)
                    if hasattr(gpsdat, 'epx'): self.info['error_longitude'] = float(gpsdat.epx)
                    if hasattr(gpsdat, 'alt'): self.info['altitude'] = float(gpsdat.alt)
                    if hasattr(gpsdat, 'epv'): self.info['error_altitude'] = float(gpsdat.epv)
                    if hasattr(gpsdat, 'speed'):
                        self.info['speedkm'] = float(gpsdat.speed)
                        self.info['speedmiles'] = float(gpsdat.speed) * kmtomiles
                    if hasattr(gpsdat, 'eps'): self.info['error_speed'] = float(gpsdat.eps)
                    if hasattr(gpsdat, 'climb'): self.info['climb'] = float(gpsdat.climb)
                    if hasattr(gpsdat, 'epc'): self.info['error_climb'] = float(gpsdat.epc)
                if hasattr(gpsdat, 'satellites'): # Read sky data
                    satellites_used = 0
                    for sat in gpsdat.satellites:
                        if hasattr(sat, 'used'):
                            if sat.used:
                                satellites_used += 1
                    self.info['satellites'] = len(gpsdat.satellites)
                    self.info['satellites_used'] = satellites_used
                                
            except KeyError:
                pass
            except StopIteration:
                # Attempt to restart
                self.gpsobj = gps.gps()
                self.gpsobj.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
            finally:
                self.__lock.release()

# Main execution
if __name__ == "__main__":
    mygps = GPSWorker()
    mygps.start()
    try:
        while True:
            # Do stuff in your application
            # The application can call .start and .stop to control the gpsd stream

            # simulate stuff with user input
            response = raw_input("Type 'q' to quit and anything else to check GPS: ")
            if response == 'q' or response == 'Q':
                break

            # Where am I?
            info = mygps.get_location_info()
            if info['mode'] < 2:
                print ("No GPS lock")
            elif info['mode'] == 2:
                print ("2D Lock - LAT:{0}, LON:{1}".format(
                    info['longitude'],
                    info['latitude']))
            else:
                print ("3D Lock - LAT:{0}, LON:{1}, ALT:{2}".format(
                    info['longitude'],
                    info['latitude'],
                    info['altitude']))
            
    except KeyboardInterrupt:
        print ("") # skip over ^C
    except:
        mygps.terminate() # Call terminate to kill the thread
        raise
        
    mygps.terminate() # Call terminate to kill the thread

    print ("Application closing")


Closing Comments

GPSD does most of the hard work of reading the data from the hardware and converting it into a standard data stream. 
I've shown an example using a serial device connected to the 40 pin header. USB dongles can be found which should slot straight into a Pi and also work with the example code (you will need to use the serial /dev/ name instead of /dev/ttyAMA0)

GPS receivers also provide an additional feature for a Raspberry Pi because it also provides a clock source when your device is not connected to a real time clock or on a network. 

No comments:

Post a Comment