Medir y registrar temperatura, humedad relativa y presión atmosférica con un ESP8266: el componente LUA

Tras instalar un intérprete de LUA en mi placa NodeMCU, como describo en Instalar un intérprete de Lua en el ESP8266, lo siguiente es encontrarle una utilidad.

Mi primer proyecto es un medidor de temperatura, humedad y presión atmosférica, registrando toda esa información en un servidor de mi propiedad. Existen plataformas IoT de uso gratuito, pero no me siento cómodo entregándoles información detallada sobre mis costumbres en casa; prefiero que mi trabajo y mis datos no contribuyan a enriquecer una empresa privada ajena y, claro, yo dispongo de servidores y conocimientos para hacerme cargo personalmente de esta tarea.

Para las pruebas empleo dos sensores diferentes. La idea futura es instalar uno dentro de casa (de hecho, en cada habitación) y otro en la calle. Conectar los dos sensores al mismo equipo para su evaluación me permite comparar su rendimiento y relación calidad/precio. Los sensores son:

  • DHT22. Sensor de temperatura y humedad barato y sensible. Conseguí mi unidad por 3.54 €.
  • BME280: Sensor de temperatura, humedad y presión atmosférica. Es más pequeño y, supuestamente, mucho más preciso que el DHT22. Compré el mío por 7.54 €.

Algunas fotos del invento:

esp8266-temp1.jpg
Abajo el módulo NodeMCU. El DHT22 es el componente blanco de arriba y el BME280 es la pequeña placa negra arriba a la izquierda. El cable negro desconectado es la masa del módulo BME280 que se me ha desprendido innumerables veces a lo largo de los meses hasta que en una ocasión hizo un cortocircuito con la alimentación y destruyó el NodeMCU.


esp8266-temp2.jpg
Detalles de las resistencias PullUp y resistencias de protección del bus I2C. Mejor prevenir. Daré detalles en un futuro artículo.


esp8266-temp3.jpg

Primer plano de un módulo NodeMCU. Le falta el regulador de tensión (en medio). No preguntes...

El ESP8266 es la antena y la placa metálica a la izquierda.


Ambos sensores tienen buen soporte en la versión de Lua de NodeMCU.

El código Lua que se graba en el NodeMCU es trivial:

 wifi.eventmon.register(wifi.eventmon.STA_CONNECTED, function(T) print('CONECTADO') end)
 wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, function(T) print('DESCONECTADO') end)

 bme280.init(6, 7, 5, 5, 5, 0, 7, 4)

 function xx()
     a,b,c = dht.read(1)
     http.get('https://iot2016.jcea.es/post_db1?status='..a..'&temp='..b..'&hr='..c, 'Authorization: Basic XXXXXXXX\r\n', function(code, data) print(code, a, b, c) end)
 end
 tmr.alarm(1, 60*1000, tmr.ALARM_AUTO, xx)

 function xxx()
     hr,temp = bme280.humi()
     baro, temp = bme280.baro()
     http.get('https://iot2016.jcea.es/post_db2?temp='..temp..'&hr='..hr..'&baro='..baro, 'Authorization: Basic XXXXXXXX\r\n', function(code, data) print(code, baro, temp, hr) end)
 end

 function xxxx()
     bme280.startreadout(3000, xxx)
 end
 tmr.alarm(2, 60*1000, tmr.ALARM_AUTO, xxxx)

Las líneas 1 y 2 hacen que se imprima en el puerto serie cuando se conecta y desconecta la WIFI. Una característica interesante de este firmware es que una vez que se configura la WIFI (en este caso, el nombre de la red y su clave de acceso), el ESP8266 lo recuerda incluso ante reseteos o pérdidas de alimentación, reconectándose automáticamente cuando es necesario. Como he dicho, las líneas 1 y 2 nos indicarán cómo estamos si enchufamos algo en el puerto serie del módulo ESP8266.

La línea 4 configura el sensor BME280. La intercomunicación con el BME280 se realiza a través de I2C y aquí indicamos que el pin de datos es el seis y el pin de reloj I2C es el siete [1]. Indicamos que queremos un oversampling de temperatura, humedad y presión de x16. Esto es más preciso, pero la lectura es más lenta y el consumo de energía es mayor. Ponemos el sensor a dormir y un filtro IIR de máxima calidad.

En las líneas 6-9 definimos la rutina que lee los datos del sensor DHT22 y los envía a mi plataforma IoT. El pin de datos del DHT22 es el uno [1]. La línea 8 es interesante: Se realiza una conexión HTTPS [2] pasando los valores de temperatura y humedad en la propia URL. Esto me permitiría incluso hacer búsquedas en los logs del servidor web. El acceso es autenticado, añadiendo una cabecera Authorization manualmente [3]. Tras la conexión se imprime el código devuelto por el servidor web, así como el código devuelto por el DHT22, la temperatura y la humedad relativa.

En la línea 10 vinculamos la rutina anterior a una alarma periódica que se dispara una vez por minuto. Esa alarma lanzará una medida y la enviará al servidor independientemente de qué más cosas esté haciendo el NodeMCU.

