Embarquer un serveur Web dans un robot en Python

Dans un article, la modification d'une page Web dans un script Python ait été présentée. Mais celle-ci ne pouvait être consultée que si un serveur Web avait été démarré sur la brique EV3 dans une session SSH.
Dans cet article, le serveur Web sera embarqué sur le robot et démarré avec celui-ci. Et, évidemment, arrêté lorsque que le robot lui-même est arrêté.
Comme tous les scripts Python qui pilotent un robot MindStorm, le script présenté ici est basé sur les classes Robot et RobotTask du module robot. Ce module fournit un FrameWork commun à tous les programmes de robotique basée sur Lego MindStorm EV3 où les parties importantes du programme sont constituées de tâches parallèles managées. Le module robot et son utilisation ont été présentés dans une série d'articles de ce blog :

Le script Python

Le script Python relatif à cet article est basé sur l'usage du module robot. Il se résumera à la programmation de deux tâches dérivées de RobotTask :
  • La classe WebServerTask. Cette tâche est chargée de démarrer et de faire fonctionner un serveur Web en parallèle.
  • La classe ZoneContentTask. Cette tâche est chargée d'incrémenter une valeur et de mettre à jour la zone d'une page Web selon le même principe que celle présentée dans l'article intitulé « Modification du contenu d'une page Web en Python » en reprenant la page test.html.
Dans cet exemple, ces deux tâches sont embarquées dans une instance de la classe générique Robot. Mais elles peuvent l'être aussi facilement dans n'importe instance d'une classe dérivée de Robot. De même, la classe ZoneContentTask peut être remplacée par une quelconque classe dérivée de RobotTask collectant des données à partir de capteurs Lego MindStorm (ou autres d'ailleurs) pour mettre à jour dans une page Web, les zones chargées de l'affichage de ces données.

La classe WebServerTask

#1#
class WebServerTask(RobotTask):
    #2#
    DEFAULT_NAME = "RobotWeb"

    #3#
    def __init__(self, robot, webport=8000, name=DEFAULT_NAME, auto=True):
        super().__init__(robot, name=name, auto=auto)
        server = http.server.HTTPServer
        handler = http.server.SimpleHTTPRequestHandler
        self._httpd = server(("", webport), handler)

    #4#
    def setup(self):
        self._httpd.serve_forever()

    #5#
    def loop(self):
        pass

    #6#
    def stop(self):
        super().stop()
        self._httpd.server_close()

  1. La classe WebServerTask dérive la classe de base RobotTask.
  2. Un nom arbitraire est attribué à la tâche dans une variable de classe DEFAULT_NAME.
  3. La méthode __init__() effectue l'initialisation des instances de la classe WebServerTask.
    • Elle reçoit en paramètres :
      • robot, le robot propriétaire de la tâche.
      • webport, le port réseau utilisé par le server Web (8000 par défaut).
      • name, le nom de la tâche (DEFAULT_NAME par défaut).
      • auto, pour indiquer si la tâche est démarrée automatiquement au lancement du robot (True par défaut).
    • Elle effectue les initialisations suivantes :
      • L'initialisation générique de la tâche est assurée en invoquant la  méthode __init__() de la classe parente RobotTask.
      • L'instance de serveur web est affectée à la variable locale server
      • L'instance du handler chargé de répondre aux requêtes HTTP est affectée à la variable local handler.
      • Une instance du service httpd est crée et affectée à l'attribut _httpd de la classe en passant en paramètres le port et le handler.
  4. Dans la méthode setup(), le service httpd est démarré en invoquant la méthode serve_forever() sur l'attribut _httpd.
  5. Dans la méthode loop(), il ne se passe rien de particulier. Cette méthode doit néanmoins être surchargée dans la classe WebServerTask, car son implémentation générique dans RobotTask termine la tâche immédiatement. Or la méthode serve_forever() doit continuer à fonctionner. 
  6. La méthode stop() est surchargée dans la classe WebServerTask.
    • La méthode stop()  de la classe est invoquée pour assurer l'arrêt générique propre de la tâche.
    • En outre, la méthode server_close() est invoquée sur l'attribut _httpd pour assurer l'arrêt du service httpd et la libération des ressources qu'il a mobilisé.

La classe ZoneContentTask

