samedi 7 décembre 2013

Client Server Bluetooth en java : le code commenté.

1. Préparation
2. Un peu de théorie sur bluetooth
3. Le code commenté
4. Le projet eclipse avec toutes les dépendances

Voici maintenant le code commenté du client et du server.

Le code client

package bluetooth;

import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;

/**
 * Le code client.
 * 
 * Ce code est à executer sur l'ordinateur qui sert de client.
 * 
 * Ce code met en oeuvre les trois étapes que nous avons décrites précédemment : 
 * 1 On découvre les appareils autour de nous (inquiry)
 * 2 Recherche de service sur les appareils trouvés.
 * 3 Utilisation du service 
 * 
 * On remarque que l'api thread est mis en oeuvre ici car la méthode startInquiry 
 * démarre dans un nouveau thread (thread enfant) mais le thread père doit disposer d'un
 * moyen de savoir quand le thread enfant à fini. Pour faire cela on utilise la technique dite du 
 * sémaphore. Si vous ne la connaissez pas je vous conseille de commencer par lire ces quelques explications :
 *  
 * http://www.javaworld.com/javaqa/1999-11/02-qa-semaphore.html.
 * 
 *  
 */
public class Client {

 /**
  * La liste des devices bluetooth trouvés.
  */
 public static final List devicesDiscovered = new ArrayList();

 /**
  * Le service que je recherche sur ce device, un service doit avoir un nom unique, on utilise 
  * un utilitaire uuidgen pour générer ces chaines de caractère uniques.
  * 
  * Sur le code du serveur c'est aussi cette chaine qui est utilisés pour publier le service.
  *  
  */
 final static String myServiceUUID = "2d26618601fb47c28d9f10b8ec891363";
 
 /**
  * L'uid doit respecter certaine règle comme par exemple être positif la chaine doit faire 
  * moins de 32 caractère etc. La classe UUID contrôle ces règles lors de la construction.
  */
 final static UUID MYSERVICEUUID_UUID = new UUID(myServiceUUID, false);
 
 /**
  * La liste des services que l'on souhaite obtenir sur le devices. 
  */
 final static UUID[] searchUuidSet = new UUID[] { MYSERVICEUUID_UUID };
 
 /**
  * Lorsque l'on obtient le service, à la demande du client le serveur peut fournir 
  * des sortes de métadonnées (pair clé-valeur) appelés attributs. On pourrait imaginer un client 
  * qui voudrait envoyer de la musique à une chaine stéréro souhaiterai connaitre la liste des
  * formats musicaux supportés par le services. Sans spécificiation une liste d'attributs par défaut est fourni.
  * @see javax.bluetooth.DiscoveryAgent.searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, DiscoveryListener discListener)
  */
 final static int[] attrIDs = new int[] { 0x0100 }; // Service handle
 
 public static void main(String[] args) throws IOException,
   InterruptedException {

  
  //inquiryCompletedEvent est un semaphore 
  //qui bloquera le thread a l'appel de sa méthode wait 
  //et qui le poursuivra a l'appel de sa méthode notifyAll
  final Object inquiryCompletedEvent = new Object();
  
  //serviceSearchCompletedEvent est un semaphore 
  //qui bloquera le thread a l'appel de sa méthode wait 
  //et qui le poursuivra a l'appel de sa méthode notifyAll
  final Object serviceSearchCompletedEvent = new Object();

  // on vide la listes des périphériques bluetooth que l'on va trouver
  devicesDiscovered.clear();

  //le listener qui déclenchera les actions quand des devices et les services seront trouvés.
  DiscoveryListener listener = new MyDiscoveryListener(devicesDiscovered, inquiryCompletedEvent, serviceSearchCompletedEvent);
  
  
  //dabord on cherche les devices 
  synchronized (inquiryCompletedEvent) {
   // on passe le listener à la méthode start inquiry, cette appel 
   // revient tout de suite car il a lieu dans un autre thread.
   boolean started = LocalDevice.getLocalDevice().getDiscoveryAgent()
     //GIAC correspond au profil générique.
     .startInquiry(DiscoveryAgent.GIAC, listener);
   if (started) {
    System.out.println("wait for device inquiry to complete...");
    // wait met en attente le thread principal qui attend 
    // maintenant qu'un autre thread le notifie.
    inquiryCompletedEvent.wait();
    System.out.println(devicesDiscovered.size()
      + " device(s) found");    
   }
  }
  
    //une fois les devices trouvés on cherche les services sur ces devices
    for (RemoteDevice rm : devicesDiscovered){
   synchronized (serviceSearchCompletedEvent) {
    System.out.println("search services on " +  rm.getBluetoothAddress() + " " + rm.getFriendlyName(false));
             LocalDevice.getLocalDevice().getDiscoveryAgent().searchServices(attrIDs, searchUuidSet, rm, listener);
    serviceSearchCompletedEvent.wait();
    System.out.println("Fin de la recherche de services");
   }
  }
  
  
  
  
 }

}

/**
 * on cree un listener que l'on passera à la méthode startInquiry pour
 * mieux comprendre le pattern listener
 * http://rom.developpez.com/java-listeners/.
 * Ce listener sera notifié a
 * chaque fois qu'un device sera découvert. Notifié signifie en fait que
 * ses méthodes seront appelées à chaque fois que le système découvrira
 * un device. On va donc redéfinir les méthodes pour mettre en oeuvre
 * nos actions à chaque dévouverte.
 * 
 * Les méthodes deviceDiscovered et inquiryCompleted du listener s'applique lors de l'inquiry,
 * alors que les méthodes servicesDiscovered et serviceSearchCompleted s'applique 
 * lors de la recherche de service sur un device. Je pense qu'ils auraient du créer 
 * deux listeners car ces deux actions sont quand même différentes. 
 * 
 *
 */
class MyDiscoveryListener implements DiscoveryListener {
 
 private List devicesDiscovered;
 
 private Object inquiryCompletedEvent;
 
 private Object serviceSearchCompletedEvent;
 
 
 /**
  * 
  * @param devicesDiscovered la liste des devices trouvés.
  * @param inquiryCompletedEvent le semaphore pour la recherhe de device.
  * @param serviceSearchCompletedEvent le semaphore pour la recherche de service.
  */
 public MyDiscoveryListener(List devicesDiscovered, Object inquiryCompletedEvent, Object serviceSearchCompletedEvent){
  this.devicesDiscovered = devicesDiscovered;
  this.inquiryCompletedEvent = inquiryCompletedEvent;
  this.serviceSearchCompletedEvent = serviceSearchCompletedEvent;
 }

