// Projeto: IR Learn & Send — ESP32 (versão estendida: Clonar/Usar Controle com Confirmação Web)
// Bibliotecas necessárias: IRremoteESP8266, WebServer, Preferences
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <IRremoteESP8266.h>
#include <IRrecv.h>
#include <IRsend.h>
#include <IRutils.h>
#include <vector>
#include <map>
/////////////////////// CONFIG ///////////////////////
// >>>>> ALTERE AQUI as credenciais da sua rede Wi-Fi <<<<<
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const uint16_t RECV_PIN = 15; // pino do receptor IR
const uint16_t SEND_PIN = 2; // pino do emissor IR (LED)
WebServer server(80);
Preferences prefs;
IRrecv irrecv(RECV_PIN);
IRsend irsend(SEND_PIN);
decode_results results;
// Estrutura para os códigos que queremos aprender (e as chaves de armazenamento)
const std::vector<String> COMMAND_KEYS = {
"Ligar", "Desligar", "ModoGelar",
"Temp18", "Temp19", "Temp20", "Temp21",
"Temp22", "Temp23", "Temp24"
};
// Mapa para armazenar os códigos RAW aprendidos (chave -> RAW code)
std::map<String, std::vector<uint16_t>> learnedCodes;
int lastFreq = 38; // Frequência típica (38kHz)
// Estado de clonagem: -1=Inativo, 0..N = Índice do COMMAND_KEYS sendo aprendido
int cloningState = -1; // -1: Inativo, 0..N: Passo atual, 99: Finalizado
// NOVO: Armazenamento temporário para o último RAW recebido, aguardando confirmação (gravação)
std::vector<uint16_t> tempRaw;
///////////////////// HELPERS /////////////////////////
// Salva um código RAW específico em Preferences
void saveCodeToPrefs(const String& key, const std::vector<uint16_t>& arr) {
// Adiciona "_raw" à chave antes de salvar para evitar conflitos
String key_s = key + "_raw";
String s;
// Otimização de memória para strings grandes
s.reserve(arr.size() * 6);
for (size_t i = 0; i < arr.size(); ++i) {
if (i) s += ',';
s += String(arr[i]);
}
prefs.putString(key_s.c_str(), s);
}
// Restaura todos os códigos RAW de Preferences
void loadCodesFromPrefs() {
learnedCodes.clear();
lastFreq = prefs.getUInt("ir_freq", 38);
for (const String& key : COMMAND_KEYS) {
String key_s = key + "_raw";
if (prefs.isKey(key_s.c_str())) {
String s = prefs.getString(key_s.c_str(), "");
std::vector<uint16_t> out;
int start = 0;
// Deserializa a string de pulsos separados por vírgula para o vetor
while (start < (int)s.length()) {
int comma = s.indexOf(',', start);
if (comma == -1) comma = s.length();
String part = s.substring(start, comma);
out.push_back((uint16_t)part.toInt());
start = comma + 1;
}
learnedCodes[key] = out;
Serial.printf("Carregado código '%s', len=%d\n", key.c_str(), (int)out.size());
}
}
}
// Converte decode_results rawbuf -> vector<uint16_t>
void copyResultsRaw(decode_results &r, std::vector<uint16_t>& out) {
out.clear();
for (uint16_t i = 0; i < r.rawlen; i++) {
uint16_t v = r.rawbuf[i];
out.push_back(v);
}
}
// Envia o código RAW de uma chave
void sendCode(const String& key) {
if (learnedCodes.count(key) && !learnedCodes[key].empty()) {
const std::vector<uint16_t>& raw = learnedCodes[key];
// Envia o vetor de RAW data. O cast para (uint16_t*) é necessário.
irsend.sendRaw((uint16_t*)raw.data(), raw.size(), lastFreq);
Serial.printf("Comando '%s' enviado: %d pulsos @ %d kHz\n", key.c_str(), (int)raw.size(), lastFreq);
} else {
Serial.printf("ERRO: Comando '%s' não encontrado ou vazio.\n", key.c_str());
}
}
///////////////////// WEB PAGES /////////////////////////////
// Página Inicial (Menu)
String indexPage() {
String s = "<html><head><meta charset='utf-8'><title>ESP32 IR</title>";
s += "<style>body{font-family: Arial, sans-serif; text-align: center; margin-top: 50px; background-color: #f4f4f9;} button{padding: 12px 25px; margin: 10px; font-size: 16px; border: none; border-radius: 8px; cursor: pointer; box-shadow: 0 4px #999; transition: all 0.1s;} button:hover{opacity: 0.9;} button:active{box-shadow: 0 1px #666; transform: translateY(3px);} .btn-main{background-color: #007bff; color: white;} .btn-secondary{background-color: #6c757d; color: white;}</style>";
s += "</head><body>";
s += "<h2>ESP32 IR Learner / Sender</h2>";
s += "<p>Selecione uma opção:</p>";
s += "<p><a href='/clone'><button class='btn-main'>Clonar Controle Remoto 📝</button></a></p>";
s += "<p><a href='/control'><button class='btn-main'>Usar Controle Configurado 🎮</button></a></p>";
s += "<p><a href='/status'><button class='btn-secondary'>Ver Status (JSON)</button></a></p>";
s += "</body></html>";
return s;
}
// Página de Clonagem
String clonePage() {
String s = "<html><head><meta charset='utf-8'><title>Clonar IR</title>";
s += "<style>body{font-family: Arial, sans-serif; text-align: center; margin-top: 20px; background-color: #f4f4f9;} h2{color: #007bff;} h3{color: #343a40;} .msg{padding: 15px; margin: 15px auto; border-radius: 8px; max-width: 400px;} .msg-success{border: 1px solid #28a745; background-color: #d4edda; color: #155724;} .msg-warning{border: 1px solid #ffc107; background-color: #fff3cd; color: #856404;} button{padding: 10px 20px; margin: 5px; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.2s;}</style>";
s += "</head><body>";
s += "<h2>Modo Clonagem (Aprendizado)</h2>";
// --- VARIÁVEIS DE ESTADO INJETADAS PARA JS ---
// Usamos o status atualizado do servidor, então o JS pode ler estes valores.
int tempRawLen = (int)tempRaw.size();
String currentKey = "";
if (cloningState >= 0 && cloningState < (int)COMMAND_KEYS.size()) {
currentKey = COMMAND_KEYS[cloningState];
}
s += "<input type='hidden' id='cloningState' value='" + String(cloningState) + "'>";
s += "<input type='hidden' id='tempRawLen' value='" + String(tempRawLen) + "'>";
// ---------------------------------------------
// Status atual
if (cloningState == -1) {
s += "<p>Clique em <b>Iniciar Clonagem</b> para começar a gravação sequencial dos códigos.</p>";
s += "<p><a href='/clone?action=start'><button style='background-color: #28a745; color: white;'>Iniciar Clonagem</button></a></p>";
} else if (cloningState < (int)COMMAND_KEYS.size()) {
s += "<h3>PASSO " + String(cloningState + 1) + " de " + String(COMMAND_KEYS.size()) + ":</h3>";
s += "<p>Aguardando código para <b>" + currentKey + "</b>.</p>";
if (tempRawLen > 0) {
// Sinal recebido e está aguardando confirmação
s += "<div class='msg msg-success'>";
s += "<p>✅ **Sinal Recebido!** " + String(tempRawLen) + " pulsos.</p>";
s += "<p>Pressione **GRAVAR** para salvar o código de <b>" + currentKey + "</b> e avançar.</p>";
// Botão Gravar que chama o handler /learn
s += "<a href='/learn?action=save'><button style='background-color: #007bff; color: white;'>GRAVAR CÓDIGO</button></a>";
// Opção de Cancelar o sinal atual e tentar novamente
s += " <a href='/learn?action=cancel'><button style='background-color: #ffc107; color: black;'>Tentar Novamente</button></a>";
s += "</div>";
} else {
// Esperando novo sinal IR
s += "<div class='msg msg-warning'>";
s += "<p>Aponte o controle remoto e pressione o botão <b>" + currentKey + "</b>.</p>";
s += "<p>O sinal será detectado automaticamente.</p>";
s += "</div>";
}
} else { // Clonagem finalizada (cloningState == 99)
s += "<div class='msg msg-success'>";
s += "<p>✅ **Todos os códigos foram salvos!** A clonagem foi concluída.</p>";
s += "</div>";
s += "<p><a href='/'><button style='background-color: #6c757d; color: white;'>Voltar ao Menu Principal</button></a></p>";
}
s += "<p style='margin-top: 30px;'><a href='/'><button style='background-color: #6c757d; color: white;'>Voltar ao Menu</button></a></p>";
// --- NOVO SCRIPT PARA POLLING E REFRESH AUTOMÁTICO ---
s += "<script>";
s += "const CLONE_STATE = parseInt(document.getElementById('cloningState').value);";
s += "const TEMP_RAW_LEN = parseInt(document.getElementById('tempRawLen').value);";
s += "if (CLONE_STATE != -1 && CLONE_STATE < " + String(COMMAND_KEYS.size()) + " && TEMP_RAW_LEN === 0) {";
s += " console.log('Iniciando polling...');";
s += " function checkSignal() {";
s += " fetch('/status')";
s += " .then(response => {";
s += " if (!response.ok) { throw new Error('Network response was not ok'); }";
s += " return response.json();";
s += " })";
s += " .then(data => {";
s += " if (data.tempRawLen > 0) {";
s += " console.log('Sinal IR detectado! Recarregando página.');";
s += " window.location.reload();";
s += " }";
s += " })";
s += " .catch(error => console.error('Erro no polling do status:', error));";
s += " }";
s += " const intervalId = setInterval(checkSignal, 2000);";
s += " window.addEventListener('beforeunload', () => clearInterval(intervalId));";
s += "}";
s += "</script>";
// ---------------------------------------------------
s += "</body></html>";
return s;
}
// Página do Controle Remoto (Envio de Códigos)
String controlPage() {
String s = "<html><head><meta charset='utf-8'><title>Controle IR</title>";
s += "<style>body{font-family: Arial, sans-serif; text-align: center; margin-top: 20px; background-color: #f4f4f9;} h2{color: #343a40;} .btn-grid{display:flex; flex-wrap:wrap; justify-content: center; gap:10px; margin: 20px auto; max-width: 600px;} button{padding:15px 20px; color:white; border:none; border-radius:8px; cursor:pointer; font-weight: bold; transition: all 0.2s;} button:active{transform: translateY(1px);}</style>";
s += "</head><body>";
s += "<h2>Controle Remoto</h2>";
// Corrigido para mostrar a frequência corretamente.
s += "<p>Clique para enviar o comando IR (frequência: "+String(lastFreq)+"kHz):</p>";
s += "<div id='message' style='color: green; font-weight: bold; margin-bottom: 10px;'></div>";
s += "<div class='btn-grid'>";
for (const String& key : COMMAND_KEYS) {
bool saved = learnedCodes.count(key) && !learnedCodes[key].empty();
String btnText = saved ? key : key + " (N/A)";
String color = saved ? "#28a745" : "#dc3545"; // Verde ou Vermelho
s += "<button style='background-color:"+color+";' ";
if (saved) {
// Chamada AJAX para não recarregar a página e mostrar feedback simples
s += "onclick=\"sendMessage('" + key + "')\"";
} else {
s += "disabled";
}
s += ">" + btnText + "</button>";
}
s += "</div>";
s += "<p><a href='/'><button style='margin-top:20px; background-color: #6c757d;'>Voltar ao Menu</button></a></p>";
// Script JS para enviar comandos e dar feedback
s += "<script>";
s += "function sendMessage(key) {";
s += " const msgDiv = document.getElementById('message');";
s += " msgDiv.textContent = 'Enviando ' + key + '...';";
s += " msgDiv.style.color = '#ffc107';";
s += " fetch('/send_cmd?key=' + key)";
s += " .then(response => response.text())";
s += " .then(text => {";
s += " msgDiv.textContent = '✅ ' + text;";
s += " msgDiv.style.color = 'green';";
s += " })";
s += " .catch(error => {";
s += " console.error('Erro ao enviar comando:', error);";
s += " msgDiv.textContent = '❌ Erro ao enviar comando: ' + key;";
s += " msgDiv.style.color = 'red';";
s += " });";
s += "}";
s += "</script>";
s += "</body></html>";
return s;
}
// Handler para a página principal
void handleRoot() {
server.send(200, "text/html", indexPage());
}
// Handler para a página de Clonagem
void handleClone() {
if (server.hasArg("action") && server.arg("action") == "start") {
cloningState = 0; // Inicia o processo de clonagem
tempRaw.clear(); // Limpa qualquer lixo
Serial.println("INICIANDO MODO CLONAGEM.");
}
server.send(200, "text/html", clonePage());
}
// Handler para a página de Controle
void handleControl() {
server.send(200, "text/html", controlPage());
}
// Handler para a ação de Gravar/Cancelar durante a clonagem
void handleLearn() {
if (cloningState == -1 || cloningState >= (int)COMMAND_KEYS.size()) {
server.send(400, "text/plain", "Modo clonagem inativo ou finalizado.");
return;
}
String action = server.arg("action");
String currentKey = COMMAND_KEYS[cloningState];
if (action == "save") {
if (tempRaw.empty()) {
server.send(400, "text/plain", "Nenhum código para salvar. Pressione o controle primeiro.");
return;
}
// 1. Salva no mapa (em memória)
learnedCodes[currentKey] = tempRaw;
// 2. Salva persistentemente (em Preferences)
saveCodeToPrefs(currentKey, tempRaw);
Serial.printf("✅ Código de '%s' SALVO. Raw len=%d\n", currentKey.c_str(), (int)tempRaw.size());
// 3. Limpa o tempRaw e avança para o próximo passo
tempRaw.clear();
cloningState++;
if (cloningState == (int)COMMAND_KEYS.size()) {
cloningState = 99; // Finalizado
}
// Redireciona para /clone para atualizar a interface
server.sendHeader("Location", "/clone", true);
server.send(302, "text/plain", "OK");
} else if (action == "cancel") {
tempRaw.clear(); // Limpa o buffer para o usuário tentar novamente
Serial.println("Sinal temporário cancelado. Tente novamente.");
// Redireciona para /clone para atualizar a interface
server.sendHeader("Location", "/clone", true);
server.send(302, "text/plain", "OK");
} else {
server.send(400, "text/plain", "Ação desconhecida.");
}
}
// Handler para enviar um código específico (usado pelo /control)
void handleSendCommand() {
if (!server.hasArg("key")) {
server.send(400, "text/plain", "Falta o parâmetro 'key'.");
return;
}
String key = server.arg("key");
if (!learnedCodes.count(key) || learnedCodes[key].empty()) {
server.send(400, "text/plain", "Código para '" + key + "' não encontrado.");
return;
}
sendCode(key);
// Pequeno delay para garantir o envio antes de responder à requisição web
delay(200);
server.send(200, "text/plain", "Comando '" + key + "' enviado com sucesso!");
}
// Handler para o status (JSON)
void handleStatus() {
String out = "{\n";
out += "\"freq\":"+String(lastFreq)+",\n";
out += "\"cloningState\":"+String(cloningState)+",\n";
out += "\"tempRawLen\":"+String((int)tempRaw.size())+",\n";
out += "\"codesSaved\": {\n";
bool first = true;
for (const String& key : COMMAND_KEYS) {
if (!first) out += ",\n";
// Encerra a chave com o tamanho do vetor RAW
out += " \"" + key + "\": " + String((int)learnedCodes[key].size());
first = false;
}
out += "\n}\n";
out += "}\n";
server.send(200, "application/json", out);
}
//---
//## ⚙️ Setup do Sistema (Inicialização)
//---
void setup() {
Serial.begin(115200);
delay(200);
// iniciar Preferences
prefs.begin("ir_control", false);
loadCodesFromPrefs();
Serial.printf("%d códigos carregados de Preferences.\n", (int)learnedCodes.size());
// WiFi
WiFi.begin(ssid, password);
Serial.print("Conectando WiFi");
int tries = 0;
while (WiFi.status() != WL_CONNECTED && tries < 20) {
delay(500);
Serial.print(".");
tries++;
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi conectado:");
Serial.print("Acesse http://");
Serial.println(WiFi.localIP());
} else {
Serial.println("Falha WiFi; continue em modo offline (Sem Web).");
}
// iniciar IR
irrecv.enableIRIn();
irsend.begin();
delay(100);
// web routes
server.on("/", handleRoot);
server.on("/clone", handleClone);
server.on("/control", handleControl);
server.on("/send_cmd", handleSendCommand);
server.on("/learn", handleLearn);
server.on("/status", handleStatus);
server.begin();
Serial.println("Servidor web iniciado.");
Serial.println("Aguardando sinais IR...");
}
//---
//## 🔄 Loop Principal (Recepção IR e Webserver)
//---
void loop() {
server.handleClient();
if (irrecv.decode(&results)) {
Serial.println("=== SINAL IR RECEBIDO ===");
// NOVO: Exibe o protocolo e o valor hexadecimal (como o 0x5BA06238 que você mencionou)
if (results.decode_type != UNKNOWN) {
Serial.printf(" Tipo Decodificado: %s\n", typeToString(results.decode_type).c_str());
Serial.printf(" Valor Hexadecimal: 0x%X\n", results.value);
Serial.printf(" Bits: %d\n", results.bits);
} else {
Serial.println(" Tipo Decodificado: RAW / DESCONHECIDO");
}
Serial.printf(" RAW Pulsos: %d\n", results.rawlen);
// Se estiver em modo clonagem, salva temporariamente
if (cloningState != -1 && cloningState < (int)COMMAND_KEYS.size()) {
String currentKey = COMMAND_KEYS[cloningState];
// Apenas copia o raw para o buffer temporário, mas SÓ se o buffer estiver vazio
if (tempRaw.empty()) {
// Tenta salvar a frequência apenas se for RAW (necessário para o irsend)
if (results.decode_type == UNKNOWN) {
// Frequência de modulação só é confiável com o receptor
// Mas o irremoteESP8266 usa 38kHz como padrão para RAW
// A frequência ideal para RAW não é detectada, usamos o padrão.
} else {
// A frequência é detectada pelo protocolo, mas para RAW é um chute.
// Mantemos a última frequência salva em prefs como fallback.
}
copyResultsRaw(results, tempRaw);
Serial.printf("✅ Código de '%s' RECEBIDO. Raw len=%d. Aguardando CONFIRMAÇÃO na Web.\n", currentKey.c_str(), (int)tempRaw.size());
} else {
Serial.println("AVISO: Sinal ignorado. Já há um código aguardando confirmação.");
}
}
irrecv.resume(); // Habilita o receptor para o próximo valor
}
delay(10);
}