01 · Un recorrido completo y reproducible
Qué vamos a construir
El proyecto retoma el controlador de banco del tema 47. Una temperatura de entrada gobierna una salida lógica mediante histéresis. La adquisición y el control se atienden antes de intentar intercambios de red. Las mediciones se encolan, el servidor las conserva y un panel permite consultar historia y proponer umbrales nuevos.
La implementación descargable usa un dispositivo simulado en Python, un servidor HTTP local y SQLite. Así podés ejecutar el recorrido completo sin placa, cuenta de nube ni bibliotecas externas. Las temperaturas son sintéticas y la salida es una variable; no se lee un sensor ni se acciona un GPIO desde estos programas.
| Parte | Qué permite comprobar |
|---|---|
| Proyecto Python descargable | Intercambios HTTP reales, permisos separados, persistencia SQLite y panel web |
| Núcleo ejecutable de esta página | Histéresis, vencimiento, cola y configuración sin servidor |
| Laboratorio interactivo | Explorar manualmente cortes, acuses perdidos y versiones en una simulación |
| Propuesta de banco físico | Identificar las interfaces que reemplazan adquisición y salida al migrar a una placa |
Descargar el proyecto completo en ZIP. Incluye todos los programas, el panel, instrucciones y pruebas. El banco utiliza HTTP sin TLS exclusivamente en 127.0.0.1; no es un despliegue de producción.
02 · Separar lógica, adaptación y medición
Banco y variante con hardware
Para la versión ejecutable necesitás Python 3.10 o posterior con el módulo sqlite3, dos terminales y un navegador. Todo corre en una computadora. Los programas usan la biblioteca estándar y no requieren pip install.
La variante física de aprendizaje puede usar un ESP32 clásico, una entrada analógica y un LED con resistencia. Un potenciómetro representa una magnitud ajustable: su posición no es una medición de temperatura. Para medir temperatura real se incorpora un sensor y se valida su conversión, precisión y diagnóstico de fallas.
| Función | Banco propuesto | Qué validar |
|---|---|---|
| Entrada de prueba | Cursor de potenciómetro de 10 kΩ a GPIO34, extremos a 3,3 V y GND | Rango eléctrico, atenuación, saturación y lectura del ADC |
| Salida visible | GPIO25 → resistencia de 1 kΩ → ánodo de LED; cátodo a GND | Polaridad, corriente y salida apagada al iniciar |
| Comunicación | Adaptador Wi-Fi y cliente de aplicación | Plazos, identidad, validación del servidor y reconexión |
| Diagnóstico | Salida serie y contadores | Evitar que registros extensos bloqueen el control |
La documentación ADC de Arduino-ESP32 distingue lectura cruda y conversión a milivoltios, y detalla rangos que dependen del chip y la atenuación. Una tensión dentro del límite eléctrico no garantiza una lectura lineal en todo ese rango. El GPIO34 del ESP32 clásico es de entrada; no se usa para manejar el LED.
El servidor descargable escucha solo en la propia computadora. 127.0.0.1 visto desde una placa designaría a esa placa, no a la PC. Conectar hardware requiere diseñar otro transporte y un servicio accesible con TLS y credenciales adecuadas; no se resuelve copiando la URL local al firmware.
La propuesta física queda como adaptación guiada. Las pruebas automáticas de este proyecto corresponden al núcleo y al recorrido local; no verifican el montaje eléctrico ni un firmware ESP32.
03 · Un archivo por responsabilidad
Componentes del proyecto
ENTRADA SINTÉTICA → NÚCLEO LOCAL → SALIDA LÓGICA
↓
COLA DE 3 REGISTROS
↓ POST /api/muestras
SERVIDOR HTTP → SQLite → PANEL
↑ ↓
GET /api/config POST /api/config
↑ ↓
CONFIGURACIÓN APLICADA ← CONFIGURACIÓN DESEADA| Archivo | Responsabilidad |
|---|---|
| nucleo.py | Reglas locales, validez temporal, configuración y cola; sin red ni GPIO |
| dispositivo.py | Secuencia sintética de lecturas, consultas y envíos HTTP con timeout |
| servidor.py | API, autorización por rol, validación y transacciones SQLite |
| panel.html | Consulta de historia y escritura de configuración deseada |
| pruebas.py | Pruebas del núcleo y de la API con una base temporal |
| README.md | Puesta en marcha, alcance y límites |
Esta separación permite cambiar el sensor o el transporte sin reescribir la histéresis. El núcleo tiene un único propietario de la salida y no conoce contraseñas. El adaptador consulta una configuración, la valida mediante el núcleo y luego transmite registros pendientes.
Orden de un ciclo del agente
- Producir una muestra y evaluar la salida con la configuración ya aplicada.
- Intentar guardar el registro en la cola acotada.
- Si corresponde comunicarse, consultar configuración deseada y validarla.
- Enviar como máximo dos registros, retirando cada uno solo con su acuse coincidente.
- Mostrar contadores y esperar el tiempo restante del ciclo de escritorio.
El ciclo usa un segundo lógico por muestra. La espera real puede aumentar por red, sistema operativo o almacenamiento. Este agente de escritorio demuestra el protocolo; no garantiza el plazo de control de un firmware embebido. Al portar, las operaciones de red deben aislarse o planificarse para sostener los plazos locales.
04 · Decidir antes de comunicar
Lectura, histéresis y antigüedad
La configuración inicial es versión 1, encender desde 28 °C y apagar hasta 26 °C. Entre ambos umbrales se conserva la salida previa. Los límites son inclusivos: a 28 se enciende; a 26 se apaga.
| Entrada | Salida resultante | Motivo |
|---|---|---|
| 24 °C | Apagada | Está por debajo del umbral de apagado |
| 29 °C | Encendida | Supera el umbral de encendido |
| 27 °C | Encendida | En la banda se conserva el estado |
| 25 °C | Apagada | Cruza el umbral de apagado |
| Lectura inválida | Apagada | Política explícita de este banco |
Una lectura también vence
Una temperatura válida conserva vigencia durante 5 segundos lógicos inclusive. Con edad mayor de 5, la salida del banco se apaga. El laboratorio permite avanzar 6 segundos sin medir para observar el vencimiento; no convierte la lectura vieja en un dato nuevo.
El rango aceptado para una entrada válida es de −40 a 125 °C. Se rechazan tipos incorrectos, valores no finitos y magnitudes fuera de ese rango. Una falla de sensor se representa como None en Python y null en JSON, acompañada de calidad inválida.
Aplicar configuración cambia la decisión
El conjunto version, on, off se valida antes de sustituir la configuración activa. Debe cumplir off < on. Una versión anterior o la misma versión con otro contenido se rechazan sin aplicación parcial. Repetir exactamente la configuración aplicada no vuelve a ejecutar un cambio.
La nueva configuración reevalúa el control con la lectura local vigente. Si esa lectura queda dentro de la nueva banda, se conserva el estado anterior según la histéresis; no hay un reinicio implícito de la salida.
05 · Contratos pequeños y verificables
API y contratos de datos
El servidor vincula el token de equipo con una única identidad admitida: medidor-01. El token de administración tiene otras rutas. Un campo libre en el JSON no concede acceso a otro dispositivo.
| Operación | Credencial | Resultado |
|---|---|---|
| GET / | Sin token | Interfaz del panel, sin datos privados ni credenciales embebidas |
| GET /api/config | Equipo | Configuración deseada guardada en servidor |
| POST /api/muestras | Equipo | Registro validado y acuse posterior a la transacción |
| GET /api/estado | Administrador | Configuración deseada, cantidad de registros y últimas 20 entradas nuevas |
| POST /api/config | Administrador | Nueva configuración deseada con versión superior |
{
"schema": 1,
"device": "medidor-01",
"boot": "demo",
"seq": 2,
"tick": 2,
"temperature": 29,
"quality": "valid",
"output": true,
"config_version": 1
}El identificador del registro combina device / boot / seq. El agente crea una sesión aleatoria al iniciar y aumenta la secuencia por muestra, incluso cuando descarta historia por cola llena. Por eso un hueco puede aportar evidencia de pérdida y no se interpreta automáticamente como reinicio.
tick es tiempo lógico desde el inicio del modelo; no es UTC. El servidor agrega su propia fecha de recepción. Después de un corte, una recepción reciente puede contener una medición anterior: ninguna de esas marcas reemplaza a la otra.
Configuración deseada y configuración del registro
{"version": 2, "on": 30, "off": 27}Guardar esa propuesta cambia el estado deseado del servidor. El equipo la conocerá cuando consulte. Los registros que ya estaban en cola conservan config_version=1; no se reescriben para aparentar que la versión 2 estaba aplicada al medir.
El servidor exige que una propuesta administrativa tenga versión superior a la almacenada. El dispositivo acepta una repetición exacta de la versión aplicada como una consulta sin cambio. Son contratos diferentes: uno registra una propuesta nueva y el otro procesa consultas periódicas.
06 · Una falla de red no detiene el control
Cola y confirmación de aplicación
La cola de este proyecto contiene como máximo tres registros en RAM. El tamaño es deliberadamente pequeño para poder llenarla en pocos pasos. No cumple la reserva de seis minutos del diseño de referencia del tema 47: aquí el límite se hace visible para estudiar descarte y recuperación.
Si está llena, se descarta el registro nuevo y aumenta un contador. La lectura y la decisión local ya se atendieron. Esta política conserva historia antigua pendiente; otra aplicación podría priorizar eventos, reservar espacio o descartar lo más viejo. La elección debe ser explícita.
EQUIPO SERVIDOR
primer pendiente ─────────→ validar e insertar
confirmar transacción
←───────── acuse de boot y seq
retirar el primero
si se pierde el acuse:
conservar pendiente ───────→ reconocer la misma identidad
←───────── confirmar sin otra inserciónRecibir bytes no basta para borrar el pendiente
El servidor emite un acuse de aplicación después de completar la transacción SQLite. El agente comprueba que boot y seq coinciden con el primero de su cola. Un timeout o un acuse diferente no autorizan retirarlo.
Si el servidor ya guardó el registro pero se perdió la respuesta, el reintento usa la misma identidad y el mismo contenido. El servidor confirma el duplicado sin otra fila. Si esa identidad llega con contenido distinto, responde conflicto y no reemplaza la historia.
Vaciar el atraso
El agente produce una muestra por ciclo e intenta enviar hasta dos. Con red disponible, esa diferencia permite reducir una cola atrasada, siempre que se confirmen ambos envíos y los plazos reales lo permitan. Si producción y entrega sostenida son iguales, la cola no disminuye.
La cola local es volátil: cerrar el agente pierde pendientes. El servidor sí conserva su base entre reinicios. Para persistir en la placa hay que adaptar almacenamiento, escrituras y recuperación, manteniendo identidades estables; no basta con guardar únicamente el número de secuencia.
07 · Guardar con identidad y transacción
Servidor y persistencia
servidor.py crea proyecto.sqlite3 junto a los scripts. Una tabla guarda la configuración deseada y otra las muestras. La identidad compuesta se materializa como clave primaria y las consultas usan parámetros SQL.
CREATE TABLE muestras (
id TEXT PRIMARY KEY,
payload TEXT NOT NULL,
recibido TEXT NOT NULL
);La transacción serializa la comprobación de identidad y su inserción. Para una muestra nueva se guarda un JSON validado con su recepción UTC. Para un duplicado exacto se mantiene la fila existente y su recepción original. El acuse no implica una promesa superior a la durabilidad que ofrezcan SQLite, el sistema de archivos y el hardware bajo su configuración.
Límites que también forman parte del contrato
- Las peticiones JSON requieren
Content-Type: application/jsony un cuerpo de hasta 4096 bytes. - Se validan campos, tipos, calidad, rango, identidad de equipo y contadores.
- El servidor admite hasta 10 000 registros nuevos; si se llena, devuelve 503 y el equipo conserva pendientes hasta aplicar su política de cola.
- El panel consulta las últimas 20 entradas nuevas. No implementa paginación, gráficos ni una política de retención automática.
- Las credenciales se toman del entorno y no se incrustan en el HTML ni se escriben en los mensajes de diagnóstico.
La documentación de sqlite3 explica conexiones, consultas parametrizadas y transacciones. La base simplifica un banco local; no representa por sí misma una plataforma de gestión de flotas.
HTTP se implementa con la biblioteca estándar. La documentación de http.server señala sus limitaciones para producción. En este proyecto el servicio está restringido a loopback y sirve para observar el intercambio en una computadora.
08 · Ejecutar el recorrido real en la computadora
Puesta en marcha paso a paso
Descargá el ZIP, extraé sus archivos y abrí una terminal en esa carpeta. Los comandos siguientes usan PowerShell y el lanzador py de Windows. En otro entorno podés usar python3 y la sintaxis correspondiente para variables de entorno.
1. Preparar credenciales del banco e iniciar el servidor
$env:IOT_DEVICE_TOKEN = [guid]::NewGuid().ToString('N')
$env:IOT_ADMIN_TOKEN = [guid]::NewGuid().ToString('N')
# Anotá ambos valores para usarlos en este banco; no los publiques.
$env:IOT_DEVICE_TOKEN
$env:IOT_ADMIN_TOKEN
py servidor.pyLos tokens deben ser distintos y tener al menos 16 caracteres. El servidor escucha en http://127.0.0.1:8765. Si el puerto ya está ocupado, detené la otra instancia del banco antes de volver a iniciarlo.
2. Iniciar el agente en otra terminal
# Segunda terminal, ubicada en la misma carpeta:
$env:IOT_DEVICE_TOKEN = 'PEGAR_EL_TOKEN_DE_EQUIPO_GENERADO'
py dispositivo.py --ciclos 30 --sin-red 5Una terminal abierta por separado no hereda las variables que asignaste en la primera. Copiá el valor del token de equipo, no el de administrador. El agente realiza 30 ciclos; en los primeros 5 omite los intercambios para simular un corte inicial.
Las lecturas se repiten en este orden: 24, 29, 27, 25, inválida, 30, 27, 25. La salida sigue respondiendo durante el corte. Al llenarse la cola aparecen descartes. Cuando se habilitan intercambios, el agente intenta recuperar pendientes.
3. Abrir el panel y consultar
Abrí http://127.0.0.1:8765 en el navegador. Pegá el token de administrador y pulsá Consultar estado. El token permanece en ese campo durante la sesión y no se guarda en almacenamiento del navegador. El panel no se actualiza automáticamente: cada consulta pide una vista nueva.
4. Ejecutar pruebas reproducibles
py pruebas.pyLas pruebas crean una base temporal y un puerto de loopback libre. No usan tus tokens ni alteran proyecto.sqlite3. Comprueban el núcleo, los roles, la validación, las transacciones, los duplicados y la configuración con intercambios HTTP reales.
5. Detener y volver a iniciar
Detené el servidor con Ctrl + C. El agente termina al completar sus ciclos o con la misma combinación. La base conserva historia y configuración deseada; no distribuyas esa base como parte del código del curso.
Un agente nuevo crea otra sesión y arranca con configuración local 1. Consulta la guardada en el servidor después de su primera medición; por eso puede existir una muestra de arranque con versión 1 aunque el servidor conserve versión 2. Es una limitación deliberada de la configuración volátil, que un producto debe resolver con su política de arranque.
09 · Escribir una intención y buscar evidencia
Configurar y observar desde el panel
Durante los ciclos del agente, proponé versión 2, encender desde 30 °C y apagar hasta 27 °C. Al guardar, el servidor confirma que la propuesta quedó almacenada. Esa confirmación no demuestra todavía que el equipo la recibió.
- El panel muestra la configuración deseada del servidor.
- El agente consulta, valida el conjunto y lo convierte en configuración aplicada.
- Una medición posterior incluye la versión aplicada en ese instante.
- Cuando esa muestra se confirma y se consulta el panel, aparece como evidencia histórica de uso de la versión.
Una muestra anterior puede llegar después del cambio. Conserva su valor, salida y versión originales. El panel etiqueta sus filas como historia recibida; no pinta un equipo «sano y conectado» solamente porque acaba de ingresar una muestra encolada.
Qué significa cada demora
La demora de propuesta a consulta depende de la frecuencia de intercambio. La demora hasta una muestra nueva depende de la adquisición. La demora de esa muestra hasta el panel depende de la cola, la entrega y la actualización de la vista. Separarlas permite diagnosticar un cambio que parece no aplicarse.
Por qué puede rechazarse un cambio
Se rechaza un token incorrecto, un rol sin permiso, un umbral fuera de rango, off ≥ on o una versión que no supera la deseada en el servidor. La configuración previa sigue disponible. La interfaz presenta el error sin convertir un rechazo en éxito.
Si reiniciás el ejercicio con una base que ya contiene versión 2, proponé una versión mayor para otro cambio. Volver a ejecutar el servidor no borra automáticamente el estado deseado.
10 · Verificar lo que ocurre cuando algo falta
Pruebas de integración y fallas
| Escenario | Resultado esperado | Cómo comprobar |
|---|---|---|
| 24 → 29 → 27 → 25 °C | Apagada → encendida → encendida → apagada | Núcleo, agente y laboratorio |
| Sensor inválido | Valor ausente, calidad inválida y salida apagada del banco | Secuencia del agente o botón del laboratorio |
| Lectura sin renovar por 6 s | Dato vencido y salida apagada | Núcleo y avance manual del laboratorio |
| Red ausente | Control activo; crece cola hasta el límite | --sin-red, detener servidor o modo sin red del laboratorio |
| Cuarta muestra sin espacio | Se descarta nueva historia, no la decisión local | Cuatro muestras sin enviar |
| Acuse perdido después de guardar | Pendiente conservado y reintento sin otra fila | Modo acuse perdido del laboratorio; repetición de POST en pruebas |
| Misma identidad, otro contenido | Conflicto; historia original sin sobrescritura | Prueba de API incluida |
| Equipo intenta escribir configuración | 401 y estado deseado sin modificación | Pruebas de roles |
| Reinicio de servidor | Historia y configuración deseada conservadas | Detener y arrancar usando la misma base |
| Reinicio de agente | Nueva sesión, pendientes perdidos y configuración local inicial | Volver a ejecutar el agente y comparar sesiones |
La pérdida de un acuse se provoca de forma determinista en el laboratorio; el programa de pruebas reproduce la deduplicación enviando otra vez el mismo registro a la API real. No hace falta generar una pérdida aleatoria en la red para verificar ese contrato.
Observar sin introducir otra falla
Los contadores de secuencia, cola y descartes ayudan a explicar lo ocurrido. Los registros de diagnóstico no incluyen tokens. Para medir tiempos de una placa conviene instrumentar la adquisición y la salida: imprimir mucho por serie puede alterar los plazos que se intentan medir.
Un resultado exitoso en el banco demuestra un recorrido bajo condiciones concretas. No demuestra resistencia a cortes eléctricos durante escrituras, autonomía, rendimiento de una flota ni funcionamiento de un sensor físico.
11 · Completar las capacidades del producto
De este banco a un producto
El proyecto mantiene un alcance pequeño para poder inspeccionar cada transición. Al convertirlo en un dispositivo instalado, las decisiones de los temas anteriores vuelven a ser necesarias y deben probarse en conjunto.
| Capacidad | Trabajo pendiente para un producto |
|---|---|
| Hardware real | Sensor validado, salida física, alimentación y condición segura durante reset |
| Persistencia local | Cola y configuración recuperables, escritura coherente y tratamiento de desgaste |
| Transporte protegido | TLS con validación, manejo de reloj y límites de trabajo que preserven control |
| Identidad y flota | Aprovisionamiento por unidad, permisos por recurso, renovación y revocación |
| OTA | Imagen autorizada, particiones, aceptación, recuperación y compatibilidad de datos |
| Operación | Estado actual separado de historia, retención, alertas y diagnóstico autorizado |
| Energía | Medición de actividad y sueño, recursos del actuador y reserva de mantenimiento |
El HTTP de loopback y los tokens del ejemplo sirven para practicar contratos en la propia PC. No se presentan como protección suficiente para una red. Antes de conectar otros equipos se prepara un servicio adecuado y se aplican las políticas de identidad y cifrado de los temas 40 a 42.
Agregar OTA sin interrumpir a ciegas
El controlador primero verifica si puede entrar en mantenimiento y qué estado físico conservará durante el reinicio. La campaña identifica publicación y hardware; la placa verifica autenticidad y compatibilidad; el primer arranque informa aceptación o recuperación. Cambiar una etiqueta de versión en un JSON no implementa ese recorrido.
Entrega que se puede revisar
Al cerrar tu adaptación, entregá código, esquema de conexiones, instrucciones de alta, contratos, presupuestos, matriz de pruebas y límites conocidos. Conservá una explicación de qué se pierde al reiniciar cada componente y cómo puede recuperarse sin acceso al servicio remoto.
12 · Ejecutar la lógica sin depender de la red
Núcleo ejecutable en Python
Este bloque incluye el mismo núcleo de nucleo.py y un recorrido de comprobación. El diccionario del final representa al servidor solamente para demostrar reintentos: no reemplaza la API ni SQLite del proyecto descargable.
Podés ejecutarlo aquí con Pyodide o copiarlo a Python 3. La primera ejecución en esta página descarga el intérprete. No abre sockets, no usa credenciales y no modifica archivos.
"""Control y cola en memoria. Sin red, GPIO ni dependencias externas."""
from copy import deepcopy
from math import isfinite
def numero(valor):
return type(valor) in (int, float) and isfinite(valor)
def validar_config(config):
if not isinstance(config, dict) or set(config) != {"version", "on", "off"}:
raise ValueError("Se requieren version, on y off")
if type(config["version"]) is not int or not 1 <= config["version"] <= 1000000:
raise ValueError("Versión entera entre 1 y 1000000")
if not all(numero(config[k]) for k in ("on", "off")):
raise ValueError("Umbrales numéricos finitos")
if not -40 <= config["off"] < config["on"] <= 125:
raise ValueError("Se exige -40 <= off < on <= 125")
return deepcopy(config)
class Dispositivo:
def __init__(self, boot="demo"):
self.boot = boot
self.ahora = 0
self.config = dict(version=1, on=28, off=26)
self.valor = None
self.medida_en = None
self.salida = False
self.seq = 0
self.cola = []
self.descartes = 0
def controlar(self):
if (self.valor is None or self.medida_en is None
or self.ahora - self.medida_en > 5):
self.salida = False
elif self.valor >= self.config["on"]:
self.salida = True
elif self.valor <= self.config["off"]:
self.salida = False
def avanzar(self, segundos):
if type(segundos) is not int or not 1 <= segundos <= 3600:
raise ValueError("Avance entero entre 1 y 3600 segundos")
self.ahora += segundos
self.controlar()
def medir(self, valor):
if valor is not None and (not numero(valor) or not -40 <= valor <= 125):
raise ValueError("Lectura fuera de rango")
self.avanzar(1)
self.valor = valor
self.medida_en = self.ahora
self.controlar()
self.seq += 1
registro = dict(schema=1, device="medidor-01", boot=self.boot,
seq=self.seq, tick=self.ahora, temperature=valor,
quality="invalid" if valor is None else "valid",
output=self.salida, config_version=self.config["version"])
if len(self.cola) < 3:
self.cola.append(registro)
return True
self.descartes += 1 # Descartar nueva historia; el control ya se atendió.
return False
def configurar(self, propuesta):
config = validar_config(propuesta)
if config["version"] < self.config["version"]:
raise ValueError("Versión anterior")
if config["version"] == self.config["version"]:
if config != self.config:
raise ValueError("Misma versión con contenido diferente")
return False # Repetición exacta: no vuelve a aplicar.
self.config = config
self.controlar()
return True
def confirmar(self, boot, seq):
if self.cola and (self.cola[0]["boot"], self.cola[0]["seq"]) == (boot, seq):
self.cola.pop(0)
return True
return False
# Recorrido de integración sin sockets ni archivos.
d = Dispositivo()
for valor, esperado in [(24, False), (29, True), (27, True), (25, False)]:
guardada = d.medir(valor)
assert d.salida is esperado
print(f"t={d.ahora}: {valor} °C; salida={d.salida}; encolada={guardada}")
assert len(d.cola) == 3 and d.descartes == 1
historia = {}
primero = deepcopy(d.cola[0])
clave = (primero["boot"], primero["seq"])
historia.setdefault(clave, primero)
# Se pierde el acuse: no se llama a confirmar y el pendiente queda en RAM.
assert len(d.cola) == 3
historia.setdefault(clave, deepcopy(d.cola[0])) # Reintento del mismo registro.
assert len(historia) == 1
assert d.confirmar(*clave)
assert len(d.cola) == 2
assert not d.confirmar("otra-sesion", 2)
print("Acuse perdido y reintento: una fila única; pendiente retirado al confirmar.")
antes = deepcopy(d.__dict__)
try:
d.configurar(dict(version=2, on=25, off=30))
except ValueError:
pass
else:
raise AssertionError("Debía rechazar umbrales invertidos")
assert d.__dict__ == antes
d.medir(29)
assert d.salida
assert d.configurar(dict(version=2, on=30, off=27))
assert d.salida # 29 está dentro de la banda nueva: conserva estado.
assert not d.configurar(dict(version=2, on=30, off=27))
d.medir(27)
assert not d.salida
d.medir(31)
assert d.salida
d.avanzar(5)
assert d.salida
d.avanzar(1)
assert not d.salida
d.medir(None)
assert d.valor is None and not d.salida
print("Versiones, histéresis, límites de frescura y sensor inválido: comprobados.")
print("Fin: núcleo verificado; API y SQLite se prueban con pruebas.py.")
El núcleo se reutiliza en el agente descargable. El servidor aplica además su propia validación; no confía en que el cliente haya usado esta clase.
13 · Manipular cada etapa del recorrido
Laboratorio de punta a punta
Este banco ficticio se ejecuta por completo en la pestaña y no se comunica con el proyecto Python. Cada muestra avanza un segundo lógico; los demás cambios solo avanzan el tiempo cuando lo indica el botón. Los intentos de envío tratan un registro por pulsación.
Dispositivo medidor y controlador
Proponer configuración al dispositivo ficticio
Este botón entrega la propuesta directamente al equipo simulado: no reproduce la espera de consulta del agente HTTP. La casilla de autorización representa una decisión previa, no una autenticación real.
Preparando banco.
Cola del dispositivo
| Secuencia | Segundo de medición | Lectura | Salida en el registro | Config. al medir |
|---|
Historia del servidor ficticio
| Secuencia | Medición (s) | Recepción (s) | Lectura | Salida histórica | Config. histórica |
|---|
Registro de operaciones
| Paso | Reloj | Resultado |
|---|
Recorrido guiado
- Medí 24, 29 y 27 °C sin enviar. La salida queda apagada, encendida y encendida; la cola llega a 3.
- Medí 25 °C. La salida se apaga y la secuencia llega a 4, pero se descarta esa nueva historia. Los tres pendientes anteriores permanecen.
- Elegí «Sin red» e intentá enviar. No cambia la cola ni aparece historia en servidor.
- Elegí «Servidor recibe, se pierde el acuse» y enviá. Hay un registro único guardado, pero siguen tres pendientes. Repetí: no se crea otra fila.
- Elegí «Red y acuse disponibles» y enviá. Se reconoce el duplicado y recién entonces la cola baja a 2.
- Enviá los otros dos. El servidor conserva tres registros y el dispositivo queda sin pendientes. La lectura actual sigue siendo 25 °C; recibir historia anterior no la sobrescribe.
- Medí 29 °C con versión 1: se enciende. Aplicá versión 2 con on=30 y off=27. La lectura de 29 queda en la banda y conserva la salida encendida.
- Medí 27 °C: se apaga con versión 2. La cola conserva el registro de 29 con versión 1 y el nuevo con versión 2.
- Repetí exactamente la propuesta: no se reaplica. Cambiá un umbral sin aumentar versión: se rechaza. Desmarcá autorización: se rechaza antes de aplicar.
- Medí 31 °C y avanzá 6 s sin medir. La lectura vence y se apaga la salida. Simulá también sensor inválido y observá que no aparece un cero ficticio.
Reiniciar el banco borra dispositivo, servidor, historial y controles de esta simulación. No representa un reinicio de un único componente real: el servidor Python conserva SQLite y el agente nuevo pierde solamente su estado local volátil.
¿El envío de un registro viejo puede encender la salida actual?
No. El control usa la lectura local y su antigüedad. El servidor almacena la decisión histórica que acompañaba a la muestra, sin reejecutarla como una orden.
14 · Práctica de integración
Predecí antes de comprobar
1. Una banda con memoria
Con on=28 y off=26, la salida está encendida y llega 27 °C. ¿Se apaga? ¿Y si ya estaba apagada?
Ver solución
En la banda conserva el estado anterior. Por eso puede estar encendida o apagada a 27 °C según el recorrido previo. Esa memoria evita alternar ante pequeñas variaciones.
2. Cuarta muestra con cola llena
Hay tres pendientes, la salida está encendida y llega una lectura de 25 °C. ¿Debe esperar a que se libere espacio para apagar?
Ver solución
No. El control se atiende primero. La salida se apaga y se descarta la nueva historia por la política de cola, aumentando el contador.
3. El acuse que no llegó
El servidor guardó la secuencia 8 y la respuesta se perdió. ¿El cliente debe reenviarla como secuencia 9?
Ver solución
No. Conserva la misma identidad y el mismo contenido. Así el servidor reconoce el duplicado. Cambiar la secuencia crearía otro registro y ocultaría que era un reintento.
4. Una versión antigua después del cambio
El servidor desea versión 2 y recibe una muestra con config_version=1 que estaba en cola. ¿Prueba que el equipo rechazó la versión nueva?
Ver solución
No. Es evidencia del instante de esa medición. Hay que examinar su contexto temporal y obtener un reporte posterior a la aplicación; una muestra atrasada no es un snapshot actual.
5. Antigüedad en el límite
La última lectura válida tiene 5 s de edad y había encendido la salida. ¿Qué ocurre a los 5 y a los 6 s sin otra lectura?
Ver solución
A los 5 s inclusive todavía se admite. A los 6 s vence y la salida del banco se apaga. Esto no crea una nueva medición.
6. Reiniciar cada componente
¿Qué conserva el servidor al reiniciar y qué pierde el agente descargable?
Ver solución
El servidor conserva historia y configuración deseada en SQLite. El agente pierde cola, configuración aplicada, lectura y salida; crea otra sesión y recupera la configuración deseada cuando logra consultarla.
7. Identidad reutilizada
Se intenta enviar otra temperatura con la misma combinación device/boot/seq de una muestra ya guardada. ¿Conviene sobrescribirla?
Ver solución
No. El servidor responde conflicto y conserva la historia original. Una identidad de registro debe describir el mismo evento en cada reintento.
8. Del modelo a una placa
¿Alcanza con copiar la histéresis al ESP32 para considerar terminado el dispositivo conectado?
Ver solución
No. Faltan adaptación y validación del sensor, salida física, reloj, planificación de red, persistencia, identidad, TLS, mantenimiento y pruebas de fallas. La lógica reusable es una parte del sistema.
15 · Cierre del curso
Lo que ya podés explicar y construir
- Transformar una lectura en una decisión local sin depender de una respuesta remota.
- Representar calidad, antigüedad y configuración aplicada sin inventar evidencia.
- Conservar pendientes dentro de límites y definir qué se pierde cuando se llenan.
- Separar entrega de bytes, transacción y confirmación de aplicación.
- Reintentar con identidad estable para tratar duplicados sin multiplicar efectos.
- Proponer cambios autorizados y distinguir intención, aplicación y observación posterior.
- Relacionar hardware, tiempos, recursos, consumo, seguridad y mantenimiento.
- Verificar el recorrido con fallas reproducibles y declarar lo que las pruebas todavía no demuestran.
Desafío final
Adaptá el proyecto a una magnitud y una salida propias. Definí condición segura, cadencias y latencia; calculá cola y recursos; implementá la adaptación física y el transporte protegido; y repetí una matriz de pruebas con cortes, datos inválidos y recuperación. Entregá evidencias y una lista concreta de limitaciones.
Con este proyecto termina Internet de las Cosas y Electrónica Conectada para Programadores. La ruta continúa con Robótica, donde las decisiones locales, la observación y la coordinación de varios subsistemas vuelven a encontrarse en un sistema físico.