mardi 30 juin 2020

Java - Loop, parallel loop, stream - approximation de Pi

Préambule

Avec Java 8 a été introduit l'API Stream qui permet de manipuler les collections, filtrer, sommer, faire une moyenne, mais bien sûr simplement boucler le tout sous forme de programmation fonctionnelle. Ce qui simplifie l'écriture du code et permet de se concentrer sur l'algorithme plutôt que sur la gestion des itérateurs et autres.

La question qui peut se poser alors est la suivante, quelle est l'impacte sur les performances de l'utilisation de l'API Stream à la place d'une simple boucle for. Intuitivement on a tendance à penser qu'une simple boucle for sera certainement plus performante, et bien c'est ce que nous allons essayer de voir.

Technos

Pour cet article nous allons utiliser Java 14.

Le test

Pour notre test nous allons utiliser une formule permettant de calculer une approximation de Pi. Cette formule demande un grand nombre d'itérations pour augmenter la précision de la décimal de Pi.
Il s'agit de la formule de Leibniz (voir Wikipedia) :

Comme vous pouvez le voir dans cette formule il faut faire tendre k vers l'infini pour avoir la meilleur précision de Pi. Bien évidemment on ne peut pas faire un calcul avec l'infini, on va donc faire varier k pour avoir plus ou moins de décimal juste de Pi

Une simple boucle

Nous commençons avec une boucle for pour laquelle le nombre d'itérations déterminera la précision du calcul.


private static void testPiSimpleLoop(int precision) {
	double pi = 0.0;
	double some = 0.0;

	for (int index = 0; index <= precision; index++) {
		some += Math.pow(-1, index) / (2*index + 1);
	}
	pi = 4 * some;
	System.out.println("Pi value (pecision : "+ precision +") : " + pi);
}

Nous allons maintenant faire plusieurs appel à notre méthode afin de voir le résultat du calcul et le temps de traitement.

public static void main(String[] args) {
	System.out.println("Simple loop");
	long start = System.currentTimeMillis();
	int prec = 100;
	while (prec <= 10000000) {
		testPiSimpleLoop(prec);
		prec = prec*10;
	}
	System.out.println("time : " + String.valueOf(System.currentTimeMillis() - start) + "s");
}

Exécutons maintenant notre application.

Simple loop
Pi value (pecision : 100) : 3.1315929035585537
Pi value (pecision : 1000) : 3.140592653839794
Pi value (pecision : 10000) : 3.1414926535900345
Pi value (pecision : 100000) : 3.1415826535897198
Pi value (pecision : 1000000) : 3.1415916535897743
Pi value (pecision : 10000000) : 3.1415925535897915
time : 288s

Nous constatons qu'avec 100 itérations nous avons une décimale de Pi qui est juste, puis en multipliant par 10 (donc 1000 itérations) nous avons deux décimales de Pi de juste et ainsi de suite.
Le temps de calcul pour 6 augmentations de précisions est de 288 seconds et il augmente de manière exponentiel à chaque augmentation de précision, nous allons donc nous contenter de 6 décimales exacte de Pi.

Une boucle avec l'API Stream

Nous allons maintenant faire le même calcul en utilisant l'API Stream, on va aussi remplacer la valeur de précision par le nombre de décimales juste en calculant la précision.

private static void testStreamPiDecimal(int decimal) {
	double pi = 0.0;
	double some = 0.0;
	long precision = (long) Math.pow(10, decimal+1);
		
	some = LongStream.rangeClosed(0, precision).mapToDouble(k -> {return Math.pow(-1, k) / (2*k + 1);}).sum();
	pi = 4 * some;
	
	System.out.println("Pi value (decimal : "+ decimal +", pecision : "+ precision +") : " + pi);
}

On ajoute ensuite l'appel à notre méthode dans notre main.

private static void testStreamPiDecimal(int decimal) {
	double pi = 0.0;
	double some = 0.0;
	long precision = (long) Math.pow(10, decimal+1);
		
	some = LongStream.rangeClosed(0, precision).mapToDouble(k -> {return Math.pow(-1, k) / (2*k + 1);}).sum();
	pi = 4 * some;
		
	System.out.println("Pi value (decimal : "+ decimal +", pecision : "+ precision +") : " + pi);
}

Exécutons maintenant notre application.

Simple loop
Pi value (pecision : 100) : 3.1514934010709914
Pi value (pecision : 1000) : 3.1425916543395442
Pi value (pecision : 10000) : 3.1416926435905346
Pi value (pecision : 100000) : 3.1416026534897203
Pi value (pecision : 1000000) : 3.1415936535887745
Pi value (pecision : 10000000) : 3.1415927535897814
time : 281s

Stream loop
Pi value (decimal : 1, pecision : 100) : 3.151493401070991
Pi value (decimal : 2, pecision : 1000) : 3.1425916543395434
Pi value (decimal : 3, pecision : 10000) : 3.1416926435905435
Pi value (decimal : 4, pecision : 100000) : 3.141602653489794
Pi value (decimal : 5, pecision : 1000000) : 3.1415936535887936
Pi value (decimal : 6, pecision : 10000000) : 3.141592753589783
time : 332s

On constate que l'utilisation de l'API Stream prend un peu plus de temps, l'écart se creuse encore si on augmente la précision.
Une solution pourrait être de lancer les calculs en parallèle et d'agréger les résultats à la fin. Ça tombe bien car l'API Stream propose une solution pour cela. Créons donc une méthode de calcul en parallèle.

Une boucle parallèle avec l'API Stream

