Brincando com ESP32 C3 para criar uma tranca eletrônica
Rapidamente desenvolvi este código experimental para implementar uma tranca eletrônica, de baixo custo. Sem muita explicação, o código a seguir cria um ponto de acesso wi-fi, e permite também se conectar a uma rede wi-fi. Acessando a porta 80, após conectado na rede, acessamos um painel com senha. A ação principal é trancar/destrancar. Outros 4 pinos podem ser configurados para outras funcionalidades.
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <esp_wifi.h>
// Objeto para persistência de dados
Preferences preferences;
// Variáveis de Configuração de Rede
String ap_ssid, ap_pass;
String sta_ssid, sta_pass;
// Variáveis do Sistema (Porta Principal)
String panelPassword;
int pino_a, pino_b, tempo_acionamento;
bool portaTrancada = true;
// Variáveis de Pinos Dinâmicos (Até 4 extras)
int dyn_pins[4] = {-1, -1, -1, -1};
int dyn_modes[4] = {0, 0, 0, 0}; // 0 = Desativado, 1 = Leitura, 2 = Gravação
String dyn_names[4];
// Tema e Redes
int tema_ativo = 1; // 1=Dark, 2=Ocean, 3=Cyberpunk, 4=Frost
bool redeConectada = false;
unsigned long ultimoLogTempo = 0;
WebServer server(80);
// ==========================================================
// FUNÇÕES AUXILIARES
// ==========================================================
bool isAuthentic() {
if (server.hasHeader("Cookie")) {
String cookie = server.header("Cookie");
if (cookie.indexOf("ESPSESSIONID=1") != -1) return true;
}
return false;
}
void configurarPinos() {
// Porta Principal
if(pino_a >= 0) { pinMode(pino_a, OUTPUT); digitalWrite(pino_a, LOW); }
if(pino_b >= 0) { pinMode(pino_b, OUTPUT); digitalWrite(pino_b, LOW); }
// Pinos Dinâmicos
for(int i = 0; i < 4; i++) {
if(dyn_pins[i] >= 0) {
if(dyn_modes[i] == 1) { // Leitura
pinMode(dyn_pins[i], INPUT);
} else if (dyn_modes[i] == 2) { // Gravação
pinMode(dyn_pins[i], OUTPUT);
digitalWrite(dyn_pins[i], LOW); // Inicia desligado
}
}
}
}
void notificarIP() {
if (WiFi.status() == WL_CONNECTED) {
String ip = WiFi.localIP().toString();
String url = "https://apps.ci.dev.br/?ip=" + ip;
WiFiClientSecure *client = new WiFiClientSecure;
if (client) {
client->setInsecure();
HTTPClient https;
Serial.println("[HTTP] Notificando: " + url);
if (https.begin(*client, url)) {
int httpCode = https.GET();
Serial.printf("[HTTP] Resposta: %d\n", httpCode);
https.end();
}
delete client;
}
}
}
// ==========================================================
// RENDERIZAÇÃO HTML E CSS (GLASS & TEMAS)
// ==========================================================
String getCSS() {
// Utilizando raw string literal para escrever CSS puro sem se preocupar com escapes
return R"raw(
<style>
/* Variáveis Base */
:root {
--bg: linear-gradient(135deg, #121212, #1e1e1e);
--glass-bg: rgba(255, 255, 255, 0.04);
--glass-border: rgba(255, 255, 255, 0.08);
--text: #eeeeee;
--text-muted: #aaaaaa;
--primary: #4caf50;
--danger: #e53935;
--input-bg: rgba(0, 0, 0, 0.3);
--item-bg: rgba(0, 0, 0, 0.2);
}
/* Temas */
body.t1 { /* Dark Glass Padrão */ }
body.t2 { /* Ocean Glass */
--bg: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
--glass-bg: rgba(255, 255, 255, 0.08);
--primary: #00bcd4; --danger: #ff5252;
}
body.t3 { /* Cyberpunk Glass */
--bg: linear-gradient(135deg, #2b0f4c, #150a21);
--glass-bg: rgba(200, 50, 255, 0.05);
--glass-border: rgba(213, 0, 249, 0.3);
--primary: #d500f9; --danger: #ff1744;
}
body.t4 { /* Frost Light Glass */
--bg: linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%);
--glass-bg: rgba(255, 255, 255, 0.4);
--glass-border: rgba(255, 255, 255, 0.5);
--text: #222; --text-muted: #555;
--primary: #6200ea; --danger: #d50000;
--input-bg: rgba(255, 255, 255, 0.6);
--item-bg: rgba(255, 255, 255, 0.3);
}
/* Estilos Globais Minimalistas */
body { font-family: 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); max-width: 420px; margin: 20px auto; padding: 0 15px; min-height: 100vh; }
nav { display: flex; justify-content: space-between; border-bottom: 1px solid var(--glass-border); padding-bottom: 15px; margin-bottom: 20px; }
nav a { color: var(--primary); text-decoration: none; font-weight: bold; font-size: 13px; text-transform: uppercase; letter-spacing: 1px; }
.card { background: var(--glass-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--glass-border); padding: 25px; border-radius: 16px; margin-bottom: 20px; box-shadow: 0 8px 32px rgba(0,0,0,0.15); }
h2, h3 { margin-top: 0; color: var(--text); font-weight: 500; }
p { margin: 5px 0 15px 0; font-size: 14px; color: var(--text-muted); line-height: 1.4; }
label { font-size: 11px; color: var(--text-muted); display: block; margin-top: 15px; text-transform: uppercase; font-weight: bold; letter-spacing: 1px;}
input, select { width: 100%; box-sizing: border-box; padding: 12px; margin-top: 5px; background: var(--input-bg); border: 1px solid var(--glass-border); color: var(--text); border-radius: 8px; outline: none; font-size: 15px; transition: 0.3s;}
input:focus, select:focus { border-color: var(--primary); box-shadow: 0 0 8px rgba(0,0,0,0.2); }
button { width: 100%; padding: 14px; background: var(--primary); color: #fff; border: none; border-radius: 8px; cursor: pointer; margin-top: 15px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; transition: 0.3s; }
button:hover { filter: brightness(1.1); transform: translateY(-2px); }
button.btn-danger { background: var(--danger); }
ul { list-style: none; padding: 0; margin: 0; }
li { background: var(--item-bg); padding: 12px; margin-top: 8px; border-radius: 8px; border: 1px solid var(--glass-border); font-size: 14px; display: flex; justify-content: space-between; align-items: center;}
a { text-decoration: none; color: inherit; }
.badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: bold; color: #fff; }
.badge.high { background: var(--primary); }
.badge.low { background: var(--text-muted); color: #222;}
.row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--glass-border); }
.row:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
</style>
)raw";
}
String getHeader(String titulo) {
String html = "<!DOCTYPE html><html lang='pt-BR'><head><meta charset='UTF-8'>";
html += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
html += "<title>" + titulo + "</title>";
html += getCSS();
html += "</head><body class='t" + String(tema_ativo) + "'>";
return html;
}
String getNav() {
return "<nav><a href='/'>Início</a> <a href='/dashboard'>Rede</a> <a href='/sistema'>Sistema</a> <a href='/logout'>Sair</a></nav>";
}
// ==========================================================
// ROTAS DO SERVIDOR WEB
// ==========================================================
void handleLoginGet() {
String html = getHeader("Acesso Restrito");
html += "<div class='card'><h2>Acesso Restrito</h2><form action='/login' method='POST'>";
html += "<label>Senha de Acesso</label><input type='password' name='senha' required>";
html += "<button type='submit'>Entrar</button></form></div></body></html>";
server.send(200, "text/html", html);
}
void handleLoginPost() {
if (server.hasArg("senha") && server.arg("senha") == panelPassword) {
server.sendHeader("Location", "/");
server.sendHeader("Cache-Control", "no-cache");
server.sendHeader("Set-Cookie", "ESPSESSIONID=1");
server.send(301);
} else {
String html = getHeader("Erro de Acesso");
html += "<div class='card'><h2 style='color:var(--danger)'>Falha no Login</h2><p>A senha informada está incorreta.</p><a href='/'><button>Voltar</button></a></div></body></html>";
server.send(200, "text/html", html);
}
}
void handleLogout() {
server.sendHeader("Location", "/");
server.sendHeader("Cache-Control", "no-cache");
server.sendHeader("Set-Cookie", "ESPSESSIONID=0");
server.send(301);
}
void handleRoot() {
if (!isAuthentic()) return handleLoginGet();
String html = getHeader("Painel de Controle") + getNav();
// Card da Porta Principal
html += "<div class='card'><h2>Controle da Porta</h2>";
html += "<p>Status atual: <strong style='color:" + String(portaTrancada ? "var(--danger)" : "var(--primary)") + "'>" + String(portaTrancada ? "TRANCADA" : "ABERTA") + "</strong></p>";
html += "<form action='/action' method='POST'>";
if (portaTrancada) {
html += "<button name='cmd' value='abrir'>Destrancar Porta</button>";
} else {
html += "<button name='cmd' value='trancar' class='btn-danger'>Trancar Porta</button>";
}
html += "</form></div>";
// Verifica se há pinos dinâmicos ativos
bool temDinamico = false;
for(int i=0; i<4; i++) if(dyn_modes[i] != 0 && dyn_pins[i] >= 0) temDinamico = true;
if(temDinamico) {
html += "<div class='card'><h3>Outros Controles</h3>";
for(int i=0; i<4; i++) {
if(dyn_modes[i] == 0 || dyn_pins[i] < 0) continue;
html += "<div class='row'><div><strong>" + dyn_names[i] + "</strong><br><span style='font-size:11px;color:var(--text-muted)'>Pino " + String(dyn_pins[i]) + "</span></div>";
if(dyn_modes[i] == 1) { // Leitura
int val = digitalRead(dyn_pins[i]);
html += "<span class='badge " + String(val ? "high" : "low") + "'>" + String(val ? "ALTO" : "BAIXO") + "</span>";
} else if (dyn_modes[i] == 2) { // Gravação
int val = digitalRead(dyn_pins[i]);
html += "<form action='/dyn_action' method='POST' style='margin:0;'>";
html += "<input type='hidden' name='idx' value='" + String(i) + "'>";
html += "<button type='submit' style='width:auto; margin:0; padding:8px 16px; font-size:12px;' class='" + String(val ? "btn-danger" : "") + "'>";
html += String(val ? "Desligar" : "Ligar") + "</button></form>";
}
html += "</div>";
}
html += "</div>";
}
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleDashboard() {
if (!isAuthentic()) { server.sendHeader("Location", "/"); server.send(301); return; }
String html = getHeader("Rede e Dashboard") + getNav();
// Status de Rede
html += "<div class='card'><h3>Status da Rede</h3>";
html += "<label>MAC Address</label><p>" + WiFi.macAddress() + "</p>";
html += "<label>IP Ponto de Acesso</label><p>" + WiFi.softAPIP().toString() + "</p>";
html += "<label>Conexão Wi-Fi (Roteador)</label>";
if (WiFi.status() == WL_CONNECTED) {
html += "<p style='color:var(--primary)'>Conectado em <b>" + sta_ssid + "</b></p>";
html += "<label>IP Local</label><p>" + WiFi.localIP().toString() + "</p>";
} else {
html += "<p style='color:var(--danger)'>Desconectado</p>";
}
html += "<div style='margin-top:20px;'><a href='/form_rede'><button>Configurar Conexões</button></a></div>";
html += "</div>";
// Scanner de Wi-Fi
html += "<div class='card'><h3>Redes Próximas</h3>";
int n = WiFi.scanNetworks();
html += "<ul>";
if (n == 0) html += "<li>Nenhuma rede encontrada</li>";
else {
for (int i = 0; i < n; ++i) {
String encrypt = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? "Aberta" : "Protegida";
html += "<a href='/form_rede?ssid=" + WiFi.SSID(i) + "'><li>";
html += "<div><b>" + WiFi.SSID(i) + "</b><br><span style='font-size:12px;color:var(--text-muted)'>" + encrypt + "</span></div>";
html += "<span class='badge " + String(WiFi.RSSI(i) > -70 ? "high" : "low") + "'>" + String(WiFi.RSSI(i)) + " dBm</span>";
html += "</li></a>";
}
}
html += "</ul></div>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleFormRede() {
if (!isAuthentic()) { server.sendHeader("Location", "/"); server.send(301); return; }
String prefill_ssid = server.hasArg("ssid") ? server.arg("ssid") : sta_ssid;
String html = getHeader("Configurar Rede") + getNav();
html += "<div class='card'><form action='/save_rede' method='POST'><h3>Conectar Wi-Fi</h3>";
html += "<label>Nome da Rede Wi-Fi (SSID)</label><input type='text' name='sta_ssid' value='" + prefill_ssid + "'>";
html += "<label>Senha Wi-Fi</label><input type='password' name='sta_pass' value='" + sta_pass + "'>";
html += "<h3 style='margin-top:30px'>Ponto de Acesso Local (AP)</h3>";
html += "<label>Nome da Rede Local</label><input type='text' name='ap_ssid' value='" + ap_ssid + "' required>";
html += "<label>Senha (mín. 8 dígitos)</label><input type='password' name='ap_pass' value='" + ap_pass + "'>";
html += "<button type='submit'>Salvar e Reiniciar</button></form></div></body></html>";
server.send(200, "text/html", html);
}
void handleSistema() {
if (!isAuthentic()) { server.sendHeader("Location", "/"); server.send(301); return; }
String html = getHeader("Configurações do Sistema") + getNav();
html += "<div class='card'><form action='/save_sistema' method='POST'>";
html += "<h3>Aparência</h3>";
html += "<label>Tema Visual</label><select name='tema'>";
html += "<option value='1' " + String(tema_ativo==1?"selected":"") + ">Dark Glass (Padrão)</option>";
html += "<option value='2' " + String(tema_ativo==2?"selected":"") + ">Ocean Glass (Azul)</option>";
html += "<option value='3' " + String(tema_ativo==3?"selected":"") + ">Cyberpunk Glass (Roxo)</option>";
html += "<option value='4' " + String(tema_ativo==4?"selected":"") + ">Frost Glass (Claro)</option>";
html += "</select>";
html += "<h3 style='margin-top:30px'>Porta Principal</h3>";
html += "<label>Nova Senha do Painel</label><input type='password' name='nova_senha' placeholder='Deixe em branco para manter'>";
html += "<div style='display:flex;gap:10px;'>";
html += "<div style='width:100%'><label>Pino A (Abrir)</label><input type='number' name='pino_a' value='" + String(pino_a) + "'></div>";
html += "<div style='width:100%'><label>Pino B (Trancar)</label><input type='number' name='pino_b' value='" + String(pino_b) + "'></div>";
html += "</div>";
html += "<label>Tempo de Acionamento (ms)</label><input type='number' name='tempo' value='" + String(tempo_acionamento) + "'>";
html += "<h3 style='margin-top:30px'>Controles Personalizados</h3>";
html += "<p>Defina outros pinos para ler sensores ou controlar relés.</p>";
for(int i=0; i<4; i++) {
html += "<div style='border-left: 3px solid var(--primary); padding-left: 10px; margin-bottom: 20px;'>";
html += "<label>Controle Extra " + String(i+1) + "</label>";
html += "<input type='text' name='d_n" + String(i) + "' value='" + dyn_names[i] + "' placeholder='Nome de Exibição (Ex: Luz da Sala)'>";
html += "<div style='display:flex;gap:10px;'>";
html += "<div style='width:100%'><label>Modo</label><select name='d_m" + String(i) + "'>";
html += "<option value='0' " + String(dyn_modes[i]==0?"selected":"") + ">Desativado</option>";
html += "<option value='1' " + String(dyn_modes[i]==1?"selected":"") + ">Leitura (Sensor)</option>";
html += "<option value='2' " + String(dyn_modes[i]==2?"selected":"") + ">Gravação (Relé/LED)</option>";
html += "</select></div>";
String p_val = dyn_pins[i] >= 0 ? String(dyn_pins[i]) : "";
html += "<div style='width:100%'><label>GPIO (Pino)</label><input type='number' name='d_p" + String(i) + "' value='" + p_val + "' placeholder='Ex: 5'></div>";
html += "</div></div>";
}
html += "<button type='submit'>Salvar Sistema</button></form></div></body></html>";
server.send(200, "text/html", html);
}
// Ações dinâmicas (Gravação de pinos extras)
void handleDynAction() {
if (!isAuthentic()) return;
if (server.hasArg("idx")) {
int idx = server.arg("idx").toInt();
if(idx >= 0 && idx < 4 && dyn_modes[idx] == 2 && dyn_pins[idx] >= 0) {
bool estadoSendoInvertido = !digitalRead(dyn_pins[idx]);
digitalWrite(dyn_pins[idx], estadoSendoInvertido);
}
}
server.sendHeader("Location", "/");
server.send(303);
}
void handleAction() {
if (!isAuthentic()) return;
if (server.hasArg("cmd")) {
String cmd = server.arg("cmd");
if (cmd == "abrir" && pino_a >= 0) {
portaTrancada = false;
digitalWrite(pino_a, HIGH); delay(tempo_acionamento); digitalWrite(pino_a, LOW);
} else if (cmd == "trancar" && pino_b >= 0) {
portaTrancada = true;
digitalWrite(pino_b, HIGH); delay(tempo_acionamento); digitalWrite(pino_b, LOW);
}
}
server.sendHeader("Location", "/");
server.send(303);
}
void handleSaveSistema() {
if (!isAuthentic()) return;
if (server.hasArg("tema")) { tema_ativo = server.arg("tema").toInt(); preferences.putInt("tema", tema_ativo); }
if (server.hasArg("pino_a")) { pino_a = server.arg("pino_a").length() > 0 ? server.arg("pino_a").toInt() : -1; preferences.putInt("pino_a", pino_a); }
if (server.hasArg("pino_b")) { pino_b = server.arg("pino_b").length() > 0 ? server.arg("pino_b").toInt() : -1; preferences.putInt("pino_b", pino_b); }
if (server.hasArg("tempo")) { tempo_acionamento = server.arg("tempo").toInt(); preferences.putInt("tempo", tempo_acionamento); }
if (server.hasArg("nova_senha") && server.arg("nova_senha").length() > 0) {
panelPassword = server.arg("nova_senha");
preferences.putString("senha", panelPassword);
}
for(int i=0; i<4; i++) {
if(server.hasArg("d_n"+String(i))) { dyn_names[i] = server.arg("d_n"+String(i)); preferences.putString(("d_n"+String(i)).c_str(), dyn_names[i]); }
if(server.hasArg("d_m"+String(i))) { dyn_modes[i] = server.arg("d_m"+String(i)).toInt(); preferences.putInt(("d_m"+String(i)).c_str(), dyn_modes[i]); }
if(server.hasArg("d_p"+String(i))) {
String p = server.arg("d_p"+String(i));
dyn_pins[i] = p.length() > 0 ? p.toInt() : -1;
preferences.putInt(("d_p"+String(i)).c_str(), dyn_pins[i]);
}
}
configurarPinos();
server.sendHeader("Location", "/sistema");
server.send(303);
}
void handleSaveRede() {
if (!isAuthentic()) return;
preferences.putString("sta_ssid", server.arg("sta_ssid"));
preferences.putString("sta_pass", server.arg("sta_pass"));
preferences.putString("ap_ssid", server.arg("ap_ssid"));
preferences.putString("ap_pass", server.arg("ap_pass"));
String html = getHeader("Reiniciando...");
html += "<div class='card'><h2>Aplicando Redes...</h2><p>O dispositivo está sendo reiniciado para conectar às novas redes configuradas.</p></div></body></html>";
server.send(200, "text/html", html);
delay(1000);
ESP.restart();
}
// ==========================================================
// SETUP & LOOP
// ==========================================================
void setup() {
Serial.begin(115200);
// Inicializa Memória Flash e Resgata Dados Globais
preferences.begin("config", false);
tema_ativo = preferences.getInt("tema", 1);
panelPassword = preferences.getString("senha", "123456@");
pino_a = preferences.getInt("pino_a", 3);
pino_b = preferences.getInt("pino_b", 4);
tempo_acionamento = preferences.getInt("tempo", 1000);
ap_ssid = preferences.getString("ap_ssid", "minharede");
ap_pass = preferences.getString("ap_pass", "123456789");
sta_ssid = preferences.getString("sta_ssid", "");
sta_pass = preferences.getString("sta_pass", "");
// Resgata Pinos Dinâmicos
for(int i=0; i<4; i++) {
dyn_pins[i] = preferences.getInt(("d_p"+String(i)).c_str(), -1);
dyn_modes[i] = preferences.getInt(("d_m"+String(i)).c_str(), 0);
dyn_names[i] = preferences.getString(("d_n"+String(i)).c_str(), "Controle " + String(i+1));
}
configurarPinos();
// Configuração Wi-Fi (Modo Misto)
WiFi.mode(WIFI_AP_STA);
Serial.println("\n[WIFI] Configurando Access Point...");
WiFi.softAP(ap_ssid.c_str(), ap_pass.c_str());
Serial.print("[WIFI] AP IP: ");
Serial.println(WiFi.softAPIP());
if (sta_ssid.length() > 0) {
Serial.println("[WIFI] Tentando conectar na rede: " + sta_ssid);
WiFi.begin(sta_ssid.c_str(), sta_pass.c_str());
}
const char * headerkeys[] = {"Cookie"};
server.collectHeaders(headerkeys, 1);
// Rotas
server.on("/", HTTP_GET, handleRoot);
server.on("/login", HTTP_POST, handleLoginPost);
server.on("/logout", HTTP_GET, handleLogout);
server.on("/action", HTTP_POST, handleAction);
server.on("/dyn_action", HTTP_POST, handleDynAction);
server.on("/dashboard", HTTP_GET, handleDashboard);
server.on("/form_rede", HTTP_GET, handleFormRede);
server.on("/sistema", HTTP_GET, handleSistema);
server.on("/save_rede", HTTP_POST, handleSaveRede);
server.on("/save_sistema", HTTP_POST, handleSaveSistema);
server.begin();
Serial.println("Servidor HTTP iniciado.");
}
void loop() {
server.handleClient();
unsigned long tempoAtual = millis();
if (tempoAtual - ultimoLogTempo >= 5000) {
ultimoLogTempo = tempoAtual;
if (sta_ssid.length() > 0) {
if (WiFi.status() == WL_CONNECTED) {
if (!redeConectada) {
notificarIP();
redeConectada = true;
}
} else {
redeConectada = false;
}
}
}
}
Comentários
Postar um comentário