# labjack-controller Demonstration Notebook

Montgomery, University of Southern Maine

Notebook I: Implications of Configurations

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()
Loading BokehJS ...

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

Step 1: Find a Labjack Device and Connect

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.

Step 2: Streaming for a Labjack Device

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.

Step 2a: Setting 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.

Step 2b: Setting 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)
[ ]: