TP – Cas d'étude : Internet of Things
BUTS PÉDAGOGIQUES
- Revoir l'écriture de classes et l'implémentation d'interfaces en Java
- Revoir les notions d'agrégation, d'héritage et de polymorphisme
- Lecture et écriture dans des fichiers textes
- Connexion au SGBD MySQL avec JDBC
- Communication réseau avec une socket
- Apprendre à s'appuyer sur une API et à utiliser sa documentation
Le contexte du cas d'étude traité dans ce TP est l'internet des objets. Aujourd'hui, le Web et les services Internet sont alimentés par les utilisateurs. Demain, ils seront alimentés par les objets connectés. Montres, chaussures… réfrigérateurs, plantes vertes… entreprises, entrepôts, production… la croissance des années 2010 atteint plusieurs dizaines de milliards d'objets connectés (soit beaucoup plus que les quelques milliards de smartphones…).
Par exemple, le ICEdot Crash Sensor, qui est déjà commercialisé, se fixe sur un casque de vélo. En cas de choc, détecté par son gyroscope et son accéléromètre, ICEdot Crash Sensor envoi un signal au smartphone du cycliste. Un compte à rebours est lancé… si le cycliste ne désactive pas le compte à rebours, alors les secours sont prévenus :
Les objets connectés ne fonctionnent pas seuls. Leurs capacités de stockage et de calcul restent très limitées et toute une infrastructure distante vient en appui. Au sein de cette infrastructure, les plateformes centrales reçoivent les données des objets connectés et mettent ces données à disposition des services qu'elles hébergent. Les données massives produites par les objets connectés sont traitées et utilisées par des logiciels spécifiques (p. ex. Big Data, Machine Learning, Business Intelligence). Dans ce TP, nous allons nous focaliser sur la plateforme centrale de l'Internet des objets :
Votre travail, dans ce TP, est de développer la plateforme centrale avec le langage Java et son API. La plateforme recevra des datagrammes de données (censés provenir d'objets connectés) et les empilera dans des fichiers de journalisation (données semi-structurées). Ce travail a été décomposé en cinq étapes de développement, ce qui correspond aux cinq versions successives du programme qui suivent. Ci-dessous une illustration du programme que vous allez obtenir :
Exercice 1 • Développer les classes principales (Version 1)
L'objectif est de créer les classes principales de la plateforme IoT pour tester une première version simple (qui sera complétée dans les versions suivantes). Les datagrammes de données seront saisis depuis le clavier par l'utilisateur et écrits dans des fichiers par la plateforme.
Le macro-diagramme de classe (sans les méthodes, ni les attributs), des cinq classes et de l'interface, de la Version 1, est illustré ci-après :
- La classe Run est le point de départ de l'exécution du programme.
- La classe Service modélise les services. Elle écrit les données des objets connectés dans des fichiers journaux (fichiers logs ou encore fichiers de traces).
- La classe Thing modélise les objets connectés. Elle permet de stocker les données des objets connectés. Elle connaît les services souscrits par l'objet connecté.
- La classe KeyboardInput lit des données saisies au clavier.
- La classe Platform gère les services, les objets connectés et les souscriptions. Elle lit les données émises par le clavier et les envoient aux objets connectés correspondants, puis demande l'écriture des données dans les fichiers de logs.
Pour mieux comprendre le programme qui va être écrit, ci-dessous le macro-diagramme de séquence d'un scénario nominal avec deux Thing, deux Services et l'envoi de deux datagrammes. Cliquez pour afficher le diagramme de séquence correspondant.
Question 1.1 : La classe Thing
Pour écrire la classe Thing, nous allons utiliser deux structures de données (très utiles) de l'API Java : les HashMap et les ArrayList.
L'utilisation des ArrayList est très proche des tableaux que vous connaissez déjà (mais qui eux sont de taille fixe). Il s'agit de stocker des données et d'y accéder par des indices entiers. En revanche, les ArrayList implémentent de nombreuses interfaces de l'API Java (Iterable<E>, Collection<E>, List<E>, RandomAccess) et offrent de nombreuses méthodes pour les manipuler. Vous allez utiliser cette structure de données pour référencer les services souscrits par la classe Thing.
Les HashMap servent toujours à stocker des données, mais l'indexation est différente. Il est, par exemple, possible d'indexer les données avec des chaînes de caractères (clés). Alors, pour accéder à un élément, il suffit de donner la clé pour directement obtenir la valeur recherchée. Ce genre d'association clé ⇒ valeur permet de retrouver rapidement une valeur et évite les nombreux if qu'il aurait fallu écrire en utilisant des tableaux. De plus, cette indexation par clé est indifférente au changement de position de la valeur, alors que dans un ArrayList la valeur de l'index peut par exemple changer suite à une insertion ou une suppression. Vous allez utiliser cette structure de données pour associer, dans la classe Thing, la clé d'une donnée (qui sera une chaîne de caractères de trois lettres) et sa valeur (qui sera une chaîne de caractères).
Le diagramme UML de la classe Thing est le suivant :
Créez la classe Thing, ses attributs et ses méthodes. Quelques remarques d'implantation :
- Entre autres choses, il ne faut pas oublier d'instancier les deux attributs de classes ArrayList et HashMap dans le constructeur Thing() :
this.arrServices = new ArrayList<Service>() ;
this.mapData = new HashMap<String, String>() ; - La méthode putData() utilisera la méthode put(K, V) de la classe HashMap, pour associer la clé et la donnée dans l'attribut
mapData
. - La méthode getData() utilisera la méthode get(Object) de la classe HashMap, pour retourner l'élément correspondant à la clé dans l'attribut
mapData
. - La méthode setFromDatagram() remplira l'attribut
mapData
à partir de l'analyse d'un datagramme (en s'appuyant sur la méthode putData() écrite précédemment).- Les datagrammes seront des chaînes de caractères formées avec une clé de 3 lettres, un espace et la valeur de la donnée, puis un point-virgule pour séparer les données suivantes. Il n'y a pas de limite dans le nombre de données que peut comporter un datagramme.
- Par exemple, avec trois données : geo 43.433331 -1.58333;pul 128;bat 90.0
- Ou encore avec une seule donnée : tem 12.3C
- Aides :
Pour décomposer le datagramme en sous-chaînes selon le caractère point-virgule « ; », le plus simple est d'utiliser la méthode split() de la classe String.
Par exemple, l'appel sur le datagramme précédent, selon le caractère « ; », produira un tableau de trois chaînes de caractères de longueurs respectives 22, 7 et 8 :
String[] tabExplode = datagram.split(";") ;
Et pour parcourir le tableau obtenu, il faut connaître le nombre d'éléments donné par : tabExplode.lengthPour récupérer les trois premiers caractères de la sous-chaîne, le plus simple est d'utiliser la méthode substring(int, int) de la classe String, avec les indices 0 pour la position de début et 3 pour la position de fin (mais le caractère de position 3 sera ignoré). Par exemple, sur la première chaîne du tableau :
String key = tabExplode[0].substring(0, 3) ;Pour récupérer les caractères suivants de la sous-chaîne, le plus simple est d'utiliser la méthode substring(int) de la classe String, avec 4 comme position de début. Par exemple, sur la première chaîne du tableau :
String dat = tabExplode[0].substring(4) ;
- La méthode existData() utilisera la méthode containsKey() de la classe HashMap, pour savoir si la clé est présente parmi les clés de l'attribut
mapData
. - La méthode resetData() utilisera la méthode clear() de la classe HashMap pour vider l'attribut
mapData
. - La méthode subscribe() utilisera la méthode add(E) de la classe ArrayList pour insérer dans l'attribut
arrServices
. - La méthode toString() retournera une chaîne de caractères obtenue à partir de la concaténation (opérateur +) des éléments suivants :
- l'adresse MAC
- un point-virgule « ; »
- le id de l'utilisateur
- un point-virgule « ; »
- les valeurs (uniquement, pas les clés) des données, avec un point-virgule entre chaque. Aide :
Pour parcourir le HashMap, procédez comme suit, avec un itérateur :
Iterator<Map.Entry<String, String>> it = this.mapData.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> couple = it.next() ;
String dat = (String)couple.getValue() ;
}
Quelques explications :
• Lors de son initialisation, l'itérateur est positionné au début du HashMap.
• L'appel it.hasNext() retourne vrai si des éléments peuvent encore être lus.
• L'appel it.next() retourne le prochain élément qui peut être lu.
- La méthode update() sera écrite plus tard (à la question 1.4). En attendant, testons déjà ce qui a été écrit, à l'aide de la classe Run de la question suivante.
Question 1.2 : La classe Run
Écrivez la classe Run :
- Instanciez un objet t1 de classe Thing, avec comme adresse MAC "f0:de:f1:39:7f:17" et comme id utilisateur "1".
- Pour plus de convivialité, affichez le message "Welcome on IoT central platform." au début du programme et "bye." à la fin du programme.
- Appelez la méthode setFromDatagram() de t1 avec ce datagramme :
"geo 43.433331 -1.58333;pul 128;bat 90.0" - Affichez l'objet t1 grâce à sa méthode toString() et un appel à System.out.println().
Bien entendu, compilez et testez l'exécution. (Si besoin, créez une classe Service vide. Elle sera complétée à la question suivante.)
Question 1.3 : La classe Service
Rappel de notation : le symbole croisillon « # » indique le niveau de visibilité protected.
Écrivez la classe Service. Elle possède un attribut de la classe PrintWriter qui permet d'écrire dans des fichiers. Quelques remarques d'implantation :
- Parmi les choses à faire dans le constructeur, il faut notamment initialiser l'attribut
pw
qui permettra d'écrire dans le fichier de log associé au service (c.-à-d. un fichier journal ou encore fichier de trace). Pour ouvrir un fichier, il faut procéder en plusieurs étapes :- Créer un FileWriter en donnant un nom de fichier. Par contre, attention, le nom du fichier dans lequel le service écrira sera composé de "log_", suivit du nom du service, puis termine avec l'extension ".txt".
FileWriter fw = new FileWriter ("nom_du_fichier_a_ecrire.txt", true); // true pour ecrire a la fin si fichier existe deja - Ensuite, créer un BufferedWriter à partir du FileWriter :
BufferedWriter bw = new BufferedWriter (fw); - Enfin, créer le PrintWriter à partir du BufferedWriter :
this.pw = new PrintWriter (bw);
- Créer un FileWriter en donnant un nom de fichier. Par contre, attention, le nom du fichier dans lequel le service écrira sera composé de "log_", suivit du nom du service, puis termine avec l'extension ".txt".
- La méthode writeData() ajoute une ligne dans le fichier (qui aura été ouvert par le constructeur). Pour écrire dans le fichier, il suffit d'appeler la méthode println() de l'attribut pw (qui aura été initialisé par le constructeur).
Chaque ligne du fichier commencera avec la date, suivit d'un ";" puis la description de l'objet thing obtenue avec sa méthode toString(). Pour formater la date sous forme d'une chaîne de caractères, faîtes comme suit :
Date now = new Date () ;
SimpleDateFormat formater = new SimpleDateFormat ("yyyy-MM-dd H:m:s");
String d = formater.format (now);
Une fois la ligne écrite, il faut synchroniser les entrées-sorties (qui sont temporisées par un tampon, ou buffer) en appelant la méthode flush() de la classe PrintWriter sur l'attribut pw.Littéralement, le mot flush veut dire « tirer la chasse d'eau ». En jargon informatique, le verbe flusher veut dire demander de « nettoyer » ou « vider » une ressource intermédiaire (tampon, cache…).
- Enfin, la méthode close() ferme le fichier. Pour ce faire, elle flushe le fichier, puis fait appel à la méthode close() de l'attribut pw.
Une fois la classe terminée, revenez dans la classe Run :
- Instanciez un objet s1 de classe Service en l'appelant « mon_service »
- Demandez à cet objet s1 d'écrire les données de t1 avec sa méthode writeData().
- N'oubliez pas d'appeler la méthode close() de s1 avant de terminer le programme.
Bien entendu, compilez et testez l'exécution !
- Regardez sur le disque si le fichier a bien été créé (dans le répertoire de votre projet Eclipse) et ouvrez-le avec Notepad++ pour consulter le contenu.
- Exécutez plusieurs fois encore et regardez que les lignes ont été ajoutées et que l'heure change à chaque ligne.
Question 1.4 : La méthode update() de la classe Thing
Nous allons ajouter une nouvelle méthode à la classe Thing : void update (). Son rôle est d'appeler la méthode writeData() de chaque service souscrit pour que les données de l'objet soit transmises à chaque service. Aide :
Vous avez deux principales manières de procéder pour parcourir le ArrayList :
1. Soit en parcourant les indices :
/* Parcours ArrayList avec les indices */
for (int i=0 ; i < this.arrServices.size() ; i++) {
Service service = this.arrServices.get(i) ;
}
2/ Soit en utilisant un itérateur :
/* Parcours ArrayList avec iterateur */
Iterator<Service> it = this.arrServices.iterator();
while (it.hasNext()) {
Service service = it.next() ;
}
• Lors de son initialisation l'itérateur est positionné au début du HashMap.
• L'appel it.hasNext() retourne vrai si des éléments peuvent encore être lus.
• L'appel it.next() retourne le prochain élément qui peut être lu.
Choisissez la manière que vous comprenez le mieux.
Une fois la classe terminée, revenez dans la classe Run :
- Créez quatre nouveaux services s2, s3, s4 et s5.
- Abonnez l'objet t1 à ces nouveaux services en utilisant sa méthode subscribe(). Appelez la méthode update() de t1.
Bien entendu, compilez et testez l'exécution !!!
- Regardez sur le disque si les fichiers journaux ont bien été créés et ouvrez-les avec Notepad++ pour consulter leurs contenus.
- Exécutez plusieurs fois encore et regardez que les lignes sont bien ajoutées.
Question 1.5 : L'interface DataReceiver et la classe KeyboardInput
Écrivez le code de l'interface DataReceiver :
La classe KeyboardInput implémente l'interface DataReceiver. Le comportement de sa méthode readDatagram() sera de lire des datagrammes depuis le clavier.
Écrivez la classe KeyboardInput. Quelques remarques d'implantation :
- Le constructeur KeyboardInput() initialise l'attribut ok à faux.
- La méthode open() initialise l'attribut ok à vrai.
- La méthode ready() renvoi l'état de l'attribut ok.
- La méthode readDatagram() lit des lignes en provenance du clavier et retourne une chaîne de caractères décrivant les données :
- Pour lire depuis le clavier, il faut utiliser un BufferedReader :
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Sa méthode readLine() permet de lire au clavier à chaque appel. - D'abord, la méthode readDatagram() demandera à l'utilisateur de saisir l'adresse MAC de l'objet connecté :
- Puis, demandera de saisir le datagramme :
- Et enfin, retournera une chaîne qui commence avec l'adresse MAC, suivie d'un ";" puis le datagramme.
- Attention, si l'utilisateur saisit le mot « quit » plutôt qu'une adresse MAC, alors l'attribut ok deviendra faux et la valeur retournée sera la valeur null.
- Pour lire depuis le clavier, il faut utiliser un BufferedReader :
- La méthode close() bascule l'attribut ok à faux.
Une fois l'écriture de la classe terminée, revenez dans la classe Run :
- Instanciez un objet k de classe KeyboardInput.
- Invoquez sa méthode open().
- Puis lisez un datagramme avec cet objet en utilisant sa méthode readDatagram(), et affichez le datagramme lu.
- Enfin, n'oubliez pas ensuite d'appeler sa méthode close().
Bien entendu, compilez et testez l'exécution.
Question 1.6 : La classe Platform
Écrivez la classe Platform. Quelques remarques d'implantation :
- Comme toujours, le constructeur Platform() doit instancier les attributs, qui seront ici de classes HashMap et ArrayList (c.-à-d. comme dans la classe Thing).
- La méthode addThing() permet d'enregistrer un nouvel objet connecté à la plateforme. Pour ce faire, utiliser la méthode put(K, V) de la classe HashMap afin d'ajouter le nouvel objet de la classe Thing à l'attribut mapThings et qui sera référencé avec son adresse MAC comme clé.
- La méthode addService() permet d'enregistrer un nouveau service auprès de la plateforme. Pour ce faire, utiliser la méthode add(E) de la classe ArrayList afin d'ajouter un nouvel objet de la classe Service à l'attribut arrServices.
- La méthode run() lit des datagrammes tant que le DataReceiver est disponible.
- Pour chaque datagramme lu, elle extrait une sous-chaîne des 17 premiers caractères qui correspondent à l'adresse MAC (Pour ce faire, utiliser la méthode substring(int, int) déjà utilisée précédemment).
- À partir de cette adresse MAC, elle essaye de retrouver l'objet de la classe Thing qui correspond, parmi les objets présents sur la plateforme. Pour ce faire, nous utilisons la méthode get() de la classe HashMap.
- Si aucun objet de la classe Thing n'a été trouvé (c.-à-d. la méthode get() de HashMap a retourné la valeur null), alors nous le signalons par un message à l'écran et nous ignorons le datagramme (car cette adresse MAC ne correspond à aucun objet enregistré sur la plateforme).
- Si un objet de la classe Thing a été trouvé, l'analyse du datagramme continue afin d'affecter de nouvelles valeurs à l'objet.
- Nous extrayons du datagramme une sous-chaîne correspondant aux données, avec les caractères qui suivent après le ";" de la position 17 et jusqu'à la fin (Pour ce faire, utiliser la méthode substring(int) déjà utilisée précédemment).
- Puis, nous affectons l'objet en utilisant sa méthode setFromDatagram() à partir de la sous-chaîne de données extraites.
- Et maintenant que l'objet est à jour avec de nouvelles données, nous demandons à l'objet de se manifester auprès des différents services auxquels il est abonné grâce à sa méthode update().
- Enfin, nous remettons l'objet à zéro avec sa méthode resetData() afin d'être prêt à recevoir de nouvelles données.
- Si jamais vous n'arrivez pas à écrire cette méthode, cliquez pour apercevoir la solution. Mais essayez de le faire par vous-même et surtout essayez de comprendre chaque instruction.
public void run (DataReceiver dataReceiver) { String datagram ; while (dataReceiver.ready()) { datagram = dataReceiver.readDatagram() ; if (datagram != null && !datagram.isEmpty()) { String mac = datagram.substring(0, 17) ; Thing theThing = this.mapThings.get(mac) ; if (theThing == null) { System.out.println("Mac address unknown: "+mac); } else { theThing.setFromDatagram(datagram.substring(18)) ; theThing.update(); theThing.resetData(); } } } }
- La méthode close() demande à tous les services de s'arrêter. Pour ce faire, procédez par un parcours des services enregistrés et invoquez leur propre méthode close(). Aide :
Le parcours peut se faire de deux manières, choisissez la vôtre :
1. Soit en parcourant les indices :
/* Parcours ArrayList avec les indices */
for (int i=0 ; i < this.arrServices.size() ; i++) {
Service service = this.arrServices.get(i) ;
}
2/ Soit en utilisant un itérateur :
/* Parcours ArrayList avec iterateur */
Iterator<Service> it = this.arrServices.iterator();
while (it.hasNext()) {
Service service = it.next() ;
}
• Lors de son initialisation, l'itérateur est positionné au début du HashMap.
• L'appel it.hasNext() retourne vrai si des éléments peuvent encore être lus.
• L'appel it.next() retourne le prochain élément qui peut être lu.
Une fois la classe terminée, revenez dans la classe Run.
- Retirez l'appel à readDatagram() de l'exercice précédent.
- Instanciez une plateforme p. Ajoutez-lui les services s1 à s5 et l'objet connecté t1 (de la classe Thing) que vous aviez déjà créés auparavant.
- Invoquez l'exécution de la plateforme p avec sa méthode run(), méthode à laquelle vous donnerez en paramètre l'objet k de classe KeyboardInput précédemment créé.
Bien entendu, compilez et testez l'exécution. Par exemple, saisissez plusieurs datagrammes (par copier-coller) pour l'objet connecté d'adresse MAC "f0:de:f1:39:7f:17" (c.-à-d. t1) signifiant la position géographique (geo), le pouls (pul) et le niveau de batterie (bat) mesurés par l'objet connecté :
- "geo 43.433331 -1.58333;pul 128;bat 90.0"
- "geo 43.433331 -1.58333;pul 128;bat 89.9"
- "geo 43.433331 -1.58333;pul 130;bat 89.8"
- "geo 43.433331 -1.58333;pul 130;bat 89.7"
Enfin, saisissez « quit » pour arrêter la saisie.
Regardez sur le disque si les fichiers journaux ont bien été créés.
Maintenant que la Version 1 est en place, nous allons la faire évoluer et la transformer. Grâce à la puissance de l'Orienté Objet, cela ne va demander que peu d'efforts.
Exercice 2 • Lire depuis un fichier (Version 2)
Dans cette nouvelle version, les données seront envoyées à partir de la lecture du fichier simu.txt. Le but est de faciliter les développements en aidant à automatiser les phases de test (plutôt que de copier-coller les datagrammes manuellement…). Grâce à la puissance de l'Orienté Objet, nous pouvons faire cela très simplement : créer une nouvelle classe FileReader qui implémente la classe DataReceiver et la substituer au KeyboardInput de la Version 1.
Le macro-diagramme de classe, avec la nouvelle classe FileReader, de la Version 2 est illustré ci-dessous :
Ci-dessous le macro-diagramme de séquence d'un scénario nominal (avec deux Thing, deux Services et l'envoi de deux datagrammes) qui montre l'utilisation de la nouvelle classe FileReader. Remarquez que rien n'a changé par rapport à la Version 1, sauf au niveau de la lecture des datagrammes. Cliquez pour afficher le diagramme de séquence correspondant.
Question 2.1 : La classe FileReader
Téléchargez le fichier de datagrammes simu.txt sur votre disque dans le répertoire de votre projet Eclipse, à côté des fichiers de configuration « .classpath » et « .project » (c.-à-d. au-dessus des répertoires bin/ et src/). Ouvrez-le avec Notepad++ et observez la composition des lignes. Nous allons écrire la classe FileReader de sorte qu'elle lise une nouvelle ligne dans un fichier de datagrammes à chaque appel de la méthode readDatagram().
Écrivez la classe FileReader. Quelques remarques d'implantation :
- La méthode open() instancie l'attribut br de sorte qu'il lise des octets depuis le fichier du nom qui a été donné au constructeur :
InputStream ips = new FileInputStream(this.filename);
InputStreamReader ipsr = new InputStreamReader(ips);
br = new BufferedReader(ipsr);
Maintenant que la lecture est possible, l'attribut ok passe à vrai. - La méthode readDatagram() fait appel à la méthode readLine() de l'attribut br pour lire une (et une seule) ligne depuis le fichier.
Si jamais la ligne lue vaut null ou que la ligne lue est une chaîne vide "",
alors l'attribut ok devient faux et la méthode retourne la valeur null. - La méthode ready() renvoi l'état de l'attribut ok.
- La méthode close() appelle la méthode close() de l'attribut br et bascule ok à faux.
Une fois la classe terminée, revenez dans la classe Run. Créez un nouvel objet f de classe FileReader et utilisez-le pour remplacer le KeyboardInput utilisé précédemment lors de l'appel à la méthode run() de la plateforme p.
Compilez et testez l'exécution.
- Regardez sur le disque si les fichiers journaux ont bien été créés et ouvrez-les avec Notepad++ pour consulter leurs contenus.
- Combien de lignes ont été écrites dans chaque fichier ?
Nous avons donc complètement changé l'utilisation du programme et ce avec un minimum de modifications. Surtout, il suffira juste de « rebrancher » le KeyboardInput k sur la méthode run() pour revenir au comportement précédent (mais, ne le faîtes pas pour le moment).
Exercice 3 • Spécialiser la plateforme (Version 3)
L'objectif est de spécialiser les classes Service et Thing pour que notre plateforme IoT puisse proposer des fonctionnalités différentes selon le type de service ou le type d'objet connecté. Nous allons illustrer avec deux services qui commencent à être de plus en plus répandus et créer les deux classes SmartHome (pour la domotique) et QuantifiedSelf (pour la mesure de soi). Ces services eux-mêmes pourraient être encore spécialisés, ou encore d'autres services pourraient être dérivés de la classe Service. Les fonctionnalités de ces classes dérivées pourraient également être complétées (envoi d'e-mail, de sms, calcul, alertes…). Les objets connectés aussi pourraient être spécialisés par de nombreuses manières, mais nous allons simplement créer une classe ThingTempo dérivée de la classe Thing.
Le macro-diagramme de classe de la Version 3 avec les trois nouvelles classes spécialisées est illustré ci-dessous :
Question 3.1 : La classe SmartHome
Nous considérons que pour pouvoir être enregistrées dans les journaux, les données de domotiques doivent définir l'état (allumé ou éteint) de l'objet connecté. La clé de trois lettres associée à ces données sera "sta" (pour state).
Pour ce faire, la classe SmartHome étend la classe Service en complétant le code de la méthode writeData(). Le nouveau comportement sera d'écrire dans le fichier journal seulement si l'objet thing comporte une donnée de clé "sta".
Écrivez la classe SmartHome.
Une fois la classe terminée, revenez dans la classe Run :
- Créez un nouveau service sh de classe SmartHome portant le nom « myKWHome ».
- Ajoutez-le à la plateforme p.
- Utilisez le FileReader f comme DataReceiver de la méthode run() de la plateforme.
Compilez et testez l'exécution.
- Regardez sur le disque si le fichier journal de ce nouveau service a bien été créé et ouvrez-le avec Notepad++ pour consulter son contenu.
- Combien de lignes ont été écrites dans chaque fichier ?
Question 3.2 : La classe QuantifiedSelf
Nous considérons que pour pouvoir être enregistrées dans les journaux, les données de mesure de soi doivent aussi renseigner la position géographique de l'objet connecté. La clé de trois lettres associée à ces données sera "geo". De plus, les données des services QuantifiedSelf ne seront pas journalisées avec la date, mais avec une mesure de temps en millisecondes : le nombre de millisecondes écoulées depuis le début de l'ère Unix (c.-à-d. le nombre de secondes écoulées depuis le 1er janvier 1970 à 00h00.00 UTC).
Pour ce faire, la classe QuantifiedSelf étend la classe Service en substituant le code de la méthode writeData(). Le nouveau comportement sera d'écrire, lorsque l'objet thing comporte une donnée de clé "geo", dans le fichier journal avec l'attribut hérité pw et en récupérant le temps en millisecondes grâce à la méthode getTime() de la classe Date comme ceci :
Date now = new Date() ;
long time = now.getTime() ;
Écrivez la classe QuantifiedSelf.
Une fois la classe terminée, revenez dans la classe Run :
- Créez un nouveau service qs de classe QuantifiedSelf portant le nom « RUNstats ».
- Ajoutez-le à la plateforme p.
- Utilisez le FileReader f comme DataReceiver de la méthode run() de la plateforme.
Compilez et testez l'exécution.
- Regardez sur le disque si le fichier journal de ce nouveau service a bien été créé et ouvrez-le avec Notepad++ pour consulter son contenu.
- Combien de lignes ont été écrites dans chaque fichier ?
Question 3.3 : La classe ThingTempo
Certains objets connectés envoient plus de données que souhaité. Nous voulons temporiser avec la classe ThingTempo en instaurant un délai qui doit être écoulé avant d'envoyer de nouvelles données aux services souscrits.
Pour ce faire, la classe ThingTempo étends la classe Thing et complète le code de la méthode update(), en appelant le code de la classe mère seulement si le délai est écoulé depuis le dernier update() :
Ce délai (en secondes) est donné en paramètre du constructeur de la classe ThingTempo (en plus du nom du service et de l'id utilisateur).
Écrivez la classe ThingTempo.
Une fois la classe terminée, revenez dans la classe Run :
- Instanciez un objet t2 de classe ThingTempo avec comme adresse MAC "f0:de:f1:39:7f:18", 1 comme id utilisateur et 60 comme délai en secondes.
- Ajoutez-le à la plateforme p.
- Abonnez-le à au moins deux services de s1 à s5 (hors QuantifiedSelf et SmartHome).
- Pour que la saisie se fasse au clavier par l'utilisateur, remettez en place le KeyboardInput k comme DataReceiver de la méthode run() de la plateforme p.
Compilez et testez l'exécution :
- 1er envoi : donnez l'adresse MAC de t2 et le datagramme "geo 43.433331 -1.58333;pul 128;bat 90.0"
- 2e envoi : donnez l'adresse MAC de t2 et le datagramme "geo 43.433331 -1.58333;pul 133;bat 90.0"
- 3e envoi : attendez 1 minute, puis donnez l'adresse MAC de t2 et le datagramme "geo 43.433331 -1.58333;pul 148;bat 89.9"
- 4e envoi : tapez « quit »
Ouvrez, avec Notepad++, le fichier journal d'un des deux services auxquels vous avez abonné l'objet. À la fin du journal, vérifiez la présence des deux lignes consécutives avec 128 et 148 pulsations et surtout notez l'absence de la ligne avec 133 pulsations.
Exercice 4 • Paramétrer la plateforme depuis une base de données (Version 4)
L'objectif de cette nouvelle version est de décrire, dans une base de données MySQL, les utilisateurs, les objets connectés qu'ils possèdent et les services qu'ils utilisent.
Pour le moment, la liste des services et des objets connectés reste figée dans le code source que vous avez écrit. Pour pouvoir ajouter des nouveaux services ou de nouveaux objets connectés, il faut donc recompiler le logiciel à chaque fois (et disposer du code source et des compétences nécessaires). Cette façon de faire est plutôt irréaliste. Pour y remédier, nous allons décrire la liste des services et des objets connectés dans une base de données. De nouveaux services pourront alors être ajoutés ou supprimés par simple requête sur la base. Ci-dessous le schéma relationnel de cette base de données :
Notation : les clés primaires sont soulignées, les clés étrangères sont précédées du symbole croisillon « # »
Question 4.1 : La base de données
Le fichier platform_iot.sql contient les requêtes SQL pour créer et peupler la base de données plaform_iot. (Rappel : vous aviez déjà créé et manipulé cette base de donnée en TP de SGBD en 1A)
- Téléchargez le fichier platform_iot.sql sur votre disque dur.
- Démarrez le service WAMP.
- Depuis phpMyAdmin, chargez la base en important le fichier plaform_iot.sql que vous venez de télécharger.
- Vérifiez que la base est en place. Consultez les lignes des tables :
- Combien de services sont présents sur la plateforme ? De quels types sont-ils ?
- Combien d'objets connectés sont présents sur plateforme ?
- Combien d'objets connectés possède chaque utilisateur ?
- À quels services est abonné chaque utilisateur ?
Question 4.2 : Modifier la classe Platform
Nous allons ajouter une méthode à la classe Platform. Cette nouvelle méthode loadFromDatabase() initialise la plateforme en consultant la base de données plaform_iot :
- La méthode loadFromDatabase() instancie de nouveaux services à partir de leur nom et de leur type, puis les ajoute à la liste des services de la plateforme avec la méthode addService(). La requête pour récupérer les ids, les noms et les types de tous les services sera :
SELECT * FROM table_service ;
- Si le type est la chaîne "smarthome", alors la méthode loadFromDatabase() construira un service de la classe SmartHome. Si le type est la chaîne "quantifiedself", alors elle construira un service de la classe QuantifiedSelf. Sinon, elle construira un service de la classe Service.
- La méthode loadFromDatabase() ajoute aussi chaque service, avec son id comme clé, dans un objet mapIds (de classe HashMap<String, Service>), que vous aurez déclaré et instancié au début de la méthode. (Rappel : c'est la méthode put(K, V) qui permet d'insérer des éléments dans un HashMap)
- La méthode loadFromDatabase() récupère les adresses MAC, id utilisateur et types de tous les objets connectés avec la requête :
SELECT * FROM table_thing ;
- Elle instancie de nouveaux objets connectés à partir des adresses MAC et des types, puis les ajoute à la liste des objets connectés de la plateforme avec la méthode addThing().
- Si le type est la chaîne "thingtempo", alors elle construit un objet connecté de la classe ThingTempo. Sinon, elle construit un objet connecté de la classe Thing.
- Dans le cas de la classe ThingTempo, il faudra aussi récupérer le champ param pour indiquer la valeur du délai. Pour obtenir un
long
depuis une chaîne de caractère, utiliser la méthode statique parseLong() de la classe Long.
- Elle fait souscrire chaque objet connecté, aux services auxquels son utilisateur est abonné, grâce à leur méthode subscribe().
- Pour récupérer la liste des services, utiliser la requête suivante (en remplaçant l'adresse MAC par l'adresse recherchée) :
SELECT id_service FROM table_subscribe WHERE id_user IN (SELECT id_user FROM table_thing WHERE mac='xx:xx:xx:xx:xx:xx') ;
Attention, ne pas réutiliser le même Statement que précédemment pour faire cette requête, mais créez un nouvel objet Statement. - Pour retrouver l'objet de classe Service correspondant, utilisez le HashMap mapIds et sa méthode get().
- Pour récupérer la liste des services, utiliser la requête suivante (en remplaçant l'adresse MAC par l'adresse recherchée) :
- Elle instancie de nouveaux objets connectés à partir des adresses MAC et des types, puis les ajoute à la liste des objets connectés de la plateforme avec la méthode addThing().
Écrivez la nouvelle méthode loadFromDatabase(). Pour se connecter et requêter le SGBD MySQL depuis votre code Java, reprenez le TP sur JDBC.
Rappel : Pensez à importer le driver JDBC mysql-connector-java-5.1.40-bin.jar dans le dossier bin\ (qu'il faut d'abord créer avec New > Folder) et à l'associer aux bibliothèques (libraries) de la configuration de votre projet.
Revenez dans la classe Run :
- Remettez en place le FileReader f comme DataReceiver de la méthode run() de la plateforme.
- Mettez en commentaire toutes les lignes de déclaration d'objets connectés ou de services, les lignes d'ajout à la plateforme p et de souscription a des services.
- À la place, faîtes simplement un appel à la méthode loadFromDatabase() sur la plateforme p.
Supprimez tous les fichiers journaux présents sur le disque. Compilez et testez l'exécution. Vérifiez que trois fichiers journaux ont été créés et consultez leurs contenus.
Exercice 5 • Communiquer au travers du réseau (Version 5)
L'objectif est d'instaurer une communication réseau pour recevoir les données. Les données seront émises sur le réseau par un deuxième programme simulateur de passerelle (Gateway : Smartphone, Homebox…).
Les sockets permettent de faire communiquer deux programmes au travers du réseau. Nous allons utiliser ce mécanisme pour recevoir des octets provenant d'un programme distant. Pour développer cela, nous allons, dans la suite, faire deux choses :
- Créer une nouvelle classe SocketServer qui implémente l'interface DataReceiver. Elle remplacera les classes KeyboardInput et FileReader précédentes et permettra de recevoir des octets depuis le réseau (et non plus depuis le clavier ou un fichier).
- Écrire un deuxième programme de test, qui enverra des datagrammes sur le réseau, afin de pouvoir tester que tout fonctionne. Ce programme qui simulera la passerelle (Gateway) lira des datagrammes depuis un fichier (et réutilisera notre classe FileReader à cet effet) et les enverra sur le réseau grâce à la classe SocketClient.
Le macro-diagramme de classe, avec les trois nouvelles classes et la nouvelle interface à écrire, pour cette Version 5, est illustré ci-dessous :
Ci-dessous le macro-diagramme de séquence d'un scénario nominal avec deux Thing, deux Services et l'envoi de deux datagrammes par le simulateur de passerelle. Cliquez pour afficher le diagramme de séquence correspondant.
Question 5.1 : La classe SocketServer
La classe SocketServer permet d'ouvrir une communication et d'écouter les octets arrivant sur un port réseau de la machine.
Créez cette nouvelle classe dans votre projet Eclipse et copiez son code : SocketServer.java
- Le constructeur SocketServer() crée une nouvelle socket sur un port réseau.
- La méthode open() attend qu'un programme client se connecte.
- La méthode readDatagram() attend que le client envoi une ligne. Si le client envoi le message "disconnect", alors la socket est refermée. Une nouvelle socket est ouverte pour attendre le client suivant.
- La méthode close() ferme la communication réseau.
Modifiez la classe Run et instanciez un objet s de classe SocketServer qui écoute sur le port 51291 et qui sera le DataReceiver utilisé par la méthode run() de la plateforme p.
Compilez le programme depuis le menu : Project > Build Project. Pour tester son exécution :
- Avec Notepad++, créer dans votre projet (à côté de « simu.txt ») un nouveau fichier Batch que vous appelerez server.bat et recopiez la ligne suivante en adaptant les chemins :
java -classpath "D:\\chemin\\de\\votre\\projet\\lib\\mysql-connector-java-5.1.40-bin.jar;D:\\chemin\\de\\votre\\projet\\bin\\" Run - Ouvrez une invite de commande,
- descendez dans le répertoire de votre projet Eclipse,
- puis exécutez votre programme serveur en invoquant : server.bat
- Le programme reste en attente d'une connexion client.
- Ne faîtes rien et laissez-le en attente.
Question 5.2 : L'interface DataSender et la classe SocketClient
L'interface DataSender décrit le comportement d'une entité capable d'écrire des datagrammes.
Créez cette nouvelle interface dans votre projet Eclipse et copiez son code : DataSender.java
La classe SocketClient permet d'ouvrir une socket pour communiquer avec un programme serveur. Pour cela, elle doit connaître l'adresse IP de la machine sur laquelle le serveur s'exécute et le numéro de port sur lequel il écoute. La classe Socket permet de se connecter au serveur et de lui envoyer des datagrammes.
Créez cette nouvelle classe dans votre projet Eclipse et copiez son code : SocketClient.java
- Le constructeur SocketClient définie l'adresse IP du serveur et le numéro de port à utiliser.
- La méthode open() ouvre une socket et se connecte au serveur.
- La méthode writeDatagram() envoi sur le réseau la ligne décrivant le datagramme à destination du serveur.
- La méthode close() ferme la communication socket.
Question 5.3 : La classe GatewaySimulator
La classe GatewaySimulator est la classe principale du programme de test.
Écrivez la classe GatewaySimulator et :
- Instanciez un objet f de classe FileReader qui lira depuis le fichier "simu.txt".
- Instanciez un objet s de classe SocketClient qui se connectera à la machine d'adresse IP "127.0.0.1" et écrira sur le port 51291.
- Appelez la méthode open() de ces deux objets.
- Tant que le FileReader f est prêt à lire :
- Lire un datagramme depuis le fichier avec le FileReader f
- Si le datagramme n'est pas null
- alors envoyer le datagramme sur le réseau grâce au SocketClient s
- Appelez la méthode close() de ces deux objets.
Compilez le programme depuis le menu : Project > Build Project. Si la compilation a réussi :
- Créez dans votre projet (à côté de « simu.txt ») un nouveau fichier client.bat avec Notepad++ et recopiez la ligne suivante en adaptant les chemins :
java -classpath "D:\\chemin\\de\\votre\\projet\\bin\\" GatewaySimulator - Avant de tester l'exécution, rendez visible, sur votre bureau Windows, la première invite de commande où le programme serveur est resté en attente. Ouvrez une autre invite de commande sur votre bureau Windows (placez-la à côté de la première), descendez dans le répertoire du projet et exécutez votre programme client en invoquant : client.bat
- Observez les messages côté serveur. Exécutez une deuxième fois votre simulateur de passerelle (le client).
Pour être sûr que tout fonctionne bien, vérifiez les dates des dernières écritures dans les fichiers journaux.
Vu que le programme serveur ne s'arrête jamais, faîtes + pour le stopper.
Bilan : La plateforme IoT est maintenant complète. Elle charge les services et les objets depuis une base de données. Elle écoute les datagrammes provenant du réseau. Elle écrit les données reçues dans des fichiers journaux. De nombreuses autres fonctionnalités pourraient être ajoutées. Cependant, les présentes fonctionnalités vous ont déjà démontré la puissance du paradigme orienté objet, la souplesse et l'agilité offertes. Une fois l'architecture de la version 1 en place, développer le reste est très facile et rapide. Écrire la même chose avec un langage procédural serait vraiment laborieux.
Question subsidiaire : Quel langage est le plus présent parmi les offres d'emplois ? Indices : le développement des Systèmes d'Information et de Gestion des entreprises (que nous étudierons au prochain semestre) exige de nombreux programmeurs et nombreux sont les applicatifs serveurs qui utilisent le langage Java.
Pour approfondir le sujet traité, vous pouvez consulter le dossier « Cap sur les plateformes IoT » à partir duquel le présent énoncé a été construit :
• Partie 1 : Blog – Fichier (Octo, septembre 2015)
• Partie 2 : Blog – Fichier (Octo, octobre 2015)
• Partie 3 : Blog – Fichier (Octo, février 2016)