Skip to Content

Blueprint do App

Esta página mostra como construir um app IronFlock de modo que ele use a plataforma do jeito que ela foi pensada para ser usada. A ideia em uma frase: os seus contêineres rodam em cada dispositivo e mantêm todo o estado do app nas tabelas do app, o board é a interface de usuário do app, e os dois nunca se comunicam diretamente. Uma interface web que o próprio dispositivo serve chega aos usuários por um túnel de acesso remoto e aparece dentro do board.

Cada peça é descrita em detalhes na sua própria página. Esta página mostra como elas se encaixam.

As Partes de um App

Um app roda em muitos dispositivos ao mesmo tempo, normalmente PCs industriais ao lado das máquinas. Ele é composto por:

ParteO que éDefinido em
Código de bordaUm contêiner, ou uma stack Docker Compose, em cada dispositivo em que o app está instaladoDockerfile ou docker-compose.yml
SDKO único elo entre o seu código e a plataforma: tabelas, atualizações em tempo real, funções de dispositivo, arquivosSDK do IronFlock (Python, JavaScript, Go)
TabelasO estado do app, no banco de dados do próprio projetodata-template.yml
BoardA interface de usuário central do app, vinculada ao vivo às tabelas, para todos os dispositivosboard-template.yml
ParâmetrosConfigurações de um dispositivo ou de um grupo de dispositivosenv-template.yml
Acesso remotoTúneis para as portas que os seus contêineres servem, como uma interface webport-template.yml
Agentes de IAAgentes para o assistente do IronFlock, com as suas funções de dispositivo como ferramentasai-template.yml

Quando um usuário instala o seu app em um projeto, o IronFlock cria as tabelas do app no banco de dados desse projeto, instala o board do app e abre um realm de mensagens privado que conecta em tempo real os contêineres do app em todos os dispositivos, as suas tabelas e o seu board. O SDK entra nesse realm com a identidade que o agente de dispositivo injeta, então o seu código não precisa de endereço nem de credenciais.

As Regras

  1. As tabelas são o estado do app. Tudo de que o board, outro dispositivo, um contêiner reiniciado ou um agente de IA precisa fica armazenado em uma tabela. Um contêiner guarda apenas o que consegue reconstruir a partir das tabelas, como uma cópia em memória das suas configurações.
  2. As partes se comunicam pelas tabelas, não umas com as outras. Um dispositivo escreve o que mede e o estado em que está, e o board exibe isso. O board escreve o que os usuários decidem, e os dispositivos o aplicam. Nenhum dos dois chama o outro, então o board funciona enquanto os dispositivos estão offline, e os dispositivos continuam funcionando enquanto ninguém está com o board aberto.
  3. Um escritor por tabela. Cada tabela é escrita pelos dispositivos (cada um sobre si mesmo), pelo board ou por um único serviço. Indique qual na description da tabela. Dois escritores das mesmas linhas sobrescrevem as alterações um do outro.
  4. Estado desejado, não comandos. Para mudar o que os dispositivos fazem, escreva o valor-alvo em uma tabela (um limite, um setpoint, uma receita, um cronograma) e deixe cada dispositivo aplicar a linha mais recente. Um dispositivo que estava offline o aplica quando volta, e aplicá-lo duas vezes não muda nada.
  5. Escreva quando mudar, acompanhe ao vivo, nunca faça polling. Escreva a linha de uma entidade quando um dos seus valores mudar, e as medições na sua taxa de amostragem. Tudo o que lê acompanha as tabelas em tempo real: as vinculações do board, as inscrições em tabelas nos dispositivos, os websockets na interface web de um dispositivo. Um timer que relê uma tabela é sempre o design errado.
  6. O board é a interface; a interface web de um dispositivo a complementa. Sirva uma página web a partir de um dispositivo apenas para o que um board não consegue mostrar: a HMI própria de uma máquina, vídeo, uma visualização ao vivo de alta frequência, uma tela de operador junto à máquina. O board a incorpora.

Onde Fica Cada Tipo de Dado

DadoOndeEscrito porLido por
Leituras ao longo do tempoUma tabela com tsppublishToTable, publishRowsToTableGráficos com janelas de tempo e agregação
O estado atual de um dispositivo, uma máquina ou uma ordemUma tabela de entidade (maintainLatestFlagFor)appendToTable: uma linha com a chave, tsp e as colunas alteradasO filtro latest
Limites, setpoints e receitas que os usuários definemUma tabela que o formulário do board escreveO formulárioCada dispositivo, como mostrado abaixo
Registros que os usuários criam, editam e excluemUma tabela de entidade com uma coluna deletedO formulário, que ativa o marcador ao excluirListas e páginas de edição no board
Erros e avisos para os operadoresA tabela error-logsreportError com um userMessageO widget de toast do board
Imagens, documentos, gravaçõesO armazenamento de arquivos do appfiles.putA URL do arquivo em uma coluna, exibida por um widget
Senhas e chaves de APIUma coluna secret: trueQualquer escrita na tabelarevealSecrets, verifySecret, apenas nos próprios contêineres do app
A instalação de um dispositivo específico: o endereço de uma máquina, uma porta serialParâmetros do AppAs configurações do app do dispositivo ou do grupoVariáveis de ambiente
Mensagens entre as suas partes de que ninguém precisa depoisOs seus próprios tópicospublishsubscribe
Uma requisição que precisa da resposta de um dispositivo na horaUma função de dispositivoregisterDeviceFunctioncallDeviceFunction, ações de widgets, ferramentas de IA

Acrescentar ou publicar? appendToTable aguarda um backend de dados que está reiniciando e avisa você quando ele recusa uma linha. publishToTable aguarda apenas a conexão, então uma linha publicada enquanto o backend de dados reinicia se perde. Acrescente mudanças de estado e registros; publique leituras de alta frequência.

Parâmetros ou uma tabela de configurações? Os parâmetros pertencem a um dispositivo ou a um grupo de dispositivos: quem instala ou configura o dispositivo os define nas configurações do app, e o seu código os lê do ambiente. Use-os para o que muda de uma instalação para outra. O que os usuários alteram enquanto o app roda — limites, receitas, cronogramas, qualquer coisa que se aplique a muitos dispositivos — fica em uma tabela que o board escreve. O board é onde os usuários trabalham com o app, todos os dispositivos acompanham uma alteração na hora, e a tabela guarda o histórico de quem mudou o quê e quando.

Acompanhando o que o Board Escreve

Um dispositivo aplica o estado escrito pelo board, como um limite definido no board, em três passos:

  1. Inscreva-se na tabela antes de lê-la, para que nenhuma linha se perca entre a leitura e a inscrição.
  2. Leia a linha mais recente uma vez na inicialização, com o filtro latest, e aplique-a.
  3. Aplique uma linha apenas se ela for mais recente (tsp) do que a que está em vigor. Uma linha salva bem no momento em que o seu programa inicia pode chegar tanto pela leitura quanto pela inscrição.
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)

Isso é tudo, também para dispositivos que ficam offline. O SDK cuida da conexão para que o seu código não precise fazer isso: ele se reconecta, restaura as suas inscrições e funções de dispositivo e, em seguida, entrega a cada inscrição de tabela as linhas armazenadas enquanto o dispositivo estava offline, das mais antigas para as mais recentes, antes das linhas que chegam ao vivo. Um limite salvo enquanto um dispositivo estava offline chega a ele assim que ele volta. A recuperação se baseia no tsp: ela entrega as linhas posteriores à mais recente que o seu handler recebeu, no máximo as 10.000 mais recentes. A leitura que ela faz aguarda o backend de dados como qualquer leitura de histórico, até o limite da janela de reconexão; se falhar, essas linhas não são entregues e um aviso é registrado. A recuperação faz parte do SDK a partir da versão 1.10.0.

Comandos, Funções de Dispositivo e Tópicos

Prefira estado. A maioria das coisas que os usuários “comandam” é estado desejado: um setpoint, ligado ou desligado, uma receita. Escreva-as em uma tabela, como acima.

Comandos que precisam ser executados uma única vez, como imprimir uma etiqueta ou iniciar um ciclo de limpeza, são linhas com um id em uma tabela que o board escreve. O dispositivo registra em uma tabela própria os ids que já executou, de modo que um comando recebido duas vezes é executado só uma vez.

Funções de dispositivo são procedimentos que um dispositivo registra com registerDeviceFunction. Use-as quando quem chama precisa da resposta do dispositivo naquele momento:

  • Agentes de IA: uma ferramenta no ai-template.yml chama a função no dispositivo que o assistente escolher. Veja IA: Definindo Agentes e Ferramentas.
  • Os widgets Interruptor e SCADA no board as chamam enquanto o dispositivo está online.
  • Os seus próprios contêineres e dispositivos as chamam entre si com callDeviceFunction.

Retorne de uma função de dispositivo um texto legível ou um JSON pequeno, e retorne uma falha esperada (uma máquina desligada) como resultado em vez de levantar um erro.

Tópicos transportam mensagens entre as suas próprias partes de que ninguém precisa depois: publish e subscribe em nomes do seu próprio namespace, como myapp.events. O board não pode se inscrever em tópicos: tudo o que ele exibe precisa estar em uma tabela. Mensagens publicadas enquanto um dispositivo está offline não são armazenadas, então use uma tabela para tudo o que precisa chegar.

O Contêiner no Dispositivo

O que o agente de dispositivo fornece ao seu contêiner e o que ele espera dele:

  • Identidade: o agente define DEVICE_KEY, DEVICE_NAME, DEVICE_SERIAL_NUMBER, SWARM_KEY, APP_KEY, APP_NAME, ENV (DEV ou PROD), TUNNEL_DOMAIN, DEVICE_LAN_IP e DEVICE_PORT_FOR_<port>, além da credencial própria do app para o SDK. O SDK lê o que precisa; nunca codifique nenhum desses valores fixamente.
  • Parâmetros: uma instalação de produção recebe os valores que os usuários definiram e, para o restante, o defaultValue de cada parâmetro. Uma execução de desenvolvimento recebe apenas os valores padrão. Dê nomes específicos aos parâmetros (PLC_HOST, não HOST).
  • Armazenamento: um app de imagem única recebe /data, privado do app e mantido entre reinicializações e atualizações (ele é excluído junto com o app), e /shared, compartilhado por todos os apps do dispositivo. Mesmo assim, mantenha o estado do app em tabelas e use essas pastas para arquivos que pertencem ao dispositivo, como um modelo baixado ou um cache.
  • Hardware: um contêiner de imagem única roda em modo privilegiado, com o /dev do dispositivo.
  • Logs: tudo o que o seu programa escreve em stdout e stderr é o log do app no IronFlock. Escreva sem buffer (python -u).
  • Inicialização: o contêiner precisa estar em execução 30 segundos depois de iniciar, ou a inicialização é considerada uma falha.
  • Quedas: quando um app de produção termina, o agente o inicia de novo, esperando mais tempo após cada falha. Uma execução de desenvolvimento não é reiniciada. Trate no seu programa as falhas esperadas, como um sensor que não responde, e deixe-o encerrar em erros dos quais ele não consegue se recuperar.
  • Parada: o seu programa é o processo principal do contêiner (PID 1), que ignora o sinal de parada SIGTERM a menos que o trate. Encerre o programa ao receber SIGTERM; caso contrário, ao parar o app, é preciso esperar até que o Docker o mate. Inicie o programa na forma exec (CMD ["python", "-u", "main.py"]), não por meio de um shell.
  • Execuções de desenvolvimento: rodar o app pelo Cloud IDE é uma execução de desenvolvimento (ENV é DEV). Ela escreve nas tabelas de desenvolvimento do app, recebe os valores padrão dos parâmetros e não ganha túneis de acesso remoto.

Docker Compose

Use um docker-compose.yml quando o seu app roda vários processos ou inclui serviços de terceiros, como um broker MQTT ou um banco de dados, ao lado do seu próprio código. Veja Docker & Compose para o básico. Em comparação com uma imagem única:

  • Todo serviço recebe o ambiente injetado. O agente dá a cada serviço o seu próprio arquivo de ambiente com as variáveis acima; um env_file: que você escreva é substituído. Um ${VAR} no arquivo é resolvido com os valores injetados.
  • Sem /data. Os serviços não recebem pasta do app. Mantenha os arquivos em volumes nomeados: eles sobrevivem a paradas e atualizações e são excluídos quando o app é removido do dispositivo.
  • Portas publicadas são remapeadas. Cada porta publicada recebe uma porta livre do host em todas as interfaces do dispositivo, e DEVICE_PORT_FOR_<port> informa ao seu código qual é a real. No port-template.yml, use a porta do host como está escrita no arquivo compose. Publique apenas o que os usuários devem alcançar, e deixe os seus serviços se comunicarem pela rede do Compose usando o nome do serviço. Uma porta do host escrita como ${VAR} não é gerenciada: ela não é remapeada nem ganha túnel.
  • Nada é adicionado. O agente não concede aos serviços do Compose nenhum privilégio nem dispositivo. Declare você mesmo privileged, devices ou device_cgroup_rules. Uma entrada devices: nomeia um hardware que precisa existir, senão o contêiner não inicia; device_cgroup_rules junto com um volume /dev funciona em qualquer dispositivo.
  • Sem profiles:. Um serviço com profiles fica de fora do release.
  • A mesma regra de inicialização. Em até 30 segundos, todo contêiner precisa estar em execução ou ter terminado com código 0, e pelo menos um precisa estar em execução. Dê aos serviços de longa duração restart: unless-stopped.

Interfaces Web no Dispositivo