El funcionamiento del BME280 es diferente. Aparte de usar el bus I2C (este chip también soporta SPI, pero no nos interesa en este caso), las lecturas son bastante lentas debido al oversampling y demás. La lectura no es inmediata, sino que solicitamos una lectura e indicamos una rutina callback que Lua debe llamar cuando los datos estén listos. Eso es lo que se hace en la línea 21, programar una alarma cada minuto que llama a una rutina (líneas 18-20) que solicita una lectura y, tras los milisegundos que le indiquemos [4], se invoca la rutina en las líneas 12-16 que lee los datos (obsérvese que recibimos la temperatura por varios lados) y los envía a mi servidor IoT. Como antes, la invocación HTTPS [2] está autenticada "manualmente" [3].

En resumen, obsérvese que cada sensor se ejecuta en un temporizador separado y la ejecución es totalmente independiente.

Warning

La documentación del módulo HTTP indica claramente:

It is not possible to execute concurrent HTTP requests using this module. Starting a new request before the previous has completed will result in undefined behavior.

Lo cierto es que en meses de funcionamiento no he tenido problemas por esta causa. Esto es por diseño porque entre una conexión y otra transcurren tres segundos y doy por hecho que la primera de ellas ya terminó cuando inicio la segunda.

Dado que este código es provisional y no me ha dado problemas hasta ahora, no me preocupa.

Como hemos usado las librerías incluídas en el Lua de NodeMCU la cosa ha resultado bastante sencilla, ¿verdad?

Solo queda explicar cómo hacer para que el Lua de NodeMCU ejecute nuestro código cuando encendemos el ESP8266, sin tener que teclearlo cada vez. Lo que yo hago es escribir el código Lua en mi servidor web y, cuando quiero actualizar el código en la placa NodeMCU, solo debo ejecutar en ella -a través del puerto serie- lo siguiente:

> http.get("URL", nil, function(code, data) print(code); file.open("init.lua", "w"); file.write(data); file.close(); end)

Este código se conectará a la URL que le indiquemos, recibirá el fichero que se encuentra ahí y lo guardará en el fichero init.lua de la memoria Flash del ESP8266. Ese fichero se ejecuta automáticamente al reiniciar el dispositivo.

[1] (1, 2) La asignación de pines es arbitraria. Podemos elegir los que mejor nos vengan. El DHT22 tiene un protocolo de comunicación muy simple. La comunicación con el BME280 se realiza mediante I2C, que se puede emular por software para poco volumen de datos.
[2] (1, 2)

Si bien la conexión HTTPS que establece la versión actual del firmware del ESP8266 es bastante segura (TLS 1.1, AES-128, SHA-1), el software no verifica el certificado del servidor. Es decir, la conexión es segura ante ataques pasivos, pero no ante ataques activos. En concreto, no es segura ante ataques Man in the middle.

Estudiando la documentación de las versiones recientes del Lua de NodeMCU, veo que ahora permiten verificar el certificado del servidor contra una CA determinada (en este caso, mi propia CA). La función en concreto es net.cert.verify(). Como en el caso de la configuración WIFI, es posible indicarle una CA una vez y esta se almacena en la memoria Flash hasta que grabemos un nuevo firmware o cambiemos la clave almacenada.

Es algo que aún no he hecho porque a) cuando empecé este proyecto esa funcionalidad no existía y b) se supone que este código en Lua es provisional y será reemplazado por Python en el futuro.

Tanto por hacer y tan poco tiempo...

[3] (1, 2)

La librería HTTP del Lua de NodeMCU no soporta autenticación de forma nativa, pero permite añadir cabeceras arbitrarias a una petición.

En una cabecera Authorization, el usuario y la clave se codifican juntos en BASE64. La rutina en Python sería:

import base64

usuario = b'USUARIO'
clave = b'CLAVE'

print(base64.b64encode(usuario + b':' + clave).strip(b'='))
[4]

El tiempo de espera entre la orden de tomar una medida en el BME280 y leer el resultado no es arbitrario. Según los parámetros que usemos en las medidas, habrá que poner más o menos tiempo para darle tiempo al chip a completar las operaciones. Los detalles se especifican en BME280 Environmental sensor - 11. Appendix B: Measurement time and current calculation.

En nuestro caso configuro el sensor para la máxima precisión y limpieza, a costa de la velocidad y el consumo de energía:

Con estos datos, el tiempo entre la orden de medir y el poder leer el resultado es:

Lectura básica: 1.25 + 2.5*16 + 2.3*16 +0.575 + 2.3*16 + 0.575 = 112.8 ms

Número de lecturas por segundo: 1000 / (112.8 + 20) = 7.53 Hz.

Con el filtro IIR de 16 coeficientes necesito 22 muestras (tabla 6). Es decir, el tiempo mínimo de espera es de 22*1000/7.53 = 2922 ms.

Por eso estoy usando un tiempo de espera de 3000 milisegundos.

Este sensor es muy potente y lo estoy exprimiendo, pero para las necesidades concretas de medidor atmosférico no necesito tanta precisión. Puedo rebajar considerablemente su tiempo de cálculo y consumo de energía sin comprometer la calidad de las medidas para el uso que le voy a dar. Es algo a explorar en el futuro.