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
PRODUCT_ID = 200
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)
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.
bytes_written = write_to_adu(device, 'SK0')
bytes_written = write_to_adu(device, 'RK0')
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.
bytes_written = write_to_adu(device, 'RPA')
data = read_from_adu(device, 200)
if data != None:
print("Received string: {}".format(data))
print("Received data as int: {}".format(int(data)))
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))
byte_str = chr(0x01) + msg_str + chr(0) * max(7 - len(msg_str), 0)
num_bytes_written = 0
try:
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:
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:])
result_str = byte_str.split('\x00',1)[0]
if len(result_str) == 0:
return None
return result_str
|