/*
============================================================
ALARMA DE GAS UNIVERSAL - UN SOLO ESP32 (detector + panel)
Asignatura: Aplicaciones Moviles para IoT (TI3V42) - Sumativa 1
Placa: ESP32 DevKit C (Wokwi) | Comunicacion: WiFi + MQTT
============================================================
Funcion:
- SENSORES: gas (MQ-2), temperatura/humedad (DHT22), presencia (PIR),
potenciometro (umbral), 2 pulsadores (silenciar y valvula).
- ACTUADORES: servo (valvula de gas), 4 LED, buzzer (piezo).
- Decide LOCALMENTE el estado (NORMAL / ALERTA / PELIGRO): la alarma
funciona aunque se caiga el WiFi (diseno "fail-safe").
- LECTURA inalambrica: publica telemetria por MQTT cada 1 s (y ante
cada cambio) para que cualquier cliente (celular / PC) la lea.
- ESCRITURA inalambrica: recibe comandos por MQTT (silenciar,
abrir/cerrar valvula, cambiar umbral) y los aplica a los actuadores.
- Seguridad: comandos firmados con HMAC-SHA256 + anti-replay,
interlock de valvula y Last Will (online/offline).
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>
#include <DHT.h>
#include "mbedtls/md.h"
// ===================== PINES =====================
#define PIN_GAS 34 // AOUT del MQ-2 (ADC1, solo entrada)
#define PIN_POT 35 // Potenciometro (ADC1, solo entrada): umbral de alerta
#define PIN_DHT 15 // DHT22
#define PIN_PIR 27 // Sensor de movimiento
#define PIN_SERVO 18 // Servo = valvula de gas
#define PIN_BUZZER 25 // Piezo
#define PIN_LED_VERDE 13
#define PIN_LED_AMARILLO 14
#define PIN_LED_ROJO 26
#define PIN_LED_VENT 33 // LED azul = extractor / ventilacion
#define PIN_BTN_MUTE 32 // Pulsador: silenciar alarma
#define PIN_BTN_VALV 4 // Pulsador: abrir / cerrar valvula
// ===================== RED / MQTT =====================
const char* WIFI_SSID = "Wokwi-GUEST"; // Red simulada de Wokwi (abierta)
const char* WIFI_PASS = "";
const int WIFI_CANAL = 6;
const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
// Prefijo unico para no mezclarnos con otros grupos en el broker publico
const String BASE = "inacap/ti3v42/alarmagas/quezada-zuniga/";
const String T_TEL = BASE + "telemetria"; // ESP32 -> cliente (LECTURA)
const String T_CMD = BASE + "comandos"; // cliente -> ESP32 (ESCRITURA)
const String T_ESTADO = BASE + "estado"; // online / offline (Last Will)
// Clave compartida para firmar mensajes (HMAC). Debe ser igual en el cliente.
const char* CLAVE = "ti3v42-gas-Q7z!k2";
// true = solo acepta comandos firmados (modo seguro, recomendado)
// false = acepta comandos sin firma (solo para pruebas rapidas con un cliente MQTT)
const bool EXIGIR_FIRMA = true;
// ===================== PARAMETROS =====================
const unsigned long CALENTAMIENTO_MS = 10000; // El MQ-2 real necesita precalentar
const unsigned long TEL_MS = 1000; // Periodo de telemetria
const unsigned long MUTE_MS = 30000; // Duracion del silencio
const unsigned long POSTVENT_MS = 10000; // Ventilar 10 s extra al normalizarse
const unsigned long ESCALADA_MS = 20000; // Alerta sin presencia -> PELIGRO
const int TEMP_FUEGO_C10 = 600; // 60.0 C (decimas) => posible incendio
const int ANGULO_ABIERTA = 90;
const int ANGULO_CERRADA = 0;
// Umbral de alerta sobre lectura ADC (0..4095). Lo ajusta el potenciometro o un comando remoto.
int umbralAlerta = 1200;
int umbralPeligro() { return min(umbralAlerta * 3 / 2, 4000); }
// ===================== ESTADOS =====================
enum Estado : uint8_t { NORMAL = 0, ALERTA = 1, PELIGRO = 2, CALENTANDO = 3 };
const char* NOMBRE_ESTADO[] = { "NORMAL", "ALERTA", "PELIGRO", "CALENTANDO" };
Estado estado = CALENTANDO;
bool valvulaAbierta = true;
bool escalada = false;
bool forzarTel = true;
int gas = 0;
int t10 = 0; // temperatura en decimas de grado (evita floats en el JSON firmado)
int hum = 0; // humedad %
int pir = 0;
unsigned long muteHasta = 0, ventHasta = 0, inicioAlerta = 0, ultimaPresencia = 0;
// ===================== OBJETOS =====================
DHT dht(PIN_DHT, DHT22);
Servo valvula;
WiFiClient net;
PubSubClient mqtt(net);
// ===================== SEGURIDAD =====================
uint32_t bootId = 0, txSeq = 0; // anti-replay (lo que enviamos)
uint32_t rxBoot = 0, rxSeq = 0; // anti-replay (lo que recibimos)
uint32_t rechazados = 0, aceptados = 0;
String firmar(const String& msg) {
byte h[32];
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1);
mbedtls_md_hmac_starts(&ctx, (const unsigned char*)CLAVE, strlen(CLAVE));
mbedtls_md_hmac_update(&ctx, (const unsigned char*)msg.c_str(), msg.length());
mbedtls_md_hmac_finish(&ctx, h);
mbedtls_md_free(&ctx);
char hex[17];
for (int i = 0; i < 8; i++) sprintf(hex + i * 2, "%02x", h[i]); // 64 bits de firma
return String(hex);
}
bool igualesCT(const String& a, const String& b) { // comparacion en tiempo constante
if (a.length() != b.length()) return false;
uint8_t d = 0;
for (size_t i = 0; i < a.length(); i++) d |= (uint8_t)(a[i] ^ b[i]);
return d == 0;
}
bool verificar(JsonDocument& doc) {
const char* s = doc["sig"];
if (!s) return false;
String sigRx = s;
doc.remove("sig");
String cuerpo;
serializeJson(doc, cuerpo);
if (!igualesCT(sigRx, firmar(cuerpo))) return false; // autenticidad / integridad
uint32_t b = doc["boot"] | 0u;
uint32_t q = doc["seq"] | 0u;
if (b == rxBoot) { if (q <= rxSeq) return false; } // anti-replay
else rxBoot = b; // el emisor inicio nueva sesion
rxSeq = q;
return true;
}
void publicarFirmado(const String& topic, JsonDocument& doc) {
doc["boot"] = bootId;
doc["seq"] = ++txSeq;
String cuerpo;
serializeJson(doc, cuerpo);
doc["sig"] = firmar(cuerpo);
String salida;
serializeJson(doc, salida);
mqtt.publish(topic.c_str(), salida.c_str());
}
// ===================== BUZZER (compatible core 2.x y 3.x) =====================
#define CANAL_BUZZER 14 // canal LEDC separado del servo para no pisar su timer
void buzzerInit() {
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcAttachChannel(PIN_BUZZER, 2000, 8, CANAL_BUZZER);
#else
ledcSetup(CANAL_BUZZER, 2000, 8);
ledcAttachPin(PIN_BUZZER, CANAL_BUZZER);
#endif
}
void buzzerTono(int f) {
static int ultimo = -1;
if (f == ultimo) return;
ultimo = f;
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcWriteTone(PIN_BUZZER, f);
#else
ledcWriteTone(CANAL_BUZZER, f);
#endif
}
// ===================== RED =====================
void mantenerRed() {
static unsigned long tW = 0, tM = 0;
if (WiFi.status() != WL_CONNECTED) {
if (millis() - tW > 15000) {
tW = millis();
WiFi.disconnect();
WiFi.begin(WIFI_SSID, WIFI_PASS, WIFI_CANAL);
}
return;
}
if (!mqtt.connected() && millis() - tM > 3000) {
tM = millis();
String id = "gas-qz-zu-" + String(bootId, HEX);
// Last Will: si el ESP32 se cae, el broker publica "offline"
if (mqtt.connect(id.c_str(), nullptr, nullptr, T_ESTADO.c_str(), 1, true, "offline")) {
mqtt.subscribe(T_CMD.c_str());
mqtt.publish(T_ESTADO.c_str(), "online", true);
Serial.println(F("[MQTT] Conectado al broker"));
}
}
}
// ===================== ACCIONES SOBRE LA VALVULA (comunes: local y remoto) =====================
void accionValvula(bool abrir, const char* origen) {
if (abrir) {
if (estado == NORMAL) { valvulaAbierta = true; Serial.printf("[%s] Valvula ABIERTA\n", origen); }
else Serial.printf("[%s] Apertura BLOQUEADA: hay gas/alerta (interlock de seguridad)\n", origen);
} else {
valvulaAbierta = false;
Serial.printf("[%s] Valvula CERRADA\n", origen);
}
forzarTel = true;
}
// ===================== ESCRITURA INALAMBRICA (comandos recibidos) =====================
void onMqtt(char* topic, byte* payload, unsigned int len) {
JsonDocument doc;
if (deserializeJson(doc, payload, len)) { rechazados++; Serial.println(F("[SEG] JSON invalido")); return; }
if (EXIGIR_FIRMA) {
if (!verificar(doc)) {
rechazados++;
Serial.println(F("[SEG] Comando RECHAZADO (firma o anti-replay invalidos)"));
return;
}
}
aceptados++;
String c = doc["c"] | "";
if (c == "umbral") {
int v = doc["v"] | umbralAlerta;
umbralAlerta = constrain(v, 300, 3500);
Serial.printf("[CMD] Umbral de alerta = %d\n", umbralAlerta);
} else if (c == "mute") {
muteHasta = millis() + MUTE_MS;
Serial.println(F("[CMD] Alarma silenciada 30 s"));
} else if (c == "valvula") {
int v = doc["v"] | 0;
accionValvula(v == 1, "CMD");
} else {
Serial.println(F("[CMD] Comando desconocido"));
}
forzarTel = true;
}
// ===================== LECTURA DE SENSORES =====================
void leerSensores() {
static unsigned long tGas = 0, tDht = 0, tPot = 0;
static bool prevM = HIGH, prevV = HIGH;
static unsigned long tM = 0, tV = 0;
static int potUlt = -1;
if (millis() - tGas >= 100) { // gas: promedio de 8 muestras + filtro
tGas = millis();
long s = 0;
for (int i = 0; i < 8; i++) s += analogRead(PIN_GAS);
int raw = s / 8;
gas = (gas * 7 + raw * 3) / 10;
}
if (millis() - tDht >= 2000) { // DHT22: maximo 1 lectura cada 2 s
tDht = millis();
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t)) t10 = (int)lroundf(t * 10.0f);
if (!isnan(h)) hum = (int)lroundf(h);
}
pir = digitalRead(PIN_PIR);
if (pir) ultimaPresencia = millis();
// Potenciometro -> umbral de alerta (400..3000, pasos de 50). Solo actua si se mueve >= 100,
// asi no pisa continuamente un umbral enviado por comando remoto.
if (millis() - tPot >= 200) {
tPot = millis();
long s = 0;
for (int i = 0; i < 8; i++) s += analogRead(PIN_POT);
int u = (map((int)(s / 8), 0, 4095, 400, 3000) / 50) * 50;
if (potUlt < 0) { potUlt = u; umbralAlerta = u; }
else if (abs(u - potUlt) >= 100) {
potUlt = u; umbralAlerta = u; forzarTel = true;
Serial.printf("[LOCAL] Umbral de alerta (potenciometro) = %d\n", u);
}
}
bool m = digitalRead(PIN_BTN_MUTE); // pulsador silenciar con anti-rebote
if (prevM == HIGH && m == LOW && millis() - tM > 200) {
tM = millis();
muteHasta = millis() + MUTE_MS;
forzarTel = true;
Serial.println(F("[LOCAL] Alarma silenciada 30 s"));
}
prevM = m;
bool v = digitalRead(PIN_BTN_VALV); // pulsador valvula: alterna abrir/cerrar
if (prevV == HIGH && v == LOW && millis() - tV > 200) {
tV = millis();
accionValvula(!valvulaAbierta, "LOCAL");
}
prevV = v;
}
// ===================== LOGICA DE ESTADO =====================
void evaluarEstado() {
Estado nuevo;
if (millis() < CALENTAMIENTO_MS) {
nuevo = CALENTANDO;
} else {
int uA = umbralAlerta, uP = umbralPeligro();
bool fuego = (t10 >= TEMP_FUEGO_C10);
// Histeresis del 10 % para evitar parpadeo entre estados
if (gas >= uP || fuego) nuevo = PELIGRO;
else if (estado == PELIGRO && gas >= uP * 0.9) nuevo = PELIGRO;
else if (gas >= uA) nuevo = ALERTA;
else if (estado == ALERTA && gas >= uA * 0.9) nuevo = ALERTA;
else nuevo = NORMAL;
// Alerta prolongada y nadie en la habitacion -> escalar a PELIGRO
if (nuevo == ALERTA) {
if (estado != ALERTA && estado != PELIGRO) inicioAlerta = millis();
if (millis() - inicioAlerta > ESCALADA_MS && millis() - ultimaPresencia > ESCALADA_MS) escalada = true;
}
if (nuevo == NORMAL) escalada = false;
if (escalada && nuevo == ALERTA) nuevo = PELIGRO;
}
if (nuevo != estado) {
Serial.printf("[ESTADO] %s -> %s (gas=%d, temp=%.1f)\n",
NOMBRE_ESTADO[estado], NOMBRE_ESTADO[nuevo], gas, t10 / 10.0);
if (nuevo == PELIGRO) valvulaAbierta = false; // cierre automatico, queda enclavado
estado = nuevo;
forzarTel = true;
}
}
// ===================== ACTUADORES =====================
void actualizarActuadores() {
unsigned long t = millis();
bool silenciado = t < muteHasta;
// LEDs de estado
digitalWrite(PIN_LED_VERDE, estado == NORMAL || (estado == CALENTANDO && (t % 1000) < 500));
digitalWrite(PIN_LED_AMARILLO, estado == ALERTA || (estado == CALENTANDO && (t % 1000) >= 500));
digitalWrite(PIN_LED_ROJO, estado == PELIGRO && (t % 300) < 150);
// Ventilacion: encendida en ALERTA/PELIGRO y 10 s despues
if (estado == ALERTA || estado == PELIGRO) ventHasta = t + POSTVENT_MS;
digitalWrite(PIN_LED_VENT, t < ventHasta);
// Valvula (servo)
static int anguloPrev = -1;
int ang = valvulaAbierta ? ANGULO_ABIERTA : ANGULO_CERRADA;
if (ang != anguloPrev) { valvula.write(ang); anguloPrev = ang; }
// Sonido
if (silenciado || estado == NORMAL || estado == CALENTANDO) buzzerTono(0);
else if (estado == ALERTA) buzzerTono((t % 1500) < 200 ? 1500 : 0);
else buzzerTono((t % 500) < 250 ? 2000 : 1000); // sirena
}
// ===================== LECTURA INALAMBRICA (telemetria) =====================
void publicarTelemetria() {
static unsigned long tTel = 0;
if (!forzarTel && millis() - tTel < TEL_MS) return;
tTel = millis();
forzarTel = false;
Serial.printf("[GAS] gas=%d (alerta>=%d, peligro>=%d) temp=%.1fC hum=%d%% pir=%d estado=%s valvula=%s wifi=%s mqtt=%s ok=%u rech=%u\n",
gas, umbralAlerta, umbralPeligro(), t10 / 10.0, hum, pir, NOMBRE_ESTADO[estado],
valvulaAbierta ? "ABIERTA" : "CERRADA",
WiFi.status() == WL_CONNECTED ? "OK" : "--", mqtt.connected() ? "OK" : "--",
aceptados, rechazados);
if (!mqtt.connected()) return;
JsonDocument doc;
doc["gas"] = gas;
doc["t"] = t10;
doc["h"] = hum;
doc["pir"] = pir;
doc["est"] = (int)estado;
doc["val"] = valvulaAbierta ? 1 : 0;
doc["vent"] = (millis() < ventHasta) ? 1 : 0;
doc["ua"] = umbralAlerta;
doc["up"] = umbralPeligro();
doc["mute"] = (millis() < muteHasta) ? 1 : 0;
publicarFirmado(T_TEL, doc);
}
// ===================== SETUP / LOOP =====================
void setup() {
Serial.begin(115200);
bootId = esp_random();
pinMode(PIN_PIR, INPUT);
pinMode(PIN_BTN_MUTE, INPUT_PULLUP);
pinMode(PIN_BTN_VALV, INPUT_PULLUP);
pinMode(PIN_LED_VERDE, OUTPUT);
pinMode(PIN_LED_AMARILLO, OUTPUT);
pinMode(PIN_LED_ROJO, OUTPUT);
pinMode(PIN_LED_VENT, OUTPUT);
analogSetPinAttenuation(PIN_GAS, ADC_11db);
analogSetPinAttenuation(PIN_POT, ADC_11db);
buzzerInit();
ESP32PWM::allocateTimer(0);
valvula.setPeriodHertz(50);
valvula.attach(PIN_SERVO, 500, 2400);
valvula.write(ANGULO_ABIERTA);
dht.begin();
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS, WIFI_CANAL); // NO bloqueante: la alarma funciona sin red
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMqtt);
mqtt.setBufferSize(512);
ultimaPresencia = millis();
Serial.println(F("=== Alarma de gas universal (ESP32 unico) iniciada. Calentando sensor de gas... ==="));
}
void loop() {
mantenerRed();
mqtt.loop();
leerSensores();
evaluarEstado();
actualizarActuadores();
publicarTelemetria();
}Loading
esp32-devkit-c-v4
esp32-devkit-c-v4