← Índice del curso

Internet de las Cosas · Tema 37

Control remoto de dispositivos

Hasta ahora observábamos: temperatura, alarmas y diagnóstico. Al agregar un botón de encendido, el sistema también puede cambiar el proceso. Entre el clic y el actuador hay permisos, red, colas, reglas locales y una respuesta que puede perderse. Vamos a diseñar ese recorrido para que una orden tenga significado y límites, incluso cuando la comunicación falla.

01 · Nuevo bloque

De observar a actuar

En el tema 36 el panel mostraba información y permitía reconocer una alarma en una simulación. Ahora comenzamos el bloque de control remoto y seguridad: el usuario puede solicitar un cambio en una salida o en los parámetros del dispositivo.

Una orden remota es una solicitud que el equipo evalúa, no un permiso para escribir cualquier pin desde Internet. El dispositivo conserva su lógica local, sus modos y sus límites. La nube expresa qué se pide; el firmware decide si puede aceptarlo en las condiciones actuales.

Observar

«El ventilador está apagado». Es información con una fuente y una antigüedad; recibirla no cambia por sí solo la salida.

Solicitar una acción

«Encender el ventilador durante diez segundos». Requiere un destinatario, permisos, condiciones de aplicación y una forma de saber qué ocurrió.

El caso guía es una salida auxiliar representada por un LED. El simulador agrega una realimentación ideal o fallida para distinguir salida eléctrica y efecto observado. Los tiempos son didácticos: no constituyen un controlador completo de refrigeración, bomba o motor.

En el tema 38 profundizamos el protocolo de comandos, estados, idempotencia y confirmaciones. Aquí construimos la base de operación: quién puede mandar, cuándo corresponde actuar y qué límites mantiene el dispositivo.

02 · Del clic al proceso

El recorrido de una orden

Un botón del panel no debería publicar directamente una instrucción ilimitada. Un servicio recibe la solicitud, comprueba identidad y alcance, registra la intención y la entrega por el canal del dispositivo. El firmware vuelve a validar lo que depende de su estado local.

Recorrido del control remoto y retorno del resultado El usuario solicita una acción al servicio, que autoriza y registra. HTTP o MQTT entrega la orden al dispositivo. El firmware comprueba modo, caducidad y bloqueos, luego maneja una salida. Un sensor de realimentación informa el efecto. El resultado y el estado vuelven al servicio y al panel. PanelSolicita ServicioAutoriza y registra DispositivoValida modo y límites SalidaActuador / LED Realimentación y resultadoLo aplicado y lo observado se reportan por separado Canal de entregaRecibir la solicitud no demuestra que se ejecutó.
  1. Solicitar: el usuario elige una operación y el panel indica que está pendiente.
  2. Autorizar: el servicio comprueba que ese usuario puede actuar sobre ese dispositivo y operación.
  3. Entregar: el canal intenta llevar la orden, con su identidad y vigencia.
  4. Admitir: el equipo revisa esquema, destinatario, caducidad, modo y condiciones locales.
  5. Aplicar y observar: se modifica la salida y se verifica la realimentación disponible.
  6. Informar: el servicio y el panel reciben un resultado o mantienen la incertidumbre si no llega.

El servicio puede aceptar registrar una solicitud aunque el equipo esté desconectado. Eso no significa que sea apropiado ejecutarla horas después. La validez de una orden y la política de cola se definen antes de permitir ese comportamiento.

Permisos con alcance

Identificar a un usuario no implica autorizar todas las acciones. Puede tener permiso de lectura, de encender una salida por un tiempo limitado o de cambiar ciertos parámetros. El servicio verifica dispositivo, sitio y operación; el firmware conserva sus propias restricciones aunque el emisor esté autorizado.

Para reconstruir lo ocurrido conviene registrar quién solicitó, qué pidió, cuándo, con qué identidad, cómo fue entregado y qué resultado se obtuvo. Una etiqueta «error» sin etapa ni causa no permite saber si fallaron permisos, red, admisión o ejecución.

03 · Operaciones con significado

Encender, apagar y configurar

El contrato debería nombrar acciones del producto, como set_output o set_threshold. Exponer «escribir GPIO 4» desde una interfaz pública acopla al usuario con la placa y permite esquivar restricciones que pertenecen al equipo.

Operaciones distintas, con resultados y validaciones propios.
OperaciónQué pideQué comprobar
EncenderLlevar una salida a ON durante un plazo acotadoModo, interbloqueos, duración y efecto observado
ApagarLlevar una salida a OFFPermisos, modo y estado resultante
Cambiar parámetroAplicar un umbral, período o consignaTipo, unidad, rango, coherencia y versión aplicada
Cambiar modoModificar quién tiene autoridad para decidirPermiso específico y transición local definida

Pedir un valor explícito

set_output(true) expresa el estado solicitado. toggle expresa «invertir lo que haya», de modo que repetirlo puede deshacer la primera acción. Las operaciones explícitas facilitan razonar sobre reintentos; aun así, necesitan identidad y una política de ejecución, que desarrollamos en el tema siguiente.

Repetir «encender diez segundos» también puede tener efectos diferentes si cada recepción reinicia el temporizador. El dispositivo debe distinguir un reintento de la misma orden de una nueva solicitud que pretende extender el plazo. Cambiar el nombre de la operación no resuelve ese detalle.

Parámetro solicitado y aplicado

Para una regla de temperatura alta, no alcanza con comprobar que el nuevo umbral sea un número. Debe quedar por encima del rearme y dentro del rango admitido. El ejemplo parte de alta = 8,0 °C y rearme = 7,5 °C; acepta altas mayores que 7,5 y de hasta 10,0 °C.

La configuración se valida completa y se aplica como una unidad entre evaluaciones, como vimos en el tema 32. El dispositivo informa la versión que realmente está usando. El panel no presenta la versión solicitada como aplicada solo porque el servidor guardó el pedido.

Guardar una configuración aceptada en memoria no volátil es otra etapa. Si el producto promete conservarla al reiniciar, comprueba esa escritura y define cómo se recupera una versión válida. El simulador mantiene el cambio en memoria y no modela cortes de energía.

04 · Quién tiene el mando

Modos y prioridades locales

Una misma salida puede recibir propuestas del control automático, de un operador local y de la nube. Si cada callback escribe el pin por su cuenta, el resultado depende de quién corrió último. Una única parte del firmware debe resolver la autoridad y aplicar la salida.

Política de modos del ejemplo. Otros equipos pueden necesitar más estados.
ModoQuién decide la salidaÓrdenes remotas del ejemplo
AutomáticoControlador local según sensores y reglasSe rechazan cambios directos de salida
RemotoÓrdenes admitidas, sujetas a restricciones localesSe evalúan salida y parámetro
LocalInterfaz o selector del equipoSe rechazan las operaciones del canal remoto

En este modelo, el canal de órdenes solo admite las operaciones mostradas en modo remoto. Es una política explícita del ejercicio: un producto podría permitir cambiar ciertos parámetros en automático sin habilitar el control directo de salidas.

Los bloqueos limitan la activación

Un interbloqueo puede impedir encender porque falta una condición local. El ejemplo tiene un bloqueo booleano que inhibe ON, pero permite OFF cuando el modo remoto está habilitado. No se modifica ese bloqueo al recibir una orden: se informa el rechazo y su causa.

En un sistema real puede ser una tapa abierta, falta de nivel, temperatura fuera de rango o un diagnóstico de sensor. Las condiciones que deben cumplirse para activar y las que obligan a desactivar se diseñan por separado; no se deducen del permiso del usuario.

Una intervención local puede terminar la acción

El simulador permite que un selector local tome el mando en t = 8 s y apague la salida. Si la orden llegó antes, la acción puede haber sido aplicada y luego interrumpida. Si llega después, se rechaza por modo. Ambas situaciones deben explicarse sin reescribir la historia de la solicitud.

La pérdida de Internet tampoco define por sí sola qué modo elegir. Un termostato puede seguir automático; una salida temporizada puede terminar su plazo; otra aplicación puede requerir desactivación inmediata. La política se establece según el proceso, no como una consecuencia accidental de la biblioteca de red.

05 · La orden como mensaje

Qué debe decir una orden

Un contrato pequeño pero explícito puede incluir versión, identidad, destinatario, operación, parámetros y fecha límite de admisión:

{
  "v": 1,
  "id": "cmd-1",
  "device": "camara07",
  "op": "set_output",
  "args": {"on": true, "duration_ms": 10000},
  "issued_at_ms": 1767225600000,
  "expires_at_ms": 1767225615000
}

La orden se crea en T0, puede admitirse antes de T0 + 15 s y solicita un encendido de diez segundos desde su aplicación. La fecha límite no dice cuánto debe durar la salida; la duración tampoco impide admitir una orden vieja si no se revisa su vigencia.

En producción, id debe ser único dentro del alcance de identificación elegido y conservarse en los reintentos. cmd-1 es un nombre de una ejecución aislada del laboratorio, no una estrategia de generación de identidades para toda una flota.

Validaciones antes de aplicar

  1. Origen y destinatario: emisor permitido y equipo correcto, según el canal autenticado y las reglas del servicio.
  2. Esquema: versión conocida, campos obligatorios, tipos y límites de tamaño.
  3. Operación: nombre permitido y parámetros con unidad y rango definidos.
  4. Vigencia: que no haya vencido al admitirla y que el reloj permita comprobarlo.
  5. Estado local: modo, interbloqueos y recursos disponibles.
  6. Identidad: reconocer si se trata de una orden nueva o una entrega repetida.

Un reloj Unix sin sincronizar no permite verificar una fecha de caducidad absoluta de forma confiable. El producto necesita una política para ese estado, por ejemplo no admitir nuevas activaciones remotas hasta recuperar una referencia válida. No basta con comparar dos números que usan orígenes distintos.

La duración de una acción ya admitida se mide con un reloj monótono local. Un ajuste de hora por red no debería extender un encendido que debía durar diez segundos. Después de un reinicio, no se restaura ciegamente un temporizador con marcas del arranque anterior.

Cancelar la espera no cancela la ejecución

Si el servicio deja de esperar un resultado, la orden puede seguir en tránsito o ya estar ejecutándose. Para cancelarla hace falta una operación y un resultado propios, y puede existir una carrera con la ejecución. Cerrar el diálogo del navegador no borra por sí solo una acción del dispositivo.

06 · Dos formas de transportar la intención

HTTP y MQTT para controlar

Ambos transportes pueden llevar órdenes; la elección no elimina validaciones ni límites. El tema 30 comparó su modelo de comunicación. Ahora los ubicamos en el recorrido de control.

HTTP: una API de solicitudes

POST /api/dispositivos/camara07/ordenes
Content-Type: application/json

{"id":"cmd-1","op":"set_output","args":{"on":true,"duration_ms":10000}}

HTTP/1.1 202 Accepted
Content-Type: application/json

{"id":"cmd-1","status":"pending","status_url":"/api/ordenes/cmd-1"}

La respuesta 202 Accepted indica aceptación para procesamiento, no ejecución completada, según RFC 9110, sección 15.3.3. La API del ejemplo devuelve una referencia para consultar el resultado. El panel conserva «pendiente» y muestra la respuesta de aplicación cuando llega.

El dispositivo puede consultar órdenes pendientes desde el servidor o mantener otro canal por el que se le entregan. Un equipo detrás de NAT no queda accesible desde Internet solo por tener una IP privada. El panel puede hablar con la API mientras el equipo inicia su propia conexión de salida.

MQTT: órdenes, resultados y estado en canales separados

sitio/camara07/cmd/salida     → solicitud al dispositivo
sitio/camara07/result        → resultado con la identidad de la orden
sitio/camara07/state/salida  → estado reportado y su tiempo de observación

El broker controla qué identidad puede publicar o suscribirse a esos canales. Un permiso de lectura de telemetría no debería incluir publicación de órdenes. El dispositivo también verifica esquema y condiciones locales; el tópico no reemplaza esos controles.

Para una orden puntual de encendido no se usa un mensaje retenido como si fuera un botón: un nuevo suscriptor podría recibirlo después. Un estado deseado persistente es otro modelo y necesita reconciliación, versión y vigencia; lo desarrollamos con comandos y gemelo digital en los temas siguientes.

MQTT 5 define caducidad del mensaje, útil para limitar su permanencia en el transporte. El firmware aún necesita revisar la vigencia de la acción al admitirla. Ni un PUBACK ni la entrega con QoS prueban que el actuador produjo su efecto. Estas propiedades pertenecen al transporte descrito por la especificación MQTT 5 de OASIS.

Las bibliotecas y versiones del cliente pueden ofrecer funciones distintas. El contrato de aplicación no debe depender de una propiedad de MQTT 5 si el firmware solo implementa MQTT 3.1.1; en ese caso se define el vencimiento en la carga y la política correspondiente en los extremos.

07 · Tres tiempos distintos

Caducidad, duración y pérdida de red

El control remoto tiene varios temporizadores con propósitos diferentes. En el simulador se fijan así:

Relojes de una solicitud creada en t = 0 s.
TiempoReglaQué no significa
Espera del servicio: 6 sSin resultado antes de 6 s, se declara espera agotadaNo demuestra que la salida siga apagada
Caducidad de la orden: configurableSe admite solo si llegada < TTL desde su creaciónNo limita por sí sola cuánto dura una ejecución admitida
Duración solicitada de ONSe mide desde que el dispositivo activa la salidaNo renueva la vigencia de una orden que llegó vieja
Máximo local: 10 sSe rechazan duraciones mayores; el equipo apaga al terminar el plazo aceptadoNo necesita que la nube mande una segunda orden

Con demora de entrega de 3 s y duración de 10 s, el equipo activa en t = 3 y apaga en t = 13 s. La realimentación normal se comprueba en t = 4. Si se corta la red en t = 8, el plazo local sigue corriendo y la salida se apaga igualmente a los 13 s.

Con demora de 12 s y caducidad de 5 s, la orden llega vencida y no activa. Con caducidad de 15 s, la misma demora todavía permite admitirla; el servicio ya agotó su espera de 6 s y recibe después un resultado tardío. Timeout y vencimiento no son sinónimos.

Que un encendido no dependa de un apagado futuro por Internet

Una acción acotada puede terminar localmente aunque no llegue otra comunicación. El temporizador se ejecuta en la misma lógica que resuelve modos y bloqueos, sin esperar al servidor. Un apagado remoto necesita que funcionen su canal y su admisión; para detener algo sin esa dependencia se necesita un mecanismo local apropiado al equipo.

Si la aplicación permite operación continua, debe definir cómo se mantiene esa autorización, qué pasa al perderla y qué límites siguen activos. No se obtiene continuidad simplemente eliminando todos los temporizadores.

Arranque e interrupciones

El estado inicial se define explícitamente. El ejemplo arranca con salida apagada y requiere una orden nueva para activar. Restaurar «último ON» desde flash podría reactivar sin una intención vigente. El hardware y el firmware deben ser coherentes sobre los niveles iniciales y las etapas de habilitación.

Un temporizador en el bucle también presupone que el bucle sigue ejecutándose. Su peor demora depende del período de revisión y de los bloqueos del programa. Si el requisito exige un límite más estricto, se diseña y verifica otro mecanismo; una resta con millis() no corrige por sí sola un firmware detenido.

08 · Qué significa «hecho»

Salida aplicada y efecto observado

Escribir un nivel lógico en un pin comprueba una decisión de software. No demuestra que una válvula se movió ni que un motor giró. La realimentación puede provenir de un contacto auxiliar, un sensor de posición, corriente, velocidad o alguna medida adecuada al proceso.

Salida aplicada

El controlador estableció ON en su salida. Se puede reportar como una etapa de ejecución, indicando qué representa.

Efecto observado

La entrada de realimentación confirmó el resultado esperado. Si esa información no existe, el sistema conserva ese límite de conocimiento.

En el simulador, la realimentación normal sigue instantáneamente la salida y se revisa un segundo después. La variante de falla permanece en OFF. Si pasan dos segundos sin confirmación, el controlador apaga su salida y declara fallo. Es una política pequeña para ver el recorrido, no un modelo físico de todo actuador.

Con entrega en t = 3 s y realimentación fallida, la salida se activa en t = 3, no se confirma y se desactiva en t = 5 s. El servicio recibe un fallo si el canal de respuesta funciona. No se presenta la sola escritura del pin como un éxito físico.

Resultado de ejecución y estado actual

«Encendido confirmado en t = 4» es un resultado histórico de una solicitud. La salida puede apagarse a los 13 s por su límite local o antes por intervención local. La pantalla necesita también el estado actual reportado con su antigüedad, como en el tema 36.

En el laboratorio se ve el estado interno del dispositivo porque todo ocurre en la misma simulación. En un panel real, perder la respuesta no habilita a mostrar ese estado como conocido: se necesita un reporte recibido. Esta separación permite observar aquí qué ocurrió realmente y qué pudo saber el servicio.

La salida requiere una interfaz eléctrica adecuada

El fragmento C++ usa un LED como indicador. Un actuador real necesita la etapa de manejo, alimentación y protección que corresponda, como estudiamos en el tema 11. El nombre «encender» no convierte un GPIO en una salida de potencia ni verifica su circuito.

09 · Un único dueño de la salida

Un núcleo de control en C++

El siguiente núcleo para Arduino/ESP32 conserva modo, interbloqueo y duración local. Se llama después de autenticar, validar destinatario y caducidad, y reconocer la identidad de la orden. No incluye el cliente HTTP/MQTT ni el parser.

#include <Arduino.h>
#include <stdint.h>

enum class Modo { Automatico, Remoto, Local };

class ControlSalida {
 public:
  explicit ControlSalida(uint8_t pin) : pin_(pin) {}

  void comenzar() {
    pinMode(pin_, OUTPUT);
    apagar();
  }

  void cambiarModo(Modo nuevo) {
    if (nuevo != modo_) apagar(); // política de transición del ejemplo
    modo_ = nuevo;
  }

  // Solo para una orden NUEVA ya admitida por el protocolo de aplicación.
  bool solicitar(bool on, uint32_t duracionMs, uint32_t ahora,
                 bool habilitacionLocal) {
    if (modo_ != Modo::Remoto) return false;
    if (!on) {
      apagar();
      return true;
    }
    if (!habilitacionLocal || duracionMs == 0 || duracionMs > MAX_ON_MS) {
      return false;
    }
    inicio_ = ahora;
    duracion_ = duracionMs;
    activa_ = true;
    digitalWrite(pin_, HIGH);
    return true; // salida aplicada; no confirma un actuador físico
  }

  void revisar(uint32_t ahora, bool habilitacionLocal) {
    if (!activa_) return;
    if (!habilitacionLocal || uint32_t(ahora - inicio_) >= duracion_) {
      apagar();
    }
  }

  bool salidaActiva() const { return activa_; }

 private:
  void apagar() {
    digitalWrite(pin_, LOW);
    activa_ = false;
  }
  static constexpr uint32_t MAX_ON_MS = 10000;
  uint8_t pin_;
  Modo modo_ = Modo::Local;
  bool activa_ = false;
  uint32_t inicio_ = 0, duracion_ = 0;
};

// Elegir un GPIO de salida permitido por la placa y un LED con su circuito.
ControlSalida indicador(4);

void setup() {
  indicador.comenzar();
  // El modo inicial es local. Su habilitación remota se define por el producto.
}

void loop() {
  const uint32_t ahora = uint32_t(millis());
  const bool habilitacionLocal = true; // reemplazar por interbloqueos reales
  indicador.revisar(ahora, habilitacionLocal);
  // Procesar red y órdenes sin bloquear; aplicar solo solicitudes nuevas.
}

El GPIO 4 es una elección de ejemplo, no una afirmación de que el LED integrado de cualquier placa use ese pin. Revisá las restricciones de tu variante. La documentación GPIO de Arduino-ESP32 distingue configurar la salida con pinMode y escribir su nivel con digitalWrite.

El ejemplo arranca en local y no acepta encendidos remotos hasta que el producto habilite ese modo. Todos los cambios de pin de esta salida pasan por el mismo controlador. El bucle revisa el límite aunque no haya red; una tarea de realimentación puede apagar y reportar una falla con una política adicional.

La resta de contadores de 32 bits tolera el retorno a cero de millis() para estos intervalos cortos, siempre que el controlador se revise regularmente y no se pierda un ciclo completo del contador. Arduino documenta su contador de tiempo en la referencia de millis(). No se compara con una fecha Unix ni se usa para reconstruir un plazo después de reiniciar.

Una salida apagada por software presupone que la etapa eléctrica responde y que el firmware se ejecuta. Los estados durante el arranque y el comportamiento ante una falla del hardware se verifican en el conjunto real; no se garantizan únicamente con el valor inicial de una variable.

10 · Un recorrido reproducible

Simular el recorrido en Python

El modelo usa un reloj común en segundos y recorre t = 0 a 40. El servicio registra una orden en t = 0, deja de esperar a los 6 s y el dispositivo aplica sus propias validaciones. La red puede estar disponible, volver a los 20 s o cortarse entre los 8 y los 20 s.

La respuesta se intenta una sola vez: si se pierde, el servicio conserva la incertidumbre. No hay reintentos de orden ni de resultado en este modelo; así podemos estudiar primero los límites y la diferencia entre ejecución y conocimiento. Esos reintentos se diseñan en el tema siguiente.

Python en el navegador. El bloque es independiente y no controla equipos reales. Podés editarlo y ejecutarlo con Pyodide o copiarlo a un archivo Python 3 local.

def simular(operacion="on", modo="remoto", permiso=True, bloqueo=False,
            red="estable", demora=3, ttl=15, duracion=10, umbral=90,
            realimentacion="normal", perderRespuesta=False, prioridadLocal=False):
    modo_actual, salida, fisico, hasta = modo, False, False, None
    entregada, verificar, respuesta, respuesta_intentada = False, None, None, False
    nube = "Pendiente" if permiso else "Rechazada por el servicio"
    resultado_en, dispositivo, cfg, alto = None, "Sin orden recibida", 1, 80
    filas = []
    for t in range(41):
        online = (red == "estable" or
                  (t >= 20 if red == "reconexion" else t < 8 or t >= 20))
        acciones = []
        if t == 0:
            acciones.append("Servicio registra la solicitud cmd-1" if permiso else
                            "Servicio rechaza: emisor sin permiso")
        if prioridadLocal and t == 8:
            modo_actual, salida, hasta = "local", False, None
            dispositivo = "Apagado por intervención local"
            acciones.append("Selector local toma el mando y apaga")
            if verificar is not None:
                respuesta, verificar = "Interrumpida por intervención local", None
        if salida and hasta is not None and t >= hasta:
            salida, hasta, dispositivo = False, None, "Apagado por límite local"
            acciones.append("Vence el tiempo de encendido: apagar sin consultar a la nube")
            if verificar is not None:
                respuesta, verificar = "Interrumpida por límite local", None
        if permiso and not entregada and online and t >= demora:
            entregada = True
            acciones.append("La orden llega al dispositivo")
            if t >= ttl:
                respuesta = "Rechazada: orden vencida"
            elif modo_actual != "remoto":
                respuesta = "Rechazada: modo no remoto"
            elif operacion == "on" and bloqueo:
                respuesta = "Rechazada: bloqueo local"
            elif operacion == "on" and not 1 <= duracion <= 10:
                respuesta = "Rechazada: duración fuera de rango"
            elif operacion == "config" and not 75 < umbral <= 100:
                respuesta = "Rechazada: umbral fuera de rango"
            elif operacion == "config":
                alto, cfg, respuesta = umbral, cfg + 1, "Confirmado: parámetro aplicado"
            elif operacion == "off":
                salida, hasta, respuesta = False, None, "Confirmado: apagado"
            else:
                salida, hasta, verificar = True, t + duracion, t
                dispositivo = "Salida activada · verificando"
                acciones.append(f"Activar salida hasta t = {hasta} s como máximo")
            if respuesta is not None:
                dispositivo = respuesta
        fisico = salida if realimentacion == "normal" else False
        if verificar is not None:
            if t >= verificar + 1 and fisico:
                dispositivo, respuesta, verificar = "Encendido verificado", "Confirmado: encendido", None
                acciones.append("La realimentación confirma encendido")
            elif t >= verificar + 2:
                salida, fisico, hasta, verificar = False, False, None, None
                dispositivo, respuesta = "Falla de realimentación · salida apagada", "Fallo: sin realimentación"
                acciones.append("No llegó realimentación: apagar y declarar fallo")
        if respuesta is not None and not respuesta_intentada:
            respuesta_intentada = True
            acciones.append(f"Resultado local: {respuesta}")
            if online and not perderRespuesta:
                nube = f"Respuesta tardía · {respuesta}" if t >= 6 else respuesta
                resultado_en = t
                acciones.append("El servicio recibe el resultado")
            else:
                acciones.append("La respuesta no llega al servicio")
        if t >= 6 and nube == "Pendiente":
            nube = "Tiempo de espera agotado"
            acciones.append("El servicio deja de esperar: el resultado sigue siendo incierto")
        filas.append(dict(t=t, online=online, modo=modo_actual, salida=salida,
                          fisico=fisico, hasta=hasta, entregada=entregada, nube=nube,
                          resultadoEn=resultado_en, dispositivo=dispositivo,
                          cfg=cfg, alto=alto, acciones=acciones))
    return filas


