MATLAB Script for Tektronix Scope Plot and Measure

I present an updated 2-byte capture, plot, and measure script that follows current best practices using MATLAB’s Instrument Control Toolbox. My previous version was posted back in 2020. To start with, there is no VISA. Instead, the code is based on the TCP client object supported by the Tektronix instrument’s TCP/IP client service.

This script demonstrates how to use MATLAB’s Instrument Control Toolbox to communicate with a Tektronix MSO64B oscilloscope via TCP/IP. It captures, scales, plots, measures signal properties, displays results, and optionally saves the results.

MATLAB Script Overview

  • Read Tektronix oscilloscope waveforms, plot results, show measurements, and save an image, the figure, or the workspace variables.
  • The MATLAB script is coded to be easy to read, using code blocks and reusable functions. For example, all user controls are in one code block titled User Inputs.
  • The script is designed to improve the value of oscilloscope data for research, engineering, and publication of results.
  • Demonstrate communication with an oscilloscope using MATLAB’s tcpclient interface (no VISA) and scope’s Ethernet connection.
  • Use the Oscilloscope SCPI CURve? command and the MATLAB readbinblock function to create a multi-channel plot with 2-byte (int16) data capture.
  • Logic is included to produce meaningful color-coded plots with corresponding legend display.
  • Logic is included to produce meaningful units for time and voltage in the plot and the legend.
  • Optionally investigate the device under test (DUT) using plot data (experiment 1) and scope measurement badges (experiment 2).
  • Modify the 2 existing experiments at a high level from the “User Inputs” code section.
  • Review the “Measure (optional)” code section to design new experiments.
  • Experiment 1 of 2 – use plot results:
    • Use a flag to turn this experiment on or off; see variable do_experiment1_measurements.
    • Show horizontal and vertical lines through maximum y value.
    • Put a marker in the plot for the maximum y value.
    • The legend displays the maximum voltage and corresponding time values below the marker.
  • Experiment 2 of 2 – use scope measurement facilities:
    • Use a flag to turn this experiment on or off; see variable do_experiment2_measurements.
    • Use a table to define scope channels and their associated measurement parameters.
    • Launch measurement badges on the oscilloscope for peak-2-peak, and frequency as examples of the many measurements available.
    • Include measurements from any channel displayed on the scope – not just the channels plotted.
    • For a list of all measurements available, refer to the Tektronix MSO 4,5,6 Programming Manual SCPI commands:
      • DISplay:SELect:WAVEView1:SOUrce
      • MEASUrement:ADDMEAS
      • MEASUrement:MEAS
    • Collect measurement badge values and optional statistics (min, max, std, population) based on trigger/acquisition mode.
    • Use a flag to turn statistics on or off; see variable do_measure_statistics.
    • Place collected badge measurement data into the plot legend.
  • Optionally save the plot image, figure, and workspace for reuse.

For this test setup, I used a single event trigger to capture the Serial Peripheral Interface (SPI) data output pin on a BME680 multifunction sensor controlled by an Arduino UNO R3.

Test Setup for MATLAB Plot and Measure Script – capturing the signal from the SPI output of a BME680 sensor.

Fritzing Project Showing Andrino Uno Connections to BME680 Sensor

Fritzing project view shows the connections between Arduino Uno and BME680 Clock, Ground, and SPI.

The MATLAB script editing environment shows some details about this script

The editor shows the script name and plot files automatically generated during run time. The command window shows the script progress for user feedback. The code editor shows examples of the comments providing an overview of the the script. The workspace shows all the variables generated during the script execution.

Plots generated using this script

The script presented here shows how to capture a large single trace event from a Tektronix MSO64B oscilloscope. I suspect this script will work fine for any Tektronix scope from the MSO 4,5,6 family without modification. The plot shows the captured results automatically scaled to a user-friendly time scale:

  • The magenta trace shows the scope’s channel 1 probe connected to the SPI data output from a BME680 sensor.
  • The green trace shows the scope’s channel 2 probe connected to the BME680 SPI clock.
  • The gold lines and markers are generated from the MATLAB plot data. In this case, the measurement shows the maximum data point, intersecting time, and voltage lines.
  • The last 3 legend entries are measurements taken from the scope confirming the maximum data point is the same as the plot data point. Also the clock frequency and bitrate are shown using scope measurements as well.
  • The legend is located outside the plot so much more data can be presented. The legend includes the automatic generation of a color-coded channel indicator and corresponding time and voltage values scaled to easy-to-read units. Statistics can also be displayed as part of the measurement badges during normal triggering.
The full plot shows a single trace using 50 Mpts sampled at 50GS/s taken over a 0.001-second interval, maximum SPI data amplitude point using plot data and scope measurement badge, SPI clock frequency, and bitrate. The plot was generated with only the MATLAB script.

The figure above can be zoomed many times using the MATLAB Figure tool. When combined with the 2-byte data captured from the oscilloscope, the user can explore meaningful performance issues at a high magnification. Both the x and y-axis scales adjust automatically during the zoom process using a callback function from this script.

The close-up shows greater surrounding detail around the maximum data point. This script includes a callback function to update axis values after each zoom. The zoom was accomplished using the MATLAB interactive figure and code from this script.

The eScope tool also works as a great diagnostic tool for new users connecting to scope via Ethernet using MATLAB’s tcpclient function. Note the measurement badges along the right margin. These badges were launched using this MATLAB script within the experiment 2 code block.

The Tektronix eScope feature allows users to configure the scope from their PC with only a web browser connection.

Plots Demonstrating Different Scope Acquisition Modes and Measurement Statistics

The results presented here are part of the code block controlled with the flag variable do_experiment2_measurements. The color-coded channels and measurement values improve documentation. The results and corresponding statistics help compare the two signal generators. In this case the frequency and the peak-to-peak values and standard deviation results show both instruments perform similarly at 45 MHz after a one hour warm-up period.

The oscilloscope envelope acquisition mode shows some jitter in the Rigol DG4062 arbitrary waveform generator (AWG) compared to the trigger-stabilized built-in Tektronix MSO64B arbitrary function generator (AFG).

MATLAB Script

Use this link to download the MATLAB script and selected sample images :

Click “expand” if the code is not visible.