private static void testStreamPiParallel(int decimal) {
	double pi = 0.0;
	double some = 0.0;
	long precision = (long) Math.pow(10, decimal+1);
		
	some = LongStream.rangeClosed(0, precision).parallel().mapToDouble(k -> {return Math.pow(-1, k) / (2*k + 1);}).sum();
	pi = 4 * some;
		
	System.out.println("Pi value (decimal : "+ decimal +", pecision : "+ precision +") : " + pi);
		
}

On ajoute ensuite l'appel à notre méthode dans notre main.

public static void main(String[] args) {
	System.out.println("Simple loop");
	long start = System.currentTimeMillis();
	int prec = 100;
	while (prec <= 10000000) {
		testPiSimpleLoop(prec);
		prec = prec*10;
	}
	System.out.println("time : " + String.valueOf(System.currentTimeMillis() - start) + "s");
		
	System.out.println("\nStream loop");
	start = System.currentTimeMillis();
	for (int decimal = 1; decimal <= 6; decimal++) {
		testStreamPiDecimal(decimal);
	}
	System.out.println("time : " + String.valueOf(System.currentTimeMillis() - start) + "s");

	System.out.println("\nStream parallel loop");
	start = System.currentTimeMillis();
	for (int decimal = 1; decimal <= 6; decimal++) {
		testStreamPiParallel(decimal);
	}
	System.out.println("time : " + String.valueOf(System.currentTimeMillis() - start) + "s");
}

Exécutons maintenant notre application.

Simple loop
Pi value (pecision : 100) : 3.1514934010709914
Pi value (pecision : 1000) : 3.1425916543395442
Pi value (pecision : 10000) : 3.1416926435905346
Pi value (pecision : 100000) : 3.1416026534897203
Pi value (pecision : 1000000) : 3.1415936535887745
Pi value (pecision : 10000000) : 3.1415927535897814
time : 282s

Stream loop
Pi value (decimal : 1, pecision : 100) : 3.151493401070991
Pi value (decimal : 2, pecision : 1000) : 3.1425916543395434
Pi value (decimal : 3, pecision : 10000) : 3.1416926435905435
Pi value (decimal : 4, pecision : 100000) : 3.141602653489794
Pi value (decimal : 5, pecision : 1000000) : 3.1415936535887936
Pi value (decimal : 6, pecision : 10000000) : 3.141592753589783
time : 319s

Stream parallel loop
Pi value (decimal : 1, pecision : 100) : 3.151493401070991
Pi value (decimal : 2, pecision : 1000) : 3.1425916543395434
Pi value (decimal : 3, pecision : 10000) : 3.1416926435905435
Pi value (decimal : 4, pecision : 100000) : 3.141602653489794
Pi value (decimal : 5, pecision : 1000000) : 3.1415936535887936
Pi value (decimal : 6, pecision : 10000000) : 3.141592753589783
time : 99s

Comme on peut le constater la parallélisation das calculs permet de réduire grandement le temps de calcul et donne même un temps de calcul inférieur à la simple boucle. En revanche ce que vous ne pouvez pas voir sur le résultat c'est la parallélisation de l'API Stream lance autant de threads que possible et consomme de se faite toutes les ressources disponible de mon processeur (tous les cœurs non occupés sont sollicités). Cela peut avoir un gros impact sur un calcul assez long, il existe un moyen de contourner ce problème, faisons une dernière méthode.

Une boucle parallèle avec l'API Stream et un pool de threads

private static void testStreamPiParallelPool4(int decimal) {
	double pi = 0.0;
	double some = 0.0;
	long precision = (long) Math.pow(10, decimal+1);
	
	ForkJoinPool customThreadPool = new ForkJoinPool(4);
	try {
		some = customThreadPool.submit(() -> LongStream.rangeClosed(0, precision).parallel().mapToDouble(k -> {return Math.pow(-1, k) / (2*k + 1);}).sum()).get();
	} catch (InterruptedException | ExecutionException e) {
		some = 0.0;
	}
	pi = 4 * some;
		
	System.out.println("Parallel pool Pi value (decimal : "+ decimal +", pecision : "+ precision +") : " + pi);
}

On ajoute ensuite l'appel à notre méthode dans notre main.

public static void main(String[] args) {
	System.out.println("Simple loop");
	long start = System.currentTimeMillis();
	int prec = 100;
	while (prec <= 10000000) {
		testPiSimpleLoop(prec);
		prec = prec*10;
	}
	System.out.println("time : " + String.valueOf(System.currentTimeMillis() - start) + "s");
		
	System.out.println("\nStream loop");
	start = System.currentTimeMillis();
	for (int decimal = 1; decimal <= 6; decimal++) {
		testStreamPiDecimal(decimal);
	}
	System.out.println("time : " + String.valueOf(System.currentTimeMillis() - start) + "s");

	System.out.println("\nStream parallel loop");
	start = System.currentTimeMillis();
	for (int decimal = 1; decimal <= 6; decimal++) {
		testStreamPiParallel(decimal);
	}
	System.out.println("time : " + String.valueOf(System.currentTimeMillis() - start) + "s");

	System.out.println("\nStream parallel loop pool 4");
	start = System.currentTimeMillis();
	for (int decimal = 1; decimal <= 6; decimal++) {
		testStreamPiParallelPool4(decimal);
	}
	System.out.println("time : " + String.valueOf(System.currentTimeMillis() - start) + "s");
}

Exécutons maintenant notre application.

Simple loop
Pi value (pecision : 100) : 3.1514934010709914
Pi value (pecision : 1000) : 3.1425916543395442
Pi value (pecision : 10000) : 3.1416926435905346
Pi value (pecision : 100000) : 3.1416026534897203
Pi value (pecision : 1000000) : 3.1415936535887745
Pi value (pecision : 10000000) : 3.1415927535897814
time : 279s

