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 to None, 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.

  1. TekHSI is 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.
  2. TekHSI receives the data in a background thread. So when mixing PyVISA and TekHSI, often data arrival appears to take little or no time.
  3. TekHSI requires less code than the normal processing of curve commands.
  4. The waveform output from TekHSI is 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