Zaber Launcher Tutorials
Zaber Motion Library
Sample Projects
Virtual DeviceDropdown icon
About3D Viewer
AccountDropdown icon
Sign InSign Up
Zaber Motion LibraryGetting Started
How-to Guides
Communication
Controlling multiple devicesFinding the right serial port nameImproving performance of X-USBDC (FTDI)Interaction with Zaber LauncherTCP/IP (network) communication
Library Features
Arbitrary unit conversionsError handlingEventsG-CodeNon-blocking (simultaneous) axis movementSaving and loading stateSending arbitrary commands
Device Features
Device I/OLockstepOscilloscopePosition Velocity Time (PVT)PVT Sequence GenerationSettingsStreamed movementTriggersWarning flags
Product-Specific APIs
MicroscopeProcess controllerVirtual devices
Advanced
Building a Standalone Application with MATLAB CompilerBuilding the C++ Library from Source CodeCustom transportsDevice databaseLoggingMATLAB Migration GuidePackaging Your Program with PyinstallerThread safety
API ReferenceSupportBinary Protocol (Legacy)
© 2026 Zaber Technologies Inc.

Oscilloscope

Zaber devices have a built-in data logging feature called the Oscilloscope. It can record the values of multiple device or axis settings in lockstep and at a high data rate, neither of which are possible using software only. All devices with Firmware 7 have this feature.

The library offers a helper class to make configuring the Oscilloscope and retrieving the recorded data easier.

Setup

Once you have a device reference in code, you can get a reference to the Oscilloscope feature just by using the device object's Oscilloscope property:

Python
C#
C++
JavaScript
Java
MATLAB (legacy)
scope = device.oscilloscope
print(f'Oscilloscope can store {scope.get_max_buffer_size()} samples.')
const scope = device.oscilloscope;
console.log(`Oscilloscope can store ${await scope.getMaxBufferSize()} samples.`);
var scope = device.Oscilloscope;
Console.WriteLine($"Oscilloscope can store {scope.GetMaxBufferSize()} samples.");
Oscilloscope scope = device.getOscilloscope();
System.out.println(String.format("Oscilloscope can store %d samples.", scope.getMaxBufferSize()));
scope = device.getOscilloscope();
fprintf("Oscilloscope can store %d samples.\n", scope.getMaxBufferSize());
auto scope = device.getOscilloscope();
std::cout << "Oscilloscope can store " << scope.getMaxBufferSize() << " samples." << std::endl;

The Oscilloscope has a fixed maximum number of samples it can record, printed out by the above sample code. There is also a fixed number of channels that can be recorded simultaneously, which you can get by using the get_max_channels method.

The buffer capacity is divided up between the channels. In recent Firmware versions the buffer is evenly divided between the channels you add. In older Firmware versions there was a maximum capacity of 1024 samples per channel regardless of how many channels you added. You can use the get_buffer_size method after adding channels to determine how many samples per channel you can record.

Reset

Generally you want to reset the Oscilloscope before each use. This clears the data from the previous recording and clears the list of channels to record.

Python
C#
C++
JavaScript
Java
MATLAB (legacy)
scope.clear()
await scope.clear();
scope.Clear();
scope.clear();
scope.clear();
scope.clear();

Channels

Next, add the channels that you want to record. Most device and axis settings can be recorded. You must specify the correct setting scope (0 for device settings or the axis number otherwise).

In this example we're adding the pos and encoder.pos settings from the same axis. This is a common experiment done when tuning the performance of direct-drive devices because it shows lead, lag and ringing as the device tries to match its ideal trajectory.

Python
C#
C++
JavaScript
Java
MATLAB (legacy)
scope.add_channel(1, 'pos')
scope.add_channel(1, 'encoder.pos')
await scope.addChannel(1, 'pos');
await scope.addChannel(1, 'encoder.pos');
scope.AddChannel(1, "pos");
scope.AddChannel(1, "encoder.pos");
scope.addChannel(1, "pos");
scope.addChannel(1, "encoder.pos");
scope.addChannel(1, "pos");
scope.addChannel(1, "encoder.pos");
scope.addChannel(1, "pos");
scope.addChannel(1, "encoder.pos");

There is a maximum number of channels you can add (currently six). You can query the maximum using the get_max_channels method.

Timing

