5 façons de créer un serveur Python sur un Raspberry Pi 2e partie

Salut, Habr.

Aujourd'hui, nous allons continuer à étudier les capacités réseau du Raspberry Pi, ou plutôt leur implémentation en Python. Dans la première partie, nous avons examiné les fonctions de base du serveur Web le plus simple fonctionnant sur le Raspberry Pi. Nous allons maintenant aller plus loin et envisager plusieurs façons de rendre notre serveur interactif.



L'article est conçu pour les débutants.

Avant de commencer quelques notes.

Premièrement, je doutais moi-même de continuer et je m'attendais à un plus grand flux de critiques et à de faibles notes, mais comme l'enquête l'a montré dans la première partie, 85% des lecteurs ont trouvé les informations fournies utiles. Je comprends que certains articles professionnels pour les "nuls" sont ennuyeux, mais ils ont tous commencé une fois, vous devez donc attendre.

Deuxièmement, j'écrirai sur la programmation, pas sur l'administration. Les problèmes de configuration de Raspbian, des configurations, du VPN, de la sécurité et d'autres choses ne seront donc pas pris en compte ici. Bien que cela soit également important, on ne peut pas embrasser l'immense. Il ne s'agira que de Python et de la façon de créer un serveur dessus.

Ceux qui ne sont pas intéressés peuvent cliquer sur le bouton de retour dans le navigateur dès maintenant et ne pas perdre leur temps précieux;)

Et nous allons commencer.

Permettez-moi de vous rappeler que dans la partie précédente, nous avons fini par lancer un simple serveur Web sur le Raspberry Pi, affichant une page statique:

image

Nous allons maintenant aller plus loin et rendre notre serveur interactif, ajouter un contrôle LED à la page Web. Bien sûr, au lieu de la LED, il peut y avoir tout autre appareil qui peut être contrôlé par GPIO, mais avec la LED, il est le plus facile de mener une expérience.

La préparation


Je ne décrirai pas comment connecter la LED au Raspberry Pi, tout le monde peut le trouver dans Google en 5 minutes. Nous allons écrire plusieurs fonctions pour utiliser GPIO à la fois, que nous insérerons ensuite dans notre serveur.

try: import RPi.GPIO as GPIO except ModuleNotFoundError: pass led_pin = 21 def raspberrypi_init(): try: GPIO.setmode(GPIO.BCM) GPIO.setup(led_pin, GPIO.OUT) except: pass def rasperrypi_pinout(pin: int, value: bool): print("LED ON" if value else "LED OFF") try: GPIO.output(pin, value) except: pass def rasperrypi_cleanup(): try: GPIO.cleanup() except: pass 

Comme vous pouvez le voir, chaque fonction d'appel GPIO est «enveloppée» dans un bloc try-catch. Pourquoi est-ce fait? Cela vous permet de déboguer le serveur sur n'importe quel PC, y compris Windows, ce qui est assez pratique. Nous pouvons maintenant insérer ces fonctions dans le code du serveur Web.

Notre tâche consiste à ajouter des boutons à la page Web qui nous permettent de contrôler la LED à partir du navigateur. 3 voies de mise en œuvre seront envisagées.

Méthode 1: faux


Cette méthode ne peut pas être qualifiée de belle, mais elle est courte et la plus facile à comprendre.

