Créer un composant device

Comme vous l’avez constaté dans les tutoriaux précédents, via leurs slots physiques les agents SKUAD ont la capacité d’interagir avec des dispositifs matériels présent dans l’environnement. Bien entendu, tous les dispositifs existants ne sont pas accessibles aux agents, en réalité seuls sont accessibles ceux qui disposent d’une interface logicielle spécifique permettant la manipulation concrète du dispositif physique via la couche logicielle SKUAD. Et cette interface logicielle spécifique se nomme Device (package skuad.uda).

Concrètement, un dispositif physique peut être par exemple une lumière. Et si on adjoint à cette lumière un montage électronique connecté à un Raspberry Pi (ou un autre nano ordinateur du même type), et que l’on installe sur cette petite machine un code logicielle implémentant l’interface Device. Alors cette lumière deviendra un dispositif matériel avec lequel les agents SKUAD pourront interagir.

1 - l'interface Device et la classe BaseDevice

Du point de vu du programmeur que vous êtes, un device SKUAD sera une classe Java qui implémente l’interface Device. Vous devrez donc coder une telle classe chaque fois que vous serez amené à concevoir un nouveau device.

L’interface Device définit 13 méthodes, voici leur signature :

//action (device actuateur)
public boolean log(String text);
public boolean move(int cmd, int power);
public boolean action(int actuator_id, int value);
public boolean action(int actuator_id, float value);
public boolean action(int actuator_id, byte[] value);
   
//lecture (device capteur)
public boolean getState(Monitor mon);
public boolean getParams(Monitor mon);
public boolean getMoveCapacity(Monitor mon);
public boolean getSensorParam(Monitor mon, int sensor_id);
public boolean getActuatorParam(Monitor mon, int actuator_id);
public boolean readSensor(Monitor mon, int sensor_id);
   
//écouteurs(device capteur observable)
public boolean addListener(Monitor mon);
public boolean removeListener(Monitor mon);

Comme d’habitude, il n’est pas nécessaire que vous comprenniez le rôle de chacune de ces méthodes pour le moment. D’autant que nous allons utiliser une classe intermédiaire qui va nous faciliter la tâche pour implémenter nos premiers devices. Il s’agit de la classe BaseDevice (package skuad.uda.device).

La classe BaseDevice implémente l’interface Device en prenant en charge les traitements courants nécessaires au bon fonctionnement des composants répondant à cette interface. Aussi, pour concevoir un device il sera généralement plus efficace de créer une classe qui hérite de BaseDevice plutôt que d’implémenter directement l’interface Device.

 
2 - l'exemple MyLight

A titre d’exemple illustratif, nous allons nous donner comme objectif de produire un device de nom MyLight. Ce device sera similaire au device DLight qui est une lumière virtuelle représentée par une ampoule colorée en jaune ou bleu en fonction du fait que la lumière est allumée ou éteinte. Ce composant est fournit dans la librairie SKUAD, vous pouvez lancer son exécution en tapant la commande suivante :

  > java   start   DLight  

Pour gagner du temps nous allons nous limiter à une interface graphique plus simple pour notre device MyLight. Ce sera juste un rectangle de couleur noir quand la lumière sera éteinte, et jaune quand elle sera allumée.

Commençons par mettre le code en place. Ouvrez Eclipse et créez un nouveau projet SKUAD de nom MyLight (voir ce tuto si nécessaire).

Ensuite, dans le dosssier src, créez une classe de nom MyLight en faisant en sorte que cette classe hérite de la classe BaseDevice (il vous faudra donc importer le package: skuad.uda.device.*).

Pour le moment votre code ressemble à ceci :

import skuad.uda.device.*;

public class MyLight extends BaseDevice {
   
    //...
   
}

Eclipse vous signale déjà une erreur dans ce code, du fait que la classe BaseDevice ne dispose pas d’un constructeur par défaut (sans argument). Nous allons régler ce petit problème dans la prochaine section.

 
3 - configuration du device

