Raspberry Pi

Lancer un script Python MQTT au démarrage avec systemd sur Raspberry Pi

Transformez un client Python MQTT en véritable service systemd sur Raspberry Pi : démarrage une fois le réseau et l'horloge prêts, redémarrage automatique, identifiants dans un fichier d'environnement et arrêt propre.

Terminal Raspberry Pi montrant un service systemd qui démarre un client Python au boot, illustrant comment lancer un script python mqtt au démarrage
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

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 :

  1. Wants/After=network-online.target retarde le démarrage jusqu'à ce que le réseau soit configuré, et pas seulement jusqu'au lancement du service réseau.
  2. time-sync.target attend l'horloge (voir la remarque sur TLS plus bas).
  3. StartLimitIntervalSec=0 dé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.
  4. Restart=always et RestartSec=5 relancent le script cinq secondes après toute sortie, plantages comme sorties propres.
  5. PYTHONUNBUFFERED=1 fait apparaître immédiatement la sortie de print() dans le journal.
  6. 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 :

  1. status=203/EXEC : systemd ne peut pas exécuter ExecStart. 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 avec ls -l.
  2. ModuleNotFoundError: paho : l'unité exécute le Python système au lieu du venv. Faites pointer ExecStart vers /opt/mqtt-client/.venv/bin/python, pas vers python3.
  3. 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'un EnvironmentFile sont de simples KEY=value sans export.
  4. PermissionError sur un fichier ou un périphérique : ProtectSystem=strict a 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.
  5. « 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.
  6. Redémarrages toutes les quelques secondes sans fin : le script se termine immédiatement. Exécutez à la main la commande ExecStart exacte en tant qu'utilisateur du service avec sudo -u mqttclient pour 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.

Articles liés

Journal d'accès RFID MQTT sur Raspberry Pi avec le lecteur RC522

Câblez un lecteur RFID RC522 sur un Raspberry Pi, lisez les badges en Python, vérifiez-les dans une liste blanche locale et publiez chaque tentative d'accès sous forme d'événement MQTT exploitable par vos outils de journalisation, tableaux de bord et alertes.

8 min de lecture

Installer Mosquitto sur Raspberry Pi comme broker MQTT local sécurisé

Installez Mosquitto sur un Raspberry Pi, sécurisez-le avec des mots de passe et une configuration de listener raisonnable, testez-le avec mosquitto_pub et mosquitto_sub, et découvrez quand un broker local doit faire un pont vers un broker cloud.

9 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.