Créez une ligne avec une page HTML.

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <body> <h2>Hello from the Raspberry Pi!</h2> <p><a href="/led/on"><button class="button button_led">Led ON</button></a></p> <p><a href="/led/off"><button class="button button_led">Led OFF</button></a></p> </body> </html>''' 

Il y a 3 points à noter ici:

  • Nous utilisons CSS pour spécifier le style des boutons. Vous ne pourriez pas faire cela et vous contenter de seulement 4 lignes de code HTML, mais notre page ressemblerait alors à des "salutations des années 90":

  • Pour chaque bouton, nous créons un lien local comme / led / on et / led / off
  • Mélanger les ressources et le code est un mauvais style de programmation, et idéalement, le HTML est mieux gardé séparé du code Python. Mais mon objectif est de montrer un code fonctionnant au minimum dans lequel il y a un minimum de superflu, donc certaines choses sont omises pour plus de simplicité. De plus, il est pratique lorsque le code peut être simplement copié à partir d'un article, sans aucun problème avec des fichiers supplémentaires.

Nous avons déjà examiné le serveur dans la partie précédente, il reste à lui ajouter le traitement des lignes '/ led / on' et '/ led / off'. Code entier mis à jour:

 from http.server import BaseHTTPRequestHandler, HTTPServer class ServerHandler(BaseHTTPRequestHandler): def do_GET(self): print("GET request, Path:", self.path) if self.path == "/" or self.path.endswith("/led/on") or self.path.endswith("/led/off"): if self.path.endswith("/led/on"): rasperrypi_pinout(led_pin, True) if self.path.endswith("/led/off"): rasperrypi_pinout(led_pin, False) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) else: self.send_error(404, "Page Not Found {}".format(self.path)) def server_thread(port): server_address = ('', port) httpd = HTTPServer(server_address, ServerHandler) try: httpd.serve_forever() except KeyboardInterrupt: pass httpd.server_close() if __name__ == '__main__': port = 8000 print("Starting server at port %d" % port) raspberrypi_init() server_thread(port) rasperrypi_cleanup() 

Nous le démarrons, et si tout a été fait correctement, nous pouvons contrôler la LED via notre serveur web:



Vous pouvez tester le serveur non seulement sur Raspberry Pi, mais aussi sur Windows ou OSX, dans la console il y aura des messages LED ON, LED OFF lorsque vous cliquez sur le bouton correspondant:



Nous découvrons maintenant pourquoi cette méthode est mauvaise et pourquoi elle est "fausse". Cet exemple fonctionne assez bien et est souvent copié dans différents didacticiels. Mais il y a deux problèmes - tout d'abord, il est faux de recharger toute la page lorsque nous voulons simplement allumer la LED. Mais c'est toujours la moitié du problème. Le deuxième problème, et plus grave, est que lorsque nous appuyons sur le bouton pour allumer la LED, l'adresse de la page devient http://192.168.1.106:8000/led/on . Les navigateurs se souviennent généralement de la dernière page ouverte, et la prochaine fois que vous ouvrirez le navigateur, la commande pour allumer la LED fonctionnera à nouveau, même si nous ne le voulions pas. Par conséquent, nous allons passer à la méthode suivante, plus correcte.

Méthode 2: à droite


Pour tout faire correctement, nous avons mis les fonctions marche / arrêt de la LED dans des demandes distinctes, et nous les appellerons de manière asynchrone en utilisant Javascript. Le code HTML de la page ressemblera maintenant à ceci:

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <script type="text/javascript" charset="utf-8"> function httpGetAsync(method, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function() { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) callback(xmlHttp.responseText); } xmlHttp.open("GET", window.location.href + method, true); xmlHttp.send(null); } function ledOn() { console.log("Led ON..."); httpGetAsync("led/on", function(){ console.log("Done"); }); } function ledOff() { console.log("Led OFF..."); httpGetAsync("led/off", function(){ console.log("Done"); }); } </script> <body> <h2>Hello from the Raspberry Pi!</h2> <p><button class="button button_led" onclick="ledOn();">Led ON</button></p> <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p> </body> </html>''' 

Comme vous pouvez le voir, nous avons abandonné href, et nous appelons les fonctions ledOn et ledOff, qui à leur tour appellent les méthodes serveur correspondantes de manière asynchrone (des méthodes asynchrones sont nécessaires pour que la page ne se bloque pas jusqu'à ce que la réponse du serveur arrive).

Reste maintenant à ajouter le traitement des requêtes get au serveur:

  def do_GET(self): print("GET request, path:", self.path) if self.path == "/": self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) elif self.path == "/led/on": self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() rasperrypi_pinout(led_pin, True) self.wfile.write(b"OK") elif self.path == "/led/off": self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() rasperrypi_pinout(led_pin, False) self.wfile.write(b"OK") else: self.send_error(404, "Page Not Found {}".format(self.path)) 

Maintenant, comme vous pouvez le voir, la page ne se recharge plus lorsque vous essayez d'allumer la LED, chaque méthode ne fait que ce qu'elle doit faire.

Méthode 3: plus correcte


Tout semble déjà fonctionner. Mais bien sûr, le code ci-dessus peut (et devrait) être amélioré. Le fait est que nous utilisons les requêtes GET pour contrôler la LED. Cela nous permet d'économiser de l'espace dans le code, mais méthodologiquement, ce n'est pas tout à fait correct - les demandes GET sont conçues pour lire les données du serveur, elles peuvent être mises en cache par le navigateur et ne doivent généralement pas être utilisées pour modifier les données. La bonne façon est d'utiliser POST (pour ceux qui sont intéressés par les détails, plus de détails ici ).

Nous allons modifier les appels en HTML de get à post, mais en même temps, puisque notre code est asynchrone, nous afficherons l'état d'attente de la réponse du serveur et les résultats du travail. Pour un réseau local, cela ne sera pas visible, mais pour une connexion lente, c'est très pratique. Pour le rendre plus intéressant, nous utiliserons JSON pour passer des paramètres.