Um servidor web no seu contêiner fica acessível de qualquer lugar pelo túnel de acesso remoto da sua porta, e o board o exibe:

  1. Sirva em uma porta fixa (escutando em 0.0.0.0) e declare-a no port-template.yml com protocol: http. Uma instalação de produção ativa o túnel e o serve por HTTPS, incluindo websockets.
  2. Escreva a URL do túnel em uma tabela. A função getRemoteAccessUrlForPort(port) do SDK retorna a URL. Na inicialização, escreva-a na linha do dispositivo em uma tabela de dispositivos. Só instalações de produção têm túneis, então uma execução de desenvolvimento não escreve nenhuma URL.
  3. Incorpore-a no board. Vincule a URL do widget Embed a essa coluna, em uma página por dispositivo que filtre pelo dispositivo. No UI Builder, isso é uma vinculação de dados como qualquer outra.

Para a página em si:

  • Quem pode abri-la: um túnel http é privado. Apenas pessoas autenticadas no IronFlock e com acesso ao dispositivo podem abri-lo, a menos que um usuário torne a porta pública. Na rede do próprio dispositivo, a página fica aberta a todos, então não mostre ali nada que a rede da planta não possa ver, e exija um login próprio para tudo o que altere o estado da máquina.
  • URLs relativas: carregue os assets, chame a sua API e abra websockets com caminhos relativos à página, para que ela funcione na rede do dispositivo, pelo túnel e atrás de um proxy reverso.
  • Push, nunca polling: envie as atualizações por um websocket.
  • Nenhuma credencial da plataforma no navegador: a página conversa com o seu próprio servidor no contêiner, e é esse servidor que mantém a conexão do SDK.

Para exibir a HMI web de uma máquina na rede da planta em vez de uma página sua, sirva-a por meio de um proxy reverso no seu contêiner. Veja Incorporando HMIs Externas.

Vários Dispositivos

  • Toda linha carrega o seu dispositivo: o IronFlock armazena em device_key a chave do dispositivo que escreveu a linha. Use device_key como chave das tabelas por dispositivo, armazene o nome do dispositivo em uma coluna com path: kwargs.DEVICE_NAME e divida os gráficos por esse nome.
  • Uma lista de dispositivos e uma página por dispositivo: uma navegação lateral montada a partir da tabela de dispositivos, com uma única página que funciona para todos os dispositivos e filtra os seus widgets pelo dispositivo na rota. Veja Construindo Dashboards.
  • Configurações por dispositivo ou por máquina: use como chave da tabela de configurações uma coluna que identifique o alvo (as linhas que o board escreve não carregam um dispositivo próprio) e preencha-a no formulário da página do alvo. Cada dispositivo aplica a linha mais recente da sua própria chave.
  • Um dispositivo que sai mantém as suas linhas. Se os usuários precisarem poder removê-lo das listas, dê à tabela de dispositivos uma coluna deleted e ao board um formulário que a marque.

Checklist

  1. Todo o estado está em tabelas, cada uma com um único escritor indicado na sua descrição, e toda coluna que o código escreve está declarada no data-template.yml.
  2. Toda linha tem um tsp, e as linhas de estado são escritas quando algo muda, apenas com a chave e as colunas alteradas.
  3. O estado escrito pelo board é acompanhado com os três passos acima, com o SDK 1.10.0 ou posterior, e o código nunca lida com a conexão nem com as suas sessões.
  4. As escritas em tabelas em loops de longa duração tratam o erro que uma escrita levanta quando a plataforma ficou inacessível por um minuto, e as falhas esperadas são reportadas com reportError e um userMessage.
  5. O programa encerra com SIGTERM, inicia na forma exec e grava logs sem buffer.
  6. Nada faz polling: o board se vincula às tabelas, os dispositivos se inscrevem, as interfaces web fazem push.
  7. Cada porta que o seu app serve está declarada no port-template.yml, a sua URL vem de getRemoteAccessUrlForPort e é escrita em uma tabela, e o board a incorpora a partir dali.
  8. Um app Compose não tem env_file:, profiles: nem portas do host ${VAR}, mantém os arquivos em volumes nomeados e publica apenas as portas de que os usuários precisam.

Comece pelo App Inicial

O plugin do IronFlock para o Claude Code contém um app inicial que segue este blueprint: um programa que lê uma máquina em cada dispositivo, mantém o estado e as leituras de cada dispositivo em tabelas, aplica o limite que os usuários definem no board e serve uma visualização ao vivo que o board incorpora na página de cada dispositivo. O Claude Code cria um novo app a partir dele e o adapta ao seu caso de uso.

Last updated on