[Из песочницы] Работа с устройствами USB в Android
В недавней статье на Geektimes в комментариях возник вопрос о поддержке в ОС Android периферии, подключенной к шине USB. Действительно, большинство вендорского ПО, к примеру, для работы с принтерами и МФУ, поддерживает только подключение по сети. Однако это не означает, что в самой ОС Android нет такой возможности — это означает лишь то, что большинство устройств не имеют полноценного USB хоста, и далеко не все имеют поддержку OTG. По сети же могут работать абсолютно все без исключения.
Большинство устройств на Android при наличии порта OTG поддерживают на уровне системы (ядра Linux или стандартных компонентов Android) следующие классы устройств:
- Устройства ввода — клавиатуры, мыши, джойстики (HID)
- Накопители (Mass Storage)
Несколько реже:
- Сотовые модемы
- Сетевые адаптеры
- Вебкамеры
Хабы поддерживаются при наличии полноценных хост-портов, но не поддерживаются на портах OTG.
Подробнее список устройств, поддерживаемых на уровне ядра Linux, можно получить в sysfs:
$ ls /sys/bus/usb/drivers
Если же модуль в принципе доступен в исходниках ядра Linux, но не включен в Android — не стоит рассчитывать на то, что его получится собрать и расставить на все целевые системы.
Однако, начиная с Android 3.1 (API 12), в системе содержатся средства, достаточные для поддержки на уровне приложения любой USB периферии. Данные средства описаны в разделе USB Host руководства по Android API. Здесь же я хочу привести примеры реальной работы с некоторыми видами устройств.
Права доступа
Как и для прочих действий, Android требует, чтобы приложение получило разрешение на доступ к USB периферии. Существует 2 способа получить такое разрешение:
- Задекларировать список устройств в AndroidManifest
- Явно показать пользователю диалог «разрешить»
Поскольку для моих задач лишние вопросы к пользователю были нежелательны, я использовал первый способ.
Итак, нам необходимо добавить в манифест следующее:
...
А в res/xml/device_filter.xml вписать следующее:
Отмечу, что хотя общепринято указывать VID: PID в 16-ричной системе счисления, здесь они должны быть указаны в десятичной. В документации заявляется, что возможно указание только класса, без VID и PID, но у меня это не стало работать.
Принтеры
На примере принтера я покажу, как непосредственно использовать API android.hardware.usb. На уровне передачи данных все принтеры поддерживают стандартый класс USB устройств:
int UsbConstants.USB_CLASS_PRINTER = 7;
Класс предельно простой. В рамках этого класса устройство должно поддерживать:
- Обязательный bulk out endpoind для отправки данных на принтер
- Опциональный bulk in endpoind для получения статуса принтера
- 3 управляющих запроса
int GET_DEVICE_ID = 0;
int GET_PORT_STATUS = 1;
int SOFT_RESET = 2;
Код, приведенный ниже, предоставляет функциональность, аналогичную устройству /dev/usb/lp в Linux. Далее нам нужен фильтр, преобразующий исходный документ в пакет данных, понятный конкретной модели принтера. Но это тема иной статьи. Как один из вариантов — можно собрать ghostscript с помощью NDK.
Для работы с устройством нам в первую очередь нужно:
1. Найти устройство. В примере для простоты я ищу первый попавшийся:
UsbDevice findDevice() {
for (UsbDevice usbDevice: mUsbManager.getDeviceList().values()) {
if (usbDevice.getDeviceClass() == UsbConstants.USB_CLASS_PRINTER) {
return usbDevice;
} else {
UsbInterface usbInterface = findInterface(usbDevice);
if (usbInterface != null) return usbDevice;
}
}
return null;
}
UsbInterface findInterface(UsbDevice usbDevice) {
for (int nIf = 0; nIf < usbDevice.getInterfaceCount(); nIf++) {
UsbInterface usbInterface = usbDevice.getInterface(nIf);
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_PRINTER) {
return usbInterface;
}
}
return null;
}
UsbDevice mUsbDevice = findDevice();
UsbInterface mUsbInterface = findInterface(mUsbDevice);
2. Получить endpoint«ы:
for (int nEp = 0; nEp < mUsbInterface.getEndpointCount(); nEp++) {
UsbEndpoint tmpEndpoint = mUsbInterface.getEndpoint(nEp);
if (tmpEndpoint.getType() != UsbConstants.USB_ENDPOINT_XFER_BULK) continue;
if ((mOutEndpoint == null)
&& (tmpEndpoint.getDirection() == UsbConstants.USB_DIR_OUT)) {
mOutEndpoint = tmpEndpoint;
} else if ((mInEndpoint == null)
&& (tmpEndpoint.getDirection() == UsbConstants.USB_DIR_IN)) {
mInEndpoint = tmpEndpoint;
}
}
if (mOutEndpoint == null) throw new IOException("No write endpoint: " + deviceName);
3. Непосредсвенно открыть устройство:
mConnection = mUsbManager.openDevice(mUsbDevice);
if (mConnection == null) throw new IOException("Can't open USB connection:" + deviceName);
mConnection.claimInterface (mUsbInterface, true);
4. После этого мы можем читать и писать в устройство:
public int read(final byte[] data) throws IOException {
int size = Math.min(data.length, mInEndpoint.getMaxPacketSize());
return mConnection.bulkTransfer(mInEndpoint, data, size, getReadTimeout());
}
public int write(final byte[] data, final int length) throws IOException {
int offset = 0;
while (offset < length) {
int size = Math.min(length - offset, mInEndpoint.getMaxPacketSize());
int bytesWritten = mConnection.bulkTransfer(mOutEndpoint,
Arrays.copyOfRange(data, offset, offset + size), size, getWriteTimeout());
if (bytesWritten <= 0) throw new IOException("None written");
offset += bytesWritten;
}
return offset;
}
5. По завершении работы — закрыть устройство:
mConnection.close();
Преобразователи USB-Serial
В отличие от притеров, преобразователи USB-Serial гораздо менее стандартизированы. Существует несколько распространенных чипов, для которых существенно отличается установка параметров последовательного порта — битрейта, чётности и проч. К счастью, есть библиотека github.com/mik3y/usb-serial-for-android, поддерживающая практически все существующие чипы. Библиотека полностью скрывает USB API, сводя все необходимые действия к минимуму вызовов с минимумом параметров.
1. Найти и открыть устройство:
UsbSerialPort mUsbSerialPort;
UsbManager mUsbManager = (UsbManager) DEVICE.getSystemService(Context.USB_SERVICE);
String type = "FTDI”;
for (UsbDevice usbDevice: mUsbManager.getDeviceList().values()) {
UsbSerialDriver usbSerialDriver = UsbSerialProber.probeSingleDevice(usbDevice);
if (usbSerialDriver == null) continue;
if (!type.equals(usbSerialDriver.getShortDeviceName())) continue;
mUsbSerialPort = usbSerialDriver.getPort(0);
mUsbSerialPort.open(mUsbManager);
break;
}
2. Установить параметры последовательного порта:
mUsbSerialPort.setParameters(baudRate, dataBits, stopBits, parity);
3. Читать и писать в порт:
public int read(final byte[] data) throws IOException {
return mUsbSerialPort.read(data, getReadTimeout());
}
public int write(final byte[] data, final int length) throws IOException {
return mUsbSerialPort.write(data, length, getWriteTimeout());
}
4. По завершении работы — закрыть порт:
mUsbSerialPort.close();
Резюме
Надеюсь, что мне удалось показать, что работа с USB периферией достаточно проста и логична. Безусловно, реализация протоколов некоторых конкретных устройств не блещет простотой —, но это проявится в любой системе в одинаковой степени.
Все приведенные примеры я взял из реального проекта, лишь исключил очевидные проверки, оставив только ключевые строки.