Haciendo del Desarrollo y la Arquitectura Web, ciencia y pasión.

Internet de las cosas, MQTT y ESP32

Es este artículo vamos a investigar sobre el tan archifamoso Internet-de-las-cosas o IOT, del inglés Internet Of Things. Vamos a tocar varias cosas, revisaremos la arquitectura de un tipico escenario de comunicación entre dispositivos, veremos el protocolo MQTT y sus características, instalaremos un servidor, y luego nos conectaremos a este. También haremos unas publicaciones y unas pertinenetes suscripciones y hasta haremos unas pruebas con un ESP32, un primo de Arduino. Desde este ESP32 lanzaremos mensajes desde un pequeño circuito conectado por wifi, super interesante.

El protocolo MQTT por sus siglas Message Queuing Telemetry Transport fue diseñado en sus origenes para la monitorización de dispositivos de telemetría en la industria de petroleo y gas. Esta infraestructura requería de funcionamiento con unos requisitos muy concretos, donde las redes no son confiables y el consumo energético debía ser mínimo. En un tiempo donde las comunicaciones hasta las estaciones petrolíferas se hacían por satélite y se facturaba por volumen de datos, el consumo de ancho de banda era un factor crucial a tener en cuenta. MQTT se ejecuta sobre TCP/IP y se basa en un patron de PUBLICACION/SUSCRIPCION.

montaje

En una topología MQTT típica tenemos dos tipos de entidades, los clientes y los Brokers. Los clientes pueden realizar las operaciones de publicación y suscripción, y el Broker notifica a todos los elementos que se han suscrito previamente a un topic. Un topic es como un "canal" por donde dos o más clientes intercambian mensajes. Un cliente puede ser simultáneamente suscriptor o publicador, y solo produciran trafico en la red cuando se hayan realizado operaciones de publicación o suscripción. No existe periodicidad o mantenimiento de conexiones para ahorrar ancho de banda. Aunque si la red es pobre se puede ejecutar las comunicaciones con acuse de recibo, lo que lógicamente va a encarecer el proceso. A pesar de que el protocolo esta pensado para minimizar las transmisiones, la carga util de un mensaje está limitada a un tope de 256MB. Las comunicaciones pueden establecerse con tres niveles de calidad de servicio o QoS, del ingles Quality of Service. Se trata de un compromiso entre volumen de información y calidad de servicio.

QoS 0: Requiere el mínimo consumo de datos. La información se envía a los suscriptores sin confirmación, con lo cual los mensajes no se retienen en los brokers para su posterior envío a otros suscriptores.

QoS 1: El Broker envía los mensajes a los suscriptores y espera de ellos una confirmación. Si el mensaje se perdió o la confirmacion no llegó este se vuelve a enviar, con lo que los clientes pueden recibir más de una vez un mensaje.

QoS 2: El Broker y el cliente usan un protocolo de comunicación en cuatro pasos, que garantiza que la entrega de mensajes se hace una única vez.

El tema de los topics, esta resuelto de un modo jerarquico en el que se pueden anidar o apilar las cadenas para suscribirse a ellas. Es decir, podriamos tener un topics como estos:


compañia/edificio1/planta1/temperatura 
compañia/edificio2/planta3/temperatura

De esta manera ambos topics estarían consultando temperatura en la planta 1º y 3ª del edificio 1 y 2 de la empresa. Esto resulta algo tedioso si lo que necesitamos es suscribirnos a todas los sensores de las plantas de los cuatro edificios que tiene la empresa.

Aquí es donde entran en juego los comodines o wildcards. 

Wildcard de nivel único: el carácter + sustituye a un único nivel permitiendo hacer lo siguiente:


compañia/edificio1/+/temperatura
compañia/edificio2/+/temperatura
compañia/edificio3/+/temperatura
compañia/edificio4/+/temperatura

Así de esta manera podríamos suscribirnos a todas las plantas de cada edificio.

Wildcard de nivel multiple: el carácter # sustituye varios niveles y únicamente puede situarse al final de la cadena:


compañia/edificio1/# -> estaría suscrito a todos los sensores que dispone el edificio 1
compañia/# -> Estaria suscrito a todos los sensores que tiene la compañía.

Entendido pero ¿cuando empezamos a practicar? Vamos allá.

Existen infinidad de servidores de MQTT disponibles en la red y algunos de ellos gratuitos, pero nosotros instalaremos el nuestro propio. Usaremos para estas pruebas a Mosquitto que es un servidor opensource muy facil de usar. Adicionalmente instalaremos los clientes que provee Mosquitto:

 


sudo apt-get update
sudo apt-get install mosquitto
sudo apt-get install mosquitto-clients

Perfecto, solo nos resta una configuración mínima. Nos vamos a la carpeta “etc/mosquito” y editamos el mosquitto.conf y añadimos la siguiente línea:


allow_anonymous true 

