Cours WpiLib

Version : 17 mai, 2020

Bienvenue dans ce tutoriel écrit par l’équipe 5553, Robo’Lyon, de France.

Son but est de permettre aux rookies de s’initier à la programmation d’un robot FRC en C++ avec la librairie WpiLib.

Attention

Si vous n’êtes pas encore à l’aise avec le C++ et/ou la programmation orientée objet, ne commencez pas ce tutoriel. Retournez plutôt vers un cours de C++ comme celui d”openclassroom.

Toute idée d’amélioration est la bienvenue. Pour en proposer, créez une issue dans le projet github ou contactez l’équipe.

Si vous voulez contribuer à ce cours, vous pouvez soumettre une pull request sur le projet github du cours. Les sources sont dans le dossier source/docs et sont écrites en reStructuredText. C’est un langage de balisage très facile à prendre en main, voici un guide pour apprendre sa syntaxe.

Sommaire

WpiLib, c’est quoi ?

Comment contrôle-t-on un robot ?

Le robot est contrôlé par le RoboRio, le cerveau du robot. C’est lui que nous allons programmer. Il communique avec de nombreux composants du robot pour, par exemple, envoyer des ordres aux moteurs et aux vérins. Il reçoit aussi les informations venant des capteurs du robot (encodeurs, ultrasons, limit switch).

Le RoboRio est connecté par ethernet à la borne wifi du robot. Il peut ainsi communiquer par wifi avec l’ordinateur du pilote.

Voici un schéma des branchements du robot pour mieux comprendre :

_images/Schema.jpg

Sur l’ordinateur du pilote, c’est le programme appelé FRC Driver Station qui permet de communiquer avec le robot. Celui-ci permet au pilote de contrôler le robot avec son joystick.

Et WpiLib dans tout ça ?

Présentation

WpiLib (Worcester Polytechnic Institute Robotics Library) est une librairie qui permet la programmation du RoboRio. C’est un ensemble de classes qui permettent l’interface entre le logiciel et le hardware.

_images/Wpilib.jpg

Cette librairie nous fournit des classes pour gérer les moteurs, la pneumatique, les capteurs et à peu près tout ce dont nous avons besoin pour contrôler un robot. C’est une librairie de haut-niveau, c’est à dire qu’elle nous permet de manipuler facilement des objets complexes.

WpiLib est disponible en C++ et en Java. Il existe aussi des versions non-officiellement supportées en Python ou en Kotlin.

Installation

Pour programmer le robot en C++ et en Java, WpiLib propose d’utiliser Visual Studio Code qui est l’IDE officiellement supporté. Pour installer l’environnement de programmation, un installer Windows est disponible. Les instructions sont à retrouver ici.

VS Code permet l’utilisation des classes de WpiLib mais aussi de déployer le programme sur le robot.

Pour contrôler le robot, il faut aussi avoir la Driver Station. Voici les instructions pour l’installer l’installer.

Documentation

Pour obtenir des informations sur la librairie, savoir à quoi sert quelle classe, quelles fonctions sont disponibles, ect…, une documentation est disponible. Chaque classe y est répertoriée avec la description de chacune des méthodes qu’elle propose.

Notre premier programme

Créer un nouveau projet sur VS Code

_images/Create_new_project.gif

Pour créer un nouveau projet, cliquez sur l’icône WPILib puis sur WPILib: Create a new project. Une page s’ouvre, choisissez Template, c++ et TimedRobot Skeleton (Advanced). Remplissez les autres champs et appuyez sur Generate Project : c’est fait !

A quoi ressemble un programme de robot ?

Le programme du robot créé est séparé en 2 fichiers src/main/include/Robot.h et src/main/cpp/Robot.cpp. Voici à quoi le fichier Robot.h ressemble :

#include <frc/TimedRobot.h>

class Robot : public frc::TimedRobot
{
public:
  void RobotInit() override;

  void AutonomousInit() override;
  void AutonomousPeriodic() override;

  void TeleopInit() override;
  void TeleopPeriodic() override;

  void TestInit() override;
  void TestPeriodic() override;
};

Mais où est le main() ?

En effet, le programme du robot est différent de ce que l’on a l’habitude de voir. Au lieu d’avoir un int main() et beaucoup de fonctions, le programme est une classe. Bien sûr, il existe quelque par un main; mais il ne change jamais et il est déjà écrit pour nous.

Le programme du robot va donc lire la classe que nous allons coder.

Lisons le programme ligne par ligne

#include <frc/TimedRobot.h> : Cette ligne inclut le fichier « frc/TimedRobot.h »

class Robot : public frc::TimedRobot : Notre classe s’appelle Robot et qu’elle hérite d’une autre classe appelée TimedRobot. TimedRobot a été inclus précédemment, cette classe fait partie de la librairie WpiLib.

Ensuite, on voit que notre classe Robot possède plusieurs méthodes : RobotInit(), AutonomousInit(), AutonomousPeriodic(), ect… Ce sont ces méthodes que nous allons coder pour programmer le robot.

