Nivel: Básico
¿Qué tengo que saber para este post?
- Diseño básico de páginas con HTML y CSS (muy poquito)
- Programación básica (Arduino) C++-17
- Entender el funcionamiento básico del modelo Cliente-Servidor
—————————————
En está oportunidad y estrenando el dominio nuevo, les traigo una experiencia sacada directamente del aula con mis estudiantes. Mientras le enseñaba un poco de diseño web con HTML y casi nada de CSS a mis estudiantes de electrónica, pensado para un WebSever embebido en un ESP8266
nos encontramos con la tediosa tarea de meter la página diseñada dentro de un string para que luego el servidor pueda devolverla al cliente. Pero por suerte para todo existe una solución y en caso de que no existiera se inventa.
Primeros pasos
Como sabrán, si es que siguen el blog, yo insisto en el uso de PlatformIO para el desarrollo de embebidos, sea un mini-sistema hobbista con Arduino o uno grande con herramientas profesionales, es una excelente herramienta de desarrollo. En este caso, utilizamos ESP8266 que son los que teníamos a mano en el colegio donde inicialmente hicimos esta práctica. Acá les dejo un link a la página de los compas de Naylamp con descripción completa y montones de referencias sobre este módulo de desarrollo en particular.
La idea es la de conectar un display LCD
con expander I²C
al NodeMCU y que éste sea accesible para escribir desde una página web servida por un WebServer en una conexión Wi-Fi modo AP (Access Point). Para ello, arrancamos un proyecto nuevo seleccionando la plataforma adecuada, que en este caso sería el Espressif ESP8266 ESP-12E
como se observa en la imagen a continuación y utilizamos el framework de Arduino para un desarrollo rápido. Y, una vez inicializado el proyecto, simplemente agregamos una nueva carpeta al mismo que llamamos simplemente html
.
En esta nueva carpeta diseñaremos nuestras páginas, utilizando el VSCodium o su alternativa con binarios no libres (VSCode) 😉 el desarrollo web es muy ágil. Basta con crear un archivo .html
y escribir html:5
para que ya nos cree una estructura básica con la cual ya podemos empezar a trabajar. Cambiando algunas cosas, y agregando algunas otras, como quería mantener simple el código y escribir lo menos posible hicimos un simple formulario con 2 inputs, uno para cada linea del LCD y limitamos el ingreso a 16 caracteres y también validamos recibir ASCII en la medida de lo posible utilizando un regex.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
input {
font-family: monospace;
margin: 5px;
padding: 8px;
border-radius: 8px;
width: 16ch;
}
</style>
<title>LCD WiFi</title>
</head>
<body>
<h1>Escribí algo en el Display</h1>
<form action="escribir" method="post">
<input type="text" name="l0" pattern="[a-zA-Z0-9.,;:=\-_#$@ ]{0,16}" maxlength="16" placeholder="Línea 1"><br>
<input type="text" name="l1" pattern="[a-zA-Z0-9.,;:=\-_#$@ ]{0,16}" maxlength="16" placeholder="Línea 2"><br>
<input type="submit" value="Escribir">
</form>
</body>
</html>
Antes de que un desarrollador web caiga acá y venga a poner el grito en el cielo porque el estilo está dentro del mismo HTML, le comento que esto es necesario, ya que la página en cuestión terminará siendo un string dentro del código en C++. Por lo tanto, no tenemos gestión de archivos ni nada parecido, al menos no lo tenemos sin complicar excesivamente el ejemplo.
Ya tengo la página, ¿ahora qué?
Entonces, una vez conformes con la página que habíamos desarrollado con mis estudiantes, nos vemos en una dificultad. Había que formatear este archivo para meterlo dentro del microcontrolador. Esta tarea es fácil de scriptear en bash, pero la mayoría de mis estudiantes ni siquiera saben con qué se come como utilizar la terminal, comandos, Linux y nada parecido. Por lo tanto, decidí armar algo que solucionara esta pequeña complicación. En mi nueva página van a poder encontrar esta herramienta ACÁ. La cual les generará algo como lo que se ve a continuación.
El script de PHP toma un archivo .html
desde la PC y genera una salida en texto plano con una variable const char*
con el mismo nombre del archivo subido. Opcionalmente se puede agregar la directiva de preprocesador PROGMEM
, pero realmente no es necesario en la mayoría de los casos. Todas las comillas dobles son reemplazadas por simples y otros detalles. Lo bueno de esto es que respeta el identado, es decir, no ofusca el código, porque no es el objetivo del script el combinarlo en una única linea inentendible e imposible de modificar si fuera necesario.
Yo opté por copiar y pegar esto directamente en un html.h
dentro de la carpeta include
del proyecto.
#ifndef __HTML_H__
#define __HTML_H__
const char * lcdpage PROGMEM =
"<!DOCTYPE html>"
"<html lang='es'>"
"<head>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width, initial-scale=1.0'>"
"<style>"
"input {"
"font-family: monospace;"
"margin: 5px;"
"padding: 8px;"
"border-radius: 8px;"
"width: 16ch;"
"}"
"</style>"
"<title>LCD WiFi</title>"
"</head>"
"<body>"
"<h1>Escribí algo en el Display</h1>"
"<form action='escribir' method='post'>"
"<input type='text' name='l0' pattern='[a-zA-Z0-9.,;:=\\-_#$@ ]{0,16}' maxlength='16' placeholder='Línea 1'><br>"
"<input type='text' name='l1' pattern='[a-zA-Z0-9.,;:=\\-_#$@ ]{0,16}' maxlength='16' placeholder='Línea 2'><br>"
"<input type='submit' value='Escribir'>"
"</form>"
"</body>"
"</html>";
#endif // __HTML_H__
El WebServer
En esto no voy a detenerme demasiado, ya que hay miles de ejemplos en internet y en las mismas bibliotecas oficiales. El código es casi auto-explicativo, pero voy a aclarar algunas cuestiones por las dudas.
Declarar peticiones válidas y respuestas al cliente
El servidor es un objeto de la clase ESP8266WebServer
, construido para atender peticiones a través del puerto 80
. Entonces, debemos configurar cuales serán las URI
s que vamos a servir. Esto se hace a través del método o función miembro on()
que recibe un string de la URI
y una función que será invocada al momento de la petición. Además hay otra función específica para cuando piden una URI no registrada onNotFound()
la cual recibe unicamente la función que atenderá estas peticiones.
server.on("/",escribir);
server.on("/escribir",escribir);
server.onNotFound([]{server.send(404,"text/plain","404 - Pagina no encontrada.");});
En este caso, solo tengo un solo html, y siempre voy a devolver la misma página, así que tanto sea la raíz «/» o el action
del formulario («/escribir») yo llamaré a la misma función, que llame escribir()
. En cuanto a la página no encontrada, decidí utilizar un lambda y escribir la función directamente sobre el argumento.
Para responder al clinte, como se observa necesitamos utilizar la función miembro send()
la cual envía el código de estado HTTP
, el formato de la respuesta y el cuerpo de la misma, estas dos últimas en formato string. Por ejemplo, para enviar la página que hicimos sería así:
server.send(200,"text/html",lcdpage);
Leer argumentos de los input de un formulario
Los argumentos son los valores que reciben los parámetros o campos del formulario. Estos se identifican con el atributo name
, por ejemplo en el html de nuestra página teníamos dos input
del tipo texto con los nombres l0
y l1
.
<input type="text" name="l0" ...>
<input type="text" name="l1" ...>
Para poder leer u obtener los valores que fueron completados dentro del formulario existe el método o función miembro de la clase ESP8266WebServer
llamada arg()
la cual devuelve siempre un objeto de la clase String
. Es por eso que podemos consultar si estos campos están vacíos que puede ocurrir por dos motivos. El primero que no exista esta variable, es decir no llegamos a la pagina a través del formulario, y la segunda, es que realmente estén vacíos. Por eso podemos chequear esto, y en el caso de que no estén ambos vacío escribirlos directamente en el LCD:
if(not (server.arg("l0").isEmpty()) or
not (server.arg("l1").isEmpty())) {
lcd.clear();
lcd.print(server.arg("l0")); // texto del input con name='l0'
lcd.setCursor(0,1);
lcd.print(server.arg("l1")); // texto del input con name='l1'
}
Atender a los clientes
Pero nada de lo anterior funcionaria si nosotros no le pedimos al servidor que atienda a las peticiones de los clientes. Eso se logra invocando continuamente a la función miembro handleClient()
. Y como es necesario llamarla todo el tiempo, entonces la pondremos en el loop()
infinito del Arduino.
void loop() {
server.handleClient();
}
Código completo y demo
Entonces, el código completo, con comentarios y la configuración del WiFi se ve de la siguiente manera. Y les dejo algunas imágenes de cómo se ve.
#include <Arduino.h>
#include <LiquidCrystal_I2C.h>
#include <ESP8266WebServer.h>
#include "html.h" // acá está declarada lcdpage
LiquidCrystal_I2C lcd{0x3F, 16, 2};
ESP8266WebServer server{80};
void escribir() {
// Si los campos no están ambos vacíos, se escribe su contenido
if(not (server.arg("l0").isEmpty()) or not (server.arg("l1").isEmpty())) {
lcd.clear(); // Se borra el display y se posiciona en (0,0)
lcd.print(server.arg("l0")); // texto del input con name='l0'
lcd.setCursor(0,1);
lcd.print(server.arg("l1")); // texto del input con name='l1'
}
server.send(200,"text/html",lcdpage); // se envia el html
}
void setup() {
// Inicialización del Display
lcd.init();
lcd.backlight();
// Inicialización del WiFi en modo AP
WiFi.mode(WIFI_AP);
while(!WiFi.softAP("El LCD Wi-Fi")) {
lcd.print('.');
delay(100);
}
// Se muestra la IP local del ESP8266
lcd.setCursor(0, 0);
lcd.print("IP:");
lcd.print(WiFi.softAPIP());
// Se configura el WebServer para atender peticiones en '/'
// y '/escribir' (misma respuesta)
server.on("/",escribir);
server.on("/escribir",escribir);
server.onNotFound([]{server.send(404,"text/plain","404 - Pagina no encontrada.");});
server.begin();
// Se muestra el mensaje de que el sevidor está funcionado
lcd.setCursor(0,1);
lcd.print(" Server Iniciado");
}
void loop() {
// Atendiendo a las peticiones
server.handleClient();
}
Podés clonarte o descargarte el proyecto completo desde mi gitlab.
¡Saludos y hasta la próxima!
Dejar una respuesta