Stream loop
Pi value (decimal : 1, pecision : 100) : 3.151493401070991
Pi value (decimal : 2, pecision : 1000) : 3.1425916543395434
Pi value (decimal : 3, pecision : 10000) : 3.1416926435905435
Pi value (decimal : 4, pecision : 100000) : 3.141602653489794
Pi value (decimal : 5, pecision : 1000000) : 3.1415936535887936
Pi value (decimal : 6, pecision : 10000000) : 3.141592753589783
time : 320s

Stream parallel loop
Pi value (decimal : 1, pecision : 100) : 3.151493401070991
Pi value (decimal : 2, pecision : 1000) : 3.1425916543395434
Pi value (decimal : 3, pecision : 10000) : 3.1416926435905435
Pi value (decimal : 4, pecision : 100000) : 3.141602653489794
Pi value (decimal : 5, pecision : 1000000) : 3.1415936535887936
Pi value (decimal : 6, pecision : 10000000) : 3.141592753589783
time : 61s

Stream parallel loop pool 4
Parallel pool Pi value (decimal : 1, pecision : 100) : 3.151493401070991
Parallel pool Pi value (decimal : 2, pecision : 1000) : 3.1425916543395434
Parallel pool Pi value (decimal : 3, pecision : 10000) : 3.1416926435905435
Parallel pool Pi value (decimal : 4, pecision : 100000) : 3.141602653489794
Parallel pool Pi value (decimal : 5, pecision : 1000000) : 3.1415936535887936
Parallel pool Pi value (decimal : 6, pecision : 10000000) : 3.141592753589783
time : 112s

On constate quand limitant le pool de threads à 4 on augmente le temps de calcul, ce qui est logique vu que mon processeur contient plus de 4 cœurs, mais on sollicite moins les ressources de la machine. Il faut toujours garder à l'esprit que lorsque l'on parallélise un traitement il faut dimensionner le pool de threads utilisés selon le nombre de cœurs disponible sur la machine physique ou virtuel. En général on va cibler un pool de threads égale au nombre de processeurs de la machine ou un peu moins si on veux laisser des ressources pour d'autres applications.

Conclusion

Nous avons pu constater dans cet article que l'utilisation de l'API Stream sur un calcul un peu complexe était plus lent que l'utilisation d'une simple boucle for. En revanche il est relativement simple avec l'API Stream de paralléliser les calcul (il faut bien sûr que les calcul s'y prêtent), et que même si elle ne propose pas nativement une limitation du nombre de threads utilisés on pouvait l'inclure dans un pool de threads pour limiter les impactes sur les ressources.

J'espère que cet article vous aura intéressé et n’hésitez pas à laisser un commentaire si vous avez des questions ou des remarques constructives.
Merci d'avoir pris le temps de lire cet article et à bientôt.

Retrouvez les sources sur github.com

dimanche 19 avril 2020

Arduino - Horloge pixel bleu

Préambule


L'idée de ce projet est d'expérimenter un peu le Arduino et les modules qui gravitent autour, ainsi que marier cela avec l'impression 3D.
Je vais donc construire une petite horloge qui affichera l'heure et la date avec des pixels bleu, on pourra passer de l'heure à la date et vis versa à l'aide d'un bouton.

Matériels


Outils


  • Imprimente 3D Alfawise U30 Pro
  • Fer à souder
  • Pinces diverses
  • IDE Arduino
  • Breadboard et fils de teste

Conception


Pour mettre au point mon câblage et programmer mon arduino nano j'ai commencé par faire un montage sur une breadboard.

Câblage des composants


Une photo du câblage sur breadboard :


Ce qui donne schématiquement le montage suivant :


Les connexions sont les suivantes :

Max7219 Arduino
Vcc 5V
Gnd Gnd
DIN D11
CS D10
CLK D13

RTC DS3231 Arduino
Vcc 3V3
Gnd Gnd
SDA A4
SCL A5

Bouton Arduino
1 D3
2 Gnd


Programmation


Pour la programmation de notre horloge on utilise les librairies LedControl.h pour la gestion de l'affichage sur la matrice led et RTClib.h pour la lecture du module RTC.

Commençons par les déclarations :

#include "LedControl.h"
#include "RTClib.h"

#define CLK_PIN   13
#define DATA_PIN  11
#define CS_PIN    10

#define MAX_DEVICES 4
#define DELAY 50

#define button 3

LedControl lc = LedControl(DATA_PIN,CLK_PIN,CS_PIN,MAX_DEVICES);
RTC_DS3231 rtc;

On inclue les deux librairies précédemment citées, puis on défini le numéro des trois PIN digital de l'arduino qui vont commander les matrices led.
Ensuite on défini le nombre de matrices led qui constitue notre afficheur et un délai entre deux rafraîchissement de l'affichage. En dernier on défini sur quel PIN digital on va recevoir les appuis sur le bouton.
Enfin on récupère les références de LedControl et de RTC.

Déclaration des variables globales :

byte data[MAX_DEVICES][8];

