Machines à états finis¶
Les machines à états finis (FSM) sont souvent utilisées en programmation pour permettre des séries d’actions plus complexes. Cela est particulièrement utile lorsque plusieurs tâches doivent être exécutées en même temps, car cela permet aux tâches de dépendre de l’exécution de chacune d’entre elles de manière non linéaire.
Qu’est-ce qu’une machine à états finis ?¶
Le nom d’un automate à états finis est très descriptif : il s’agit d’un automate à états, avec un nombre fini d’états. Elle peut se trouver dans un état à la fois, et peut passer à un autre état lorsque quelque chose se produit. L’exemple Wikipedia d’un tourniquet explique très bien le concept.
Mise en œuvre¶
Mise en œuvre naïve¶
Lorsque les programmeurs découvrent les FSM, il est fréquent qu’ils essaient de les utiliser. Souvent, ils essaient d’appliquer une FSM à leurs programmes autonomes en segmentant leur programme autonome en une déclaration « switch » géante, et cela ressemble souvent à quelque chose comme ça :
while (opModeIsActive()) {
switch (state) {
case DETECT_SKYSTONE:
// skystone detection code here
int position = detectSkystone();
if (position == 0) {
state = SKYSTONE_POS_0;
}
else if (position == 1) {
state = SKYSTONE_POS_1;
}
else {
state = SKYSTONE_POS_2;
}
break;
case SKYSTONE_POS_0:
// skystone position 0 here
doSkystone(0);
state = MOVE_FOUNDATION;
break;
case SKYSTONE_POS_1:
case SKYSTONE_POS_2:
// etc etc
break;
case MOVE_FOUNDATION:
// foundation move code
state = PARK;
break;
case PARK:
// park the bot
break;
}
}
Cependant, cela ne présente pas vraiment d’avantages par rapport au fait que le programmeur aurait simplement placé chacun des segments du code dans des fonctions et les aurait exécutés dans l’ordre. En fait, les programmeurs structurent souvent leur code de cette manière au lieu de le diviser en fonctions. Il en résulte un code autonome plus difficile à déboguer et, en fin de compte, plus difficile à corriger à la volée lors d’une compétition ou d’un autre problème de temps.
Si l’on dessinait le diagramme de transition d’état pour chacun des états, pour l’autonomus ci-dessus, il serait très linéaire, et les transitions d’état se produisent toujours parce que la section du code s’est terminée :
En fait, dans de nombreuses implémentations, il est souvent difficile d’effectuer des transitions d’état pour toute autre raison parce que le code s’exécute de manière linéaire et n’est dans une boucle que pour réexécuter les instructions de commutation. (Souvent, cela signifie que le code a du mal à réagir à une demande d’arrêt au milieu de l’autonomie).
Avertissement
Il est déconseillé d’écrire du code de cette manière. Si votre autonomie est synchrone, il est préférable de diviser votre code en fonctions et de les exécuter dans l’ordre, car cela sera plus facile à comprendre et à modifier à la volée.
Une mise en œuvre utile¶
Les FSM sont le bon outil à utiliser lorsqu’un robot doit accomplir plusieurs tâches à la fois ; un exemple courant est celui d’un robot qui doit être automatisé en téléopération, tout en gardant le contrôle de la chaîne cinématique.
Souvent, les équipes rencontrent des problèmes parce que leur téléopération s’exécute en boucle et que leur logique d’asservissement comporte des périodes de sommeil. Mais nous pouvons éviter cela si nous écrivons le code de manière asynchrone - où, au lieu d’attendre la fin d’une tâche avant d’exécuter la suivante, les tâches sont exécutées en même temps, et l’état de chaque tâche est vérifié sans interrompre l’exécution des autres tâches.
Par exemple, si l’on dispose d’un robot semblable au robot Rover Ruckus de Gluten Free <https://www.youtube.com/watch?v=NQvhvYJXVMA>`__, et que l’on souhaite automatiser l’élévateur de pointage afin que les chauffeurs n’aient pas à réfléchir pendant que le robot dépose les minéraux. Deux parties du robot nous intéressent dans le cadre de cet exercice : l’élévateur de pointage incliné et le servomoteur qui fait basculer le tombereau pour que les minéraux tombent. L’objectif est de pouvoir appuyer sur un bouton pour que le robot s’exécute :
étendre l’ascenseur,
à l’extension complète de l’élévateur, inclinez le servo du godet à minéraux pour déposer les minéraux,
attendre que les minéraux tombent,
remettre le servo dans sa position initiale
rétracter l’ascenseur
Si les pilote appuient sur un autre bouton spécifique, nous cesserons d’exécuter les actions ci-dessus en guise de sécurité - au cas où le robot se briserait d’une manière ou d’une autre et où les pilotes devraient prendre le contrôle manuellement. Pendant ce temps, les pilotes devraient toujours être en mesure de contrôler notre châssis afin que nous puissions effectuer des ajustements. Bien entendu, ce schéma est un peu simplifié (et ne correspond probablement pas tout à fait à ce que GF a fait), mais il suffit pour l’instant.
Avant de programmer quoi que ce soit, il peut être utile de dessiner le diagramme d’état pour obtenir une meilleure compréhension de ce que le robot devrait réellement faire. Cela peut également contribuer à la présentation d’un prix de contrôle.
Remarquez que la réinitialisation du servo de déversement et la rétraction de l’ascenseur partagent un même état. C’est parce que le robot n’a pas besoin d’attendre la réinitialisation du servo pour faire descendre l’ascenseur ; les deux peuvent se produire en même temps.
Passons maintenant à l’implémentation du code. Dans un OpMode traditionnel, qui est communément utilisé pour la téléopération, le code s’exécute de façon répétée dans une fonction loop(), donc au lieu d’attendre qu’une transition d’état se produise directement, le code vérifiera de façon répétée à chaque appel loop()` s'il doit effectuer une transition d'état. Ce type de modèle de "mise à jour de notre état" empêche le code de bloquer l'exécution du reste du code ``loop(), tel que le groupe motopropulseur.
/*
* Some declarations that are boilerplate are
* skipped for the sake of brevity.
* Since there are no real values to use, named constants will be used.
*/
@TeleOp(name="FSM Example")
public class FSMExample extends OpMode {
// An Enum is used to represent lift states.
// (This is one thing enums are designed to do)
public enum LiftState {
LIFT_START,
LIFT_EXTEND,
LIFT_DUMP,
LIFT_RETRACT
};
// The liftState variable is declared out here
// so its value persists between loop() calls
LiftState liftState = LiftState.LIFT_START;
// Some hardware access boilerplate; these would be initialized in init()
// the lift motor, it's in RUN_TO_POSITION mode
public DcMotorEx liftMotor;
// the dump servo
public Servo liftDump;
// used with the dump servo, this will get covered in a bit
ElapsedTime liftTimer = new ElapsedTime();
final double DUMP_IDLE; // the idle position for the dump servo
final double DUMP_DEPOSIT; // the dumping position for the dump servo
// the amount of time the dump servo takes to activate in seconds
final double DUMP_TIME;
final int LIFT_LOW; // the low encoder position for the lift
final int LIFT_HIGH; // the high encoder position for the lift
public void init() {
liftTimer.reset();
// hardware initialization code goes here
// this needs to correspond with the configuration used
liftMotor = hardwareMap.get(DcMotorEx.class, "liftMotor");
liftDump = hardwareMap.get(Servo.class, "liftDump");
liftMotor.setTargetPosition(LIFT_LOW);
liftMotor.setMode(DcMotor.RunMode.RUN_TO_POSITION);
}
public void loop() {
liftMotor.setPower(1.0);
switch (liftState) {
case LIFT_START:
// Waiting for some input
if (gamepad1.x) {
// x is pressed, start extending
liftMotor.setTargetPosition(LIFT_HIGH);
liftState = LiftState.LIFT_EXTEND;
}
break;
case LIFT_EXTEND:
// check if the lift has finished extending,
// otherwise do nothing.
if (Math.abs(liftMotor.getCurrentPosition() - LIFT_HIGH) < 10) {
// our threshold is within
// 10 encoder ticks of our target.
// this is pretty arbitrary, and would have to be
// tweaked for each robot.
// set the lift dump to dump
liftDump.setTargetPosition(DUMP_DEPOSIT);
liftTimer.reset();
liftState = LiftState.LIFT_DUMP;
}
break;
case LIFT_DUMP:
if (liftTimer.seconds() >= DUMP_TIME) {
// The robot waited long enough, time to start
// retracting the lift
liftDump.setTargetPosition(DUMP_IDLE);
liftMotor.setTargetPosition(LIFT_LOW);
liftState = LiftState.LIFT_RETRACT;
}
break;
case LIFT_RETRACT:
if (Math.abs(liftMotor.getCurrentPosition() - LIFT_LOW) < 10) {
liftState = LiftState.LIFT_START;
}
break;
default:
// should never be reached, as liftState should never be null
liftState = LiftState.LIFT_START;
}
// small optimization, instead of repeating ourselves in each
// lift state case besides LIFT_START for the cancel action,
// it's just handled here
if (gamepad1.y && liftState != LiftState.LIFT_START) {
liftState = LiftState.LIFT_START;
}
// mecanum drive code goes here
// But since none of the stuff in the switch case stops
// the robot, this will always run!
updateDrive(gamepad1, gamepad2);
}
}