Servidor Web de archivos en Esp32 con MicroPython




Presentación


En este proyecto desarrollo un Servidor Web de archivos con una placa Wemos D1 R32 que permite un vez iniciado subir archivos html e instalar una pagina web sencilla en la placa.

Una vez arrancada la placa nos muestra en la consola el IP asignado y al ingresar a traves del navegador con la direccion http://{ip}/servidor se accede a la siguiente pantalla desde donde se puede administrar el contenido del servidor



Materiales


Placa Wemos D1 R32


Montaje del proyecto en la placa Wemos D1 R32


Para utilizar la placa Wemos D1 R32 con MicroPython es necesario flashear primero la misma con el firmware correspondiente. En el caso de este proyecto se utilizo el provisto por MicroPython.org que puede descargarse de https://micropython.org/resources/firmware/ESP32_GENERIC-20231005-v1.21.0.bin

Si bien esta no es la ultima version es el que he utilizado pero a criterio de quien lo pruebe podra usar versiones mas actuales que se van publicando peridocamente en la pagina https://micropython.org/download/ESP32_GENERIC/.

Una vez instalado el firmware en la placa utilizando la utilidad que viene provista por el IDE Thonny, o por aquella que ustedes utilicen ademas del firmware quedara grabado en la placa un archivo boot.py que debe ser borrado, y se deben copiar a la placa los archivos transcriptos a continuacion en el capitulo Código respetando sus nombres.


Código


El código se ha escrito en dos archivos conec_wifi.py, main.py donde esta el programa, en los archivos servidor, admarchivo, grabarchivo que son las paginas de configuracion del servidor, en los archivos error, aviso_d, aviso_e, aviso_h, aviso_m que tienen los mensajes del servidor y finalmente en el archivo index.html que se abre al llamar la ip del servidor y que puede ser modificado de acuerdo a las necesidades del usuario.

El archivo conec_wifi.py contiene los datos necesarios para la conexión a la red wifi a la que se va a conectar los cuales deben ser agregados en el lugar indicado.

El archivo main.py es el nucleo del servidor, el nombre elegido para el primero es a efectos que el programa se inicie automaticamente. En la confeccion de este Codigo debo agradecer la colaboracion del usuario 𝙔𝙞𝙡𝙡𝙩𝙧𝙤𝙣𝙞𝙘𝙨 Valencia del canal de Telegram MicroPython que me facilito la función decodifica(cadena): que adapte agregando la linea cadena=cadena.replace("+"," ").

En relación al archivo main.py cabe destacar dos cuestiones que estan pendientes de resolver y que hasta ahora no he encontrado una solución satisfactoria:

La primera, es la linea if t_html > 17480: que tiene como funcion evitar que el servidor se caiga cuando el tamaño de la pagina html es demasiado grande, al respecto el numero 17480 ha sido colocado por tanteo y no se exactamente cual es el valor maximo que admite.(Se aceptan sugerencias)

La segunda es la instruccion dada a partir de la linea if linea.find('<optionselect>') == 10: que tiene por funcion llenar el select que muestra al operador los archivos html existentes en el servidor, por alguna razon que no he podido establecer veran que agrega al select dos veces el utimo archivo, lo cual no he podido evitar (Se aceptan sugerencias)

Los archivos servidor, admarchivo, grabarchivo son las pagintas html que contienen la presentacion de la administración del servidor, se colocaron sin extensión a efectos que no sean visibles y no puedan ser modificadas en linea.

Los archivos error, aviso_d, aviso_e, aviso_h, aviso_m contienen los mensajes del servidor para el operador y al igual que los anteriores tampoco tienen extensión a efectos que no sean visibles y no puedan ser modificadas en linea.

Finalmente el archivo index.html es el que se inicia por defecto al llamar al servidor por su ip y puede ser modificado conforme el uso que desee darle el usuario


conec_wifi.py




#Configuracion de conexion WIFI:

from time import sleep
import network
        
