O ESP32 é o microcontrolador definitivo para automação residencial DIY, combinando Wi-Fi, Bluetooth, processamento dual-core e preço acessível (R$ 25-40). Com mais GPIO’s que Arduino e conectividade nativa, é perfeito para criar dispositivos smart personalizados. Este guia apresenta 5 projetos práticos completos, do básico ao avançado.
Por Que ESP32 é Ideal para Smart Home
Vantagens Técnicas
Especificações Impressionantes:
- Dual-core 240MHz: Processamento paralelo
- Wi-Fi 802.11n: Conectividade nativa
- Bluetooth 4.2/BLE: Comunicação de baixo consumo
- 36 GPIO’s: Muito mais que Arduino
- ADC 12-bit: Leituras analógicas precisas
- DAC, PWM, I2C, SPI: Interfaces completas
Comparativo com Concorrentes:
ESP32 vs Arduino Uno + Wi-Fi Shield:
- Preço: R$ 35 vs R$ 150+
- Processamento: 240MHz vs 16MHz
- Memória: 520KB vs 2KB
- Conectividade: Nativa vs Módulo adicional
- GPIO's: 36 vs 14
Ecossistema de Desenvolvimento
Plataformas Suportadas:
- Arduino IDE: Familiar e simples
- PlatformIO: Profissional e poderoso
- ESP-IDF: Framework nativo Espressif
- MicroPython: Python para microcontroladores
Bibliotecas Abundantes:
- WiFi, WebServer, HTTPClient nativas
- MQTT, JSON, NTP integrados
- Sensores (DHT, DS18B20, BME280)
- Displays (OLED, TFT, E-Paper)
- Protocolos (Modbus, LoRa, Zigbee)
Projeto 1: Relé Wi-Fi Universal
Visão Geral do Projeto
Funcionalidades:
- Controle de até 8 relés via Wi-Fi
- Interface web responsiva
- Integração MQTT para Home Assistant
- Controle manual com botões
- Estados salvos na memória
Aplicações:
- Controle de luzes residenciais
- Acionamento de portões e bombas
- Automação de eletrodomésticos
- Sistema de irrigação inteligente
Lista de Materiais
Componentes Eletrônicos:
- ESP32 DevKit: R$ 35-45
- Módulo relé 8 canais: R$ 40-60
- Optoacopladores: Inclusos no módulo
- LEDs indicadores: R$ 5-10
- Resistores 220Ω: R$ 2-5
- Botões push: R$ 10-15 (8 unidades)
- Jumpers: R$ 10-15
- Protoboard: R$ 20-30
Alimentação e Proteção:
- Fonte 5V 3A: R$ 25-40
- Regulador 3.3V: R$ 5-10
- Fusíveis: R$ 5-10
- Caixa plástica: R$ 20-35
Custo Total: R$ 180-280
Esquema de Ligação
ESP32 Pinout para Relés:
GPIO 2 → Relé 1
GPIO 4 → Relé 2
GPIO 5 → Relé 3
GPIO 18 → Relé 4
GPIO 19 → Relé 5
GPIO 21 → Relé 6
GPIO 22 → Relé 7
GPIO 23 → Relé 8
Botões (Pull-up interno):
GPIO 12 → Botão 1
GPIO 13 → Botão 2
GPIO 14 → Botão 3
GPIO 15 → Botão 4
GPIO 25 → Botão 5
GPIO 26 → Botão 6
GPIO 27 → Botão 7
GPIO 32 → Botão 8
LEDs Indicadores:
GPIO 33 → LED Status Wi-Fi
GPIO 35 → LED Status MQTT
Código Completo
#include <WiFi.h>
#include <WebServer.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Preferences.h>
// Configurações Wi-Fi
const char* ssid = "SUA_REDE_WIFI";
const char* password = "SUA_SENHA_WIFI";
// Configurações MQTT
const char* mqtt_server = "192.168.1.100"; // IP do seu Home Assistant
const char* mqtt_user = "homeassistant";
const char* mqtt_password = "sua_senha_mqtt";
// Pinos dos relés
const int relayPins[8] = {2, 4, 5, 18, 19, 21, 22, 23};
const int buttonPins[8] = {12, 13, 14, 15, 25, 26, 27, 32};
const int wifiLED = 33;
const int mqttLED = 35;
// Estados dos relés
bool relayStates[8] = {false, false, false, false, false, false, false, false};
String relayNames[8] = {"Luz Sala", "Luz Quarto", "Ventilador", "Bomba",
"Portão", "Jardim", "Garagem", "Reserva"};
// Objetos
WebServer server(80);
WiFiClient espClient;
PubSubClient mqttClient(espClient);
Preferences preferences;
// Variáveis de controle
unsigned long lastMqttReconnect = 0;
unsigned long lastButtonCheck = 0;
bool buttonStates[8] = {true, true, true, true, true, true, true, true};
void setup() {
Serial.begin(115200);
// Inicializar preferências
preferences.begin("relay_states", false);
// Configurar pinos
setupPins();
// Carregar estados salvos
loadRelayStates();
// Conectar Wi-Fi
setupWiFi();
// Configurar servidor web
setupWebServer();
// Configurar MQTT
setupMQTT();
Serial.println("Sistema iniciado!");
}
void loop() {
// Manter conexões ativas
server.handleClient();
if (!mqttClient.connected()) {
reconnectMQTT();
}
mqttClient.loop();
// Verificar botões (debounce)
if (millis() - lastButtonCheck > 50) {
checkButtons();
lastButtonCheck = millis();
}
// Atualizar LEDs de status
updateStatusLEDs();
}
void setupPins() {
// Configurar relés como saída
for (int i = 0; i < 8; i++) {
pinMode(relayPins[i], OUTPUT);
digitalWrite(relayPins[i], HIGH); // Relé desligado (lógica invertida)
}
// Configurar botões com pull-up
for (int i = 0; i < 8; i++) {
pinMode(buttonPins[i], INPUT_PULLUP);
}
// Configurar LEDs de status
pinMode(wifiLED, OUTPUT);
pinMode(mqttLED, OUTPUT);
}
void setupWiFi() {
WiFi.begin(ssid, password);
Serial.print("Conectando ao Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Wi-Fi conectado! IP: ");
Serial.println(WiFi.localIP());
}
void setupWebServer() {
// Página principal
server.on("/", HTTP_GET, handleRoot);
// API para controlar relés
server.on("/relay", HTTP_POST, handleRelayControl);
// API para status
server.on("/status", HTTP_GET, handleStatus);
// Arquivos estáticos
server.on("/style.css", HTTP_GET, handleCSS);
server.begin();
Serial.println("Servidor web iniciado na porta 80");
}
void setupMQTT() {
mqttClient.setServer(mqtt_server, 1883);
mqttClient.setCallback(mqttCallback);
}
void handleRoot() {
String html = generateHTML();
server.send(200, "text/html", html);
}
void handleRelayControl() {
if (server.hasArg("relay") && server.hasArg("state")) {
int relayIndex = server.arg("relay").toInt();
bool state = server.arg("state") == "1";
if (relayIndex >= 0 && relayIndex < 8) {
setRelay(relayIndex, state, "web");
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(400, "application/json", "{\"error\":\"Relé inválido\"}");
}
} else {
server.send(400, "application/json", "{\"error\":\"Parâmetros faltando\"}");
}
}
void handleStatus() {
StaticJsonDocument<512> json;
JsonArray relays = json.createNestedArray("relays");
for (int i = 0; i < 8; i++) {
JsonObject relay = relays.createNestedObject();
relay["index"] = i;
relay["name"] = relayNames[i];
relay["state"] = relayStates[i];
}
json["wifi_rssi"] = WiFi.RSSI();
json["mqtt_connected"] = mqttClient.connected();
json["uptime"] = millis() / 1000;
String jsonString;
serializeJson(json, jsonString);
server.send(200, "application/json", jsonString);
}
String generateHTML() {
String html = R"(
<!DOCTYPE html>
<html>
<head>
<title>Controle de Relés ESP32</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="container">
<h1>🏠 Controle de Relés</h1>
<div class="status-bar">
<span class="status-item">📶 Wi-Fi: <span id="wifi-status">)</span></span>
<span class="status-item">🔗 MQTT: <span id="mqtt-status">)</span></span>
<span class="status-item">⏱️ Uptime: <span id="uptime">)</span></span>
</div>
<div class="relay-grid">
)";
for (int i = 0; i < 8; i++) {
html += "<div class=\"relay-card\">";
html += "<h3>" + relayNames[i] + "</h3>";
html += "<div class=\"relay-status " + String(relayStates[i] ? "on" : "off") + "\">";
html += relayStates[i] ? "🟢 LIGADO" : "🔴 DESLIGADO";
html += "</div>";
html += "<div class=\"relay-controls\">";
html += "<button onclick=\"toggleRelay(" + String(i) + ")\" class=\"btn-toggle\">";
html += relayStates[i] ? "DESLIGAR" : "LIGAR";
html += "</button>";
html += "</div>";
html += "</div>";
}
html += R"(
</div>
<div class="controls">
<button onclick="allRelays(true)" class="btn-all btn-on">🔥 LIGAR TODOS</button>
<button onclick="allRelays(false)" class="btn-all btn-off">❄️ DESLIGAR TODOS</button>
<button onclick="updateStatus()" class="btn-refresh">🔄 ATUALIZAR</button>
</div>
</div>
<script>
function toggleRelay(index) {
const currentState = getRelayState(index);
fetch('/relay', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `relay=${index}&state=${currentState ? 0 : 1}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
setTimeout(updateStatus, 200);
}
});
}
function allRelays(state) {
for (let i = 0; i < 8; i++) {
fetch('/relay', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `relay=${i}&state=${state ? 1 : 0}`
});
}
setTimeout(updateStatus, 500);
}
function updateStatus() {
fetch('/status')
.then(response => response.json())
.then(data => {
// Atualizar status dos relés
data.relays.forEach((relay, index) => {
updateRelayDisplay(index, relay.state);
});
// Atualizar informações do sistema
document.getElementById('wifi-status').textContent = data.wifi_rssi + ' dBm';
document.getElementById('mqtt-status').textContent = data.mqtt_connected ? 'Conectado' : 'Desconectado';
document.getElementById('uptime').textContent = formatUptime(data.uptime);
});
}
function updateRelayDisplay(index, state) {
const cards = document.querySelectorAll('.relay-card');
const card = cards[index];
const statusDiv = card.querySelector('.relay-status');
const button = card.querySelector('.btn-toggle');
statusDiv.className = `relay-status ${state ? 'on' : 'off'}`;
statusDiv.textContent = state ? '🟢 LIGADO' : '🔴 DESLIGADO';
button.textContent = state ? 'DESLIGAR' : 'LIGAR';
}
function getRelayState(index) {
const cards = document.querySelectorAll('.relay-card');
const statusDiv = cards[index].querySelector('.relay-status');
return statusDiv.classList.contains('on');
}
function formatUptime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`;
}
// Atualizar status a cada 5 segundos
setInterval(updateStatus, 5000);
updateStatus(); // Primeira execução
</script>
</body>
</html>
)";
return html;
}
void handleCSS() {
String css = R"(
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2.5em;
}
.status-bar {
display: flex;
justify-content: space-around;
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.status-item {
font-weight: bold;
color: #666;
}
.relay-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.relay-card {
background: #fff;
border: 2px solid #e9ecef;
border-radius: 15px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.relay-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.relay-card h3 {
color: #333;
margin-bottom: 15px;
font-size: 1.2em;
}
.relay-status {
padding: 10px 20px;
border-radius: 25px;
margin-bottom: 15px;
font-weight: bold;
font-size: 1.1em;
}
.relay-status.on {
background: #d4edda;
color: #155724;
border: 2px solid #c3e6cb;
}
.relay-status.off {
background: #f8d7da;
color: #721c24;
border: 2px solid #f5c6cb;
}
button {
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin: 5px;
}
.btn-toggle {
background: #007bff;
color: white;
font-size: 1em;
min-width: 120px;
}
.btn-toggle:hover {
background: #0056b3;
transform: scale(1.05);
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
.btn-all {
font-size: 1.1em;
padding: 15px 30px;
}
.btn-on {
background: #28a745;
color: white;
}
.btn-on:hover {
background: #1e7e34;
}
.btn-off {
background: #dc3545;
color: white;
}
.btn-off:hover {
background: #c82333;
}
.btn-refresh {
background: #17a2b8;
color: white;
}
.btn-refresh:hover {
background: #138496;
}
@media (max-width: 768px) {
.container {
margin: 10px;
padding: 20px;
}
h1 {
font-size: 2em;
}
.relay-grid {
grid-template-columns: 1fr;
}
.status-bar {
flex-direction: column;
gap: 10px;
}
}
)";
server.send(200, "text/css", css);
}
void setRelay(int index, bool state, String source) {
if (index >= 0 && index < 8) {
relayStates[index] = state;
digitalWrite(relayPins[index], !state); // Lógica invertida
// Salvar estado na memória
preferences.putBool("relay_" + String(index), state);
// Publicar no MQTT
publishMQTT(index, state);
Serial.println("Relé " + String(index) + " (" + relayNames[index] + ") " +
(state ? "LIGADO" : "DESLIGADO") + " via " + source);
}
}
void loadRelayStates() {
for (int i = 0; i < 8; i++) {
relayStates[i] = preferences.getBool("relay_" + String(i), false);
digitalWrite(relayPins[i], !relayStates[i]);
}
Serial.println("Estados dos relés carregados da memória");
}
void checkButtons() {
for (int i = 0; i < 8; i++) {
bool currentState = digitalRead(buttonPins[i]);
if (currentState != buttonStates[i] && currentState == LOW) {
// Botão pressionado (borda de descida)
setRelay(i, !relayStates[i], "button");
delay(200); // Debounce adicional
}
buttonStates[i] = currentState;
}
}
void updateStatusLEDs() {
// LED Wi-Fi
digitalWrite(wifiLED, WiFi.status() == WL_CONNECTED);
// LED MQTT
digitalWrite(mqttLED, mqttClient.connected());
}
void reconnectMQTT() {
if (millis() - lastMqttReconnect > 5000) {
Serial.print("Tentando conectar MQTT...");
String clientId = "ESP32_Relay_" + WiFi.macAddress();
if (mqttClient.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
Serial.println("MQTT conectado!");
// Subscrever nos tópicos de comando
for (int i = 0; i < 8; i++) {
String topic = "homeassistant/switch/relay" + String(i) + "/command";
mqttClient.subscribe(topic.c_str());
}
// Publicar descoberta automática do Home Assistant
publishDiscovery();
} else {
Serial.print("Falha na conexão MQTT. Estado: ");
Serial.println(mqttClient.state());
}
lastMqttReconnect = millis();
}
}
void mqttCallback(char* topic, byte* payload, unsigned int length) {
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
// Extrair número do relé do tópico
String topicStr = String(topic);
int relayIndex = -1;
for (int i = 0; i < 8; i++) {
if (topicStr.indexOf("relay" + String(i)) > 0) {
relayIndex = i;
break;
}
}
if (relayIndex >= 0) {
bool state = (message == "ON");
setRelay(relayIndex, state, "mqtt");
}
}
void publishMQTT(int relayIndex, bool state) {
if (mqttClient.connected()) {
String topic = "homeassistant/switch/relay" + String(relayIndex) + "/state";
String payload = state ? "ON" : "OFF";
mqttClient.publish(topic.c_str(), payload.c_str(), true);
}
}
void publishDiscovery() {
for (int i = 0; i < 8; i++) {
StaticJsonDocument<512> config;
config["name"] = relayNames[i];
config["unique_id"] = "esp32_relay_" + String(i);
config["command_topic"] = "homeassistant/switch/relay" + String(i) + "/command";
config["state_topic"] = "homeassistant/switch/relay" + String(i) + "/state";
config["payload_on"] = "ON";
config["payload_off"] = "OFF";
config["retain"] = true;
JsonObject device = config.createNestedObject("device");
device["name"] = "ESP32 Relay Controller";
device["model"] = "ESP32 8-Channel Relay";
device["manufacturer"] = "DIY Smart Home";
device["identifiers"] = WiFi.macAddress();
String configTopic = "homeassistant/switch/relay" + String(i) + "/config";
String configPayload;
serializeJson(config, configPayload);
mqttClient.publish(configTopic.c_str(), configPayload.c_str(), true);
}
Serial.println("Configuração de descoberta MQTT publicada");
}
Projeto 2: Sensor de Temperatura e Umidade Inteligente
Funcionalidades do Projeto
Recursos Principais:
- Múltiplos sensores (DHT22, DS18B20, BME280)
- Display OLED para visualização local
- Histórico de dados interno
- Alertas personalizáveis
- Dashboard web em tempo real
Código do Sensor Inteligente
#include <WiFi.h>
#include <WebServer.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// Configurações de rede
const char* ssid = "SUA_REDE_WIFI";
const char* password = "SUA_SENHA_WIFI";
const char* mqtt_server = "192.168.1.100";
// Pinos dos sensores
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define ONE_WIRE_BUS 5
#define BUZZER_PIN 18
#define LED_PIN 2
// Display OLED
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
// Objetos dos sensores
DHT dht(DHT_PIN, DHT_TYPE);
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_BME280 bme;
WebServer server(80);
WiFiClient espClient;
PubSubClient mqttClient(espClient);
// Estrutura para armazenar leituras
struct SensorReading {
float temperature;
float humidity;
float pressure;
float tempDS18B20;
unsigned long timestamp;
};
// Array para histórico (últimas 144 leituras = 24h com 10min interval)
SensorReading readings[144];
int readingIndex = 0;
bool bufferFull = false;
// Configurações de alerta
float tempMin = 15.0;
float tempMax = 35.0;
float humidityMin = 30.0;
float humidityMax = 70.0;
unsigned long lastReading = 0;
unsigned long lastDisplay = 0;
unsigned long lastMQTT = 0;
void setup() {
Serial.begin(115200);
// Inicializar hardware
pinMode(LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
// Inicializar sensores
dht.begin();
sensors.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("Falha ao inicializar display OLED");
}
if (!bme.begin(0x76)) {
Serial.println("Sensor BME280 não encontrado");
}
// Configurar display
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.display();
// Conectar Wi-Fi
connectWiFi();
// Configurar servidor web
setupWebServer();
// Configurar MQTT
mqttClient.setServer(mqtt_server, 1883);
Serial.println("Sistema sensor iniciado!");
}
void loop() {
server.handleClient();
if (!mqttClient.connected()) {
reconnectMQTT();
}
mqttClient.loop();
// Leitura dos sensores a cada 10 segundos
if (millis() - lastReading > 10000) {
readSensors();
lastReading = millis();
}
// Atualizar display a cada 2 segundos
if (millis() - lastDisplay > 2000) {
updateDisplay();
lastDisplay = millis();
}
// Publicar MQTT a cada 30 segundos
if (millis() - lastMQTT > 30000) {
publishSensorData();
lastMQTT = millis();
}
}
void readSensors() {
SensorReading reading;
reading.timestamp = millis();
// Ler DHT22
reading.humidity = dht.readHumidity();
reading.temperature = dht.readTemperature();
// Ler DS18B20
sensors.requestTemperatures();
reading.tempDS18B20 = sensors.getTempCByIndex(0);
// Ler BME280
reading.pressure = bme.readPressure() / 100.0F; // hPa
// Verificar se as leituras são válidas
if (isnan(reading.humidity) || isnan(reading.temperature)) {
Serial.println("Falha na leitura do DHT22");
return;
}
// Armazenar no histórico
readings[readingIndex] = reading;
readingIndex = (reading
