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.
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 using C 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. An alternate way
of working with ADU devices in Linux is to use the
adutux kernel driver to access the device as a file
descriptor (outlined here:
https://www.ontrak.net/Linux/APG.htm).
All
source code is provided so that you may review details
that are not highlighted here.
Lets have a look at the code......
libusb is
a C library that provides generic access to USB devices.
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.
#include <libusb-1.0/libusb.h>
#define VENDOR_ID 0x0a07
#define PRODUCT_ID 208
Low-speed and full-speed devices have different transfer
sizes for the data sent between host (computer) and
device (ADU).
A low-speed device writes and reads 8 byte packets,
whereas a high-speed device's transfer size is 64 bytes.
Be sure to uncomment the one matching your connected
device. In Linux, 'dmesg' may be used to find the device
speed. Uncomment the appropriate speed based on your
connected device.
#define TRANSFER_SIZE 8
Next, let's declare our libusb USB device handle. This
handle will be used for all of our interactions with the
USB device via libusb (opening, closing, reading and
writing commands). We'll also initialize the libusb
library and check the returned result. For information
purposes, we will also set our debugging output to the
maximum level.
int main( int argc, char **argv )
{
struct libusb_device_handle * device_handle = NULL;
char value_str[8];
int result;
int result = libusb_init( NULL );
if ( result < 0 )
{
printf( "Error initializing libusb: %s\n", libusb_error_name( result ) );
exit( -1 );
}
libusb_set_option( NULL, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_WARNING );
If initialization of libusb was successful, we will now
attempt to open the connected USB device that matches
our vendor and product ID.
itialization of libusb was successful, we will now
attempt to open the connected USB device that matches
our vendor and product ID.
device_handle = libusb_open_device_with_vid_pid( NULL, VENDOR_ID, PRODUCT_ID );
if ( !device_handle )
{
printf( "Error finding USB device\n" );
libusb_exit( NULL );
exit( -2 );
}
We should also enable auto-detaching of the kernel
driver. If a kernel driver currently has an interface
claimed, it will be automatically be detached when we
claim that interface. When the interface is restored,
the kernel driver is allowed to be re-attached. This can
alternatively be manually done via
libusb_detach_kernel_driver().
libusb_set_auto_detach_kernel_driver( device_handle, 1 );
At this point we also need to claim the interface that
we wish to use. ADU devices have interface 0 defined
with an IN and OUT endpoint used for reading and
writing, respectively.
result = libusb_claim_interface( device_handle, 0 );
if ( result < 0 )
{
printf( "Error claiming interface: %s\n", libusb_error_name( result ) );
if ( device_handle )
{
libusb_close( device_handle );
}
libusb_exit( NULL );
exit( -3 );
}
Now that we have successfully opened our device and
specified the interface we wish to use, we can use
interrupt transfers to 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 won't be covering
these functions in detail, however comments in the
source code provide an explanation of what is occuring.
Feel free to modify these if required.
planation of what is occuring. Feel free to modify these
if required.
result = write_to_adu( device_handle, "RK0", 200 );
result = write_to_adu( device_handle, "SK0", 200 );
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 ADU208 is
RPK0. This requests the value of relay 0, which we
previously set with the RK0 and SK0 commands in the
above code block.
It's important to note that if a command that requests a
return value (such as RPK0) is sent to an ADU device,
the return value must be read from the device by calling
read_from_adu before issuing any other commands that
request return values. Failing to do so could cause the
OS to disconnect the device and require it to be reset.
result = write_to_adu( device_handle, "RPK0", 200 );
int value = 0;
result = read_from_adu( device_handle, &value, 200 );
if ( result == 0 )
{
printf( "Read value: %i\n", value );
}
Since we are finished with the device, we should release
the interface and close the device. If any kernel driver
was previously attached to interface 0, it will now be
reattached due to calling
libusb_set_auto_detach_kernel_driver earlier in our
code.
libusb_release_interface( device_handle, 0 );
libusb_close( device_handle );
libusb_exit( NULL );
return 0;
}
Further Details
When newly connecting to a device, we should also read
until there is nothing left to read from the device.
This is to clear any pending reads that was not
initiated by us:
while ( 0 == read_from_adu( device_handle, value, 200 ) )
{
}
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 do this via a memset.
We use the OUT endpoint to send data from the host to
the device. Endpoint address 0x81 is standard for IN and
0x01 is standard for OUT.
We'll use libusb_interrupt_transfer to write out command
to the device on ENDPOINT_OUT (0x01). Interrupt
transfers are a transfer type used for sending
infrequent and small amounts of data. Compared to
streaming video from a webcam, the ADU devices send
small amounts of data. After sending, we check to result
to make sure the transfer succeeded. The number of bytes
sent should match our command buffer's size (8 bytes
since we are using a low-speed device, and 64 bytes if a
high speed device).
int write_to_adu( libusb_device_handle * _device_handle, const char * _cmd, int _timeout )
{
const int command_len = strlen( _cmd );
int bytes_sent = 0;
unsigned char buffer[ TRANSFER_SIZE ];
if ( command_len > TRANSFER_SIZE )
{
printf( "Error: command is larger than our limit of %i\n", TRANSFER_SIZE );
return -1;
}
memset( buffer, 0, TRANSFER_SIZE );
buffer[0] = 0x01;
memcpy( &buffer[1], _cmd, command_len );
int result = libusb_interrupt_transfer( _device_handle, 0x01, buffer, TRANSFER_SIZE, &bytes_sent, _timeout );
printf( "Write '%s' result: %i, Bytes sent: %u\n", _cmd, result, bytes_sent );
if ( result < 0 )
{
printf( "Error sending interrupt transfer: %s\n", libusb_error_name( result ) );
}
return result;
}
If the interrupt transfer succeeds, we should now have a
result to read from the command we sent. We can use
libusb_interrupt_transfer with the ENDPOINT_IN endpoint
to read the value. The parameters are the same as for
sending, however after the call the specified buffer
will contain the data read from the device and
bytes_read will contain the number of bytes that were
read and stored in the buffer.
If reading from the device was successful, let's extract
the data we are interested in. It's important to note
that the data we are interested in is the first 8 bytes
of the data read from the device. The first byte 0x01
and is followed by an ASCII representation of the
number. The remainder of the bytes are padded with 0x00
(NULL) values.
int read_from_adu( libusb_device_handle * _device_handle, char * _read_str, int _read_str_len, int _timeout )
{
if ( _read_str == NULL || _read_str_len < 8 )
{
return -2;
}
int bytes_read = 0;
unsigned char buffer[ TRANSFER_SIZE ];
memset( buffer, 0, TRANSFER_SIZE );
int result = libusb_interrupt_transfer( _device_handle, 0x81, buffer, TRANSFER_SIZE, &bytes_read, _timeout );
printf( "Read result: %i, Bytes read: %u\n", result, bytes_read );
if ( result < 0 )
{
printf( "Error reading interrupt transfer: %s\n", libusb_error_name( result ) );
return result;
}
memcpy( _read_str, &buffer[1], 7 );
buffer[7] = '\0';
return result;
}
This example illustrates the basics of reading and
writing to ADU devices using libusb, interrupt
transfers, and endpoints.
In a larger application, you may want to use libusb's
asynchronous interface as to not block the application.
Using this approach, function callbacks can be set to
notify you on completion of transfers. More information
can be found here:
http://libusb.sourceforge.net/api-1.0/libusb_io.html
NOTE: When
running the example, it must be run with root privileges
in order to access the USB device.
DOWNLOAD C libusb Example (Linux)
|