// el proyecto por dentro

El timbre por dentro

Cómo está hecho el timbre automático, con el código del proyecto a la vista: las piezas del lenguaje una por una, el bucle que decide cuándo sonar, las librerías, el protocolo de órdenes y la simulación con la que se prueba. Es el material de clase, en versión web.

// la idea

El cerebro vive en el ESP32,
no en la nube

El timbre es un computador pequeño (un ESP32, programado en MicroPython) que guarda su horario y su reloj en su propia memoria. La página web y la nube son un control remoto y un espejo: si se cae internet, el timbre programado sigue sonando.

Dos caminos hacia la misma campana: por internet (con login y token escondido) o directo en la red del colegio.

// cómo leer el código

No todo el código
se lee igual

El proyecto tiene unas 1.170 líneas. Nadie las lee todas, ni hace falta. Se leen en tres profundidades distintas, y saber cuál le toca a cada archivo es la primera lección de ingeniería del curso.

●●●

Se lee línea por línea

El cerebro del timbre: variables, decisiones, listas, diccionarios, bucles y archivos. Son 253 líneas y la meta del curso es entenderlas todas.

main.py · config.py

●●

Se entiende la idea

Conectarse al WiFi, guardar un archivo, hablar con la nube. Sabemos qué hacen y para qué sirven, aunque no cómo lo logran por dentro.

lo que hacen las librerías

●●

Se usa y ya

El chip del reloj, el servidor web, la red de rescate. Cajas negras: se usan por su contrato, se abrirán en cursos futuros.

lib_reloj · lib_red · lib_web

// las piezas del lenguaje

Las siete piezas
del cerebro

El cerebro del timbre está hecho con siete ideas de Python. Ninguna es avanzada; todas se ven en clase. Los fragmentos que siguen están tomados de los archivos del proyecto, no son ejemplos aparte.

Pieza 1 · Variables

Los ajustes son variables

Una variable es una caja con nombre que guarda un valor. En MAYÚSCULAS escribimos las que no cambian: son los tornillos del sistema, y viven todas juntas en config.py.

Para que la campana suene 6 segundos en vez de 4 se cambia el número y se vuelve a subir el archivo. No hay que tocar nada más.

config.py
# ---- Pines (dónde va cada cable) ----
PIN_RELAY   = 23   # pin del relé

# ---- Comportamiento del timbre ----
DURACION_S  = 4    # segundos del toque largo

# ---- Zona horaria ----
OFFSET_HORA = -5   # Colombia = UTC - 5

Pieza 2 · El mundo real

Un pin enciende cosas de verdad

El pin 23 del ESP32 saca una señal de 3,3 voltios. El relé usa esa señal para cerrar el circuito de la campana, que funciona con mucha más corriente de la que el chip podría dar.

La primera orden del programa es apagar el relé: antes de hacer nada más, asegurarse de que la campana está en silencio.

main.py — seguridad primero
from machine import Pin

relay = Pin(PIN_RELAY, Pin.OUT)
relay.value(0)   # silencio, pase lo que pase

# ...más adelante, al tocar:
relay.value(1)   # ¡RING!
relay.value(0)   # silencio

Pieza 3 · Listas

Una fila ordenada: los días

Una lista guarda varios valores en orden, y a cada uno se le pide por su posición. La posición se cuenta desde cero: por eso el lunes es el día 0 y el domingo el 6.

El relojero devuelve un número del 0 al 6, y esta lista lo traduce a un nombre que el resto del programa entiende.

0lun
1mar
2mié
3jue
4vie
5sáb
6dom
config.py + lib_reloj.py
DIAS_SEMANA = ["lunes", "martes", "miercoles",
               "jueves", "viernes", "sabado",
               "domingo"]

DIAS_SEMANA[0]   # → "lunes"
DIAS_SEMANA[6]   # → "domingo"

Pieza 4 · Diccionarios

El horario del colegio
es un diccionario

En un diccionario no se busca por posición sino por nombre, igual que en un diccionario de verdad se busca por palabra. Cada día es una clave, y su valor es otro diccionario: sus horas.

El número que acompaña a cada hora es el patrón de toque, que explica la pieza siguiente.

"lunes" sus horas del día
"07:00" patrón 1 · entrada
así se ve el horario por dentro
horario = {
  "lunes":  {"07:00": 1, "10:30": 2},
  "martes": {"07:00": 1},
}

horario["lunes"]["07:00"]   # → 1

Pieza 5 · Datos en vez de caminos

Los tres patrones del timbre

El timbre no siempre suena igual: la entrada es un toque largo, el cambio de clase es corto y largo, la salida es corto, corto y largo.

