●●●
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// el proyecto 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 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
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.
●●●
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●●●
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●●●
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
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
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.
# ---- 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
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.
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 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.
DIAS_SEMANA = ["lunes", "martes", "miercoles",
"jueves", "viernes", "sabado",
"domingo"]
DIAS_SEMANA[0] # → "lunes"
DIAS_SEMANA[6] # → "domingo"
Pieza 4 · Diccionarios
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.
horario = {
"lunes": {"07:00": 1, "10:30": 2},
"martes": {"07:00": 1},
}
horario["lunes"]["07:00"] # → 1
Pieza 5 · Datos en vez de caminos
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.
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
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.
if minuto_hoy in horas_hoy and hoy not in canceladas:
patron = horas_hoy[minuto_hoy]
sonar_timbre(patron)
Pieza 7 · Archivos
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.
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
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.
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)
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.
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
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.
¿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.
# 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:
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"
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
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()
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.
En el código aparecen async y await. Por ahora la regla del curso es
leerlas y seguir, pero vale saber qué resuelven:
time.sleep(4) congela todo. Mientras suena el timbre, la página
web no respondería a nadie.
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
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
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.
wokwi-sim/.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.demo la simulación habla con un servidor público de pruebas; en modo blynk, con la nube real del proyecto.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
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.
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
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.