Commandes

Les commandes sont de simples à machines état fini qui exécutent des fonctions de haut niveau du robot en utilisant les méthodes définies par les sous-systèmes. Les commandes peuvent être soit inactives et, dans cas, elles ne font rien, soit planifiées, dans ce cas, le planificateur de commandes exécute un ensemble spécifique du code de la commande en fonction de l’état de la commande. L’objet CommandScheduler reconnaît que les commandes planifiées se font selon l’un des trois états : initialisation, exécution ou terminaison. Les commandes spécifient ce qui se fait dans chacun de ces états à travers les méthodes initialize(), execute() et end(). Les commandes sont représentées dans la librairie orientée commande par l’interface Command (Java, C++).

Créer des commandes

Note

Dans l’API C++, un CRTP est utilisé pour permettre certaines méthodes Command de fonctionner avec le modèle de propriété d’objets. L’utilisateur devrait toujours étendre la classe CommandHelper lorsqu’il définir sa propre classes de commande, comme démontré ci-dessous.

De même que les sous-systèmes, la méthode recommandée pour la plupart des utilisateurs pour créer une commande est de dériver la classe abstraite CommandBase (Java, C++), tel que vu dans le modèle orienté commande (Java, C++):

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import edu.wpi.first.wpilibj.templates.commandbased.subsystems.ExampleSubsystem;
import edu.wpi.first.wpilibj2.command.CommandBase;

/**
 * An example command that uses an example subsystem.
 */
public class ExampleCommand extends CommandBase {
  @SuppressWarnings({"PMD.UnusedPrivateField", "PMD.SingularField"})
  private final ExampleSubsystem m_subsystem;

  /**
   * Creates a new ExampleCommand.
   *
   * @param subsystem The subsystem used by this command.
   */
  public ExampleCommand(ExampleSubsystem subsystem) {
    m_subsystem = subsystem;
    // Use addRequirements() here to declare subsystem dependencies.
    addRequirements(subsystem);
  }

Comme avant, cela permet l’accès à des fonctionnalités pratiques. Il remplace automatiquement la méthode getRequirements() pour les utilisateurs, qui retourne une liste des exigences qui est vide par défaut; mais qui peut être ajoutée avec la méthode addRequirements(). Il implémente aussi l’interface Sendable et peut donc être envoyée au Dashboard - cela permet de programmer facilement des commandes pour les tests (par un bouton sur le tableau de bord) sans avoir besoin de les lier à des boutons sur un contrôleur.

Aussi comme avant, les utilisateurs avancées recherchant plus de flexibilité peuvent simplement créer leur propre classe qui implémente l’interface Command.

La structure d’une commande

Tandis que les sous-systèmes, qui n’ont pas de structure définie et qui peuvent généralement ressembler à ce que l’utilisateur souhaite, les commandes sont beaucoup plus contraignantes. Le code des commandes doit spécifier qu’est-ce que la commande va faire dans chacun des états possibles. Cela se fait en remplacer les méthodes initialize(), execute() et end(). De plus, une commande doit être capable de dire au planificateur quand elle a terminé son exécution (s’il y a lieu) - cela se fait par le remplacement de la méthode isFinished(). Toutes ces méthodes sont vide par défaut pour réduire l’encombrement dans le code de l’utilisateur. initialize(), execute() et end() sont vide par défaut, pour ne rien exécuter, et isFinished() retourne faux par défaut (résultant en une commande qui ne termine jamais).

Initialisation

La méthode initialize() (Java, C++) est exécutée exactement une fois à chaque fois qu’une commande est planifiée, dans la méthode schedule() du planificateur. La méthode run() du planificateur n’a pas besoin d’être appelée pour que la méthode initialize() s’exécute. Le bloc d’initialisation est lieu à utiliser pour placer la commande dans un état de départ connu pour l’exécution. Il est également utile pour effectuer des tâches qui n’ont besoin d’être exécutées qu’une fois à chaque itération du planificateur, comme le réglage des moteurs pour fonctionner à une vitesse constante ou le réglage de l’état d’un actionneur de type solénoïde.

Exécution

La méthode execute() (Java, C++) est appelée à plusieurs reprises tant que la commande est planifiée, chaque fois que la méthode run() du programmeur est appelée (cela se fait généralement dans la méthode periodic() du robot, qui s’exécute toutes les 20ms par défaut). Le bloc execute() doit être utilisé pour toute tâche qui doit être effectuée en permanence pendant que la commande est planifiée, comme la mise à jour des sorties du moteur pour correspondre aux entrées joystick, ou l’utilisation de la sortie d’une boucle de contrôle.

Terminaison

La méthode end() (Java, C++) est appelée une fois que la commande se termine, si elle se termine normalement (c’est-à-dire isFinished() retourne la valeur logique vrai) ou elle a été interrompue (soit par une autre commande ou en étant explicitement annulée). L’argument de la méthode précise la façon dont la commande a pris fin; les utilisateurs peuvent l’utiliser pour différencier le comportement de la fin de leur commande en conséquence. Le bloc final doit être utilisé pour « envelopper » l’état de commande d’une manière soignée, comme le réglage des moteurs à zéro ou le retour d’un actionneur de type solénoïde à un état par « défaut ».

Spécifier les conditions de terminaison

La méthode isFinished() (Java, C++) est appelée à plusieurs reprises pendant que la commande est planifiée, chaque fois que la méthode run() du planificateur est appelée. Dès qu’il retourne la valeur logique vraie, la méthode end() de la commande est appelée et elle n’est pas planifiée. La méthode isFinished() est appelée après la méthode execute() de sorte que la commande s’exécutera une fois dans la même itération qu’elle n’est pas planifiée.

Exemple de commande de base

À quoi pourrait ressembler une commande fonctionnelle dans la pratique? Comme précedemment, voici une commande simple du projet hatchbot exemple (Java, C++) qui utilise le HatchSubsystem introduit dans la section précédente:

 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
package edu.wpi.first.wpilibj.examples.hatchbottraditional.commands;

import edu.wpi.first.wpilibj2.command.CommandBase;

import edu.wpi.first.wpilibj.examples.hatchbottraditional.subsystems.HatchSubsystem;

/**
 * A simple command that grabs a hatch with the {@link HatchSubsystem}.  Written explicitly for
 * pedagogical purposes.  Actual code should inline a command this simple with {@link
 * edu.wpi.first.wpilibj2.command.InstantCommand}.
 */
public class GrabHatch extends CommandBase {
  // The subsystem the command runs on
  private final HatchSubsystem m_hatchSubsystem;