You should always specify the timebase for the recording. The timebase is the interval between samples, and is the inverse of the sample rate. The default and minimum value is 0.1ms (10kHz), and it can be set to multiples of 0.1ms. The example below explicitly sets it to 0.1ms (10kHz).

You can also optionally specify a delay between receipt of the start command and the recording of the first sample. The delay uses the same time units as the timebase.

Python
C#
C++
JavaScript
Java
MATLAB (legacy)
scope.set_timebase(0.1, Units.TIME_MILLISECONDS)
scope.set_delay(0)
await scope.setTimebase(0.1, Time.MILLISECONDS);
await scope.setDelay(0);
scope.SetTimebase(0.1, Units.Time_Milliseconds);
scope.SetDelay(0);
scope.setTimebase(0.1, Units.TIME_MILLISECONDS);
scope.setDelay(0);
scope.setTimebase(0.1, Units.TIME_MILLISECONDS);
scope.setDelay(0);
scope.setTimebase(0.1, Units::TIME_MILLISECONDS);
scope.setDelay(0);

Recording and Retrieving Data

To start the data recording, just use the start method of the Oscilloscope object. You can optionally specify a number of samples to record per channel. The default is the maximum number of samples that will fit in the buffer.

Typically you will want to send a move command to the device in order to have something interesting to record. The example below assumes a linear device and starts a 5cm movement. You can send the move command either before or after calling

start

but if you send it before you may miss data at the start of the movement, depending on the sample rate and the connection speed.

When the recording is finished, use read to retrieve the data. You can call this method immediately after starting the recording and it will block until the data is ready, or you can retrieve the data later if your program has other things to do.

You can call read multiple times and get the same data, if no more recordings have been started in the intervening time. Note that on slower connections like serial ports, it can take several seconds to read the data from the device and this may cause other commands sent to the same device to time out.

Optionally, you can call read to abort a recording in progress. If you do this, read will return whatever data has been recorded so far.

Python
C#
C++
JavaScript
Java
MATLAB (legacy)
print('Starting experiment')
scope.start()
device.get_axis(1).move_relative(5, Units.LENGTH_CENTIMETRES)
data = scope.read()
console.log('Starting experiment');
await scope.start();
await device.getAxis(1).moveRelative(5, Length.CENTIMETRES);
const data = await scope.read();
Console.WriteLine("Starting experiment");
scope.Start();
device.GetAxis(1).MoveRelative(5, Units.Length_Centimetres);
var data = scope.Read();
System.out.println("Starting experiment");
scope.start();
device.getAxis(1).moveRelative(5, Units.LENGTH_CENTIMETRES);
OscilloscopeData[] data = scope.read();
fprintf("Starting experiment\n");
scope.start();
device.getAxis(1).moveRelative(5, Units.LENGTH_CENTIMETRES);
data = scope.read();
std::cout << "Starting experiment" << std::endl;
scope.start();
device.getAxis(1).moveRelative(5, Units::LENGTH_CENTIMETRES);
auto data = scope.read();

Interpreting the Data

The read method returns an array of OscilloscopeData objects, one for each channel you added, in the order they were added.

Each data object identifies the setting it records and the axis or device scope it was recorded from. You can also retrieve the start delay and timebase from this object, and there is a get_sample_time helper method to calculate the time of a given sample index, relative to the start of the recording. The device records all channels in lockstep, so the time of a given sample index will be the same across all channels from the same recording.

Use the get_data method of the data object to get the actual sample values, with optional unit conversion.

The example below shows how to write the data to a .csv spreadsheet file. This example assumes the two channels added in the above example code (position and temperature) and converts to appropriate units.

Python
C#
C++
JavaScript
Java
MATLAB (legacy)

print('Writing results')
pos = data[0]
pos_samples = pos.get_data(Units.LENGTH_MILLIMETRES)
encoder = data[1]
encoder_samples = encoder.get_data(Units.LENGTH_MILLIMETRES)
with open('scope.csv', 'wt') as file:
    file.write('Time (ms),Trajectory Position (mm),Measured Position (mm)\n')
    for i in range(len(pos_samples)):
        file.write(f'{pos.get_sample_time(i, Units.TIME_MILLISECONDS)},')
        file.write(f'{pos_samples[i]},{encoder_samples[i]}\n')
console.log('Writing results');
const pos = data[0]
const pos_samples = await pos.getData(Length.MILLIMETRES);
const encoder = data[1]
const encoder_samples = await encoder.getData(Length.MILLIMETRES);

