Using C and the HIDAPI library  with ADU USB Data Acquisition Products (Linux & OS X)

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.

The sample source code outlines the basics of communicating directly with an ADU device on Linux and OS X using C and libhidapi. 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). The preferred method of interfacing with ADU devices on Windows is via the DLL.

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

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


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

 

HIDAPI is a C library that provides simple access to HID compliant USB devices (https://github.com/libusb/hidapi). 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.

// libhidapi library must be available. 
// It can be installed on Debian/Ubuntu using apt-get install libhiapi-dev
#include "hidapi/hidapi.h"

#define VENDOR_ID      0x0a07  // Ontrak vendor ID. This should never change
#define PRODUCT_ID     200     // ADU200 product ID. Set this product ID to match your device.
                               // Product IDs can be found at https://www.ontrak.net/Nodll.htm.

#define COMMAND_SIZE   8       // ADU commands are 8 bytes in length

Next, let's declare our USB device handle. This handle will be used for all of our interactions with the USB device via HIDAPI (opening, closing, reading and writing commands). We'll also initialize the HIDAPI library and check the returned result.


int main( int argc, char **argv )
{
	hid_device * handle;
	char value_str[8]; // 8-byte buffer to store string values read from device 
//(7 byte string + NULL terminating character)
	int result; // HIDAPI function call result

	// Initialize the HIDAPI library
	result = hid_init();

If initialization of HIDAPI was successful, we will now attempt to open the connected USB device that matches our vendor and product ID.


	// Open the device using the VID, PID,
	// and optionally the Serial number.
	handle = hid_open( VENDOR_ID, PRODUCT_ID, NULL );

	if ( handle == NULL )
	{
		wprintf( L"Unable to open ADU device with product ID %i. 
Please check that it is connected. If it is connected, ensure you are running the example
with root privileges such as with sudo.\n", PRODUCT_ID );
		result = hid_exit();
		exit( -1 );
	}

Now that we have successfully opened our device, 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_to_adu() return the number of bytes written, or -1 on failure
	result = write_to_adu( handle, "RK0" ); // reset relay 0
    	result = write_to_adu( handle, "SK0" ); // close 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 RPA. This requests the value of PORT A in binary.

read_from_adu() takes four parameters, the device object, a destination buffer to store the string result, the length of this buffer (must be 8 bytes or larger), and a timeout in milliseconds.


	// Send a command to request the value PORT A. We'll need to read the result using read_from_adu() 
	result = write_to_adu( handle, "RPA" );

	// Read the result into our 8 byte value_str buffer that we declared at the beginning of main()
	if ( -1 != read_from_adu( handle, value_str, 8, 200 ) )
	{
		wprintf( L"Read value as string: %s\n", value_str );

// The data we are interested in follows in ASCII form. We will need to convert the ASCII data to an integer
// in order for it to be usable to us in a numeric format. The buffer is null terminated, and as such atoi()
// can be used for this.

// Convert the ASCII string result stored in the buffer's 2nd index and onward from a string to an integer
// and store it in the read_value pointer user argument
		int value_int = atoi( value_str );
		wprintf( L"Read value as int: %i\n", value_int );
	}

Since we are finished with the device, we should close the device and shutdown the HIDAPI library.

	// Close the device
	hid_close( handle );

	// Finalize the HIDAPI library
	result = hid_exit();

    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( 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'll use hid_write() to write our command to the device. 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)


// Write a command to an ADU device by creating a command buffer based on the provided command string
// Returns -1 on failure, number of bytes written on success
int write_to_adu( hid_device * _handle, const char * _cmd )
{
	const int command_len = strlen( _cmd ); // Get the length of the command string we are sending

	// Buffer to hold the command we will send to the ADU device.
	unsigned char buffer[ COMMAND_SIZE ];
	int bytes_sent;

	if ( command_len > COMMAND_SIZE - 1 )
	{
		printf( "Error: command is larger than our limit of %i\n", COMMAND_SIZE - 1 );
		return -1;
	}

    // 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

	memset( buffer, 0, COMMAND_SIZE ); // Zero out buffer to pad with null values (command buffer needs to be padded with 0s)

	buffer[ 0 ] = 0x01; // First byte of the command buffer needs to be set to a decimal value of 1

	// Copy the command ASCII bytes into our buffer, starting at the second byte (we need to leave the first byte as decimal value 1)
	memcpy( &buffer[ 1 ], _cmd, command_len );

	// Attempt to write the command to the device
	bytes_sent = hid_write( _handle, buffer, COMMAND_SIZE );

	if ( bytes_sent == -1 ) // Was the write successful?
	{
		wprintf( L"Error writing to device: %s\n", hid_error( _handle ) );
	}

	return bytes_sent;
}

If the interrupt transfer succeeds, we should now have a result to read from the command we sent. We can use hid_read_timeout() to read the value. The parameters are the same as for sending, plus a timeout value. The buffer in this case 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 returned is 8 bytes in length. 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.

NULL) values.


// Read a command from an ADU device with a specified timeout
// It is expected that _read_str is of size 8 bytes (or greater)
int read_from_adu( hid_device * _handle, char * _read_str, int _read_str_len, int _timeout )
{
	if ( _read_str == NULL || _read_str_len < 8 )
	{
		return -1;
	}

	int bytes_read;

	// Buffer to hold the command we will receive from the ADU device
	// Its size is set to the transfer size for low or full speed USB devices (ADU model specific - see defines at top of file)
	unsigned char buffer[ COMMAND_SIZE ];
	memset( buffer, 0, COMMAND_SIZE ); // Zero out buffer to pad with null values (command buffer needs to be padded with 0s)

	// Attempt to read the result from the device
	bytes_read = hid_read_timeout( _handle, buffer, COMMAND_SIZE, _timeout );

	if ( bytes_read == -1 ) // Was the interrupt transfer successful?
	{
		wprintf( L"Error reading interrupt transfer: %s\n", hid_error( _handle ) );
	}

	// The buffer should now hold the data read from the ADU device. The first byte will contain 0x01, the remaining bytes
	// are the returned value in string format. Let's copy the string from the read buffer, starting at index 1, to our _read_str buffer 
	_read_str[7] = '\0';
	memcpy( _read_str, &buffer[1], 7 );

	return bytes_read; // returns 0 on success, a negative number specifying the libusb error otherwise
}

 


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

NOTE: When running the example, it must be run with root privileges in order to access the USB device. To compile the makefile for Linux or OS X use: make -f Makefile-linux or make -f Makefile-osx.

DOWNLOAD C HIDAPI Example (Linux and OS X)