class conexion:
        
           red = "XXXXXXXXX" 
           clave = "xxxxxxxxx"
        
           def __init__(mi, red='', clave=''):
              network.WLAN(network.AP_IF).active(False)# Scan for available access points
              mi.wlan = network.WLAN(network.STA_IF)
              mi.wlan.active(True)
              if red == '':
                mi.red = conexion.red
                mi.clave = conexion.clave 
              else:
                mi.red = red
                mi.clave = clave
        
           def conectar(mi, red='', clave=''):
              if red != '':
                mi.red = red
                mi.clave = clave
        
              if not mi.wlan.isconnected(): 
                mi.wlan.connect(mi.red, mi.clave)
        
           def status(mi):
              if mi.wlan.isconnected():
                return mi.wlan.ifconfig()
              else:
                return ()
        
           def esperar(mi):
              cnt = 30
              while cnt > 0:
                 print("Esperando ..." )
                 # con(mi.red, mi.clave) # Connect to an red
                 if mi.wlan.isconnected():
                   print('Se conecto a: %s' % mi.red)
                   print('IP: %s\nSUBNET: %s\nGATEWAY: %s\nDNS: %s' % mi.wlan.ifconfig()[0:4])
                   cnt = 0
                 else:
                   sleep(5)
                   cnt -= 5
              return
        
           def scan(mi):
              return mi.wlan.scan()   
             


    

