Skip to content

Custom Transports

Zaber Motion Library allows connecting to devices using serial port and TCP/IP protocol. For some applications, it is useful to carry Zaber ASCII protocol over other transports (e.g., UDP) or embed messages into proprietary protocols. For those cases, the library provides the functionality of custom transports.

This article describes an advanced topic that requires knowledge of multi-threaded or asynchronous programming. We only provide example code in Python as your implementation highly depends on the purpose, and API is similar in all languages.

The following code demonstrates the usage of custom transports to carry Zaber ASCII protocol over UDP datagrams.

import socket
import threading
import time

from zaber_motion import Library, LogOutputMode
from zaber_motion.ascii import Connection, Transport

Library.set_log_output(LogOutputMode.STDOUT)

HOST = "localhost"
PORT = 5000
DEVICE_ADDRESS = 1

transport = Transport.open()
connection = Connection.open_custom(transport)

udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

def loop_read():
  try:
    while True:
      message = transport.read()
      data = str.encode(message)
      udp_socket.send(data)
  except Exception as err:
    transport.close_with_error(str(err))

def loop_write():
  try:
    while True:
      data, addr = udp_socket.recvfrom(1024)
      message = data.decode()
      transport.write(message)
  except Exception as err:
    transport.close_with_error(str(err))

udp_socket.connect((HOST, PORT))
with udp_socket:
  thread_read = threading.Thread(target=loop_read, daemon=True)
  thread_read.start()
  thread_write = threading.Thread(target=loop_write, daemon=True)
  thread_write.start()

  while True:
    reply = connection.generic_command("tools echo Hello", DEVICE_ADDRESS)
    print(reply.data)
    time.sleep(1)

The snippets below describe each part of the code in detail.

transport = Transport.open()
connection = Connection.open_custom(transport)

The first line creates the transport object. The second line creates a connection binding it to the transport. The resulting connection is no different than the one opened to e.g. serial port.

The transport instance provides access to the underlying connection. It allows reading messages generated by the library as your code uses the connection. Deliver the messages to the device by your method of choice (e.g. UDP).

The transport also allows writing replies to the underlying connection. Those are the messages generated by the device and transported by your method of choice (e.g. UDP).

udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

Creates an UDP socket with Python API.

def loop_read():
  try:
    while True:
      message = transport.read()
      data = str.encode(message)
      udp_socket.send(data)
  except Exception as err:
    transport.close_with_error(str(err))

The first loop function keeps reading messages from the transport and writes them to the socket.

Note the error handling. If the writing of the message to the socket fails, the transport is closed with the occurred error. Closing the transport will close the connection and end all the pending calls with the supplied error. This way, any error from your custom transport is propagated throughout the library to the code that is using the connection.

The read method of transport must be called in a loop. Each subsequent read confirms that the previous message was handled by your code. Failing to do so results in the calls to the connection being blocked. This behavior ensures proper flow control and error propagation mentioned above.

def loop_write():
  try:
    while True:
      data, addr = udp_socket.recvfrom(1024)
      message = data.decode()
      transport.write(message)
  except Exception as err:
    transport.close_with_error(str(err))

The second loop function keeps reading from the socket and writes the received messages to the transport. Note the error handling here as well. If there is an error while receiving from the socket, the transport and therefore underlying connection is closed the error. This occurs, for example, when the socket is closed.

udp_socket.connect((HOST, PORT))
with udp_socket:
  thread_read = threading.Thread(target=loop_read, daemon=True)
  thread_read.start()
  thread_write = threading.Thread(target=loop_write, daemon=True)
  thread_write.start()

This code connects the socket to the specified host and port and starts the threads with the loops. It's crucial the loops run in their own threads. Otherwise, the connection methods will block as no-one is reading from the transport.

while True:
  reply = connection.generic_command("tools echo Hello", DEVICE_ADDRESS)
  print(reply.data)
  time.sleep(1)

Finally, this code demonstrates a simple use of the connection. The generated messages will be passed to and receive from the UDP socket.

Please let us know if you run into any issues or need help using the custom transports.