Toutes ces déclarations de méthode sont suivies du mot-clé override. Petite explication : ces méthodes sont en fait héritées de la classe mère TimedRobot. Cela permet à n’importe quelle programme d’appeler ces méthodes en étant sûr qu’elles existent. Par défaut, ces méthodes sont vides et ne font rien. C’est pourquoi on peut les override : ce sera ainsi notre version de la méthode qui sera appelée au lieu de la version vide du TimedRobot.

Les différentes méthodes

Le TimedRobot nous propose donc une structure pour coder le robot qui gère les transitions entre les états du robot et les boucles dans ces états. Pour chaque état (autonomous, teleop, disabled, test), deux méthodes sont appelées :

  • Init : cette méthode est appelée chaque fois que l’on entre dans l’état correspondant (par exemple si l’on passe de Disable à Teleop, la méthode TeleopInit() sera appelée une fois)

  • Periodic : cette méthode est appelée toutes les 20ms quand on est dans l’état correspondant (par exemple si l’on est en Teleop, la méthode TeleopPeriodic() sera appelée un fois toutes les 20ms).

Pour comprendre le fonctionnement du robot, essayez d’afficher un message pour chaque méthode. Les sorties cout sont redirigées vers le réseau et on peut les lire grâce à Riolog ou sur la Driver Station.

Attention

Comme ces méthodes sont appelées très fréquemment, il ne faut pas y écrire du code trop long à s’executer. Sinon, cela bloque le programme et pose des problèmes. Les boucles while, do .. while et for sont donc en général à éviter. On utilisera à la place de celles-ci des if qui seront appelés régulièrement.

Utiliser WpiLib

Pour l’instant, on utilise une seule classe fournie par WpiLib, la classe TimedRobot. Elle est incluse grâce à la ligne :

#include <frc/TimedRobot.h>

Mais pour programmer le robot nous allons avoir besoin des autres classes WpiLib pour controller les moteurs, les capteurs, … Une façon très rapide d’inclure toutes ces classes à la fois est d’écrire cette ligne au début de notre programme :

#include <frc/WPILib.h>

Vous pouvez jeter un coup d’oeil à l’intérieur de ce ficher. Il inclut en fait à son tour toutes les classes de WpiLib (dont TimedRobot).

#include "frc/ADXL345_I2C.h"
#include "frc/ADXL345_SPI.h"
#include "frc/ADXL362.h"
#include "frc/ADXRS450_Gyro.h"
#include "frc/AnalogAccelerometer.h"
#include "frc/AnalogGyro.h"
#include "frc/AnalogInput.h"
#include "frc/AnalogOutput.h"
#include "frc/AnalogPotentiometer.h"
#include "frc/AnalogTrigger.h"
#include "frc/AnalogTriggerOutput.h"
#include "frc/BuiltInAccelerometer.h"
#include "frc/Compressor.h"
#include "frc/ControllerPower.h"
#include "frc/Counter.h"
#include "frc/DMC60.h"
#include "frc/DigitalInput.h"
#include "frc/DigitalOutput.h"
........
......

Contrôler un moteur

Les contrôleurs moteur

Pour contrôler les moteurs présents sur le robot, nous avons besoin de contrôleurs moteurs. En un mot, ceux-ci reçoivent un signal de faible intensité de la part du RoboRio et envoient au moteur un signal de plus forte intensité. Voici quelques exemples de contrôleurs moteur que nous utilisons : VictorSP, Spark et SparkMax.

_images/Controllers.jpg

PWM et CAN

Il y a 2 manières de contrôler ceux-ci : en PWM ou en CAN. Tous les contrôleurs supportent le PWM mais seulement une partie d’entre eux supporte le CAN.

La principale différence entre les deux modes de transmission est que le PWM ne peut commander que la vitesse du moteur tandis que le CAN permet des contrôles plus avancés et la communication d’informations entre le contrôleur et le RoboRio.

_images/CanPwm.jpg

La méthode la plus simple est le PWM. Elle nous permet de contrôler rapidement le moteur voulu en branchant le cable du contrôleur sur le bon port PWM du RoboRio (encadrés en rouge).

_images/Roborio.jpg

Dans le Code

Du côté du RoboRio, il nous suffit de créer un objet correspondant au contrôleur pour pouvoir asservir le moteur. WpiLib propose une classe pour chaque contrôleur. En fait, avec le PWM, ces classes sont toutes presque identiques car elles dérivent toutes de la même classe PWMSpeedController. Cependant, elles ont été dérivées sous plusieurs noms :

#include <frc/VictorSP.h>
#include <frc/Spark.h>
#include <frc/PWMVictorSPX.h>

