#include #include #include #include #include #include #include #include #include #include #include // Para HTTPS ESP8266WebServer server(80); WiFiClient espClient; WiFiClientSecure secureClient; PubSubClient client(espClient); Ticker watchdogReset; Ticker verificadorWiFi; Ticker verificadorMQTT; Ticker tickerA0; std::map pines; std::map modosPines; std::map ultimoEstadoEntrada; std::map esAnalogico; String ssid, password, mqttUser, mqttPass, mqttServer, url_puertos_json; String wifiSSID = ""; unsigned long ultimoIntentoMQTT = 0; const unsigned long intervaloReintentoMQTT = 5000; int mqttPort = 1883; struct TopicsMQTT { String state; String comando; String ip; String valores; String reboot; }; TopicsMQTT topics; bool estadoInicialPublicado = false; int ultimoValorA0 = -1; // Valor inicial imposible para detectar primer lectura const int UMBRAL_A0 = 10; // Diferencia mínima para publicar String getChipID() { String id = String(ESP.getChipId(), HEX); id.toUpperCase(); return id; } void inicializarTopicsMQTT() { topics.state = "dispositivo/" + getChipID() + "/state"; topics.comando = "dispositivo/" + getChipID() + "/comando"; topics.ip = "dispositivo/" + getChipID() + "/ip"; topics.reboot = topics.comando + "/reboot"; } void resetWatchdog() { ESP.wdtFeed(); } void setupOTA() { ArduinoOTA.setHostname("ESP-Dispositivo"); ArduinoOTA.onStart([]() { Serial.println("\n🔄 OTA iniciado..."); }); ArduinoOTA.onEnd([]() { Serial.println("\n✅ OTA completado"); }); ArduinoOTA.onError([](ota_error_t error) { Serial.printf("❌ OTA Error[%u]\n", error); }); ArduinoOTA.begin(); } void configurarHora() { configTime(-3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); // UTC-3 (Argentina Serial.print("⏳ Sincronizando hora"); while (time(nullptr) < 100000) { Serial.print("."); delay(500); } Serial.println("\n✅ Hora sincronizada"); } void leerConfig() { if (!SPIFFS.exists("/config.json")) { Serial.println("config.json no existe. Creando valores por defecto."); ssid = ""; password = ""; mqttServer = ""; mqttUser = ""; mqttPass = ""; url_puertos_json = ""; guardarConfig(); return; } File file = SPIFFS.open("/config.json", "r"); if (!file) { Serial.println("No se pudo abrir config.json"); return; } StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { Serial.println("Error al parsear JSON"); return; } ssid = doc["ssid"] | ""; password = doc["password"] | ""; mqttServer = doc["mqttServer"] | ""; mqttUser = doc["mqttUser"] | ""; mqttPass = doc["mqttPass"] | ""; mqttPort = doc["mqttPort"] | 1883; url_puertos_json = doc["url_puertos_json"] | ""; } void guardarConfig() { StaticJsonDocument<512> doc; doc["ssid"] = ssid; doc["password"] = password; doc["mqttServer"] = mqttServer; doc["mqttUser"] = mqttUser; doc["mqttPass"] = mqttPass; doc["mqttPort"] = mqttPort; doc["url_puertos_json"] = url_puertos_json; File file = SPIFFS.open("/config.json", "w"); serializeJson(doc, file); file.close(); } bool descargarYActualizarPuertos() { String urlCompleta = "https://esp.penki.com.ar/api/get_puertos.php?chipid=" + getChipID(); Serial.println("📡 Solicitando: " + urlCompleta); // Eliminar esta línea: BearSSL::WiFiClientSecure secureClient; secureClient.setInsecure(); // Temporalmente ignorar verificación SSL (más compatible) HTTPClient http; http.setAuthorization("api", "API#2025"); if (!http.begin(secureClient, urlCompleta)) { Serial.println("❌ Error al iniciar conexión HTTPS"); return false; } int httpCode = http.GET(); if (httpCode > 0) { Serial.printf("📥 HTTP %d recibido\n", httpCode); } else { Serial.printf("❌ Error HTTP al intentar descargar: %d\n", httpCode); http.end(); return false; } if (httpCode == HTTP_CODE_OK) { String contenidoRemoto = http.getString(); http.end(); // --- Parsear JSON remoto --- StaticJsonDocument<4096> docRemoto; DeserializationError errorRemoto = deserializeJson(docRemoto, contenidoRemoto); if (errorRemoto) { Serial.println("❌ Error al parsear JSON remoto"); return false; // No actualizar ni sobreescribir el local } const char* fechaRemota = docRemoto["update"] | ""; Serial.printf("📅 Fecha remota: %s\n", fechaRemota); // --- Leer archivo local (si existe) --- if (SPIFFS.exists("/puertos.json")) { File fileLocal = SPIFFS.open("/puertos.json", "r"); StaticJsonDocument<4096> docLocal; DeserializationError errorLocal = deserializeJson(docLocal, fileLocal); fileLocal.close(); if (!errorLocal) { const char* fechaLocal = docLocal["update"] | ""; Serial.printf("📅 Fecha local: %s\n", fechaLocal); // --- Comparar fechas --- if (strcmp(fechaRemota, fechaLocal) <= 0) { Serial.println("✅ El archivo local está actualizado o es más reciente."); return true; // No se necesita actualizar } } } // --- Guardar archivo remoto en /puertos.json --- File fileNuevo = SPIFFS.open("/puertos.json", "w"); if (!fileNuevo) { Serial.println("❌ No se pudo abrir puertos.json para guardar"); return false; } fileNuevo.print(contenidoRemoto); fileNuevo.close(); Serial.println("✅ Archivo puertos.json actualizado desde servidor remoto"); return true; } else { Serial.printf("❌ Error HTTP al intentar descargar: %d\n", httpCode); http.end(); return false; // Falló la descarga } } void leerPuertos() { pines.clear(); modosPines.clear(); ultimoEstadoEntrada.clear(); esAnalogico.clear(); if (!SPIFFS.exists("/puertos.json")) { Serial.println("❌ puertos.json no existe"); return; } File file = SPIFFS.open("/puertos.json", "r"); if (!file) { Serial.println("❌ No se pudo abrir puertos.json"); return; } StaticJsonDocument<4096> doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { Serial.println("❌ Error al parsear puertos.json"); return; } for (JsonPair kv : doc.as()) { String key = kv.key().c_str(); if (key == "update") continue; JsonObject obj = kv.value().as(); if (!obj.containsKey("gpio") || !obj.containsKey("modo")) continue; String gpioStr = obj["gpio"].as(); uint8_t gpio; if (gpioStr == "A0") { gpio = A0; } else { gpio = gpioStr.toInt(); } String alias = key; // Usamos "D0", "D1", "A0", etc. String modoStr = obj["modo"].as(); uint8_t modoPin; if (modoStr == "INPUT") { modoPin = INPUT; } else if (modoStr == "OUTPUT") { modoPin = OUTPUT; } else if (modoStr == "INPUT_PULLUP") { modoPin = INPUT_PULLUP; } else { Serial.printf("⚠️ Modo desconocido para %s: %s, usando INPUT_PULLUP\n", alias.c_str(), modoStr.c_str()); modoPin = INPUT_PULLUP; } pines[alias] = gpio; modosPines[alias] = modoPin; esAnalogico[alias] = (gpio == A0); // 📌 Marca como analógico si corresponde Serial.printf("🔧 Alias %s => GPIO %d, modo %s\n", alias.c_str(), gpio, modoStr.c_str()); } Serial.println("✅ Pines y modos cargados dinámicamente"); } void imprimirPuertosJson() { if (!SPIFFS.begin()) { Serial.println("Error al montar SPIFFS"); return; } if (!SPIFFS.exists("/puertos.json")) { Serial.println("Archivo puertos.json no existe"); return; } File file = SPIFFS.open("/puertos.json", "r"); if (!file) { Serial.println("No se pudo abrir puertos.json"); return; } Serial.println("Contenido de puertos.json:"); while (file.available()) { Serial.write(file.read()); } file.close(); Serial.println(); // Nueva línea al final } void conectarWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid.c_str(), password.c_str()); Serial.print("Conectando a WiFi"); int intentos = 20; while (WiFi.status() != WL_CONNECTED && intentos-- > 0) { delay(500); Serial.print("."); } if (WiFi.status() == WL_CONNECTED) { Serial.println("\n✅ WiFi conectado!"); Serial.print("IP: "); Serial.println(WiFi.localIP()); wifiSSID = WiFi.SSID(); } else { Serial.println("\n❌ No se pudo conectar. Modo AP."); WiFi.softAP("ESP_Config", "12345678"); Serial.print("AP IP: "); Serial.println(WiFi.softAPIP()); server.begin(); } } void conectarMQTT() { if (client.connected()) return; if ((long)(millis() - ultimoIntentoMQTT) < intervaloReintentoMQTT) return; ultimoIntentoMQTT = millis(); client.setServer(mqttServer.c_str(), mqttPort); client.setCallback(callback); String clientId = "WemosClient-" + String(ESP.getChipId()); Serial.print("Conectando a MQTT: "); Serial.println(mqttServer); if (client.connect(clientId.c_str(), mqttUser.c_str(), mqttPass.c_str(), topics.state.c_str(), 1, true, "offline")) { Serial.println("✅ Conectado al broker MQTT"); estadoInicialPublicado = false; // para asegurarte que se publique String ip = WiFi.localIP().toString(); client.publish(topics.state.c_str(), "online", true); client.publish(topics.ip.c_str(), ip.c_str(), true); client.subscribe(topics.comando.c_str()); client.subscribe(topics.reboot.c_str()); client.publish(topics.reboot.c_str(), "0", true); } else { Serial.print("❌ MQTT falló. Código: "); Serial.println(client.state()); } } void callback(char* topic, byte* payload, unsigned int length) { payload[length] = '\0'; String mensaje = String((char*)payload); Serial.printf("Mensaje recibido en topic: %s -> %s\n", topic, mensaje.c_str()); // Verificar si es el topic de reboot if (String(topic) == topics.reboot.c_str()) { if (mensaje == "1") { Serial.println("Reiniciando dispositivo..."); delay(100); // Corto retardo para que se imprima el mensaje ESP.restart(); return; } } if (String(topic) == topics.comando) { StaticJsonDocument<256> doc; DeserializationError err = deserializeJson(doc, mensaje); if (err) { Serial.println("Error al parsear comando JSON"); return; } String alias = doc["alias"] | ""; String valor = doc["valor"] | ""; if (pines.count(alias)) { Serial.printf("Alias '%s' existe en pines\n", alias.c_str()); if (modosPines[alias] == OUTPUT) { Serial.printf("Modo de '%s' es OUTPUT\n", alias.c_str()); } else { Serial.printf("Modo de '%s' NO es OUTPUT (modo=%d)\n", alias.c_str(), modosPines[alias]); } } else { Serial.printf("Alias '%s' NO existe en pines\n", alias.c_str()); } if (pines.count(alias) && (modosPines[alias] == OUTPUT)) { uint8_t gpio = pines[alias]; if (valor == "ON") { digitalWrite(gpio, HIGH); Serial.printf("Pin %d (%s) puesto ON\n", gpio, alias.c_str()); } else if (valor == "OFF") { digitalWrite(gpio, LOW); Serial.printf("Pin %d (%s) puesto OFF\n", gpio, alias.c_str()); } else { Serial.println("Valor desconocido, usar ON u OFF"); } } else { Serial.println("Alias no encontrado o no es salida"); } } } void publicarEstadoInicialEntradas() { for (auto const& [alias, gpio] : pines) { if (esAnalogico[alias]) continue; // Saltar A0 int val = digitalRead(gpio); ultimoEstadoEntrada[alias] = val; String estado = val == HIGH ? "ON" : "OFF"; String modoStr; switch (modosPines[alias]) { case INPUT: modoStr = "INPUT"; break; case OUTPUT: modoStr = "OUTPUT"; break; case INPUT_PULLUP: modoStr = "INPUT_PULLUP"; break; default: modoStr = "DESCONOCIDO"; break; } // Crear JSON: {"valor": "ON", "modo": "INPUT"} StaticJsonDocument<128> doc; doc["valor"] = estado; doc["modo"] = modoStr; char buffer[128]; serializeJson(doc, buffer, sizeof(buffer)); String topic = "dispositivo/" + getChipID() + "/valores/" + alias; client.publish(topic.c_str(), buffer, true); Serial.printf("Publicado estado inicial %s => %s / %s\n", alias.c_str(), estado.c_str(), modoStr.c_str()); } } void publicarCambiosEntradas() { for (auto const& [alias, gpio] : pines) { if (esAnalogico[alias]) continue; // Salta A0 int val = digitalRead(gpio); if (!ultimoEstadoEntrada.count(alias) || ultimoEstadoEntrada[alias] != val) { ultimoEstadoEntrada[alias] = val; String estado = val == HIGH ? "ON" : "OFF"; String modoStr; switch (modosPines[alias]) { case INPUT: modoStr = "INPUT"; break; case OUTPUT: modoStr = "OUTPUT"; break; case INPUT_PULLUP: modoStr = "INPUT_PULLUP"; break; default: modoStr = "DESCONOCIDO"; break; } StaticJsonDocument<128> doc; doc["valor"] = estado; doc["modo"] = modoStr; char buffer[128]; serializeJson(doc, buffer, sizeof(buffer)); String topic = "dispositivo/" + getChipID() + "/valores/" + alias; client.publish(topic.c_str(), buffer, true); Serial.printf("📤 Cambio en %s (digital): %s / %s\n", alias.c_str(), estado.c_str(), modoStr.c_str()); } } } void manejarRoot() { String pagina = "

