Using Python and the libusb library with ADU USB Data Acquisition Products (Linux & Windows)

View the ADU series of USB based Data Acquisition Products


Introduction


Communicating with USB devices via software involves a few simple steps. Unlike RS232 based devices which are connected to physical COM ports, USB devices are assigned a logical handle by operating systems when they are first plugged in. This process is known as enumeration. Once a USB device has been enumerated, it is ready for use by the host computer software. For the host application software to communicate with the USB device, it must first obtain the handle assigned to the USB device during the enumeration process. The handle can be obtained using an open function along with some specific information about the USB device. Information that can be used to obtain a handle to a USB device include, serial number, product ID, or vendor ID.

Once we obtain a USB device handle, we can read and write information to and from the USB device via our application. Once the application has finished with all communication with the USB device, the handle is closed. The handle is generally closed when the application terminates.

USB devices have defined interfaces which relate to their functionality. For example, a USB keyboard with built in LEDs may have an interface for sending key presses and an interface for controlling the lights on the keyboard. Interfaces as defined as a set of endpoints. Endpoints are used as communication channels to and from the device and host and can either be IN or OUT. They are defined relative to the host - OUT endpoints transport data to the device (write) and IN endpoints transport data to the host (read).

Once we obtain a USB device handle, we must claim the interface we want to use. This will allow us to read and write information to and from the USB device via our application. Once the application has finished with all communication with the USB device, the handle is closed. The handle is generally closed when the application terminates.

The sample source code outlines the basics of communicating directly with an ADU device on Linux and Windows using Python and libusb. Basics of opening a USB device handle, writing and reading data, as well as closing the handle of the ADU usb device is provided as an example. The suggested way of working with ADU devices in Python and Linux is with the HIDAPI module (see: ). For working with Python and ADU devices in Windows, it's preferred to use the AduHid module (see: )

All source code is provided so that you may review details that are not highlighted here.

NOTE: See also Python and HIDAPI library with ADU Devices for alternate method of USB communication using HIDAPI


Lets have a look at the code......

This example illustrates the basics of reading and writing to ADU devices using the libusb library.

NOTE: When running the example, it must be run with root privileges in order to access the USB device.

libusb is a library that provides low level access to USB devices (https://pypi.org/project/libusb/). We will need a vendor ID and product ID in order to open the USB device. The VENDOR_ID define will always remain the same as this is OnTrak's USB vendor ID, however, PRODUCT_ID must be set to match the product that is connected via USB. See this link for a list of OnTrack product IDs: https://www.ontrak.net/Nodll.htm.

First we'll import the libusb library. If you haven't yet installed it, you may do so in the command line via pip install libusb or by installing via requirements.txt (pip install -r requirements.txt).

We'll declare OnTrak's vendor ID and the product ID for the ADU device we wish to use (in our case 200 for ADU200).


import usb.core
import usb.backend.libusb1

VENDOR_ID = 0x0a07 # OnTrak Control Systems Inc. vendor ID
PRODUCT_ID = 200 # ADU200 Device product name - change this to match your product

Next, we'll open the connected USB device that matches our vendor and product ID. This device will be used for all of our interactions with the ADU via libusb (opening, closing, reading and writing commands).


device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)

if device is None:
    raise ValueError('ADU Device not found. Please ensure it is connected to the tablet.')
    sys.exit(1)

# Claim interface 0 - this interface provides IN and OUT endpoints to write to and read from
usb.util.claim_interface(device, 0)

Now that we have successfully opened our device and claimed an interface, we can write commands to the ADU device and read the result.

Two convenience functions have been written to help properly format command packets to send to the ADU device, as well as to read a result from the ADU device: write_to_adu() and read_from_adu(). We'll cover the internals of these functions at the end of this page.


# Write commands to ADU
bytes_written = write_to_adu(device, 'SK0') # set relay 0
bytes_written = write_to_adu(device, 'RK0') # reset relay 0

In order to read from the ADU device, we can send a command that requests a return value (as defined in our product documentation). Such a command for the ADU200 is RPK0. This requests the value of relay 0, which we previously set with the RK0 and SK0 commands in the above code block.

We can then use read_from_adu() to read the result. read_from_adu() returns the data read string format on success, and None on failure. A timeout is supplied for the maximum amount of time that the host (computer) will wait for data from the read request.


# Read from the ADU
bytes_written = write_to_adu(device, 'RPA') # request the value of PORT A in binary 

data = read_from_adu(device, 200) # read from device with a 200 millisecond timeout

if data != None:
    print("Received string: {}".format(data))
    print("Received data as int: {}".format(int(data))) # the returned value is a string - we can convert it to a number (int) if we wish
When we are finished with the device, we should release any interfaces we have claimed (in our case interface 0) and then close the device. This is gennerally done when the application terminates.

usb.util.release_interface(device, 0)
device.close()

Further Details


If you're interested in the internals of write_to_adu() and read_from_adu() as well as how to structure a command packet, the details are below.

All ADU commands have their first byte set to 0x01 and the following bytes contain the ASCII representation of the command. The ADU command packet format is described here: https://www.ontrak.net/Nodll.htm. As described in the link, the remaining bytes in the command buffer must be null padded (0x00).

We'll use device.write() to write our command to the device.


def write_to_adu(dev, msg_str):
    print('Writing command: {}'.format(msg_str))

    # message structure:
    #   message is an ASCII string containing the command
    #   8 bytes in length
    #   0th byte must always be 0x01 (decimal 1)
    #   bytes 1 to 7 are ASCII character values representing the command
    #   remainder of message is padded to 8 bytes with character code 0

    byte_str = chr(0x01) + msg_str + chr(0) * max(7 - len(msg_str), 0)

    num_bytes_written = 0

	try:
		# 0x01 is the OUT endpoint
        num_bytes_written = dev.write(0x01, byte_str)
    except usb.core.USBError as e:
        print (e.args)

    return num_bytes_written

If the write is successful, we should now have a result to read from the command we previously sent. We can use read_from_adu() to read the value. The arguments are the USB device and a timeout. device.read() should return the data read from the device.

If reading from the device was successful, we will need to extract the data we are interested in. The first byte of the data returned is 0x01 and is followed by an ASCII representation of the number. The remainder of the bytes are padded with 0x00 (NULL) values. We can construct a string from the second byte to the end and strip out the null '\x00' characters.


def read_from_adu(dev, timeout):
    try:
		# try to read a maximum of 64 bytes from 0x81 (IN endpoint)
        data = dev.read(0x81, 64, timeout)
    except usb.core.USBError as e:
        print ("Error reading response: {}".format(e.args))
        return None

    byte_str = ''.join(chr(n) for n in data[1:]) # construct a string out of the read values, starting from the 2nd byte
    result_str = byte_str.split('\x00',1)[0] # remove the trailing null '\x00' characters

    if len(result_str) == 0:
        return None

    return result_str

DOWNLOAD Linux & Windows Python libusb Example ( ZIP 107K)