A forma confiável de executar um script Python MQTT na inicialização é um serviço systemd: ele inicia seu cliente depois que a rede está disponível, reinicia quando ele trava, captura os logs e o encerra de forma limpa no desligamento. Neste tutorial você vai transformar um script paho-mqtt em um serviço no Raspberry Pi, guardar as credenciais fora do código, contornar o problema do relógio no boot que quebra o TLS e aprender os comandos da operação do dia a dia. O mesmo arquivo de unidade funciona em qualquer distribuição Linux moderna.
Por que systemd e não cron ou rc.local
Há várias formas de iniciar um script no boot, e elas não são equivalentes:
| Método | Reinicia após travar | Espera a rede | Logs | Desligamento limpo |
|---|---|---|---|---|
@reboot no crontab |
Não | Não | Só se você redirecionar a saída | Não |
/etc/rc.local |
Não | Não confiável | Não | Não |
| Autostart do desktop | Não | Não | Não | Só com sessão gráfica |
| Serviço systemd | Sim, configurável | Sim, com ordenação | journald integrado | Sim, SIGTERM e depois SIGKILL |
Uma entrada no crontab parece mais simples, mas o resultado é um gateway MQTT que morre silenciosamente às 3 da manhã e nunca mais volta. O systemd já roda no Raspberry Pi OS, então não há nada a instalar.
Passo 1: prepare o script
Um script que roda como serviço deve ler a configuração de variáveis de ambiente, reconectar sozinho e encerrar de forma limpa quando o systemd enviar SIGTERM. Este exemplo publica um heartbeat com o uptime a cada 30 segundos e usa um Last Will para que os assinantes vejam quando o dispositivo fica offline.
#!/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"] # obrigatório e ú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()
connect_async() faz diferença aqui. Um connect() simples lança uma exceção se o broker estiver inacessível naquele instante, o que é comum nos primeiros segundos após o boot. connect_async() entrega a conexão ao loop de rede, que tenta de novo com o backoff definido por reconnect_delay_set().
Passo 2: instale em um local estável
Não rode serviços a partir da pasta Downloads do seu diretório pessoal. Dê ao gateway seu próprio diretório, ambiente virtual e usuário 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
Se o script usa GPIO, I2C ou portas seriais, adicione o usuário ao grupo correspondente, por exemplo sudo usermod -aG gpio,i2c,dialout mqttclient.
Passo 3: guarde as credenciais em um arquivo de ambiente
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 o primeiro teste, isto aponta para o servidor MQTT público, que não exige credenciais e serve apenas para testes e protótipos. O systemd lê o arquivo como root antes de iniciar o processo, então ele pode ficar legível apenas pelo root.
Passo 4: escreva o arquivo de unidade para executar o script Python MQTT na inicialização
Crie /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
O que cada linha importante faz:
Wants/After=network-online.targetadia o início até que a rede esteja configurada, e não apenas até o serviço de rede ter iniciado.time-sync.targetespera pelo relógio (veja a observação sobre TLS abaixo).StartLimitIntervalSec=0desativa o limite de taxa de inicialização, para que o systemd continue tentando para sempre em vez de desistir após algumas falhas rápidas.Restart=alwayseRestartSec=5reiniciam o script cinco segundos após qualquer encerramento, incluindo travamentos e saídas limpas.PYTHONUNBUFFERED=1faz a saída deprint()aparecer imediatamente no journal.- As opções
Protect*isolam o processo em sandbox; o checklist de segurança para Raspberry Pi IoT as explica melhor.
Habilite e inicie:
sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-client.service
systemctl status mqtt-client.service
Passo 5: verifique e opere
Acompanhe os logs ao vivo e depois assine a partir de outra máquina para confirmar que as mensagens chegam:
journalctl -u mqtt-client.service -f
mosquitto_sub -h broker.connectmq.com -p 1883 -i boot-viewer-s8t4n6 -t "connectmq/demo/s8t4n6/pi1/#" -v
Agora reinicie o Pi com sudo reboot. O assinante vê offline (do desligamento limpo) e, quando o Pi volta, online e os heartbeats. Para testar a recuperação após travamento, mate o processo com sudo systemctl kill -s SIGKILL mqtt-client.service e veja o systemd reiniciá-lo depois de cinco segundos.
Comandos que você vai usar no dia a dia:
| Tarefa | Comando |
|---|---|
| Reiniciar após mudar código ou env | sudo systemctl restart mqtt-client |
| Parar temporariamente | sudo systemctl stop mqtt-client |
| Desabilitar no boot | sudo systemctl disable mqtt-client |
| Logs desde o último boot | journalctl -u mqtt-client -b |
| Mostrar a unidade efetiva | systemctl cat mqtt-client |
A armadilha do TLS e do relógio
A maioria dos modelos de Raspberry Pi não tem relógio de tempo real com bateria (o Pi 5 tem RTC, mas precisa de uma bateria para manter a hora sem energia). No boot, o relógio parte do último horário salvo até o NTP sincronizar. Se estiver muito errado, a validação do certificado TLS falha porque o certificado parece ainda não válido ou expirado, e seu script registra erros de conexão repetidos na porta 8883.
A ordenação por time-sync.target só ajuda se algum serviço realmente esperar pela sincronização. Habilite o que faz isso:
sudo systemctl enable systemd-time-wait-sync.service
timedatectl status
Com ele habilitado, o cliente MQTT só inicia depois que o relógio estiver sincronizado. Como o script também reconecta sozinho, ele se recupera mesmo sem isso, só que mais devagar.
Solucionando um serviço que não fica de pé
Quando um serviço se comporta mal, systemctl status e o journal quase sempre contêm a resposta. Estas são as falhas mais vistas com clientes MQTT em Python:
status=203/EXEC: o systemd não consegue executar oExecStart. O caminho para o Python do venv está errado ou o venv foi criado por outro usuário e não pode ser lido. Use caminhos absolutos e confira comls -l.ModuleNotFoundError: paho: a unidade roda o Python do sistema em vez do venv. Aponte oExecStartpara/opt/mqtt-client/.venv/bin/python, não parapython3.KeyError: 'MQTT_CLIENT_ID': falta uma variável no arquivo de ambiente ou há um erro de digitação. Lembre que as linhas deEnvironmentFilesãoKEY=valuesimples, semexport.PermissionErrorem um arquivo ou dispositivo:ProtectSystem=strictdeixou o caminho somente leitura ou o usuário do serviço não está em um grupo necessário. Adicione o diretório aReadWritePaths=ou o usuário ao grupo certo.- "connection refused" ou "not authorised" repetidos: porta, credenciais erradas ou client ID duplicado. Dois processos com o mesmo client ID ficam desconectando um ao outro, então confirme que nenhum teste manual continua rodando.
- Reinícios a cada poucos segundos, sem parar: o script encerra imediatamente. Rode à mão o comando exato do
ExecStartcomo usuário do serviço, comsudo -u mqttclient, para ver o erro no seu terminal.
Migrando para um broker privado
Quando o serviço estiver funcionando, mude para credenciais de produção. Crie um broker no ConnectMQ e edite /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
Reinicie o serviço e o script se conecta via TLS. Um broker MQTT gerenciado mantém o lado do servidor funcionando enquanto o systemd mantém o lado do cliente, e juntos eles cobrem a maioria das quedas. Para travamentos que parecem congelamentos em vez de encerramentos, adicione um watchdog de aplicação, como descrito nas dicas de confiabilidade para Raspberry Pi, e guarde os dados durante quedas de rede com o buffer offline de mensagens MQTT em Python. O manual do systemd.service lista todas as opções.
Novo em MQTT no Pi? Comece pelo tutorial de MQTT com Raspberry Pi e depois volte aqui para tornar seu cliente permanente.
Mantenha seu Pi conectado com o ConnectMQ
Cadastre-se em app.connectmq.com no plano Serverless gratuito, sem cartão de crédito, e cole as credenciais geradas no seu arquivo de ambiente. O portal ConnectMQ lista seus dispositivos, então após a próxima reinicialização você pode confirmar que o serviço voltou a ficar online sem precisar entrar no Pi.
Perguntas frequentes
Por que meu serviço inicia antes de a rede estar pronta?
After=network.target só espera pelo serviço de rede, não por uma conexão funcionando. Use Wants= e After=network-online.target e deixe o script reconectar sozinho com connect_async().
Por que não vejo nenhuma saída no journalctl?
O Python faz buffer do stdout quando ele não é um terminal. Defina PYTHONUNBUFFERED=1, rode o Python com -u ou passe flush=True para print().
Devo usar um serviço de usuário em vez de um serviço de sistema?
Serviços de usuário só rodam enquanto aquele usuário tem uma sessão, a menos que o lingering esteja habilitado. Um serviço de sistema com um User= dedicado é mais simples e inicia no boot sem nenhum login.
Como rodo vários scripts MQTT?
Crie um arquivo de unidade por script ou use uma unidade template como mqtt-client@.service com um arquivo de ambiente separado por instância. Cada instância precisa do seu próprio client ID único.



