01 · Lo que significan los bytes
La capa de aplicación
Con los temas 29 a 31 un dispositivo ya puede abrir una conexión TCP con otro equipo y mandar bytes. Todavía falta lo más importante: ponerse de acuerdo en qué quieren decir. Un protocolo de aplicación define cuatro cosas:
- El formato de los mensajes (texto o binario, qué campos, en qué orden).
- El diálogo: quién inicia, quién responde, qué pasa si algo sale mal.
- El direccionamiento de lo que se pide o se publica (una ruta, un tema).
- Las garantías que ofrece o que delega en TCP.
Es el mismo papel que Modbus (tema 25) cumple sobre RS-485, o que los protocolos de CANopen y J1939 (tema 28) cumplen sobre CAN, pero en una escala distinta: aquí el «otro extremo» puede ser un servidor en otro continente, y el mismo protocolo lo comparten dispositivos de miles de fabricantes. Dos filosofías muy diferentes dominan el área: el modelo cliente-servidor de HTTP, donde el cliente pregunta y el servidor contesta, y el modelo de publicación y suscripción de MQTT, donde un intermediario reparte los mensajes.
02 · La web
HTTP: pedidos y respuestas
HTTP (HyperText Transfer Protocol) funciona sobre TCP (puerto 80, o 443 con TLS) y se basa en un intercambio simple: el cliente envía una petición y el servidor devuelve una respuesta. El cliente siempre inicia; el servidor nunca puede enviar algo por su cuenta. Ambos mensajes son texto con la misma estructura:
- Una línea inicial. En la petición: el método, la ruta del recurso y la versión (
POST /api/lecturas HTTP/1.1). En la respuesta: la versión, el código de estado y un texto (HTTP/1.1 201 Created). - Los encabezados, uno por línea, con el formato
Nombre: valor. Los más usados:Host(obligatorio en HTTP/1.1: a qué servidor va),Content-Type(formato del cuerpo, por ejemploapplication/json),Content-Length(cuántos bytes tiene el cuerpo),Authorization(credenciales) yConnection. - Una línea en blanco, que marca el fin de los encabezados.
- El cuerpo, opcional, con los datos. Su largo lo dice
Content-Length, así el receptor sabe cuándo terminó (recordá que TCP no delimita mensajes).
Cada línea termina con dos bytes, CR LF (\r\n). Es un detalle que importa cuando se arma un mensaje a mano en un microcontrolador: println envía ambos.
Los métodos expresan la acción sobre el recurso:
| Método | Acción | ¿Con cuerpo? | ¿Idempotente? |
|---|---|---|---|
GET | Leer un recurso | No | Sí |
POST | Crear algo o enviar datos para procesar | Sí | No |
PUT | Reemplazar un recurso por completo | Sí | Sí |
PATCH | Modificar una parte | Sí | No necesariamente |
DELETE | Borrar un recurso | No | Sí |
«Idempotente» quiere decir que repetir la petición produce el mismo resultado que hacerla una sola vez: si no llegó la respuesta a un PUT, se puede repetir sin miedo; con un POST se arriesga a duplicar el efecto (dos lecturas guardadas, dos pedidos).
La respuesta empieza con un código de estado de tres dígitos. La primera cifra indica la familia:
| Familia | Significado | Ejemplos | Qué hace el dispositivo |
|---|---|---|---|
| 2xx | Éxito | 200 OK, 201 Created, 204 No Content | Continuar |
| 3xx | Redirección | 301 Moved Permanently, 304 Not Modified | Seguir la nueva dirección (o usar la copia guardada) |
| 4xx | Error del cliente | 400, 401, 403, 404, 429 | No repetir igual: corregir; en el 429, esperar |
| 5xx | Error del servidor | 500, 502, 503, 504 | Reintentar más tarde, con esperas crecientes |
El HTTP original abría una conexión TCP por cada petición. HTTP/1.1 (la versión que usan los dispositivos pequeños) mantiene la conexión abierta por defecto para varias peticiones (keep-alive); con Connection: close el servidor la cierra al terminar. HTTP/2 y HTTP/3 multiplexan muchas peticiones y usan formato binario, pero son pesados para un microcontrolador y rara vez se usan en ellos.
03 · Datos legibles
JSON: datos en texto
HTTP dice cómo viaja el mensaje, no qué datos lleva. El formato más usado hoy es JSON (JavaScript Object Notation): texto estructurado que entienden todos los lenguajes. Tiene seis tipos de valor:
- Objeto: un conjunto de pares clave-valor entre llaves, con las claves siempre en texto entre comillas dobles:
{"id":"nodo1","t":23.5}. - Arreglo: una lista ordenada entre corchetes:
[21.5, 21.7, 22.0]. - Texto: entre comillas dobles, en UTF-8, con barra invertida para escapar (
\",\n). - Número: entero o decimal, con punto (
23.5,-4,1.2e3). No hayNaNni infinito. - Lógico:
trueofalse, en minúsculas. - Nulo:
null, ausencia de valor.
Las reglas son estrictas y los errores típicos de quien escribe JSON a mano son siempre los mismos: comillas simples en lugar de dobles, una coma de más antes de la llave de cierre, claves sin comillas, comentarios (no existen) y números con coma decimal. Un solo error y el receptor rechaza todo el documento.
Las ventajas de JSON son que se puede leer y escribir a mano, se depura con cualquier visor, todos los lenguajes lo procesan, y es extensible: agregar un campo nuevo no rompe a los programas viejos que lo ignoran. El costo: es voluminoso. Cada número viaja como dígitos de texto y cada clave se repite en cada mensaje. El objeto {"id":"nodo1","t":23.5,"ok":true} mide 33 bytes para transportar un texto, un número y un lógico que en binario cabrían en una decena de bytes. En un enlace Ethernet o Wi-Fi no importa; en uno celular o de baja velocidad (LoRa, tema 34 en adelante), sí. Hay alternativas binarias (CBOR, MessagePack, Protocol Buffers) que conservan la estructura y ahorran espacio.
En un microcontrolador el análisis de JSON no es trivial porque consume memoria. La biblioteca más usada en Arduino y ESP32 es ArduinoJson, que reserva un bloque de memoria fijo para el documento. Reglas prácticas: mantener los mensajes pequeños, dimensionar el documento según lo máximo esperado, y comprobar siempre el resultado del análisis antes de usar los campos.
04 · HTTP desde el hardware
HTTP en un dispositivo
Un dispositivo usa HTTP de dos maneras. Como cliente, envía sus lecturas a un servicio (un POST con un JSON) o consulta configuración y comandos (un GET). Como servidor, expone una pequeña página o una API en su propia dirección IP: sirve para configurarlo desde el navegador o leer su estado, sin ninguna aplicación especial. Los dos casos son muy útiles, y los dos tienen un límite estructural.
Ese límite es que el servidor no puede avisar. Si un dispositivo quiere recibir una orden de la nube, solo puede preguntar cada tanto: sondeo (polling). Con un sondeo cada 5 segundos, la orden tarda hasta 5 segundos en llegar, y mientras no haya órdenes se están enviando cientos de peticiones inútiles por hora, cada una con unos 100 bytes de encabezados y su saludo TCP. Existen soluciones (long polling, WebSockets, eventos enviados por el servidor), pero son más complejas. Y si el dispositivo está detrás de NAT (tema 30), nadie puede conectarse a él desde afuera: por eso un dispositivo en una casa normalmente actúa de cliente.
Otros puntos para tener en cuenta:
- Reintentos con espera creciente. Ante un 5xx, un timeout o un 429, esperar 1, 2, 4, 8... segundos hasta un máximo. Si miles de dispositivos reintentan al mismo tiempo, tumban el servidor (con un poco de variación aleatoria se evita).
- Tiempos límite. Cada conexión y lectura tiene que tener un máximo: un servidor que no contesta no puede trabar el programa.
- Pocos bytes. Enviar solo lo necesario, con claves cortas, y agrupar varias lecturas en un solo
POSTsi el tiempo lo permite (tema 29, eficiencia). - HTTPS. La versión segura (HTTP sobre TLS, puerto 443) cifra el tráfico y autentica al servidor. Para un microcontrolador pequeño tiene un costo real: decenas de kilobytes de memoria, un saludo más largo y la gestión de certificados (qué autoridades se aceptan, cuándo vencen). El ESP32 la soporta; un Arduino con un W5500 de hardware básico, no. Nunca enviar contraseñas o claves por HTTP sin TLS.
05 · Un cartero en el medio
MQTT: publicar y suscribirse
MQTT (Message Queuing Telemetry Transport) nació en 1999 para monitorear oleoductos por enlaces satelitales lentos y caros, y hoy es el protocolo más usado en IoT. Su idea: los dispositivos no se hablan entre sí. Cada uno abre una conexión TCP con un broker (un servidor intermediario, como Mosquitto, EMQX o HiveMQ, o los servicios de IoT de las nubes), y el broker reparte los mensajes. Hay dos acciones:
- Publicar (publish): enviar un mensaje a un tema (topic), por ejemplo
casa/sala/temperaturacon el dato23.5. - Suscribirse (subscribe): decirle al broker «mandame todo lo que se publique en los temas que coincidan con este filtro».
Esta separación tiene consecuencias muy valiosas. El que publica no sabe quién recibe, ni si hay alguien: se desacopla en espacio (no hace falta conocer direcciones IP) y en tiempo (con mensajes retenidos, el suscriptor puede conectarse después). Un mismo dato llega a muchos suscriptores sin que el publicador envíe nada más, y agregar un nuevo consumidor (un registro, una alarma, un tablero) no toca a los dispositivos. Y como todas las conexiones salen del dispositivo hacia el broker, funciona detrás de NAT y firewalls: el dispositivo recibe comandos por la misma conexión que usa para publicar, sin que nadie tenga que conectarse a él.
Los temas son cadenas jerárquicas con niveles separados por «/»: casa/sala/temperatura. No hay que crearlos: existen desde que alguien publica en ellos. Un buen esquema de nombres es lo más importante del diseño de una red MQTT: de lo general a lo particular (planta/linea2/motor5/temperatura), sin una barra inicial, en minúsculas, con nombres que describan la magnitud y no el dispositivo, y separando datos (.../temperatura), comandos (.../luz/set) y estado (.../estado).
Quien se suscribe usa filtros, que pueden llevar dos comodines:
+reemplaza exactamente un nivel:casa/+/temperaturacoincide concasa/sala/temperaturay concasa/cocina/temperatura, pero no concasa/sala/luz/temperatura.#reemplaza todos los niveles restantes y solo puede ir al final:casa/#coincide con todo lo que cuelga decasa(incluidacasamisma). El filtro#solo recibe todo.
El que publica siempre usa un tema concreto, sin comodines. Los temas que empiezan con $ (como $SYS, donde el broker publica estadísticas) quedan fuera de los comodines del primer nivel.
06 · Binario y mínimo
Los paquetes MQTT
A diferencia de HTTP, MQTT es binario y sus paquetes son muy pequeños. Todo paquete empieza con un encabezado fijo de al menos 2 bytes: el primero tiene el tipo de paquete en los cuatro bits altos y marcas en los bajos; después viene el largo restante (cuántos bytes siguen), codificado con una técnica de largo variable: cada byte aporta 7 bits de valor y su bit alto indica si sigue otro byte. Así, hasta 127 bytes se codifican con un byte, hasta 16 383 con dos, y el máximo (256 MB) con cuatro. Por ejemplo, 27 es 1B, 128 es 80 01 y 16 384 es 80 80 01.
| Paquete | Primer byte | Quién lo envía | Para qué |
|---|---|---|---|
| CONNECT | 0x10 | Cliente | Abrir la sesión: identificador, latido, usuario, última voluntad |
| CONNACK | 0x20 | Broker | Aceptar o rechazar la conexión |
| PUBLISH | 0x30 a 0x3F | Ambos | Enviar un mensaje a un tema (las marcas son DUP, QoS y RETAIN) |
| PUBACK / PUBREC / PUBREL / PUBCOMP | 0x40 / 0x50 / 0x62 / 0x70 | Ambos | Confirmaciones de QoS 1 y 2 |
| SUBSCRIBE / SUBACK | 0x82 / 0x90 | Cliente / broker | Pedir y confirmar una suscripción |
| PINGREQ / PINGRESP | 0xC0 / 0xD0 | Cliente / broker | Mantener viva la conexión |
| DISCONNECT | 0xE0 | Cliente | Cerrar la sesión de forma ordenada |
Un CONNECT mínimo para el cliente sensor1 con latido de 60 s: 10 13 00 04 4D 51 54 54 04 02 00 3C 00 07 73 65 6E 73 6F 72 31, es decir, tipo CONNECT, largo 19, el nombre del protocolo «MQTT» (precedido por su largo de 2 bytes), el nivel de protocolo 4 (versión 3.1.1), las marcas de conexión (02 = sesión limpia), el latido (00 3C = 60) y el identificador del cliente con su largo. El broker responde con un CONNACK de 4 bytes (20 02 00 00) que dice que aceptó.
Un PUBLISH lleva, después del encabezado fijo, el largo del tema en 2 bytes, el tema, un identificador de paquete de 2 bytes (solo si QoS es 1 o 2) y los datos. Publicar 23.5 en casa/sala/temperatura con QoS 0 son 29 bytes: 30 1B 00 15, los 21 caracteres del tema y 32 33 2E 35. La misma información en una petición HTTP mínima ocupa casi el triple, y la respuesta es otro mensaje aparte. El broker no interpreta los datos: son bytes opacos (texto, JSON, binario), de hasta 256 MB, aunque en la práctica se mantienen en decenas o cientos de bytes.
MQTT 5 (2019) agrega códigos de motivo en las respuestas, propiedades de usuario en los mensajes (como los encabezados de HTTP), caducidad de mensajes, temas por alias y suscripciones compartidas. La versión 3.1.1 sigue siendo la más extendida en dispositivos pequeños y es la que describimos.
07 · Cuánta garantía
Calidad de servicio
MQTT corre sobre TCP, que ya entrega los bytes en orden, pero una conexión TCP puede cortarse en cualquier momento (un dispositivo móvil que pierde cobertura), y los mensajes en tránsito se pierden. Para eso MQTT define tres niveles de calidad de servicio (QoS) por mensaje:
- QoS 0: como máximo una vez. Se envía el PUBLISH y listo, sin confirmación. Si la conexión falla, el mensaje se pierde. Es el más barato (un solo paquete) y suficiente para datos que se repiten, como una temperatura cada segundo.
- QoS 1: al menos una vez. El receptor responde con un PUBACK con el identificador del paquete; si el emisor no lo recibe a tiempo, reenvía el PUBLISH con la marca DUP. El mensaje seguro llega, pero puede llegar duplicado (si se perdió el PUBACK, no el PUBLISH). Es el nivel más usado.
- QoS 2: exactamente una vez. Un intercambio de cuatro paquetes (PUBLISH, PUBREC, PUBREL, PUBCOMP) que garantiza que el mensaje se entrega una sola vez. Es el más lento y pesado, y se usa donde un duplicado causaría daño (una orden de pago, un conteo).
| QoS | Garantía | Paquetes | Duplicados | Uso típico |
|---|---|---|---|---|
| 0 | Como máximo una vez | 1 | No | Telemetría periódica |
| 1 | Al menos una vez | 2 | Posibles | Eventos, comandos que se pueden repetir |
| 2 | Exactamente una vez | 4 | No | Operaciones que no admiten repetirse |
Dos precisiones. El QoS se negocia en cada tramo: el publicador envía al broker con su QoS, y el broker entrega a cada suscriptor con el menor entre el QoS del mensaje y el de la suscripción. Y QoS no es lo mismo que la confiabilidad de TCP: TCP asegura que los bytes lleguen en una conexión viva; QoS asegura que el mensaje sobreviva a la caída de la conexión, porque cliente y broker guardan los mensajes sin confirmar y los retoman al reconectar (si la sesión es persistente).
Para el diseño: usar QoS 0 para lo que se repite, QoS 1 para lo que no debe perderse y la aplicación puede tolerar duplicados (los comandos idempotentes, como «poner la luz en encendida»; no «alternar la luz»), y QoS 2 solo si es imprescindible. Hay bibliotecas pequeñas que soportan solo QoS 0 al publicar: conviene verificarlo antes de depender de ello.
08 · Funciones pensadas para IoT
Retenidos, última voluntad y latido
MQTT trae tres mecanismos que resuelven problemas típicos de los dispositivos conectados:
- Mensaje retenido (retain). Si un PUBLISH lleva la marca RETAIN, el broker guarda el último mensaje de ese tema y se lo entrega a todo suscriptor nuevo apenas se suscribe. Es el mecanismo para publicar estados: un tablero que se conecta a las 3 de la tarde recibe de inmediato el último valor conocido, sin esperar al próximo ciclo del sensor. Para borrar un retenido se publica un mensaje retenido vacío en el mismo tema.
- Última voluntad (last will and testament). Al conectarse, el cliente le deja al broker un mensaje y un tema. Si el cliente se desconecta de forma anormal (corte de energía, cable, error), el broker lo publica en su nombre; si se desconecta con DISCONNECT, no. Combinado con el retenido, el patrón clásico es: el dispositivo publica
conectado(retenido) en su tema de estado al conectarse, con la última voluntaddesconectado(retenida) en el mismo tema. Así cualquiera sabe en todo momento si el dispositivo está vivo. - Latido (keep alive). En CONNECT el cliente declara un intervalo (por ejemplo 60 s). Si no envía nada en ese tiempo, debe mandar un PINGREQ. Si el broker no oye nada durante 1,5 veces el intervalo, considera caído al cliente, cierra la conexión y publica la última voluntad. Así se detectan las caídas «silenciosas» que TCP solo notaría tras mucho tiempo.
También se configura si la sesión es limpia o persistente. Con sesión limpia, al reconectar el broker olvida las suscripciones y los mensajes pendientes; con sesión persistente (identificador de cliente fijo y sesión no limpia), el broker conserva las suscripciones y los mensajes QoS 1 y 2 acumulados mientras el cliente estuvo desconectado y se los entrega al volver. Un dispositivo que duerme para ahorrar batería y despierta a leer comandos necesita esto.
El identificador de cliente tiene que ser único en el broker: si dos dispositivos se conectan con el mismo, el broker echa al más viejo, y los dos pasan el día reconectándose uno al otro. Es un error muy común al clonar un programa en varias placas: hay que derivar el identificador de algo propio (la MAC, por ejemplo).
09 · Una decisión de diseño
HTTP o MQTT, y seguridad
| Aspecto | HTTP | MQTT |
|---|---|---|
| Modelo | Pedido y respuesta (cliente-servidor) | Publicar y suscribirse (con broker) |
| Formato | Texto | Binario |
| Encabezado típico | 100 a 500 bytes | 2 a 10 bytes |
| Conexión | Corta, o larga con pedidos por turnos | Una sola, permanente |
| Enviar al dispositivo | Solo preguntando (sondeo) | Inmediato por la misma conexión |
| Detrás de NAT | Funciona como cliente | Funciona: la conexión sale del dispositivo |
| Uno a muchos | No directamente | Natural: los suscriptores |
| Garantías | Las de TCP, más reintentos del cliente | QoS 0, 1 y 2, retenidos, última voluntad |
| Requisitos | Un servidor web (o una API) | Un broker |
| Herramientas | Cualquier navegador, curl | Clientes MQTT (por ejemplo MQTT Explorer, mosquitto_sub) |
Una regla práctica: HTTP para configurar un dispositivo desde un navegador, para integrarse con servicios web y para enviar datos de vez en cuando; MQTT para telemetría continua, comandos en tiempo real y redes con muchos dispositivos y consumidores. En muchos proyectos conviven: el dispositivo publica por MQTT y ofrece una página HTTP para configurarlo.
Sobre la seguridad, ambos protocolos son de texto claro por omisión (HTTP en el puerto 80, MQTT en el 1883), de modo que cualquiera que vea el tráfico lee todo, incluidas las contraseñas. La protección se logra con TLS: HTTPS (443) y MQTT sobre TLS (puerto 8883). Los puntos a cuidar:
- Cifrado con TLS en todo lo que salga de la red local.
- Autenticación: usuario y contraseña (o certificados) en el CONNECT de MQTT, y un token o clave en el encabezado
Authorizationde HTTP. Cada dispositivo con su propia credencial, para poder revocarla. - Autorización: que cada cliente solo pueda publicar y suscribirse a sus temas (listas de control de acceso del broker).
- No dejar un broker sin autenticación abierto a Internet. Es una de las fallas más frecuentes y cualquiera puede leer y comandar los dispositivos.
- Secretos fuera del código fuente que se comparte.
10 · Con el microcontrolador
Con el microcontrolador
El primer programa usa HTTP con el módulo W5500: cada 10 segundos envía un POST con un JSON a un servidor de la red local y lee el código de estado de la respuesta. Arma la petición a mano, para que se vean los encabezados del texto (println agrega el CR LF):
#include <SPI.h>
#include <Ethernet.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // cambiar el último byte en cada placa
const int PIN_CS = 10;
IPAddress servidor(192, 168, 1, 10); // la computadora con el servicio: ajustarla
const uint16_t PUERTO = 8000;
EthernetClient cliente;
int enviarLectura(float temperatura) {
if (!cliente.connect(servidor, PUERTO)) return -1; // no se pudo abrir la conexión TCP
String cuerpo = "{\"t\":" + String(temperatura, 1) + "}";
cliente.println("POST /api/lecturas HTTP/1.1");
cliente.println("Host: 192.168.1.10");
cliente.println("Content-Type: application/json");
cliente.print("Content-Length: "); cliente.println(cuerpo.length());
cliente.println("Connection: close");
cliente.println(); // línea en blanco: terminan los encabezados
cliente.print(cuerpo);
unsigned long t0 = millis();
while (!cliente.available() && millis() - t0 < 3000) delay(5); // espera la respuesta, con tiempo límite
String estado = cliente.readStringUntil('\n'); // por ejemplo "HTTP/1.1 201 Created"
cliente.stop();
return estado.length() >= 12 ? estado.substring(9, 12).toInt() : -2;
}
void setup() {
Serial.begin(115200);
Ethernet.init(PIN_CS);
if (Ethernet.begin(mac, 10000) == 0) { Serial.println("Sin DHCP"); while (true) delay(1000); }
}
void loop() {
static unsigned long ultimo = 0;
if (millis() - ultimo >= 10000) {
ultimo = millis();
int codigo = enviarLectura(23.5);
Serial.print("Respuesta: "); Serial.println(codigo); // 201 si todo salió bien
// -1 = sin conexión, -2 = respuesta ilegible; 4xx no reintentar igual; 5xx reintentar con espera
}
Ethernet.maintain();
}
Para probarlo, un servidor mínimo en una computadora (copialo a un archivo y ejecutalo con Python; escucha en el puerto 8000, muestra lo que recibe y responde 201):
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
class Manejador(BaseHTTPRequestHandler):
def do_POST(self):
largo = int(self.headers.get("Content-Length", 0))
datos = json.loads(self.rfile.read(largo))
print("Lectura recibida:", datos)
respuesta = b'{"id": 17}'
self.send_response(201)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(respuesta)))
self.end_headers()
self.wfile.write(respuesta)
HTTPServer(("0.0.0.0", 8000), Manejador).serve_forever()
El segundo programa usa MQTT con la biblioteca PubSubClient. Se conecta a un broker (por ejemplo Mosquitto en la misma computadora), declara una última voluntad retenida, publica su estado y una temperatura cada 5 segundos y se suscribe a un tema de comandos. Si la conexión se cae, reintenta sin trabar el programa:
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
const int PIN_CS = 10;
IPAddress broker(192, 168, 1, 10); // el broker: ajustarlo
const char *ID_CLIENTE = "sensor1"; // único en el broker: derivarlo de la MAC si hay varias placas
EthernetClient red;
PubSubClient mqtt(red);
void alRecibir(char *tema, byte *datos, unsigned int largo) { // se llama desde mqtt.loop()
String mensaje;
for (unsigned int i = 0; i < largo; i++) mensaje += (char)datos[i];
Serial.print(tema); Serial.print(" = "); Serial.println(mensaje);
if (String(tema) == "casa/sala/luz/set") digitalWrite(LED_BUILTIN, mensaje == "1" ? HIGH : LOW);
}
bool conectar() {
// última voluntad: "desconectado", retenida, QoS 1, en el tema de estado
if (!mqtt.connect(ID_CLIENTE, nullptr, nullptr, "casa/sala/sensor1/estado", 1, true, "desconectado")) return false;
mqtt.publish("casa/sala/sensor1/estado", "conectado", true); // retenido: quien se suscriba lo ve de inmediato
mqtt.subscribe("casa/sala/luz/set"); // comandos para este nodo
return true;
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
Ethernet.init(PIN_CS);
if (Ethernet.begin(mac, 10000) == 0) { Serial.println("Sin DHCP"); while (true) delay(1000); }
mqtt.setServer(broker, 1883);
mqtt.setKeepAlive(30); // latido de 30 s
mqtt.setCallback(alRecibir);
}
void loop() {
static unsigned long ultimoIntento = 0, ultimoEnvio = 0;
if (!mqtt.connected()) {
if (millis() - ultimoIntento >= 5000) { // reintento cada 5 s, sin bloquear
ultimoIntento = millis();
Serial.println(conectar() ? "Conectado al broker" : "No se pudo conectar");
}
} else {
mqtt.loop(); // atiende los paquetes entrantes y el latido
if (millis() - ultimoEnvio >= 5000) {
ultimoEnvio = millis();
mqtt.publish("casa/sala/temperatura", "23.5"); // QoS 0
}
}
Ethernet.maintain();
}
Para probarlo desde la computadora con Mosquitto instalado: mosquitto_sub -h 192.168.1.10 -t "casa/#" -v muestra todo lo que publica la placa, y mosquitto_pub -h 192.168.1.10 -t casa/sala/luz/set -m 1 le envía un comando. Dos advertencias sobre la biblioteca: publish solo envía con QoS 0 (para QoS 1 al publicar hay que usar otra), y el tamaño máximo del paquete es de 256 bytes por omisión (se cambia con setBufferSize); un mensaje más grande se descarta sin aviso.
11 · Analogía con programación
Aplicaciones en Python
Los protocolos de aplicación son formatos de datos y reglas de diálogo, así que se entienden bien escribiéndolos. El modelo siguiente arma una petición HTTP y lee una respuesta, serializa JSON, codifica el largo restante de MQTT, construye los paquetes CONNECT, PUBLISH y SUBSCRIBE a mano, implementa los filtros con comodines y simula un pequeño broker con mensajes retenidos.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
import json
def peticion_http(metodo, ruta, host, cuerpo=""):
lineas = [f"{metodo} {ruta} HTTP/1.1", f"Host: {host}"]
if cuerpo:
lineas += ["Content-Type: application/json", f"Content-Length: {len(cuerpo.encode())}"]
lineas.append("Connection: close")
return ("\r\n".join(lineas) + "\r\n\r\n" + cuerpo).encode()
def leer_respuesta(datos):
cabecera, _, cuerpo = datos.partition(b"\r\n\r\n")
lineas = cabecera.decode().split("\r\n")
version, codigo, motivo = lineas[0].split(" ", 2)
encabezados = dict(l.split(": ", 1) for l in lineas[1:])
return int(codigo), motivo, encabezados, cuerpo
def largo_restante(n):
salida = []
while True:
byte, n = n % 128, n // 128
salida.append(byte | (0x80 if n else 0))
if not n:
return bytes(salida)
def cadena(texto):
b = texto.encode()
return len(b).to_bytes(2, "big") + b
def mqtt_publish(tema, datos, qos=0, retener=False, id_paquete=10):
variable = cadena(tema) + (id_paquete.to_bytes(2, "big") if qos else b"")
cuerpo = variable + datos
return bytes([0x30 | (qos << 1) | int(retener)]) + largo_restante(len(cuerpo)) + cuerpo
def mqtt_connect(id_cliente, keepalive=60):
variable = cadena("MQTT") + bytes([4, 0x02]) + keepalive.to_bytes(2, "big") # nivel 4 = MQTT 3.1.1, sesión limpia
cuerpo = variable + cadena(id_cliente)
return bytes([0x10]) + largo_restante(len(cuerpo)) + cuerpo
def mqtt_subscribe(filtro, qos=0, id_paquete=1):
cuerpo = id_paquete.to_bytes(2, "big") + cadena(filtro) + bytes([qos])
return bytes([0x82]) + largo_restante(len(cuerpo)) + cuerpo
def coincide(filtro, tema):
f, t = filtro.split("/"), tema.split("/")
if t[0].startswith("$") and f[0] in ("#", "+"):
return False
for i, nivel in enumerate(f):
if nivel == "#":
return True
if i >= len(t) or (nivel != "+" and nivel != t[i]):
return False
return len(f) == len(t)
class Broker:
def __init__(self):
self.suscripciones = [] # (cliente, filtro)
self.retenidos = {}
def suscribir(self, cliente, filtro):
self.suscripciones.append((cliente, filtro))
return [(t, d) for t, d in self.retenidos.items() if coincide(filtro, t)] # lo último retenido llega al suscribirse
def publicar(self, tema, datos, retener=False):
if retener:
self.retenidos[tema] = datos
return [c for c, f in self.suscripciones if coincide(f, tema)]
print("Petición:", repr(peticion_http("POST", "/api/lecturas", "192.168.1.10", '{"t":23.5}')))
print(" tamaño:", len(peticion_http("POST", "/api/lecturas", "192.168.1.10", '{"t":23.5}')), "bytes")
resp = b'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 10\r\n\r\n{"id": 17}'
codigo, motivo, enc, cuerpo = leer_respuesta(resp)
print("Respuesta:", codigo, motivo, enc["Content-Type"], json.loads(cuerpo))
dato = {"id": "nodo1", "t": 23.5, "ok": True}
print("JSON:", json.dumps(dato, separators=(",", ":")), "->", len(json.dumps(dato, separators=(",", ":"))), "bytes")
print("Largo restante de 27, 128, 16383 y 16384:", [largo_restante(n).hex(" ") for n in (27, 128, 16383, 16384)])
print("CONNECT:", mqtt_connect("sensor1").hex(" ").upper())
print("PUBLISH QoS 0:", mqtt_publish("casa/sala/temperatura", b"23.5").hex(" ").upper())
print("PUBLISH QoS 1 retenido:", mqtt_publish("casa/sala/temperatura", b"23.5", 1, True).hex(" ").upper())
print("SUBSCRIBE:", mqtt_subscribe("casa/+/temperatura", 1).hex(" ").upper())
for filtro, tema in (("casa/+/temperatura", "casa/sala/temperatura"), ("casa/+/temperatura", "casa/sala/humedad"),
("casa/#", "casa/sala/luz/estado"), ("casa/#", "casa"), ("+/sala", "casa/sala"), ("#", "$SYS/uptime")):
print(f"filtro {filtro!r} con tema {tema!r}:", coincide(filtro, tema))
b = Broker()
b.suscribir("tablero", "casa/+/temperatura")
b.suscribir("alarma", "casa/#")
print("Recibe 'casa/sala/temperatura':", b.publicar("casa/sala/temperatura", b"23.5", retener=True))
print("Recibe 'garaje/puerta':", b.publicar("garaje/puerta", b"abierta"))
print("Nuevo suscriptor recibe retenidos:", b.suscribir("celular", "casa/#"))
print("Paquetes por mensaje según QoS 0, 1 y 2:", [1, 2, 4])
Resultado: la petición POST de {"t":23.5} mide 132 bytes y la respuesta 201 Created se lee como código 201 con cuerpo {"id": 17}. El JSON {"id":"nodo1","t":23.5,"ok":true} mide 33 bytes compactado. El largo restante de MQTT para 27, 128, 16383 y 16384 es 1b, 80 01, ff 7f y 80 80 01. El CONNECT del cliente sensor1 es 10 13 00 04 4D 51 54 54 04 02 00 3C 00 07 73 65 6E 73 6F 72 31, el PUBLISH QoS 0 de «23.5» en casa/sala/temperatura empieza con 30 1B 00 15 y mide 29 bytes, con QoS 1 y retenido empieza con 33 1D y lleva el identificador 00 0A, y el SUBSCRIBE a casa/+/temperatura es 82 17 00 01 00 12 …. Los filtros dan: casa/+/temperatura coincide con casa/sala/temperatura y no con casa/sala/humedad; casa/# coincide con casa/sala/luz/estado y con casa; +/sala con casa/sala; y # no coincide con $SYS/uptime. En el broker, la publicación en casa/sala/temperatura llega a tablero y alarma, la de garaje/puerta a nadie, y un suscriptor nuevo a casa/# recibe el último valor retenido.
Observá que los paquetes MQTT son realmente chicos, que el largo restante se codifica de a 7 bits y que el broker solo necesita dos estructuras (las suscripciones y los retenidos) para dar el comportamiento de publicar y suscribirse: el resto es red y calidad de servicio.
12 · Exploración interactiva
Laboratorio: HTTP, JSON y MQTT
El laboratorio tiene cinco herramientas. La primera arma una petición HTTP y mide cuánto ocupa. La segunda interpreta los códigos de estado y propone qué hacer. La tercera valida un JSON. La cuarta arma un paquete MQTT PUBLISH y lo compara con HTTP. La quinta prueba filtros de temas con comodines.
1. Petición HTTP
Elegí el método, la ruta y el cuerpo.
2. Código de estado
Escribí un código de estado.
3. Validar JSON
Escribí o modificá un JSON.
4. Paquete MQTT PUBLISH
Elegí el tema, los datos y el QoS.
5. Filtro de temas
Escribí un filtro y un tema.
En la herramienta 1, GET y DELETE no llevan cuerpo; en los otros métodos, si hay cuerpo se agregan Content-Type y Content-Length. En la 4, el identificador de paquete (QoS 1 y 2) vale 10, y la petición HTTP equivalente es la mínima posible: POST con el tema como ruta, Host y Content-Length. En la 5, los temas que empiezan con «$» no coinciden con comodines en el primer nivel.
Recorrido guiado
- Con los valores iniciales (POST a
/api/lecturas, cuerpo{"t":23.5}): 132 bytes en total, con 10 de cuerpo; el 92 % son encabezados. Cambiá el método a GET: se ignora el cuerpo, y no hay Content-Type ni Content-Length. - En la herramienta 2, con 201: «Created», éxito. Probá 404 (error del cliente: revisar la dirección), 429 (esperar), 503 (error del servidor: reintentar con espera) y 302 (redirección).
- En la herramienta 3, el JSON inicial es válido, de 33 bytes, con un texto, un número y un lógico. Cambiá las comillas dobles por simples, o agregá una coma antes de la llave final: «JSON inválido». Probá
[21.5, 21.7, 22.0]: un arreglo válido. - En la herramienta 4, con los valores iniciales:
30 1B 00 15 63 61 73 61 2F …, primer byte 0x30 (PUBLISH, QoS 0), largo restante 27, 29 bytes en total y un solo paquete. La petición HTTP equivalente es de 83 bytes, casi el triple. - Subí el QoS a 1: el primer byte pasa a 0x32, el largo a 29 (se suma el identificador de 2 bytes), 31 bytes en total y dos paquetes (PUBLISH y PUBACK). Con QoS 2: 0x34 y cuatro paquetes. Poné Retenido = Sí con QoS 1: 0x33.
- Con un dato de 200 caracteres, el largo restante pasa de 127 y ocupa dos bytes: aparece la marca de continuación.
- En la herramienta 5, con el filtro
casa/+/temperaturay el temacasa/sala/temperatura: coincide. Con el temacasa/sala/humedad: no. Concasa/sala/luz/temperatura: no (el+cubre un solo nivel). - Probá el filtro
casa/#concasa/sala/luz/estado(coincide) y concasa(también). Con el filtro#y el tema$SYS/uptime: no coincide. El filtrocasa/#/luzes inválido: el#va solo y al final.
Desafío: el costo de la telemetría
Un sensor envía cada 10 segundos una temperatura de 4 caracteres. Comparemos el tráfico en un día. Con MQTT QoS 0 son 29 bytes por mensaje (herramienta 4): 8640 mensajes por día, unos 250 KB. Con HTTP, la petición mínima es de 83 bytes (y la respuesta agrega 100 o más), y si cada lectura abre su propia conexión TCP se suman el saludo (tres segmentos de al menos 40 bytes) y el cierre: más de 400 bytes por lectura, unos 3,5 MB por día, más de diez veces. En una conexión celular que se paga por megabyte, o en un enlace lento, la diferencia define cuál protocolo conviene.
13 · Un procedimiento
Diagnóstico de aplicaciones de red
Cuando una aplicación de red no funciona, se avanza por las capas hasta encontrar el nivel que falla:
- Conectividad. ¿El dispositivo tiene IP, puerta de enlace y DNS (tema 30)? ¿Llega al servidor (ping)? ¿Está abierto el puerto (tema 31)?
- Con otra herramienta, el mismo servicio. Probar el servidor con
curl(HTTP) o conmosquitto_pubymosquitto_sub(MQTT) desde una computadora. Si anda con la herramienta y no con el dispositivo, el problema está en el dispositivo; si no anda con ninguna, en el servicio o en la red. - Mirar el diálogo. Un analizador de paquetes (Wireshark) entiende HTTP y MQTT y muestra los mensajes. En un broker, habilitar el registro detallado. En un servidor HTTP, el registro de accesos.
- Revisar el formato. Un JSON mal formado, un Content-Length incorrecto, una ruta equivocada, un tema con mayúsculas distintas.
- Revisar credenciales y permisos (401, 403, CONNACK de rechazo).
- Revisar memoria y tiempos en el dispositivo: buffers chicos, tiempos límite cortos, bloqueos en el bucle principal.
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| La petición HTTP no obtiene respuesta o se cuelga | Falta la línea en blanco final, o Content-Length no coincide con el cuerpo | Revisar los encabezados y el CR LF; contar los bytes del cuerpo |
| 400 Bad Request | JSON mal formado o ruta/parámetros incorrectos | Validar el JSON; comparar con una petición hecha con curl |
| 401 o 403 | Credenciales faltantes, incorrectas o sin permiso | Revisar el token y los permisos |
| El cliente MQTT no conecta (CONNACK de error) | Usuario/contraseña, identificador repetido o broker inaccesible | Probar con mosquitto_sub; mirar el registro del broker |
| Dos dispositivos se desconectan mutuamente | Mismo identificador de cliente | Identificador único por dispositivo |
| Los mensajes se publican pero nadie los recibe | Tema escrito distinto (mayúsculas, barra inicial) o filtro incorrecto | Suscribirse a # un momento y ver el tema real |
| Los mensajes grandes se pierden, los chicos llegan | Buffer del cliente MQTT demasiado chico | Aumentar el buffer (setBufferSize) o reducir el mensaje |
| El dispositivo aparece «conectado» aunque esté apagado | Sin latido ni última voluntad | Configurar keep alive y la última voluntad retenida |
| Un tablero nuevo no ve el estado actual hasta que cambia | Los mensajes no se publicaron como retenidos | Publicar el estado con RETAIN |
14 · Comprensión
Confusiones frecuentes
«HTTP es solo para páginas web»
Es un protocolo genérico de pedido y respuesta. Hoy la mayoría de las APIs (de servicios en la nube, de dispositivos, de aplicaciones móviles) usan HTTP con JSON.
«Un 200 significa que mi dato se procesó bien»
Significa que el servidor aceptó la petición. Si se procesa luego (202), o si la aplicación devuelve un error dentro del cuerpo con un 200, hay que mirar también el cuerpo. Conviene usar el código de estado correcto en el servidor.
«Con MQTT los dispositivos se hablan entre sí»
Nunca: cada uno habla con el broker. Dos dispositivos no se conocen ni se necesitan; se comunican a través de los temas.
«El broker entiende los datos que lleva»
Solo mira el tema (y los metadatos). Los datos son bytes opacos: el formato lo acuerdan publicador y suscriptor.
«QoS 1 garantiza que el mensaje llega una sola vez»
Garantiza que llega al menos una vez; puede duplicarse. Para evitar duplicados hace falta QoS 2, o diseñar los mensajes para que repetirlos sea inofensivo (idempotentes).
«El QoS es entre el publicador y el suscriptor»
Se negocia por tramos: publicador a broker, y broker a cada suscriptor. El efectivo es el menor de los dos.
«Los comodines sirven para publicar»
Solo para suscribirse. Un mensaje se publica siempre en un tema concreto.
«Un broker sin contraseña en mi red está bien si el router no abre puertos»
Quizá, pero cualquier equipo de la red local puede leer y comandar todo, y un error de configuración del router lo expone a Internet. Autenticación y permisos por tema son la protección mínima.
15 · Práctica guiada
Predecí antes de comprobar
1. Content-Length
Un dispositivo envía por POST el cuerpo {"t":21.5,"h":60}. ¿Qué valor lleva Content-Length?
Ver solución
Se cuentan los caracteres: {"t":21.5,"h":60} son 1 + 3 + 1 + 4 + 1 + 3 + 1 + 2 + 1 = 17 bytes (llave, "t", dos puntos, 21.5, coma, "h", dos puntos, 60, llave). Content-Length: 17.
2. Qué hacer con un código
¿Qué debe hacer el dispositivo si recibe un 400, un 404, un 429 y un 503?
Ver solución
400: el pedido está mal formado; reintentarlo igual dará lo mismo, hay que corregirlo. 404: la ruta no existe; revisar la dirección. 429: demasiadas peticiones; esperar (hasta el tiempo de Retry-After) antes de volver a enviar. 503: servidor sobrecargado o en mantenimiento; reintentar con esperas crecientes.
3. Un JSON inválido
¿Qué está mal en {'id': "n1", "t": 23,5, }?
Ver solución
Tres errores: la clave 'id' usa comillas simples (tienen que ser dobles), 23,5 usa coma decimal (debe ser 23.5, y la coma separaría campos) y la coma final antes de la llave no está permitida.
4. Largo restante
Un PUBLISH con QoS 0 al tema a/b (3 caracteres) con 5 bytes de datos, ¿cuántos bytes mide y cuál es su largo restante?
Ver solución
Largo restante = 2 (largo del tema) + 3 (tema) + 5 (datos) = 10, que cabe en un byte (0A). Total = 1 (primer byte) + 1 (largo restante) + 10 = 12 bytes: 30 0A 00 03 61 2F 62 ….
5. Comodines
Un tablero se suscribe a planta/+/motor/temperatura. ¿Cuáles de estos temas recibe: planta/l1/motor/temperatura, planta/l1/l2/motor/temperatura, planta/motor/temperatura?
Ver solución
Solo el primero. El + reemplaza exactamente un nivel: el segundo tiene dos niveles en ese lugar y el tercero ninguno.
6. Elegir el QoS
Elegí un QoS para: (a) una temperatura enviada cada 2 segundos; (b) el comando «abrir la válvula»; (c) un contador de piezas producidas que se suma en el servidor.
Ver solución
(a) QoS 0: un valor perdido se compensa con el siguiente. (b) QoS 1: tiene que llegar, y abrir una válvula ya abierta no cambia nada (idempotente). (c) QoS 2: un duplicado haría sumar de más; también se podría diseñar el mensaje para que lleve el total acumulado y no el incremento, y usar QoS 1.
7. El estado de un nodo
¿Cómo se logra que cualquier tablero sepa en todo momento si un sensor está conectado?
Ver solución
Con mensaje retenido y última voluntad en el mismo tema de estado: al conectar, el sensor publica conectado con RETAIN; en el CONNECT declara como última voluntad desconectado con RETAIN. Si cae de forma anormal, el broker publica la última voluntad (detectada por el latido), y el tablero que se suscriba, ahora o después, recibe el último estado.
16 · Síntesis
Ideas para recordar
- Un protocolo de aplicación define formato, diálogo, direccionamiento y garantías sobre TCP. HTTP es de pedido y respuesta; MQTT es de publicar y suscribirse con un broker.
- HTTP son mensajes de texto: línea inicial, encabezados (Host, Content-Type, Content-Length), línea en blanco y cuerpo, con CR LF. Los métodos (GET, POST, PUT, DELETE) expresan la acción; el código de estado dice cómo salió: 2xx éxito, 3xx redirección, 4xx falla del cliente (no repetir igual), 5xx falla del servidor (reintentar con espera creciente).
- JSON representa datos con objetos, arreglos, textos, números, true/false y null, con reglas estrictas (comillas dobles, sin comas finales). Es legible y universal, pero voluminoso; hay alternativas binarias.
- En HTTP el servidor no puede avisar: el dispositivo tiene que preguntar (sondeo). Conviene usar tiempos límite, reintentos con espera creciente, mensajes chicos y TLS fuera de la red local.
- MQTT desacopla a los participantes: cada dispositivo abre una conexión TCP con el broker, publica en temas jerárquicos y se suscribe con filtros (+ un nivel, # el resto). Como la conexión sale del dispositivo, atraviesa NAT y permite recibir comandos al instante.
- Los paquetes MQTT son binarios y mínimos: primer byte con el tipo, largo restante de 7 bits por byte, tema, identificador (si QoS > 0) y datos. Publicar 4 bytes de dato en un tema de 21 caracteres son 29 bytes.
- QoS 0 (como máximo una vez, 1 paquete), QoS 1 (al menos una vez, 2 paquetes, posibles duplicados) y QoS 2 (exactamente una vez, 4 paquetes). Se negocia por tramo y vale el menor. Retenido guarda el último mensaje del tema, la última voluntad avisa de una caída anormal, y el latido la detecta.
- HTTP para configurar, integrar con servicios web y enviar de vez en cuando; MQTT para telemetría continua, comandos y muchos dispositivos. Ambos van en claro por omisión: usar TLS (443 y 8883), credenciales por dispositivo y permisos por tema, y no exponer un broker sin autenticación.
¿Listo para el siguiente tema?
Antes de avanzar, calculá el Content-Length de {"v":7} (7 bytes); decidí si casa/+/luz coincide con casa/sala/luz (sí) y con casa/sala/techo/luz (no); y explicá por qué un dispositivo detrás de NAT puede recibir comandos por MQTT pero no por un servidor HTTP propio (en MQTT la conexión con el broker la abre el propio dispositivo hacia afuera). Si podés justificar las tres respuestas, tenés los fundamentos de las aplicaciones de red para dispositivos.
En el próximo y último taller de esta parte integramos todo: un sensor en la red con Ethernet, con un módulo W5500, un servidor web de configuración, un cliente MQTT y el envío periódico de lecturas.