Une passerelle Modbus vers MQTT lit les registres d'un automate (PLC) en Modbus TCP et les publie sous forme de messages MQTT, pour que tableaux de bord, historiens et services cloud puissent s'abonner aux données de l'usine sans jamais toucher l'automate. Vous pouvez en écrire une fiable en environ 150 lignes de Python avec pymodbus et paho-mqtt.
Ce tutoriel construit cette passerelle étape par étape : une table de tags, une boucle de scrutation, le décodage des valeurs 32 bits, la publication sur changement, un statut online/offline avec Last Will, et une façon de tout tester avant d'approcher une ligne de production.
Où se place la passerelle
Modbus est un protocole requête/réponse : un maître (client) demande des registres à un esclave (serveur), une requête à la fois. MQTT est publish/subscribe : les producteurs publient une fois, un nombre quelconque de consommateurs s'abonnent. La passerelle fait la traduction entre les deux mondes :
| Côté | Protocole | Rôle de la passerelle |
|---|---|---|
| Réseau automate | Modbus TCP, port 502 | Client Modbus qui interroge holding/input registers et coils |
| Réseau usine / IT | MQTT sur TCP ou TLS | Client MQTT qui publie un topic par tag |
Exécutez la passerelle sur un boîtier edge (PC industriel, Raspberry Pi, conteneur sur le serveur de ligne) qui voit les deux réseaux. L'automate ne voit jamais de trafic MQTT et les consommateurs MQTT n'ouvrent jamais de socket Modbus — exactement la séparation que veulent les équipes OT. Pour la vue d'ensemble, voir MQTT pour l'IIoT et l'Industrie 4.0.
1. Décrire vos tags
Modbus n'a pas de noms, seulement des adresses. La première étape est une table de tags qui dit ce que signifie chaque registre. Gardez-la dans un fichier à côté du code pour la relire comme une configuration :
# nom, fonction, adresse, type de donnée, échelle, unité
TAGS = [
("temperature", "holding", 0, "float32", 1.0, "degC"),
("pressure", "holding", 2, "float32", 1.0, "bar"),
("motor_speed", "holding", 4, "uint16", 1.0, "rpm"),
("part_count", "holding", 5, "uint32", 1.0, "pcs"),
("tank_level", "input", 0, "int16", 0.1, "%"),
("running", "coil", 0, "bool", 1, ""),
]Les adresses sont ici des adresses protocole commençant à zéro. La documentation des fabricants utilise souvent la numérotation « 40001 » qui commence à un : le holding register 40001 correspond à l'adresse 0, le 40003 à l'adresse 2. Le décalage d'une unité est le bug Modbus le plus fréquent, donc vérifiez une valeur connue avant de faire confiance au reste.
2. Lire et décoder les registres
Un registre Modbus fait 16 bits. Les flottants et les compteurs 32 bits occupent deux registres, et les fabricants ne s'accordent pas sur le mot qui vient en premier. Décoder avec struct rend l'ordre des mots explicite :
import struct
from pymodbus.client import ModbusTcpClient
WORD_ORDER_BIG = True # mettre False pour les équipements en « word swap » little-endian
def decode(regs, dtype):
if dtype == "uint16":
return regs[0]
if dtype == "int16":
return struct.unpack(">h", struct.pack(">H", regs[0]))[0]
words = regs if WORD_ORDER_BIG else list(reversed(regs))
raw = struct.pack(">HH", *words)
return struct.unpack(">f" if dtype == "float32" else ">I", raw)[0]
def read_tag(client, unit, func, addr, dtype):
count = 2 if dtype in ("float32", "uint32") else 1
if func == "coil":
rr = client.read_coils(addr, count=1, device_id=unit)
return None if rr.isError() else bool(rr.bits[0])
reader = client.read_holding_registers if func == "holding" else client.read_input_registers
rr = reader(addr, count=count, device_id=unit)
return None if rr.isError() else decode(rr.registers, dtype)En production, lisez des blocs contigus (par exemple les registres 0–5 en une seule requête) plutôt qu'une requête par tag : un automate répond plus vite à une grosse requête qu'à six petites. La version par tag ci-dessus est plus lisible et suffit pour quelques dizaines de tags.
3. Publier vers MQTT sur changement
Combinez maintenant le poller avec un client MQTT. La passerelle publie un topic par tag, n'envoie une valeur que lorsqu'elle varie au-delà d'une bande morte, marque les valeurs comme retained pour que les nouveaux abonnés reçoivent l'état courant immédiatement, et utilise un Last Will pour annoncer sa déconnexion.
import json, os, time
import paho.mqtt.client as mqtt
from pymodbus.client import ModbusTcpClient
from modbus_read import read_tag
from tags import TAGS
PLC_HOST, PLC_UNIT = os.getenv("PLC_HOST", "192.168.10.20"), 1
BASE = "testmqtt/acme/plant1/line2/plc1"
POLL_S, DEADBAND = 1.0, 0.05 # secondes, variation relative (5 %)
mq = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="gw-line2-plc1")
mq.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
mq.tls_set()
mq.will_set(f"{BASE}/status", "offline", qos=1, retain=True)
def on_connect(client, userdata, flags, reason_code, properties):
print("MQTT connecté :", reason_code)
client.publish(f"{BASE}/status", "online", qos=1, retain=True)
mq.on_connect = on_connect
mq.connect("broker.connectmq.com", 8883, keepalive=30)
mq.loop_start()
plc = ModbusTcpClient(PLC_HOST, port=502, timeout=2)
last = {}
def changed(name, value):
old = last.get(name)
if old is None or isinstance(value, bool):
return old != value
return abs(value - old) > abs(old) * DEADBAND
while True:
if not plc.connected and not plc.connect():
mq.publish(f"{BASE}/status", "plc-unreachable", qos=1, retain=True)
time.sleep(5)
continue
for name, func, addr, dtype, scale, unit in TAGS:
value = read_tag(plc, PLC_UNIT, func, addr, dtype)
if value is None:
continue
if not isinstance(value, bool):
value = round(value * scale, 3)
if changed(name, value):
last[name] = value
payload = {"v": value, "unit": unit, "ts": int(time.time() * 1000)}
mq.publish(f"{BASE}/{name}", json.dumps(payload), qos=1, retain=True)
time.sleep(POLL_S)Quelques choix qui méritent une explication :
- Un topic par tag (
.../plc1/temperature) permet aux consommateurs de s'abonner exactement à ce dont ils ont besoin, et des wildcards comme.../plc1/+leur donnent tout. Testez vos motifs avec le topic matcher. - Retained + QoS 1 : un tableau de bord ouvert à 3 h du matin affiche immédiatement la dernière valeur connue, et les messages survivent aux courtes coupures réseau. Voir messages retenus.
- Topic de statut avec Last Will : il distingue « la température est stable » de « la passerelle est morte ». Le broker publie
offlineà votre place si la connexion de la passerelle tombe sans déconnexion propre. - Horodatage dans le payload : il enregistre quand la valeur a été lue, pas quand elle est arrivée — important pour les historiens.
L'arborescence de topics suit une hiérarchie de type ISA-95 (entreprise / site / ligne / équipement), point de départ d'un Unified Namespace.
4. Tester sans automate de production
Ne développez jamais contre une ligne en fonctionnement. Commencez avec un simulateur Modbus — pymodbus en fournit un, et des outils comme diagslave ou ModbusPal servent des registres configurables sur le port 502 (ou 5020 si vous n'êtes pas root). Pointez PLC_HOST dessus et lancez la passerelle.
Vérifiez ensuite le côté MQTT :
- Ouvrez le client MQTT en ligne, connectez-vous à
broker.connectmq.comet abonnez-vous àtestmqtt/acme/plant1/#. Vous devriez voir aussitôt lestatus=onlineretenu et un message par tag. - Modifiez un registre dans le simulateur. Seul ce tag doit publier un nouveau message ; les tags inchangés restent silencieux.
- Arrêtez la passerelle avec
kill -9(sans déconnexion propre). Après environ une fois et demie le keep-alive,statusdoit passer àoffline. - Arrêtez le simulateur.
statusdoit indiquerplc-unreachablependant que MQTT reste connecté.
Suivre les topics de la passerelle dans le navigateur
Depuis un terminal, la même vérification tient en une commande :
mosquitto_sub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/acme/plant1/line2/#' -vDurcissement pour la production
| Sujet | Que faire |
|---|---|
| Redémarrages de la passerelle | La lancer sous systemd ou Docker avec restart: always ; le statut et les valeurs retenues se rétablissent seuls |
| Charge de l'automate | Regrouper les registres contigus, ne pas interroger plus vite que nécessaire, une connexion par automate |
| Nombreux automates | Un thread ou une tâche asyncio par automate, chacun avec son propre topic de statut |
| Panne du broker | paho se reconnecte automatiquement après loop_start() ; mettez les mesures en tampon localement si vous ne pouvez pas tolérer de trous |
| Écritures vers l'automate | Désactivées par défaut ; si besoin, abonnez-vous à un topic .../cmd dédié, autorisez une liste de registres et validez les plages |
| Sécurité | Séparer réseaux OT et IT, ne jamais exposer le port 502 sur internet, utiliser TLS et des identifiants par passerelle pour MQTT |
Modbus n'a aucune authentification, la passerelle est donc souvent la seule frontière contrôlée entre l'automate et tout le reste. Gardez ses droits MQTT restreints et son chemin d'écriture fermé tant que vous n'en avez pas vraiment besoin.
Si vos sites passent plus tard à Sparkplug B ou ont besoin d'une gestion d'état standardisée pour de nombreux équipements, le même cœur de scrutation peut alimenter un edge node Sparkplug B au lieu de simples topics JSON.
Une passerelle Modbus vers MQTT, c'est peu de code pour beaucoup d'effet : elle transforme des registres que seul le programmeur de l'automate comprend en données nommées, horodatées et accessibles par abonnement. Commencez par une table de tags claire, publiez sur changement avec valeurs retenues et Last Will, testez contre un simulateur et le client en ligne, et seulement ensuite raccordez-la à la ligne.
Questions fréquentes
Un automate peut-il publier directement en MQTT sans passerelle Modbus ?
Beaucoup d'automates et de contrôleurs edge récents intègrent un client MQTT, et dans ce cas utilisez-le. Une passerelle Modbus vers MQTT est le bon outil pour l'immense parc installé d'automates, de compteurs et de variateurs qui ne parlent que Modbus TCP ou RTU.
À quelle fréquence faut-il interroger les registres Modbus ?
Aussi lentement que le procédé le permet. Une à cinq secondes est courant pour des températures et des compteurs ; les signaux rapides peuvent demander 100 à 250 ms. Interrogez vite si nécessaire, mais ne publiez qu'en cas de changement (avec une bande morte) pour garder un trafic MQTT réduit.
La passerelle doit-elle écrire des valeurs dans l'automate depuis MQTT ?
Seulement de façon délibérée. Limitez les écritures à une liste explicite de registres, validez les plages, protégez le topic de commande avec des ACL par topic et journalisez chaque écriture. Beaucoup d'usines gardent la passerelle en lecture seule.