  public GrabHatch(HatchSubsystem subsystem) {
    m_hatchSubsystem = subsystem;
    addRequirements(m_hatchSubsystem);
  }

  @Override
  public void initialize() {
    m_hatchSubsystem.grabHatch();
  }

  @Override
  public boolean isFinished() {
    return true;
  }
}

Remarquez que le HatchSubsystem utilisé par la commande est passé dans la commande via le constructeur de la commande. C’est un modèle appelé l’injection de dépendances, et qui permet à l’utilisateur d’éviter de déclarer chaque sous-systèmes comme une variable globale. Ceci est largement accepté comme une bonne pratique - le raisonnement derrière ceci est expliqué dans une section ultérieure.

Remarquez aussi que la commande ci-dessus appelle la méthode de sous-système une fois dès l’initialisation et se termine immédiatement (puisque isFinished() retourne vrai). Ceci est normal pour les commandes qui changent les états d’un sous-système, et en fait, la bibliothèque orientée commande contient du code pour faire des commandes comme celle-ci encore plus succinctement.

Mais que se passe-t-il dans un cas plus compliqué? Ci-dessous se trouve une commande de pilotage, du même projet d’exemple :

 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
package edu.wpi.first.wpilibj.examples.hatchbottraditional.commands;

import java.util.function.DoubleSupplier;

import edu.wpi.first.wpilibj2.command.CommandBase;

import edu.wpi.first.wpilibj.examples.hatchbottraditional.subsystems.DriveSubsystem;

/**
 * A command to drive the robot with joystick input (passed in as {@link DoubleSupplier}s). Written
 * explicitly for pedagogical purposes - actual code should inline a command this simple with {@link
 * edu.wpi.first.wpilibj2.command.RunCommand}.
 */
public class DefaultDrive extends CommandBase {
  private final DriveSubsystem m_drive;
  private final DoubleSupplier m_forward;
  private final DoubleSupplier m_rotation;

  /**
   * Creates a new DefaultDrive.
   *
   * @param subsystem The drive subsystem this command wil run on.
   * @param forward The control input for driving forwards/backwards
   * @param rotation The control input for turning
   */
  public DefaultDrive(DriveSubsystem subsystem, DoubleSupplier forward, DoubleSupplier rotation) {
    m_drive = subsystem;
    m_forward = forward;
    m_rotation = rotation;
    addRequirements(m_drive);
  }

  @Override
  public void execute() {
    m_drive.arcadeDrive(m_forward.getAsDouble(), m_rotation.getAsDouble());
  }
}

Remarquez que la commande ne remplace pas isFinished(), et ne se terminera donc jamais; ceci est la norme pour les commandes qui sont destinées à être utilisées comme comandes par défaut (et, comme on peut le deviner, la bibliothèque inclus des outils pour rendre ce genre de commande plus facile a écrire aussi!).