Con esto permitiremos, como indica la línea, conexiones no autenticadas. Ni que decir que es para hacer unas pruebas, esto llevarlo a produccion sería una mala idea. MQTT permite varios modos de autenticación con certificados o con usuario y contraseña. Muy bien, pues vamos a hacer nuestra primera suscripción, abrimos una consola y ponemos:


mosquitto_sub -h 192.168.1.102 -t empresa/edificio1/planta1/temperatura -v 

En otra consola escribimos una publicación contra ese topic:


mosquitto_pub -h 192.168.1.102 -t empresa/edificio1/planta1/temperatura -m "{temp:’21grados’}" 

Aqui he puesto un mensaje en formato json, no es imprescindible, podria haber puesto una cadena de texto simple. En todo caso vemos que ha aparecido una linea nueva en la primera consola:


 danie@localserver:/etc/mosquitto$ mosquitto_sub -h 192.168.1.102 -t empresa/edificio1/planta1/temperatura -v 
empresa/edificio1/planta1/temperatura {temp:’21grados’}

Pues ya hemos hecho nuestra primera publicación!!

Y por que hemos usado un json ? Por que lo realmente potente es poder tener nuestro propio código para poder integrarlo con nuestra plataforma. El hecho de poder desacoplar las partes emisoras y receptoras, tiene multiples ventajas. Al no conocerse nos permite sustituir cualquiera de las entidades sin afectar otras piezas, reemplazar tecnologías o localizaciones, dejarlo en local o llevarlo a la nube. Nos permite distribuir las competencias teniendo un emisor y multiples receptores de una manera asíncrona de tal manera que los consumidores pueden consumir a su ritmo. Por haber distribuido las responsabilidades tenemos un bajo acoplamiento, que siempre es deseable y nos permite hacer un escalado horizontal. Por último, tenemos un escenario resiliente, la caida de una de las piezas no afecta al resto. 

Muy bien, como seguimos? Si nos vamos a mqtt.org podemos encontrar en la sección de software diversas implementaciones del protocolo y los clientes en varios lenguajes, vamos a trastear como de costumbre con PHP y Python. Vamos a hacer una primera prueba de conectarnos con dos clientes haciendo de productor y consumidor, en PHP y Python respectivamente. En la parte productora tenemos que preparar nuestro entorno para PHP. Usaremos composer con el comando:


 composer require bluerhinos/phpmqtt=@dev

Y escribimos nuestro código, mqtt_pub_single.php:


<?php

require_once('./vendor/autoload.php');

$server = 'localhost'; // change if necessary
$port = 1883; // change if necessary
$username = ''; // set your username
$password = ''; // set your password
$client_id = 'phpMQTT-publisher'; // make sure this is unique for connecting to sever - you could use uniqid()

$mqtt = new Bluerhinos\phpMQTT($server, $port, $client_id);

if ($mqtt->connect(true, NULL, $username, $password)) {$mqtt->publish('empresa/edificio1/planta1/temperatura', '{temp:"21grados"}' . date('r'), 0, false);
$mqtt->close();
} else {
   echo "Time out!\n";
}

y en Python, en una carpeta aparte, preparamos la parte de python, nos creamos un virtual env para nuestro proyecto.

 

python3 -m venv mqttpy
source mqttpy/bin/activate
pip install paho-mqtt

 

Tras preparar nuestro entorno podemos escribir un consumidor, mqttp_sub.py de la siguiente forma:


import paho.mqtt.client as mqtt
# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
print("Connected with result code "+str(rc))

# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
client.subscribe("empresa/#")

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
print(msg.topic+" "+str(msg.payload))

client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message

client.connect("192.168.1.102", 1883, 60)

# Blocking call that processes network traffic, dispatches callbacks and
# handles reconnecting.
# Other loop*() functions are available that give a threaded interface and a
# manual interface.
client.loop_forever()

Ahora lanzamos la parte productora:


daniel@localserver:~/proyectos/bluerhinos_mqtt$ php mqtt_pub_single.php

 

Y en la parte suscriptora lanzamos:


daniel@localserver:~/proyectos/mqtt_python$ python mqttp_sub.py 

Connected with result code 0
empresa/edificio1/planta1/temperatura b'{temp:"21grados"}Fri, 15 Apr 2022 23:50:10 +0200'

y recordemos que aun tenemos el suscriptor del Mosquitto escuchando, y nos dice:


daniel@dockerserver:/etc/mosquitto$ mosquitto_sub -h 192.168.1.102 -t empresa/edificio1/planta1/temperatura -v
empresa/edificio1/planta1/temperatura {temp:’21grados’}
empresa/edificio1/planta1/temperatura {temp:"21grados"}Fri, 15 Apr 2022 23:50:10 +0200

Hemos hecho una publicación desde PHP que ha sido consumida desde el cliente mosquitto y desde python.

