Ilya Siganov's blog
Android and Usb Story

Так как наметилась тенденция избавляться от аудиоразъема, то не остаётся в андроиде больше внешних интерфейсов кроме как USB. Надо изучить как с ним работать.

Раньше, во времена мамонтов, только некоторые андроид устройства позволяли подключать к ним по USB периферию. В этом случае говорят, что используется фича usb-host. Для этого покупали специальные OTG(On-The-Go)-usb переходники, которые одной стороной вставляются в android(mini/micro-usb), а с другой стороны обычный USB тип-A, в который можно вставлять другие USB устройства. Т.е. можно подключить флешку, видеокамеру, клавиатуру, мышку(sic!). Всё это конечно круто, но если вы хотите создать своё новое USB-slave устройство, которое можно будет подключать к host андроиду, то это не тривиальная задача совсем.

Но прогресс не стоял на месте и Android зарелизил новую фичу USB-accessory mode, доступную для всех андроид устройств старше 4.4. Так вот, этот режим позволяет подключать андроид к другим устройствам в режиме accessory, т.е. теперь не андроид является хостом и питает другое устройство, а наоборот. Вместе с этим, google любезно подготовили библиотеки совместимые со многими arduino и usb-shield-ами. Таким образом, для разработчика железа, можно просто купить arduino, плату расширения usb-host и всё — можно паять своё устройство. А процесс обмена данными будет не простым, а очень простым — чистая передача бинарных данных в обе стороны. Не надо будет разбираться в том как именно работает usb, как правильно подключаться, так как это реализовано с одной стороны на андроиде и с другой в библиотеках для плат расширения. Очень удобно.

Но перейдем ближе к делу.

Краткий обзор API

Есть два способа получить usb-accessory в андроиде, первый способ — это явно запросить usb менеджер выдать вам список всех подключенных устройств и явно запросить доступ к одному из них, а второй способ — это подписаться на событие подключения устройства. Для этого вам нужно узнать manufacturer, name, version устройства и прописать это в файле src/main/res/xml/accessory_filter.xml. А в манифесте объявить какая активность будет реагировать на это событие. И конечно же в манифесте надо прописать, что вы используете feature usb-accessory.

<!-- accessory_filter.xml  -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-accessory manufacturer="SomeManufacturer" model="SUPER9000" version="1.0" />
</resources>
<!-- manifest.xml -->
<uses-feature
        android:name="android.hardware.usb.accessory"
        android:required="true" />

<application>
  ...
  <activity>
    ...
    <meta-data
      android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
      android:resource="@xml/accessory_filter" />
  </activity>
</application>

Интересны факт. Можно не создавать accessory_filter.xml, а пользоваться только первым подходом. Сейчас андроид не требует явного определения устройств с которыми вы будете работать.

// activity.kt

// вариант с явным запросом всех подключенных устройств.
val usbAccessory = context.usbManager.accessoryList?.firstOrNull()

// вариант с подписыванием на событие и его обработкой
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

  log.info("USB device listener woke up")
  log.info("Got intent $intent")
  if (UsbManager.ACTION_USB_ACCESSORY_ATTACHED == intent.action) {
    val refreshDeviceIntent = Intent(UsbCanManager.ACTION_USB_DEVICE_ATTACHED)
    refreshDeviceIntent.putExtras(intent.extras)
    sendBroadcast(refreshDeviceIntent)
    log.info("Resend USB intent to service $refreshDeviceIntent")
  }

  finish()
}

Но в любом случае, вы должны попросить у пользователя права на работу с данным устройством.

context.usbManager.requestPermission(accessory, permissionIntent)

И уже потом можно будет начать работу с usb:

val parcelFd = usbManager.openAccessory(accessory) // открыть дескриптор
val inputStream = FileInputStream(pfd.fileDescriptor) // открыть потоки чтения и записи
val outputStream = FileOutputStream(pfd.fileDescriptor)

И естественно, если есть событие на подключение устройства, то есть и обратное ему: UsbManager.ACTION_USB_ACCESSORY_DETACHED.

История из жизни. Пришлось мне разрабатывать приложение для какой-то штуки, которая снимает показатели с датчиков машины и может быть подключена к андроиду по usb. По счастливой случайности я не знал ни формат бинарного протокола, ни имя-версию устройства. Вообще ничего. Даже эмулятора всей штуки не было, а тестировать все будет клиент, который находится +6 часов от меня. И вот тут-то в режиме реального времени через VPN я отлаживал процесс подключения через accessory. И как раз таки первый подход, где можно получить список устройств и спас меня. Так как те крохи спецификации железяки были неверными и подход с событием совсем не работал.

Я рекомендую в приложении использовать сразу два подхода, на случай проблем с именем железяки, что приведет к неработоспособности подхода с фильтром. Стратегия работы будет тогда следующей — при запуске приложение проверяет подключен ли к нему девайс, если подключен, тогда запускаем вручную наши сервисы. Можно добавить кнопку, переподключиться (или swipe-to-refresh), чтобы можно было в любой момент повторить всю процедуру сначала.

Напоминание! Вся эта магия будет у вас происходить асинхронно и не однопоточно, поэтому подумайте о критических секциях и блокировках. Лучше конечно всё завернуть в один event-loop, чтобы проблем многопоточности у вас не было в принципе.

Тестирование

Всё это конечно отлично, даже замечательно. Но чудесная библиотека от гугла, внезапно не заработала для моего arduino UNO и sparkfun shield. Что же делать? В этот момент логично предположить, что мы бы могли использовать наш десктоп с linux на борту выступать в роли хоста для нашего девайса. Я потратил некоторое время, чтобы наконец-то найти пример этой чудесной программы на чистом C, которая не требует тучи зависимостей и работает как надо.

Круто, подумал было я, и принялся отлаживать! Но как дебажить, если USB уже занят? Для этих целей есть функция проброса adb через вайфай. Это описано в официальной документации. Перечислю кратко шаги:

  1. Подключить android к компу по USB.
  2. Прописать adb tcpip 5555.
  3. Отсоединить девайс.
  4. Найти ip адрес андроид устройства в его настройках.
  5. adb connect <DEVICE_IP_ADDRESS>:5555
  6. Profit!

Подводные камни? Какие подводные камни?

Вы было подумали, что это идеально работает? Теперь вы сможете отлаживать работу usb?? Как бы не так! В документации совсем не говорят, что почему-то после подключения устройства по USB к другому устройству, даже не к КОМПУ с которого разрешили отладку, соединение по wifi-adb пропадает! ВСЕГДА. Нужно переподключаться.

Если вы хотели отладить процесс подсоединения устройства, тот момент прилетания события от ОС о факте подключения usb-accessory, то ничего у вас никогда не получится. Андроид разорвет adb соединение. Вам придется опять писать adb connect. Но момент будет упущен!

Литература