Proyecto Cancerbero.

 ADM

PROYECTO CANCERBERO 
FASE 1 Los Cimientos
Vamos a empezar con un Arduino UNO como Base Central de Operaciones, ESP32 NodeMCU (Formato Arduino Uno), como módulo de comunicaciones. Un sensor de entrada HC-SR501 PIR  y un actuador de salida 1 Diodo RGB con Ánodo Común que brillará de color verde y pasara a rojo cuando se dispare un sensor.  El módulo de comunicaciones enviará un mensaje al movil a través de la aplicación PUSHBULLET que se tiene instalada tanto en el smartphone Android como en el PC.

Materiales
Arduino UNO
ESP32 NodeMCU (Formato Arduino Uno)
HC-SR501 PIR
Diodo RGB con Ánodo Común
2 Resistencias 330 para el Diodo RGB
1 Resistencia de 1k para el divisor de tensión
1 Resistencia de 2k2 para el divisor de tensión

Esquema de Bloques


Código Fuente Homologado para Arduino
// ====================================================================
// PROYECTO CANCERBERO - FASE 1: BASE DE OPERACIONES (ARDUINO UNO)
// Lógica: Ánodo Común (LOW = Encendido) | Comunicación: SoftwareSerial
// Pines Serie Virtuales: Pin 10 (RX) y Pin 11 (TX)
// ====================================================================

#include <SoftwareSerial.h> // Incluimos la librería oficial para clonar el puerto serie

// --- CONFIGURACIÓN DE PINES ---
const int PIN_PIR = 2;    // Entrada de señal del sensor de movimiento HC-SR501
const int PIN_ROJO = 3;   // Salida digital para el color Rojo del LED RGB
const int PIN_VERDE = 4;  // Salida digital para el color Verde del LED RGB

const int NUEVO_RX = 10;  // Recibe datos desde el GPIO 17 del ESP32 (Cable directo)
const int NUEVO_TX = 11;  // Envía datos hacia el Divisor de Tensión -> GPIO 16 del ESP32

// Creamos nuestro puerto serie virtual y le asignamos sus pines
SoftwareSerial puertoESP32(NUEVO_RX, NUEVO_TX);

// --- VARIABLES DE CONTROL ---
int estadoPresencia = 0;  // Almacena el estado del sensor (HIGH = Movimiento, LOW = Quietud)

void setup() {
  // Inicializamos el puerto Serie virtual a 9600 baudios para hablar con el ESP32
  puertoESP32.begin(9600);
 
  // (Opcional) Inicializamos el puerto USB nativo por si quieres ver cosas en el Monitor Serie del PC
  Serial.begin(9600);
 
  // Configuración de los modos de trabajo de los pines
  pinMode(PIN_PIR, INPUT);
  pinMode(PIN_ROJO, OUTPUT);
  pinMode(PIN_VERDE, OUTPUT);
 
  // --- ESTADO INICIAL SEGURO (LED VERDE ENCENDIDO) ---
  // Lógica de Ánodo Común:
  // LOW (0V) actúa como masa -> ENCIENDE el color.
  // HIGH (5V) iguala el voltaje -> APAGA el color.
  digitalWrite(PIN_VERDE, LOW);  // Enciende color Verde (Sistema Protegido)
  digitalWrite(PIN_ROJO, HIGH);  // Apaga color Rojo
 
  Serial.println("Sistema Cancerbero: Cerebro listo y vigilando...");
}

