Using Android to Communicate with a USB HID Device
Asked Answered
D

4

19

I am new to USB and to Android so please forgive me if I don't explain myself clearly.

I have a USB HID device that I can communicate with in Windows. I am trying to establish communication using an Acer Iconia A500 tablet running Android 3.1.

I am able to find the device, enumerate it, get its only available interface, get the only available endpoint (0), and determine what type of endpoint it is (transfer interrupt from device to host).

My understanding of the USB spec is that all HID devices are required at a munimum to have a control endpoint (Endpoint 0) and an interrupt IN endpoint. But it seems that endpoint 0 here is the interrupt In endpoint, not the control endpoint.

Yet in order for the device to enumerate it must successfully transfer its descriptor data across the control endpoint. I deduce that the control endpoint therefore must be getting found (and used) because the host does, in fact, enumerate the device.

This is as far as I am able to proceed, as stated above, the only interface/endpoint presented to me at the application level is an interrupt type going from device to host. No endpoint available to my app going from host to device, interrupt or control. So the device waits to be told what to do and the host waits for something to happen in the device. Not very stimulating.

Bear in mind that this device responds properly when connected to Windows, e.g. I am able to send a report containing 13 bytes of data that causes the device to light an LED. So it seems to be complying with the USB HID spec. As an act of desperation I have tried using this one endpoint as both a control endpoint and as a interrupt OUT endpoint, using controltransfer() and UsbRequest() to submit the data to the device, no response in either case.

So my question is: "The control transfer endpoint is (?) being used to set up the device, why am I not able to find & use it?"

Thanks for any insight, below is the relevant code, I can include the rest in its entirety if needed:

private UsbManager mUsbManager;
private UsbDevice mDevice;
private UsbDeviceConnection mConnectionRead;
private UsbDeviceConnection mConnectionWrite;
private UsbEndpoint mEndpointRead;
private UsbEndpoint mEndpointWrite;

    // check for existing devices
    for (UsbDevice device :  mUsbManager.getDeviceList().values())
    {
        //Need to filter for my device when other HIDs are also connected, but for now...           
        String devName = device.getDeviceName();
        if (DEBUG == 1){
        Toast.makeText(UsbHidDeviceTesterActivity.this, "My device got connected: " + devName, Toast.LENGTH_LONG).show();
        }
        //mDevice = device;
        setHIDDevice(device);
    }

