Dans le domaine de la robotique, le robot autonome peut avoir à choisir quelle tâche accomplir en fonction de son environnement. Par exemple, un robot aspirateur peut avoir à décider s’il doit nettoyer une pièce ou retourner à sa base de recharge. Pour gérer ces situations, on utilise souvent une machine à états finis (FSM - Finite State Machine en anglais) pour décrire les différents états possibles du robot et les transitions entre ces états.
Dans cette leçon, nous allons voir comment représenter une machine à états finis avec un diagramme d’états et comment le programmer pour un microcontrôleur Arduino (ou tout autre microcontrôleur compatible avec le langage C++).
La machine à états finis est utile pour choisir une tâche ou une action parmis plusieurs tâches et se concentrer uniquement sur la tâche choisie. Pour accomplir plusieurs tâches ou sous-tâches simultanément, on utilise une autre approche appelée la multi-tâche. C’est le sujet de la prochaine leçon. Les deux approches sont souvent combinées pour gérer le fonctionnement d’un robot autonome.
Énumération (enum
) :
un type de données qui permet de définir un ensemble de constantes numériques. C’est utile pour définir les différents états de la machine à états finis parce qu’on peut les nommer de façon descriptive au lieu de se rappeler du chiffre associé.
if-else if
pour gérer ce type de cas. Dans les FSM, la valeur à comparer est l’état et chaque valeur possible est un état défini dans l’énumération.Imagine un robot qui fait 3 tours à gauche et ensuite 3 tours à droite et finalement s’arrête.
C’est un exemple très simple qu’on peut programmer (avec raison) sans une machine à états finis. Mais sa simplicité nous permet de nous concentrer sur les nouveaux éléments de planification et de code qu’on voudra inclure pour gérer des cas plus complexes qui en auraient de besoin.
Voici le diagramme d’états pour ce robot :
Dans d’autres contextes, c’est possible qu’il y ait des boucles dans le diagramme d’états. Par exemple :
Il pourrait aussi y avoir des embranchements dans le diagramme d’états. Par exemple :
Si vous ajoutez l’extension Draw.io Integration à VS Code, vous pouvez produire des diagrammes comme celui ci-dessus directement dans VS Code. Simplement créer un nouveau fichier avec l’extension de fichier .drawio.png
et l’ouvrir en choisissant Draw.io comme éditeur. Vous aurez accès à la même interface que sur le site web app.diagrams.net mais sans avoir à quitter votre environnement de travail ni à télécharger le fichier pour l’inclure dans votre projet.
Préparer votre projet maintenant pour le reste des exercices qui suivent à la fin des notes.
FSM
.platformio.ini
: lib_deps = arduino-libraries/Servo@^1.2.1
afin d’ajouter la bibliothèque externe Servo
à votre projet.RobotDrive
de vos bibliothèques personnelles dans le dossier lib
du projet./src
de votre projet en faisant un clic-droit et en choisissant Enregistrez l'image sous...
pour le télécharger. C’est un fichier de type .drawio.png
que vous pouvez ouvrir et modifier avec l’extension Draw.io Integration de VS Code.Le code pour déclarer nos états en C++ ressemblerait, en version la plus simple à ceci :
1
2
3
4
5
6
7
8
9
10
11
12
13
// ... directives #include
// ... déclarations d'autres variables globales
enum class States {
SETUP,
TURN_LEFT,
TURN_RIGHT,
STOP
};
States currentState = States::SETUP;
// ... fonctions setup(), loop() et autres
enum class States
. Avec cette déclaration, nous venons de créer un nouveau type d’objet C++, comme les int
, float
, String
, etc. Notre type s’appelle States
.
C’est possible de déclarer un
enum
sans le mot-cléclass
, mais c’est déconseillé parce que les noms des états ne seront pas nécessairement exclusifs dans le programme ce qui peut introduire des erreurs difficiles à isoler.
{}
et séparés par des virgules ,
. Ils sont généralement écrits en majuscules pour indiquer qu’ils sont des constantes.;
.States
est déclarée en dehors de toute fonction, les états sont accessibles de partout dans le code (globalement).States currentState
est déclarée pour stocker l’état initial du robot. Notez qu’elle est de type States
parce que ses valeurs seront limitées à celles déclarées dans l’énumération.States
avec la syntaxe NomDeClasse::NomDeValeur
, dans notre cas States::SETUP
. Notez les double deux-points entre les identifiants. En arrivant à chaque condition de transition dans le programme, on assigne une nouvelle valeur à cette variable.
Un autre avantage de déclarer un
enum class
au lieu d’un simpleenum
est que les outils dans VS Code vous aident à compléter les noms des états. Par exemple, si vous tapezStates::
vous verrez une info-bulle apparaître avec une liste cliquable des états possibles.
Internellement, C++ assigne une valeur entière à chaque état dans l’énumération. Par défaut, la première valeur est 0 et chaque valeur suivante est incrémentée de 1. Ainsi, SETUP
est 0, TURN_LEFT
est 1, TURN_RIGHT
est 2 et STOP
est 3. Par défaut, chaque état reçoit une valeur unique. On peut définir nos propres valeurs si on veut, mais c’est rarement nécessaire pour une FSM où c’est simplement l’identifiant de l’état qui importe.
enum
Sans un enum
on peut déclarer nos états comme suit mais là on passe du temps à inventer des valeurs pour chaque état et on risque de faire des erreurs :
1
2
3
4
5
6
7
// alternative sans enum - déconseillée
const int SETUP = 0;
const int TURN_LEFT = 1;
const int TURN_RIGHT = 2;
const int STOP = 3;
int currentState = SETUP; // même type que les états (int)
Avec une machine à états finis (MEF ou FSM en anglais), la fonction loop()
d’un sketch Arduino sert uniquement à vérifier en continue une cascade conditionnelle pour savoir quel code exécuter en fonction de l’état courant du robot.
if-else if
Avec if-else if
, cela ressemblerait à ceci :
1
2
3
4
5
6
7
8
9
10
11
void loop() {
if (currentState == States::SETUP) {
currentState = States::TURN_LEFT; // transition immédiate
} else if (currentState == States::TURN_LEFT) {
// code pour l'état TURN_LEFT
} else if (currentState == States::TURN_RIGHT) {
// code pour l'état TURN_RIGHT
} else if (currentState == States::STOP) {
// code pour l'état STOP
}
}
On peut voir qu’à chaque fois que la boucle se répète, on vérifie l’état actuel du robot afin de choisir le code approprié à exécuter. Si l’état ne change pas, on exécute le même code à chaque itération de la boucle. Il faut alors inclure un mécanisme pour activer la condition de transition vers le prochain état dans le code pour chaque état.
La première transition, de l’état SETUP à l’état TURN_LEFT, est immédiate car la condition est simplement la fin de la fonction setup()
. Ainsi, la première fois que la fonction loop()
est appelée, on passe directement à l’état TURN_LEFT.
Cette structure if-else if
est tout à fait acceptable, mais on se répète beaucoup : la condition est toujours currentState == ÉTAT
. En plus, ces conditions sont un peu masquées par la structure du code, soit derrière } else if ()
et on peut avoir de la difficulté à trouver un cas spécifique.
Il y a une meilleure structure pour ce type de comparaison : switch-case
.
switch-case
Avec switch-case
, la même logique ressemblerait à ceci :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void loop() {
switch (currentState) {
case States::SETUP:
currentState = States::TURN_LEFT; // transition immédiate
break;
case States::TURN_LEFT:
// code pour l'état TURN_LEFT
break;
case States::TURN_RIGHT:
// code pour l'état TURN_RIGHT
break;
case States::STOP:
// code pour l'état STOP
break;
}
}
C’est immédiatement beaucoup plus lisible, mais il y a quelques détails à noter. Voici la structure générale d’un switch-case
:
1
2
3
4
5
6
7
8
switch (variable) {
case VALEUR1:
// code pour VALEUR1
break;
// autres cas
default:
// code pour toutes les autres valeurs
}
variable
est la variable qu’on veut comparer à des valeurs numériques spécifiquescase VALEUR1:
où VALEUR1
est une valeur possible pour variable
case VALEUR1:
et break;
Il ne faut pas oublier le mot-clé
break
à la fin de chaque cas. Sinon, le code continuera à exécuter tous les cas suivants jusqu’à ce qu’il trouve unbreak
ou qu’il arrive à la fin duswitch-case
.
default:
est optionnel et représente le cas où variable
ne correspond à aucune des valeurs spécifiées. C’est une façon de gérer les erreurs ou les cas inattendus.
Avec les FSM, on n’utilise pas souvent
default:
parce qu’on se réfère uniquement aux cas déclarés dans l’énumération.
Intégrant les nouveautées pour la FSM et le code que nous avons utilisé pour gérer les déplacements du robot (la bibliothèque personnelle RobotDrive
), le code pour la FSM pourrait ressembler à ceci :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <Arduino.h>
#include <RobotDrive.h>
/*
DÉFINIR LES CONNEXIONS MATÉRIELLES
*/
const int millisForOneTurn = 2100; // à calibrer; avec turnLeft() et turnRight()
/*
DÉFINIR LES ÉTATS DU ROBOT
*/
enum class States {
SETUP,
TURN_LEFT,
TURN_RIGHT,
STOP
};
States currentState = States::SETUP;
// initialiser le matériel et les connexions
void setup() {
setRobotDrivePins(10, 11);
}
// dans une MEF (FSM), sert à vérifier en perpétuité l'état de la machine
void loop() {
switch (currentState) {
case States::SETUP:
currentState = States::TURN_LEFT;
case States::TURN_LEFT:
turnLeft();
delay(3 * millisForOneTurn);
currentState = States::TURN_RIGHT;
break;
case States::TURN_RIGHT:
turnRight();
delay(3 * millisForOneTurn);
currentState = States::STOP;
break;
case States::STOP:
stop();
break;
}
}
Vous voyez sans doute que la FSM n’était pas nécessaire ici : on aurait pu simplement décrire les trois tours d’un côté et les trois tours de l’autre dans la fonction setup()
et laisser la fonction loop()
vide. Notamment, les conditions de transition ici sont simplement la fin de l’état précédent.
En général, la condition de transition est plus complexe que cela. Et certains états doivent gérer plusieurs actions en simultané. C’est là que la FSM devient très utile pour organiser le code. On verra un exemple dans la leçon sur la multi-tâche.
/src/main.cpp
et compilez-le pour vérifier qu’il n’y a pas d’erreurs de transcription.millisForOneTurn
pour que le robot fasse exactement 3 tours à gauche et 3 tours à droite.switch-case
.