 /**
  * Méthode qui sera appelee à chaque fois qu'un device est decouvert.
  */
 public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) {
  System.out.println("Device " + btDevice.getBluetoothAddress()
    + " found");
  // on alimente la liste des devices trouvés pour pouvoir
  // exploiter
  // cette information ultérieurement.
  devicesDiscovered.add(btDevice);
  try {
   System.out.println("     name "
     + btDevice.getFriendlyName(false));
   System.out.println("Blutooth Adress "  + btDevice.getBluetoothAddress());
  } catch (IOException cantGetDeviceName) {
   cantGetDeviceName.printStackTrace();
  }
 }

 /**
  * Méthode appelée quand le système estime avoir fini son travail
  * d'inquiry pour nous cela signifie que notre liste de device est
  * complète.
  */
 public void inquiryCompleted(int discType) {
  if (INQUIRY_COMPLETED==discType){
   System.out.println("Device Inquiry completed!");
   synchronized (inquiryCompletedEvent) {
    //on notifie cette objet pour permettre 
    //au thread principal de revenir de sa méthode wait.
    inquiryCompletedEvent.notifyAll();
   }
  }else if (INQUIRY_ERROR==discType){
   System.out.println("inquiry request failed to complete normally, but was not cancelled.");
  }else if (INQUIRY_TERMINATED==discType){
   System.out.println("device discovery has been canceled by the application and did not complete.");
  }
 }

 

 /**
  * Quand l'ensemble des services publiés que vous avez souhaité
  * retrouver sont découverts cette méthode est appelée.
  * 
  * A chaque service correspond un service record c'est pourquoi 
  * on reçoit un tableau, qui correspond au tableau de service que vous avez demandé 
  * lors de l'appel de  javax.bluetooth.DiscoveryAgent.searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, DiscoveryListener discListener)
  * 
  * Si sur le device serveur vous n'avez publié 
  * qu'un seul service vous  recevrez logiquement un tableau avec un seul élément.
  * 
  * Au sein de ces services record se trouve des serviceAttribute. 
  * Les attributs par défaut sont fourni mais il est possible de 
  * demander à recevoir d'autres attributs. 
  * 
  * @see javax.bluetooth.DiscoveryAgent.searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, DiscoveryListener discListener)
  * 
  */
 public void servicesDiscovered(int transID,
   ServiceRecord[] servRecords) {
      
  for (ServiceRecord sr : servRecords){
   System.out.println("Service trouvé " + sr.getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false) );
   
   //je vais maintenant pouvoir parler au sevice !!!!
   System.out.println("Envoie du message en cours");
   //on récupère l'url du service 
   //on pourrait sauvegarder cette url 
   //pour un acces direct au service sans passer
   //par la phase d'Inquiry
         String connectionURL =  sr.getConnectionURL (
                 ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
         try {
             System.out.println(
                 "Connection a " + sr.getHostDevice().getFriendlyName(false) + 
                 ", " + connectionURL);
             StreamConnection streamConnection =  (StreamConnection) Connector.open (connectionURL);
             DataOutputStream dataout = 
                 streamConnection.openDataOutputStream();
             //maintenant j'envoie hello world au serveur.
             dataout.writeUTF("Hello World");
             System.out.println("Message envoye, fermeture du canal");
             streamConnection.close();
         } catch (IOException ioe) {
             System.out.println(
                 " exception & + ioe");
         }
  }
 }
 
 /*
  * Cette méthode sera appelée quand la recherche de service 
  * sera finie. Comme dans cette exemple on parle immédiatement 
  * au service dès qu'on le trouve. elle ne nous sera pas d'une 
  * grande utilité.
  *  
  * @see javax.bluetooth.DiscoveryListener#serviceSearchCompleted(int, int)
  */
 public void serviceSearchCompleted(int transID, int respCode) {
  System.out.println("Service search completed!");
  synchronized (serviceSearchCompletedEvent) {
   //on notifie cette objet pour permettre 
   //au thread principal de revenir de sa méthode wait.
   serviceSearchCompletedEvent.notifyAll();
  }
 }
}


Le code Serveur

package bluetooth;

import java.io.DataInputStream;
import java.io.IOException;

import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.UUID;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

/**
 * C'est un serveur bluetooth il ne fait que répéter dans la console 
 * le message qu'il a reçu.
 * 
 * Ce code est a executer sur l'ordinateur qui sert de serveur.
 * 
 * @author michael
 *
 */
public class Server {
 
 public static void main(String[] args) throws IOException {
  // le nom du service car l'uuid n'est pas très parlant ...
  final String myServiceName = "echoService";
  /**
   * Le service que je publie sur ce device, un service doit avoir un nom unique, on utilise 
   * un utilitaire uuidgen pour générer ces chaines de caractère uniques.
   * 
   * Sur le code du client c'est aussi cette chaine qui est utilisés pour obtenir le service.
   *  
   */
  final String myServiceUUID = "2d26618601fb47c28d9f10b8ec891363";
  UUID MYSERVICEUUID_UUID = new UUID(myServiceUUID, false);

  // Définit l'url du service.
  //localhost car on est le serveur 
  String connURL = "btspp://localhost:"+MYSERVICEUUID_UUID.toString()+";name="+myServiceName;
 
  //on se rend découvrable 
  LocalDevice.getLocalDevice().setDiscoverable(DiscoveryAgent.GIAC);
  
  System.out.println("Creation d'un service " + connURL);
  
  // On publie le service record dans le SRDB (Service record database)
  StreamConnectionNotifier scn = (StreamConnectionNotifier)  
                                           Connector.open(connURL);                   
 
  // On accepte la connexion d'un client, tant qu'aucun client 
  //ne frappe à la porte cette méthode bloque.
  StreamConnection sc = scn.acceptAndOpen();
  //on lit ce que le client nous envoie puis on l'écrit dans la console.
  DataInputStream dataIn = sc.openDataInputStream();
  String s = dataIn.readUTF();
  System.out.println("Echo > " + s);
  sc.close();
  scn.close();
 }
 
 

}