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.");
Oscilloscopescope= 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.
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.
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
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.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)
withopen('scope.csv', 'wt') as file:
file.write('Time (ms),Trajectory Position (mm),Measured Position (mm)\n')
for i inrange(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.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);
usingvar 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]}");
}
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.
Connect Zaber Launcher to the device first.
Open the Oscilloscope app and select the device.
In the Capture Settings section at the bottom of the window, change the Capture Mode to Externally triggered.
Press Start. Zaber Launcher will wait for your script to connect and initiate an oscilloscope recording.
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.asciiimport 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)
withopen('scope.csv', 'wt') as file:
file.write('Time (ms),Trajectory Position (mm),Measured Position (mm)\n')
for i inrange(len(pos_samples)):
file.write(f'{pos.get_sample_time(i, Units.TIME_MILLISECONDS)},')
file.write(f'{pos_samples[i]},{encoder_samples[i]}\n')
using Zaber.Motion;
using Zaber.Motion.Ascii;
usingvar 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);
usingvar 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]}");
}