Travailler avec USB HID personnalisé sur Android

Conçu par freepik
Dans les applications Android modernes pour interagir avec d'autres appareils, les protocoles de transfert de données sans fil, tels que Bluetooth, sont le plus souvent utilisés. Dans les années où certains appareils ont une charge sans fil, il est difficile d'imaginer un tas d'appareils et de périphériques Android, qui nécessitent l'utilisation d'interfaces câblées. Cependant, lorsqu'un tel besoin se fait sentir, l'USB vient immédiatement à l'esprit.

Prenons un cas hypothétique avec vous. Imaginez qu'un client vienne vers vous et vous dise: «J'ai besoin d'une application Android pour contrôler un appareil de collecte de données et afficher ces données à l'écran. Il y a un MAIS - l'application doit être écrite sur un ordinateur monocarte avec le système d'exploitation Android, et le périphérique est connecté via USB "

Cela semble fantastique, mais cela arrive parfois. Et ici, il est très utile d'avoir une connaissance approfondie de la pile USB et de ses protocoles, mais cet article ne traite pas de cela. Dans cet article, nous verrons comment contrôler un périphérique à l'aide du protocole USB Custom HID à partir d'un périphérique Android. Pour plus de simplicité, nous allons écrire une application Android (HOST) qui contrôlera la LED sur le périphérique (DEVICE) et recevra l'état du bouton (appuyez). Je ne donnerai pas le code de la carte périphérique, qui sont intéressés - écrivez dans les commentaires.

Commençons donc.

Théorie Le plus court possible


D'abord, un peu de théorie, aussi courte que possible. Il s'agit d'un minimum simplifié, suffisant pour comprendre le code, mais pour une meilleure compréhension, je vous conseille de vous familiariser avec cette ressource .

Pour communiquer via USB sur un périphérique, il est nécessaire de mettre en place une interface d'interaction. Diverses fonctions (par exemple, USB HID, USB Mass Strorage ou USB CDC) implémenteront leurs interfaces, et certaines auront plusieurs interfaces. Chaque interface contient des points d'extrémité - des canaux de communication spéciaux, une sorte de presse-papiers.

Mon périphérique a un HID personnalisé avec une interface et deux points de terminaison, un pour la réception et un pour la transmission. Habituellement, les informations avec les interfaces et les points de terminaison existant sur l'appareil sont écrites dans les spécifications de l'appareil, sinon elles peuvent être déterminées via des programmes spéciaux, par exemple USBlyzer.

Les périphériques USB HID communiquent via des rapports. Quels sont les rapports? Étant donné que les données sont transmises via des points d'extrémité, nous devons en quelque sorte les identifier et les analyser conformément au protocole. Les appareils ne se jettent pas seulement des octets de données, mais échangent des paquets qui ont une structure clairement définie, qui est décrite sur l'appareil dans un descripteur de rapport spécial. Ainsi, selon le descripteur du rapport, nous pouvons déterminer avec précision quel identifiant, structure, taille et fréquence de transmission ont certaines données. Le paquet est identifié par le premier octet, qui est l'ID de rapport. Par exemple, l'état du bouton va au rapport avec ID = 1, et nous contrôlons la LED via le rapport avec ID = 2.

Loin du fer, plus proche d'Android


Dans Android, la prise en charge des périphériques USB est apparue à partir de l'API version 12 (Android 3.1). Pour fonctionner avec un périphérique, nous devons implémenter le mode hôte USB. Travailler avec USB est assez bien décrit dans la documentation.

Vous devez d'abord identifier votre appareil connecté, parmi la grande variété de périphériques USB. Les périphériques USB sont identifiés par une combinaison de vid (identifiant du vendeur) et pid (identifiant du produit). Dans le dossier xml, créez le fichier device_filter.xml avec le contenu suivant:

<resources> <usb-device vendor-id="1155" product-id="22352" /> </resources> 

Vous devez maintenant définir les autorisations et actions appropriées (si vous en avez besoin) dans le manifeste de l'application:

 <uses-permission android:name="android.permission.USB_PERMISSION" /> <uses-feature android:name="android.hardware.usb.host" /> <activity android:name=".MainActivity"> <intent-ilter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" /> </activity> 

Dans Android: ressource, nous spécifions un fichier avec les filtres nécessaires pour les appareils. De plus, comme je l'ai dit plus tôt, vous pouvez attribuer des filtres d'intention pour lancer l'application, par exemple, à la suite de la connexion de votre appareil.

