Raspberry Pi

Sensor de Distância com Raspberry Pi e MQTT: HC-SR04 para Nível de Tanques e Detecção de Presença

Ligue um sensor ultrassônico HC-SR04 a um Raspberry Pi com um divisor de tensão seguro, filtre leituras ruidosas em Python e publique o nível de enchimento de tanques e eventos de presença via MQTT para dashboards e automações.

Sensor ultrassônico HC-SR04 montado sobre uma caixa d'água e ligado a um Raspberry Pi enviando leituras de nível, um projeto de sensor de distância Raspberry Pi MQTT
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

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:

  1. EMPTY_CM: do sensor até o fundo (ou até o nível útil mais baixo).
  2. 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:

  1. Controle de bomba: um segundo script ou fluxo Node-RED assina level e 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.
  2. Alertas: em um broker ConnectMQ privado, uma regra de alerta por limite em level_pct avisa sobre um tanque quase vazio, como descrito em alertas de limite MQTT.
  3. Automação residencial: o Home Assistant pode ler o topic retido presence como 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.

Artigos relacionados

Registro de Acessos Raspberry Pi RFID MQTT com o Leitor RC522

Ligue um leitor RFID RC522 a um Raspberry Pi, leia tags em Python, confira-as em uma lista de permissões local e publique cada tentativa de acesso como um evento MQTT que loggers, dashboards e ferramentas de alerta podem consumir.

8 min de leitura

Confiabilidade do Raspberry Pi: Rodando um Pi 24/7 como Gateway IoT

Um guia prático para manter um gateway MQTT com Raspberry Pi rodando por meses: desgaste do armazenamento, sistemas de arquivos somente leitura, alimentação, refrigeração, watchdogs de hardware e software e monitoramento remoto de saúde.

9 min de leitura

Pico W vs Pico 2 W para MQTT: Desempenho, Memória e TLS do RP2040 vs RP2350

O Pico 2 W mantém o formato e o chip Wi-Fi do Pico W, mas troca o RP2040 pelo RP2350, mais rápido. Esta comparação mostra o que isso significa para dispositivos MQTT: CPU, memória, TLS, recursos de segurança, suporte de software e qual placa comprar para cada projeto.

9 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.