Programmation objet d'un robot MindStrom en Python

Bien que le langage python permette une programmation linéaire, il est d'une bonne pratique de structurer le programme en classes d'objet. Car le langage Python est un puissant langage à objet. L'API (Application Programming Inteface) ev3dev2 est elle-même architecturée en classes d'objet.
Utiliser les classes python pour programmer un robot permet de réutiliser celle-ci pour y ajouter ou modifier des fonctionnalité tout en conservant la classe initiale.
Le présent article reprend le programme présenté sous forme linéaire dans l'article intitulé « Pilotage d'un robot à chenille avec la télécommande infra-rouge » pour le structuré dans une classe Python.

Programmation

Dans l'IDE VSCode, créer un nouveau fichier nommé remoteir2.py.

Codage

#!/usr/bin/env python3

#1#
from ev3dev2.motor import MoveTank, OUTPUT_B, OUTPUT_C
from ev3dev2.sensor.lego import InfraredSensor
from ev3dev2.sensor import INPUT_4

#2#
class IRControlledTank():
    #3#
    DEFAULT_MOTORS = MoveTank(OUTPUT_B, OUTPUT_C)
    DEFAULT_IRSENSOR = InfraredSensor(INPUT_4)
    DEFAULT_SPEED = 50

    #4#
    def __init__(self,
                motors=DEFAULT_MOTORS,
                irsensor=DEFAULT_IRSENSOR,
                speed=DEFAULT_SPEED):
        #5#
        self._motors = motors
        self._irsensor = irsensor
        self._speed = speed

    #6#
    def run(self):
        self.setup()
        while True:
            self.loop()

    #7#
    def setup(self):
        pass

    #8#
    def loop(self):
        speed_left, speed_right = 0, 0
        if self._irsensor.top_left(1):
            speed_left = self._speed
        if self._irsensor.bottom_left(1):
            speed_left = -self._speed
        if self._irsensor.top_right(1):
            speed_right = self._speed
        if self._irsensor.bottom_right(1):
            speed_right = -self._speed
        self._motors.on(speed_left, speed_right)

#9#
if __name__ == '__main__':
    #10#
    robot = IRControlledTank()
    robot.run()

#11#
/*
    robot = IRControlledTank(irsensor=InfraredSensor(INPUT_3))
    robot.run()
*/