void loop() {
  // 1. LECTURA SENSOR DE MOVIMIENTO
  estadoPresencia = digitalRead(PIN_PIR);
 
  // 2. CONTROL DE ALERTA POR INTRUSIÓN
  if (estadoPresencia == HIGH) {
    // ¡Movimiento detectado! El intruso está en el rango del sensor.
   
    // Alerta Visual Local
    digitalWrite(PIN_VERDE, HIGH); // Apaga el LED Verde
    digitalWrite(PIN_ROJO, LOW);   // Enciende el LED Rojo (Alarma visual)
   
    // COMUNICACIÓN DE SALIDA: Enviamos la ráfaga al ESP32 a través del Pin 11
    puertoESP32.println("ALERTA");
   
    // También lo reflejamos en el monitor serie del PC por si estás mirando la pantalla
    Serial.println("¡INTRUSO DETECTADO! Enviando señal al ESP32...");
   
    // Antirebote y pausa de estabilidad (3 segundos)
    // Evita saturar al ESP32 enviándole cientos de mensajes por segundo
    delay(3000);
   
  } else {
    // Sin novedad en el frente: Volvemos al estado de calma
    digitalWrite(PIN_VERDE, LOW);  // Enciende el LED Verde
    digitalWrite(PIN_ROJO, HIGH);  // Apaga el LED Rojo
  }

  // 3. CANAL DE ENTRADA (ESCUCHA DE ÓRDENES EN SEGUNDO PLANO)
  // Comprobamos si el ESP32 nos está enviando algún comando por el Pin 10
  if (puertoESP32.available() > 0) {
    // Leemos la cadena de texto entrante hasta encontrar el salto de línea (\n)
    String comandoRecibido = puertoESP32.readStringUntil('\n');
    comandoRecibido.trim(); // Limpiamos espacios o caracteres invisibles de la transmisión
   
    // Si el ESP32 nos pide restaurar el sistema a distancia (Fase 2)
    if (comandoRecibido == "RESET") {
      digitalWrite(PIN_ROJO, HIGH); // Apaga el Rojo inmediatamente
      digitalWrite(PIN_VERDE, LOW); // Vuelve al Verde seguro
      Serial.println("Comando RESET recibido desde el ESP32.");
    }
  }
 
  // Breve pausa de 100ms para no exprimir el procesador continuamente
  delay(100);
}

Código Fuente homologado para ESP32 de Comunicaciones (MAC 88:57:21:8e:9f:88)
// ====================================================================
// PROYECTO CANCERBERO - FASE 1: MÓDULO DE COMUNICACIONES (ESP32)
// Solución de Alta Velocidad: Alertas vía Pushbullet
// ====================================================================

#include <WiFi.h>
#include <HTTPClient.h>

// --- CONFIGURACIÓN DE TU RED WIFI ---
const char* WIFI_SSID = "MOVISTAR_D182";          // <--- Pon el nombre de tu WiFi aquí
const char* WIFI_PASSWORD = "Cme8WCaJZAexdvHDtlod";   // <--- Pon la contraseña de tu WiFi aquí

// --- CONFIGURACIÓN DE PUSHBULLET (YA CONFIGURADO) ---
const String PUSHBULLET_TOKEN = "o.N0tl8t8D0GYJawOpAPilMlXz29tbL5vB";

#define RX2_PIN 16  
#define TX2_PIN 17  

void enviarAlertaPush(String titulo, String cuerpo) {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
   
    // Conexión segura con la API oficial de Pushbullet
    http.begin("https://api.pushbullet.com/v2/pushes");
   
    // Añadimos las cabeceras obligatorias de autenticación
    http.addHeader("Access-Token", PUSHBULLET_TOKEN);
    http.addHeader("Content-Type", "application/json");
   
    // Creamos el mensaje en formato JSON limpio
    String datosJson = "{\"type\": \"note\", \"title\": \"" + titulo + "\", \"body\": \"" + cuerpo + "\"}";
   
    int codigoRespuesta = http.POST(datosJson);
   
    Serial.print("Respuesta de Pushbullet (Código): ");
    Serial.println(codigoRespuesta);
   
    if (codigoRespuesta == 200) {
      Serial.println("¡¡NOTIFICACIÓN ENVIADA CON ÉXITO!!");
    } else {
      Serial.println("Error. Comprueba el Token de Pushbullet.");
    }
   
    http.end();
  }
}

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1, RX2_PIN, TX2_PIN);
 
  Serial.println("\nIniciando modulo Cancerbero v8 (Pushbullet)...");

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Conectando a WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\n¡Conectado con éxito a la red WiFi!");

  Serial.println("Enviando notificacion de arranque...");
  enviarAlertaPush("Cancerbero", "Modulo en linea. Sistema listo.");
}