#1#
class ZoneContentTask(RobotTask):
    #2#
    DEFAULT_NAME = "IncZoneContent"

    #3#
    def __init__(self, robot, delay=1, name=DEFAULT_NAME):
        super().__init__(robot, name=name)
        self.__delay = delay
        self.__zoneContent = 0
        self.__pageWeb = ET.parse("test.html")
        self.__zone = self.__pageWeb.find(".//*[@id='zone']")

    #4#
    def loop(self):
        self.__zoneContent += 1
        self.__zone.text = str(self.__zoneContent)
        self.__pageWeb.write("result.html")
        time.sleep(self.__delay)

  1. La classe ZoneContentTask dérive la classe de base RobotTask.
  2. Un nom arbitraire est attribué à la tâche dans une variable de classe DEFAULT_NAME.
  3. La méthode __init__() effectue l'initialisation des instances de la classe WebServerTask.
    • Elle reçoit en paramètres :
      • robot, le robot propriétaire de la tâche.
      • delay, le temps écoulé entre deux incrémentations de la zone (1 seconde par défaut).
      • name, le nom de la tâche (DEFAULT_NAME par défaut).
    • Elle effectue les initialisations suivantes :
      • L'initialisation générique de la tâche est assurée en invoquant la  méthode __init__() de la classe parente RobotTask.
      • Le paramètre delay est affecté à l'attribut __delay.
      • L'attribut __zoneContent est initialisé à 0.
      • La page web test.html est chargée en mémoire en invoquant la méthode parse(), puis affectée à l'attribut __pageWeb.
      • La zone est repérée par une requête XPath en invoquant la méthode find() sur la page Web. La requête signifie « Trouve-moi l'élément HTML dont l'attribut id est égal à 'zone' à partir de la racine de la page, quelque soit le niveau d'imbrication des balises ». L'élément HTML trouvé est affecté à l'attribut __zone.
  4. La méthode loop() est exécutée tant que le robot fonctionne. Elle effectue à chaque itération les opérations suivantes :
    • Incrémentation de l'attribut __zoneContent.
    • Mise à jour du contenu de l'élément HTML __zone avec la nouvelle valeur de l'attribut __zoneContent converti en chaîne de caractères.
    • Enregistrement de la page Web dans le fichier result.tml. C'est ce fichier qu'il faut consulter dans le navigateur.  
    • Mise en sommeil de la tâche pendant le temps exprimé en secondes contenu dans l'attribut __delay.

Le script complet

#!/usr/bin/env python3

#1#
import time
import http.server
from robot import RobotTask, Robot
import xml.etree.ElementTree as ET

#2#
class WebServerTask(RobotTask):
    DEFAULT_NAME = "RobotWeb"

    def __init__(self, robot, webport=8000, name=DEFAULT_NAME, auto=True):
        super().__init__(robot, name=name, auto=auto)
        server = http.server.HTTPServer
        handler = http.server.CGIHTTPRequestHandler
        self._httpd = server(("", webport), handler)

    def setup(self):
        self._httpd.serve_forever()

    def loop(self):
        pass

    def stop(self):
        super().stop()
        self._httpd.server_close()


#3#
class ZoneContentTask(RobotTask):
    DEFAULT_NAME = "IncZoneContent"

    def __init__(self, robot, delay=1, name=DEFAULT_NAME):
        super().__init__(robot, name=name)
        self.__delay = delay
        self.__zoneContent = 0
        self.__pageWeb = ET.parse("test.html")
        self.__zone = self.__pageWeb.find(".//*[@id='zone']")


    def loop(self):
        self.__zoneContent += 1
        self.__zone.text = str(self.__zoneContent)
        self.__pageWeb.write("result.html")
        time.sleep(1)
        print("Zone_content =", self.__zoneContent, file=sys.stderr)


#4#
if __name__ == '__main__':
    robot = Robot()
    WebServerTask(robot)
    ZoneContentTask(robot)
    robot.run()

  1. Importation des modules et des classes utilisés dans le script.
  2. Implémentation de la classe WebServerTask, telle que décrite ci-dessus.
  3. Implémentation de la classe ZoneContentTask , telle que décrite ci-dessus.
  4. Ce script et les deux classes qu'il contient peuvent être utilisés dans un autre script. A ce moment ces instructions ne doivent pas être exécutées. C'est pourquoi on vérifie dans un test que ce script est le script principal.
    •  La classe générique Robot est instanciée dans la variable robot.
    • Une instance de WebServerTask est instanciée en passant robot en paramètre. La tâche est automatiquement enregistrée dans le dictionnaire des tâches de robot.
    • Une instance de ZoneContentTask est instanciée en passant robot en paramètre. La tâche est automatiquement enregistrée dans le dictionnaire des tâches de robot.
    • Le robot est démarré en invoquant la méthode run() sur la variable robot. Toutes les tâches ayant été instanciées avec le paramètre par défaut auto=True, celles-ci sont automatiquement démarrées également.

Consultation de la page

Une fois le script ci-dessus lancé, un serveur Web est opérationnel et permet de consulter la pages Web qui se trouvent dans le projet. En parallèle, un tâche incrément un valeur et met a jour cette valeur dans la page Web result.html. Cette page peut donc être consultée à distance dans un navigateur.
Le serveur Web est démarré dans le répertoire du projet. Ce répertoire constitue alors la racine du serveur Web. L'url de consultation est donc 192.168.1.150:8000/result.html.


A remarquer que la page originale peut, elle-aussi, être consultée par l'url 192.168.1.150:8000/test.html.

Conclusion

Cette solution, très simple à mettre en oeuvre, permet de publier des données collectées par un robot Lego MindStorm.
Si dans cet exemple, il ne s'agit que d'une donnée numérique simple, la classe ZoneContentTask peut être remplacée ou adaptée pour publier des données collectées à partir des capteurs Lego. Il en est de même pour la page test.html.
En revanche, la classe WebServerTask peut être utilisée telle quelle sans modification.

Dans cet exemple, la classe WebServerTask ne permet pas de servir des scripts CGI rédigé en Python. Cet technique testée sur la brique EV3 n'est pas du tout performante. En effet, l'affichage d'une page aussi simple que celle de cet exemple prend plus de dix secondes alors qu'en passant par l'utilisation du module etree, comme c'est le cas ici,  celui-ci est quasi instantané. L'utilisation de script CGI en Python sera présentée dans un article ultérieur. Mais son usage opérationnel devra être réservé à des micro-contrôleurs plus puissants que la brique EV3, comme par exemple PiStorm qui basée sur un Raspberry Pi.  

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