main.py




      from conec_wifi import conexion
      from machine import reset
      
      import network
      import socket
      import time
      
      import os
      import sys
      
      
      import gc
      gc.collect()
      
      
              
      #Pagina Web
      def pagina_web(recibo):
                  with open(recibo) as archivo:
                                  html = ""
                                  for linea in archivo:
                                      if linea.find('<optionselect>') == 10:    
                                          arch = os.listdir()
                                          for x in arch:
                                              if x[-4:] == "html":
                                                  linea = "<option value='" + x + "'>" + x + "</option>"
                                                  html = html + linea
                                      if linea.find('<paso>') == 8:
                                          linea = "<h3>" + paso + "</h3>"
                                          linea = linea + "<textarea id='texto' name='pagtext' cols='100' rows='30'>"
                                          with open(paso) as incrusta:
                                              for nlinea in incrusta:
                                                  linea = linea + nlinea
                                          linea = linea + "</textarea>" 
                                      
                                      
                                      
                                      
                                      n_html = len(html)
                                      l_html = len(linea)
                                      t_html = n_html + l_html
                                      if t_html > 17480:
                                          html = control_error(1)
                                          break
                                          
                                      else:    
                                          html = html + linea
                                      
                  return html
              
              
      
      def control_error(error):
          if error == 1:
              print("Pagina web demasiado grande")
              pag = """
                  <html>
                      <head>
                          <title>Exceso de tamano</title>
                      </head>
                      <body align="center">
                          <h1>Pagina se excedio del tamano maximo</h1>
                          <h3>Error</h3>
                          <p>La pagina solicitada se excedio del tamaño maximo que permite este servidor.</p>
                          <hr>
                          <address>Esp32 Servidor: 192.168.0.82</address>
                      </body>
                  </html>
                  """
              return pag
      
      
      
      #Solicita pagina
      def solicita_web(pedido):
                      global archivos
                      global paso
                      if pedido.find('GET /') == 2:
                          ini = 7
                      if pedido.find('POST /') == 2:
                          ini = 8
                                      
                      fin = pedido.find('HTTP')
                      pedido = pedido[ini:fin].strip()
                      
                      if pedido.find('?archivos=') == 8:
                          ini = 18
                          paso = pedido[ini:fin]
                          if paso[-4:] == "html":
                              pedido = "admarchivo"
                          else:
                              pedido = "aviso_h"
                          
                      if pedido == "eliminar":
                          os.remove(paso)
                          archivos = os.listdir()
                          pedido = "aviso_e"
                      
                      if pedido.find('guardar?pagtext=') == 0:
                          data = pedido[16:len(pedido)]
                          data = decodifica(data)
                          file = open(paso,"wb") 
                          file.write(data)
                          file.close()
                          archivos = os.listdir()
                          pedido = "aviso_m"
                          
                      if pedido.find('crear?nuevo=') == 0:
                          data = pedido[12:len(pedido)] + ".html"
                          if data in archivos:
                              pedido = "aviso_d"
                          else:
                              file = open(data,"w") 
                              file.close()
                              paso = data
                              pedido = "admarchivo"
                              
                      if (fin - ini) == 1:
                          pedido = "index.html"
                      
                      if pedido in archivos:
                          print()
                      else:
                          pedido = "error"
                      return pedido    
      #conversion URL
      def decodifica(cadena):
          codigos=['%25', '%09', '%0a', '%0d', '%21', '%22', '%23', '%24', '%26', '%27', '%28', '%29', '%2a', '%2b', '%2c', '%2d', '%2e', '%2f', '%3a', '%3b', '%3c', '%3d', '%3e', '%3f', '%40', '%5b', '%5c', '%5d', '%5e', '%5f', '%60', '%7b', '%7c', '%7d', '%7e', '%a1', '%a2', '%a3', '%a4', '%a5', '%a6', '%a7', '%a8', '%a9', '%aa', '%ab', '%ac', '%ad', '%ae', '%af', '%b0', '%b1', '%b2', '%b3', '%b4', '%b5', '%b6', '%b7', '%b8', '%b9', '%ba', '%bb', '%bc', '%bd', '%be', '%bf', '%c0', '%c1', '%c2', '%c3', '%c4', '%c5', '%c6', '%c7', '%c8', '%c9', '%ca', '%cb', '%cc', '%cd', '%ce', '%cf', '%d0', '%d1', '%d2', '%d3', '%d4', '%d5', '%d6', '%d7', '%d8', '%d9', '%da', '%db', '%dc', '%dd', '%de', '%df', '%e0', '%e1', '%e2', '%e3', '%e4', '%e5', '%e6', '%e7', '%e8', '%e9', '%ea', '%eb', '%ec', '%ed', '%ee', '%ef', '%f0', '%f1', '%f2', '%f3', '%f4', '%f5', '%f6', '%f7', '%f8', '%f9', '%fa', '%fb', '%fc', '%fd', '%fe', '%ff']
          for i in codigos:
              if i in cadena :
                  cadena=cadena.replace(i,chr(int(i.replace('%',''),16)))
              if i.upper() in cadena :
                  cadena=cadena.replace(i.upper(),chr(int(i.replace('%',''),16)))
                  cadena=cadena.replace("+"," ") 
          return cadena
      
      
      #Conexion WIFI
      
      conec = conexion()
      conec.conectar()
      conec.esperar()
      wc = 0
      while not conec.wlan.isconnected():
           print("Wi-Fi no está listo. Esperar...")
           time.sleep(2)
           wc += 1
           if wc >= 5:
             break
      if wc >= 5:
           print("WIFI no está listo. ¡No puede continuar!")
      else:
           print("WIFI Conectado")         
      
                  
                  
      # Configuracion Socket 
      try:
                  tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                  tcp_socket.bind(('', 80))
                  tcp_socket.listen(5)
                  time.sleep(1)
                  print('Configuracion de socket exitosa\n')
      except OSError as e:
                  print('Fallo configuracion de socket. Reeinicia...\n')
                  time.sleep(3)
                  reset()
      print('Listo...!\n********************************\n')
      
      #listado de archivos
      archivos = os.listdir()
      
      
      #Servidor        
      while True:
                  try:
                      
                      conn, addr = tcp_socket.accept()
                      conn.settimeout(3.0)
                      print('Nueva conexion de: %s' % str(addr[0]))
                      pedido = conn.recv(1024)
                      conn.settimeout(None)
                      pedido = str(pedido)
                      pagina = solicita_web(pedido)
                      conn.send('HTTP/1.1 200 OK\n')
                      conn.send('Content-Type: text/html\n')
                      conn.send('Connection: close\n\n')
                      conn.sendall(pagina_web(pagina))
                      conn.close()
                  except OSError as e:
                      conn.close()
                      time.sleep(0.1)
            
          
      
            
          
              
        
        
    

servidor




        <html>
        <head>
        </head>
        <body align="center">
            
            <h2>Lista de Archivos en servidor</h2>
            <form action="#">
          
              <select name="archivos" id="lang" size="10" width="200px">
              <optionselect>
              </select>
              <br><br>
              <input type="submit" id="boton01" value="Editar archivo" />
            </form>
            <h2>Creacion de Archivos en servidor</h2>
            <p>Solo se pueden crear archivos html, para ello debe colocarse el nombre del archivo sin expetension<p>
            <form action="crear">
              <input type="text" id="nuevo" name="nuevo"/>  
              <input type="submit" id="boton02" value="Crear Archivo" />  
            </form>  
        </body>
        </html>
        
    

