[Из песочницы] Оптимизация работы 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.

Для работы с устройством нам в первую очередь нужно:


  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);


  1. Получить 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);\


  1. Непосредсвенно открыть устройство:
mConnection = mUsbManager.openDevice(mUsbDevice);
if (mConnection == null) throw new IOException("Can't open USB connection:" + deviceName);
mConnection.claimInterface (mUsbInterface, true);


  1. После этого мы можем читать и писать в устройство:
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;
}


  1. По завершении работы — закрыть устройство:
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;
}


  1. Установить параметры последовательного порта:

mUsbSerialPort.setParameters(baudRate, dataBits, stopBits, parity);


  1. Читать и писать в порт:
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());
}


  1. По завершении работы — закрыть порт:
mUsbSerialPort.close();


Резюме

Надеюсь, что мне удалось показать, что работа с USB периферией достаточно проста и логична. Безусловно, реализация протоколов некоторых конкретных устройств не блещет простотой —, но это проявится в любой системе в одинаковой степени.

Все приведенные примеры я взял из реального проекта, лишь исключил очевидные проверки, оставив только ключевые строки.

© Habrahabr.ru