// 4 x 7
byte numeric[][8] = {
  {0xF0,0x90,0x90,0x90,0x90,0x90,0xF0,0x00}, // 0
  {0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x00}, // 1
  {0xF0,0x10,0x10,0xF0,0x80,0x80,0xF0,0x00}, // 2
  {0xF0,0x10,0x10,0x70,0x10,0x10,0xF0,0x00}, // 3
  {0x90,0x90,0x90,0xF0,0x10,0x10,0x10,0x00}, // 4
  {0xF0,0x80,0x80,0xF0,0x10,0x10,0xF0,0x00}, // 5
  {0xF0,0x80,0x80,0xF0,0x90,0x90,0xF0,0x00}, // 6
  {0xF0,0x10,0x10,0x10,0x10,0x10,0x10,0x00}, // 7
  {0xF0,0x90,0x90,0xF0,0x90,0x90,0xF0,0x00}, // 8
  {0xF0,0x90,0x90,0xF0,0x10,0x10,0xF0,0x00}, // 9
  {0x00,0x00,0x80,0x00,0x80,0x00,0x00,0x00}, // :
  {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}  // empty
};

byte tempc[][8] = {
  {0x00,0xF8,0x20,0x20,0x20,0x20,0x20,0x00}, // T (5)
  {0x00,0x00,0x70,0x80,0x80,0x80,0x70,0x00}, // c (4)
  {0x00,0x00,0x00,0x80,0x00,0x80,0x00,0x00}  // : (1)
};

// 3 x 5
byte numdate[][8] = {
  {0x00,0x00,0xE0,0xA0,0xA0,0xA0,0xE0,0x00}, // 0
  {0x00,0x00,0x20,0x20,0x20,0x20,0x20,0x00}, // 1
  {0x00,0x00,0xE0,0x20,0xE0,0x80,0xE0,0x00}, // 2
  {0x00,0x00,0xE0,0x20,0xE0,0x20,0xE0,0x00}, // 3
  {0x00,0x00,0xA0,0xA0,0xE0,0x20,0x20,0x00}, // 4
  {0x00,0x00,0xE0,0x80,0xE0,0x20,0xE0,0x00}, // 5
  {0x00,0x00,0xE0,0x80,0xE0,0xA0,0xE0,0x00}, // 6
  {0x00,0x00,0xE0,0x20,0x20,0x20,0x20,0x00}, // 7
  {0x00,0x00,0xE0,0xA0,0xE0,0xA0,0xE0,0x00}, // 8
  {0x00,0x00,0xE0,0xA0,0xE0,0x20,0xE0,0x00}, // 9
  {0x00,0x00,0x20,0x40,0x40,0x40,0x80,0x00}  // /
};

int buttonVal = 1;

On commence par définir un tableau à deux dimensions de 4x8 pour accueillir l'état des led de chacune des quatre matrice led.
On construit ensuite des tableaux contenant les différentes combinaisons de valeurs des leds afin d'afficher les chiffres et les caractères nécessaires. Pour cela on donne l'état (1 ou 0) de chaque bit sur 8 lignes et 8 colonnes.
Pour définir les valeurs de chaque caractère je me suis aidé d'un petit outil permettant de dessiner graphiquement l'état de chaque led, il s'agit de PixelToMatrix que vous pouvez trouver à l'adresse suivante : http://generator1116.rssing.com/chan-36314998/all_p1.html
Enfin on déclare une variable qui nous servira à savoir quel affichage est demandé lors de l'appui sur le bouton.

Initialisation :

void setup() {
  pinMode(button, INPUT_PULLUP);
  
  for (int i = 0; i < MAX_DEVICES; i++)
  {
    lc.shutdown(i,false);
    lc.setIntensity(i,5);
    lc.clearDisplay(i);
    for (int j = 0; j < 8; j++)
    {
      data[i][j] = numeric[11][j];
    }
  }

  if (!rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }

  if (rtc.lostPower()) {
    Serial.println("RTC lost power, lets set the time!");
    // If the RTC have lost power it will sets the RTC to the date & time this sketch was compiled in the following line
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    // This line sets the RTC with an explicit date & time, for example to set
    // January 21, 2014 at 3am you would call:
    // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
  }
}

Dans la fonction setup() on initialise le PIN du bouton sur un mode de pullup interne, ensuite initialise des quatre matrices leds en les effaçant et en réglant leur intensité.
On initialise ensuite le module RTC en ajustant notamment la date et l'heure en fonction de celle de l'ordinateur qui téléverse le programme.

Boucle du programme :

void loop() {
  if (!digitalRead(button)) 
  {
    changeDisplay();
    delay(500);
  }
  fillData(true);
  display();
  delay(DELAY);
}

Dans la boucle du programme on commence par lire l'état du bouton (le bouton étant configuré en pullup on reçoit LOW quand il est appuyé et HIGH autrement), s'il est appuyé on change la valeur de display et on temporise pendant 500ms.
Ensuite on rempli le tableau de chaque matrice led en fonction de la valeur d'affichage sélectionnée par le bouton.
On envoi enfin les données à chaque matrice led pour l'affichage et on applique un délai correspondant à la valeur de délai défini au début du programme.

Remplissage des données :

