/*
* ========================================
* DETECTOR DE FANTASMAS - Halloween 2025
* ========================================
*
* Proyecto: Detector simulado de fantasmas gamificado
* Autor: ArtificialRoot
* Web: https://artificialroot.es
* Versión: 1.0
* Fecha: Octubre 2025
*
* Descripción:
* Detector de fantasmas simulado con efectos de sonido tipo contador Geiger,
* indicadores LED, linterna automática y pantalla OLED. El sistema detecta
* "presencias paranormales" de forma aleatoria con diferentes niveles de
* intensidad hasta llegar a una aparición completa del fantasma.
*
* Hardware necesario:
* - ESP8266 (NodeMCU o similar)
* - Pantalla OLED 128x64 SSD1306 (I2C)
* - Buzzer pasivo
* - LEDs indicadores (con transistor 2N2222A para mayor corriente)
* - LED linterna
* - Botón pulsador
* - Resistencias: 1kΩ (base transistor), 220-330Ω (LEDs)
*
* Conexiones:
* - OLED SDA -> D6 (GPIO14)
* - OLED SCL -> D5 (GPIO12)
* - Buzzer -> D8 (GPIO15)
* - LEDs indicadores (via 2N2222A) -> D7 (GPIO13)
* - LED Linterna -> D4 (GPIO2)
* - Botón -> D3 (GPIO0) con pull-up interno
*
* Características:
* - 5 niveles de detección (Ninguno, Leve, Medio, Fuerte, Fantasma)
* - Sonido tipo contador Geiger que aumenta con la intensidad
* - LEDs indicadores con parpadeo variable según nivel
* - Linterna automática durante la detección
* - Animaciones en pantalla OLED
* - Medidor visual en zona amarilla de la pantalla
* - Sistema activado por botón
* - Mensajes aleatorios de escape del fantasma
*
* Licencia: Código abierto para uso educativo y personal
*
* ========================================
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ========================================
// DEFINICIÓN DE PINES
// ========================================
// Pines I2C para ESP8266
#define OLED_SDA 14 // D6 - Datos I2C de la pantalla OLED
#define OLED_SCL 12 // D5 - Reloj I2C de la pantalla OLED
// Configuración de la pantalla OLED
#define SCREEN_WIDTH 128 // Ancho en píxeles
#define SCREEN_HEIGHT 64 // Alto en píxeles
#define OLED_RESET -1 // Sin pin de reset (compartido con ESP)
#define SCREEN_ADDRESS 0x3C // Dirección I2C de la OLED (0x3C es la más común)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Pines de salida
#define BUZZER_PIN 15 // D8 - Buzzer para efectos de sonido
#define LED_PIN 13 // D7 - LEDs indicadores (via transistor 2N2222A)
#define FLASHLIGHT_PIN 2 // D4 - LED linterna (se enciende durante detección)
#define BUTTON_PIN 0 // D3 (GPIO0) - Botón de inicio con pull-up interno
// ========================================
// CONFIGURACIÓN DE PANTALLA Y MEDIDOR
// ========================================
// Zona amarilla a la derecha (últimos ~16 píxeles para el medidor)
#define YELLOW_ZONE_START 112 // Píxel donde comienza la zona amarilla
#define METER_WIDTH 14 // Ancho del medidor en píxeles
// ========================================
// NIVELES Y ESTADOS DEL SISTEMA
// ========================================
// Niveles de detección paranormal
#define LEVEL_NONE 0 // Sin detección
#define LEVEL_LOW 1 // Detección leve
#define LEVEL_MEDIUM 2 // Presencia media
#define LEVEL_HIGH 3 // Alerta fuerte
#define LEVEL_GHOST 4 // ¡Fantasma detectado!
// Estados del sistema
#define STATE_WAITING 0 // Esperando inicio por botón
#define STATE_SEARCHING 1 // Buscando actividad paranormal
#define STATE_GHOST_FOUND 2 // Fantasma encontrado
// ========================================
// VARIABLES GLOBALES
// ========================================
// Control de estados
int systemState = STATE_WAITING; // Estado actual del detector
int currentLevel = LEVEL_NONE; // Nivel actual de detección
int targetLevel = LEVEL_NONE; // Nivel objetivo (transición suave)
// Temporizadores
unsigned long lastUpdate = 0; // Último cambio de nivel objetivo
unsigned long scanTimer = 0; // Timer para animaciones de escaneo
unsigned long animTimer = 0; // Timer para transiciones de nivel
int scanLine = 0; // Posición de línea de escaneo
bool increasing = true; // Dirección de animación
int animFrame = 0; // Frame actual de animación
// Control del botón
unsigned long lastButtonPress = 0; // Última pulsación del botón
const unsigned long debounceDelay = 50; // Tiempo de antirrebote (ms)
// Efecto contador Geiger
unsigned long lastClickTime = 0; // Último click del buzzer
int clickInterval = 1000; // Intervalo entre clicks (ms)
// Control de LEDs indicadores
unsigned long lastLedUpdate = 0; // Última actualización de LEDs
bool ledState = false; // Estado actual de LEDs (on/off)
int ledBlinkSpeed = 1000; // Velocidad de parpadeo (ms)
// ========================================
// BITMAP DEL FANTASMA (32x32 píxeles)
// ========================================
// Imagen del fantasma que aparece en el nivel máximo
const unsigned char ghostBitmap[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xf0, 0x00,
0x00, 0x3f, 0xf8, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x7b, 0xbe, 0x00, 0x00, 0x73, 0x9e, 0x00,
0x00, 0x73, 0x8e, 0x00, 0x00, 0x73, 0x8e, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x07, 0x7f, 0xfe, 0xe0,
0x07, 0xfe, 0x7e, 0xe0, 0x07, 0xfe, 0x7f, 0xe0, 0x07, 0xfe, 0x7f, 0xe0, 0x07, 0xff, 0xff, 0xe0,
0x03, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0x10, 0x01, 0xff, 0xfe, 0x10,
0x00, 0x7f, 0xfe, 0x10, 0x00, 0x7f, 0xfe, 0x70, 0x00, 0x7f, 0xff, 0xf0, 0x00, 0x3f, 0xff, 0xf0,
0x00, 0x3f, 0xff, 0xe0, 0x00, 0x1f, 0xff, 0xc0, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x07, 0xff, 0x00,
0x00, 0x03, 0xfe, 0x00, 0x00, 0x01, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// ========================================
// MENSAJES DE ESCAPE DEL FANTASMA
// ========================================
// Mensajes aleatorios que aparecen cuando el fantasma desaparece
const char* escapeMessages[] = {
"HA DESAPARECIDO!",
"SE HA ALEJADO",
"HA HUIDO",
"SE DESVANECE",
"VUELVE AL VACIO",
"REGRESA A LAS SOMBRAS"
};
const int numMessages = 6; // Número total de mensajes
// ========================================
// SETUP - INICIALIZACIÓN DEL SISTEMA
// ========================================
void setup() {
// Inicializar comunicación serial para debug
Serial.begin(115200);
Serial.println(F("========================================"));
Serial.println(F("DETECTOR DE FANTASMAS v1.0"));
Serial.println(F("by ArtificialRoot - artificialroot.es"));
Serial.println(F("========================================"));
Serial.println(F("Iniciando sistema..."));
// Inicializar bus I2C con pines específicos del ESP8266
Wire.begin(OLED_SDA, OLED_SCL);
// Configurar pines de salida
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(FLASHLIGHT_PIN, OUTPUT);
// Asegurar que todo esté apagado al inicio
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(LED_PIN, LOW);
digitalWrite(FLASHLIGHT_PIN, LOW);
// Configurar botón con resistencia pull-up interna
// El botón conecta a GND, por lo que LOW = presionado
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Inicializar pantalla OLED
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("ERROR: No se encuentra la OLED"));
Serial.println(F("Verifica las conexiones:"));
Serial.println(F("SDA -> D6 (GPIO14)"));
Serial.println(F("SCL -> D5 (GPIO12)"));
// Bucle infinito si falla la pantalla
for(;;);
}
Serial.println(F("OLED inicializada correctamente"));
// Configurar orientación horizontal (sin rotación)
display.setRotation(0);
display.clearDisplay();
// Mostrar pantalla de inicio con información del proyecto
showStartScreen();
// Test de LEDs al inicio para verificar funcionamiento
Serial.println(F("Test de LEDs..."));
// Test linterna: encender medio segundo
digitalWrite(FLASHLIGHT_PIN, HIGH);
delay(500);
digitalWrite(FLASHLIGHT_PIN, LOW);
delay(300);
// Test LEDs indicadores: 3 parpadeos
for(int i = 0; i < 3; i++) {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
delay(200);
}
delay(1500);
// Inicializar generador de números aleatorios
randomSeed(analogRead(0));
// Mostrar pantalla de espera del botón
showWaitingScreen();
Serial.println(F("Sistema listo!"));
Serial.println(F("Presiona el boton para comenzar la busqueda"));
}
// ========================================
// LOOP PRINCIPAL
// ========================================
void loop() {
// Verificar constantemente si se presiona el botón
checkButton();
// Comportamiento según el estado actual del sistema
if (systemState == STATE_WAITING) {
// ESTADO: Esperando que se pulse el botón
// La linterna permanece apagada para ahorrar energía
digitalWrite(FLASHLIGHT_PIN, LOW);
// Mostrar pantalla de espera con texto parpadeante
showWaitingScreen();
delay(500);
return; // Salir del loop hasta la próxima iteración
}
if (systemState == STATE_SEARCHING) {
// ESTADO: Búsqueda activa de fantasmas
// Encender linterna durante toda la búsqueda
digitalWrite(FLASHLIGHT_PIN, HIGH);
unsigned long currentTime = millis();
// Cada 10 segundos, seleccionar un nuevo nivel objetivo aleatorio
// Esto simula que el fantasma se mueve o cambia de intensidad
if (currentTime - lastUpdate >= 10000) {
selectNewTarget();
lastUpdate = currentTime;
}
// Transición GRADUAL hacia el nivel objetivo
// El nivel aumenta más rápido que cuando disminuye
if (currentLevel < targetLevel && currentTime - animTimer >= 1000) {
// Aumentar nivel cada segundo
currentLevel++;
animTimer = currentTime;
updateClickInterval(); // Actualizar velocidad del buzzer
updateLedSpeed(); // Actualizar velocidad de LEDs
Serial.print("Nivel aumentado a: ");
Serial.println(currentLevel);
} else if (currentLevel > targetLevel && currentTime - animTimer >= 800) {
// Disminuir nivel cada 800ms (más lento)
currentLevel--;
animTimer = currentTime;
updateClickInterval();
updateLedSpeed();
Serial.print("Nivel disminuido a: ");
Serial.println(currentLevel);
}
// Reproducir efecto de contador Geiger continuo
playGeigerClick();
// Actualizar parpadeo de LEDs indicadores
updateLeds();
// Actualizar pantalla OLED
display.clearDisplay();
drawMeter(); // Dibujar medidor de nivel
drawContent(); // Dibujar contenido según nivel actual
display.display();
// Delay corto para mejor respuesta del buzzer
delay(10);
}
}
// ========================================
// FUNCIÓN: Verificar pulsación del botón
// ========================================
void checkButton() {
// Leer estado del botón (LOW = presionado por pull-up)
if (digitalRead(BUTTON_PIN) == LOW) {
unsigned long currentTime = millis();
// Implementar antirrebote: ignorar pulsaciones muy rápidas
if (currentTime - lastButtonPress > debounceDelay) {
lastButtonPress = currentTime;
// Solo iniciar búsqueda si estamos en estado de espera
if (systemState == STATE_WAITING) {
Serial.println(F("Boton presionado - Iniciando busqueda"));
startSearch(); // Iniciar nueva búsqueda
}
// Feedback sonoro y visual al pulsar
tone(BUZZER_PIN, 2000, 100); // Beep corto
digitalWrite(LED_PIN, HIGH);
delay(100);
digitalWrite(LED_PIN, LOW);
}
}
}
// ========================================
// FUNCIÓN: Iniciar búsqueda de fantasmas
// ========================================
void startSearch() {
// Cambiar al estado de búsqueda activa
systemState = STATE_SEARCHING;
currentLevel = LEVEL_NONE;
targetLevel = LEVEL_NONE;
lastUpdate = millis();
// Mostrar animación de inicio en pantalla
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 20);
display.println(F("INICIANDO"));
display.setTextSize(1);
display.setCursor(25, 45);
display.println(F("BUSQUEDA..."));
display.display();
// Encender linterna al iniciar
digitalWrite(FLASHLIGHT_PIN, HIGH);
// Secuencia de sonido ascendente (5 tonos)
for(int i = 0; i < 5; i++) {
tone(BUZZER_PIN, 1000 + (i * 300), 80);
digitalWrite(LED_PIN, HIGH);
delay(80);
digitalWrite(LED_PIN, LOW);
delay(80);
}
delay(1000);
}
// ========================================
// FUNCIÓN: Mostrar pantalla de espera
// ========================================
void showWaitingScreen() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
// Título principal
display.setCursor(30, 8);
display.println(F("GHOST"));
display.setCursor(15, 28);
display.println(F("DETECTOR"));
// Texto parpadeante para indicar que se espera acción
if ((millis() / 500) % 2 == 0) {
display.setTextSize(1);
display.setCursor(18, 50);
display.println(F("Presiona Boton"));
}
display.display();
}
// ========================================
// FUNCIÓN: Actualizar velocidad de parpadeo de LEDs
// ========================================
void updateLedSpeed() {
// Ajustar velocidad según nivel de detección
// Más nivel = parpadeo más rápido = más tensión
switch(currentLevel) {
case LEVEL_NONE:
ledBlinkSpeed = 2000; // Muy lento (2 segundos)
break;
case LEVEL_LOW:
ledBlinkSpeed = 800; // Lento
break;
case LEVEL_MEDIUM:
ledBlinkSpeed = 400; // Medio
break;
case LEVEL_HIGH:
ledBlinkSpeed = 150; // Rápido
break;
case LEVEL_GHOST:
ledBlinkSpeed = 50; // Muy rápido (frenético)
break;
}
}
// ========================================
// FUNCIÓN: Controlar parpadeo de LEDs indicadores
// ========================================
void updateLeds() {
unsigned long currentTime = millis();
if (currentLevel == LEVEL_NONE) {
// Nivel bajo: parpadeo muy esporádico y aleatorio
if (currentTime - lastLedUpdate >= ledBlinkSpeed && random(100) < 10) {
digitalWrite(LED_PIN, HIGH);
delay(50);
digitalWrite(LED_PIN, LOW);
lastLedUpdate = currentTime;
}
} else if (currentLevel == LEVEL_GHOST) {
// Nivel fantasma: parpadeo muy rápido y continuo
if (currentTime - lastLedUpdate >= ledBlinkSpeed) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
lastLedUpdate = currentTime;
}
} else {
// Niveles intermedios: parpadeo regular según velocidad
if (currentTime - lastLedUpdate >= ledBlinkSpeed) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
lastLedUpdate = currentTime;
}
}
}
// ========================================
// FUNCIÓN: Actualizar intervalo de clicks del buzzer
// ========================================
void updateClickInterval() {
// Ajustar intervalo según nivel (más nivel = clicks más frecuentes)
switch(currentLevel) {
case LEVEL_NONE: clickInterval = 2000; break; // 1 click cada 2 seg
case LEVEL_LOW: clickInterval = 800; break; // 1 click cada 800ms
case LEVEL_MEDIUM: clickInterval = 400; break; // 1 click cada 400ms
case LEVEL_HIGH: clickInterval = 150; break; // 1 click cada 150ms
case LEVEL_GHOST: clickInterval = 50; break; // 1 click cada 50ms (frenético)
}
}
// ========================================
// FUNCIÓN: Reproducir efecto contador Geiger
// ========================================
void playGeigerClick() {
unsigned long currentTime = millis();
if (currentLevel == LEVEL_NONE) {
// Nivel bajo: clicks muy esporádicos y aleatorios
if (currentTime - lastClickTime >= clickInterval && random(100) < 20) {
tone(BUZZER_PIN, 2000, 20); // Click corto de 2000Hz
lastClickTime = currentTime;
}
} else if (currentLevel == LEVEL_GHOST) {
// Nivel fantasma: clicks rápidos con frecuencia variable
if (currentTime - lastClickTime >= clickInterval) {
int freq = random(2500, 3500); // Frecuencia aleatoria alta
tone(BUZZER_PIN, freq, 25); // Click un poco más largo
lastClickTime = currentTime;
clickInterval = random(30, 70); // Intervalo aleatorio para efecto caótico
}
} else {
// Niveles intermedios: clicks regulares con ligera variación
if (currentTime - lastClickTime >= clickInterval) {
// Frecuencia base que aumenta con el nivel
int baseFreq = 2000 + (currentLevel * 300);
// Duración que aumenta con el nivel
int clickDuration = 15 + (currentLevel * 3);
tone(BUZZER_PIN, baseFreq, clickDuration);
lastClickTime = currentTime;
// Añadir variación aleatoria al intervalo para naturalidad
int variation = random(-50, 50);
updateClickInterval();
clickInterval += variation;
if (clickInterval < 30) clickInterval = 30; // Límite mínimo
}
}
}
// ========================================
// FUNCIÓN: Seleccionar nuevo nivel objetivo aleatorio
// ========================================
void selectNewTarget() {
// Generar número aleatorio para determinar el próximo nivel
// Probabilidades diseñadas para crear tensión gradual
int roll = random(100);
if (roll < 20) {
targetLevel = LEVEL_NONE; // 30% probabilidad - Sin detección
} else if (roll < 45) {
targetLevel = LEVEL_LOW; // 25% probabilidad - Detección leve
} else if (roll < 65) {
targetLevel = LEVEL_MEDIUM; // 20% probabilidad - Presencia media
} else if (roll < 80) {
targetLevel = LEVEL_HIGH; // 15% probabilidad - Alerta alta
} else {
targetLevel = LEVEL_GHOST; // 10% probabilidad - ¡Fantasma!
}
Serial.print("Nuevo objetivo: ");
Serial.println(targetLevel);
}
// ========================================
// FUNCIÓN: Dibujar medidor de nivel en pantalla
// ========================================
void drawMeter() {
// Dibujar marco del medidor en la zona amarilla (derecha)
display.drawRect(YELLOW_ZONE_START, 0, METER_WIDTH, 64, SSD1306_WHITE);
// Calcular altura del medidor según nivel actual
int meterHeight = 0;
switch(currentLevel) {
case LEVEL_NONE: meterHeight = 0; break; // Sin relleno
case LEVEL_LOW: meterHeight = 12; break; // 20% aprox
case LEVEL_MEDIUM: meterHeight = 28; break; // 44% aprox
case LEVEL_HIGH: meterHeight = 44; break; // 69% aprox
case LEVEL_GHOST: meterHeight = 60; break; // 94% (casi lleno)
}
// Llenar medidor desde abajo hacia arriba
if (meterHeight > 0) {
display.fillRect(YELLOW_ZONE_START + 2, 64 - meterHeight,
METER_WIDTH - 4, meterHeight, SSD1306_WHITE);
// Efecto de parpadeo en niveles altos para mayor urgencia
if (currentLevel >= LEVEL_HIGH && (millis() / 200) % 2 == 0) {
display.fillRect(YELLOW_ZONE_START + 2, 64 - meterHeight,
METER_WIDTH - 4, meterHeight, SSD1306_INVERSE);
}
}
// Dibujar marcas de nivel (4 divisiones)
for(int i = 1; i < 5; i++) {
int y = 64 - (i * 12);
display.drawLine(YELLOW_ZONE_START, y, YELLOW_ZONE_START + 3, y, SSD1306_WHITE);
}
}
// ========================================
// FUNCIÓN: Dibujar contenido según nivel actual
// ========================================
void drawContent() {
display.setTextColor(SSD1306_WHITE);
// Mostrar contenido diferente según el nivel de detección
switch(currentLevel) {
case LEVEL_NONE:
// Nivel bajo: modo búsqueda
drawScanAnimation(); // Animación de escaneo
display.setTextSize(1);
display.setCursor(22, 8);
display.println(F("BUSCANDO"));
display.setCursor(20, 22);
display.println(F("ACTIVIDAD"));
display.setCursor(20, 36);
display.println(F("FANTASMAL"));
display.setCursor(35, 52);
display.println(F("..."));
break;
case LEVEL_LOW:
// Detección leve: ondas suaves
drawWaveAnimation();
display.setTextSize(1);
display.setCursor(10, 8);
display.println(F("DETECCION"));
display.setCursor(15, 25);
display.setTextSize(2);
display.println(F("LEVE"));
display.setTextSize(1);
display.setCursor(10, 50);
display.println(F("Algo cerca..."));
break;
case LEVEL_MEDIUM:
// Presencia media: pulsos concéntricos
drawPulseAnimation();
display.setTextSize(1);
display.setCursor(10, 5);
display.println(F("PRESENCIA"));
display.setCursor(12, 20);
display.setTextSize(2);
display.println(F("MEDIA"));
display.setTextSize(1);
display.setCursor(8, 42);
display.println(F("Acercate con"));
display.setCursor(15, 54);
display.println(F("cuidado!"));
break;
case LEVEL_HIGH:
// Alerta alta: triángulos de advertencia
drawAlertAnimation();
display.setTextSize(1);
display.setCursor(35, 8);
display.println(F("ALERTA!"));
display.setCursor(18, 23);
display.setTextSize(2);
display.println(F("FUERTE"));
display.setTextSize(1);
display.setCursor(25, 50);
// Texto parpadeante para mayor urgencia
if ((millis() / 400) % 2 == 0) {
display.println(F("MUY CERCA!"));
}
break;
case LEVEL_GHOST:
// Nivel máximo: ¡Mostrar fantasma!
showGhost();
break;
}
}
// ========================================
// ANIMACIONES DE PANTALLA
// ========================================
// Animación de escaneo: línea horizontal que se mueve
void drawScanAnimation() {
scanLine = (millis() / 100) % 60; // Ciclo de 60 píxeles
display.drawLine(0, scanLine, YELLOW_ZONE_START - 2, scanLine, SSD1306_WHITE);
// Añadir puntos aleatorios tipo "ruido"
for(int i = 0; i < 8; i++) {
int x = random(YELLOW_ZONE_START - 10);
int y = random(60);
display.drawPixel(x, y, SSD1306_WHITE);
}
}
// Animación de ondas: círculos concéntricos que crecen
void drawWaveAnimation() {
int frame = (millis() / 150) % 3; // 3 frames
for(int i = 0; i <= frame; i++) {
int radius = 8 + (i * 8); // Radios: 8, 16, 24
display.drawCircle(80, 40, radius, SSD1306_WHITE);
}
}
// Animación de pulso: círculo que crece y se encoge
void drawPulseAnimation() {
// Usar función seno para movimiento suave
int radius = 10 + abs(sin(millis() / 300.0) * 8);
display.drawCircle(85, 40, radius, SSD1306_WHITE);
display.drawCircle(85, 40, radius -5, SSD1306_WHITE);
}
// Animación de alerta: triángulos parpadeantes
void drawAlertAnimation() {
if ((millis() / 300) % 2 == 0) {
// Triángulo izquierdo
display.fillTriangle(5, 55, 15, 35, 25, 55, SSD1306_WHITE);
// Triángulo derecho
display.fillTriangle(80, 55, 90, 35, 100, 55, SSD1306_WHITE);
}
}
// ========================================
// FUNCIÓN: Animación de escape del fantasma
// ========================================
void showGhostEscape() {
// Seleccionar mensaje aleatorio de escape
int messageIndex = random(numMessages);
const char* message = escapeMessages[messageIndex];
// Animación de desvanecimiento del fantasma (5 frames)
for(int fade = 0; fade < 5; fade++) {
display.clearDisplay();
drawMeter();
// Mostrar fantasma solo en frames pares (efecto parpadeo)
if (fade % 2 == 0) {
int x = (YELLOW_ZONE_START - 32) / 2;
int y = 16;
display.drawBitmap(x, y, ghostBitmap, 32, 32, SSD1306_WHITE);
}
// LEDs también parpadean durante el desvanecimiento
digitalWrite(LED_PIN, fade % 2);
display.display();
// Sonido descendente (fantasma alejándose)
tone(BUZZER_PIN, 3000 - (fade * 400), 20);
delay(200);
}
// Apagar LEDs indicadores
digitalWrite(LED_PIN, LOW);
// Mostrar mensaje de escape
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Líneas decorativas
display.drawLine(10, 20, 118, 20, SSD1306_WHITE);
display.drawLine(10, 44, 118, 44, SSD1306_WHITE);
// Centrar mensaje en pantalla
int textLength = strlen(message);
int charWidth = 6; // Ancho aproximado por carácter
int startX = (128 - (textLength * charWidth)) / 2;
display.setCursor(startX, 28);
display.println(message);
// Puntos suspensivos animados (1, 2, 3 puntos)
for(int i = 0; i < 3; i++) {
display.setCursor(55, 52);
for(int j = 0; j <= i; j++) {
display.print(".");
}
display.display();
delay(400);
}
delay(1500);
// Mensaje para iniciar nueva búsqueda
display.clearDisplay();
display.setTextSize(1);
display.setCursor(10, 18);
display.println(F("PRESIONA BOTON"));
display.setCursor(20, 35);
display.println(F("PARA NUEVA"));
display.setCursor(25, 48);
display.println(F("BUSQUEDA"));
display.display();
// Secuencia de sonido y LED de finalización
for(int i = 0; i < 3; i++) {
digitalWrite(LED_PIN, HIGH);
tone(BUZZER_PIN, 1500 + (i * 200), 50);
delay(100);
digitalWrite(LED_PIN, LOW);
delay(100);
}
delay(1000);
// Apagar linterna al terminar la detección
digitalWrite(FLASHLIGHT_PIN, LOW);
// Volver al estado de espera
systemState = STATE_WAITING;
Serial.println(F("Esperando boton para nueva busqueda"));
}
// ========================================
// FUNCIÓN: Mostrar fantasma detectado
// ========================================
void showGhost() {
display.clearDisplay();
drawMeter();
// Centrar fantasma en el área principal
int x = (YELLOW_ZONE_START - 32) / 2;
int y = 25;
display.drawBitmap(x, y, ghostBitmap, 32, 32, SSD1306_WHITE);
// Texto "BOO!" parpadeante
if ((millis() / 250) % 2 == 0) {
display.setTextSize(2);
display.setCursor(35, 1);
display.println(F("BOO!"));
}
display.display();
// LEDs indicadores encendidos fijos
digitalWrite(LED_PIN, HIGH);
// Linterna sigue encendida durante la aparición
// Mantener efecto Geiger frenético durante 3 segundos
unsigned long ghostStart = millis();
while(millis() - ghostStart < 3000) {
playGeigerClick();
delay(10);
}
// Iniciar secuencia de escape
showGhostEscape();
// Resetear niveles para próxima búsqueda
currentLevel = LEVEL_NONE;
targetLevel = LEVEL_NONE;
updateClickInterval();
updateLedSpeed();
digitalWrite(LED_PIN, LOW);
}
// ========================================
// FUNCIÓN: Mostrar pantalla de inicio
// ========================================
void showStartScreen() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
// Título del proyecto
display.setCursor(30, 10);
display.println(F("GHOST"));
display.setCursor(15, 30);
display.println(F("DETECTOR"));
// Información de versión y autor
display.setTextSize(1);
display.setCursor(10, 52);
display.println(F("ArtificialRoot 2025"));
display.display();
}
// ========================================
// FIN DEL PROGRAMA
// Proyecto creado por ArtificialRoot
// Web: https://artificialroot.es
// ====================================