Skip to Content
Sviluppo App IoTBlueprint dell'App

Blueprint dell’App

Questa pagina mostra come costruire un’app IronFlock in modo che usi la piattaforma così come è stata pensata. L’idea in una frase: i tuoi container girano su ogni dispositivo e mantengono tutto lo stato dell’app nelle sue tabelle, la board è l’interfaccia utente dell’app, e le due parti non comunicano mai direttamente tra loro. Una web UI servita dal dispositivo stesso raggiunge gli utenti attraverso un tunnel di accesso remoto e compare all’interno della board.

I singoli elementi sono descritti in dettaglio nelle rispettive pagine. Questa pagina mostra come si combinano tra loro.

Le Parti di un’App

Un’app gira su molti dispositivi contemporaneamente, di solito PC industriali installati accanto alle macchine. È composta da:

ParteChe cos’èDefinita in
Codice edgeUn container, o uno stack Docker Compose, su ogni dispositivo su cui l’app è installataDockerfile o docker-compose.yml
SDKL’unico collegamento tra il tuo codice e la piattaforma: tabelle, aggiornamenti in tempo reale, funzioni del dispositivo, fileIronFlock SDK (Python, JavaScript, Go)
TabelleLo stato dell’app, nel database dedicato del progettodata-template.yml
BoardL’interfaccia utente centrale dell’app, collegata in tempo reale alle tabelle, per tutti i dispositiviboard-template.yml
ParametriImpostazioni di un dispositivo o di un gruppo di dispositivienv-template.yml
Accesso remotoTunnel verso le porte servite dai tuoi container, come una web UIport-template.yml
Agenti AIAgenti per l’assistente IronFlock, con le tue funzioni del dispositivo come strumentiai-template.yml

Quando un utente installa la tua app in un progetto, IronFlock crea le tabelle dell’app nel database di quel progetto, ne installa la board e apre un realm di messaggistica privato che collega in tempo reale i container dell’app su tutti i dispositivi, le sue tabelle e la sua board. L’SDK entra in quel realm con l’identità iniettata dall’agente del dispositivo, quindi il tuo codice non ha bisogno né di un indirizzo né di credenziali.

Le Regole

  1. Le tabelle sono lo stato dell’app. Tutto ciò di cui hanno bisogno la board, un altro dispositivo, un container riavviato o un agente AI è memorizzato in una tabella. Un container conserva solo ciò che può ricostruire dalle tabelle, come una copia in memoria delle sue impostazioni.
  2. Le parti comunicano attraverso le tabelle, non direttamente tra loro. Un dispositivo scrive ciò che misura e lo stato in cui si trova, e la board lo mostra. La board scrive ciò che decidono gli utenti, e i dispositivi lo applicano. Nessuna delle due parti chiama l’altra, quindi la board funziona anche quando i dispositivi sono offline, e i dispositivi continuano a lavorare anche quando nessuno ha la board aperta.
  3. Una sola fonte di scrittura per tabella. Ogni tabella viene scritta dai dispositivi (ciascuno con i propri dati), dalla board oppure da un unico servizio. Indica quale nella description della tabella. Due fonti che scrivono le stesse righe si sovrascrivono a vicenda le modifiche.
  4. Stato desiderato, non comandi. Per cambiare ciò che fanno i dispositivi, scrivi in una tabella lo stato da raggiungere (un limite, un setpoint, una ricetta, una pianificazione) e lascia che ogni dispositivo applichi la riga più recente. Un dispositivo che era offline la applica quando torna online, e applicarla due volte non cambia nulla.
  5. Scrivi quando qualcosa cambia, segui in tempo reale, non fare mai polling. Scrivi la riga di un’entità quando cambia uno dei suoi valori, e le misure alla loro frequenza di campionamento. Tutto ciò che legge segue le tabelle in tempo reale: i collegamenti ai dati della board, le sottoscrizioni alle tabelle sui dispositivi, i websocket nella web UI di un dispositivo. Un timer che rilegge periodicamente una tabella è sempre un errore di progettazione.
  6. La board è la UI; la web UI di un dispositivo la completa. Servi una pagina web da un dispositivo solo per ciò che una board non può mostrare: l’HMI della macchina stessa, un video, una vista live ad alta frequenza, una schermata per l’operatore a bordo macchina. La board la incorpora.

