PVT sequences provide a way to travel a trajectory defined by points with position, velocity and time. Since the points are queued by the device internally, the device can travel the trajectory smoothly without any effects from communication delay. Additionally, PVT sequences can perform other actions such as controlling device's IO. You can use PVT sequences to coordinate the movement of multiple axes as well as just to move a single axis. Please note, however, that you can only perform coordinated movement of multiple axes connected to same controller. You cannot use PVT to coordinate the movement of multiple controllers or integrated devices.
This guide mostly describes the
PvtSequence
class.
Additionally, much of the presented API's behavior originates in the underlying Zaber ASCII Protocol.
Refer to the pvt section in the ASCII Protocol Manual for an in depth explanation of the behavior.
You may check if the device supports PVT movement by checking if the pvt.numseqs setting has a non-zero value.
The feature is available since Firmware version 7.33.
This API uses arrays as arguments.
Sometimes you may need to create an array of a complex type with size of 1.
Such an array must be allocated using the javaArray function.
The function takes two arguments: fully qualified name of the type and the size of the array.
The example follows.
In any other case the array can be created in the standard way.
To start using PVT, get a PVT sequence from the device. In this example we get the first sequence.
PvtSequence pvtSequence = device.getPvt().getSequence(1);
The instance returned represents a handle for PVT sequence number 1 on the device.
The PVT sequence needs to be set up before it can be used. Once the PVT sequence is set up in either “Live” mode or “Store” mode, the methods of this instance allow you to append points onto the sequence.
When a sequence is in Live mode, points in the sequences's action queue will immediately execute.
To set up a PVT sequence in Live mode, call the
setupLive
method with an arguments containing the axis numbers of the axes that the sequence will target.
You can provide a variable number of arguments (axis numbers).
pvtSequence.setupLive(1, 2);
PVT sequences set up in Store mode do not execute the points in their action queue. Instead, they store them to a PVT buffer on the device. Once the points are stored, PVT buffers can be played back by live sequence via the
callmethod, so that the stored points are added to the live sequences's action queue.
Stored sequences may also be set up to trigger if a device reaches a condition, such as when an I/O port equals a value. Read more about triggers here.
Buffers on a device are identified by numbers starting from 1.
Since Store sequences cannot be set up to use non-empty buffers, it is recommended to erase the buffer before setting up.
See the following code for an example of setting up a PVT sequence in Store mode:
PvtBuffer pvtBuffer = device.getPvt().getBuffer(1);
pvtBuffer.erase();
pvtSequence.setupStore(pvtBuffer, 1, 2);
// add PVT points...
// finish writing to the buffer
pvtSequence.disable();
Once you have appended some points to the PVT buffer, you can get its content, or call it from a live sequence. Before calling the buffer ensure that the live sequence is set up on the same set of axes as the original store sequence. If the axes are different, the unit conversions applied when storing the commands may result in incorrect motion. You can ignore this condition if the conversion factors of your axes are the same (e.g. the peripherals are the same product).
std::vector<std::string> content = pvtBuffer.getContent();
for (auto line : content) {
std::cout << line << std::endl;
}
// outputs:
// setup store 1 2
// point abs p 0 125984 v 0 206413 t 400.0
// point abs p 18142 298163 v 29723 488510 t 520.0
// ...
pvtSequence.setupLive(1, 2);
// the sequence will now execute the stored points
pvtSequence.call(pvtBuffer);
Note that the points printed are in native units and their format is described in the pvt buffer print command documentation in the ASCII Protocol Manual.
A PVT point can be relative or absolute. If a point is relative, the device treats the current position of the axes as the origin. If a point is absolute, the device treats the position zero of the axes as origin. The time of a point is always specified as relative to the previous one.
Parameters for sequence points make use of
Measurement
objects for all: position, velocity, and time.
Measurement
objects are simple objects that contain a
value
field as well as an optional
unit
field.
The following example shows how to do a PVT movement.
// {x, y, v_x, v_y, time}
double path[5][5] = {
{0.00, 3.00, 0.0, 1.0, 4.0},
{6.10, 5.10, 4.0, 0.3, 3.0},
{9.40, 4.15, 1.0, 0.7, 2.5},
{4.60, 6.00, 2.0, 0.4, 3.5},
{7.20, 3.20, 0.0, 0.0, 3.6},
};
for (double* point : path) {
pvtSequence.point({
Measurement(point[0], Units::LENGTH_MILLIMETRES),
Measurement(point[1], Units::LENGTH_MILLIMETRES)
}, {
Measurement(point[2], Units::VELOCITY_MILLIMETRES_PER_SECOND),
Measurement(point[3], Units::VELOCITY_MILLIMETRES_PER_SECOND)
}, Measurement(point[4], Units::TIME_SECONDS));
}
It's crucial that your sequence ends with a point that has 0 velocities of all axes.
Otherwise, the device runs out of points stopping unexpectedly, and the library raises
PvtExecutionException
exception.
Instead of providing velocities of the points, the library can calculate them from the positions of the surrounding points. This is useful when you just want to smoothly traverse a number of points with an average speed but without concern for specific velocities. The library uses Finite Difference calculation that only relies on the position of the immediate neighbor points. See the example below on how to utilize the feature.
// {x, y, v_x, v_y, time}
std::optional<double> path[5][5] = {
{0.00, 3.00, 0.0, std::nullopt, 4.0},
{6.10, 5.10, std::nullopt, std::nullopt, 3.0},
{9.40, 4.15, std::nullopt, std::nullopt, 2.5},
{4.60, 6.00, std::nullopt, std::nullopt, 3.5},
{7.20, 3.20, 0.0, 0.0, 3.6},
};
for (std::optional<double>* point : path) {
pvtSequence.point({
Measurement(point[0].value(), Units::LENGTH_MILLIMETRES),
Measurement(point[1].value(), Units::LENGTH_MILLIMETRES)
}, {
(point[2] ? std::make_optional<Measurement>(point[2].value(), Units::VELOCITY_MILLIMETRES_PER_SECOND) : std::nullopt),
(point[3] ? std::make_optional<Measurement>(point[3].value(), Units::VELOCITY_MILLIMETRES_PER_SECOND) : std::nullopt),
}, Measurement(point[4].value(), Units::TIME_SECONDS));
}
You can either provide an empty array to calculate all velocities or
std::nullopt
instead of
Measurementto calculate velocity of a specific axis.
You can either provide an empty array to calculate all velocities or create partially empty array using javaArray to calculate velocity of a specific axis.
The velocity calculation comes with some limitations. Notably, it cannot calculate the velocity from a combination
of absolute and relative points. We recommend sticking to only one or another in your programs.
Additionally, the calculation requires that the immediate points before and after the calling of a PVT buffer have
defined velocities. The first point of a stored sequence must have a defined velocity as well.
Lastly, the endpoint of the sequence must have 0 velocities of all axes.
A PVT sequence is busy if it is currently executing points in its action queue.
You can check if a live sequence is busy using the
isBusy
method.
The
waitUntilIdle
method waits until the sequence becomes idle.
Here is an example of their usage:
if (pvtSequence.isBusy())
{
std::cout << "Sequence is busy." << std::endl;
}
pvtSequence.waitUntilIdle();
To disable a sequence that has been setup call the
disable
method.
Any other movement (e.g. home, stop) will disable the sequence as well.
pvtSequence.disable();
PVT sequences can set the digital or analog output ports of a device as an action at points. See the
PvtIo
class for a full list of available functions.
pvtSequence.getIo().setDigitalOutput(1, DigitalOutputAction::ON);
pvtSequence.getIo().setDigitalOutputSchedule(1, DigitalOutputAction::ON, DigitalOutputAction::OFF, 100);
pvtSequence.getIo().setAnalogOutput(1, 0.42);
Corking reduces the likelihood of running out of points by first populating the action queue on the device, and then telling the device to execute it. Corked sequence blocks the execution of points in a live sequence's action queue. Execution resumes when the sequence is uncorked, or when the number of queued points reaches queue's limit.
Corking is useful when you are executing a large number of fast actions, such as moving in small incremements or interacting with I/O multiple times.
If points are executed faster than they are received, the action queue will empty and the library throws
PvtExecutionException
exception.
In the case corking does not help and the sequence still runs out of points, you can preload the points to a PVT buffer and call the buffer.
// block action execution
pvtSequence.cork();
// ... add some actions to queue
// unblock action execution
pvtSequence.uncork();
Note that the sequence must be idle when issuing the cork command.
See the PVT sequence fifo command documentation in the ASCII Protocol Manual for more details.
The PVT sequences can also include axes in lockstep groups.
In the following example the stream is set up on a lockstep group with number 1 and a single physical axis with number 3.
pvtSequence.setupLiveComposite(
PvtAxisDefinition(1, PvtAxisType::LOCKSTEP),
PvtAxisDefinition(3, PvtAxisType::PHYSICAL)
);
See the Lockstep how-to guide for more information about lockstep groups.
Instead of making individual calls to the
point
and various output methods as
shown above, you can represent an entire PVT sequence as an array of items, where each item can be a
point, a buffer call or an IO action.
There are two types of PVT sequences: Complete and partial. The difference between them is that in complete
sequences all the points are expected to have values for every axis and time values. Partial sequences allow
the points to have certain combinations of values missing. Most of the PVT functions deal with complete
sequences; the partial type is intended for loading CSV files that have missing columns; you must use the
generateVelocities
,
generatePositions
or
generateVelocitiesAndTimes
methods to transform them into complete sequences
before you can use them with devices. The non-point item types are the same between complete and partial
sequences.
Complete PVT sequences are represented as arrays of type
PvtSequenceItem
, each
of which can hold a value that is one of these types:
PvtPoint
represents a point with all values definedPvtCallAction
represents a call to another bufferPvtSetAnalogOutputAction
changes the state of one analog output pinPvtSetAllAnalogOutputsAction
changes the states of all analog outputsPvtSetDigitalOutputAction
changes the state of one digital output pinPvtSetAllDigitalOutputsAction
changes the states of all digital outputsPvtCancelOutputScheduleAction
cancels a future scheduled change to one analog or digital output pinPvtCancelAllOutputsScheduleAction
cancels future scheduled changes to all analog or digital output pinsPartial PVT sequences are similar, except the type of the array elements
is
PvtPartialSequenceItem
and
PvtPoint
is replaced
with
PvtPartialPoint
, which allows some of its member data to be unspecified.
The array representation is convenient because it allows treating a complete sequence as a single object, and supports loading and saving sequences to CSV files for backup, manual editing or programmatic generation with your own code or external programs. The following methods are relevant when using the array representation:
pvtBuffer
:retrieveSequenceData
reads the contents of an existing
buffer on a devicepvtSequence
:loadSequenceData
reads complete sequence data from
a CSV file on diskloadPartialSequenceData
reads partial seqeunce
data from a CSV filesaveSequenceData
saves complete sequence
data to a CSV filesubmitSequenceData
sends sequence data
to a buffer or live stream on a devicegeneratePositions
converts partial sequence
data to complete by filling in missing positionsgenerateVelocities
converts partial sequence
data to complete by filling in missing velocitiesgenerateVelocitiesAndTimes
converts partial
sequence data to complete by filling in missing velocities and times (useful if you only want to calcuate the positions yourself)convertTimeAbsoluteToRelative
and
convertTimeRelativeToAbsolute
convert the time values of points in a sequence between
relative (relative to the previous point) and absolute (relative to the start of the sequence) representations. Most of the
above methods expect the relative representation but it is sometimes convenient to calculate time in absolute form or
store them that way in CSV files. There are also variations of these two functions for partial sequences.If you're connecting your device via a serial port or USB to serial bridge, it can potentially take several seconds to transfer all your PVT data to the device.
With devices that have them, consider using the direct USB connection or a TCP/IP connection for better communication performance.
If you are having trouble, these are some common points of confusion using PVT:
Additionally, it is wise to avoid specifying position or velocity values that are very close to device limits. PVT sequences become increasingly difficult to debug as this happens, as trajectories are more likely to briefly exceed axis limits when control points are close to them.