const lines = [];
lines.push('Time (ms),Trajectory Position (mm),Measured Position (mm)');
pos_samples.forEach((sample, i) => {
    lines.push(`${pos.getSampleTime(i, Time.MILLISECONDS)},${sample},${encoder_samples[i]}`);
});

await fs.writeFile('scope.csv', lines.join('\n'));
Console.WriteLine("Writing results");
var pos = data[0];
var pos_samples = pos.GetData(Units.Length_Millimetres);
var encoder = data[1];
var encoder_samples = encoder.GetData(Units.Length_Millimetres);

using var writer = new StreamWriter("scope.csv");
writer.WriteLine("Time (ms),Trajectory Position (mm),Measured Position (mm)");
for (var i = 0; i < pos_samples.Length; i++)
{
    writer.WriteLine($"{pos.GetSampleTime(i, Units.Time_Milliseconds)},{pos_samples[i]},{encoder_samples[i]}");
}
System.out.println("Writing results");
OscilloscopeData pos = data[0];
double[] pos_samples = pos.getData(Units.LENGTH_CENTIMETRES);
OscilloscopeData encoder = data[1];
double[] encoder_samples = encoder.getData(Units.LENGTH_CENTIMETRES);

try (FileWriter file = new FileWriter("scope.csv")) {
    file.write("Time (ms),Trajectory Position (mm),Measured Position (mm)\n");
    for (int i = 0; i < pos_samples.length; i++)
    {
        file.write(String.format("%f,%f,%f\n",
            pos.getSampleTime(i, Units.TIME_MILLISECONDS),
            pos_samples[i],
            encoder_samples[i]));
    }
}
catch (IOException e) {
    System.out.println("An error occurred.");
    e.printStackTrace();
}
fprintf("Writing results\n");
pos = data(1);
pos_samples = pos.getData(Units.LENGTH_CENTIMETRES);
encoder = data(2);
encoder_samples = encoder.getData(Units.LENGTH_CENTIMETRES);

file = fopen("scope.csv","w");
fprintf(file, "Time (ms),Trajectory Position (mm),Measured Position (mm)\n");
for i = 1:length(pos_samples)
    fprintf(file, "%f,%f,%f\n", ...
        pos.getSampleTime(i, Units.TIME_MILLISECONDS), ...
        pos_samples(i), ...
        encoder_samples(i));
end
fclose(file);
std::cout << "Writing results" << std::endl;
OscilloscopeData& pos = data[0];
auto pos_samples = pos.getData(Units::LENGTH_MILLIMETRES);
OscilloscopeData& encoder = data[1];
auto encoder_samples = encoder.getData(Units::LENGTH_MILLIMETRES);

std::ofstream file("scope.csv");
file << "Time(ms), Trajectory Position(mm), Measured Position(mm)\n";
for (int i = 0; i < pos_samples.size(); i++)
{
    file << pos.getSampleTime(i, Units::TIME_MILLISECONDS) << "," << pos_samples[i] << "," << encoder_samples[i] << "\n";
}

file.close();

Zaber Launcher Interoperability

If your device is connected to your computer via RS-232 or USB, you can optionally have Zaber Launcher graph the recorded data for you.

  1. Connect Zaber Launcher to the device first.
  2. Open the Oscilloscope app and select the device.
  3. In the Capture Settings section at the bottom of the window, change the Capture Mode to Externally triggered.
  4. Press Start. Zaber Launcher will wait for your script to connect and initiate an oscilloscope recording.
  5. Now run your script (connecting to the same serial port). When the data is ready and after your script has downloaded it, Zaber Launcher will also automatically download and plot it.

Complete example

Below is a complete example program incorporating all of the above snippets plus the initial code for openning the connection and finding a device.

Python
C#
C++
JavaScript
MATLAB
Java
from zaber_motion import Units
from zaber_motion.ascii import Connection

