Backend de données
IronFlock provisionne une base de données privée pour chaque projet, propulsée par TimescaleDB. Votre application définit le schéma de données ; IronFlock crée les tables et commence à collecter les données dès qu’un appareil est ajouté à l’application.
Fonctionnement
- Définissez votre schéma de données dans
.ironflock/data-template.yml. - Utilisez le SDK IronFlock pour publier des données depuis votre code edge.
- IronFlock configure automatiquement les tables de base de données dans chaque projet où l’application est installée.
- Les données circulent des appareils via le système de messagerie vers la base de données du projet.
Chaque projet dispose de sa propre base de données physique — il n’y a aucun partage de données entre les projets.
L’utilisateur a le contrôle total sur les données collectées par votre application dans son projet. En tant que développeur, vous n’avez pas accès à ces données.
Définir le schéma de données
Créez un fichier data-template.yml dans le répertoire .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: stringOptions des colonnes
| Champ | Description |
|---|---|
id | Identifiant interne de la colonne (utilisez tsp pour les colonnes timestamp) |
name | Nom de colonne lisible affiché dans les boards |
description | Description optionnelle |
path | Chemin vers la valeur dans l’objet de données publié (p. ex. args[0].temperature) |
dataType | L’un des types : timestamp, numeric, string, boolean |
secret | Chiffre cette colonne au repos et ne la renvoie jamais en clair lors d’une lecture normale — voir Colonnes secrètes plus bas |
Colonnes secrètes
Certaines valeurs doivent être stockées sans jamais être affichées : un jeton d’API, un mot de passe d’appareil, une clé de licence. Marquez la colonne secret: true et le backend de données la chiffre à l’insertion :
- tablename: credentials
columns:
- id: tsp
dataType: timestamp
- id: device_id
dataType: string
- id: api_token
dataType: string
secret: trueDès lors, aucune lecture ordinaire ne renvoie la valeur en clair — pas même à l’application qui l’a écrite :
| Chemin de lecture | Ce que vous obtenez en retour |
|---|---|
| Les boards, les widgets et tout le reste via le routeur de messages | l’espace réservé __secret__ |
| L’accès SQL (l’identifiant Postgres FleetDB Access) | le texte chiffré stocké, ifsec:1:… |
| La fonction de révélation du SDK | la valeur déchiffrée |
NULL reste NULL partout, de sorte que « défini mais masqué » reste distinguable de « jamais écrit ».
Relire un secret en clair n’est possible que depuis les conteneurs de l’application elle-même, via le SDK — voir Colonnes secrètes dans la référence du SDK. Les boards et les autres applications ne peuvent pas l’obtenir du tout, et c’est ce qui rend « jamais affiché en clair » vrai plutôt que simplement cosmétique.
Mettre à jour une ligne sans perdre son secret. Sur une table d’entités, un client qui modifie une ligne ne peut jamais renvoyer le vrai secret — les lectures ne lui ont jamais donné que l’espace réservé. Le backend de données traite donc l’espace réservé __secret__ (ainsi que le masque •••••••• affiché par les boards) comme conserver la valeur précédente à l’écriture, et une colonne secrète omise la conserve elle aussi. Envoyer un null explicite efface le secret. Modifier la description d’une machine dans un formulaire de board n’efface donc jamais son code d’accès.
Deux conséquences en découlent : les chaînes littérales __secret__ et •••••••• ne peuvent pas elles-mêmes être stockées comme valeurs secrètes (et un texte chiffré brut ifsec:… est rejeté en entrée), et sur une table sans clé d’entité une écriture d’espace réservé est rejetée — il n’existe aucune ligne précédente à conserver.
Trois conséquences méritent d’être anticipées, car ce sont des limites strictes et non de simples recommandations :
- Les colonnes secrètes sont exclusivement des chaînes. Le chiffrement produit du texte, donc les colonnes
numeric,booleanettimestampne peuvent pas être secrètes. La colonnetspobligatoire ne peut pas être secrète non plus. - Vous ne pouvez ni filtrer, ni grouper, ni trier sur une colonne secrète. Chaque ligne est chiffrée avec sa propre valeur aléatoire, donc deux lignes contenant le même secret stockent un texte chiffré différent. Les filtres d’égalité,
GROUP BY,ORDER BYetDISTINCTsur une colonne secrète ne peuvent tout simplement pas fonctionner. Pour répondre à la question « cette valeur correspond-elle ? », utilisez la fonction de vérification du SDK, qui compare à l’intérieur du backend de données au lieu de renvoyer quoi que ce soit. - Une colonne secrète ne peut pas servir de clé d’entité. Comme le texte chiffré diffère d’une ligne à l’autre, les lectures « état le plus récent par entité » traiteraient chaque ligne comme une entité distincte : nommer une colonne secrète dans
maintainLatestFlagForest donc rejeté d’emblée.
Les deux dernières restrictions sont vérifiées lors de la validation de votre data-template : une table qui les enfreint échoue donc à la publication de la version, au lieu de mal se comporter plus tard.
Options des tables
Outre columns, une table accepte quelques clés optionnelles qui contrôlent la façon dont elle est décrite et dont ses données vieillissent :
data:
tables:
- tablename: sensordata
description: Relevés environnementaux de l'atelier
chunkTimeInterval: 1 hour
dropAfter: 30 days
columns:
# ...| Champ | Description |
|---|---|
tablename | Nom de la table |
description | Description optionnelle, affichée dans l’interface et utilisée par les agents IA pour comprendre la table |
chunkTimeInterval | Taille des partitions temporelles dans lesquelles la table est découpée. Par défaut 7 days |
dropAfter | Fenêtre de rétention — les partitions plus anciennes sont supprimées automatiquement |
downsample | Maintient une copie pré-agrégée pour des graphiques rapides sur de longues fenêtres — voir Sous-échantillonnage continu plus bas |
maintainLatestFlagFor | Colonnes identifiant une entité unique — voir Suivi de l’état actuel d’une entité plus bas |
private | Masque cette table aux autres applications — voir Partager des données avec d’autres applications plus bas |
chunkTimeInterval détermine la façon dont les données de séries temporelles sont partitionnées sur le disque. Choisissez-le pour qu’une partition corresponde à peu près à ce que vous interrogez en une fois : les données à haute fréquence collectées chaque seconde bénéficient de petits chunks (de quelques minutes à quelques heures), les données qui évoluent lentement de grands chunks (des semaines). Ce n’est que la valeur par défaut de l’application — le propriétaire du projet peut l’ajuster ensuite sur son propre backend de données.
dropAfter transforme la table en fenêtre glissante. Des partitions entières plus anciennes que l’intervalle indiqué sont supprimées, ce qui est bien moins coûteux que de supprimer des lignes une à une. La tâche de nettoyage s’exécute selon un rythme de dropAfter / 4 : un enregistrement peut donc survivre à son expiration pendant un quart de l’intervalle au maximum, avant que sa partition ne disparaisse. Omettez dropAfter pour conserver les données indéfiniment.
Les deux acceptent des chaînes d’intervalle PostgreSQL — 30 minutes, 1 hour, 7 days, 6 months.
Sous-échantillonnage continu
Les tableaux de bord peuvent demander à la base de données d’agréger les données — moyennes horaires, totaux journaliers, nombres d’enregistrements par machine. Calculer cela à partir des enregistrements bruts est acceptable sur une journée et coûteux sur une année. Ajoutez downsample à une table et la plateforme en maintient une copie pré-agrégée, continuellement mise à jour, puis répond aux requêtes portant sur de longues fenêtres à partir de cette copie :
data:
tables:
- tablename: sensordata
dropAfter: 30 days
downsample:
bucket: 1 minute
keepFor: 2 years
paths:
- payload.temperature
columns:
# ...| Champ | Description |
|---|---|
bucket | Granularité de la copie pré-agrégée. Par défaut 1 minute |
keepFor | Durée de conservation de l’historique sous-échantillonné. Omettez-le pour le conserver indéfiniment |
paths | Chemins de champs JSON à inclure, dans la même notation que celle des tableaux de bord |
bucket est la résolution la plus fine à partir de laquelle un graphique peut être servi — un graphique demandant des intervalles nettement plus fins lit la table brute à la place. Il accepte des intervalles de largeur fixe de 1 second à 1 day qui divisent une journée de façon régulière (1 minute, 5 minutes, 1 hour). La valeur par défaut de 1 minute convient à pratiquement tous les tableaux de bord ; un intervalle plus grossier coûte moins de stockage et moins de débit en écriture.
keepFor est ce qui rend les historiques longs possibles. Les enregistrements bruts disparaissent avec dropAfter, mais la copie sous-échantillonnée a sa propre rétention : conservez les données brutes 30 jours et les données sous-échantillonnées 2 ans, et un board pourra toujours tracer deux ans de moyennes horaires pour une fraction du stockage. Définissez-le plus long que dropAfter — la plateforme refuse l’inverse comme une erreur de configuration.
paths étend le sous-échantillonnage aux valeurs situées à l’intérieur de colonnes JSON. Les colonnes numériques sont incluses automatiquement ; les champs JSON doivent être nommés explicitement, car une colonne JSON n’a pas d’ensemble de clés fixe. Les champs non déclarés fonctionnent quand même dans les tableaux de bord — ils sont simplement calculés à partir de la table brute.
Tout le reste est automatique. Chaque colonne numérique voit ses statistiques maintenues (moyenne, somme, minimum, maximum, première et dernière valeur, ainsi qu’un nombre d’enregistrements), groupées par la clé d’entité de la table (maintainLatestFlagFor, ou l’appareil publiant). Les tableaux de bord n’ont besoin d’aucune configuration ni même d’en avoir connaissance : un widget interroge comme d’habitude, et la plateforme décide requête par requête si la copie pré-agrégée peut y répondre — en se rabattant de façon transparente sur la table brute lorsque ce n’est pas le cas, par exemple quand un filtre porte sur une colonne selon laquelle la copie ne regroupe pas.
Les changements de schéma reconstruisent la copie. Ajouter, supprimer ou retyper une colonne d’une table sous-échantillonnée — ou modifier le bloc
downsamplelui-même — reconstruit la copie pré-agrégée à partir de la table brute. Tout ce qui est plus ancien quedropAfterne peut pas être reconstitué et est perdu. Mettez ce bloc en place en même temps que la table lorsque c’est possible, et traitez les changements de schéma ultérieurs sur des tables à longue durée de vie comme une décision délibérée.
Publier des données depuis le code edge
Utilisez le SDK IronFlock pour envoyer des données depuis votre application :
Python
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"
})Pour les données à haute fréquence, envoyez plusieurs lignes dans un seul message au lieu d’un aller-retour par ligne en utilisant publish_rows_to_table / publishRowsToTable (fire-and-forget) ou append_rows_to_table / appendRowsToTable (qui retourne le résultat de l’insertion). Chaque lot est inséré de manière atomique — tout ou rien. Consultez la référence du SDK pour plus de détails.
Tables de transformation
Vous pouvez définir des transformations SQL qui agrègent ou traitent automatiquement vos données brutes :
data:
tables:
- tablename: sensordata
columns:
# ... colonnes de données brutes ...
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| Champ | Description |
|---|---|
tablename | Nom de la table dérivée |
materialize | Si true, les résultats sont persistés sous forme de table |
schedule | Expression cron indiquant quand la transformation s’exécute |
sql | Requête SQL qui calcule la transformation |
columns | Définitions des colonnes de sortie |
Les tables de transformation sont accessibles dans les boards et via le SDK, exactement comme les tables ordinaires.
Une transformation est lue exactement telle que son propre SQL la retourne : les fenêtres temporelles des widgets, les filtres de calendrier et le mode latest s’appliquent aux tables et sont ignorés pour une transformation. Trois règles en découlent. Bornez la plage de temps dans la requête (WHERE tsp > now() - interval '7 days'), triez une série temporelle de la plus récente à la plus ancienne (ORDER BY <colonne de temps> DESC) afin qu’une limite de lignes conserve les lignes les plus récentes, et sélectionnez toutes les colonnes qu’un board doit tracer ou par lesquelles il doit filtrer — une transformation n’a pas de colonnes d’horodatage ou d’appareil implicites. Une lecture unique retourne au plus 3000 lignes : agrégez donc dans la requête.
Livrer une transformation avec l’application n’est pas le seul moyen d’en obtenir une : un membre du projet disposant du privilège Accès aux données peut enregistrer le même genre de transformation depuis la vue de données du projet, sans aucune application, et l’assistant IA le peut aussi. Voir Transformations personnalisées.
Suivi de l’état actuel d’une entité
Pour les tables représentant l’état actuel d’entités réelles — machines, actifs, ordres de production — IronFlock supporte un modèle appelé suivi de l’état le plus récent.
Au lieu d’écraser une ligne lorsqu’une valeur change, vous ajoutez toujours une nouvelle ligne. Vous déclarez quelles colonnes identifient une entité unique, et IronFlock en déduit la ligne la plus récente par entité à chaque lecture de la table. Vous obtenez ainsi l’historique complet de chaque modification tout en pouvant interroger facilement le seul état actuel.
Activez-le sur une table avec 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: stringmaintainLatestFlagFor prend une liste de colonnes identifiant une entité unique. Rien n’est écrit dans la ligne elle-même : IronFlock indexe la table selon cette clé d’entité et l’horodatage, puis sélectionne la ligne la plus récente de chaque entité au moment de la requête. Une ligne qui arrive en retard ou dans le désordre ne peut donc jamais laisser derrière elle un marqueur obsolète.
Mettre à jour une entité avec une ligne partielle
Une ligne ajoutée à une table d’entités est une nouvelle version de cette entité — et elle n’a pas besoin d’être complète. Les colonnes que la ligne ne fournit pas sont héritées de la précédente ligne la plus récente de l’entité : mettre à jour un seul champ revient donc à publier uniquement la clé d’entité, un horodatage et ce champ :
| La colonne de la nouvelle ligne est | La version stockée contient |
|---|---|
fournie (0, false et "" comptent comme fournis) | la valeur fournie |
explicitement null | NULL — la colonne est effacée |
| absente | la valeur de la précédente ligne la plus récente |
Deux détails méritent d’être connus :
- Une ligne qui fournit toutes les colonnes saute entièrement la recherche de la ligne précédente : les lignes complètes restent donc exactement aussi peu coûteuses qu’elles l’ont toujours été — continuez d’envoyer des lignes complètes sur les chemins à haute fréquence.
- L’héritage respecte le temps : une ligne qui arrive avec un horodatage plus ancien (un rattrapage) n’hérite que des lignes dont le
tspest antérieur ou égal au sien, jamais des plus récentes.
Les tables sans maintainLatestFlagFor conservent la sémantique d’ajout pur : une colonne que la ligne ne fournit pas est stockée comme NULL.
Pour n’interroger que l’état actuel des machines :
SELECT DISTINCT ON (machinename) *
FROM machineform
ORDER BY machinename, tsp DESCPour consulter l’historique complet d’une machine donnée :
SELECT * FROM machineform WHERE machinename = 'Assembly-Line-01' ORDER BY tspVous écrivez rarement cette requête à la main. Les widgets d’un board connectés à cette table disposent d’un commutateur latest dans leurs paramètres de filtre, de sorte que les utilisateurs voient toujours les valeurs actuelles sans effort supplémentaire. Depuis le SDK, activez le même mode en ajoutant {"latest": true} à filterAnd — voir getHistory.
Migration depuis
latest_flag: les versions précédentes d’IronFlock stockaient une colonne booléenne physique nomméelatest_flag. Cette colonne n’existe plus — l’état actuel est désormais dérivé en SQL, ce qui le garde correct lorsque les lignes arrivent dans le désordre. Les boards et les appels SDK existants qui filtrent surlatest_flag = truecontinuent de fonctionner : IronFlock les reconnaît et applique le mode « état le plus récent ». Le nouveau code doit utiliser l’option latest ou l’entrée de filtre{"latest": true}.
Suppression logique des enregistrements
Le modèle append-only d’IronFlock signifie que les enregistrements ne sont jamais physiquement supprimés. Utilisez plutôt une colonne booléenne deleted pour marquer un enregistrement comme supprimé. Cela préserve la piste d’audit complète tout en masquant les enregistrements supprimés dans les dashboards.
Ajoutez une colonne deleted à n’importe quelle table d’entités :
- id: deleted
name: Deleted
dataType: booleanLorsqu’un utilisateur supprime un enregistrement (par exemple via un formulaire sur le board), votre application publie une nouvelle ligne pour cette entité avec deleted: true. Combinée à maintainLatestFlagFor, cette nouvelle ligne devient l’état le plus récent. Les lignes partielles ultérieures héritent du marqueur deleted comme de n’importe quelle autre colonne, de sorte qu’une mise à jour qui ne mentionne pas deleted ne ressuscite pas l’entité et ne la masque pas non plus.
Pour ne consulter que les enregistrements actuels et actifs (non supprimés) :
SELECT * FROM (
SELECT DISTINCT ON (machinename) *
FROM machineform
ORDER BY machinename, tsp DESC
) latest
WHERE deleted IS NULL OR deleted = falseLe contrôle sur deleted s’applique après la sélection de la ligne la plus récente de chaque machine. Cet ordre est important : filtrer d’abord les lignes supprimées ferait remonter la ligne précédente, non supprimée, comme état actuel.
Les widgets des boards et le SDK appliquent automatiquement le même ordre — combinez le commutateur latest (ou {"latest": true}) avec un filtre sur deleted et vous obtenez exactement ce comportement. Les enregistrements supprimés disparaissent du dashboard dès l’envoi du formulaire, mais restent dans la base de données à des fins d’historique et d’audit.
Partager des données avec d’autres applications
Votre data backend appartient à votre seule application : aucune autre application installée dans le projet ne voit vos tables. Deux clés optionnelles de data-template.yml changent cela.
Pour lire les données d’une autre application, listez les applications concernées dans une section consumes: de premier niveau — à côté de data:, et non à l’intérieur :
consumes:
- app: machine-monitor
reason: "Calcule le TRS à partir des flux d'état machine et de compteurs du moniteur"
data:
tables:
- tablename: oee_results
columns:
# ... les tables propres à votre application, comme d'habitudeapp est le nom technique de l’application fournisseuse, ou "*" (les guillemets sont obligatoires) pour toutes les applications du projet. reason est affichée à l’utilisateur dans la boîte de dialogue de consentement — la déclaration seule n’accorde rien tant qu’il n’a pas approuvé.
Pour garder certaines tables privées, marquez-les private: true. Tout ce que vous définissez est partageable par défaut ; une table ou une transformation privée n’apparaît jamais dans le catalogue que voient les autres applications.
data:
tables:
- tablename: measurements # partagée (par défaut)
columns: [ ... ]
- tablename: calibration_state # interne — jamais visible par les autres applications
private: true
columns: [ ... ]L’accès est en lecture seule, accordé par l’utilisateur projet par projet, et révocable à tout moment. Voir Consommer les données d’autres applications pour le modèle complet et les appels SDK qui lisent l’historique et les flux en direct d’une application fournisseuse.