Доступ к HID-устройствам из программы на Qt под Android

Введение


С выходом Qt 5 появилась удобная возможность расширить список поддерживаемых программой платформ и на мобильные ОС, в частности на Android.
Сам процесс портирования программы с десктопной версии Qt на мобильную свелся к банальной перекомпиляции. Интерфейс и логика завелись сразу, за исключением той части, без которой, собственно, программа бесполезна: обмену с HID-устройством.

Первые трудности


С самого начала для общения с устройством используется библиотека HIDAPI. Она мультиплатформенная и ее легко использовать.
Первая трудность, с которой пришлось столкнуться заключалась в том что под Android нет hidraw, который использовался для доступа к устройствам под десктопный Linux. Для обхода пришлось перейти на использование библиотеки libusb и ее интерфейса к ней в HIDAPI.
Первый запуск показал, что работает перечисление устройств, но открыть файл устройства нельзя, из-за отсутствия прав у приложения.
В файл README libusb/android есть описание возможных путей обхода этой проблемы: либо менять права у файла устройств, либо воспользоваться интерфейсом android.hardware.usb.UsbDevice, для открытия устройств.

Простая смена маски доступа к файлу на 777 на рутованом устройстве подтвердила работоспособность выбранной схемы, но и привела к пониманию того, что это не совсем верный путь, т.к. он работоспособен на очень маленьком круге устройств. Поэтому пришлось лезть в дебри API Android.

Чтение документации показало что есть два пути доступа к устройству: использование intent filter и простое перечисление устройств.
Первый способ заставить работать не удалось, никаких событий при появлении устройства в системе программе не приходило. Собственно этот путь тоже тупиковый, т.к. подразумевает, что программа должна быть запущена раньше, чем подключено устройство, и это значит что нам следует в полной мере воспользоваться предоставляемым API для доступа к USB.

Что бы обойтись минимумом переделок в существующем коде было решено вынести в Java-часть только код связанный с запросом разрешения доступа к устройству у пользователя и собственно открытие устройства. Всю остальную работу по перечислению устройств и обмена данными выполняет связка HIDAPI-libusb.

Реализация.


Первое, что пришлось сделать, это запрос разрешения у пользователя на открытие устройства.
Опять же, подстраиваясь под существующий алгоритм получилось следующее: при нахождении устройства, путь к его файлу передается в функцию в класса Activity программы посредством JNI-интерфейса:

int HidTransport::openAndroidDevice(QString devPath)
{
    QAndroidJniObject dP = QAndroidJniObject::fromString(devPath);
    jint dFD = QAndroidJniObject::callStaticMethod<jint>("org/HidManager/HidDevice", "tryOpenDevice", "(Ljava/lang/String;)I", dP.object<jstring>());
    return dFD;
}


Здесь хочется сделать некоторое отступление: в силу каких-то причин Qt интерфейс к JNI может вызывать только статические методы класса. Поэтому мы создаем фактически сингелтон. Ниже приведены конструктор и onCreate класса Activity:

private static HidDevice m_instance;
private static UsbManager m_usbManager;
private static PendingIntent mPermissionIntent;
private static HashMap<String, Integer> deviceCache = new HashMap<String, Integer>();
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";

public HidDevice()
{
       m_instance = this;
}

public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    if (m_usbManager==null) {
      m_usbManager = (UsbManager) m_instance.getSystemService(Context.USB_SERVICE);
    }

    mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
    IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
    registerReceiver(mUsbReceiver, filter);
}

@Override
public void onDestroy()
{
    super.onDestroy();
}


Функция tryOpenDevice, вызываемая из нативного кода выглядит следующим образом:
public static int tryOpenDevice(String devPath)
{
    if (deviceCache.containsKey(devPath)) {
        int fd = deviceCache.get(devPath);
        return fd;
    }

    deviceCache.put(devPath, -1);

    HashMap<String, UsbDevice> deviceList = m_usbManager.getDeviceList();
    Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
    while(deviceIterator.hasNext()) {
        UsbDevice device = deviceIterator.next();
        if (devPath.compareTo(device.getDeviceName())==0) {
            m_usbManager.requestPermission(device, mPermissionIntent);
            break;
        }
    }
}


deviceCache выполняет роль промежуточного хранилища состояния процесса открытия устройства. Такой вариант был выбран потому, что алгоритм нативного кода пытается с некоторой периодичностью открыть каждое найденное, но еще не открытое устройство.

Далее в работу вступает механизм разрешений Android и для получения результатов служит эта функция:

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (ACTION_USB_PERMISSION.equals(action)) {
            synchronized (this) {
                UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    if(device != null){
                        m_instance.openDevice(device);
                   }
                }
            }
        }
    }
};


