Working with USB Custom HID on Android

Designed by Freepik

In modern Android applications for interacting with other devices, wireless data transfer protocols, such as Bluetooth, are most often used. In the years when some devices have wireless charging, it is difficult to imagine a bunch of Android devices and peripheral modules, which require the use of wired interfaces. However, when such a need arises, USB immediately comes to mind.



Let's take a hypothetical case with you. Imagine that a customer comes to you and says: “I need an Android application to control the data collection device and display this data on the screen. There is one BUT - the application must be written to a single-board computer with the Android operating system, and the peripheral device is connected via USB ”



It sounds fantastic, but it happens sometimes. And here, an in-depth knowledge of the USB stack and its protocols comes in handy, but this article is not about that. In this article, we will look at how to control a peripheral device using the USB Custom HID protocol from an Android device. For simplicity, we will write an Android application (HOST) that will control the LED on the peripheral device (DEVICE) and receive the state of the button (press). I will not give the code for the peripheral board, anyone who is interested - write in the comments.



So let's get started.



Theory. As short as possible



First, a little theory, as short as possible. This is a simplified minimum, sufficient to understand the code, but for a better understanding, I advise you to familiarize yourself with this resource .



To communicate via USB on a peripheral device, you must implement an interaction interface. Various functions (for example, USB HID, USB Mass Strorage or USB CDC) will implement their interfaces, and some will have several interfaces. Each interface contains endpoints - special communication channels, a kind of clipboard.



My peripheral has a Custom HID with one interface and two endpoints, one for receiving and one for transmitting. Usually the information with the interfaces and endpoints existing on the device is written in the specification for the device, otherwise they can be determined through special programs, for example USBlyzer.



Devices in USB HID communicate through reports. What are reports? Since the data is transmitted through the endpoints, we need to somehow identify and parse it in accordance with the protocol. Devices not only throw data bytes to each other, but exchange packets that have a clearly defined structure, which is described on the device in a special report descriptor. Thus, according to the descriptor of the report, we can accurately determine which identifier, structure, size and frequency of transmission of certain data. The packet is identified by the first byte, which is the report ID. For example, the data on the state of the button goes to the report with ID = 1, and we control the LED through the report with ID = 2.



Away from iron, closer to Android



In Android, support for USB devices appeared starting with API version 12 (Android 3.1). To work with a peripheral device, we need to implement USB host mode. Working with USB is pretty well described in the documentation.



First you need to identify your connected device, among the whole variety of USB devices. USB devices are identified by a combination of vid (vendor id) and pid (product id). In the xml folder, create the device_filter.xml file with the following contents:



<resources> <usb-device vendor-id="1155" product-id="22352" /> </resources>
      
      





Now you need to make the appropriate permissions and action (if you need them) in the application manifest:



 <uses-permission android:name="android.permission.USB_PERMISSION" /> <uses-feature android:name="android.hardware.usb.host" /> <activity android:name=".MainActivity"> <intent-ilter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" /> </activity>
      
      





In android: resource we specify a file with the necessary filters for devices. Also, as I said earlier, you can assign intent filters to launch the application, for example, as a result of connecting your device.



First you need to get the UsbManager, find the device, interface and endpoints of the device. This must be done each time the device is connected.



 val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager private var usbConnection: UsbDeviceConnection? = null private var usbInterface: UsbInterface? = null private var usbRequest: UsbRequest? = null private var usbInEndpoint: UsbEndpoint? = null private var usbOutEndpoint: UsbEndpoint? = null fun enumerate(): Boolean { val deviceList = usbManager.deviceList for (device in deviceList.values) { /*      VID  PID */ if ((device.vendorId == VENDOR_ID) and (device.productId == PRODUCT_ID)) { /*      */ usbInterface = device.getInterface(CUSTOM_HID_INTERFACE) /*            */ for (idx in 0..usbInterface!!.endpointCount) { if (usbInterface?.getEndpoint(idx)?.direction == USB_DIR_IN) usbInEndpoint = usbInterface?.getEndpoint(idx) else usbOutEndpoint = usbInterface?.getEndpoint(idx) } usbConnection = usbManager.openDevice(device) usbConnection?.claimInterface(usbInterface, true) usbRequest = UsbRequest() usbRequest?.initialize(usbConnection, usbInEndpoint) } } /*    */ return usbConnection != null }
      
      





Here we see the very interfaces and endpoints that were discussed in the last section. Knowing the interface number, we find both endpoints, to receive and transmit, and initiate a usb connection. That's all, now you can read the data.



As I said earlier, devices communicate through reports.



  fun sendReport(data: ByteArray) { usbConnection?.bulkTransfer(usbOutEndpoint, data, data.size, 0) } fun getReport(): ByteArray { val buffer = ByteBuffer.allocate(REPORT_SIZE) val report = ByteArray(buffer.remaining()) if (usbRequest.queue(buffer, REPORT_SIZE)) { usbConnection?.requestWait() buffer.rewind() buffer.get(report, 0, report.size) buffer.clear() } return report }
      
      





We send an array of bytes to the sendReport method, in which the zero byte is the report ID, we take the current USB connection to the device and perform the transfer. As parameters, we pass the endpoint number, data, their size and transmission timeout to the BulkTransfer method. It is worth noting that the UsbDeviceConnection class has methods for implementing data exchange with a USB device - bulkTransfer and controlTransfer methods. Their use depends on the type of transmission that an endpoint supports. In this case, we use bulkTransfer, although the HID is most often characterized by the use of endpoints of type control. But we have Custom HID, so we do what we want. I advise you to read about the type of transmission separately, since the volume and frequency of the transmitted data depends on it.



To obtain data, it is necessary to know the size of the data received, which can be known in advance or obtained from the endpoint.



The method of receiving data via USB HID is synchronous and blocking and must be performed in a different thread, in addition, reports from the device can be received constantly, or at any time, therefore, it is necessary to implement a constant poll of the report so as not to miss the data. Let's do it with RxJava:



 fun receive() { Observable.fromCallable<ByteArray> { getReport() } .subscribeOn(Schedulers.io()) .observeOn(Schedulers.computation()) .repeat() .subscribe({ /* check it[0] (this is report id) and handle data */ },{ /* handle exeption */ }) }
      
      





Having received an array of bytes, we must check the zero byte, since it is a report ID and, according to it, parse the received data.



Upon completion of all actions with USB, you need to close the connection. You can do this in onDestroy activity or in onCleared in ViewModel.



  fun close() { usbRequest?.close() usbConnection?.releaseInterface(usbInterface) usbConnection?.close() }
      
      





Conclusion



The article discusses a very small and primitive, extremely demonstrative code with implementation for a specific device. Of course, there are many USB classes, not only HID, and for them the implementation will naturally be different. However, all the methods are fairly well documented and having a good understanding of the USB stack, you can easily figure out how to apply them.



X. Useful materials






All Articles