Les deux articles précédents ( ici et là ) nous ont appris comment un téléphone Android peut utiliser la liaison USB pour envoyer ses flux audio et recevoir des informations d'un périphérique HID ( souris ou clavier ), mais l'OS de Google est capable de faire mieux que ça... Il peut recevoir des identifiants venant du maître connecté, choisir une application à lancer pour communiquer avec ce maître et établir une connexion de données entre le maître et cette application.
Cet article va expliquer comment gérer cela côté Raspberry et côté Android.
Transformer notre Raspberry en périphérique "maison"
Android gère les docks et les périphériques HID nativement. Toutes les modifications que nous avons réalisées jusqu'à maintenant l'ont été sur la Raspberry. Le téléphone savait déjà ce qu'il avait à faire. Android peut également apprendre à gérer des périphériques USB spécifiques en déléguant la gestion à une application Java appropriée.
Nous allons nous intéresser à la façon de gérer un simple canal de données entre une application sur la Raspberry et une application sur le téléphone. Il est intéressant de noter que les périphériques spécifiques sont supportés depuis la version 3.1 (API 12) avec possibilité de backporter jusqu'à la version 2.3.4 (API 10).
Configuration de la connexion
Pour permettre au téléphone de trouver l'application appropriée, nous envoyons une série d'identifiants lors de la phase de configuration. (nous étudierons dans la partie Java de ce tutoriel comment Android utilise cette information pour trouver l'application à utiliser).
Les identifiants sont envoyés par les lignes de code suivantes dans usbAccConfig avant l'ordre de bascule du périphérique.
response = libusb_control_transfer(handle,0x40,52,0,0,(unsigned char*)MANUFACTURER,strlen(MANUFACTURER)+1,0); response = libusb_control_transfer(handle,0x40,52,0,1,(unsigned char*)MODEL,strlen(MODEL)+1,0); response = libusb_control_transfer(handle,0x40,52,0,2,(unsigned char*)DESCRIPTION,strlen(DESCRIPTION)+1,0); response = libusb_control_transfer(handle,0x40,52,0,3,(unsigned char*)VERSION,strlen(VERSION)+1,0); response = libusb_control_transfer(handle,0x40,52,0,4,(unsigned char*)URI,strlen(URI)+1,0); response = libusb_control_transfer(handle,0x40,52,0,5,(unsigned char*)SERIAL,strlen(SERIAL)+1,0);
Deux choses intéressantes sont à noter ici :
- Le champ URI ne sert pas à retrouver l'application. Il est affiché à l'écran du téléphone si aucune application n'est trouvée. Vous y mettrez typiquement un lien vers le play-store de google pour télécharger l'application.
- Les autres champs seront utilisés par Android pour rechercher une application capable de traiter les communications avec notre application.
Si vous configurez ainsi votre téléphone mais que vous n'installez pas l'application Java un pop-up devrait apparaître vous proposant d'ouvrir un navigateur vers l'URL que vous aurez configuré.
Communication, le côté Raspberry
Une fois que usbAccConfig a fini de faire basculer le téléphone en mode accessoire, le programme usbAccReadWrite peut être lancé. Ce programme lit des données sur stdin et les envoie vers le téléphone et reçoit les données du téléphone et les affiche sur stdout. usbAccReadWrite est un programme libusb un peu plus complexe qu'usbAccConfig (il utilise l'API asynchrone de libusb et doit surveiller à la fois stdin et les périphériques USB).
Le téléphone présente plusieurs interfaces USB. La première est celle dédiée au périphérique spécifique. Cette interface à deux endpoints, l'un pour la lecture, l'autre pour l'écriture. Notre programme ne fera qu'utiliser ces endpoints pour envoyer et recevoir des données en mode bulk. A nouveau, nous ne ferons que décrire brièvement le programme d'exemple. Le code étant par ailleurs assez simple...
- Parcours de la liste des périphériques USB.
- Recherche d'un périphérique ayant le vendorId de google et un productId correspondant à un périphérique Android en mode Accessory.
- Recherche de la première interface et des deux endpoints sur cette interface.
- Préparation des callbacks nécessaire aux transferts libusb.
- select sur stdin et sur le descripteur libusb pour lire du texte sur stdin et recevoir les messages depuis le périphérique USB.
Communication, le côté Android
Du côté Android la gestion des accessoires dédiés n'est pas non plus très compliquée. Mais comme le public de ce blog est sans doute plus à l'aise dans l'écriture de drivers C que dans l'écriture d'applications Java, nous allons aller un peu plus dans les détails. Vous trouverez les sources de l'application Android à la fin de cet article. Pour les compiler il suffit d'installer ADT (qui est un éclipse modifié) et d'importer le projet. Il y a deux fichiers fondamentaux dans notre application : AndroidManifest.xml et TestUsbApplication.java.
Le fichier AndroidManifest.xml
Le fichier AndroidManifest.xml est utilisé par le système Android pour avoir toutes les informations sur l'application et pouvoir la lancer correctement. Il permet à l'OS d'avoir toutes les informations nécessaires pour gérer l'application correctement :
- Quelles sont les permissions nécessaires pour que l'application fonctionne.
- Dans quelles circonstances doit-on lancer l'application.
- Quelles versions d'Android sont supportés par l'application.
- Quelles fonctionnalités spécifiques sont nécessaires sur le terminal pour utiliser l'application.
- etc...
Vous trouverez la documentation de ce fichier ici.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="fr.openwide.testusbapplication" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="12" android:targetSdkVersion="15" /> <uses-feature android:required="true" android:name="android.hardware.usb.accessory"/> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="fr.openwide.testusbapplication.TestUsbApplication" android:label="@string/app_name" android:launchMode="singleTask"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/accessory_filter" /> </activity> </application> </manifest>
Notre application n'est pas une application Android standard. Elle peut être lancée manuellement par l'utilisateur, mais elle doit également être lancée automatiquement par le système lorsque notre Raspberry est connectée.
Tout d'abord, la ligne uses-feature indique au système qu'il doit refuser d'installer cette application si la gestion de l'usb n'est pas présente. Elle indique aussi au play-store que l'application ne doit pas être listée pour les terminaux ne supportant pas les accessoires USB.
La section intent-filter décrit quand lancer l'activité principale (c'est à dire le code Java). Un intent en terminologie Android est un message sur le bus système décrivant un événement (l’équivalent Android d'un message D-Bus). Ce message peut être à destination d'une application particulière ou être broadcasté à toutes les applications. Dans notre cas nous voulons être lancé sur deux événements. Sur demande de l'utilisateur (MAIN) et sur branchement de notre accessoire USB.
Le branchement d'un accessoire USB génère un événement USB_ACCESSORY_ATTACHED mais la ligne meta-data nous permet d'être plus précis. Nous pointons le système sur un deuxième fichier contenant plus d'information. Il s'agit du fichier res/xml/accessory_filter.xml ci-dessous :
<?xml version="1.0" encoding="utf-8"?> <resources> <usb-accessory manufacturer="Openwide"/> </resources>
Celui-ci précise au système que seuls les accessoires qui ont envoyé Openwide comme nom de fabriquant nous intéressent. Nous avons vu dans le code de usbAccConfig que la Raspberry envoie ses identifiants lors de la connexion du périphérique USB.
Enfin, notons que, pour cet exemple, nous demandons que notre activité soit lancée en mode singleTask. Cela signifie que si l'activité est déjà en cours d'exécution et qu'un périphérique est connecté, le système Android enverra l'intent à l'activité existante au lieu d'en créer une nouvelle.
L'application Java
Notre application Android est une classe Java avec des points d'entrée correspondant aux différents événements du cycle de vie d'une application Android.
- onCreate : est appelé à la création de l'application. Il faut y créer toutes les ressources qui seront utilisées par l'application.
- onNewIntent : un callback spécifique aux applicationx singleTask comme la nôtre. Un événement aurait dû lancer notre application mais celle-ci était déjà lancée. Le nouvel intent est passé en paramètre au callback.
- onResume : un callback appelé lorsque l'application vient au premier plan (ce callback est également appelé après onCreate).
- onPause : un callback appelé quand l'application passe en arrière plan. Rappelons que les applications Android ne se terminent pas sur demande utilisateur, elles sont mises en pause. La terminaison d'une application n'a lieu que lorsque le système a besoin de libérer des ressources.
- onDestroy : la terminaison véritable de notre application. Il faut libérer toutes les ressources configurées dans onCreate.
onCreate va également créer un objet de type Receiver. Cet objet recevra les intents qui ne sont pas destinés aux événements ci-dessus, notamment les déconnexions ou les résultats de demande d'autorisation.
Pour nous aider dans la gestion de l'accessoire notre classe dispose de quatre fonctions secondaires :
- openAccessory : tente d'ouvrir l'accessoire passé en paramètre et renseigne les membres de notre classe. Si l'application ne dispose pas encore des droits pour accéder à l'accessoire, openAccessory fera la demande et rendra la main immédiatement. Le résultat de la demande nous parviendra sous forme d'un Intent (qui rapellera openAccessory avec le même device).
- closeAccessory : ferme l'accessoire et remet à null tous les membres de la structure.
- findAccessory : recherche dans la liste des accessoires si il y en a un que nous connaissons et appelle openAccessory si il en trouve un.
- treatIntent : reçoit un intent et le traite. Il peut s'agir du branchement d'un accessoire, du débranchement d'un accessoire ou de l'arrivée d'une autorisation d'utilisation d'un périphérique.
Notre classe utilise un thread secondaire (créé par openAccessory) pour gérer la lecture du périphérique. Ce thread se met en lecture bloquante sur le descripteur de fichier du périphérique et cette lecture ne peut pas être interrompue. La déconnexion du périphérique fera se terminer anormalement la lecture et nous permet de terminer notre Thread. Android gère les événements de façon fiable. L'application recevra bien tous les messages de connexion et de déconnexion.
Il y a de nombreux chemins de codes et le plus simple est de regarder le fichier source et de tester. L'application d'exemple affiche toutes ses actions sur la moitié inférieure de l'écran. Vous pouvez tester les scénarios suivants :
- Lancement de l'application sans périphérique.
- (dé)branchement du périphérique lorsque l'application n'est pas lancée.
- (dé)branchement du périphérique lorsque l'application est en pause.
- (dé)branchement du périphérique lorsque l'application est active.
- Lancement de l'application lorsque le périphérique est déjà branché.
- etc...
L'application d'exemple devrait toujours retrouver son périphérique et être capable de communiquer avec lui. Cet exemple ne fait que transmettre et afficher du texte mais cette base de communication devrait être suffisante pour vous permettre de développer vos propres applications.
Code source, exemples et limitation
Vous trouverez le code source des différentes applications ci-dessous
usbAccConfig
Cette application prend trois paramètres, un action (a ou s) et le vendor-id et product-id d'un périphérique USB. Il enverra les commandes de configuration au périphérique pour le mettre en mode dock audio ou en mode accessoire. Ce programme doit être lancé en tant que root pour avoir les droits d'accès sur le périphérique USB
usbAccReadWrite
Ce programme recherche un périphérique HID (il prend le premier qu'il trouve) et un périphérique Android configuré (à nouveau, il prend le premier qu'il trouve).
Si il a trouvé un périphérique HID il enverra les paquets vers le périphérique Android.
Si le périphérique Android est en mode accessoire et que l'application UsbTest est installée sur le terminal Android il ouvrira un canal de communication vers cette application, affichera les paquets reçu sur stdout et enverra ce qu'il lit sur stdin vers l'application.
Ce programme doit également être lancé en tant que root pour les mêmes raisons.
Attention. Ce programme va prendre la main sur un périphérique HID, le déconnectant totalement du kernel. Il suffit de débrancher et rebrancher le périphérique pour le récupérer.
buildroot Bundle
Cette archive contient les bases pour construire votre propre projet buildroot
- un defconfig pour la Raspberry
- un répertoire overlay contenant les fichiers modifiés
Notez que ce tutoriel utilise buildroot comme exemple mais que les techniques et logiciels utilisés sont courant sur toutes les distributions linux et qu'il est très facile d'adapter cela à d'autres distributions que ce soient des distributions embarquées ou bureautiques...
UsbTest
Il s'agit de l'application Android de test.
Cette application permet d'envoyer du texte vers l'hôte, d'afficher du texte envoyé depuis l'hôte et d'afficher des traces sur tous les événements que l'application gère.
Lors de l'écriture de cet article nous avons rencontré quelques limitations du système et des applications qu'il est utile de mentionner :
- Un bug Android empêche d'utiliser simultanément le mode Audio et le mode Accessoire. Si on tente de le faire, Alsa ne reconnaîtra pas correctement le périphérique Audio. Il s'agit d'un bug Android que vous pouvez suivre ici une analyse du bug sur la mailing list Alsa contient un patch kernel permettant de contourner le bug (le thread commence ici) Ce patch a été intégré dans le kernel 3.9 ainsi que dans les dernières mises à jour des kernels 3.0 et 3.4.
- Les applications de test ne gèrent pas pulseaudio (il faut lancer la commande manuellement). L'intégration de pulseaudio est possible dans une application réelle mais n'a pas été traitée ici.
- De même nous nous limitons au premier périphérique HID. Android peut gérer plusieurs périphériques HID simultanément, mais ce n'est pas géré dans le programme d'exemple
Bonjour,
Je vous remercie énormément pour ce tutoriel.
J'ai repris votre code source pour tester le transfert bidirectionnel de données entre ma Rpi et mon Smartphone. La transmission Smartphone - > Rpi se passe très bien contrairement à celle de mon Rpi vers mon smartphone qui bloque au niveau de la ligne mFin.read() de l'app android malgré l'envoie de données de la part de la Rpi "Sending nb sur la console". Pouvez vous m'expliquer comment je pourrais résoudre ce problème?
Bien cordialement,