Tutoriel CodeLab n°3
Conception d'un module simulateur
En suivant ce tutoriel, vous allez réaliser le plugin "MotorModule" qui visualise un moteur Lego NXT et permet de simuler son fonctionnement. Le rendu ci-dessous est le résultat de l'exécution du programme C suivant terminé, c'est-à-dire après une rotation de 45 degrés :
#include "MotorModule/NXTMotor.c"
void main() {
resetMotorRotationCount();
motorOn(25);
while (getMotorRotationCount() < 45);
motorOff();
}
La différence entre ce module et ceux des précédents tutoriels réside dans leurs fonctionnements : les deux premiers correspondent à des visualisations, celui-ci à une simulation.
Prérequis pour réaliser ce tutoriel :
- Avoir suivi les deux premiers tutoriels
- Maîtriser les concepts objet et la POO en Java
- Savoir exploiter les API Java awt et Swing
- Comprendre la syntaxe des fichiers XML
Table des matières :
- Arborescence de développement
- Classes principales du module
- Définition de la classe MotorSimulator
- Définition de l'API du module
- [Définition de la barre d'outils] (#5)
- [Génération du module] (#6)
- [Ajout d'un tableau de bord] (#7)
1. Arborescence de développement
Comme pour les tutos précédents, allez dans le dossier plugins
et dupliquez l'arborescence-modèle YourModule
que vous renommerez en MotorModule
.
- Renommez également le package principal et les classes principales ;
- Renommez le fichier
yourAPI.xml
enNXTMotor.xml
; - Ajoutez une classe vide
MotorSimulator
; - Téléchargez dans le dossier
data
les ressources axis.png et motor.png :
MotorModule/ ← Dossier renommé ├── class/ ├── data/ │ ├── icon_clear.png │ ├── icon_hello.png │ ├── axis.png ← Nouvelle ressource │ └── motor.png ← Nouvelle ressource ├── includes/ │ ├── api.dtd │ └── NXTMotor.xml ← fichier XML renommé ├── lib/ │ └── codelab.jar ├── src/ │ └── codelab/ │ └── modules/ │ └── motormodule/ ← Package renommé │ ├── MotorModule.java ← Classe renommée │ ├── MotorToolbar.java ← Classe renommée │ └── MotorSimulator.java ← Nouvelle classe ├── templates/ ├── build.bat └── build.sh
2. Classes principales du module
Le modèle objet du MotorModule
est similaire à celui du DrawingModule
à ceci près que nous allons remplacer le DrawingPanel
par un MotorSimulator
hérité de la classe AbstractSimulator
. Les trois classes que vous allez définir sont en rouge dans le diagramme suivant :
Commençez par définir la classe MotorModule
comme ci-dessous. Les instructions que le gestionnaire d'instructions (handleRequest) devra prendre en compte sont les suivantes :
motorOn(puissance)
pour faire tourner le moteurmotorOff()
pour arrêter le moteurgetMotorRotationCount()
pour récupérer la valeur du compteur de rotationresetMotorRotationCount()
pour réinitialiser le compteur de rotation
Remarquez que toutes ces instructions correspondent à des méthodes publiques de la classe MotorSimulator
(qui n'est pas encore définie) que vous pouvez voir dans le diagramme de classes. Remarquez également que les méthodes start()
, stop()
et reset()
ont été complétées pour piloter le simulateur.
Ci-dessous le code de la classe MotorModule
:
package codelab.modules.motormodule;
import codelab.CodeLab;
import codelab.AbstractModule;
import java.util.Map;
public class MotorModule extends AbstractModule {
private MotorSimulator simulator;
///////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////
public MotorModule(CodeLab codelab) {
super(codelab);
setTitle("NXT motor");
setToolbar(new MotorToolbar(this));
setComponent(simulator = new MotorSimulator(this));
}
///////////////////////////////////////////////////
// AbstractModule methods to implement (or not)
///////////////////////////////////////////////////
public void init() {}
public void exit() {}
public void start() {
simulator.start();
}
public void stop() {
simulator.stop();
}
public void reset() {
simulator.reset();
}
///////////////////////////////////////////////////
// Instruction handler
///////////////////////////////////////////////////
public String handleRequest(Map<String, String> params) {
String cmd = params.get("cmd");
switch (cmd) {
case "motorOn":
int power = Integer.valueOf(params.get("power"));
return String.valueOf(simulator.motorOn(power));
case "motorOff":
return String.valueOf(simulator.motorOff());
case "getMotorRotationCount":
return String.valueOf(simulator.getMotorRotationCount());
case "resetMotorRotationCount":
return String.valueOf(simulator.resetMotorRotationCount());
default:
return unknownCommandError(cmd);
}
}
}
3. Définition de la classe MotorSimulator
Nous allons maintenant écrire le simulateur MotorSimulator
qui hérite de la classe abstraite AbstractSimulator
fournie dans le kit de développement. Cette dernière est une classe "runnable" qui actualise le simulateur de façon asynchrone, en faisant appel à la méthode update()
que vous allez définir dans MotorSimulator
.
Comme les précédents tutoriels, celui-ci n'a pas vocation à expliciter les API Java awt et Swing (classes graphiques, placement des composants, etc.), ces classes étant largement abordées sur le web.
La classe MotorSimulator
comporte les méthodes suivantes :
- Les méthodes correspondant aux instructions de l'API du module (motorOn, motorOff, etc.) ;
- Les méthodes invoquées par les boutons de la barre d'outils (non encore définie) ;
- La méthode
update()
d'actualisation des paramètres du simulateur ; - La méthode
paintComponent(Graphics graphics)
qui réalise la visualisation ;
Voilà ci-dessous une première version du simulateur :
package codelab.modules.motormodule;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import codelab.helper.AbstractSimulator;
import codelab.helper.PluginUtils;
public class MotorSimulator extends AbstractSimulator {
private MotorModule module;
private BufferedImage image1; // Image of the motor block
private BufferedImage image2; // Image of the rotating part
private double power = 0; // Power given to he motor
private double alpha = 0; // Actual angle of the axis
private double counter = 0; // Resetable counter
///////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////
public MotorSimulator(MotorModule module) {
this.module = module;
image1 = PluginUtils.loadBufferedImage(module, "motor.png");
image2 = PluginUtils.loadBufferedImage(module, "axis.png");
startSimulationThread(10); // Update every 10 ms
}
///////////////////////////////////////////////////
// Instruction methods
///////////////////////////////////////////////////
public int motorOn(int power) {
this.power = ((double) power) / 1000;
return 1;
}
public int motorOff() {
power = 0;
return 1;
}
public int getMotorRotationCount() {
return (int) Math.toDegrees(counter);
}
public int resetMotorRotationCount() {
counter = 0;
return 1;
}
///////////////////////////////////////////////////
// Toobar methods
///////////////////////////////////////////////////
public void stop() {
power = 0;
super.stop(); // Stop simulation
repaint(); // Repaint the motor
}
public void reset() {
power = 0;
alpha = 0;
counter = 0;
super.stop(); // Stop simulation
repaint(); // Repaint the motor
}
///////////////////////////////////////////////////
// Updating method
///////////////////////////////////////////////////
protected void update() {
alpha += power;
counter += power;
repaint(); // Repaint the motor
}
///////////////////////////////////////////////////
// Drawing method
///////////////////////////////////////////////////
private final int WIDTH = 2000; // Drawing zone
private final int HEIGHT = 1500; // Drawing zone
private final int X_AXIS = 540; // Position of image2
private final int Y_AXIS = 118; // Position of image2
private final int X_AXIS_LOCAL = 53; // Position of the axis in image2
private final int Y_AXIS_LOCAL = 50; // Position of the axis in image2
private final Color bgcolor = new Color(231, 231, 231);
private final int X0 = X_AXIS + X_AXIS_LOCAL;
private final int Y0 = Y_AXIS + Y_AXIS_LOCAL;
public void paintComponent(Graphics graphics) {
// Transformation of the motor's axis
AffineTransform affine = new AffineTransform();
affine.setToTranslation(X_AXIS, Y_AXIS);
affine.rotate(alpha, X_AXIS_LOCAL, Y_AXIS_LOCAL);
super.paintComponent(graphics);
Graphics2D g2d = getGraphics2D(graphics);
// Clear the background
g2d.setBackground(bgcolor);
g2d.clearRect(0, 0, WIDTH, HEIGHT);
// Draw the motor
g2d.drawImage(image1, null, null);
g2d.drawImage(image2, affine, null);
// Draw the graduation
g2d.setColor(Color.red);
for (int deg = 0; deg < 360; deg += 10) {
double a = Math.toRadians(deg);
double cos = Math.cos(a);
double sin = Math.sin(a);
g2d.drawLine(
X0 + (int)(120*cos), Y0 + (int)(120*sin),
X0 + (int)(130*cos), Y0 + (int)(130*sin));
}
g2d.dispose();
}
}
4. Définition de l'API du module
La cohérence de notre module repose sur une bonne correspondance entre :
- Le gestionnaire d'instructions de la classe
MotorModule
- Les méthodes publiques de la classe
MotorSimulator
- Les définitions du fichier XML décrivant l'API du module
Ce dernier permet notamment de générer les fichiers à inclure pour chaque langage cible. Rappellons que le nom de ce fichier est important puisqu'il détermine les noms des fichiers à inclure. Écrivez le document XML suivant dans le fichier NXTMotor.xml
et placez-le dans le répertoire includes
:
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE api SYSTEM 'api.dtd'>
<api module='MotorModule'>
<instruction name='motorOn' type='int'>
<arg name='power' type='int'/>
</instruction>
<instruction name='motorOff' type='int'/>
<instruction name='getMotorRotationCount' type='int'/>
<instruction name='resetMotorRotationCount' type='int'/>
</api>
5. Définition de la barre d'outils
L'écriture de la barre d'outils est on ne peut plus simple, puisqu'il suffit juste de demander l'ajout d'un bouton dédié RESET
avec un addButton("RESET")
dans le constructeur :
package codelab.modules.motormodule;
import codelab.AbstractToolbar;
public class MotorToolbar extends AbstractToolbar {
///////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////
public MotorToolbar(MotorModule module) {
super(module);
addSeparator();
addButton("RESET");
endToolbar();
}
///////////////////////////////////////////////////
// Action handler
///////////////////////////////////////////////////
public void action(String ident) {
switch (ident) {
// No specific button
default: super.action(ident);
}
}
}
6. Génération du module
Le module est maintenant prêt à être généré. Avant cela, vous pouvez éventuellement définir des templates, des exercices, voire internationnaliser votre module, comme cela est expliqué dans le premier tutoriel. Pour générer le module, exécutez le script de build, ce qui aura pour effet de placer un fichier MotorModule.pac
dans le dossier module
de CodeLab :
7. Ajout d'un tableau de bord
Pour terminer ce tutoriel, vous allez ajouter un tableau de bord pour afficher certaines données (puissance et compteur de rotation) en provenance du simulateur. Cette fonctionnalité, prévue dans CodeLab, est facilement réalisable. Il suffit d'impémenter l'interface DataTableInterface
qui comporte notamment trois méthodes principales :
- Une méthode
getNbProp()
qui retourne le nombre de propriétés à afficher ; - Une méthode
getPropKey(int)
qui retourne le nom de la ième propriété ; - Une méthode
getPropVal(int)
qui retourne la valeur de la ième propriété ;
Vous êtes libres de choisir la façon dont ces méthodes accèdent aux données, mais le plus simple est d'utiliser deux tableaux, comme dans l'exemple ci-dessous. L'actualisation des tableaux est réalisée dans la méthode updateValues()
qui est automatiquement invoquée par le simulateur.
Tout d'abord, complétez la classe MotorSimulator
de la façon suivante :
import codelab.DataTableInterface;
...
public class MotorSimulator extends AbstractSimulator implements DataTableInterface {
...
///////////////////////////////////////////////////
// DataTableInterface Implementation
///////////////////////////////////////////////////
private final int NBDATA = 2;
private String[] propKey = new String[NBDATA];
private String[] propVal = new String[NBDATA];
public int getNbProp() {
return NBDATA;
}
public String getPropKey(int i) {
return propKey[i];
}
public String getPropVal(int i) {
return propVal[i];
}
public void updateValues() {
propKey[0] = "Power";
propVal[0] = String.valueOf(power * 1000);
propKey[1] = "Counter";
propVal[1] = String.format("%d°", getMotorRotationCount());
}
}
Ensuite, précisez dans le constructeur de MotorModule
qu'un tableau de bord est disponible :
public MotorModule(CodeLab codelab) {
super(codelab);
setTitle("NXT motor");
setToolbar(new MotorToolbar(this));
setComponent(simulator = new MotorSimulator(this));
setSystem(simulator);
}
Enfin, ajoutez le bouton dédié DATA
dans la barre d'outils :
public MotorToolbar(MotorModule module) {
super(module);
addSeparator();
addButton("RESET");
addButton("DATA");
endToolbar();
}
Re-générez et testez votre module : vous devrier avoir un nouveau bouton qui ouvre un panel à droite, qui affiche les données numériques "Power" et "Counter" en provenance du simulateur.
Copyright © 2021 Jérôme Lehuen, Le Mans Université, France Version 21-06-2021