from machine import Pin, PWM
import time
import network
from umqtt.simple import MQTTClient
# Configuración de Wi-Fi
WIFI_SSID = "Wokwi-GUEST" # Nombre de la red Wi-Fi para Wokwi
WIFI_PASSWORD = "" # Sin contraseña para Wokwi-GUEST
# Configuración de MQTT (Adafruit IO)
MQTT_BROKER = "io.adafruit.com"
MQTT_PORT = 1883 # Puerto estándar de MQTT (para SSL sería 8833, pero es más complejo)
MQTT_CLIENT_ID = b"Nahuel_lorelli" # ID único de cliente MQTT. Usando username como ID.
MQTT_USERNAME = "Nahuel_lorelli" # USERNAME DE ADAFRUIT IO!
MQTT_PASSWORD = b"aio_jOMD29XbjfjxfoRdk6Lfadb0MFXq" # AIO KEY DE ADAFRUIT IO
# Tópicos MQTT para la cerradura (basados en Feeds de Adafruit IO)
# El formato es "USERNAME/feeds/NOMBRE_DEL_FEED" USERNAME = "Nahuel_lorelli"
TOPIC_STATUS = b"Nahuel_lorelli/feeds/cerradura-estado" # Feed para el estado de la cerradura (BLOQUEADA/DESBLOQUEADA)
TOPIC_DOOR_SENSOR = b"Nahuel_lorelli/feeds/puerta-estado" # Feed para el estado del sensor de puerta (CERRADA/ABIERTA)
TOPIC_COMMAND = b"Nahuel_lorelli/feeds/cerradura-comando" # Feed para recibir comandos (LOCK/UNLOCK)
TOPIC_EVENTS = b"Nahuel_lorelli/feeds/cerradura-eventos" # Nuevo tópico para eventos y alertas
TOPIC_DOOR_COMMAND = b"Nahuel_lorelli/feeds/puerta-comando" # Nuevo feed para comandos de puerta (ABIERTA/CERRADA)
# Pines ESP32
# Keypad
ROWS_PINS = [Pin(21, Pin.IN, Pin.PULL_UP), Pin(19, Pin.IN, Pin.PULL_UP), Pin(18, Pin.IN, Pin.PULL_UP), Pin(5, Pin.IN, Pin.PULL_UP)]
COLS_PINS = [Pin(17, Pin.OUT), Pin(16, Pin.OUT), Pin(4, Pin.OUT), Pin(2, Pin.OUT)]
# Servo
SERVO_PIN = Pin(13)
servo = PWM(SERVO_PIN, freq=50)
# Slide Switch (Sensor de Contacto de Puerta)
DOOR_SENSOR_PIN = Pin(23, Pin.IN, Pin.PULL_UP)
# RGB LED (Configurado para Cátodo Común: Pin común a GND)
LED_R_PIN = Pin(25, Pin.OUT)
LED_G_PIN = Pin(26, Pin.OUT)
LED_B_PIN = Pin(27, Pin.OUT)
# Teclas del Keypad
KEYPAD_KEYS = [
['1', '2', '3', 'A'],
['4', '5', '6', 'B'],
['7', '8', '9', 'C'],
['*', '0', '#', 'D']
]
# Variables de la Cerradura
CORRECT_PASSWORD = "1234" # Contraseña inicial
current_password_input = ""
MAX_PASSWORD_LENGTH = 4
is_door_locked = True # Estado inicial de la cerradura: BLOQUEADA
# Variables para el Cierre Automático Temporizado de la CERRADURA (MQTT)
unlock_by_mqtt_timestamp = 0 # Rastrea el tiempo en que se desbloqueó por MQTT. 0 significa no desbloqueado por MQTT.
MQTT_UNLOCK_DURATION_MS = 15000 # Duración del desbloqueo antes del cierre automático por MQTT (15 segundos)
# Variables para el Control de Estado de Puerta por MQTT
is_door_simulated_open = False # True si la puerta está "simuladamente" abierta por MQTT, False si "simuladamente" cerrada
is_door_state_mqtt_controlled = False # Indica si el estado de la puerta está siendo controlado por MQTT (True) o por el sensor físico (False)
# NUEVAS VARIABLES PARA EL TEMPORIZADOR DE CIERRE DE PUERTA SIMULADA
door_simulated_open_timestamp = 0 # Tiempo en que la puerta fue "abierta" por MQTT (0 si no está simulada abierta)
DOOR_SIMULATED_CLOSE_DURATION_MS = 14000 # 14 segundos para cerrar la puerta simulada
# Variables para la Alerta de Puerta Abierta y Titileo del LED
last_door_alert_time = 0
DOOR_ALERT_INTERVAL_MS = 5000 # Enviar alerta de puerta abierta cada 5 segundos
last_blink_time = 0
BLINK_INTERVAL_MS = 250 # Intervalo de parpadeo del LED (250 ms encendido, 250 ms apagado)
is_blink_on = False # Estado actual del LED en el parpadeo
# Variables para el temporizador de entrada de contraseña
password_input_start_time = 0 # Tiempo en que se inició la entrada de contraseña (0 si no está activa)
PASSWORD_INPUT_TIMEOUT_MS = 30000 # 30 segundos para ingresar la contraseña
# VARIABLES PARA EL CAMBIO DE CONTRASEÑA Y ESTADOS
# Modos del sistema
NORMAL_MODE = 0
CHANGE_PASSWORD_INIT_MODE = 1 # Esperando la nueva contraseña después de A04
system_mode = NORMAL_MODE
new_password_buffer = "" # Buffer para la nueva contraseña
change_password_start_time = 0 # Temporizador para el proceso de cambio de contraseña
CHANGE_PASSWORD_TIMEOUT_MS = 30000 # 30 segundos para el proceso completo de cambio de contraseña
# VARIABLES PARA EL BLOQUEO DEL TECLADO
failed_attempts_count = 0
LOCKOUT_THRESHOLD = 3 # Número de intentos fallidos antes del bloqueo
lockout_start_time = 0 # Tiempo en que inició el bloqueo
TEMPORARY_LOCKOUT_DURATION_MS = 30000 # Duración del bloqueo temporal (30 segundos)
is_permanently_locked_by_keypad = False # True si el teclado está permanentemente bloqueado
BLINK_POLICE_INTERVAL_MS = 200 # Intervalo de parpadeo de "policía" (rojo/azul)
# Bandera para recordar si ya hubo un bloqueo permanente en esta sesión de encendido
has_been_permanently_locked_before_since_power_on = False
# Cliente MQTT global
client = None
last_ping_time = 0
PING_INTERVAL_MS = 30000 # Enviar un ping cada 30 segundos para mantener la conexión
# Funciones de Conectividad
def connect_wifi(ssid, password):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print(f"Conectando a red Wi-Fi: {ssid}...")
wlan.connect(ssid, password)
max_attempts = 10
while not wlan.isconnected() and max_attempts > 0:
print(".", end="")
time.sleep(1)
max_attempts -= 1
if wlan.isconnected():
print("\n¡Conectado a Wi-Fi!")
print("Dirección IP:", wlan.ifconfig()[0])
return True
else:
print("\n¡ERROR! No se pudo conectar a Wi-Fi.")
return False
return True
def mqtt_callback(topic, msg):
global unlock_by_mqtt_timestamp, is_door_simulated_open, is_door_state_mqtt_controlled, door_simulated_open_timestamp, \
is_permanently_locked_by_keypad, failed_attempts_count, lockout_start_time, has_been_permanently_locked_before_since_power_on
print(f"Mensaje MQTT recibido en tópico '{topic.decode()}': '{msg.decode()}'")
# Comandos de Cerradura (desde el dashboard de cerradura)
if topic.decode() == TOPIC_COMMAND.decode():
command = msg.decode().strip().upper()
if command == "LOCK":
if not is_door_locked:
lock_door()
publish_event("Comando MQTT: Cerradura BLOQUEADA")
# Al bloquear/desbloquear desde MQTT, se reinicia el estado de bloqueo del keypad
is_permanently_locked_by_keypad = False
failed_attempts_count = 0
lockout_start_time = 0
has_been_permanently_locked_before_since_power_on = False # <-- REINICIAR ESTA BANDERA
print("Keypad y contadores de bloqueo reiniciados por comando MQTT LOCK.")
publish_event("Keypad: Desbloqueado/Reiniciado por comando MQTT LOCK.")
elif command == "UNLOCK":
if is_door_locked:
unlock_door()
publish_event("Comando MQTT: Cerradura DESBLOQUEADA")
# Inicia el temporizador para el cierre automático de la CERRADURA
unlock_by_mqtt_timestamp = time.ticks_ms()
print(f"DEBUG: Temporizador para cierre automático de CERRADURA iniciado. La cerradura se cerrará en {MQTT_UNLOCK_DURATION_MS / 1000} segundos.")
# Al bloquear/desbloquear desde MQTT, se reinicia el estado de bloqueo del keypad
is_permanently_locked_by_keypad = False
failed_attempts_count = 0
lockout_start_time = 0
has_been_permanently_locked_before_since_power_on = False # <-- REINICIAR ESTA BANDERA
print("Keypad y contadores de bloqueo reiniciados por comando MQTT UNLOCK.")
publish_event("Keypad: Desbloqueado/Reiniciado por comando MQTT UNLOCK.")
elif command == "0": # Comando de liberación del botón (ignorar)
print("Comando de liberación de botón de cerradura (0) recibido, ignorando.")
else:
print(f"Comando desconocido de cerradura: {command}")
publish_event(f"Comando MQTT desconocido de cerradura: {command}")
# Comandos de Puerta (desde los botones de puerta)
elif topic.decode() == TOPIC_DOOR_COMMAND.decode():
door_command = msg.decode().strip().upper()
print(f"Comando de puerta recibido: {door_command}")
if door_command == "ABIERTA":
is_door_simulated_open = True
is_door_state_mqtt_controlled = True # Activa el control de la puerta por MQTT
door_simulated_open_timestamp = time.ticks_ms() # Inicia el temporizador de cierre de puerta simulada
print(f"DEBUG: Temporizador para cierre automático de PUERTA SIMULADA iniciado. Se cerrará en {DOOR_SIMULATED_CLOSE_DURATION_MS / 1000} segundos.")
publish_event("Comando MQTT: Puerta simulada como ABIERTA")
elif door_command == "CERRADA":
is_door_simulated_open = False
is_door_state_mqtt_controlled = True # Activa el control de la puerta por MQTT
door_simulated_open_timestamp = 0 # Reinicia el temporizador de puerta simulada
publish_event("Comando MQTT: Puerta simulada como CERRADA")
elif door_command == "0": # Comando de liberación del botón (ignoramos)
print("Comando de liberación de botón de puerta (0) recibido, ignorando.")
else:
print(f"Comando desconocido de puerta: {door_command}")
publish_event(f"Comando MQTT desconocido de puerta: {door_command}")
def connect_mqtt():
global client, last_ping_time
try:
client = MQTTClient(MQTT_CLIENT_ID, MQTT_BROKER, port=MQTT_PORT, user=MQTT_USERNAME, password=MQTT_PASSWORD, keepalive=60)
client.set_callback(mqtt_callback)
client.connect()
print(f"¡Conectado al broker MQTT: {MQTT_BROKER}!")
client.subscribe(TOPIC_COMMAND)
client.subscribe(TOPIC_DOOR_COMMAND) # <--- Suscribirse al nuevo tópico de comandos de puerta
print(f"Suscrito a tópico de comandos de cerradura: {TOPIC_COMMAND.decode()}")
print(f"Suscrito a tópico de comandos de puerta: {TOPIC_DOOR_COMMAND.decode()}") # Nuevo mensaje de depuración
last_ping_time = time.ticks_ms()
return True
except Exception as e:
print(f"¡ERROR! No se pudo conectar al broker MQTT: {e}")
return False
# Funciones de publicación
def publish_status():
global client
status_msg = "BLOQUEADA" if is_door_locked else "DESBLOQUEADA"
try:
if client:
client.publish(TOPIC_STATUS, status_msg.encode())
print(f"MQTT: Publicado estado '{status_msg}' en '{TOPIC_STATUS.decode()}'")
except Exception as e:
print(f"ERROR al publicar estado MQTT: {e}")
reconnect_mqtt_safe()
def publish_door_state(state):
global client
door_state_msg = "CERRADA" if state == 0 else "ABIERTA"
try:
if client:
client.publish(TOPIC_DOOR_SENSOR, door_state_msg.encode())
print(f"MQTT: Publicado estado puerta '{door_state_msg}' en '{TOPIC_DOOR_SENSOR.decode()}'")
publish_event(f"Puerta: {door_state_msg}")
except Exception as e:
print(f"ERROR al publicar estado puerta MQTT: {e}")
reconnect_mqtt_safe()
def publish_event(event_message):
global client
try:
if client:
client.publish(TOPIC_EVENTS, event_message.encode())
print(f"MQTT: Publicado evento '{event_message}' en '{TOPIC_EVENTS.decode()}'")
except Exception as e:
print(f"ERROR al publicar evento MQTT: {e}")
reconnect_mqtt_safe()
def reconnect_mqtt_safe():
global client
print("Intentando reconectar MQTT de forma segura...")
try:
client.disconnect()
except:
pass
if not connect_mqtt():
print("Reconexión MQTT fallida.")
# Funciones de Control de Hardware
def set_rgb_color(r, g, b):
LED_R_PIN.value(r)
LED_G_PIN.value(g)
LED_B_PIN.value(b)
def turn_off_rgb():
set_rgb_color(0, 0, 0)
# Función para parpadear el LED un número específico de veces
def blink_rgb(r, g, b, count, delay_ms):
for _ in range(count):
set_rgb_color(r, g, b)
time.sleep_ms(delay_ms)
turn_off_rgb()
time.sleep_ms(delay_ms)
def set_servo_angle(angle):
duty = int(25 + (angle / 180.0) * 100)
servo.duty(duty)
time.sleep_ms(500) # Pequeña pausa para que el servo alcance la posición
def lock_door():
global is_door_locked
set_servo_angle(0) # Posición de bloqueo para el servo
is_door_locked = True
print("Cerradura: BLOQUEADA")
# El color del LED para el estado de bloqueo se gestiona al final del bucle principal
publish_status() # Publica el estado de la cerradura a Adafruit IO
publish_event("Cerradura: BLOQUEADA")
def unlock_door():
global is_door_locked, failed_attempts_count, lockout_start_time, is_permanently_locked_by_keypad, has_been_permanently_locked_before_since_power_on
set_servo_angle(90) # Posición de desbloqueo para el servo
is_door_locked = False
print("Cerradura: DESBLOQUEADA")
# REINICIAR CONTADORES DE BLOQUEO CUANDO SE DESBLOQUEA LA CERRADURA
failed_attempts_count = 0
lockout_start_time = 0
is_permanently_locked_by_keypad = False
has_been_permanently_locked_before_since_power_on = False # <-- MUY IMPORTANTE REINICIAR ESTA BANDERA
print("Keypad y contadores de bloqueo reiniciados al desbloquear la cerradura.")
# El color del LED para el estado de desbloqueo se gestiona al final del bucle principal
publish_status() # Publica el estado de la cerradura a Adafruit IO
publish_event("Cerradura: DESBLOQUEADA")
# Lógica de Inicio del Sistema
print("Cargando sistema de cerradura...")
turn_off_rgb()
time.sleep_ms(1000)
if not connect_wifi(WIFI_SSID, WIFI_PASSWORD):
print("No se pudo conectar a Wi-Fi, abortando.")
while True: # Bucle de error si no hay Wi-Fi
set_rgb_color(1,0,0) # LED rojo parpadeante
time.sleep_ms(500)
turn_off_rgb()
time.sleep_ms(500)
if not connect_mqtt():
print("No se pudo conectar a MQTT, abortando.")
while True: # Bucle de error si no hay MQTT
set_rgb_color(1,1,0) # LED amarillo parpadeante
time.sleep_ms(500)
turn_off_rgb()
time.sleep_ms(500)
lock_door() # Asegura que la cerradura esté bloqueada al iniciar
time.sleep_ms(500)
# Publica el estado inicial de la puerta
# Ahora el 'last_door_state' inicial se basará en el sensor físico
last_door_state = DOOR_SENSOR_PIN.value()
publish_door_state(last_door_state)
# Bucle Principal del Programa
print("Iniciando bucle principal. Intenta presionar teclas del keypad.")
while True:
current_time = time.ticks_ms() # Obtener el tiempo actual al inicio del bucle
# --- Manejar mensajes MQTT entrantes y mantener conexión ---
try:
if client:
client.check_msg() # Procesa mensajes MQTT pendientes
if time.ticks_diff(current_time, last_ping_time) > PING_INTERVAL_MS:
client.ping() # Envía un PING para mantener la conexión activa
last_ping_time = current_time
print("MQTT: Enviando PING para mantener conexión.")
except Exception as e:
print(f"Error al verificar o mantener mensajes MQTT: {e}")
reconnect_mqtt_safe()
# --- Lógica de Cierre Automático Temporizado (para el comando MQTT UNLOCK de la cerradura) ---
if unlock_by_mqtt_timestamp > 0: # Si hay un timestamp de desbloqueo por MQTT activo
if time.ticks_diff(current_time, unlock_by_mqtt_timestamp) > MQTT_UNLOCK_DURATION_MS:
if not is_door_locked: # Solo bloquear si actualmente está desbloqueada
lock_door() # Llama a la función de bloqueo
publish_event("La cerradura ha realizado su cierre automático temporizado (por MQTT).")
unlock_by_mqtt_timestamp = 0
# IMPORTANTE: Aquí la cerradura se cierra, pero la puerta (sensor simulado) no se ve afectada
# si fue abierta por MQTT. Eso lo gestiona el siguiente bloque.
# --- Lógica de CIERRE AUTOMÁTICO de la PUERTA SIMULADA (MQTT) ---
if is_door_state_mqtt_controlled and is_door_simulated_open and door_simulated_open_timestamp > 0:
if time.ticks_diff(current_time, door_simulated_open_timestamp) > DOOR_SIMULATED_CLOSE_DURATION_MS:
print("Tiempo de puerta simulada abierta agotado. Cerrando automáticamente.")
is_door_simulated_open = False # Cambia el estado a cerrada
door_simulated_open_timestamp = 0 # Reinicia el temporizador
# IMPORTANTE: No se desactiva is_door_state_mqtt_controlled aquí
# porque la puerta sigue bajo control de MQTT, solo que ahora está cerrada.
publish_event("Puerta simulada: Cerrada automáticamente por temporizador.")
# La publicación del estado se maneja en el bloque de monitoreo de puerta abajo.
# --- Gestión del temporizador para cambio de contraseña ---
if system_mode == CHANGE_PASSWORD_INIT_MODE: # Solo si estamos en modo de cambio de contraseña
if time.ticks_diff(current_time, change_password_start_time) > CHANGE_PASSWORD_TIMEOUT_MS:
print(f"Tiempo para cambio de contraseña agotado. Volviendo a NORMAL_MODE.")
publish_event(f"Keypad: Tiempo agotado en cambio de contraseña. Volviendo a NORMAL_MODE.")
system_mode = NORMAL_MODE
current_password_input = "" # Limpiar cualquier entrada en curso
new_password_buffer = "" # Limpiar buffer de nueva contraseña
turn_off_rgb() # Asegurarse de apagar LED de cambio de contraseña
# NO reiniciar failed_attempts_count aquí si es por timeout de cambio de contraseña.
blink_rgb(1, 0, 0, 3, 200) # 3 parpadeos rojos rápidos
turn_off_rgb()
# Lógica de Bloqueo del Keypad
key_pressed = None
# PRIORIDAD: Bloqueo permanente > Bloqueo temporal > Cerradura abierta > Modo normal
if is_permanently_locked_by_keypad:
# El keypad está permanentemente bloqueado, ignoramos todas las entradas (excepto para limpiar si se presiona '*')
for col_idx, col_pin in enumerate(COLS_PINS):
col_pin.value(0)
for row_idx, row_pin in enumerate(ROWS_PINS):
if not row_pin.value():
key_pressed = KEYPAD_KEYS[row_idx][col_idx]
while not row_pin.value():
time.sleep_ms(20)
time.sleep_ms(200)
break
col_pin.value(1)
if key_pressed:
break
if key_pressed == '*':
print("Keypad: Bloqueado permanentemente. Reiniciando entrada con '*'.")
current_password_input = ""
new_password_buffer = ""
system_mode = NORMAL_MODE
password_input_start_time = 0
change_password_start_time = 0
blink_rgb(0, 0, 1, 2, 100) # Azul para indicar acción
turn_off_rgb()
else:
if key_pressed:
print(f"Keypad: Bloqueado permanentemente. Tecla '{key_pressed}' ignorada.")
key_pressed = None # Asegurar que la tecla no se procese más
elif lockout_start_time > 0 and time.ticks_diff(current_time, lockout_start_time) < TEMPORARY_LOCKOUT_DURATION_MS:
# Estamos en un bloqueo temporal, ignoramos las entradas del keypad (excepto '*')
for col_idx, col_pin in enumerate(COLS_PINS):
col_pin.value(0)
for row_idx, row_pin in enumerate(ROWS_PINS):
if not row_pin.value():
key_pressed = KEYPAD_KEYS[row_idx][col_idx]
while not row_pin.value():
time.sleep_ms(20)
time.sleep_ms(200)
break
col_pin.value(1)
if key_pressed:
break
if key_pressed == '*':
print("Keypad: Bloqueo temporal activo. Reiniciando entrada con '*'.")
current_password_input = ""
new_password_buffer = ""
system_mode = NORMAL_MODE
password_input_start_time = 0
change_password_start_time = 0
blink_rgb(0, 0, 1, 2, 100) # Azul para indicar acción en bloqueo
turn_off_rgb()
else:
if key_pressed: # Solo si se presionó algo diferente de '*'
print(f"Keypad: Bloqueado temporalmente. Tecla '{key_pressed}' ignorada.")
blink_rgb(1, 0, 1, 1, 100) # Parpadeo magenta para indicar que la tecla es ignorada
turn_off_rgb()
key_pressed = None # Asegurar que la tecla no se procese más
elif is_door_locked == False and system_mode == NORMAL_MODE: # Añadida condición de "system_mode == NORMAL_MODE"
# La cerradura está abierta y estamos en modo normal. El keypad está inactivo.
# Leemos el keypad para "consumir" la pulsación, pero no la procesamos.
key_pressed_dummy = None # <--- CORRECCIÓN: Inicializar key_pressed_dummy aquí
for col_idx, col_pin in enumerate(COLS_PINS):
col_pin.value(0)
for row_idx, row_pin in enumerate(ROWS_PINS):
if not row_pin.value():
key_pressed_dummy = KEYPAD_KEYS[row_idx][col_idx] # Leemos la tecla pero no la usamos
while not row_pin.value():
time.sleep_ms(20)
time.sleep_ms(200)
print(f"Keypad: Cerradura abierta. Tecla '{key_pressed_dummy}' ignorada.")
blink_rgb(0, 1, 1, 1, 100) # CIAN para indicar que la tecla es ignorada por puerta abierta
turn_off_rgb()
break
col_pin.value(1)
if key_pressed_dummy: # Si se presionó cualquier tecla, salimos para no seguir leyendo
key_pressed = None # Aseguramos que 'key_pressed' sea None
break
else:
# El keypad no está bloqueado por intentos fallidos ni por puerta abierta, procesar la entrada normalmente
# Lógica para reiniciar la entrada de contraseña si el tiempo se agota
if system_mode == NORMAL_MODE and \
current_password_input != "" and password_input_start_time != 0 and \
time.ticks_diff(current_time, password_input_start_time) > PASSWORD_INPUT_TIMEOUT_MS:
print("Tiempo de entrada de contraseña agotado. Reiniciando entrada. Se suma como intento fallido.")
publish_event("Keypad: Tiempo de entrada agotado. Contando como intento fallido.")
current_password_input = ""
password_input_start_time = 0 # Reiniciar el timestamp
failed_attempts_count += 1 # Incrementar intentos fallidos por timeout
# Lógica de bloqueo de keypad refinada y simplificada para timeout
if failed_attempts_count >= LOCKOUT_THRESHOLD:
if has_been_permanently_locked_before_since_power_on:
# Si ya estuvo en bloqueo permanente antes, va directo al permanente
print("¡Keypad BLOQUEADO PERMANENTEMENTE! Desbloquee desde Adafruit IO. (Reincidencia por timeout)")
publish_event("Keypad: Bloqueo PERMANENTE activado (reincidencia por timeout).")
is_permanently_locked_by_keypad = True
failed_attempts_count = 0
lockout_start_time = 0
set_rgb_color(1, 0, 0)
elif lockout_start_time == 0: # Primer bloqueo: temporal
print(f"¡Keypad BLOQUEADO temporalmente por {TEMPORARY_LOCKOUT_DURATION_MS / 1000} segundos!")
publish_event(f"Keypad: Bloqueo TEMPORAL activado por {failed_attempts_count} intentos fallidos (incluido timeout).")
lockout_start_time = current_time
blink_rgb(1, 0, 1, 5, 200) # Parpadeo Magenta para bloqueo temporal
turn_off_rgb()
else: # Segundo bloqueo (o más) consecutivo sin haber llegado a permanente antes -> permanente
print("¡Keypad BLOQUEADO PERMANENTEMENTE! Desbloquee desde Adafruit IO.")
publish_event("Keypad: Bloqueo PERMANENTE activado por múltiples intentos fallidos (incluido timeout).")
is_permanently_locked_by_keypad = True
has_been_permanently_locked_before_since_power_on = True # Marcar que ya alcanzó el permanente
failed_attempts_count = 0
lockout_start_time = 0
set_rgb_color(1, 0, 0)
else:
blink_rgb(1, 1, 0, 2, 200) # Amarillo para timeout sin bloqueo
turn_off_rgb()
elif system_mode == CHANGE_PASSWORD_INIT_MODE and \
new_password_buffer == "" and password_input_start_time != 0 and \
time.ticks_diff(current_time, password_input_start_time) > PASSWORD_INPUT_TIMEOUT_MS:
print("Tiempo de inicio de nueva contraseña agotado. Volviendo a NORMAL_MODE.")
publish_event("Keypad: Tiempo agotado para nueva contraseña. Volviendo a NORMAL_MODE.")
system_mode = NORMAL_MODE
current_password_input = ""
new_password_buffer = ""
turn_off_rgb()
password_input_start_time = 0
change_password_start_time = 0
blink_rgb(1, 0, 0, 3, 200) # 3 parpadeos rojos rápidos
turn_off_rgb()
for col_idx, col_pin in enumerate(COLS_PINS):
col_pin.value(0) # Activa la columna
for row_idx, row_pin in enumerate(ROWS_PINS):
if not row_pin.value(): # Si se detecta una pulsación (pin en LOW)
key_pressed = KEYPAD_KEYS[row_idx][col_idx]
while not row_pin.value(): # Espera a que la tecla sea liberada (debounce simple)
time.sleep_ms(20)
time.sleep_ms(200) # Pequeño retardo adicional para evitar múltiples lecturas rápidas
break # Sale del bucle de filas
col_pin.value(1) # Desactiva la columna
if key_pressed:
break # Sale del bucle de columnas
if key_pressed: # Solo si una tecla fue efectivamente leída y no ignorada por un bloqueo o puerta abierta
print(f"Tecla presionada: {key_pressed}")
# Reiniciar entrada/proceso por '*' (siempre permitido)
if key_pressed == '*':
print("Entrada/Proceso reiniciado por usuario.")
current_password_input = ""
new_password_buffer = ""
system_mode = NORMAL_MODE # Volver a modo normal
set_rgb_color(0, 0, 1) # Azul para reinicio/cancelación
time.sleep(1)
turn_off_rgb()
publish_event("Keypad: Entrada/Proceso reiniciado por usuario.")
password_input_start_time = 0
change_password_start_time = 0
# IMPORTANTE: No reiniciar failed_attempts_count ni lockout_start_time ni has_been_permanently_locked_before_since_power_on aquí con '*'.
# Lógica principal del keypad basada en el modo del sistema y estado de bloqueo
elif system_mode == NORMAL_MODE:
# Lógica para la secuencia A04 (cambio de contraseña)
if key_pressed == 'A':
if current_password_input == "":
current_password_input = 'A'
print("DEBUG: Posible inicio de secuencia de cambio de contraseña (A).")
password_input_start_time = current_time # Iniciar/reiniciar temporizador
elif current_password_input in ["A", "A0"]: # A + A o A0 + A -> error, se suma como intento fallido
print(f"DEBUG: Secuencia A04 interrumpida por 'A' repetida o mal colocada. Sumando intento fallido.")
current_password_input = ""
password_input_start_time = 0
failed_attempts_count += 1 # Sumar intento fallido
publish_event(f"Keypad: Secuencia A04 interrumpida por 'A' incorrecta. Intentos fallidos: {failed_attempts_count}.")
# Lógica de bloqueo de keypad refinada y simplificada
if failed_attempts_count >= LOCKOUT_THRESHOLD:
if has_been_permanently_locked_before_since_power_on:
print("¡Keypad BLOQUEADO PERMANENTEMENTE! Desbloquee desde Adafruit IO. (Reincidencia por secuencia A04 incorrecta)")
publish_event("Keypad: Bloqueo PERMANENTE activado (reincidencia por secuencia A04 incorrecta).")
is_permanently_locked_by_keypad = True
failed_attempts_count = 0
lockout_start_time = 0
set_rgb_color(1, 0, 0)
elif lockout_start_time == 0: # Primer bloqueo: temporal
print(f"¡Keypad BLOQUEADO temporalmente por {TEMPORARY_LOCKOUT_DURATION_MS / 1000} segundos!")
publish_event(f"Keypad: Bloqueo TEMPORAL activado por {failed_attempts_count} intentos fallidos.")
lockout_start_time = current_time
blink_rgb(1, 0, 1, 5, 200)
turn_off_rgb()
else: # Segundo bloqueo (o más) consecutivo sin haber llegado a permanente antes -> permanente
print("¡Keypad BLOQUEADO PERMANENTEMENTE! Desbloquee desde Adafruit IO.")
publish_event("Keypad: Bloqueo PERMANENTE activado por múltiples intentos fallidos.")
is_permanently_locked_by_keypad = True
has_been_permanently_locked_before_since_power_on = True
failed_attempts_count = 0
lockout_start_time = 0
set_rgb_color(1, 0, 0)
else:
blink_rgb(1, 0, 0, 2, 200) # Rojo para error
turn_off_rgb()
elif key_pressed == '0' and current_password_input == 'A':
current_password_input = 'A0'
print("DEBUG: Secuencia A0 detectada.")
password_input_start_time = current_time # Reiniciar/actualizar temporizador
elif key_pressed == '4' and current_password_input == 'A0':
current_password_input = 'A04'
print("DEBUG: Secuencia A04 detectada.")
password_input_start_time = current_time # Reiniciar/actualizar temporizador
# Si la secuencia A04 se completa, cambiamos el modo inmediatamente
print("Secuencia de cambio de contraseña 'A04' ingresada. Ingresa la nueva contraseña.")
publish_event("Keypad: Iniciando cambio de contraseña.")
system_mode = CHANGE_PASSWORD_INIT_MODE # CAMBIO CLAVE: Cambiar al modo de iniciar cambio
current_password_input = "" # Reiniciar buffer para la nueva contraseña
new_password_buffer = ""
change_password_start_time = current_time # Iniciar temporizador para el proceso completo
password_input_start_time = 0 # Resetear temporizador de entrada regular
failed_attempts_count = 0 # Reiniciar intentos fallidos AL INICIAR CAMBIO DE CONTRASEÑA (acceso autorizado)
lockout_start_time = 0 # Asegurar que cualquier bloqueo temporal se levante
is_permanently_locked_by_keypad = False # Asegurar que el bloqueo permanente se levante
has_been_permanently_locked_before_since_power_on = False # Reiniciar también esta bandera al cambiar contraseña
# Esto significa un "nuevo ciclo" de seguridad
blink_rgb(0, 1, 0, 6, 500) # 6 parpadeos de 0.5s = 3 segundos (verde)
turn_off_rgb() # Asegurar apagado al final
print("Regresando al modo normal de ingreso de contraseña.")
elif key_pressed == '#': # Fin de entrada de contraseña
if current_password_input == CORRECT_PASSWORD:
print("Contraseña CORRECTA.")
publish_event("Keypad: Contraseña CORRECTA.")
if is_door_locked:
unlock_door()
publish_event("Keypad: Cerradura DESBLOQUEADA por contraseña correcta.")
# Reiniciar el temporizador para el cierre automático de la CERRADURA (si aplica)
unlock_by_mqtt_timestamp = time.ticks_ms()
print(f"DEBUG: Temporizador para cierre automático de CERRADURA iniciado por Keypad. Se cerrará en {MQTT_UNLOCK_DURATION_MS / 1000} segundos.")
else:
lock_door()
publish_event("Keypad: Cerradura BLOQUEADA por contraseña correcta.")
current_password_input = ""
password_input_start_time = 0 # Reiniciar el timestamp
failed_attempts_count = 0 # Reiniciar intentos fallidos
lockout_start_time = 0 # Asegurar que cualquier bloqueo temporal se levante
is_permanently_locked_by_keypad = False
has_been_permanently_locked_before_since_power_on = False
set_rgb_color(0, 1, 0) # Verde para éxito
time.sleep(1)
turn_off_rgb()
else:
print(f"Contraseña INCORRECTA: '{current_password_input}'")
publish_event(f"Keypad: Contraseña INCORRECTA. Intentos fallidos: {failed_attempts_count+1}.")
current_password_input = ""
password_input_start_time = 0 # Reiniciar el timestamp
failed_attempts_count += 1 # Incrementar intentos fallidos
set_rgb_color(1, 0, 0) # Rojo para error
time.sleep(1)
turn_off_rgb()
# Lógica de bloqueo de keypad
if failed_attempts_count >= LOCKOUT_THRESHOLD:
if has_been_permanently_locked_before_since_power_on:
print("¡Keypad BLOQUEADO PERMANENTEMENTE! Desbloquee desde Adafruit IO. (Reincidencia por intentos fallidos)")
publish_event("Keypad: Bloqueo PERMANENTE activado (reincidencia por intentos fallidos).")
is_permanently_locked_by_keypad = True
failed_attempts_count = 0
lockout_start_time = 0
set_rgb_color(1, 0, 0)
elif lockout_start_time == 0: # Primer bloqueo: temporal
print(f"¡Keypad BLOQUEADO temporalmente por {TEMPORARY_LOCKOUT_DURATION_MS / 1000} segundos!")
publish_event(f"Keypad: Bloqueo TEMPORAL activado por {failed_attempts_count} intentos fallidos.")
lockout_start_time = current_time
blink_rgb(1, 0, 1, 5, 200) # Parpadeo Magenta para bloqueo temporal
turn_off_rgb()
else: # Segundo bloqueo (o más) consecutivo sin haber llegado a permanente antes -> permanente
print("¡Keypad BLOQUEADO PERMANENTEMENTE! Desbloquee desde Adafruit IO.")
publish_event("Keypad: Bloqueo PERMANENTE activado por múltiples intentos fallidos.")
is_permanently_locked_by_keypad = True
has_been_permanently_locked_before_since_power_on = True
failed_attempts_count = 0
lockout_start_time = 0
set_rgb_color(1, 0, 0)
elif key_pressed.isdigit(): # Si es un dígito
if len(current_password_input) < MAX_PASSWORD_LENGTH:
current_password_input += key_pressed
print(f"Entrada actual: {current_password_input}")
password_input_start_time = current_time # Reiniciar/actualizar temporizador
else:
print("Límite de caracteres de contraseña alcanzado.")
publish_event("Keypad: Límite de caracteres de contraseña alcanzado.")
blink_rgb(1, 1, 0, 1, 200) # Amarillo para indicar límite
turn_off_rgb()
else: # Carácter no válido para entrada de contraseña en modo normal (ej: 'B', 'C', 'D')
print(f"Tecla '{key_pressed}' no válida para entrada de contraseña en modo normal. Entrada reseteada.")
publish_event(f"Keypad: Tecla '{key_pressed}' no válida en modo normal. Entrada reseteada.")
current_password_input = ""
password_input_start_time = 0
blink_rgb(1, 0, 0, 1, 200) # Rojo para error
turn_off_rgb()
elif system_mode == CHANGE_PASSWORD_INIT_MODE: # Modo de cambiar contraseña (después de A04)
if key_pressed == '#': # Fin de la entrada de la nueva contraseña
if len(new_password_buffer) >= 4: # La nueva contraseña debe tener al menos 4 caracteres
CORRECT_PASSWORD = new_password_buffer
print(f"Contraseña CAMBIADA a: {CORRECT_PASSWORD}")
publish_event(f"Keypad: Contraseña CAMBIADA exitosamente a: {CORRECT_PASSWORD}")
system_mode = NORMAL_MODE # Volver al modo normal
current_password_input = ""
new_password_buffer = ""
password_input_start_time = 0
change_password_start_time = 0 # Resetear temporizador general de cambio
failed_attempts_count = 0 # Reiniciar intentos fallidos al cambiar contraseña
lockout_start_time = 0
is_permanently_locked_by_keypad = False
has_been_permanently_locked_before_since_power_on = False # Reiniciar también esta bandera al cambiar contraseña
# Esto significa un "nuevo ciclo" de seguridad
blink_rgb(0, 1, 0, 6, 500) # 6 parpadeos de 0.5s = 3 segundos (verde)
turn_off_rgb() # Asegurar apagado al final
print("Regresando al modo normal de ingreso de contraseña.")
else:
print(f"La nueva contraseña debe tener al menos 4 caracteres. Actual: {len(new_password_buffer)}.")
publish_event(f"Keypad: Nueva contraseña muy corta ({len(new_password_buffer)}).")
blink_rgb(1, 0, 0, 2, 250) # Parpadeo rojo para error
# No resetea el modo ni el buffer, solo muestra error. Puede seguir intentando.
elif key_pressed in ['A', 'B', 'C', 'D']: # No permitir letras en la nueva contraseña
print(f"La nueva contraseña solo puede contener dígitos (0-9). Tecla '{key_pressed}' no válida.")
blink_rgb(1, 0, 0, 2, 250) # Parpadeo rojo para error
# No resetea el buffer, solo muestra error.
else: # Es un dígito (0-9)
if len(new_password_buffer) < MAX_PASSWORD_LENGTH: # Puedes ajustar MAX_PASSWORD_LENGTH para nuevas contraseñas
new_password_buffer += key_pressed
print(f"Nueva contraseña actual: {new_password_buffer}")
else:
print(f"Límite de caracteres para nueva contraseña alcanzado ({MAX_PASSWORD_LENGTH}).")
publish_event(f"Keypad: Límite de caracteres para nueva contraseña alcanzado.")
blink_rgb(1, 0, 0, 2, 250) # Parpadeo rojo para error
# Sensor de Puerta (1 o 0) / Cerrada o abierta
# El estado de la puerta que se publica depende de si está siendo controlada por MQTT.
# Prioridad: Comando MQTT de puerta > Sensor físico
current_door_physical_state = DOOR_SENSOR_PIN.value()
# Determinar el estado de la puerta a monitorear y publicar
# Si 'is_door_state_mqtt_controlled' es True, usaremos el estado simulado.
# Si es False, usaremos el estado del sensor físico.
door_state_to_monitor_and_publish = current_door_physical_state # Por defecto, usa el sensor físico
if is_door_state_mqtt_controlled:
# Si la puerta está bajo control MQTT, usa el estado simulado (1 para abierta, 0 para cerrada)
door_state_to_monitor_and_publish = 1 if is_door_simulated_open else 0
# Solo publica si el estado actual a publicar es diferente al último estado que se publicó
if door_state_to_monitor_and_publish != last_door_state:
print(f"Cambio en el estado de la puerta detectado (Publicando): {door_state_to_monitor_and_publish}")
publish_door_state(door_state_to_monitor_and_publish)
last_door_state = door_state_to_monitor_and_publish # Actualiza el último estado publicado
# --- Lógica de Alerta de Puerta Abierta Estando Bloqueada (con titileo) ---
alert_condition_active = is_door_locked and door_state_to_monitor_and_publish == 1
# --- Mantiene el color del LED RGB según el estado actual del sistema ---
# Prioridad: Bloqueo permanente > Bloqueo temporal > Alerta > Cambio de Contraseña (azul titilante) > Estado normal de cerradura
if is_permanently_locked_by_keypad:
# Parpadeo de "policía" (Rojo/Azul) para bloqueo permanente
if time.ticks_diff(current_time, last_blink_time) > BLINK_POLICE_INTERVAL_MS:
is_blink_on = not is_blink_on
if is_blink_on:
set_rgb_color(1, 0, 0) # Rojo
else:
set_rgb_color(0, 0, 1) # Azul
last_blink_time = current_time
# También puedes enviar una alerta periódica si lo deseas
if time.ticks_diff(current_time, last_door_alert_time) > DOOR_ALERT_INTERVAL_MS: # Reutilizando el intervalo
print("ALERTA: Keypad en bloqueo PERMANENTE.")
publish_event("ALERTA: Keypad en bloqueo PERMANENTE (esperando desbloqueo MQTT).")
last_door_alert_time = current_time
elif lockout_start_time > 0 and time.ticks_diff(current_time, lockout_start_time) < TEMPORARY_LOCKOUT_DURATION_MS:
# Parpadeo rápido rojo/azul para bloqueo temporal
if time.ticks_diff(current_time, last_blink_time) > 150: # Parpadeo más rápido
is_blink_on = not is_blink_on
if is_blink_on:
set_rgb_color(1, 0, 1) # Magenta (Rojo + Azul) para bloqueo
else:
turn_off_rgb()
last_blink_time = current_time
# También enviar una alerta de bloqueo temporal si es necesario
if time.ticks_diff(current_time, last_door_alert_time) > DOOR_ALERT_INTERVAL_MS: # Reutilizando el intervalo
print("ALERTA: Keypad en bloqueo TEMPORAL.")
publish_event("ALERTA: Keypad en bloqueo TEMPORAL.")
last_door_alert_time = current_time
elif not is_door_locked and system_mode == NORMAL_MODE and not alert_condition_active:
# Cerradura DESBLOQUEADA y en modo normal, sin alerta (puerta cerrada si está desbloqueada)
set_rgb_color(0, 1, 0) # Verde si está desbloqueada
last_door_alert_time = 0
last_blink_time = 0
is_blink_on = False
elif alert_condition_active:
# Gestiona el parpadeo del LED rojo para la alerta (puerta abierta estando BLOQUEADA)
if time.ticks_diff(current_time, last_blink_time) > BLINK_INTERVAL_MS:
is_blink_on = not is_blink_on # Cambia el estado de parpadeo (on/off)
if is_blink_on:
set_rgb_color(1, 0, 0) # Enciende Rojo
else:
turn_off_rgb() # Apaga el LED
last_blink_time = current_time # Actualiza el tiempo del último cambio de parpadeo
# Envía el evento MQTT de alerta solo a intervalos definidos
if time.ticks_diff(current_time, last_door_alert_time) > DOOR_ALERT_INTERVAL_MS:
print("¡ALERTA! Puerta abierta mientras la cerradura está BLOQUEADA.")
publish_event("ALERTA: Puerta abierta estando BLOQUEADA")
last_door_alert_time = current_time # Actualiza el tiempo de la última alerta enviada
elif system_mode == CHANGE_PASSWORD_INIT_MODE:
# Luz azul titilando cada medio segundo (0.5s encendido, 0.5s apagado)
if time.ticks_diff(current_time, last_blink_time) > 500: # 500ms para 0.5 segundos
is_blink_on = not is_blink_on
if is_blink_on:
set_rgb_color(0, 0, 1) # Azul
else:
turn_off_rgb()
last_blink_time = current_time
elif system_mode == NORMAL_MODE:
# Si la condición de alerta NO está activa y NO estamos en modo de cambio de contraseña:
# Reinicia los temporizadores de parpadeo y alerta, y establece el color normal.
last_door_alert_time = 0
last_blink_time = 0
is_blink_on = False # Asegura que el LED no siga parpadeando
if is_door_locked:
set_rgb_color(1, 0, 0) # Rojo si está bloqueada
# else: Esta condición se maneja en el primer 'if' de este bloque de LEDs (verde).
# No ponemos 'set_rgb_color(0,1,0)' acà xk el primer 'if' tiene prioridad.
time.sleep_ms(200)