larskanis/libusb

ERROR_IO when writing to device, but only sometimes?

xxx opened this issue · 7 comments

xxx commented

I'm working on a little driver to set key colors and actuation heights on a Realforce RGB keyboard, but am seeing inconsistent results, which I'm sure is something I'm missing, but I cannot figure out what it is.

Most of the time when I run the code below, it fails with LIBUSB::ERROR_IO, and does not make it far enough to be seen by Wireshark. I see the same behavior whether running as root or normal user, and with both Ruby 2.3.3 and 2.4.1.

It fails far more often than it works, but when it gets into a working state, it stays that way until I unplug and replug the device.

Is there anything obvious I'm missing here? It's clear from various searches that getting devices into the correct state for reading and writing is something that others struggle with as well.

system uname (Ubuntu 17.04):

Linux vatican 4.10.0-26-generic #30-Ubuntu SMP Tue Jun 27 09:30:12 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

the code:

require 'libusb'
require 'pry'

VENDOR_ID = 0x0853
PRODUCT_ID = 0x013a
#ENDPOINT = 0x04
data = "\xaa\xaa\x62\x00\x04\x00\xff\x00\x00".force_encoding(Encoding::BINARY)

usb = LIBUSB::Context.new
usb.debug = 10

devices = usb.devices(idVendor: VENDOR_ID, idProduct: PRODUCT_ID)
device = devices.first

raise 'No device found' unless device

begin
  endpoint_in = device.endpoints.find { |ep| !(ep.bEndpointAddress & LIBUSB::ENDPOINT_IN).zero? }
  endpoint_out = device.endpoints.find { |ep| (ep.bEndpointAddress & LIBUSB::ENDPOINT_IN).zero? }
  # binding.pry

  h = device.open
  h.auto_detach_kernel_driver = true
  h.claim_interface(0)
  h.clear_halt endpoint_in
  response = h.interrupt_transfer(
    endpoint: endpoint_out,
    dataOut: data,
    timeout: 10000
  )

  p response
ensure
  h.release_interface(0)
  h.close
end

When it fails:

libusb: debug [libusb_get_device_list]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_config_descriptor] index 0
libusb: debug [libusb_get_config_descriptor] index 0
libusb: debug [libusb_open] open 1.47
libusb: debug [usbi_add_pollfd] add fd 14 events 4
libusb: debug [libusb_claim_interface] interface 0
libusb: debug [libusb_clear_halt] endpoint 81
libusb: debug [libusb_alloc_transfer] transfer 0x55d35fc5d7e0
libusb: debug [libusb_submit_transfer] transfer 0x55d35fc5d7e0
libusb: debug [add_to_flying_list] arm timerfd for timeout in 10000ms (first in line)
libusb: debug [submit_bulk_transfer] need 1 urbs for new transfer with length 9
libusb: error [submit_bulk_transfer] submiturb failed error -1 errno=16
libusb: debug [submit_bulk_transfer] first URB failed, easy peasy
libusb: debug [disarm_timerfd]
libusb: debug [libusb_release_interface] interface 0
libusb: debug [libusb_close]
libusb: debug [usbi_remove_pollfd] remove fd 14
/home/mpd/.rvm/gems/ruby-2.3.3/gems/libusb-0.6.2-x86_64-linux/lib/libusb/constants.rb:62:in `raise_error': LIBUSB::ERROR_IO in libusb_submit_transfer (LIBUSB::ERROR_IO)
        from /home/mpd/.rvm/gems/ruby-2.3.3/gems/libusb-0.6.2-x86_64-linux/lib/libusb/transfer.rb:202:in `submit!'
        from /home/mpd/.rvm/gems/ruby-2.3.3/gems/libusb-0.6.2-x86_64-linux/lib/libusb/transfer.rb:229:in `submit_and_wait'
        from /home/mpd/.rvm/gems/ruby-2.3.3/gems/libusb-0.6.2-x86_64-linux/lib/libusb/dev_handle.rb:544:in `submit_transfer'
        from /home/mpd/.rvm/gems/ruby-2.3.3/gems/libusb-0.6.2-x86_64-linux/lib/libusb/dev_handle.rb:465:in `interrupt_transfer'
        from usb.rb:35:in `<main>'
libusb: debug [libusb_free_transfer] transfer 0x55d35fc5d7e0

When it works:

libusb: debug [libusb_get_device_list]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_device_descriptor]
libusb: debug [libusb_get_config_descriptor] index 0
libusb: debug [libusb_get_config_descriptor] index 0
libusb: debug [libusb_open] open 1.43
libusb: debug [usbi_add_pollfd] add fd 14 events 4
libusb: debug [libusb_claim_interface] interface 0
libusb: debug [libusb_clear_halt] endpoint 81
libusb: debug [libusb_alloc_transfer] transfer 0x563855794c20
libusb: debug [libusb_submit_transfer] transfer 0x563855794c20
libusb: debug [add_to_flying_list] arm timerfd for timeout in 10000ms (first in line)
libusb: debug [submit_bulk_transfer] need 1 urbs for new transfer with length 9
libusb: debug [libusb_handle_events_timeout_completed] doing our own event handling
libusb: debug [handle_events] poll fds modified, reallocating
libusb: debug [handle_events] poll() 3 fds with timeout in 60000ms
libusb: debug [handle_events] poll() returned 1
libusb: debug [reap_for_handle] urb type=1 status=0 transferred=9
libusb: debug [handle_bulk_completion] handling completion status 0 of bulk urb 1/1
libusb: debug [handle_bulk_completion] last URB in transfer --> complete!
libusb: debug [disarm_timerfd]
libusb: debug [usbi_handle_transfer_completion] transfer 0x563855794c20 has callback 0x7f3bdb7b5000
libusb: debug [libusb_release_interface] interface 0
libusb: debug [libusb_close]
libusb: debug [usbi_remove_pollfd] remove fd 14
libusb: debug [libusb_free_transfer] transfer 0x563855794c20

Full lsusb output for this device:

Bus 001 Device 016: ID 0853:013a Topre Corporation 
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.01
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0         8
  idVendor           0x0853 Topre Corporation
  idProduct          0x013a 
  bcdDevice            0.01
  iManufacturer           1 Topre
  iProduct                2 BackLightKB
  iSerial                 0 
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength           91
    bNumInterfaces          3
    bConfigurationValue     1
    iConfiguration          0 
    bmAttributes         0xa0
      (Bus Powered)
      Remote Wakeup
    MaxPower              500mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      1 Boot Interface Subclass
      bInterfaceProtocol      1 Keyboard
      iInterface              3 (error)
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.11
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength      65
         Report Descriptors: 
           ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0008  1x 8 bytes
        bInterval               1
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        1
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 No Subclass
      bInterfaceProtocol      0 None
      iInterface              4 (error)
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.11
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength     119
         Report Descriptors: 
           ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x82  EP 2 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0020  1x 32 bytes
        bInterval               1
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        2
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 No Subclass
      bInterfaceProtocol      0 None
      iInterface              0 
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.11
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength      34
         Report Descriptors: 
           ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x83  EP 3 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     0x04  EP 4 OUT
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               1
Device Status:     0x0000
  (Bus Powered)

Sorry I can not really help you. I didn't use a device with interrupt transfers so far (but I know others did). Recovering the device from failures is often an issue. Maybe you need some control transfer(s) to get the device into the required state - just guessing...

One small optimization: you can write endpoint_in = device.endpoints.find { |ep| ep.direction == :in }

xxx commented

Ok, thank you for responding. I'll close this, but will try to update with the solution if I find it for the sake of future searches.

xxx commented

It looks like I needed to explicitly set the configuration of the handle before claiming the interface. It throws an exception if it's already set, but I don't know how to test for it, so I'm using a rescue nil until I find out how. Seems to be possible in the C library but doesn't look like it's exposed here?

Anyway, working code is below:

require 'libusb'
require 'pry'

VENDOR_ID = 0x0853
PRODUCT_ID = 0x013a
ENDPOINT = 0x04

data = "\xaa\xaa\x62\x00\x04\x00\xff\x00\x00".force_encoding(Encoding::BINARY)

usb = LIBUSB::Context.new
usb.debug = 4

devices = usb.devices(idVendor: VENDOR_ID, idProduct: PRODUCT_ID)
device = devices.first

raise 'No device found' unless device

handle = nil
begin
  handle = device.open
  handle.auto_detach_kernel_driver = true
  handle.set_configuration(1) rescue nil
  handle.claim_interface(0)

  response = handle.interrupt_transfer(
    endpoint: ENDPOINT,
    dataOut: data,
    timeout: 10000
  )

  p response

ensure
  handle.release_interface(0)
  handle.close
end
xxx commented

I should also add that I had to do a little bit of work with udev config on ubuntu linux to get it working as a non-root user and stop usbhid from taking the device from me. I created /etc/udev/rules.d/99-topre.rules with the following content:

SUBSYSTEM=="usb", ATTRS{idVendor}=="0853", ATTRS{idProduct}=="013a", MODE="0666", GROUP="plugdev"
ATTRS{idVendor}=="0853", ATTRS{idProduct}=="013a", RUN="/bin/sh -c 'echo -n $kernel >/sys/bus/usb/drivers/usbhid/unbind'"  

You could try set_configuration(nil) to get an unconfigured state before.

xxx commented

I gave that a shot, but it just throws ERROR_BUSY