Remarquez que VictorSPX est précédé de PWM, c’est parce qu’il peut être contrôlé via le CAN. Pour différencier les 2 classes (qui sont totalement différentes, celui-ci se nomme donc PWMVictorSPX.

Quand on crée un objet qui représente un contrôleur PWM, on doit spécifier dans le constructeur le port sur lequel il est branché. Par exemple, pour un VictorSP branché sur le port n°0 :

frc::VictorSP mon_moteur(0);

On a ensuite accès à tout un tas de méthodes qui nous permettent de contrôler le moteur.

Pour « donner » une puissance voulue à un moteur, on utilise la méthode : void Set(double value) qui attend en argument un double entre -1 (pleine puissance vers l’arrière) et 1 (pleine puissance vers l’avant). Si je veux faire tourner mon moteur à la moitié de sa vitesse maximum :

mon_moteur.Set(0.5);

On a aussi void StopMotor() qui remplace Set(0) et void SetInverted(bool isInverted) qui inverse la direction du moteur si on lui donne comme argument true.

mon_moteur.SetInverted(true);

mon_moteur.StopMotor();

Lire les entrées d’un joystick

Le Joystick

Pour permettre au pilote de contrôler le robot, celui-ci utilise un joystick. Nous pouvons aussi utiliser une manette de Xbox.

Le joystick que nous utilisons est le Logitech 3D Pro. Il possède 3 axes (avant/arrière, gauche/droite et twist), 12 boutons, un POV (bouton avec 9 positions possibles) et un throttle (manette type avion).

_images/Joystick.jpg

On peut connaître l’état de chaque bouton/axe du joystick en temps réel sur la Driver Station :

_images/Ds.jpg

Dans le Code

WpiLib fournit une classe Joystick qui nous permet de récupérer les infos de celui-ci. Son constructeur attend en argument le numéro du joystick. Les numéros sont attribués selon l’ordre de branchement : le 1er joystick sera le 0, le 2ème le 1, ect … Si on a un seul joystick, il aura donc pour numéro 0 :

#include <frc/Joystick.h>

frc::Joystick mon_joystick(0);

Pour récupérer l’état d’un bouton (appuyé/relâché, on peut utiliser la méthode bool GetRawButton(int button) qui attend en argument le numéro du bouton et qui renvoi true si le bouton est appuyé et false si il est relâché.

// Récupère l'état de la gâchette (trigger)
bool gachetteAppuyee = mon_joystick.GetRawButton(1);

Pour récupérer la position d’un axe entre -1 et 1, on peut utiliser les méthodes double GetX(), double GetY(), double GetZ() (ou GetTwist()) et double GetThrottle().

// Récupère l'état de chaque axe
double x = mon_joystick.GetX();
double y = mon_joystick.GetY();
double twist = mon_joystick.GetTwist();
double throttle = mon_joystick.GetThrottle();

Note

Pour adoucir les valeurs du joystick, il est possible d’ajouter une deadband : une zone dans laquelle les valeur sont rejetées car trop petites. Pour en apprendre plus sur les deadbands, voici un site trés intéressant.

On a aussi la méthode int GetPOV() qui renvoi l’angle formé par le POV ou -1 si il est situé au centre.

int pov = mon_joystick.GetPOV();

switch (pov)
{
    case -1:
        // Centre
        break;

    case 0:
        // Haut
        break;

    case 180:
        // Bas
        break;

    case 90
        // Droite
        break;

    case 270
        // Gauche
        break;
}

1er Défi : Contrôler un moteur grâce au joystick

Coder la méthode TeleopPeriodic d’un TimedRobot pour répondre à ces objectifs :

  • Quand le joystick est proche de zéro (entre -0.2 et 0.2), le moteur ne tourne pas.

  • Sinon, le moteur tourne à une vitesse proportionnelle à la position du joystick

  • Quand la gâchette (bouton 1) du joystick est appuyée, le moteur tourne pas

Correction

Normalement, votre programme sera séparé en 2 fichiers différents : Robot.h et Robot.cpp. Ici, le programme est dans un seul fichier pour plus de simplicité :

#include <frc/TimedRobot.h>
#include <frc/VictorSP.h>
#include <frc/Joystick.h>

class Robot : public frc::TimedRobot
{
public:
    void TeleopPeriodic() override
    {
        if(m_Joystick.GetButton(1))
        {
            m_Moteur.Set(0);
        }
        else
        {
            double y = m_Joystick.GetY();

            if(y < 0.2 && y > -0.2)
            {
                y = 0;
            }

            m_Moteur.Set(y);
        }
    }

private:
    frc::Joystick m_Joystick(0);
    frc::VictorSP m_Moteur(0);
};

Utiliser les capteurs

Maintenant que l’on sait faire bouger notre robot, faisons le bouger intelligemment. Mais cela n’est pas possible si nous n’avons pas d’indications sur l’état du robot. C’est pourquoi nous utilisons des capteurs.

Limit switch

Description

L’un des types de capteur les plus simple à utiliser est le limit switch. C’est un interrupteur qui revient à sa place quand il n’est pas pressé (un bouton poussoir).

_images/Limit_switch.jpg

Ce capteur nous permet de savoir si un mécanisme a atteint un certain point. Par exemple, un limit switch situé au bout d’un bras pivotant peut nous dire si le bras touche le bord du robot.

Un limit switch se branche aux ports DIO du Roborio grâce à deux cables. Un va sur le ground (⏚) et l’autre sur le signal (S). Pourtant, le switch possède souvent 3 connecteurs : Normally Open (NO), Normally Closed (NC), et ground (C ou COM). Normally Open signifie que le switch est normalement non-pressé. Quand il sera pressé, un message sera envoyé au Roborio. Normally Closed signifie le contraire. Connectez un des cables au connecteur NO ou NC, et l’autre au ground.

Dans le Code

Pour programmer un limit switch, créez une instance de la classe DigitalInput, son constructeur attend comme argument le port DIO sur lequel le switch est branché :

#include <frc/DigitalInput.h>
frc::DigitalInput mon_switch(0);

La méthode bool Get() renvoie true ou false suivant la position du switch (appuyé/relâché) et ses branchements (NO/NC) :

bool switchPresse = mon_switch.Get();

Encodeurs

Description

Les encodeurs permettent de connaître la position précise d’un mécanisme. Ils se fixent sur des axes et comptent le nombre de tours que ceux-ci font. Ce sont des sortes de compteurs : quand l’axe tourne dans un sens la « position » augmente, quand il tourne dans le sens inverse la « position » diminue. Les encodeurs peuvent être optiques ou bien magnétiques.

_images/Encodeur.jpg

Voici la façon dont il se branche :

_images/Encodeur_wiring.jpg

Dans le Code

Pour programmer un encodeur, créez une instance de la classe Encoder, son constructeur attend comme argument les port DIO sur lesquels l’encodeur est branché :

#include <frc/Encoder.h>
frc::Encoder mon_encodeur(0, 1);

La méthode int Get() renvoie la distance angulaire mesurée par l’encodeur en ticks. Selon le modèle, un tour équivaut à 360 ticks, 22 ticks, … :

double distance = mon_encodeur.Get();

La méthode void Reset() remet le compteur à zéro :

mon_encodeur.Reset();

Les méthodes void     SetDistancePerPulse(double distancePerPulse) et double GetDistance() permettent de convertir automatiquement les tick en une autre unité :

// 1 tour équivaut à 360 ticks
mon_encodeur.SetDistancePerPulse(1.0/360);
double nombre_de_tours = mon_encodeur.GetDistance();

La méthode void GetRate() renvoie la vitesse actuelle convertie en distance selon le facteur de conversion (1 par défaut) :

double vitesse = mon_encodeur.GetRate();

Gyroscopes

Description

Les gyroscopes permettent de connaître la vitesse et le sens de rotation du robot. Ils permettent aussi de connaître l’angle du robot sur le terrain. Ils peuvent se brancher sur le port SPI ou les ports Analog In 0 et 1 du Roborio.

Dans le Code

Pour programmer un gyroscope, créez une instance de la classe ADXRS450_Gyro (SPI) ou AnalogGyro (Analog In) en fonction du gyroscope. Le constructeur d”AnalogGyro attend comme argument le port Analog In (0 ou 1) sur lequel le gyroscope est branché :

#include <frc/ADXRS450_Gyro.h>
frc::ADXRS450_Gyro mon_gyro();
#include <frc/AnalogGyro.h>
frc::AnalogGyro mon_gyro(0);

La méthode double GetAngle() renvoie l’angle du robot en degrés dans le sens des aiguilles d’une montre :

double angle = mon_gyro.GetAngle();

La méthode double GetRate() renvoie la vitesse de rotation du robot en degrés par secondes dans le sens des aiguilles d’une montre :

double vitesse_rotation = mon_gyro.GetRate();

La méthode void Calibrate() calibre le gyroscope en définissant son centre. La méthode void Reset() remet le gyroscope à zéro :

// Initialisation du gyro
mon_gyro.Calibrate();
mon_gyro.Reset();

Le contrôleur PID

Contrôler un mécanisme

Maintenant que nous savons contrôler les moteurs et lire les informations des capteurs, il faut les utiliser ensemble pour contrôler intelligemment et efficacement vos mécanismes. Voici quelques termes qui seront utiles pour la suite :

Setpoint

Le setpoint est l’objectif que le mécanisme doit atteindre. Pour un élévateur, le setpoint est la hauteur désirée. Pour un pivot, c’est un angle. On peut aussi imaginer un shooter dont le setpoint serait la vitesse de rotation adéquate pour lancer l’objet à la bonne distance.

Erreur

L’erreur est la différence entre le setpoint et l’état (position, vitesse) du mécanisme à un instant donné. Pour un élévateur, c’est la distance (positive ou négative) qu’il reste à parcourir pour atteindre le setpoint.

Output

L’output est la correction exercé sur le mécanisme pour le rapprocher du setpoint. Il peut être exprimé sur une échelle de -1 à 1 représentant la puissance donnée au moteur ou bien en volts.

PID, ça veut dire quoi ?

Le PID est une méthode pour contrôler les mécanismes efficacement. C’est la boucle de contrôle la plus utilisée dans l’industrie car elle peut s’appliquer à de nombreuses situations (thermostat, régulateur de position, de vitesse). C’est un acronyme signifiant : Proportionnel, Intégral, Dérivé, les 3 termes qui composent le PID.

L’équation d’un contrôleur PID est la somme de ces 3 termes :

output = P \times erreur + I \times \sum erreur + D \times \frac{\Delta erreur}{\Delta t}

Proportionnel

P \times erreur

https://upload.wikimedia.org/wikipedia/commons/a/a3/PID_varyingP.jpg

Le terme proportionnel est égal au produit d’un coefficient constant (kP ou P gain) et de l’erreur. Ce terme est ainsi élevé quand l’erreur est élevé (au début) et diminue lorsque le mécanisme se rapproche du setpoint. Plus le coefficient est élevé, plus la réponse du système sera rapide mais plus le mécanisme risquera d’osciller.

Intégral

I \times \sum erreur

https://upload.wikimedia.org/wikipedia/commons/c/c0/Change_with_Ki.png

En utilisant seulement le terme proportionnel, le mécanisme peut osciller (kP trop élevé) ou bien rester en dessous du setpoint (kP trop faible). Pour cela, on peut utiliser le terme intégral. Celui-ci est égal à la somme de toutes les erreurs depuis le début. Ce terme va ainsi augmenter de plus en plus si le mécanisme reste en dessous du setpoint trop longtemps.

Dérivé

D \times \frac{\Delta erreur}{\Delta t}

https://upload.wikimedia.org/wikipedia/commons/c/c7/Change_with_Kd.png

Le terme dérivé est égal à la variation de l’erreur sur la variation du temps. C’est la « pente » de l’erreur. Dans le code du robot, le delta temps sera toujours le même entre 2 itérations. On peut donc résumer le terme dérivé en la variation de l’erreur entre 2 itérations soit la différence entre l’erreur actuelle et l’erreur précédente.

D \times (erreur - erreurPrecedente)

Le coefficient kD est souvent négatif afin de réguler « l’accélération » du mécanisme. Si l’accélération est trop élevée, le terme dérivé sera alors d’autant plus important et ralentira le mécanisme.

Feed-Forward

Au PID on peut ajouter un 4ème terme, le terme F pour feed forward. Il peut être calculé en connaissant les caractéristiques du mécanisme :

Élévateur : Pour contrer la gravité exercée sur un élévateur, le voltage nécessaire peut être calculé en fonction de la masse de l’élévateur, du torque du moteur et du ratio de la gearbox.

Pivot : Pour contrer la gravité exercée sur le bras du pivot, le terme F peut être calculé en fonction de l’angle \theta du bras : k \cos \theta

Il existe d’autres cas comme les bases roulantes où le terme F peut être utile pour contrer les forces de frottement ou d’accélération.

Coder un PID

Le Code

Maintenant que nous avons appris la théorie du PID, utilisons le pour déplacer notre élévateur de façon autonome. Pour l’exemple, un dira que l’unique moteur de l’élévateur sera contrôlé par un VictorSP et que la position de l’élévateur nous sera donnée par un Encoder. A vous de jouer.

Correction

Normalement, votre programme sera séparé en 2 fichiers différents : Robot.h et Robot.cpp. Ici, le programme est dans un seul fichier pour plus de simplicité :

#include <frc/TimedRobot.h>
#include <frc/VictorSP.h>
#include <frc/Encoder.h>

class Robot : public frc::TimedRobot
{
public:
    void RobotInit() override
    {
        // Le sens de rotation du moteur
        m_Moteur.SetInverted(false);

        // Le sens dans lequel compte l'encodeur
        m_Encodeur.SetReverseDirection(false);

        // Conversion ticks -> mètres
        m_Encodeur.SetDistancePerPulse(m_DistanceParTick);

        m_Setpoint = 0.0;
        m_Erreur = 0.0;
        m_ErreurPrecedente = 0.0;
        m_SommeErreurs = 0.0;
        m_Derivee = 0.0;
    }

    void RobotPeriodic () override
    {
        position = m_Encodeur.GetDistance();

        m_Erreur = m_Setpoint - position;
        m_SommeErreurs += m_Erreur;
        m_Derivee = m_Erreur - m_ErreurPrecedente;

        double output = m_P * m_Erreur + m_I * m_SommeErreurs + m_D * m_Derivee + m_F;

        m_Moteur.Set(output);

        m_ErreurPrecedente = m_Erreur;
    }

    void TeleopPeriodic() override
    {
        // En fonction des actions du pilote :
        // Utiliser la fonction SetSetpoint pour déplacer l'élévateur
    }

    void SetSetpoint(double setpoint)
    {
        if(setpoint < m_MinSetpoint)
        {
            m_Setpoint = m_MinSetpoint;
        }
        else if(setpoint > m_MaxSetpoint)
        {
            m_Setpoint = m_MaxSetpoint:
        }
        else
        {
            m_Setpoint = setpoint;
        }
    }

private:
    // Moteurs et Capteurs
    frc::VictorSP m_Moteur(0);
    frc::Encoder m_Encodeur(0, 1);

    // Facteur de conversion des ticks vers une distance en mètre
    const double m_DistanceParTick = 0.05;

    // Variables du PID
    double m_Setpoint;
    double m_Erreur;
    double m_ErreurPrecedente;
    double m_SommeErreurs;
    double m_Derivee;

    // Valeurs déterminées scientifiquement
    const double m_P = 0.8;
    const double m_I = 0.01;
    const double m_D = - 0.2;
    const double m_F = 0.15;

    // L'élévateur peut aller de 0 m jusqu'à 1.5 m de hauteur
    const double m_MinSetpoint = 0.0;
    const double m_MaxSetpoint = 1.5;
};

Le Réglage

L’étape de tuning (de réglage) du PID consiste à trouver les bonnes valeurs pour les 3 coefficients P, I et D. Il faut commencer avec I et D à zéro et en réglant seulement P. C’est le coefficient P qui va determiner la « vitesse de réaction » du mécanisme. Ensuite, si il y a besoin, on peut ajuster les 2 autres coefficients afin d’améliorer le PID.

https://upload.wikimedia.org/wikipedia/commons/3/33/PID_Compensation_Animated.gif

Le réglage d’un PID se fait souvent de façon empirique (au talent) Il existe cependant différentes méthodes censées faciliter cette étape mais souvent régler le PID à l’instinct suffit.

Attention

Régler un PID peu s’avérer très dangereux si des précautions ne sont pas prises. Pensez, au tout début, à calculer l’ordre de grandeur de vos coefficients en fonction des valeurs de l’erreur.

Par exemple, pour un élévateur dont l’erreur sera au maximum égale à 1,5 (m), on veut commencer avec un output maximum inférieur à 0,1.

P \times erreur = output

P \times erreurMax < outputMax

P \times 1.5 < 0.1

P < 0.06666

On peut donc commencer avec un coefficient P aux alentours de 0.06666 sans prendre trop de risques. En revanche, si la distance parcourue par l’élévateur était exprimée en cm, un coefficient de 0.06666 serait beaucoup trop élevé et dangereux (0.06666 \times 150 = 10 !!!).

Utiliser la Pneumatique

Nous avons appris comment contrôler les moteurs et les capteurs du robot. Mais ces éléments ne sont pas les seuls présents sur le robot : il peut aussi y avoir de la pneumatique.

_images/Schema_pneumatique.jpg

Le circuit pneumatique est composé de tous les éléments utilisant l’air compressé au sein du robot : un compresseur, de réservoirs d’air (air tanks), de jauges de pression, de solénoïdes et de vérins.

Les vérins sont les seuls actionneurs utilisant la pneumatique, ils sont utilisés pour des mouvements rapides ne nécessitant que 2 positions (rentré/sortir).

_images/Solenoides.jpg

Les solénoïdes (ou solenoid valves) contrôlent le flux d’air. Ils permettent de diriger ou non l’air dans un vérin. Il existe 2 types de solénoïdes : les solénoïdes simple et doubles. Ils sont contrôlés par le PCM (Pneumatic Control Module) et se branchent donc sur les différents ports que celui-ci possède.

Solénoïdes simples

Les solénoïdes simples peuvent seulement appliquer ou bloquer la pression de leur unique sortie. Ils ne peuvent ainsi appliquer une pression qu’à une seule entrée d’un vérin. Ils sont utiles lorsqu’une force extérieur (gravité, …) permet de rentrer/sortir le vérin ou bien lorsque le vérin n’est utilisé qu’une seule fois (Sorti mais jamais rentré).

Dans le code

Pour programmer un solénoïde simple, il faut créer une instance de la classe Solenoid, son constructeur attend comme argument le port sur lequel le solenoid est branché :

#include <frc/Solenoid.h>
frc::Solenoid mon_solenoide(0);

La méthode void Set(bool on) permet d’ouvrir ou de fermer le solénoïde :

// Je sors le vérin
mon_solenoide.Set(true);

Note

Quand on déclare une instance d’un solénoïde (simple ou double), le compresseur est automatiquement mis en route.

Solénoïdes doubles

Les solénoïdes possèdent une entrée et deux sorties. La pression peut donc être appliquée à une sortie, à l’autre ou bien à aucune des deux. Quand un solénoïde double est branché à un vérin, il peut ainsi le sortir, le rentrer ou bien le laisser « libre ».

_images/Piston.gif

Dans le code

La classe DoubleSolenoid est similaire à la classe Solenoid. Son constructeur attend comme argument les 2 ports (Forward et Reverse) sur lequel le solenoid est branché :

#include <frc/DoubleSolenoid.h>
frc::DoubleSolenoid mon_solenoide(0, 1);

La méthode void Set(Value value) permet de contrôler l’état du solénoïde. Il attend en argument une des valeur de l’enum Value : Off, Forward or Reverse :

// Je sors le vérin
mon_solenoide.Set(frc::DoubleSolenoid::Value::kForward);

// Je rentre le vérin
mon_solenoide.Set(frc::DoubleSolenoid::Value::kReverse);

// Je laisse le vérin libre
mon_solenoide.Set(frc::DoubleSolenoid::Value::kOff);

Communiquer avec le robot

Une des notions que nous n’avons pas encore abordée et la communication entre le robot et l’ordinateur du pilote. Afficher des messages ou des valeurs numériques peut en effet être très utile tant pour faire des tests qu’en compétition.

Cout et Printf()

La première méthode à notre disposition est d’utiliser l’affichage console classique du C++ (cout et printf()). Le flux de sortie du robot est en effet redirigé sur le réseau. On peut le lire directement sur la Driver Station.

_images/DsOutput.jpg

On peut aussi lire le flux de sortie du robot avec le RioLog. On peut lancer le RioLog dans VS Code en entrant riolog dans la palette de commandes (Ctrl + Shift + P) puis en sélectionnant WpiLib: Start RioLog.

_images/RioLog.jpg

SmartDashboard

Afficher des informations avec cout pose un problème : si on affiche régulièrement plusieurs messages, il peut devenir très compliqué de suivre le défilement de ceux-ci. Pour cela, une alternative existe : c’est le SmartDashboard. Pour l’ouvrir dans VS Code : Ctrl + Shift + P puis Start Tool et sélectionner SmartDashboard.

Pour utiliser le SmartDashboard, il n’y a pas besoin de déclarer une instance de la classe SmartDashboard. On peut appeler les méthodes de celle-ci avec l’opérateur de résolution de portée ::. Pour afficher des données sur le SmartDashboard, il faut fournir 2 choses : une key qui identifie la donnée et sa valeur. Il existe plusieurs fonctions selon le type de donnée à afficher (texte, nombre ou booléen) :

#include <frc/smartdashboard/SmartDashboard.h>

frc::SmartDashboard::PutString("2020 Game","WaterGame");
frc::SmartDashboard::PutNumber("Best Team", 5553);
frc::SmartDashboard::PutBoolean("148+254=5553", false);

La fonction PutNumber attend comme argument un double. Ici, le int 5553 sera converti en un double.

Du côté du pilote, on peut lire les valeurs sur le SmartDashboard et on peut aussi les modifier. On peut alors récupérer ces valeurs avec les fonctions GetString, GetNumber et GetBoolean. Il faut alors donner en argument la key de la donnée et une valeur qui sera renvoyée si la donnée n’existe pas. Le pilote peut aussi modifier le widget qui affiche une donnée et ses propriétés.

NetworkTables

Pour assurer la communication entre le robot et l’ordinateur, le SmartDashboard utilise en fait un protocole de communication créé par WpiLib : les NetworkTables. Pour le voir, il suffit d’ouvrir l’OutlineViewer : Ctrl + Shift + P puis Start Tool puis OutlineViewer.

L’OutlineViewer permet de lire toutes les données transférées via les NetworkTables. On voit ainsi apparaître un « dossier » (une Table) nommé /SmartDashboard dans lequel on retrouve toutes les données (key + valeur) créées. Chaque Table est identifiable par une chaîne de caractères. Elle peut posséder plusieurs données (key + valeur) qui peuvent être, comme pour le SmartDashboard, des string, des double ou des bool.

Pour accéder à ces données, on peut passer par la classe NetworkTable. Premièrement, il faut créer et récupérer une l’instance de la classe NetworkTable grâce au nom de la Table :

#include <networktables/NetworkTable.h>
auto table = NetworkTable::GetTable("datatable");

Note

Le mot clé auto remplace la déclaration du type d’une variable ou d’un objet. Le type est automatiquement déduit. Par exemple, ici, la variable x sera automatiquement un int :

int i = 5553;
auto x = i;

Ensuite, on peut utiliser la NetworkTable (son pointeur en fait) comme avec le SmartDashboard :

table->PutNumber("PositionPivot", encodeur.GetDistance());
table->PutBoolean("Pince ouverte", isPinceOuverte);
table->PutString("Alliance", "Rouge");

Les Tables sont automatiquement synchronisées à intervalles réguliers. Mais pour plus de performances, il existe une fonction qui synchronise immédiatement tous les changements effectués sur les NetworkTables quand on l’appelle :

nt::NetworkTable::Flush();

ShuffleBoard

Le Shuffleboard est une version améliorée du SmartDashboard et il fonctionne de la même manière que ce dernier. Il possède une interface et des widgets plus plaisant et peut avoir plusieurs fenêtres (ou tabs).

Sa principale utilité vis-à-vis du SmartDashboard est que l’on peut configurer dans le code du robot la disposition des widgets et par example changer de fenêtre avec le Joystick. Pour découvrir toutes ses fonctionnalités : voici la documentation.

Streamer une video

Partager un flux vidéo peut s’avérer utile en match. En effet, voir sur son PC la vue d’une caméra montée sur le robot peut aider le pilote à se positionner correctement sur le terrain.

Quelle camera ?

Le RoboRio possède deux ports USB. C’est pourquoi il est possible d’utiliser des caméras USB avec celui-ci. Ces cameras sont pratiques car disponibles partout dans le commerce, bon marché, faciles à remplacer et à brancher.

Une des caméras USB les plus utilisées en FRC est la Microsoft LifeCam HD-3000 :

_images/Camera.jpg

Il existe aussi des caméras IP qui permettent directement de streamer un flux vidéo sur le réseau. Cependant, celles-ci sont de moins en moins utilisées.

Dans le code

Bien évidement, WPILib a créé une classe afin de streamer le flux vidéo d’une caméra USB. Il s’agit de la classe CameraServer.

Le constructeur de cette classe est privé, on ne peut donc pas créer d’instances de cette classe. Au lieu de cela, il est possible de récupérer l’unique instance de la classe grâce à la méthode static CameraServer* GetInstance ().

Note

Les variables et les fonctions membres static appartiennent à la classe mais pas aux objets instanciés à partir de la classe. Une fonction membre déclarée static a ainsi la particularité de pouvoir être appelée sans devoir instancier la classe.

Après avoir récupérer un pointeur sur l’instance de la classe, on peut utiliser ses méthodes :

La fonction cs::UsbCamera StartAutomaticCapture() crée un flux vidéo à partir de la caméra USB n°0. Elle renvoie aussi une instance de la classe UsbCamera créée :

#include <CameraServer.h>
CameraServer::GetInstance()->StartAutomaticCapture();

Il existe aussi d’autres méthodes qui permettent de récupérer les images du flux pour, par exemple, les analyser.

Les alternatives

Utiliser le RoboRio pour streamer un flux vidéo n’est pas toujours la meilleure solution. En effet, le processeur du RoboRio n’est pas adapté à la compression vidéo et le flux vidéo utilise ainsi une grande partie de la bande passante.

Pour pouvoir utiliser moins de bande passante, il peut alors être intéressant d’avoir recours à un coprocesseur comme un Raspberry Pi. Celui-ci pourra alors utiliser un codec vidéo plus avantageux comme le H.264.

Organiser son Programme

Diviser le programme en Classes

Pour l’instant, tout le code que nous écrivions était uniquement dans la classe principale Robot. Cependant, pour des projets plus importants, la quantité de code commence à être trop grande pour se située dans un unique fichier.

Pour organiser le programme du Robot, il est donc nécessaire de le structurer en plusieurs fichiers. Chacun gérant une fonctionnalité différente du code.

Une première méthode pour structurer le code peut être de créer une classe pour chacun des mécanismes du robot (ou subsystems) :

subsystems/
├── BaseRoulante/
├── Grimpeur/
├── Pince/
└── Pivot/

Chacune de ses classes contient ainsi les méthodes nécessaires au fonctionnement du subsytem. Par exemple, voici à quoi pourrait ressembler le fichier BaseRoulante.h :

class BaseRoulante
{
public:
  BaseRoulante();

  void Drive(double x, double y);
  void Stop();

  void ActiverVitesse1();
  void ActiverVitesse2();
  void ChangerVitesse();

  double GetDistanceDroite();
  double GetDistanceGauche();
  double GetAngle();
  void ResetCapteurs();

private:
  // Variables, objets et méthodes
  // Privés pour gérer en interne le subsystem

};

Cablage.h ou Constants.h

En séparant les subsystems en plusieurs classes/fichiers, on sépare aussi les objets qu’ils contiennent (contrôleurs moteur, capteurs, …). Il peut ainsi, par exemple, être compliqué de savoir si le port X du RoboRio est déjà utilisé. Sur quels ports sont branchés les encodeurs de la base ?

Pour simplifier cela, on crée un fichier nommé Cablage.h ou Constants.h qui contient des constantes utilisées dans les autres fichiers :

// PWM MOTORS
constexpr int PWM_ROUES_PINCE = 0;
constexpr int PWM_BASE_DROITE_1 = 1;
constexpr int PWM_BASE_DROITE_2 = 2;
constexpr int PWM_BASE_GAUCHE_1 = 3;
constexpr int PWM_BASE_GAUCHE_2 = 4;

// CAN MOTORS
constexpr int CAN_PIVOT = 1;

// DIO ENCODEURS
constexpr int DIO_ENCODEUR_PIVOT_A = 0;
constexpr int DIO_ENCODEUR_PIVOT_B = 1;

// PCM PNEUMATICS
constexpr int PCM_PINCE_A = 0;
constexpr int PCM_PINCE_B = 1;

Grâce à la présence de ca fichier, il est maintenant facile de savoir où chacun des contrôleur moteur doit être branché, quels sont les port PWM libres, ect …

Le Programme Principal

Maintenant que les classes permettant de contrôler les subsystems existent, il faut les intégrer dans notre classe principale Robot. Pour cela, on a juste à créer une instance de chacune des classes dans Robot. Pour la partie Téléopérée, le but du programme principal est d’utiliser des if qui, en fonction des entrée du joystick, appellent certaines fonctions.

#include <frc/TimedRobot.h>
#include <frc/Joystick.h>
#include "BaseRoulante.h"
#include "Pince.h"

class Robot : public frc::TimedRobot
{
public:
  void TeleopPeriodic() override
  {
    if(m_Joystick.GetRawButton(1))
    {
      m_Pince.Attraper();
    }
    else if(m_Joystick.GetRawButton(2))
    {
      m_Pince.Ejecter();
    }
    else
    {
      m_Pince.Stop();
    }

    m_BaseRoulante.Drive(m_Joystick.GetX(), m_Joystick.GetY());
  }

private:
  frc::Joystick m_Joystick(0);
  BaseRoulante m_BaseRoulante;
  Pince m_Pince;
};

Attention

Encore une fois, les méthodes appelées par le programme principal ne doivent pas durer dans le temps au risque de rester bloqué dans une des fonctions. Les boucles while, do while et for sont donc généralement à éviter partout dans le code.

Lien utiles et sources

Tutos, Cours

Autres librairies

  • Cross The Road Electronics : pour contrôler les Victor SPX, Talon SRX, … via le bus CAN

  • Rev : pour contrôler le Spark MAX, seul contrôleur moteur disponible pour le NEO

  • OpenCV : pour faire de la reconnaissance visuelle

Visual Processing