private boolean setHIDDevice(UsbDevice device)
{    
    UsbInterface usbInterfaceRead = null;
    UsbInterface usbInterfaceWrite = null;
    UsbEndpoint ep1 = null;
    UsbEndpoint ep2 = null;
    boolean UsingSingleInterface = true;

    mDevice = device;

    //This HID device is using a single interface
    if (UsingSingleInterface)
    {
        //usbInterfaceRead = device.getInterface(0x00);//only 1 EP on this interface
        usbInterfaceRead = findInterface(device);

        //Try getting an interface at next index
        //usbInterfaceWrite = device.getInterface(0x01);//throws exception

        // Try using the same interface for reading and writing
        usbInterfaceWrite = usbInterfaceRead;

        int endPointCount = usbInterfaceWrite.getEndpointCount();
        if (DEBUG == 2)
        {
            Toast.makeText(UsbHidDeviceTesterActivity.this, "Endpoints: " + endPointCount, Toast.LENGTH_LONG).show();
            //Toast.makeText(UsbHidDeviceTesterActivity.this, "Interface: " + usbInterfaceRead, Toast.LENGTH_LONG).show();
        }

        if (endPointCount == 1)//only getting 1 endpoint
        {
            ep1 = usbInterfaceRead.getEndpoint(0);
            //As an act of desperation try equating ep2 to this read EP, so that we can later attempt to write to it anyway
            ep2 = usbInterfaceRead.getEndpoint(0);
        }
        else if (endPointCount == 2)
        {
            ep1 = usbInterfaceRead.getEndpoint(0);
            ep2 = usbInterfaceRead.getEndpoint(1);
        }
    }

    else        // ! UsingSingleInterface
    {
        usbInterfaceRead = device.getInterface(0x00);
        usbInterfaceWrite = device.getInterface(0x01);
        if ((usbInterfaceRead.getEndpointCount() == 1) && (usbInterfaceWrite.getEndpointCount() == 1))
        {
            ep1 = usbInterfaceRead.getEndpoint(0);
            ep2 = usbInterfaceWrite.getEndpoint(0);
        }
        if (DEBUG == 3)
        {
            Toast.makeText(UsbHidDeviceTesterActivity.this, "Using Dual Interface", Toast.LENGTH_LONG).show();
        }
    }

    //because ep1 = ep2 this will now not cause a return unless no ep is found at all
    if ((ep1 == null) || (ep2 == null))
    {
        if (DEBUG == 4)
        {
            Toast.makeText(UsbHidDeviceTesterActivity.this, "One EP is null", Toast.LENGTH_LONG).show();
        }
        return false;
    }

    // Determine which endpoint is the read, and which is the write
    if (ep1.getType() == UsbConstants.USB_ENDPOINT_XFER_INT)//I am getting a return of 3, which is an interrupt transfer
    {
        if (ep1.getDirection() == UsbConstants.USB_DIR_IN)//I am getting a return of 128, which is a device-to-host endpoint
        {
            mEndpointRead = ep1;
            if (DEBUG == 5)
            {
                Toast.makeText(UsbHidDeviceTesterActivity.this, "EP1 type: " + ep1.getType(), Toast.LENGTH_LONG).show();
            }
        }
        if (ep1.getDirection() == UsbConstants.USB_DIR_OUT)//nope
        {
            mEndpointWrite = ep1;
            if (DEBUG == 6)
            {
                Toast.makeText(UsbHidDeviceTesterActivity.this, "EP1 is a write", Toast.LENGTH_LONG).show();
            }
        }
    }

    if (ep2.getType() == UsbConstants.USB_ENDPOINT_XFER_INT)
    {
        if (ep2.getDirection() == UsbConstants.USB_DIR_IN)
        {
            //Try treating it as a write anyway             
            //mEndpointRead = ep2;
            mEndpointWrite = ep2;
        }
        else if (ep2.getDirection() == UsbConstants.USB_DIR_OUT)
        {
            //usbEndpointWrite = ep2;
            mEndpointWrite = ep2;
        }
    }

    //check that we should be able to read and write
    if ((mEndpointRead == null) || (mEndpointWrite == null))
    {
        return false;
    }
    if (device != null)
    {
        UsbDeviceConnection connection = mUsbManager.openDevice(device);
        if (connection != null && connection.claimInterface(usbInterfaceRead, true))
        {
            Log.d(TAG, "open SUCCESS");
            mConnectionRead = connection;
            // Start the read thread
            //Comment out while desperately attempting to write on this connection/interface
            //Thread thread = new Thread(this);
            //thread.start();

        }
        else
        {
            Log.d(TAG, "open FAIL");
            mConnectionRead = null;
        }
     }
    if (UsingSingleInterface)
    {
        mConnectionWrite = mConnectionRead;
    }
    else //! UsingSingleInterface
    {
        mConnectionWrite = mUsbManager.openDevice(device);
        mConnectionWrite.claimInterface(usbInterfaceWrite, true);
    }
    return true;
}

// searches for an interface on the given USB device
 private UsbInterface findInterface(UsbDevice device) {
    Log.d(TAG, "findInterface " + device);
    int count = device.getInterfaceCount();
    if (DEBUG == 7)
    {
        Toast.makeText(UsbHidDeviceTesterActivity.this, "Interface count: " + count, Toast.LENGTH_LONG).show();
    }

    for (int i = 0; i < count; i++) {
        UsbInterface intf = device.getInterface(i);
        String InterfaceInfo = intf.toString();
        Log.d(TAG, "Interface: " + InterfaceInfo);
        //Class below is 3 for USB_HID
        if (intf.getInterfaceClass() == 3 && intf.getInterfaceSubclass() == 0 &&
                intf.getInterfaceProtocol() == 0) {
            return intf;
        }
        //....try just returning the interface regardless of class/subclass
        //return intf;
    }

    return null;
} 
 private boolean sendControlTransfer(byte[] dataToSend)
 {
    synchronized (this)
    { 
    if (mConnectionRead != null)
     { 
        //byte[] message = new byte[13];  // or 14?
        byte[] message = dataToSend;
         if (DEBUG == 9)
         {
             Toast.makeText(UsbHidDeviceTesterActivity.this, "Sending Control Transfer", Toast.LENGTH_LONG).show();
         } 

         //first field ox21 is bin 00100001 which splits into 0 01 00001 for direction(1bit)/type(2b)/recipient(5b)
         //To set direction as 'host to Device' we need 0, To set type to HID we need 11 (3), and for recipient we want 00001
         //second field 0x09 is class specific request code, 0x09 is listed as 'reserved for future use'
         //third field 0x200 is value
         //int transfer = mConnectionRead.controlTransfer(0x21, 0x9, 0x200, 0, message, message.length, 0);
         //try with type set to HID
         int transfer = mConnectionRead.controlTransfer(0xC1, 0x9, 0x200, 0, message, message.length, 0);
         if (DEBUG == 10)
         {
             Toast.makeText(UsbHidDeviceTesterActivity.this, "Transfer returned " + transfer, Toast.LENGTH_LONG).show();
         }
     } 
    }
    return true;
 }