with Connection.open_serial_port('COM3') as connection:
    connection.enable_alerts()

    device_list = connection.detect_devices()
    device = device_list[0]
    device.get_axis(1).home()

    scope = device.oscilloscope
    print(f'Oscilloscope can store {scope.get_max_buffer_size()} samples.')

    scope.clear()
    scope.add_channel(1, 'pos')
    scope.add_channel(1, 'encoder.pos')

    scope.set_timebase(0.1, Units.TIME_MILLISECONDS)
    scope.set_delay(0)

    print('Starting experiment')
    scope.start()
    device.get_axis(1).move_relative(5, Units.LENGTH_CENTIMETRES)
    data = scope.read()

    print('Writing results')
    pos = data[0]
    pos_samples = pos.get_data(Units.LENGTH_MILLIMETRES)
    encoder = data[1]
    encoder_samples = encoder.get_data(Units.LENGTH_MILLIMETRES)
    with open('scope.csv', 'wt') as file:
        file.write('Time (ms),Trajectory Position (mm),Measured Position (mm)\n')
        for i in range(len(pos_samples)):
            file.write(f'{pos.get_sample_time(i, Units.TIME_MILLISECONDS)},')
            file.write(f'{pos_samples[i]},{encoder_samples[i]}\n')
const fs = require('fs').promises;

const { Length, Time, ascii: { Connection } } = require('@zaber/motion');

async function main() {
    const connection = await Connection.openSerialPort('COM3');
    try {
        await connection.enableAlerts();

        const deviceList = await connection.detectDevices();
        const device = deviceList[0];
        await device.getAxis(1).home();

        const scope = device.oscilloscope;
        console.log(`Oscilloscope can store ${await scope.getMaxBufferSize()} samples.`);

        await scope.clear();
        await scope.addChannel(1, 'pos');
        await scope.addChannel(1, 'encoder.pos');

        await scope.setTimebase(0.1, Time.MILLISECONDS);
        await scope.setDelay(0);

        console.log('Starting experiment');
        await scope.start();
        await device.getAxis(1).moveRelative(5, Length.CENTIMETRES);
        const data = await scope.read();

        console.log('Writing results');
        const pos = data[0]
        const pos_samples = await pos.getData(Length.MILLIMETRES);
        const encoder = data[1]
        const encoder_samples = await encoder.getData(Length.MILLIMETRES);

        const lines = [];
        lines.push('Time (ms),Trajectory Position (mm),Measured Position (mm)');
        pos_samples.forEach((sample, i) => {
            lines.push(`${pos.getSampleTime(i, Time.MILLISECONDS)},${sample},${encoder_samples[i]}`);
        });

        await fs.writeFile('scope.csv', lines.join('\n'));
    } finally {
        // close the port to allow Node.js to exit
        await connection.close();
    }
}

main();
using Zaber.Motion;
using Zaber.Motion.Ascii;

using var connection = Connection.OpenSerialPort("COM3");

var devices = connection.DetectDevices();
var device = devices[0];
var axis = device.GetAxis(1);
axis.Home();

var scope = device.Oscilloscope;
Console.WriteLine($"Oscilloscope can store {scope.GetMaxBufferSize()} samples.");

scope.Clear();

scope.AddChannel(1, "pos");
scope.AddChannel(1, "encoder.pos");

scope.SetTimebase(0.1, Units.Time_Milliseconds);
scope.SetDelay(0);

Console.WriteLine("Starting experiment");
scope.Start();
device.GetAxis(1).MoveRelative(5, Units.Length_Centimetres);
var data = scope.Read();

Console.WriteLine("Writing results");
var pos = data[0];
var pos_samples = pos.GetData(Units.Length_Millimetres);
var encoder = data[1];
var encoder_samples = encoder.GetData(Units.Length_Millimetres);

using var writer = new StreamWriter("scope.csv");
writer.WriteLine("Time (ms),Trajectory Position (mm),Measured Position (mm)");
for (var i = 0; i < pos_samples.Length; i++)
{
    writer.WriteLine($"{pos.GetSampleTime(i, Units.Time_Milliseconds)},{pos_samples[i]},{encoder_samples[i]}");
}
package zaber.example;

import java.io.FileWriter;
import java.io.IOException;

import zaber.motion.Units;
import zaber.motion.ascii.Axis;
import zaber.motion.ascii.Connection;
import zaber.motion.ascii.Device;
import zaber.motion.ascii.Oscilloscope;
import zaber.motion.ascii.OscilloscopeData;

