Examples

ASCII

Setting Up an AsciiSerial and Homing All Devices

The AsciiSerial class models a serial port. All of the devices connected to this serial port are assumed to be using the Zaber ASCII Protocol.

To send what we call a “global” command, we must send a command to device 0. All Zaber devices will always respond to commands addressed to device 0. The AsciiCommand class has a default device address of 0. If we do not specify a device address, then the address of the command will be set to 0.

So, in order to connect to our serial port and home all devices, we need to create an AsciiSerial object, and an AsciiCommand object to send:

# Assume that our serial port can be found at /dev/ttyUSB0.
port = AsciiSerial("/dev/ttyUSB0")
command = AsciiCommand("home")

port.write(command)

At this point, we have achieved our goal of homing all devices. However, each device has sent a reply acknowledging that it received a command. These replies are now available in the input buffer of the port. We can use our AsciiSerial to read those messages:

# Assume that we have 3 devices connected.
ndevices = 3

for i in range(0, ndevices):
    port.read()

In the above example, port.read() returns an AsciiReply containing the reply read from a device. We discard this reply by not assigning it to a variable. In production-quality code, it is recommended that you always check replies for errors. This is demonstrated in the next example.

Checking Replies for Errors

In the last example, we sent a global “home” command to 3 devices. We the read and discarded the replies the devices sent. It is recommended to always check the reply for errors. These can appear in two places in a reply: the “Reply Flag” and “Warning Flag” fields.

Replies read using AsciiSerial.read() will be returned in the form of AsciiReply objects. The AsciiReply class provides easy access to the reply and warning flags through its reply_flag and warning_flag attributes.

Let’s first check the reply flag of our reply. The reply_flag attribute will always contain a string of length 2, whose value is either “OK” or “RJ”. A value of “RJ” indicates that the command which caused this reply to be sent was rejected. The “data” field (accessible through the data attribute of AsciiReply) will contain the reason for the rejection. See the Reply section of the ASCII Protocol Manual for a complete list of rejection reasons. Here is an example of a simple check for command rejection:

# Assume that we have an AsciiSerial object called "port".
reply = port.read()

if reply.reply_flag == "RJ":
    print("A command was rejected! Reason: {}".format(reply.data))

The warning flag is also always a string of length 2. When there are no warnings to display, then the field will contain "--". If not, then the field will contain one of the 2-letter warning flags described in the Warning Flags section of the ASCII Protocol Manual. The following code checks the warning_flag attribute for problems:

if reply.warning_flag != "--":
    print("Warning received! Flag: {}".format(reply.warning_flag))

Sending Commands Using AsciiDevice

The AsciiDevice class is intended to represent a single Zaber device. It provides a blocking, one-message-at-a-time method for sending commands and receiving replies. The AsciiDevice.send() function will send a command to the device, and then wait for a reply. Once it receives that reply, it will return it in the form of an AsciiReply.

AsciiDevice.send() accepts either a string or an AsciiCommand as an argument. In either case, the function will overwrite the device address to be the address of the AsciiDevice. This allows you to omit the device number when sending commands, like so:

# Assume we have an AsciiSerial called "port".
device = AsciiDevice(port, 1)

# All of the following are equivalent.
device.send("home")
device.send("1 home")
device.send(AsciiCommand("home"))
device.send(AsciiCommand(1, "home"))

In rare cases, this may result in some unexpected behaviour. Consider the following:

# A full, properly-formatted ASCII command string,
# specifying the "home" command, addressed to device 2.
command_string = "/2 0 home\r\n"
command_obj = AsciiCommand(command_string)

# This device has an address of 1.
device1 = AsciiDevice(port, 1)

# What happens when a command with address 2
# is sent by a device with address 1?
device1.send(command_obj)

In the above example, a command created to be sent to device 2 is sent using an AsciiDevice with address 1. The AsciiDevice.send() function will change the address of the command to its own address before sending the command.

Working With Individual Axes Using AsciiAxis

Some Zaber devices have multiple axes. Just as AsciiDevice models a single Zaber device using the ASCII protocol, AsciiAxis models a single axis of a Zaber device. An AsciiAxis must have a parent AsciiDevice. A new AsciiAxis can be created either by using AsciiDevice.axis(), or by using the AsciiAxis constructor.