Por último, vamos a irnos al IDE de Arduino y vamos a instalar las librerías de ESP32. ESP32 es un microcontrolador similar a Arduino, emplea un microprocesador Tensilica Xtensa LX6, de bajo consumo que incluye entre otras características Wifi y bluetooth. Las posibilidades son amplísimas. Existen módulos de ESP32 que además tienen un conector para una cámara de 2K bastante decente. En este caso usaremos una ESP32 wroom 32. No entraré en detalles de instalación, por que hay mucha info y es muy sencillo. Despues de la instalación de sus librerias disponemos de unos ejemplos muy ilustrativos en el propio IDE. He modificado uno de ellos para que actue como consumidor de dos tipos de topics (evento1 y evento2) que usaré para encender dos leds y para publicar un topic (boton) al presionar un boton: este es el código para grabar en el ESP32:


#include <WiFi.h>
#include <PubSubClient.h>

// Update these with values suitable for your network.
const char* ssid = "***********";
const char* password = "**********";
const char* mqtt_server = "192.168.1.102";
const int pinled = 14;
const int pinled2 = 12;
WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastMsg = 0;
#define MSG_BUFFER_SIZE (50)
char msg[MSG_BUFFER_SIZE];
int value = 0;
const int pinbotonon = 13;

int botonon = HIGH;
void setup_wifi() {
delay(10);
// We start by connecting to a WiFi network
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);

WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
  Serial.print((char)payload[i]);
}
Serial.println();

if (strcmp(topic,"evento1")){
  if ((char)payload[0] == '1') {
    digitalWrite(pinled, HIGH); // Turn the LED on (Note that LOW is the voltage level
  } else {
    digitalWrite(pinled, LOW); // Turn the LED off by making the voltage HIGH
  }
} else
  if (strcmp(topic,"evento2")){
    if ((char)payload[0] == '1') {
      digitalWrite(pinled2, HIGH); // Turn the LED on (Note that LOW is the voltage level
    } else {
   digitalWrite(pinled2, LOW); // Turn the LED off by making the voltage HIGH
}
}


}
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Create a random client ID
String clientId = "ESP8266Client-";
clientId += String(random(0xffff), HEX);
// Attempt to connect
if (client.connect(clientId.c_str())) {
   Serial.println("connected");
  // Once connected, publish an announcement...
  client.publish("outTopic", "hello world");
  // ... and resubscribe
  client.subscribe("evento1"); 
  client.subscribe("evento2");
} else {
  Serial.print("failed, rc=");
  Serial.print(client.state());
  Serial.println(" try again in 5 seconds");
  // Wait 5 seconds before retrying
  delay(5000);
}
}
}
void setup() {
pinMode(pinled, OUTPUT); // Initialize the BUILTIN_LED pin as an output
pinMode(pinled2, OUTPUT);
pinMode(pinbotonon, INPUT);
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void loop() {

if (!client.connected()) {
  reconnect();
}
client.loop();

unsigned long now = millis();
if (now - lastMsg > 200) {
  lastMsg = now;
  ++value;
  botonon = digitalRead(pinbotonon);
if (botonon==HIGH){
  snprintf (msg, MSG_BUFFER_SIZE, "hello world high #%ld", value);
  Serial.print("Publish message: ");
  Serial.println(msg);
  client.publish("boton", msg);
}
}
}

Y el código que encenderá los leds será este:


#!/usr/bin/php
<?php
require_once('./vendor/autoload.php'); $server = 'localhost'; // change if necessary $port = 1883; // change if necessary $username = ''; // set your username $password = ''; // set your password $client_id = 'phpMQTT-publisher'; // make sure this is unique for connecting to sever - you could use uniqid() $mqtt = new Bluerhinos\phpMQTT($server, $port, $client_id); $estado1=0; $estado2=0; if ($mqtt->connect(true, NULL, $username, $password)) { system("stty -icanon"); echo "input# "; while ($c = fread(STDIN, 1)) { echo "Read from STDIN: " . $c . "\ninput# "; if ($c=="1"){ $estado1 = ($estado1==1)?0:1; $mqtt->publish('evento1', $estado1, 0, false); } if ($c=="2"){$estado2 = ($estado2==1)?0:1; $mqtt->publish('evento2', $estado2, 0, false); } } $mqtt->close(); } else { echo "Time out!\n"; } ?>

El circuito que vamos a hacer es este:

montaje

A mi me quedó así (no me darán un premio por este circuito, no :^))

montaje

 



Bien pues cada presión de la tecla [1] encenderá y apagará el primer led, y lo mismo con la tecla [2]. Del mismo modo cuando presionas el pulsador, se generará una publicación y el suscriptor nos muestra:

 


daniel@localserver:~$ mosquitto_sub -h 192.168.1.102 -t boton -v
Que pasa tron, soy el boton!! #209
Que pasa tron, soy el boton!! #213
Que pasa tron, soy el boton!! #217

 

Y con esto hemos terminado por hoy, espero que os haya aparecido tan interesante como a mi, un saludo.