La version finale ressemble à ceci:

 html = '''<html> <style>html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} .button_led {display: inline-block; background-color: #e7bd3b; border: none; border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;} </style> <script type="text/javascript" charset="utf-8"> function httpPostAsync(method, params, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function() { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) callback(xmlHttp.responseText); else callback(`Error ${xmlHttp.status}`) } xmlHttp.open("POST", window.location.href + method, true); xmlHttp.setRequestHeader("Content-Type", "application/json"); xmlHttp.send(params); } function ledOn() { document.getElementById("textstatus").textContent = "Making LED on..."; httpPostAsync("led", JSON.stringify({ "on": true }), function(resp) { document.getElementById("textstatus").textContent = `Led ON: ${resp}`; }); } function ledOff() { document.getElementById("textstatus").textContent = "Making LED off..."; httpPostAsync("led", JSON.stringify({ "on": false }), function(resp) { document.getElementById("textstatus").textContent = `Led OFF: ${resp}`; }); } </script> <body> <h2>Hello from the Raspberry Pi!</h2> <p><button class="button button_led" onclick="ledOn();">Led ON</button></p> <p><button class="button button_led" onclick="ledOff();">Led OFF</button></p> <span id="textstatus">Status: Ready</span> </body> </html>''' 

Ajoutez la prise en charge des requêtes GET et POST au serveur:

 import json class ServerHandler(BaseHTTPRequestHandler): def do_GET(self): print("GET request, path:", self.path) if self.path == "/": self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode('utf-8')) else: self.send_error(404, "Page Not Found {}".format(self.path)) def do_POST(self): content_length = int(self.headers['Content-Length']) body = self.rfile.read(content_length) try: print("POST request, path:", self.path, "body:", body.decode('utf-8')) if self.path == "/led": data_dict = json.loads(body.decode('utf-8')) if 'on' in data_dict: rasperrypi_pinout(led_pin, data_dict['on']) self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write(b"OK") else: self.send_response(400, 'Bad Request: Method does not exist') self.send_header('Content-Type', 'application/json') self.end_headers() except Exception as err: print("do_POST exception: %s" % str(err)) 

Comme vous pouvez le voir, nous utilisons maintenant une fonction led, dans laquelle le paramètre "on" est passé en utilisant json, qui accepte True ou False (lorsqu'il est appelé en HTML, une chaîne json de la forme {"on": true} est transmise, respectivement). Il vaut également la peine de faire attention à try-catch - cela empêche le serveur de tomber, par exemple, si quelqu'un envoie une chaîne avec json invalide au serveur.

Si tout a été fait correctement, nous obtenons un serveur avec des commentaires, qui devrait ressembler à ceci:



Le feedback, dans notre cas, le message «OK», vous permet de voir la confirmation du serveur que le code a bien été traité.

Ce serveur peut-il encore être amélioré? Vous pouvez, par exemple, avoir un sens pour remplacer l'utilisation de la fonction d'impression par l'utilisation de la journalisation, c'est plus correct, et vous permet d'afficher les journaux du serveur non seulement à l'écran, mais aussi si vous souhaitez les écrire dans un fichier avec rotation automatique. Ceux qui le souhaitent peuvent le faire par eux-mêmes.

Conclusion


Si tout a été fait correctement, nous aurons un mini-serveur qui vous permettra de contrôler la LED ou tout autre appareil via un navigateur depuis n'importe quel appareil réseau.

Important: Précautions de sécurité

Encore une fois, je note qu'il n'y a pas de protection ou d'authentification ici, vous ne devez donc pas «publier» une telle page sur Internet si vous prévoyez de gérer une charge plus ou moins responsable. Bien que les cas d'attaques contre de tels serveurs ne me soient pas connus, cela ne vaut toujours pas la peine de donner à quiconque veut pouvoir ouvrir à distance la porte du garage ou allumer le chauffe-eau. Si vous souhaitez contrôler à distance via une telle page, cela vaut la peine de configurer un VPN ou quelque chose de similaire.

En conclusion, je répète que le matériel est conçu pour les débutants, et j'espère que cela a été plus ou moins utile. Il est clair que tout le monde sur Khabr n'est pas satisfait de la disponibilité d'articles "pour les nuls", donc si la prochaine partie dépendra ou non des notes finales. Si tel est le cas, il considérera les frameworks Flask et WSGI, ainsi que les méthodes d'authentification de base.

Toutes les expériences réussies.

Source: https://habr.com/ru/post/fr472464/


All Articles