void loop() {
  if (Serial2.available() > 0) {
    String mensajeRecibido = Serial2.readStringUntil('\n');
    mensajeRecibido.trim();
   
    if (mensajeRecibido == "ALERTA") {
      Serial.println("¡Alerta detectada! Enviando notificacion Push...");
      enviarAlertaPush("CANCERBERO ALERTA", "Movimiento detectado en el perimetro.");
     
      // Limpieza de ráfagas del sensor y pequeña pausa de 5 segundos
      while(Serial2.available() > 0) { Serial2.read(); }
      delay(5000);
     
      Serial.println("Listo para otra deteccion.");
    }
  }
  delay(50);
}

EL Multisensor de la Azotea

vamos a diseñar el Vigía de la Azotea, lo hacemos con el arsenal completo, con mentalidad de producción y sin códigos a medias. Vamos a armar el firmware definitivo con arquitectura limpia, control de interrupciones, monitorización analógica y gestión de energía para la placa solar.

Antes de ir al código, como ingenieros, mapeemos los pines del ESP32 para que todo encaje sin colisiones de hardware (aprovechando que tu placa tipo Arduino UNO tiene los pines bien distribuidos):

🗺️ Asignación de Pines (Tu Guía de Cableado)

  • Pines I2C (SDA / SCL) ➔ Pines Físicos Dedicados (SDA/SCL al lado de AREF): Compartidos por el BMP280 y el AS5600 (cada uno con su dirección de bus diferente: 0x76 y 0x36).

  • Sensor de Lluvia MH-RD (Lluvia Tejado) ➔ IO32 (Analógico ADC1): Para medir la intensidad del agua.

  • Anemómetro IR de Herradura ➔ IO14 (Digital/Interrupción): Cuenta los pulsos por rastro de ranuras.

  • DHT22 (Humedad/Temp) ➔ IO4 (Digital): Lectura de un solo hilo.

  • PIR HC-SR501 (Presencia Azotea) ➔ IO12 (Digital/Interrupción): Para seguridad del tejado.

  • Relé Reed (Contacto Magnético) ➔ IO13 (Digital): Estado de la trampilla/puerta de la azotea.

  • Sensor de Lluvia KEYES (Cerradura) ➔ IO25 (Digital): Alerta rápida de agua cerca de la electrónica de cierre.

  • Testigo Carga Solar ➔ IO26 (Digital): Lee el pin de estado del cargador (TP4056 o similar, pin CHG que cae a bajo cuando carga).

  • Nivel de Batería ➔ IO33 (Analógico ADC1): Conectado a un divisor de tensión (2 resistencias de $10\text{ k}\Omega$) para medir la celda de litio de $4.2\text{ V}$ sin quemar el pin de $3.3\text{ V}$.

💻 Firmware Completo: El Vigía de la Azotea v1.0

Asegúrate de instalar las librerías necesarias en tu Arduino IDE: Adafruit BMP280 Library, DHT sensor library, y AS5600 (por Seeed Studio o similar, aunque usaremos lectura directa por Wire para evitar fallos de incompatibilidad).

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>
#include <DHT.h>

// ==========================================
//   CONFIGURACIÓN DE PINES (HARDWARE MAP)
// ==========================================
#define PIN_DHT          4    // Pin digital para el DHT22
#define DHTTYPE          DHT22
#define PIN_ANEMOMETRO   14   // Pin con interrupción para sensor IR herradura
#define PIN_PIR          12   // Pin con interrupción para el HC-SR501
#define PIN_REED         13   // Pin digital para sensor magnético de puerta
#define PIN_KEYES_LLUVIA 25   // Pin digital para el sensor de lluvia de la cerradura
#define PIN_TESTIGO_SOLAR 26  // Pin digital para detectar si la placa carga (LOW = Cargando)

// Pines Analógicos (ADC1 - Seguros para usar con Wi-Fi en el ESP32)
#define PIN_MHRD_LLUVIA  32   // Entrada analógica del sensor de lluvia del tejado
#define PIN_BAT_MONITOR  33   // Entrada analógica del divisor de voltaje de la batería