Dove Va Ogni Tipo di Dato

DatoDoveScritto daLetto da
Letture nel tempoUna tabella con tsppublishToTable, publishRowsToTableGrafici con finestre temporali e aggregazione
Lo stato corrente di un dispositivo, una macchina o un ordineUna tabella entità (maintainLatestFlagFor)appendToTable: una riga con la chiave, tsp e le colonne modificateIl filtro latest
Limiti, setpoint, ricette impostati dagli utentiUna tabella scritta dal form della boardIl formOgni dispositivo, come mostrato più avanti
Record che gli utenti creano, modificano ed eliminanoUna tabella entità con una colonna deletedIl form, che imposta il flag all’eliminazioneElenchi e pagine di modifica sulla board
Errori e avvisi per gli operatoriLa tabella error-logsreportError con un userMessageIl widget toast della board
Immagini, documenti, registrazioniL’archiviazione dei file dell’appfiles.putL’URL del file in una colonna, mostrato da un widget
Password e chiavi APIUna colonna secret: trueQualsiasi scrittura in tabellarevealSecrets, verifySecret, solo nei container dell’app stessa
L’installazione di un singolo dispositivo: l’indirizzo di una macchina, una porta serialeParametri dell’appLe impostazioni dell’app del dispositivo o del gruppoVariabili d’ambiente
Messaggi tra le parti della tua app che non servono più dopo la consegnaI tuoi topicpublishsubscribe
Una richiesta che ha bisogno subito della risposta di un dispositivoUna funzione del dispositivoregisterDeviceFunctioncallDeviceFunction, azioni dei widget, strumenti AI

Aggiungere o pubblicare? appendToTable attende un data backend che si sta riavviando e ti segnala quando rifiuta una riga. publishToTable attende solo la connessione, quindi una riga pubblicata mentre il data backend si riavvia va persa. Aggiungi i cambi di stato e i record; pubblica le letture ad alta frequenza.

Parametri o una tabella di impostazioni? I parametri appartengono a un dispositivo o a un gruppo di dispositivi: chi installa o configura il dispositivo li imposta nelle impostazioni dell’app, e il tuo codice li legge dal proprio ambiente. Usali per ciò che varia da un’installazione all’altra. Ciò che gli utenti modificano mentre l’app è in esecuzione — limiti, ricette, pianificazioni, qualsiasi cosa valga per molti dispositivi — va in una tabella scritta dalla board. La board è il luogo in cui gli utenti lavorano con l’app, ogni dispositivo recepisce subito una modifica, e la tabella conserva la cronologia di chi ha cambiato cosa e quando.

Seguire Ciò che Scrive la Board

Un dispositivo applica lo stato scritto dalla board, come un limite impostato sulla board, in tre passi:

  1. Sottoscriviti alla tabella prima di leggerla, in modo che nessuna riga sfugga tra la lettura e la sottoscrizione.
  2. Leggi la riga più recente una volta all’avvio, con il filtro latest, e applicala.
  3. Applica una riga solo se è più recente (tsp) di quella in vigore. Una riga salvata proprio mentre il tuo programma si avvia può arrivare sia dalla lettura sia dalla sottoscrizione.
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)

