.. 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)
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)
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"])
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"])
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)
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))
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)
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"])
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))
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"]);
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.