Club robotique de Sophia-Antipolis

Accueil > Projets, études > Nos réalisations > TaxiBot > Le logiciel mBot

Le logiciel mBot

dimanche 6 juin 2021, par Eric P.

Un article séparé a été consacré au logiciel du mBot, car c’est l’occasion de présenter une méthode de programmation de l’Arduino un peu différente du sempiternel setup-loop.

Introduction

Comme vous le savez tous, quels que soient les cartes électroniques et les langages utilisés pour réaliser cette fonction, la caractéristique du programme de contrôle de tout robot est d’être réactif.

Qu’est-ce que cela signifie ?

Eh bien la même chose que dans la vie courante, à savoir réagir immédiatement à ce qui se passe autour. Réactif, réagir : vous voyez le rapport maintenant 😉

La partie loop du programme doit donc assurer la séquence suivante à chaque itération :

  • capturer l’environnement, c’est à dire lire les valeurs courantes des capteurs, qu’ils soient reliés directement sur des GPIO ou via un bus I2C ou SPI
  • si le robot communique avec l’extérieur via une liaison série, lire des données en attente s’ii y en a
  • fusionner l’ensemble de ces informations de manière à décider quoi faire. Certains appellent cela la partie IA du robot, moi je dirais plus modestement qu’il s’agit de la partie décisionnelle
  • appliquer les décisions en modifiant l’état de GPIOs, en envoyant des commandes sur des bus I2C ou SPI, voire des messages sur la liaison série

Le problème

On voit que même pour un robot basique (par exemple deux moteurs et deux capteurs de sol pour suivre une ligne) c’est déjà assez complexe, surtout si on veut itérer le plus rapidement possible de manière à ne pas réagir à la façon d’un Aï :

Encore plus délicat : les pauses. Imaginons que pour une raison quelconque, vous deviez attendre un certain délai pour faire quelque chose. Par exemple, vous utilisez un capteur de type bouton poussoir, ou interrupteur de fin de course mécanique. Un défaut de ce genre de capteur est l’existence de rebonds dus à la nature même du contact mécanique. Ca se traduit souvent par une série de changements d’états avant de se stabiliser sur la bonne valeur. Comment gère-t-on cela ? En procédant à ce qui s’appelle en Anglais le debouncing, qu’on pourait traduire par "suppression du rebond". Dans la pratique, ça consiste à refaire une seconde lecture en cas de changement d’état, mais avec un léger délai. Si on lit à nouveau la même valeur, c’est que le capteur a bien changé d’état et on peut donc prendre l’événement en considération. Sinon, il s’agissait d’un parasite et on peut donc ignorer l’événement.

Qu’a-t-on mentionné juste à l’instant ? Après un certain délai. Pas de problème allez-vous me dire, il suffit de glisser un delay() du nombre de microsecondes et l’affaire est entendue.

Horreur, malheur  😳

Ne faites jamais cela, c’est le mal absolu.

Mais pourquoi ?  🙁

Parce que pendant tout le temps que va durer le delay, votre robot sera aveugle, muet et paralysé car votre programme passe au point mort durant la pause.

OK, mais comment faire alors ?

On gère le temps au sein de notre célèbre loop. Autrement dit, lorsqu’on veut faire quelque chose plus tard (ici, une lecture de confirmation), on mémorise quelque part cette action et le moment auquel elle doit être réalisée et on continue le cours de la boucle. En plus des tâches vues précédemment (lire les capteurs, récupérer des données sur une liaison série...), la boucle va donc devoir surveiller également à chaque itération le temps courant (à l’aide de la fonction « millis() » par exemple) et vérifier s’il n’est pas temps de réaliser une action préalablement "programmée" pour s’exécuter à une certaine heure. Si oui, elle la fait et elle retire le rappel que nous nous étions mis pour ne pas oublier de la faire.

Pour des actions à réaliser périodiquement (par exemple toutes les secondes), on y parvient en re-postant à chaque fois qu’on débute l’exécution de l’action un rappel pour l’instant dans le futur où la prochaine répétition doit arriver.

OK, facile.

Oui s’il y a peu de choses à faire et peu de scénarios avec des délais à gérer. Mais si notre robot utilise une dizaine de capteurs, reçoit des instructions par radio sur sa liaison série, gère ses moteurs avec des accélérations et des décélérations, et j’en passe, ça devient vite inextricable.

La solution : Reactduino

Il s’agit d’une petite librairie Arduino qu’on trouve sur GitHub. Au lieu de tout faire à la main, on va lui confier la gestion de tout cela et se contenter de lui dire : s’il arrive cela, exécute telle séquence de code. Plus de setup, plus de loop. Reactduino s’en charge pour vous.

Illustrons cela par le classique blink.

 version classique