admarchivo




    <html>
    <head>
    </head>
    <body align="center">
        <h2>Edicion de Archivo</h2>
        
        <form action="/guardar" method="get" style="display:inline">
        <paso>
        <br>
        
            <button type="submit">Guardar</button>
        </form>
        <form action="/eliminar" method="post" style="display:inline">
            <button type="submit">Eliminar</button>
        </form>
        <form action="/servidor" method="post" style="display:inline">
            <button type="submit">Cancelar</button>
        </form>
        
    </body>
    </html>    
          
        

grabarchivo




    <html>
    <head>
    </head>
    <body align="center">
        <h2>Edicion de Archivo</h2>
        
        <form action="/grabar" method="get" style="display:inline">
        <paso>
        <br>
            input
            <button type="submit">Grabar</button>
        </form>
        <form action="/servidor" method="post" style="display:inline">
            <button type="submit">Cancelar</button>
        </form>
        
    </body>
    </html>
              
            

error



    
    <html>
    <head>
        <title>404 Pagina Inexistente</title>
    </head>
    <body align="center">
        <h1>Pagina Inexistente</h1>
        <h3>Error 404</h3>
        <p>La URL solicitada no se encontro en este servidor.</p>
        <hr>
        <address>Esp32 Servidor: 192.168.0.82</address>
    </body>
    </html>          

            

aviso_d



    
    <html>
    <head>
        <title>Mensaje</title>
    </head>
    <body align="center">
        <h1>Mensaje</h1>
        <h3>El archivo existe, NOS SE PUEDEN CREAR DOS ARCHIVOS CON EL MISMO NOMBRE</h3>
        <form action="/servidor" method="post" style="display:inline">
            <button type="submit">Continuar</button>
        </form>
        
    </body>
    </html>


    

            

aviso_e



    
    <html>
    <head>
        <title>Mensaje</title>
    </head>
    <body align="center">
        <h1>Mensaje</h1>
        <h3>El archivo se elimino correctamente</h3>
        <form action="/servidor" method="post" style="display:inline">
            <button type="submit">Continuar</button>
        </form>
        
    </body>
    </html>

            

aviso_h



    
    <html>
    <head>
        <title>Mensaje</title>
    </head>
    <body align="center">
        <h1>Mensaje</h1>
        <h3>Solo se pueden editar archivos html</h3>
        <form action="/servidor" method="post" style="display:inline">
            <button type="submit">Continuar</button>
        </form>
        
    </body>
    </html>           

            

aviso_m



    
    <html>
    <head>
        <title>Mensaje</title>
    </head>
    <body align="center">
        <h1>Mensaje</h1>
        <h3>El archivo se guardo correctamente</h3>
        <form action="/servidor" method="post" style="display:inline">
            <button type="submit">Continuar</button>
        </form>
        
    </body>
    </html>
            

index.html




    <html>
    <head>
    </head>
    <body align="center">

Código


<br><br><br><br> <h1>Servidor de archivos en Esp32</h1> <p>Este es un servidor web montado en una placa Esp32 que permite trabajar con archivos html</p> <p>Una vez iniciado permite editar y modificar los archivos html cargados en la placa</p> <p>Tambien es posible crear y eliminar archivos html</p> </body> </html>

Funcionamiento


Al terminar de copiar los archivos en la placa Esp32 y reiniciarla con el boton de reset de la misma vemos el siguiente resultado en la ventana del Thonny

Hay podemos ver como esta señalado en rojo el IP de la placa, que es la dirección asignanda por nuestra red a la que deberemos acceder para utilizar el servidor, si ingresamos simplemente la IP en el caso de la imagen 192.168.0.82 se abrira la siguiente pagina que es la pagina index.html que copiamos a la placa:

En cambio si ingresamos la direccion http://{ip}/servidor, en este caso 192.168.0.82/servidor se abrirá el administrador del servidor:

En el siguiente video veremos como se puede editar, crear y eliminar archivos html del servidor (la primera vez solo aparecera el arhivo index.html, aparece dos veces por lo que mencione antes) pero luego les iran apareciendo todos los archivos html que vayan creando (el ultimo archivo siempre aparce repetido aunqur esta una sola vez)

Cerrar