%% File: tcpclient_scope_demo.m
%
% Created on Saturday, July 27, 2024
% Update: September 9, 2024
% 
% Target Audience:
%   Experimenters using MATLAB and Tektronix MSO 4, 5, 6 oscilloscopes.
%   MATLAB versions tested: 2022b ... 2024a.
%   MATLAB versions 2022a and earlier would require several changes.
%
% Purpose: 
%   - Read Tektronix oscilloscope waveforms, plot results,
%     show measurements, and save image, figure, or workspace.
%   - The MATLAB script is coded to be easy to read, using code blocks and 
%     reusable functions. For example, all user controls are in one code block 
%     titled User Inputs.
%   - The script is designed to improve the value of oscilloscope data for 
%     research, engineering, and publication of results.
%   - Demonstrate communication with an oscilloscope using MATLAB's
%     tcpclient interface (no VISA) and scope's Ethernet connection. 
%   - Use the SCPI CURve? command and the MATLAB readbinblock function to 
%     create a multi-channel plot with 2-byte (int16) data capture. 
%   - Logic is included to produce meaningful color-coded plots with 
%     corresponding legend display.
%   - Logic is included to produce meaningful units for time and voltage in 
%     the plot and the legend. During plot zoom, units extend to the limit of 
%     the scope's timeline increment (8.0e-11 seconds for example).   
%   - Optionally investigate the device under test (DUT) using 
%     plot data (experiment 1) and scope measurement badges (experiment 2). 
%   - Modify the 2 existing experiments at a high level from the "User Inputs" 
%     code section.
%   - Review the "Measure (optional)" code section to design new experiments. 
%   - Experiment 1 of 2 - use plot results:
%       + Use a flag to turn this experiment on or off; 
%         see variable do_experiment1_measurements.
%       + Show horizontal and vertical lines through maximum y value.
%       + Put a marker in the plot for the maximum y value.  
%       + The legend displays the maximum voltage and corresponding time values
%         below the marker.
%   - Experiment 2 of 2 - use scope measurement facilities:
%       + Use a flag to turn this experiment on or off; 
%         see variable do_experiment2_measurements.
%       + Use a table to define scope channels and their associated measurement 
%         parameters.
%       + Launch measurement badges on the oscilloscope for peak-2-peak, and 
%         frequency as examples of the many measurements available.
%       + Include measurements from any channel displayed on scope - not
%         just the channels plotted in MATLAB.
%       + For a list of all measurements available, refer to the Tektronix 
%         MSO 4,5,6 Programming Manual SCPI commands:
%           DISplay:SELect:WAVEView1:SOUrce
%           MEASUrement:ADDMEAS
%           MEASUrement:MEAS 
%       + Collect measurement badge values and optional statistics 
%         (min, max, std, population) based on trigger/acquisition mode. 
%       + Use a flag to turn statistics on or off.
%       + Place collected badge measurement data into the plot legend.
%   - Optionally save the plot image, figure, and workspace for reuse.
%
% Author: BiophysicsLab.com
%
% Discussion Web Page: 
%   https://www.biophysicslab.com/2024/08/31/matlab-script-for-tektronix-scope-plot-and-measure/
%
% Products Used:
%   Tektronix 6 Series B MSO Mixed Signal Oscilloscope
%   Model MSO64B with Firmware Updated to FV:2.8.1.1496 now to FV:2.10.5.1825
%   https://www.tek.com/en/products/oscilloscopes/6-series-mso
%
%   MATLAB Instrument Control Toolbox (ICT)
%   Control test and measurement instruments and communicate with computer peripherals
%   MATLAB Version R2023b with Instrument Control Toolbox (ICT)
%   https://www.mathworks.com/products/instrument.html
%   https://www.mathworks.com/help/instrument/transition-your-code-to-tcpclient-interface.html
%
%   Mac Studio M1 Ultra (Apple Silicon) MacOS Sonoma Version: 14.5
%   https://www.apple.com/mac-studio/
%
% Parameters used for accurate time and voltage plotting:
%   ymult > SCPI command: 'WFMOutpre:YMULT?'
%   yzero > SCPI command: 'WFMOutpre:YZERO?'
%   yoff > SCPI command: 'WFMOutpre:YOFF?'
%   xincr > SCPI command: 'WFMOutpre:XINCR?'
%   xzero > SCPI command: 'WFMOutpre:XZERO?'
%   pre_trig_record > SCPI command: 'WFMOutpre:PT_OFF?'
%
%   SCPI command reference:
%       https://www.tek.com/en/sitewide-content/manuals/4/5/6/4-5-6-series-mso-programmer-manual
%
% Useful Discussion Threads:
%
%   MATLAB Script for Tektronix Scope Plot and Measure
%   https://www.biophysicslab.com/2024/08/01/matlab-script-for-tektronix-scope-plot-and-measure/
%
%   MATLAB ICT: MDO Simple Plot
%   https://forum.tek.com/viewtopic.php?f=580&t=141809
%
%   MATLAB Oscilloscope App
%   https://www.mathworks.com/matlabcentral/fileexchange/69847-oscilloscope-app
%
%   Python MSO64 2-byte read binary waveform example
%   https://forum.tek.com/viewtopic.php?f=580&t=142410
%
%   Python: MDO Simple Plot
%   https://forum.tek.com/viewtopic.php?t=138684
%
%   MSO6 read Meas with SCPI command (eg PYTHON)
%   https://forum.tek.com/viewtopic.php?t=142486
%
% Code sections:
%   User Inputs
%   Initialize Instrument
%   Instrument Control
%   Get Waveform(s)
%   Plot
%   Measure (optional)
%   Save (optional)
%   Close Session
%   Reusable Functions
%
% Reusable Functions:
%   function byte = get_byteFcn(x)
%   function result_flag = is_scipi_nan(val)
%   function [scale_exp, scale_txt] = scale_resultsFcn(values, top_units)
%   function color = get_plotColorFcn(reset_colorcount)
%   function chan_txt  = get_channelNameFcn(chan)
%   function sesr = test_sesrFcn(scope, code_msg)
%   function scope_model = get_identityFcn(scope, scope_vendor)
%   function channel_used = is_channelFcn(scope, chan)
%   function scope_trigger = get_triggerFcn(scope)
%   function zoomCallbackFcn(obj,evd)
%   function [new_xyticks_rnd, cell_labels, errFlg] = scale_ticksFcn(xyround, xyround_lmt, new_xylim, max_xytickdivs)
%   function adjacent_equal = adjacentEqualFcn(a, tol)
%   function zoomErrorFcn()
%   function legend_text_append = makeBadgeFcn(title, value, units, color)
%   function num_zeros = nearZeroFcn(num, tol)
%   function rgbstr = hex2rgbStringFcn(hex)
%   function [ rgb ] = hex2rgb(hex,range) ** MATLAB Central File Exchange **
%
% TODO(s):
%   - Convert this code to an object oriented program (OOP):
%       + Convert some User Input code section into a constructor
%       + Use getter/setter methods to change experiment input code
%       + Instrument class
%       + Plot class
%       + Experiment class(s)
%       + Convert functions to methods
%   - MATHx and REFx scope channels require additional programming to plot.
%   - Add a stacked option for plotting each channel
%       https://www.mathworks.com/support/search.html/answers/1573168-adding-event-markers-to-imported-eeg-data-stacked-plot.html
%   - Feature: With the existing workspace loaded, run this program again 
%     without instrument capture, allowing new plot measurements and formatting 
%     with the instrument turned off.


%% User Inputs
% User Input Sections:
%   - Controls for oscilloscope
%   - Controls for saving data: image, figure, workspace mat
%   - Controls for plot
%   - Controls for lab experiments
%       + plot max point
%       + scope measurements from Tektronix measurement badges


% Controls for oscilloscope
% -------------------------


% Define value for "Not a number" from SCPI definition.
scpi_nan = 9.91E+37; 

% Define parameters required for tcpclient scope connect function.
% Requires MATLAB Instrument Control Toolbox (ICT).
% Notes for troubleshooting: 
%   - On PC, enter the IP address into a web browser (to get eScope utility)
%   - On MSO64B Scope, refer to scope menu: Utility > IO > Lan
%   - New security issue in firmware Version, 2.10.5.1825, on June 13, 2024.
%     Go to menu > Utilities > Security > turn on both:
%       + Remote Access security and 
%       + LAN Port security. 
%     Reboot scope.
TCPIP_address = "192.168.1.160";
TCPIP_port = 4000;

timeout = 10; % time allowed to complete operations in seconds
buffer_size = 3E6; % large enough to capture 2-byte int16 data
scope_vendor = "Tektronix";


% Controls for saving data: image, figure, workspace mat
% ------------------------------------------------------


file_name_base = 'scope';
% example file name for saving data: scope_mso64b_channel1_07312024_131849

% Set plot size:
%   true : full-size plot
%   false : default plot size (~ 30% of screen)
plot_fullsize = true;

% Save plot image.
% save_plot = false;
save_plot = true;

save_plot_resolution = 144; % dpi such as 72, 144 (improved quality), 300 (print quality)
save_plot_extension = 'png'; % 'jpeg' | 'png' | 'tiff' | 'pdf' | 'eps' | ...

% Use fig file to further investigate plot results - such as to zoom.
save_fig = false; 
% save_fig = true; 

% Save all workspace data except graphics handles.
save_workspace = false; 
% save_workspace = true; 

% Set output display format to 15 digits after decimal for double values.
[~] = format('long');


% Controls for plot
% -----------------


% Waveforms to plot
%   - Add scope channel names as array (multiple channels) or string (one
%     channel) to control number of scope waveforms to plot.
%   - For list of scope channel names see function get_channelNameFcn(chan).
% input_channels = "CH1";
input_channels = ["CH1" "CH2"];

% Set the line width for channel plots.
line_width_plot = 2;

% Initialize plot color list:
%   - Reset control for function: get_plotColorFcn(reset_colorcount).
%   - Use 1 to start from the beginning of the function's color list.
%   - Use any number greater than 1 to start from another point in the color list.
%   - One way to choose a number is by selected the first channel number plotted,
%     like 1 for "CH1", or 3 for "CH3".
reset_colorcount = 1;
% reset_colorcount = 3;

% Use default channel names with function get_channelNameFcn.
% (ie, "channel 1")
% custom_channelnames = false;
custom_channelnames = true;

% Use custom channel names.
% Note use of Tex Interpreter for subscripts in legend "_{xxx}"
% channel_names = ["tektronix_{afg}" "rigol_{dg4062}"];
channel_names = ["bme680_{sda}" "bme680_{scl}"];

% Create figure name.
% Figure name reused as the plot title.
% I use comments here to remember what experiments I have used previously
% in the event I want to re-setup this experiment again in the future.
%  - This example is suitable for a single aquisition trace plot:
% experiment_info = "Capture Single Trace SPI Output Pulse Event from BME680 Sensor";
%  - This example is suitable for an envelope continuous aquistion plot
%    with optional statistics:
% experiment_info = "Capture Envelope Oscilloscope Built-in Arbitrary Function Generator with Rigol: Sine wave";
%  - This example is suitable for an envelope continuous aquistion plot
%    with optional statistics:
% experiment_info = "Capture High-Res Oscilloscope Built-in Arbitrary Function Generator with Rigol: Sine wave";
experiment_info = "Capture and Compare Single Trace SPI Clock and Data from BME680 Sensor";

% Position legend 
%   - Examples: 'best' | 'northeast' 'northwest' [default] | 'northeastoutside' | ...
legend_location = 'northeastoutside'; 

% A number near zero for x and y axis during initial channel plots.
xy_tol = .0005; 


% Controls for lab experiments
% ----------------------------


% Experiment 1.
% -------------
% Perform and display a custom measurement using scope and plot data: 
%   - Display selected channels from custom_channels for max point, x/y lines, and 
%     time/volts values on the plot.
%   - Results display in plot and in legend.
% do_experiment1_measurements = false;
do_experiment1_measurements = true;

% Create a list of scope channels to plot custom measurement.
% experiment1_channels = ["CH3" "CH4"];
experiment1_channels = "CH1";

% Set the line and marker plot width for experiment 1.
line_width_ex1 = 2;


% Experiment 2.
% -------------
% Enable code and display for scope badge measurements.
%   - Results display in command window and plot legend.
%   - Badge measurements can be captured independent of whether a scope channel
%     is plotted or not.
% do_experiment2_measurements = false;
do_experiment2_measurements = true;