void fillData(boolean sep) 
{
  DateTime now = rtc.now();
  if (buttonVal == 1) // Display hour
  {
    int num1 = now.hour() / 10;
    int num2 = now.hour() % 10;
    int num3 = now.minute() / 10;
    int num4 = now.minute() % 10;
    int num5 = now.second() / 10;
    int num6 = now.second() % 10;
    int separator = 10;
    if (!sep) {
      separator = 11;
    }
  
    for(int i = 0; i < 8; i++)
    {
      data[3][i] = (numeric[num1][i] >> 1) + (numeric[num2][i] >> 6);
      data[2][i] = (numeric[num2][i] << 2) + (numeric[separator][i] >> 2) + (numeric[num3][i] >> 3); //+ (numeric[num4][i] >> 7);
      data[1][i] = (numeric[num4][i]) + (numeric[separator][i] >> 4) + (numeric[num5][i] >> 5);
      data[0][i] = (numeric[num5][i] << 3) + (numeric[num6][i] >> 2);
    }
  }
  else if (buttonVal == 3) // Display temperature
  {
    int num1 = 0;
    int num2 = 0;
    int temperature = rtc.getTemperature();
    if (temperature < 10)
    {
      num2 = temperature;
    }
    else
    {
      num1 = temperature / 10;
      num2 = temperature % 10;
    }
    
    for(int i = 0; i < 8; i++)
    {
      data[3][i] = tempc[0][i] + (numdate[num1][i] >> 6);
      data[2][i] = (numdate[num1][i] << 2) + (numdate[num2][i] >> 2) + (tempc[1][i] >> 6);
      data[1][i] = (tempc[1][i] << 2);
      data[0][i] = numeric[11][i];
    }
  }
  else if (buttonVal == 2) // Display date
  {
    int jour1 = now.day() / 10;
    int jour2 = now.day() % 10;
    int mois1 = now.month() / 10;
    int mois2 = now.month() % 10;
    int annee1 = (now.year() % 100) / 10;
    int annee2 = (now.year() % 100) % 10;

    for(int i = 0; i < 8; i++)
    {
      data[3][i] = numdate[jour1][i] + (numdate[jour2][i] >> 4);
      data[2][i] = numdate[10][i] + (numdate[mois1][i] >> 4);
      data[1][i] = numdate[mois2][i] + (numdate[10][i] >> 4);
      data[0][i] = numdate[annee1][i] + (numdate[annee2][i] >> 4);
    }
  }
}

Dans la fonction filldata() on récupère l'heure et la date courante ou la température afin de préparer l'affichage sur chaque matrice led.
Pour l'heure par exemple on fait un découpage en des heures, minutes et secondes en deux chiffres puis on va chercher le caractère correspondant dans le tableau prédéfini pour l'afficher sur la matrice led. On effectue des décalage de bits pour placer chaque chiffre correctement sur l'affichage.

Affichage et changement de l'affichage :

void display()
{
  for (int matrix = 0; matrix < MAX_DEVICES; matrix++)
  {
    for (int row = 0; row < 8; row++)
    {
      lc.setRow(matrix,row,data[matrix][row]);
    }
  }
}

void changeDisplay() {
  buttonVal++;
  if (buttonVal > 3)
  {
    buttonVal = 1;
  }
}

Dans la fonction display() on parcoure le tableau de données pour l'envoyer aux matrices led qui constitues l'afficheur.
La fonction changeDisplay() quand à elle va changer la valeur de display lors de chaque appui sur le bouton pour passer de 1 à 3 puis reboucler lorsque la valeur 3 est atteinte.


Mise à jour de l'heure et de la date :

Pour finir sur la programmation j'ai intégré le code provenant de cet article : http://www.semageek.com/arduino-un-utilitaire-bien-pratique-pour-mettre-a-lheure-les-modules-rtc
Ce qui me permet de mettre à jour l'heure et la date sans avoir à re-téléverser le programme sur l'Arduino pour le changement d'heure d'été/hiver par exemple.


Impression 3D


Le boîtier est réalisé à l'aide de l'impression 3D et de la modélisation 3D avec le logiciel Fusion 360. Il se compose d'une partie supérieur et d'une partie inférieur, ainsi que d'une grille permettant de transformer les leds rondes en pixels carrés.


En haut de l'image la partie supérieur du boîtier avec la grille et un film opacifiant, en bas la partie inférieur avec en orange le bouton.


Les deux parties sous un autre angle de vue.


La photo avec le flash ne rend pas aussi bien que dans la réalité (affichage plus lumineux, etc..) mais cela donne une idée du résultat.
Un axe d'amélioration serait d'ajouter un film noir pour cacher le film opacifiant et assombrir l'écran.

Conclusion


Voici une petite réalisation qui ma permit d'expérimenter un peu avec un Arduino et quelques modules et de le combiner avec l'impression 3D.
J'espère que cet article vous aura intéressé et n’hésitez pas à laisser un commentaire si vous avez des questions ou des remarques constructives.
Merci d'avoir pris le temps de lire cet article et à bientôt.

Retrouvez les sources sur github.com

lundi 2 mars 2020

JavaFX + Spring Boot + Database

Préambule


Dans un précédent article j'ai détaillé comment intégrer Spring Boot dans une application JavaFX en utilisant Maven (Vous pouvez le consulter ici).
Cet article sera assez court car nous allons simplement reprendre l'application du précédant article et voir comme y intégrer une base de données SQL.


Technos

 

Nous reprenons les technologies précédemment utilisées en y ajoutant ce qui est nécessaire à l'intégration de la base de données.
  • JavaFX 8 (version de la JRE 8 en l’occurrence)
  • Maven 3.x (on pourrait aussi bien utiliser Gradle)
  • Spring Boot 2.0.5
  • H2Database 1.4.197 (version compatible avec la version de Spring Boot)

Tutoriel

 

Pour commencer reprenons les sources du précédent projet à partir du repos Github, vous pouvez les télécharger à cette adresse : jfxspringboot

Dépendances 

 

Nous avons besoin d'ajouter quelques dépendances dans le pom.xml de notre application pour nous connecter à la base de données et pour faire des requêtes.


...
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
...

On ajoute la dépendance pour la base de données H2 qui est une base de données assez légère pouvant être utilisée soit en mémoire, soit en fichiers.
Ensuite on ajoute le starter spring-boot-starter-data-jpa qui va nous ajouter les librairies, dont un ORM par défaut Hibernate, afin de créer, mapper les entités et requêter la base de données.