// Dirección I2C de los sensores
#define ADDR_BMP280      0x76 // Dirección estándar del barómetro
#define ADDR_AS5600      0x36 // Dirección fija de fábrica del codificador magnético

// ==========================================
//   INSTANCIACIÓN DE OBJETOS Y VARIABLES
// ==========================================
DHT dht(PIN_DHT, DHTTYPE);
Adafruit_BMP280 bmp;

// Variables Globales Volátiles para Interrupciones (ISR)
volatile unsigned long contadorAnemometro = 0;
volatile bool movimientoDetectado = false;

// Variables de Control de Tiempo (No-bloqueante / millis)
unsigned long tiempoAnteriorSensores = 0;
const unsigned long INTERVALO_LECTURA = 3000; // Muestreo cada 3 segundos

// Constantes de Calibración
const float FACTOR_ANEMOMETRO = 2.4; // Ajustar según las ranuras y radio de las cazoletas
const float VOLTAJE_REFERENCIA = 3.3;
const float FACTOR_DIVISOR_BAT = 2.0; // Divisor 1:1 (2 resistencias iguales) reduce el voltaje a la mitad

// ==========================================
//   FUNCIONES DE INTERRUPCIÓN (ISR)
// ==========================================
// Guarda esta lógica en la memoria RAM interna (IRAM_ATTR) para máxima velocidad
void IRAM_ATTR ISR_Anemometro() {
  contadorAnemometro++;
}

void IRAM_ATTR ISR_PIR() {
  movimientoDetectado = true;
}

// ==========================================
//   SETUP: CONFIGURACIÓN INICIAL
// ==========================================
void setup() {
  Serial.begin(115200);
  Serial.println(F("\n============================================="));
  Serial.println(F("    INICIALIZANDO SISTEMA VIGÍA DE LA AZOTEA   "));
  Serial.println(F("=============================================\n"));

  // 1. Inicializar el Bus I2C de la placa
  Wire.begin();

  // 2. Inicializar Sensores I2C y SPI
  dht.begin();
 
  if (!bmp.begin(ADDR_BMP280)) {
    Serial.println(F("[ERROR] ¡BMP280 no detectado! Revisa cableado I2C."));
  } else {
    Serial.println(F("[OK] BMP280 inicializado correctamente."));
  }

  // 3. Configurar Pines Digitales de Entrada
  pinMode(PIN_REED, INPUT_PULLUP);         // Pull-up para evitar ruidos cuando el imán se aleje
  pinMode(PIN_KEYES_LLUVIA, INPUT);        // Entrada digital del sensor Keyes
  pinMode(PIN_TESTIGO_SOLAR, INPUT_PULLUP);// Pull-up para leer colector abierto del cargador

  // 4. Configurar Interrupciones Físicas
  pinMode(PIN_ANEMOMETRO, INPUT_PULLUP);   // Sensor IR herradura normalmente en alto
  attachInterrupt(digitalPinToInterrupt(PIN_ANEMOMETRO), ISR_Anemometro, FALLING);

  pinMode(PIN_PIR, INPUT);                 // El PIR entrega 3.3V directamente al detectar
  attachInterrupt(digitalPinToInterrupt(PIN_PIR), ISR_PIR, RISING);

  Serial.println(F("[OK] Pines e Interrupciones configuradas con éxito."));
}