Once you have created your AsciiAxis, you can send commands to it just like you can to an AsciiDevice:

# Assume we have an AsciiDevice called "device".
axis1 = device.axis(1)
axis2 = device.axis(2)

axis1.send("home")

The PollUntilIdle Function

In the Zaber ASCII Protocol, devices respond as soon as they have understood a command. This means that we need to poll the device to see if it has finished executing a command. The AsciiDevice.poll_until_idle() and AsciiAxis.poll_until_idle() functions are provided specifically for this purpose. They will block code execution until the device reports itself as idle, polling the device at a regular interval:

# Tell the device to move a fairly large distance.
device.send("move rel 10000")

# Wait for the device to finish moving.
device.poll_until_idle()

“Sugar” Functions

AsciiDevice also provides some convenience functions for sending the most common commands to the device. These include functions to home the device, and to send the “move” commands:

device = AsciiDevice(port, 1)

device.home()
device.move_rel(2000)

Note that in the above example, we do not call poll_until_idle(). The “sugar” functions will block until the device has finished moving, with the exception of move_vel, which returns immediately. The move_abs, move_rel, and move_vel functions also take an optional argument, blocking, which will cause the function to return immediately if it is set to False. If set to True, the function will poll the device until it is idle.

A Longer Example

Here’s an example script that you should be able to copy to a file and run:

from zaber.serial import AsciiSerial, AsciiDevice, AsciiCommand, AsciiReply
import time

# Helper to check that commands succeeded.
def check_command_succeeded(reply):
    """
    Return true if command succeeded, print reason and return false if command
    rejected

    param reply: AsciiReply

    return: boolean
    """
    if reply.reply_flag != "OK": # If command not accepted (received "RJ")
        print ("Danger! Command rejected because: {}".format(reply.data))
        return False
    else: # Command was accepted
        return True


# Open a serial port. You may need to edit this section for your particular
# hardware and OS setup.
with AsciiSerial("/dev/ttyUSB0") as port:  # Linux
#with AsciiSerial("COM3") as port:  # Windows

    # Get a handle for device #1 on the serial chain. This assumes you have a
    # device already in ASCII 115,220 baud mode at address 1 on your port.
    device = AsciiDevice(port, 1) # Device number 1

    # Home the device and check the result.
    reply = device.home()
    if check_command_succeeded(reply):
        print("Device homed.")
    else:
        print("Device home failed.")
        exit(1)

    # Make the device has finished its previous move before sending the
    # next command. Note that this is unnecessary in this case as the
    # AsciiDevice.home command is blocking, but this would be required if
    # the AsciiDevice.send command is used to trigger movement.
    device.poll_until_idle()

    # Now move the device to a non-home position.
    reply = device.move_rel(2000) # move rel 2000 microsteps
    if not check_command_succeeded(reply):
        print("Device move failed.")
        exit(1)

    # Wait for the move to finish.
    device.poll_until_idle()

    # Read back what position the device thinks it's at.
    print("Device position is now %d" % device.get_position())

    # Port is automatically cleaned up by 'with' statement above.

Binary

Setting Up a BinarySerial and Homing All Devices

The BinarySerial class models a serial port. All of the devices connected to this serial port are assumed to be using the Zaber Binary Protocol.

To send a “home” command to all devices, we send a command to device number 0. All Zaber devices will always respond to commands sent to device number 0.

We can use the BinaryCommand class to easily encode commands to be sent using the Binary Protocol. In the Binary Protocol, “home” is command number 1. So, we can send a “home” command (command number 1) to all devices (device number 0) like so:

# Assume that our serial port can be found at /dev/ttyUSB0.
port = BinarySerial("/dev/ttyUSB0")

# Device number 0, command number 1.
command = BinaryCommand(0, 1)

port.write(command)

In the Binary Protocol, a device does not respond until it has completed a movement. Once the devices have finished homing, they will each respond. We can listen for these responses like so:

# Assume that we have 3 devices connected.
ndevices = 3

for i in range(0, ndevices):
    port.read()

