[Из песочницы] Оптимизация работы USB-устройств под Android
Большинство устройств на 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.
Для работы с устройством нам в первую очередь нужно:
- Найти устройство. В примере для простоты я ищу первый попавшийся:
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);
- Получить 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);\
- Непосредсвенно открыть устройство:
mConnection = mUsbManager.openDevice(mUsbDevice);
if (mConnection == null) throw new IOException("Can't open USB connection:" + deviceName);
mConnection.claimInterface (mUsbInterface, true);
- После этого мы можем читать и писать в устройство:
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;
}
- По завершении работы — закрыть устройство:
mConnection.close();
Преобразователи USB-Serial
В отличие от притеров, преобразователи USB-Serial гораздо менее стандартизированы. Существует несколько распространенных чипов, для которых существенно отличается установка параметров последовательного порта — битрейта, чётности и проч. К счастью, есть библиотека github.com/mik3y/usb-serial-for-android, поддерживающая практически все существующие чипы. Библиотека полностью скрывает USB API, сводя все необходимые действия к минимуму вызовов с минимумом параметров.
- Найти и открыть устройство:
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;
}
- Установить параметры последовательного порта:
mUsbSerialPort.setParameters(baudRate, dataBits, stopBits, parity);
- Читать и писать в порт:
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());
}
- По завершении работы — закрыть порт:
mUsbSerialPort.close();
Резюме
Надеюсь, что мне удалось показать, что работа с USB периферией достаточно проста и логична. Безусловно, реализация протоколов некоторых конкретных устройств не блещет простотой —, но это проявится в любой системе в одинаковой степени.
Все приведенные примеры я взял из реального проекта, лишь исключил очевидные проверки, оставив только ключевые строки.