В современных 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..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. Полезные материалы
Комментарии (23)
lingvo
07.10.2019 17:07Два, точнее три вопроса:
- Насколько это решение применимо к телефонам/планшетам на Андроиде с заделом на будущее?
- Если да, то каковы шансы провести приложение в Google Play?
- А что с Apple?
Foxek Автор
07.10.2019 17:44Возможно не совсем правильно понимаю суть всех трех вопросов, но попробую ответить:
1. Решение прекрасно ложиться на телефоны/планшеты на android, так как нет никаких видимых причин на изменение поведения. Работа с USB хорошо документирована, поддерживается (возникают новые методы, устаревают старые) гуглом и нет предпосылок к изменению этого в будущем.
2. Опять же не вижу причин не попасть в Google Play. Политика гугл в этом отношении сейчас достаточно жесткая, но конкретно к работе с USB, каких-то особых требований я не видел. К тому же, как правило, все что Вам нужно сделать, это в манифесте задекларировать uses-feature, остальное Google Play сделает за Вас. Вероятно у Вас возникают сомнения, когда вы видите слово Custom. Но это напрямую относится к периферийному устройству, но даже с его сертификацией, проблем не возникнет (если вы приобрели свой VID и PID). С точки зрения Android, каких то отклонений от нормы нет.
3. Не могу Вам ответить на этот вопрос, не смотрел. Но опять же повторю, протокол достаточно стандартизирован, и если у Apple есть возможность дотянуться до USB из вашего application, Вы с большой вероятностью сможете легко подружить его с Вашим периферийным устройством.
Повторюсь, основная магия все равно идет в железе на периферийном устройстве, по причине того, что в данном случае Android используется, как host. Вероятно, если вы захотите использовать Android в качестве device, то Вам придется пережить все танцы с бубном, связанные с дескрипторами, но это уже немного другой уровень погружения в usb стек, хоть и тоже весьма поверхностныйlingvo
07.10.2019 18:22Тут вопрос в том, что не идет ли предпосылка в телефонах/планшетах к отказу от USB вообще? Т.е. в Apple, например, USB вообще нет, а всякие Lightning, что вынуждает делать отдельную версию устройства под них, да еще и с чипом. В андроидах сейчас идет Type C и может быть в конце-концов тоже уйдет, как и разъем для наушников, или придумают такую же фигню, как Apple с чипом "Certified for Android"?
"если вы приобрели свой VID и PID" — вот это уже начинает напрягать. Т.е. прикинуться чайником (или FTDI каким-нибудь) может уже не получиться? Просто кастомизация все больше и больше не в почете у гигантов.
Foxek Автор
07.10.2019 19:421. Мне на самом деле сложно предположить. Если говорить в контексте этой статьи, то как я уже говорил считаю использование USB HID, да в общем и остальных классов для взаимодействия с периферийным устройством с телефона/планшета архитектурнй ошибкой проекта, в котором все это используется. С однопланиками все понятно, там вероятно есть смысл, но в таком случае туда сам собой напрашивается Linux. Но опять же подчеркну, все зависит от задачи.
Что касается использования USB в смартфонах в отрыве от статьи, прогнозировать сложно, но мне на данный момент сложно предположить подобное развитие событий именно для android смартфонов. USB протокол прост в реализации на уровне железа, программный стек достаточно хорошо подходит почти для всех задач и прекрасно отработан и документирован. Изменение типа разъема, спецификации и прочего это одно, а вот убрать разъем целиком, это совсем другое дело. Согласен, большую часть стандартных вещей можно и без USB провернуть, даже дебажить можно по Wi-Fi. Но боюсь закрыть все дыры таким образом не получится. К тому же я не по совсем понимаю, зачем его убирать. С разъемом наушников все понятно, а вот USB… Чип USB стоит немного, небольшой по размеру, если его убрать, места на плате не сильно уменьшится. К тому же текущая тенденция увеличения размера экрана, позволяет об этом не париться. Если речь идет о размере разъема, то если уже и он мешает, то я не знаю, насколько тонкие телефоны нужны человечеству)
2. Кастомизация происходит на уровне периферийного устройства, к Android отношения не имеет, если речь идет о host. VID и PID придется приобрести, правда не во всех случаях)) FDTI — это не к USB. Сам протокол он не реализует, а является лишь преобразователем интерфейса. К слову о FTDI, USART на Android, насколько мне известно, тоже можно потрогать из Вашего Application402d
09.10.2019 10:20fun sendReport(data: ByteArray) {
usbConnection?.bulkTransfer(usbOutEndpoint, data, data.size, 0)
}
собственно Вы уже привели тут единственный метод.
FTDI, USART на чистой яве? это фантастика. Вам придется
работать через оберку с двоичной либой.
github.com/chzhong/serial-androidFoxek Автор
09.10.2019 11:23У UsbDeviceConnection есть еще метод controlTransfer, который тоже может использоваться, но насколько мне известно только для передачи управляющих команд через нулевую конечную точку, через которую идет энумерация. Насчёт USART, как я уже говорил, не уверен, возможно и через либу. Видел в сети варианты использования, так что в случае необходимости решение найти можно
ginkage
07.10.2019 23:02Кстати, в качестве Device устройства на Android можно использовать через Bluetooth, начиная с Android Pie для этого есть стандартный API (почему-то выключенный на телефонах Nokia и OnePlus, но тем не менее).
Foxek Автор
07.10.2019 23:11Я так понимаю Вы говорите именно про Bluetooth, безотносительно к USB.
Насколько мне известно Android смартфон мог играть роль Device достаточно давно по крайней мере в Bluetooth Low Energy. В классическом Bluetooth это вероятно является нововведением, к сожалению не знаю, так как при разработке чаще пользовался BLE — стабильнее работает, по крайней мере со стороны embedded железки (точнее соотношение цена/стабильность получше)ginkage
07.10.2019 23:50Да, про Bluetooth, но там, по сути, используются те же самые дескрипторы, меняется только физический транспорт, а протокол более-менее тот же.
Играть роль Device (например, мышь) через BLE мне его стабильно заставить так и не удалось, что-то в стеке всё-таки реализовано странно, по крайней мере в последних версиях: где-то на Marshmallow-Nougat это ещё работало, но дальше практически перестало нормально работать. Ну а Device через Bluetooth Classic — это действительно нововведение.Foxek Автор
08.10.2019 00:01Извините, но я с Вами не согласен. Да, с одной стороны характеристики и сервисы в bluetooth low energy действительно напоминают usb hid report, но только по структуре разве что. На этом сходства заканчиваются. Работают они абсолютно по разному. Я хотел даже привести здесь пример различий, но понял, что привести его достаточно сложно, потому что отличается все кроме этой самой структуры.
ginkage
09.10.2019 10:19Я говорил всё-таки о дескрипторах и репортах в Bluetooth Classic, а не BLE. :)
BLE действительно не похож ни разу.Foxek Автор
09.10.2019 11:10Если честно не понимаю о каких дескрипторах и репортах классического Bluetooth идет речь. Впервые слышу о подобных вещах.
Если речь идет про протокол HID натянутый поверх Bluetooth, то это просто протокол HID, который по идее спецификой USB не является. можно пользовать где угодно. Вот только отношения к Bluetooth я так и не просек)ginkage
09.10.2019 17:34Речь о стандартном профиле Bluetooth, таком же стандартном, как A2DP, см. википедию:
Bluetooth HID is a lightweight wrapper of the human interface device protocol defined for USB. The use of the HID protocol simplifies host implementation (ex: support by operating systems) by enabling the re-use of some of the existing support for USB HID to also support Bluetooth HID.
Все Bluetooth Classic устройства ввода (а это разнообразные мыши, клавиатуры, джойстики, и т.п.) по сути общаются по тому же самому USB HID протоколу, сообщая хосту те же самые дескрипторы и репорты.
Ну а BLE — это уже другой профиль, известный как GATT…Foxek Автор
09.10.2019 17:44Согласен, я не совсем корректно вас понял, поэтому никак не мог осознать о каких дескрипторах в классическом Bluetooth идет речь)
roboter
08.10.2019 16:52У STM есть VirualComPort тогда, по идее, не надо свой VID PID получать.
Foxek Автор
08.10.2019 17:02Если речь идет о USB классе VCP, то свои VID и PID там не нужны, можно использовать те, которые предоставляет STM для устройств на базе их контроллеров. STM также предоставляет драйвера под ОС для работы с подобными устройствами. Но если ваш проект планирует развиваться на рынке, VID и PID приобрести рано или поздно придется, а также написать свой драйвер для ОС.
Для коммерческих продуктов стандартный драйвер не всегда хорошая идея. К примеру драйвер VCP под Windows просто ужасный. Позволяет все прекрасно запустить на малых скоростях и небольших объёмах данных, но шаг вправо, шаг влево и драйвер выдает удивительные вещи. Попробуйте предавать по VCP с STM большой объем данных и при этом щелкать мышкой на ПК (знаю, звучит глупо), на котором вы принимаете данные. Уверяю Вас, часть данных вы не получите.
402d
09.10.2019 09:38На телефонах с андроидом работа с usb больше зависит от железа.
например. флайчик. все программные тесты говорят юсб поддерживается, а железки нет.
Поэтому простой тест. Берем мышку. Берем флешку. Втыкаем через отг кабель.
Вместо конкретных vid&tid в хмл для фильтрации можно просто классы написать.
Просто работать с устройствами, которые поддерживают булк на ендпоинтах.
Эмуляция COM порта гиморой.
Foxek Автор
09.10.2019 09:47Согласен, к тому же я заметил, что android работает и с interrupt конечными точками через булк методы. Подробное поведение нигде не описано, так что можно схватить не логичное поведение
402d
09.10.2019 09:56что еще очень раздражает.
Каждый раз после коннекта устройство считается новым и идет запрос пермишинов.
Единственное юзабельное решение.
Становится слушателем на все подключения. Тогда у пользователя появиться
вариант «открывать ваше приложение при подключении всегда».
Активити слушатель этого может быть пустышкой. Нужно только для автополучения
разрешения. Ну а я в своей проге меняю настроки в этом случае так (транспорт-usb,vid,pid).
Еще в андроиде не получиться подключить два устройства с одинаковыми vid&pid одновременно, так как смотри выше обход дерева требуется, чтобы найти устройство.
А uri адрес устройства при каждом конекте новый.
nmrulin
09.10.2019 17:09А где можно найти расшифровку кодов ошибок. Например, если
usbConnection?.bulkTransfer(usbOutEndpoint, data, data.size, 0), выполнилось неправильно.Foxek Автор
09.10.2019 17:19Данный метод возвращает размер переданных данных либо ошибку (Возвращает -1). Ошибка не имеет различных кодов, есть только факт возникновения.
shkola
:) это случайно не я ваш "заказчик"? прям про меня написано :)
Foxek Автор
Хах, насколько мне известно нет, проект был — проект успешно завершен. Но я уверен, что не Вы один сталкиваетесь с подобными задачами) Но я все таки считаю, что это не совсем правильное архитектурное решение. Вероятно использование Linux было бы более приемлемым решением. Хотя, опять же, зависит от конкретной задачи и конечной цели