% Enable statistics during badge display:
%   - Statistics also requires continuous waveform acquisition using
%     scope's trigger settings.
%   - Statistics will not be displayed if some values are too
%     low, such as "frequency" measurement on low amplitude waveforms.
% do_measure_statistics = false;
do_measure_statistics = true;

% When do_experiment2_measurements is true:
%   - Define a table of all measurements to collect from the scope.
%   - Measurements include values and optional statistics 
%     (when do_measure_statistics flag is set to true).
%   - Display measurements in the legend.

% Unique measurement names define the keys for the table.
%   - Measurement names support tex: "freq_{afg}" will subscript the afg text.
%   - Statistics are included for continuous waveforms. A 2-second delay after 
%     badges increases population count for meaningful statistics.
%   - Only measurement values are included for single trace waveforms.
% t_meas_unique_display_names = ["pk2pk_{afg}"; "freq_{afg}"; "pk2pk_{rigol}"; "freq_{rigol}"];
t_meas_unique_display_names = ["max_{data}"; "freq_{clock}"; "bitrate_{clock}"];

% Send badge measurement names to the scope to create badges.
%   - Consult Tektronix programmer manual section:
%       + MEASUrement:ADDMEAS (No Query Form) for valid SCPI measurement
%         names.
%   - Parameters I have tested:
%       + "amplitude"
%       + "maximum"
%       + "minimum"
%       + "frequency"
%       + "pk2pk"
% t_meas_SCPI_names = ["pk2pk"; "frequency"; "pk2pk"; "frequency"];
t_meas_SCPI_names = ["maximum"; "frequency"; "datarate"];

% Declare the measurement units for each badge.
%   - The units will be updated during rescale to optimum values for 
%     legend display. For example mV or MHz.
%   - See function scale_resultsFcn for details.
t_units = ["V"; "Hz"; "Bits/s"];

% Define which scope channel is associated with each badge measurement.
%   - The channel should be displayed on the oscilloscope.
%   - The channel waveform may also be plotted but is not required.
%   - See variable input_channels for declaration of plotted channels.
t_channels = ["CH1"; "CH2"; "CH2"];


%% Initialize Instrument

% Update command window during instrument initialization.
fprintf('%s %s %s %i %s\n', 'Connecting to', scope_vendor, ...
    'oscilloscope with timeout set to:', timeout, 'seconds...')

% Connect to the oscilloscope and create a scope object.
scope = tcpclient(TCPIP_address, TCPIP_port, "ConnectTimeout",timeout);
scope.OutputBufferSize = buffer_size;
scope.Timeout = timeout;
scope.ByteOrder = "little-endian";


%% Instrument Control

% Clear instrument status data structures, including the
% Standard Event Status Register (SESR).
writeline(scope, "*CLS"); 

% Disable echo attribute for SCPI writeread (query) replies.
writeline(scope, "HEADer OFF"); % 

% Test and report errors using the (SESR).
%   - Note: use of [~] to avoid MATLAB's "value not used warning". 
%   - Probably a hack.
[~] = test_sesrFcn(scope, "Script initialization");

% Get the oscilloscope's model name.
%   - Display warning in command window if scope vendor is not Textronix.
scope_model = get_identityFcn(scope, scope_vendor);
if scope_model == '?'
    error("Scope identity not found")
end

% Get the horizontal record length
%   - Preallocate waveform y-axis data using this value.
%   - Generate x-axis time data points using this value.
%   - Example value: 2500
record_length = str2double(writeread(scope, "HORizontal:RECORDLength?"));

% Specify transfer of the full waveform: from 1 to record_length
writeline(scope, "DATa:STARt 1");
writeline(scope, strcat( "DATa:STOP ", num2str(record_length) ));

% Transfer 2-bytes per point for waveform data
writeline(scope, "DATA:WIDTH 2");

% Sets the format (encoding) for collecting waveform data.
%   - Specifies signed integer data-point representation.
%   - Transfer least significant byte first.
writeline(scope, "DATA:ENCdg SRIbinary");

% % Check for instrument operator error: one or more waveforms present.
% %   - Trigger state should not be waiting for an event (ie, READY).
trigger_state = get_triggerFcn(scope);
if trigger_state == "READY"
    errMsg = "Scope is waiting for a trigger event." + newline + ... 
        "    Use single trace aquisition or" + newline +... 
        "    select auto (not norm) trace mode.";
    error(errMsg)
end


%% Get Waveform(s)

fprintf('%s\n', 'Begin plotting scope channel(s)')

length_inputchannels = length(input_channels);

% Preallocate scaled data matrix with SCPI NaN value.
%   - Matrix geometry: channels (rows) by record length (columns).
scaled_data_mat = repmat(scpi_nan, length_inputchannels, record_length);

% Preallocate y axis scale vectors.
yscale_exp_mat = zeros(length_inputchannels, 1); % scale exponent
yscale_txt_mat = strings(length_inputchannels, 1); % scale units

% Loop through each scope input channel and plot
for i=1:length_inputchannels

    % Check for instrument operator error: requested scope channel(s) are
    % not active.
    if ~is_channel_activeFcn(scope, input_channels(i))
        error("Scope channel " + input_channels(i) +  ...
            " is not active." + newline + ...
            "    Activate the channel on the scope or" + newline + ...
            "    modify the variable input_channels in this code.")
    end

    % Specify the data source for waveform transfer from the instrument
    writeline(scope, strcat( "DATa:SOUrce ", input_channels(i) ));
    
    % Transfer waveform data from the instrument.
    %   - Waveform preamble that contains information.
    %   - First and last data points are controlled by DATa:STARt and DATa:STOP commands
    %   - Data transfer is different for DATa:WIDth 1 and 2
    %   - Use DATA:ENCdg SRIbinary for DATa:WIDth 2
    writeline(scope, "CURve?");
    
    % MATLAB: read one binary block of data from a serial port. 
    %  - Data interpreted by precision parameter: int16. 
    %  - Data encoded using SCPI DATA:ENCdg SRIbinary command.
    %  - Use after SCPI CURve? command.
    data = readbinblock(scope, 'int16');
    
    % Tektronix oscilloscopes transfer an extra terminator with waveform - toss it.
    [~] = read(scope, 1);
    
    % Test and report errors using the Standard Event Status Register
    % (SESR).
    [~] = test_sesrFcn(scope, "Near read(scope, 1) command.");

    % Test for data transfer error - no value in data should contain NaN.
    if ~isempty( find(data==scpi_nan,1) )
        errMsg = 'readbinblock: An error occurred while reading ' + ...
            input_channels(i) +  ' waveform.';
        error(errMsg);
    end

    % Waveform parameters and scaling

    % Get the vertical scale
    %   - Units specified by WFMOutpre:YUNit 
    %   - Units assumed to be in volts
    %   - Waveform data source must exist or an error is reported
    %   - Example: 1.562500000000000e-04 volts
    ymult = str2double( writeread(scope, "WFMOutpre:YMUlt?") );
    if isempty(ymult)
        error('ymult is empty');
    end
    
    % Get the combined vertical position and offset for the source waveform.
    %   - This definition for yzero only applies to Tek MSO 4,5,and 6 scopes.
    %   - Check programming manual for previous scope models as needed.
    %   - Example: 0
    yzero = str2double( writeread(scope, "WFMOutpre:YZEro?") );
    
    % Get the vertical offset for the source waveform.
    %   - Example: 0
    yoff = str2double( writeread(scope, "WFMOutpre:YOFf?") );

    % Calculate scaled data in Volts. 
    scaled_data_mat(i,:) = ((data - yoff) .* ymult) + yzero;

    % Determine optimum exponent and units for plot axis labels.
    % Example:
    %   yscale_exp_mat(i) set to 1
    %   yscale_txt_mat(i) set to "V"
    [yscale_exp_mat(i), yscale_txt_mat(i)] = scale_resultsFcn(scaled_data_mat(i,:), 'V');

end % for loop

% Find and use smallest yscale exponent values for y axis plot label
[~, idx] = min(yscale_exp_mat);
yscale_exp = yscale_exp_mat(idx);
yscale_txt = char(yscale_txt_mat(idx));

% Time scale parameters

% Get the horizontal point spacing for the source waveform 
%   - Units specified by WFMOutpre:XUNit
%   - Units assumed to be in seconds
%   - Example: 4.0000e-11 seconds/point
xincr = str2double( writeread(scope, "WFMOutpre:XINcr?") );

% Get the sub-sample time between the trigger sample (designated by PT_OFF) 
% and the occurrence of the actual trigger for the waveform.
%   - Note: During steady state operation, when all control changes have settled and
%     triggers are arriving regularly, this is the only part of the 
%     preamble that changes on each acquisition.
%   - Example: 4.0000e-11
xzero = str2double( writeread(scope, "WFMOutpre:XZEro?") );

% Get the trigger point relative to DATa:STARt (1) for the waveform.
%   - Example: 475
pre_trig_record = str2double( writeread(scope, "WFMOutpre:PT_Off?") );

% Time scaling.
% Reported examples here use the same acquisition data described in above.
total_time = record_length * xincr; % Example: 1.0000e-07 
t_start = (-1 * pre_trig_record * xincr) + xzero; % Example: -1.8960e-08
t_stop = t_start + total_time; % Example: 8.1040e-08
scaled_time = linspace(t_start, t_stop, record_length);


