Um gateway IoT com Raspberry Pi fica entre os dispositivos que não conseguem chegar à internet sozinhos e os sistemas que precisam dos dados deles. Neste guia você vai projetar um gateway que coleta leituras de sensores Bluetooth Low Energy, dispositivos Modbus e instrumentos seriais, padroniza tudo em um único modelo de topics e payloads, armazena os dados em disco quando a rede falha e os encaminha com segurança para um broker MQTT na nuvem. O código é um esqueleto funcional que você pode expandir, não um brinquedo.
O que um gateway IoT realmente faz
Muitos dispositivos de campo falam protocolos que nunca foram pensados para a internet: um termômetro BLE tem alcance de poucos metros, um medidor de energia Modbus RTU conversa por um par RS-485 e uma balança de laboratório imprime linhas por uma porta serial. Um gateway faz quatro trabalhos por eles:
- Adaptação de protocolo. Lê cada dispositivo no seu protocolo nativo.
- Padronização. Transforma tudo em topics e payloads JSON consistentes, com unidades e timestamps.
- Confiabilidade. Guarda os dados quando o uplink cai e os entrega depois, em ordem.
- Uplink seguro. Faz uma única conexão de saída autenticada e criptografada, em vez de expor os dispositivos.
Um Raspberry Pi é uma boa plataforma para isso: roda Linux completo e Python, tem USB para adaptadores RS-485 e seriais, Bluetooth integrado e armazenamento suficiente para dias de dados em buffer. Para ambientes mais agressivos, como o chão de fábrica de uma planta em Joinville ou uma subestação, controladores industriais baseados no Pi, como o Revolution Pi, seguem a mesma abordagem de software em um gabinete para trilho DIN.
Arquitetura do gateway
BLE sensors ----> [ble adapter] \
Modbus RTU/TCP -> [modbus adapter] >--> normalise --> SQLite outbox --> MQTT uplink --> cloud broker
Serial devices -> [serial adapter] / ^
| commands (optional)
Cada adaptador roda de forma independente e entrega as leituras a uma outbox compartilhada. Uma única tarefa de uplink esvazia a outbox para o broker e só apaga as linhas depois que o broker confirma o recebimento. Essa separação é a decisão de design mais importante de todo o gateway: um dispositivo Modbus lento ou um sensor BLE instável nunca bloqueia o uplink, e uma conexão de internet morta nunca bloqueia a coleta de dados.
| Camada | Biblioteca ou ferramenta | Observações |
|---|---|---|
| BLE | bleak |
Varredura assíncrona; funciona com o BlueZ no Pi OS |
| Modbus RTU/TCP | pymodbus |
Adaptador USB RS-485 para RTU |
| Serial | pyserial |
Instrumentos baseados em linhas, GPS, leitores de código de barras |
| Buffer | SQLite (biblioteca padrão) | Sobrevive a reboots, fácil de inspecionar |
| Uplink | paho-mqtt 2.x |
TLS, QoS 1, last will |
| Gerenciador de processos | systemd | Reinicia em caso de falha, sobe no boot |
Escolhendo topics e payloads
Defina o modelo de dados antes de escrever os adaptadores, porque mudá-lo depois significa mudar todos os consumidores. Uma hierarquia que espelha o local físico funciona bem e escala para nomenclaturas industriais como a ISA-95:
<site>/<area>/<device>/telemetry {"ts": 1760600000, "values": {"temp_c": 21.4}, "src": "ble"}
<site>/<area>/<device>/status "online" / "offline" (retained)
<site>/gateway/<gateway-id>/status "online" / "offline" (retained, last will)
Coloque o timestamp no payload, definido pelo gateway no momento da leitura. Quando os dados em buffer forem entregues com uma hora de atraso, os consumidores ainda saberão quando cada leitura foi feita. Para orientações sobre hierarquias em sites maiores, as convenções do Sparkplug B e do Unified Namespace são uma boa referência.
O esqueleto do gateway IoT com Raspberry Pi
Instale as dependências em um ambiente virtual:
sudo apt update && sudo apt install -y python3-venv bluetooth sqlite3
python3 -m venv ~/gw-env
~/gw-env/bin/pip install "paho-mqtt>=2.0" "pymodbus>=3.10" pyserial bleak
sudo usermod -aG dialout $USER # acesso à porta serial; saia e entre de novo na sessão
Salve o núcleo como ~/gateway.py. Ele inclui a outbox, o uplink e um adaptador de exemplo por protocolo.
#!/usr/bin/env python3
"""Raspberry Pi IoT gateway: BLE + Modbus + serial -> SQLite outbox -> MQTT."""
import asyncio
import json
import os
import socket
import sqlite3
import ssl
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") # YOUR_USERNAME
PASSWORD = os.getenv("MQTT_PASSWORD") # YOUR_PASSWORD
GW_ID = socket.gethostname()
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"gw-{GW_ID}-91b6f0") # YOUR_CLIENT_ID
SITE = os.getenv("SITE", "connectmq/demo/91b6f0/site1")
DB_PATH = os.getenv("OUTBOX", "/var/lib/gateway/outbox.db")
# ---------- outbox ----------
db = sqlite3.connect(DB_PATH, isolation_level=None, check_same_thread=False)
db.execute("PRAGMA journal_mode=WAL")
db.execute("CREATE TABLE IF NOT EXISTS outbox (id INTEGER PRIMARY KEY, topic TEXT, payload TEXT)")
def enqueue(area, device, values, src):
payload = json.dumps({"ts": int(time.time()), "values": values, "src": src})
db.execute("INSERT INTO outbox (topic, payload) VALUES (?, ?)",
(f"{SITE}/{area}/{device}/telemetry", payload))
# ---------- uplink ----------
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID,
protocol=mqtt.MQTTv5)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
status_topic = f"{SITE}/gateway/{GW_ID}/status"
client.will_set(status_topic, "offline", qos=1, retain=True)
client.on_connect = lambda c, u, f, rc, p: (not rc.is_failure) and c.publish(
status_topic, "online", qos=1, retain=True)
client.reconnect_delay_set(1, 60)
client.connect_async(HOST, PORT, keepalive=60)
client.loop_start()
async def uplink(batch=50):
while True:
if not client.is_connected():
await asyncio.sleep(2)
continue
rows = db.execute("SELECT id, topic, payload FROM outbox ORDER BY id LIMIT ?",
(batch,)).fetchall()
if not rows:
await asyncio.sleep(1)
continue
for row_id, topic, payload in rows:
info = client.publish(topic, payload, qos=1)
# Aguarda o PUBACK sem bloquear o event loop
for _ in range(100):
if info.is_published():
break
await asyncio.sleep(0.05)
if not info.is_published():
break # mantém a linha, tenta de novo depois
db.execute("DELETE FROM outbox WHERE id = ?", (row_id,))
await asyncio.sleep(0.11) # fica abaixo de ~10 msg/s no broker público
# ---------- adapters ----------
async def modbus_adapter():
from pymodbus.client import AsyncModbusTcpClient
mb = AsyncModbusTcpClient(os.getenv("MODBUS_HOST", "192.168.1.60"), port=502)
while True:
try:
if not mb.connected:
await mb.connect()
rr = await mb.read_holding_registers(0, count=2, device_id=1)
if not rr.isError():
enqueue("utilities", "meter1",
{"voltage_v": rr.registers[0] / 10, "current_a": rr.registers[1] / 100},
"modbus")
except Exception as exc:
print("modbus:", exc)
await asyncio.sleep(10)
async def serial_adapter():
import serial
port = os.getenv("SERIAL_PORT", "/dev/ttyUSB1")
while True:
try:
with serial.Serial(port, 9600, timeout=1) as ser:
while True:
line = await asyncio.to_thread(ser.readline)
text = line.decode(errors="ignore").strip()
if text:
enqueue("lab", "scale1", {"reading": text}, "serial")
except Exception as exc:
print("serial:", exc)
await asyncio.sleep(5)
async def ble_adapter():
from bleak import BleakScanner
wanted = {m.strip().upper() for m in os.getenv("BLE_MACS", "").split(",") if m.strip()}
def on_adv(device, adv):
if device.address.upper() in wanted:
enqueue("rooms", device.address.replace(":", "").lower(),
{"rssi": adv.rssi,
"mfr": {str(k): v.hex() for k, v in adv.manufacturer_data.items()}},
"ble")
async with BleakScanner(on_adv):
while True:
await asyncio.sleep(3600)
async def main():
tasks = [uplink()]
if os.getenv("ENABLE_MODBUS") == "1":
tasks.append(modbus_adapter())
if os.getenv("ENABLE_SERIAL") == "1":
tasks.append(serial_adapter())
if os.getenv("ENABLE_BLE") == "1":
tasks.append(ble_adapter())
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
Cada adaptador é propositalmente mínimo. Dispositivos reais precisam de decodificação: sensores BLE empacotam a temperatura em dados de fabricante ou de serviço em formatos específicos de cada fornecedor, e registradores Modbus precisam da escala e da ordem de palavras indicadas no manual do equipamento. O tutorial Raspberry Pi BLE para MQTT e o gateway Modbus para MQTT com Raspberry Pi se aprofundam em cada um. Observe que a pymodbus 3.10 e posteriores chamam o argumento de unidade de device_id; versões 3.x anteriores o chamam de slave.
Crie a pasta de dados e teste primeiro com o Modbus desativado, só o uplink, inserindo uma linha manualmente:
sudo mkdir -p /var/lib/gateway && sudo chown $USER /var/lib/gateway
ENABLE_MODBUS=0 ~/gw-env/bin/python ~/gateway.py &
sqlite3 /var/lib/gateway/outbox.db \
"INSERT INTO outbox(topic,payload) VALUES('connectmq/demo/91b6f0/site1/test/dev/telemetry','{\"ok\":true}')"
Veja a mensagem chegar no servidor MQTT público gratuito com mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-91b6f0 -t 'connectmq/demo/91b6f0/#' -v. Ele serve para testes e protótipos, não para dados de produção.
Por que o store-and-forward importa
O padrão outbox significa que os dados são gravados em disco no momento em que são lidos e só são apagados depois que o broker confirma o recebimento. Isso traz várias propriedades de graça:
- Reboots não perdem nada. As linhas persistem no SQLite.
- As quedas são invisíveis para os adaptadores. Eles continuam enfileirando; a outbox cresce e esvazia depois.
- A ordem é preservada. As linhas são enviadas por ID crescente.
- Você pode inspecionar o backlog com um único
SELECT COUNT(*).
Dois cuidados. Primeiro, limite a outbox: adicione um DELETE periódico das linhas mais antigas acima de um limite, para que uma queda de uma semana não encha o disco. Segundo, cuidado com o desgaste do cartão SD. O modo WAL reduz as gravações, mas um gateway que usa muito o buffer deve ter um SSD ou um cartão de grau industrial. Mais detalhes no guia de buffer offline.
Alternativa: Mosquitto local mais uma bridge
Algumas equipes preferem adaptadores que publicam em um broker Mosquitto local no Pi, com uma bridge encaminhando para a nuvem. Isso funciona bem quando ferramentas existentes (Node-RED, Telegraf, software de fornecedores) já falam MQTT e quando consumidores locais precisam dos dados mesmo sem internet. A contrapartida é que o buffer passa a depender dos limites de fila do Mosquitto, e não da sua própria outbox. O tutorial de configuração de bridge no Mosquitto mostra esse design de ponta a ponta.
Protegendo o uplink do gateway IoT com Raspberry Pi
O gateway é a única coisa no local que fala com a internet, então trate-o como uma fronteira de segurança:
- Use TLS na porta 8883 com verificação de certificado. Nunca desative a checagem de hostname.
- Dê a cada gateway credenciais e client ID próprios, para que um Pi comprometido possa ser revogado isoladamente.
- Mantenha as redes de campo (RS-485, BLE) e o uplink logicamente separados; o gateway não deve rotear tráfego entre eles.
- Rode o serviço com um usuário sem privilégios e mantenha o Pi OS atualizado.
- Desative o login SSH por senha em favor de chaves e remova os serviços que você não usa.
No lado da nuvem, um broker MQTT na nuvem como o ConnectMQ elimina a necessidade de operar e blindar um broker por conta própria; por ser um broker MQTT gerenciado, atualizações e operação ficam com o serviço. Crie um broker no portal, copie usuário, senha e client ID e defina MQTT_PORT=8883, MQTT_USERNAME=YOUR_USERNAME, MQTT_PASSWORD=YOUR_PASSWORD e MQTT_CLIENT_ID=YOUR_CLIENT_ID no ambiente do serviço. O ConnectMQ vincula os client IDs às credenciais, aplica cotas por plano e proteção contra flood e bloqueia IPs após repetidas falhas de login. Se os seus topics seguem uma estrutura ISA-95, a visão de dispositivos do portal os agrupa automaticamente por empresa, site, área e linha.
Fique atento aos limites do plano ao esvaziar um backlog. O plano Serverless gratuito permite QoS até 1 e mensagens de 256 KB; o servidor público permite cerca de 10 mensagens por segundo por cliente, e é por isso que o uplink faz uma pequena pausa entre as publicações.
Rode como serviço
sudo tee /etc/systemd/system/gateway.service >/dev/null <<'EOF'
[Unit]
Description=Raspberry Pi IoT gateway
After=network-online.target bluetooth.target
Wants=network-online.target
[Service]
User=pi
EnvironmentFile=/etc/gateway.env
ExecStart=/home/pi/gw-env/bin/python /home/pi/gateway.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now gateway
Coloque as variáveis MQTT_*, SITE e ENABLE_* em /etc/gateway.env com chmod 600.
Comece com o ConnectMQ
Dê ao seu gateway um uplink seguro em minutos: crie um broker em app.connectmq.com no plano Serverless gratuito, sem cartão de crédito, e cole as credenciais geradas em /etc/gateway.env. O tutorial MQTT com Raspberry Pi traz os fundamentos, se você precisar.
Perguntas frequentes
Qual modelo de Raspberry Pi é melhor para um gateway IoT?
Um Pi 4 ou Pi 5 com 2 GB ou mais dá conta com folga de vários adaptadores de protocolo e de um buffer local. Um Pi Zero 2 W aguenta um único adaptador leve, mas tem pouca margem.
O gateway deve publicar direto na nuvem ou por um Mosquitto local?
Publique direto quando o gateway for o único produtor e você quiser controle total do buffer. Use um Mosquitto local com bridge quando várias ferramentas locais já falam MQTT ou precisam dos dados durante as quedas.
Como envio comandos da nuvem para os dispositivos de campo?
Faça o gateway assinar um topic de comando como <site>/<area>/<device>/set e traduza as mensagens em escritas Modbus ou comandos seriais. Valide cada comando antes de executá-lo.
Por quanto tempo o gateway consegue armazenar dados offline?
Pelo tempo que o seu disco e o limite da outbox permitirem. Dimensione o limite a partir da sua taxa de mensagens e da queda mais longa que você precisa suportar.



