Objetivo y caso de uso
Lo que construirás: Construirás un contador direccional de personas para puertas utilizando sensores infrarrojos (IR) de barrera dobles para rastrear a los individuos que entran y salen de una habitación, transmitiendo el recuento de ocupación en tiempo real a través de Bluetooth de baja energía (BLE).
Por qué es importante / Casos de uso
- Climatización y optimización energética: Ajusta dinámicamente el control de clima basándose en la ocupación real de la habitación, reduciendo el desperdicio de energía hasta en un 20%.
- Mantenimiento basado en el uso: Activa alertas de limpieza automáticamente después de un umbral específico (por ejemplo, 50 entradas) en lugar de depender de horarios fijos ineficientes.
- Análisis de espacios y comercio minorista: Rastrea las horas pico de visitantes y correlaciona el tráfico peatonal con los datos de ventas, o monitorea la utilización de las salas de conferencias para optimizar la distribución del edificio.
Resultado esperado
- Un dispositivo edge implementado que rastrea con precisión el movimiento direccional con una latencia de sondeo de sensores de <50ms.
- Anuncio BLE en tiempo real de la ocupación actual de la habitación con una tasa de actualización de 1Hz y un consumo de energía mínimo.
- Lógica robusta de eliminación de rebotes (debouncing) que ignora cruces parciales o falsos disparos para mantener un recuento preciso.
Audiencia: Desarrolladores de IoT, Gerentes de instalaciones, Makers; Nivel: Intermedio
Arquitectura/flujo: Sensores IR de barrera dobles → Interrupciones GPIO del microcontrolador → Lógica de conteo direccional → Anuncio BLE → Puerta de enlace/Panel de control
Nota educativa de validación
Antes de publicar este caso, el contenido pasó la puerta automática de validación de Prometeo con estado PASS. Para este perfil ESP32 DevKitC, el proyecto se comprobó como proyecto PlatformIO: el validador extrajo platformio.ini y src/main.cpp, creó un proyecto temporal y ejecutó pio run contra platform = espressif32, board = esp32dev y framework = arduino. También revisó la estructura del artículo, que los comandos sean copiables con guiones ASCII, y que no aparezcan stacks no soportados como ESP-IDF directo o placas ESP32 no acotadas.
Evidencia de validación publicada
- Resultado automático: PASS.
- Estructura parseada: 3 apartados, 3 tablas y 2 bloques de código detectados antes de publicar.
- Código comprobado: 1 PlatformIO config + 1 ESP32 source/pio run.
- Catálogo soportado: el texto se contrastó contra los perfiles de dispositivo validables de Prometeo y los stacks no soportados bloquean la publicación.
- Hallazgos del informe: sin hallazgos bloqueantes.
Esta validación confirma compatibilidad sintáctica y de herramientas para el código publicado, pero no sustituye la prueba física sobre tu placa ESP32 DevKitC exacta, tu cableado, alimentación y entorno WiFi local.
Nota educativa de seguridad
Este prototipo está diseñado estrictamente para fines educativos y de monitoreo básico. No debe usarse para el control crítico de capacidad, cumplimiento del código de incendios, seguimiento de evacuación de emergencia o control de acceso de seguridad. Los sensores IR utilizados en los kits estándar para aficionados son seguros para los ojos, pero se debe tener cuidado para asegurar que los cables que atraviesan una puerta no creen un peligro de tropiezo. Si se implementa en un entorno real, asegura todo el cableado de forma segura y usa cajas adecuadas.
Diagrama de bloques conceptual
Vista de alto nivel: qué entra, qué procesa cada bloque y qué sale del sistema.
Arquitectura funcional
Flujo conceptual: configuración local, emisión BLE y lectura desde el móvil.
Ruta de validación
Resumen conceptual de las herramientas usadas para comprobar el proyecto ESP32 publicado.
Requisitos previos
- Software: Visual Studio Code (VSCode) con la extensión PlatformIO IDE instalada.
- Controladores: Controladores USB a UART CP210x o CH34x instalados en tu computadora host (dependiendo del puente USB de tu ESP32 DevKitC específico).
- Aplicación móvil: Una aplicación de escáner BLE en tu teléfono inteligente (por ejemplo, LightBlue o BLE Scanner) para leer las transmisiones BLE.
- Conocimientos: Familiaridad básica con el uso de protoboards y programación en C++.
Materiales
- Microcontrolador: ESP32 DevKitC + sensores IR de barrera dobles + LED de estado
- Nota: Los sensores IR de barrera generalmente vienen en pares (un emisor, un receptor). Necesitas dos pares completos para la detección direccional.
- Fuente de alimentación: Cable Micro-USB o USB-C estándar (dependiendo de tu variante de DevKitC) conectado a tu computadora para alimentación y programación.
- Cableado: Protoboard y cables puente variados de macho a macho y de macho a hembra.
- Componentes pasivos: Una resistencia de 220Ω o 330Ω para el LED de estado externo (si no se usa el LED integrado).
Configuración/Conexión
La configuración de hardware requiere alinear los dos pares de sensores IR de barrera a través de una puerta o un arco de cartón simulado. Los emisores simplemente se alimentan, mientras que los receptores actúan como interruptores digitales conectados al ESP32. Cuando el haz no se interrumpe, el receptor emite una señal HIGH. Cuando un objeto interrumpe el haz, el receptor emite una señal LOW.
Monta los sensores horizontalmente. El Sensor A debe colocarse en el «exterior» del umbral, y el Sensor B debe colocarse en el «interior», separados entre sí unas 3 a 5 pulgadas, lo suficientemente cerca como para que una persona que pase interrumpa ambos haces secuencialmente, pero lo suficientemente lejos para detectar una secuencia direccional clara.
| Componente | Pin / Terminal | Pin del ESP32 DevKitC | Notas |
|---|---|---|---|
| Emisor IR A (Exterior) | VCC / 5V | 5V (VIN) | Los emisores solo necesitan alimentación y tierra. |
| Emisor IR A (Exterior) | GND | GND | Conectar al riel de tierra común. |
| Receptor IR A (Exterior) | VCC / 5V | 3.3V o 5V | Verifica las especificaciones del sensor; la mayoría de la lógica es segura para 3.3V. |
| Receptor IR A (Exterior) | GND | GND | Conectar al riel de tierra común. |
| Receptor IR A (Exterior) | OUT / Señal | GPIO 32 | Entrada digital (HIGH = sin interrumpir, LOW = interrumpido). |
| Emisor IR B (Interior) | VCC / 5V | 5V (VIN) | Alimentación para el segundo haz. |
| Emisor IR B (Interior) | GND | GND | Conectar al riel de tierra común. |
| Receptor IR B (Interior) | VCC / 5V | 3.3V o 5V | Verifica las especificaciones del sensor. |
| Receptor IR B (Interior) | GND | GND | Conectar al riel de tierra común. |
| Receptor IR B (Interior) | OUT / Señal | GPIO 33 | Entrada digital (HIGH = sin interrumpir, LOW = interrumpido). |
| LED de estado | Ánodo (Pata larga) | GPIO 2 | A través de una resistencia de 220Ω. (A menudo se asigna al LED integrado). |
| LED de estado | Cátodo (Pata corta) | GND | Conectar al riel de tierra común. |
Nota: Si tus receptores IR emiten un nivel lógico alto de 5V, debes usar un convertidor de nivel lógico o un divisor de voltaje antes de conectarlos a los pines GPIO de 3.3V del ESP32 para evitar daños. Muchos receptores IR de barrera estándar son de colector abierto o funcionan bien con alimentación de 3.3V.
Código validado
Los siguientes archivos constituyen el proyecto completo de PlatformIO. La lógica utiliza una máquina de estados por sondeo para rastrear la secuencia de interrupciones de los sensores, filtrando el ruido y asegurando que un conteo solo se registre cuando una persona pase completamente a través de ambos haces en un orden específico.
platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
src/main.cpp
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
// ---------------------------------------------------------
// Pin Definitions
// ---------------------------------------------------------
#define SENSOR_A_PIN 32 // Outside sensor
#define SENSOR_B_PIN 33 // Inside sensor
#define LED_PIN 2 // Status LED
// ---------------------------------------------------------
// BLE Configuration
// ---------------------------------------------------------
// Generate custom UUIDs using a tool like uuidgenerator.net
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
BLEServer* pServer = nullptr;
BLECharacteristic* pCharacteristic = nullptr;
bool deviceConnected = false;
bool oldDeviceConnected = false;
// ---------------------------------------------------------
// Application State
// ---------------------------------------------------------
int peopleCount = 0;
enum DirectionState {
STATE_IDLE,
STATE_A_BROKEN_FIRST,
STATE_B_BROKEN_FIRST,
STATE_WAIT_CLEAR_ENTER,
STATE_WAIT_CLEAR_EXIT
};
DirectionState currentState = STATE_IDLE;
// ---------------------------------------------------------
// BLE Server Callbacks
// ---------------------------------------------------------
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("BLE Client Connected");
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("BLE Client Disconnected");
}
};
// ---------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------
void updateBLECount() {
if (deviceConnected && pCharacteristic != nullptr) {
// Format the count as a readable string for easy validation
char countStr[32];
snprintf(countStr, sizeof(countStr), "Occupancy: %d", peopleCount);
pCharacteristic->setValue((uint8_t*)countStr, strlen(countStr));
pCharacteristic->notify();
Serial.print("BLE Updated: ");
Serial.println(countStr);
}
}
void blinkLED() {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
}
// ---------------------------------------------------------
// Setup
// ---------------------------------------------------------
void setup() {
Serial.begin(115200);
Serial.println("Starting BLE Doorway People Counter...");
// Initialize Pins
pinMode(SENSOR_A_PIN, INPUT_PULLUP);
pinMode(SENSOR_B_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// Initialize BLE
BLEDevice::init("ESP32_Counter");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
// Create Characteristic with Read and Notify properties
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
// Add CCCD descriptor for notifications
pCharacteristic->addDescriptor(new BLE2902());
// Set initial value
char initStr[32];
snprintf(initStr, sizeof(initStr), "Occupancy: %d", peopleCount);
pCharacteristic->setValue((uint8_t*)initStr, strlen(initStr));
pService->start();
// Start advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(false);
pAdvertising->setMinPreferred(0x0);
BLEDevice::startAdvertising();
Serial.println("BLE Advertising started. Waiting for connections...");
}
// ...#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
// ---------------------------------------------------------
// Pin Definitions
// ---------------------------------------------------------
#define SENSOR_A_PIN 32 // Outside sensor
#define SENSOR_B_PIN 33 // Inside sensor
#define LED_PIN 2 // Status LED
// ---------------------------------------------------------
// BLE Configuration
// ---------------------------------------------------------
// Generate custom UUIDs using a tool like uuidgenerator.net
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
BLEServer* pServer = nullptr;
BLECharacteristic* pCharacteristic = nullptr;
bool deviceConnected = false;
bool oldDeviceConnected = false;
// ---------------------------------------------------------
// Application State
// ---------------------------------------------------------
int peopleCount = 0;
enum DirectionState {
STATE_IDLE,
STATE_A_BROKEN_FIRST,
STATE_B_BROKEN_FIRST,
STATE_WAIT_CLEAR_ENTER,
STATE_WAIT_CLEAR_EXIT
};
DirectionState currentState = STATE_IDLE;
// ---------------------------------------------------------
// BLE Server Callbacks
// ---------------------------------------------------------
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("BLE Client Connected");
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("BLE Client Disconnected");
}
};
// ---------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------
void updateBLECount() {
if (deviceConnected && pCharacteristic != nullptr) {
// Format the count as a readable string for easy validation
char countStr[32];
snprintf(countStr, sizeof(countStr), "Occupancy: %d", peopleCount);
pCharacteristic->setValue((uint8_t*)countStr, strlen(countStr));
pCharacteristic->notify();
Serial.print("BLE Updated: ");
Serial.println(countStr);
}
}
void blinkLED() {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
}
// ---------------------------------------------------------
// Setup
// ---------------------------------------------------------
void setup() {
Serial.begin(115200);
Serial.println("Starting BLE Doorway People Counter...");
// Initialize Pins
pinMode(SENSOR_A_PIN, INPUT_PULLUP);
pinMode(SENSOR_B_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// Initialize BLE
BLEDevice::init("ESP32_Counter");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
// Create Characteristic with Read and Notify properties
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
// Add CCCD descriptor for notifications
pCharacteristic->addDescriptor(new BLE2902());
// Set initial value
char initStr[32];
snprintf(initStr, sizeof(initStr), "Occupancy: %d", peopleCount);
pCharacteristic->setValue((uint8_t*)initStr, strlen(initStr));
pService->start();
// Start advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(false);
pAdvertising->setMinPreferred(0x0);
BLEDevice::startAdvertising();
Serial.println("BLE Advertising started. Waiting for connections...");
}
// ---------------------------------------------------------
// Main Loop
// ---------------------------------------------------------
void loop() {
// Handle BLE disconnection/reconnection gracefully
if (!deviceConnected && oldDeviceConnected) {
delay(500); // Give the bluetooth stack the chance to get things ready
pServer->startAdvertising();
Serial.println("Restarted BLE advertising");
oldDeviceConnected = deviceConnected;
}
if (deviceConnected && !oldDeviceConnected) {
oldDeviceConnected = deviceConnected;
}
// Read sensor states (LOW means the beam is broken)
bool aBroken = (digitalRead(SENSOR_A_PIN) == LOW);
bool bBroken = (digitalRead(SENSOR_B_PIN) == LOW);
// State Machine for Directional Counting
switch (currentState) {
case STATE_IDLE:
if (aBroken && !bBroken) {
currentState = STATE_A_BROKEN_FIRST;
Serial.println("State: A Broken (Potential Entry)");
} else if (bBroken && !aBroken) {
currentState = STATE_B_BROKEN_FIRST;
Serial.println("State: B Broken (Potential Exit)");
}
break;
case STATE_A_BROKEN_FIRST:
// If B breaks while A is or was broken, person is moving forward
if (bBroken) {
currentState = STATE_WAIT_CLEAR_ENTER;
Serial.println("State: B Broken (Completing Entry)");
} else if (!aBroken && !bBroken) {
// False alarm, person backed out before breaking B
currentState = STATE_IDLE;
Serial.println("State: Idle (False Alarm A)");
}
break;
case STATE_B_BROKEN_FIRST:
// If A breaks while B is or was broken, person is moving outward
if (aBroken) {
currentState = STATE_WAIT_CLEAR_EXIT;
Serial.println("State: A Broken (Completing Exit)");
} else if (!aBroken && !bBroken) {
// False alarm, person backed out before breaking A
currentState = STATE_IDLE;
Serial.println("State: Idle (False Alarm B)");
}
break;
case STATE_WAIT_CLEAR_ENTER:
// Wait for both sensors to clear before counting to avoid multiple counts
if (!aBroken && !bBroken) {
peopleCount++;
Serial.print("Person Entered! Total: ");
Serial.println(peopleCount);
updateBLECount();
blinkLED();
currentState = STATE_IDLE;
}
break;
case STATE_WAIT_CLEAR_EXIT:
// Wait for both sensors to clear before counting
if (!aBroken && !bBroken) {
if (peopleCount > 0) {
peopleCount--;
}
Serial.print("Person Exited! Total: ");
Serial.println(peopleCount);
updateBLECount();
blinkLED();
currentState = STATE_IDLE;
}
break;
}
// Small delay for debouncing and CPU yield
delay(20);
}
Comandos de compilación/flasheo/ejecución
Usa la CLI de PlatformIO para compilar, cargar y monitorear el proyecto.
| Acción | Comando |
|---|---|
| Inicializar/Compilar | pio run |
| Cargar al ESP32 | pio run --target upload |
| Abrir monitor serie | pio device monitor |
Flujo de trabajo:
1. Abre tu terminal en VSCode en la raíz de tu proyecto de PlatformIO.
2. Ejecuta pio run para descargar el framework de ESP32 y compilar el código en C++. Asegúrate de que termine con un mensaje de SUCCESS.
3. Conecta el ESP32 DevKitC vía USB. Ejecuta pio run --target upload para flashear el firmware.
4. Ejecuta pio device monitor para observar los registros de arranque y las transiciones de estado de los sensores.
Validación paso a paso
Usa estos puntos de control para verificar la funcionalidad de tu contador de personas para puertas.
- Arranque e inicialización de BLE
- Acción: Abre el monitor serie y presiona el botón
EN(Reinicio) en el ESP32. - Observación esperada: El monitor imprime «Starting BLE Doorway People Counter…» seguido de «BLE Advertising started. Waiting for connections…».
- Condición de aprobación: No ocurren bucles de arranque ni volcados de memoria por fallos.
- Acción: Abre el monitor serie y presiona el botón
- Descubrimiento y conexión BLE
- Acción: Abre una aplicación de escáner BLE (como LightBlue) en tu teléfono inteligente. Escanea en busca de dispositivos y busca «ESP32_Counter». Toca «Connect».
- Observación esperada: El monitor serie imprime «BLE Client Connected». La aplicación muestra el Servicio personalizado (
4fafc201...) y la Característica (beb5483e...). - Condición de aprobación: Puedes suscribirte con éxito a las notificaciones para la característica en la aplicación.
- Detección de entrada (Ingreso)
- Acción: Bloquea el Sensor A (Exterior) con tu mano, luego bloquea el Sensor B (Interior), luego retira tu mano de ambos.
- Observación esperada: El monitor serie registra transiciones de estado que terminan con «Person Entered! Total: 1». El LED de estado parpadea una vez.
- Condición de aprobación: La aplicación de escáner BLE se actualiza automáticamente para mostrar «Occupancy: 1».
- Detección de salida (Egreso)
- Acción: Bloquea el Sensor B (Interior), luego bloquea el Sensor A (Exterior), luego retira tu mano de ambos.
- Observación esperada: El monitor serie registra transiciones de estado que terminan con «Person Exited! Total: 0». El LED de estado parpadea una vez.
- Condición de aprobación: La aplicación de escáner BLE se actualiza automáticamente para mostrar «Occupancy: 0».
- Manejo de falsas alarmas
- Acción: Bloquea solo el Sensor A, luego retira tu mano sin bloquear el Sensor B.
- Observación esperada: El monitor serie imprime «State: A Broken (Potential Entry)» seguido de «State: Idle (False Alarm A)». El conteo no cambia.
- Condición de aprobación: El conteo de ocupación se mantiene estable y no se activa ninguna notificación BLE.
Solución de problemas
| Síntoma | Causa probable | Solución |
|---|---|---|
| El código falla al cargarse en el ESP32 | Falta el controlador CP210x/CH34x, o el ESP32 no está en modo de arranque. | Instala los controladores. Mantén presionado el botón BOOT en el DevKitC cuando aparezca «Connecting…» durante la carga. |
| El conteo se incrementa múltiples veces por pasada | Rebote del sensor o haces colocados demasiado cerca. | Aumenta la distancia física entre el Sensor A y B. Asegúrate de que la eliminación de rebotes delay(20) en el código sea suficiente. |
| Los sensores nunca se activan (siempre en IDLE) | Los receptores están mal cableados o los haces están desalineados. | Asegúrate de que los emisores estén perfectamente alineados con los receptores. Verifica que los receptores estén conectados a los GPIO 32 y 33. |
| El conteo se vuelve negativo | El conteo inicial era 0 y alguien salió, o la lógica direccional está invertida. | El código evita conteos negativos (if (peopleCount > 0)). Si la entrada se registra como salida, intercambia los cables en los GPIO 32 y 33. |
| Dispositivo BLE no encontrado en el teléfono | El ESP32 no se está anunciando o la caché del Bluetooth del teléfono está desactualizada. | Reinicia el ESP32. Apaga y enciende el Bluetooth de tu teléfono para borrar la caché de escaneo. |
Mejoras
Una vez que hayas dominado el prototipo básico, considera estas mejoras para hacer el dispositivo más robusto para una implementación en el mundo real:
Lógica avanzada y fiabilidad
* Manejo de tiempo de espera: Implementa un temporizador en la máquina de estados. Si una persona interrumpe el Sensor A pero se queda allí durante 10 segundos sin interrumpir el Sensor B, el estado debería restablecerse a IDLE para evitar que el sistema se cuelgue.
* Ajuste de eliminación de rebotes: Reemplaza el simple delay(20) con una eliminación de rebotes no bloqueante basada en millis() para asegurar que la pila BLE nunca se quede sin tiempo de CPU durante eventos rápidos de los sensores.
Integración de datos y escalabilidad
* Integración de puerta de enlace BLE: En lugar de un teléfono inteligente, usa una PC u otro ESP32 como dispositivo Central BLE para agregar datos de múltiples puertas y enviarlos a un broker MQTT.
* Registro de series temporales: Agrega un módulo de reloj de tiempo real (RTC) y un lector de tarjetas SD para registrar las marcas de tiempo de cada entrada y salida para su posterior análisis de datos.
Optimización de energía
* Modos de suspensión: Si funciona con batería, configura el ESP32 para entrar en suspensión ligera (light sleep), usando los pines GPIO conectados a los receptores IR como fuentes de reactivación. Ten en cuenta que el anuncio BLE consume mucha energía, por lo que es posible que desees agrupar los datos y transmitirlos solo periódicamente.
Encuentra este producto y/o libros sobre este tema en Amazon
Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.




