commit 06719b56a1234a76a37f1716cab2d1e7a88da9fa Author: Marco Gallegos Date: Wed Dec 24 16:57:32 2025 -0600 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a69e3d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# ========================================== +# Ignora credenciales y ajustes locales +# ========================================== +secrets.h +.env + +# ========================================== +# Configuración de IDEs / SO +# ========================================== +.vscode/ +.idea/ +.DS_Store +Thumbs.db + +# ========================================== +# Salida de compilación de Arduino / C++ +# ========================================== +/build/ +*.o +*.a +*.elf +*.bin + +# ========================================== +# Archivos temporales y logs +# ========================================== +*.log +*.tmp +*.swp +*.orig +backup_* diff --git a/Config.cpp b/Config.cpp new file mode 100644 index 0000000..998f910 --- /dev/null +++ b/Config.cpp @@ -0,0 +1,59 @@ +#include "Config.h" + +void saveConfig(const Config& config) { + Serial.println("Guardando configuración..."); + DynamicJsonDocument doc(512); + doc["ssid"] = config.ssid; + doc["password"] = config.password; + doc["webhookUrl"] = config.webhookUrl; + doc["deviceName"] = config.deviceName; + + File configFile = LittleFS.open("/config.json", "w"); + if (!configFile) { + Serial.println("Error: No se pudo abrir el archivo de configuración para escritura."); + return; + } + + serializeJson(doc, configFile); + configFile.close(); + Serial.println("Configuración guardada exitosamente."); +} + +bool loadConfig(Config& config) { + Serial.println("Intentando cargar configuración..."); + if (LittleFS.exists("/config.json")) { + Serial.println("Archivo 'config.json' encontrado."); + File configFile = LittleFS.open("/config.json", "r"); + if (configFile) { + String fileContent = configFile.readString(); + Serial.println("Contenido del archivo: " + fileContent); + + DynamicJsonDocument doc(512); + DeserializationError error = deserializeJson(doc, fileContent); + + if (!error) { + Serial.println("JSON parseado correctamente."); + strlcpy(config.ssid, doc["ssid"], sizeof(config.ssid)); + strlcpy(config.password, doc["password"], sizeof(config.password)); + strlcpy(config.webhookUrl, doc["webhookUrl"], sizeof(config.webhookUrl)); + strlcpy(config.deviceName, doc["deviceName"] | "Checador_Generico", sizeof(config.deviceName)); + + Serial.println("SSID cargado: " + String(config.ssid)); + Serial.println("Webhook URL cargado: " + String(config.webhookUrl)); + Serial.println("Device Name cargado: " + String(config.deviceName)); + + configFile.close(); + return true; + } else { + Serial.print("Error al parsear JSON: "); + Serial.println(error.c_str()); + } + configFile.close(); + } else { + Serial.println("Error: No se pudo abrir el archivo de configuración para lectura."); + } + } else { + Serial.println("No se encontró el archivo 'config.json'."); + } + return false; +} diff --git a/Config.h b/Config.h new file mode 100644 index 0000000..2a6a614 --- /dev/null +++ b/Config.h @@ -0,0 +1,18 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include + +struct Config { + char ssid[32]; + char password[64]; + char webhookUrl[256]; + char deviceName[32]; +}; + +void saveConfig(const Config& config); +bool loadConfig(Config& config); + +#endif // CONFIG_H diff --git a/README.md b/README.md new file mode 100644 index 0000000..da8b4d9 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# 🕒 Checador Inteligente NFC IoT (ESP8266 · Production Ready) + +Este repositorio contiene el firmware para un sistema de control de asistencia profesional basado en **NFC** e **IoT**. Diseñado para ser robusto, autogestionable y fácil de desplegar en entornos reales. + +El sistema utiliza un **NodeMCU ESP8266** para leer tarjetas NFC, validar datos y enviarlos a un Webhook en la nube, todo configurable mediante un **Portal Cautivo Web** sin necesidad de recompensar el código para cambiar credenciales. + +--- + +## ⚡️ Características Destacadas + +* **🛡️ Robustez Industrial:** + * **NFC Watchdog:** Verificación cada 15s. Si el lector se congela, el sistema realiza un *Hard Reset* físico al módulo RC522 y fuerza la ganancia de antena al máximo. + * **Auto-Formato:** Gestión automática del sistema de archivos (`LittleFS`) si se detecta corrupción. + * **Anti-Bloqueo:** Uso de `ESP8266WebServer` síncrono y gestión de memoria optimizada (PROGMEM) para evitar reinicios por desbordamiento de pila. + +* **⚙️ Configuración Vía Web (Portal Cautivo):** + * No requiere editar código (`secrets.h`) para cambiar WiFi o URLs. + * Configura **SSID**, **Password**, **Webhook URL** y **Nombre del Dispositivo** (User-Agent) desde el navegador. + +* **🎮 Controles Físicos y Gestuales:** + * **Doble Reset (Double Tap):** Presiona el botón `RST` dos veces rápido para forzar el Modo Configuración sin abrir la carcasa. + * **Botón Físico:** Opción de forzar config manteniendo presionado el botón (GPIO 0 / Flash) al arrancar. + +--- + +## 🚀 Guía de Inicio Rápido + +### 1. Hardware Requerido +| Componente | Descripción | +| :--- | :--- | +| **NodeMCU v2/v3** | ESP8266 (ESP-12E) | +| **RC522** | Lector NFC (SPI) | +| **OLED 0.96"** | Pantalla SSD1306 (I2C) | +| **Buzzer** | Activo/Pasivo (3.3V) | +| **LED** | Indicador de estado | + +### 2. Carga del Firmware +Usa el script automatizado para compilar y subir (requiere `arduino-cli`): +```bash +./compile_and_upload.sh +``` + +### 3. Configuración Inicial +1. Al encender por primera vez, el dispositivo creará una red WiFi llamada: + * **SSID:** `Soul23` + * **Password:** `1234567890` +2. Conéctate con tu móvil o PC. +3. Accede a `http://192.168.4.1` en tu navegador. +4. Ingresa los datos: + * **SSID / Password:** Tu red WiFi local. + * **Webhook URL:** Endpoint donde recibirás los registros (ej. `https://api.tuempresa.com/asistencia`). + * **Nombre del Dispositivo:** Identificador único (se envía como `User-Agent` y en el JSON). +5. Dale a "Guardar y Reiniciar". ¡Listo! + +--- + +## 🔌 Diagrama de Conexión (Pinout Seguro) + +Este pinout evita conflictos con el proceso de arranque (Boot) del ESP8266. + +| RC522 | NodeMCU | GPIO | Notas | +| :--- | :--- | :--- | :--- | +| **SDA (SS)** | D8 | GPIO15 | Pull-down interno | +| **SCK** | D5 | GPIO14 | SPI Clock | +| **MOSI** | D7 | GPIO13 | SPI MOSI | +| **MISO** | D6 | GPIO12 | SPI MISO | +| **RST** | D0 | GPIO16 | **CRÍTICO:** Usado para Hard Reset del lector | +| **3.3V** | 3V3 | - | ⚠️ No usar 5V | +| **GND** | GND | - | - | + +| Periférico | NodeMCU | GPIO | Notas | +| :--- | :--- | :--- | :--- | +| **OLED SDA** | D2 | GPIO4 | I2C Data | +| **OLED SCL** | D1 | GPIO5 | I2C Clock | +| **Buzzer** | D4 | GPIO2 | Inicializado tarde en setup() | +| **LED / Botón** | D3 | GPIO0 | Flash Button (Pull-up) | + +--- + +## 🧠 Funcionamiento del Watchdog NFC + +El lector RC522 es propenso a congelarse por interferencia electromagnética. Este firmware implementa una solución de 3 capas: + +1. **Monitorización:** Cada 15 segundos se consulta la versión del firmware del chip RC522. +2. **Detección:** Si responde `0x00` o `0xFF`, se considera fallo. +3. **Recuperación:** + * Se pone el pin `RST` (D0) en `LOW` por 50ms (corte de energía lógico). + * Se restaura a `HIGH`. + * Se re-inicializa la librería y se fuerza la ganancia de antena a `Max (48dB)`. + +--- + +## 📡 Payloads + +### Entrada (En Tarjeta NFC) +El tag debe contener un registro NDEF con un JSON válido: +```json +{ + "name": "Ana Lopez", + "num_emp": "EMP001", + "sucursal": "Centro", + "telegram_id": "12345" +} +``` + +### Salida (Webhook POST) +El dispositivo envía este JSON al servidor: +```json +{ + "uuid": "AbC123XyZ", + "timestamp": 1703435600, + "datetime_utc": "2025-12-24T12:00:00Z", + "date": "2025-12-24", + "num_empleado": "EMP001", + "name": "Ana Lopez", + "branch": "Centro", + "telegram_id": "12345" +} +``` +*Headers:* `User-Agent`: (Nombre configurado en el portal) + +--- + +## 🗺️ Roadmap de Desarrollo + +### 📍 Fase 1: Optimización de Transmisión (Corto Plazo) +* **Payload Base64:** Migrar el formato de envío de JSON plano a su equivalente codificado en **Base64**. Esto mejorará la integridad de los datos y facilitará el manejo de caracteres especiales en el transporte HTTP. + +### 📍 Fase 2: Persistencia Offline (Medio Plazo) +* **Lector MicroSD:** Integración de hardware para almacenamiento local mediante bus SPI. +* **Sincronización Inteligente:** + * Guardado automático de registros en la MicroSD cuando falla la conexión WiFi/Internet. + * Lógica de "Checkpoint": Al recuperar la conexión, el dispositivo verificará cuál fue el último registro recibido por el servidor y enviará en ráfaga los datos pendientes para garantizar **cero pérdida de información**. + +### 📍 Fase 3: Modernización de Hardware (Investigación) +* **Soporte para Apple/Google Wallet:** Investigación y prototipado con lectores que soporten protocolos **NFC VAS (Apple)** y **Smart Tap (Google)**. El objetivo es permitir que los empleados usen sus pases digitales en el móvil en lugar de tarjetas físicas, mejorando la seguridad y la experiencia de usuario. + +--- + +## 📄 Licencia +MIT License. \ No newline at end of file diff --git a/compile_and_upload.sh b/compile_and_upload.sh new file mode 100755 index 0000000..990c266 --- /dev/null +++ b/compile_and_upload.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# Script para compilar, cargar y abrir monitor serial del checador NFC +# Carga con barra de progreso REAL (parseando esptool) + +# ───────────────────── Colores ───────────────────── +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' +BOLD='\033[1m' + +# ─────────────────── Configuración ────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKETCH_DIR="$SCRIPT_DIR" +SKETCH_NAME="soul23_time_attendance" +FQBN="esp8266:esp8266:nodemcuv2" +PORT="/dev/ttyUSB0" +BAUD_RATE="115200" + +# ───────────────── Funciones ──────────────────────── +show_progress() { + local current=$1 + local total=$2 + local width=50 + local percent=$((current * 100 / total)) + local filled=$((current * width / total)) + local empty=$((width - filled)) + + printf "\r${CYAN}[" + printf "%${filled}s" | tr ' ' '█' + printf "%${empty}s" | tr ' ' '░' + printf "] %3d%%%s${NC}" "$percent" " " +} + +check_device_status() { + local port=$1 + + if [ -e "$port" ]; then + if arduino-cli board list 2>/dev/null | grep -q "$port"; then + echo -e "${GREEN}✅ Conectado${NC}" + else + echo -e "${YELLOW}⚠️ Puerto existe (no identificado)${NC}" + fi + else + echo -e "${RED}❌ No conectado${NC}" + fi +} + +detect_port() { + arduino-cli board list | grep -E "ttyUSB|ttyACM" | head -1 | awk '{print $1}' +} + +# ───────────────── Encabezado ─────────────────────── +clear +echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════════════╗${NC}" +echo -e "${BOLD}${BLUE}║ Checador NFC - Compilación y Carga Automática ║${NC}" +echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════════════╝${NC}\n" + +# ───────────────── Paso 1 ─────────────────────────── +echo -e "${BOLD}${CYAN}[1/4]${NC} Verificando conexión del dispositivo..." +echo -e " Puerto configurado: ${BOLD}$PORT${NC}" +echo -e " Estado: $(check_device_status "$PORT")" + +if [ ! -e "$PORT" ]; then + auto_port=$(detect_port) + if [ -z "$auto_port" ]; then + echo -e "${RED}❌ No se detectó ningún dispositivo${NC}" + exit 1 + fi + PORT="$auto_port" + echo -e " Puerto detectado automáticamente: ${BOLD}$PORT${NC}" +fi + +echo + +# ───────────────── Paso 2 ─────────────────────────── +echo -e "${BOLD}${CYAN}[2/4]${NC} Compilando sketch..." +arduino-cli compile --fqbn "$FQBN" "$SKETCH_DIR/$SKETCH_NAME.ino" +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Error de compilación${NC}" + exit 1 +fi +echo -e "${GREEN}✅ Compilación exitosa${NC}\n" + +# ───────────────── Paso 3 (PROGRESO REAL) ─────────── +echo -e "${BOLD}${CYAN}[3/4]${NC} Cargando firmware al dispositivo..." +echo -e " Puerto: ${BOLD}$PORT${NC}" +echo -e " Estado: $(check_device_status "$PORT")\n" + +upload_failed=0 +last_percent=0 + +arduino-cli upload \ + -p "$PORT" \ + --fqbn "$FQBN" \ + "$SKETCH_DIR/$SKETCH_NAME.ino" \ + --verbose 2>&1 | \ +while IFS= read -r line; do + if [[ "$line" =~ \(([0-9]+)\ \%\) ]]; then + percent="${BASH_REMATCH[1]}" + if [ "$percent" -ge "$last_percent" ]; then + show_progress "$percent" 100 + last_percent="$percent" + fi + elif echo "$line" | grep -qi "Hash of data verified"; then + show_progress 100 100 + printf "\n" + elif echo "$line" | grep -qi "error\|failed\|timeout"; then + upload_failed=1 + echo -e "\n${RED}❌ Error durante la carga${NC}" + echo "$line" + fi +done + +if [ "$upload_failed" -ne 0 ]; then + exit 1 +fi + +echo -e "${GREEN}✅ Firmware cargado exitosamente${NC}\n" + +# ───────────────── Paso 4 ─────────────────────────── +echo -e "${BOLD}${CYAN}[4/4]${NC} Abriendo monitor serial..." +echo -e " Puerto: ${BOLD}$PORT${NC}" +echo -e " Baud rate: ${BOLD}$BAUD_RATE${NC}\n" + +sleep 1 +arduino-cli monitor -p "$PORT" --config baudrate="$BAUD_RATE" + diff --git a/compiler_info.md b/compiler_info.md new file mode 100644 index 0000000..ad4bad4 --- /dev/null +++ b/compiler_info.md @@ -0,0 +1,332 @@ +# 🔧 Guía de Compilación y Carga del Firmware + +Esta guía te ayudará a compilar, cargar y monitorear el firmware del checador NFC en tu ESP8266 NodeMCU. + +--- + +## 🚀 Método Rápido (Script Automático) + +El proyecto incluye un script automatizado que realiza todo el proceso en un solo comando: + +```bash +./compile_and_upload.sh +``` + +### ✨ Características del Script + +#### 1. **Verificación de Estado del Dispositivo** 🔌 +- ✅ Detecta automáticamente si el ESP8266 está conectado +- ✅ Muestra información detallada del puerto y tipo de placa +- ✅ Detecta automáticamente el puerto si el configurado no está disponible +- ✅ Verifica el estado antes de cada operación crítica + +#### 2. **Barras de Progreso y Spinners** 📊 +- ✅ Spinner animado durante la compilación +- ✅ Indicadores de progreso durante la carga del firmware +- ✅ Etapas visuales: "Iniciando", "Escribiendo", "Verificando", "Completando" +- ✅ Feedback visual claro en cada paso + +#### 3. **Interfaz Mejorada** 🎨 +- ✅ Encabezado visual con bordes +- ✅ Colores diferenciados para cada tipo de mensaje +- ✅ Indicadores de progreso paso a paso [1/4], [2/4], etc. +- ✅ Mensajes de error más descriptivos con sugerencias + +#### 4. **Detección Automática** 🔍 +- ✅ Busca automáticamente dispositivos disponibles si el puerto configurado no funciona +- ✅ Pregunta al usuario si desea usar el puerto detectado +- ✅ Muestra todos los puertos disponibles si no encuentra ninguno + +### 📋 Ejemplo de Salida del Script + +``` +╔══════════════════════════════════════════════════════════╗ +║ Checador NFC - Compilación y Carga Automática ║ +╚══════════════════════════════════════════════════════════╝ + +[0/4] Verificando configuración... +✅ secrets.h encontrado + +[1/4] Verificando conexión del dispositivo... + Puerto configurado: /dev/ttyUSB0 + Estado: ✅ Conectado (NodeMCU 1.0) + Puerto seleccionado: /dev/ttyUSB0 + +[2/4] Compilando el sketch... + FQBN: esp8266:esp8266:nodemcuv2 + Sketch: soul23_time_attendance.ino + [|] Compilando... | ← Spinner animado + [✓] Compilación completada +✅ Compilación exitosa + +[3/4] Cargando firmware al dispositivo... + Puerto: /dev/ttyUSB0 + Estado: ✅ Conectado (NodeMCU 1.0) + [|] Escribiendo... | ← Spinner con etapas + [✓] Carga completada +✅ Firmware cargado exitosamente + +[4/4] Abriendo monitor serial... + Puerto: /dev/ttyUSB0 + Baud rate: 115200 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Monitor Serial (Presiona Ctrl+C para salir) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 🛠️ Personalización del Script + +Puedes modificar estas variables al inicio del script `compile_and_upload.sh`: + +```bash +PORT="/dev/ttyUSB0" # Puerto del dispositivo +FQBN="esp8266:esp8266:nodemcuv2" # Tipo de placa +BAUD_RATE="115200" # Velocidad del monitor serial +``` + +--- + +## 🔧 Comandos Individuales + +Si prefieres ejecutar los comandos manualmente o necesitas más control: + +### 1. Verificar puertos disponibles + +```bash +arduino-cli board list +``` + +### 2. Compilar el sketch + +```bash +arduino-cli compile --fqbn esp8266:esp8266:nodemcuv2 soul23_time_attendance.ino +``` + +**Nota:** El FQBN `esp8266:esp8266:nodemcuv2` es para NodeMCU 1.0 (ESP-12E Module). + +### 3. Cargar al dispositivo + +```bash +arduino-cli upload -p /dev/ttyUSB0 --fqbn esp8266:esp8266:nodemcuv2 soul23_time_attendance.ino +``` + +**⚠️ Importante:** Reemplaza `/dev/ttyUSB0` con el puerto correcto de tu dispositivo (verifica con `arduino-cli board list`) + +### 4. Abrir monitor serial + +```bash +arduino-cli monitor -p /dev/ttyUSB0 --config baudrate=115200 +``` + +**Para salir del monitor:** Presiona `Ctrl+C` + +### 5. Compilar sin cargar (solo verificar errores) + +```bash +arduino-cli compile --fqbn esp8266:esp8266:nodemcuv2 soul23_time_attendance.ino +``` + +### 6. Ver información detallada de compilación + +```bash +arduino-cli compile --fqbn esp8266:esp8266:nodemcuv2 --verbose soul23_time_attendance.ino +``` + +### 7. Limpiar archivos de compilación + +```bash +rm -rf build/ +``` + +--- + +## 📦 Instalación de Dependencias + +### Instalar Core de ESP8266 + +```bash +arduino-cli core install esp8266:esp8266 +``` + +### Instalar Librerías Necesarias + +```bash +# Librerías principales +arduino-cli lib install "MFRC522" +arduino-cli lib install "Adafruit SSD1306" +arduino-cli lib install "Adafruit GFX Library" +arduino-cli lib install "ArduinoJson" +arduino-cli lib install "Time" +``` + +--- + +## 🔍 Verificar Instalación + +### Ver librerías instaladas + +```bash +arduino-cli lib list +``` + +### Ver cores instalados + +```bash +arduino-cli core list +``` + +### Verificar FQBN correcto + +```bash +arduino-cli board listall | grep -i nodemcu +``` + +Salida esperada: +``` +NodeMCU 0.9 (ESP-12 Module) esp8266:esp8266:nodemcu +NodeMCU 1.0 (ESP-12E Module) esp8266:esp8266:nodemcuv2 +``` + +--- + +## 🐛 Solución de Problemas + +### Error: "No se encuentra secrets.h" + +```bash +cp secrets.h.example secrets.h +# Luego edita secrets.h con tus credenciales +``` + +### Error: "Puerto no encontrado" + +1. Verifica que el ESP8266 esté conectado: + ```bash + arduino-cli board list + ``` + +2. Si no aparece, verifica permisos: + ```bash + sudo usermod -a -G dialout $USER + ``` + Luego reinicia sesión o ejecuta: + ```bash + newgrp dialout + ``` + +### Error: "FQBN no válido" + +Verifica el modelo exacto de tu NodeMCU y ajusta el FQBN: +- **NodeMCU 1.0 (ESP-12E):** `esp8266:esp8266:nodemcuv2` +- **NodeMCU 0.9:** `esp8266:esp8266:nodemcu` + +### Monitor serial no muestra datos + +1. Verifica el baud rate (debe ser **115200**) +2. Presiona el botón **RESET** del ESP8266 +3. Verifica que el puerto sea correcto +4. Asegúrate de que no haya otro programa usando el puerto serial + +### Error durante la carga del firmware + +1. **Desconecta el buzzer** temporalmente (desconecta el GND) durante la carga +2. Verifica que no haya otro programa usando el puerto +3. Presiona el botón **RESET** del ESP8266 antes de cargar +4. Si es necesario, mantén presionado el botón **BOOT** mientras cargas + +### El script no encuentra el dispositivo + +1. El script buscará automáticamente puertos disponibles +2. Si encuentra uno, te preguntará si deseas usarlo +3. Si no encuentra ninguno, verifica: + - Que el cable USB esté bien conectado + - Que el driver USB-Serial esté instalado + - Que tengas permisos para acceder al puerto + +--- + +## 💡 Tips y Mejores Prácticas + +### Uso del Script Automático + +- El script detecta automáticamente el puerto si el configurado no está disponible +- Los spinners muestran que el proceso está en ejecución +- Los mensajes de error incluyen sugerencias para solucionarlos +- El estado del dispositivo se verifica antes de cada operación crítica + +### Desarrollo y Depuración + +- **Mantén el monitor serial abierto** mientras pruebas para ver los mensajes de debug +- El código imprime información útil en Serial a 115200 baud +- Si cambias el código, solo necesitas recompilar y cargar (no necesitas cerrar el monitor) +- Para depuración, busca mensajes que empiezan con `--- Checador IOT (Public Version) ---` + +### Flujo de Trabajo Recomendado + +1. **Primera vez:** + ```bash + # Instalar dependencias + arduino-cli core install esp8266:esp8266 + arduino-cli lib install "MFRC522" "Adafruit SSD1306" "Adafruit GFX Library" "ArduinoJson" "Time" + + # Configurar secrets.h + cp secrets.h.example secrets.h + # Editar secrets.h con tus credenciales + + # Compilar y cargar + ./compile_and_upload.sh + ``` + +2. **Desarrollo iterativo:** + ```bash + # Solo necesitas ejecutar el script + ./compile_and_upload.sh + ``` + +3. **Solo verificar compilación:** + ```bash + arduino-cli compile --fqbn esp8266:esp8266:nodemcuv2 soul23_time_attendance.ino + ``` + +### Optimización + +- Si tienes problemas de espacio, puedes limpiar archivos de compilación: + ```bash + rm -rf build/ + ``` +- Para compilaciones más rápidas, el script ya optimiza el proceso automáticamente + +--- + +## 📝 Resumen de Comandos Rápidos + +```bash +# Método rápido (recomendado) +./compile_and_upload.sh + +# Comandos individuales +arduino-cli board list # Ver puertos +arduino-cli compile --fqbn esp8266:esp8266:nodemcuv2 soul23_time_attendance.ino +arduino-cli upload -p /dev/ttyUSB0 --fqbn esp8266:esp8266:nodemcuv2 soul23_time_attendance.ino +arduino-cli monitor -p /dev/ttyUSB0 --config baudrate=115200 + +# Verificación +arduino-cli lib list # Ver librerías +arduino-cli core list # Ver cores +arduino-cli board listall | grep -i nodemcu # Ver placas disponibles +``` + +--- + +## 🎯 Próximos Pasos + +Una vez que hayas compilado y cargado el firmware exitosamente: + +1. ✅ Verifica que el dispositivo se conecte a WiFi +2. ✅ Revisa el monitor serial para confirmar la sincronización de tiempo +3. ✅ Prueba acercando una tarjeta NFC para verificar la lectura +4. ✅ Verifica que los registros se envíen correctamente al webhook + +Para más información sobre el hardware y la configuración, consulta el [README.md](README.md). + + diff --git a/kicad/attendance_1/attendance_1-backups/attendance_1-2025-12-18_214149.zip b/kicad/attendance_1/attendance_1-backups/attendance_1-2025-12-18_214149.zip new file mode 100644 index 0000000..6e57c1b Binary files /dev/null and b/kicad/attendance_1/attendance_1-backups/attendance_1-2025-12-18_214149.zip differ diff --git a/kicad/attendance_1/attendance_1.kicad_pcb b/kicad/attendance_1/attendance_1.kicad_pcb new file mode 100644 index 0000000..c547853 --- /dev/null +++ b/kicad/attendance_1/attendance_1.kicad_pcb @@ -0,0 +1,2 @@ +(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0") +) \ No newline at end of file diff --git a/kicad/attendance_1/attendance_1.kicad_prl b/kicad/attendance_1/attendance_1.kicad_prl new file mode 100644 index 0000000..803fdf9 --- /dev/null +++ b/kicad/attendance_1/attendance_1.kicad_prl @@ -0,0 +1,98 @@ +{ + "board": { + "active_layer": 0, + "active_layer_preset": "", + "auto_track_width": true, + "hidden_netclasses": [], + "hidden_nets": [], + "high_contrast_mode": 0, + "net_color_mode": 1, + "opacity": { + "images": 0.6, + "pads": 1.0, + "shapes": 1.0, + "tracks": 1.0, + "vias": 1.0, + "zones": 0.6 + }, + "selection_filter": { + "dimensions": true, + "footprints": true, + "graphics": true, + "keepouts": true, + "lockedItems": false, + "otherItems": true, + "pads": true, + "text": true, + "tracks": true, + "vias": true, + "zones": true + }, + "visible_items": [ + "vias", + "footprint_text", + "footprint_anchors", + "ratsnest", + "grid", + "footprints_front", + "footprints_back", + "footprint_values", + "footprint_references", + "tracks", + "drc_errors", + "drawing_sheet", + "bitmaps", + "pads", + "zones", + "drc_warnings", + "drc_exclusions", + "locked_item_shadows", + "conflict_shadows", + "shapes" + ], + "visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff", + "zone_display_mode": 0 + }, + "git": { + "repo_type": "", + "repo_username": "", + "ssh_key": "" + }, + "meta": { + "filename": "attendance_1.kicad_prl", + "version": 5 + }, + "net_inspector_panel": { + "col_hidden": [], + "col_order": [], + "col_widths": [], + "custom_group_rules": [], + "expanded_rows": [], + "filter_by_net_name": true, + "filter_by_netclass": true, + "filter_text": "", + "group_by_constraint": false, + "group_by_netclass": false, + "show_unconnected_nets": false, + "show_zero_pad_nets": false, + "sort_ascending": true, + "sorting_column": -1 + }, + "open_jobsets": [], + "project": { + "files": [] + }, + "schematic": { + "selection_filter": { + "graphics": true, + "images": true, + "labels": true, + "lockedItems": false, + "otherItems": true, + "pins": true, + "symbols": true, + "text": true, + "wires": true + } + } +} diff --git a/kicad/attendance_1/attendance_1.kicad_pro b/kicad/attendance_1/attendance_1.kicad_pro new file mode 100644 index 0000000..48c1135 --- /dev/null +++ b/kicad/attendance_1/attendance_1.kicad_pro @@ -0,0 +1,413 @@ +{ + "board": { + "3dviewports": [], + "design_settings": { + "defaults": {}, + "diff_pair_dimensions": [], + "drc_exclusions": [], + "rules": {}, + "track_widths": [], + "via_dimensions": [] + }, + "ipc2581": { + "dist": "", + "distpn": "", + "internal_id": "", + "mfg": "", + "mpn": "" + }, + "layer_pairs": [], + "layer_presets": [], + "viewports": [] + }, + "boards": [], + "cvpcb": { + "equivalence_files": [] + }, + "erc": { + "erc_exclusions": [], + "meta": { + "version": 0 + }, + "pin_map": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 2 + ], + [ + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2 + ], + [ + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 2, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ] + ], + "rule_severities": { + "bus_definition_conflict": "error", + "bus_entry_needed": "error", + "bus_to_bus_conflict": "error", + "bus_to_net_conflict": "error", + "different_unit_footprint": "error", + "different_unit_net": "error", + "duplicate_reference": "error", + "duplicate_sheet_names": "error", + "endpoint_off_grid": "warning", + "extra_units": "error", + "footprint_filter": "ignore", + "footprint_link_issues": "warning", + "four_way_junction": "ignore", + "global_label_dangling": "warning", + "hier_label_mismatch": "error", + "label_dangling": "error", + "label_multiple_wires": "warning", + "lib_symbol_issues": "warning", + "lib_symbol_mismatch": "warning", + "missing_bidi_pin": "warning", + "missing_input_pin": "warning", + "missing_power_pin": "error", + "missing_unit": "warning", + "multiple_net_names": "warning", + "net_not_bus_member": "warning", + "no_connect_connected": "warning", + "no_connect_dangling": "warning", + "pin_not_connected": "error", + "pin_not_driven": "error", + "pin_to_pin": "warning", + "power_pin_not_driven": "error", + "same_local_global_label": "warning", + "similar_label_and_power": "warning", + "similar_labels": "warning", + "similar_power": "warning", + "simulation_model_issue": "ignore", + "single_global_label": "ignore", + "unannotated": "error", + "unconnected_wire_endpoint": "warning", + "undefined_netclass": "error", + "unit_value_mismatch": "error", + "unresolved_variable": "error", + "wire_dangling": "error" + } + }, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "attendance_1.kicad_pro", + "version": 3 + }, + "net_settings": { + "classes": [ + { + "bus_width": 12, + "clearance": 0.2, + "diff_pair_gap": 0.25, + "diff_pair_via_gap": 0.25, + "diff_pair_width": 0.2, + "line_style": 0, + "microvia_diameter": 0.3, + "microvia_drill": 0.1, + "name": "Default", + "pcb_color": "rgba(0, 0, 0, 0.000)", + "priority": 2147483647, + "schematic_color": "rgba(0, 0, 0, 0.000)", + "track_width": 0.2, + "via_diameter": 0.6, + "via_drill": 0.3, + "wire_width": 6 + } + ], + "meta": { + "version": 4 + }, + "net_colors": null, + "netclass_assignments": null, + "netclass_patterns": [] + }, + "pcbnew": { + "last_paths": { + "gencad": "", + "idf": "", + "netlist": "", + "plot": "", + "pos_files": "", + "specctra_dsn": "", + "step": "", + "svg": "", + "vrml": "" + }, + "page_layout_descr_file": "" + }, + "schematic": { + "annotate_start_num": 0, + "bom_export_filename": "${PROJECTNAME}.csv", + "bom_fmt_presets": [], + "bom_fmt_settings": { + "field_delimiter": ",", + "keep_line_breaks": false, + "keep_tabs": false, + "name": "CSV", + "ref_delimiter": ",", + "ref_range_delimiter": "", + "string_delimiter": "\"" + }, + "bom_presets": [], + "bom_settings": { + "exclude_dnp": false, + "fields_ordered": [ + { + "group_by": false, + "label": "Reference", + "name": "Reference", + "show": true + }, + { + "group_by": false, + "label": "Qty", + "name": "${QUANTITY}", + "show": true + }, + { + "group_by": true, + "label": "Value", + "name": "Value", + "show": true + }, + { + "group_by": true, + "label": "DNP", + "name": "${DNP}", + "show": true + }, + { + "group_by": true, + "label": "Exclude from BOM", + "name": "${EXCLUDE_FROM_BOM}", + "show": true + }, + { + "group_by": true, + "label": "Exclude from Board", + "name": "${EXCLUDE_FROM_BOARD}", + "show": true + }, + { + "group_by": true, + "label": "Footprint", + "name": "Footprint", + "show": true + }, + { + "group_by": false, + "label": "Datasheet", + "name": "Datasheet", + "show": true + } + ], + "filter_string": "", + "group_symbols": true, + "include_excluded_from_bom": true, + "name": "Default Editing", + "sort_asc": true, + "sort_field": "Reference" + }, + "connection_grid_size": 50.0, + "drawing": { + "dashed_lines_dash_length_ratio": 12.0, + "dashed_lines_gap_length_ratio": 3.0, + "default_line_thickness": 6.0, + "default_text_size": 50.0, + "field_names": [], + "intersheets_ref_own_page": false, + "intersheets_ref_prefix": "", + "intersheets_ref_short": false, + "intersheets_ref_show": false, + "intersheets_ref_suffix": "", + "junction_size_choice": 3, + "label_size_ratio": 0.375, + "operating_point_overlay_i_precision": 3, + "operating_point_overlay_i_range": "~A", + "operating_point_overlay_v_precision": 3, + "operating_point_overlay_v_range": "~V", + "overbar_offset_ratio": 1.23, + "pin_symbol_size": 25.0, + "text_offset_ratio": 0.15 + }, + "legacy_lib_dir": "", + "legacy_lib_list": [], + "meta": { + "version": 1 + }, + "net_format_name": "", + "page_layout_descr_file": "", + "plot_directory": "", + "space_save_all_events": true, + "spice_current_sheet_as_root": false, + "spice_external_command": "spice \"%I\"", + "spice_model_current_sheet_as_root": true, + "spice_save_all_currents": false, + "spice_save_all_dissipations": false, + "spice_save_all_voltages": false, + "subpart_first_id": 65, + "subpart_id_separator": 0 + }, + "sheets": [], + "text_variables": {} +} diff --git a/kicad/attendance_1/attendance_1.kicad_sch b/kicad/attendance_1/attendance_1.kicad_sch new file mode 100644 index 0000000..a975c4d --- /dev/null +++ b/kicad/attendance_1/attendance_1.kicad_sch @@ -0,0 +1,14 @@ +(kicad_sch + (version 20250114) + (generator "eeschema") + (generator_version "9.0") + (uuid 82b3a318-584c-487b-a2ab-9cfe2954db04) + (paper "A4") + (lib_symbols) + (sheet_instances + (path "/" + (page "1") + ) + ) + (embedded_fonts no) +) \ No newline at end of file diff --git a/kicad/attendance_1/sym-lib-table b/kicad/attendance_1/sym-lib-table new file mode 100644 index 0000000..3e7db0e --- /dev/null +++ b/kicad/attendance_1/sym-lib-table @@ -0,0 +1,4 @@ +(sym_lib_table + (version 7) + (lib (name "ESP8266")(type "Legacy")(uri "/home/marco/kicad/libraries/kicad-ESP8266/ESP8266.lib")(options "")(descr "")) +) diff --git a/secrets.h.example b/secrets.h.example new file mode 100644 index 0000000..933d1c8 --- /dev/null +++ b/secrets.h.example @@ -0,0 +1,25 @@ +// Por favor, renombra este archivo a "secrets.h" y rellena tus datos. +// Este archivo NO se subirá a Git, protegiendo tu información. + +#ifndef SECRETS_H +#define SECRETS_H + +// ========================================== +// ⚙️ CONFIGURACIÓN DE CREDENCIALES +// ========================================== + +// Red WiFi +const char* ssid = "TU_WIFI_SSID"; +const char* password = "TU_WIFI_PASSWORD"; + +// Webhook y Time Server +const char* webhookUrl = "URL_DE_TU_WEBHOOK"; +const char* timeServerUrl = "https://worldtimeapi.org/api/ip"; // Servidor público de ejemplo + +// Nombre del dispositivo para la cabecera User-Agent (Identifica el origen) +const char* DEVICE_NAME = "Checador_Oficina_1"; + +// Zona Horaria (Ej: Monterrey UTC-6 -> -6 * 3600 = -21600) +const long timeZoneOffset = -21600; + +#endif // SECRETS_H diff --git a/soul23_time_attendance.ino b/soul23_time_attendance.ino new file mode 100644 index 0000000..325ad61 --- /dev/null +++ b/soul23_time_attendance.ino @@ -0,0 +1,774 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // Requiere instalar: arduino-cli lib install "Time" +#include +#include + +#include "Config.h" + +// Constantes globales de configuración +const char* timeServerUrl = "https://worldtimeapi.org/api/ip"; +// const char* DEVICE_NAME = "Checador_Oficina_1"; // Eliminado, ahora es dinámico +const long timeZoneOffset = -21600; + +// Intervalo de resincronización del reloj (1 hora) +const unsigned long syncInterval = 3600000; + +// ========================================== +// 🔌 PINES SEGUROS (NO INTERFIEREN CON BOOT) +// ========================================== +#define SS_PIN D8 // GPIO 15 +#define RST_PIN D0 // GPIO 16 +#define OLED_SDA D2 +#define OLED_SCL D1 +#define LED_PIN D3 // LED de estatus (GPIO 0) - Inicializado con pull-up para evitar problemas de boot +#define BUZZER_PIN D4 // Buzzer (GPIO 2) - ⚠️ IMPORTANTE: Inicializado MUY TARDE en setup() para evitar problemas de boot + +MFRC522 mfrc522(SS_PIN, RST_PIN); + +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); + +// Variables para el modo configuración +Config config; +ESP8266WebServer *server = nullptr; +DNSServer *dnsServer = nullptr; +bool configurationMode = false; +bool shouldRestart = false; + +// Variables de control +unsigned long lastSyncTime = 0; +int lastMinuteDisplayed = -1; +bool hasError = false; +unsigned long lastBlinkTime = 0; +bool ledState = LOW; + +// Control del Watchdog del Lector NFC +const unsigned long nfcCheckInterval = 15000; // 15 segundos para detección rápida de fallos +unsigned long lastNfcCheck = 0; + + +// ========================================== +// 📝 DECLARACIONES DE FUNCIONES +// ========================================== +void checkAndResetNFC(); +void showStatus(String line1, String line2); +void syncTimeWithServer(); +bool sendToWebhook(String uid, String name, String num_emp, String sucursal, String telegram_id); +void showClockScreen(); +String getFormattedTime(); +bool readCardData(byte *data, byte *length); +void processTag(); +String generateUUID(byte length); +String getISO8601DateTime(time_t t); +String getFormattedDate(); +void beep(int times, int duration); +void alarmSound(); +void silenceBuzzer(); +void updateStatusLED(); +void scanI2C(); +void startConfigurationMode(); + +// ========================================== +// 🚀 SETUP +// ========================================== +void setup() { + Serial.println("\n--- Iniciando setup ---"); + // ⚠️ CRÍTICO: Inicializar GPIO 2 (BUZZER) PRIMERO para evitar ruido durante boot + // GPIO 2 tiene pull-up interno que puede causar problemas con buzzer pasivo + // Configurarlo como INPUT sin pull-up/pull-down para evitar interferencia + pinMode(BUZZER_PIN, INPUT); // INPUT sin pull para evitar ruido durante boot + delay(10); // Pequeña pausa para estabilizar + + // ⚠️ INICIALIZAR GPIO 0 (LED) CON PULL-UP para evitar problemas de boot + // Si GPIO 0 está LOW durante boot, el ESP8266 entra en modo programación + pinMode(LED_PIN, INPUT_PULLUP); // Primero como INPUT con pull-up interno + delay(10); // Pequeña pausa para estabilizar + + Serial.begin(115200); + delay(100); + + WiFi.persistent(false); // Evita la reconexión automática que causa conflictos + + Serial.println("\n\n--- Checador IOT (Public Version) ---"); + + randomSeed(analogRead(A0)); + + SPI.begin(); + Wire.begin(OLED_SDA, OLED_SCL); + + scanI2C(); + mfrc522.PCD_Init(); + mfrc522.PCD_DumpVersionToSerial(); + mfrc522.PCD_SetAntennaGain(mfrc522.RxGain_max); // 🚀 Maxima ganancia antena + + if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { + Serial.println(F("Intentando dirección OLED 0x3D...")); + if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { + Serial.println(F("Error: No se encuentra OLED")); + sos(); + } + } + display.clearDisplay(); + display.setTextColor(WHITE); + display.dim(false); + Serial.println("OLED inicializado y configurado."); + + if (!LittleFS.begin()) { + Serial.println("LittleFS mount failed. Formatting..."); + if (LittleFS.format()) { + Serial.println("LittleFS formatted successfully"); + if (LittleFS.begin()) { + Serial.println("LittleFS mounted after format"); + } else { + Serial.println("LittleFS mount failed after format"); + } + } else { + Serial.println("LittleFS format failed"); + } + } else { + Serial.println("LittleFS mounted successfully"); + } + // --- Lógica de Doble Reset (Double Tap) --- + uint32_t rtcMagic = 0; + ESP.rtcUserMemoryRead(0, &rtcMagic, sizeof(rtcMagic)); + bool doubleResetDetected = (rtcMagic == 0x1A2B3C4D); + + // Armar el detector para el siguiente reset (se desarmará en loop después de 3s) + rtcMagic = 0x1A2B3C4D; + ESP.rtcUserMemoryWrite(0, &rtcMagic, sizeof(rtcMagic)); + + // Verificar botón físico (Flash/D3) O Doble Reset + bool forceConfigMode = (digitalRead(LED_PIN) == LOW) || doubleResetDetected; + + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + Serial.print("Modo Config forzado: "); + if (digitalRead(LED_PIN) == LOW) Serial.print("BOTON "); + if (doubleResetDetected) Serial.print("DOBLE_RESET "); + Serial.println(forceConfigMode ? "(SI)" : "(NO)"); + + bool configLoaded = loadConfig(config); + Serial.print("Configuración cargada exitosamente: "); + Serial.println(configLoaded ? "Sí" : "No"); + + if (configLoaded && !forceConfigMode) { + showStatus("CONECTANDO", "WIFI..."); + WiFi.mode(WIFI_STA); + WiFi.begin(config.ssid, config.password); + + int timeout = 0; + while (WiFi.status() != WL_CONNECTED && timeout < 60) { + delay(500); Serial.print("."); timeout++; + } + + if (WiFi.status() == WL_CONNECTED) { + showStatus("WIFI", "OK"); + delay(1000); + configurationMode = false; + } else { + showStatus("ERROR WIFI", "MODO CONFIG"); + delay(2000); + configurationMode = true; + } + } else { + showStatus("HOLA!", "MODO CONFIG"); + delay(2000); + configurationMode = true; + } + + if (configurationMode) { + startConfigurationMode(); + } else { + syncTimeWithServer(); + Serial.println("Sistema Listo."); + + delay(100); + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); + delay(50); + digitalWrite(BUZZER_PIN, LOW); + delay(10); + digitalWrite(BUZZER_PIN, LOW); + + blinkLed(5, 200); + digitalWrite(LED_PIN, LOW); + silenceBuzzer(); + } +} + +// ========================================== +// 🔄 LOOP PRINCIPAL +// ========================================== +void loop() { + // Desarmar Doble Reset después de 3 segundos de funcionamiento estable + static bool drDisarmed = false; + if (!drDisarmed && millis() > 3000) { + uint32_t rtcMagic = 0; + ESP.rtcUserMemoryWrite(0, &rtcMagic, sizeof(rtcMagic)); + drDisarmed = true; + // Serial.println("Double Reset: Desarmado"); // Opcional para debug + } + + if (shouldRestart) { + Serial.println(F("Reiniciando sistema...")); + delay(2000); + ESP.restart(); + } + + if (configurationMode) { + if (dnsServer) dnsServer->processNextRequest(); + if (server) server->handleClient(); + delay(10); + } else { + checkAndResetNFC(); + + if (timeStatus() == timeNotSet || (millis() - lastSyncTime > syncInterval)) { + syncTimeWithServer(); + } + + if (minute() != lastMinuteDisplayed) { + showClockScreen(); + lastMinuteDisplayed = minute(); + } + + if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) { + processTag(); + lastMinuteDisplayed = -1; + } + + silenceBuzzer(); + delay(50); + } +} + +// ========================================== +// 📋 LÓGICA DE PROCESAMIENTO +// ========================================== +void processTag() { + String uid = ""; + for (byte i = 0; i < mfrc522.uid.size; i++) { + uid += (mfrc522.uid.uidByte[i] < 0x10 ? "0" : ""); + uid += String(mfrc522.uid.uidByte[i], HEX); + } + uid.toUpperCase(); + + byte buffer[128]; + byte bufferSize = sizeof(buffer); + + if (readCardData(buffer, &bufferSize)) { + int jsonStart = -1; + for (int i = 0; i < bufferSize; i++) { if (buffer[i] == '{') { jsonStart = i; break; } } + + if (jsonStart != -1) { + DynamicJsonDocument docCard(512); + DeserializationError error = deserializeJson(docCard, (const char*)(buffer + jsonStart)); + + if (error) { + showStatus("ERROR", "JSON ILEGIBLE"); + signalTemporaryError(); + } else { + String name = docCard["name"]; + String num_emp = docCard["num_emp"]; + String sucursal = docCard["sucursal"]; + String telegram_id = docCard["telegram_id"]; + + sendToWebhook(uid, name, num_emp, sucursal, telegram_id); + } + } else { + showStatus("ERROR", "SIN JSON"); + signalTemporaryError(); + } + } else { + showStatus("ERROR", "LECTURA FALLO"); + signalTemporaryError(); + } + + mfrc522.PICC_HaltA(); + mfrc522.PCD_StopCrypto1(); + // El delay y el apagado del LED se manejan ahora en las funciones de señalización +} + +// ========================================== +// 🚨 WATCHDOG DEL LECTOR NFC +// ========================================== +/** + * @brief Verifica el estado del lector NFC y lo reinicia si no responde. + * + * Se ejecuta periódicamente según 'nfcCheckInterval'. Lee el registro de versión + * del chip MFRC522. Si devuelve 0x00 o 0xFF, asume que está colgado e intenta + * reinicializarlo mediante un HARD RESET físico (Pin RST). + */ +void checkAndResetNFC() { + if (millis() - lastNfcCheck > nfcCheckInterval) { + // 1. Heartbeat check: Read Firmware Version + byte v = mfrc522.PCD_ReadRegister(MFRC522::VersionReg); + + // Si la versión es 0x00 o 0xFF, la comunicación SPI o el módulo fallaron + if (v == 0x00 || v == 0xFF) { + Serial.println(F("NFC Watchdog: Lector NO responde. Ejecutando HARD RESET...")); + + // 2. Hard Reset Sequence (Physical Pin Toggle) + // Forzamos el pin RST a LOW para reiniciar el chip físicamente + pinMode(RST_PIN, OUTPUT); + digitalWrite(RST_PIN, LOW); + delay(50); // Mantener reset + digitalWrite(RST_PIN, HIGH); + delay(50); // Esperar a que el chip despierte + + // 3. Re-Inicializar Librería + mfrc522.PCD_Init(); + delay(10); + + // 4. Boost Sensitivity (Max Gain) - Importante para estabilidad + mfrc522.PCD_SetAntennaGain(mfrc522.RxGain_max); + + // 5. Verificar recuperación + byte v_new = mfrc522.PCD_ReadRegister(MFRC522::VersionReg); + if (v_new == 0x00 || v_new == 0xFF) { + Serial.println(F("NFC Watchdog: FALLO. El lector sigue sin responder.")); + } else { + Serial.print(F("NFC Watchdog: RECUPERADO. Versión: 0x")); + Serial.println(v_new, HEX); + } + } + // Si responde bien, no hacemos nada para no interrumpir el flujo normal + + lastNfcCheck = millis(); + } +} + +// ========================================== +// ☁️ RED Y LECTURA DE TARJETA +// ========================================== +/** + * @brief Lee los datos almacenados en los bloques de la tarjeta MIFARE. + * + * Intenta autenticarse con la clave por defecto (NFC Forum Key) y leer + * los bloques 4 a 12 de la tarjeta. + * + * @param data Puntero al buffer donde se guardarán los datos leídos. + * @param length Puntero al tamaño del buffer (se actualiza con bytes leídos). + * @return true Si la lectura fue exitosa. + * @return false Si hubo error de autenticación o lectura. + */ +bool readCardData(byte *data, byte *length) { + MFRC522::MIFARE_Key key; + byte nfcForumKey[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; + memcpy(key.keyByte, nfcForumKey, 6); + + MFRC522::StatusCode status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, 4, &key, &(mfrc522.uid)); + if (status != MFRC522::STATUS_OK) { return false; } + + delay(5); + + byte readBlock[18]; + byte index = 0; + for (byte block = 4; block < 12; block++) { + if (block % 4 == 3) { + status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block + 1, &key, &(mfrc522.uid)); + if (status != MFRC522::STATUS_OK) break; + continue; + } + byte size = sizeof(readBlock); + status = mfrc522.MIFARE_Read(block, readBlock, &size); + if (status != MFRC522::STATUS_OK) break; + memcpy(&data[index], readBlock, 16); + index += 16; + } + *length = index; + return index > 0; +} + +/** + * @brief Sincroniza la hora interna del ESP8266 con un servidor NTP/API externo. + * + * Realiza una petición HTTP GET a 'timeServerUrl' (WorldTimeAPI). + * Parsea el JSON de respuesta y establece la hora del sistema usando 'TimeLib'. + */ +void syncTimeWithServer() { + if (WiFi.status() != WL_CONNECTED) return; + std::unique_ptr client(new WiFiClientSecure); + client->setInsecure(); + HTTPClient http; + if (http.begin(*client, timeServerUrl)) { + http.setTimeout(10000); // Timeout de 10 segundos para evitar resets por watchdog + http.setUserAgent(config.deviceName); + int httpCode = http.GET(); + if (httpCode == HTTP_CODE_OK) { + DynamicJsonDocument doc(1024); + deserializeJson(doc, http.getString()); + // Ajuste para el servidor de ejemplo worldtimeapi.org + setTime(doc["unixtime"].as() + timeZoneOffset); + lastSyncTime = millis(); + } + http.end(); + } +} + +/** + * @brief Envía el registro de asistencia al Webhook configurado. + * + * Construye un JSON con los datos del empleado y la tarjeta, y lo envía + * mediante una petición HTTP POST segura (HTTPS). + * + * @param uid UID de la tarjeta leída. + * @param name Nombre del empleado. + * @param num_emp Número de empleado. + * @param sucursal Sucursal asignada. + * @param telegram_id ID de Telegram para notificaciones. + * @return true Si el envío fue exitoso (HTTP 2xx). + * @return false Si hubo error en la conexión o respuesta. + */ +bool sendToWebhook(String uid, String name, String num_emp, String sucursal, String telegram_id) { + if (timeStatus() == timeNotSet) { + showStatus("ERROR", "SIN HORA"); + signalTemporaryError(); + return false; + } + + std::unique_ptr client(new WiFiClientSecure); + client->setInsecure(); + HTTPClient http; + + unsigned long utcTimestamp = now() - timeZoneOffset; + String uuid = generateUUID(11); + String date = getFormattedDate(); + String datetime_utc = getISO8601DateTime(utcTimestamp); + + showStatus("ENVIANDO", "REGISTRO..."); + + Serial.print("Webhook URL: "); Serial.println(config.webhookUrl); + + if (http.begin(*client, config.webhookUrl)) { + http.setTimeout(15000); // Aumentar timeout a 15s + http.setUserAgent(config.deviceName); + http.addHeader("Content-Type", "application/json"); + + DynamicJsonDocument doc(1024); + JsonArray array = doc.to(); + + JsonObject obj = array.createNestedObject(); + obj["uuid"] = uuid; + obj["timestamp"] = utcTimestamp; + obj["datetime_utc"] = datetime_utc; + obj["date"] = date; + obj["num_empleado"] = num_emp; + obj["name"] = name; + obj["branch"] = sucursal; + obj["telegram_id"] = telegram_id; + + String jsonPayload; + serializeJson(doc, jsonPayload); + Serial.print("Payload: "); Serial.println(jsonPayload); // Debug payload + + int httpResponseCode = http.POST(jsonPayload); + Serial.print("HTTP Code: "); Serial.println(httpResponseCode); // Debug code + + if (httpResponseCode > 0) { + String response = http.getString(); + Serial.println("Respuesta: " + response); + } else { + Serial.print("Error HTTP: "); + Serial.println(http.errorToString(httpResponseCode)); + } + + if (httpResponseCode >= 200 && httpResponseCode < 300) { + showStatus("HOLA :)", name); + http.end(); + blinkLed(5, 200); // Éxito - Parpadeo de 5 veces (mismo comportamiento que boot) + return true; + } else { + showStatus("ERROR", "AL ENVIAR"); + http.end(); + signalTemporaryError(); + return false; + } + } + signalTemporaryError(); + return false; +} + +// ========================================== +// 🔊 BUZZER Y LEDS +// ========================================== + +void signalTemporaryError() { + alarmSound(); + digitalWrite(LED_PIN, HIGH); + delay(5000); + digitalWrite(LED_PIN, LOW); +} + +void blinkLed(int times, int duration) { + for (int i = 0; i < times; i++) { + digitalWrite(LED_PIN, HIGH); + delay(duration); + digitalWrite(LED_PIN, LOW); + if (i < times - 1) delay(duration); + } +} + +void morseDot() { + digitalWrite(LED_PIN, HIGH); + delay(250); + digitalWrite(LED_PIN, LOW); + delay(250); +} + +void morseDash() { + digitalWrite(LED_PIN, HIGH); + delay(750); + digitalWrite(LED_PIN, LOW); + delay(250); +} + +void sos() { + while(true) { // Bucle infinito para S.O.S. + // SOS con la luz: ... --- ... (3 segundos) + morseDot(); morseDot(); morseDot(); + delay(500); + morseDash(); morseDash(); morseDash(); + delay(500); + morseDot(); morseDot(); morseDot(); + delay(3000); // 3 segundos de SOS + delay(10000); // Esperar 10 segundos antes de repetir + } +} + +void beep(int times, int duration) { + // Asegurar que el pin esté configurado (por si se llama antes del setup completo) + static bool buzzerInitialized = false; + if (!buzzerInitialized) { + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); + buzzerInitialized = true; + } + + for (int i = 0; i < times; i++) { + digitalWrite(BUZZER_PIN, HIGH); + delay(duration); + digitalWrite(BUZZER_PIN, LOW); + if (i < times - 1) delay(duration / 2); + } + + // Asegurar que el buzzer quede silenciado después de cada beep + digitalWrite(BUZZER_PIN, LOW); + delay(10); // Pequeña pausa para estabilizar +} + +void alarmSound() { + beep(3, 150); // 3 beeps cortos y rápidos para el error + silenceBuzzer(); // Asegurar que quede silenciado después del error +} + +void silenceBuzzer() { + // Función para asegurar que el buzzer esté completamente silenciado + // Útil para evitar ruido o interferencia electromagnética + // Asegurar que el pin esté configurado como OUTPUT y en LOW + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); +} + +// ========================================== +// 🖥️ PANTALLA Y UTILIDADES +// ========================================== +String generateUUID(byte length) { + String randomString = ""; + const char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (byte i = 0; i < length; i++) { + randomString += charset[random(sizeof(charset) - 1)]; + } + return randomString; +} + +String getISO8601DateTime(time_t t) { + char buff[21]; + sprintf(buff, "%04d-%02d-%02dT%02d:%02d:%02dZ", + year(t), month(t), day(t), hour(t), minute(t), second(t)); + return String(buff); +} + +String getFormattedDate() { + String dateStr = String(year()) + "-"; + if (month() < 10) dateStr += "0"; dateStr += String(month()); + dateStr += "-"; + if (day() < 10) dateStr += "0"; dateStr += String(day()); + return dateStr; +} + +String getFormattedTime() { + String timeStr = ""; + int h = hour(); int m = minute(); String ampm = "AM"; + if (h >= 12) { ampm = "PM"; if (h > 12) h -= 12; } + if (h == 0) h = 12; + if (h < 10) timeStr += " "; timeStr += String(h); timeStr += ":"; + if (m < 10) timeStr += "0"; timeStr += String(m); timeStr += " " + ampm; + return timeStr; +} + +void showClockScreen() { + display.clearDisplay(); + display.setTextSize(1); display.setCursor(0, 0); display.println(F("CHECADOR")); + display.setTextSize(2); display.setCursor(10, 25); + if (timeStatus() != timeNotSet) { display.println(getFormattedTime()); } + else { display.println("--:--"); } + display.setTextSize(1); display.setCursor(0, 55); display.println(F("ACERQUE TARJETA...")); + display.display(); +} + +void showStatus(String line1, String line2) { + display.clearDisplay(); + display.setTextSize(2); display.setCursor(0, 10); display.println(line1); + if(line2.length() > 8) display.setTextSize(1); else display.setTextSize(2); + display.setCursor(0, 35); display.println(line2); + display.display(); +} + +void scanI2C() { + byte error, address; + int nDevices = 0; + Serial.println("Dispositivos I2C encontrados:"); + for (address = 1; address < 127; address++) { + Wire.beginTransmission(address); + error = Wire.endTransmission(); + if (error == 0) { + Serial.print("Encontrado en 0x"); + if (address < 16) Serial.print("0"); + Serial.println(address, HEX); + nDevices++; + } else if (error == 4) { + Serial.print("Error desconocido en 0x"); + if (address < 16) Serial.print("0"); + Serial.println(address, HEX); + } + } + if (nDevices == 0) Serial.println("Ningún dispositivo I2C encontrado"); + else Serial.println("Escaneo terminado"); +} + +// ========================================== +// 🌐 MODO CONFIGURACIÓN WEB +// ========================================== + +const char index_html[] PROGMEM = R"rawliteral( + + + + Configuracion Checador + + + + +
+

Configurar Checador

+
+ + + + + + + + + +
+
+ + +)rawliteral"; + +const char saved_html[] PROGMEM = R"rawliteral( + + + + Guardado + + + + +
+

Configuracion Guardada!

+

El dispositivo se reiniciara...

+
+ + +)rawliteral"; + +void startConfigurationMode() { + Serial.println("DEBUG: Entering startConfigurationMode"); + const char* ap_ssid = "Soul23"; + const char* ap_password = "1234567890"; + + showStatus("MODO CONFIG", ap_ssid); + + WiFi.disconnect(true); + delay(1000); + + WiFi.mode(WIFI_AP); + delay(100); + WiFi.softAP(ap_ssid, ap_password); + delay(100); + + IPAddress apIP = WiFi.softAPIP(); + Serial.print("AP IP address: "); + Serial.println(apIP); + + if (!dnsServer) dnsServer = new DNSServer(); + dnsServer->start(53, "*", apIP); + + if (!server) server = new ESP8266WebServer(80); + + server->on("/", HTTP_GET, [](){ + server->send_P(200, "text/html", index_html); + }); + + server->on("/save", HTTP_POST, [](){ + if (server->hasArg("ssid") && server->hasArg("webhookUrl") && server->hasArg("deviceName")) { + String ssidRecibido = server->arg("ssid"); + String passRecibido = server->hasArg("password") ? server->arg("password") : ""; + String urlRecibida = server->arg("webhookUrl"); + String deviceNameRecibido = server->arg("deviceName"); + + strlcpy(config.ssid, ssidRecibido.c_str(), sizeof(config.ssid)); + strlcpy(config.webhookUrl, urlRecibida.c_str(), sizeof(config.webhookUrl)); + strlcpy(config.password, passRecibido.c_str(), sizeof(config.password)); + strlcpy(config.deviceName, deviceNameRecibido.c_str(), sizeof(config.deviceName)); + + showStatus("SAVED", "REINICIANDO..."); + saveConfig(config); + + server->send_P(200, "text/html", saved_html); + + shouldRestart = true; + } else { + server->send(400, "text/plain", "Faltan datos"); + } + }); + + server->begin(); +} + +// ========================================== +// 💾 GESTIÓN DE CONFIGURACIÓN +// ==========================================