Skip to Content
Sviluppo App IoTData Backend

Data Backend

IronFlock provvede un database privato per ogni progetto, basato su TimescaleDB. La tua app definisce lo schema dati; IronFlock crea le tabelle e inizia a raccogliere dati nel momento in cui un dispositivo viene aggiunto all’app.

Come Funziona

  1. Definisci il tuo schema dati in .ironflock/data-template.yml.
  2. Usa l’IronFlock SDK per pubblicare dati dal tuo codice edge.
  3. IronFlock configura automaticamente le tabelle del database in ogni progetto dove l’app è installata.
  4. I dati fluiscono dai dispositivi attraverso il sistema di messaggistica nel database del progetto.

Ogni progetto ha il proprio database fisico — non c’è condivisione di dati tra progetti.

L’utente ha il pieno controllo sui dati raccolti dalla tua app nel suo progetto. Come sviluppatore, non hai accesso a questi dati.

Definire lo Schema Dati

Crea un file data-template.yml nella directory .ironflock/:

data: tables: - tablename: sensordata columns: - id: tsp name: Timestamp description: Timestamp of measurement path: args[0].timestamp dataType: timestamp - id: temperature name: Temperature description: Temperature reading in Celsius path: args[0].temperature dataType: numeric - id: humidity name: Humidity description: Relative humidity percentage path: args[0].humidity dataType: numeric - id: device_id name: Device ID description: Source device identifier path: args[0].device_id dataType: string

Opzioni delle Colonne

CampoDescrizione
idIdentificatore interno della colonna (usa tsp per le colonne timestamp)
nameNome leggibile della colonna mostrato nelle dashboard
descriptionDescrizione opzionale
pathPercorso al valore nell’oggetto dati pubblicato (es. args[0].temperature)
dataTypeUno tra: timestamp, numeric, string, boolean
secretCifra questa colonna a riposo e non la restituisce mai in chiaro in una lettura normale — vedi Colonne Segrete più avanti

Colonne Segrete

Alcuni valori devono essere memorizzati ma non devono mai essere mostrati: un token API, la password di un dispositivo, una chiave di licenza. Contrassegna la colonna con secret: true e il data backend la cifra al momento dell’inserimento:

- tablename: credentials columns: - id: tsp dataType: timestamp - id: device_id dataType: string - id: api_token dataType: string secret: true

Da quel momento nessuna lettura ordinaria restituisce il valore in chiaro — nemmeno all’app che lo ha scritto:

Percorso di letturaCosa ottieni in risposta
Board, widget e tutto il resto attraverso il router dei messaggiil segnaposto __secret__
Accesso SQL (il login Postgres di FleetDB Access)il testo cifrato memorizzato, ifsec:1:…
La funzione di rivelazione dell’SDKil valore decifrato

NULL resta NULL ovunque, così “impostato ma nascosto” rimane distinguibile da “mai scritto”.

Rileggere un segreto in chiaro è possibile solo dai container dell’app stessa, attraverso l’SDK — vedi Colonne Segrete nel riferimento dell’SDK. Le board e le altre app non possono ottenerlo in alcun modo, ed è questo che rende “mai visualizzato in chiaro” una garanzia reale e non solo formale.

Aggiornare una riga senza perderne il segreto. Su una tabella entità, un client che modifica una riga non può mai rispedire il segreto reale — le letture gli hanno sempre e solo consegnato il segnaposto. Il data backend tratta perciò il segnaposto __secret__ (e la maschera •••••••• visualizzata dalle board) come mantieni il valore precedente in scrittura, e anche una colonna segreta omessa lo mantiene. Inviare un null esplicito cancella il segreto. Modificare la descrizione di una macchina in un form della board, quindi, non ne cancella mai il codice di accesso.

Ne derivano due conseguenze: le stringhe letterali __secret__ e •••••••• non possono a loro volta essere memorizzate come valori segreti (e il testo cifrato grezzo ifsec:… viene rifiutato come input), e su una tabella senza chiave di entità una scrittura del segnaposto viene rifiutata — non esiste alcuna riga precedente da mantenere.