Esto empezó siendo tres if que repetían el mismo bloque. Ahora cada patrón es una lista con la duración de sus toques y un solo bucle los recorre. Agregar un patrón nuevo es agregar una línea a la tabla, sin tocar el bucle.

main.py — los patrones
PATRONES = {
    1: [DURACION_S],            # entrada
    2: [0.5, DURACION_S],       # cambio
    3: [0.5, 0.5, DURACION_S],  # salida
}

for numero, segundos in enumerate(toques):
    if numero > 0:
        await asyncio.sleep(PAUSA_S)
    relay.value(1)
    await asyncio.sleep(segundos)
    relay.value(0)

Pieza 6 · El operador in

La pregunta que decide

in pregunta ¿está adentro? y responde sí o no. Si el minuto de ahora aparece entre las horas de hoy, la campana suena.

Su opuesto, not in, se encarga de los festivos: las fechas canceladas están en una lista, y si hoy está ahí, silencio.

main.py — la decisión
if minuto_hoy in horas_hoy and hoy not in canceladas:
    patron = horas_hoy[minuto_hoy]
    sonar_timbre(patron)

Pieza 7 · Archivos

Memoria que sobrevive
al apagón

Las variables viven en la memoria mientras hay corriente. Si se va la luz, se borran… ¿y el horario que programamos?

Por eso el horario se guarda como archivo JSON en la memoria flash del ESP32, la misma que guarda el programa. Ahí sobrevive sin energía.

Por eso, después de un apagón, el timbre arranca sabiendo su horario.

main.py — leer y guardar
def _guardar(archivo, datos):
    with open(archivo, "w") as f:
        json.dump(datos, f)      # dict → archivo

def _leer(archivo, por_defecto):
    try:
        with open(archivo, "r") as f:
            return json.load(f)  # archivo → dict
    except:
        return por_defecto     # aún no existe

// el cerebro completo

Las siete piezas,
juntas

Esto es todo el cerebro del timbre. Un bucle que nunca termina y que, cada vez que cambia el minuto, hace la pregunta clave. Si ya leíste las piezas de arriba, aquí no hay nada nuevo: solo están juntas.

main.py — el bucle, línea por línea
while True:                          # (1)
    dia_hoy    = dia_semana()          # (2)
    minuto_hoy = hora_hhmm()
    horas_hoy  = horario.get(dia_hoy, {})

    if minuto_hoy != ultimo_sonado:    # (3)
        if minuto_hoy in horas_hoy:      # (4)
            sonar_timbre(horas_hoy[minuto_hoy])
        ultimo_sonado = minuto_hoy

    await asyncio.sleep(30)           # (5)
  1. Bucle infinito: el timbre nunca "termina", vigila para siempre.
  2. Le pregunta el día y la hora al relojero.
  3. La memoria: ¿ya revisamos este minuto? (lo explica el recuadro de abajo)
  4. La pregunta clave: ¿este minuto está en el horario de hoy?
  5. Descansa 30 segundos y vuelve a empezar.

El error de sonar dos veces

El problema

El bucle revisa cada 30 segundos, pero un minuto dura 60. Las 07:00 se revisan dos veces: a las 07:00:00 y a las 07:00:30. La campana sonaría dos veces seguidas.

La solución: recordar

Una variable guarda el último minuto ya revisado. Si el minuto de ahora es el mismo, no se repite. Es memoria, en una sola variable: ultimo_sonado.

Es el tipo de error que no se ve leyendo el código, sino pensando en cómo pasa el tiempo mientras el código corre.

// las cajas negras

Las cuatro librerías

Antes de mirar las del timbre: ya usas cajas negras todos los días, y nunca te ha hecho falta abrirlas.

print("hola")

¿Leíste el código que dibuja cada letra en la pantalla? No.

time.sleep(1)

¿Sabes cómo cuenta el tiempo el chip por dentro? No.

Enviar un mensaje

¿Sabes cómo viaja tu mensaje hasta el otro celular? No.

Usarlas sin mirar adentro es lo normal al programar. Lo que sí hay que conocer es el contrato: qué le doy, qué me devuelve y qué pasa si algo sale mal. El cómo lo hace queda del lado de la caja.

el contrato de lib_reloj
# le pides algo...
lib_reloj.obtener_hora()

# ...y te devuelve algo, siempre con la misma forma
→ (2026, 8, 10, 6, 30, 0)
   año   mes día hora min seg

El cerebro del timbre delega en cuatro librerías. Cada una tiene su contrato y su analogía:

lib_reloj

el relojero

Sabe la hora exacta aunque se vaya la luz: módulo de reloj con pila + sincronización por internet cuando la hay.

hora_hhmm() → "06:30"
dia_semana() → "lunes"

lib_red

el técnico de WiFi

Conecta el ESP32 a la red del colegio. Si no puede, levanta su propia red de rescate para reconfigurarlo desde un celular.

conectar() → True / False

lib_web

la recepcionista local

La página que se ve dentro del colegio en timbre.local: login, tocar el timbre y editar el horario desde cualquier celular de la red.

iniciar(sonar=…, agregar=…)
servidor()

lib_nube

el mensajero

Mantiene la conexión con Blynk por MQTT: recibe órdenes de afuera, confirma que las cumplió y publica el estado y el horario.

conectar() · atender()
publicar_estado(…)

Todas reciben del cerebro una "lista de teléfonos": las funciones que saben hacer cada cosa (sonar, agregar, borrar…). La recepcionista y el mensajero no saben tocar la campana — saben a quién llamar para que suene. Ojo al detalle: se entrega sonar_timbre sin paréntesis. Se da el número de teléfono, no se hace la llamada.

Las dos palabras que aún no estudiamos

En el código aparecen async y await. Por ahora la regla del curso es leerlas y seguir, pero vale saber qué resuelven:

Espera egoísta

time.sleep(4) congela todo. Mientras suena el timbre, la página web no respondería a nadie.

Espera educada

await asyncio.sleep(4) deja al ESP32 atender la página y la nube mientras espera. Como un cocinero que revuelve otra olla mientras una hierve.

// el protocolo

El viaje de una orden

Cuando alguien autorizado toca "Cambio" en el panel, la orden viaja en un sobre muy simple: 17|sonar|2 — un número de orden, la acción y el patrón. El número evita que el timbre suene dos veces si el mensaje llega repetido, y la confirmación 17|ok hace el viaje de vuelta.

El token que autoriza escribir en Blynk vive solo en el servidor: el navegador nunca lo ve.

// la simulación

Probarlo sin hardware

Antes de subir nada al timbre del colegio, el proyecto se prueba en Wokwi, un simulador de ESP32 que corre en el navegador. El circuito tiene los mismos componentes que la caja del colegio y ejecuta los mismos archivos, así que lo que falla, falla ahí primero.

Abrir la simulación del timbre →
Se abre en el navegador y arranca con el botón verde. No hay que instalar nada.

  1. El proyecto está montado en Wokwi con los mismos archivos de la carpeta wokwi-sim/.
  2. El circuito virtual (diagram.json) trae el ESP32 con su relé y su módulo de reloj. Donde va la campana hay un led: se enciende cuando el relé cierra, así que muestra el ritmo de cada patrón de toque.
  3. El reloj del circuito arranca un lunes a las 06:59:30, así que a los 30 segundos se dispara solo el timbre de las 07:00: el cerebro se prueba sin esperar a una hora de verdad.
  4. En modo demo la simulación habla con un servidor público de pruebas; en modo blynk, con la nube real del proyecto.
  5. Se envía una orden desde el panel… y en el navegador se ve conmutar el relé —el mismo interruptor que en el colegio le da corriente a la campana— con su confirmación de vuelta.

El circuito no lleva ledes de diagnóstico ni botones, aunque se vería más vistoso: cada componente de adorno obliga al código a manejarlo, y entonces el programa que se prueba deja de ser el que se instala. El único añadido es el led que ocupa el lugar de la campana, y el código ni se entera de que existe: lo enciende el relé, igual que encendería la campana.

Con ese montaje se probó la capa de nube (lib_nube) antes de que el código llegara al ESP32 del colegio.

// seguridad

Quién puede
hacerlo sonar

El token nunca sale del servidor

La llave que autoriza hablar con la nube vive como secreto en el servidor. Quien mire el código fuente de esta página no encuentra nada que le sirva para hacer sonar el timbre.

Login con cookie firmada

Para dar órdenes hay que iniciar sesión. La sesión es una cookie firmada criptográficamente que el navegador no puede leer ni falsificar, y cada orden lleva un número único que impide repetirla.

// para cerrar

Las cuentas del proyecto

El timbre son 1.174 líneas: 290 en main.py y config.py, que se leen enteras en clase, y 884 repartidas en las tres librerías, que se usan por su contrato.

Esa proporción es la que hace que el proyecto quepa en un curso. No hay que entender las 1.174 para poder cambiar el horario, agregar un patrón de toque o arreglar un error del cerebro: hay que entender 290 y saber qué promete cada caja.

Ver el horario que suena hoy →