Um projeto de sensor de distância Raspberry Pi MQTT com o HC-SR04 resolve dois problemas comuns com uma única peça barata: quão cheio está um tanque, e se há algo (ou alguém) ali. Neste tutorial você vai ligar o sensor ultrassônico com segurança, obter leituras estáveis em Python com gpiozero e publicar tanto a porcentagem de nível quanto eventos de presença via MQTT, para que um dashboard ou automação possa agir sobre eles.
Como o HC-SR04 mede distância
O HC-SR04 emite uma rajada curta de ultrassom de 40 kHz quando seu pino TRIG recebe um pulso de 10 microssegundos e mantém o pino ECHO em nível alto até o eco voltar. A duração desse pulso de eco, multiplicada pela velocidade do som e dividida por dois, é a distância. O sensor funciona de alguns centímetros até alguns metros, com melhores resultados contra superfícies planas e duras em ângulo reto, como a superfície da água ou uma parede.
Duas consequências importam em projetos reais. Primeiro, o Pi precisa cronometrar o pulso de eco em software, então o escalonamento do Linux adiciona algum jitter; você vai filtrá-lo. Segundo, a velocidade do som muda com a temperatura do ar, então espere uma pequena deriva entre uma manhã fria e uma tarde quente. Para um medidor de nível de tanque ou um detector de presença, os dois efeitos são administráveis.
Ligações com divisor de tensão
O HC-SR04 opera em 5 V e sua saída ECHO também é de 5 V, o que pode danificar um pino GPIO do Raspberry Pi. Use sempre um divisor de tensão no ECHO:
| Pino do HC-SR04 | Ligação |
|---|---|
| VCC | 5 V do Pi (pino físico 2) |
| GND | GND do Pi (pino físico 6) |
| TRIG | GPIO23 (pino físico 16), direto |
| ECHO | Resistor de 1 k para o GPIO24 (pino físico 18); resistor de 2 k do GPIO24 para o GND |
O par de 1 k e 2 k divide os 5 V para cerca de 3,3 V. Uma combinação de 1 k com 1,8 k ou 2,2 k também funciona. O TRIG pode ser ligado diretamente, porque a saída de 3,3 V do Pi basta para disparar o sensor. Se você comprar a variante do módulo compatível com 3,3 V, consulte a documentação; talvez não precise do divisor.
Instalando o software
sudo apt update
sudo apt install -y python3-venv python3-lgpio
mkdir -p ~/distance-mqtt && cd ~/distance-mqtt
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
pip install "paho-mqtt>=2.0" gpiozero
Teste rápido sem MQTT:
python -c "from gpiozero import DistanceSensor; from time import sleep; s = DistanceSensor(echo=24, trigger=23, max_distance=4); sleep(1); print(round(s.distance * 100, 1), 'cm')"
Aponte o sensor para uma parede e compare o resultado com uma trena. Uma leitura sempre igual ao valor de max_distance significa que nenhum eco é detectado, geralmente um problema de ligação no ECHO.
Dois modos: nível e presença
A mesma leitura pode ser interpretada de duas formas, e o script publica ambas:
| Modo | Cálculo | Topic | Quando é publicado | Retido |
|---|---|---|---|---|
| Nível do tanque | (empty_distance - distance) / (empty_distance - full_distance) |
.../level |
A cada 30 s | Sim |
| Presença | Distância abaixo de um limite, com histerese | .../presence |
Apenas quando muda | Sim |
| Bruto | Distância mediana em cm | dentro dos dois payloads |
Reter os dois topics significa que um dashboard aberto a qualquer momento mostra imediatamente o nível atual e se há alguém presente. Publicar a presença apenas quando ela muda mantém o tráfego mínimo e simplifica as automações: assine e reaja a cada mensagem.
Calibrando o tanque
Monte o sensor no topo do tanque, apontando direto para baixo, longe da entrada de água e das paredes. Depois meça duas distâncias:
EMPTY_CM: do sensor até o fundo (ou até o nível útil mais baixo).FULL_CM: do sensor até a água na marca de cheio. Mantenha pelo menos o alcance mínimo do sensor, de alguns centímetros, entre a face do sensor e o nível mais alto da água.
Defina os dois valores no ambiente. O script limita o resultado entre 0 e 100 por cento para que respingos ou um tanque vazio não produzam valores impossíveis.
O script do sensor de distância Raspberry Pi MQTT
Salve como distance_mqtt.py:
#!/usr/bin/env python3
"""HC-SR04 tank level and presence detection over MQTT."""
import json
import os
import statistics
import time
import uuid
import paho.mqtt.client as mqtt
from gpiozero import DistanceSensor
BROKER = 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.getenv("MQTT_CLIENT_ID", f"distance-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/9c3d55/tank1")
EMPTY_CM = float(os.getenv("EMPTY_CM", "120"))
FULL_CM = float(os.getenv("FULL_CM", "20"))
PRESENCE_ON_CM = float(os.getenv("PRESENCE_ON_CM", "60"))
PRESENCE_OFF_CM = float(os.getenv("PRESENCE_OFF_CM", "80"))
LEVEL_INTERVAL = 30
SAMPLES = 7
sensor = DistanceSensor(echo=24, trigger=23, max_distance=4, queue_len=5)
def measure_cm():
"""Median of several readings, ignoring out-of-range values."""
values = []
for _ in range(SAMPLES):
d = sensor.distance * 100
if 2 < d < 399: # 400 cm significa nenhum eco dentro de max_distance
values.append(d)
time.sleep(0.06) # deixa os ecos se dissiparem entre os pulsos
return statistics.median(values) if values else None
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Connection refused: {reason_code}")
return
print(f"Connected as {CLIENT_ID}")
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
client.on_connect = on_connect
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
client.connect(BROKER, PORT, keepalive=30)
client.loop_start()
present = None
last_level = 0.0
try:
while True:
distance = measure_cm()
now = time.time()
if distance is None:
print("No valid echo")
time.sleep(1)
continue
# Presença com histerese: liga abaixo de ON, desliga acima de OFF.
new_present = present
if distance < PRESENCE_ON_CM:
new_present = True
elif distance > PRESENCE_OFF_CM:
new_present = False
if new_present is not None and new_present != present:
present = new_present
payload = {"present": present, "distance_cm": round(distance, 1), "ts": int(now)}
client.publish(f"{PREFIX}/presence", json.dumps(payload), qos=1, retain=True)
print("presence", payload)
if now - last_level >= LEVEL_INTERVAL:
span = EMPTY_CM - FULL_CM
percent = max(0.0, min(100.0, (EMPTY_CM - distance) / span * 100))
payload = {"level_pct": round(percent, 1), "distance_cm": round(distance, 1), "ts": int(now)}
client.publish(f"{PREFIX}/level", json.dumps(payload), qos=1, retain=True)
print("level", payload)
last_level = now
time.sleep(0.5)
except KeyboardInterrupt:
pass
finally:
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(2)
client.loop_stop()
client.disconnect()
sensor.close()
Em uma implantação real você normalmente usaria um modo por sensor: um sensor de tanque não precisa de eventos de presença, e um sensor de porta não precisa de nível. Manter os dois em um único script facilita experimentar, e você pode apagar o bloco de que não precisa.
Por que o filtro de mediana e a histerese importam
Leituras ultrassônicas ocasionalmente saltam: um eco perdido vindo da parede do tanque, uma ondulação na água ou um soluço de temporização no Linux. Com a média, um único valor ruim puxa o resultado; a mediana de sete leituras simplesmente o ignora. A histerese resolve outro problema. Sem ela, uma pessoa parada exatamente no limite faria a presença ligar e desligar várias vezes por segundo, inundando os subscribers com eventos. Com distâncias separadas para ligar e desligar, o estado muda uma vez e permanece.
Testando a saída
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-9c3d55 \
-t 'connectmq/demo/9c3d55/tank1/#' -v
Aproxime a mão do sensor: uma mensagem presence com "present": true aparece quando você cruza os 60 cm, e false quando se afasta além dos 80 cm. A cada 30 segundos, uma mensagem level informa a porcentagem de enchimento com base na sua calibração. Lembre-se de que o servidor MQTT público de testes pode ser lido por todos, então use um prefixo aleatório e mantenha nomes reais de locais fora dos topics de teste.
Automatizando com os dados
Com nível e presença no MQTT, a automação vira um problema de subscriber, e não de sensor:
- Controle de bomba: um segundo script ou fluxo Node-RED assina
levele liga uma bomba pelo controlador de relé com Raspberry Pi via MQTT quando o nível cai abaixo de 20 por cento, desligando-a acima de 90 por cento. - Alertas: em um broker ConnectMQ privado, uma regra de alerta por limite em
level_pctavisa sobre um tanque quase vazio, como descrito em alertas de limite MQTT. - Automação residencial: o Home Assistant pode ler o topic retido
presencecomo sensor de ocupação e acender as luzes.
Se você precisa de um sensor de nível a bateria em um tanque remoto sem energia elétrica, a montagem do sensor de nível de caixa d'água com ESP32 usa o mesmo payload com deep sleep, para que os dois alimentem o mesmo dashboard.
Solução de problemas
| Problema | Causa provável e correção |
|---|---|
| Sempre lê 400 cm | Nenhum eco detectado; verifique o divisor e a ligação do ECHO |
| As leituras saltam muito | O sensor vê a parede do tanque; monte-o no centro e longe das bordas |
| As leituras derivam ao longo do dia | A velocidade do som muda com a temperatura; recalibre ou adicione compensação de temperatura |
| Erros por condensação em um tanque | Umidade nos transdutores; use um módulo ultrassônico à prova d'água |
GPIO busy |
Outro script está usando o GPIO23 ou o GPIO24 |
Configuração de produção
Para um tanque em uma fazenda, uma oficina ou um prédio, migre do servidor de testes para um broker privado. O ConnectMQ é um broker MQTT gerenciado com TLS na porta 8883 e credenciais geradas. Defina no ambiente MQTT_PORT=8883, MQTT_USERNAME=YOUR_USERNAME, MQTT_PASSWORD=YOUR_PASSWORD, MQTT_CLIENT_ID=YOUR_CLIENT_ID e um prefixo significativo como farm1/water/tank1 e depois rode o script como serviço do systemd. O tutorial de MQTT com Raspberry Pi traz o modelo do arquivo de serviço e explica o comportamento de reconexão usado aqui.
Comece com o ConnectMQ
Coloque seus dados de nível e presença em um broker MQTT na nuvem que todo dashboard e automação consiga alcançar. Cadastre-se no ConnectMQ no plano Serverless gratuito, sem cartão de crédito, crie um broker e conecte seu Pi via TLS em minutos.
Perguntas frequentes
Preciso mesmo de um divisor de tensão para o HC-SR04?
Sim, para o módulo padrão de 5 V. Seu pino ECHO emite 5 V, e os pinos GPIO do Raspberry Pi são especificados apenas para 3,3 V.
Qual a precisão do HC-SR04 em um Raspberry Pi?
Cerca de um centímetro em boas condições, depois que as leituras passam pelo filtro de mediana. A temporização por software no Linux e as mudanças de temperatura adicionam alguma variação.
O HC-SR04 consegue medir o nível de água em um tanque fechado?
Sim, desde que aponte direto para a superfície e fique seco. Em tanques úmidos, um módulo ultrassônico à prova d'água dura mais.
Por que publicar a presença apenas quando ela muda?
Isso mantém o tráfego mínimo e simplifica as automações. Como a mensagem é retida, novos subscribers ainda recebem o estado atual imediatamente.