Как из нее видно, если разрешение получено, то вызывается функция открытия устройства:
public void openDevice(UsbDevice device)
{
    try {
        if (!res) return;
        UsbDeviceConnection devConn = m_usbManager.openDevice(device);
        Integer fd = devConn.getFileDescriptor();
        deviceCache.put(device.getDeviceName(), fd);
    }
    catch (InterruptedException e) {
            return;
    }
}

Которая сохраняет полученный файловый дескриптор в deviceCache.

После прохождения всех этих этапов мы получаем файловый дескриптор открытого устройства. Но тут появляется другая проблема: HIDAPI и libusb не умеют принимать дескрипторы в качестве указателя на устройство.

К счастью, эта проблема решилась просто. Существует форк libusb, принимающий в качестве аргумента файловый дескриптор открытого устройства.

Заключение


Вот так, достаточно просто, можно получить доступ к USB из нативного кода.

К сожалению, данный подход работает не на всех устройствах. Многие производители не включают разрешение
android.hardware.usb.host в свои прошивки, что приводит к ситуации, когда физически планшет может работать в качестве хоста для флешек или мышей, но другие устройства не работают. При этом файл USB-устройства ядром создается, но даже UsbManager их не видит.

В данном случае, это ограничение возможно обойти на устройствах с работоспособным root, меняя права доступа на файл, т.к. libusb способен видеть подключенные устройства. Но пока что это только теория.
  • +15
  • 10,6k
  • 6
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 6
    0
    Тоже портировал ствой Qt-проект на Android. У меня изначально использовался libusb и проблем быть не должно было. Остановился на смене маски на 777, благо проект распространяться не будет, а свои устройства все рутую.
    Если не трудно, опишите пожалуйста как интегрировать JNI-интерфейс в свой существующий проект. Имею ввиду, какие файлы создать, куда положить. При этом проект же продолжит корректно собираться под windows/linux?
      +1
      Не совсем понял суть вопроса, но попробую угадать…
      Если имеется ввиду, как включить Java-файлы в свой проект, то это достаточно просто: достаточно включить переменную ANDROID_PACKAGE_SOURCE_DIR в pro-файл, указывающую на то, где лежат файлы специфичные для сборки Android, в том числе и исходники на Java. А для использования JNI ничего, кроме как включить в проект androidextras и не требуется.
      В примерах есть «Qt Notifier». Я когда только начинал заниматься портом, то делал по подобию, за исключением того, что мой проект использует QtWidgets.
      И да, все прекрасно собирается под десктопные платформы, если платформозависимые куски как в .pro-файле, так и в коде обложить ifdef'ами.
      0
      Вопрос немного не по теме. Насколько я могу судить по странице загрузки www.qt.io/download/ в Community редакции нет возможности загружать мобильные приложения в публичные магазины. На чем основывается это идиотское ограничение? Или я что-то не так понимаю, может тут подразумевается какой-то встроенный механизм дистрибуции, который доступен только в коммерческих версиях?
        +1
        Эта таблица не совсем точная. Тут уже обсуждают некоторые моменты.

        Дальше, насколько я понимаю лицензию LGPL:
        — Если библиотеки Qt идут предустановленные с ОС (Symbian^3 Anna / Belle, Harmattan, Sailfish OS, BlackBerry 10), то проблем быть не должно.
        — Если библиотеки можно установить централизованно и пользователь будет иметь возможность заменить их на новые/свои версии (Symbian S60 3rd / 5th Edition со Smart Installer, Android с Ministro), то проблем тоже быть не должно.
        — А вот если библиотеки надо распространять вместе с приложением или, вообще, линковать статически, и при этом приложения можно устанавливать только через аппстор (iOS, Windows Phone), то тут проблематично. Т.к. пользователь не сможет просто заменить библиотеки / перелиноквать приложение и установить его на телефон.
          0
          Теперь понял. Спасибо. В общем, главное дать пользователю приложения возможность перелинковки библиотек. Ох уж эти лицензии!)
            +1
            Для динамически слинкованных приложений даже перелинковка не нужна — то-есть, формально, условие LGPL выполняется: подсовывай свои бинарно совместимые библиотеки и радуйся. И этого достаточно для десктопных приложений или платформ, где официально можно устанавливать в обход аппстора (sideloading). А с залоченными платформами, как раз, и начинается основной спор: достаточно ли дать пользователю динамически слинкованное приложение / скомпилированные объектные файлы для статической линковки, или пользователь должен иметь возможность установить это приложение на телефон без его разлочки или других «танцев с бубном».

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое