Un Arduino Uno o Mega con Ethernet Shield sigue siendo una de las formas más fiables de llevar un sensor o un relé a MQTT: sin Wi-Fi que se caiga, sin interferencias de radio y con un enlace cableado que simplemente funciona dentro de un armario o en un sótano. La pila estándar es la librería Ethernet para el chip de WIZnet más PubSubClient para MQTT.
Este tutorial conecta un shield W5100 o W5500 a un broker, publica el valor de un sensor y acepta comandos, con un bucle de reconexión que sobrevive a cables desenchufados y reinicios del broker. Después lo verificas de extremo a extremo desde el navegador.
Hardware y librerías
| Componente | Notas |
|---|---|
| Arduino Uno R3 o Mega 2560 | Uno: 2 KB de RAM, Mega: 8 KB de RAM |
| Ethernet Shield W5100 o Ethernet Shield 2 (W5500) | Usa SPI, chip select en el pin 10 |
| Cable de red a un router con DHCP | O una IP estática en tu LAN |
Instala dos librerías desde el Library Manager: Ethernet (de Arduino, soporta W5100, W5200 y W5500) y PubSubClient (de Nick O'Leary). La mayoría de shields también llevan una ranura microSD cuyo chip select es el pin 4; si no usas tarjeta, pon el pin 4 en alto para que no interfiera en el bus SPI.
El sketch completo
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
// Usa una MAC única por placa en tu red
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x10, 0x01 };
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* CLIENT_ID = "arduino-eth-0001";
const char* MQTT_USER = "username";
const char* MQTT_PASS = "password";
const char* T_STATUS = "testmqtt/arduino-eth/0001/status";
const char* T_TEMP = "testmqtt/arduino-eth/0001/temp";
const char* T_CMD = "testmqtt/arduino-eth/0001/cmd/led";
EthernetClient ethClient;
PubSubClient mqtt(ethClient);
unsigned long lastReconnect = 0;
unsigned long lastPublish = 0;
void onMessage(char* topic, byte* payload, unsigned int length) {
Serial.print(F("<- "));
Serial.print(topic);
Serial.print(F(" = "));
Serial.write(payload, length);
Serial.println();
if (strcmp(topic, T_CMD) == 0 && length > 0) {
digitalWrite(LED_BUILTIN, payload[0] == '1' ? HIGH : LOW);
}
}
bool connectMqtt() {
Serial.print(F("MQTT connecting... "));
// id, user, pass, willTopic, willQos, willRetain, willMessage
if (mqtt.connect(CLIENT_ID, MQTT_USER, MQTT_PASS, T_STATUS, 1, true, "offline")) {
Serial.println(F("connected"));
mqtt.publish(T_STATUS, "online", true);
mqtt.subscribe(T_CMD, 1);
return true;
}
Serial.print(F("failed, rc="));
Serial.println(mqtt.state());
return false;
}
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(4, OUTPUT);
digitalWrite(4, HIGH); // deselecciona la tarjeta SD
Serial.begin(9600);
Ethernet.init(10); // chip select del W5100/W5500
Serial.println(F("DHCP..."));
if (Ethernet.begin(mac) == 0) {
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println(F("No Ethernet shield found"));
} else {
Serial.println(F("DHCP failed - check the cable or use a static IP"));
}
while (true) delay(1000);
}
Serial.print(F("IP: "));
Serial.println(Ethernet.localIP());
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
}
void loop() {
if (!mqtt.connected()) {
unsigned long now = millis();
if (now - lastReconnect > 5000) { // reintenta cada 5 s sin bloquear
lastReconnect = now;
connectMqtt();
}
} else {
mqtt.loop();
}
if (mqtt.connected() && millis() - lastPublish > 10000) {
lastPublish = millis();
char buf[8];
int raw = analogRead(A0);
itoa(raw, buf, 10);
mqtt.publish(T_TEMP, buf);
}
Ethernet.maintain(); // renueva la concesión DHCP
}Algunas decisiones de este sketch importan más de lo que parece:
- Las cadenas
F("...")guardan el texto constante en flash en lugar de en RAM. En un Uno, una docena de cadenas de depuración pueden comerse una cuarta parte de la memoria. - La reconexión no bloquea.
loop()sigue ejecutándose entre intentos, así la lógica local (botones, relés, un watchdog) no se congela mientras el broker es inalcanzable. mqtt.loop()debe ejecutarse a menudo. Lee los paquetes entrantes y envía el PINGREQ del keep alive. Undelay()largo en tu código hace que el broker corte la conexión tras 1,5 × keep alive.- Un Last Will y un
onlineretained dan a cualquier dashboard un indicador fiable de online/offline. Consulta MQTT Last Will.
IP estática en lugar de DHCP
Las redes industriales a menudo no tienen DHCP. Pasa explícitamente la dirección, el servidor DNS, la puerta de enlace y la máscara; sin un DNS no se puede resolver el hostname broker.connectmq.com:
IPAddress ip(192, 168, 1, 50);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
Ethernet.begin(mac, ip, dns, gateway, subnet);Si no hay DNS disponible, mqtt.setServer() también acepta una IPAddress, pero entonces un cambio de IP del broker rompe todos los dispositivos. Usa un hostname siempre que la red lo permita.
Pruébalo de extremo a extremo
- Abre el Monitor Serie a 9600 baudios. Deberías ver una dirección IP y después
MQTT connecting... connected. - Abre el cliente MQTT online, conéctate a
broker.connectmq.compor WebSocket seguro (puerto 8084, ruta/mqtt) y suscríbete atestmqtt/arduino-eth/0001/#. - El
status = onlineretained llega de inmediato, seguido de un valortempcada diez segundos. - Publica
1entestmqtt/arduino-eth/0001/cmd/led. El LED de la placa se enciende y el Monitor Serie imprime el mensaje. Publica0para apagarlo. - Desenchufa el cable de red. Tras unos 45 segundos el broker envía el will y el estado cambia a
offline. Vuelve a enchufarlo: el sketch se reconecta en pocos segundos y publicaonlinede nuevo.
Observa el Arduino en el cliente online
Si prefieres probar desde un terminal, mosquitto_pub y mosquitto_sub hacen lo mismo: mosquitto_sub -h broker.connectmq.com -t 'testmqtt/arduino-eth/#' -v.
Interpretar mqtt.state()
Cuando connect() falla, state() te dice por qué. Estos son los códigos que define PubSubClient:
| Código | Constante | Significado |
|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT | Sin respuesta dentro del keep alive |
| -3 | MQTT_CONNECTION_LOST | Se rompió la conexión de red |
| -2 | MQTT_CONNECT_FAILED | Falló la conexión TCP (DNS, IP, puerto, firewall) |
| -1 | MQTT_DISCONNECTED | Desconexión limpia |
| 1 | MQTT_CONNECT_BAD_PROTOCOL | El broker rechazó la versión del protocolo |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID | Client ID rechazado |
| 3 | MQTT_CONNECT_UNAVAILABLE | Broker no disponible |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS | Usuario o contraseña incorrectos |
| 5 | MQTT_CONNECT_UNAUTHORIZED | No autorizado |
failed, rc=-2 es con diferencia el más común: el shield nunca llegó al broker por TCP. Comprueba que Ethernet.localIP() no sea 0.0.0.0, que el servidor DNS sea accesible y que el puerto 1883 saliente esté permitido. La guía de problemas de PubSubClient repasa cada código en detalle.
Límites de memoria y de tamaño de paquete
PubSubClient reserva un único buffer para los paquetes entrantes y salientes. Su tamaño por defecto es de 256 bytes, e incluye la cabecera fija, el topic y el payload. Un paquete mayor que el buffer se descarta en silencio al recibirlo y publish() devuelve false al enviarlo.
mqtt.setBufferSize(512); // llamar una vez en setup(), antes de connect()En un Uno, mantén el buffer moderado y los topics cortos. Puedes calcular exactamente el tamaño de un paquete con la calculadora de tamaño de paquetes. En un Mega, buffers de 1–2 KB son realistas. Si un payload grande sigue fallando, la guía de paquete demasiado grande cubre también los límites del broker.
Otros límites que debes tener en cuenta:
- Solo publicaciones QoS 0. PubSubClient publica con QoS 0 y se suscribe con QoS 0 o 1. Usa un topic de estado retained o una confirmación de aplicación cuando la entrega importe.
- Solo MQTT 3.1.1. Sin reason codes ni propiedades de MQTT 5.
- Número de sockets. El W5100 tiene 4 sockets y el W5500 tiene 8. DHCP y DNS usan uno brevemente, así que un sketch que además sirva una web puede quedarse sin ellos.
Cuándo ir más allá del Uno
La combinación Uno + Ethernet es perfecta para dispositivos locales, simples y cableados. Si necesitas TLS, payloads JSON más grandes o funciones de MQTT 5, el mismo código de PubSubClient se porta casi sin cambios a un ESP32 con W5500 o a placas Wi-Fi: consulta ESP32 con Arduino y MQTT. Si quieres publicar con QoS 1 en AVR, merece la pena echar un vistazo a la librería arduino-mqtt.
Un Arduino cableado con MQTT es sobre todo cuestión de disciplina: MAC y client ID únicos, cadenas F(), una reconexión no bloqueante y un bucle que nunca se detiene. Consigue que el log serie diga connected, demuestra el publish, los comandos y el Last Will desde el cliente online, y la placa funcionará sin tocarla durante años.
Preguntas frecuentes
¿Puede un Arduino Uno usar MQTT sobre TLS?
En la práctica, no. El ATmega328P tiene 2 KB de RAM, muy poco para un handshake TLS, y los chips W5100/W5500 no hacen TLS por sí mismos. Usa MQTT sin cifrar en una red de confianza, un gateway local que reenvíe por TLS, o pasa a un ESP32 o una placa con más memoria.
¿Por qué PubSubClient no recibe mis mensajes más largos?
PubSubClient descarta cualquier paquete mayor que su buffer, que por defecto es de 256 bytes incluyendo topic y cabeceras. Llama a setBufferSize() con un valor mayor, teniendo en cuenta que en un Uno cada byte sale de los 2 KB de RAM.
¿PubSubClient permite publicar con QoS 1?
No. PubSubClient publica solo con QoS 0 y soporta MQTT 3.1.1. Puede suscribirse con QoS 0 o 1. Si necesitas publicaciones QoS 1, añade una confirmación a nivel de aplicación o usa otra librería.

