Skip to Content
Desarrollo de apps IoTPlano de la aplicación

Plano de la aplicación

Esta página muestra cómo construir una app de IronFlock para que use la plataforma tal y como está pensada. La idea, en una frase: tus contenedores se ejecutan en cada dispositivo y guardan todo el estado de la app en las tablas de la app, el panel es la interfaz de usuario de la app, y contenedores y panel nunca se comunican directamente entre sí. Una interfaz web que sirve el propio dispositivo llega a los usuarios a través de un túnel de acceso remoto y aparece dentro del panel.

Cada pieza se describe en detalle en su propia página. Esta página muestra cómo encajan entre sí.

Las partes de una app

Una app se ejecuta en muchos dispositivos a la vez, normalmente PCs industriales junto a las máquinas. Se compone de:

ParteQué esSe define en
Código edgeUn contenedor, o un stack de Docker Compose, en cada dispositivo donde está instalada la appDockerfile o docker-compose.yml
SDKEl único vínculo entre tu código y la plataforma: tablas, actualizaciones en tiempo real, funciones de dispositivo, archivosIronFlock SDK (Python, JavaScript, Go)
TablasEl estado de la app, en la base de datos propia del proyectodata-template.yml
PanelLa interfaz de usuario central de la app, enlazada en vivo a las tablas, para todos los dispositivosboard-template.yml
ParámetrosAjustes de un dispositivo o de un grupo de dispositivosenv-template.yml
Acceso remotoTúneles hacia los puertos que sirven tus contenedores, como una interfaz webport-template.yml
Agentes de IAAgentes para el asistente de IronFlock, con tus funciones de dispositivo como herramientasai-template.yml

Cuando un usuario instala tu app en un proyecto, IronFlock crea las tablas de la app en la base de datos de ese proyecto, instala su panel y abre un realm de mensajería privado que conecta en tiempo real los contenedores de la app en todos los dispositivos, sus tablas y su panel. El SDK se une a ese realm con la identidad que inyecta el agente del dispositivo, así que tu código no necesita ninguna dirección ni credenciales.

Las reglas

  1. Las tablas son el estado de la app. Todo lo que necesitan el panel, otro dispositivo, un contenedor reiniciado o un agente de IA se guarda en una tabla. Un contenedor solo conserva lo que puede reconstruir a partir de las tablas, como una copia en memoria de sus ajustes.
  2. Las partes se comunican a través de las tablas, no entre sí. Un dispositivo escribe lo que mide y el estado en que se encuentra, y el panel lo muestra. El panel escribe lo que deciden los usuarios, y los dispositivos lo aplican. Ninguno llama al otro, así que el panel funciona mientras los dispositivos están desconectados, y los dispositivos siguen funcionando mientras nadie tiene abierto el panel.
  3. Un solo escritor por tabla. Cada tabla la escriben los dispositivos (cada uno sobre sí mismo), el panel o un único servicio. Indica cuál en la description de la tabla. Dos escritores de las mismas filas se sobrescriben mutuamente los cambios.
  4. Estado deseado, no comandos. Para cambiar lo que hacen los dispositivos, escribe el valor objetivo en una tabla (un límite, una consigna, una receta, un horario) y deja que cada dispositivo aplique la fila más reciente. Un dispositivo que estaba desconectado lo aplica cuando vuelve, y aplicarlo dos veces no cambia nada.
  5. Escribe al cambiar, sigue en vivo, nunca sondees. Escribe la fila de una entidad cuando cambie uno de sus valores, y las mediciones a su frecuencia de muestreo. Todo lo que lee sigue las tablas en tiempo real: los enlaces de datos del panel, las suscripciones a tablas en los dispositivos, los websockets de la interfaz web de un dispositivo. Un temporizador que vuelve a leer una tabla es siempre un diseño equivocado.
  6. El panel es la interfaz de usuario; la interfaz web de un dispositivo la complementa. Sirve una página web desde un dispositivo solo para lo que un panel no puede mostrar: la HMI propia de una máquina, video, una vista en vivo de alta frecuencia, una pantalla de operador junto a la máquina. El panel la incrusta.

Dónde va cada tipo de dato