Non serve altro, nemmeno per i dispositivi che vanno offline. Della connessione si occupa l’SDK, non il tuo codice: si riconnette, ripristina le tue sottoscrizioni e funzioni del dispositivo, e poi consegna a ogni sottoscrizione di tabella le righe memorizzate mentre il dispositivo era offline, a partire dalla più vecchia, prima delle righe che arrivano in tempo reale. Un limite salvato mentre un dispositivo era offline lo raggiunge non appena torna online. Il recupero si basa su tsp: consegna le righe successive all’ultima ricevuta dal tuo gestore, al massimo le 10.000 più recenti. La sua lettura attende il data backend come qualsiasi lettura di storico, entro la finestra di riconnessione; se fallisce, quelle righe non vengono consegnate e viene registrato un avviso. Il recupero fa parte dell’SDK dalla versione 1.10.0.

Comandi, Funzioni del Dispositivo e Topic

Preferisci lo stato. Quasi tutto ciò che gli utenti “comandano” è uno stato desiderato: un setpoint, acceso o spento, una ricetta. Scrivilo in una tabella, come sopra.

I comandi che devono essere eseguiti una sola volta, come stampare un’etichetta o avviare un ciclo di pulizia, sono righe con un id in una tabella scritta dalla board. Il dispositivo registra gli id già eseguiti in una tabella propria, così un comando ricevuto due volte viene eseguito una volta sola.

Le funzioni del dispositivo sono procedure che un dispositivo registra con registerDeviceFunction. Usale quando chi chiama ha bisogno subito della risposta del dispositivo:

  • Agenti AI: uno strumento in ai-template.yml chiama la funzione sul dispositivo scelto dall’assistente. Vedi Agenti e Strumenti AI.
  • I widget Interruttore e SCADA sulla board le chiamano mentre il dispositivo è online.
  • I tuoi container e dispositivi si chiamano a vicenda con callDeviceFunction.

Restituisci da una funzione del dispositivo testo leggibile o un piccolo JSON, e restituisci un esito negativo previsto (una macchina spenta) come risultato, invece di sollevare un errore.

I topic trasportano tra le parti della tua app messaggi che non servono più dopo la consegna: publish e subscribe su nomi nel tuo namespace, come myapp.events. La board non può sottoscriversi ai topic: tutto ciò che mostra deve trovarsi in una tabella. I messaggi pubblicati mentre un dispositivo è offline non vengono memorizzati, quindi usa una tabella per tutto ciò che deve arrivare a destinazione.

Il Container sul Dispositivo

Cosa fornisce l’agente del dispositivo al tuo container e cosa si aspetta da esso:

  • Identità: l’agente imposta DEVICE_KEY, DEVICE_NAME, DEVICE_SERIAL_NUMBER, SWARM_KEY, APP_KEY, APP_NAME, ENV (DEV o PROD), TUNNEL_DOMAIN, DEVICE_LAN_IP e DEVICE_PORT_FOR_<port>, oltre alla credenziale propria dell’app per l’SDK. L’SDK legge ciò di cui ha bisogno; non inserire mai nessuno di questi valori direttamente nel codice.
  • Parametri: un’installazione di produzione riceve i valori impostati dagli utenti e, per gli altri, il defaultValue di ciascun parametro. Un’esecuzione di sviluppo riceve solo i valori predefiniti. Dai ai parametri nomi specifici (PLC_HOST, non HOST).
  • Archiviazione: un’app a immagine singola riceve /data, una cartella privata dell’app e conservata tra riavvii e aggiornamenti (viene eliminata insieme all’app), e /shared, condivisa da tutte le app sul dispositivo. Mantieni comunque lo stato dell’app nelle tabelle e usa queste cartelle per i file che appartengono al dispositivo, come un modello scaricato o una cache.
  • Hardware: un container a immagine singola viene eseguito in modalità privilegiata, con accesso al /dev del dispositivo.
  • Log: tutto ciò che il tuo programma scrive su stdout e stderr costituisce il log dell’app in IronFlock. Scrivilo senza buffering (python -u).
  • Avvio: 30 secondi dopo la partenza il container deve essere ancora in esecuzione, altrimenti l’avvio viene considerato fallito.
  • Crash: quando un’app di produzione termina, l’agente la riavvia, attendendo più a lungo dopo ogni fallimento. Un’esecuzione di sviluppo non viene riavviata. Gestisci nel tuo programma i fallimenti previsti, come un sensore che non risponde, e lascia che termini in caso di errori da cui non può riprendersi.
  • Arresto: il tuo programma è il processo principale del container (PID 1), che ignora il segnale di arresto SIGTERM a meno che non lo gestisca. Fai terminare il programma alla ricezione di SIGTERM, altrimenti l’arresto dell’app resta in attesa finché Docker non termina il processo forzatamente. Avvia il programma in forma exec (CMD ["python", "-u", "main.py"]), non tramite una shell.
  • Esecuzioni di sviluppo: eseguire l’app dall’IDE cloud è un’esecuzione di sviluppo (ENV è DEV). Scrive nelle tabelle di sviluppo dell’app, riceve i valori predefiniti dei parametri e nessun tunnel di accesso remoto.

