.. gps-receivers:

GPS Receivers

Most GPS receivers have data logging capabilities that you can use with Pipecat to view navigational information. Some receivers connect to your computer via a serial port or a serial-over-USB cable that acts like a traditional serial port. Others can push data to a network socket. For this demonstration, we will receive GPS data sent from an iPhone to a UDP socket:


In [1]:
# nbconvert: hide
from __future__ import absolute_import, division, print_function

import sys
sys.path.append("../features/steps")

import test
socket = test.mock_module("socket")

path = "../data/gps"
client = "172.10.0.20"

socket.socket().recvfrom.side_effect = test.recvfrom_file(path=path, client=client, stop=6)

In [2]:
import pipecat.record
import pipecat.udp
pipe = pipecat.udp.receive(address=("0.0.0.0", 7777), maxsize=1024)
for record in pipe:
    pipecat.record.dump(record)


client: 172.10.0.20
message: $GPTXT,01,01,07,Pipecat*12


client: 172.10.0.20
message: $GPGGA,164100,3511.33136,N,10643.48435,W,1,8,0.9,1654.0,M,46.9,M,0,2*50


client: 172.10.0.20
message: $GPRMC,164100,A,3511.33136,N,10643.48435,W,0.00,0.00,311216,003.1,W*7C


client: 172.10.0.20
message: $GPGLL,3511.33136,N,10643.48435,W,164100,A*36


client: 172.10.0.20
message: $HCHDG,129.5,,,8.7,E*29


client: 172.10.0.20
message: $PASHR,164100190,138.24,T,+32.56,+48.49,+00.00,3.141,3.141,35.000,1,0*17


Here, we used :func:pipecat.udp.receive to open a UDP socket listening on port 7777 on all available network interfaces ("0.0.0.0") and convert the received messages into Pipecat :ref:records, which we dump to the console. Note that each record includes the address of the client (the phone in this case), along with a "message" field containing the raw data of the message. In this case the raw data is in NMEA format, a widely-used standard for exchanging navigational data. To decode the contents of each message, we add the appropriate Pipecat device to the end of the pipe:


In [3]:
# nbconvert: hide
socket.socket().recvfrom.side_effect = test.recvfrom_file(path=path, client=client, stop=6)

In [4]:
import pipecat.device.gps
pipe = pipecat.udp.receive(address=("0.0.0.0", 7777), maxsize=1024)
pipe = pipecat.device.gps.nmea(pipe, key="message")
for record in pipe:
    pipecat.record.dump(record)


id: GPTXT
text: Pipecat

altitude: 1654.0 meter
dop: 0.9
geoid-height: 46.9 meter
id: GPGGA
latitude: 35.188856 degree
longitude: -106.724739167 degree
quality: 1
satellites: 8
time: 164100

active: True
date: 311216
id: GPRMC
latitude: 35.188856 degree
longitude: -106.724739167 degree
speed: 0.0 knot
time: 164100
track: 0.0 degree
variation: -3.1 degree

active: True
id: GPGLL
latitude: 35.188856 degree
longitude: -106.724739167 degree
time: 164100

heading: 129.5 degree
id: HCHDG
variation: 8.7 degree

heading: 138.24 degree
heading-accuracy: 35.0 degree
heave: 0.0 meter
id: PASHR
pitch: 48.49 degree
pitch-accuracy: 3.141 degree
roll: 32.56 degree
roll-accuracy: 3.141 degree
time: 164100190

As you can see, :func:pipecat.device.gps.nmea has converted the raw NMEA messages into records containing human-readable navigational fields with appropriate physical units. Note that unlike the :ref:battery-chargers example, not every record produced by the GPS receiver has the same fields. The NMEA standard includes many different types of messages, and most GPS receivers will produce more than one type. This will increase the complexity of our code - for example, we might have to test for the presence of a field before extracting it from a record:


In [5]:
# nbconvert: hide
socket.socket().recvfrom.side_effect = test.recvfrom_file(path=path, client=client, start=100, stop=110)

In [6]:
pipe = pipecat.udp.receive(address=("0.0.0.0", 7777), maxsize=1024)
pipe = pipecat.device.gps.nmea(pipe, key="message")
for record in pipe:
    if "latitude" in record:
        print("Latitude:", record["latitude"], "Longitude:", record["longitude"])


Latitude: 35.1949926667 degree Longitude: -106.7111135 degree
Latitude: 35.1949926667 degree Longitude: -106.7111135 degree
Latitude: 35.1949926667 degree Longitude: -106.7111135 degree
Latitude: 35.1952843333 degree Longitude: -106.710192667 degree
Latitude: 35.1952843333 degree Longitude: -106.710192667 degree
Latitude: 35.1952843333 degree Longitude: -106.710192667 degree

Alternatively, we might use the record id field to key our code off a specific type of NMEA message:


In [7]:
# nbconvert: hide
socket.socket().recvfrom.side_effect = test.recvfrom_file(path=path, client=client, start=100, stop=120)

In [8]:
pipe = pipecat.udp.receive(address=("0.0.0.0", 7777), maxsize=1024)
pipe = pipecat.device.gps.nmea(pipe, key="message")
for record in pipe:
    if record["id"] == "PASHR":
        print("Pitch:", record["pitch"])


Pitch: 66.82 degree
Pitch: 67.3 degree
Pitch: 66.8 degree
Pitch: 66.18 degree

Another alternative would be to add a filter to our pipe so we only receive records that match some criteria:


In [9]:
# nbconvert: hide
socket.socket().recvfrom.side_effect = test.recvfrom_file(path=path, client=client, start=100, stop=120)