DatoDóndeEscrito porLeído por
Lecturas a lo largo del tiempoUna tabla con tsppublishToTable, publishRowsToTableGráficos con ventanas de tiempo y agregación
El estado actual de un dispositivo, una máquina o una ordenUna tabla de entidades (maintainLatestFlagFor)appendToTable: una fila con la clave, tsp y las columnas modificadasEl filtro latest
Límites, consignas y recetas que fijan los usuariosUna tabla que escribe el formulario del panelEl formularioCada dispositivo, como se muestra más abajo
Registros que los usuarios crean, editan y eliminanUna tabla de entidades con una columna deletedEl formulario, que activa el marcador al eliminarListas y páginas de edición en el panel
Errores y avisos para los operadoresLa tabla error-logsreportError con un userMessageEl widget de toast del panel
Imágenes, documentos, grabacionesEl almacenamiento de archivos de la appfiles.putLa URL del archivo en una columna, mostrada por un widget
Contraseñas y claves de APIUna columna secret: trueCualquier escritura en la tablarevealSecrets, verifySecret, solo en los propios contenedores de la app
La instalación concreta de un dispositivo: la dirección de una máquina, un puerto serieParámetros de la aplicaciónLa configuración de la app del dispositivo o del grupoVariables de entorno
Mensajes entre tus partes que nadie necesita despuésTus propios topicspublishsubscribe
Una petición que necesita al momento la respuesta de un dispositivoUna función de dispositivoregisterDeviceFunctioncallDeviceFunction, acciones de widgets, herramientas de IA

¿Añadir o publicar? appendToTable espera a un backend de datos que se está reiniciando y te avisa cuando este rechaza una fila. publishToTable solo espera a la conexión, así que una fila publicada mientras el backend de datos se reinicia se pierde. Añade los cambios de estado y los registros; publica las lecturas de alta frecuencia.

¿Parámetros o una tabla de ajustes? Los parámetros pertenecen a un dispositivo o a un grupo de dispositivos: quien instala o configura el dispositivo los establece en la configuración de la app, y tu código los lee de su entorno. Úsalos para lo que cambia de una instalación a otra. Lo que los usuarios modifican mientras la app se ejecuta — límites, recetas, horarios, cualquier cosa que se aplique a muchos dispositivos — va en una tabla que escribe el panel. El panel es donde los usuarios trabajan con la app, todos los dispositivos siguen un cambio al instante, y la tabla conserva el historial de quién cambió qué y cuándo.

Seguir lo que escribe el panel

Un dispositivo aplica el estado que escribe el panel, como un límite fijado en el panel, en tres pasos:

  1. Suscríbete a la tabla antes de leerla, para que no se pierda ninguna fila entre la lectura y la suscripción.
  2. Lee la fila más reciente una vez al arrancar, con el filtro latest, y aplícala.
  3. Aplica una fila solo si es más reciente (tsp) que la que está en vigor. Una fila guardada justo cuando arranca tu programa puede llegar tanto por la lectura como por la suscripción.
await ironflock.subscribe_to_table("settings", lambda row, *args, **kwargs: apply(row)) rows = await ironflock.getHistory("settings", {"limit": 1, "filterAnd": [{"latest": True}]}) for row in rows or []: apply(row)

Eso es todo, también para los dispositivos que se quedan sin conexión. El SDK se ocupa de la conexión para que tu código no tenga que hacerlo: se reconecta, restaura tus suscripciones y funciones de dispositivo y, a continuación, entrega a cada suscripción de tabla las filas almacenadas mientras el dispositivo estaba desconectado, de la más antigua a la más reciente, antes que las filas que llegan en vivo. Un límite guardado mientras un dispositivo estaba desconectado le llega en cuanto vuelve. La puesta al día se rige por tsp: entrega las filas posteriores a la más reciente que haya recibido tu handler, como máximo las 10.000 más recientes. La lectura que hace espera al backend de datos como cualquier lectura de historial, como máximo durante la ventana de reconexión; si falla, esas filas no se entregan y se registra una advertencia. La puesta al día forma parte del SDK desde la versión 1.10.0.

Comandos, funciones de dispositivo y topics

Prefiere el estado. La mayoría de las cosas que los usuarios “ordenan” son estado deseado: una consigna, encendido o apagado, una receta. Escríbelas en una tabla, como se ha descrito arriba.

Los comandos que deben ejecutarse una sola vez, como imprimir una etiqueta o iniciar un ciclo de limpieza, son filas con un id en una tabla que escribe el panel. El dispositivo registra en una tabla propia los ids que ya ha ejecutado, de modo que un comando que recibe dos veces se ejecuta una sola vez.

Las funciones de dispositivo son procedimientos que un dispositivo registra con registerDeviceFunction. Úsalas cuando quien llama necesita la respuesta del dispositivo en ese mismo momento:

  • Agentes de IA: una herramienta de ai-template.yml llama a la función en el dispositivo que elige el asistente. Consulta Agentes y Herramientas de IA.
  • Los widgets Interruptor y SCADA del panel las llaman mientras el dispositivo está conectado.
  • Tus propios contenedores y dispositivos las llaman entre sí con callDeviceFunction.

Devuelve desde una función de dispositivo texto legible o un JSON pequeño, y devuelve un fallo previsto (una máquina apagada) como resultado en lugar de lanzar un error.

Los topics transportan mensajes entre tus propias partes que nadie necesita después: publish y subscribe sobre nombres de tu propio espacio de nombres, como myapp.events. El panel no puede suscribirse a topics: todo lo que muestra tiene que estar en una tabla. Los mensajes publicados mientras un dispositivo está desconectado no se almacenan, así que usa una tabla para todo lo que tenga que llegar.

El contenedor en el dispositivo

Lo que el agente del dispositivo le da a tu contenedor y lo que espera de él:

  • Identidad: el agente establece DEVICE_KEY, DEVICE_NAME, DEVICE_SERIAL_NUMBER, SWARM_KEY, APP_KEY, APP_NAME, ENV (DEV o PROD), TUNNEL_DOMAIN, DEVICE_LAN_IP y DEVICE_PORT_FOR_<port>, además de la credencial propia de la app para el SDK. El SDK lee lo que necesita; nunca fijes ninguno de estos valores en el código.
  • Parámetros: una instalación de producción recibe los valores que establecieron los usuarios y, para el resto, el defaultValue de cada parámetro. Una ejecución de desarrollo recibe solo los valores por defecto. Da a los parámetros nombres específicos (PLC_HOST, no HOST).
  • Almacenamiento: una app de una sola imagen recibe /data, privado para la app, que se conserva entre reinicios y actualizaciones (y se elimina junto con la app), y /shared, compartido por todas las apps del dispositivo. Aun así, guarda el estado de la app en tablas y usa estas carpetas para archivos que pertenecen al dispositivo, como un modelo descargado o una caché.
  • Hardware: un contenedor de una sola imagen se ejecuta en modo privilegiado, con el /dev del dispositivo.
  • Logs: todo lo que tu programa escribe en stdout y stderr es el log de la app en IronFlock. Escríbelo sin búfer (python -u).
  • Arranque: el contenedor debe estar en ejecución 30 segundos después de arrancar; si no, el arranque se considera fallido.
  • Caídas: cuando una app de producción termina, el agente la vuelve a iniciar y espera más tiempo tras cada fallo. Una ejecución de desarrollo no se reinicia. Gestiona en tu programa los fallos previstos, como un sensor que no responde, y deja que termine ante los errores de los que no puede recuperarse.
  • Parada: tu programa es el proceso principal del contenedor (PID 1), que ignora la señal de parada SIGTERM a menos que la gestione. Haz que termine al recibir SIGTERM; de lo contrario, al detener la app hay que esperar a que Docker lo mate. Inicia el programa en forma exec (CMD ["python", "-u", "main.py"]), no a través de un shell.
  • Ejecuciones de desarrollo: ejecutar la app desde el IDE en la nube es una ejecución de desarrollo (ENV es DEV). Escribe en las tablas de desarrollo de la app, recibe los valores por defecto de los parámetros y no tiene túneles de acceso remoto.

Docker Compose

Usa un docker-compose.yml cuando tu app ejecuta varios procesos o incluye servicios de terceros, como un broker MQTT o una base de datos, junto a tu propio código. Consulta Docker y Compose para lo básico. En comparación con una sola imagen:

  • Cada servicio recibe el entorno inyectado. El agente da a cada servicio su propio archivo de entorno con las variables anteriores; un env_file: que escribas tú se reemplaza. Un ${VAR} en el archivo se resuelve con los valores inyectados.
  • Sin /data. Los servicios no reciben ninguna carpeta de la app. Guarda los archivos en volúmenes con nombre: sobreviven a paradas y actualizaciones, y se eliminan cuando la app se quita del dispositivo.
  • Los puertos publicados se reasignan. Cada puerto publicado recibe un puerto libre del host en todas las interfaces del dispositivo, y DEVICE_PORT_FOR_<port> indica a tu código cuál es el real. En port-template.yml, usa el puerto del host tal como está escrito en el archivo compose. Publica solo lo que deban alcanzar los usuarios, y deja que tus servicios se comuniquen entre sí por la red de Compose mediante el nombre del servicio. Un puerto del host escrito como ${VAR} no se gestiona: no se reasigna ni recibe túnel.
  • No se añade nada. El agente no concede a los servicios de Compose ningún privilegio ni ningún dispositivo. Declara tú mismo privileged, devices o device_cgroup_rules. Una entrada devices: nombra hardware que tiene que existir; si no, el contenedor no arranca. device_cgroup_rules junto con un volumen /dev funciona en cualquier dispositivo.
  • Sin profiles:. Un servicio con perfiles queda fuera del release.
  • La misma regla de arranque. En un plazo de 30 segundos, todos los contenedores deben estar en ejecución o haber terminado con el código 0, y al menos uno debe estar en ejecución. Da a los servicios de larga duración restart: unless-stopped.

