Pull to refresh

Работаем с USB Custom HID на Android

Reading time 6 min
Views 13K
Designed by Freepik
В современных Android — приложениях для взаимодействия с другими устройствами чаще всего используются беспроводные протоколы передачи данных, как например Bluetooth. В годы, когда часть устройств имеют беспроводную зарядку, сложно представить себе связку Android устройства и периферийного модуля, в которой необходимо использование проводных интерфейсов. Однако когда такая необходимость возникает, на ум сразу же приходит USB.

Давайте разберем с вами гипотетический кейс. Представьте, что к вам приходит заказчик и говорит: “Мне нужно Android приложение для управления устройством сбора данных и вывода этих самых данных на экран. Есть одно НО — приложение надо написать на одноплатный компьютер с операционной системой Android, а периферийное устройство подключено по USB”

Звучит фантастически, но и такое иногда случается. И тут как нельзя кстати пригодится глубокое знание USB стека и его протоколов, но данная статья не об этом. В данной статье мы рассмотрим, как управлять периферийным устройством по протоколу USB Custom HID с Android устройства. Для простоты напишем Android-приложение (HOST), которое будет управлять светодиодом на периферийным устройством (DEVICE) и получать состояние кнопки (нажатия). Код для периферийной платы приводить не буду, кому интересно — пишите в комментариях.

Итак, приступим.

Теория. Максимально коротко


Для начала немного теории, максимально коротко. Это упрощенный минимум, достаточный для понимания кода, но для большего понимания советую ознакомиться с этим ресурсом.

Для общения по USB на периферийном устройстве необходимо реализовать интерфейс взаимодействия. Разные функции (например, USB HID, USB Mass Strorage или USB CDC) будут реализовывать свои интерфейсы, а некоторые будут иметь несколько интерфейсов. Каждый интерфейс содержит в себе конечные точки — специальные каналы связи, своего рода буферы обмена.

На моем периферийном устройстве реализован Custom HID с одним интерфейсом и с двумя конечными точками, одной для приёма, другой для передачи. Обычно информация с существующими на устройстве интерфейсами и конечными точками написана в спецификации на устройство, в противном случае определить их можно через специальные программы, к примеру USBlyzer.

Устройства в USB HID общаются через репорты. Что такое репорты? Так как данные передаются через конечные точки, то нам надо как-то идентифицировать, а также распарсить в соответствие с протоколом. Устройства не просто кидают друг другу байты данных, а обмениваются пакетами, имеющими четко определенную структуру, которая описывается на устройстве в специальном дескрипторе репорта. Таким образом, по дескриптору репорта, мы можем точно определить, какой идентификатор, структуру, размер и частоту передачи имеют те или иные данные. Идентификация пакета происходит по первому байту, который представляет из себя ID репорта. Например данные о состоянии кнопки, идут в репорта с ID = 1, а светодиодом мы управляем через репорт с ID = 2.

Подальше от железа, поближе к Android


В Android поддержка USB устройств появилась начиная с API версии 12 (Android 3.1) Для работы с периферийным устройством нам необходимо реализовать режим USB host. Работа с USB достаточно неплохо описана в документации.

Для начала необходимо идентифицировать ваше подключаемое устройство, среди всего разнообразия USB девайсов. USB девайсы идентифицируются по сочетанию vid (vendor id) и pid (product id). Создадим в папке xml файл device_filter.xml со следующим содержимым:

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

Теперь необходимо внести соответствующие разрешения и action (если вам они необходимы) в манифест приложения:

<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:resource мы указываем файл с необходимыми фильтрами для устройств. Также, как я уже говорил ранее, можно назначить intent фильтры, для запуска приложения, к примеру, в результате подключения вашего устройства.

Для начала необходимо получить 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. Их использование зависит от типа передачи, который поддерживает та или иная конечная точка. В данном случае используем bulkTransfer, хотя для HID чаще всего характерно использование конечных точек с типом control. Но у нас Custom 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 */
			})
}

Получив массив байт, мы должны проверить нулевой байт, так как он является report ID и в соответствии с ним парсить полученные данные.

По завершении всех действий с USB нужно закрыть соединение. Можно выполнять это в onDestroy activity или в onCleared во ViewModel.

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

Заключение


В статье рассмотрен очень небольшой и примитивный, исключительно демонстративный код с реализацией для конкретного устройства. Конечно, классов USB существует много, не только HID и для них естественно реализация будет иная. Однако все методы достаточно неплохо документированы и имея хорошее представление о USB стеке можно легко разобраться в том, как их применять.

X. Полезные материалы


Tags:
Hubs:
+17
Comments 49
Comments Comments 49

Articles