Configuration Spring Boot

 

Nous allons à présent définir les différents paramètres qui vont nous permettre de gérer notre base de données pour l'application.
Grace à Spring Boot et à son système de configuration automatique le paramétrage est très simple à effectuer, il nous suffit d'ajouter les lignes suivantes dans le fichier application.properties de notre application.
spring.datasource.url=jdbc:h2:file:./database/h2/appdb
spring.datasource.username=mydb
spring.datasource.password=
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false

Les propriétés spring.datasource vont permettre à Spring de configurer la base de données H2 en fournissant les paramètres de connexion.
Les propriétés spring.jpa vont quant à elles définir les paramètres de l'ORM (ici Hibernate).

La première ligne nous indique que nous utilisons une base de données JDBC de type H2 au format fichiers qui seront stockés dans le répertoire "database/h2/appdb" de notre application.
spring.datasource.url=jdbc:h2:file:./database/h2/appdb

Les deux lignes suivantes définissent le login/mot de passe de la base de données, ici pour l'exemple on ne met pas de mot de passe ce qui n'est pas vraiment recommandé :)
spring.datasource.username=mydb
spring.datasource.password=

La ligne suivante indique que nous générons le schéma de la base de données à partir du mapping des entités.
spring.jpa.generate-ddl=true

Ensuite nous configurons le mode "update" du genarate-ddl car par défaut il est configuré en "create/drop", ce qui détruit la base de données à l'arrêt de l'application.
spring.jpa.hibernate.ddl-auto=update

Et enfin nous indiquons que nous ne voulons pas tracer les requêtes SQL effectuées par l'ORM.
spring.jpa.show-sql=false

Entités, repositories, services

 

Afin d'utiliser notre base de données il nous faut définir ce qu'elle va contenir. Pour cela nous allons commencer par créer une entité qui correspondra à la table utilisateur de notre application.

package fr.jbe.jfxspringboot.db.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;

@Entity
public class User {

 @Id
 @SequenceGenerator(name = "userSequence", sequenceName = "SEQ_USER", allocationSize = 1)
 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "userSequence")
 private Long id;
 
 private String firstname;
 
 private String lastname;
 
 public User() {
 }

 public User(String firstname, String lastname) {
  this.firstname = firstname;
  this.lastname = lastname;
 }

 // Getter and Setter
 
}

Puis nous créons le repository qui va nous permettre de communiquer avec la base de données afin de créer et récupérer les données de la table User.

package fr.jbe.jfxspringboot.db.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import fr.jbe.jfxspringboot.db.domain.User;

@Repository
public interface IUserRepository extends JpaRepository<User, Long> {

 public User findByFirstname(String firstname);

}

Enfin nous ajoutons un service qui sera notre point d'entrée pour manipuler les données utilisateurs de la table User.

package fr.jbe.jfxspringboot.db.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import fr.jbe.jfxspringboot.db.domain.User;
import fr.jbe.jfxspringboot.db.repository.IUserRepository;

@Service
@Transactional
public class UserService {

 @Autowired
 private IUserRepository userRepository;
 
 public User findByFirstname(String firstname) {
  return userRepository.findByFirstname(firstname);
 }
 
 public List<User> findAll() {
  return userRepository.findAll();
 }
 
 public User save(User user) {
  return userRepository.save(user);
 }
}

Intégration à la vue JavaFX

 

Pour finir nous allons faire appel à notre service utilisateur dans le controller de la vue FXML de notre application afin de lister les utilisateurs présents dans la base de données.

Nous ajoutons dans le fichier FXML un bouton et un textarea qui vont servir à afficher notre liste d'utilisateurs.

<Label text="Liste des utilisateurs" GridPane.halignment="RIGHT" GridPane.rowIndex="3">
   <font>
      <Font name="System Bold" size="12.0" />
   </font>
</Label>
<Button mnemonicParsing="false" onAction="#handleDisplay" text="Afficher" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<TextArea fx:id="userList" prefHeight="200.0" prefWidth="200.0" GridPane.columnSpan="2" GridPane.rowIndex="4">
   <GridPane.margin>
       <Insets top="5.0" />
   </GridPane.margin>
</TextArea>

Puis dans la méthode initialize() du contrôler de la vue nous créons des nouveaux utilisateurs dans la table User s'ils n'existe pas déjà. Lors de l'appuis sur le bouton "Afficher" la méthode handleDisplay() va requêter la liste des utilisateur dans la base de données et les afficher dans le textarea.

 
 @FXML
 private void initialize() {
  name.clear();
  displayName.setText("");
  
  User user = userService.findByFirstname("Julien");
  if (user == null) {
   user = new User("Julien", "B");
   userService.save(user);
  }
  User user2 = userService.findByFirstname("David");
  if (user2 == null) {
   user2 = new User("David", "M");
   userService.save(user2);
  }
 }
 
 @FXML
 public void handleDisplay() {
  List users = userService.findAll();
  for (User user : users) {
   userList.appendText(user.getFirstname().concat(" ").concat(user.getLastname().concat("\n")));
  }
 }


Résultat

 

Voici ce que cela donne dans l'application :


Conclusion 

 

Nous avons pu voir qu'il était assez simple de configurer une base de données intégrée à notre application en se base sur l'auto-configuration qu'apport Spring Boot. Il ne reste plus ensuite qu'à injecter le service utilisateur dans le contrôler de notre choix pour pouvoir communiquer avec la base de données.