Interfaces web en el dispositivo

Un servidor web en tu contenedor pasa a ser accesible desde cualquier lugar a través del túnel de acceso remoto de su puerto, y el panel lo muestra:

  1. Sirve en un puerto fijo (escuchando en 0.0.0.0) y decláralo en port-template.yml con protocol: http. Una instalación de producción activa el túnel y lo sirve por HTTPS, websockets incluidos.
  2. Escribe la URL del túnel en una tabla. La función getRemoteAccessUrlForPort(port) del SDK devuelve la URL. Escríbela al arrancar en la fila del dispositivo de una tabla de dispositivos. Solo las instalaciones de producción tienen túneles, así que una ejecución de desarrollo no escribe ninguna URL.
  3. Incrústala en el panel. Enlaza la URL del widget Embed a esa columna, en una página por dispositivo que filtre por el dispositivo. En el UI Builder, esto es un enlace de datos como cualquier otro.

Para la propia página:

  • Quién puede abrirla: un túnel http es privado. Solo pueden abrirlo las personas que han iniciado sesión en IronFlock y tienen acceso al dispositivo, salvo que un usuario cambie el puerto a público. En la red propia del dispositivo, la página está abierta a todo el mundo, así que no muestres ahí nada que no deba ver la red de la planta, y exige un inicio de sesión propio para todo lo que cambie el estado de la máquina.
  • URLs relativas: carga los recursos, llama a tu API y abre los websockets con rutas relativas a la página, para que funcione en la red del dispositivo, a través del túnel y detrás de un proxy inverso.
  • Actualizaciones por push, nunca por sondeo: envíalas por un websocket.
  • Sin credenciales de la plataforma en el navegador: la página habla con su propio servidor en el contenedor, y ese servidor mantiene la conexión del SDK.

Para mostrar la HMI web de una máquina de la red de la planta en lugar de una página propia, sírvela a través de un proxy inverso en tu contenedor. Consulta Integrar HMIs externas.

Varios dispositivos

  • Cada fila lleva su dispositivo: IronFlock guarda en device_key la clave del dispositivo que escribió la fila. Usa device_key como clave de las tablas por dispositivo, guarda el nombre del dispositivo en una columna con path: kwargs.DEVICE_NAME y desglosa los gráficos por ese nombre.
  • Una lista de dispositivos y una página por dispositivo: una navegación lateral generada a partir de la tabla de dispositivos, con una sola página que sirve para todos los dispositivos y filtra sus widgets por el dispositivo de la ruta. Consulta Crear paneles.
  • Ajustes por dispositivo o por máquina: usa como clave de la tabla de ajustes una columna que identifique el destino (las filas que escribe el panel no llevan un dispositivo propio) y rellénala en el formulario de la página del destino. Cada dispositivo aplica la fila más reciente de su propia clave.
  • Un dispositivo que se retira conserva sus filas. Si los usuarios deben poder quitarlo de las listas, añade a la tabla de dispositivos una columna deleted y al panel un formulario que la active.

Lista de verificación

  1. Todo el estado está en tablas, cada una con un único escritor indicado en su descripción, y cada columna que escribe el código está declarada en data-template.yml.
  2. Cada fila tiene un tsp, y las filas de estado se escriben al cambiar, solo con la clave y las columnas modificadas.
  3. El estado que escribe el panel se sigue con los tres pasos de arriba, con el SDK 1.10.0 o posterior, y el código nunca se ocupa de la conexión ni de sus sesiones.
  4. Las escrituras en tablas dentro de bucles de larga duración gestionan el error que lanza una escritura cuando la plataforma lleva un minuto inaccesible, y los fallos previstos se notifican con reportError y un userMessage.
  5. El programa termina con SIGTERM, arranca en forma exec y escribe los logs sin búfer.
  6. Nada sondea: el panel se enlaza a las tablas, los dispositivos se suscriben y las interfaces web envían por push.
  7. Cada puerto que sirve tu app está declarado en port-template.yml, su URL se obtiene con getRemoteAccessUrlForPort y se escribe en una tabla, y el panel la incrusta desde allí.
  8. Una app de Compose no tiene env_file:, ni profiles:, ni puertos del host ${VAR}; guarda los archivos en volúmenes con nombre y publica solo los puertos que necesitan los usuarios.

Empezar con la app de partida

El plugin de IronFlock para Claude Code contiene una app de partida que sigue este plano: un programa que lee una máquina en cada dispositivo, guarda el estado y las lecturas de cada dispositivo en tablas, aplica el límite que los usuarios fijan en el panel y sirve una vista en vivo que el panel incrusta en la página de cada dispositivo. Claude Code crea una app nueva a partir de ella y la adapta a tu caso de uso.

Last updated on