Vale la pena tenere conto di tre conseguenze, perché sono limiti invalicabili e non semplici raccomandazioni:

  • Le colonne segrete sono solo stringhe. La cifratura produce testo, quindi le colonne numeric, boolean e timestamp non possono essere segrete. Nemmeno la colonna obbligatoria tsp può essere segreta.
  • Non puoi filtrare, raggruppare o ordinare per una colonna segreta. Ogni riga è cifrata con un proprio valore casuale, quindi due righe che contengono lo stesso segreto memorizzano testi cifrati diversi. Filtri di uguaglianza, GROUP BY, ORDER BY e DISTINCT su una colonna segreta semplicemente non possono funzionare. Per rispondere alla domanda “questo valore corrisponde?”, usa la funzione di verifica dell’SDK, che effettua il confronto all’interno del data backend invece di restituire alcunché.
  • Una colonna segreta non può essere una chiave di entità. Poiché il testo cifrato di ogni riga è diverso, le letture della riga più recente per entità tratterebbero ciascuna riga come un’entità a sé, quindi indicarne una in maintainLatestFlagFor viene rifiutato senza appello.

Le ultime due restrizioni vengono controllate durante la validazione del data template, perciò una tabella che le viola fallisce al momento del rilascio invece di comportarsi in modo scorretto in seguito.

Opzioni delle Tabelle

Oltre a columns, una tabella accetta alcune chiavi opzionali che controllano come viene descritta e come invecchiano i suoi dati:

data: tables: - tablename: sensordata description: Letture ambientali dal reparto produzione chunkTimeInterval: 1 hour dropAfter: 30 days columns: # ...
CampoDescrizione
tablenameNome della tabella
descriptionDescrizione opzionale, mostrata nell’interfaccia e usata dagli agenti AI per comprendere la tabella
chunkTimeIntervalDimensione delle partizioni temporali in cui la tabella viene suddivisa. Predefinito 7 days
dropAfterFinestra di conservazione — le partizioni più vecchie vengono eliminate automaticamente
downsampleMantiene una copia pre-aggregata per grafici rapidi su finestre lunghe — vedi Downsampling Continuo più avanti
maintainLatestFlagForColonne che identificano un’entità univoca — vedi Tracciare lo Stato Corrente di un’Entità più avanti
privateNasconde questa tabella alle altre app — vedi Condividere Dati con Altre App più avanti

chunkTimeInterval controlla come i dati time-series vengono partizionati su disco. Scegli un valore per cui una partizione corrisponda all’incirca a ciò che interroghi in una volta: i dati ad alta frequenza raccolti ogni secondo traggono vantaggio da chunk piccoli (da minuti a ore), quelli che cambiano lentamente da chunk grandi (settimane). È solo il valore predefinito dell’app — il proprietario del progetto può modificarlo in seguito sul proprio data backend.

dropAfter trasforma la tabella in una finestra scorrevole. Vengono eliminate intere partizioni più vecchie dell’intervallo indicato, il che è molto più economico che cancellare singole righe. Il job di pulizia viene eseguito con cadenza dropAfter / 4, quindi un record può sopravvivere alla propria scadenza fino a un quarto dell’intervallo prima che la sua partizione venga rimossa. Ometti dropAfter per conservare i dati a tempo indeterminato.

Entrambi accettano stringhe di intervallo PostgreSQL — 30 minutes, 1 hour, 7 days, 6 months.

Downsampling Continuo

Le dashboard possono chiedere al database di aggregare i dati — medie orarie, totali giornalieri, conteggi per macchina. Calcolarli dai record grezzi va benissimo per un giorno ed è costoso per un anno. Aggiungi downsample a una tabella e la piattaforma ne mantiene una copia pre-aggregata, aggiornata di continuo, e risponde da quella copia alle query su finestre lunghe:

data: tables: - tablename: sensordata dropAfter: 30 days downsample: bucket: 1 minute keepFor: 2 years paths: - payload.temperature columns: # ...
CampoDescrizione
bucketGranularità della copia pre-aggregata. Predefinito 1 minute
keepForPer quanto tempo conservare la cronologia sottocampionata. Ometti per conservarla a tempo indeterminato
pathsPercorsi dei campi JSON da includere, nella stessa notazione usata dalle dashboard

bucket è la risoluzione più fine con cui può essere servito un grafico — un grafico che richiede intervalli molto più fini legge invece la tabella grezza. Accetta intervalli a larghezza fissa da 1 second a 1 day che dividono la giornata in modo esatto (1 minute, 5 minutes, 1 hour). Il valore predefinito di 1 minute è adatto praticamente a ogni dashboard; un bucket più grossolano costa meno spazio di archiviazione e meno throughput in scrittura.

keepFor è ciò che rende possibili le cronologie lunghe. I record grezzi spariscono con dropAfter, ma la copia sottocampionata ha una propria conservazione: conserva i dati grezzi per 30 giorni e quelli sottocampionati per 2 anni, e una board potrà comunque tracciare due anni di medie orarie occupando una frazione dello spazio. Impostalo su un valore più lungo di dropAfter — la piattaforma rifiuta il caso opposto come configurazione errata.

paths estende il downsampling ai valori all’interno delle colonne JSON. Le colonne numeriche vengono incluse automaticamente; i campi JSON devono essere indicati esplicitamente, poiché una colonna JSON non ha un insieme fisso di chiavi. I campi non dichiarati funzionano comunque nelle dashboard — vengono semplicemente calcolati dalla tabella grezza.

Tutto il resto è automatico. Per ogni colonna numerica vengono mantenute le statistiche (media, somma, minimo, massimo, primo, ultimo e un conteggio dei record), raggruppate per la chiave di entità della tabella (maintainLatestFlagFor, oppure il dispositivo che pubblica). Le dashboard non richiedono alcuna configurazione né devono esserne a conoscenza: un widget interroga come sempre e la piattaforma decide query per query se la copia pre-aggregata è in grado di rispondere — ricadendo in modo trasparente sulla tabella grezza quando non lo è, ad esempio quando un filtro fa riferimento a una colonna per cui la copia non raggruppa.

Le modifiche di schema ricostruiscono la copia. Aggiungere, rimuovere o cambiare il tipo di una colonna di una tabella sottocampionata — o modificare il blocco downsample stesso — comporta la ricostruzione della copia pre-aggregata a partire dalla tabella grezza. Tutto ciò che è più vecchio di dropAfter non può essere ricostruito e va perso. Dove possibile, imposta il blocco insieme alla tabella e considera le modifiche di schema successive su tabelle di lunga durata come una decisione consapevole.

Pubblicare Dati dal Codice Edge

Usa l’IronFlock SDK per inviare dati dalla tua app:

from ironflock import IronFlock flock = IronFlock() flock.publish_to_table("sensordata", { "timestamp": "2025-01-15T10:30:00Z", "temperature": 23.5, "humidity": 62.1, "device_id": "sensor-001" })

Per dati ad alta frequenza, invia molte righe in un singolo messaggio invece di un round-trip per riga usando publish_rows_to_table / publishRowsToTable (fire-and-forget) o append_rows_to_table / appendRowsToTable (restituisce l’esito dell’inserimento). Ogni batch viene inserito in modo atomico — tutto-o-niente. Consulta il riferimento dell’SDK per i dettagli.

Tabelle di Trasformazione

Puoi definire trasformazioni SQL che aggregano o elaborano automaticamente i tuoi dati grezzi:

data: tables: - tablename: sensordata columns: # ... raw data columns ... transforms: - tablename: hourly_averages materialize: true schedule: "0 * * * *" sql: > SELECT time_bucket('1 hour', tsp) AS hour, avg(temperature) AS avg_temp, avg(humidity) AS avg_humidity FROM sensordata GROUP BY hour columns: - id: hour name: Hour dataType: timestamp - id: avg_temp name: Average Temperature dataType: numeric - id: avg_humidity name: Average Humidity dataType: numeric
CampoDescrizione
tablenameNome della tabella derivata
materializeSe true, i risultati vengono persistiti come tabella
scheduleEspressione cron per quando viene eseguita la trasformazione
sqlQuery SQL che calcola la trasformazione
columnsDefinizioni delle colonne per l’output