// ==========================================
//   LOOP: BUCLE PRINCIPAL DE EJECUCIÓN
// ==========================================
void loop() {
 
  // --- Gestión de Eventos en Tiempo Real (Fuera del temporizador) ---
  if (movimientoDetectado) {
    Serial.println(F("\n[ALERTA INTRUSIÓN] ¡Movimiento detectado en el tejado por el HC-SR501!"));
    movimientoDetectado = false; // Resetear bandera
  }

  // --- Gestión de Lecturas Cíclicas (Muestreo no bloqueante) ---
  if (millis() - tiempoAnteriorSensores >= INTERVALO_LECTURA) {
    tiempoAnteriorSensores = millis();

    Serial.println(F("\n---------------- Lectura de Sensores ----------------"));

    // 1. LECTURA DHT22
    float humedad = dht.readHumidity();
    float tempDHT = dht.readTemperature();

    // 2. LECTURA BMP280 (Presión y Altitud Relativa)
    float presion = bmp.readPressure() / 100.0F; // Conversión a Hectopascales (hPa)
    float tempBMP = bmp.readTemperature();

    // 3. CÁLCULO DEL ANEMÓMETRO (Velocidad del viento)
    noInterrupts(); // Pausa Crítica: Evita que el contador cambie mientras calculamos
    unsigned long pulsosCopiados = contadorAnemometro;
    contadorAnemometro = 0; // Resetear para el próximo ciclo
    interrupts();   // Reanudamos interrupciones de inmediato
   
    // Convertir pulsos en el intervalo de tiempo a velocidad calculada
    float velocidadViento = (pulsosCopiados / (INTERVALO_LECTURA / 1000.0)) * FACTOR_ANEMOMETRO;

    // 4. LECTURA ENCODER AS5600 I2C (Dirección del Viento - Veleta)
    int anguloVeleta = leerAnguloAS5600();

    // 5. SENSORES DE LLUVIA (Tejado y Cerradura)
    int lecturaMHRD = analogRead(PIN_MHRD_LLUVIA); // 0 (Inundado) a 4095 (Seco)
    int porcentajeLluvia = map(lecturaMHRD, 4095, 1000, 0, 100); // Mapeo inverso a porcentaje
    porcentajeLluvia = constrain(porcentajeLluvia, 0, 100); // Forzar límites de seguridad

    bool lluviaCerradura = (digitalRead(PIN_KEYES_LLUVIA) == LOW); // LOW significa agua detectada

    // 6. SENSOR DE APERTURA (Trampilla / Puerta Azotea)
    bool puertaAbierta = (digitalRead(PIN_REED) == HIGH); // HIGH significa imán separado

    // 7. ENERGÍA: MONITOR DE BATERÍA Y ESTADO SOLAR
    bool cargandoSolar = (digitalRead(PIN_TESTIGO_SOLAR) == LOW); // Si pin baja a tierra, está cargando
    float voltajeBateria = calcularVoltajeBateria();
    int porcentajeBateria = calcularPorcentajeBateria(voltajeBateria);

    // ==========================================
    //   DESPLIEGUE DE DATOS POR MONITOR SERIE
    // ==========================================
    Serial.print(F("Temperatura Ambiente: ")); Serial.print(tempDHT); Serial.println(F(" °C"));
    Serial.print(F("Humedad Relativa: ")); Serial.print(humedad); Serial.println(F(" %"));
    Serial.print(F("Presión Barométrica: ")); Serial.print(presion); Serial.println(F(" hPa"));
    Serial.print(F("Temp. Interna Barómetro: ")); Serial.print(tempBMP); Serial.println(F(" °C"));
   
    Serial.print(F("Velocidad Viento: ")); Serial.print(velocidadViento); Serial.println(F(" km/h"));
    Serial.print(F("Dirección Viento (Veleta): ")); Serial.print(anguloVeleta); Serial.println(F("°"));
   
    Serial.print(F("Intensidad Lluvia Tejado: ")); Serial.print(porcentajeLluvia); Serial.println(F(" %"));
    Serial.print(F("Agua en Cerradura: ")); Serial.println(lluviaCerradura ? F("¡SÍ! RIESGO CORROSIÓN") : F("NO"));
    Serial.print(F("Trampilla Azotea: ")); Serial.println(puertaAbierta ? F("ABIERTA (Alerta)") : F("CERRADA"));
   
    // Datos de Energía del Sistema
    Serial.print(F("Estado Panel Solar: ")); Serial.println(cargandoSolar ? F("CARGANDO [⚡]") : F("BATERÍA EN DESCARGA [🔋]"));
    Serial.print(F("Voltaje Celda: ")); Serial.print(voltajeBateria); Serial.println(F(" V"));
    Serial.print(F("Capacidad Batería: ")); Serial.print(porcentajeBateria); Serial.println(F(" %"));
    Serial.println(F("-----------------------------------------------------"));
  }
}

