_images/pipecat.png

Battery Chargers

Many modern “smart” battery chargers have data logging capabilities that you can use with Pipecat to view the state of a battery during charging. Typically, these chargers connect to your computer via a serial port or a serial-over-USB cable that acts like a traditional serial port. Data is then sent to the port during charging. For example, data from an iCharger 208B connected to a Mac computer using a USB cable could be read using the following:

import serial
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
for line in port:
    print(line)
$1;1;;12250;3874;93;0;0;0;0;0;0;0;0;291;236;0;20

$1;1;;12250;3875;92;0;0;0;0;0;0;0;0;294;236;0;17

$1;1;;12250;3877;89;0;0;0;0;0;0;0;0;291;236;1;29

Here, we used the pySerial (http://pyserial.readthedocs.io) library to open a serial port and read data from the charger, which sends data to the port one line at a time. Note that the device name for the port - “/dev/cu.SLAB_USBtoUART” in this case - will vary depending on your device and operating system. For example, you might use “COM1” or “COM2” on Windows, or “/dev/ttyS0” on Linux.

Our first step in using Pipecat to decipher the raw device output is to turn the for-loop from the previous example into a pipe:

import pipecat.utility
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
for record in pipe:
    print(record)
{'string': u'$1;1;;12250;3874;93;0;0;0;0;0;0;0;0;291;236;0;20rn'}
{'string': u'$1;1;;12250;3875;92;0;0;0;0;0;0;0;0;294;236;0;17rn'}
{'string': u'$1;1;;12250;3877;89;0;0;0;0;0;0;0;0;291;236;1;29rn'}

In this case, pipecat.utility.readline() converts the raw data into Records that store each line of data for further processing. To decode the contents of each line, we add the appropriate Pipecat device to the end of the pipe:

import pipecat.device.charger
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
for record in pipe:
    print(record)
{('battery', 'cell4', 'voltage'): <Quantity(0.0, 'volt')>, ('charger', 'temperature', 'external'): <Quantity(23.6, 'degC')>, ('battery', 'cell5', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell8', 'voltage'): <Quantity(0.0, 'volt')>, ('charger', 'mode'): 'charge', ('charger', 'temperature', 'internal'): <Quantity(29.1, 'degC')>, ('battery', 'cell2', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'voltage'): <Quantity(3.874, 'volt')>, ('battery', 'cell6', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell1', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'charge'): <Quantity(0.0, 'hour * milliampere')>, ('battery', 'cell3', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell7', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'current'): <Quantity(930.0, 'milliampere')>, ('charger', 'supply'): <Quantity(12.25, 'volt')>}
{('battery', 'cell4', 'voltage'): <Quantity(0.0, 'volt')>, ('charger', 'temperature', 'external'): <Quantity(23.6, 'degC')>, ('battery', 'cell5', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell8', 'voltage'): <Quantity(0.0, 'volt')>, ('charger', 'mode'): 'charge', ('charger', 'temperature', 'internal'): <Quantity(29.4, 'degC')>, ('battery', 'cell2', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'voltage'): <Quantity(3.875, 'volt')>, ('battery', 'cell6', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell1', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'charge'): <Quantity(0.0, 'hour * milliampere')>, ('battery', 'cell3', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell7', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'current'): <Quantity(920.0, 'milliampere')>, ('charger', 'supply'): <Quantity(12.25, 'volt')>}
{('battery', 'cell4', 'voltage'): <Quantity(0.0, 'volt')>, ('charger', 'temperature', 'external'): <Quantity(23.6, 'degC')>, ('battery', 'cell5', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell8', 'voltage'): <Quantity(0.0, 'volt')>, ('charger', 'mode'): 'charge', ('charger', 'temperature', 'internal'): <Quantity(29.1, 'degC')>, ('battery', 'cell2', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'voltage'): <Quantity(3.877, 'volt')>, ('battery', 'cell6', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell1', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'charge'): <Quantity(1.0, 'hour * milliampere')>, ('battery', 'cell3', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'cell7', 'voltage'): <Quantity(0.0, 'volt')>, ('battery', 'current'): <Quantity(890.0, 'milliampere')>, ('charger', 'supply'): <Quantity(12.25, 'volt')>}

As you can see, pipecat.device.charger.icharger208b() has converted the raw data records into charger-specific records containing human-readable fields whose values have appropriate physical units. Let’s use pipecat.record.dump() to make the output a little more readable:

import pipecat.record
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
for record in pipe:
    pipecat.record.dump(record)
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 930.0 milliampere
battery/voltage: 3.874 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.1 degC

battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 920.0 milliampere
battery/voltage: 3.875 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.4 degC

battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 1.0 hour * milliampere
battery/current: 890.0 milliampere
battery/voltage: 3.877 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.1 degC

Now, you can extract just the data you care about from a record:

port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
for record in pipe:
    print(record[("battery", "voltage")], record[("battery", "current")])
3.874 volt 930.0 milliampere
3.875 volt 920.0 milliampere
3.877 volt 890.0 milliampere

And you can convert units safely and explicitly:

port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
for record in pipe:
    print(record[("battery", "current")].to(pipecat.units.amps))
0.93 ampere
0.92 ampere
0.89 ampere

As an aside, you may be wondering at this point why it’s necessary to explicitly create the serial port and connect it to readline … why not code that functionality directly into icharger208b? The answer is flexibility: by separating Pipecat’s functionality into discrete, well-defined components, those components can be easily combined in new and unexpected ways. For example, you could use icharger208b with a charger that communicated over a network socket instead of a serial port. Or you could “replay” data from a charger stored in a file:

fobj = open("../data/icharger208b-charging-sample")
pipe = pipecat.utility.readline(fobj)
pipe = pipecat.device.charger.icharger208b(pipe)
for record in pipe:
    print(record[("battery", "charge")])
0.0 hour * milliampere
0.0 hour * milliampere
1.0 hour * milliampere
1.0 hour * milliampere
2.0 hour * milliampere

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, you might want to append timestamps:

port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.utility.add_timestamp(pipe)
for record in pipe:
    pipecat.record.dump(record)
timestamp: 2017-12-27T00:21:35.210018+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 930.0 milliampere
battery/voltage: 3.874 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.1 degC

timestamp: 2017-12-27T00:21:37.215579+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 920.0 milliampere
battery/voltage: 3.875 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.4 degC

timestamp: 2017-12-27T00:21:39.219548+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 1.0 hour * milliampere
battery/current: 890.0 milliampere
battery/voltage: 3.877 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.1 degC

Note that pipecat.utility.add_timestamp() has added a timestamp field to each record. Timestamps in Pipecat are always recorded using UTC (universal) time, so you will likely want to convert them to your local timezone before formatting them for display:

port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.utility.add_timestamp(pipe)
for record in pipe:
    print(
        record["timestamp"].to("local").format("YYYY-MM-DD hh:mm:ss a"),
        record[("battery", "voltage")],
    )
2017-12-26 05:21:41 pm 3.874 volt
2017-12-26 05:21:43 pm 3.875 volt
2017-12-26 05:21:45 pm 3.877 volt

You could also use pipecat.utility.add_field() to append your own custom field to every record that passes through the pipe.

Now let’s consider calculating some simple statistics, such as the minimum and maximum battery voltage while charging. 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” minimum and maximum 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:

import pipecat.store
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.store.cache(pipe)
for record in pipe:
    pass
print(len(pipe.table))
print(pipe.table[("battery", "voltage")])
1390
[ 3.874  3.875  3.877 ...,  4.119  4.119  4.119] volt

Here, 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:

import pipecat.store
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.store.cache(pipe)
for record in pipe:
    pass
print("Min:", pipe.table[("battery", "voltage")].min())
print("Max:", pipe.table[("battery", "voltage")].max())
Min: 3.874 volt
Max: 4.152 volt

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

import toyplot

canvas = toyplot.Canvas()
axes = canvas.cartesian(grid=(3, 1, 0), label="Battery", ylabel="Voltage (V)")
axes.plot(pipe.table[("battery", "voltage")].to(pipecat.units.volt))

axes = canvas.cartesian(grid=(3, 1, 1), ylabel="Current (A)")
axes.plot(pipe.table[("battery", "current")].to(pipecat.units.amp))

axes = canvas.cartesian(grid=(3, 1, 2), ylabel="Charge (mAH)")
axes.plot(pipe.table[("battery", "charge")].to(pipecat.units.milliamp * pipecat.units.hour));
0500100015003.94.04.04.2Voltage (V)Battery0500100015000.00.51.0Current (A)0500100015000100200300400500Charge (mAH)

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.

Moving on, you will likely want to store records to disk for later access. Pipecat provides components to make this easy too. First, you can add pipecat.store.pickle.write() to the end of a pipe, to write records to disk using Python’s pickle format:

import pipecat.store.pickle
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.utility.add_timestamp(pipe)
pipe = pipecat.store.pickle.write(pipe, "charger.pickle")
for record in pipe:
    pass

Later, you can use pipecat.store.pickle.read() to read the records back in again:

pipe = pipecat.store.pickle.read("charger.pickle")
pipe = pipecat.store.cache(pipe)
for record in pipe:
    pipecat.record.dump(record)
print("Average:", pipe.table[("battery", "voltage")].mean())
timestamp: 2017-12-27T00:21:49.522898+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 930.0 milliampere
battery/voltage: 3.874 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.1 degC

timestamp: 2017-12-27T00:21:51.525903+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 920.0 milliampere
battery/voltage: 3.875 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.4 degC

timestamp: 2017-12-27T00:21:53.532511+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 1.0 hour * milliampere
battery/current: 890.0 milliampere
battery/voltage: 3.877 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.1 degC

Average: 3.87533333333 volt

This is another example of the interchangeability of the Pipecat components: the pickle writer is a record consumer, and the pickle reader is a record generator. In essence, we “broke” our previous pipe into two separate pipes that communicate via the filesystem. While we won’t go into detail here, a similar approach could be used to communicate between threads using a message queue or between processes over a socket.

There is one final issue that we’ve ignored so far: when to stop logging data. The pipecat.utility.readline() function will read data from the serial port as long as the port is open, blocking indefinitely if no data is arriving. That means that for all the preceeding examples the for loop will never end unless the serial port is closed (i.e. the external device is turned-off or unplugged), or the code is interrupted using Control-C. While that’s fine for prototyping at the command line, we need to have a way to stop collecting data and shutdown cleanly if we’re going to automate data logging processes. Fortunately, Pipecat provides several easy-to-use functions to do just that:

import pipecat.limit
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.utility.add_timestamp(pipe)
pipe = pipecat.limit.count(pipe, count=2)
for record in pipe:
    pipecat.record.dump(record)
timestamp: 2017-12-27T00:21:55.588725+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 930.0 milliampere
battery/voltage: 3.874 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.1 degC

timestamp: 2017-12-27T00:21:57.597235+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 920.0 milliampere
battery/voltage: 3.875 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.4 degC

Here, pipecat.limit.count() ends the loop when count records have been received. This is often handy during development to limit the amount of data consumed from a device that produces output continuously. However, this approach is no good for devices like our charger that will produce a finite, indeterminate number of records - if the device stops sending records before the count has been reached, the loop will still block. Instead, we could use pipecat.limit.duration() to limit the total amount of time the loop is allowed to run instead:

import pipecat.limit
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.utility.add_timestamp(pipe)
pipe = pipecat.limit.duration(pipe, duration=pipecat.quantity(4, pipecat.units.seconds))
for record in pipe:
    pipecat.record.dump(record)
timestamp: 2017-12-27T00:21:57.614914+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 930.0 milliampere
battery/voltage: 3.874 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.1 degC

timestamp: 2017-12-27T00:21:59.615434+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 0.0 hour * milliampere
battery/current: 920.0 milliampere
battery/voltage: 3.875 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.6 degC
charger/temperature/internal: 29.4 degC

This approach is an improvement because it puts an upper-bound on the amount of time the loop will run, whether the device has stopped sending records or not. However, it’s still error-prone, since we don’t know in advance how long charging will take - if we set the duration too low, it may stop the loop before charging is complete. If we set the duration too high, we will capture all the records we want, but we will likely waste time waiting for records that will never come. Ideally, we would like to exit the loop as soon as the charger tells us it’s finished. Fortunately, the charger provides a field - charger/mode that can do just that:

import pipecat.limit
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.utility.add_timestamp(pipe)
pipe = pipecat.limit.until(pipe, key=("charger", "mode"), value="finished")
for record in pipe:
    pipecat.record.dump(record)
timestamp: 2017-12-27T00:22:01.639667+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 503.0 hour * milliampere
battery/current: 110.0 milliampere
battery/voltage: 4.141 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.8 degC
charger/temperature/internal: 32.5 degC

timestamp: 2017-12-27T00:22:03.648685+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 503.0 hour * milliampere
battery/current: 120.0 milliampere
battery/voltage: 4.14 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.9 degC
charger/temperature/internal: 32.7 degC

timestamp: 2017-12-27T00:22:05.653453+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 503.0 hour * milliampere
battery/current: 0.0 milliampere
battery/voltage: 4.138 volt
charger/mode: finished
charger/supply: 12.25 volt
charger/temperature/external: 23.9 degC
charger/temperature/internal: 32.7 degC

pipecat.limit.until() terminates the loop as soon as it receives a record with the given key and value. This approach finally gets us our desired behavior (loop ends as soon as the charger is finished), but it could use just a little more work to make it robust. For example, what happens if the charger stops sending data before the mode changes? We could combine pipecat.limit.until() with pipecat.limit.duration(), but that would still suffer from the terminate-too-soon / waste-too-much-time problem. Fortunately, we know from testing that our charger sends records every two seconds (if at all), and Pipecat provides pipecat.limit.timeout(), which can terminate the loop if it doesn’t receive a record within a specified time interval:

import pipecat.limit
port = serial.serial_for_url("/dev/cu.SLAB_USBtoUART", baudrate=128000)
pipe = pipecat.utility.readline(port)
pipe = pipecat.device.charger.icharger208b(pipe)
pipe = pipecat.utility.add_timestamp(pipe)
pipe = pipecat.limit.until(pipe, key=("charger", "mode"), value="finished")
pipe = pipecat.limit.timeout(pipe, timeout=pipecat.quantity(5, pipecat.units.seconds))
for record in pipe:
    pipecat.record.dump(record)
timestamp: 2017-12-27T00:22:05.673115+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 503.0 hour * milliampere
battery/current: 110.0 milliampere
battery/voltage: 4.141 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.8 degC
charger/temperature/internal: 32.5 degC

timestamp: 2017-12-27T00:22:07.678467+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 503.0 hour * milliampere
battery/current: 120.0 milliampere
battery/voltage: 4.14 volt
charger/mode: charge
charger/supply: 12.25 volt
charger/temperature/external: 23.9 degC
charger/temperature/internal: 32.7 degC

timestamp: 2017-12-27T00:22:09.683829+00:00
battery/cell1/voltage: 0.0 volt
battery/cell2/voltage: 0.0 volt
battery/cell3/voltage: 0.0 volt
battery/cell4/voltage: 0.0 volt
battery/cell5/voltage: 0.0 volt
battery/cell6/voltage: 0.0 volt
battery/cell7/voltage: 0.0 volt
battery/cell8/voltage: 0.0 volt
battery/charge: 503.0 hour * milliampere
battery/current: 0.0 milliampere
battery/voltage: 4.138 volt
charger/mode: finished
charger/supply: 12.25 volt
charger/temperature/external: 23.9 degC
charger/temperature/internal: 32.7 degC

This example still exits normally when the charger is finished, but it will also exit cleanly if a record isn’t received within a five seconds. More importantly, you can see how easy it is to combine limit functions to control when the for loop terminates.