Explications

  1. Comme dans la version linéaire du programme, un programme objet en python commence par les instructions d'importation des objets utilisés. Dans cet exemple, on utilise les classes MoveTank pour le groupe des moteurs qui contrôlent les chenilles, InfraredSensor pour le capteur infra-rouge et les constantes OUTPUT_B, OUTPUT_C et INPUT_4 relatives aux  ports sur lesquels ils sont connectés.
  2. En développement objet, on programme des classes. En Python, une classe se déclare par l'instruction class suivie d'un nom de classe (IRControlledTank dans l'exemple).
  3. DEFAULT_MOTORSDEFAULT_IRSENSOR et  DEFAULT_SPEED sont des variable de classe. C'est à dire qu'elle sont uniques pour toutes les instances de la classe dans laquelle elle sont déclarées. Par convention, le fait de les avoir déclarées en MAJUSCULE signifie qu'il s'agit de constantes dont la valeur ne doit pas être modifiée.
    Tous les robots instanciés à partir de cette classe sont construit sur le même modèle avec deux moteur pour actionner les chenilles connectés respectivement aux ports B et C et un capteur infra-rouge connecté sur le port 1. 
  4. La fonction __init__(self) d'une classe correspond à l'initialisation de l'instance au moment de sa construction. En robotique, c'est une bonne pratique que de passer en paramètre de construction, les éléments constitutifs du robot, les capteurs, les moteurs et les constantes de fonctionnement (comme ici la vitesse) et de les initialiser par des valeurs par défaut instanciée comme des constantes de la classe. Cela a plusieurs avantages :
    1. Lorsque la classe du robot est utilisée telle quelle, il n'est pas nécessaire de passer de paramètres lors de l'instanciation lorsque les constantes correspondent à l'architecture physique du robot (moteurs et capteurs connectés sur tel ou tel port).
    2. Lorsqu'on modifie physiquement l'un des port de connexion (en connectant le câble du capteur infra-rouge sur le port 3 au lieu de 4 par exemple), on ne modifie pas la programmation de la classe du robot. A l'initialisation, il suffit juste de passer en paramètre une nouvelle instance de la classe InfraredSensor initialisée sur le bon port. Une illustration de ceci est décrite au point 10.
    3. Si la classe du robot est utilisée comme classe de base pour un autre robot (dans le cas on on modifierait l'architecture physique du robot en y ajoutant de nouveau capteurs par exemple qui bousculerait les branchements initiaux), on n'a pas besoin de modifier la programmation de la classe de base du robot. Grâce à l'héritage de la programmation objet, il suffit d'invoquer, dans le constructeur de la nouvelle classe, le constructeur de la classe de base  en passant en paramètre les moteurs ou les capteurs connectés sur les bons ports. Un futur article décrira ce fonctionnement en créant une nouvelle classe de robot, basée sur celle-ci, et utilisant un groupe MoveSteering pour les moteurs plutôt qu'un groupe MoveTank.
  5. Les paramètres d'initialisation du robot sont affectés à des variables d'instance. Contrairement aux variables de classe, chaque instance d'une classe peut avoir des valeurs différentes. En python, les variables d'instance sont associées à self.  Il convient d'en déclarer une pour chaque paramètre d’initialisation passé.
    Le fait de les avoir préfixées par un souligné indique qu'elle ne sont pas visible de l'extérieur de l'objet. En développement objet, cela s'appelle l'encapsulation. Cela permet pour une classe d'objet, de publier à l'extérieur une interface extrêmement différente de sa structure interne. Le souligné correspond à la déclaration protected des autres langages objet (Java ou C#). Ce qui permet d'utiliser ces valeurs dans les classes dérivées. A remarquer qu'un double souligné en Python correspond à private. Dans ce cas les variables ne seraient pas non plus visibles dans les classes dérivées.
  6. La méthode run() constitue le corps fonctionnel du robot. Tous le fonctionnel aurait aussi pu être programmé dans le code de la fonction init(). Mais cela serait revenu à une programmation linéaire dans une classe d'objet sans profiter des avantages du développement objet. En séparant le corps fonctionnel de la construction de l'objet, cela permet de désolidariser les actions dépendante de  la construction de l'exécution de l'objet lui-même. C'est très important dans le cas d'objets composites (des objets qui contiennent d'autre objets) car, il est inutile, voire dangereux, de commencer l'exécution d'un objet avant que tous les autres faisant partie de son univers n'aient encore d'existence. Pour utiliser un objet relatif à un robot, il faut d'abord l'instancier en invoquant le constructeur de la classe, puis invoquer la méthode run() sur celui-ci.
    Le corps fonctionnel d'un robot contient deux parties : une série d’instructions exécutées une seule fois au début du fonctionnement du robot et une autre série d'instructions exécutées dans une boucle sans fin. Ces deux séries d'instructions sont rassemblées respectivement dans les fonctions setup() (point 7) et loop() (point 8). Ces deux méthodes peuvent être surchargée dans les classes dérivées. 
  7. La méthode setup() est exécutée une seule fois au début du fonctionnement du robot. Il ne faut pas confondre son rôle avec celui de la fonction __init__(). La fonction setup() rassemble les instructions d’initialisation du fonctionnement du robot. Celles-ci exigent que tous les objets Python qui sont liés à la classe  de l'objet sont déjà tous instanciés et correctement construits. Ces constructions respectives étant effectué par la fonction __init__() de la classe du robot qui invoque elle-même la fonction __init__()  de tous les objets Python soit implicitement à l'instanciation de ceux-ci, soit par un appel explicite.
  8. La méthode loop() est exécutée plusieurs fois dans une boucle sans fin. Elle consiste à lire des données sur les capteurs, puis, en fonction des valeurs collectées,  à déclencher des actions adéquates sur les organes actifs (moteurs, sons, affichages,  par exemple) du robot. Dans cet exemple, la méthode loop() reprend la série d'instructions déjà utilisée dans l'article intitulé « Pilotage d'un robot à chenille avec la télécommande infra-rouge » : l'état des boutons de la télécommande sont testés via le capteur infra-rouge et l'activation des moteurs est  déclenchée en conséquence pour effectuer les déplacements du robot attendu. 
  9. La condition if __name__ == '__main__': permet d'exécuter le code  de la classe de deux façons : 
    • Soit c'est le fichier lui-même qui est exécuté. Dans ce cas, la condition est vraie et les instructions qu'elle contrôle sont exécutées. Ici il s'agit de l'instanciation et de l'exécution du robot. 
    • Soit les classes contenues dans le fichier sont utilisées comme classes de base dans une autre fichier. Auquel cas, instancier et exécuter ce robot n'a pas de sens puisque c'est le robot décrit dans la classe dérivée qui sera instancié et exécuté. La condition étant fausse, le code qu'elle contrôle ne sera pas exécuté.
  10. Dans un premier temps, le robot est instancié en invoquant le constructeur de la classe IRControlledTank. L'instance créée est affectée à la variable robot sur laquelle on invoque le méthode run() pour lancer l'exécution du robot.
  11. Dans le cas où le câble reliant le capteur infra-rouge à la brique serait connecté sur un autre connecteur que le connecteur 4, le robot ne fonctionnerait plus. Pour que celui-ci fonctionne à nouveau, sans modifier la classe IRControlledTank, il suffit de passer en paramètre le capteur infra-rouge (instance de la classe InfraredSensor) correctement initialisé sur le port INPUT_3. Voir points 3 et 4. Ce code est mis en commentaire. Il doit être utilisé en alternative au point 10.
La programmation de ce robot n'est pas encore totalement satisfaisante. En effet, sinon à interrompre brutalement le programme soit en cliquant le petit carré rouge dans la barre de debug de VSCode, soit en appuyant le bouton retour sur la brique, celui-ci, une fois démarré, est condamné à fonctionner à perpétuité jusqu'à épuisement des piles.

Conclusion

Cet article fait partie d'une série constituant un tutoriel pour programmer un robot MindStorm EV3 en Python. L'exercice suivant permet d'illustrer le point 4.3 en créant sur la base de celle-ci une autre classe de robot utilisant les moteurs MoveSteering en lieu et place de MoveTank en minimisant le codage en utilisant le technique d'héritage de la programmation objet.

Commentaires

Posts les plus consultés de ce blog

Connecter ev3dev2 à Internet en WiFi

Connecter Visual Studio Code à un robot MindStorm EV3 avec ev3dev-browser

Installer les modules EV3DEV2 sur Python