← Índice del curso

Internet de las Cosas · Tema 48

Proyecto integrador

Vamos a completar el recorrido: una lectura produce una decisión local, queda pendiente de entrega, llega a una base de datos y aparece en un panel. Desde ese panel cambiaremos la configuración y observaremos cuándo el equipo la aplica. También vamos a cortar la red y perder confirmaciones para comprobar qué conserva cada parte del sistema.

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.

Entregables y alcance de cada uno.
ParteQué permite comprobar
Proyecto Python descargableIntercambios HTTP reales, permisos separados, persistencia SQLite y panel web
Núcleo ejecutable de esta páginaHistéresis, vencimiento, cola y configuración sin servidor
Laboratorio interactivoExplorar manualmente cortes, acuses perdidos y versiones en una simulación
Propuesta de banco físicoIdentificar 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.

Adaptación propuesta para un ESP32 clásico; verificar siempre el pinout de la placa.
FunciónBanco propuestoQué validar
Entrada de pruebaCursor de potenciómetro de 10 kΩ a GPIO34, extremos a 3,3 V y GNDRango eléctrico, atenuación, saturación y lectura del ADC
Salida visibleGPIO25 → resistencia de 1 kΩ → ánodo de LED; cátodo a GNDPolaridad, corriente y salida apagada al iniciar
ComunicaciónAdaptador Wi-Fi y cliente de aplicaciónPlazos, identidad, validación del servidor y reconexión
DiagnósticoSalida serie y contadoresEvitar 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
Archivos incluidos en la descarga.
ArchivoResponsabilidad
nucleo.pyReglas locales, validez temporal, configuración y cola; sin red ni GPIO
dispositivo.pySecuencia sintética de lecturas, consultas y envíos HTTP con timeout
servidor.pyAPI, autorización por rol, validación y transacciones SQLite
panel.htmlConsulta de historia y escritura de configuración deseada
pruebas.pyPruebas del núcleo y de la API con una base temporal
README.mdPuesta 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

  1. Producir una muestra y evaluar la salida con la configuración ya aplicada.
  2. Intentar guardar el registro en la cola acotada.
  3. Si corresponde comunicarse, consultar configuración deseada y validarla.
  4. Enviar como máximo dos registros, retirando cada uno solo con su acuse coincidente.
  5. 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.

Recorrido de control sin depender del servidor.
EntradaSalida resultanteMotivo
24 °CApagadaEstá por debajo del umbral de apagado
29 °CEncendidaSupera el umbral de encendido
27 °CEncendidaEn la banda se conserva el estado
25 °CApagadaCruza el umbral de apagado
Lectura inválidaApagadaPolí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.

Endpoints del banco local.
OperaciónCredencialResultado
GET /Sin tokenInterfaz del panel, sin datos privados ni credenciales embebidas
GET /api/configEquipoConfiguración deseada guardada en servidor
POST /api/muestrasEquipoRegistro validado y acuse posterior a la transacción
GET /api/estadoAdministradorConfiguración deseada, cantidad de registros y últimas 20 entradas nuevas
POST /api/configAdministradorNueva 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ón

Recibir 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/json y 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.py

Los 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 5

Una 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.py

Las 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ó.

  1. El panel muestra la configuración deseada del servidor.
  2. El agente consulta, valida el conjunto y lo convierte en configuración aplicada.
  3. Una medición posterior incluye la versión aplicada en ese instante.
  4. 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

Plan de aceptación del proyecto y dónde reproducir cada caso.
EscenarioResultado esperadoCómo comprobar
24 → 29 → 27 → 25 °CApagada → encendida → encendida → apagadaNúcleo, agente y laboratorio
Sensor inválidoValor ausente, calidad inválida y salida apagada del bancoSecuencia del agente o botón del laboratorio
Lectura sin renovar por 6 sDato vencido y salida apagadaNúcleo y avance manual del laboratorio
Red ausenteControl activo; crece cola hasta el límite--sin-red, detener servidor o modo sin red del laboratorio
Cuarta muestra sin espacioSe descarta nueva historia, no la decisión localCuatro muestras sin enviar
Acuse perdido después de guardarPendiente conservado y reintento sin otra filaModo acuse perdido del laboratorio; repetición de POST en pruebas
Misma identidad, otro contenidoConflicto; historia original sin sobrescrituraPrueba de API incluida
Equipo intenta escribir configuración401 y estado deseado sin modificaciónPruebas de roles
Reinicio de servidorHistoria y configuración deseada conservadasDetener y arrancar usando la misma base
Reinicio de agenteNueva sesión, pendientes perdidos y configuración local inicialVolver 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.

Extensiones y evidencia necesaria.
CapacidadTrabajo pendiente para un producto
Hardware realSensor validado, salida física, alimentación y condición segura durante reset
Persistencia localCola y configuración recuperables, escritura coherente y tratamiento de desgaste
Transporte protegidoTLS con validación, manejo de reloj y límites de trabajo que preserven control
Identidad y flotaAprovisionamiento por unidad, permisos por recurso, renovación y revocación
OTAImagen autorizada, particiones, aceptación, recuperación y compatibilidad de datos
OperaciónEstado actual separado de historia, retención, alertas y diagnóstico autorizado
EnergíaMedició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

La luz representa la salida lógica; su estado también se informa como texto.
Reloj lógico—
Último valor local—
Calidad y antigüedad local—
Salida lógica—
Configuración aplicada—
Pendientes en RAM—
Historia descartada por falta de espacio—
Registros únicos en servidor ficticio—

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

Snapshots inmutables del instante de medición, en orden de envío.
SecuenciaSegundo de mediciónLecturaSalida en el registroConfig. al medir

Historia del servidor ficticio

Últimos 10 registros nuevos ingresados. Una recepción de historia no cambia el valor ni la salida actuales del dispositivo.
SecuenciaMedición (s)Recepción (s)LecturaSalida históricaConfig. histórica

Registro de operaciones

Últimas 12 operaciones del banco.
PasoRelojResultado

Recorrido guiado

  1. Medí 24, 29 y 27 °C sin enviar. La salida queda apagada, encendida y encendida; la cola llega a 3.
  2. Medí 25 °C. La salida se apaga y la secuencia llega a 4, pero se descarta esa nueva historia. Los tres pendientes anteriores permanecen.
  3. Elegí «Sin red» e intentá enviar. No cambia la cola ni aparece historia en servidor.
  4. Elegí «Servidor recibe, se pierde el acuse» y enviá. Hay un registro único guardado, pero siguen tres pendientes. Repetí: no se crea otra fila.
  5. Elegí «Red y acuse disponibles» y enviá. Se reconoce el duplicado y recién entonces la cola baja a 2.
  6. 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.
  7. 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.
  8. 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.
  9. Repetí exactamente la propuesta: no se reaplica. Cambiá un umbral sin aumentar versión: se rechaza. Desmarcá autorización: se rechaza antes de aplicar.
  10. 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.