Le serveur The Things Network MQTT fournit à chaque application un flux en direct de ses uplinks LoRaWAN, ce qui facilite l'intégration de données de capteurs longue portée dans vos propres systèmes. Dans ce tutoriel, vous allez écrire un payload formatter, vous connecter au serveur MQTT de TTN avec une clé d'API et exécuter un petit pont Python qui republie les mesures décodées en JSON propre dans l'arborescence de topics de votre propre broker, où tableaux de bord, Node-RED et automatisations peuvent les exploiter aux côtés de vos autres appareils.
Pourquoi relayer TTN vers votre propre broker
The Things Network (TTN) gère la partie radio de LoRaWAN : passerelles, activation des appareils, déduplication et déchiffrement. Son serveur MQTT publie chaque uplink sous la forme d'un volumineux document JSON contenant les métadonnées de chaque passerelle ayant capté le paquet. C'est excellent pour le débogage, mais peu pratique comme modèle de données à long terme :
- les topics sont ceux de TTN (
v3/<app>@ttn/devices/<device>/up), pas la hiérarchie de votre site ; - les payloads sont gros et imbriqués, alors que la plupart des consommateurs ne veulent que trois ou quatre valeurs ;
- vos appareils Wi-Fi, automates et contrôleurs se trouvent déjà sur un autre broker.
Un pont résout ces trois problèmes. Il s'abonne à TTN, extrait l'essentiel et publie des messages compacts dans la même arborescence de topics que tout le reste. Les consommateurs n'ont jamais besoin des identifiants TTN, et si vous passez plus tard à un serveur de réseau privé, seul le pont change. Le guide agriculture intelligente IoT explique où cela s'inscrit dans une conception à l'échelle de l'exploitation.
Étape 1 : décoder le payload dans TTN
Les appareils LoRaWAN envoient des octets compacts. Ajoutez un payload formatter dans la console TTN sous Applications > votre application > Payload formatters > Uplink, type Custom Javascript formatter. Cet exemple décode un nœud de sol qui envoie l'humidité (uint16, en dixièmes de pour cent), la température du sol (int16, en dixièmes de degré) et la batterie (uint16, en millivolts) :
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: []
};
}
Utilisez le panneau Test avec un payload hexadécimal comme 0136 008C 0E74 pour vérifier le résultat avant de déployer le moindre appareil. L'objet décodé apparaît dans chaque uplink sous uplink_message.decoded_payload.
Étape 2 : obtenir les identifiants MQTT auprès de TTN
Dans la console, ouvrez Integrations > MQTT. Vous y verrez :
| Paramètre | Valeur |
|---|---|
| Serveur | Votre cluster, par exemple eu1.cloud.thethings.network |
| Ports | 1883 (en clair) et 8883 (TLS) |
| Nom d'utilisateur | <application-id>@ttn |
| Mot de passe | Une clé d'API ; cliquez sur Generate new API key |
Copiez immédiatement la clé d'API ; TTN ne l'affiche qu'une seule fois. Utilisez toujours le port 8883 vers TTN, car la clé donne accès aux données de votre application. La section consacrée au serveur MQTT dans la documentation officielle de The Things Stack liste l'ensemble des topics, y compris les événements de join, de downlink et de service.
Le topic d'uplink d'une application est :
v3/<application-id>@ttn/devices/<device-id>/up
Vous pouvez vérifier le flux avant d'écrire la moindre ligne de code :
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
Étape 3 : le pont The Things Network MQTT en Python
Le pont utilise deux clients paho-mqtt 2.x : l'un abonné à TTN, l'autre publiant vers votre broker. Exécutez-le d'abord avec le serveur MQTT public gratuit broker.connectmq.com, réservé aux tests, en remplaçant t6n1b4 par votre propre chaîne aléatoire, car l'espace public est lisible par tous.
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") # à définir en production
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 sortie --------------------------------------------------------
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()
Exécutez-le :
pip install paho-mqtt
export TTN_API_KEY='NNSXS.YOUR_TTN_API_KEY'
python ttn_bridge.py
Et observez la sortie propre depuis un autre terminal :
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-t6n1b4 \
-t 'connectmq/demo/t6n1b4/farm/#' -v
Chaque uplink devient un retained message comme {"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}. Comme il est retained, un tableau de bord qui démarre plus tard voit immédiatement la dernière mesure de chaque capteur, ce qui compte pour des appareils qui n'émettent que toutes les 15 minutes.
Notes de conception
Pourquoi inclure le RSSI, le SNR et le nombre de passerelles ? Ce sont les premiers signes d'une antenne défaillante, d'une passerelle déplacée ou d'un capteur qui s'enfonce dans la boue. Les stocker ne coûte presque rien.
Pourquoi des retained messages ? Les appareils LoRaWAN sont silencieux la plupart du temps. Sans retain, tout consommateur qui se connecte entre deux uplinks ne voit rien jusqu'au suivant.
Pourquoi la QoS 1 des deux côtés ? Le pont peut redémarrer pendant que TTN livre des messages ; la QoS 1 associée à un client qui se reconnecte réduit les trous. Pour les compromis, consultez les niveaux de QoS MQTT expliqués.
Profondeur des topics. connectmq/demo/t6n1b4/farm/<device>/telemetry compte six niveaux, bien en deçà de la limite de huit niveaux du serveur MQTT public gratuit. En production, utilisez votre propre hiérarchie, comme farm1/north/<device>/telemetry.
Taille des messages. Un uplink TTN brut avec les métadonnées de plusieurs passerelles peut atteindre quelques kilo-octets. L'enregistrement republié pèse quelques centaines d'octets, bien en dessous de la limite de 64 Ko de l'espace public de test et de celle de 256 Ko du plan gratuit de ConnectMQ, et il est beaucoup moins coûteux à traiter pour les tableaux de bord et les clients mobiles sur batterie.
Gérer les trous et les doublons
LoRaWAN est par conception une liaison radio avec pertes : les appareils émettent sans accusé de réception, et un paquet qu'aucune passerelle ne capte est tout simplement perdu. Le compteur de trames (f_cnt) inclus dans chaque enregistrement permet aux consommateurs de détecter les trous. Si le compteur passe de 410 à 413, deux uplinks ont été perdus côté radio, pas dans MQTT. Cette distinction fait gagner du temps lorsque quelqu'un signale des données manquantes.
Le pont peut aussi voir deux fois le même uplink s'il se reconnecte à TTN pendant qu'un message QoS 1 est en transit. Les consommateurs qui stockent les données doivent traiter la combinaison identifiant d'appareil et compteur de trames comme une clé unique et ignorer les répétitions. Notez que le compteur est réinitialisé lorsqu'un appareil rejoint à nouveau le réseau : utilisez-le donc pour détecter les trous sur de courtes périodes plutôt que comme identifiant permanent.
Envoyer des downlinks
Pour envoyer une commande à un appareil (par exemple un nouvel intervalle d'émission), le pont peut s'abonner à un topic de commande sur votre broker et la transmettre au 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)
# exemple : régler l'intervalle à 30 minutes (un octet) sur le port 2
# forward_downlink("soil-node-07", 2, bytes([30]))
Les appareils de classe A ne reçoivent les downlinks qu'après leur prochain uplink : les commandes arrivent donc avec un délai égal à l'intervalle d'émission. Limitez les downlinks ; le temps d'antenne LoRaWAN et les règles de rapport cyclique les rendent coûteux.
Le faire fonctionner de manière fiable
- Exécutez le pont comme service systemd ou conteneur avec redémarrage automatique.
- Conservez la clé d'API TTN dans une variable d'environnement ou un coffre de secrets, jamais dans le script.
- N'accordez à la clé d'API que les droits nécessaires (lecture du trafic de l'application et, le cas échéant, écriture des downlinks).
- Surveillez
bridge/status: le last will le passe àofflinesi le processus meurt. - Gardez à l'esprit la politique d'utilisation équitable de TTN pour le réseau communautaire ; il est conçu pour des capteurs à faible fréquence, pas pour des appareils bavards.
Production sur un broker privé
Définissez OUT_HOST=broker.connectmq.com, OUT_PORT=8883, OUT_USER, OUT_PASS et OUT_CLIENT_ID avec les identifiants délivrés pour votre broker privé, et remplacez OUT_PREFIX par votre propre hiérarchie. Le reste du script ne change pas. Un broker MQTT managé offre à vos données LoRaWAN, capteurs Wi-Fi et contrôleurs un emplacement commun et chiffré que vous n'avez pas à maintenir. Si vous exploitez votre propre serveur de réseau au lieu de TTN, le tutoriel ChirpStack MQTT présente le pont équivalent, et le montage capteur d'humidité du sol MQTT est une alternative Wi-Fi pour les parcelles proches des bâtiments. D'autres modèles sont réunis dans le guide de l'automatisation IoT, et les bases sur le rôle d'un broker MQTT vous aideront si MQTT est nouveau pour vous.
Amenez vos données LoRaWAN sur ConnectMQ
ConnectMQ offre à vos données TTN relayées un broker MQTT privé avec TLS, un explorateur de topics en direct, un tableau de bord et des règles d'alerte à seuil pour des valeurs comme une batterie faible ou un sol sec. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, et dirigez-y la sortie du pont.
Questions fréquentes
Quels sont le nom d'utilisateur et le mot de passe MQTT de TTN ?
Le nom d'utilisateur est l'identifiant de votre application suivi de @ttn (pour l'édition communautaire de The Things Network), et le mot de passe est une clé d'API générée sous Integrations > MQTT.
Pourquoi decoded_payload est-il absent de mes uplinks ?
Aucun payload formatter d'uplink n'est configuré, ou il a levé une erreur. Testez-le dans le panneau du formatter de la console avec un vrai payload hexadécimal.
Puis-je connecter TTN directement à un autre broker sans code ?
TTN propose des webhooks et d'autres intégrations, mais pour publier dans votre propre broker MQTT selon votre propre structure de topics, un petit pont comme celui-ci reste l'option la plus simple et la plus transparente.
Le pont doit-il tourner à proximité de la passerelle ?
Non. Les deux connexions passent par internet : il peut donc tourner sur n'importe quel serveur, un Raspberry Pi ou un conteneur.



