Basic Usage¶
Connect to a Scope and save waveform data¶
This example shows how to use TekHSI and tm_data_types to pull analog waveforms from a
Tektronix oscilloscope and save them to a csv file very easily.
Important
Matching the type of waveform with the channel type is critical. See the supported data types section for more information.
"""A script for connecting to a scope, retrieving waveform data, and saving it to a file."""
from tm_data_types import AnalogWaveform, write_file
from tekhsi import AcqWaitOn, TekHSIConnect
addr = "192.168.0.1" # Replace with the IP address of your instrument
# Connect to instrument, select channel 1
with TekHSIConnect(f"{addr}:5000", ["ch1"]) as connect:
# Save data from 10 acquisitions as a set of CSV files
for i in range(10):
# Use AcqWaitOn.NextAcq to wait for the next new acquisition
with connect.access_data(AcqWaitOn.NextAcq):
wfm: AnalogWaveform = connect.get_data("ch1")
# Save the waveform to a file
write_file(f"{wfm.source_name}_{i}.csv", wfm)
Supported Data types¶
Currently, 3 data types are supported: AnalogWaveform,
DigitalWaveform, and
IQWaveform.
Please keep in mind that as this library evolves, we will be adding new data types.
Analog Waveforms¶
AnalogWaveform is the most commonly retrieved type of data from an
oscilloscope. It represents analog data captured by an oscilloscope. It is accessed by being
granted access to the data (which ensures access to the data from the current acquisition) and
then using get_data() and the name of the channel to be accessed. The data returned will be
whatever type is requested. In the example below, we know the type is Analog, so we type
waveform accordingly. This gives access to type hinting in your IDE.
"""Use TekHSI to plot an analog waveform."""
import matplotlib.pyplot as plt
from tm_data_types import AnalogWaveform
from tekhsi import TekHSIConnect
with TekHSIConnect("192.168.0.1:5000") as connection:
# Get one data set to setup plot
with connection.access_data():
waveform: AnalogWaveform = connection.get_data("ch1")
# Data converted into vertical units
# This is the usual way to access the data
vd = waveform.normalized_vertical_values
# Horizontal Times - returns an array of times
# that corresponds to the time at each index in
# vertical array
hd = waveform.normalized_horizontal_values
# Simple Plot Example
_, ax = plt.subplots()
ax.plot(hd, vd)
ax.set(xlabel=waveform.x_axis_units, ylabel=waveform.y_axis_units, title="Simple Plot")
plt.show()
Digital Waveforms¶
DigitalWaveform is available when you have plugged a digital
probe into a scope channel and have turned on that channel. This returns an array of n-bit integers
that represent the digital data, where ‘n’ is the number of bits available on the digital probe.
In addition, there are special methods for digital waveforms available from
tm_data_types. Probably the most useful is
get_nth_bitstream()
which returns just one of the selected bits as a bitstream for use.
"""Use TekHSI to plot a digital waveform."""
import matplotlib.pyplot as plt
import numpy as np
from tm_data_types import DigitalWaveform
from tekhsi import AcqWaitOn, TekHSIConnect
with TekHSIConnect("192.168.0.1:5000") as connection:
# Get one data set to set up plot
with connection.access_data(AcqWaitOn.NewData):
waveform: DigitalWaveform = connection.get_data("ch4_DAll")
# Digital retrieval of bit 3 in the digital array
vd = waveform.get_nth_bitstream(3).astype(np.float32)
# Horizontal Times - returns an array of times
# that corresponds to the time at each index in
# vertical array
hd = waveform.normalized_horizontal_values
# Simple Plot Example
_, ax = plt.subplots()
ax.plot(hd, vd)
ax.set(xlabel=waveform.x_axis_units, ylabel=waveform.y_axis_units, title="Simple Plot")
plt.show()
IQ Waveforms¶
IQWaveform data allows for live-streaming of IQ data from an
oscilloscope. Turning Spectrum View on for a channel causes a corresponding symbol to be available.
These symbols can be accessed using the appropriate name with get_data().
Proper usage of the IQWaveform type requires accessing metadata about the waveform. Other than that,
the data type was designed to make usage with signal processing libraries a breeze. The example
below shows how easy it is to feed that live data into Python libraries.
This shows the minimal code required to display a spectrogram using matplotlib.
"""Use TekHSI to plot an IQ waveform."""
import matplotlib.pyplot as plt
from tm_data_types import IQWaveform
from tekhsi import TekHSIConnect
with TekHSIConnect("192.168.0.1:5000") as connection:
# Get one data set to setup plot
with connection.access_data():
waveform: IQWaveform = connection.get_data("ch1_iq")
# IQ Data Access (Complex Array)
iq_data = waveform.normalized_vertical_values
# Simple Plot Example
_, ax = plt.subplots()
ax.specgram(
iq_data,
NFFT=int(waveform.meta_info.iq_fft_length),
Fc=waveform.meta_info.iq_center_frequency,
Fs=waveform.meta_info.iq_sample_rate,
)
ax.set_title("Spectrogram")
plt.show()
TekHSIConnect¶
The TekHSIConnect class handles the connection and
data retrieval for TekHSI, check out its API documentation
for more information.
Acquisition Filters¶
An acquisition filter allows custom rules to be applied that can be used to filter (or restrict)
the acquisitions that are accepted for processing by the client. Normally the filter is set to None,
which lets all acquisitions through. However, there are several predefined filters that can be used:
any_acq- This is equivalent to setting the acquisition filter toNone, and it allows all acquisitions to be processed.any_vertical_change- This looks at the previous and current acquisition and checks to see if any of the channels have seen any vertical change. If so, that acquisition is processed, otherwise it is skipped.any_horizontal_change- This looks at the previous and current acquisition and checks to see if any of the channels have seen any horizontal change. If so, that acquisition is processed, otherwise it is skipped.
Custom rules can also be created by the user.
These filters (pre-defined or user-defined) are either set during TekHSIConnect
instantiation or by using the set_acq_filter() method.
Below is the source code of any_horizontal_change().
The arguments are the previous header and the current header. This allows you to compare changes
from the current headers against the previous header. If this returns True the acquisition is
passed on, otherwise it is ignored. This provides an easy way to only consider the changes which are
relevant. For example, this allows a change to be made to the scope while it is continuously running
and then only consider the change when it arrives. It reduces the need for expensive
synchronization using start and *OPC?.
def any_horizontal_change(
previous_header: dict[str, WaveformHeader],
current_header: dict[str, WaveformHeader],
) -> bool:
"""Acq acceptance filter that accepts only acqs with changes to horizontal settings.
Args:
previous_header: Previous header dictionary.
current_header: Current header dictionary.
Returns:
True if the acquisition is accepted, False otherwise.
"""
for key, cur in current_header.items():
if key not in previous_header:
return True
prev = previous_header[key]
if prev is None and cur is not None:
return True
if prev is not None and (
prev.noofsamples != cur.noofsamples
or prev.horizontalspacing != cur.horizontalspacing
or prev.horizontalzeroindex != cur.horizontalzeroindex
):
return True
return False
Mixing TekHSI and PyVISA¶
TekHSI is compatible with PyVISA (or tm_devices!) and provides
several benefits over the traditional data retrieval methods.
TekHSIis much faster than using curve queries, because no data transformation is done on the scope, only the underlying binary data is moved. This means there is no need to process the data on the instrument side, the buffers are directly moved.TekHSIreceives the data in a background thread. So when mixingPyVISAandTekHSI, often data arrival appears to take little or no time.TekHSIrequires less code than the normal processing of curve commands.- The waveform output from
TekHSIis easy to use with file readers/writers that allow this data to be quickly exported using the tm_data_types package.
Example using pyvisa¶
"""Command & control using PyVISA, but retrieving waveform data using TekHSI."""
import pyvisa
from tm_data_types import AnalogWaveform
from tekhsi import AcqWaitOn, TekHSIConnect
addr = "192.168.0.1" # Replace with the IP address of your instrument
rm = pyvisa.ResourceManager("@py")
# write command to instrument sample using pyvisa
visa_scope = rm.open_resource(f"TCPIP0::{addr}::INSTR")
sample_query = visa_scope.query("*IDN?")
print(sample_query)
# Make the waveform display OFF
visa_scope.write("DISplay:WAVEform OFF")
# Set the Horizontal mode to Manual
visa_scope.write("HOR:MODE MAN")
# Set the horizontal Record Length
visa_scope.write("HOR:MODE:RECO 2500")
# time.sleep(2) # Optional delay
# Connect to instrument via TekHSI, select channel 1
with TekHSIConnect(f"{addr}:5000", ["ch1"]) as connect:
# Save data from 10 acquisitions
for i in range(10):
with connect.access_data(AcqWaitOn.NextAcq):
waveform: AnalogWaveform = connect.get_data("ch1")
print(f"{waveform.source_name}_{i}:{waveform.record_length}")
visa_scope.write("DISplay:WAVEform ON")
# close visa connection
visa_scope.close()
Example using tm_devices¶
"""Command & control using tm_devices, but retrieving waveform data using TekHSI."""
from tm_data_types import AnalogWaveform
from tm_devices import DeviceManager
from tm_devices.drivers import MSO6B
from tekhsi import AcqWaitOn, TekHSIConnect
addr = "192.168.0.1" # Replace with the IP address of your instrument
with DeviceManager(verbose=True) as device_manager:
scope: MSO6B = device_manager.add_scope(f"{addr}")
idn_response = scope.commands.idn.query()
print(idn_response)
# Make the waveform display OFF
scope.commands.display.waveform.write("OFF")
# Set the Horizontal mode to Manual
scope.commands.horizontal.mode.write("OFF")
# Set the horizontal Record Length
scope.commands.horizontal.mode.recordlength.write("2500")
# time.sleep(2) # Optional delay
# Connect to instrument via TekHSI, select channel 1
with TekHSIConnect(f"{scope.ip_address}:5000", ["ch1"]) as connect:
# Save data from 10 acquisitions
for i in range(10):
with connect.access_data(AcqWaitOn.NewData):
waveform: AnalogWaveform = connect.get_data("ch1")
print(f"{waveform.source_name}_{i}:{waveform.record_length}")
# Make the waveform display ON
scope.commands.display.waveform.write("ON")
Customize logging and console output¶
The amount of console output and logging saved to the log file can be customized as needed. This
configuration can be done in the Python code itself as demonstrated here. If no logging is
explicitly configured, the default logging settings will be used (as defined by the
configure_logging() function).
"""A script demonstrating how to customize the logging that happens during runtime."""
from tm_data_types import AnalogWaveform, write_file
from tekhsi import configure_logging, LoggingLevels, TekHSIConnect
addr = "192.168.0.1" # Replace with the IP address of your instrument
configure_logging(
log_console_level=LoggingLevels.NONE, # completely disable console logging
log_file_level=LoggingLevels.DEBUG, # log everything to the file
log_file_directory="./log_files", # save the log file in the "./log_files" directory
log_file_name="custom_log_filename.log", # customize the filename
)
# Connect to instrument, select channel 1
with TekHSIConnect(f"{addr}:5000", ["ch1"]) as connect:
# Save data from 10 acquisitions as a set of CSV files
for i in range(10):
with connect.access_data():
wfm: AnalogWaveform = connect.get_data("ch1")
# Save the waveform to a file
write_file(f"{wfm.source_name}_{i}.csv", wfm)
Experimental Parallel Waveform Reads¶
Warning
This feature is experimental and disabled by default.
TekHSI includes optional experimental support for parallel waveform reads.
This behavior can be controlled using environment variables.
Environment Variables¶
| Variable | Type | Default | Description |
|---|---|---|---|
TEKHSI_USE_PARALLEL_READS |
Boolean (1, true, yes) |
false |
Enables experimental parallel waveform reads |
TEKHSI_PARALLEL_THRESHOLD |
Integer | 2 |
Minimum number of waveforms required before parallelization is used |
TEKHSI_PARALLEL_WORKERS |
Integer | 4 |
Number of worker threads used for parallel reads |
TEKHSI_DISABLE_PARALLEL_READS |
Boolean (1, true, yes) |
false |
Forces parallel reads to be disabled even if otherwise enabled |
Usage Examples¶
Linux / macOS
export TEKHSI_USE_PARALLEL_READS=1
export TEKHSI_PARALLEL_THRESHOLD=3
Windows (PowerShell)
setx TEKHSI_USE_PARALLEL_READS 1
setx TEKHSI_PARALLEL_THRESHOLD 3