[TP] Baignade interdite !

Afin d’appliquer vos connaissances acquises durant la lecture de ce tutoriel, nous allons maintenant faire un gros TP. Il regroupera tout ce que vous êtes censé savoir en terme de matériel (LED, boutons, voie série et bien entendu Arduino) et je vous fais aussi confiance pour utiliser au mieux vos connaissances en terme de "savoir coder" (variables, fonctions, tableaux…). Bon courage et, le plus important : Amusez-vous bien !

Sujet du TP

Contexte

Imaginez-vous au bord de la plage. Le ciel est bleu, la mer aussi… Ahhh le rêve. Puis, tout un coup le drapeau rouge se lève ! "Requiiiinn" crie un nageur… L’application que je vous propose de développer ici correspond à ce genre de situation. Vous êtes au QG de la zPlage, le nouvel endroit branché pour les vacances. Votre mission si vous l’acceptez est d’afficher en temps réel un indicateur de qualité de la plage et de ses flots. Pour cela, vous devez informer les zTouristes par l’affichage d’un code de 3 couleurs. Des zSurveillants sont là pour vous prévenir que tout est rentré dans l’ordre si un incident survient.

Objectif

Comme expliqué ci-dessus, l’affichage de qualité se fera au travers de 3 couleurs qui seront représentées par des LEDs :

  • Rouge : Danger, ne pas se baigner
  • Orange : Baignade risquée pour les novices
  • Vert : Tout baigne !

La zPlage est équipée de deux boutons. L’un servira à déclencher un SOS (si quelqu’un voit un nageur en difficulté par exemple). La lumière passe alors au rouge clignotant jusqu’à ce qu’un sauveteur ait appuyé sur l’autre bouton signalant "Problème réglé, tout revient à la situation précédente". Enfin, dernier point mais pas des moindres, le QG (vous) reçoit des informations météorologiques et provenant des marins au large. Ces messages sont retransmis sous forme de textos (symbolisés par la voie série) aux sauveteurs sur la plage pour qu’ils changent les couleurs en temps réel. Voici les mots-clés et leurs impacts :

  • Rouge : meduse, tempete, requin : Des animaux dangereux ou la météo rendent la zPlage dangereuse. Baignade interdite
  • Orange : vague : La natation est réservée aux bons nageurs
  • Vert : surveillant, calme : Tout baigne, les zSauveteurs sont là et la mer est cool
Conseil

Voici quelques conseils pour mener à bien votre objectif.

Réalisation

Une fois n’est pas coutume, nommez bien vos variables ! Vous verrez que dès qu’une application prend du volume il est agréable de ne pas avoir à chercher qui sert à quoi. - N’hésitez pas à décomposer votre code en fonction. Par exemple les fonctions changerDeCouleur() peuvent-être les bienvenues. :euh:

Précision sur les chaines de caractères

Lorsque l’on écrit une phrase, on a l’habitude de la finir par un point. En informatique c’est pareil mais à l’échelle du mot ! Je m’explique. Une chaîne de caractères (un mot) est, comme l’indique son nom, une suite de caractères. Généralement on la déclare de la façon suivante :

char mot[20] = "coucou"

Lorsque vous faites ça, vous ne le voyez pas, l’ordinateur rajoute juste après le dernier caractère (ici 'u’) un caractère invisible qui s’écrit \0 (antislash-zéro). Ce caractère signifie "fin de la chaîne". En mémoire, on a donc :

case   car.
mot[0] 'c'
mot[1] 'o'
mot[2] 'u'
mot[3] 'c'
mot[4] 'o'
 mot[5] 'u'
mot[6] '\0'
Correspondance des lettres vers le tableau

Ce caractère est très important pour la suite car je vais vous donner un petit coup de pouce pour le traitement des mots reçus.

Une bibliothèque, nommée "string" (chaîne en anglais) et présente nativement dans votre logiciel Arduino, permet de traiter des chaînes de caractères. Vous pourrez ainsi plus facilement comparer deux chaînes avec la fonction strcmp(chaine1, chaine2). Cette fonction vous renverra 0 si les deux chaînes sont identiques. Vous pouvez par exemple l’utiliser de la manière suivante :

// utilisation de la fonction strcmp(chaine1, chaine2) pour comparer des mots
int resultat = strcmp(motRecu, "requin");

if(resultat == 0)
    Serial.print("Les chaines sont identiques");
else
    Serial.print("Les chaines sont différentes");
Utilisation de strcmp

Le truc, c’est que cette fonction compare caractère par caractère les chaînes, or celle de droite : "requin" possède ce fameux '\0' après le 'n'. Pour que le résultat soit identique, il faut donc que les deux chaînes soient parfaitement identiques ! Donc, avant d’envoyer la chaîne tapée sur la voie série, il faut lui rajouter ce fameux '\0'.

Je comprends que ce point soit délicat à comprendre, je ne vous taperais donc pas sur les doigts si vous avez des difficultés lors de la comparaison des chaînes et que vous allez vous balader sur la solution… Mais essayez tout de même, c’est tellement plus sympa de réussir en réfléchissant et en essayant ! :)

