Skip to Content
Développement d'applications IoTArchitecture de référence d'une application

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 :

PartieDescriptionDéfinie dans
Code edgeUn conteneur, ou une stack Docker Compose, sur chaque appareil où l’application est installéeDockerfile ou docker-compose.yml
SDKLe seul lien entre votre code et la plateforme : tables, mises à jour en temps réel, fonctions d’appareil, fichiersSDK IronFlock (Python, JavaScript, Go)
TablesL’état de l’application, dans la base de données propre au projetdata-template.yml
BoardL’interface utilisateur centrale de l’application, liée en direct aux tables, pour tous les appareilsboard-template.yml
ParamètresLes réglages d’un appareil ou d’un groupe d’appareilsenv-template.yml
Accès à distanceDes tunnels vers les ports que servent vos conteneurs, comme une interface webport-template.yml
Agents IADes agents pour l’assistant IA d’IronFlock, avec vos fonctions d’appareil comme outilsai-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

  1. 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.
  2. 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.
  3. 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 description de la table. Deux sources qui écrivent les mêmes lignes écrasent mutuellement leurs modifications.
  4. 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.
  5. É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.
  6. 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éesOùÉcrites parLues par
Mesures au fil du tempsUne table avec tsppublishToTable, publishRowsToTableGraphiques avec fenêtres temporelles et agrégation
L’état actuel d’un appareil, d’une machine ou d’un ordre de productionUne table d’entités (maintainLatestFlagFor)appendToTable : une ligne avec la clé, tsp et les colonnes modifiéesLe filtre latest
Seuils, consignes et recettes définis par les utilisateursUne table qu’écrit le formulaire du boardLe formulaireChaque appareil, comme indiqué ci-dessous
Enregistrements que les utilisateurs créent, modifient et supprimentUne table d’entités avec une colonne deletedLe formulaire, qui active le marqueur lors d’une suppressionListes et pages d’édition sur le board
Erreurs et messages destinés aux opérateursLa table error-logsreportError avec un userMessageLe widget toast du board
Images, documents, enregistrements audio ou vidéoLe stockage de fichiers de l’applicationfiles.putL’URL du fichier dans une colonne, affichée par un widget
Mots de passe et clés d’APIUne colonne secret: trueToute écriture dans la tablerevealSecrets, 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érieParamètres d’applicationLes paramètres de l’application pour l’appareil ou le groupeVariables d’environnement
Messages entre vos composants dont personne n’a besoin plus tardVos propres topicspublishsubscribe
Une requête qui exige immédiatement la réponse d’un appareil précisUne fonction d’appareilregisterDeviceFunctioncallDeviceFunction, 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 :

  1. Abonnez-vous à la table avant de la lire, afin qu’aucune ligne ne se perde entre la lecture et l’abonnement.
  2. Lisez la ligne la plus récente une fois au démarrage, avec le filtre latest, et appliquez-la.
  3. 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.
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.yml appelle 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 (DEV ou PROD), TUNNEL_DOMAIN, DEVICE_LAN_IP et DEVICE_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 defaultValue de 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, pas HOST).
  • 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 /dev de 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 (ENV vaut DEV). 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. Dans port-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, devices ou device_cgroup_rules. Une entrée devices: désigne un matériel qui doit exister, faute de quoi le conteneur ne démarre pas ; device_cgroup_rules associé à un volume /dev fonctionne 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 :

  1. Écoutez sur un port fixe (lié à 0.0.0.0) et déclarez-le dans port-template.yml avec protocol: http. Une installation de production active le tunnel et l’expose en HTTPS, WebSockets compris.
  2. É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.
  3. 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_key la clé de l’appareil qui a écrit une ligne. Utilisez device_key comme clé des tables par appareil, stockez le nom de l’appareil dans une colonne avec path: 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 deleted et au board un formulaire qui l’active.

Liste de contrôle

  1. 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.
  2. 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.
  3. 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.
  4. 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 reportError et un userMessage.
  5. Le programme se termine sur SIGTERM, démarre sous la forme exec et journalise sans mise en mémoire tampon.
  6. Aucun polling : le board s’appuie sur des liaisons de données, les appareils s’abonnent, les interfaces web poussent leurs mises à jour.
  7. Chaque port servi par votre application est déclaré dans port-template.yml, son URL provient de getRemoteAccessUrlForPort et est écrite dans une table, et le board l’intègre à partir de là.
  8. Une application compose n’a ni env_file:, ni profiles:, 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.

Last updated on