5 maneiras de criar um servidor Python em um Raspberry Pi Parte 2

Oi Habr.

Hoje continuaremos estudando os recursos de rede do Raspberry Pi, ou melhor, sua implementação em Python. Na primeira parte, examinamos as funções básicas do servidor Web mais simples em execução no Raspberry Pi. Agora vamos além e consideramos várias maneiras de tornar nosso servidor interativo.



O artigo foi desenvolvido para iniciantes.

Antes de começar algumas notas.

Antes de tudo, duvidei de que valia a pena fazer uma sequência e esperava um maior fluxo de críticas e classificações baixas, mas como a pesquisa mostrou na primeira parte, 85% dos leitores acharam as informações fornecidas úteis. Entendo que alguns artigos profissionais para “manequins” são irritantes, mas todos começaram uma vez, então você tem que esperar.

Em segundo lugar, vou escrever sobre programação, não sobre administração. Portanto, os problemas de configuração do Raspbian, configurações, VPN, segurança e outras coisas não serão considerados aqui. Embora isso também seja importante, não se pode abraçar o imenso. Será apenas sobre Python, e como criar um servidor nele.

Aqueles que não estiverem interessados ​​podem clicar no botão voltar no navegador agora mesmo e não perder seu precioso tempo;)

E nós começaremos.

Deixe-me lembrá-lo que na parte anterior acabamos lançando um servidor Web simples no Raspberry Pi, mostrando uma página estática:

imagem

Agora iremos além e tornaremos nosso servidor interativo, adicionar controle de LED à página da web. Obviamente, em vez do LED, pode haver qualquer outro dispositivo que possa ser controlado pelo GPIO, mas com o LED é mais fácil conduzir um experimento.

Preparação


Não vou descrever como conectar o LED ao Raspberry Pi, qualquer um pode encontrá-lo no Google em 5 minutos. Vamos escrever várias funções para usar o GPIO de uma só vez, que depois inserimos em nosso servidor.

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 

Como você pode ver, cada função de chamada GPIO é "empacotada" em um bloco try-catch. Por que isso é feito? Isso permite depurar o servidor em qualquer PC, incluindo o Windows, o que é bastante conveniente. Agora podemos inserir essas funções no código do servidor da web.

Nossa tarefa é adicionar botões à página da web que nos permitem controlar o LED no navegador. Serão consideradas três formas de implementação.

Método 1: Errado


Este método não pode ser chamado de bonito, mas é curto e mais fácil de entender.

Crie uma linha com uma página 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>''' 

Há três pontos a serem observados aqui:

  • Usamos CSS para especificar o estilo dos botões. Você não pode fazer isso e se dar bem com apenas 4 linhas de código HTML, mas nossa página se pareceria com "saudações dos anos 90":

  • Para cada botão, criamos um link local como / led / on e / led / off
  • A mistura de recursos e código é um estilo de programação ruim e, idealmente, é melhor manter o HTML separado do código Python. Mas meu objetivo é mostrar um código minimamente funcional, no qual exista um mínimo de supérfluo, para que algumas coisas sejam omitidas por simplicidade. Além disso, é conveniente quando o código pode ser simplesmente copiado de um artigo, sem qualquer problema com arquivos adicionais.

Já examinamos o servidor na parte anterior, resta adicionar o processamento das linhas '/ led / on' e '/ led / off' a ele. Código inteiro atualizado:

 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() 

Começamos e, se tudo foi feito corretamente, podemos controlar o LED através do nosso servidor da web:



Você pode testar o servidor não apenas no Raspberry Pi, mas também no Windows ou OSX; no console, haverá mensagens LED ON, LED OFF quando você clicar no botão correspondente:



Agora descobrimos por que esse método é ruim e por que está "errado". Este exemplo é bastante funcional e, muitas vezes, copiado em diferentes tutoriais. Mas existem dois problemas - primeiro, é errado recarregar a página inteira quando queremos apenas acender o LED. Mas isso ainda é metade do problema. O segundo problema, e mais sério, é que, quando pressionamos o botão para ligar o LED, o endereço da página se torna http://192.168.1.106:8000/led/on . Os navegadores geralmente lembram a última página aberta e, da próxima vez que você abrir o navegador, o comando para ligar o LED funcionará novamente, mesmo que não desejássemos. Portanto, seguiremos para o próximo caminho, mais correto.

Método 2: Direito


Para fazer tudo certo, colocamos as funções de ativação e desativação do LED em solicitações separadas e as chamaremos de forma assíncrona usando Javascript. O código HTML da página agora terá a seguinte aparência:

 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>''' 