private boolean sendInterruptTransfer(byte[] dataToSend)
{ 
    int bufferDataLength = mEndpointWrite.getMaxPacketSize();//The write endpoint is null unless we just copy the read endpoint
    if (DEBUG == 12)
    {
        Toast.makeText(UsbHidDeviceTesterActivity.this, "Max Packet Size: " + bufferDataLength, Toast.LENGTH_LONG).show();
    }

    ByteBuffer buffer = ByteBuffer.allocate(bufferDataLength + 1);
    UsbRequest request = new UsbRequest();
    buffer.put(dataToSend);

    request.initialize(mConnectionWrite, mEndpointWrite);
    request.queue(buffer, bufferDataLength);

    try
    {
        /* only use requestwait on a read
        if (request.equals(mConnectionWrite.requestWait()))
        {
            return true;
        }
        */
    }
    catch (Exception ex)
    {
        // An exception has occurred
        if (DEBUG == 13)
        {
            Toast.makeText(UsbHidDeviceTesterActivity.this, "Caught Write Exception", Toast.LENGTH_LONG).show();
        }
    }

    return true;
}   
Darsey answered 8/11, 2012 at 0:19 Comment(0)
K
11

So, I have been researching similar things. I cannot confirm, but what I believe is happening is:

  1. Android does not list the control endpoint when it enumerates it's endpoints. It only lists other endpoints.
  2. A connection to any endpoint can send control transfers to endpoint 0, through the controlTransfer method, which (quoting from the api) "Performs a control transaction on endpoint zero for this device."
  3. So, in your above code, I would use the 0th endpoint as an interrupt input endpoint, but it will still allow for control transfers.
  4. An example of someone using a HID device is the Missle Launcher demo, the device it uses is a HID device with an interrupt endpoint.
Kindred answered 5/1, 2013 at 2:27 Comment(1)
@zabuni- you are exactly correct, even though the single endpoint (EP0) was being identified as a interrupt endpoint, if I just treated is as a write anyway and performed a control transfer on it then it worked.Darsey
J
10

You can get a full list of the details of interfaces and endpoint by using the following:

UsbManager mManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = mManager.getDeviceList();
Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();