public class App
{
    public static void main(String[] args)
    {
        try (Connection connection = Connection.openSerialPort("COM3")) {
            connection.enableAlerts();

            Device[] deviceList = connection.detectDevices();
            Device device = deviceList[0];
            device.getAxis(1).home();

            Oscilloscope scope = device.getOscilloscope();
            System.out.println(String.format("Oscilloscope can store %d samples.", scope.getMaxBufferSize()));

            scope.clear();

            scope.addChannel(1, "pos");
            scope.addChannel(1, "encoder.pos");

            scope.setTimebase(0.1, Units.TIME_MILLISECONDS);
            scope.setDelay(0);

            System.out.println("Starting experiment");
            scope.start();
            device.getAxis(1).moveRelative(5, Units.LENGTH_CENTIMETRES);
            OscilloscopeData[] data = scope.read();

            System.out.println("Writing results");
            OscilloscopeData pos = data[0];
            double[] pos_samples = pos.getData(Units.LENGTH_CENTIMETRES);
            OscilloscopeData encoder = data[1];
            double[] encoder_samples = encoder.getData(Units.LENGTH_CENTIMETRES);

            try (FileWriter file = new FileWriter("scope.csv")) {
                file.write("Time (ms),Trajectory Position (mm),Measured Position (mm)\n");
                for (int i = 0; i < pos_samples.length; i++)
                {
                    file.write(String.format("%f,%f,%f\n",
                        pos.getSampleTime(i, Units.TIME_MILLISECONDS),
                        pos_samples[i],
                        encoder_samples[i]));
                }
            }
            catch (IOException e) {
                System.out.println("An error occurred.");
                e.printStackTrace();
            }
        }
    }
}
import zaber.motion.ascii.Connection;
import zaber.motion.Units;

connection = Connection.openSerialPort('COM3');
try
    connection.enableAlerts();

    deviceList = connection.detectDevices();
    device = deviceList(1);
    device.getAxis(1).home();

    scope = device.getOscilloscope();
    fprintf("Oscilloscope can store %d samples.\n", scope.getMaxBufferSize());

    scope.clear();
    scope.addChannel(1, "pos");
    scope.addChannel(1, "encoder.pos");

    scope.setTimebase(0.1, Units.TIME_MILLISECONDS);
    scope.setDelay(0);

    fprintf("Starting experiment\n");
    scope.start();
    device.getAxis(1).moveRelative(5, Units.LENGTH_CENTIMETRES);
    data = scope.read();

    fprintf("Writing results\n");
    pos = data(1);
    pos_samples = pos.getData(Units.LENGTH_CENTIMETRES);
    encoder = data(2);
    encoder_samples = encoder.getData(Units.LENGTH_CENTIMETRES);

    file = fopen("scope.csv","w");
    fprintf(file, "Time (ms),Trajectory Position (mm),Measured Position (mm)\n");
    for i = 1:length(pos_samples)
        fprintf(file, "%f,%f,%f\n", ...
            pos.getSampleTime(i, Units.TIME_MILLISECONDS), ...
            pos_samples(i), ...
            encoder_samples(i));
    end
    fclose(file);

    connection.close();
catch exception
    connection.close();
    rethrow(exception);
end
#include <fstream>
#include <iostream>
#include <vector>
#include <zaber/motion/ascii.h>

using namespace zaber::motion;
using namespace zaber::motion::ascii;

int main() {
    Connection connection = Connection::openSerialPort("COM3");
    connection.enableAlerts();

    std::vector<Device> deviceList = connection.detectDevices();
    Device device = deviceList[0];
    device.getAxis(1).home();

    auto scope = device.getOscilloscope();
    std::cout << "Oscilloscope can store " << scope.getMaxBufferSize() << " samples." << std::endl;

    scope.clear();
    scope.addChannel(1, "pos");
    scope.addChannel(1, "encoder.pos");

    scope.setTimebase(0.1, Units::TIME_MILLISECONDS);
    scope.setDelay(0);

    std::cout << "Starting experiment" << std::endl;
    scope.start();
    device.getAxis(1).moveRelative(5, Units::LENGTH_CENTIMETRES);
    auto data = scope.read();

    std::cout << "Writing results" << std::endl;
    OscilloscopeData& pos = data[0];
    auto pos_samples = pos.getData(Units::LENGTH_MILLIMETRES);
    OscilloscopeData& encoder = data[1];
    auto encoder_samples = encoder.getData(Units::LENGTH_MILLIMETRES);

    std::ofstream file("scope.csv");
    file << "Time(ms), Trajectory Position(mm), Measured Position(mm)\n";
    for (int i = 0; i < pos_samples.size(); i++)
    {
        file << pos.getSampleTime(i, Units::TIME_MILLISECONDS) << "," << pos_samples[i] << "," << encoder_samples[i] << "\n";
    }

    file.close();

    return 0;
}