%% Plot

% Control plot figure
close all
fig_handle = figure('Name', experiment_info);
if plot_fullsize
    % Set plot size using normalized units 0 to 1, then restore default
    % units.
    set(fig_handle, 'units','normalized','outerposition',[0 0 1 1])
    set(fig_handle, 'units','pixels')
end

% Reset persistent color count.
[~] = get_plotColorFcn(reset_colorcount);

% Use a dictionary to associate scope channels to plot with a unique color.
% Reuse the dictionary in the plot legend for color coded lab experiment results.
d_plot_colors = dictionary; 

% Plot instrument channels
hold on
for i=1:length_inputchannels

    % Store color map to scope channels for reuse in legend measurement badge display
    % Insert function requires MATLAB version 2023b
    % d_plot_colors = insert(d_plot_colors, input_channels(i), get_plotColorFcn(0) );
    d_plot_colors(input_channels(i)) = get_plotColorFcn(0);

    % Prepare plot channel name for legend.
    if custom_channelnames == false
        plot_channel_name  = get_channelNameFcn(input_channels(i));
    else
        % Prepend color coded scope channel name to the custom channel name
        % for display in legend. 
        % (same method used for other badge measurements).
        plot_channel_name  = "\color[rgb]{" + hex2rgbStringFcn(d_plot_colors(input_channels(i)))   + "}" + ...
                lower(input_channels(i)) + " " + "\color{black}" + channel_names(i);
    end

    plot(scaled_time, scaled_data_mat(i,:), 'color', d_plot_colors(input_channels(i)), ...
        'LineWidth', line_width_plot, 'DisplayName', plot_channel_name);

end % for i=1:length_inputchannels

% Estimate font size for plot title and legend for both full-size and
% default size figure.
plot_pos = get(gcf, 'Position'); % (1) x left, (2) y bottom, (3) width, (4) height
% Experiment on my monitor:
%   plot_pos(3): default width 560, full-size 2048 pixels.
%   plot_pos(4): default height 420, full-size 1101 pixels.
if plot_pos(4) > 700
    title_fontvalue = 22;
    legend_fontvalue = 16;
else
    title_fontvalue = 12;
    legend_fontvalue = 9;
end

% Create a 2-line plot title.
bottom_title_left = (scope_vendor + " " ...
    + scope_model ...
    + " Oscilliscope: Time vs. Volts");
bottom_title_right = ('Date: ' ...
    + string(datetime('now','TimeZone','local','Format','MM/dd/yyyy hh:mm a')));
title('\fontsize{' + string(title_fontvalue) + '}' ...
    + experiment_info + newline ...
    + '\fontsize{' + string(title_fontvalue-2) + '}' ...
    + '\color{' + 'gray' + '}' ...
    + bottom_title_left ...
    + '     ' + bottom_title_right);

% Manage legend location and font size.
lgd = legend('Location', legend_location);
fontsize(lgd, legend_fontvalue, 'points') 
title(lgd, 'Legend')
lgd.Title.Visible = 'on';

% Manage x-axis label, limit, and plot units.
[xscale_exp, xscale_txt] = scale_resultsFcn(scaled_time, 's');
xt = xticks;
xt_new = xt(:) * xscale_exp;
xt_new = nearZeroFcn(xt_new, xy_tol);
xticklabels(xt_new);
xlim([t_start t_stop])
xlabel(['Time (' xscale_txt ')'], 'FontSize', title_fontvalue)

% Manage y-axis plot units.
yt = yticks;
yt_new = yt(:) * yscale_exp;
yt_new = nearZeroFcn(yt_new, xy_tol);
yticklabels(yt_new);
ylabel(['Voltage (' yscale_txt ')'],'FontSize', title_fontvalue)

% Manage both x and y axis font size.
axis_handle = gca(fig_handle);
axis_handle.FontSize = legend_fontvalue-2;

grid on
grid minor

% Manage change in x and y axis units using a zoom callback function.
max_xtickdivs = length(xt);
max_ytickdivs = length(yt);
fig_handle.UserData = struct("xincr", xincr, ...
    "max_xtickdivs", max_xtickdivs, "max_ytickdivs", max_ytickdivs);
h = zoom;
set(h,'ActionPostCallback',@zoomCallbackFcn);
set(h,'Enable','on');

fprintf('    %s\n', 'Plot for scope channel(s) complete')


%% Measure (optional)

% Demonstrate how to collect and display measurements from the oscilloscope.
%   - display measurements in the  command window, incl. maximum y value
%   - find x value at maximum y value using plot data
%   - highlight maximum value as y and x lines and as data point in the plot
%   - use plot legend to show all measurements: maximum, minimum, pk2pk, and frequency 

if do_experiment1_measurements == true % use == construct to avoid MATLAB code block unreachable warning

    % Experiment 1 of 2:
    % ------------------
    
    % Highlight the maximum value for a waveform:
    %  - See "User Inputs" code section to activate this experiment's flag 
    %    (do_experiment1_measurements) in section titled: 
    %    "Controls for lab experiments".
    %   - use plotted data for this measurement: 
    %       + scaled_data_mat for y (voltage)
    %       + scaled_time for x (time)
    %   - place a horizontal line at y's maximum value 
    %     (maximum voltage)
    %   - place a vertical line at x corresponding to y's maximum value 
    %     (time and maximum voltage)
    %   - place a marker in the plot at max point 
    %   - include the time and voltage values for max point in the legend

    fprintf('%s\n', 'Capture and report a few measurements from the plot results')

    for i=1:length(experiment1_channels)

        legend_text = ""; % initialize legend text for measurement results

        % Find data channel:
        %  - Waveform data is stored in the same order as plotted channels 
        %    (input_channels).
        dc_i = find(input_channels==experiment1_channels(i),1);
        if isempty(dc_i)
            errMsg = "Error in Experiment 1: Scope channel " + experiment1_channels(i) + " is not plotted.";
            error(errMsg)
        end

        legend_text_append = newline + "\color[rgb]{" + hex2rgbStringFcn(d_plot_colors(experiment1_channels(i)))   + "}" + ...
        lower(experiment1_channels(i)) + " " + "\color{black}" + "max" + ...
        newline;
        legend_text = append( legend_text, legend_text_append );
        % Plot invisible marker as a hack to display blank line in legend.
        plot( nan, nan, 'o', ...
        'MarkerSize', 20, 'color', 'none', ...
        'DisplayName', legend_text)

         legend_text = "";

        [plot_y_at_ymax, idx] = max(scaled_data_mat(dc_i,:));
        plot_x_at_ymax = scaled_time(idx);

        % Store color map to experiment1 for reuse in legend measurement badge display
        string_chan_exp1 = strcat(experiment1_channels(i),"EX1");
        % Insert function requires MATLAB version 2023b
        % d_plot_colors = insert(d_plot_colors, string_chan_exp1 , get_plotColorFcn(0) );
        d_plot_colors(string_chan_exp1) = get_plotColorFcn(0);

        yline(plot_y_at_ymax, 'color', d_plot_colors(string_chan_exp1), 'DisplayName', 'y at max_y', 'LineWidth', line_width_ex1) 
        xline(plot_x_at_ymax, 'color', d_plot_colors(string_chan_exp1), 'DisplayName', 'x at max_y', 'LineWidth', line_width_ex1) 
    
        % Put a marker at max point using the MATLAB's channel plot data.
        plot( plot_x_at_ymax, plot_y_at_ymax, 'o', ...
            'MarkerSize', 20, 'color', d_plot_colors(string_chan_exp1), ...
            'LineWidth', line_width_ex1, 'DisplayName', 'max_{point}' )
    
        % Use plot data for maximum data-point display (special case).
        [xtempscale_exp, xtempscale_txt] = scale_resultsFcn(plot_x_at_ymax, 's');
        [ytempscale_exp, ytempscale_txt] = scale_resultsFcn(plot_y_at_ymax, 'V');
        legend_text_append =  (string( plot_x_at_ymax * xtempscale_exp)  + " " + xtempscale_txt + ...
                           newline + string( plot_y_at_ymax * ytempscale_exp )  + " " + ytempscale_txt) + ...
                           newline;
        legend_text = append( legend_text, legend_text_append );

        % Plot invisible marker as a hack to display measurements using legend text.
        plot( nan, nan, 'o', ...
        'MarkerSize', 20, 'color', 'none', ...
        'DisplayName', legend_text)

    end % for i=1:length(custom_channels)

    fprintf('    %s\n', 'Capture and report plot results complete')

else
    fprintf('%s\n', 'Note: Set do_experiment1_measurements flag to include plot measurements');

end % do_experiment1_measurements


