Доступ до 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 здатний бачити підключені пристрої. Але поки що це тільки теорія.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.