Report by exception (RBE) es la práctica de publicar un valor solo cuando cambia en una cantidad significativa, en lugar de enviar cada muestra sondeada. Es una de las técnicas más sencillas y eficaces en los sistemas MQTT industriales. En este artículo aprenderás cómo encajan las bandas muertas, los intervalos mínimos y los heartbeats, y ejecutarás un gateway en Python que aplica RBE antes de publicar en un broker.
Sondeo frente a report by exception
Los SCADA tradicionales y muchas integraciones Modbus u OPC UA funcionan por sondeo: cada tag se lee en un ciclo fijo y cada resultado se reenvía. Es sencillo, pero la mayoría de los valores industriales cambian despacio. Una temperatura de depósito leída cada segundo durante una hora genera 3.600 mensajes que quizá contengan solo un puñado de cambios significativos.
MQTT está diseñado para el estilo contrario. Los clientes publican cuando ocurre algo y el broker entrega el nuevo valor a todos los suscriptores. RBE combina ambos: el gateway sigue sondeando el dispositivo en local (porque el protocolo del dispositivo lo exige), pero solo publica hacia arriba cuando el valor ha cambiado lo suficiente como para importar.
| Enfoque | Mensajes enviados | Actualidad | Riesgo típico |
|---|---|---|---|
| Publicación a intervalo fijo | Cada muestra | Predecible | Ancho de banda, almacenamiento y procesamiento desperdiciados |
| Report by exception puro | Solo al cambiar | Inmediata para los cambios | Un valor en silencio parece igual que un dispositivo muerto |
| RBE con heartbeat | Cambios más refresco periódico | Inmediata, con antigüedad acotada | Requiere un poco más de lógica |
La tercera fila es lo que quieres en la práctica.
Los componentes de report by exception
Banda muerta
Una banda muerta (deadband) es el cambio mínimo que cuenta como "distinto". Filtra el ruido del sensor y las derivas insignificantes.
- Banda muerta absoluta: publicar si la diferencia absoluta entre el nuevo valor y el último valor publicado es al menos, por ejemplo, 0,5 grados.
- Banda muerta porcentual: publicar si el cambio supera un porcentaje del último valor o, de forma más robusta, del rango del instrumento. El porcentaje sobre el rango es más seguro cerca de cero, donde un porcentaje del valor haría que fluctuaciones minúsculas parecieran enormes.
Compara siempre con el último valor publicado, no con el último valor muestreado. Comparar con la muestra anterior permite que una deriva lenta se cuele sin que nadie lo note para siempre, porque cada paso queda por debajo de la banda muerta.
Intervalo mínimo (límite de frecuencia)
Una señal ruidosa u oscilante puede cruzar la banda muerta repetidamente. Un intervalo mínimo, por ejemplo un segundo por tag, limita la frecuencia. Ajústalo con cuidado para alarmas y cambios de estado: normalmente los quieres de inmediato, así que dales un intervalo mínimo de cero.
Heartbeat (intervalo máximo)
Si un valor nunca cambia, RBE nunca lo publica, y los consumidores no pueden distinguir "estable" de "gateway muerto". Un heartbeat publica el valor actual tras un silencio máximo, por ejemplo cada cinco minutos, aunque no haya cambio. Combinado con un Last Will de MQTT en el topic de estado del gateway, los suscriptores siempre pueden distinguir un valor en calma de una conexión perdida.
Cambios de calidad
Un cambio en la calidad del dato (una lectura de sensor que falla, un dispositivo que se desconecta) debe publicarse siempre, independientemente de la banda muerta. Un valor "GOOD" obsoleto que en realidad procede de un dispositivo averiado es peor que no tener valor.
Cómo elegir las bandas muertas: pautas prácticas
- Parte de la precisión del instrumento. Una banda muerta menor que la precisión del sensor solo reenvía ruido.
- Pregunta al consumidor qué resolución necesita. Un panel que muestra grados enteros no necesita cambios de 0,01 grados.
- Usa banda muerta cero para valores discretos: estados, modos, alarmas, contadores y consignas.
- Ten cuidado con los totalizadores y los contadores de energía. Usa una banda muerta en unidades del contador y un heartbeat regular para que los cálculos de tipo facturación sigan siendo exactos.
- Revisa las bandas muertas tras la puesta en marcha comparando la frecuencia de mensajes por tag; unos pocos tags muy habladores suelen dominar el tráfico.
Un gateway en Python con report by exception
Este ejemplo simula cinco tags leídos cada 500 ms, aplica por tag reglas de banda muerta, intervalo mínimo y heartbeat, y publica los cambios como mensajes JSON retenidos en broker.connectmq.com, el servidor MQTT público gratuito pensado para pruebas y prototipos, no para datos de producción. Muestra cuántas muestras se leyeron frente a cuántos mensajes se enviaron, para que veas el efecto directamente. Usa paho-mqtt 2.x y Python 3.10 o superior.
import json
import random
import time
from dataclasses import dataclass
from datetime import datetime, timezone
import paho.mqtt.client as mqtt
BROKER = "broker.connectmq.com"
PORT = 1883
CLIENT_ID = "rbe-gateway-6e41"
BASE = "connectmq/demo/rbe-6e41/plant1/boiler1"
POLL_S = 0.5
@dataclass
class Tag:
name: str
deadband: float # absoluta, en unidades de ingeniería (0 = cualquier cambio)
min_interval: float = 1.0 # segundos entre publicaciones
heartbeat: float = 300.0 # segundos, publica aunque no haya cambio
last_value: float | None = None
last_quality: str | None = None
last_ts: float = 0.0
def should_publish(self, value, quality, now):
if self.last_value is None or quality != self.last_quality:
return True # primer valor o cambio de calidad
if now - self.last_ts >= self.heartbeat:
return True # heartbeat
if now - self.last_ts < self.min_interval:
return False # límite de frecuencia
if self.deadband == 0:
return value != self.last_value
return abs(value - self.last_value) >= self.deadband
def mark(self, value, quality, now):
self.last_value, self.last_quality, self.last_ts = value, quality, now
TAGS = {
"temperature_c": Tag("temperature_c", deadband=0.5),
"pressure_bar": Tag("pressure_bar", deadband=0.05),
"flow_m3h": Tag("flow_m3h", deadband=0.2),
"burner_on": Tag("burner_on", deadband=0, min_interval=0),
"alarm_code": Tag("alarm_code", deadband=0, min_interval=0),
}
state = {"temperature_c": 82.0, "pressure_bar": 3.20, "flow_m3h": 12.0,
"burner_on": 1, "alarm_code": 0}
def poll_device():
"""Simula una lectura del dispositivo: poco ruido más cambios reales ocasionales."""
state["temperature_c"] += random.gauss(0, 0.08)
state["pressure_bar"] += random.gauss(0, 0.008)
state["flow_m3h"] += random.gauss(0, 0.03)
if random.random() < 0.005:
state["burner_on"] = 1 - state["burner_on"]
quality = "BAD" if random.random() < 0.002 else "GOOD"
return {k: round(v, 3) for k, v in state.items()}, quality
def on_connect(client, userdata, flags, reason_code, properties):
print("Connected:", reason_code)
client.publish(f"{BASE}/status", "online", qos=1, retain=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
client.on_connect = on_connect
client.will_set(f"{BASE}/status", "offline", qos=1, retain=True)
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()
samples = sent = 0
started = time.monotonic()
try:
while True:
values, quality = poll_device()
now = time.monotonic()
for name, value in values.items():
samples += 1
tag = TAGS[name]
if tag.should_publish(value, quality, now):
payload = {
"v": value,
"q": quality,
"ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
}
client.publish(f"{BASE}/{name}", json.dumps(payload), qos=1, retain=True)
tag.mark(value, quality, now)
sent += 1
if now - started >= 60:
print(f"last 60 s: {samples} samples read, {sent} messages published "
f"({100 * sent / samples:.1f} %)")
samples = sent = 0
started = now
time.sleep(POLL_S)
except KeyboardInterrupt:
client.loop_stop()
client.disconnect()
Observa el resultado:
mosquitto_sub -h broker.connectmq.com -p 1883 -i rbe-6e41-viewer -t "connectmq/demo/rbe-6e41/#" -v
La línea de resumen que aparece cada minuto muestra qué parte de los datos sondeados hacía falta enviar realmente. Con bandas muertas realistas, la reducción es grande, y los mensajes que quedan son los que aportan información. Ajusta las bandas muertas y observa cómo cambia la proporción.
Por qué importan los mensajes retenidos con report by exception
Cuando los valores solo se publican al cambiar, un suscriptor que se conecta ahora podría esperar minutos hasta la siguiente actualización. Publicar cada tag como retained message lo resuelve: el broker guarda el último valor de cada topic y lo entrega de inmediato a cada nuevo suscriptor. Para esto funciona mejor un topic por tag que un gran bloque JSON, porque cada tag conserva su propio último valor retenido. Si necesitas repasar las garantías de entrega, consulta los niveles de QoS de MQTT explicados.
RBE en Sparkplug B
Report by exception forma parte de la especificación Sparkplug B. Los nodos edge envían un certificado de nacimiento (birth certificate) completo con todas las métricas al conectarse y después solo envían las métricas modificadas en los mensajes DATA. Si una aplicación host pierde el hilo, solicita un rebirth. Eso da a Sparkplug el mismo comportamiento de "estado completo al conectar, cambios después" que los retained messages más RBE en MQTT plano. ConnectMQ puede decodificar payloads Sparkplug B y volver a publicarlos en topics ISA-95 legibles, de modo que el tráfico Sparkplug optimizado con RBE sigue siendo fácil de inspeccionar.
Dónde más rinde RBE
- Enlaces medidos o limitados. En conexiones celulares y por satélite, cada byte cuenta. RBE suele ser la primera optimización que se aplica, como se describe en MQTT sobre red celular.
- Búferes store-and-forward. Menos mensajes significan que una caída llena el búfer más despacio y se reenvía más rápido; consulta store and forward para MQTT.
- Historiadores y almacenamiento. Los datos basados en cambios se comprimen bien y siguen permitiendo reconstruir la señal dentro de la banda muerta.
- Cuotas del broker. Todos los brokers aplican límites de frecuencia de mensajes y de conexiones. En el área de pruebas pública, por ejemplo, cada cliente puede enviar 10 mensajes por segundo.
RBE suele implementarse en un gateway edge IIoT, junto a los drivers de protocolo. Para la visión arquitectónica general, consulta la guía de IoT industrial con MQTT.
Errores habituales
- Sin heartbeat. Los consumidores no pueden distinguir un valor plano de un gateway muerto.
- Comparar con la muestra anterior en lugar de con el último valor publicado, lo que deja pasar la deriva sin que se note.
- Bandas muertas en señales discretas. Un cambio de estado de 1 a 2 nunca debe filtrarse.
- No retener los valores. Los nuevos suscriptores no ven nada hasta el siguiente cambio.
- Una misma banda muerta para todos los tags. Ajústala por tag, según la precisión y las necesidades del consumidor.
Publica de forma más inteligente con ConnectMQ
RBE reduce lo que envías; un broker fiable se asegura de que lo que envías llegue. ConnectMQ es un broker MQTT gestionado con retained messages, MQTT 5, TLS y un explorador de topics en vivo para revisar el tráfico por tag, y puedes probar antes el ejemplo anterior en el servidor público broker.connectmq.com. Cuando estés listo, crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito.
Preguntas frecuentes
¿Qué es report by exception en pocas palabras?
Significa enviar un valor solo cuando cambia de forma significativa, en lugar de en cada lectura. Una banda muerta define qué es "significativo" y un heartbeat garantiza que el valor se siga enviando de vez en cuando.
¿Se pierden datos con report by exception?
Por diseño descarta los cambios menores que la banda muerta. Elige las bandas muertas según la precisión del sensor y lo que realmente necesitan los consumidores, y conserva los datos en bruto en local si los ingenieros pueden necesitar la resolución completa más adelante.
¿Cuál es un buen intervalo de heartbeat?
Lo habitual es entre uno y quince minutos. Debe ser más corto que el tiempo a partir del cual un consumidor consideraría obsoleto un valor.
¿Deben las alarmas usar report by exception?
Sí, con banda muerta cero y sin intervalo mínimo, para que cada transición se publique de inmediato.



