Raspberry Pi

Ejecutar un script Python MQTT al arrancar con systemd en Raspberry Pi

Convierte un cliente MQTT en Python en un servicio systemd en toda regla en una Raspberry Pi: que arranque cuando la red y el reloj estén listos, se reinicie solo, guarde las credenciales en un archivo de entorno y se detenga limpiamente.

Terminal de Raspberry Pi con un servicio systemd que inicia un cliente Python al arrancar, ilustrando cómo ejecutar un script Python MQTT al arrancar
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

La forma fiable de ejecutar un script Python MQTT al arrancar es un servicio systemd: inicia tu cliente cuando la red ya está activa, lo reinicia si se cae, captura sus logs y lo detiene limpiamente al apagar. En este tutorial convertirás un script de paho-mqtt en un servicio en una Raspberry Pi, guardarás las credenciales fuera del código, resolverás el problema del reloj en el arranque que rompe TLS y aprenderás los comandos para la operación diaria. El mismo archivo de unidad funciona en cualquier distribución Linux moderna.

Por qué systemd y no cron o rc.local

Hay varias formas de iniciar un script al arrancar y no son equivalentes:

Método Reinicia si falla Espera a la red Logs Apagado limpio
@reboot en crontab No No Solo si rediriges la salida No
/etc/rc.local No Poco fiable No No
Autoarranque del escritorio No No No Solo con sesión de escritorio
Servicio systemd Sí, configurable Sí, con ordenación journald integrado Sí, SIGTERM y luego SIGKILL

Una entrada de crontab parece más sencilla, pero el resultado es un gateway MQTT que muere en silencio a las 3 de la madrugada y nunca vuelve. systemd ya está funcionando en Raspberry Pi OS, así que no hay nada que instalar.

Paso 1: preparar el script

Un script que se ejecuta como servicio debe leer la configuración de variables de entorno, reconectarse por sí mismo y salir limpiamente cuando systemd envíe SIGTERM. Este ejemplo publica un heartbeat con el tiempo de actividad cada 30 segundos y usa un Last Will para que los suscriptores vean cuándo el dispositivo se desconecta.

#!/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"]            # obligatorio y único
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()

Aquí connect_async() es importante. Un connect() normal lanza una excepción si el broker no es accesible en ese instante, algo habitual en los primeros segundos tras el arranque. connect_async() delega la conexión en el bucle de red, que reintenta con el backoff definido por reconnect_delay_set().

Paso 2: instalarlo en una ubicación estable

No ejecutes servicios desde la carpeta de Descargas de tu directorio personal. Dale al gateway su propio directorio, entorno virtual y usuario de sistema:

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 el script usa GPIO, I2C o puertos serie, añade el usuario al grupo correspondiente, por ejemplo sudo usermod -aG gpio,i2c,dialout mqttclient.

Paso 3: guardar las credenciales en un archivo de entorno

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

Para la primera prueba, esto apunta al servidor MQTT público gratuito, que no necesita credenciales y está pensado para pruebas, no para datos de producción. systemd lee el archivo como root antes de iniciar el proceso, así que puede quedar legible solo para root.

Paso 4: escribir el archivo de unidad para ejecutar el script Python MQTT al arrancar

Crea /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

Qué hace cada línea importante:

  1. Wants/After=network-online.target retrasa el inicio hasta que la red está configurada, y no solo hasta que el servicio de red ha arrancado.
  2. time-sync.target espera al reloj (consulta la nota sobre TLS más abajo).
  3. StartLimitIntervalSec=0 desactiva el límite de frecuencia de arranque, para que systemd siga reintentando indefinidamente en lugar de rendirse tras unos cuantos fallos rápidos.
  4. Restart=always y RestartSec=5 reinician el script cinco segundos después de cualquier salida, incluidos los fallos y las salidas limpias.
  5. PYTHONUNBUFFERED=1 hace que la salida de print() aparezca de inmediato en el journal.
  6. Las opciones Protect* aíslan el proceso en un sandbox; la checklist para asegurar una Raspberry Pi IoT las explica con más detalle.

Habilítalo e inícialo:

sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-client.service
systemctl status mqtt-client.service

Paso 5: verificar y operar

Observa los logs en vivo y después suscríbete desde otra máquina para confirmar que llegan los mensajes:

journalctl -u mqtt-client.service -f
mosquitto_sub -h broker.connectmq.com -p 1883 -i boot-viewer-s8t4n6 -t "connectmq/demo/s8t4n6/pi1/#" -v

Ahora reinicia la Pi con sudo reboot. El suscriptor ve offline (por el apagado limpio) y después online y los heartbeats cuando la Pi vuelve. Para probar la recuperación ante fallos, mata el proceso con sudo systemctl kill -s SIGKILL mqtt-client.service y observa cómo systemd lo reinicia a los cinco segundos.

Comandos que usarás a diario:

Tarea Comando
Reiniciar tras cambiar el código o el entorno sudo systemctl restart mqtt-client
Detener temporalmente sudo systemctl stop mqtt-client
Desactivar en el arranque sudo systemctl disable mqtt-client
Logs desde el último arranque journalctl -u mqtt-client -b
Mostrar la unidad efectiva systemctl cat mqtt-client

La trampa de TLS y el reloj

La mayoría de los modelos de Raspberry Pi no traen un reloj de tiempo real con batería (la Pi 5 tiene RTC, pero necesita una batería para mantener la hora sin alimentación). Al arrancar, el reloj parte de la última hora guardada hasta que NTP sincroniza. Si está muy desfasado, la validación del certificado TLS falla porque el certificado parece todavía no válido o caducado, y tu script registra errores de conexión repetidos en el puerto 8883.

La ordenación con time-sync.target solo ayuda si un servicio realmente espera a la sincronización. Habilita el que lo hace:

sudo systemctl enable systemd-time-wait-sync.service
timedatectl status

Con eso habilitado, el cliente MQTT solo arranca cuando el reloj está sincronizado. Como el script también se reconecta por sí mismo, se recupera incluso sin ello, solo que más despacio.

Solución de problemas de un servicio que no se mantiene activo

Cuando un servicio se comporta mal, systemctl status y el journal casi siempre contienen la respuesta. Estos son los fallos más frecuentes con clientes MQTT en Python:

  1. status=203/EXEC: systemd no puede ejecutar ExecStart. La ruta al Python del venv es incorrecta, o el venv lo creó otro usuario y no es legible. Usa rutas absolutas y compruébalas con ls -l.
  2. ModuleNotFoundError: paho: la unidad ejecuta el Python del sistema en lugar del venv. Apunta ExecStart a /opt/mqtt-client/.venv/bin/python, no a python3.
  3. KeyError: 'MQTT_CLIENT_ID': al archivo de entorno le falta una variable o tiene una errata. Ten en cuenta que las líneas de EnvironmentFile son simples KEY=value sin export.
  4. PermissionError en un archivo o dispositivo: ProtectSystem=strict ha dejado la ruta en solo lectura, o al usuario del servicio le falta un grupo. Añade el directorio a ReadWritePaths= o el usuario al grupo adecuado.
  5. "connection refused" o "not authorised" repetidos: puerto o credenciales incorrectos, o un client ID duplicado. Dos procesos con el mismo client ID se desconectan mutuamente sin parar, así que asegúrate de que no sigue activa una prueba manual.
  6. Reinicios cada pocos segundos sin fin: el script sale inmediatamente. Ejecuta a mano el comando exacto de ExecStart como usuario del servicio con sudo -u mqttclient para ver el error en tu terminal.

Pasar a un broker privado

Cuando el servicio funcione, cambia a credenciales de producción. Crea un broker en ConnectMQ y edita /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

Reinicia el servicio y el script se conectará por TLS. Un broker MQTT gestionado mantiene funcionando el lado del servidor mientras systemd mantiene funcionando el lado del cliente, y juntos cubren la mayoría de las caídas. Para fallos que parecen bloqueos en lugar de salidas, añade un watchdog de aplicación como se describe en los consejos de fiabilidad para Raspberry Pi, y guarda datos en buffer durante los cortes de red con el buffer de mensajes MQTT sin conexión en Python. El manual de systemd.service enumera todas las opciones.

¿Eres nuevo en MQTT con la Pi? Empieza por el tutorial de Raspberry Pi con MQTT y vuelve aquí para hacer permanente tu cliente.

Mantén tu Pi conectada con ConnectMQ

Regístrate en app.connectmq.com en el plan gratuito Serverless, sin tarjeta de crédito, y pega las credenciales generadas en tu archivo de entorno. El portal de ConnectMQ muestra tus dispositivos, así que tras el siguiente reinicio puedes confirmar que tu servicio volvió a estar en línea sin entrar en la Pi.

Preguntas frecuentes

¿Por qué mi servicio arranca antes de que la red esté lista?

After=network.target solo espera al servicio de red, no a una conexión operativa. Usa Wants= y After=network-online.target, y deja que el script se reconecte por sí mismo con connect_async().

¿Por qué no veo ninguna salida en journalctl?

Python almacena stdout en buffer cuando no es una terminal. Define PYTHONUNBUFFERED=1, ejecuta Python con -u o pasa flush=True a print().

¿Debería usar un servicio de usuario en lugar de un servicio de sistema?

Los servicios de usuario solo se ejecutan mientras ese usuario tiene una sesión abierta, salvo que se active lingering. Un servicio de sistema con un User= dedicado es más sencillo y arranca con el sistema sin necesidad de iniciar sesión.

¿Cómo ejecuto varios scripts MQTT?

Crea un archivo de unidad por script o usa una unidad plantilla como mqtt-client@.service con un archivo de entorno distinto por instancia. Cada instancia necesita su propio client ID único.

Artículos relacionados

Fiabilidad de la Raspberry Pi: cómo usar una Pi 24/7 como gateway IoT

Una guía práctica para mantener un gateway MQTT con Raspberry Pi funcionando durante meses: desgaste del almacenamiento, sistemas de archivos de solo lectura, alimentación, refrigeración, watchdogs de hardware y software y monitorización remota de la salud.

9 min de lectura

Pico W vs Pico 2 W para MQTT: rendimiento, memoria y TLS de RP2040 frente a RP2350

La Pico 2 W mantiene el formato y el chip Wi-Fi de la Pico W, pero sustituye el RP2040 por el RP2350, más rápido. Esta comparativa analiza qué significa para los dispositivos MQTT: CPU, memoria, TLS, funciones de seguridad, soporte de software y qué placa comprar para cada proyecto.

9 min de lectura

Registro de accesos Raspberry Pi RFID MQTT con el lector RC522

Conecta un lector RFID RC522 a una Raspberry Pi, lee etiquetas en Python, compáralas con una lista blanca local y publica cada intento de acceso como un evento MQTT que pueden consumir registradores, dashboards y herramientas de alertas.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.