Como você pode ver, abandonamos o href e chamamos as funções ledOn e ledOff, que, por sua vez, chamam os métodos correspondentes do servidor de forma assíncrona (métodos assíncronos são necessários para que a página não seja bloqueada até que a resposta do servidor chegue).

Agora resta adicionar o processamento de solicitação de solicitação ao servidor:

  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)) 

Agora, como você pode ver, a página não recarrega mais quando você tenta acender o LED, cada método faz apenas o que deve fazer.

Método 3: Mais correto


Tudo parece estar funcionando já. Mas é claro que o código acima pode (e deve) ser aprimorado. O fato é que usamos solicitações GET para controlar o LED. Isso economiza espaço no código, mas metodologicamente isso não está totalmente correto - as solicitações GET são projetadas para ler dados do servidor, podem ser armazenadas em cache pelo navegador e, geralmente, não devem ser usadas para modificar dados. A maneira correta é usar o POST (para aqueles que estão interessados ​​em detalhes, mais detalhes aqui ).

Mudaremos as chamadas em HTML de chegar para postar, mas, ao mesmo tempo, como nosso código é assíncrono, exibiremos o status de aguardar a resposta do servidor e exibir os resultados do trabalho. Para uma rede local, isso não será perceptível, mas para uma conexão lenta é muito conveniente. Para torná-lo mais interessante, usaremos JSON para passar parâmetros.

A versão final é assim:

 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>''' 

Adicione suporte para solicitações GET e POST ao servidor:

 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)) 

Como você pode ver, agora usamos uma função led, na qual o parâmetro "on" é passado usando json, que aceita True ou False (quando chamado em HTML, uma string json do formato {"on": true} é transmitida, respectivamente). Também vale a pena prestar atenção ao try-catch - isso impede que o servidor caia, por exemplo, se alguém enviar uma string com json inválido ao servidor.

Se tudo foi feito corretamente, obtemos um servidor com feedback, que deve ser algo como isto:



O feedback, no nosso caso, a mensagem "OK", permite que você veja a confirmação do servidor de que o código foi realmente processado.

Este servidor ainda pode ser melhorado? Você pode, por exemplo, fazer sentido substituir o uso da função de impressão pelo uso de log, isso é mais correto e permite exibir os logs do servidor não apenas na tela, mas também se você deseja gravá-los em um arquivo com rotação automática. Quem desejar pode fazer isso por conta própria.

Conclusão


Se tudo foi feito corretamente, obteremos um mini-servidor que permite controlar o LED ou qualquer outro dispositivo através de um navegador a partir de qualquer dispositivo de rede.

Importante: Precauções de segurança

Mais uma vez, observo que não há proteção ou autenticação aqui; portanto, você não deve "postar" uma página na Internet se planeja gerenciar uma carga mais ou menos responsável. Embora os casos de ataques a esses servidores sejam desconhecidos para mim, ainda não vale a pena dar a quem quiser abrir remotamente a porta da garagem ou ligar o aquecedor de quilowatt. Se você deseja controlar remotamente através dessa página, vale a pena configurar uma VPN ou algo semelhante.

Concluindo, repito que o material foi desenvolvido para iniciantes e espero que isso tenha sido mais ou menos útil. É claro que nem todo mundo em Khabr está satisfeito com a disponibilidade de artigos "para manequins", portanto, se a próxima parte dependerá ou não das notas finais. Se houver, ele considerará as estruturas Flask e WSGI, bem como os métodos básicos de autenticação.

Todas as experiências bem sucedidas.

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


All Articles