base = simular()
for f in base:
    if f["acciones"]:
        print(f't={f["t"]:2} s | salida={f["salida"]} | {". ".join(f["acciones"])}')
print("Resultado histórico:", base[-1]["nube"])
print("Estado final de salida:", base[-1]["salida"])

assert not base[2]["salida"] and base[3]["salida"]
assert base[4]["nube"] == "Confirmado: encendido"
assert base[12]["salida"] and not base[13]["salida"]
assert not any(f["salida"] for f in simular(permiso=False))
assert "modo no remoto" in simular(modo="local")[3]["nube"]
assert "bloqueo local" in simular(bloqueo=True)[3]["nube"]
assert "orden vencida" in simular(demora=12, ttl=5)[12]["nube"]
assert simular(perderRespuesta=True)[6]["nube"] == "Tiempo de espera agotado"
assert not simular(realimentacion="falla")[5]["salida"]
assert not simular(red="corte")[13]["salida"]
assert simular(prioridadLocal=True)[8]["modo"] == "local"
assert simular(operacion="config", umbral=90)[3]["alto"] == 90
assert simular(operacion="config", umbral=70)[3]["cfg"] == 1
assert not any(f["salida"] for f in simular(duracion=20))
assert "orden vencida" in simular(demora=3, ttl=3)[3]["nube"]
print("\n15 comprobaciones superadas.")

Resultado inicial: salida ON en t = 3 s, realimentación y resultado en t = 4 s, salida OFF en t = 13 s por límite local. El resultado histórico continúa indicando que aquel encendido fue confirmado. Las quince comprobaciones terminan sin errores.

Alcance del modelo

Se simula una solicitud aislada, con esquema y tipos ya validados y reloj común confiable. Los modos automático y local no ejecutan aquí otro proceso: mantienen apagado y rechazan estas órdenes remotas. La falla física modelada es falta de encendido; no representa todas las averías posibles ni una garantía de que un actuador real podrá apagarse.

Los registros muestran lo que sabe el simulador tanto del dispositivo como del servicio. En un despliegue, la interfaz solo conoce el estado que efectivamente recibió. No debe rellenar ese conocimiento mirando la acción solicitada ni la animación de un botón.

11 · Exploración interactiva

Laboratorio de control remoto

«Preparar simulación» registra una nueva ejecución aislada de cmd-1 en t = 0. Avanzá segundo a segundo o completá los cuarenta segundos. Los controles cambian únicamente el modelo de esta página: no se envían órdenes a una red ni a un dispositivo real.

Solicitud y ejecución de una salida auxiliar

Preparando configuración.

Tiempo simulado0 s
Salida eléctrica del modeloApagada
Realimentación simuladaApagado observado
Modo actual del dispositivo—
Plazo local vigente—
Configuración aplicada en memoria—
Enlace en este instante—

Estado interno del dispositivo simulado

Sin orden recibida

Resultado de la solicitud visto por el servicio

Pendiente

Sin resultado recibido

Preparando recorrido.

Solo se muestran instantes con operaciones o cambios. El estado del dispositivo es conocido por el simulador; el servicio depende de respuestas entregadas.
t (s)Qué ocurreSalidaRealimentaciónServicio

