first commit

This commit is contained in:
Marco Gallegos
2025-12-24 16:57:32 -06:00
commit 06719b56a1
14 changed files with 2041 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@@ -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_*

59
Config.cpp Normal file
View File

@@ -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;
}

18
Config.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <Arduino.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
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

141
README.md Normal file
View File

@@ -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.

130
compile_and_upload.sh Executable file
View File

@@ -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"

332
compiler_info.md Normal file
View File

@@ -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).

View File

@@ -0,0 +1,2 @@
(kicad_pcb (version 20241229) (generator "pcbnew") (generator_version "9.0")
)

View File

@@ -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
}
}
}

View File

@@ -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": {}
}

View File

@@ -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)
)

View File

@@ -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 ""))
)

25
secrets.h.example Normal file
View File

@@ -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

774
soul23_time_attendance.ino Normal file
View File

@@ -0,0 +1,774 @@
#include <SPI.h>
#include <MFRC522.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <TimeLib.h> // Requiere instalar: arduino-cli lib install "Time"
#include <ESP8266WebServer.h>
#include <DNSServer.h>
#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 ? "" : "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<WiFiClientSecure> 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<unsigned long>() + 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<WiFiClientSecure> 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<JsonArray>();
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(
<!DOCTYPE html>
<html>
<head>
<title>Configuracion Checador</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { background-color: #1a1a1a; color: #f0f0f0; font-family: Arial, sans-serif; text-align: center; padding: 20px; }
h1 { color: #00bcd4; }
.container { max-width: 400px; margin: auto; background: #262626; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.5); }
input[type=text], input[type=password] { width: 90%; padding: 12px; margin: 8px 0; display: inline-block; border: 1px solid #444; border-radius: 4px; box-sizing: border-box; background: #333; color: #f0f0f0; }
button { background-color: #008CBA; color: white; padding: 14px 20px; margin: 8px 0; border: none; border-radius: 4px; cursor: pointer; width: 95%; font-size: 16px; }
button:hover { background-color: #005f7a; }
label { text-align: left; display: block; margin-left: 5%; }
</style>
</head>
<body>
<div class="container">
<h1>Configurar Checador</h1>
<form action="/save" method="POST">
<label for="ssid">SSID (WiFi)</label>
<input type="text" id="ssid" name="ssid" required>
<label for="password">Contraseña (WiFi)</label>
<input type="password" id="password" name="password">
<label for="webhookUrl">URL del Webhook</label>
<input type="text" id="webhookUrl" name="webhookUrl" required>
<label for="deviceName">Nombre del Dispositivo</label>
<input type="text" id="deviceName" name="deviceName" placeholder="Ej: Oficina_Principal" required>
<button type="submit">Guardar y Reiniciar</button>
</form>
</div>
</body>
</html>
)rawliteral";
const char saved_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Guardado</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { background-color: #1a1a1a; color: #f0f0f0; font-family: Arial, sans-serif; text-align: center; padding-top: 50px; }
.message { font-size: 24px; color: #00bcd4; }
</style>
</head>
<body>
<div class="message">
<h1>Configuracion Guardada!</h1>
<p>El dispositivo se reiniciara...</p>
</div>
</body>
</html>
)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
// ==========================================