




O monitoramento de nível de água é uma aplicação bastante comum em sistemas de automação residencial, comercial e industrial. Acompanhar o nível de uma caixa-d’água permite identificar situações de abastecimento, detectar quando o líquido atinge determinada altura e criar soluções para auxiliar no controle de reservatórios.
Neste projeto, vamos construir um monitor de nível utilizando o ESP32 e o sensor XKC-Y25, com uma interface web acessível pelo celular ou computador. O diferencial será desenvolver todo o firmware no Visual Studio Code, utilizando a extensão PlatformIO IDE e os recursos do FreeRTOS para organizar a execução do programa.
Em vez de concentrar todas as operações em um único laço principal, vamos dividir o software em tarefas independentes. Uma tarefa ficará responsável pela leitura do sensor e pela filtragem das mudanças de estado. Outra será responsável pelo servidor web, que disponibilizará as informações para o navegador.
A página apresentará uma representação gráfica de uma caixa-d’água, uma indicação do estado do sensor e informações sobre as leituras realizadas. A interface será atualizada automaticamente, sem que o usuário precise recarregar a página.Para simplificar a montagem, o ESP32 funcionará como ponto de acesso Wi-Fi, criando uma rede própria. Dessa forma, será possível acessar o painel sem depender de um roteador ou de uma conexão com a internet.É importante esclarecer que o XKC-Y25 detecta a presença de líquido em um ponto específico. Com apenas um sensor, não conseguimos medir diretamente a porcentagem real de enchimento da caixa. A representação gráfica será esquemática, mostrando se o líquido atingiu ou não a altura de detecção. Para monitorar diferentes alturas, poderemos utilizar sensores adicionais.
O XKC-Y25 é um sensor capacitivo de nível que pode ser instalado na parte externa de determinados recipientes. Seu funcionamento se baseia na alteração das propriedades capacitivas percebidas pelo sensor quando o líquido se aproxima da região de detecção.
Uma das vantagens dessa tecnologia é a possibilidade de detectar líquidos sem colocar o sensor diretamente em contato com eles. Isso pode simplificar a instalação e evitar mecanismos móveis no interior do reservatório.Entre as aplicações possíveis, destacam-se:O FreeRTOS é um sistema operacional de tempo real utilizado em sistemas embarcados. Ele permite organizar um programa em tarefas que executam de forma concorrente, com prioridades e intervalos de execução próprios. O ambiente ESP32 disponibiliza recursos do FreeRTOS, como criação de tarefas, temporizações, filas e mutexes. Esses recursos são úteis quando diferentes partes do programa precisam trabalhar sem depender de um único laço de execução.
Neste projeto, utilizaremos duas tarefas principais:Tarefa de monitoramento: lê a entrada digital do sensor periodicamente e confirma mudanças de estado após várias amostras consecutivas.Tarefa do servidor web: atende às requisições HTTP e disponibiliza a página e a API de consulta do estado.Como as duas tarefas precisarão acessar informações compartilhadas, utilizaremos um mutex para proteger a estrutura de dados. Isso evita que uma tarefa leia os dados durante uma atualização feita pela outra. Entenda mais sobre mutex nesse artigo: FreeRTOS – Utilizando MUTEX
Essa arquitetura também facilita a inclusão de novas funcionalidades, como histórico de eventos, comunicação MQTT ou acionamento de uma bomba.Para montar o projeto, você precisará de:
Para desenvolver o projeto apresentado neste artigo serão utilizados alguns componentes, todos facilmente encontrados na loja da Eletrogate.
Vamos utilizar o GPIO 27 como entrada digital para receber o sinal condicionado do sensor.
| Sinal | Conexão |
|---|---|
| Saída lógica condicionada do sensor, fio Amarelo | GPIO 27 |
| GND da interface, fio Azul | GND do ESP32 |
| Alimentação do sensor, fio Marrom | VIN(5v), verifique o datasheet |
| Fio preto, modo de saída (pino MODE) | Não usado |

Depois de montar o circuito, confira a alimentação e a continuidade das conexões antes de energizar o sistema.Para desenvolver o firmware, utilizaremos o Visual Studio Code com a extensão PlatformIO IDE. Veja o nosso artigo: Como Programar o Arduino com VS Code e PlatformIOComo Programar o Arduino com VS Code e PlatformIO
O PlatformIO facilita o gerenciamento de bibliotecas, a configuração da placa, a compilação e o envio do firmware ao ESP32.MonitorCaixaAguaEspressif ESP32 Dev Module, ou a placa equivalente utilizada;Arduino.Abra o arquivo platformio.ini e utilize uma configuração semelhante à seguinte:
[env:esp32dev] platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200 build_flags = -DCORE_DEBUG_LEVEL=1 board_build.filesystem = littlefs
Essa configuração seleciona uma placa ESP32 Dev Module, utiliza o framework Arduino e configura o monitor serial para 115200 baud. Se a sua placa possuir outra identificação, ajuste o campo board conforme a documentação do PlatformIO.
MonitorCaixaAgua/
├── platformio.ini
├── src/
│ └── main.cpp
└── data/
└── index.htmlO arquivo main.cpp conterá o programa do ESP32, incluindo as tarefas FreeRTOS, a leitura do sensor e o servidor HTTP. O arquivo index.html conterá a interface web, com a representação gráfica da caixa-d’água e o código JavaScript responsável pela atualização automática.
Essa separação torna o projeto mais fácil de manter e permite modificar o painel sem misturar todo o HTML ao código do firmware.Abra o arquivo src/main.cpp e adicione o código a seguir. Ele cria o ponto de acesso Wi-Fi, inicializa o servidor HTTP e executa as tarefas independentes do FreeRTOS.O exemplo pressupõe que a interface elétrica forneça nível baixo quando o líquido for detectado. Se a sua montagem utilizar lógica inversa, altere a constante correspondente.
/******************************************************************************* * ESP32 com FreeRTOS: Monitor de Caixa-d’Água com Sensor XKC-Y25 e Painel Web * ============================================================================ * Artigo para o blog Eletrogate. * ============================================================================ * @author Saulo Aislan ([email protected]) * @version 1.0 * @date 2026-10-29 * @copyright Copyright (c) 2026 */ #include <Arduino.h> #include <WiFi.h> #include <WebServer.h> #include <LittleFS.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" // -------------------------------------- // CONFIGURACAO DO SENSOR // -------------------------------------- constexpr uint8_t PINO_SENSOR = 27; constexpr bool DETECTA_EM_LOW = true; constexpr uint8_t AMOSTRAS_CONFIRMACAO = 5; constexpr uint32_t INTERVALO_SENSOR_MS = 100; // -------------------------------------- // REDE WI-FI // -------------------------------------- const char* NOME_REDE = "MonitorCaixa"; const char* SENHA_REDE = "Caixa1234"; WebServer server(80); // -------------------------------------- // ESTADO COMPARTILHADO // -------------------------------------- struct EstadoSistema { bool detectado = false; bool inicializado = false; uint32_t mudancas = 0; uint32_t ultimaMudancaMs = 0; }; EstadoSistema estado; SemaphoreHandle_t mutexEstado = nullptr; // -------------------------------------- // LEITURA DO SENSOR // -------------------------------------- bool lerSensor() { const int valor = digitalRead(PINO_SENSOR); if (DETECTA_EM_LOW) { return valor == LOW; } return valor == HIGH; } // -------------------------------------- // TAREFA DO SENSOR // -------------------------------------- void tarefaSensor(void* parametro) { bool candidato = false; uint8_t contador = 0; bool primeiraLeitura = true; for (;;) { const bool leitura = lerSensor(); if (primeiraLeitura) { candidato = leitura; contador = 1; primeiraLeitura = false; } else if (leitura == candidato) { if (contador < AMOSTRAS_CONFIRMACAO) { contador++; } } else { candidato = leitura; contador = 1; } bool estadoMudou = false; bool primeiroEstado = false; uint32_t totalMudancas = 0; if (xSemaphoreTake( mutexEstado, pdMS_TO_TICKS(50)) == pdTRUE) { if (contador >= AMOSTRAS_CONFIRMACAO) { if (!estado.inicializado) { estado.detectado = candidato; estado.inicializado = true; estado.ultimaMudancaMs = millis(); estadoMudou = true; primeiroEstado = true; } else if (estado.detectado != candidato) { estado.detectado = candidato; estado.mudancas++; estado.ultimaMudancaMs = millis(); estadoMudou = true; } } totalMudancas = estado.mudancas; xSemaphoreGive(mutexEstado); } if (estadoMudou) { Serial.print("["); Serial.print(millis()); Serial.print(" ms] Sensor "); Serial.print(primeiroEstado ? "inicializado: " : "mudou: "); Serial.print(candidato ? "NIVEL DETECTADO" : "NIVEL NAO DETECTADO"); if (!primeiroEstado) { Serial.print(" (mudancas: "); Serial.print(totalMudancas); Serial.print(")"); } Serial.println(); } vTaskDelay( pdMS_TO_TICKS(INTERVALO_SENSOR_MS) ); } } // -------------------------------------- // SERVIR A PAGINA WEB // -------------------------------------- void handlePagina() { server.sendHeader("Location", "/index.html"); server.send(302, "text/plain", ""); } // -------------------------------------- // API DE STATUS // -------------------------------------- void handleStatus() { EstadoSistema copia; if (xSemaphoreTake( mutexEstado, pdMS_TO_TICKS(100)) != pdTRUE) { server.send( 503, "application/json", "{\"erro\":\"estado indisponivel\"}" ); return; } copia = estado; xSemaphoreGive(mutexEstado); String json = "{"; json += "\"inicializado\":"; json += copia.inicializado ? "true" : "false"; json += ",\"detectado\":"; json += copia.detectado ? "true" : "false"; json += ",\"mudancas\":"; json += String(copia.mudancas); json += ",\"ultimaMudancaMs\":"; json += String(copia.ultimaMudancaMs); json += "}"; server.sendHeader("Cache-Control", "no-store"); server.send(200, "application/json", json); } // -------------------------------------- // TAREFA DO SERVIDOR WEB // -------------------------------------- void tarefaWeb(void* parametro) { for (;;) { server.handleClient(); vTaskDelay(pdMS_TO_TICKS(2)); } } // -------------------------------------- // INICIALIZACAO // -------------------------------------- void setup() { Serial.begin(115200); pinMode(PINO_SENSOR, INPUT_PULLUP); mutexEstado = xSemaphoreCreateMutex(); if (mutexEstado == nullptr) { Serial.println("Falha ao criar mutex"); while (true) { delay(1000); } } // Inicializa o sistema de arquivos LittleFS if (!LittleFS.begin(true)) { Serial.println("ERRO: Falha ao montar LittleFS!"); while (true) { delay(1000); } } Serial.println("LittleFS montado com sucesso!"); WiFi.mode(WIFI_AP); if (!WiFi.softAP(NOME_REDE, SENHA_REDE)) { Serial.println("Falha ao iniciar Wi-Fi"); while (true) { delay(1000); } } Serial.print("Rede Wi-Fi: "); Serial.println(NOME_REDE); Serial.print("Acesse: http://"); Serial.println(WiFi.softAPIP()); // A pagina sera servida pelo sistema de arquivos. // O arquivo index.html precisa ser enviado para // a particao LittleFS pelo PlatformIO. server.serveStatic( "/index.html", LittleFS, "/index.html" ); server.on("/", HTTP_GET, handlePagina); server.on("/api/status", HTTP_GET, handleStatus); server.begin(); // Tarefa de leitura do sensor BaseType_t sensorOk = xTaskCreate( tarefaSensor, "LeituraSensor", 3072, nullptr, 2, nullptr ); // Tarefa de atendimento HTTP BaseType_t webOk = xTaskCreate( tarefaWeb, "ServidorWeb", 6144, nullptr, 1, nullptr ); if (sensorOk != pdPASS || webOk != pdPASS) { Serial.println("Falha ao criar tarefas"); while (true) { delay(1000); } } Serial.println("Sistema iniciado."); } void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }
Agora vamos criar a interface gráfica.No VS Code, crie a pasta data na raiz do projeto e, dentro dela, o arquivo index.html.A página tem uma caixa-d’água desenhada em CSS, uma marcação para o ponto de detecção do sensor e um indicador de estado. O JavaScript consulta a API /api/status a cada dois segundos.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>Monitor de Caixa-d'Agua</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #edf2f7;
color: #1e293b;
}
.painel {
max-width: 460px;
margin: 20px auto;
padding: 24px;
background: white;
border-radius: 18px;
text-align: center;
box-shadow: 0 6px 24px #00000012;
}
.estado {
padding: 13px;
margin: 15px 0;
border-radius: 10px;
font-weight: bold;
}
.ok {
background: #dcfce7;
color: #166534;
}
.alerta {
background: #ffedd5;
color: #9a3412;
}
.erro {
background: #fee2e2;
color: #991b1b;
}
.caixa {
position: relative;
width: 190px;
height: 245px;
margin: 24px auto;
border: 5px solid #475569;
border-top: 0;
border-radius: 0 0 17px 17px;
background: #f8fafc;
overflow: visible;
}
.agua {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 0;
background: #38bdf8;
transition: height 0.5s ease;
}
.sensor-linha {
position: absolute;
left: -15px;
right: -15px;
bottom: 55%;
border-top: 2px dashed #ea580c;
z-index: 2;
}
.sensor-texto {
position: absolute;
left: 100%;
top: -12px;
padding-left: 8px;
width: 100px;
font-size: 11px;
text-align: left;
color: #c2410c;
}
.detalhes {
padding: 13px;
background: #f8fafc;
border-radius: 10px;
text-align: left;
font-size: 14px;
line-height: 1.8;
}
</style>
</head>
<body>
<main class="painel">
<h1>Monitor de Caixa-d'Agua</h1>
<div id="estado" class="estado alerta">
Consultando sensor...
</div>
<div class="caixa">
<div id="agua" class="agua"></div>
<div class="sensor-linha">
<span class="sensor-texto">
Ponto de deteccao
</span>
</div>
</div>
<p id="legenda">
Aguardando dados do sensor.
</p>
<div class="detalhes">
<div>Estado: <strong id="detalhe">--</strong></div>
<div>Mudancas: <strong id="mudancas">0</strong></div>
<div>Ultima mudanca: <strong id="mudanca">--</strong></div>
</div>
<p id="conexao">
Atualizacao automatica a cada 2 segundos.
</p>
</main>
<script>
async function atualizarPainel() {
const estado = document.getElementById("estado");
const agua = document.getElementById("agua");
try {
const resposta = await fetch("/api/status", {
cache: "no-store"
});
if (!resposta.ok) {
throw new Error("Erro HTTP");
}
const dados = await resposta.json();
if (!dados.inicializado) {
estado.textContent = "INICIALIZANDO SENSOR";
estado.className = "estado alerta";
agua.style.height = "0%";
} else if (dados.detectado) {
estado.textContent = "NÍVEL DETECTADO";
estado.className = "estado ok";
agua.style.height = "55%";
} else {
estado.textContent = "NÍVEL NAO DETECTADO";
estado.className = "estado alerta";
agua.style.height = "20%";
}
document.getElementById("detalhe").textContent =
!dados.inicializado
? "Aguardando"
: (dados.detectado ? "Detectado" : "Nao detectado");
document.getElementById("mudancas").textContent =
dados.mudancas;
document.getElementById("mudanca").textContent =
dados.ultimaMudancaMs + " ms";
document.getElementById("conexao").textContent =
"Conectado ao ESP32";
document.getElementById("legenda").textContent =
dados.detectado
? "Liquido presente na altura do sensor."
: "Liquido nao detectado nessa altura.";
} catch (erro) {
estado.textContent = "SEM COMUNICACAO";
estado.className = "estado erro";
document.getElementById("conexao").textContent =
"Nao foi possivel consultar o ESP32.";
}
}
atualizarPainel();
setInterval(atualizarPainel, 2000);
</script>
</body>
</html>
Na interface web, três informações ajudam a acompanhar o funcionamento do sistema:MonitorCaixaAgua no VS Code.MonitorCaixa.http://192.168.4.1.Uma das principais possibilidades de expansão do projeto é utilizar dois sensores XKC-Y25 instalados em alturas diferentes da caixa-d’água. Dessa forma, podemos monitorar os níveis mínimo e máximo do reservatório, tornando o sistema mais útil para aplicações de automação.O primeiro sensor deve ser instalado próximo ao nível mínimo desejado, enquanto o segundo deve ficar próximo ao nível máximo. Cada sensor será conectado a uma entrada digital do ESP32, permitindo que o firmware identifique as condições do reservatório.Com essa configuração, o sistema poderá trabalhar com três condições principais:
Essas condições podem ser representadas visualmente no painel web. A interface poderá destacar o nível baixo com um alerta, apresentar o nível intermediário como condição normal de acompanhamento e indicar quando a água atingir o nível máximo. Além do monitoramento, essa configuração permite implementar um sistema automático de controle de bomba. Por exemplo, quando o nível cair abaixo do sensor inferior, o ESP32 poderá solicitar o acionamento da bomba. Quando a água atingir o sensor superior, o sistema poderá solicitar seu desligamento. Para evitar acionamentos frequentes, o firmware deverá manter o estado de operação entre os dois limites. Essa expansão mantém a arquitetura baseada em FreeRTOS: a tarefa de monitoramento passa a ler os dois sensores, enquanto a tarefa do servidor web disponibiliza os estados atualizados para o navegador. A API também poderá informar separadamente o estado de cada sensor, permitindo que o painel apresente os níveis mínimo e máximo de forma independente. É importante destacar que dois sensores fornecem detecção por pontos, e não uma medição contínua da porcentagem de água. O nível intermediário representa apenas a região entre os sensores, e não um percentual exato do volume armazenado.
Vídeo demonstrando da detecção do nível da água na garrafa. Observação na demonstração o sensor é um pouco diferente do descrito no artigo, mas o principio é o mesmo.
Neste tutorial, desenvolvemos a base de um sistema de monitoramento de caixa-d’água utilizando ESP32, FreeRTOS, sensor XKC-Y25 e uma interface web.
A utilização do Visual Studio Code com PlatformIO permite organizar o firmware em arquivos separados, compilar o projeto e gerenciar o sistema de arquivos utilizado pela página web.
Com tarefas independentes, sincronização por mutex e atualização automática da interface, o projeto demonstra como combinar programação embarcada, comunicação HTTP e visualização de dados em uma única aplicação.
O resultado é um ponto de partida para soluções mais completas de monitoramento e automação de reservatórios.
Para mais materiais como esse, continue acompanhando as postagens semanais do blog e não deixe de visitar nossa loja. Lá você encontra todos os componentes necessários para desenvolver esse e muitos outros projetos!
Que a força esteja com você! Até mais!
Neste projeto, vamos construir um monitor de nível utilizando o ESP32 e o sensor XKC-Y25, com uma interface web acessível pelo celular ou computador.
Encontre tudo na Loja Eletrogate com frete grátis para compras acima de R$ 200