IoT industrial

Report by exception (RBE) en sistemas MQTT: bandas muertas, heartbeats y un ejemplo de gateway

Report by exception publica un valor solo cuando cambia de forma significativa. Aprende cómo trabajan juntos las bandas muertas, los límites de frecuencia y los heartbeats, y ejecuta un gateway en Python que aplica RBE antes de publicar en MQTT.

Gráfico que compara las muestras sondeadas con las publicaciones de report by exception tras un filtro de banda muerta en un gateway MQTT
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

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

  1. Parte de la precisión del instrumento. Una banda muerta menor que la precisión del sensor solo reenvía ruido.
  2. Pregunta al consumidor qué resolución necesita. Un panel que muestra grados enteros no necesita cambios de 0,01 grados.
  3. Usa banda muerta cero para valores discretos: estados, modos, alarmas, contadores y consignas.
  4. 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.
  5. 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

  1. Sin heartbeat. Los consumidores no pueden distinguir un valor plano de un gateway muerto.
  2. Comparar con la muestra anterior en lugar de con el último valor publicado, lo que deja pasar la deriva sin que se note.
  3. Bandas muertas en señales discretas. Un cambio de estado de 1 a 2 nunca debe filtrarse.
  4. No retener los valores. Los nuevos suscriptores no ven nada hasta el siguiente cambio.
  5. 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.

Artículos relacionados

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.