labjack-controller is a Python package that enables powerful interactions with Labjack DAQs. Significant benefits include:
Getting started is as simple as
from labjackcontroller.labtools import LabjackReader
# Instantiate a LabjackReader
with LabjackReader("T7") as my_lj:
# Stream data on the analog channel AIN0 in 10v mode for 10 seconds at 100 Hz.
my_lj.collect_data(["AIN0"], [10.0], 10, 100)
# Get the data we collected.
print(my_lj.to_dataframe())
Note
The LJM library is a collection of C functions that Labjack has bundled with different tools depending on your platform. For example, on Linux, Kipling is also installed. None of these additional applications are needed to use this library.
You can install this package with pip through our PyPi package with the command
pip install labjackcontroller
Alternatively, you can install from this github repository with
git clone https://github.com/university-of-southern-maine-physics/labjack-controller.git
cd labjack-controller
pip install .
Licensed using the MIT license.
MIT License
Copyright (c) 2018 Ben Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
There are multiple demos for different aspects of this package avaliable.
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)
[ ]:
Using the information from last time, we can start doing fun things with LJM devices, like using parallel processing to back up our data as we acquire it.
[1]:
import pandas as pd
import matplotlib.pyplot as plt
from labjackcontroller.labtools import LabjackReader
from multiprocessing.managers import BaseManager
from multiprocessing import Process
import time
from bokeh.plotting import figure, show, output_notebook, gridplot
from bokeh.models.widgets import DataTable, TableColumn
from bokeh.models import ColumnDataSource
from bokeh.palettes import Spectral6
output_notebook()
[2]:
device_type = "T7"
connection_type = "USB"
duration = 180 # seconds
freq = 100 # sampling frequency
channels = ["AIN0", "AIN1", "AIN2", "AIN3"]
voltages = [10.0, 10.0, 10.0, 10.0]
[3]:
def backup(labjack: LabjackReader, filename: str, num_seconds: int) -> None:
"""
Simple function to backup all data into a pickle.
Parameters
----------
labjack: LabjackReader
A LabjackReader that is collecting data at the
time of this function's call.
filename: str
The name of the file to write to.
If it does not exist yet, it will be created.
num_seconds: int
The number of seconds to try live backup.
After this time, write any remaining data in
the labjack's buffer.
Returns
-------
None
"""
start_time = time.time()
# Write data until time is up.
while time.time() - start_time <= num_seconds:
if not (time.time() - start_time) % 60:
print("Backup at", time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()))
labjack.to_dataframe().to_pickle(filename)
BaseManagers can be used to share complex objects, attributes and all, across multiple processes. This is fantastic for our application, because we want all processes to be able to read the data we are in the process of collecting.
[4]:
BaseManager.register('LabjackReader', LabjackReader)
manager = BaseManager()
manager.start()
# Instantiate a shared LabjackReader
my_lj = manager.LabjackReader(device_type, connection_type=connection_type)
At this point, we declare the functions we want to run in parallel.
[5]:
# Declare a data-gathering process
data_proc = Process(target=my_lj.collect_data,
args=(channels, voltages, duration, freq),
kwargs={'resolution': 1, 'scans_per_read': 1})
# Declare a data backup process
backup_proc = Process(target=backup, args=(my_lj, "backup.pkl",
duration))
And now we actually run them.
[6]:
# Start all threads, and join when finished.
data_proc.start()
backup_proc.start()
data_proc.join()
backup_proc.join()
Backup at 2019-04-02 17:27:45
Backup at 2019-04-02 17:28:45
[7]:
datarun = my_lj.to_dataframe()
[8]:
fig_width = 800
tools = ["box_select", "box_zoom", "hover", "reset"]
[9]:
datarun_source = ColumnDataSource(datarun[4:])
# Table plot
Columns = [TableColumn(field=Ci, title=Ci) for Ci in datarun.columns] # bokeh columns
data_table = DataTable(columns=Columns, source=datarun_source, width=fig_width) # bokeh table
# Time graph
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")
# AIN0..3 vs device time graph
data_time_fig = figure(plot_width=fig_width, plot_height=400, title="AIN0-3 vs Device Time",
x_axis_label="Device Time (sec)", y_axis_label="Voltage (V)", tools=tools)
for i, column in enumerate(["AIN0", "AIN1", "AIN2", "AIN3"]):
data_time_fig.line(source=datarun_source, x="Time", y=column, line_width=1, color=Spectral6[i + 2],
alpha=0.8, muted_color=Spectral6[i + 2], muted_alpha=0.075,
legend=column + " Column")
data_time_fig.circle(source=datarun_source, x="Time", y=column, line_width=1, color=Spectral6[i + 2],
alpha=0.8, muted_color=Spectral6[i + 2], muted_alpha=0.075,
legend=column + " Column", size=1)
data_time_fig.legend.location = "top_left"
data_time_fig.legend.click_policy="mute"
# AIN0..3 vs system time graph
data_sys_time_fig = figure(plot_width=fig_width, plot_height=400, title="AIN0-3 vs System Time",
x_axis_label="System Time (sec)", y_axis_label="Voltage (V)", tools=tools)
for i, column in enumerate(["AIN0", "AIN1", "AIN2", "AIN3"]):
data_sys_time_fig.line(source=datarun_source, x="Time", y=column, line_width=1, color=Spectral6[i + 2],
alpha=0.8, muted_color=Spectral6[i + 2], muted_alpha=0.075,
legend=column + " Column")
data_sys_time_fig.circle(source=datarun_source, x="Time", y=column, line_width=1, color=Spectral6[i + 2],
alpha=0.8, muted_color=Spectral6[i + 2], muted_alpha=0.075,
legend=column + " Column", size=1)
data_sys_time_fig.legend.location = "top_left"
data_sys_time_fig.legend.click_policy="mute"
# Organize and show all plots.
p = gridplot([[data_sys_time_fig], [data_time_fig], [data_table], [time_fig]])
show(p)
[ ]: