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. Но момент будет упущен!

Литература


Here are all the notes in this garden, along with their links, visualized as a graph. (Use ctrl + scroll to zoom in-out)