while (deviceIterator.hasNext())
    {
        UsbDevice device = deviceIterator.next();
        Log.i(TAG,"Model: " + device.getDeviceName());
        Log.i(TAG,"ID: " + device.getDeviceId());
        Log.i(TAG,"Class: " + device.getDeviceClass());
        Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
        Log.i(TAG,"Vendor ID " + device.getVendorId());
        Log.i(TAG,"Product ID: " + device.getProductId());
        Log.i(TAG,"Interface count: " + device.getInterfaceCount());
        Log.i(TAG,"---------------------------------------");
   // Get interface details
        for (int index = 0; index < device.getInterfaceCount(); index++)
        {
        UsbInterface mUsbInterface = device.getInterface(index);
        Log.i(TAG,"  *****     *****");
        Log.i(TAG,"  Interface index: " + index);
        Log.i(TAG,"  Interface ID: " + mUsbInterface.getId());
        Log.i(TAG,"  Inteface class: " + mUsbInterface.getInterfaceClass());
        Log.i(TAG,"  Interface protocol: " + mUsbInterface.getInterfaceProtocol());
        Log.i(TAG,"  Endpoint count: " + mUsbInterface.getEndpointCount());
    // Get endpoint details 
            for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
        {
            UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
            Log.i(TAG,"    ++++   ++++   ++++");
            Log.i(TAG,"    Endpoint index: " + epi);
            Log.i(TAG,"    Attributes: " + mEndpoint.getAttributes());
            Log.i(TAG,"    Direction: " + mEndpoint.getDirection());
            Log.i(TAG,"    Number: " + mEndpoint.getEndpointNumber());
            Log.i(TAG,"    Interval: " + mEndpoint.getInterval());
            Log.i(TAG,"    Packet size: " + mEndpoint.getMaxPacketSize());
            Log.i(TAG,"    Type: " + mEndpoint.getType());
        }
        }
    }
    Log.i(TAG," No more devices connected.");
}
Jammie answered 10/11, 2012 at 23:26 Comment(4)
@user1815293- Thanks for the reply. Your code is a much cleaner version of the DEBUGs that I had scattered throughout my code. However, when I run your code it confirms what I already had found: that there is only 1 interface with a single endpoint, and that endpoint is an interrupt type that runs in the direction from device to host. So in summary: In Android I am not given an endpoint on which to send a message to the device. Yet I know that the endpoint must exist because it is available and working in Windows. Therefore Android must not be properly supporting USB HIDDarsey
Whether it is a control endpoint or an interrupt endpoint that is not being made available is unclear. Any other insight into this problem would be greatly appreciated, even if it just to corroborate or contradict my results.Darsey
How do you get permissions to perform this function? I have a device plugged in, and the list is empty for me.Kellar
You need to add this to the AndroidManifest.xml file: <uses-feature android:name="android.hardware.usb.accessory" />Darsey
G
6

Control transfer doesn't show any interface descriptor and its endpoint number is 0 by default, for both in and out transfer.

if you have other interfaces the index of those interfaces should start from 0 i.e. default control transfer interface does not count.

So your interface 0 holds the endpoint 1 descriptor. use the UsbEndpoint methods to find the attributes of the endpoint whether it is interrupt type or not. if it is then endpoint type by UsbEndpoint.getType() should return 0x03 and endpoint number by UsbEndpoint.getEndpointNumber() should return 0x81 which is usual value for endpoint 1.

below your code is wrong:

//first field ox21 is bin 00100001 which splits into 0 01 00001 for direction(1bit)/type(2b)/recipient(5b)
     //To set direction as 'host to Device' we need 0, **To set type to HID we need 11 (3)**, and for recipient we want 00001
     //second field 0x09 is class specific request code, **0x09 is listed as 'reserved for future use'**
     //**third field 0x200 is value**
     //int transfer = mConnectionRead.controlTransfer(0x21, 0x9, 0x200, 0, message, message.length, 0);
     //try with type set to HID
     int transfer = mConnectionRead.controlTransfer(0xC1, 0x9, 0x200, 0, message, message.length, 0);

Type 2 bits is used to indicate class specific request, i.e. its value is 01, 0x09 is Hid class specific request SET_REPORT, not reserved. value is the wValue which is used as Report ID for Hid class, for your case it is probably 0, if you have only one report at you HID descriptor. and the 4 th parameter is wIndex which should be used to indicate the recipient, for your case it should be 0x01 for interface as recipient.

So your code for control transfer for Read or receive data form device should be:

int transfer = mConnectionRead.controlTransfer(0xA1, 0x01, 0x00, 0x01, message, message.length, 0);

where 0x01 in second parameter is GET_REPORT is Hid calls specific request.

And your code for control transfer for Write or send data to device should be:

int transfer = mConnectionWrite.controlTransfer(0x21, 0x09, 0x00, 0x01, message, message.length, 0);

Since you only have Interrupt IN endpoint 1, Bulk or Interrupt transfer should be like:

int transfer = bulkTransfer (ep1, message, message.length, 0);

to have the interrupt Out endpoint there should be a endpoint descriptor for that at the interface descriptor of firmware of your device.

Godding answered 25/8, 2013 at 17:29 Comment(1)
Great rundown. Thank you for this!Earthnut
O
1

Maybe it is a late answer or off topic. However, I hope someone will one day find this answer useful.

Github now contains great Android libraries to communicate with custom HID devices:

The good thing is that if you are lucky, then you only need to know the device VID:PID and the commands it accepts. You don't need to worry about USB protocol or communication details.

If interested about how USB works, then you can have a closer look at library source code.

Oporto answered 11/1, 2023 at 10:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.