In [10]:
import pipecat.filter
pipe = pipecat.udp.receive(address=("0.0.0.0", 7777), maxsize=1024)
pipe = pipecat.device.gps.nmea(pipe, key="message")
pipe = pipecat.filter.keep(pipe, key="id", value="GPGLL")
for record in pipe:
    pipecat.record.dump(record)


active: True
id: GPGLL
latitude: 35.1949926667 degree
longitude: -106.7111135 degree
time: 164252

active: True
id: GPGLL
latitude: 35.1952843333 degree
longitude: -106.710192667 degree
time: 164257

active: True
id: GPGLL
latitude: 35.1956116667 degree
longitude: -106.709064 degree
time: 164303

active: True
id: GPGLL
latitude: 35.1958851667 degree
longitude: -106.708156167 degree
time: 164308

Note that :func:pipecat.filter.keep discards all records that don't meet the given criteria, which allows our downstream code to rely on the availability of specific fields.

Regardless of the logic you employ to identify fields of interest, Pipecat always makes it easy to convert units safely and explicitly:


In [11]:
# nbconvert: hide
socket.socket().recvfrom.side_effect = test.recvfrom_file(path=path, client=client, start=100, stop=120)

In [12]:
pipe = pipecat.udp.receive(address=("0.0.0.0", 7777), maxsize=1024)
pipe = pipecat.device.gps.nmea(pipe, key="message")
for record in pipe:
    if "speed" in record:
        print(record["speed"].to(pipecat.units.mph))


39.9320468464 mph
40.0586325857 mph
40.1276793526 mph
38.5626193033 mph

Let's explore other things we can do with our pipe. To begin, you might want to add additional metadata to the records returned from a device. For example, if you were collecting data from multiple devices you might want to "tag" records with a user-specific unique identifier:


In [13]:
# nbconvert: hide
socket.socket().recvfrom.side_effect = test.recvfrom_file(path=path, client=client, start=100, stop=115)

In [14]:
import pipecat.utility
pipe = pipecat.udp.receive(address=("0.0.0.0", 7777), maxsize=1024)
pipe = pipecat.device.gps.nmea(pipe, key="message")
pipe = pipecat.filter.keep(pipe, key="id", value="GPGLL")
pipe = pipecat.utility.add_field(pipe, "serial", "1237V")
for record in pipe:
    pipecat.record.dump(record)


active: True
id: GPGLL
latitude: 35.1949926667 degree
longitude: -106.7111135 degree
serial: 1237V
time: 164252

active: True
id: GPGLL
latitude: 35.1952843333 degree
longitude: -106.710192667 degree
serial: 1237V
time: 164257

active: True
id: GPGLL
latitude: 35.1956116667 degree
longitude: -106.709064 degree
serial: 1237V
time: 164303

Now let's consider calculating some simple statistics, such as our average speed on a trip. When we iterate over the contents of a pipe using a for loop, we receive one record at-a-time until the pipe is empty. We could keep track of a "running" average during iteration, and there are use-cases where that is the best way to solve the problem. However, for moderately-sized data, Pipecat provides a more convenient approach:


In [15]:
# nbconvert: hide
socket.socket().recvfrom.side_effect = test.recvfrom_file(path=path, client=client)

In [16]:
import pipecat.store
pipe = pipecat.udp.receive(address=("0.0.0.0", 7777), maxsize=1024)
pipe = pipecat.device.gps.nmea(pipe, key="message")
pipe = pipecat.store.cache(pipe)
for record in pipe:
    pass
print(pipe.table["speed"])


[  0.     0.    10.59  17.96   4.39  24.14  30.65  33.59  33.28  32.85  34.08  34.78  35.28  34.66  34.46  34.1   34.64  34.41  33.88  33.75  34.7   34.81  34.87  33.51  33.71  35.38  32.09  28.94  18.     0.     1.19  21.23  31.92  33.55  34.91  34.78  33.75  32.71  31.67  31.14  31.45  31.94  31.16  32.27  35.46  35.34  34.06  33.82  34.91  34.72  34.83  34.95  33.38  33.08  27.39   5.21   0.     2.45  22.68  33.1   33.8  34.64  33.96  34.37  34.81  32.75  29.55  21.71  13.8   14.48  27.29  25.21  11.68  13.86   9.16   0.  ] knot

Here, :func:pipecat.store.cache creates an in-memory cache that stores every record it receives. We have a do-nothing for loop that reads data from the charger to populate the cache. Once that's complete, we can use the cache table attribute to retrieve data from the cache using the same keys and syntax we would use with a record. Unlike a record, the cache returns every value for a given key at once (using a Numpy array), which makes it easy to compute the statistics we're interested in:


In [17]:
print("Average speed:", pipe.table["speed"].mean().to(pipecat.units.mph))


Average speed: 30.8533055116 mph

Consolidating fields using the cache is also perfect for generating plots with a library like Toyplot (http://toyplot.readthedocs.io):


In [18]:
import toyplot

canvas = toyplot.Canvas(width=600, height=400)
axes = canvas.cartesian(grid=(2, 1, 0), xlabel="Record #", ylabel="Speed (MPH)")
axes.plot(pipe.table["speed"].to(pipecat.units.mph))
axes = canvas.cartesian(grid=(2, 1, 1), xlabel="Record #", ylabel="Track")
axes.plot(pipe.table["track"]);


0255075Record #010203040Speed (MPH)0255075Record #0100200300Track

Note that nothing prevents us from doing useful work in the for loop that populates the cache, and nothing prevents us from accessing the cache within the loop. For example, we might want to display field values from individual records alongside a running average computed from the cache. Or we might want to update our plot periodically as the loop progresses.