Résultat

Prenez votre temps, faites-moi quelque chose de beau et amusez-vous bien ! Je vous laisse aussi choisir comment et où brancher les composants sur votre carte Arduino. :ninja: Voici une photo d’illustration du montage ainsi qu’une vidéo du montage en action.

Photo du TP : Baignade Intedite
Photo du TP : Baignade Intedite

Bon Courage !

Pour ceux voulant uniquement se plonger dans le code ou ne disposant pas du matériel, un simulateur avec uniquement le montage est disponible dans la correction, à la fin de la partie concernant l’électronique.

Correction !

J’espère que vous avez réussi à avoir un bout de solution ou une solution complète et que vous vous êtes amusé. Si vous êtes énervé sans avoir trouvé de solutions mais que vous avez cherché, ce n’est pas grave, regardez la correction et essayez de comprendre où et pourquoi vous avez fait une erreur. :)

Le schéma électronique

Commençons par le schéma électronique, voici le mien, entre vous et moi, seules les entrées/sorties ne sont probablement pas les mêmes. En effet, il est difficile de faire autrement que comme ceci :

[TP] Baignade Interdite - Schéma
[TP] Baignade Interdite - Schéma
[TP] Baignade Interdite - Montage
[TP] Baignade Interdite - Montage

Quelles raisons nous ont poussés à faire ces branchements ? Eh bien :

  • On utilise la voie série, donc il ne faut pas brancher de boutons ou de LED sur les broches 0 ou 1 (broche de transmission/réception)
  • On utilisera les LED à l’état bas, pour éviter que la carte Arduino délivre du courant
  • Les rebonds des boutons sont filtrés par des condensateurs (au passage, les boutons sont actifs à l’état bas)

Voici le simulateur avec juste l’électronique de prêt, à vous de faire le code !

