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 lectures d’historique et les lignes en temps réel : ce que reçoivent les boards, les widgets, le SDK et les autres applications qui lisent la table | l’espace réservé __secret__ |
| L’accès SQL (la console de requêtes SQL, les exports de données et l’identifiant Postgres FleetDB Access) et les sauvegardes de la base de données | 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()
await flock.start()
await flock.publish_to_table("sensordata", {
"timestamp": "2025-01-15T10:30:00Z",
"temperature": 23.5,
"humidity": 62.1,
"device_id": "sensor-001"
})Chaque ligne a besoin d’une valeur pour la colonne obligatoire tsp : le backend de données refuse une ligne qui n’en a pas. L’écriture dans la table par le SDK échoue alors avec une erreur ; une ligne publiée par un board est abandonnée, et seule une entrée dans la table error-logs de l’application en garde la trace. La définition de sensordata ci-dessus mappe cette colonne avec path: args[0].timestamp, si bien que tsp provient du champ timestamp de la ligne. Une colonne sans path lit le champ de la ligne qui porte le même nom (args[0].<id>) : sans ce mappage, chaque ligne aurait besoin de son propre champ tsp.
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 / PublishRowsToTable ou append_rows_to_table / appendRowsToTable / AppendRowsToTable — les deux sont identiques et retournent 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, le résultat est stocké et rafraîchi selon la planification. Si false, la transformation est une vue, calculée à chaque lecture |
schedule | Expression cron des exécutions de la transformation : cinq champs (minute, heure, jour du mois, mois, jour de la semaine), ou six avec un champ des secondes en tête. */5 * * * * s’exécute toutes les 5 minutes, */10 * * * * * toutes les 10 secondes. Chaque exécution rafraîchit un résultat matérialisé et envoie le résultat actuel aux boards liés à la transformation. Le code de l’application peut aussi lancer une telle exécution à la demande, voir Rafraîchir à la demande |
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.
Une transformation sert à ce qu’une lecture de table ne peut pas exprimer : une jointure, un ratio, une agrégation sur les lignes actuelles de plusieurs tables. Ce n’est pas le moyen d’afficher l’état actuel des entités ni de masquer les enregistrements supprimés. Ce sont des modes de lecture de la table elle-même, le commutateur latest et un filtre sur une colonne deleted (voir ci-dessous), et ils fonctionnent en direct : un widget lié à la table suit chaque nouvelle ligne en moins d’une seconde. Une transformation n’atteint un board qu’à sa planification (ou lorsque le code de l’application la rafraîchit après ses propres écritures, ce qu’un enregistrement depuis un board ne déclenche jamais), de sorte qu’une vue qui sélectionne la ligne la plus récente par entité ou écarte les lignes supprimées montre le même résultat en retard. Lorsqu’une transformation a réellement besoin de l’état actuel d’une table d’entités, par exemple pour la joindre à une autre table, sélectionnez-le dans son SQL avec DISTINCT ON et appliquez-y le contrôle sur deleted, dans l’ordre indiqué sous Suppression logique des enregistrements.
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.
Quand une transformation matérialisée se rafraîchit
Une exécution planifiée ne rafraîchit une transformation matérialisée que si une table qu’elle lit a reçu des écritures depuis son dernier rafraîchissement, et au moins toutes les 10 minutes quoi qu’il arrive. Une exécution qui n’a rien de nouveau à lire laisse le résultat stocké tel quel : une transformation planifiée chaque minute sur une table qui reçoit une ligne par heure se rafraîchit après cette ligne, et sinon toutes les 10 minutes.
Une transformation matérialisée se rafraîchit à chaque exécution lorsque son résultat peut changer sans écriture :
- son SQL utilise l’heure actuelle ou le hasard, comme
now(),CURRENT_DATEourandom(); - elle appelle une fonction qui peut renvoyer un résultat différent pour la même entrée : une fonction volatile, ou une fonction stable qui n’est pas intégrée à PostgreSQL ;
- elle lit autre chose que des tables d’application et des transformations matérialisées, par exemple une transformation non matérialisée, une copie sous-échantillonnée ou l’une des tables propres à la plateforme.
Lorsque de nombreuses transformations arrivent à échéance au même moment, la plateforme en rafraîchit quelques-unes à la fois et les autres attendent leur tour : un rafraîchissement peut donc se terminer plus tard que prévu. Une exécution qui arrive à échéance alors que le rafraîchissement précédent de la même transformation est encore en cours ou en attente est sautée.
Rafraîchir à la demande
Le code de l’application elle-même peut lancer l’exécution d’une transformation sans attendre sa planification : refresh_transform / refreshTransform / RefreshTransform du SDK rafraîchit le résultat matérialisé (une simple vue est seulement lue) et envoie le résultat actuel aux boards liés à la transformation, de sorte qu’un board affiche aussitôt un lot que l’application vient d’écrire. Les mêmes règles s’appliquent qu’à une exécution planifiée : le rafraîchissement est sauté — et signalé comme sauté — lorsqu’aucune table que la transformation lit n’a reçu d’écriture depuis le précédent, un appel qui arrive alors qu’un rafraîchissement de la même transformation est en cours attend celui qui est mis en file derrière lui, et au plus un rafraîchissement par seconde démarre par transformation. Les écritures dans les tables du SDK sont validées avant de retourner, de sorte qu’une écriture suivie d’un rafraîchissement ne manque jamais l’écriture. Seuls les conteneurs de l’application elle-même peuvent rafraîchir ses transformations ; un board reçoit les lignes, et une application qui consomme les données d’une autre application lit ses transformations mais ne peut pas les rafraîchir. Les transformations enregistrées depuis la vue de données du projet n’appartiennent à aucune application et ne s’exécutent que selon leur planification. Voir refreshTransform / refresh_transform.
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.
Une table d’entités contient une ligne par entité et par horodatage. Une ligne qui reprend à la fois la clé d’entité et le tsp d’une ligne stockée est rejetée, tout comme un lot dans lequel deux lignes le font : l’écriture du SDK échoue avec URI_ENTITY_KEY_CONFLICT, et la table error-logs de l’application consigne le refus. Donnez à chaque version d’une entité son propre tsp. Une ligne dont la colonne de clé d’entité est vide (NULL) n’appartient à aucune entité : elle n’est jamais rejetée comme doublon, et les lectures de l’état le plus récent l’excluent.
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 |
Pour vider une colonne, envoyez-la comme null (None en Python, nil en Go). L’omettre conserve la valeur précédente. Méfiez-vous du code qui omet un champ sans le vouloir : une propriété JavaScript valant undefined, par exemple, disparaît du payload. La référence du SDK liste ces cas par langage.
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, et vous ne devez pas la stocker sous forme de transformation. 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. Le commutateur fonctionne en direct : le widget reçoit chaque nouvelle ligne de la table dès qu’elle arrive et remplace par elle la ligne précédente de l’entité, si bien qu’un changement apparaît moins d’une seconde après son écriture, tandis que la table conserve chaque version. Rien n’est recalculé selon une planification — c’est tout ce qu’une transformation ajouterait. 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}.
Changer la clé d’entité
Dès qu’une version de l’application est publiée, la clé d’entité de chacune de ses tables est figée. Mettre à jour la structure de données dans le panneau latéral de l’IDE cloud et le build d’une nouvelle version refusent un data-template.yml qui change la clé d’entité d’une table que possède la version la plus récente de l’application :
- ajouter une colonne à
maintainLatestFlagFor, en retirer une, en remplacer une ou changer leur ordre ; - ajouter
maintainLatestFlagForà une table qui n’en avait pas, ou le retirer.
Un tel changement modifierait silencieusement ce que la table retourne dans chaque projet qui a installé l’application. Une colonne nouvelle dans la clé est vide dans chaque ligne existante, et les lectures de l’état le plus récent excluent les lignes dont une colonne de clé est vide : toutes les entités existantes disparaîtraient donc. Une colonne retirée de la clé peut fusionner des entités dont les lignes partagent un horodatage, alors que la table ne peut contenir qu’une ligne par entité et par horodatage.
Pour utiliser une autre clé d’entité, créez une nouvelle table avec la nouvelle clé et déplacez-y les données, par exemple depuis le code de votre application. Le déplacement des données incombe à l’application. Une table qu’aucune version ne contient encore peut toujours changer de clé d’entité pendant le développement. Une version de l’application publiée avant l’existence de cette règle peut encore changer une clé d’entité : un projet qui se met à jour vers elle refuse la mise à jour de la structure de données et conserve la précédente.
Lignes qui répètent une entité et un horodatage
Les tables écrites avant qu’IronFlock n’impose une ligne par entité et par horodatage peuvent encore contenir des doublons, tout comme une table de développement dont vous avez changé la clé d’entité. Lorsque le service de données met en place une telle table, à son démarrage ou après une mise à jour de la structure de données, il déplace chaque ligne de chaque groupe de doublons vers une table nommée <table>__entitykey_dupes dans le même backend de données et remet en place une ligne de chaque groupe. À partir de là, la table rejette les doublons. Une entrée dans la table error-logs de l’application indique combien de lignes ont été déplacées.
La nouvelle table conserve chaque ligne déplacée telle quelle, avec deux colonnes en plus : quarantined_at, l’heure du déplacement, et restored, qui marque la ligne remise en place. Le choix de la ligne remise en place est arbitraire, comme il l’était déjà pour les lectures de l’état le plus récent. Interrogez-la dans la console de requêtes de la vue de données du projet sous le nom databackend_<key>.<table>__entitykey_dupes. Si la table d’origine est privée, la nouvelle table l’est aussi. Si les lignes ne peuvent pas être déplacées, l’entrée de error-logs en indique la raison, et le service de données réessaie à son prochain démarrage.
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.
Sur le board, un formulaire supprime un enregistrement via son paramètre Delete-Flag Column, qui désigne la colonne deleted : le formulaire écrit true lors d’une suppression et false à chaque sauvegarde, de sorte que sauvegarder un enregistrement supprimé le restaure (voir Suppression des données). Les widgets qui listent les enregistrements filtrent la table elle-même. Aucune vue ni transformation n’est nécessaire pour nettoyer la liste, et une telle vue ne montrerait une suppression qu’à sa prochaine exécution planifiée.
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.
Accès privilégié
Les conteneurs d’une application s’exécutent isolés de l’appareil, avec les droits par défaut de Docker. Certaines applications ont besoin de plus : un port série ou un adaptateur USB, des GPIO, une interface CAN ou Bluetooth, le réseau de l’hôte. C’est l’accès privilégié. Une application disposant d’un accès privilégié peut contrôler l’ensemble de l’appareil, y compris les autres applications et leurs données ; votre application doit donc le demander, et l’utilisateur décide si elle l’obtient.
Pour le demander, ajoutez une section privileged: de premier niveau avec un reason — à côté de data:, et non à l’intérieur :
privileged:
reason: "Lit l'automate via le port série /dev/ttyUSB0"
data:
tables:
# ... les tables propres à votre application, comme d'habitudereason est obligatoire. Il est affiché à l’utilisateur dans la boîte de dialogue de consentement : rédigez-le pour une personne qui décide si elle fait confiance à votre application.
Une application sans tables écrit data: {} à côté de privileged : un data-template.yml sans la clé data: ne peut pas être publié.
Ce que l’utilisateur accorde dépend de la façon dont l’application est construite :
- Une application à image unique (un
Dockerfileseulement) s’exécute en mode privilégié lorsque l’utilisateur l’accorde : avec toutes les capabilities Linux, le/devde l’appareil et le D-Bus de l’hôte. Sans la déclaration, elle s’exécute sans les périphériques de l’hôte, sans capabilities supplémentaires et sans le D-Bus de l’hôte ; la version est tout de même publiée, et son journal de build le signale. - Une application Docker Compose obtient exactement les réglages que demande son
docker-compose.yml, et la boîte de dialogue de consentement les énumère. Si le fichier compose utilise l’un des réglages ci-dessous, l’application doit déclarerprivileged, sinon la publication de la version échoue avec une erreur qui nomme le service et le réglage. Déclarerprivilegedalors que le fichier compose n’en utilise aucun échoue également.
Ces réglages compose comptent comme privilégiés :
| Réglage | Privilégié lorsque |
|---|---|
privileged | toute valeur sauf false |
cap_add, devices, device_cgroup_rules | il contient une entrée |
security_opt | il contient une entrée autre que no-new-privileges |
network_mode, pid, ipc, uts, userns_mode, cgroup | il vaut host ou container:… |
cgroup_parent | il est défini |
volumes d’un service | il monte un chemin de l’hôte (/…, ~/…, C:\…), un chemin hors du dossier de l’application (../…) ou un named pipe. /etc/localtime et /etc/timezone en lecture seule sont autorisés. |
volumes_from | il désigne container:… |
volumes de premier niveau | un volume a driver_opts, external ou un name fixe |
networks de premier niveau | un réseau définit un driver autre que bridge (comme macvlan, ipvlan ou host) ou des driver_opts, est external ou a un name fixe |
secrets, configs de premier niveau | une entrée lit un file hors du dossier de l’application |
Les volumes nommés, les réseaux avec le pilote par défaut bridge, les bind mounts à l’intérieur du dossier de l’application (./config:/config), les ports publiés et les réservations de GPU (gpus, deploy.resources.reservations.devices) ne sont pas privilégiés.
Un espace réservé ${VAR} dans l’un de ces réglages est autorisé et compte comme privilégié ; il nécessite donc lui aussi la déclaration. C’est ainsi qu’un paramètre d’application peut laisser les utilisateurs choisir le port série :
services:
plc:
build: .
devices:
- "${SERIAL_PORT}:/dev/ttyS0"Un fichier compose qui utilise include, ou extends avec un file, ne peut pas être publié, avec ou sans déclaration : une version ne contient que le docker-compose.yml. Fusionnez-y les autres fichiers.
Ne demandez pas plus que ce dont l’application a besoin. Si elle a besoin d’un port série, écrivez un fichier compose avec un seul service et devices: [/dev/ttyUSB0] plutôt qu’une image unique ou privileged: true. L’utilisateur voit exactement ce que l’application obtient, et l’application ne peut rien faire de plus.
Consentement et révocation
- À l’installation, l’utilisateur voit votre
reasonet, pour une application compose, la liste des réglages, puis active ou désactive l’accès privilégié. Un refus ne bloque jamais l’installation. - Lors d’une mise à jour, si la nouvelle version demande l’accès privilégié et que le projet ne l’a pas accordé, la boîte de dialogue de mise à jour pose la question. Même en cas de refus, l’application est mise à jour ; la nouvelle version s’exécute alors sans accès privilégié.
- Ensuite, l’accès privilégié est un interrupteur dans les paramètres d’applications du projet, où les utilisateurs peuvent le révoquer ou l’accorder à tout moment. Les appareils en fonctionnement redémarrent l’application avec le nouveau réglage.
Une exécution en mode développement depuis l’IDE cloud suit la déclaration et n’a pas d’interrupteur : le build de développement s’exécute en mode privilégié lorsque .ironflock/data-template.yml déclare privileged, et sans accès privilégié sinon. Si le fichier compose utilise des réglages privilégiés sans la déclaration, le build de développement échoue avec la même erreur qu’une publication.
Sans autorisation
L’application démarre quand même. Une application à image unique s’exécute avec les valeurs par défaut de Docker : pas de /dev, pas de capabilities supplémentaires, pas de D-Bus de l’hôte. Pour une application compose, l’agent de l’appareil retire les réglages privilégiés du fichier compose avant de démarrer l’application, et écrit dans le journal de l’application quels réglages il a retirés de quel service. Faites en sorte que votre code signale ce qu’il ne peut pas atteindre, comme un port série qu’il ne peut pas ouvrir, au lieu de planter.
Versions antérieures
Les versions publiées avant que les applications ne doivent déclarer l’accès privilégié comptent comme le demandant, et les installations existantes le conservent : ces applications fonctionnent donc comme avant. Les utilisateurs peuvent le révoquer dans les paramètres d’applications du projet, comme pour toute autre application. À partir de votre prochaine version, déclarez privileged si l’application en a besoin.