# Universidad Nacional de La Matanza
# Fundamentos de Sistemas Embebidos
# Alumno: Chateloin Pereyra Kamila Nicole
# Proyecto: Tender para Colgados
# Descripción:
#Este proyecto es un tender automático que mete o saca la ropa solo,
#según el clima y el tiempo que el usuario elija.
#Detecta temperatura, humedad, luz y viento para proteger la ropa
#si se viene lluvia, viento fuerte o se hace de noche.
#Muestra todo en una pantalla, suena con alertas y además se puede
#controlar desde el celular gracias a su conexión a internet
#CONSTANTES EN MAYUS
import time
import machine
import network
from machine import Pin, PWM, ADC, I2C
from umqtt.simple import MQTTClient
from dht import DHT22
from lcd_i2c import LCD #librería personalizada del lcd
#ESTADOS DEL SISTEMA
ESTADO_ESPERA = 0 #Tender: adentro, esperando que le digan cuánto tiempo secar.
ESTADO_SETEO = 1 #El usuario está eligiendo el tiempo (seteando minutos).
ESTADO_SECADO = 2 #El tender está afuera y el reloj corriendo.
ESTADO_GUARDADO = 3 #Se guardó (ya sea porque termino o porque llovio).
#Conexion
ssid = 'Wokwi-GUEST'
clave_wifi = ''
servidor_mqtt = 'io.adafruit.com'
puerto = 1883
usuario = 'kamichate'
clave_mqtt = 'aio_uuuV5254JSYUbUzKpdS2wlBL0MTf'
id_cliente = 'tender_colgados_kami'
#Tópicos MQTT
TOPICO_PUB_MOTOR = 'kamichate/feeds/Motor' #Esta afuera o adentro?
TOPICO_PUB_HUMEDAD = 'kamichate/feeds/Humedad' #Que porcentaje de humedad hay?
TOPICO_PUB_TEMPERATURA = 'kamichate/feeds/Temperatura' #Q temperatura hace?
TOPICO_PUB_LUZ_ESTADO = 'kamichate/feeds/Luz_solar' #Es de día o de noche?
TOPICO_PUB_DATOS = 'kamichate/feeds/Datos_salida' #mensaje lcd
TOPICO_SUB_CONTROL = 'kamichate/feeds/Control_General' #botones: MAS, MENOS, ENTER
TOPICO_PUB_VIENTO = 'kamichate/feeds/Viento' #viento fuerte?
#WiFi copiado del pdf
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
sta_if.connect(ssid, clave_wifi)
print("Conectando a WiFi... (puede tardar unos segundos)")
while not sta_if.isconnected():
print(".", end="")
time.sleep(0.1) # Esperamos un poquito...
print("\n¡Conectado a WiFi exitosamente! IP asignada:")
print(sta_if.ifconfig())
#Hardware
#botones físicos
PIN_BOTON_MAS = Pin(12, Pin.IN, Pin.PULL_UP)
PIN_BOTON_MENOS = Pin(13, Pin.IN, Pin.PULL_UP)
PIN_BOTON_ENTER = Pin(14, Pin.IN, Pin.PULL_UP)
#lcd i2c
#pines 22 (SCL) y 21 (SDA) estándar del ESP32
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
lcd = LCD(i2c, 0x27, 2, 16)
lcd.clear()
lcd.putstr("Hola!")
#Buzzer
PIN_BUZZER = PWM(Pin(25), freq=1000, duty=0)
#sensores
SENSOR_DHT = DHT22(Pin(4)) #temperatura y humedad
ADC_LDR = ADC(Pin(34)) #sensor de luz ambiental
ADC_LDR.atten(ADC.ATTN_11DB) #calibración para leer hasta 3.3V
ADC_VIENTO = ADC(Pin(35)) #potenciómetro físico calibrado tambien para 3.3v
ADC_VIENTO.atten(ADC.ATTN_11DB)
#Servo
DUTY_MIN = 25 # Posición 0 grados
DUTY_MAX = 120 # Posición 180 grados
SERVO = PWM(Pin(26), freq=50)
#VARIABLES GLOBALES
estado_actual = ESTADO_ESPERA
tiempo_seteo_minutos = 0
tiempo_restante_segundos = 0
tender_afuera = False #Es falso porque sig que no esta afuera
ultimo_mensaje_lcd = "" # Memoria para no parpadear la pantalla
ultimo_estado_mas = 1
ultimo_estado_menos = 1
ultimo_estado_enter = 1
#Copia de los valores anteriores para saber si algo cambió
temp_reportada_anterior = 0.0
humedad_reportada_anterior = 0.
viento_reportado_anterior = 0
estado_motor_anterior = "INICIO"
luz_estado_anterior = "INICIO"
#timer para descontar los minutos de secado
tiempo_ultimo_decremento = time.ticks_ms()
INTERVALO_DECREMENTO_MS = 60000 #1 minuto real son 60000 ms
#limites (definidos por experimentos de personas publicados en internet y mi propia experiencia)
UMBRAL_HUMEDAD_ALTA = 80 #80%
UMBRAL_TEMP_BAJA = 10 #10°C
UMBRAL_LUZ_BAJA = 1000 #valor sensor ldr para noche
UMBRAL_VIENTO_FUERTE = 3000 #valor viento para tormenta
UMBRAL_TEMP_ALTA = 40
#Funciones
def mover_servo(angulo):
global tender_afuera
#asegurar que el angulo este entre 0 y 180 para no forzar el motor
angulo = max(0, min(180, angulo))
duty = int(DUTY_MIN + (angulo / 180) * (DUTY_MAX - DUTY_MIN))
SERVO.duty(duty)
time.sleep(0.5) #medio segundo para llegar
#para saber dónde quedo el motor
tender_afuera = (angulo > 90)
# print(f"[MOTOR] Me moví a {angulo} grados.")
#fuerzo que el motor arranque adentro.
mover_servo(0)
#fun buzzer
def sonar_buzzer(duracion):
PIN_BUZZER.duty(512) #volumen al 50% porque va de 0 a 1023
time.sleep(duracion)
PIN_BUZZER.duty(0) #silencio
def mostrar_en_lcd(linea1, linea2=""):
global ultimo_mensaje_lcd
#creo un string unico con el mensaje para compararlo con el último publicado
mensaje_nuevo = str(linea1) + "|" + str(linea2)
#si el mensaje es igual al anterior, no se publica nada (ahorramos recursos)
if mensaje_nuevo != ultimo_mensaje_lcd:
lcd.clear()
lcd.putstr(str(linea1))
if linea2:
lcd.set_cursor(0, 1)
lcd.putstr(str(linea2))
ultimo_mensaje_lcd = mensaje_nuevo
#publicar en adafruit sincronizar el dashboard
try:
cliente_mqtt.publish(TOPICO_PUB_DATOS, mensaje_nuevo.replace("|", "\n")) # Reemplaza | por salto de línea para el dashboard
except:
pass #si falla, no bloquea el código
def formato_tiempo(minutos_totales):
#transforma en horas:minutos para informar en la pantalla
horas = minutos_totales // 60
minutos = minutos_totales % 60
return f"{horas:02d}:{minutos:02d}hs" #para insertar variables favil
#LOGICA DE CONTROL
def sumar_tiempo():
global tiempo_seteo_minutos, estado_actual
#solo dejamos cambiar el tiempo si no estamos secando ya
if estado_actual != ESTADO_SECADO:
tiempo_seteo_minutos = min(tiempo_seteo_minutos + 30, 360) #tope 6 horas, mas tiempo seria absurdo
estado_actual = ESTADO_SETEO
#print(f"[BOTON +] Sumamos tiempo: {tiempo_seteo_minutos} min, Estado: {estado_actual}")#para mi, el usuario no lo ve
def restar_tiempo():
global tiempo_seteo_minutos, estado_actual
if estado_actual != ESTADO_SECADO:
tiempo_seteo_minutos = max(tiempo_seteo_minutos - 30, 0) #no bajar de 0
if tiempo_seteo_minutos == 0:
estado_actual = ESTADO_ESPERA
estado_actual = ESTADO_SETEO
#print(f"[BOTON -] Restamos tiempo: {tiempo_seteo_minutos} min, Estado: {estado_actual}") #para mi el usuario no lo ve
def iniciar_secado():
global tiempo_restante_segundos, estado_actual
#print(f"[ENTER] Intentando iniciar secado. Estado: {estado_actual}, Tiempo: {tiempo_seteo_minutos}")
#si tenemos tiempo puesto, arrancamos
if estado_actual == ESTADO_SETEO and tiempo_seteo_minutos > 0:
tiempo_restante_segundos = tiempo_seteo_minutos * 60
mover_servo(180) #tender afuera
estado_actual = ESTADO_SECADO
print(f"tender fuera Por {tiempo_seteo_minutos} minutos.")# mensaje para mi, el usuario no lo ve
sonar_buzzer(0.5)
#si ya estaba secando y aprieta enter, cancelamos (parada de emergencia)
elif estado_actual == ESTADO_SECADO:
cancelar_secado()
def cancelar_secado():
global estado_actual, tiempo_seteo_minutos
mover_servo(0) #tender dentro
estado_actual = ESTADO_GUARDADO
tiempo_seteo_minutos = 0
print("Secado CANCELADO manualmente.") #msj para mi
mostrar_en_lcd("CANCELADO", "Guardando...")#msj usuario
sonar_buzzer(1)
#Callback MQTT
def funcion_callback(topico, mensaje):
#esta función se ejecuta sola cuando llega un mensaje de Adafruit
#decodificamos el mensaje de adafruit a texto normal
topico_recibido = topico.decode('utf-8')
mensaje_recibido = mensaje.decode('utf-8').upper()
print(f"Llegó mensaje en '{topico_recibido}': {mensaje_recibido}") #moi
#si es un botón de control
if topico_recibido == TOPICO_SUB_CONTROL:
if mensaje_recibido == "MAS":
sumar_tiempo()
elif mensaje_recibido == "MENOS":
restar_tiempo()
elif mensaje_recibido == "ENTER":
iniciar_secado()
#conexion mqtt
try:
cliente_mqtt = MQTTClient(id_cliente, servidor_mqtt, user=usuario, password=clave_mqtt, port=puerto)
cliente_mqtt.set_callback(funcion_callback) #le decimos qué función usar al recibir mensajes
cliente_mqtt.connect()
#suscripcion a topicos para botones y viento
cliente_mqtt.subscribe(TOPICO_SUB_CONTROL)
print("conectado a adafruit :)")
except OSError as e:
print(f"Falló la conexión MQTT: {e}. Reiniciando en 5 segs...")
time.sleep(5)
machine.reset()
#Sensores
def leer_sensores():
#Lee temperatura, humedad, luz y el viento simulado
try:
SENSOR_DHT.measure()
t = SENSOR_DHT.temperature()
h = SENSOR_DHT.humidity()
except Exception:
t, h = 20, 50 #valores por defecto por si falla el sensor
luz = ADC_LDR.read() #se mueve entre 32 y 4063, mientras mas bajo, mas de noche es
#print(f"valor de luz {luz}")
viento = ADC_VIENTO.read()#se mueve entre 0 y 4095
#print(f"valor de viento {viento}")
#time.sleep(3)
return t, h, luz, viento
def verificar_excepciones(temperatura, humedad, luz, viento):
#Decide si hay que guardar la ropa por mal clima
guardar = False
razon = ""
if luz < UMBRAL_LUZ_BAJA: #luz menor a 1000 (nro aproximado)
guardar = True
razon = "OSCURIDAD / NOCHE"
elif humedad > UMBRAL_HUMEDAD_ALTA: #humedad mayor a 80 = lluvia
guardar = True
razon = "LLUVIA DETECTADA"
elif viento > 2500 and humedad > 70:
guardar = True
razon = "ROCIO"
elif viento > UMBRAL_VIENTO_FUERTE: #viento mayor a 3000 (numero aproximado)
guardar = True
razon = "MUCHO VIENTO"
elif temperatura < UMBRAL_TEMP_BAJA and humedad > 60: #temp menor a 10
guardar = True
razon = "FRIO Y HUMEDO"
elif temperatura < 2 and humedad < 50:
guardar = True
razon = "TEMPERATURA BAJO CERO"
elif temperatura > UMBRAL_TEMP_ALTA:
guardar = True
razon = "CALOR EXTREMO"
return guardar, razon
def publicar_datos_condicionales(temp_actual, humedad_actual, luz_actual, viento_actual):
#nvia datos a adafruit solo si hubo cambios significativos
global temp_reportada_anterior, humedad_reportada_anterior, estado_motor_anterior, luz_estado_anterior, viento_reportado_anterior
#estado del Motor
estado_motor_actual = "AFUERA" if tender_afuera else "ADENTRO"
motor_cambio = (estado_motor_actual != estado_motor_anterior)
#temp,viento y hum
temp_cambio = (abs(temp_actual - temp_reportada_anterior) >= 5)
humedad_cambio = (abs(humedad_actual - humedad_reportada_anterior) >= 10)
viento_cambio = (abs(viento_actual - viento_reportado_anterior) >= 500)
#luz
luz_estado_actual = "DIA" if luz_actual > UMBRAL_LUZ_BAJA else "NOCHE"
luz_cambio = (luz_estado_actual != luz_estado_anterior)
hubo_publicacion = False
#si cambio la temp, reportar
if temp_cambio:
cliente_mqtt.publish(TOPICO_PUB_TEMPERATURA, str(temp_actual))
temp_reportada_anterior = temp_actual
hubo_publicacion = True
#si cambio la hum, reportar
if humedad_cambio:
cliente_mqtt.publish(TOPICO_PUB_HUMEDAD, str(humedad_actual))
humedad_reportada_anterior = humedad_actual
hubo_publicacion = True
#si cambio el viento, reportar
if viento_cambio:
cliente_mqtt.publish(TOPICO_PUB_VIENTO, str(viento_actual))
viento_reportado_anterior = viento_actual
hubo_publicacion = True
#si cambio el motor, reportar
if motor_cambio:
cliente_mqtt.publish(TOPICO_PUB_MOTOR, estado_motor_actual)
estado_motor_anterior = estado_motor_actual
hubo_publicacion = True
#si cambio la luz, reportar
if luz_cambio:
cliente_mqtt.publish(TOPICO_PUB_LUZ_ESTADO, luz_estado_actual)
luz_estado_anterior = luz_estado_actual # Actualiza la memoria de la luz
hubo_publicacion = True
#pequeña pausa para atosigar
if hubo_publicacion:
time.sleep(0.1)
#bucle (tengo que tenes todas las funciones definidas)
while True:
try:
# 1. Leer botones físicos (0 = presionado, 1 = no presionado)
estado_mas = PIN_BOTON_MAS.value()
estado_menos = PIN_BOTON_MENOS.value()
estado_enter = PIN_BOTON_ENTER.value()
#print(f"[DEBUG] Pines: MAS={estado_mas}, MENOS={estado_menos}, ENTER={estado_enter}")
if estado_mas == 0 and ultimo_estado_mas == 1: # Presionado y antes no lo estaba
sumar_tiempo()
time.sleep(0.2) # Debounce simple
ultimo_estado_mas = estado_mas
if estado_menos == 0 and ultimo_estado_menos == 1:
restar_tiempo()
time.sleep(0.2)
ultimo_estado_menos = estado_menos
if estado_enter == 0 and ultimo_estado_enter == 1:
iniciar_secado()
time.sleep(0.2)
ultimo_estado_enter = estado_enter
#actualización inmediata del LCD después de botones
if estado_actual == ESTADO_ESPERA:
mostrar_en_lcd("Hola Kami!", "Presiona +")
elif estado_actual == ESTADO_SETEO:
msj_tiempo = formato_tiempo(tiempo_seteo_minutos)
mostrar_en_lcd("TIEMPO A SECAR:", msj_tiempo)
#2. hay mensajes nuevos de Adafruit?
cliente_mqtt.check_msg()
#3. como está el clima
temp, hum, luz, viento = leer_sensores()
#4.hay que publicar algo a internet
publicar_datos_condicionales(temp, hum, luz, viento)
#5. FSM
if estado_actual == ESTADO_ESPERA:
mostrar_en_lcd("Hola Kami!", "Presiona +")
elif estado_actual == ESTADO_SETEO:
msj_tiempo = formato_tiempo(tiempo_seteo_minutos)
mostrar_en_lcd("TIEMPO A SECAR:", msj_tiempo)
elif estado_actual == ESTADO_SECADO:
#control del reloj (se descuenta 1 minuto cuando pasa 1 minuto real)
ahora = time.ticks_ms()
if time.ticks_diff(ahora, tiempo_ultimo_decremento) >= INTERVALO_DECREMENTO_MS:
tiempo_restante_segundos -= 60
tiempo_ultimo_decremento = ahora
#mostrar cuenta regresiva
mins = tiempo_restante_segundos // 60
mostrar_en_lcd("SECANDO ROPA...", formato_tiempo(mins))
#si el tiempo llegó a cero, entonces
if tiempo_restante_segundos <= 0:
mover_servo(0) # Guardar tender
estado_actual = ESTADO_GUARDADO
sonar_buzzer(3) # Alarma larga de fin
cliente_mqtt.publish(TOPICO_PUB_DATOS, "FIN: Ropa Seca")
#REVISIÓN DE EMERGENCIA (Lluvia/Viento)
guardar_urgente, motivo = verificar_excepciones(temp, hum, luz, viento)
if guardar_urgente:
mover_servo(0)
estado_actual = ESTADO_GUARDADO
sonar_buzzer(2) #alarma de emergencia
mins_faltantes = tiempo_restante_segundos // 60
mostrar_en_lcd("ALERTA CLIMA!", motivo)
cliente_mqtt.publish(TOPICO_PUB_DATOS, f"EMERGENCIA: {motivo}")
elif estado_actual == ESTADO_GUARDADO:
#estado de transición para mostrar el mensaje final un ratito
time.sleep(1)
estado_actual = ESTADO_ESPERA # Volvemos al inicio
tiempo_seteo_minutos = 0
time.sleep(0.01)
except OSError as e:
print(f"Ups, error en el bucle: {e}. Reintentando en 5 segs...")
time.sleep(5)
# FIN DEL CODIGO-PROYECTO TENDER PARA COLGADOS
# Hecho con mucho mate<3