labjack-controller
Demonstration Notebook¶Montgomery, University of Southern Maine
The majority of operations included in the labjack-controller
library are very straightforward and require no explaination. However, there are some configurations of the LJM devices that ultimately lead to non-intuitive behavior. In this notebook, we attempt to explain these results and explain the root cause behind it.
[1]:
import pandas as pd
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show, output_notebook
output_notebook()
This library is divided up into multiple objects; the two that we expect users to interact with is the LabjackReader
object, which represents a LJM device (T4, T7, T7-Pro, and Digit devices in a very limited sense, and the LJMLibrary
singleton, which controls global library functionality. It is very unlikely you will ever need to use LJMLibrary
in most streaming applications.
[2]:
from labjackcontroller.labtools import LabjackReader, LJMLibrary
Before doing anything, let’s play dumb and assume we don’t know what devices are plugged in, and list all devices that the computer knows about. To do this, we need a reference to the base library.
[3]:
ljmlib_reference = LJMLibrary()
Now, we can list all connected devices. Output format is in the format (device model, connection type, serial number, IP) We’ll use the zeroth device we find.
[4]:
all_devices = ljmlib_reference.list_all()
all_devices
[4]:
[('T7', 'USB', 470014713, '0.0.0.0')]
The default constructor for the LabjackReader
expects you to give it at least a model name (“T7”, for instance), and it will figure out the rest. This is great if you have a simple setup with only one or two devices, because the model is enough to serve as a unique identifier. In real life, this never happens, so you’ll need to use the serial number or IP of the device as arguments to the device_identifier
kwarg.
[5]:
# Connect to the zeroth device, using the model, connection type, and serial number we found.
my_lj = LabjackReader(*all_devices[0][:3])
Before we proceed any further, it is important to understand that the method we connect to our LJM device will affect latency when communicating to this notebook. WiFi (a T7 Pro only feature) has the most latency, USB the second most, Ethernet has the least.
The effect of this latency is to increase the amount of time between the device recording data and the notebook recieving it; this means when you plot device recorded time vs. system recorded time, there will be a notably offset along one axis. Additionally, if you are perofrming a time-sensitive live analysis of the data recorded by your LJM device, knowledge of the presence of this latency is important.
While this latency varies on device, cable, firmware, and release of the labjack-controller
library, we can get a general estimate on a Labjack T7 running firmware revision 1.046:
Method | USB High-High | USB Other | Ethernet | WiFi |
---|---|---|---|---|
No I/O - Overhead | 0.6 | 2.2 | 1.1 | 6.7 |
Read All DI | 0.7 | 2.3 | 1.2 | 6.8 |
Write All DO | 0.7 | 2.3 | 1.2 | 6.8 |
Write Both DACs | 0.7 | 2.3 | 1.2 | 6.8 |
Courtesy Labjack, Command-Response Data Rates. All times are in milliseconds.
Moving on to actual data recording, we provide the function collect_data
for streaming. This function has the following outline and characteristics: 1. Connect to the LJM device, a process that takes about 1-2 seconds. collect_data
will not try to open a connection if there is already one open, therefore, if you want to start a stream the moment an event occurs, you should pre-open a connection to the LJM device with the open
function. 2. Start a stream, a process that takes 1-2
seconds and cannot be avoided. 2. Start the actual scan process by reading the values on all the specified channels and treating it like a row of data. This row is put into a data bundle of size #inputs
+ 2 (for the device and recieved times) by scans_per_read
and conveyed over the provided connection using as many packets as needed. scans_per_read
is clearly constrained by the scan frequency; if your scan freqency is 100 Hz and you set scans_per_read
to be 50 Hz, you will get
two data bundles per second. 3. Scans record data of resolution resolution
.
There’s a lot to unpack here, so we’ll start from the top.
resolution
¶resolution
changes the number of bits of precision that each data channel is recorded at. A higher resolution limits the maximum stream frequency, as each scan takes longer. This limitation varies by model, so the datatables are reproduced below.
In the labjack-controller
package, the default resoultion is 4. Pick a setting that makes sense for your application.
scans_per_read
¶The reason why scans_per_read
can be set by the user is due to the fact that you can safely pick a value in the range [1, frequency] and get meaningfully different data characteristics.
When scans_per_read
is small, each data row will have a on-device recorded time similar to the notebook’s recorded recieving time, but you limit the frequency that can be effectively communicated at. When this parameter is similar to the frequency and frequency
\(\gg 1\), the host system gets a batch of many rows recorded at multiple times all at once, which leads to a System Time vs Device Time graph that is non-linear and instead looks like a collection of ascending 90-degree
turns.
By default, collect_data
configures this parameter to be as high as possible in order to max scanning at very high frequencies easy.
[6]:
# Scan the channel AIN0 in 10v mode for 15 seconds @ 10 kHz using the default scans_per_read
my_lj.collect_data(["AIN0"], [10.0], 15, 10000, resolution=1)
[6]:
(15.040863037109375, 0.0)
[7]:
fig_width = 800
tools = ["box_select", "box_zoom", "hover", "reset"]
[8]:
# Note the effect that waiting to assemble large data transmissions has.
datarun_source = ColumnDataSource(my_lj.to_dataframe()[4:])
time_fig = figure(plot_width=fig_width, title="Time vs. System Time",
x_axis_label="Device Time (sec)", y_axis_label="System Time (Sec)", tools=tools)
time_fig.line(source=datarun_source, x="Time", y="System Time")
show(time_fig)
Now, let’s demonstrate the effect of a small scans_per_read
:
[9]:
# Scan the channel AIN0 in 10v mode for 15 seconds @ 10 kHz using the default scans_per_read
my_lj.collect_data(["AIN0"], [10.0], 15, 10000, resolution=1, scans_per_read=1)
[9]:
(14.998372316360474, 0.0)
[10]:
# Our graph looks like a 45-degree line with a vertical offset to account to latency, as expected.
datarun_source = ColumnDataSource(my_lj.to_dataframe()[4:])
time_fig = figure(plot_width=fig_width, title="Time vs. System Time",
x_axis_label="Device Time (sec)", y_axis_label="System Time (Sec)", tools=tools)
time_fig.line(source=datarun_source, x="Time", y="System Time")
show(time_fig)
[ ]: