El servidor MQTT de The Things Network ofrece a cada aplicación un flujo en directo de sus uplinks LoRaWAN, lo que facilita incorporar datos de sensores de largo alcance a tus propios sistemas. En este tutorial escribirás un payload formatter, te conectarás al servidor MQTT de TTN con una API key y ejecutarás un pequeño puente en Python que republica las lecturas decodificadas como JSON limpio en el árbol de topics de tu propio broker, donde los paneles, Node-RED y las automatizaciones pueden usarlas junto a tus demás dispositivos.
Por qué llevar TTN a tu propio broker
The Things Network (TTN) se encarga de la parte de radio de LoRaWAN: gateways, activación de dispositivos, deduplicación y descifrado. Su servidor MQTT publica cada uplink como un gran documento JSON que contiene metadatos de cada gateway que escuchó el paquete. Eso es excelente para depurar, pero incómodo como modelo de datos a largo plazo:
- los topics son los de TTN (
v3/<app>@ttn/devices/<device>/up), no la jerarquía de tu instalación; - los payloads son grandes y anidados, mientras que la mayoría de los consumidores solo quieren tres o cuatro valores;
- tus dispositivos Wi-Fi, PLC y controladores ya viven en otro broker.
Un puente resuelve los tres problemas. Se suscribe a TTN, extrae lo importante y publica mensajes compactos en el mismo árbol de topics que todo lo demás. Los consumidores nunca necesitan credenciales de TTN y, si más adelante pasas a un servidor de red privado, solo cambia el puente. La guía de IoT para agricultura inteligente explica dónde encaja esto en un diseño para toda la finca, del campo agroindustrial al invernadero.
Paso 1: decodificar el payload en TTN
Los dispositivos LoRaWAN envían bytes compactos. Añade un payload formatter en la consola de TTN en Applications > your app > Payload formatters > Uplink, tipo Custom Javascript formatter. Este ejemplo decodifica un nodo de suelo que envía humedad (uint16, décimas de porcentaje), temperatura del suelo (int16, décimas de grado) y batería (uint16, milivoltios):
function decodeUplink(input) {
var b = input.bytes;
if (input.fPort !== 1 || b.length < 6) {
return { errors: ["unexpected port or length"] };
}
var moisture = ((b[0] << 8) | b[1]) / 10;
var tRaw = (b[2] << 8) | b[3];
if (tRaw & 0x8000) tRaw -= 0x10000;
var battery = ((b[4] << 8) | b[5]) / 1000;
return {
data: {
moisture_pct: moisture,
soil_temp_c: tRaw / 10,
battery_v: battery
},
warnings: [],
errors: []
};
}
Usa el panel Test con un payload hexadecimal como 0136 008C 0E74 para confirmar la salida antes de desplegar cualquier dispositivo. El objeto decodificado aparece en cada uplink como uplink_message.decoded_payload.
Paso 2: obtener las credenciales MQTT de TTN
En la consola, abre Integrations > MQTT. Verás:
| Ajuste | Valor |
|---|---|
| Servidor | Tu clúster, por ejemplo eu1.cloud.thethings.network |
| Puertos | 1883 (sin cifrar) y 8883 (TLS) |
| Usuario | <application-id>@ttn |
| Contraseña | Una API key; haz clic en Generate new API key |
Copia la API key de inmediato; TTN solo la muestra una vez. Usa siempre el puerto 8883 con TTN, ya que la clave da acceso a los datos de tu aplicación. La sección sobre el servidor MQTT de la documentación oficial de The Things Stack enumera el conjunto completo de topics, incluidos los eventos de join, downlink y servicio.
El topic de uplink para una aplicación es:
v3/<application-id>@ttn/devices/<device-id>/up
Puedes comprobar el flujo antes de escribir código:
mosquitto_sub -h eu1.cloud.thethings.network -p 8883 --capath /etc/ssl/certs \
-u 'my-farm-app@ttn' -P 'NNSXS.YOUR_TTN_API_KEY' \
-t 'v3/my-farm-app@ttn/devices/+/up' -v
Paso 3: el puente MQTT de The Things Network en Python
El puente usa dos clientes paho-mqtt 2.x: uno suscrito a TTN y otro que publica en tu broker. Ejecútalo primero contra el servidor MQTT público gratuito, pensado para pruebas y no para datos de producción, sustituyendo t6n1b4 por tu propia cadena aleatoria, porque cualquiera puede leer el área pública.
import json
import os
import ssl
import paho.mqtt.client as mqtt
TTN_HOST = "eu1.cloud.thethings.network"
TTN_APP = os.environ.get("TTN_APP", "my-farm-app")
TTN_KEY = os.environ["TTN_API_KEY"]
OUT_HOST = os.environ.get("OUT_HOST", "broker.connectmq.com")
OUT_PORT = int(os.environ.get("OUT_PORT", "1883"))
OUT_USER = os.environ.get("OUT_USER") # definir en producción
OUT_PASS = os.environ.get("OUT_PASS")
OUT_CLIENT_ID = os.environ.get("OUT_CLIENT_ID", "ttn-bridge-out-t6n1b4")
OUT_PREFIX = os.environ.get("OUT_PREFIX", "connectmq/demo/t6n1b4/farm")
# --- broker de salida --------------------------------------------------------
out = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=OUT_CLIENT_ID)
if OUT_USER:
out.username_pw_set(OUT_USER, OUT_PASS)
if OUT_PORT == 8883:
out.tls_set(cert_reqs=ssl.CERT_REQUIRED)
out.will_set(f"{OUT_PREFIX}/bridge/status", "offline", qos=1, retain=True)
def out_on_connect(client, userdata, flags, reason_code, properties):
print("out broker:", reason_code)
client.publish(f"{OUT_PREFIX}/bridge/status", "online", qos=1, retain=True)
out.on_connect = out_on_connect
out.connect(OUT_HOST, OUT_PORT, keepalive=60)
out.loop_start()
# --- TTN ---------------------------------------------------------------------
def ttn_on_connect(client, userdata, flags, reason_code, properties):
print("ttn:", reason_code)
client.subscribe(f"v3/{TTN_APP}@ttn/devices/+/up", qos=1)
def ttn_on_message(client, userdata, msg):
try:
up = json.loads(msg.payload)
device = up["end_device_ids"]["device_id"]
um = up["uplink_message"]
decoded = um.get("decoded_payload")
except (ValueError, KeyError) as exc:
print("skipping malformed uplink:", exc)
return
if not decoded:
print(f"{device}: no decoded payload (check the formatter)")
return
rx = um.get("rx_metadata", [])
best = max(rx, key=lambda g: g.get("rssi", -999)) if rx else {}
record = {
"ts": up.get("received_at"),
**decoded,
"rssi": best.get("rssi"),
"snr": best.get("snr"),
"gateways": len(rx),
"f_cnt": um.get("f_cnt"),
}
topic = f"{OUT_PREFIX}/{device}/telemetry"
out.publish(topic, json.dumps(record), qos=1, retain=True)
print(topic, record)
ttn = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id=f"ttn-bridge-in-{TTN_APP}")
ttn.username_pw_set(f"{TTN_APP}@ttn", TTN_KEY)
ttn.tls_set(cert_reqs=ssl.CERT_REQUIRED)
ttn.on_connect = ttn_on_connect
ttn.on_message = ttn_on_message
ttn.connect(TTN_HOST, 8883, keepalive=60)
ttn.loop_forever()
Ejecútalo:
pip install paho-mqtt
export TTN_API_KEY='NNSXS.YOUR_TTN_API_KEY'
python ttn_bridge.py
Y observa la salida limpia desde otro terminal:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-t6n1b4 \
-t 'connectmq/demo/t6n1b4/farm/#' -v
Cada uplink se convierte en un mensaje retenido como {"ts":"2026-10-16T06:15:02Z","moisture_pct":31.0,"soil_temp_c":14.0,"battery_v":3.7,"rssi":-108,"snr":4.5,"gateways":2,"f_cnt":412}. Como es retenido, un panel que arranque más tarde ve de inmediato la última lectura de cada sensor, algo importante en dispositivos que solo informan cada 15 minutos.
Notas de diseño
¿Por qué incluir RSSI, SNR y el número de gateways? Son el primer aviso de una antena averiada, un gateway que se ha movido o un sensor que se hunde en el barro. Almacenarlos no cuesta casi nada.
¿Por qué mensajes retenidos? Los dispositivos LoRaWAN permanecen en silencio la mayor parte del tiempo. Sin retain, cualquier consumidor que se conecte entre uplinks no ve nada hasta el siguiente.
¿Por qué QoS 1 en ambos lados? El puente podría reiniciarse mientras TTN entrega mensajes; QoS 1 con un cliente que se reconecta reduce los huecos. Para conocer las ventajas e inconvenientes, consulta los niveles de QoS de MQTT explicados.
Profundidad de los topics. connectmq/demo/t6n1b4/farm/<device>/telemetry tiene seis niveles, holgadamente dentro del límite de ocho niveles del servidor público. En producción, usa tu propia jerarquía, como farm1/north/<device>/telemetry.
Tamaño de los mensajes. Un uplink en bruto de TTN con metadatos de varios gateways puede ocupar unos cuantos kilobytes. El registro republicado ocupa un par de cientos de bytes, muy por debajo del límite de 64 KB del área pública de pruebas y del límite de 256 KB del plan gratuito de ConnectMQ, y es mucho más barato de procesar para paneles y clientes móviles que funcionan con batería.
Gestión de huecos y duplicados
LoRaWAN es, por diseño, un enlace de radio con pérdidas: los dispositivos transmiten sin confirmación, y un paquete que ningún gateway escucha simplemente se pierde. El contador de tramas (f_cnt) incluido en cada registro permite a los consumidores detectar huecos. Si el contador salta de 410 a 413, se perdieron dos uplinks en la parte de radio, no en MQTT. Esa distinción ahorra tiempo cuando alguien informa de que faltan datos.
El puente también puede ver el mismo uplink dos veces si se reconecta a TTN mientras un mensaje QoS 1 está en tránsito. Los consumidores que almacenan datos deben tratar la combinación de ID de dispositivo y contador de tramas como clave única e ignorar las repeticiones. Ten en cuenta que el contador se reinicia cuando un dispositivo vuelve a unirse a la red, así que úsalo para detectar huecos en ventanas cortas y no como identificador permanente.
Envío de downlinks
Para enviar un comando a un dispositivo (por ejemplo, un nuevo intervalo de envío), el puente puede suscribirse a un topic de comandos en tu broker y reenviarlo al topic de downlink de TTN:
import base64
def forward_downlink(device_id, port, raw_bytes):
body = {"downlinks": [{
"f_port": port,
"frm_payload": base64.b64encode(raw_bytes).decode(),
"priority": "NORMAL",
}]}
ttn.publish(f"v3/{TTN_APP}@ttn/devices/{device_id}/down/push",
json.dumps(body), qos=1)
# ejemplo: fijar el intervalo en 30 minutos (un byte) en el puerto 2
# forward_downlink("soil-node-07", 2, bytes([30]))
Los dispositivos de clase A solo reciben downlinks después de su siguiente uplink, así que los comandos llegan con un retraso igual al intervalo de envío. Usa los downlinks con moderación; el tiempo de emisión y las reglas de ciclo de trabajo de LoRaWAN los hacen costosos.
Ejecución fiable
- Ejecuta el puente como servicio systemd o contenedor con reinicio automático.
- Guarda la API key de TTN en una variable de entorno o un almacén de secretos, nunca en el script.
- Concede a la API key solo los permisos que necesita (leer el tráfico de la aplicación y, si se usan, escribir downlinks).
- Supervisa
bridge/status: el last will lo pone enofflinesi el proceso muere. - Recuerda la política de uso justo de TTN para la red comunitaria; está pensada para sensores de baja frecuencia, no para dispositivos que envían mensajes sin parar.
Producción en un broker privado
Define OUT_HOST=broker.connectmq.com, OUT_PORT=8883, OUT_USER, OUT_PASS y OUT_CLIENT_ID con las credenciales emitidas para tu broker privado, y cambia OUT_PREFIX por tu propia jerarquía. El resto del script no cambia. Un broker MQTT gestionado ofrece a tus datos LoRaWAN, sensores Wi-Fi y controladores un hogar compartido y cifrado que no tienes que mantener. Si ejecutas tu propio servidor de red en lugar de TTN, el tutorial de ChirpStack MQTT muestra el puente equivalente, y el montaje del sensor de humedad del suelo MQTT es una alternativa Wi-Fi para campos cercanos a edificios. Encontrarás más patrones en la guía de automatización IoT, y la introducción a qué hace un broker MQTT te ayudará si MQTT es nuevo para ti.
Lleva tus datos LoRaWAN a ConnectMQ
ConnectMQ ofrece a tus datos de TTN un broker MQTT privado con TLS, un explorador de topics en directo, un panel y reglas de alerta por umbral para valores como batería baja o suelo seco. Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, y apunta a él la salida del puente.
Preguntas frecuentes
¿Cuál es el usuario y la contraseña MQTT de TTN?
El usuario es el ID de tu aplicación seguido de @ttn (en la edición comunitaria de The Things Network), y la contraseña es una API key generada en Integrations > MQTT.
¿Por qué falta decoded_payload en mis uplinks?
No hay ningún payload formatter de uplink configurado, o el que hay ha lanzado un error. Pruébalo en el panel del formatter de la consola con un payload hexadecimal real.
¿Puedo conectar TTN directamente a otro broker sin código?
TTN ofrece webhooks y otras integraciones, pero para publicar en tu propio broker MQTT con tu propia estructura de topics, un pequeño puente como este es la opción más sencilla y transparente.
¿El puente tiene que ejecutarse cerca del gateway?
No. Ambas conexiones van por internet, así que puede ejecutarse en cualquier servidor, una Raspberry Pi o un contenedor.