Pour gérer les traitements des méthodes de l’interface Device, la classe BaseDevice doit disposer d’un minimum d’information sur le device implémenté. Et en particulier les informations concernant la configuration de ce device : son type, les capteurs et les actuateurs dont il dispose (sensor et actuator en anglais). Aussi, quand vous héritez de la classe BaseDevice vous devez lui fournir ces informations de configuration.

Les premières informations nécessaires sont : le nom du device, son type et s’il est capable de gérer ou non la commande log (c’est une commande facultative pour un device, elle permet de lui transmettre du texte). Ces 3 informations sont attendues par le constructeur de la classe BaseDevice dont voici la signature :

public BaseDevice(String name, int type_device, boolean can_log)

Vous devez donc ajouter un constructeur dans votre classe MyLight, et la première instruction de ce constructeur doit être d’invoquer le constructeur de BaseDevice (mot clé super) avec ces 3 valeurs.

Pour notre composant MyLight nous allons choisir les valeurs suivantes :

  • nom : MyLight
  • type : UDA.LIGHT , c’est une constante de la classe UDA qui désigne le type de device lumière. Nous ne sommes pas obligé de respecter cette valeur (ce n’est qu’une constante numérique), mais il est quand même plus logique de le faire.
  • log : non

Le reste de la configuration du device qu’il est nécessaire d’exprimer concerne ses capteurs (chacun correspond à une information qu’il est possible de lire sur le device) et ses actuateurs (chacun correspond à une action qu’il est possible de déclencher sur le device). Un device peut comporter : un ou plusieurs capteurs (c’est alors un device capteur); un ou plusieurs actuateurs (c’est dans ce cas un device actuateur); ou à la fois des capteurs et des actuateurs (c’est alors un device mixte).

La classe BaseDevice fournit 2 méthodes pour réaliser ces configurations capteur/actuateur :

protected int addSensor(String name, int type_sensor, int type_value, boolean observable)
protected int addActuator(String name, int type_actuator, int type_value)

Dans ces méthodes :

  • les arguments type_sensor et type_actuator renseignent le type du capteur ou actuateur : ce sera généralement des constantes UDA tel que : UDA.SBUTTON pour un capteur de type bouton, ou UDA.ALIGHT pour un actuateur de type lumière.
  • l’argument type_value indique le type de valeur fournie par le capteur ou attendue pour la commande de l’actuateur : les valeurs possibles ici sont DataBytes.INT, DataBytes.FLOAT ou DataBytes.TBYTES (package skuad.util.databytes).
  • l’argument observable (pour addSensor seulement) précise si l’information du capteur est observable ou non : une information observable signifie que le device émettra automatiquement une notification à chaque fois que la valeur du capteur aura changée. Dans ce cas il sera possible d’être informé de chaque changement de valeur en s’enregistrant comme écouteur sur ce device (ce que fait automatiquement la classe AgentUAdapter, que nous utilisons pour impémenter nos agents, sur chaque device pluggué à l’un des slots physiques, et qui conduit donc aux appels à la méthode sensorChange(…) de l’agent chaque fois que la valeur d’un capteur observable change). Pour les capteurs non observable, seule la consultation « à la demande » est possible.

Ces méthodes procèdent à l’enregistrement des capteurs et des actuateurs du device. Chacune de ces 2 méthodes renvoie une valeur entière qui représente l’identifiant unique attribué au capteur/actuateur qui vient d’être enregsitré (il s’agit simplement d’un numéro de 0 à N-1 correspondant à l’ordre d’enregistrement du capteur/actuateur). Cet identifiant sera utilisé dans les commandes de lecture des capteurs, ou d’actionnement des actuateurs, pour indiquer précisément quel est le capteur, ou l’actuateur, ciblé (un device pouvant porter plusieurs capteurs/actuateurs). Dans les cas simples, comme pour notre device MyLight, il y aura le plus souvant qu’un seul capteur/actuateur et son identifiant sera donc 0. Mais dans les autres cas, il vous sera nécessaire de documenter le device pour indiquer la valeur des identifiants de chacun de ses capteurs/actuateurs.

Notez enfin que ces méthodes sont qualifiées protected. Cela signifie qu’elles ne peuvent être employées que dans le code de la classe fille (ici notre classe MyLight). Et le bon endroit pour le faire est dans le constructeur, à la suite de l’appel à super(…).

Notre device MyLight est un cas volontairement simple. Il ne dispose que d’un seul actuateur (la commande pour allumer/éteindre la lumière) et d’aucun capteur. Nous n’avons donc qu’un appel addActuator(…) à réaliser. On va nommer cet actuateur « light », son type sera UDA.ALIGHT et le type de valeur attendu pour la commande sera DataBytes.INT : la valeur 0 conduira à éteindre la lumière, et tout autre valeur conduira à l’allumer.

Une fois que le code de configuration du device est terminé il faut impérativement penser à le signaler à la classe BaseDevice en invoquant sa méthode :

protected void ready(boolean flag)

Cet appel va permettre à BaseDevice de fixer l’état du device sur ready (« prêt à fonctionner ») et de propager l’information de cet état dans le système. Si vous n’invoquez pas cette méthode votre composant sera considéré comme « non prêt » et il ne sera pas réellement utilisable.

A ce state, le code de votre classe MyLight doit donc ressembler à ceci :

import skuad.uda.UDA;
import skuad.uda.device.*;
import skuad.util.databytes.DataBytes;

public class MyLight extends BaseDevice {
   
    public MyLight() {
        super("MyLight", UDA.LIGHT, false);
       
        addActuator("light", UDA.ALIGHT, DataBytes.INT);
       
        ready(true);
    }
}

Et Eclipse ne doit signaler aucune erreur.

 
4 - traitement interne du device

Notre device est maintenant correctement configuré, mais il n’est pas encore fonctionnel pour autant. En effet, nous n’avons encore écrit aucun code concernant les traitements effectifs de notre device. Il faut donc s’attendre à ce que rien ne se passe si on déploie notre device dans l’état ou il se trouve actuellement. L’étape cruciale dans la conception d’un device est donc l’implémentation de ses traitements internes.

Pour un device comportant des actionneurs

Concernant les actionneurs cette implémentation consiste à réagir aux commandes de déclenchement d’action reçues par le device. Au niveau de l’interface Device, la réception de tels commandes se traduit par le déclenchement de l’une des méthodes 3 méthodes suivantes :

public boolean action(int actuator_id, int value);
public boolean action(int actuator_id, float value);
public boolean action(int actuator_id, byte[] value);

Le choix de l’une de ces 3 méthodes sera déterminée par le type de valeur fourni dans la demande de déclenchement (int, float, ou byte[]) et qui sera la valeur du deuxième argument. Le premier argument correspond à l’identifiant de l’actuateur concerné par la demande.

Pour mettre en oeuvre ces traitements d’action dans un composant device, il suffit donc d’écrire les méthodes action(…) nécessaires (les 3 si des actuateurs existent pour chacun des 3 types de valeurs possibles, ou seulement les méthodes pertinantes).

Les méthodes action(…) retourne une valeur booléenne, celle-ci sert à indiquer si l’opération s’est bien passée : on doit retourner true si c’est le cas, et false sinon (par exemple si l’identifiant d’actuateur demandé n’est pas connu).

 
Pour un device comportant des capteurs

Au sujet des capteurs cette implémentation consiste à réagir aux commandes de lecture reçues par le device. Au niveau de l’interface Device, la réception de tels commandes se traduit par le déclenchement de la méthode suivante :

public boolean readSensor(Monitor mon, int sensor_id);

Le deuxième argument de cette méthode correspond à l’identifiant du capteur sur lequel la lecture est demandée. Et le premier argument est l’objet Monitor qui doit être utilisé pour fournir la réponse.