In the above example, port.read() returns a BinaryReply containing the reply read from a device. We discard this reply by not assigning it to a variable. In production-quality code, it is recommended that you always check that the replies you receive are not error messages indicating that something has gone wrong. This is demonstrated in the next example.

Checking Replies for Errors

In the Binary Protocol, devices may send messages at any time indicating that they have encountered an error. Error messages always have a command number of 255. The data field of the message will contain one of the error codes specified in the Error section of the Binary Protocol Manual.

In order to check if a device encountered an error, we must check that the reply’s command_number attribute is not 255. If it is 255, then we must examine the data of the reply to determine what has gone wrong. The following example demonstrates this:

# Assume that we have a BinarySerial object called "port".
reply = port.read()

if reply.command_number == 255:
    print("An error occurred in device {}. Error code: {}".format(
            reply.device_number, reply.data))

Using BinaryDevice to Send Commands

The BinaryDevice class serves to model a single Zaber device using the Binary Protocol. It provides a selection of “sugar” functions to send some of the most common commands to a device. The following example creates a new BinaryDevice and sends it the “move absolute” and “move relative” commands:

# Assume we have a BinarySerial object called "port".
device = BinaryDevice(port, 1)

device.move_abs(300)
device.move_rel(1000)

The BinaryDevice class can also be used to send any command to the device, using BinaryDevice.send(). BinaryDevice.send() accepts either a BinaryCommand object, or several integers representing a command to be sent. For example, the “reset” command is command number 0. To send the reset command, we can do the following:

device.send(0)

BinaryDevice.send() will always overwrite the device number of a command to be sent. This can result in some unexpected behaviour in certain cases. Consider the following code:

# "device2" has a device number of 2.
device2 = BinaryDevice(port, 2)

# "command" is addressed to device number 1.
command = BinaryCommand(1, 21, 1000)

# Should the command be sent to device 1, or device 2?
device2.send(command)

In the above case, the command’s device_number attribute will be overwritten by device2 to be 2. The command will be sent to device 2, despite being originally addressed to device 1.

This edge case can be used to your advantage. Consider the following:

command = BinaryCommand(0, 55, 1234)

device1 = BinaryDevice(port, 1)
device2 = BinaryDevice(port, 2)
device3 = BinaryDevice(port, 3)

device1.send(command)
device2.send(command)
device3.send(command)

One command can be sent to multiple devices easily by relying on BinaryDevice.send() to properly set the device number.

A Longer Example

Here’s an example script that you should be able to copy to a file and run:

from zaber.serial import BinarySerial, BinaryDevice, BinaryCommand, BinaryReply
import time

# Helper to check that commands succeeded.
def check_command_succeeded(reply):
    """
    Return true if command succeeded, print reason and return false if command
    rejected

    param reply: BinaryReply

    return: boolean
    """
    if reply.command_number == 255: # 255 is the binary error response code.
        print ("Danger! Command rejected. Error code: " + str(reply.data))
        return False
    else: # Command was accepted
        return True


# Open a serial port. You may need to edit this section for your particular
# hardware and OS setup.
with BinarySerial("/dev/ttyUSB0") as port:  # Linux
#with BinarySerial("COM3") as port:         # Windows

    # Get a handle for device #1 on the serial chain. This assumes you have a
    # device already in Binary 9,600 baud mode at address 1 on your port.
    device = BinaryDevice(port, 1) # Device number 1

    # Home the device and check the result.
    reply = device.home()
    if check_command_succeeded(reply):
        print("Device homed.")
    else:
        print("Device home failed.")
        exit(1)

    # Note that unlike the ASCII example, there is no poll_until_idle() call
    # here. This is because in the Binary protocol, device replies do not
    # arrive until after the device has completed the command.

    # Now move the device to a non-home position.
    reply = device.move_rel(2000) # move rel 2000 microsteps
    if not check_command_succeeded(reply):
        print("Device move failed.")
        exit(1)

    # Read back what position the device thinks it's at.
    print("Device position is now %d" % device.get_position())

    # Port is automatically closed by 'with' statement above.

What Now?

If you need more info, then continue on to the zaber.serial package API reference pages, or check out the Advanced Topics page.