Architecture de référence d’une application
Cette page montre comment construire une application IronFlock qui utilise la plateforme comme prévu. L’idée en une phrase : vos conteneurs s’exécutent sur chaque appareil et conservent tout l’état de l’application dans ses tables, le board est l’interface utilisateur de l’application, et les deux ne communiquent jamais directement. Une interface web servie par l’appareil lui-même parvient aux utilisateurs via un tunnel d’accès à distance et s’affiche dans le board.
Chaque élément est décrit en détail sur sa propre page. Cette page montre comment ils s’articulent.
Les parties d’une application
Une application s’exécute sur de nombreux appareils à la fois, généralement des PC industriels installés à côté des machines. Elle se compose des éléments suivants :
| Partie | Description | Définie dans |
|---|---|---|
| Code edge | Un conteneur, ou une stack Docker Compose, sur chaque appareil où l’application est installée | Dockerfile ou docker-compose.yml |
| SDK | Le seul lien entre votre code et la plateforme : tables, mises à jour en temps réel, fonctions d’appareil, fichiers | SDK IronFlock (Python, JavaScript, Go) |
| Tables | L’état de l’application, dans la base de données propre au projet | data-template.yml |
| Board | L’interface utilisateur centrale de l’application, liée en direct aux tables, pour tous les appareils | board-template.yml |
| Paramètres | Les réglages d’un appareil ou d’un groupe d’appareils | env-template.yml |
| Accès à distance | Des tunnels vers les ports que servent vos conteneurs, comme une interface web | port-template.yml |
| Agents IA | Des agents pour l’assistant IA d’IronFlock, avec vos fonctions d’appareil comme outils | ai-template.yml |
Lorsqu’un utilisateur installe votre application dans un projet, IronFlock crée les tables de l’application dans la base de données de ce projet, installe son board et ouvre un realm de messagerie privé qui relie en temps réel les conteneurs de l’application sur tous les appareils, ses tables et son board. Le SDK rejoint ce realm avec l’identité que l’agent de l’appareil injecte : votre code n’a donc besoin ni d’adresse ni d’identifiants.
Les règles
- Les tables sont l’état de l’application. Tout ce dont ont besoin le board, un autre appareil, un conteneur redémarré ou un agent IA est stocké dans une table. Un conteneur ne garde que ce qu’il peut reconstruire à partir des tables, comme une copie en mémoire de ses réglages.
- Les parties communiquent par les tables, pas entre elles. Un appareil écrit ce qu’il mesure et l’état dans lequel il se trouve, et le board l’affiche. Le board écrit ce que décident les utilisateurs, et les appareils l’appliquent. Aucun des deux n’appelle l’autre : le board fonctionne donc même quand des appareils sont hors ligne, et les appareils continuent de fonctionner quand personne n’a ouvert le board.
- Une seule source d’écriture par table. Chaque table est écrite soit par les appareils (chacun pour son propre compte), soit par le board, soit par un seul service. Précisez-le dans la
descriptionde la table. Deux sources qui écrivent les mêmes lignes écrasent mutuellement leurs modifications. - Un état souhaité, pas des commandes. Pour changer ce que font les appareils, écrivez la cible dans une table (un seuil, une consigne, une recette, un planning) et laissez chaque appareil appliquer la ligne la plus récente. Un appareil qui était hors ligne l’applique à son retour, et l’appliquer deux fois ne change rien.
- Écrire à chaque changement, suivre en direct, ne jamais faire de polling. Écrivez la ligne d’une entité lorsque l’une de ses valeurs change, et les mesures à leur fréquence d’échantillonnage. Tout ce qui lit suit les tables en temps réel : les liaisons de données du board, les abonnements aux tables sur les appareils, les WebSockets de l’interface web d’un appareil. Un timer qui relit périodiquement une table est toujours la mauvaise approche.
- Le board est l’interface utilisateur ; l’interface web d’un appareil le complète. Ne servez une page web depuis un appareil que pour ce qu’un board ne peut pas afficher : l’IHM propre à une machine, de la vidéo, une vue en direct à haute fréquence, un écran opérateur au pied de la machine. Le board l’intègre.
Où va chaque type de données
| Données | Où | Écrites par | Lues par |
|---|---|---|---|
| Mesures au fil du temps | Une table avec tsp | publishToTable, publishRowsToTable | Graphiques avec fenêtres temporelles et agrégation |
| L’état actuel d’un appareil, d’une machine ou d’un ordre de production | Une table d’entités (maintainLatestFlagFor) | appendToTable : une ligne avec la clé, tsp et les colonnes modifiées | Le filtre latest |
| Seuils, consignes et recettes définis par les utilisateurs | Une table qu’écrit le formulaire du board | Le formulaire | Chaque appareil, comme indiqué ci-dessous |
| Enregistrements que les utilisateurs créent, modifient et suppriment | Une table d’entités avec une colonne deleted | Le formulaire, qui active le marqueur lors d’une suppression | Listes et pages d’édition sur le board |
| Erreurs et messages destinés aux opérateurs | La table error-logs | reportError avec un userMessage | Le widget toast du board |
| Images, documents, enregistrements audio ou vidéo | Le stockage de fichiers de l’application | files.put | L’URL du fichier dans une colonne, affichée par un widget |
| Mots de passe et clés d’API | Une colonne secret: true | Toute écriture dans la table | revealSecrets, verifySecret, uniquement dans les conteneurs de l’application elle-même |
| L’installation d’un appareil donné : l’adresse d’une machine, un port série | Paramètres d’application | Les paramètres de l’application pour l’appareil ou le groupe | Variables d’environnement |
| Messages entre vos composants dont personne n’a besoin plus tard | Vos propres topics | publish | subscribe |
| Une requête qui exige immédiatement la réponse d’un appareil précis | Une fonction d’appareil | registerDeviceFunction | callDeviceFunction, actions de widgets, outils IA |
Ajouter ou publier ? appendToTable attend un backend de données en cours de redémarrage et vous signale lorsqu’il refuse une ligne. publishToTable n’attend que la connexion : une ligne publiée pendant que le backend de données redémarre est donc perdue. Ajoutez les changements d’état et les enregistrements ; publiez les mesures à haute fréquence.
Paramètres ou table de réglages ? Les paramètres appartiennent à un appareil ou à un groupe d’appareils : la personne qui installe ou configure l’appareil les définit dans les paramètres de l’application, et votre code les lit dans son environnement. Utilisez-les pour ce qui diffère d’une installation à l’autre. Ce que les utilisateurs modifient pendant que l’application s’exécute — seuils, recettes, plannings, tout ce qui s’applique à de nombreux appareils — a sa place dans une table écrite par le board. C’est dans le board que les utilisateurs travaillent avec l’application, chaque appareil suit immédiatement une modification, et la table conserve l’historique de qui a modifié quoi et quand.
Suivre ce qu’écrit le board
Un appareil applique un état écrit par le board, comme un seuil défini sur le board, en trois étapes :
- Abonnez-vous à la table avant de la lire, afin qu’aucune ligne ne se perde entre la lecture et l’abonnement.
- Lisez la ligne la plus récente une fois au démarrage, avec le filtre
latest, et appliquez-la. - N’appliquez une ligne que si elle est plus récente (
tsp) que celle en vigueur. Une ligne enregistrée au moment précis où votre programme démarre peut arriver à la fois par la lecture et par l’abonnement.
Python
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)C’est tout, y compris pour les appareils qui passent hors ligne. Le SDK décharge votre code de la gestion de la connexion : il se reconnecte, restaure vos abonnements et vos fonctions d’appareil, puis transmet à chaque abonnement de table les lignes stockées pendant que l’appareil était hors ligne, de la plus ancienne à la plus récente, avant les lignes qui arrivent en direct. Un seuil enregistré pendant qu’un appareil était hors ligne lui parvient dès son retour. Le rattrapage se fait selon tsp : il transmet les lignes postérieures à la plus récente que votre gestionnaire a reçue, dans la limite des 10 000 plus récentes. Sa lecture attend le backend de données comme toute lecture d’historique, au plus pendant la fenêtre de reconnexion ; si elle échoue, ces lignes ne sont pas transmises et un avertissement est journalisé. Le rattrapage fait partie du SDK à partir de la version 1.10.0.
Commandes, fonctions d’appareil et topics
Privilégiez l’état. La plupart des « commandes » des utilisateurs sont en réalité un état souhaité : une consigne, marche ou arrêt, une recette. Écrivez-les dans une table, comme ci-dessus.
Les commandes qui doivent s’exécuter une seule fois, comme imprimer une étiquette ou lancer un cycle de nettoyage, sont des lignes dotées d’un identifiant dans une table écrite par le board. L’appareil consigne les identifiants des commandes qu’il a exécutées dans une table qui lui est propre : une commande reçue deux fois ne s’exécute ainsi qu’une fois.
Les fonctions d’appareil sont des procédures qu’un appareil enregistre avec registerDeviceFunction. Utilisez-les lorsque l’appelant a besoin de la réponse de l’appareil sur-le-champ :
- Agents IA : un outil défini dans
ai-template.ymlappelle la fonction sur l’appareil que choisit l’assistant. Voir Agents IA et outils. - Les widgets Interrupteur et SCADA du board les appellent tant que l’appareil est en ligne.
- Vos propres conteneurs et appareils s’appellent mutuellement avec
callDeviceFunction.
Renvoyez depuis une fonction d’appareil du texte lisible ou un JSON compact, et renvoyez un échec attendu (une machine éteinte) comme résultat plutôt que de lever une erreur.
Les topics transportent entre vos propres composants des messages dont personne n’a besoin plus tard : publish et subscribe sur des noms de votre propre espace de noms, comme myapp.events. Le board ne peut pas s’abonner à des topics : tout ce qu’il affiche doit se trouver dans une table. Les messages publiés pendant qu’un appareil est hors ligne ne sont pas stockés ; utilisez donc une table pour tout ce qui doit arriver à destination.
Le conteneur sur l’appareil
Ce que l’agent de l’appareil fournit à votre conteneur, et ce qu’il attend de lui :
- Identité : l’agent définit
DEVICE_KEY,DEVICE_NAME,DEVICE_SERIAL_NUMBER,SWARM_KEY,APP_KEY,APP_NAME,ENV(DEVouPROD),TUNNEL_DOMAIN,DEVICE_LAN_IPetDEVICE_PORT_FOR_<port>, ainsi que les identifiants propres à l’application pour le SDK. Le SDK lit ce dont il a besoin ; ne codez en dur aucune de ces valeurs. - Paramètres : une installation de production reçoit les valeurs définies par les utilisateurs, et la
defaultValuede chaque paramètre pour les autres. Une exécution en mode développement ne reçoit que les valeurs par défaut. Donnez aux paramètres des noms explicites (PLC_HOST, pasHOST). - Stockage : une application à image unique reçoit
/data, privé à l’application et conservé lors des redémarrages et des mises à jour (il est supprimé avec l’application), ainsi que/shared, partagé par toutes les applications de l’appareil. Conservez malgré tout l’état de l’application dans des tables, et réservez ces dossiers aux fichiers qui appartiennent à l’appareil, comme un modèle téléchargé ou un cache. - Matériel : un conteneur à image unique s’exécute en mode privilégié, avec le
/devde l’appareil. - Journaux : tout ce que votre programme écrit sur stdout et stderr constitue le journal de l’application dans IronFlock. Écrivez-le sans mise en mémoire tampon (
python -u). - Démarrage : le conteneur doit être en cours d’exécution 30 secondes après son démarrage, faute de quoi le démarrage est considéré comme un échec.
- Plantages : lorsqu’une application de production se termine, l’agent la redémarre, en attendant plus longtemps après chaque échec. Une exécution en mode développement n’est pas redémarrée. Gérez dans votre programme les échecs attendus, comme un capteur qui ne répond pas, et laissez-le se terminer sur les erreurs dont il ne peut pas se remettre.
- Arrêt : votre programme est le processus principal du conteneur (PID 1), qui ignore le signal d’arrêt SIGTERM s’il ne le gère pas. Faites en sorte qu’il se termine à la réception de SIGTERM, sinon l’arrêt de l’application attend que Docker le tue. Lancez le programme sous la forme exec (
CMD ["python", "-u", "main.py"]), et non via un shell. - Mode développement : une application lancée depuis l’IDE cloud s’exécute en mode développement (
ENVvautDEV). Elle écrit alors dans les tables de développement de l’application, reçoit les valeurs par défaut des paramètres et n’obtient aucun tunnel d’accès à distance.
Docker Compose
Utilisez un docker-compose.yml lorsque votre application exécute plusieurs processus, ou qu’elle embarque à côté de votre propre code des services tiers comme un broker MQTT ou une base de données. Pour les bases, voir Docker et Compose. Par rapport à une image unique :
- Chaque service reçoit l’environnement injecté. L’agent donne à chaque service son propre fichier d’environnement contenant les variables ci-dessus ; un
env_file:que vous écrivez est remplacé. Un${VAR}dans le fichier est résolu à partir des valeurs injectées. - Pas de
/data. Les services n’ont pas de dossier d’application. Conservez les fichiers dans des volumes nommés : ils survivent aux arrêts et aux mises à jour, et sont supprimés lorsque l’application est retirée de l’appareil. - Les ports publiés sont remappés. Chaque port publié reçoit un port hôte libre sur toutes les interfaces de l’appareil, et
DEVICE_PORT_FOR_<port>indique à votre code le port réel. Dansport-template.yml, utilisez le port hôte tel qu’il est écrit dans le fichier compose. Ne publiez que ce que les utilisateurs doivent pouvoir atteindre, et laissez vos services communiquer entre eux par nom de service sur le réseau compose. Un port hôte écrit sous la forme${VAR}n’est pas géré : il n’est ni remappé ni tunnelisé. - Rien n’est ajouté. L’agent n’accorde aux services compose ni privilèges ni périphériques. Déclarez vous-même
privileged,devicesoudevice_cgroup_rules. Une entréedevices:désigne un matériel qui doit exister, faute de quoi le conteneur ne démarre pas ;device_cgroup_rulesassocié à un volume/devfonctionne sur tous les appareils. - Pas de
profiles:. Un service doté de profils est exclu de la version publiée. - La même règle de démarrage. Dans les 30 secondes, chaque conteneur doit être en cours d’exécution ou s’être terminé avec le code 0, et au moins un doit être en cours d’exécution. Donnez aux services de longue durée
restart: unless-stopped.
Interfaces web sur l’appareil
Un serveur web dans votre conteneur devient joignable de partout via le tunnel d’accès à distance de son port, et le board l’affiche :
- Écoutez sur un port fixe (lié à
0.0.0.0) et déclarez-le dansport-template.ymlavecprotocol: http. Une installation de production active le tunnel et l’expose en HTTPS, WebSockets compris. - Écrivez l’URL du tunnel dans une table.
getRemoteAccessUrlForPort(port)du SDK fournit l’URL. Écrivez-la au démarrage dans la ligne de l’appareil d’une table d’appareils. Seules les installations de production disposent de tunnels : une exécution en mode développement n’écrit donc aucune URL. - Intégrez-la dans le board. Liez l’URL du widget Intégration à cette colonne, sur une page par appareil qui filtre selon l’appareil. Dans l’UI Builder, il s’agit d’une liaison de données comme une autre.
Pour la page elle-même :
- Qui peut l’ouvrir : un tunnel http est privé. Seules les personnes connectées à IronFlock et ayant accès à l’appareil peuvent l’ouvrir, sauf si un utilisateur passe le port en Public. Sur le réseau propre de l’appareil, la page est ouverte à tous : n’y affichez rien que le réseau de l’usine ne doit pas voir, et exigez votre propre authentification pour tout ce qui modifie l’état de la machine.
- URL relatives : chargez les ressources, appelez votre API et ouvrez les WebSockets relativement à la page, afin qu’elle fonctionne sur le réseau de l’appareil, à travers le tunnel et derrière un proxy inverse.
- Pousser, ne jamais faire de polling : envoyez les mises à jour via un WebSocket.
- Aucun identifiant de la plateforme dans le navigateur : la page communique avec son propre serveur dans le conteneur, et c’est ce serveur qui détient la connexion du SDK.
Pour afficher l’IHM web d’une machine du réseau de l’usine au lieu de votre propre page, faites-la passer par un proxy inverse dans votre conteneur. Voir Intégration d’IHM externes.
Plusieurs appareils
- Chaque ligne porte son appareil : IronFlock stocke dans
device_keyla clé de l’appareil qui a écrit une ligne. Utilisezdevice_keycomme clé des tables par appareil, stockez le nom de l’appareil dans une colonne avecpath: kwargs.DEVICE_NAME, et fractionnez les graphiques selon ce nom. - Une liste d’appareils et une page par appareil : une navigation latérale construite à partir de la table des appareils, avec une seule page qui fonctionne pour chaque appareil et filtre ses widgets selon l’appareil indiqué dans la route. Voir Créer des boards.
- Des réglages par appareil ou par machine : utilisez comme clé de la table de réglages une colonne qui désigne la cible (les lignes écrites par le board ne portent pas d’appareil propre), et remplissez-la dans le formulaire de la page de la cible. Chaque appareil applique la ligne la plus récente pour sa propre clé.
- Un appareil qui s’en va conserve ses lignes. Si les utilisateurs doivent pouvoir le retirer des listes, donnez à la table des appareils une colonne
deletedet au board un formulaire qui l’active.
Liste de contrôle
- Tout l’état se trouve dans des tables, chacune avec une seule source d’écriture, nommée dans sa description, et chaque colonne écrite par le code est déclarée dans
data-template.yml. - Chaque ligne a un
tsp, et les lignes d’état sont écrites lors d’un changement, avec uniquement la clé et les colonnes modifiées. - L’état écrit par le board est suivi selon les trois étapes ci-dessus, avec le SDK 1.10.0 ou ultérieur, et le code ne s’occupe jamais de la connexion ni de ses sessions.
- Les écritures dans les tables au sein de boucles de longue durée gèrent l’erreur qu’une écriture lève lorsque la plateforme est restée injoignable pendant une minute, et les échecs attendus sont signalés avec
reportErroret unuserMessage. - Le programme se termine sur SIGTERM, démarre sous la forme exec et journalise sans mise en mémoire tampon.
- Aucun polling : le board s’appuie sur des liaisons de données, les appareils s’abonnent, les interfaces web poussent leurs mises à jour.
- Chaque port servi par votre application est déclaré dans
port-template.yml, son URL provient degetRemoteAccessUrlForPortet est écrite dans une table, et le board l’intègre à partir de là. - Une application compose n’a ni
env_file:, niprofiles:, ni ports hôtes en${VAR}, conserve ses fichiers dans des volumes nommés et ne publie que les ports dont les utilisateurs ont besoin.
Partir de l’application de démarrage
Le plugin IronFlock pour Claude Code contient une application de démarrage qui suit cette architecture de référence : un programme qui interroge une machine sur chaque appareil, conserve l’état et les mesures de chaque appareil dans des tables, applique le seuil défini par les utilisateurs sur le board, et sert une vue en direct que le board intègre sur la page de chaque appareil. Claude Code crée une nouvelle application à partir de celle-ci et l’adapte à votre cas d’usage.