Cet objet Monitor correspond à l’interface de même nom située dans le package skuad.uda. Dans le modèle UDA, cette interface caractérise les composants pouvant interagir avec un device, c’est donc en quelque sorte l’inverse de l’interface Device (un peu à la façon des rôles joués par une prise USB male et femelle). Si vous observez attentivement les méthodes de l’interface Device listées dans la section 1 de ce tutoriel, vous constaterez qu’un objet de ce type se trouve systématiquement en premier argument de chaque méthode nécessitant une réponse. Dans le modèle UDA la régle est ainsi que chaque réponse exprimée par le device ne doit pas être produite par la valeur de retour de ses méthodes, mais injectée dans l’objet moniteur (qui représente le demandeur) passé en première argument. A première vu ce fonctionnement doit vous paraître étrange, mais vous comprendrez bientôt que c’est sur cette clé de voute que repose l’agilité de déploiement des devices UDA.

Nous reviendrons dans un autre tutoriel sur le détail de l’ensemble des méthodes de l’interface Monitor, car pour le moment seuls 3 de ces méthodes nous intéresse ici. Il s’agit des méthodes :

public void sensorValue(Device dev, int sensor_id, int value);
public void sensorValue(Device dev, int sensor_id, float value);
public void sensorValue(Device dev, int sensor_id, byte[] value);

En effet, dans le code que nous devrons produire pour la méthode readSensor(…), nous aurons à utiliser l’une de ces 3 méthodes pour communiquer la réponse à l’objet moniteur demandeur. Le choix exacte de la méthode sera déterminée par le type de valeur du capteur lu (int, float, ou byte[]) et qui sera la valeur du troisième argument. La valeur du premier argument est le device lui même (this), et le second argument est l’identifiant du capteur.

Comme pour les méthodes action(…), la méthode readSensor(…) retourne également une valeur booléenne pour indiquer si l’opération s’est bien passée : on doit retourner true si c’est le cas, et false sinon (par exemple si l’identifiant du capteur demandé n’est pas connu).

Notez de plus que les traitement nécessaire pour un capteur doivent être complétés pour le cas ou le capteur est observable. C’est à dire si les changements de valeurs du capteur doivent automatiquement être notifiés aux composants qui se sont enregistrés comme écouteur sur le device. La spécification de cette capacité observable est exprimée par l’argument boolean observable de l’appel addSensor(…) produit pour configurer le capteur.

La classe BaseDevice se charge de gérer l’enregisterement et le désenregistrement des écouteurs (ce qui correspondent aux méthodes addListener(Monitor mon) et removeListener(Monitor mon) de l’interface Device), vous n’avez donc rien à faire de spécial à ce niveau. Par contre, vous devez explicitement signaler à la classe BaseDevice tout changement de valeur subit par l’un des capteurs observables, afin que BaseDevice produise effectivement les appels de notifications aux moments appropriés. Et pour réaliser ces signalements vous devez invoquer l’une des méthodes suivantes de la classe BaseDevice (en fonction du type de valeur du capteur) :

protected void fireSensorValue(int sensor_id, int value);
protected void fireSensorValue(int sensor_id, float value);
protected void fireSensorValue(int sensor_id, byte[] value);

Aussi, partout dans le code de votre classe où une modification de la valeur d’un capteur observable est réalisée, vous devez tout de suite déclencher la méthode fireSensorValue(…) appropriée.

 
Le cas de notre exemple MyLight

Notre composant MyLight ne dispose que d’un actionneur (la commande d’allumage de la lumière) dont la valeur de commande est un entier (0 ou 1). Aussi, il nous faut juste coder la méthode action(…) de signature :

public boolean action(int actuator_id, int value);

Notez que par défaut, le code écrit dans la classe BaseDevice pour cette méthode contient une instruction d’appel de la méthode de même nom avec le type de valeur float. Ceci afin d’orienter les commandes invoquées sur des valeurs entières vers la méthode action(…) attendant un nombre décimal (float). Ce qui permet dans les classes filles de recevoir aussi les appelles produits avec des entiers quand on se limite à ne coder que la méthode :

public boolean action(int actuator_id, float value);

De ce fait, nous aurions pu choisir de coder cette méthode à la place de celle attendant un int sans que cela ne nuise au bon fonctionnement de notre composant.

