April 18, 2024

Raspberry Pi, C and custom HID Devices

So I’m writing a program in C that needs to interact with a custom HID device I built. This program will be running on a Raspberry Pi. This isn’t a massively complicated task but it can be daunting when there’s not a single “barebone” example or tutorial out there on how to do this. So I decided to write this sort of guide in case it may come in handy for anyone (including myself, in a future).

Compiling libhid

Libhid is an open source library designed on top of libusb to deal with HID devices, so the first step is compiling libhid. I’d say this is relatively straight-forward except for the fact that “as-is”, the library fails to build in the Pi. Luckily the problem is a single line of code in one of the examples (yes, and that prevents the whole library from being compiled and installed).

Matthew McMillan wrote on his blog how to solve this, based on solutions he found in the Raspberry Pi forums. The fix is basically editing the problematic code in the device_iterator() function inside src/test/lshid.c:

The following line

len = *((unsigned long*)custom);

needs to be changed to:

len = len;
custom = custom;

And that’s it.

Why is this working? Well, in the original code, len is assigned a value but never used in the function (and apparently this warning is promoted to error by the make script).

To avoid this problem we have to “use” the variable somehow in the code, so we fix it with a dummy “len = len” statement. Since this line is no longer using the “custom” variable, we need to do the same with it, so it won’t raise the same “variable not used” error that len was triggering.

With this you can run ./configure, make and make install as you normally would and compile libhid without problems.

Using libhid

To be honest libhid is really easy to use despite its flaws, but the almost non-existent documentation (a doxygen API listing and a couple of example files) makes it kinda hard to get started with it.

But don’t worry, as I said before it’s really easy to use.

The first thing we want to do is opening our HID device (provided you have its vendor ID and Product ID). Here’s a handy function for that:

#include <hid.h>

HIDInterface *openUsbDevice(int vid, int pid){
  HIDInterface *hidDev;
  HIDInterfaceMatcher hidDevSpec;

  // This will be used to find the device. You can optionally specify
  // a custom matcher function to check other properties of the USB
  // device before asserting a match, but since VID and PID suffice to
  // find our device we don't need that.
  hidDevSpec.vendor_id = vid;
  hidDevSpec.product_id = pid;
  hidDevSpec.matcher_fn = NULL;
  hidDevSpec.custom_data = NULL;
  hidDevSpec.custom_data_length = 0;

  // init the library and create the interface
  if (!hid_is_initialised() && hid_init() != HID_RET_SUCCESS) {
    return NULL;
  }
  hidDev = hid_new_HIDInterface();


  // Open the device. Since it's most-likely already opened by the
  // kernel modules we need to close it first. hid_force_open does
  // exactly that. The "4" here is the number of times it will try to
  // close and gain control over the device before giving up.
  if (hid_force_open (hidDev, 0, &hidDevSpec, 4) != HID_RET_SUCCESS) {
    return NULL;
  }

  return hidDev;
}

If you try this code in your Pi it will probably compile just fine (you may need to add -l hid when compiling to link the libhid library to your code), but it will fail to open the device.

Why? because you probably don’t have enough privileges to access usb devices in your Pi.

Fixing usb permissions

This can be fixed by adding a udev rule that will “mount” USB devices with enough permissions for you to actually do something with it. I saw this “fix” detailed on a forum, but I couldn’t find the link now, so I’ll explain the process here the best I can.

You’ll need to create a /etc/udev/rules.d/50-usbdevices.rules file with the following:

SUBSYSTEM=="usb", ACTION=="add", MODE="0664", GROUP="plugdev"

This will give read-write permissions over usb devices to anyone in the plugdev group. The user “pi” is already member of this group.

You can also specify a particular vendor and product id so it will only run this rule for that device:

SUBSYSTEM=="usb", ATTRS{idVendor}=="1000", ATTRS{idProduct}=="2000", ACTION=="add", MODE="0664", GROUP="plugdev"

For the changes to have effect you can reboot your Pi or you can run the following commands instead:

udevadm control --reload-rules
udevadm trigger

(unplug your device and plug it again)

Now you should be able to open the device with no problem.

Back to libhid…

Finally, we want to read and write data to it. This step will be a little different depending on the USB device you are trying to talk to. In my case, it’s a simple HID device that takes a 64-byte input buffer containing a command with parameters, performs and action depending on the command, and returns the result, in another 64-byte buffer.

We need to find the endpoint addresses. To do that, we use lsusb. Assuming our device’s VID is 0x1000 and its PID is 0x2000 the command to dump info about the device will be lsusb -d 1000:2000 -vvv.