J'espère que cet article vous aura intéressé et n’hésitez pas à laisser un commentaire si vous avez des questions ou des remarques constructives.
Merci d'avoir pris le temps de lire cet article et à bientôt.

Retrouvez les sources sur github.com 

jeudi 20 septembre 2018

JavaFX + Spring Boot + Maven

Préambule


Dans mon travail de tous les jours je développe des applications web avec des technologies comme JSF + Spring + Hibernate, Angular + Spring Boot + Spring Data / Hibernate, etc...
Lorsque je me suis intéressé à JavaFX pour un petit projet personnel, j'ai commencé à développer mon application en utilisant le plugin e(fx)clipse afin de générer le projet.
Ensuite j'ai voulu ajouter une base de données fichiers pour sauvegarder les données de l'application, ajouter des librairie pour générer du PDF, etc... Et la je me suis rendu compte que toutes les choses bien pratiques que j'utilisais tous les jours dans le cadre du développement d'applications web (tel que l'IoC, l'ORM, la gestion des dépendances, ...) je devais les gérer ou les ajouter manuellement.
Partant de ce constat je me suis dit que j'allais faire de mon projet JavaFX un projet Maven et ensuite je me suis demandé s'il ne serait pas possible de marier JavaFX et Spring Boot pour profiter des avantages de ce dernier.
Et nous voilà donc dans cet article pour voir comment tout cela s'articule. Bon allé j'arrête de raconter ma vie, fort intéressante vous en conviendrez, et passons au coté technique.


Technos


Tout d'abord voici la liste des technologies utilisées dans cet article et leurs version au moment de l'écriture.
  • JavaFX 8 (version de la JRE 8 en l’occurrence)
  • Maven 3.x (on pourrait aussi bien utiliser Gradle)
  • Spring Boot 2.0.5


Tutoriel


Tout d'abord je tiens à préciser que cet article n'a pas pour but d'apprendre le développement d'une application JavaFX, il y a déjà pléthore d'articles sur le net pour cela, je vous laisse rechercher ça dans votre moteur de recherche favoris.


Spring Boot 


Pour commencer nous allons générer une application Spring Boot, pour cela personnellement j'utilise Spring Initializr qui permet de choisir ce que vous voulez ajouter dans votre application.

Choisissez Maven project, remplissez Group et Artifact, puis générez le projet.


Une fois le fichier zip téléchargé décompressez le et copiez le dans votre workspace afin de l'ouvrir avec votre IDE favoris (Pour ma part Eclipse).
Vous devriez vous retrouver avec quelque chose comme ça :



Pour le moment le fichier pom.xml ressemble à ce qui suit :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>fr.jbe</groupId>
 <artifactId>jfxspringboot</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>jfxspringboot</name>
 <description>Demo project for Spring Boot</description>

 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.0.5.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
 </parent>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  <java.version>1.8</java.version>
 </properties>

 <dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter</artifactId>
  </dependency>

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
  </plugins>
 </build>

</project>

Et le main de l'application devrait ressembler à quelque chose comme ça :



package fr.jbe.jfxspringboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JfxspringbootApplication {

 public static void main(String[] args) {
  SpringApplication.run(JfxspringbootApplication.class, args);
 }
}

Nous avons maintenant notre main classe avec l'annotation @SpringBootApplication qui permet de pré-configurer ce qui est nécessaire pour utiliser Spring. Bien sûr comme pour JavaFX je ne vais pas entrer dans le détail du fonctionnement de Spring Boot car ce n'est pas le sujet de cet article. Vous pouvez vous renseigne directement sur le site de Spring https://spring.io/projects/spring-boot.



JavaFX


Nous pouvons maintenant intégrer JavaFX à notre application Spring Boot. Pour cela il faut commencer par faire étendre Application.java à notre main classe.
Puis implémenter la méthode start() de l'application JavaFX dans laquelle on va construire la fenêtre principal de l'application puis l'afficher.


package fr.jbe.jfxspringboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javafx.application.Application;
import javafx.stage.Stage;

@SpringBootApplication
public class JfxspringbootApplication extends Application {

 public static void main(String[] args) {
  SpringApplication.run(JfxspringbootApplication.class, args);
 }

 @Override
 public void start(Stage primaryStage) throws Exception {
  // TODO Auto-generated method stub
  
 }
}

Ensuite afin de mieux intégrer Spring Boot à l'applicarion JavaFX nous allons récupérer le contexte Spring qui nous permettra plus tard d'injecter les contrôleurs lors du chargement des vue FXML.
Il faut donc ajouter la variable de contexte à notre classe, puis implémenter la méthode init() de JavaFX pour démarrer Spring Boot via la méthode run() et récupérer le contexte. La main méthode appelle maintenant la méthode launch() de JavaFX pour lancer l'application.


 private ConfigurableApplicationContext context;
 
 public static void main(String[] args) {
  launch(JfxspringbootApplication.class ,args);
 }
 
 @Override
 public void init() throws Exception {
  context = SpringApplication.run(JfxspringbootApplication.class);
 }


Voilà nous avons notre contexte prêt à être utilisé. Je prends le parti ici d'utiliser la création d'interfaces graphiques via les fichiers FXML pour profiter de l'utilisation de l'éditeur SceneBuilder qui est bien pratique. Mais bien sûr ce n'est pas obligatoire et on peut très bien construire les IHMs programmatiquement.



FMXL Loader


Afin de tirer partie de l'utilisation de Spring et de son contexte nous allons créer un loader pour notre application qui va intégrer le contexte au loader FMXL de JavaFX.


package fr.jbe.jfxspringboot.view;

