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.