Le tabelle di trasformazione sono accessibili nelle dashboard e tramite l’SDK, esattamente come le tabelle normali.

Una trasformazione viene letta esattamente come il suo SQL la restituisce: le finestre temporali dei widget, i filtri di calendario e la modalità latest si applicano alle tabelle e per una trasformazione vengono ignorati. Da qui derivano tre regole. Limita l’intervallo temporale all’interno della query (WHERE tsp > now() - interval '7 days'), ordina una serie temporale dalla più recente (ORDER BY <time column> DESC) così che un limite di righe mantenga le righe più recenti, e seleziona ogni colonna che una dashboard deve rappresentare o usare come filtro — una trasformazione non ha colonne timestamp o dispositivo implicite. Una singola lettura restituisce al massimo 3000 righe, quindi aggrega nella query.

Distribuire una trasformazione insieme all’app non è l’unico modo per averne una: un membro del progetto con il privilegio Data Access può salvare lo stesso tipo di trasformazione dalla vista dati del progetto, senza alcuna app, e lo stesso può fare l’assistente AI. Vedi Trasformazioni personalizzate.

Tracciare lo Stato Corrente di un’Entità

Per le tabelle che rappresentano lo stato corrente di entità del mondo reale — macchine, asset, ordini di produzione — IronFlock supporta un pattern chiamato tracciamento dello stato corrente.

Invece di sovrascrivere una riga quando qualcosa cambia, si aggiunge sempre una nuova riga. Dichiari quali colonne identificano un’entità univoca e IronFlock deriva la riga più recente per ogni entità ogni volta che la tabella viene letta. Questo ti dà un’intera cronologia di ogni modifica rendendo allo stesso tempo facile interrogare solo lo stato corrente.

Abilitala su una tabella con maintainLatestFlagFor:

- tablename: machineform maintainLatestFlagFor: ['machinename'] columns: - id: tsp dataType: timestamp - id: machinename dataType: string - id: machinetype dataType: string - id: active dataType: boolean - id: description dataType: string

maintainLatestFlagFor accetta una lista di colonne che insieme identificano un’entità univoca. Nulla viene scritto nella riga stessa: IronFlock indicizza la tabella in base a questa chiave di entità più il timestamp e seleziona la riga più recente per ogni entità al momento della query. Una riga che arriva in ritardo o fuori ordine non può quindi mai lasciare dietro di sé un marcatore obsoleto.

Aggiornare un’Entità con una Riga Parziale

Una riga accodata a una tabella entità è una nuova versione di quell’entità — e non deve necessariamente essere completa. Le colonne che la riga non fornisce vengono ereditate dalla riga più recente precedente dell’entità, quindi aggiornare un singolo campo significa pubblicare soltanto la chiave di entità, un timestamp e quel campo:

La colonna della nuova riga èLa versione memorizzata contiene
fornita (0, false e "" contano come valori forniti)il valore fornito
esplicitamente nullNULL — la colonna viene svuotata
assenteil valore della riga più recente precedente

Vale la pena conoscere due dettagli:

  • Una riga che fornisce tutte le colonne salta completamente la ricerca della riga precedente, quindi le righe complete restano esattamente economiche quanto lo sono sempre state — continua a inviare righe complete sui percorsi ad alta frequenza.
  • L’ereditarietà rispetta il tempo: una riga che arriva con un timestamp più vecchio (un backfill) eredita solo dalle righe con tsp pari o anteriore al proprio, mai da quelle più recenti.

Le tabelle senza maintainLatestFlagFor mantengono la semantica di semplice append: una colonna che la riga non fornisce viene memorizzata come NULL.

Per interrogare solo gli stati correnti delle macchine:

SELECT DISTINCT ON (machinename) * FROM machineform ORDER BY machinename, tsp DESC

Per visualizzare l’intera cronologia di una macchina specifica:

SELECT * FROM machineform WHERE machinename = 'Assembly-Line-01' ORDER BY tsp

Raramente dovrai scrivere questa query a mano. I widget su una dashboard che si collegano a questa tabella dispongono di un interruttore latest nelle impostazioni del filtro, in modo che gli utenti vedano sempre i valori correnti senza lavoro extra. Dall’SDK, richiedi la stessa modalità aggiungendo {"latest": true} a filterAnd — consulta getHistory.

Migrazione da latest_flag: le versioni precedenti di IronFlock memorizzavano una colonna booleana fisica chiamata latest_flag. Quella colonna non esiste più — lo stato corrente viene invece derivato in SQL, il che lo mantiene corretto quando le righe arrivano fuori ordine. Le dashboard e le chiamate SDK esistenti che filtrano per latest_flag = true continuano a funzionare: IronFlock le riconosce e applica la modalità stato corrente. Il codice nuovo dovrebbe usare l’interruttore latest o la voce di filtro {"latest": true}.

Eliminazione Logica dei Record

Il modello append-only di IronFlock significa che i record non vengono mai eliminati fisicamente. Invece, usa una colonna booleana deleted per marcare un record come rimosso. Questo preserva la traccia di audit completa nascondendo i record eliminati dalle dashboard.

Aggiungi una colonna deleted a qualsiasi tabella entità:

- id: deleted name: Deleted dataType: boolean

Quando un utente elimina un record (ad esempio tramite un form sulla dashboard), la tua app pubblica una nuova riga per quell’entità con deleted: true. Combinato con maintainLatestFlagFor, questa nuova riga diventa lo stato più recente. Le righe parziali successive ereditano il marcatore deleted come qualsiasi altra colonna, quindi un aggiornamento che non menziona deleted non resuscita l’entità né la nasconde.

Per interrogare solo i record attivi (non eliminati) correnti:

SELECT * FROM ( SELECT DISTINCT ON (machinename) * FROM machineform ORDER BY machinename, tsp DESC ) latest WHERE deleted IS NULL OR deleted = false

Il controllo su deleted viene eseguito dopo che è stata selezionata la riga più recente di ogni macchina. Quest’ordine è importante: filtrare prima le righe eliminate farebbe riemergere come stato corrente la riga precedente, non eliminata.

I widget della dashboard e l’SDK applicano automaticamente lo stesso ordine — combina l’interruttore latest (o {"latest": true}) con un filtro su deleted e ottieni esattamente questo comportamento. I record eliminati spariscono dalla dashboard immediatamente dopo l’invio del form, ma rimangono nel database per la cronologia e scopi di audit.

Condividere Dati con Altre App

Il tuo data backend è privato della tua app: nessun’altra app installata nel progetto può vedere le tue tabelle. Due chiavi opzionali in data-template.yml cambiano questo.

Per leggere i dati di un’altra app, elenca le app da cui vuoi leggere in una sezione consumes: di primo livello — accanto a data:, non al suo interno:

consumes: - app: machine-monitor reason: "Calcola l'OEE dai flussi di stato macchina e contatori del monitor" data: tables: - tablename: oee_results columns: # ... le tabelle della tua app, come sempre

app è il nome tecnico dell’app fornitrice, oppure "*" (le virgolette sono obbligatorie) per tutte le app del progetto. reason viene mostrata all’utente nella finestra di consenso — la dichiarazione da sola non concede nulla finché non viene approvata.

Per tenere per te singole tabelle, marcale con private: true. Tutto ciò che definisci è condivisibile per impostazione predefinita; una tabella o trasformazione privata non compare mai nel catalogo che vedono le altre app.

data: tables: - tablename: measurements # condivisa (predefinito) columns: [ ... ] - tablename: calibration_state # interna — mai visibile alle altre app private: true columns: [ ... ]

L’accesso è in sola lettura, concesso dall’utente per progetto e revocabile in qualsiasi momento. Vedi Consumare Dati da Altre App per il modello completo e le chiamate SDK che leggono la cronologia e i flussi live di un’app fornitrice.

Last updated on