import java.net.URL;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;

import javafx.fxml.FXMLLoader;

@Component
public class AppFXMLLoader {

 @Autowired
 private ConfigurableApplicationContext context;

 public FXMLLoader getLoader(URL url) {
  FXMLLoader loader = new FXMLLoader(url);
  loader.setControllerFactory(context::getBean);
  return loader;
 }
 
 public FXMLLoader getLoader(String fxmlPath) {
  return getLoader(this.getClass().getResource(fxmlPath));
 }

}


Comme on peut le voir ce loader créé une instance FXMLLoader en lui donnant le context::getBean comme controllerFactory ce qui lui permet d'utiliser le contexte Spring pour demander une instance du contrôleur associé à la vue FXML. Nous pouvons constater au passage que nous utilisons l'annotation @Autowired pour injecter le contexte.
Nous disposons maintenant d'un composant que nous allons pouvoir utiliser pour construire nos vue et notre scène. 

Fichier FXML


Tout d'abord il nous faut créer le FXML qui va correspondre à la fenêtre principale de notre application. faisons quelque chose de simple.

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>


<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.141" fx:controller="fr.jbe.jfxspringboot.view.controller.MainController">
   <center>
      <GridPane hgap="10.0" BorderPane.alignment="CENTER">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints maxHeight="-Infinity" minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints maxHeight="-Infinity" minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints maxHeight="-Infinity" minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <TextField fx:id="name" />
            <Button mnemonicParsing="false" onAction="#handleHello" text="Hello" GridPane.columnIndex="1" />
            <Label text="Bonjour" GridPane.halignment="RIGHT" GridPane.rowIndex="1" />
            <Label fx:id="displayName" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="1" />
         </children>
         <BorderPane.margin>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </BorderPane.margin>
      </GridPane>
   </center>
</BorderPane>

Voilà juste un textfield, un bouton et un label pour afficher le résultat.


FXML controller


Il faut maintenant associé un contrôleur à notre vue FXML afin de réagir à l'appui sur le bouton. C'est ce contrôleur qui sera gérer via le contexte Spring.

package fr.jbe.jfxspringboot.view.controller;

import org.springframework.stereotype.Component;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

@Controller
public class MainController {

 @FXML
 private TextField name;
 
 @FXML
 private Label displayName;
 
 @FXML
 private void initialize() {
  name.clear();
  displayName.setText("");
 }
 
 @FXML
 public void handleHello() {
  if (!name.getText().isEmpty()) {
   displayName.setText(name.getText());
  }
 }
 
}

Notez bien de la classe du contrôleur est annotée @Controller afin d'être prise en compte par l'IoC de Spring.


Créer la vue


Je vais maintenant créer une classe pour la création de la vue principale de l'application. Cette classe a pour but de charger la vue FXML principale et d'effectuer les opérations nécessaires, ici pas grand chose mais on pourrait imaginer setter des données sur le contrôleur. Elle permet aussi d'initialiser l'utilisation de l'IoC car comme vous le savez si vous instanciez à la main une classe elle ne peut plus bénéficier du DI.

package fr.jbe.jfxspringboot.view;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;

@Component
public class MainView {

 @Autowired
 AppFXMLLoader appFXMLLoader;
 
 public Scene createScene() {
  Scene scene = null;
  try {
   FXMLLoader loader = appFXMLLoader.getLoader(MainView.class.getResource("fxml/MainLayout.fxml"));
   BorderPane borderPane = loader.load();
   scene = new Scene(borderPane);
  } catch (IOException e) {
   e.printStackTrace();
  }
  return scene;
 }
 
}

Il ne nous reste plus qu'à implémenter la méthode start() de notre application.


Démarrons l'application


Voici l'intégration de tous ce que nous avons parlé au-dessus. Dans la classe JfxspringbootApplication.java il faut implémenter la méthode start() comme suit.

@Override
 public void start(Stage primaryStage) throws Exception {
  primaryStage.setTitle("Hello guys !");
  
  MainView mainView = context.getBean(MainView.class);
  primaryStage.setScene(mainView.createScene());
  primaryStage.show();
 }

Maintenant si nous lançons l'application nous devrions voir quelque chose qui ressemble à ce qui suit.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

2018-09-21 17:05:09.419  INFO 13800 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : Starting application on NUANDA-PC with PID 13800 (started by nuanda in D:\Dev\workspace\workspace_oxygen\jfxspringboot)
2018-09-21 17:05:09.422  INFO 13800 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : No active profile set, falling back to default profiles: default
2018-09-21 17:05:09.459  INFO 13800 --- [JavaFX-Launcher] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@79e35f75: startup date [Fri Sep 21 17:05:09 CEST 2018]; root of context hierarchy
2018-09-21 17:05:09.885  INFO 13800 --- [JavaFX-Launcher] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-09-21 17:05:09.896  INFO 13800 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : Started application in 0.707 seconds (JVM running for 1.206)

Avec la fenêtre de l'application.



Conclusion


Voilà pour les grandes lignes de la création d'une application JavaFX, Spring Boot et Maven. Comme vous avez pu le voir il n'y a rien de très compliqué, on peut bien sûr ajouter bien d'autre choses tel qu'une datasource pour se connecter à une base de données par exemple, mais cela sera peut être le sujet d'un prochain article.

J'espère que cet article vous aura intéressé et n’hésitez pas à laisser un commentaire si vous avez des questions ou des remarques constructives.
Merci d'avoir pris le temps de lire cet article et à bientôt.

Retrouvez les sources sur github.com

Voir l'article suivant : JavaFX + Spring Boot + Database