Le code que nous devons écrire dans cette méthode doit commencer par vérifier que l’identifiant de l’actuateur scollicité a bien la valeur 0 (car notre composant ne comporte qu’un seul actuateur). Si c’est le cas nous devons changer l’état de la lumière pour l’éteindre ou l’allumer, en fonction du fait que la valeur de commande fourni soit égale à 0 ou non.

Nous mettrons en place l’interface visuel de notre composant dans la prochaine section. Donc pour le moment écrivons le traitement de cette méthode en réalisant juste un affichage dans la console (System.out.println(…)) du nouvel état de la lumière.

Le code complet de notre classe MyLight pourrait ainsi être le suivant :

public class MyLight extends BaseDevice {
    public MyLight() {
        super("MyLight", UDA.LIGHT, false);
        addActuator("light", UDA.ALIGHT, DataBytes.INT);
        ready(true);
    }

    public boolean action(int actuator_id, int value) {
        if(actuator_id!=0) return(false); //id actuateur non valide
       
        if(value==0) System.out.println("on éteind la lumière !");
        else System.out.println("on allume la lumière  !");
        return(true);
    }
}

Voila, nous avons là un composant device parfaitement fonctionnel. Mais il nous reste à mettre en place l’interface graphique de ce composant pour répondre pleinement à l’objectif que nous nous sommes fixé.

 
5 - l'interface graphique de notre composant

Le device que nous sommes en train de coder est un device virtuel, ou purement logiciel, dans le sens ou le dispositif qu’il encapsule n’est pas un équipement physique (une vraie lumière) mais un simple élément d’interface graphique colorée. Le code que nous avons écrit jusqu’à présent sera néanmoins similaire que le device soit virtuel ou non. La différence va plutôt se situer dans ce qui va suivre. Ici nous allons mettre en place une interface graphique pour notre composant. Car il est virtuel et il sera juste représenté dans une fenêtre sur l’écran de l’ordinateur. Pour le cas d’un composant physique, ce travail prendra plutôt la forme d’un code de pilotage d’une interface de sortie électronique (tel que les pins GPIO d’un Raspberry Pi, par exemple) reliée au circuit d’allumage d’une lampe réelle.

Nous avons fait le choix de représenter notre lumière virtuel par un rectangle coloré : noir quand la lumière est éteinte, et jaune quand elle est allumée. Pour implémenter cela en Java nous pouvons utiliser la classe JPanel (c’est un composant d’interface graphique minimaliste qui n’affiche rien de spécial), et changer sa couleur de fond via sa méthode setBackground(…) quand la lumière doit s’allumer et s’éteindre.

public class MyLight extends BaseDevice {

    JPanel lumiere; //l'interface graphique de notre composant
   
    public MyLight() {
        //...
        lumiere = new JPanel();
        //...
    }

    public boolean action(int actuator_id, int value) {
        if(actuator_id!=0) return(false); //id actuateur non valide
       
        if(value==0) lumiere.setBackground(Color.BLACK); //on éteind
        else lumiere.setBackground(Color.YELLOW);        //on allume
        return(true);
    }
}

D’autre part, la librairie SKUAD comportent un certains nombres d’utilitaires dédiés à faciliter la réalisation d’interface graphique. L’usage de ces utilitaires n’est pas obligatoire pour la réalisation d’un device, ici nous allons les utiliser afin de réduire le nombre de ligne à écrire. Parmi ces utilitaires il y a notamment une interface de nom HaveGUI (package skuad.util.gui) qui permet de spécifier qu’un composant dispose d’une interface graphique. Cette interface définie la méthode suivante :

public JComponent getGUI()

Notre composant MyLight dispose justement d’une interface graphique. Nous allons donc compléter notre classe pour qu’elle implémente l’interface HaveGUI, et dans la méthode getGUI() nous allons retourner le composant JPanel qui représente cette interface graphique (notez que la classe JPanel hérite de la classe JComponent) :

public class MyLight extends BaseDevice implements HaveGUI {

    //...
       
    public JComponent getGUI() { return(lumiere); }

}