We are only interested in the report description. For my device I get this:

Report Descriptor: (length is 29)
  Item(Global): Usage Page, data= [ 0x00 0xff ] 65280
          (null)
  Item(Local ): Usage, data= [ 0x01 ] 1
          (null)
  Item(Main  ): Collection, data= [ 0x01 ] 1
          Application
  Item(Local ): Usage Minimum, data= [ 0x01 ] 1
          (null)
  Item(Local ): Usage Maximum, data= [ 0x40 ] 64
          (null)
  Item(Global): Logical Minimum, data= [ 0x00 ] 0
  Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255
  Item(Global): Report Size, data= [ 0x08 ] 8
  Item(Global): Report Count, data= [ 0x40 ] 64
  Item(Main  ): Input, data= [ 0x00 ] 0
          Data Array Absolute No_Wrap Linear
          Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Local ): Usage Minimum, data= [ 0x01 ] 1
          (null)
  Item(Local ): Usage Maximum, data= [ 0x40 ] 64
          (null)
  Item(Main  ): Output, data= [ 0x00 ] 0
          Data Array Absolute No_Wrap Linear
          Preferred_State No_Null_Position Non_Volatile Bitfield
  Item(Main  ): End Collection, data=none
Endpoint Descriptor:
bLength                 7
bDescriptorType         5
bEndpointAddress     0x81  EP 1 IN
bmAttributes            3
  Transfer Type            Interrupt
  Synch Type               None
  Usage Type               Data
wMaxPacketSize     0x0040  1x 64 bytes
bInterval               1
Endpoint Descriptor:
bLength                 7
bDescriptorType         5
bEndpointAddress     0x01  EP 1 OUT
bmAttributes            3
  Transfer Type            Interrupt
  Synch Type               None
  Usage Type               Data
wMaxPacketSize     0x0040  1x 64 bytes

As you can see, there’s a single report declared, with its usage page at  “0x00 0xff” (65280). From the decimal equivalent (in parentheses) we can guess that the address is being printed LSB first, so in  our code this would actually be 0xff00. This makes sense since the USB specs state that vendor specific usage pages should be in the FF00-FFFF range.

Then there’s a bunch of Item usage specs, setting max data sizes and range of values, but among the “Items” you’ll see one labeled “Input”, and one labeled “Output”, both being “arrays” of 64 bytes with range 0 to 255 (unsigned).

The report data size is 64, for both reading and writing, and in the Endpoint table at the bottom you can see that the “Input” buffer is at address 0x81 (offset from the page address) and the Output buffer is at 0x01.

With this information we can read and write to our device:

// Device config
#define HID_DEVICE_VID      0x1000
#define HID_DEVICE_PID      0x2000
#define HID_REPORT_ADDRESS  0xff00
#define HID_IN_ENDPOINT     (HID_REPORT_ADDRESS<<16) + 0x81
#define HID_OUT_ENDPOINT    (HID_REPORT_ADDRESS<<16) + 0x01

#define HID_DATA_SIZE       64

void testDevice(){
  unsigned char buffer[HID_DATA_SIZE];
  int r;

  HIDInterface *dev = openUsbDevice (HID_DEVICE_VID, HID_DEVICE_PID);
  if (!dev) {
    printf("Couldn't open device!\n");
    return;
  }

  // Set the bytes we want to send
  buffer[0] = 0x10;
  buffer[1] = 0x20;

  // The last parameter is a timeout, in milliseconds (in this case 1 sec)
  r = hid_interrupt_write(dev, HID_OUT_ENDPOINT, buffer, HID_DATA_SIZE, 1000);
  if (r == HID_RET_SUCCESS){
    // I'm reusing the same buffer for receiving data.
    r = hid_interrupt_read(dev, HID_IN_ENDPOINT, buffer, HID_DATA_SIZE, 1000);
    if (r != HID_RET_SUCCESS) {
      printf("Failed to read from device\n");
    } else {
      // Print the received bytes. Not very elegant but good enough
      // for testing.
      for (r = 0; r < sizeof(buffer); r++){
         printf("%02x ", buffer[r]);
      }
      printf("\n");
    }
  }else {
    printf("Failed to send data to USB device [%d]!\n", r);
  }
  hid_close (dev);
}

This is a very “raw” example but I hope it gives you an idea of how to use libhid, (particularly on a Raspberry Pi) to interact with custom HID devices.

 

Leave a Reply

Your email address will not be published. Required fields are marked *