Pour ceux qui se souviennent, dans le film de James Bond - Tomorrow Never Dies (demain ne meurt jamais), Pierce Brosnan se voit équipé par l'un de mes idoles: "Q", d'une voiture télécommandable par son mobile GSM. Un gadget à la James-Bond disponible pour tout le monde dès à présent par cet article ;-). Nous ne télécommanderons pas la voiture, mais le robot mobile. (Mais après vous faites ce que vous voulez...).
Pour ceux qui auraient raté un épisode: Séance de rattrapage qui explique le fonctionnement du "cerveau" du robot mobile ici.
Objectif: en déplaçant le doigt sur l'écran le robot se dirigera vers l'orientation souhaitée, à la vitesse souhaitée.
Le matériel nécessaire est un device, mobile GSM ou tablette android (version d'Android 2.1 au minimum) munie d'une connexion Bluetooth grâce à laquelle on va communiquer avec le module bluetooth <-> uart du robot mobile. Il faudra également les kits de développement logiciel (sdk) pour Android téléchargeables gratuitement ici.
Valider le mode développement
Première chose à faire après avoir installé le sdk est d'autoriser l'installation et le fonctionnement d'application en mode debug sur votre device Android. Pour cela, allez dans "Paramètres" > "Applications" > "Développement" > et activez "Débogage USB".
Création du projet
Démarrez Eclipse, puis créez un nouveau projet. Choisissez minimum Android 2.1 comme compatibilité. Personnellement je l'ai appelé "RobotMobile" et j'ai donné comme nom au package "your.robotmobile.roboticus".
Une application Android doit avoir des permissions pour utiliser les ressources du device, comme par exemple le bluetooth. Ces autorisations doivent être données dans le Manifest de l'application que l'on retrouve dans le projet sous le nom de "AndroidManifest.xml". Il faut ajouter deux lignes octroyant les permissions d'utiliser le Bluetooth. Voici le contenu du Manifest après l'intégration:
Manifest de l'application android pour télécommandé le robot depuis le bluetooth | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="your.robotmobile.roboticus" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="7" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".RobotMobileActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> |
Le Layout
Le layout est l'interface graphique de l'application. L'application écrite ici doit permettre en déplaçant son doigt sur l'écran d'envoyer une vitesse pour chaque moteur au robot. L'application doit également se connecter au robot et afficher les informations transmises. Le Layout aura donc besoin, d'un bouton de connexion, d'un TextView pour afficher des messages et d'une surfaceview pour détecter les mouvements du doigt. Le fichier contenant l'interface graphique principale de l'application se trouve dans "Res" > "Layout" > "main.xml". Et le contenu du fichier pour cette application est le suivant:
Layout de l'application | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:id="@+id/Position" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> <Button android:id="@+id/Connect" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Connect" /> <SurfaceView android:id="@+id/Touch" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="0.76" /> </LinearLayout> |
Le résultat est un design minimaliste et simple pour mettre en avant le fonctionnement de l'application:
Ne pas oublier de sauvegarder le fichier "main.xml", sinon vous aurez des erreurs à la compilation.
La communication Bluetooth
S'interfacer et communiquer avec le module Bluetooth est une histoire de programmation pour laquelle il existe des modules tout fait permettant de simplifier grandement la tâche. J'ai personnellement utilisé un objet de communication que j'ai trouvé sur le site de Nononux. Il suffit de glisser le fichier "BtInterface.java" (disponible avec le reste des téléchargements) dans votre package "src" > "your.robotmobile.roboticus" pour que la classe soit ajoutée au projet. Il faut ensuite modifier dans ce fichier le nom du package par celui de cette application :
1 |
package your.robotmobile.roboticus;
|
Cette classe permet de créer une communication avec le module Bluetooth <-> UART utilisé, d'envoyer des trames qui seront directement émises sur la sortie UART du module et de recevoir les messages reçu sur l'entrée UART du module.
Pour simplifier la paramétrisation, j'ai légèrement modifié le code du fichier BtInterface.java en ajoutant dans le constructeur un paramètre donnant le nom du module bluetooth (préalablement ce nom était encodé dans le constructeur). Voici le nouveau code (pour ceux qui aurait du mal, je mets à disposition tout le projet en téléchargement à la fin de l'article).
Constructeur de la classe de communication bluetooth modifié pour accepter en paramètre le nom du module | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public BtInterface(Handler hstatus, Handler h, String name) { Set<BluetoothDevice> setpairedDevices = BluetoothAdapter.getDefaultAdapter().getBondedDevices(); BluetoothDevice[] pairedDevices = (BluetoothDevice[]) setpairedDevices.toArray(new BluetoothDevice[setpairedDevices.size()]); for(int i=0;i<pairedDevices.length;i++) { if(pairedDevices[i].getName().contains(name)) { device = pairedDevices[i]; try { socket = device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")); receiveStream = socket.getInputStream(); sendStream = socket.getOutputStream(); } catch (IOException e) { e.printStackTrace(); } break; } } handler = hstatus; receiverThread = new ReceiverThread(h); } |
Une brève explication du constructeur:
Parmi tous les devices connectés au mobile, le programme cherche celui qui porte le nom donné en paramètre du constructeur (variable string name). S'il le trouve il va tenter de créer une communication avec. Cette communication est définie et pour le faire remarquer il précise un UUID spécifique à savoir dans ce cas : "00001101-0000-1000-8000-00805F9B34FB".
Pour communiquer avec le programme principale, la classe utilise deux "Handler" un, qui est défini par la variable "hstatus", permet d'envoyer un message au programme sur l'état de la connexion. Le deuxième, défini par la variable "h", permet de communiquer au programme principal les messages reçus de la carte. (Ces deux handler doivent apparaitre dans le code du programme principal que nous découvrirons plus bas).
En fin du constructeur nous voyons que le handler de status est enregistré dans la variable "handler" qui est une variable globale accessible par les autres méthodes de la classe. Le handler "h" est quant à lui envoyé en paramètre de la création d'un Thread "receiver" qui est une tâche qui va tourner en fond de l'application et à chaque fois qu'elle va recevoir un message de la carte va le renvoyer au programme principal au travers du handler.
Le programme principal
La méthode onCreate est la première méthode appelée par le programme, elle permet d'initialiser le reste. Nous allons initialiser les ressources, pour le TextView "myPosition", on va indiquer que l'application à bien démarré. Pour la surface "mySurface" lui indiquer que lorsqu'elle est touchée le programme doit générer un évènement (setOnTouchListener). Pour le bouton "connect" lui indiquer que lorsqu'il y a un click, il faut générer un évènement. Eclipse va certainement vous mettre en erreur les deux évènements et vous proposez plusieurs possibilités pour corriger ces erreurs. Choisissez "Let implement setOn..." dans les deux cas. En dernier nous initialisons la communication Bluetooth avec le module souhaité (dans mon cas, le nom par défaut était "linvor", ce nom est facilement trouvable lorsque vous allez dans les périphériques bluetooth connectés à votre mobile, les noms des devices sont affichés).
L'initialisation | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); /* Initialise les ressources */ myPosition = (TextView)findViewById(R.id.Position); myPosition.setText("Lancement ok"); mySurface = (SurfaceView)findViewById(R.id.Touch); mySurface.setOnTouchListener(this); connect = (Button)findViewById(R.id.Connect); connect.setOnClickListener(this); /* "linvor" est le nom par défaut de la carte Bluetooth<->Uart */ bt = new BtInterface(handlerStatus, handler, "linvor"); } |
Le onClickListener est la méthode appelée quand on appuie sur le bouton:
onClickListener | |
1 2 3 4 5 6 |
/* Méthode appelée quand on clique (OnClickListener) */ public void onClick(View v) { if(v == connect) { bt.connect(); } } |
Le onTouchListener (commenté dans le code) est la méthode appelée lorsque l'on touche le surfaceView, elle peut être divisée en trois cas, soit on vient de toucher, soit on vient de relâcher, soit on vient de bouger le doigt:
onTouchListener | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
public boolean onTouch(View arg0, MotionEvent event) { /* action quand le doigt touche la surface */ if(event.getAction() == MotionEvent.ACTION_DOWN){ } /* action quand le doigt bouge sur la surface */ else if(event.getAction() == MotionEvent.ACTION_MOVE){ /* Calcule de la position en pourcent par rapport au centre de la surface */ float posx = (100*(event.getX()- mySurface.getWidth()/2)/(mySurface.getWidth()/2))/100; float posy = (-100*(event.getY()-mySurface.getHeight()/2)/(mySurface.getHeight()/2))/100; /* Si jamais android nous jouait un tour */ if (posx >1 ) posx=1; if (posy >1 ) posy=1; if (posx <-1 ) posx=-1; if (posy <-1 ) posy=-1; /* affiche la position touchée dans le label */ myPosition.setText("Touché " + posx + " " + posy ); /* On crée la commande à envoyer au robot dans un premier temps les roues tourne vers l'avant*/ String cmd=""; char rouedroite ='F'; char rouegauche ='F'; int vitessedroite; int vitessegauche; /* calcule des vitesses de chaque roue permettant de tourner ou d'avancer en ligne droite * utilisation de pythagore 48000² = 33941² + 33941²*/ if (posx>0){ vitessegauche = 48000-(int)(Math.sqrt((double)(33941*posx)*(33941*posx)+(33941*posy)*(33941*posy))); vitessedroite= 48000-(int)((Math.sqrt((double)(33941*posx)*(33941*posx)+(33941*posy)*(33941*posy)))*(1-posx)); } else{ vitessegauche = 48000-(int)((Math.sqrt((double)(33941*posx)*(33941*posx)+(33941*posy)*(33941*posy)))*(1+posx)); vitessedroite= 48000-(int)((Math.sqrt((double)(33941*posx)*(33941*posx)+(33941*posy)*(33941*posy)))); } /* en avant ou en arrière */ if (posy<0) { rouedroite = 'B'; rouegauche = 'B'; } /* ou si pas de vitesse */ if (posy==0) { vitessegauche = 48000; vitessedroite= 48000; } /* on solidarise les éléments de la commande et on termine par 'e' comme caractère de fin */ cmd= "R"+rouedroite+""+vitessedroite+"L"+rouegauche+""+vitessegauche+"e"; myPosition.setText(cmd); /* on envoie la commande */ bt.sendData(cmd); } /* Action quand le doigt se retire */ else if(event.getAction() == MotionEvent.ACTION_UP) { myPosition.setText("Hop c'est remonté"); } return true; } |
Les handlers: Chaque handler possède une méthode handleMessage qui est appelée quand un message est envoyé par BtInterface.
Handler | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
final Handler handler = new Handler() { public void handleMessage(Message msg) { String data = msg.getData().getString("receivedData"); /* Décommenter si on veut le message renvoyer par la carte */ // myPosition.setText(data); } }; final Handler handlerStatus = new Handler() { public void handleMessage(Message msg) { int co = msg.arg1; if(co == 1) { myPosition.setText("Connected\n"); } else if(co == 2) { myPosition.setText("Disconnected\n"); } } }; |
Conclusion, vidéo et téléchargement:
Comme vous allez pouvoir le constater dans la vidéo ci-dessous, ça fonctionne, on envoie bien des commandes au robot et ce dernier réagit. Il est dès lors possible de faire communiquer un device Android avec le Robot Mobile, de pouvoir envoyer les informations des capteurs du robot à un mobile GSM ou tablette et de répondre avec différentes commandes. L'application écrite ici venait principalement en exemple pour la mise en place de cette communication, ce qui est chose réussie. Quand à la qualité du pilotage... je ne m'en vanterai pas ;-) et serait à perfectionner si le but recherché y était.
Je vous invite à télécharger le projet Android ici.
Le robot mobile télécommandé depuis un device Android.
La suite ?
Pour la suite, nous allons nous attarder sur différents types de capteurs et les utiliser dans l'une ou l'autre application sympa ;-)