if do_experiment2_measurements == true % use == construct to avoid MATLAB code block unreachable warning

    % Experiment 2 of 2:
    % ------------------

    % Use Tektronix oscilloscope measurement badges to measure and display 
    % results in the plot legend.
    %   - Controls for this experiment are configured in User Inputs code
    %     section titled: Controls for lab experiments.
    %   - Measured results may include statistics (Max, Min, StDev, and
    %     Population).
    %     + Statistics can be disabled with a flag in controls section.
    %     + Statistics are automatically disabled when trigger is set to
    %       single trace, or when a measured value is not available (ie too
    %       low to read).

    fprintf('%s\n', 'Capture and report a few measurements from the scope:')

    % Remove all measurement badges on the oscilloscope.
    writeline(scope, "MEASUrement:DELETEALL");
    [~] = writeread(scope, "*OPC?"); % wait for operation to complete
    
    % See Input Control code section for definition of:
    %   t_channels
    %   t_meas_SCPI_names
    %   t_units
    %   t_meas_unique_display_names
    % SCPI NaN (9.91E+37) is used instead of zeros for all possible results
    t_rows = length(t_meas_unique_display_names);
    t_values = repmat(9.91E+37, t_rows, 1); 
    t_values_min = t_values;
    t_values_max = t_values;
    t_values_sd = t_values;
    t_popu = t_values;
    t = table(t_channels, t_meas_SCPI_names, t_units, ...
       t_values, t_values_min, t_values_max, ...
       t_values_sd, t_popu, ...
       'RowNames', t_meas_unique_display_names);
    t_columns = width(t);

    % Put a few measurement badges onto the scope
    for i = 1:t_rows
        % Select which channel or source for measurements.
        if ~is_channel_activeFcn( scope, char(t.t_channels(i)) )
            errMsg = "Scope channel " + ...
                t.t_channels(i) + " is not present for measurement " + ...
                    string(t.Properties.RowNames(i));
            error(errMsg)
        end

        % Create oscilloscope channel and measurment badge assignment
        writeline(scope, strcat( "DISplay:SELect:WAVEView1:SOUrce ", t.t_channels(i) ));
        writeline(scope, strcat( "MEASUrement:ADDMEAS ", t_meas_SCPI_names(i) ));

        % Verify measurement and channel badge assignment.
        scpi_meas_raw = writeread(scope, strcat( "MEASUrement:MEAS",string(i), ":TYPE?;SOUR1?" ));
        if scpi_meas_raw ~= upper( strcat(t_meas_SCPI_names(i), ";", t.t_channels(i)) )
            msg_string = strcat( "Error: Invalid measurement or channel in ", ...
                "Row ", string(i), ": ", t.t_channels(i), ", ", t_meas_SCPI_names(i) );
            error(msg_string)
        end

    end % for i = 1:t_rows
    [~] = writeread(scope, "*OPC?"); % wait for the last operation(s) to complete
    
    if trigger_state == "TRIGGER" || trigger_state == "AUTO"
        % Both TRIGGER and AUTO allow for statistics
        trigger_state = "TRIGGERorAUTO";
    end

    % Read values from scope's measurement badges
    if trigger_state == "TRIGGERorAUTO" && do_measure_statistics == true
        % Scope collects statistics during automatic trigger.
        collect_stats_time = 2; 
        pause('on')
        fprintf('    %s\n', 'Note: Disable do_measure_statistics flag to skip statistics')
        fprintf('    %s: %i %s\n', 'Collecting measurement statistics for', collect_stats_time, 'seconds')
        pause(collect_stats_time)

        for i = 1:t_rows
            % Load all accumulated measurement acquisitions for a specified 
            %   measurement (badge) into the table: Mean, Min, Max, Std, and
            %   Population.
            %   raw example: 5.34593750;5.34593750;5.34593750;0.0E+0;100  
            %       (mean, min, max, std, and population)
            %   error example: 9.91E+37;9.91E+37;9.91E+37;9.91E+37;0  
            %       (where 9.91E+37 indicates "not a number")
            scpi_meas_str = strcat( "MEASUrement:MEAS",string(i), ...
                ":RESULTS:ALLACQS:MEAN?;MINI?;MAX?;STDDEV?;POPU?" );
            scpi_meas_raw = writeread(scope, scpi_meas_str);
            fprintf('    %s\n', scpi_meas_raw);
            % Convert raw values into table results
            temp = split(scpi_meas_raw, ";");
            t.t_values(i) = str2double(temp(1));
            t.t_values_min(i) = str2double(temp(2));
            t.t_values_max(i) = str2double(temp(3));
            t.t_values_sd(i) = str2double(temp(4));
            t.t_popu(i) = int32( str2double(temp(5)) );
        end % for i = 1:t_row for TRIGGER state
        
    else
        % No statistics to collect during single sequence acquisition mode
        if trigger_state == "TRIGGERorAUTO" && do_measure_statistics == false
            fprintf('    %s\n', 'Note: Set do_measure_statistics flag to enable statistics');
        elseif trigger_state ~= "TRIGGERorAUTO" && do_measure_statistics == true
            fprintf('    %s\n', 'Note: Use scope''s continuous (auto or norm) trigger to collect statistics');
        end
        for i = 1:t_rows
            % Load the current value into the table from the selected scope  
            %   measurement badges. Other table results remain as NaN.
            %   Example: 5.34593750;"V";PK2PK;CH1
            scpi_meas_str = strcat( "MEASUrement:MEAS",string(i), ...
                ":VALUE?;YUNIT?;TYPE?;SOUR1?;SOUR2?" );
            scpi_meas_raw = writeread(scope, scpi_meas_str);
            fprintf('    %s\n', scpi_meas_raw);
            % Convert raw values into table results
            temp = split(scpi_meas_raw, ";");
            t.t_values(i) = str2double(temp(1));
        end % for i = 1:t_row for single trace state

    end % if trigger_state

    % Pack all scope measurement results into legend_text for display in
    % legend.

    legend_text = ""; % initialize legend text for measurement results

    for i = 1:t_rows

        if ~is_scipi_nan(t.t_values(i))
            if ~isKey(d_plot_colors, t.t_channels(i))
                % Add scope color to channel map
                % Insert function requires MATLAB version 2023b
                % d_plot_colors = insert(d_plot_colors, t.t_channels(i), get_plotColorFcn(0) );
                d_plot_colors(t.t_channels(i)) = get_plotColorFcn(0);
            end

            if i ~= 1
                legend_text_append = newline;
                legend_text = append( legend_text, legend_text_append );
            end

            % Display measurement value.
            [tempscale_exp, tempscale_txt] = scale_resultsFcn(t.t_values(i), t.t_units(i));
            legend_text_append = newline + "\color[rgb]{" + hex2rgbStringFcn(d_plot_colors(t.t_channels(i)))   + "}" + ...
                lower(t.t_channels(i)) + " " + "\color{black}" + string(t.Properties.RowNames(i)) + ...
                newline + ( string( t.t_values(i) * tempscale_exp ) ) + " " + ...
                tempscale_txt;
            legend_text = append( legend_text, legend_text_append );

            % Display measurement value statistics if available.
            legend_text_append = makeBadgeFcn("min_{", t.t_values_min(i), t.t_units(i), "gray");
            legend_text = append( legend_text, legend_text_append );
            legend_text_append = makeBadgeFcn("max_{", t.t_values_max(i), t.t_units(i), "gray");
            legend_text = append( legend_text, legend_text_append );
            legend_text_append = makeBadgeFcn("\sigma_{", t.t_values_sd(i), t.t_units(i), "gray");
            legend_text = append( legend_text, legend_text_append );
            legend_text_append = makeBadgeFcn("N", t.t_popu(i), "", "gray");
            legend_text = append( legend_text, legend_text_append );

        else
            % Put placeholder in legend for requested measurement value not 
            % displayed (usually a value too small to read).
            legend_text_append = newline + " " + newline + "\color[rgb]{" + hex2rgbStringFcn(d_plot_colors(t.t_channels(i)))   + "}" + ...
            lower(t.t_channels(i)) + " " + "\color{black}" + string(t.Properties.RowNames(i)) + ...
            newline + "n/a";
            legend_text = append( legend_text, legend_text_append );

        end % if ~is_scipi_nan(t.t_values(i))

    end % for loop

    % Plot invisible marker as a hack to display measurements using legend text.
    plot( nan, nan, 'o', ...
    'MarkerSize', 20, 'color', 'none', ...
    'DisplayName', legend_text)

    hold off
    fprintf('    %s\n', 'Scope measurements complete');

else
    fprintf('%s\n', 'Note: Set do_experiment2_measurements flag to include scope measurements');

end % do_experiment2_measurements


%% Save (optional)

% Create a file name for saved data
%   example: scope_mso64b_channel1_08042024_185841
file_date_string = string(datetime('now', 'TimeZone', 'local', 'Format', 'MMddyyyy_HHmmss'));
file_name = lower((file_name_base + "_" + scope_model ...
 +  "_" ...
 + file_date_string));
file_name = replace(file_name, " ", "");

if save_plot == true
    file_ext='-d' + string(save_plot_extension);
    print_res = '-r' + string(save_plot_resolution);
    fprintf('%s %s, %s, %s\n', 'Saving plot image:', file_name, file_ext, print_res);
    print(gcf,file_name,file_ext,print_res);
    fprintf('    %s\n', 'Plot image saved');
else
    fprintf('%s\n', 'Note: Set save_plot flag true to save a plot image');
end

if save_fig == true
    file_name_fig = file_name + ".fig";
    fprintf('%s %s\n', 'Saving plot figure:', file_name_fig);
    fprintf('    %s\n', 'This may take some time...');
    savefig(gcf, file_name_fig);
    fprintf('    %s\n', 'Plot figure saved');
else
    fprintf('%s\n', 'Note: Set save_fig flag true to save MATLAB figure');
end

