From fe0492a406d6e8baab92a5ec7413551a014e4b7d Mon Sep 17 00:00:00 2001 From: Marco Gallegos Date: Wed, 24 Dec 2025 16:54:11 -0600 Subject: [PATCH] first commit --- .gitignore | 31 + README.md | 231 ++++ compile_and_upload.sh | 251 ++++ compiler_info.md | 332 +++++ .../attendance_1-2025-12-18_214149.zip | Bin 0 -> 1079 bytes .../attendance_1-2025-12-23_151245.zip | Bin 0 -> 6133 bytes kicad/attendance_1/attendance_1.kicad_pcb | 2 + kicad/attendance_1/attendance_1.kicad_prl | 98 ++ kicad/attendance_1/attendance_1.kicad_pro | 418 +++++++ kicad/attendance_1/attendance_1.kicad_sch | 1086 +++++++++++++++++ kicad/attendance_1/sym-lib-table | 4 + secrets.h.example | 25 + soul23_time_attendance.ino | 541 ++++++++ 13 files changed, 3019 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 compile_and_upload.sh create mode 100644 compiler_info.md create mode 100644 kicad/attendance_1/attendance_1-backups/attendance_1-2025-12-18_214149.zip create mode 100644 kicad/attendance_1/attendance_1-backups/attendance_1-2025-12-23_151245.zip create mode 100644 kicad/attendance_1/attendance_1.kicad_pcb create mode 100644 kicad/attendance_1/attendance_1.kicad_prl create mode 100644 kicad/attendance_1/attendance_1.kicad_pro create mode 100644 kicad/attendance_1/attendance_1.kicad_sch create mode 100644 kicad/attendance_1/sym-lib-table create mode 100644 secrets.h.example create mode 100644 soul23_time_attendance.ino 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/README.md b/README.md new file mode 100644 index 0000000..032831d --- /dev/null +++ b/README.md @@ -0,0 +1,231 @@ +# 🕒 Checador Inteligente NFC IoT (ESP8266 · MVP) + +Este repositorio contiene el **Producto Mínimo Viable (MVP)** de un sistema de control de asistencia basado en **NFC** e **IoT**, diseñado para entornos reales de operación (oficinas, talleres, sucursales). + +El sistema utiliza un **NodeMCU ESP8266** para leer tarjetas NFC de empleados, obtener la hora exacta desde un servidor en la nube y enviar los registros de asistencia a un **Webhook HTTP** para su posterior procesamiento (bases de datos, bots, dashboards o sistemas de RRHH). + +--- + +## ⚡️ Guía Rápida (TL;DR) + +1. Instala dependencias (ESP8266 core + librerías listadas más abajo) en Arduino IDE. +2. Duplica `secrets.h.example` → `secrets.h` y rellena Wi‑Fi, URLs y zona horaria. +3. Carga el sketch `soul23_time_attendance.ino` en tu NodeMCU 1.0 (ESP‑12E). +4. Cablea RC522 y OLED siguiendo el **pinout seguro** descrito en este documento. +5. Graba tarjetas NFC con **JSON válido** usando NFC Tools. +6. Abre el monitor serie (115200) para verificar conexión, hora y peticiones HTTP. + +--- + +## 📁 Estructura del Repositorio + +``` +soul23_time_attendance/ +├── soul23_time_attendance.ino +├── README.md +├── secrets.h.example +├── secrets.h +└── test/ +``` + +> Solo es necesario editar `secrets.h` para credenciales y constantes sensibles. + +--- + +## 🚀 Funcionalidades + +* **Lectura NFC/RFID** (Mifare / NTAG 13.56 MHz) +* **Decodificación NDEF en JSON** +* **Sincronización de hora en la nube** (sin RTC físico) +* **Feedback visual en OLED** +* **Integración vía Webhook HTTP (POST JSON)** + +--- + +## 🧠 Arquitectura del Firmware + +| Módulo | Función | +| ---------------------- | -------------------------------------------------- | +| `setup()` | Inicializa Wi‑Fi, OLED, SPI, NFC y sincroniza hora | +| `loop()` | Muestra hora, refresca estado y espera tarjeta | +| `processTag()` | Lee UID + JSON NFC y dispara envío | +| `syncTimeWithServer()` | Obtiene `unixtime` desde servidor | +| `sendToWebhook()` | Envía payload JSON vía POST | + +--- + +## 🔬 Flujo de Operación + +1. **Arranque:** conexión Wi‑Fi + sincronización de hora. +2. **Espera:** muestra reloj y mensaje *ACERQUE TARJETA*. +3. **Lectura NFC:** UID + payload JSON. +4. **Validación:** deserialización segura. +5. **Envío:** POST al webhook. +6. **Feedback:** confirmación visual/sonora. + +--- + +## 🛠️ Hardware Requerido + +| Componente | Descripción | +| --------------- | ----------------- | +| NodeMCU ESP8266 | MCU con Wi‑Fi | +| RC522 | Lector NFC SPI | +| OLED 0.96" | SSD1306 I2C | +| LED | Indicador visual | +| Buzzer | Indicador audible | +| Tags NFC | NTAG213 / Mifare | + +--- + +## 🔌 Pinout Seguro (Producción) + +### RC522 (SPI) + +| RC522 | NodeMCU | GPIO | Color Cable | Nota | +| ----- | ------- | ------ | ----------- | ---------------------------------- | +| SDA | D8 | GPIO15 | Azul | Pull‑down interno (seguro como SS) | +| SCK | D5 | GPIO14 | Amarillo | SPI | +| MOSI | D7 | GPIO13 | Morado | SPI | +| MISO | D6 | GPIO12 | Blanco | SPI | +| IRQ | NC | — | Naranja | No conectado | +| GND | GND | — | Verde | Tierra común | +| RST | D0 | GPIO16 | Negro | Seguro para reset | +| 3.3V | 3.3V | — | Rojo | ⚠️ No usar 5V | + +### OLED (I2C) + +| OLED | NodeMCU | Color Cable | +| ---- | ------- | ----------- | +| SDA | D2 | Azul | +| SCL | D1 | Naranja | +| VCC | 3.3V | Rojo | +| GND | GND | Negro | + +### LED y Buzzer + +| Componente | NodeMCU | GPIO | Uso | +| ---------- | ------- | ----- | --------------- | +| LED | D3 | GPIO0 | Estado visual | +| Buzzer | D4 | GPIO2 | Feedback sonoro | + +--- + +## 🔧 Pull‑Up Resistors y Pines de Boot (Explicación Clave) + +El **ESP8266 tiene pines críticos durante el arranque**. Si alguno está en el nivel incorrecto, el microcontrolador entra en **modo programación** o **falla el boot**. + +### GPIO 0 (D3) – LED + +* **Requisito:** debe estar en **HIGH** durante el boot. +* **Riesgo:** un LED mal cableado puede forzar LOW. +* **Solución aplicada:** + + * El firmware inicializa primero: + + ```cpp + pinMode(LED_PIN, INPUT_PULLUP); + ``` + * Luego se configura como salida cuando el sistema ya arrancó. +* **Resultado:** se evita entrar en modo flash accidentalmente. + +### GPIO 2 (D4) – Buzzer + +* Tiene **pull‑up interno por defecto**. +* **Problema común:** un buzzer pasivo conectado durante la carga puede generar interferencia. +* **Buenas prácticas:** + + * Inicializar el buzzer **al final del `setup()`**. + * Desconectar GND del buzzer durante la carga del firmware. + * Opcional: resistencia serie de **220 Ω**. + +### GPIO 15 (D8) + +* Tiene **pull‑down interno**. +* Ideal para **SS (SDA) del RC522**. +* **Nunca usar** para señales que requieran HIGH en boot. + +### GPIO 16 (D0) + +* No tiene pull‑up/pull‑down. +* Seguro para RST del RC522. +* No soporta PWM. + +--- + +## 💳 Formato del Payload NFC (Entrada) + +El tag NFC debe contener un **registro NDEF de texto** con JSON válido. + +Ejemplo recomendado: + +```json +{ + "name": "Juan", + "num_emp": "XXXX250117", + "sucursal": "sucursal_1", + "telegram_id": "123456789" +} +``` + +Campos adicionales son ignorados de forma segura si no se usan. + +--- + +## 📡 Payload Enviado al Webhook (Salida) + +Ejemplo real del POST generado por el dispositivo: + +```json +{ + "card_uid": "04A1B2C3", + "name": "Juan", + "num_emp": "XXXX250117", + "sucursal": "sucursal_1", + "telegram_id": "123456789", + "timestamp": 1765913424, + "device": "Checador_Oficina_1" +} +``` + +--- + +## ⚙️ Configuración (`secrets.h`) + +```cpp +const char* ssid = "TU_WIFI"; +const char* password = "TU_PASSWORD"; + +const char* webhookUrl = "https://api.tudominio.com/webhook"; +const char* timeServerUrl = "https://soul23.cloud/time-server"; + +#define DEVICE_NAME "Checador_Oficina_1" +#define TIMEZONE_OFFSET -21600 +``` + +--- + +## 🐛 Troubleshooting Rápido + +* **No arranca:** revisar GPIO0 y GPIO2. +* **No lee NFC:** revisar 3.3 V y RST. +* **No carga firmware:** desconectar buzzer temporalmente. +* **JSON inválido:** validar comillas rectas y formato. + +--- + +## 📌 Estado + +**MVP funcional**, listo para pilotos, dashboards y automatización RH. + +--- + +## 👤 Autor + +**Marco** — Inventor · Automatización · IoT + +--- + +## 📄 Licencia + +MIT diff --git a/compile_and_upload.sh b/compile_and_upload.sh new file mode 100755 index 0000000..ce1c76c --- /dev/null +++ b/compile_and_upload.sh @@ -0,0 +1,251 @@ +#!/bin/bash +# Script para compilar, cargar y revisar el monitor serial del checador NFC + +# Colores para output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color +BOLD='\033[1m' + +# Configuración +# Obtener el directorio del script para hacerlo portable +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKETCH_DIR="$SCRIPT_DIR" +SKETCH_NAME="soul23_time_attendance" +FQBN="esp8266:esp8266:nodemcuv2" # NodeMCU 1.0 (ESP-12E Module) +PORT="/dev/ttyUSB0" +BAUD_RATE="115200" + +# Función para mostrar barra de progreso +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 "] ${percent}%%${NC}" +} + +# Función para verificar estado del dispositivo +check_device_status() { + local port=$1 + local status="" + local details="" + + if [ -e "$port" ]; then + # Verificar si el puerto está en la lista de arduino-cli + local board_list=$(arduino-cli board list 2>/dev/null) + if echo "$board_list" | grep -q "$port"; then + local board_info=$(echo "$board_list" | grep "$port") + local protocol=$(echo "$board_info" | awk '{print $2}') + local board_type=$(echo "$board_info" | awk '{print $3}') + + if echo "$board_info" | grep -q "Unknown"; then + status="${YELLOW}⚠️ Conectado${NC}" + details="(Placa no identificada - Protocolo: $protocol)" + else + local board_name=$(echo "$board_info" | awk '{for(i=4;i<=NF;i++) printf $i" "; print ""}') + status="${GREEN}✅ Conectado${NC}" + details="($board_name)" + fi + else + status="${YELLOW}⚠️ Puerto existe${NC}" + details="(No detectado por arduino-cli - puede requerir permisos)" + fi + else + status="${RED}❌ No conectado${NC}" + details="(Puerto $port no encontrado)" + fi + + echo -e "$status $details" +} + +# Función para detectar puerto automáticamente +detect_port() { + local detected_port=$(arduino-cli board list | grep -E "ttyUSB|ttyACM" | head -1 | awk '{print $1}') + if [ -n "$detected_port" ]; then + echo "$detected_port" + else + echo "" + fi +} + +# 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 0: Verificar secrets.h +echo -e "${BOLD}${CYAN}[0/4]${NC} Verificando configuración..." +if [ ! -f "$SKETCH_DIR/secrets.h" ]; then + echo -e "${RED}❌ Error: No se encuentra secrets.h${NC}" + echo -e "${YELLOW}💡 Crea secrets.h desde secrets.h.example y configura tus credenciales${NC}" + exit 1 +fi +echo -e "${GREEN}✅ secrets.h encontrado${NC}\n" + +# Paso 1: Verificar y detectar dispositivo +echo -e "${BOLD}${CYAN}[1/4]${NC} Verificando conexión del dispositivo..." +echo -e " Puerto configurado: ${BOLD}$PORT${NC}" + +# Verificar puerto configurado +echo -e " Estado: $(check_device_status "$PORT")" + +if [ ! -e "$PORT" ] || ! arduino-cli board list 2>/dev/null | grep -q "$PORT"; then + echo -e "\n${YELLOW}🔍 Buscando dispositivos disponibles...${NC}" + + # Intentar detectar automáticamente + auto_port=$(detect_port) + if [ -n "$auto_port" ]; then + echo -e "${GREEN} ✓ Dispositivo detectado en: $auto_port${NC}" + read -p " ¿Usar este puerto? (s/n): " -n 1 -r + echo + if [[ $REPLY =~ ^[SsYy]$ ]]; then + PORT="$auto_port" + echo -e " Estado: $(check_device_status "$PORT")" + else + echo -e "${RED}❌ Por favor, conecta el dispositivo y vuelve a intentar${NC}" + exit 1 + fi + else + echo -e "${RED} ✗ No se encontraron dispositivos${NC}" + echo -e "\n${YELLOW}Puertos disponibles:${NC}" + arduino-cli board list + echo -e "\n${YELLOW}Por favor, conecta el dispositivo o modifica PORT en el script${NC}" + exit 1 + fi +fi + +echo -e " Puerto seleccionado: ${BOLD}$PORT${NC}\n" + +# Paso 2: Compilar +echo -e "${BOLD}${CYAN}[2/4]${NC} Compilando el sketch..." +echo -e " FQBN: ${BOLD}$FQBN${NC}" +echo -e " Sketch: ${BOLD}$SKETCH_NAME.ino${NC}\n" + +# Compilar con barra de progreso animada +compile_output=$(mktemp) +( + arduino-cli compile --fqbn "$FQBN" "$SKETCH_DIR/$SKETCH_NAME.ino" > "$compile_output" 2>&1 + echo $? > "${compile_output}.exit" +) & +compile_pid=$! + +# Mostrar barra de progreso animada mientras compila +spinner="|/-\\" +spinner_idx=0 +while kill -0 $compile_pid 2>/dev/null; do + spinner_char=${spinner:$spinner_idx:1} + printf "\r ${CYAN}[${spinner_char}]${NC} Compilando... ${CYAN}%s${NC}" "$spinner_char" + spinner_idx=$(( (spinner_idx + 1) % 4 )) + sleep 0.2 +done + +wait $compile_pid +compile_result=$(cat "${compile_output}.exit" 2>/dev/null || echo "1") +rm -f "${compile_output}.exit" + +printf "\r ${GREEN}[✓]${NC} Compilación completada \n" + +# Mostrar errores si los hay +if [ $compile_result -ne 0 ]; then + echo -e "\n${RED}❌ Error en la compilación:${NC}" + cat "$compile_output" | grep -i "error\|warning" | head -10 + rm -f "$compile_output" + exit 1 +fi + +# Mostrar resumen de compilación +if [ -s "$compile_output" ]; then + size_info=$(grep -i "sketch uses\|global variables" "$compile_output" | head -2) + if [ -n "$size_info" ]; then + echo -e " ${BLUE}ℹ️${NC} $(echo "$size_info" | tr '\n' ' ')" + fi +fi +rm -f "$compile_output" + +echo -e "${GREEN}✅ Compilación exitosa${NC}\n" + +# Paso 3: Cargar al dispositivo +echo -e "${BOLD}${CYAN}[3/4]${NC} Cargando firmware al dispositivo..." +echo -e " Puerto: ${BOLD}$PORT${NC}" + +# Verificar nuevamente que el dispositivo está conectado +if [ ! -e "$PORT" ]; then + echo -e "${RED}❌ Error: El dispositivo se desconectó${NC}" + exit 1 +fi + +# Verificar estado antes de cargar +echo -e " Estado: $(check_device_status "$PORT")\n" + +# Cargar con indicador de progreso +upload_output=$(mktemp) +( + arduino-cli upload -p "$PORT" --fqbn "$FQBN" "$SKETCH_DIR/$SKETCH_NAME.ino" > "$upload_output" 2>&1 + echo $? > "${upload_output}.exit" +) & +upload_pid=$! + +# Mostrar spinner animado durante la carga +spinner="|/-\\" +spinner_idx=0 +stage=0 +stages=("Iniciando" "Escribiendo" "Verificando" "Completando") +while kill -0 $upload_pid 2>/dev/null; do + spinner_char=${spinner:$spinner_idx:1} + # Cambiar etapa cada 2 segundos aproximadamente + if [ $((spinner_idx % 10)) -eq 0 ] && [ $stage -lt ${#stages[@]} ]; then + stage=$((stage + 1)) + fi + if [ $stage -ge ${#stages[@]} ]; then + stage=$((stage % ${#stages[@]})) + fi + printf "\r ${CYAN}[${spinner_char}]${NC} ${stages[$stage]}... ${CYAN}%s${NC}" "$spinner_char" + spinner_idx=$(( (spinner_idx + 1) % 4 )) + sleep 0.2 +done + +wait $upload_pid +upload_result=$(cat "${upload_output}.exit" 2>/dev/null || echo "1") +rm -f "${upload_output}.exit" + +printf "\r ${GREEN}[✓]${NC} Carga completada \n" + +if [ $upload_result -ne 0 ]; then + echo -e "\n${RED}❌ Error al cargar el firmware:${NC}" + cat "$upload_output" | grep -i "error\|failed\|timeout" | head -5 + rm -f "$upload_output" + echo -e "\n${YELLOW}💡 Verifica que:${NC}" + echo -e " • El dispositivo esté conectado" + echo -e " • No haya otro programa usando el puerto" + echo -e " • Presiona el botón RESET del ESP8266 si es necesario" + echo -e " • Mantén presionado BOOT mientras cargas si es necesario" + exit 1 +fi + +rm -f "$upload_output" +echo -e "${GREEN}✅ Firmware cargado exitosamente${NC}\n" + +# Paso 4: Monitor serial +echo -e "${BOLD}${CYAN}[4/4]${NC} Abriendo monitor serial..." +echo -e " Puerto: ${BOLD}$PORT${NC}" +echo -e " Baud rate: ${BOLD}$BAUD_RATE${NC}" +echo -e "\n${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${YELLOW} Monitor Serial (Presiona ${BOLD}Ctrl+C${NC}${YELLOW} para salir)${NC}" +echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" + +# Pequeña pausa antes de abrir el monitor +sleep 1 + +# Abrir monitor serial +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 0000000000000000000000000000000000000000..6e57c1bf02d98f0a26ec8f62783fa0395302cdca GIT binary patch literal 1079 zcmWIWW@Zs#U|`^2SiWXb^t~%Q3qJyR8cYlfVn8~vq$D*jB{45KHQrD!J2N>kCBC32 zKR3`b@2~;Sp5LNv=Ugww{hqo=A)~Xbes(fxp#}3v8c^B=ACQgEUiTr zpF{5zPYQPLoXyY~Vri1en6~2Bz1K0)_or!HH!V5A)ECXwnLI<{zP+}y&V{SL{>l4P z`LjEUZb{#<;qv818v{Atw2U)_@9K3nmr0(Vy1-y<^^87e1@_XOTb%5zXKw!5!}0Q3 z*v0iP9$lRDNGp8v@&?_fU6z|&zuURLP?@#-frAn69Lf3J9twY}lapB=%-;5-Fl9D!7voDte{ny*%FOnwh z>@S`@-T2h0jA^^F?iIL*+a$H{wglZv%V*3cp>RDR_{aI__FVnuMXt|Z>InIK zU$uOZG>f^cIR9PEWhLgbIB!@KtUkGQ|7zJ!-zLYuc(tWxzHhs!n51}v%G`d>O6HC0 zyzF&3?ic>+p23(HbwMlMG2wE8ZiFT4e5Fqd*WT7g_1R9jNI83;ul#WPtROk*@`-ak zemujzoXjNMlEkE(RNY=ju0sX_uHP+> zP6*v}p;2#@c)@e04-BcZ_@r`X8Ff14y`NI{_5b6~Kh|wAIa4U3)oU!x@I6`6P`}`# z^h=h!g+~(d&mZ6Vy83l&?&?o-<3(mY3RjA#d46Puhtn~C9+QQuoP>71sj;fD`Lt&D zsmis#!VYB3-@$TomifIui~-(^ObQIR^9IndNC10IK{o?E=^zYHWyG4>fC&lR4D?7s onDHOG8Net*HUnFzBg`lQ=6sYm2=HcQ14*+0Av@3lDP|B40BWO&>i_@% literal 0 HcmV?d00001 diff --git a/kicad/attendance_1/attendance_1-backups/attendance_1-2025-12-23_151245.zip b/kicad/attendance_1/attendance_1-backups/attendance_1-2025-12-23_151245.zip new file mode 100644 index 0000000000000000000000000000000000000000..ae2d1fdaf9461fdcb94d3dd58082338603cb3ef5 GIT binary patch literal 6133 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 -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..d6f52ea --- /dev/null +++ b/soul23_time_attendance.ino @@ -0,0 +1,541 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // Requiere instalar: arduino-cli lib install "Time" + +#include "secrets.h" + +// 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 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 (Revisión frecuente para auto-reparación rápida) +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); +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(); + +// ========================================== +// 🚀 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 + pinMode(LED_PIN, OUTPUT); // Luego como OUTPUT + digitalWrite(LED_PIN, LOW); // Apagado inicial + + Serial.begin(115200); + delay(100); + Serial.println("\n\n--- Checador IOT (Public Version) ---"); + + randomSeed(analogRead(A0)); + + // 1. Inicializar los buses de comunicación primero + // ⚠️ NO inicializar el buzzer aquí para evitar problemas de boot + // El pin D4 (GPIO 2) tiene pull-up interno, pero el buzzer puede causar problemas si está conectado + SPI.begin(); + Wire.begin(OLED_SDA, OLED_SCL); + + // 2. Luego, inicializar los dispositivos periféricos + Serial.println("Escanenado dispositivos I2C..."); + scanI2C(); // Escanear dispositivos I2C conectados + mfrc522.PCD_Init(); + mfrc522.PCD_DumpVersionToSerial(); // Imprime datos del lector NFC para depuración + + + 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(); // Error crítico, S.O.S. + } + } + display.clearDisplay(); + display.setTextColor(WHITE); + // display.setContrast(255); // Comentado: Método no soportado + display.dim(false); // Configurar brillo máximo (equivalente a alto contraste) + Serial.println("OLED inicializado y configurado."); + + showStatus("CONECTANDO", "WIFI..."); + WiFi.begin(ssid, password); + + int timeout = 0; + while (WiFi.status() != WL_CONNECTED && timeout < 30) { + delay(500); Serial.print("."); timeout++; + } + + if (WiFi.status() == WL_CONNECTED) { + showStatus("WIFI", "OK"); + delay(1000); // Pausa para estabilizar la red + } else { + showStatus("ERROR", "WIFI"); + sos(); // Error crítico, S.O.S. + } + + syncTimeWithServer(); + Serial.println("Sistema Listo."); + + // ⚠️ CRÍTICO: Configurar buzzer como OUTPUT SOLO AL FINAL, después de que TODO esté listo + // El pin ya está configurado como INPUT al inicio para evitar ruido durante boot + // Ahora lo cambiamos a OUTPUT y lo ponemos en LOW para silenciarlo + delay(100); // Pausa adicional para asegurar que todo esté estable + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); // Buzzer apagado inicial - CRÍTICO para evitar ruido + delay(50); // Pausa más larga para estabilizar + // Asegurar múltiples veces que el buzzer esté silenciado + digitalWrite(BUZZER_PIN, LOW); + delay(10); + digitalWrite(BUZZER_PIN, LOW); + + blinkLed(5, 200); // Boot OK - Parpadeo de 5 veces + digitalWrite(LED_PIN, LOW); // LED apagado en estado de reposo + silenceBuzzer(); // Asegurar que el buzzer esté silenciado +} + +// ========================================== +// 🔄 LOOP PRINCIPAL +// ========================================== +void loop() { + Serial.println("Loop ejecutándose..."); // Debug: confirmar que loop se ejecuta + checkAndResetNFC(); // Comprobar estado del lector NFC + + if (timeStatus() == timeNotSet || (millis() - lastSyncTime > syncInterval)) { + syncTimeWithServer(); + } + + if (minute() != lastMinuteDisplayed) { + showClockScreen(); + lastMinuteDisplayed = minute(); + } + + if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) { + processTag(); + lastMinuteDisplayed = -1; + } + + // Asegurar que el buzzer esté silenciado en cada iteración + 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 +// ========================================== +void checkAndResetNFC() { + // Solo ejecutar si ha pasado el intervalo de tiempo definido + if (millis() - lastNfcCheck > nfcCheckInterval) { + Serial.println("NFC Watchdog: Verificando lector..."); + + // Verificación más segura leyendo la versión del firmware + byte v = mfrc522.PCD_ReadRegister(MFRC522::VersionReg); + + // 0x00 o 0xFF indican error de comunicación + if (v == 0x00 || v == 0xFF) { + Serial.println(F("NFC Watchdog: Lector no responde. Intentando Soft Reset...")); + mfrc522.PCD_Init(); + delay(50); + + // Verificar si revivió + v = mfrc522.PCD_ReadRegister(MFRC522::VersionReg); + if (v == 0x00 || v == 0xFF) { + Serial.println(F("NFC Watchdog: Soft Reset falló. Intentando HARD RESET...")); + + // Hard Reset usando el pin RST + pinMode(RST_PIN, OUTPUT); + digitalWrite(RST_PIN, LOW); + delay(100); // Mantener en bajo un momento + digitalWrite(RST_PIN, HIGH); + delay(50); // Esperar a que arranque + + mfrc522.PCD_Init(); // Re-inicializar registros + + v = mfrc522.PCD_ReadRegister(MFRC522::VersionReg); + if (v == 0x00 || v == 0xFF) { + Serial.println(F("NFC Watchdog: ¡FALLO CRÍTICO! Hard Reset tampoco funcionó.")); + } else { + Serial.println(F("NFC Watchdog: Hard Reset exitoso.")); + } + } else { + Serial.println(F("NFC Watchdog: Soft Reset exitoso.")); + } + } else { + Serial.print(F("NFC Watchdog: Lector OK (v=0x")); + Serial.print(v, HEX); + Serial.println(F("). Refrescando configuración...")); + // Re-inicializar de todos modos para asegurar configuración correcta (Gain, etc.) + mfrc522.PCD_Init(); + } + + // Actualizar el tiempo de la última comprobación + lastNfcCheck = millis(); + } +} + +// ========================================== +// ☁️ RED Y LECTURA DE TARJETA +// ========================================== +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; +} + +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(DEVICE_NAME); + 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(); + } +} + +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..."); + + if (http.begin(*client, webhookUrl)) { + http.setTimeout(10000); // Timeout de 10 segundos para evitar resets por watchdog + http.setUserAgent(DEVICE_NAME); + 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); + + int httpResponseCode = http.POST(jsonPayload); + 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"); +}