Recorrido guiado

  1. Con los valores iniciales, avanzá a t = 3 s: la salida está activada y la solicitud sigue pendiente. En t = 4 se verifica y llega «Confirmado: encendido». En t = 13 se apaga por el plazo local.
  2. Ejecutá hasta 40 s. La salida queda apagada, pero el resultado histórico sigue siendo «encendido confirmado». Eso describe la ejecución de aquella orden, no un reporte continuo del estado actual.
  3. Marcá Perder la respuesta y prepará otra simulación. La salida se activa y luego se apaga, mientras el servicio agota su espera a los 6 s. No puede concluir que la ejecución no ocurrió.
  4. Elegí demora de 12 s y caducidad de 5 s. La orden se rechaza al llegar y nunca activa. Con caducidad de 15 s sí se admite a los 12, y el resultado llega tarde a los 13.
  5. Elegí red que vuelve a los 20 s. Con caducidad de 15 s se rechaza. Con caducidad de 60 s y demora de 3 s, se aplica en t = 20, se confirma en 21 y se apaga en 30.
  6. Volvé a la configuración inicial y elegí realimentación fallida. La salida se activa en t = 3 y se apaga en 5 por falta de confirmación; el servicio recibe el fallo si su respuesta no se pierde.
  7. Probá modo local, usuario sin permiso y bloqueo de activación. Los tres impiden el encendido por motivos diferentes. Una duración solicitada de 20 s también se rechaza, sin reducirla silenciosamente a diez.
  8. Con red que cae en t = 8 y demora de 3 s, el límite local sigue apagando en t = 13. Si marcás además la intervención local, se apaga a los 8 y el modo cambia a local.
  9. Elegí Cambiar umbral en modo remoto. 9,0 °C se aplica como versión 2; 7,0 °C y 12,0 °C se rechazan y mantienen versión 1 con alta de 8,0 °C. No se activa la salida para configurar.

Cuando la red está caída, el modelo conserva la orden hasta poder intentar entregarla. La fecha límite se vuelve a comprobar en el dispositivo. En producción conviene que el servicio y el transporte también descarten lo vencido; la última validación local evita depender solo de ese filtro previo.

Cada ejecución aislada empieza con salida apagada. Por eso la operación OFF confirma que permanece en ese estado, sin necesitar un cambio previo. El núcleo de control también permite apagar una salida que ya estaba encendida; una secuencia de varias órdenes requiere además el protocolo de identidades y orden que estudiamos a continuación.

Desafío: ¿por qué una respuesta tardía todavía puede ser importante?

Porque aclara una ejecución que quedó incierta al vencer la espera del servicio. Debe asociarse a la misma identidad y conservar la hora del hecho. No debe hacer que una interfaz interprete una confirmación antigua de encendido como un estado actual reciente.

12 · Diseñar antes de publicar un botón

Método y confusiones frecuentes

  1. Nombrá operaciones del producto: destinatario, unidad, rango y resultado esperado.
  2. Asigná autoridad: permisos del servicio y prioridad entre modos y condiciones locales.
  3. Separá tiempos: vigencia de admisión, espera del solicitante, duración y verificación de efecto.
  4. Concentrá la salida: un controlador resuelve el estado; los callbacks no compiten por el pin.
  5. Definí el estado sin red y al arrancar: debe cumplirse sin esperar una nueva orden remota.
  6. Decidí qué podés confirmar: recepción, salida aplicada o efecto observado.
  7. Registrá identidad y causa: se deben poder distinguir rechazos, fallas y respuestas tardías.
  8. Probá fallas: orden vencida, respuesta perdida, interbloqueo, cambio local y realimentación ausente.
  9. Revisá el circuito y el firmware juntos: tiempos, niveles de arranque y comportamiento ante fallas deben coincidir con el requisito.
«El servidor respondió 202, así que el equipo ya encendió»

La aceptación para procesamiento no confirma ejecución. La interfaz mantiene pendiente hasta tener el resultado previsto y distingue ese resultado del estado actual reportado.

«Si se agotó la espera, puedo asegurar que no pasó nada»

La orden pudo ejecutarse y perder su respuesta, o todavía estar en tránsito. Se conserva la incertidumbre y se consulta o reconcilia por la misma identidad; no se deduce el estado físico del timeout.

«QoS alto confirma que el actuador se movió»

QoS describe entrega de mensajes en el intercambio MQTT. El firmware y la realimentación definen qué efecto se aplicó y qué evidencia existe. Son confirmaciones de niveles diferentes.

«Dejo un ON retenido para que el equipo lo ejecute cuando vuelva»

Una orden puntual puede volverse inapropiada con el tiempo. Necesita vigencia y condiciones de admisión. Un estado deseado persistente es otro contrato y debe diseñarse con versión y reconciliación.

«La nube autorizada puede saltear un bloqueo local»

En este diseño no. El permiso habilita a solicitar una operación; el equipo verifica si es admisible ahora. Cambiar o anular un bloqueo sería otra operación y otra política, no un efecto implícito de encender.

«Repito ON hasta ver que responde»

Si cada recepción reinicia el plazo, los reintentos pueden extender la activación. La aplicación distingue una entrega repetida de una orden nueva y conserva su identidad. El próximo tema implementa esa disciplina.

«Cambiar un parámetro siempre es menos importante que activar una salida»