!(https://www.tinkercad.com/embed/7NX9hi4SkUJ)

Les variables globales et la fonction setup()

Poursuivons notre explication avec les variables que nous allons utiliser dans le programme et les paramètres à déclarer dans la fonction setup().

Les variables globales
#define VERT 0
#define ORANGE 1
#define ROUGE 2

int etat = 0; // stock l'état de la situation (vert = 0, orange = 1, rouge = 2)
char mot[20]; // le mot lu sur la voie série

// numéro des broches utilisées
const int btn_SOS = 2;
const int btn_OK = 3;
const int leds[3] = {11,12,13}; // tableau de 3 éléments contenant les numéros de broches des LED
TP, initialisation des variables

Afin d’appliquer le cours, on se servira ici d’un tableau pour contenir les numéros des broches des LED. Cela nous évite de mettre trois fois int leds_xxx (vert, orange ou rouge). Bien entendu, dans notre cas, l’intérêt est faible, mais ça suffira pour l’exercice.

 Et c’est quoi ça "#define" ?

Le "#define" est ce que l’on appelle une directive de préprocesseur. Lorsque le logiciel Arduino va compiler votre programme, il va remplacer le terme défini par la valeur qui le suit. Par exemple, chaque fois que le compilateur verra le terme VERT (en majuscule), il mettra la valeur 0 à la place. Tout simplement ! C’est exactement la même chose que d’écrire : const int btn_SOS = 2;

La fonction setup()

Rien de particulier dans la fonction setup() par rapport à ce que vous avez vu précédemment, on initialise les variables

void setup()
{
    // On démarre la voie série avec une vitesse de 9600 bits/seconde
    Serial.begin(9600);

    // réglage des entrées/sorties
    // les entrées (2 boutons)
    pinMode(btn_SOS, INPUT);
    pinMode(btn_OK, INPUT);

    // les sorties (3 LED) éteintes
    for(int i=0; i<3; i++)
    {
        pinMode(leds[i], OUTPUT);
        digitalWrite(leds[i], HIGH);
    }
}
TP, le setup

Dans le code précédent, l’astuce mise en œuvre est celle d’utiliser une boucle for pour initialiser les broches en tant que sorties et les mettre à l’état haut en même temps ! Sans cette astuce, le code d’initialisation (lignes 11 à 15) aurait été comme ceci :

// on définit les broches, où les LED sont connectées, en sortie
pinMode(led_vert, OUTPUT);
pinMode(led_rouge, OUTPUT);
pinMode(led_orange, OUTPUT);

// On éteint les LED
digitalWrite(led_vert, HIGH);
digitalWrite(led_orange, HIGH);
digitalWrite(led_rouge, HIGH);

Si vous n’utilisez pas cette astuce dans notre cas, ce n’est pas dramatique. En fait, cela est utilisé lorsque vous avez 20 ou même 100 LED et broches à initialiser ! C’est moins fatigant comme ça… Qui a dit programmeur ? o_O

La fonction principale et les autres
Algorithme

Prenez l’habitude de toujours rédiger un brouillon de type algorithme ou quelque chose qui y ressemble avant de commencer à coder, cela vous permettra de mieux vous repérer dans l’endroit où vous en êtes sur l’avancement de votre programme. Voilà l’organigramme que j’ai fait lorsque j’ai commencé ce TP :

Organigramme baignade interdite
Organigramme baignade interdite

Et voilà en quelques mots la lecture de cet organigramme:

  • On démarre la fonction loop
  • Si on a un appui sur le bouton SOS :
    • On commence par faire clignoter la led rouge pour signaler l’alarme
    • Et on clignote tant que le sauveteur n’a pas appuyé sur le second bouton
  • Sinon (ou si l’évènement est fini) on vérifie la présence d’un mot sur la voie série
    • S’il y a quelque chose à lire on va le récupérer
    • Sinon on continue dans le programme
  • Enfin, on met à jour les drapeaux
  • Puis on repart au début et refaisons le même traitement

Fort de cet outil, nous allons pouvoir coder proprement notre fonction loop() puis tout un tas de fonctions utiles tout autour.

Fonction loop()

Voici dès maintenant la fonction loop(), qui va exécuter l’algorithme présenté ci-dessus. Vous voyez qu’il est assez "léger" car je fais appel à de nombreuses fonctions que j’ai créées. Nous verrons ensuite le rôle de ces différentes fonctions. Cependant, j’ai fait en sorte quelles aient toutes un nom explicite pour que le programme soit facilement compréhensible sans même connaître le code qu’elles contiennent.

void loop()
{
    // on regarde si le bouton SOS est appuyé
    if(digitalRead(btn_SOS) == LOW)
    {
        // si oui, on émet l'alerte en appelant la fonction prévue à cet effet
        alerte();
    }

    // puis on continu en vérifiant la présence de caractère sur la voie série
    // s'il y a des données disponibles sur la voie série
    // (Serial.available() renvoi un nombre supérieur à 0)
    if(Serial.available())
    {
        // alors on va lire le contenu de la réception
        lireVoieSerie();
        // on entre dans une variable la valeur retournée par
        // la fonction comparerMot()
        etat = comparerMot(mot);
    }
    // Puis on met à jour l'état des LED
    allumerDrapeau(etat);
}
TP, la boucle principale
Lecture des données sur la voie série

Afin de garder la fonction loop "légère", nous avons rajouté quelques fonctions annexes. La première sera celle de lecture de la voie série. Son job consiste à aller lire les informations contenues dans le buffer de réception du micro-contrôleur. On va lire les caractères en les stockant dans le tableau global mot[] déclaré plus tôt. La lecture s’arrête sous deux conditions :

  • Soit on a trop de caractère et donc on risque d’inscrire des caractères dans des variables n’existant pas (ici tableau limité à 20 caractères)
  • Soit on a rencontré le caractère symbolisant la fin de ligne. Ce caractère est '\n'.

Voici maintenant le code de cette fonction :

// lit un mot sur la voie série (lit jusqu'à rencontrer le caractère '\n')
void lireVoieSerie(void)
{
    // variable locale pour l'incrémentation des données du tableau
    int i = 0;

    // on lit les caractères tant qu'il y en a
    // OU si jamais le nombre de caractères lus atteint 19
    // (limite du tableau stockant le mot - 1 caractère)
    while(Serial.available() > 0 && i <= 19)
    {
        // on enregistre le caractère lu
        mot[i] = Serial.read();
        // laisse un peu de temps entre chaque accès a la mémoire
        delay(10);
        // on passe à l'indice suivant
        i++;
    }
    // on supprime le caractère '\n'
    // et on le remplace par celui de fin de chaine '\0'
    mot[i] = '\0';
}
TP, la fonction lireVoieSerie
Allumer les drapeaux

Voilà un titre à en rendre fou plus d’un ! Vous pouvez ranger vos briquets, on en aura pas besoin. ^^ Une deuxième fonction est celle permettant d’allumer et d’éteindre les LED. Elle est assez simple et prend un paramètre : le numéro de la LED à allumer. Dans notre cas : 0, 1 ou 2 correspondant respectivement à vert, orange, rouge. En passant le paramètre -1, on éteint toutes les LED.

/*
Rappel du fonctionnement du code qui précède celui-ci :
> lit un mot sur la voie série (lit jusqu'à rencontrer le caractère '\n')
Fonction allumerDrapeau() :
> Allume un des trois drapeaux
> paramètre : le numéro du drapeau à allumer
> (note : si le paramètre est -1, on éteint toutes les LED)
*/

void allumerDrapeau(int numLed)
{
    // On commence par éteindre les trois LED
    for(int j=0; j<3; j++)
    {
        digitalWrite(leds[j], HIGH);
    }
    // puis on allume une seule LED si besoin
    if(numLed != -1)
    {
        digitalWrite(leds[numLed], LOW);
    }

    /* Note : vous pourrez améliorer cette fonction en
    vérifiant par exemple que le paramètre ne
    dépasse pas le nombre présent de LED
    */
}
TP, la fonction allumerDrapeau

Vous pouvez voir ici un autre intérêt du tableau utilisé dans la fonction setup() pour initialiser les LED. Une seule ligne permet de faire l’allumage de la LED concernée !

Faire clignoter la LED rouge

Lorsque quelqu’un appui sur le bouton d’alerte, il faut immédiatement avertir les sauveteurs sur la zPlage. Dans le programme principal, on va détecter l’appui sur le bouton SOS. Ensuite, on passera dans la fonction alerte() codée ci-dessous. Cette fonction est assez simple. Elle va tout d’abord relever le temps à laquelle elle est au moment même (nombre de millisecondes écoulées depuis le démarrage). Ensuite, on va éteindre toutes les LED. Enfin, et c’est là le plus important, on va attendre du sauveteur un appui sur le bouton. TANT QUE cet appui n’est pas fait, on change l’état de la LED rouge toute les 250 millisecondes (choix arbitraire modifiable selon votre humeur). Une fois que l’appui du Sauveteur a été réalisé, on va repartir dans la boucle principale et continuer l’exécution du programme.

// Éteint les LED et fais clignoter la LED rouge
// en attendant l'appui du bouton "sauveteur"

void alerte(void)
{
    long temps = millis();
    boolean clignotant = false;
    allumerDrapeau(-1); // on éteint toutes les LED

    // tant que le bouton de sauveteur n'est pas appuyé
    // on fait clignoté la LED rouge
    while(digitalRead(btn_OK) != LOW)
    {
        // S'il s'est écoulé 250 ms ou plus depuis la dernière vérification
        if(millis() - temps > 250)
        {
            // on change l'état de la LED rouge
            // si clignotant était FALSE, il devient TRUE et inversement
            clignotant = !clignotant;
            // la LED est allumée au gré de la variable clignotant
            digitalWrite(leds[ROUGE], clignotant);
            // on se rappel de la date de dernier passage
            temps = millis();
        }
    }
}
TP, la fonction alerte
Comparer les mots

Et voici maintenant le plus dur pour la fin, enfin j’exagère un peu. En effet, il ne vous reste plus qu’à comparer le mot reçu sur la voie série avec la banque de données de mots possible. Nous allons donc effectuer cette vérification dans la fonction comparerMot(). Cette fonction recevra en paramètre la chaîne de caractères représentant le mot qui doit être vérifié et comparé. Elle renverra ensuite "l’état" (vert (0), orange (1) ou rouge (2)) qui en résulte. Si aucun mot n’a été reconnu, on renvoie "ORANGE" car incertitude.

int comparerMot(char mot[])
{
    // on compare les mots "VERT" (surveillant, calme)
    if(strcmp(mot, "surveillant") == 0)
    {
        return VERT;
    }
    if(strcmp(mot, "calme") == 0)
    {
        return VERT;
    }
    // on compare les mots "ORANGE" (vague)
    if(strcmp(mot, "vague") == 0)
    {
        return ORANGE;
    }
    // on compare les mots "ROUGE" (meduse, tempete, requin)
    if(strcmp(mot, "meduse") == 0)
    {
        return ROUGE;
    }
    if(strcmp(mot, "tempete") == 0)
    {
        return ROUGE;
    }
    if(strcmp(mot, "requin") == 0)
    {
        return ROUGE;
    }

    // si on a rien reconnu on renvoi ORANGE
    return ORANGE;
}
TP, la fonction comparerMot
Code complet

Comme vous avez été sage jusqu’à présent, j’ai rassemblé pour vous le code complet de ce TP. Bien entendu, il va de pair avec le bon câblage des LED, placées sur les bonnes broches, ainsi que les boutons et le reste… Je vous fais cependant confiance pour changer les valeurs des variables si les broches utilisées sont différentes.

#define VERT 0
#define ORANGE 1
#define ROUGE 2

int etat = 0; // stock l'état de la situation (vert = 0, orange = 1, rouge = 2)
char mot[20]; // le mot lu sur la voie série

// numéro des broches utilisées
const int btn_SOS = 2;
const int btn_OK = 3;

// tableau de 3 éléments contenant les numéros de broches des LED
const int leds[3] = {11,12,13};

void setup()
{
    // On démarre la voie série avec une vitesse de 9600 bits/seconde
    Serial.begin(9600);

    // réglage des entrées/sorties
    // les entrées (2 boutons)
    pinMode(btn_SOS, INPUT);
    pinMode(btn_OK, INPUT);

    // les sorties (3 LED) éteintes
    for(int i=0; i<3; i++)
    {
        pinMode(leds[i], OUTPUT);
        digitalWrite(leds[i], HIGH);
    }
}


void loop()
{
    // on regarde si le bouton SOS est appuyé
    if(digitalRead(btn_SOS) == LOW)
    {
        // si oui, on émet l'alerte en appelant la fonction prévue à cet effet
        alerte();
    }

    // puis on continu en vérifiant la présence de caractère sur la voie série
    // s'il y a des données disponibles sur la voie série
    // (Serial.available() renvoi un nombre supérieur à 0)
    if(Serial.available())
    {
        // alors on va lire le contenu de la réception
        lireVoieSerie();
        // on entre dans une variable la valeur retournée
        // par la fonction comparerMot()
        etat = comparerMot(mot);
    }
    // Puis on met à jour l'état des LED
    allumerDrapeau(etat);
}


// lit un mot sur la voie série (lit jusqu'à rencontrer le caractère '\n')
void lireVoieSerie(void)
{
    int i = 0; // variable locale pour l'incrémentation des données du tableau

    // on lit les caractères tant qu'il y en a
    // OU si jamais le nombre de caractères lus atteint 19
    // (limite du tableau stockant le mot - 1 caractère)
    while(Serial.available() > 0 && i <= 19)
    {
        mot[i] = Serial.read(); // on enregistre le caractère lu
        delay(10); // laisse un peu de temps entre chaque accès a la mémoire
        i++; // on passe à l'indice suivant
    }
    // on supprime le caractère '\n'
    // et on le remplace par celui de fin de chaine '\0'
    mot[i] = '\0';
}


/*
Rappel du fonctionnement du code qui précède celui-ci :
> lit un mot sur la voie série (lit jusqu'à rencontrer le caractère '\n')
Fonction allumerDrapeau() :
> Allume un des trois drapeaux
> paramètre : le numéro du drapeau à allumer
> (note : si le paramètre est -1, on éteint toutes les LED)
*/

void allumerDrapeau(int numLed)
{
    // On commence par éteindre les trois LED
    for(int j=0; j<3; j++)
    {
        digitalWrite(leds[j], HIGH);
    }
    // puis on allume une seule LED si besoin
    if(numLed != -1)
    {
        digitalWrite(leds[numLed], LOW);
    }

    /* Note : vous pourrez améliorer cette fonction en
vérifiant par exemple que le paramètre ne
dépasse pas le nombre présent de LED
*/
}


// Éteint les LED et fais clignoter la LED rouge
// en attendant l'appui du bouton "sauveteur"

void alerte(void)
{
    long temps = millis();
    boolean clignotant = false;
    allumerDrapeau(-1); // on éteint toutes les LED

    // tant que le bouton de sauveteur n'est pas appuyé
    // on fait clignoté la LED rouge
    while(digitalRead(btn_OK) != LOW)
    {
        // S'il s'est écoulé 250 ms ou plus depuis la dernière vérification
        if(millis() - temps > 250)
        {
            // on change l'état de la LED rouge
            // si clignotant était FALSE, il devient TRUE et inversement
            clignotant = !clignotant;
            // la LED est allumée au gré de la variable clignotant
            digitalWrite(leds[ROUGE], clignotant);
            // on se rappel de la date de dernier passage
            temps = millis();
        }
    }
}


int comparerMot(char mot[])
{
    // on compare les mots "VERT" (surveillant, calme)
    if(strcmp(mot, "surveillant") == 0)
    {
        return VERT;
    }
    if(strcmp(mot, "calme") == 0)
    {
        return VERT;
    }
    // on compare les mots "ORANGE" (vague)
    if(strcmp(mot, "vague") == 0)
    {
        return ORANGE;
    }
    // on compare les mots "ROUGE" (meduse, tempete, requin)
    if(strcmp(mot, "meduse") == 0)
    {
        return ROUGE;
    }
    if(strcmp(mot, "tempete") == 0)
    {
        return ROUGE;
    }
    if(strcmp(mot, "requin") == 0)
    {
        return ROUGE;
    }

    // si on a rien reconnu on renvoi ORANGE
    return ORANGE;
}
TP, code complet

Je rappel que si vous n’avez pas réussi à faire fonctionner complètement votre programme, aidez vous de celui-ci pour comprendre le pourquoi du comment qui empêche votre programme de fonctionner correctement ! A bons entendeurs. ;)

