702 lines
20 KiB
C++
702 lines
20 KiB
C++
#include <ESP8266WiFi.h>
|
|
#include <ESP8266WebServer.h>
|
|
#include <FS.h>
|
|
#include <ArduinoJson.h>
|
|
#include <PubSubClient.h>
|
|
#include <ArduinoOTA.h>
|
|
#include <Ticker.h>
|
|
#include <ESP8266HTTPClient.h>
|
|
#include <map>
|
|
#include <time.h>
|
|
#include <WiFiClientSecure.h> // Para HTTPS
|
|
|
|
ESP8266WebServer server(80);
|
|
WiFiClient espClient;
|
|
WiFiClientSecure secureClient;
|
|
PubSubClient client(espClient);
|
|
Ticker watchdogReset;
|
|
Ticker verificadorWiFi;
|
|
Ticker verificadorMQTT;
|
|
Ticker tickerA0;
|
|
|
|
std::map<String, uint8_t> pines;
|
|
std::map<String, uint8_t> modosPines;
|
|
std::map<String, int> ultimoEstadoEntrada;
|
|
std::map<String, bool> 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<JsonObject>()) {
|
|
String key = kv.key().c_str();
|
|
if (key == "update") continue;
|
|
|
|
JsonObject obj = kv.value().as<JsonObject>();
|
|
if (!obj.containsKey("gpio") || !obj.containsKey("modo")) continue;
|
|
|
|
String gpioStr = obj["gpio"].as<String>();
|
|
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<String>();
|
|
|
|
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 = "<html><head><meta name='viewport' content='width=device-width'><link rel='stylesheet' href='/bootstrap.min.css'></head><body class='d-flex justify-content-center align-items-center vh-100'><div class='container'><div class='card p-4 shadow'><h3>Configuración WiFi</h3><form method='POST' action='/guardar'>";
|
|
|
|
pagina += "<label>SSID:</label><select name='ssid' class='form-select'>";
|
|
int n = WiFi.scanNetworks();
|
|
for (int i = 0; i < n; i++) {
|
|
String net = WiFi.SSID(i);
|
|
pagina += "<option value='" + net + "'";
|
|
if (net == ssid) pagina += " selected";
|
|
pagina += ">" + net + "</option>";
|
|
}
|
|
pagina += "</select><label>Password:</label><input name='password' class='form-control' value='" + password + "'>";
|
|
pagina += "<label>MQTT Server:</label><input name='mqttServer' class='form-control' value='" + mqttServer + "'>";
|
|
pagina += "<label>MQTT User:</label><input name='mqttUser' class='form-control' value='" + mqttUser + "'>";
|
|
pagina += "<label>MQTT Pass:</label><input name='mqttPass' class='form-control' value='" + mqttPass + "'><br>";
|
|
pagina += "<label>MQTT Port:</label><input name='mqttPort' class='form-control' type='number' value='" + String(mqttPort) + "'><br>";
|
|
pagina += "<label>URL Base para puertos.json (host sin ruta):</label>";
|
|
pagina += "<input name='url_puertos_json' class='form-control' value='" + url_puertos_json + "'>";
|
|
pagina += "<button type='submit' class='btn btn-primary'>Guardar</button></form></div></div></body></html>";
|
|
|
|
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", "<html><body><h2>Guardado con éxito</h2><p>Reinicia el ESP para aplicar cambios.</p><a href='/'>Volver</a></body></html>");
|
|
}
|
|
|
|
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);
|
|
}
|