La méthode fiable pour lancer un script Python MQTT au démarrage est un service systemd : il démarre votre client une fois le réseau opérationnel, le relance en cas de plantage, capture ses logs et l'arrête proprement à l'extinction. Dans ce tutoriel, vous allez transformer un script paho-mqtt en service sur un Raspberry Pi, stocker les identifiants hors du code, régler le problème d'horloge au démarrage qui casse TLS et apprendre les commandes d'exploitation courantes. Le même fichier d'unité fonctionne sur toute distribution Linux moderne.
Pourquoi systemd plutôt que cron ou rc.local
Il existe plusieurs façons de lancer un script au démarrage, et elles ne se valent pas :
| Méthode | Redémarre après un plantage | Attend le réseau | Logs | Arrêt propre |
|---|---|---|---|---|
@reboot dans la crontab |
Non | Non | Seulement si vous redirigez la sortie | Non |
/etc/rc.local |
Non | Peu fiable | Non | Non |
| Démarrage automatique du bureau | Non | Non | Non | Seulement avec une session graphique |
| Service systemd | Oui, configurable | Oui, avec l'ordonnancement | journald intégré | Oui, SIGTERM puis SIGKILL |
Une entrée crontab paraît plus simple, mais elle aboutit à une passerelle MQTT qui meurt en silence à 3 h du matin et ne revient jamais. systemd tourne déjà sur Raspberry Pi OS : il n'y a rien à installer.
Étape 1 : préparer le script
Un script exécuté comme service doit lire sa configuration depuis des variables d'environnement, se reconnecter tout seul et se terminer proprement lorsque systemd envoie SIGTERM. Cet exemple publie un heartbeat avec le temps de fonctionnement toutes les 30 secondes et utilise un Last Will pour que les abonnés voient quand l'appareil passe hors ligne.
#!/usr/bin/env python3
import json
import os
import signal
import threading
import time
import paho.mqtt.client as mqtt
HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.environ["MQTT_CLIENT_ID"] # obligatoire et unique
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/s8t4n6/pi1")
INTERVAL = int(os.getenv("PUBLISH_INTERVAL", "30"))
stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop.set())
signal.signal(signal.SIGINT, lambda *_: stop.set())
def on_connect(client, userdata, flags, reason_code, properties):
print(f"connected to {HOST}:{PORT} ({reason_code})", flush=True)
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
def on_disconnect(client, userdata, flags, reason_code, properties):
print(f"disconnected ({reason_code}), will retry", flush=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.connect_async(HOST, PORT, keepalive=30)
client.loop_start()
start = time.monotonic()
while not stop.wait(INTERVAL):
payload = {"uptime_s": int(time.monotonic() - start), "ts": int(time.time())}
client.publish(f"{PREFIX}/heartbeat", json.dumps(payload), qos=1)
print("stopping", flush=True)
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(3)
client.disconnect()
client.loop_stop()
connect_async() est important ici. Un simple connect() lève une exception si le broker est injoignable à cet instant, ce qui est fréquent dans les premières secondes après le démarrage. connect_async() confie la connexion à la boucle réseau, qui réessaie avec le backoff défini par reconnect_delay_set().
Étape 2 : l'installer dans un emplacement stable
N'exécutez pas de services depuis le dossier Téléchargements de votre répertoire personnel. Donnez à la passerelle son propre répertoire, son environnement virtuel et son utilisateur système :
sudo useradd --system --home /opt/mqtt-client --shell /usr/sbin/nologin mqttclient
sudo mkdir -p /opt/mqtt-client
sudo cp heartbeat.py /opt/mqtt-client/
sudo python3 -m venv /opt/mqtt-client/.venv
sudo /opt/mqtt-client/.venv/bin/pip install "paho-mqtt>=2.0"
sudo chown -R mqttclient:mqttclient /opt/mqtt-client
Si le script utilise le GPIO, l'I2C ou des ports série, ajoutez l'utilisateur au groupe correspondant, par exemple sudo usermod -aG gpio,i2c,dialout mqttclient.
Étape 3 : conserver les identifiants dans un fichier d'environnement
sudo mkdir -p /etc/mqtt-client
sudo tee /etc/mqtt-client/env > /dev/null <<'EOF'
MQTT_HOST=broker.connectmq.com
MQTT_PORT=1883
MQTT_CLIENT_ID=pi1-heartbeat-s8t4n6
TOPIC_PREFIX=connectmq/demo/s8t4n6/pi1
PUBLISH_INTERVAL=30
EOF
sudo chmod 600 /etc/mqtt-client/env
Pour le premier test, cette configuration pointe vers le serveur MQTT public gratuit, qui ne demande aucun identifiant et convient aux tests et prototypes, pas aux données de production. systemd lit le fichier en tant que root avant de lancer le processus : il peut donc rester lisible par root uniquement.
Étape 4 : écrire le fichier d'unité pour lancer le script Python MQTT au démarrage
Créez /etc/systemd/system/mqtt-client.service :
[Unit]
Description=Python MQTT client
Wants=network-online.target time-sync.target
After=network-online.target time-sync.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=mqttclient
Group=mqttclient
WorkingDirectory=/opt/mqtt-client
EnvironmentFile=/etc/mqtt-client/env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mqtt-client/.venv/bin/python /opt/mqtt-client/heartbeat.py
Restart=always
RestartSec=5
TimeoutStopSec=15
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
Le rôle de chaque ligne importante :
Wants/After=network-online.targetretarde le démarrage jusqu'à ce que le réseau soit configuré, et pas seulement jusqu'au lancement du service réseau.time-sync.targetattend l'horloge (voir la remarque sur TLS plus bas).StartLimitIntervalSec=0désactive la limite de fréquence de démarrage, pour que systemd continue de réessayer indéfiniment au lieu d'abandonner après quelques échecs rapides.Restart=alwaysetRestartSec=5relancent le script cinq secondes après toute sortie, plantages comme sorties propres.PYTHONUNBUFFERED=1fait apparaître immédiatement la sortie deprint()dans le journal.- Les options
Protect*placent le processus dans un bac à sable ; la checklist de sécurisation du Raspberry Pi IoT les détaille.
Activez-le et démarrez-le :
sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-client.service
systemctl status mqtt-client.service
Étape 5 : vérifier et exploiter
Suivez les logs en direct, puis abonnez-vous depuis une autre machine pour confirmer l'arrivée des messages :
journalctl -u mqtt-client.service -f
mosquitto_sub -h broker.connectmq.com -p 1883 -i boot-viewer-s8t4n6 -t "connectmq/demo/s8t4n6/pi1/#" -v
Redémarrez maintenant le Pi avec sudo reboot. L'abonné voit offline (issu de l'arrêt propre), puis online et les heartbeats une fois le Pi revenu. Pour tester la reprise après plantage, tuez le processus avec sudo systemctl kill -s SIGKILL mqtt-client.service et observez systemd le relancer au bout de cinq secondes.
Les commandes que vous utiliserez au quotidien :
| Tâche | Commande |
|---|---|
| Redémarrer après une modification du code ou de l'env | sudo systemctl restart mqtt-client |
| Arrêter temporairement | sudo systemctl stop mqtt-client |
| Désactiver au démarrage | sudo systemctl disable mqtt-client |
| Logs depuis le dernier démarrage | journalctl -u mqtt-client -b |
| Afficher l'unité effective | systemctl cat mqtt-client |
Le piège de TLS et de l'horloge
La plupart des modèles de Raspberry Pi n'ont pas d'horloge temps réel sauvegardée par pile (le Pi 5 dispose d'une RTC, mais il lui faut une pile pour garder l'heure hors tension). Au démarrage, l'horloge repart de la dernière heure enregistrée jusqu'à la synchronisation NTP. Si l'écart est important, la validation du certificat TLS échoue car le certificat semble pas encore valide ou expiré, et votre script enregistre des erreurs de connexion répétées sur le port 8883.
L'ordonnancement sur time-sync.target n'aide que si un service attend réellement la synchronisation. Activez celui qui le fait :
sudo systemctl enable systemd-time-wait-sync.service
timedatectl status
Une fois ce service activé, le client MQTT ne démarre qu'après la synchronisation de l'horloge. Comme le script se reconnecte aussi tout seul, il récupère même sans lui, simplement plus lentement.
Dépanner un service qui ne tient pas
Quand un service se comporte mal, systemctl status et le journal contiennent presque toujours la réponse. Voici les échecs les plus fréquents avec les clients Python MQTT :
status=203/EXEC: systemd ne peut pas exécuterExecStart. Le chemin vers le Python du venv est erroné, ou le venv a été créé par un autre utilisateur et n'est pas lisible. Utilisez des chemins absolus et vérifiez-les avecls -l.ModuleNotFoundError: paho: l'unité exécute le Python système au lieu du venv. Faites pointerExecStartvers/opt/mqtt-client/.venv/bin/python, pas verspython3.KeyError: 'MQTT_CLIENT_ID': il manque une variable dans le fichier d'environnement ou elle contient une faute de frappe. Notez que les lignes d'unEnvironmentFilesont de simplesKEY=valuesansexport.PermissionErrorsur un fichier ou un périphérique :ProtectSystem=stricta rendu le chemin en lecture seule, ou l'utilisateur du service n'appartient pas au bon groupe. Ajoutez le répertoire àReadWritePaths=ou l'utilisateur au groupe adéquat.- « connection refused » ou « not authorised » à répétition : mauvais port, mauvais identifiants ou client ID en double. Deux processus avec le même client ID se déconnectent mutuellement en boucle : assurez-vous qu'un test manuel n'est pas encore actif.
- Redémarrages toutes les quelques secondes sans fin : le script se termine immédiatement. Exécutez à la main la commande
ExecStartexacte en tant qu'utilisateur du service avecsudo -u mqttclientpour voir l'erreur dans votre terminal.
Passer à un broker privé
Une fois le service fonctionnel, passez aux identifiants de production. Créez un broker sur ConnectMQ, puis modifiez /etc/mqtt-client/env :
MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
TOPIC_PREFIX=site1/gateways/pi1
Redémarrez le service et le script se connecte en TLS. Un broker MQTT managé assure le fonctionnement côté serveur pendant que systemd assure celui côté client, ce qui couvre ensemble la plupart des pannes. Pour les plantages qui ressemblent à des blocages plutôt qu'à des sorties, ajoutez un watchdog applicatif comme décrit dans les conseils de fiabilité pour Raspberry Pi, et mettez les données en tampon pendant les coupures réseau grâce à la mise en tampon MQTT hors ligne en Python. Le manuel systemd.service liste toutes les options.
Vous débutez avec MQTT sur le Pi ? Commencez par le tutoriel Raspberry Pi MQTT, puis revenez ici pour rendre votre client permanent.
Gardez votre Pi connecté avec ConnectMQ
Inscrivez-vous sur app.connectmq.com pour l'offre Serverless gratuite, sans carte bancaire, et collez les identifiants générés dans votre fichier d'environnement. Le portail ConnectMQ liste vos appareils : après le prochain redémarrage, vous pouvez vérifier que votre service est revenu en ligne sans vous connecter au Pi.
Questions fréquentes
Pourquoi mon service démarre-t-il avant que le réseau soit prêt ?
After=network.target n'attend que le service réseau, pas une connexion fonctionnelle. Utilisez Wants= et After=network-online.target, et laissez le script se reconnecter tout seul avec connect_async().
Pourquoi ne vois-je aucune sortie dans journalctl ?
Python met stdout en tampon lorsqu'il ne s'agit pas d'un terminal. Définissez PYTHONUNBUFFERED=1, lancez Python avec -u ou passez flush=True à print().
Dois-je utiliser un service utilisateur plutôt qu'un service système ?
Les services utilisateur ne tournent que pendant la session de cet utilisateur, sauf si le lingering est activé. Un service système avec un User= dédié est plus simple et démarre au boot sans aucune connexion.
Comment exécuter plusieurs scripts MQTT ?
Créez un fichier d'unité par script, ou utilisez une unité modèle comme mqtt-client@.service avec un fichier d'environnement distinct par instance. Chaque instance doit avoir son propre client ID unique.