Vous devez d'abord obtenir l'UsbManager, trouver le périphérique, l'interface et les points de terminaison du périphérique. Cela doit être fait à chaque fois que l'appareil est connecté.

 val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager private var usbConnection: UsbDeviceConnection? = null private var usbInterface: UsbInterface? = null private var usbRequest: UsbRequest? = null private var usbInEndpoint: UsbEndpoint? = null private var usbOutEndpoint: UsbEndpoint? = null fun enumerate(): Boolean { val deviceList = usbManager.deviceList for (device in deviceList.values) { /*      VID  PID */ if ((device.vendorId == VENDOR_ID) and (device.productId == PRODUCT_ID)) { /*      */ usbInterface = device.getInterface(CUSTOM_HID_INTERFACE) /*            */ for (idx in 0 until usbInterface!!.endpointCount) { if (usbInterface?.getEndpoint(idx)?.direction == USB_DIR_IN) usbInEndpoint = usbInterface?.getEndpoint(idx) else usbOutEndpoint = usbInterface?.getEndpoint(idx) } usbConnection = usbManager.openDevice(device) usbConnection?.claimInterface(usbInterface, true) usbRequest = UsbRequest() usbRequest?.initialize(usbConnection, usbInEndpoint) } } /*    */ return usbConnection != null } 

Ici, nous voyons les interfaces et les points de terminaison mêmes qui ont été discutés dans la dernière section. Connaissant le numéro d'interface, nous trouvons les deux points d'extrémité, pour recevoir et transmettre, et initier une connexion USB. C'est tout, maintenant vous pouvez lire les données.

Comme je l'ai dit plus tôt, les appareils communiquent via des rapports.

  fun sendReport(data: ByteArray) { usbConnection?.bulkTransfer(usbOutEndpoint, data, data.size, 0) } fun getReport(): ByteArray { val buffer = ByteBuffer.allocate(REPORT_SIZE) val report = ByteArray(buffer.remaining()) if (usbRequest.queue(buffer, REPORT_SIZE)) { usbConnection?.requestWait() buffer.rewind() buffer.get(report, 0, report.size) buffer.clear() } return report } 

Nous envoyons un tableau d'octets à la méthode sendReport, dans laquelle l'octet zéro est l'ID du rapport, nous prenons la connexion USB actuelle à l'appareil et effectuons le transfert. En tant que paramètres, nous transférons le numéro de point final, les données, leur taille et le délai de transmission à la méthode BulkTransfer. Il convient de noter que la classe UsbDeviceConnection possède des méthodes pour implémenter l'échange de données avec un périphérique USB - les méthodes bulkTransfer et controlTransfer. Leur utilisation dépend du type de transmission pris en charge par un point d'extrémité. Dans ce cas, nous utilisons bulkTransfer, bien que l'utilisation de points de terminaison avec le type de contrôle soit la plus courante pour les HID. Mais nous avons un HID personnalisé, nous faisons donc ce que nous voulons. Je vous conseille de lire séparément le type de transmission, car le volume et la fréquence des données transmises en dépendent.

Pour recevoir des données, vous devez connaître la taille des données qui peuvent être obtenues, savoir à l'avance et obtenir du point de terminaison.

La méthode de réception des données via USB HID est synchrone et bloquante et doit être effectuée dans un thread différent.En outre, les rapports de l'appareil peuvent arriver en permanence ou à tout moment, vous devez donc implémenter une interrogation constante du rapport afin de ne pas manquer les données. Faisons-le avec RxJava:

 fun receive() { Observable.fromCallable<ByteArray> { getReport() } .subscribeOn(Schedulers.io()) .observeOn(Schedulers.computation()) .repeat() .subscribe({ /* check it[0] (this is report id) and handle data */ },{ /* handle exeption */ }) } 

Après avoir reçu un tableau d'octets, nous devons vérifier l'octet zéro, car il s'agit d'un ID de rapport et, selon lui, analyser les données reçues.

À la fin de toutes les actions avec USB, vous devez fermer la connexion. Vous pouvez le faire dans l'activité onDestroy ou dans onCleared dans ViewModel.

  fun close() { usbRequest?.close() usbConnection?.releaseInterface(usbInterface) usbConnection?.close() } 

Conclusion


L'article traite d'un code très petit et primitif, extrêmement démonstratif avec implémentation pour un périphérique spécifique. Bien sûr, il existe de nombreuses classes USB, pas seulement HID, et pour elles, bien sûr, la mise en œuvre sera différente. Cependant, toutes les méthodes sont assez bien documentées et ayant une bonne compréhension de la pile USB, vous pouvez facilement comprendre comment les utiliser.

X. Matériel utile


Source: https://habr.com/ru/post/fr470365/


All Articles