if save_workspace == true
    file_name_mat = file_name + ".mat";
    fprintf('%s %s\n', 'Saving workspace:', file_name_mat);
    fprintf('    %s\n', 'This may take some time...');

    % Remove graphics handles (ie, fig_handle) to avoid redundant data
    % warning.
    % https://stackoverflow.com/questions/45560181/avoid-saving-of-graphics-in-matlab
    varData = whos;
    saveIndex = cellfun(@isempty, regexp({varData.class}, 'matlab.(graphics|ui)'));
    saveVars = {varData(saveIndex).name};

    save (file_name_mat, saveVars{:});
    fprintf('    %s\n', 'Workspace saved');
    fprintf('    %s\n', 'Explore workspace details using whos.');
    fprintf('    %s\n', 'Example: whos -file scope_mso64b_channel1_07312024_132214.mat');
    % Use these commands to view contents of the mat file:
    %   fs = load(file_name_mat);
    %   display(fs);
else
    fprintf('%s\n', 'Note: Set save_mat flag true to save the MATLAB workspace');
end


%% Close Session

delete(scope);
clear scope;

fprintf('%s\n\n', 'Script complete')


%% Functions

function byte = get_byteFcn(x)
% Purpose:
%   Convert a single char (ASCII) to one 8-bit byte (numeric).
% Last updated: August 19, 2024
% Auther: BiophysicsLab.com
% Input:
%   x : An ASCII char, string, or numeric value.
% Output:
%   byte : An unsigned 8-bit byte (uint8).
% Error checking:
%   Extensive error checking to catch oscilloscope writeread (or query) 
%   functions returning more than 1 byte:
%       Variable type not string or numeric returns 0xff
%       Array larger than 1 element returns just the first byte
%       Numeric input value larger than 1 byte returns 0xff
%       Empty or NaN value returns 0xff
% Example:
%   getbyte('0') returns 0 (not ASCII  value 48)
% Note:
%   Inital use for this function is to parse SESR register bit values.

    if ischar(x)
        byte = uint8(str2double(x));
    elseif isnumeric(x)
        byte = uint8(x);
    elseif isstring(x)
        byte = uint8(str2double(x));
    else
        fprintf('%s %s\n', 'Error from get_byte function: invalid input parameter type:', class(x));
        byte = uint8(0xff);
        fprintf('%s 0x%x\n', 'byte returned:', byte)
    end

    if length(x) > 1
        fprintf('%s\n', 'Error from get_byte function: input parameter is an array: ');
        fprintf(repmat('%s ', 1, length(x)), x);
        fprintf('\n%s 0x%x\n', 'byte returned:', byte)
    end

    if isnumeric(x) && x > uint8(0xff)
        fprintf('%s 0x%x\n', 'Error from get_byte function: numeric input parameter larger than 1 byte:', x);
        fprintf('%s 0x%x\n', 'byte returned:', byte)
    end
    
    if isempty(byte) || isnan(byte)
        fprintf('%s 0x%o\n', 'Error from get_byte function: invalid input parameter:', x);
        byte = unit8(0xff);
        fprintf('%s 0x%x\n', 'byte returned:', byte)
    end

end % End function


function result_flag = is_scipi_nan(val)
% Purpose:
%   Test scope reading for SCPI Not a Number (NaN) value defined as 9.91E+37 
% Last updated: August 11, 2024
% Auther: BiophysicsLab.com
% Input:
%   val : An Oscilloscope measurement obtained through SCPI as a float or as a string.
% Output:
%   result_flag : True if value is NaN, false if value is valid scope result.
% Error checking:
%   Return true if value is unexpected (not a number or a string)

    result_flag = false;
    if isnumeric(val)
        if val == 9.91E+37
            result_flag = true;
        end
    elseif isstring(val)
        if val == "9.91E+37"
            result_flag = true;
        end
    else
        % On unexpected result, return true
        result_flag = true;
    end

end % End function