Docker Compose

Usa un docker-compose.yml quando la tua app esegue più processi, oppure include servizi di terze parti, come un broker MQTT o un database, accanto al tuo codice. Vedi Docker & Compose per le basi. Rispetto a un’immagine singola:

  • Ogni servizio riceve l’ambiente iniettato. L’agente fornisce a ogni servizio un proprio file d’ambiente con le variabili indicate sopra; un env_file: scritto da te viene sostituito. Un ${VAR} nel file viene risolto con i valori iniettati.
  • Nessun /data. I servizi non ricevono alcuna cartella dell’app. Conserva i file in volumi con nome: sopravvivono ad arresti e aggiornamenti, e vengono eliminati quando l’app viene rimossa dal dispositivo.
  • Le porte pubblicate vengono rimappate. Ogni porta pubblicata riceve una porta host libera su tutte le interfacce del dispositivo, e DEVICE_PORT_FOR_<port> indica al tuo codice quella reale. In port-template.yml, usa la porta host così come è scritta nel file compose. Pubblica solo ciò che gli utenti devono raggiungere, e lascia che i tuoi servizi comunichino tra loro sulla rete compose tramite il nome del servizio. Una porta host scritta come ${VAR} non viene gestita: non riceve né rimappatura né tunnel.
  • Non viene aggiunto nulla. L’agente non concede ai servizi compose né privilegi né dispositivi. Dichiara tu stesso privileged, devices o device_cgroup_rules. Una voce devices: indica hardware che deve esistere, altrimenti il container non si avvia; device_cgroup_rules insieme a un volume /dev funziona su ogni dispositivo.
  • Niente profiles:. Un servizio con profili viene escluso dalla release.
  • La stessa regola di avvio. Entro 30 secondi ogni container deve essere in esecuzione o essere terminato con codice 0, e almeno uno deve essere in esecuzione. Assegna restart: unless-stopped ai servizi di lunga durata.

Web UI sul Dispositivo

Un web server nel tuo container diventa raggiungibile da ovunque attraverso il tunnel di accesso remoto della sua porta, e la board lo mostra:

  1. Servi su una porta fissa (in ascolto su 0.0.0.0) e dichiarala in port-template.yml con protocol: http. Un’installazione di produzione attiva il tunnel e lo serve su HTTPS, websocket compresi.
  2. Scrivi l’URL del tunnel in una tabella. Il metodo getRemoteAccessUrlForPort(port) dell’SDK restituisce l’URL. All’avvio, scrivilo nella riga del dispositivo in una tabella dei dispositivi. Solo le installazioni di produzione hanno tunnel, quindi un’esecuzione di sviluppo non scrive alcun URL.
  3. Incorporalo nella board. Collega l’URL del widget Embed a quella colonna, su una pagina per dispositivo che filtra in base al dispositivo. Nell’UI Builder si tratta di un collegamento ai dati come un altro.