#include <Arduino.h>

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

 version Reactduino

#include <Reactduino.h>

Reactduino app([] () {
  pinMode(LED_BUILTIN, OUTPUT);

  app.repeat(1000, [] () {
      static bool state = false;
      digitalWrite(LED_BUILTIN, state = !state);
  });
});

Cet exemple est directement tiré du site et la syntaxe peut paraître bizarre car on y a recours à des fonctions anonymes (aka lambda functions), assez peu souvent utilisées par la plupart des Arduinistes. En fait on aurait pu l’écrire avec des fonctions normales, mais on y gagne en concision étant donné que les fonctions concernées ne sont référencées qu’à un seul endroit.

Ce qu’il faut en retenir est qu’on initialise une application Reactduino en lui passant une fonction qui enchaîne l’initialisation (ici pinMode pour la LED) et la définition des réactions aux événements. Dans cet exemple, on définit un timer périodique (T = 1 seconde) et on fournit le code à exécuter lorsque le moment est venu, à savoir inverser l’état de la LED, mémorisé dans une variable déclarée static afin de persister sa valeur.

Et c’est tout. Oubliés les setup() et loop(), Reactduino va s’en occuper pour nous. Si on va faire un tour dans son code source, on constate que l’habituelle fonction loop() existe bien, et qu’on y retrouve la gestion des différents types d’événements et de réactions supportés par Reactduino.

Ca peut sembler bien complexe pour un simple clignotement, encore que la version Reactduino est plus naturelle dans une certaine mesure, car on dit au programme ce qu’on veut faire (changer l’état de la LED toutes les secondes) alors que dans la version traditionnelle on lui dit comment le faire, à grands coups de delay() pour gérer le temps.

Les exemples qui suivent dans la documentation de Reactduino montrent par contre très rapidement combien son approche est séduisante et propre dès que les choses deviennent plus complexes, en introduisant progressivement plus d’actions et événements à gérer simultanément. Je vous engage à les étudier, et si vous avez déjà eu entre les mains un programme plat de spaghetti car devant gérer un tas de choses en même temps, vous comprendrez pourquoi je vous suggère cette bibliothèque (et pas uniquement parce que je fais partie de ses contributeurs 😉)

Un exemple du vrai monde

Le programme de contrôle du mBot de Taxibot a utilisé cette approche, et vous pouvez le consulter ici.

Pour se persuader de la clareté apportée par Reactduino, il suffit de lire le code du main de l’application, vers la fin du fichier app.cpp :

void app_main() {
    // initialize the command and tx_buffer buffer
    memset(rx_buffer, 0, sizeof(rx_buffer));
    cmde_p = rx_buffer;

    memset(tx_buffer, 0, sizeof(tx_buffer));
    tx_buffer[0] = ' ';

    // setup

    Serial.begin(SERIAL_BAUDS);
    app_reset();

    // register the reactions

    app.onSigUser(SIG_OBSTACLE, obstacle_detected);

    // periodic robot state update (sensors value, motion,...)
    app.onTick(update_application_state);

     // periodic sensor reading publication
    app.repeat(500, stream_sensors_to_controller);

   // serial command handling
    app.onAvailable(&Serial, handle_serial_rx);
}

Une fois les initialisations passées, on y trouve de manière explicite la définition des règles de comportement du robot :

  • si un obstacle a été détecté, on invoque le traitement associé
  • à chaque pas de temps (i.e. à chaque itération) on récupère les informations donnant l’état de l’environnement par le biais des capteurs du robot
  • on envoie au contrôlleur l’état des capteurs toutes les 500 ms
  • si des données ont été reçues sur la liaison série, on appelle le traitement associé

Difficile de faire plus explicite et clair. Aucun code pour gérer les actions périodiques, la détection de changement dans l’environnement ou l’arrivée de données sur la liaison série. Uniquement l’énumération de ce qu’on doit faire, et pas de comment détecter ceci ou cela.

Conclusion

De toute manière, c’est bien simple : je n’écris plus aucun programme Arduino sans elle. Même si le code pourrait ne pas le nécessiter au départ, le gros avantage est de savoir qu’on pourra ajouter par la suite la gestion de scénarios plus complexes sans craindre d’arriver à un moment donné à un labyrinthe inextricable truffé de bugs car ayant dérivé vers une logique totalement absconse.

Vos commentaires

  • Le 18 juillet 2021 à 12:27, par jn En réponse à : Le logiciel mBot

    Très sympa. Y aurait-il un tuto complet de toutes les fonctions en français quelque part ? en tant qu’éternel débutant en code, je suis toujours à l’affut d’une vulgarisation maximale.

    Répondre à ce message

Un message, un commentaire ?

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.