Une fois ce complément réalisé, notre composant est enfin prêt. Il ne nous reste plus qu’a créer un programme pour instancier notre composant et l’afficher sur l’écran. Nous allons donc ajouter une fonction main(…) dans laquelle nous allons instancier notre classe MyLight. Pour l’afficher à l’écran nous allons simplement utiliser la fonction utilitaire suivante de l’interface HaveGUI (en java une interface POO peut avoir des méthodes statiques) :

public static JFrame FrameFor(HaveGUI thegui, String title, boolean exit_on_close)

Cette fonction va générer une fenêtre graphique dans laquelle va s’afficher l’interface du composant spécifié par l’argument thegui (qui sera ici notre composant MyLight). Nous n’aurons plus qu’a donner une dimension à cette fenêtre et le tour est joué :

public class MyLight extends BaseDevice implements HaveGUI {
        //...
       
    public static void main(String[] args){
        MyLight my_light =  new MyLight();
       
        HaveGUI.FrameFor(my_light, "Ma lumière", true).setSize(100,100);
    }
}

 
6 - rendre le dispositif visible sur le réseau local

Notre composant est maintenant finalisé, et nous disposons d’un programme pour lancer son exécution. Toutefois, si nous nous arrêtons là, les agents SKUAD ne seront pas encore en mesure d’interagir avec notre composant. En effet, pour le moment aucun traitement ne rend notre composant visible sur le réseau. Aussi, en l’état, notre composant n’est manipulable que dans l’espace du programme qui l’instancie.

Pour augmenter l’accessibilité de notre composant nous allons utiliser la fonction suivante définie par la classe UdaUdp (package skuad.uda.net) :

public static int expose(Hub hub, Device d)

Cette fonction permet d’enregistrer un device (largument d) dans une routine de traitement interne qui va se charger de rendre le device accessible sur le réseau.

Nous allons produire cette exposition dans la fonction main(…), juste avant d’afficher notre composant à l’écran. Nous utiliserons la valeur null pour l’argument hub, nous verrons dans un prochain tutoriel l’utilité exacte de cet argument.

public static void main(String[] args){
    MyLight my_light =  new MyLight();

    UdaUdp.expose(null, my_light); //on expose le device sur le réseau
   
    HaveGUI.FrameFor(my_light, "Ma lumière", true).setSize(300,300);
}

Voilà cette fois le code est complet (la version intégrale du code est reprise dans la section suivante). Vous pouvez donc expérimenter ce nouveau device. Par exemple vous pouvez l’utiliser à la place du device DLight dans la mise en pratique de la section 5 du tutoriel : Les capacités d’un agent/Les slots physiques des agents. Pour obtenir l’identifiant global du spot qui porte le composant MyLight vous pouvez utiliser l’utilitaire wit en tapant la commande :

  > java   start   wit  -no_exit  

 
7 - le code complet du device MyLight

import java.awt.Color;
import javax.swing.JComponent;
import javax.swing.JPanel;

import skuad.uda.UDA;
import skuad.uda.device.*;
import skuad.uda.net.UdaUdp;
import skuad.util.databytes.DataBytes;
import skuad.util.gui.*;

public class MyLight extends BaseDevice implements HaveGUI {
   
    JPanel lumiere; //l'interface graphique de notre composant
   
    public MyLight() {
        super("MyLight", UDA.LIGHT, false);
       
        lumiere = new JPanel();
       
        addActuator("light", UDA.ALIGHT, DataBytes.INT);
       
        ready(true);
    }
   
    public boolean action(int actuator_id, int value) {
        if(actuator_id!=0) return(false); //id actuateur non valide
       
        if(value==0) lumiere.setBackground(Color.BLACK);
        else lumiere.setBackground(Color.YELLOW);
        return(true);
    }
   
    public JComponent getGUI() { return(lumiere); }
   
    public static void main(String[] args){
        MyLight my_light =  new MyLight();
       
        UdaUdp.expose(null, my_light); //on expose le device sur le réseau
       
        HaveGUI.FrameFor(my_light, "Ma lumière", true).setSize(300,300);
    }
}