// ==========================================
//   FUNCIONES AUXILIARES DE INGENIERÍA
// ==========================================

// Lee los registros internos del AS5600 a través del bus I2C sin librerías pesadas
int leerAnguloAS5600() {
  Wire.beginTransmission(ADDR_AS5600);
  Wire.write(0x0C); // Registro de inicio del ángulo en bruto de 12 bits (Raw Angle High Byte)
  if (Wire.endTransmission() != 0) return -1; // Error en bus I2C

  Wire.requestFrom(ADDR_AS5600, 2); // Pedimos los 2 bytes del ángulo
  if (Wire.available() >= 2) {
    uint8_t alto = Wire.read();
    uint8_t bajo = Wire.read();
    int bruto = (alto << 8) | bajo; // Combinar los dos bytes
    // Convertir resolución de 12 bits (0 a 4095) a grados geográficos (0 a 359)
    return (bruto * 360) / 4096;
  }
  return -1;
}

// Lee el ADC, compensa la atenuación y calcula el voltaje real antes del divisor
float calcularVoltajeBateria() {
  // Tomar un promedio de 10 lecturas analógicas para estabilizar el ruido eléctrico
  long sumaADC = 0;
  for(int i = 0; i < 10; i++) {
    sumaADC += analogRead(PIN_BAT_MONITOR);
    delay(2);
  }
  float promedioADC = sumaADC / 10.0;
 
  // El ESP32 mapea de 0 a 4095 para voltajes de 0 a 3.3V
  float voltajePin = (promedioADC * VOLTAJE_REFERENCIA) / 4095.0;
 
  // Multiplicamos por el factor del divisor de tensión para recuperar el valor real de la batería
  return voltajePin * FACTOR_DIVISOR_BAT;
}

// Mapeo no lineal aproximado para celdas de Litio de 3.7V (Lipo / 18650)
int calcularPorcentajeBateria(float voltios) {
  int porcentaje = 0;
  if (voltios >= 4.20) porcentaje = 100;
  else if (voltios >= 4.05) porcentaje = 90;
  else if (voltios >= 3.96) porcentaje = 80;
  else if (voltios >= 3.88) porcentaje = 70;
  else if (voltios >= 3.82) porcentaje = 60;
  else if (voltios >= 3.78) porcentaje = 50;
  else if (voltios >= 3.75) porcentaje = 40;
  else if (voltios >= 3.71) porcentaje = 30;
  else if (voltios >= 3.65) porcentaje = 20;
  else if (voltios >= 3.60) porcentaje = 10;
  else if (voltios >= 3.50) porcentaje = 5;
  else porcentaje = 0; // Umbral crítico de corte para proteger la química del litio
  return porcentaje;
}

🛡️ Notas de ingeniería para cuando vayas a soldar/conectar:

  1. El divisor de tensión para la Batería: El pin IO33 se quemará si recibe más de $3.3\text{ V}$. Pon una resistencia de $10\text{ k}\Omega$ desde el positivo de la batería al pin IO33, y otra resistencia de $10\text{ k}\Omega$ desde el pin IO33 a masa (GND). Eso divide el voltaje exactamente por la mitad, de modo que cuando la batería esté a tope ($4.2\text{ V}$), el pin del ESP32 solo verá $2.1\text{ V}$ seguros.

  2. El Testigo Solar: Los módulos de carga habituales tienen LEDs SMD indicando "Carga" y "Listo". Si sueldas un hilo fino al pin del chip que va hacia el LED de carga, verás que cae a $0\text{ V}$ (GND) cuando la placa solar inyecta energía y se apaga (sube de voltaje) cuando cesa el sol. El código está preparado para detectar esa lógica a través del pin IO26.

¡Ya tienes el Plano Maestro de Código de la Azotea completo! ¿Está claro el flujo del programa o tienes alguna duda con las interrupciones críticas antes de meter el circuito en producción?


















No hay comentarios:

Publicar un comentario