Voici la correction interactive :

!(https://www.tinkercad.com/embed/9xf4ByxOK1B)

Améliorations

Je peux vous proposer quelques idées d’améliorations que je n’ai pas mises en oeuvre, mais qui me sont passées par la tête au moment où j’écrivais ces lignes :

Améliorations logicielles

Avec la nouvelle version d’Arduino, la version 1.0,; il existe une fonction SerialEvent() qui est exécutée dès qu’il y a un évènement sur la voie série du micro-contrôleur. Je vous laisse le soin de chercher à comprendre comment elle fonctionne et s’utilise, sur cette page.

Améliorations matérielles
  • On peut par exemple automatiser le changement d’un drapeau en utilisant un système mécanique avec un ou plusieurs moteurs électriques. Ce serait dans le cas d’utilisation réelle de ce montage, c’est-à-dire sur une plage…
  • Une liaison filaire entre un PC et une carte Arduino, ce n’est pas toujours la joie. Et puis bon, ce n’est pas toujours facile d’avoir un PC sous la main pour commander ce genre de montage. Alors pourquoi ne pas rendre la connexion sans-fil en utilisant par exemple des modules XBee ? Ces petits modules permettent une connexion sans-fil utilisant la voie série pour communiquer. Ainsi, d’un côté vous avez la télécommande (à base d’Arduino et d’un module XBee) de l’autre vous avez le récepteur, toujours avec un module XBee et une Arduino, puis le montage de ce TP avec l’amélioration précédente.

Sérieusement si ce montage venait à être réalité avec les améliorations que je vous ai données, prévenez-moi par MP et faites en une vidéo pour que l’on puisse l’ajouter en lien ici même ! ^^


Voila une grosse tâche de terminée ! J’espère qu’elle vous a plu même si vous avez pu rencontrer des difficultés. Souvenez-vous, "à vaincre sans difficulté on triomphe sans gloire", donc tant mieux si vous avez passé quelques heures dessus et, surtout, j’espère que vous avez appris des choses et pris du plaisir à faire votre montage, le dompter et le faire fonctionner comme vous le souhaitiez !