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

Postagens mais visitadas deste blog

Procêmica: A Linguagem Silenciosa do Espaço

Roadmap do Programador Iniciante: Do Zero ao Primeiro Código

Usenet: A Rede de Usuários Que Plantou as Sementes da Internet Moderna