function [scale_exp, scale_txt] = scale_resultsFcn(values, top_units)
% Purpose:
%   Scale range of values to the optimum scale exponent and units.
% Last updated: August 20, 2024
% Auther: BiophysicsLab.com
% Input:
%   values : A range of values (such as time or voltage).
%   top_units : The units of measure for the values under review 
% %             (such as 's' for seconds or 'V' for voltage).
% Output:
%   scale_exp : The optimum scale exponent for the scope's time or voltage axis.
%               The goal is to find a scale that is easy to read and 
%               does not include a x10^exp as part of an axis label. 
%   scale_txt : The corresponding units for the scope axis
%               (such as "microseconds or millivolts).
% Error checking:
%   Manage the condition where all values are the same.
%   Manage the condition where the max value is too large or small
%   for this scale algorithm.
%   Manage the condition where top_units is empty ('' or "").

    % if max(values) == min(values)
    %     value_max = abs(max(values));
    % else
    %     value_max = abs(max(values)-min(values));
    % end

    value_max = max(abs(values));

    if ~ischar(top_units)
        top_units = char(top_units);
    end

    if isempty(top_units)
        scale_exp = 1;
        scale_txt = '';
        return
    end

    if value_max < 1e-9
        scale_exp = 1e12;
        scale_txt = ['p' top_units];
    elseif value_max > 1e-9 && value_max <= 1e-6
        scale_exp = 1e9;
        scale_txt = ['n' top_units];
    elseif value_max > 1e-6 && value_max <= 1e-3
        scale_exp = 1e6;
        scale_txt = ['\mu' top_units]; % for tex '\mu' can work
    elseif value_max > 1e-3 && value_max <= .9
        scale_exp = 1e3;
        scale_txt = ['m' top_units];
    elseif value_max > .89 && value_max <= 1e3
        scale_exp = 1;
        scale_txt = top_units; 
    elseif value_max > 1e3 && value_max <= 1e6
        scale_exp = 1e-3;
        scale_txt = ['k' top_units];
    elseif value_max > 1e6 && value_max <= 1e9
        scale_exp = 1e-6;
        scale_txt = ['M' top_units];
    elseif value_max > 1e9 && value_max <= 1e12
        scale_exp = 1e-9;
        scale_txt = ['G' top_units];
    else
        % error condition - units out of scope
        scale_exp = 1;
        scale_txt = top_units;
        fprintf('%s %f\n', 'Warning from scale_results function: Units out of range: value_max =', value_max)
    end

end % End function


function color = get_plotColorFcn(reset)
% Purpose:
%   Pick the next color for plotting from the list of stored colors
%   matching those found on an old Tektronix TDS scope along with some
%   additional colors listed in MATLAB docs under "Specify Plot Colors".
% Last updated: September 10, 2024
% Auther: BiophysicsLab.com
% Input:
%   reset : Use a number greater than 0 for reset value to initialize persistent color_count.
%           Black is the color returned during a reset.
%           Use once to change starting color, otherwise not needed.
%           For example, use reset=3 to plot first curve with the third
%           color in plot_colors list.
%
%           Use 0 for reset value to increment color_count with each function call. 
%           The next color from plot_colors list is returned.
%           For example, use reset=0 to plot curve with next color in
%           plot_colors list. Explicit reset is not needed.
% Persistance:
%   color_count : Increment pointer into plot_colors with each function call.
% Output:
%   color : One color from the list of hex color codes returned as a string.
%           Example: "#660033"
% Error checking:
%   n/a

    persistent color_count;

    % Plot colors are loosly based on Tektronix TDS color scope defaults.
        %   Magenta ("#660033")
        %   Green ("#006633")
        %   Orange ("#FF9966")
        %   Purple ("#7E2F8E")
        %   Gold ("#EDB120")
        %   Turquoise ("#009999")
        %   Blue ("#0072BD")
        %   Light Blue ("#4DBEEE")
    plot_colors = ["#660033" "#006633" "#FF9966" "#7E2F8E" "#EDB120"  ...
        "#009999"  "#0072BD"  "#4DBEEE"];

    color_max = length(plot_colors);

    % Select a color by cycling through the list of plot_colors from 1 to color_max
    if reset ~= 0
        % Handle the case where color_count is being initialized.
        % Returned color is set to black during a reset.
        color_count = max( 1, mod( reset, color_max) ) - 1;
        color = "#ffffff";
    else
        % Handle the case where function returns the next color
        if isempty(color_count) == 1
            % Handle the case where color_count was not initialized before
            % first use.
            color_count = 0;
        end
        color_count = mod( color_count,color_max ) + 1 ;
        color = plot_colors(color_count);
    end
    

end % End function


function chan_txt  = get_channelNameFcn(chan)
% Purpose:
%   Correlate the scope's SCPI input channel to a text-friendly channel name.
% Last updated: August 12, 2024
% Auther: BiophysicsLab.com
% Input:
%   chan : The desired SCPI scope channel (ie, 'CH1').
% Output:
%   chan_txt : The plot channel text name (ie, 'Channel 1').
% Error checking:
%   When chan input is not found in the Channels_available array, print an error message
%   to the command window and return '?' for the channel name.

    % Instrument channel names.
    % TODO: MATHx and REFx channels require additional programming
    Channels_available = [...
        "CH1" "CH2" "CH3" "CH4" ...    
        "MATH1" "MATH2" "MATH3" ...      
        "REF1" "REF2" "REF3" "REF4"];  

    % Plot channel title (math and ref channels not tested)
        Channel_names = [...
        "channel 1" "channel 2" "channel 3" "channel 4" ...
        "math 1" "math 2" "math 3" ...
        "reference 1" "reference 2" "reference 3"]; 

    idx = find( ismember(Channels_available, upper(chan)) );
    if isempty(idx)
        fprintf('%s %s\n', 'Error: Invalid channel input to plot_properties function:', chan)
        chan_txt = "Channel ?";
    else
        chan_txt = Channel_names(idx);
    end

end % End function


function sesr = test_sesrFcn(scope, code_msg)
% Purpose:
%   Test the Standard Event Status Register (SESR). If not zero
%   report all event errors generated by the oscilloscope.
% Last updated: July 28, 2024
% Auther: BiophysicsLab.com
% Input:
%   scope : The scope interface object.
%   code_msg : A text message describing conditions before this function call.
% Output:
%   SESR register : an unsigned 8-bit byte (uint8)
% Error checking:
%   If SESR is not zero, print all event errors to the command window. 

    r = writeread(scope, "*ESR?");
    sesr = get_byteFcn(r);

    if sesr
        % display SESR value in hex format
        fprintf('\n%s%x\n', 'Warning from test_sesrFcn: Scope SESR is not zero: ', sesr);
        if strtrim(code_msg) == ""
            code_msg = "Check prior SCPI calls for source of error";
        end
        fprintf('%s\n', code_msg);
        % display all event errors found
        r = writeread(scope, "ALLEv?");
        fprintf('%s\n',r);
    end

end % End function


function scope_model = get_identityFcn(scope, scope_vendor)
% Purpose:
%   Verify that the oscilloscope is connected to this MATLAB script.
% Last updated: July 29, 2024
% Auther: BiophysicsLab.com
% Input:
%   scope : The scope interface object.
%   scope_vendor : An oscilloscope vendor.
% Output:
%   scope_model contains the scope's model name, 'MSO64B', for example.
% Error checking:
%   - verify valid scope model name, on error: report error message and return '?'
%   - verify support for scope_model : report a warning if the scope is not a mixed
%     signal oscilloscope with meeting this test: contains(scope_model, scope_family)

    % Get scope's identity
    %   example r: TEKTRONIX,MSO64,C012872,CF:91.1CT FV:1.22.4.7207
    r = writeread(scope, "*IDN?");
    scope_idn = split(r,',');

    if isempty(scope_idn) || length(scope_idn) < 4
        fprintf('%s\n', 'Error from get_identity function: SCPI command *idn? failed')
        scope_model='?';
        return
    end

    scope_model = scope_idn{2};
    scope_make = scope_idn{1};
    if ~contains(scope_make, upper(scope_vendor))
        % Example scope_family: MSO; scope_model: MSO64B
        fprintf('%s %s, %s\n', 'Warning from get_identity function: unknown scope vendor:', scope_vendor, scope_model)
    end

end % End function


function channel_used = is_channel_activeFcn(scope, chan)
% Purpose:
%   Verify that the oscilloscope channel has data to acquire. 
% Last updated: August 19, 2024
% Auther: BiophysicsLab.com
% Input:
%   scope : The scope interface object.
%   chan : The desired scope channel.
% Output:
%   Return logical true if the scope channel is displayed on the screen,
%   otherwise false.
% Error checking:
%   none

    scpi_command = strcat( "DISplay:GLObal:", chan, ":STATE?" );
    channel_used= get_byteFcn(writeread(scope, scpi_command));

end % End function


function scope_trigger = get_triggerFcn(scope)
% Purpose:
%   Verify the state of the oscilloscope trigger
% Last updated: September 2, 2024
% Auther: BiophysicsLab.com
% Input:
%   scope : The scope interface object.
% Output:
%   Return char array indicating one of these trigger states:
%       READY       : Waiting for a waveform trigger event (no waveform displayed).
%       SAVE        : Single trace waveform triggered.
%       AUTO        : Continuous waveform trigger in auto mode.
%       TRIGGER     : Continuous waveform trigger in norm mode.
% Error checking:
%   Report single event trigger state 'READY' as an error because there
%   will be no trace on the scope screen - causing a SCPI Curve? read error.

    scope_trigger = writeread(scope, ":TRIGger:STATE?");
    if scope_trigger == "READY"
        fprintf('%s %s\n', 'Warning no scope trigger event:', scope_trigger)
    end

end % End function


function zoomCallbackFcn(obj,evd)
% Purpose:
%   Update the x and y axis labels during a change in zoom.
% Last updated: September 10, 2024
% Auther: BiophysicsLab.com
% Input:
%   obj : Figure object   
%   evd : Event object
% Output:
%   Update xticklabels and yticklabels for current figure after a zoom request. 
% Error checking:
%   Avoid updating x or y axis when zoom limit has been exceeded (divide by zero).
%   Display a warning message in the command window and as a msgbox.
% Call back function setup within the main program:
%   fig_handle = figure('Name', figure_name);
%   ...
%   fig_handle.UserData = struct("xincr", xincr, ...
%       "max_xtickdivs", max_xtickdivs, "max_ytickdivs", max_ytickdivs);
%   h = zoom;
%   set(h,'ActionPostCallback',@zoomCallbackFcn);

    % Get maximum number of X and Y axis ticks for label 
    max_xtickdivs = obj.UserData.max_xtickdivs;
    max_ytickdivs = obj.UserData.max_ytickdivs;

    % Set rounding for x and y axis labels.
    % Use 2 digits to start, then increase value to avoid divide by zero.
    xround = 2;
    yround = 2;

    % Set maximum limit for digits to round axis labels.
    xyincr = obj.UserData.xincr;
    xyround_lmt = round( abs(log10(xyincr)) ) - 1;

    % Calculate X axis after zoom
    new_xlim = get(evd.Axes,'XLim');
    [xscale_exp, xscale_txt] = scale_resultsFcn(new_xlim, 's');
    new_xlim = new_xlim(:) * xscale_exp;

    % Update plot axis ticks and labels
    [new_xticks_rnd, cellxlabels, errFlg] = scale_ticksFcn(xround, xyround_lmt, ...
        new_xlim,  max_xtickdivs);
    if errFlg == true
        zoomErrorFcn("x-axis")
        return
    end

    xticks(new_xticks_rnd / xscale_exp)
    xticklabels(cellxlabels)
    xlabel(['Time (' xscale_txt ')']);

    % Calculate Y axis after zoom
    new_ylim = get(evd.Axes,'YLim');
    [yscale_exp, yscale_txt] = scale_resultsFcn(new_ylim, 'V');
    new_ylim = new_ylim(:) * yscale_exp;

    % Update plot axis ticks and labels
    [new_yticks_rnd, cellylabels, errFlg] = scale_ticksFcn(yround, xyround_lmt, ...
        new_ylim,  max_ytickdivs);
    if errFlg == true
        zoomErrorFcn("y-axis")
        return
    end

    % Display Y axis labels after zoom
    yticks(new_yticks_rnd / yscale_exp) 
    yticklabels(cellylabels);
    ylabel(['Voltage (' yscale_txt ')']);

end % End function


function [new_xyticks_rnd, cell_labels, errFlg] = scale_ticksFcn(xyround, xyround_lmt, ...
    new_xylim, max_xytickdivs)
% Purpose:
%   Update plot axis ticks and labels during a change in zoom.
%   Solves zoom plotting problems:
%   - Avoids divide by zero error when zoom is so large that 2 adjacent ticks are
%     the same value.
%   - Determines optimum rounding of axis ticks (seconds or volts for
%     example) such that adjacent tick values visually look unique.
%   - Avoids 5-digit maximum round off while creating cell labels.
% Last updated: September 7, 2024
% Auther: BiophysicsLab.com
% Input:
%   xyround : initial rounding amount, number of digits to the right
%   xyround_lmt : upper limit to rounding amount, based on scale increment
%   new_xylim : low high limit for an axis
%   max_xytickdivs : number of ticks
% Output:
%   new_xyticks_rnd : new axis ticks with the optimum round-off
%   cell_labels : new axis labels, floating point values packed as stings
%   errFlg : return false when zoom stays withing rounding limit
%            returns true when two adjacent ticks have the same value
% Error checking:
    errFlg = false;
    new_xyticks = linspace(new_xylim(1), new_xylim(2), max_xytickdivs);

    % Find optimum x or y axis scale values using round function.
    while true
        new_xyticks_rnd = round( new_xyticks, xyround );
        if adjacentEqualFcn(new_xyticks_rnd, eps)
            xyround = xyround + 1;
        else
            break
        end
        % Check for limit errors
        if xyround > xyround_lmt
            % Error when two adjacent elements are the same
            % Complete function but return with error flag to be handled by
            % caller.
            errFlg = true;
        end
    end

    % Display X axis labels after zoom
    % Craft the correct number of digits to right for xticklables
    cell_labels = repmat({''}, 1, max_xytickdivs);
    for i = 1:max_xytickdivs
        cell_labels(i) = cellstr(sprintf(strcat('%.', string(xyround), 'f'), ...
            new_xyticks_rnd(i)));
    end

end % End function


function adjacent_equal = adjacentEqualFcn(a, tol)
% Purpose:
%   Search for duplicate adjacent elements in a floating point array. 
%   Catch potential floating point error calculations during zoom axis rescaling.
% Last updated: September 2, 2024
% Auther: BiophysicsLab.com
% Input:
%   a : an array of floating point numbers
%   tol : a tolerance value near zero, such as MATLAB's eps
% Output:
%   adjacent_equal : Returns true if two adjacent numbers are equal,
%                   where equal means within tolarance.
% Error checking:
%   The array of elements under test must have two or more elements.
%   Simply return with adjacentFlag set to false.
% Note:
%   MATLAB's eps stands for floating-point relative accuracy.

    adjacent_equal = false;

    % algorithm assumes array tesing for 2 or more elements
    % MATLAB version 2024a recommends use of isscalar function
    % if length(a) == 1
    if isscalar(a)
        return
    end

    for i=1:length(a)
        switch i
            case 1
            if ismembertol(a(1), a(2), tol)
                adjacent_equal = true;
                return
            end
            case length(a)
            if ismembertol(a(i), a(i-1), tol)
                adjacent_equal = true;
                return
            end
            otherwise
            if ismembertol(a(i), a(i-1), tol) || ismembertol(a(i), a(i+1), tol)
                adjacent_equal = true;
                return
            end
        end % end switch
    end % end for loop

end % End function


function zoomErrorFcn(axis)
% Purpose:
%   Display an error message when figure ploy zoom might generate
%   an axis label divide by zero error.
% Last updated: August 20. 2024
% Auther: BiophysicsLab.com
% Input:
%   axis : string - include which axis is the cause for too much zooming
% Output:
%   msgbox zoom error message box inside the figure.
%   Command Window zoom error message.
%   Prompt user to "Zoom Out" to restore x and y axis labels.
% Error checking:
%   none

    msgbox(["Too much zooming on " + axis; "Restore using: Zoom out"], "Error","error");
    fprintf('%s %s. %s\n', 'Warning: Too much zooming on', axis, 'Restore using Zoom Out.');

end % End function


function legend_text_append = makeBadgeFcn(title, value, units, color)
% Purpose:
%   Format a text string simulating a Tektronix Measurement Badge for use
%   in plot legend.
% Last updated: August 20, 2024
% Auther: BiophysicsLab.com
% Input:
%   title : Title for the value ("amplitude" or "frequency".
%           Title supports tex such as "\sigma" for standard deviation.
%           Special case where title ends in "_{" such as "min_{",
%           indicating title will have subscripted units.
%   value : Measured value
%   units : Measured value units (ie, "V", "Hz", "" for no units)
%   color : Color for legend text (ie, "black", "grey")
% Output:
%   legend_test_append : Formatted text for use in plot legend.
% Error checking:
%   Return blank legend_text_append if value is SCPI NaN (ie, 9.91E+37)

    if ~is_scipi_nan(value)
        [tempscale_exp, tempscale_txt] = scale_resultsFcn(value, units);
        if extractAfter( title, max(1,strlength(title)-2) ) == "_{"
            % Include value's units in title name as tex subscript
            title_str = title + tempscale_txt + "} ";
        else
            title_str = title + " ";
        end
        legend_text_append = newline + "\color{" + color + "}" + title_str  + ...
            string(value * tempscale_exp);
    else
        legend_text_append = "";
    end % if ~is_scipi_nan(value)

end % End function


function num_zeros = nearZeroFcn(nums, tol)
% Purpose:
%   Convert values in a vector near zero to exactly zero
% Last updated: August 23, 2024
% Auther: BiophysicsLab.com
% Input:
%   nums : row vector of numbers
%   tol : define value from which all numbers lower will be set 0
%         (tolerence)
% Output:
%   num_zeros : row vector with numbers near zero set to 0
% Error checking:
%   none

    % Create a logical array to replace numbers near zero.
    test = abs(nums) >= tol;
    num_zeros = nums .* test;

end % End function


function rgbstr = hex2rgbStringFcn(hex)
% Purpose:
%   Wrap MATLAB Exchange function hex2rgb(hex,range) to support:
%   - Input hex values as string array.
%   - Output rgb values as a comma separated string array suitable for use
%     in legend text.
%   Example: "\color[rgb]{" + hex2rgbStringFcn("#660033")   + "}"
% Last updated: August 22, 2024
% Auther: BiophysicsLab.com
% Input:
%   hex : A hexadecimal color code as a string array or character vector.
%         Examples "#334D66" or '#334D66'
% Output:
%   rgbstr : RGB triplet as a comma separated string array.
%            Example "0.2,0.30196,0.4"
% Requirements:
%   hex2rgb(hex) available from MATLAB File Exchange, or builtin in v. 2024a 
% Error checking:
%   none

    if isstring(hex)
        hex = char(hex);
    end

    rgbstr = string(replace(num2str(hex2rgb(hex)), whitespacePattern, ', '));

end % End function


function [ rgb ] = hex2rgb(hex,range)
% hex2rgb converts hex color values to rgb arrays on the range 0 to 1. 
% 
% 
% * * * * * * * * * * * * * * * * * * * * 
% SYNTAX:
% rgb = hex2rgb(hex) returns rgb color values in an n x 3 array. Values are
%                    scaled from 0 to 1 by default. 
%                    
% rgb = hex2rgb(hex,256) returns RGB values scaled from 0 to 255. 
% 
% 
% * * * * * * * * * * * * * * * * * * * * 
% EXAMPLES: 
% 
% myrgbvalue = hex2rgb('#334D66')
%    = 0.2000    0.3020    0.4000
% 
% 
% myrgbvalue = hex2rgb('334D66')  % <-the # sign is optional 
%    = 0.2000    0.3020    0.4000
% 
%
% myRGBvalue = hex2rgb('#334D66',256)
%    = 51    77   102
% 
% 
% myhexvalues = ['#334D66';'#8099B3';'#CC9933';'#3333E6'];
% myrgbvalues = hex2rgb(myhexvalues)
%    =   0.2000    0.3020    0.4000
%        0.5020    0.6000    0.7020
%        0.8000    0.6000    0.2000
%        0.2000    0.2000    0.9020
% 
% 
% myhexvalues = ['#334D66';'#8099B3';'#CC9933';'#3333E6'];
% myRGBvalues = hex2rgb(myhexvalues,256)
%    =   51    77   102
%       128   153   179
%       204   153    51
%        51    51   230
% 
% HexValsAsACharacterArray = {'#334D66';'#8099B3';'#CC9933';'#3333E6'}; 
% rgbvals = hex2rgb(HexValsAsACharacterArray)
% 
% * * * * * * * * * * * * * * * * * * * * 
% Chad A. Greene, April 2014
% https://www.mathworks.com/matlabcentral/fileexchange/46289-rgb2hex-and-hex2rgb
%
% Updated August 2014: Functionality remains exactly the same, but it's a
% little more efficient and more robust. Thanks to Stephen Cobeldick for
% the improvement tips. In this update, the documentation now shows that
% the range may be set to 256. This is more intuitive than the previous
% style, which scaled values from 0 to 255 with range set to 255.  Now you
% can enter 256 or 255 for the range, and the answer will be the same--rgb
% values scaled from 0 to 255. Function now also accepts character arrays
% as input. 
%
% Updated to fix 2 MATLAB code warnings related to assert by RDF August 2024.
% 
% * * * * * * * * * * * * * * * * * * * * 
% See also rgb2hex, dec2hex, hex2num, and ColorSpec. 
% 

%% Input checks:

assert(nargin>0&nargin<3,'hex2rgb function must have one or two inputs.') 

% This test is both redundant with the "switch range" code block and broken since
% any scalar value will test true (not just 1 or 256). Remove "==1" and change 
% assertion to "range argument must be a scalar". 
% MATLAB code warning is also satisfied. - RDF 8/22/2024
if nargin==2
    % assert(isscalar(range)==1,'Range must be a scalar, either "1" to scale from 0 to 1 or "256" to scale from 0 to 255.')
    assert(isscalar(range),'The range parameter must be a scalar.')
end

%% Tweak inputs if necessary: 

if iscell(hex)

    % Change code from "isvector(hex)==1" to "isvector(hex)" because a logcal
    % test  for true is already "1". MATLAB code warning is also satisfied.
    % - RDF 8/22/2024.
    % assert(isvector(hex)==1,'Unexpected dimensions of input hex values.')
    assert(isvector(hex),'Unexpected dimensions of input hex values.')

    
    % In case cell array elements are separated by a comma instead of a
    % semicolon, reshape hex:
    if isrow(hex)
        hex = hex'; 
    end
    
    % If input is cell, convert to matrix: 
    hex = cell2mat(hex);
end

if strcmpi(hex(1,1),'#')
    hex(:,1) = [];
end

if nargin == 1
    range = 1; 
end

%% Convert from hex to rgb: 

switch range
    case 1
        rgb = reshape(sscanf(hex.','%2x'),3,[]).'/255;

    case {255,256}
        rgb = reshape(sscanf(hex.','%2x'),3,[]).';
    
    otherwise
        error('Range must be either "1" to scale from 0 to 1 or "256" to scale from 0 to 255.')
end

end

References

Author: Ron Fredericks

Ron Fredericks is a research technologist focused on aqueous computing methodologies. He is available for consulting projects. His client success stories include improved productivity within research labs, hands-on electronics, MATLAB scripting, python applications, WordPress plugins, optics bench demonstrations, and leadership in technical marketing. His awards include being co-author of record for two biophysics patents, technical and leadership awards for embedded systems from Mentor Graphics and Wind River, and being recognized as a technology educator by Adobe.

Leave a Reply

Your email address will not be published. Required fields are marked *