Configuración WiFi

"; pagina += ""; pagina += ""; pagina += ""; pagina += "
"; pagina += "
"; pagina += ""; pagina += ""; pagina += "
"; server.send(200, "text/html", pagina); } void manejarGuardar() { ssid = server.arg("ssid"); password = server.arg("password"); mqttServer = server.arg("mqttServer"); mqttUser = server.arg("mqttUser"); mqttPass = server.arg("mqttPass"); mqttPort = server.arg("mqttPort").toInt(); mqttPort = constrain(mqttPort, 1, 65535); // validación String nuevo_url = server.arg("url_puertos_json"); if (nuevo_url.length() > 0) { url_puertos_json = nuevo_url; } guardarConfig(); server.send(200, "text/html", "

Guardado con éxito

Reinicia el ESP para aplicar cambios.

Volver"); } void manejarBootstrap() { if (SPIFFS.exists("/bootstrap.min.css")) { File file = SPIFFS.open("/bootstrap.min.css", "r"); server.streamFile(file, "text/css"); file.close(); } else { server.send(404, "text/plain", "No encontrado"); } } void verificarConexionWiFi() { if (WiFi.status() != WL_CONNECTED) { Serial.println("⚠️ WiFi desconectado. Intentando reconectar..."); WiFi.disconnect(); WiFi.begin(ssid.c_str(), password.c_str()); unsigned long inicio = millis(); while (WiFi.status() != WL_CONNECTED && millis() - inicio < 10000) { delay(500); Serial.print("."); } if (WiFi.status() == WL_CONNECTED) { Serial.println("\n✅ Reconectado al WiFi!"); } else { Serial.println("\n❌ No se pudo reconectar. Reiniciando ESP..."); ESP.restart(); } } } int intentosMQTTFallidos = 0; const int MAX_INTENTOS_MQTT = 5; void verificarConexionMQTT() { if (WiFi.status() != WL_CONNECTED) return; // Solo verifica MQTT si hay WiFi if (!client.connected()) { Serial.println("⚠️ MQTT desconectado. Intentando reconectar..."); conectarMQTT(); if (client.connected()) { Serial.println("✅ Reconexion MQTT exitosa"); intentosMQTTFallidos = 0; } else { intentosMQTTFallidos++; Serial.printf("❌ Fallo reconexión MQTT (%d/%d)\n", intentosMQTTFallidos, MAX_INTENTOS_MQTT); if (intentosMQTTFallidos >= MAX_INTENTOS_MQTT) { Serial.println("🚨 Demasiados fallos MQTT. Reiniciando ESP..."); ESP.restart(); } } } else { intentosMQTTFallidos = 0; // Resetea contador si está conectado } } void imprimirConfigJSON() { if (!SPIFFS.exists("/config.json")) { Serial.println("❌ El archivo config.json no existe."); return; } File file = SPIFFS.open("/config.json", "r"); if (!file) { Serial.println("❌ No se pudo abrir config.json."); return; } Serial.println("📄 Contenido de config.json:"); while (file.available()) { Serial.write(file.read()); } file.close(); } void publicarA0() { int valorActual = analogRead(A0); if (ultimoValorA0 == -1 || abs(valorActual - ultimoValorA0) > UMBRAL_A0) { ultimoValorA0 = valorActual; String topic = "dispositivo/" + getChipID() + "/valores/A0"; String payload = String(valorActual); // 👈 conversión de int a String client.publish(topic.c_str(), payload.c_str(), true); Serial.printf("Publicado cambio A0: %d\n", valorActual); } } void setup() { Serial.begin(115200); ESP.wdtDisable(); ESP.wdtEnable(8000); if (!SPIFFS.begin()) { Serial.println("❌ Error al montar SPIFFS"); // Aquí podés decidir reiniciar o modo seguro } else { Serial.println("✅ SPIFFS montado correctamente"); } leerConfig(); imprimirConfigJSON(); imprimirPuertosJson(); inicializarTopicsMQTT(); conectarWiFi(); if (WiFi.status() == WL_CONNECTED) { if (!descargarYActualizarPuertos()) { Serial.println("⚠️ Usando puertos.json local"); } leerPuertos(); } else { Serial.println("⚠️ No hay WiFi, usando puertos.json local"); } // Configurar pines según modos cargados for (auto const& [alias, gpio] : pines) { if (modosPines.count(alias)) { pinMode(gpio, modosPines[alias]); Serial.printf("Pin %d (%s) configurado modo %d\n", gpio, alias.c_str(), modosPines[alias]); } else { pinMode(gpio, INPUT); Serial.printf("Pin %d (%s) configurado modo por defecto INPUT\n", gpio, alias.c_str()); } } // Inicializar salidas en LOW for (auto const& [alias, modo] : modosPines) { if (modo == OUTPUT) { digitalWrite(pines[alias], LOW); } } if (WiFi.status() == WL_CONNECTED) { conectarMQTT(); } setupOTA(); // Servidor web server.on("/", manejarRoot); server.on("/guardar", HTTP_POST, manejarGuardar); server.on("/bootstrap.min.css", HTTP_GET, manejarBootstrap); server.begin(); // Watchdog y verificadores watchdogReset.attach_ms(1000, resetWatchdog); verificadorWiFi.attach(60, verificarConexionWiFi); // Chequeo WiFi cada 60s verificadorMQTT.attach(15, verificarConexionMQTT); // Chequeo MQTT cada 15s tickerA0.attach_ms(10, publicarA0); } // LOOP principal void loop() { if (WiFi.status() == WL_CONNECTED) { client.loop(); // Mantener MQTT vivo conectarMQTT(); // Reintentar conexión si está desconectado } server.handleClient(); ArduinoOTA.handle(); publicarCambiosEntradas(); // Leer entradas digitales y publicar solo si cambian // (Opcional) Podés agregar publicación periódica de A0 separado, si quieres: // publicarAnalogicoA0(); // Reset watchdog watchdogReset.attach_ms(1000, resetWatchdog); verificadorWiFi.attach(60, verificarConexionWiFi); verificadorMQTT.attach(15, verificarConexionMQTT); }