在Android上使用USB自定义HID

由Freepik设计
在用于与其他设备进行交互的现代Android应用程序中,最常用的是无线数据传输协议,例如蓝牙。 在某些设备具有无线充电功能的年代,很难想象一堆需要使用有线接口的Android设备和外围设备。 但是,当出现这种需求时,USB马上就浮现在脑海。

让我们假设一个案例。 想象有一个客户来找您说:“我需要一个Android应用程序来控制数据收集设备并在屏幕上显示这些数据。 但是只有一个-应用程序必须在具有Android操作系统的单板计算机上编写,并且外围设备通过USB连接

听起来很棒,但有时会发生。 在这里,深入了解USB堆栈及其协议非常有用,但本文并非如此。 在本文中,我们将研究如何使用Android设备上的USB Custom HID协议来控制外围设备。 为简单起见,我们将编写一个Android应用程序(HOST),该应用程序将控制外围设备(DEVICE)上的LED并接收按钮状态(按)。 我不会提供有兴趣的外围板的代码-在注释中写。

因此,让我们开始吧。

理论 越短越好


首先,有一点理论,尽可能简短。 这是一个简化的最低要求,足以理解代码,但是为了更好地理解,我建议您熟悉此资源

为了通过外围设备上的USB通信,必须实现一个交互接口。 各种功能(例如USB HID,USB Mass Strorage或USB CDC)将实现它们的接口,有些将具有多个接口。 每个接口都包含端点-特殊的通信通道,一种剪贴板。

我的外围设备有一个带有一个接口和两个端点的自定义HID,一个端点用于接收,另一个端点用于发送。 通常,将具有设备上存在的接口和端点的信息写入设备的规范中,否则可以通过特殊程序(例如USBlyzer)确定它们。

USB HID中的设备通过报告进行通信。 什么是报告? 由于数据是通过端点传输的,因此我们需要按照协议以某种方式标识和解析它。 设备不仅互相扔数据字节,而且交换具有明确定义结构的数据包,该结构在设备上的特殊报告描述符中进行了描述。 因此,根据报告的描述符,我们可以准确确定传输某些数据的标识符,结构,大小和频率。 数据包由第一个字节标识,即报告ID。 例如,按钮的状态进入ID = 1的报告,我们通过ID = 2的报告控制LED。

远离铁,更接近Android


在Android中,从API版本12(Android 3.1)开始出现了对USB设备的支持。要与外围设备一起使用,我们需要实现USB主机模式。 文档中对USB的使用进行了很好的描述。

首先,您需要在所有USB设备中识别您连接的设备。 USB设备通过vid(供应商ID)和pid(产品ID)的组合来标识。 在xml文件夹中,创建具有以下内容的device_filter.xml文件:

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

现在,您需要在应用程序清单中进行适当的权限和操作(如果需要):

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

在android:资源中,我们指定了一个带有设备必需过滤器的文件。 另外,正如我之前所说,您可以分配意图过滤器以启动应用程序,例如,作为连接设备的结果。

首先,您需要获取UsbManager,找到设备,设备的接口和端点。 每次连接设备时都必须这样做。

 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 until 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 } 

在这里,我们看到了上一节中讨论的接口和端点。 知道了接口号后,我们找到了两个端点以进行接收和传输,并启动了USB连接。 就是这样,现在您可以读取数据了。

如前所述,设备通过报告进行通信。

  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 } 

我们将字节数组发送到sendReport方法,其中零字节是报告ID,我们将当前USB连接到设备并执行传输。 作为参数,我们将端点号,数据,它们的大小和传输超时传递给BulkTransfer方法。 值得注意的是,UsbDeviceConnection类具有用于实现与USB设备进行数据交换的方法-bulkTransfer和controlTransfer方法。 它们的使用取决于端点支持的传输类型。 在这种情况下,尽管HID最典型地使用具有控件类型的端点,但我们使用bulkTransfer。 但是我们有自定义HID,因此我们可以做我们想要的。 我建议您分别阅读传输类型,因为传输数据的数量和频率取决于传输数据。

要接收数据,您需要知道可以获取的数据大小,如何事先知道以及如何从端点获取数据。

通过USB HID接收数据的方法是同步且阻塞的,并且必须在不同的线程中执行,此外,可以连续或随时接收来自设备的报告,因此,有必要对报告进行恒定轮询,以免丢失数据。 让我们用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 */ }) } 

收到字节数组后,我们必须检查零字节,因为它是报告ID,并据此解析收到的数据。

使用USB完成所有操作后,您需要关闭连接。 您可以在onDestroy活动或ViewModel的onCleared中执行此操作。

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

结论


本文讨论了一种非常小而原始的,极具说明性的代码,以及针对特定设备的实现。 当然,不仅有HID,还有许多USB类,对于它们来说,实现方式当然会有所不同。 但是,所有方法都有很好的文档记录,并且对USB堆栈有很好的了解,您可以轻松地弄清楚如何使用它们。

十。有用的材料


Source: https://habr.com/ru/post/zh-CN470365/


All Articles