Un parámetro puede cambiar cuándo el controlador actúa o qué condición acepta. Se validan unidad, rango, coherencia y versión, además de los permisos. Una configuración solicitada no se presenta como aplicada sin evidencia.

13 · Práctica guiada

Predecí antes de comprobar

1. Tres plazos

La orden se crea en t = 0, vence a los 15 s y llega en t = 3. Pide ON durante 10 s; el servicio espera 6 s. ¿Hasta cuándo puede quedar activada por esa orden y qué plazo gobierna ese apagado?

Ver solución

Hasta t = 13 s, salvo una condición local que la detenga antes. Lo gobierna la duración local desde la aplicación. La caducidad de admisión y la espera del servicio tienen otros propósitos.

2. Llegar exactamente al vencimiento

Se admite solo antes de t = 5 s. La orden llega exactamente a los 5 s. ¿Se acepta según el contrato del tema?

Ver solución

No. La condición es llegada < vencimiento; la igualdad ya está vencida. La comparación exacta forma parte del contrato y debe probarse.

3. Se perdió la respuesta

El equipo encendió y verificó la salida, pero el resultado no llegó. ¿Qué puede afirmar el servicio al agotar su espera?

Ver solución

Que no obtuvo la confirmación dentro de su plazo. No puede concluir que la acción falló ni que la salida sigue encendida. Necesita consultar un estado recibido o resolver la solicitud por su identidad.

4. Cambio a modo local

La salida encendió en t = 3 con plazo hasta t = 13. El selector local toma el mando en t = 8 y la política indica apagar. ¿Qué ocurre con el resultado recibido en t = 4?

Ver solución

La salida se apaga a los 8 s. La confirmación de t = 4 sigue siendo un hecho histórico válido; el estado actual y la causa de interrupción se reportan aparte. No se borra ni se interpreta como continuidad del encendido.

5. GPIO y realimentación

Se escribió ON en t = 3, pero la realimentación sigue OFF. El límite de verificación es de 2 s. ¿Qué hace el modelo a los 5 s?

Ver solución

Apaga la salida y declara fallo de realimentación. No presenta la escritura de GPIO como un efecto físico confirmado. El comportamiento ante otras fallas del actuador necesita su propio diseño.

6. Un umbral incoherente

El rearme es 7,5 °C y el rango admite alta hasta 10,0 °C. Se piden 7,0; 9,0 y 12,0 °C. ¿Cuál se aplica?

Ver solución

Solo 9,0 °C. 7,0 no queda por encima del rearme; 12,0 supera el máximo. En los rechazos permanece la configuración válida previa y se informa la causa.

7. La red vuelve tarde

La orden se creó en t = 0, vence a los 15 s y el equipo vuelve a conectarse en t = 20. ¿Puede usar que «acaba de recibirla» como comienzo de vigencia?

Ver solución

No. La vigencia está ligada a la creación y al vencimiento del contrato. Recibirla ahora no la vuelve nueva. Se rechaza sin activar, incluso si el transporte la conservó durante el corte.

8. Repetir una solicitud

Un encendido de diez segundos se aplicó en t = 3. Una entrega repetida de esa misma orden llega en t = 7. ¿Qué problema aparece si el núcleo reinicia su plazo sin distinguir identidad?

Ver solución

El apagado se desplaza de t = 13 a t = 17 s. Un reintento cambió el efecto de la solicitud original. El protocolo debe reconocerlo y devolver su resultado sin volver a ejecutar ni extender el plazo.

14 · Síntesis

Ideas para recordar

  • El control remoto expresa solicitudes con identidad y límites. El dispositivo conserva la admisión según modo, bloqueos y condiciones actuales.
  • Autorizar en el servicio, entregar por la red, aplicar una salida y observar el efecto son etapas distintas.
  • Operaciones explícitas como encender, apagar o aplicar un parámetro facilitan el contrato; reintentar todavía exige reconocer la identidad.
  • Caducidad, espera del servicio y duración de ejecución son relojes diferentes. Un timeout no cancela ni demuestra ausencia de ejecución.
  • El límite de encendido se cumple localmente y no depende de que llegue otra orden de apagado.
  • Una confirmación de ejecución es histórica. El panel necesita además estado actual reportado y antigüedad.
  • En el caso base, la salida enciende a los 3 s, se verifica a los 4 y apaga a los 13, incluso si la red se corta antes.
  • Los parámetros remotos se validan y versionan; la configuración aplicada no se deduce de la solicitada.

¿Listo para el tema 38?

Antes de avanzar, elegí una operación de tu proyecto y escribí su destinatario, permisos, modo permitido, vigencia, límites locales y evidencia de resultado. Probá qué diría el panel si la acción se ejecuta y se pierde la respuesta.

En el próximo tema, Comandos, estados y confirmaciones, formalizamos ese intercambio: estado deseado frente a estado real, identidades, reintentos e idempotencia para que una entrega repetida no vuelva a ejecutar una acción.