Per la pagina stessa:

  • Chi può aprirla: un tunnel http è privato. Possono aprirlo solo le persone che hanno effettuato l’accesso a IronFlock e hanno accesso al dispositivo, a meno che un utente non renda pubblica la porta. Sulla rete del dispositivo stesso la pagina è invece aperta a tutti, quindi non mostrare lì nulla che la rete dello stabilimento non debba vedere, e richiedi un tuo login per qualsiasi operazione che modifichi lo stato della macchina.
  • URL relativi: carica le risorse, chiama la tua API e apri i websocket con percorsi relativi alla pagina, così che funzioni sulla rete del dispositivo, attraverso il tunnel e dietro un reverse proxy.
  • Push, mai polling: invia gli aggiornamenti tramite un websocket.
  • Nessuna credenziale della piattaforma nel browser: la pagina comunica con il proprio server nel container, ed è quel server a mantenere la connessione SDK.

Per mostrare l’HMI web di una macchina sulla rete dello stabilimento al posto di una tua pagina, inoltralo tramite reverse proxy dal tuo container. Vedi Incorporare HMI Esterni.

Più Dispositivi

  • Ogni riga porta con sé il proprio dispositivo: IronFlock memorizza in device_key la chiave del dispositivo che ha scritto una riga. Usa device_key come chiave delle tabelle per dispositivo, memorizza il nome del dispositivo in una colonna con path: kwargs.DEVICE_NAME e suddividi i grafici in base a quel nome.
  • Un elenco dei dispositivi e una pagina per dispositivo: una navigazione laterale costruita a partire dalla tabella dei dispositivi, con un’unica pagina che funziona per ogni dispositivo e filtra i propri widget in base al dispositivo indicato nel percorso. Vedi Creare Dashboard.
  • Impostazioni per dispositivo o per macchina: usa come chiave della tabella delle impostazioni una colonna che identifica la destinazione (le righe scritte dalla board non hanno un proprio dispositivo), e compilala nel form sulla pagina della destinazione. Ogni dispositivo applica la riga più recente per la propria chiave.
  • Un dispositivo rimosso conserva le sue righe. Se gli utenti devono poterlo togliere dagli elenchi, aggiungi alla tabella dei dispositivi una colonna deleted e alla board un form che la imposta.

Lista di Controllo

  1. Tutto lo stato è nelle tabelle, ciascuna con un’unica fonte di scrittura indicata nella sua descrizione, e ogni colonna scritta dal codice è dichiarata in data-template.yml.
  2. Ogni riga ha un tsp, e le righe di stato vengono scritte quando qualcosa cambia, solo con la chiave e le colonne modificate.
  3. Lo stato scritto dalla board viene seguito con i tre passi descritti sopra, con l’SDK 1.10.0 o successivo, e il codice non si occupa mai della connessione né delle sue sessioni.
  4. Le scritture in tabella nei cicli di lunga durata gestiscono l’errore che una scrittura solleva quando la piattaforma è irraggiungibile da un minuto, e i fallimenti previsti vengono segnalati con reportError e un userMessage.
  5. Il programma termina alla ricezione di SIGTERM, si avvia in forma exec e scrive i log senza buffering.
  6. Niente fa polling: la board si collega ai dati, i dispositivi si sottoscrivono, le web UI inviano gli aggiornamenti in push.
  7. Ogni porta servita dalla tua app è dichiarata in port-template.yml, il suo URL proviene da getRemoteAccessUrlForPort e viene scritto in una tabella, e la board lo incorpora da lì.
  8. Un’app compose non ha env_file:, né profiles:, né porte host ${VAR}, conserva i file in volumi con nome e pubblica solo le porte di cui gli utenti hanno bisogno.

Inizia dall’App di Partenza

Il plugin IronFlock per Claude Code contiene un’app di partenza che segue questo blueprint: un programma che su ogni dispositivo legge i dati di una macchina, mantiene nelle tabelle lo stato e le letture di ciascun dispositivo, applica il limite che gli utenti impostano sulla board e serve una vista live che la board incorpora nella pagina di ciascun dispositivo. Claude Code crea a partire da essa una nuova app e la adatta al tuo caso d’uso.

Last updated on