Revisitons la machine à états finis¶
Introduction¶
Nous avons vu les bases de la programmation orientée objet en C++ dans le cours précédent. Nous allons maintenant voir comment utiliser la programmation orientée objet pour créer une machine à états finis. Cependant avant de débuter, nous allons voir une nouvelle façon de structurer notre code pour les machines à états finis. Nous allons séparer les blocs d'entrée, d'exécution et de sortie pour chaque état. Cela nous permettra de mieux structurer notre code et de le rendre plus lisible.
Première version¶
Dans la première version de la machine à états finis, nous avons utilisé une structure simple pour gérer les états et les transitions. Tout le code était regroupé dans une seule fonction pour chaque état.
Il y avait certaines lacunes qui pouvaient devenir compliquées à gérer. Dans plusieurs situations, il est nécessaire de préparer un état avant de l'exécuter ou encore de faire des actions pour sortir d'un état. Par exemple, si nous avons un état qui fait tourner un moteur mais il doit allumer une DEL avant de s'exécuter, nous allons utiliser l'entrée pour allumer la DEL. De plus, si nous avons un état qui doit éteindre une DEL lorsque nous sortons de l'état, nous allons utiliser la sortie pour éteindre la DEL.
Nouvelle structure¶
Comme dans l'exemple précédent, les états nécessitaient une certaine préparation avant de s'exécuter ou encore des modifications pour sortir de l'état. Cela pouvait vous occasionner des petits maux de tête. Nous allons voir comment séparer un état en 3 parties: l'entrée, l'exécution et la sortie.
Prenez note que les entrées et sorties peuvent être optionnelles. Par exemple, si vous avez un état qui ne nécessite pas de préparation avant de s'exécuter, vous n'aurez pas besoin d'une fonction d'entrée.
Les états¶
Comme indiqué dans la section précédente, un état peut être séparé en 3 parties: l'entrée, l'exécution et la sortie.
L'entrée¶
L'entrée est exécutée lorsque l'état est activé. Elle est utilisée pour préparer l'état avant son exécution. Par exemple, si nous avons un état qui doit allumer une DEL avant de s'exécuter, nous allons utiliser l'entrée pour allumer la DEL.
L'entrée est le code d'initialisation de l'état.
L'exécution¶
Les instructions sont exécutées tant que l'état est actif. Ce bloc est utilisé pour exécuter l'état.
Par exemple, le moteur doit tourner. Nous allons utiliser l'exécution pour faire tourner le moteur.
La sortie¶
La sortie est exécutée lorsqu'une transition est validée. Elle est utilisée pour terminer l'état.
Par exemple, le moteur tourne tant et aussi longtemps qu'un bouton n'est pas appuyé. De plus, on veut que la DEL s'éteigne lorsque le moteur arrête de tourner. Nous allons utiliser la sortie pour éteindre la DEL.
Seconde transition¶
Il est possible d'avoir plusieurs transitions. Par exemple, en plus du clic du bouton, nous pouvons aussi faire en sorte que le moteur arrête de tourner après un certain délai. Nous allons utiliser une seconde transition pour cela.
Exemple¶
Voici un exemple quasiment complet d'une machine à états finis qui gère l'état d'un moteur lorsqu'un bouton est appuyé. Le moteur tourne tant que le bouton n'est pas réappuyé ou que 3 secondes ne sont pas écoulées. De plus, une DEL est allumée lorsque le moteur tourne et elle s'éteint lorsque le moteur arrête de tourner.
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
Attention
Dans le code ci-dessus, les transitions sont fusionnées dans le même bloc, car elles pointent vers le même état. Cependant, il est possible d'avoir des transitions avec des codes de sortie différents. Dans ce cas, il faut séparer les transitions dans des blocs différents.
Le code ci-dessus est un exemple pour illustrer la structure d'une machine à états finis. Il n'est pas complet et il peut y avoir des erreurs. Il est de votre responsabilité de compléter le code et de le tester.
Résumé¶
La mécanique de la machine à états finis est simple. Il suffit de définir :
- les états requis
- les transitions entre les états
- les fonctions pour chaque état.
La fonction est divisée en 3 parties:
- l'entrée
- l'exécution
- les sorties
Les sorties sont déclenchées par des transitions. Les transitions sont déclenchées par des événements ou des délais.
Définir les états requis¶
Avant de commencer à coder, il est important de définir les états requis pour notre machine à états finis. De plus, il faut aussi définir les transitions entre les états.
Un astuce qui permet de simplifier la structure de l'application est de se faire un schéma de la machine à états finis. Cela permet de visualiser les états et les transitions.
Voici un exemple de schéma de machine à états finis.
---
title: Machine à états finis
---
stateDiagram-v2
[*] --> Arrêt : Initialisation
Arrêt --> Fonctionne : Bouton désactivé
Fonctionne --> Arrêt : Bouton activé
On identifie deux états: - Arrêt - Fonctionne
On identifie deux transitions: - Bouton activé - Bouton désactivé
Utiliser la programmation orientée objet¶
Reprenons l'exemple précédent, mais convertissons-le en utilisant la programmation orientée objet. Nous allons aussi modifier le projet.
- Avant que le moteur entre en action, pour avertir l'utilisateur on doit faire clignoter une DEL pendant 3 secondes.
- Pendant que le moteur tourne, la DEL doit être allumée.
- Pendant que le moteur arrête de tourner, la DEL doit être éteinte graduellement.
---
title: Machine à états finis
---
stateDiagram-v2
[*] --> Arrêt : Initialisation
Arrêt --> Avertissement : Long appui
Avertissement --> Fonctionne : t > 3s
Fonctionne --> Arrêt : Bouton activé
Définir la classe¶
La classe doit avoir un constructeur qui prend en paramètre la broche du bouton, la broche de la DEL ainsi que celle du moteur. Elle doit aussi avoir une fonction update() qui devra être appelée dans la fonction loop().
Voici le code pour l'entête de la classe.
Voici le code du fichier .cpp.
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
Dans votre fichier .ino, vous pouvez maintenant utiliser la classe Motor.
Note sur les static_cast
Dans la bibliothèque OneButton, les fonctions de rappel (callbacks) doivent avoir cette signature :
Lorsque vous appelez attachClick(callback, this), la fonction reçoit un void* que nous devons convertir en pointeur vers la classe adéquate (Motor*). C’est le rôle de static_cast<Motor*>(context) :
Le static_cast<Motor*>(context) nous permet de convertir le paramètre générique void* reçu par la fonction de rappel en un pointeur de type Motor*. Ainsi, on peut accéder aux variables et méthodes de l'instance Motor liée au bouton correspondant. Cette conversion est nécessaire, car la bibliothèque OneButton ne connaît pas le type réel de l'objet passé en paramètre.
Conclusion¶
Nous avons vu comment réaliser une machine à états finis dans une classe qui définit un système. Lorsque nous sommes en mesure de déterminer les éléments d'un système, nous pouvons les encapsuler dans une classe. Cela nous permet de mieux structurer notre code et de le rendre plus lisible.