Skip to Content

SDK IronFlock

Le SDK IronFlock permet à vos applications edge d’interagir avec la plateforme IronFlock. Il existe en trois langages — Python, JavaScript et Go — avec les mêmes fonctionnalités, documentés côte à côte ci-dessous. Il gère l’authentification automatiquement lorsqu’il s’exécute sur un appareil enregistré et fournit des fonctions pour publier des données, interroger l’historique, appeler des procédures distantes entre appareils et mettre à jour les métadonnées des appareils.

SDKPackagePrérequis
Pythonironflock sur PyPIPython 3.11+
JavaScriptironflock sur npmNode.js 22+ ou navigateur moderne
Gogithub.com/ironflock/sdk-goGo 1.27.2+

Cette page est le guide des trois SDK. Le README de chaque SDK sur GitHub en constitue la référence complète : IronFlock/sdk-py, IronFlock/sdk-js et IronFlock/sdk-go.

Installation

pip install ironflock

Ou ajoutez ironflock au requirements.txt de votre application.

Démarrage rapide

import asyncio from datetime import datetime, timezone from ironflock import IronFlock async def main(): while True: await ironflock.publish_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60 }) await asyncio.sleep(5) ironflock = IronFlock(mainFunc=main) ironflock.run()

Lorsqu’il est utilisé à l’intérieur d’un conteneur d’application IronFlock, le SDK lit automatiquement les identifiants de connexion depuis l’environnement — aucune configuration manuelle n’est nécessaire. Chaque ligne que vous écrivez dans une table porte un tsp, son horodatage (voir publishToTable).

Options du constructeur

ironflock = IronFlock( mainFunc=main, # async function to run after connecting serial_number="abc123", # override device serial (optional) reconnect_window=60, # seconds an operation rides out a platform restart (optional) )
ParamètreDescription
mainFuncUne fonction asynchrone (async def) qui s’exécute une fois la connexion établie. Lorsqu’elle se termine ou lève une exception, run() et run_async() arrêtent la connexion (voir Propriétés & cycle de vie de la connexion)
ironFlockUrlL’URL WebSocket du routeur, par ex. wss://cbw.ironflock.com/ws-ua-usr. Sans elle, le SDK prend l’URL dans l’environnement (voir Variables d’environnement)
serial_numberRemplace le numéro de série de l’appareil envoyé avec les publications et les écritures dans les tables. Par défaut, la variable d’environnement DEVICE_SERIAL_NUMBER. Ne change pas l’identité avec laquelle la connexion s’authentifie tant que l’agent de l’appareil injecte des identifiants propres à l’app
reconnect_windowDurée en secondes pendant laquelle les opérations sur les tables, publish, subscribe et register_device_function attendent le retour de la plateforme après un redémarrage avant de lever une erreur. Par défaut 60 ; 0 la désactive (voir Fiabilité de la connexion). IronFlock() lève une ValueError pour une valeur négative, NaN ou une valeur infinie, et une TypeError pour une valeur qui n’est pas un nombre

Publier des données

publishToTable / publish_to_table

Écrit un enregistrement de données dans une table de flotte. Le nom de la table doit correspondre à une table définie dans le data-template.yml de votre application. Le SDK achemine automatiquement les données vers la bonne base de données du projet.

Cette méthode est identique à appendToTable / append_to_table : elle appelle la procédure d’ajout de la table et survit donc à un redémarrage de la plateforme — elle attend la connexion au plus pendant la fenêtre de reconnexion et réessaie tant que le backend de données ne sert pas encore la table — et elle lève une exception (rejette, renvoie une erreur) lorsque le backend de données refuse la ligne. Aucune ligne n’est perdue pendant le redémarrage de la plateforme.

Jusqu’à la version 1.9.1 incluse, les trois SDK publiaient plutôt la ligne sur le topic d’écriture de la table : une ligne publiée pendant le redémarrage du backend de données était perdue, et une ligne refusée était abandonnée sans erreur. Désormais, une telle ligne fait échouer l’appel — par exemple une ligne sans tsp, ou une ligne destinée à une table absente du data template (une fois la fenêtre de reconnexion écoulée).

from datetime import datetime, timezone await ironflock.publish_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60, "device_id": "sensor-001" })

Le backend de données lit la ligne depuis le premier argument : un dict en Python, le premier élément du tableau en JavaScript, le premier argument positionnel en Go (une map ou une struct). Les arguments nommés supplémentaires — en Go, une valeur ironflock.Kwargs parmi les arguments — ne sont lus que pour les colonnes que le data template leur associe (kwargs.<name>).

Chaque ligne a besoin d’un tsp. Chaque table possède une colonne tsp obligatoire, l’horodatage de la ligne — par ex. une chaîne ISO 8601, ou en Go un time.Time, envoyé en RFC 3339 en UTC —, que le backend de données lit dans la ligne, sauf si le data template l’associe à une autre source. Il refuse une ligne sans tsp, comme décrit sous appendToTable / append_to_table.

Lignes partielles dans les tables d’entités. Dans une table dotée d’une clé d’entité (maintainLatestFlagFor), une ligne peut ne contenir que la clé d’entité, tsp et les colonnes qui changent. Une colonne que la ligne omet conserve la valeur précédente de l’entité ; une colonne envoyée comme None / null / nil est effacée. Qu’un champ compte comme omis dépend de la façon dont vous construisez la ligne :

  • Python : une clé dont la valeur est None est envoyée comme null et efface la colonne. Retirez la clé du dict pour conserver l’ancienne valeur.
  • JavaScript : une propriété dont la valeur est undefined disparaît du payload, elle conserve donc l’ancienne valeur. Mettez-la à null pour effacer la colonne.
  • Go : un champ de struct marqué omitempty ou omitzero est omis lorsqu’il est vide, il conserve donc l’ancienne valeur. Sinon, un champ pointeur nil, comme une valeur nil dans un ironflock.Row, est envoyé comme null et efface la colonne.

Il en va de même pour les quatre méthodes d’écriture. Dans une table sans clé d’entité, une colonne que la ligne omet est stockée comme NULL.

appendToTable / append_to_table

Ajoute des données à une table de flotte via un appel de procédure distante : une ligne refusée est une erreur. publishToTable / publish_to_table fait de même ; la seule différence est le nom que le message d’erreur donne à l’opération.

result = await ironflock.append_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60 })

La ligne a besoin d’un tsp, comme pour publishToTable. Le backend de données refuse une ligne sans tsp avec wamp.error.runtime_error et sans raison ; l’erreur du SDK ajoute alors “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table”. Les refus typés, comme un disque plein sur une appliance, sont listés sous Refus du backend de données.

publishRowsToTable / publish_rows_to_table

Écrit plusieurs lignes dans un seul message (insertion en masse) dans une table de flotte. La plateforme insère l’intégralité du lot de manière atomique (tout ou rien) en une seule opération. Utilisez ceci pour les données à haute fréquence, où un aller-retour par ligne serait trop coûteux. Cette méthode est identique à appendRowsToTable / append_rows_to_table : elle appelle la procédure d’ajout en masse, survit comme publishToTable à un redémarrage de la plateforme et échoue lorsque le backend de données refuse le lot.

await ironflock.publish_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ])

Le second argument est une liste non vide d’objets de lignes à insérer, chacun avec son propre tsp — en Go, une slice de lignes ([]ironflock.Row, []map[string]any ou une slice de structs). Un lot que le backend de données refuse — parce qu’une ligne n’a pas de tsp, par exemple — fait échouer l’appel, et aucune de ses lignes n’est stockée. (Jusqu’à la version 1.9.1 incluse, les SDK publiaient le lot sur le topic bulk de la table, où un lot refusé était abandonné sans erreur.)

appendRowsToTable / append_rows_to_table

Ajoute plusieurs lignes via un seul appel de procédure distante (insertion en masse) dans une table de flotte. La plateforme insère l’intégralité du lot de manière atomique (tout ou rien) : si une ligne est invalide, le lot entier est rejeté et rien n’est persisté. publishRowsToTable / publish_rows_to_table fait de même. Chaque ligne a besoin de son propre tsp ; les refus sont ceux d’appendToTable, y compris l’indication pour un lot refusé sans raison.

result = await ironflock.append_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ]) # result -> {"success": True, "count": 2}

reportError / report_error

Signale une erreur applicative dans la table error-logs de votre flotte. Il s’agit d’un wrapper pratique au-dessus de publishToTable / appendToTable : il estampille la ligne avec source: "app", un niveau de gravité level et un horodatage, puis l’écrit comme n’importe quelle ligne de table normale. L’erreur arrive dans la même table error-logs que celle utilisée par les erreurs système de fleetdb (étiquetées source: "system"), elle est donc interrogeable avec getHistory, diffusable en flux avec subscribeToTable / subscribe_to_table, utilisable dans les modèles de tableaux et livrée en temps réel sur transformed.error-logs — sans déclencher la notification d’erreur système de la plateforme.

# Writes a row to the error-logs table await ironflock.report_error("Sensor read timed out", level="warning") # Pass an exception to capture its traceback (falls back to the message) try: risky_operation() except Exception as err: await ironflock.report_error(err) # Show the operator a friendlier line than the raw error await ironflock.report_error( "Calibration failed: offset 0.42 out of range", user_message="Please recalibrate the sensor", )

La ligne est { tsp, msg, user_message, source: "app", level } : msg contient le texte technique (une trace ou une pile d’appels), user_message la ligne qu’affiche la notification d’erreur du board — le message utilisateur que vous passez ou, à défaut, le message lui-même.

Paramètres :

ParamètreTypeDescription
errorstr / string ou exception / ErrorLe message d’erreur, ou une exception dont la trace/pile (ou le message) est enregistrée
levelstr / string, optionnelGravité : "error", "warning", "info" ou "debug", soit les types qu’affiche le widget toast du board. Par défaut "error". "warn", la graphie du SDK 1.9.1 et des versions antérieures, est envoyé sous la forme "warning"
appendbool / boolean, optionnelConservé pour compatibilité : la ligne est ajoutée dans tous les cas, puisque publishToTable et appendToTable effectuent tous deux un ajout. Il détermine seulement laquelle des deux méthodes le message d’erreur nomme
tspstr / string, optionnelHorodatage ISO-8601 de remplacement. Par défaut, l’heure actuelle
user_message / userMessagestr / string, optionnelTexte destiné à l’opérateur, stocké dans user_message. Par défaut, le message enregistré

En Python, les options sont des arguments nommés (report_error(error, level=..., append=..., tsp=..., user_message=...)) ; en JavaScript, elles sont passées via un objet d’options (reportError(error, { level, append, tsp, userMessage })) ; en Go, via une struct (ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), où errOrMsg est une error ou une chaîne, Level l’une des valeurs ironflock.LevelError, LevelWarning, LevelInfo, LevelDebug, et où le *Result renvoyé est le résultat de l’insertion.

publish

Publie un message sur un topic WAMP qui vous est propre. Utilisez ceci pour la messagerie personnalisée ou les événements qui ne correspondent pas à une table de base de données. Le routeur réserve certains noms à la plateforme — voir URI autorisées pour une app.

Comme les écritures dans les tables, publish ajoute les métadonnées de l’appareil aux arguments nommés : DEVICE_SERIAL_NUMBER, DEVICE_KEY et DEVICE_NAME. Si vous passez vous-même un argument nommé portant l’un de ces noms, c’est le vôtre qui l’emporte.

Sans connexion, publish attend une connexion au plus pendant la fenêtre de reconnexion (60 secondes par défaut, voir Fiabilité de la connexion), si bien qu’une publication survit à une courte coupure. Le message est ensuite envoyé une seule fois, jamais retenté. Le routeur acquitte un message auquel personne n’est abonné : un abonné hors service entre-temps le manque donc.

await ironflock.publish("com.myapp.alerts", { "level": "warning", "message": "Temperature threshold exceeded" })

Interroger les données historiques

getHistory

Récupère des données historiques depuis une table de flotte ou un transform : les lignes correspondantes les plus récentes, par ordre croissant de tsp. Supporte le filtrage, les plages temporelles et la pagination.

# Simple query data = await ironflock.getHistory("sensordata", {"limit": 100}) # Query with time range and filters data = await ironflock.getHistory("sensordata", { "limit": 500, "offset": 0, "timeRange": ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"], "filterAnd": [ {"column": "temperature", "operator": ">", "value": 20}, {"column": "humidity", "operator": "<=", "value": 80} ] }) # Current value(s) only: the "latest" marker returns the newest row per entity current = await ironflock.getHistory("sensordata", { "limit": 100, "filterAnd": [{"latest": True}] }) # One field of a large json column: the column comes back pruned to its paths readings = await ironflock.getHistory("sensordata", { "limit": 100, "columns": ["json_data"], "columnPaths": ["json_data.sensor.temp"], })

Paramètres de requête :

ChampTypeDescription
limitint / numberNombre maximum de lignes à renvoyer (1–10 000, obligatoire ; 1–3 000 pour un transform)
offsetint / numberDécalage pour la pagination
timeRangelist / array[start, end] — chaînes date-heure ISO ou nombres en millisecondes epoch, jamais mélangés ; None / null pour une borne ouverte. Un ancien objet {"start": ..., "end": ...} est converti en paire
filterAndlist / arrayEntrées de filtre combinées par AND : conditions, groupes et/ou le marqueur latest (voir ci-dessous)
columnslist / arrayColonnes à renvoyer (optionnel). tsp, device_key et authid sont toujours inclus ; omettez ce champ pour obtenir toutes les colonnes
columnPathslist / arrayÉlague les colonnes json (optionnel) : chemins JSON dans la notation qu’utilisent les filtres ("json_data.a.b", "json_data['key']"). Une colonne json listée dans columns revient en ne contenant que l’union de ses chemins ; une colonne sans chemin ici revient entière. Ignoré par les transforms et par les backends de données antérieurs à fleetdb v1.0.52

Lorsque vous passez des paramètres de requête, limit est obligatoire ; sans aucun paramètre (nil en Go), getHistory lit les 10 lignes les plus récentes.

En Go, ce sont les champs Limit, Offset, TimeRange, FilterAnd, Columns et ColumnPaths de ironflock.TableQueryParams. TimeRange est un *ironflock.TimeRange{Start, End} : chaque borne est un time.Time, une chaîne ISO 8601, un nombre en millisecondes epoch, ou nil pour une borne ouverte (ironflock.Between, Since et Until en construisent un à partir de valeurs time.Time). FilterAnd est un []ironflock.Filter. Il n’existe pas d’ancienne forme objet.

Opérateurs de filtre : =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL. JavaScript exporte cette liste sous le nom SQL_OPERATORS ; en Go, ironflock.SQLOperators() la renvoie.

Chaque condition de filtre est un objet avec les clés column, operator et value — en Go, construit avec ironflock.Where(column, operator, value), ironflock.IsNull(column) et ironflock.IsNotNull(column). Les opérateurs sont reconnus quelle que soit la casse, les espaces qui les entourent sont ignorés, et ils sont envoyés sous la forme listée ci-dessus — la seule que le backend de données accepte —, de sorte que "like" part sous la forme "LIKE", y compris à l’intérieur des groupes. Un opérateur hors de cette liste est envoyé tel quel, avec un avertissement. IS NULL et IS NOT NULL ne prennent pas de value et sont le seul moyen de tester NULL : tout autre opérateur exclut une ligne dont la colonne est NULL, exactement comme en SQL. IN et NOT IN prennent une liste (une slice en Go). LIKE est sensible à la casse ; utilisez ILIKE pour une correspondance insensible à la casse.

Combiner avec OR. Les entrées de filterAnd sont combinées par AND. Une entrée de groupe combine ses propres entrées — conditions, marqueur latest ou autres groupes — avec un seul opérateur, "AND" ou "OR" :

# Rows that are not soft-deleted: deleted IS NULL OR deleted = false "filterAnd": [ { "combinator": "OR", "filters": [ {"column": "deleted", "operator": "IS NULL"}, {"column": "deleted", "operator": "=", "value": False}, ], } ]

Un backend de données antérieur aux groupes (fleetdb avant v1.2.0) rejette cette forme au lieu d’en appliquer une partie : l’appel échoue donc de manière explicite au lieu de renvoyer silencieusement des lignes que le filtre exclut.

Lire les valeurs actuelles. Une entrée {"latest": true} dans filterAnd (ironflock.Latest() en Go) n’est pas une condition de filtre mais un changement de mode : le backend de données ne renvoie que la ligne la plus récente de chaque entité, dérivée en SQL à partir de la clé d’entité que la table déclare avec maintainLatestFlagFor. Une table sans clé d’entité renvoie son unique ligne la plus récente.

Les autres conditions se combinent au marqueur comme on peut s’y attendre : les conditions portant sur les colonnes de la clé d’entité restreignent quelles entités sont renvoyées, tandis que toutes les autres conditions et timeRange s’appliquent aux lignes les plus récentes obtenues. Ainsi, combiner {"latest": true} avec un filtre sur deleted masque les entités supprimées au lieu de faire remonter leur ligne précédente.

Les versions précédentes d’IronFlock stockaient une colonne latest_flag physique. Elle n’existe plus — un filtre hérité latest_flag = true est toujours accepté et traité comme le marqueur, mais le nouveau code doit utiliser {"latest": true}. Le marqueur latest n’est pas disponible dans getSeriesHistory.

Plages temporelles. Le backend de données lit une borne fournie sous forme de chaîne avec le Date de JavaScript ; écrivez donc une date ou une date-heure ISO 8601 (2026-07-01, 2026-07-01T12:30:15.250Z) et séparez la date et l’heure par T. Les SDK refusent un espace entre la date et l’heure pour les années 0 à 99, que Date interprète mal : "0001-01-01 00:00:00" — ce que donne str(datetime.min) en Python — serait lu comme 2001. Le SDK Python refuse aussi une chaîne que datetime.fromisoformat ne sait pas lire. Une heure sans fuseau est lue comme UTC. Une borne numérique est exprimée en millisecondes epoch et doit être finie : NaN et ±Infinity arriveraient au backend de données comme une borne ouverte et supprimeraient silencieusement le filtre temporel ; les SDK les refusent donc avant l’envoi. En Go, une borne time.Time est envoyée en RFC 3339 en UTC, et une borne sous forme de chaîne doit être l’une des formes ISO 8601 que Date lit correctement — une date, ou une date et une heure du jour avec un Z ou un décalage UTC facultatif (+02:00 ou +0200) ; les autres chaînes, comme le format de base 20260701T123000Z, un décalage en heures seulement ou un jour que le mois ne compte pas (2026-02-30), échouent avec ironflock.ErrInvalidArgument avant tout envoi.

Transforms. Un transform (une vue SQL d’un data template, voir Tables de transformation) se lit différemment d’une table. Le backend de données n’applique que limit — 3000 au maximum, une valeur supérieure est refusée avec URI_INVALID_LIMIT —, offset et filterAnd. Il ignore timeRange, columns, columnPaths et le marqueur latest, et abandonne un filtre portant sur une colonne que le transform ne déclare pas (un transform n’a pas de colonnes implicites tsp, device_key ou authid), ce qui élargit le résultat. Ses lignes arrivent dans l’ordre propre de la vue, inversé : vous n’obtenez les lignes les plus récentes, les plus anciennes en premier, que lorsque la vue trie les plus récentes en premier. Bornez la plage temporelle d’un transform dans son SQL.

Résultats volumineux. Un résultat trop volumineux pour un seul message du backend de données (8 Mio de JSON) arrive en fragments, que le SDK réassemble en mémoire : vous obtenez les lignes d’une seule lecture, dans le même ordre. Des fragments qui ne concordent pas — un fragment manquant ou dupliqué, ou un nombre de lignes différent du récapitulatif du backend de données — font échouer la lecture au lieu de renvoyer une partie des lignes. Une ligne unique de plus de 8 Mio, ou un résultat dépassant le garde-fou de 1 Gio du backend de données, est refusé avec URI_RESULT_TOO_LARGE : réduisez limit, resserrez timeRange, sélectionnez moins de columns ou élaguez avec columnPaths.

getSeriesHistory / get_series_history

Récupère des données de séries temporelles sous-échantillonnées d’une table de flotte : des colonnes agrégées par intervalle de temps (par ex. des moyennes horaires). Idéal pour les graphiques couvrant de longues périodes. Disponible pour les tables (pas pour les transforms), sur les backends de données à partir de fleetdb v1.0.58.

from datetime import datetime, timedelta, timezone from ironflock import SeriesMetric now = datetime.now(timezone.utc) avg = SeriesMetric(ref="temperature", method="AVG") # AVG, SUM, COUNT, MIN, MAX, FIRST, LAST series = await ironflock.get_series_history("sensordata", { "metrics": [avg, {"ref": "humidity", "method": "MAX"}], "bucketMs": 3_600_000, # one-hour buckets; omit to divide the range into `limit` buckets "limit": 24, # the bucket budget, 1–10000 "timeRange": [(now - timedelta(hours=24)).isoformat(), now.isoformat()], # required, with a start "groupBy": ["device_key"], }) for row in series: print(row["tsp"], row["device_key"], row[avg.column]) # avg.column is "AVG:temperature"

Paramètres de requête :

ChampTypeDescription
metricslist / arrayLes agrégats calculés par intervalle, au moins un : {"ref": ..., "method": ...}. ref est une colonne, ou un chemin JSON dans une colonne json ("json_data.temp") ; method vaut "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" ou "LAST". Chaque métrique revient sous "<METHOD>:<ref>", qui doit tenir en 63 octets
bucketMsint / numberLargeur d’un intervalle en millisecondes entières, au moins 1000 (optionnel). Omettez-le pour diviser la plage temporelle en limit intervalles
limitint / numberLe budget d’intervalles (1–10 000) — pas une limite de lignes
timeRangelist / array[start, end] — chaînes date-heure ISO ou nombres epoch-ms. Obligatoire, avec un début ; None / null comme fin signifie « maintenant ». La fin doit être postérieure au début
groupBylist / arrayColonnes qui divisent chaque intervalle en une ligne par groupe (optionnel)
filterAndlist / arrayConditions et groupes combinés par AND, comme pour getHistory (optionnel). Le marqueur latest est refusé ici, y compris à l’intérieur d’un groupe ; utilisez getHistory pour lire les valeurs actuelles

Lignes. Chaque ligne correspond à un intervalle — d’un groupe, avec groupBy. tsp est le début de l’intervalle en millisecondes epoch, chaque colonne de groupBy revient sous son propre nom, et chaque métrique sous "<METHOD>:<ref>" (SeriesMetric.column en Python, seriesColumn(metric) en JavaScript, SeriesMetric.Column() en Go), par ex. row["AVG:temperature"]. Les lignes sont triées par tsp croissant ; un intervalle sans lignes est absent (pas de comblement des trous). AVG et SUM acceptent des colonnes numériques, MIN et MAX aussi des chaînes et des horodatages ; FIRST et LAST donnent la valeur de la ligne la plus ancienne et de la plus récente de l’intervalle ; COUNT compte les valeurs non nulles (pour tsp : les lignes). Un résultat volumineux est réassemblé à partir de fragments, comme pour getHistory.

Intervalles et fin ouverte. limit compte des intervalles, pas des lignes. Sans bucketMs, la plage temporelle est divisée en limit intervalles. Avec bucketMs, le backend de données élargit les intervalles à un multiple de bucketMs lorsque la plage en contient plus que limit — et une fin ouverte s’étend jusqu’au « maintenant » du backend de données, un peu après le vôtre. Les dernières 24 heures avec une fin ouverte contiennent 25 intervalles d’une heure, si bien que limit: 24 donne des intervalles de deux heures. Bornez la fin, comme dans l’exemple, ou prévoyez un intervalle de plus : avec bucketMs et une fin ouverte, limit + 1 intervalles peuvent revenir.

Les SDK vérifient avant l’envoi ce que le backend de données refuserait : au moins une métrique, des refs non vides, des méthodes connues, des noms de colonnes de 63 octets au plus, un début, une fin postérieure au début, et bucketMs sous forme d’entier d’au moins 1000 (en Go, un Bucket d’au moins 1 seconde, en millisecondes entières). Le backend de données refuse ce à quoi il ne peut pas répondre avec URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY ou URI_SERIES_TOO_MANY_GROUPS (plus de 50 000 lignes) — voir Refus du backend de données.

Forme obsolète. Les versions du SDK jusqu’à 1.9.0 envoyaient metrics sous forme de noms de colonnes plus un method de premier niveau ({"metrics": ["temperature"], "method": "AVG", ...}), ce que les backends de données refusent depuis fleetdb v1.0.58. Cette forme fonctionne toujours : le SDK envoie une métrique {ref, method} par colonne, émet un avertissement (un DeprecationWarning en Python, un avertissement console une fois par processus en JavaScript) et renomme les colonnes du résultat d’après les noms de colonnes, de sorte que l’ancien code continue de lire row["temperature"] — sauf si la ligne possède déjà une colonne de ce nom, comme tsp pour un COUNT de tsp. Passez aux métriques {ref, method} et lisez row["AVG:temperature"]. Le SDK Go ne connaît que la forme actuelle : une méthode par métrique (SeriesMetric).

S’abonner aux données

subscribeToTable / subscribe_to_table

S’abonne aux mises à jour en temps réel d’une table de flotte. Le gestionnaire est appelé chaque fois que le backend de données stocke une nouvelle ligne, avec comme premier argument la ligne telle qu’elle a été stockée — valeurs typées selon les colonnes du data template, colonnes secrètes masquées. Les lignes écrites via le chemin d’insertion en masse (publishRowsToTable / appendRowsToTable) sont transmises à votre gestionnaire une à la fois, de sorte que le code du gestionnaire reste identique quelle que soit la manière dont les données ont été écrites.

def on_sensor_data(*args, **kwargs): row = args[0] print("New reading:", row["tsp"], row["temperature"]) await ironflock.subscribe_to_table("sensordata", on_sensor_data)

subscribeToTable / subscribe_to_table renvoie un TableSubscription, le handle de ses deux flux : son unsubscribe() les retire et met fin au rattrapage décrit ci-dessous (voir unsubscribe / unregister).

Les lignes stockées pendant la coupure de la connexion arrivent après la reconnexion. Le routeur ne rejoue pas ce qui a été publié pendant que la connexion était coupée : après chaque reconnexion, le SDK lit donc les lignes stockées entre-temps et les transmet à votre gestionnaire, de la plus ancienne à la plus récente, avant les lignes qui arrivent en direct. Il s’agit de toutes les lignes dont le tsp est postérieur à celui de la ligne la plus récente reçue par votre gestionnaire. Tant que celui-ci n’a reçu aucune ligne, le point de départ est la ligne la plus récente stockée au moment de l’abonnement : le SDK la lit juste après l’abonnement et, si cette lecture échoue, part de l’instant de l’abonnement (selon l’horloge de l’appareil) ; une table alors vide est lue depuis son début. Une ligne écrite entre-temps avec un tsp plus ancien (par exemple une mesure mise en mémoire tampon qu’un appareil envoie plus tard) n’est pas rattrapée. Un rattrapage transmet au plus les 10 000 lignes les plus récentes et journalise un avertissement lorsqu’il en a lu autant. La lecture de rattrapage attend le backend de données comme toute lecture d’historique, au plus pendant la fenêtre de reconnexion, et les lignes en direct attendent qu’elle se termine ; si elle échoue, les lignes stockées entre-temps ne sont pas transmises et un avertissement est journalisé. Les lignes rattrapées proviennent d’une lecture, et non d’événements : le gestionnaire reçoit donc chacune d’elles sans les détails de l’événement — en Python la ligne seule, en JavaScript ([row], {}), en Go un Event avec Topic et Args, distribué dans l’ordre sur le DeliveryGroup de l’abonnement. Le rattrapage fait partie du SDK à partir de la version 1.10.0.

Un transform n’a pas de lignes propres : à chaque déclenchement de sa planification, le backend de données publie la vue entière — 3000 lignes au maximum, dans l’ordre inverse de la vue — sous la forme d’un seul événement dont le premier argument est la liste des lignes. Un transform n’a pas besoin de rattrapage : son prochain déclenchement après une reconnexion publie de nouveau la vue entière. event_rows(args[0]) en Python, eventRows(args) en JavaScript et ev.Rows() en Go lisent les deux formes : la ligne d’une table donne [row], la liste d’un transform la liste elle-même ([] pour une vue vide), et tout le reste None / undefined / nil. En Go, ev.Row() vaut nil pour l’événement d’un transform.

from ironflock import event_rows def on_hourly(*args, **kwargs): for row in event_rows(args[0]) or []: print(row) await ironflock.subscribe_to_table("hourly_averages", on_hourly)

subscribe

S’abonne à un topic WAMP qui vous est propre pour de la messagerie en temps réel personnalisée. Pour les lignes de table, utilisez subscribeToTable / subscribe_to_table : le routeur refuse les abonnements aux topics bruts d’écriture des tables (voir URI autorisées pour une app).

def on_alert(*args, **kwargs): print("Alert received:", args, kwargs) alerts = await ironflock.subscribe("com.myapp.alerts", on_alert) # Later: the handler is called for no further event await alerts.unsubscribe()

subscribe renvoie un Subscription, le handle de l’abonnement (voir unsubscribe / unregister). Chaque appel renvoie son propre handle, y compris pour un topic qui fait déjà l’objet d’un abonnement.

Sans connexion, subscribe attend une connexion au plus pendant la fenêtre de reconnexion (60 secondes par défaut), comme publish, et subscribeToTable / subscribe_to_table fait de même (voir Fiabilité de la connexion).

La correspondance du topic est exacte. Des options d’abonnement, un argument facultatif placé après le gestionnaire, permettent de changer cela : un match valant "prefix" abonne à tous les topics qui commencent par celui indiqué. En Python, les options sont les SubscribeOptions d’autobahn (SubscribeOptions(match="prefix"), importées de autobahn.wamp.types), en JavaScript un objet ({ match: "prefix" }), et en Go ironflock.SubscribeOptions{Match: "prefix"}. L’abonnement est restauré avec les mêmes options après chaque reconnexion. subscribeToTable / subscribe_to_table accepte les mêmes options et les applique à ses deux flux.

unsubscribe / unregister

subscribe, subscribeToTable / subscribe_to_table et registerDeviceFunction / register_device_function renvoient chacun un handle. Un handle reste valide d’une reconnexion à l’autre jusqu’à ce que vous le retiriez : le SDK restaure ce qu’il représente après chaque reconnexion.

HandleRenvoyé parMembres
Subscriptionsubscribeunsubscribe(), topic, active
TableSubscriptionsubscribeToTable / subscribe_to_table, de votre propre app et d’une app consomméeunsubscribe(), table, active
RegistrationregisterDeviceFunction / register_device_function et ses aliasunregister(), procedure, active

topic, table et procedure désignent ce que représente le handle ; procedure est l’URI WAMP complète. active indique si l’abonnement ou l’enregistrement est actuellement actif auprès du routeur : il vaut false tant que la connexion est coupée, après une restauration qui a échoué jusqu’à ce qu’une nouvelle tentative réussisse, et dès que le handle est retiré. Un TableSubscription est actif tant que ses deux flux le sont. ironflock.unsubscribe(handle) accepte un Subscription ou un TableSubscription, et ironflock.unregister(registration) un Registration ; chacune fait la même chose que la méthode du handle lui-même.

status = await ironflock.subscribe("com.myapp.status", on_status) readings = await ironflock.subscribe_to_table("sensordata", on_row) add = await ironflock.register_device_function("com.myapp.add", add_numbers) print(readings.table, readings.active) # sensordata True await status.unsubscribe() # the same as: await ironflock.unsubscribe(status) await ironflock.unsubscribe(readings) # both feeds of the table, and its catch-up await ironflock.unregister(add) # the same as: await add.unregister()

unsubscribe() et unregister() agissent immédiatement, dès que vous les appelez, et renvoient un awaitable : attendez-le pour savoir que le routeur a répondu ; c’est là qu’un refus est levé. Un gestionnaire qui est une fonction ordinaire, et non async, peut appeler subscription.unsubscribe() sans l’attendre. Les trois classes de handle sont exportées par ironflock.

Un retrait obéit aux mêmes règles dans les trois SDK :

  • Il prend effet immédiatement. Le gestionnaire n’est plus appelé pour aucun événement : ni pour un événement encore en file d’attente, ni pour les lignes restantes d’une insertion en masse, ni pour les lignes qu’un rattrapage transmettrait. Une fonction retirée n’est plus appelée ; un appel que le routeur lui achemine encore avant d’avoir supprimé l’enregistrement est refusé avec wamp.error.no_such_procedure. Un appel déjà en cours n’est pas attendu : un gestionnaire peut donc retirer son propre abonnement. Ce que vous retirez n’est jamais restauré après une reconnexion.
  • Il n’attend pas de connexion. Tant que la connexion est coupée, l’abonnement ou l’enregistrement est retiré localement, car le routeur l’a supprimé avec la session, et l’appel rend la main immédiatement. Tant que la connexion est établie, le routeur en est informé, et l’appel se termine dès que le routeur a répondu. Un refus est levé (Python, une RuntimeError), lancé (JavaScript, une WampError) ou renvoyé (Go), mais le retrait est maintenu malgré tout.
  • Il est idempotent. Un second appel ne fait rien, pas plus qu’un appel sans handle (None, null ou undefined, nil). Après stop() / Stop, un appel ne retire l’abonnement ou l’enregistrement que localement : en Python et en JavaScript, le start() suivant ne le restaure alors pas.
  • Les handles sont indépendants. Retirer un handle ne retire jamais le gestionnaire d’un autre, même sur le même topic ou la même table. L’abonnement auprès du routeur prend fin avec le dernier handle de son topic.
  • Un TableSubscription retire ses deux flux, transformed.<table> et transformed.bulk.<table>, et met fin à son rattrapage.

Migration : avant la version 1.10.0, les SDK Python et JavaScript renvoyaient les objets d’abonnement et d’enregistrement d’autobahn, et subscribe_to_table / subscribeToTable ne renvoyait que l’abonnement au flux des lignes : le retirer laissait donc en place l’abonnement au flux des insertions en masse. Le code qui appelle unsubscribe() ou unregister() sur la valeur renvoyée continue de fonctionner, et ce qu’il retire n’est plus restauré après une reconnexion. Les autres membres des objets d’autobahn, comme id, ont disparu.

Colonnes secrètes

Une colonne déclarée secret: true dans le data-template de l’application est chiffrée au repos. Aucun chemin de lecture ordinaire ne renvoie la valeur en clair, pas même à l’application qui l’a écrite :

Chemin de lectureÀ quoi ressemble une colonne secrète
getHistory, subscribeToTable / subscribe_to_table et les lectures d’une app consommée (les lectures d’historique et les lignes en temps réel qu’utilisent aussi les boards)l’espace réservé __secret__, exporté sous le nom SECRET_PLACEHOLDER (ironflock.SecretPlaceholder en Go)
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éesle texte chiffré stocké, ifsec:1:…
revealSecrets / reveal_secretsla valeur en clair déchiffrée

NULL est transmis tel quel — NULL — sur tous les chemins, de sorte que « défini mais masqué » reste distinguable de « jamais écrit ».

Sur une table d’entités, le cycle lecture-modification-écriture est sûr : republier une ligne qui porte encore l’espace réservé __secret__ — ou omettre complètement la colonne secrète — conserve le secret stocké au lieu de l’écraser, et un null explicite l’efface. Voir Colonnes secrètes dans le guide du backend de données pour les règles exactes.

Seule une colonne string peut être secrète, et ni tsp ni une clé d’entité ne peuvent l’être. Le backend de données chiffre chaque valeur avec AES-256-GCM et un IV aléatoire qui lui est propre, si bien que le même secret est stocké sous un texte chiffré différent dans chaque ligne : une colonne secrète ne peut être ni filtrée, ni regroupée, ni triée. Un filtre sur une telle colonne est refusé avec URI_SECRET_COLUMN ; pour tester une valeur, utilisez verifySecret.

from datetime import datetime, timezone # Written like any other column — the data backend encrypts it on insert await ironflock.publish_to_table("credentials", { "tsp": datetime.now(timezone.utc).isoformat(), "device_id": "d-1", "api_token": "hunter2", }) # A normal read redacts it rows = await ironflock.getHistory("credentials", {"limit": 1}) # [{"tsp": "...", "device_id": "d-1", "api_token": "__secret__"}]

Les deux fonctions ci-dessous ne sont appelables que depuis les conteneurs de l’application elle-même. Le routeur de messages les refuse à tous les rôles navigateur ainsi qu’aux apps consommatrices, ce qui explique qu’un handle ConsumedApp n’ait pas d’équivalent : les colonnes secrètes d’une autre app arrivent masquées et le restent.

revealSecrets / reveal_secrets

Lit les lignes de l’une de vos propres tables avec ses colonnes secrètes déchiffrées.

rows = await ironflock.reveal_secrets("credentials", { "limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}], }) # [{"tsp": "...", "device_id": "d-1", "api_token": "hunter2"}]

Les paramètres de requête sont les mêmes que ceux de getHistory, à ceci près que limit ne peut pas dépasser 100. Ce plafond existe pour empêcher qu’un seul appel déchiffre une table entière, et le backend de données rejette une valeur supérieure au lieu de la ramener silencieusement à la limite. Sans paramètres de requête, l’appel lit les 10 lignes les plus récentes. Contrairement à getHistory, il renvoie les lignes de la plus récente à la plus ancienne.

Le backend de données répond au plus à 30 révélations par minute par identifiant d’app sur un appareil ; au-delà, l’appel échoue avec URI_RATE_LIMITED. Révélez un secret une fois et conservez-le, plutôt qu’à chaque utilisation. Un filtre sur une colonne secrète est refusé avec URI_SECRET_COLUMN. Le backend de données ne sert cette fonction que pour les tables qui déclarent une colonne secrète : pour toute autre table, le routeur répond wamp.error.no_such_procedure ; le SDK réessaie alors pendant toute la fenêtre de reconnexion, puis l’appel échoue avec une erreur indiquant que la procédure n’est pas enregistrée. L’erreur indique les causes possibles, en commençant par la plus probable : la table ne déclare aucune colonne secrète (secret: true dans le data template), la table n’est pas déclarée, ou le backend de données est antérieur aux colonnes secrètes.

verifySecret / verify_secret

Vérifie une valeur candidate par rapport à une colonne secrète sans la révéler — l’équivalent « vérification de mot de passe » de la fonction de révélation. La comparaison s’exécute à l’intérieur du backend de données, en temps constant, et seule la réponse est renvoyée.

ok = await ironflock.verify_secret( "credentials", "api_token", submitted_token, {"limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}]}, ) if ok: ...

Paramètres :

ParamètreTypeDescription
tablenamestr / stringLa table sur laquelle effectuer la vérification
columnstr / stringLa colonne secrète à comparer
candidatestr / stringLa valeur en clair testée. Comparée côté serveur et jamais écrite dans la table
query_params / queryParamsSecretQueryParamsQuelles lignes participent. Par défaut la ligne la plus récente ; limit ne peut pas dépasser 100

En Go, les paramètres sont des chaînes positionnelles placées après le contexte, et la requête est un *ironflock.TableQueryParams (nil pour la ligne la plus récente).

JavaScript renvoie { match, checked } et Go un *ironflock.SecretVerifyResult avec Match et Checked ; Python renvoie un simple bool. Lorsque vous disposez de checked, il vaut la peine de l’utiliser : il distingue une valeur candidate erronée (checked > 0) d’un sélecteur qui n’a trouvé aucune ligne (checked === 0), deux défaillances très différentes à signaler à un utilisateur.

Comme chaque ligne est chiffrée avec sa propre valeur aléatoire, deux lignes contenant le même secret stockent un texte chiffré différent. C’est pourquoi un filtre = sur une colonne secrète ne peut pas fonctionner — le backend de données refuse tout filtre sur une telle colonne avec URI_SECRET_COLUMN — et que cette fonction existe à la place.

Le backend de données répond au plus à 120 vérifications par minute par identifiant d’app sur un appareil ; au-delà, l’appel échoue avec URI_RATE_LIMITED. Pour une vérification à chaque requête (une clé d’API, par exemple), mettez brièvement en cache un résultat positif. Une colonne qui n’est pas une colonne secrète est refusée avec URI_NOT_A_SECRET_COLUMN ; sur une table sans aucune colonne secrète, l’appel échoue de la même manière que revealSecrets / reveal_secrets sur cette table.

Accès aux données inter-apps

Lisez les données de flotte d’une autre app depuis votre propre app, au sein du même projet. Votre app doit déclarer l’app fournisseur dans la section consumes: de son propre data-template.yml (ou détenir l’autorisation wildcard, voir plus bas), et l’utilisateur du projet doit accorder l’accès. L’accès est en lecture seule : vous pouvez interroger l’historique et vous abonner en temps réel aux lignes des tables et des transforms que le fournisseur partage, mais vous ne pouvez pas y écrire. Les connexions aux apps consommées sont mises en cache par app et par stage et fermées automatiquement lorsque votre instance s’arrête.

La connexion nécessite les identifiants propres à l’app que l’agent de l’appareil injecte (APP_AUTH_ID, APP_AUTH_SECRET). La plateforme refuse les anciens identifiants d’appareil — le repli sur le numéro de série d’une app qui n’a pas reçu d’identifiants propres, par ex. sous un agent de l’appareil plus ancien — sur le realm d’une autre app (NOT_AUTHORIZED, avec un message qui le précise), sauf si l’app fournisseur s’exécute sur le même appareil : la connexion rejoint alors son realm avec les pleins droits de cette app, et non en lecture seule.

Si votre app détient l’autorisation wildcard (consumes: [{ app: "*" }]), vous pouvez découvrir et ouvrir dynamiquement les fournisseurs avec listConsumableApps / list_consumable_apps et connectToAllApps / connect_to_all_apps (ci-dessous).

connectToApp / connect_to_app

Ouvre une connexion en lecture seule au backend de données d’une autre app et renvoie un handle. Le handle expose getHistory / get_history, subscribeToTable / subscribe_to_table et getSeriesHistory / get_series_history (tables uniquement) — les mêmes interrogations et abonnements que vous utilisez sur vos propres tables, avec les mêmes paramètres et résultats — ainsi que close et les catalogues partagés tables / transforms. Son subscribeToTable / subscribe_to_table renvoie le même handle TableSubscription que pour vos propres tables, que ironflock.unsubscribe accepte également (voir unsubscribe / unregister), et ses abonnements aux tables font l’objet du même rattrapage après une reconnexion (voir subscribeToTable).

Le handle porte aussi le nom et le stage du fournisseur (app et stage, en Go App et Stage), ainsi que sa connexion au realm du fournisseur : is_connected / isConnected / IsConnected() indique si elle est ouverte, connection / Connection() est la connexion elle-même. En JavaScript, close() se résout une fois la connexion fermée, en 5 secondes au plus.

# Open a read-only handle on another app's data backend weather = await ironflock.connect_to_app("weather-app") # Inspect what the provider shares print([t["tablename"] for t in weather.tables]) # Query history and subscribe, just like your own tables rows = await weather.get_history("forecasts", {"limit": 100}) def on_forecast(*args, **kwargs): print("New forecast:", args) await weather.subscribe_to_table("forecasts", on_forecast)

Paramètres :

ParamètreTypeDescription
app_name / appNamestr / stringNom de l’app fournisseur, tel que déclaré dans votre section consumes:
stagestr / string, optionnelStage du fournisseur : "dev" ou "prod". Par défaut, le stage de votre propre app
on_error / onErrorcallable, optionnelAppelé avec un CrossAppAccessError lorsque l’accès est refusé définitivement à la connexion ouverte (par ex. si l’autorisation est révoquée ultérieurement — voir ci-dessous quand un refus est considéré comme définitif)

Si l’accès est refusé ou mal utilisé, un CrossAppAccessError est levé (Python) / lancé (JavaScript) / renvoyé (Go, un *ironflock.CrossAppAccessError à tester avec errors.As) avec un champ code (Code en Go, avec des constantes comme ironflock.CodeNoGrant) : NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (une requête qui filtre — y compris à l’intérieur d’un groupe — ou sélectionne une colonne que le fournisseur marque comme secrète) ou NOT_AUTHORIZED (l’autorisation a été révoquée ou n’a pas pu être vérifiée à ce moment-là — ou votre app a présenté les anciens identifiants d’appareil, auquel cas le message le précise). Un subscribeToTable / subscribe_to_table d’une app consommée que le routeur du fournisseur refuse (wamp.error.not_authorized, wamp.error.authorization_failed ou wamp.error.authentication_failed) échoue lui aussi avec un CrossAppAccessError portant NOT_AUTHORIZED, comme une lecture refusée.

Un refus pendant que connectToApp / connect_to_app attend fait échouer l’appel immédiatement. Une fois un handle ouvert, un refus n’est considéré comme définitif que s’il persiste — 3 refus ou plus depuis la dernière fois que la connexion était établie (la fermeture de la session par le routeur pour une raison d’authentification compte pour un, de même que chaque reconnexion refusée), le premier remontant à au moins 60 secondes —, car la plateforme refuse de la même manière lorsqu’elle ne peut brièvement pas vérifier l’accès. D’ici là, le handle continue de se reconnecter et se rétablit si une reconnexion réussit. Une fois le refus définitif, on_error / onError / OnError est appelé et le handle est évincé : le prochain connectToApp / connect_to_app / ConnectToApp ouvre une nouvelle connexion. À partir de là, les lectures et les abonnements du handle échouent immédiatement, avec l’erreur transmise à on_error / onError — en Go, avec une erreur qui enveloppe wamp.ErrStopped.

En Python, stage et on_error sont des arguments nommés ; en JavaScript, ils sont passés via un objet d’options (connectToApp(appName, { stage, onError })) ; en Go, via une struct (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), où OnError est une func(*ironflock.CrossAppAccessError). En Go, ctx ne borne que l’attente de l’appelant : les appels simultanés pour la même app partagent une seule tentative, et Close(ctx) ferme un handle. Go compare les noms d’app sans tenir compte de la casse, et ConsumedApp.Stage est le stage en minuscules ("dev" ou "prod"), contrairement à ifl.Stage().

listConsumableApps / list_consumable_apps

Liste chaque fournisseur non privé du projet — la primitive de découverte pour les apps qui détiennent l’autorisation de consommation wildcard (consumes: [{ app: "*" }] dans votre data-template.yml, accordée par l’utilisateur du projet). Elle effectue un seul appel et n’ouvre aucune connexion : affichez les catalogues renvoyés dans un sélecteur, puis appelez connectToApp / connect_to_app pour ceux que vous souhaitez — ou connectToAllApps / connect_to_all_apps pour tous les ouvrir en une seule fois.

Remarque : Déclarez l’autorisation dans le data-template.yml de votre app, et mettez le * entre guillemets — un * nu est un alias YAML et ne sera pas analysé :

consumes: - app: "*"
providers = await ironflock.list_consumable_apps() for p in providers: print(p["app"], list(p["stages"].keys())) # e.g. "weather-app" ["dev", "prod"]

Chaque entrée décrit un fournisseur :

ChampTypeDescription
appstr / stringNom de l’app fournisseur
provider_app_keyint / numberLa clé d’app du fournisseur
stagesdict / objectCatalogue par stage { dev?, prod? } ; un stage n’est présent que si le fournisseur dispose d’un backend de données pour celui-ci. Chaque catalogue contient les tables et transforms non privés qu’il partage

En Go, chaque entrée est un ironflock.ConsumedAppInfo avec App, ProviderAppKey et Stages.Dev / Stages.Prod (un *StageCatalog avec Tables et Transforms, nil pour un stage absent) ; Catalog(stage) en renvoie un par son nom.

Lève (Python) / lance (JavaScript) / renvoie (Go) un CrossAppAccessError avec code: NO_GRANT si votre app ne détient aucune autorisation wildcard.

connectToAllApps / connect_to_all_apps

Ouvre des handles en lecture seule vers chaque fournisseur non privé du projet en un seul appel (consommateurs wildcard uniquement). Énumère les fournisseurs via listConsumableApps / list_consumable_apps et ouvre chacun d’eux, en ignorant ceux qui n’ont pas de backend de données pour le stage demandé. Chaque handle est mis en cache sous la même clé que connectToApp / connect_to_app, de sorte qu’un connectToApp(name) ultérieur renvoie le handle déjà préchauffé. Les handles renvoyés sont fermés ensemble lorsque votre instance s’arrête.

apps = await ironflock.connect_to_all_apps( on_error=lambda err: print("Provider skipped:", err) ) for app in apps: rows = await app.get_history(app.tables[0]["tablename"], {"limit": 10}) print(app.app, rows)

Paramètres :

ParamètreTypeDescription
stagestr / string, optionnelStage du fournisseur : "dev" ou "prod". Par défaut, le stage de votre propre app
continue_on_error / continueOnErrorbool / boolean, optionnelLorsque true (la valeur par défaut), un fournisseur qui échoue à s’ouvrir est signalé à on_error / onError et omis du résultat. Lorsque false, le premier échec est levé/lancé
on_error / onErrorcallable, optionnelAppelé avec l’erreur de chaque fournisseur qui n’a pas pu être ouvert (tant que continue_on_error / continueOnError vaut true), et avec un CrossAppAccessError si l’accès à une connexion déjà établie est ultérieurement refusé définitivement (par ex. si l’autorisation est révoquée)

Renvoie les handles de fournisseurs ouverts avec succès (même type de handle que connectToApp / connect_to_app). Lève (Python) / lance (JavaScript) / renvoie (Go) un CrossAppAccessError avec code: NO_GRANT si votre app ne détient aucune autorisation wildcard.

En Python, stage, on_error et continue_on_error sont des arguments nommés ; en JavaScript, ils sont passés via un objet d’options (connectToAllApps({ stage, onError, continueOnError })) ; en Go, via une struct (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), où OnError est une func(error) et où StopOnError: true correspond à continueOnError: false.

Stockage de fichiers managé

Chaque backend de données d’app dispose, à côté de ses tables, d’un stockage d’objets privé accessible via la propriété files (ifl.Files() en Go, un *filestore.FileStore). La propriété n’effectue aucune E/S : dans les trois SDK, elle peut donc être lue avant le démarrage de la connexion. Utilisez-le pour des images, des PDF, des images de caméra, des blobs de firmware — tout ce qui n’a pas sa place dans une ligne de table. Aucune configuration n’est nécessaire : une app dépourvue de section files: dans son data-template obtient malgré tout un espace de noms nommé default (un template qui déclare ses propres espaces de noms le remplace — voir Espaces de noms).

Cette section est la référence côté client. Pour le côté plateforme — déclaration des espaces de noms, quotas et rétention, partage de fichiers entre apps, identifiants S3 par app et comportement sur les appliances sur site — voir Stockage de fichiers.

L’idée clé est que stocker un objet vous renvoie une URL permanente que vous pouvez écrire directement dans une colonne de table, de sorte qu’un widget de tableau de bord puisse l’afficher sans travail supplémentaire :

from datetime import datetime, timezone # Store an object and get a permanent URL back in the same call info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg") # The URL is safe to store in a table column — a dashboard widget can then # render <img src="{{photo_url}}"> without any extra round trip await ironflock.publish_to_table("inspections", { "tsp": datetime.now(timezone.utc).isoformat(), "part_id": "1", "photo_url": info.url, }) # Read it back data = await ironflock.files.get("part-1.jpg") # Every object in the folder 2026/, in its subfolders too (pages are fetched for you) async for obj in ironflock.files.iter(prefix="2026/"): print(obj.key, obj.size)

Cette URL n’expire jamais, mais ce n’est pas un lien public : elle ne reste lisible que par un demandeur authentifié disposant de l’accès READ sur ce backend de données, et un proxy d’authentification le vérifie à chaque requête, en gardant sa décision en cache pendant 5 minutes au plus. Elle peut donc être stockée sans risque dans la base de données.

Espaces de noms

Un espace de noms est un préfixe de clé porteur de règles — rétention, règles de partage, types de contenu autorisés. Ce n’est pas un bucket distinct ; tous les espaces de noms d’une app vivent dans l’unique zone de stockage de cette app. N’en déclarez un que lorsqu’un ensemble d’objets a besoin de règles différentes ; sinon, restez dans default et organisez vos objets avec des chemins de clés comme 2026/03/part-1.jpg.

Déclarez les espaces de noms dans data-template.yml. Dès que vous en déclarez, ils remplacent le default implicite : listez aussi un espace de noms nommé default, sinon chaque appel sans espace de noms échoue avec NO_SUCH_NAMESPACE.

files: # Storage budget the app suggests for itself. The project user can change it, # and their setting is the one that gets enforced. quotaBytes: 5368709120 namespaces: # Keep default: declaring namespaces replaces the implicit one. - name: default - name: frames description: Raw camera frames, one JPEG per inspected part. contentTypes: ["image/jpeg"] maxObjectBytes: 20971520 retention: { deleteAfter: 30 days }

Notez que le budget est déclaré une seule fois pour toute l’app, et non par espace de noms. Un espace de noms n’est qu’un préfixe de clé à l’intérieur de l’unique zone de stockage de l’app : il n’y a donc rien contre quoi un budget par préfixe pourrait être appliqué. maxObjectBytes, lui, est défini par espace de noms — il plafonne un objet unique, pas un total : 100 Mio s’il n’est pas défini, 5 Gio au maximum (OBJECT_TOO_LARGE au-delà).

Chaque méthode ci-dessous accepte l’espace de noms comme argument optionnel et utilise default par défaut ; un espace de noms vide ("") signifie aussi default.

Stocker et lire des objets

# Bytes in, bytes out info = await ironflock.files.put("reports/march.pdf", pdf_bytes, content_type="application/pdf") data = await ironflock.files.get("reports/march.pdf") # Or straight from/to a local file — these stream on the large-object path, # so a multi-gigabyte file never has to fit in memory await ironflock.files.put_file("firmware/v2.bin", "/data/build/v2.bin") await ironflock.files.get_to_file("firmware/v2.bin", "/tmp/v2.bin")
MéthodeDescription
put(key, data, …)Stocke un objet (bytes en Python, Uint8Array en JavaScript, []byte en Go). Renvoie les métadonnées de l’objet, dont son url
get(key, namespace?)Renvoie le contenu de l’objet. Un objet dépassant la limite inline provient directement du stockage d’objets ou, lorsque ce n’est pas possible, par plages d’octets via le routeur (voir Objets volumineux)
put_file(key, path, …) / get_to_file(key, path, …)Python et Go (PutFile / GetToFile ; Go transfère aussi en flux depuis un io.Reader avec PutReader et vers un io.Writer avec GetTo). Stocke depuis un fichier local, ou écrit dans un fichier local. Transfère en flux sur le chemin des objets volumineux
delete(key, namespace?)Supprime un objet
copy(key, to, …)Copie un objet, éventuellement vers un autre espace de noms
move(key, to, …)Copie puis suppression. Non atomique — le service ne dispose d’aucun verbe move, une suppression échouée laisse donc les deux copies en place

put et get gardent l’objet entier en mémoire. Pour un objet volumineux, utilisez les variantes pour fichiers, qui transfèrent en flux (en Go aussi PutReader et GetTo). JavaScript n’offre pas d’assistants de chemins de fichiers, car le paquet est livré en un seul build pour Node comme pour le navigateur — lisez et écrivez vous-même les fichiers locaux avec fs.

En Python, le travail sur les fichiers de put_file et get_to_file s’exécute hors de la boucle d’événements. get_to_file crée les répertoires parents manquants et écrit le fichier sur place dès que les premiers octets de l’objet sont disponibles : une lecture refusée (objet inexistant, non autorisé) laisse le chemin intact, tandis qu’une lecture qui échoue en cours de route laisse ce qui a été écrit jusque-là. Un FIFO est écrit au fil de l’arrivée de l’objet, et son ouverture attend un lecteur : un FIFO que personne ne lit bloque donc l’appel (pas la boucle d’événements) ; asyncio.wait_for permet d’y renoncer. put_file lit un FIFO jusqu’au bout avant de le stocker.

En Go, GetToFile crée lui aussi les répertoires parents manquants, mais télécharge dans un fichier temporaire à côté de la cible et le renomme à sa place une fois le téléchargement terminé : un téléchargement échoué ou annulé ne laisse jamais de fichier partiel et conserve le fichier précédent. Les liens symboliques sont suivis (le fichier vers lequel ils pointent est écrit, et les liens restent) ; le fichier de remplacement reçoit, dans la mesure où le processus le peut, le propriétaire, le groupe et les bits de permission de l’ancien fichier. Une FIFO ou un périphérique est écrit sur place au fil du flux de l’objet. Pour une cible à côté de laquelle aucun nouveau fichier ne peut être créé, par exemple sur un système de fichiers en lecture seule ou un pseudo-système de fichiers, utilisez GetTo avec un fichier que vous ouvrez vous-même.

put accepte : content_type / contentType (le type MIME ; l’espace de noms peut restreindre ceux qui sont autorisés) et namespace. En Python, ce sont des arguments nommés ; en JavaScript, ils sont passés dans un objet d’options ; en Go, ce sont des options (filestore.ContentType(…), filestore.Namespace(…)).

Lister et inspecter

MéthodeDescription
list(…)Une page du listage du service de fichiers, façon dossiers par défaut : objects contient les objets dont les clés prolongent le préfixe sans /, prefixes les dossiers en dessous. Un delimiter égal à "" liste à plat — chaque objet sous le préfixe, y compris dans les sous-dossiers. Une page contient jusqu’à 200 objets, sauf si vous indiquez un autre limit. Renvoie objects, prefixes, is_truncated / isTruncated / IsTruncated ainsi qu’un cursor à repasser pour la page suivante
iter(…) / iterate(…)Itérateur asynchrone sur chaque objet situé sous un préfixe, y compris dans les sous-dossiers, avec pagination automatique. Nommé iter en Python, iterate en JavaScript, Iter en Go (un iter.Seq2[*ObjectInfo, error] pour une boucle range)
stat(key, namespace?)Métadonnées d’un objet sans transférer son contenu
exists(key, namespace?)Indique si un objet existe
namespaces()Les espaces de noms que cette app peut utiliser
usage(…)Quelle quantité de stockage l’app utilise — voir ci-dessous
catalog()Espaces de noms, ainsi que les limites et quotas émis par le serveur. Récupéré une fois puis mis en cache : catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) le récupère à nouveau

Dossiers. Un préfixe qui se termine par / désigne un dossier : iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) renvoie 2026/a.txt et 2026/sub/c.txt, mais pas 2026-notes.txt. list ne peut pas ouvrir un dossier par son nom, car le service de fichiers refuse un préfixe de listage qui se termine par / (FleetFiles v0.2 répond INTERNAL) : list avec le préfixe 2026 renvoie 2026-notes.txt parmi les objets et le dossier 2026/ parmi les préfixes. Le service stocke les clés en forme de normalisation Unicode C ; donnez donc aussi un préfixe de dossier en NFC. Une itération qui rencontre une clé sous une autre forme du préfixe échoue au lieu d’ignorer toutes les clés.

Les objets sont décrits par les mêmes champs dans les trois SDK, selon le style de nommage propre à chaque langage : namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 et url — en Go, les champs Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256 et URL de filestore.ObjectInfo.

Utilisation du stockage et quota

usage obtient sa réponse du magasin d’objets en un seul appel : les totaux sont donc exacts, et non additionnés par le SDK :

u = await ironflock.files.usage() print(u.size_bytes, u.object_count, u.quota_bytes, u.free_bytes) # Break the total down per namespace (costs one listing per namespace) detailed = await ironflock.files.usage(detail=True) print(detailed.per_namespace) # {"default": 1048576, "frames": 73400320}

En Go, les champs sont SizeBytes, ObjectCount, QuotaBytes, FreeBytes et PerNamespace de filestore.StorageUsage.

ChampSignification
size_bytes / sizeBytesOctets actuellement stockés
object_count / objectCountNombre d’objets stockés
quota_bytes / quotaBytesLe budget appliqué. 0 signifie illimité
free_bytes / freeBytesOctets restants. -1 signifie illimité — indiquer 0 ici se lirait comme « plein »
per_namespace / perNamespaceOctets par espace de noms. Présent uniquement lorsque vous demandez le détail

La ventilation par espace de noms est désactivée par défaut, car le magasin ne sait pas y répondre directement : il comptabilise par zone de stockage, et un espace de noms n’est qu’un préfixe ; le SDK doit donc lister chaque espace de noms pour en additionner les tailles. Demandez-la quand vous en avez besoin, pas sur un chemin critique.

Deux quotas différents apparaissent, et il vaut la peine de ne pas les confondre. catalog() renvoie les deux :

ChampSignification
quota_bytes / quotaBytesCe qui est réellement appliqué, lu depuis le magasin d’objets — le réglage de l’utilisateur du projet
suggested_quota_bytes / suggestedQuotaBytesCe que le data template de l’app a demandé. 0 s’il n’a rien demandé

En Go, Catalog(ctx) renvoie un *filestore.Catalog avec QuotaBytes et SuggestedQuotaBytes.

Ils diffèrent dès qu’un utilisateur a augmenté ou réduit le budget de l’app, et c’est précisément pourquoi la valeur appliquée est lue depuis le magasin plutôt que depuis le template — redéployer l’app ne doit pas réinitialiser silencieusement le choix d’un utilisateur. Une interface peut afficher les deux (« l’app suggère X, vous avez réglé Y »). L’application du quota s’appuie toujours sur la première valeur. L’utilisateur peut modifier le budget à tout moment, et catalog() le rapporte tel qu’il était lors de sa dernière récupération : lisez la valeur actuelle avec usage().

Partager des objets

Il existe deux types de liens, et la différence a son importance :

MéthodeDurée de vieQui peut le lire
url(key, …)PermanenteUniquement un demandeur authentifié disposant de READ sur ce backend de données — vérifié à chaque requête (une décision reste en cache 5 minutes au plus). Peut être stockée sans risque dans une colonne de table
share_url / shareUrl / ShareURLExpirante (15 min par défaut, plafonnée par le serveur)Quiconque détient le lien. Rien ne revérifie l’autorisation au moment de son utilisation

share_url / shareUrl est une capacité au porteur : remettez-la à une personne qui a besoin d’un accès temporaire, et ne la stockez pas dans la base de données. Utilisez url pour tout ce qu’affiche un tableau de bord.

url renvoie None / undefined / "" (Go) lorsque le déploiement n’a pas de frontal HTTP (par exemple une appliance en HTTP simple) — c’est le signal qu’il faut se rabattre sur get. Dans un espace de noms déclaré avec share: none, l’URL permanente ne fonctionne pas non plus, mais url ne peut pas le savoir, car le catalogue n’indique pas comment un espace de noms est partagé : il renvoie l’URL malgré tout, et le frontal de fichiers y répond par une erreur 404. Lisez ces objets avec get ; les liens de share_url / shareUrl / ShareURL ne dépendent pas de share. Passer l’etag d’un objet comme argument version (filestore.Version(etag) en Go) permet aux navigateurs de mettre la réponse en cache de façon immuable.

En Python et en Go, cloud_url(key, …) / CloudURL(ctx, key, …) renvoie l’URL permanente via le tunnel de fichiers cloud d’une appliance (https://i<instanceKey>-files.<cloud-edge>) : joignable depuis Internet tant que l’appliance est en ligne, avec la même autorisation que url. Elle renvoie None ("" en Go) sur les déploiements hébergés dans le cloud (où url est déjà joignable depuis Internet), là où il n’y a pas de frontal HTTP, et tant que le partage cloud du backend de données est désactivé. L’utilisateur peut le basculer à tout moment ; cloud_url / CloudURL relit donc le catalogue lorsque celui en cache date de plus de 30 secondes.

upload_url / uploadUrl / UploadURL génère une URL expirante (valable 1 heure par défaut) qui accepte un envoi direct, et renvoie url, method, headers et expires_in / expiresIn (en Go, un *filestore.UploadTarget avec URL, Method, Headers et ExpiresIn). Envoyez les en-têtes tels qu’ils sont renvoyés, afin que l’objet soit stocké avec le type de contenu fixé lors de la génération. Les types de contenu et la taille maximale de l’espace de noms, une size déclarée et le quota sont vérifiés au moment où l’URL est générée, et non lorsqu’elle est utilisée : l’URL n’est signée que pour l’hôte, de sorte que quiconque la détient peut écrire n’importe quel contenu sous sa clé jusqu’à son expiration. Comme share_url / shareUrl, c’est une capacité au porteur — une capacité d’écriture.

Objets volumineux

Le SDK choisit le transport en fonction de la taille, automatiquement — il n’y a rien à configurer :

Taille de l’objetComment il transite
Jusqu’à la limite inline (actuellement 6 Mio)Un seul appel via le routeur de messages
Au-delàDirectement vers le stockage d’objets en HTTPS, en contournant le routeur
Au-delà, en lecture là où il n’y a pas de chemin directPar plages d’octets via le routeur de messages

La limite exacte est indiquée à l’exécution par le serveur sous la forme inline_max_bytes / inlineMaxBytes / InlineMaxBytes dans catalog() ; elle peut donc être relevée sans nouvelle version du SDK.

Le chemin direct exige un déploiement capable de pré-signer des URL et un appareil capable de joindre l’hôte du stockage d’objets, et pas seulement le routeur. Lorsque l’un des deux manque — le catalogue indique que le déploiement ne peut pas pré-signer (une appliance sur site, par défaut), la génération de l’URL de lecture répond NOT_SUPPORTED, ou le stockage d’objets est injoignable avant l’arrivée du moindre octet —, get (et get_to_file en Python, GetTo et GetToFile en Go) lisent l’objet par plages d’octets via le routeur à la place (FleetFiles v0.1.33 et ultérieur), chacune d’au plus la limite inline. Chaque plage est une lecture à part entière : un objet remplacé pendant une telle lecture la fait échouer avec INTERNAL (“object changed while it was read”). Les écritures n’ont pas de tel repli.

Deux plafonds subsistent pour les écritures, et tous deux signalent TOO_LARGE avec une raison précisant lequel a été atteint :

  • 5 Gio — la limite d’envoi en une seule fois du stockage d’objets. L’envoi multipart n’est pas encore implémenté.
  • La limite inline, lorsqu’il n’existe pas de point de terminaison direct — un déploiement qui ne peut pas pré-signer (une appliance, par défaut) ne peut tout simplement pas stocker un objet plus volumineux. Aucun nouvel essai ni découpage plus fin n’y changera quoi que ce soit, et le message le précise.

Deux défaillances du chemin direct courantes sur le terrain reçoivent leurs propres codes au lieu de ressembler à des problèmes d’autorisation : PRESIGN_UNREACHABLE (un proxy qui n’autorise que le routeur — une lecture se rabat sur les plages d’octets, une écriture échoue) et CLOCK_SKEW (le stockage d’objets rejette les requêtes décalées de plus de 15 minutes — vérifiez NTP sur l’appareil).

Erreurs de stockage de fichiers

Une défaillance du service de fichiers ou d’un transfert direct lève (Python) / lance (JavaScript) / renvoie (Go) une FileStoreError portant un code stable et une reason lisible par un humain. Faites vos branchements sur code, jamais sur reason. Les erreurs de fichiers locaux, un appel annulé et les erreurs WAMP du routeur autres que celles associées ci-dessous sont levées telles quelles.

from ironflock.filestore import FileStoreError try: await ironflock.files.put("huge.bin", payload) except FileStoreError as e: if e.code == "QUOTA_EXCEEDED": print("Filestore is full:", e.reason) else: raise
CodeSignification
NOT_AUTHORIZEDL’appelant n’a pas le droit d’effectuer cette opération
NO_SUCH_NAMESPACEL’espace de noms n’est pas déclaré dans le data-template
NO_SUCH_OBJECTLa clé n’existe pas
TOO_LARGEDépasse la limite de transfert en un seul appel
OBJECT_TOO_LARGEDépasse le maxObjectBytes propre à l’espace de noms
QUOTA_EXCEEDEDLe filestore est plein
CONTENT_TYPE_NOT_ALLOWEDL’espace de noms restreint les contentTypes
INVALID_KEYUne clé ou un préfixe mal formé (voir ci-dessous ; FleetFiles v0.2 le signale encore comme INTERNAL)
INVALID_RANGEUne plage d’octets en dehors de l’objet ; les SDK n’en demandent jamais
NOT_SUPPORTEDLe backend ne sait pas faire cela
NOT_AVAILABLECe déploiement n’a pas de service de fichiers — ou pas encore : après un redémarrage de la plateforme, ou pendant que le service de fichiers enregistre ses procédures. L’appel n’a été exécuté nulle part et peut être répété
PRESIGN_UNREACHABLELe stockage d’objets n’est pas joignable directement (un proxy ?)
CLOCK_SKEWL’horloge de l’appareil est trop décalée
INTERNALTout le reste — y compris un objet modifié pendant une lecture par plages

Un serveur plus récent peut introduire des codes que cette version du SDK ne connaît pas. Ils sont transmis tels quels dans code plutôt que ramenés à une valeur générique : traitez donc une valeur non reconnue comme un échec générique.

Les appels de fichiers attendent la connexion jusqu’à 10 secondes (s’ils sont émis avant start() / Start, ils attendent aussi celui-ci) et ne sont pas retentés : contrairement aux opérations sur les tables, ils ne survivent pas à un redémarrage de la plateforme, si bien qu’un appel peut alors échouer avec NOT_AVAILABLE jusqu’à ce que le service de fichiers se soit de nouveau enregistré. Un tel appel n’a été exécuté nulle part ; votre app peut donc le répéter.

Clés. Le service de fichiers fait autorité sur les clés ; les SDK envoient les clés et les préfixes tels quels. Une clé ou un préfixe mal formé — vide ; avec un / initial ou final (hormis le préfixe de dossier d’iter / iterate) ; avec un segment vide, . ou .., ou un segment se terminant par . ou commençant ou se terminant par une espace ; avec l’un des caractères \ : * ? " < > | ^ ~, un caractère de contrôle ou un caractère de format Unicode ; de plus de 900 octets, 20 segments ou 255 octets par segment ; ou sous le préfixe réservé _sys/ — est refusé avec INVALID_KEY par un service de fichiers qui classe les erreurs de clé, et avec INTERNAL par FleetFiles v0.2. Attention aux horodatages : 2026-10-09T12:00:00Z n’est pas un segment de clé valide, à cause du :.

En Python, FileStoreError est importée depuis ironflock.filestore ; en JavaScript, elle est exportée depuis la racine du paquet (import { FileStoreError } from "ironflock") ; en Go, c’est *filestore.Error (aussi ironflock.FileStoreError), avec les codes sous forme de constantes comme filestore.CodeQuotaExceeded.

Communication inter-appareils

registerDeviceFunction / register_device_function

Enregistre une procédure que d’autres appareils exécutant la même app (dans le même stage) peuvent appeler, tout comme les actions des widgets de tableau de bord. Le SDK construit pour vous l’URI complète <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>, où STAGE vaut PROD lorsque ENV vaut PROD (quelle que soit la casse), et DEV sinon. Avant tout envoi, le SDK refuse un topic vide ainsi qu’une clé d’appareil, de projet ou d’app absente ou égale à 0 ; callDeviceFunction / call_device_function fait de même. Le routeur n’accepte aucun autre enregistrement de la part d’une app, et uniquement des enregistrements simples (voir URI autorisées pour une app). Sans connexion, la méthode attend une connexion au plus pendant la fenêtre de reconnexion (60 secondes par défaut, voir Fiabilité de la connexion). L’enregistrement est restauré après chaque reconnexion. La méthode renvoie un Registration, le handle de l’enregistrement : son unregister() le retire (voir unsubscribe / unregister).

def add(a, b): return a + b await ironflock.register_device_function("com.myapp.add", add)

register() est un alias de register_device_function().

Les anciens noms register_function / registerFunction / RegisterFunction et call_function / callFunction / CallFunction fonctionnent toujours, mais sont obsolètes.

Apps qui redéfinissent ENV : les versions du SDK jusqu’à 1.8.6 (Python) et 1.8.4 (JavaScript) placent la valeur brute de ENV dans le segment du stage. Cela n’a d’incidence que lorsqu’une app définit elle-même ENV avec une autre graphie, comme prod : les appareils équipés de ces versions et ceux équipés de versions plus récentes ne peuvent alors pas appeler mutuellement leurs fonctions.

callDeviceFunction / call_device_function

Appelle une procédure enregistrée par un autre appareil de la même app. Le SDK assemble automatiquement le topic WAMP complet en utilisant la clé de l’appareil cible : <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic>, la même URI que celle que registerDeviceFunction / register_device_function enregistre sur l’appareil cible.

result = await ironflock.call_device_function( 42, # target device key "com.myapp.add", # procedure name args=[3, 5] # arguments ) print(result) # 8

call

Appelle une procédure distante en utilisant une URI WAMP complète. Une app ne peut pas enregistrer un nom comme some.full.wamp.topic ; un tel nom n’atteint donc que les procédures que les services de la plateforme enregistrent sur le realm de données de l’app, comme celles du backend de données et du stockage de fichiers. Pour appeler une fonction qu’un autre appareil de votre app a enregistrée, utilisez callDeviceFunction / call_device_function.

result = await ironflock.call("some.full.wamp.topic", args=[42])

URI autorisées pour une app

Le conteneur de votre app rejoint son propre realm de données, realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>, avec le rôle de routeur app. Le routeur y vérifie chaque publication, abonnement, appel et enregistrement par rapport à la liste d’autorisations de ce rôle. Les méthodes propres au SDK restent dans ce cadre ; les règles comptent lorsque vous passez vos propres URI à publish, subscribe ou call, ou que vous utilisez directement la connection (Connection() en Go). Une action refusée échoue avec wamp.error.not_authorized. En Python, la méthode lève une RuntimeError, et wamp_error_uri(err) renvoie l’URI ; en JavaScript, l’erreur est une WampError dont error est l’URI ; en Go, ironflock.WampURI(err) la renvoie.

URI sur votre propre realmAutoriséUtilisé par
Vos propres noms, par ex. com.myapp.statuspublier, s’abonner (y compris par préfixe), appeler — jamais enregistrerpublish, subscribe, call
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table>publier — jamais s’abonner– (les topics d’écriture des tables ; les écritures dans les tables du SDK effectuent plutôt un ajout)
transformed.<table>, transformed.bulk.<table>s’abonner uniquementsubscribeToTable / subscribe_to_table
databackend.errors.…, files.events…s’abonner uniquementLes événements d’erreur et de fichiers du backend de données
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>enregistrer (simple), appeler, publier — jamais s’abonnerregisterDeviceFunction / register_device_function, callDeviceFunction / call_device_function
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.…appeler (le SDK les appelle)Méthodes de table, d’historique, de secrets et de fichiers
sys.appaccess.list, sys.appaccess.resolveappelerlistConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps
wamp.session.getappelerLe heartbeat du SDK JavaScript

Noms soumis au contrôle d’identité. Les noms qui commencent par un chiffre — les topics d’écriture des tables et les URI des fonctions d’appareil — passent en outre le contrôle d’identité de la plateforme, qui les lie à la clé de swarm de votre realm. Un enregistrement n’est accepté que pour une URI de fonction de votre propre appareil avec la clé de swarm, la clé d’app et le stage de votre realm ; un enregistrement par préfixe ou par wildcard, uniquement s’il fixe ces quatre segments. La publication et l’appel n’exigent que la clé de swarm de votre realm : une ligne publiée sur un topic de table avec une autre clé d’app est acceptée, mais n’atteint aucun backend de données et est perdue. Laissez le SDK construire ces URI.

Noms réservés. Le routeur refuse sys., hormis les deux procédures inter-apps ci-dessus, et wamp., hormis wamp.session.get. Les autres noms du tableau appartiennent à la plateforme : ne commencez pas vos propres noms par un chiffre, bulk., transformed., databackend.errors., files., append., appendBulk., history. ou secret.. Tout autre premier segment constitue l’espace de topics propre à votre app.

Personne ne s’abonne aux topics bruts des tables. Ils transportent la ligne exactement telle qu’elle a été envoyée, y compris le texte en clair des colonnes secrètes, que le backend de données ne chiffre qu’au moment où il stocke la ligne. subscribeToTable / subscribe_to_table lit plutôt les lignes stockées depuis transformed.<table>, typées et avec les colonnes secrètes masquées.

Les données des autres apps sont en lecture seule. Sur le realm d’un fournisseur, votre app (rôle de routeur app_reader) peut uniquement s’abonner à transformed.… et files.events… et appeler history.transformed.… et files.read.…, en plus du heartbeat wamp.session.get — pas de publication, pas d’enregistrement, pas d’écritures et pas de procédures de secrets.

Métadonnées de l’appareil

setDeviceLocation / set_device_location

Demande à la plateforme de mettre à jour la localisation GPS de l’appareil, que les cartes IronFlock affichent en temps réel.

Indisponible pour le moment : aucun service de la plateforme ne répond encore à cet appel sur le realm de données de l’app ; il échoue donc avec wamp.error.no_such_procedure. Conservez les localisations dans une table qui vous est propre en attendant.

await ironflock.set_device_location(long=8.6821, lat=50.1109)
ParamètrePlage
long-180 à 180
lat-90 à 90

L’historique de localisation n’est pas stocké. Pour suivre la localisation dans le temps, créez une table dédiée et utilisez publish_to_table / publishToTable.

getRemoteAccessUrlForPort

Renvoie l’URL sous laquelle un port déclaré dans le port-template.yml de l’app est joignable une fois son tunnel actif.

url = ironflock.getRemoteAccessUrlForPort(8080) # "https://<device_key>-<app_name>-8080.app.ironflock.com" vnc = ironflock.getRemoteAccessUrlForPort(5900, protocol="tcp") # "tcp://app.ironflock.com:<the tunnel's remote port>"
ParamètreTypeDescription
portint / numberLe port, tel que déclaré dans port-template.yml
protocolstr / string, optionnelLe protocole du port : "http" (par défaut), "https", "tcp" ou "udp", quelle que soit la casse ; les espaces qui l’entourent sont ignorés, et un protocole vide vaut "http"
remote_port_environment / remotePortEnvironmentstr / string, optionneltcp/udp uniquement : la variable qui contient le port distant, au lieu de REMOTE_PORT_FOR_<port> — le remote_port_environment propre au port dans le template, dont les agents d’appareil antérieurs à 0.19.8 ont besoin

Les ports http et https sont servis sous le label de tunnel <device_key>-<app name, lower case>-<port> (les ports https avec le préfixe secure- de la plateforme) sur le domaine de tunnel que l’agent de l’appareil injecte (TUNNEL_DOMAIN), toujours sous forme d’URL https://. Sur un appareil qui appartient à une appliance sur site (l’agent injecte INSTANCE_KEY), c’est la route relayée par le cloud https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN> qui est renvoyée.

Une URL tcp ou udp contient le port public attribué par le tunnel, que l’agent de l’appareil annonce sous REMOTE_PORT_FOR_<port> (agent 0.19.8 et ultérieur) : <protocol>://<TUNNEL_DOMAIN>:<remote port>. Sur l’appareil d’une appliance, le port exposé sur Internet, REMOTE_PORT_FOR_<port>_CLOUD, est préféré tant que l’agent l’annonce ; avant qu’il n’apparaisse, puis de nouveau une fois que l’agent l’a supprimé (relais cloud désactivé), c’est l’URL locale à l’appliance qui est renvoyée. Un port 0 n’a pas encore été attribué.

Les valeurs des ports sont lues en direct depuis /data/env ; rappelez donc la méthode plutôt que de mettre le résultat en cache. Certains cas ne peuvent pas être servis par l’URL :

  • L’agent de l’appareil ne crée des tunnels que pour les ports des installations de production (PROD). Dans un conteneur de développement (DEV), l’URL http(s) est celle de l’installation PROD de la même app sur cet appareil, et tcp/udp n’en donnent aucune ; joignez plutôt une app DEV via l’adresse LAN de l’appareil (DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>).
  • Sur un appareil edge d’une appliance installée sans domaine (mode HTTP en clair), l’agent définit TUNNEL_DOMAIN sur localhost, de sorte que les URL composées sur cet appareil pointent vers localhost : une telle appliance sert les ports des apps sur ses ports hôtes, ce qu’une URL de tunnel ne peut pas exprimer.
  • Les domaines de tunnel non définis ou vides valent par défaut app.ironflock.com. La plateforme ne définit pas CLOUD_TUNNEL_DOMAIN aujourd’hui : les URL d’une appliance utilisent app.ironflock.com, sauf si l’app définit elle-même cette variable dans ses paramètres d’environnement.
  • Dans un conteneur qui s’exécute sous un utilisateur autre que root, les valeurs sont celles avec lesquelles le conteneur a démarré (voir Variables d’environnement).

La méthode renvoie None / null (en Go "" et false) lorsque l’URL ne peut pas être composée : la clé de l’appareil ou le nom de l’app est manquant, aucun port distant n’a encore été annoncé pour un port tcp/udp (ou il vaut 0), le protocole est inconnu, ou le label n’est pas un label DNS valide (plus de 63 caractères, ou un nom d’app contenant un point). Dans un navigateur, il n’y a pas d’environnement : l’URL http(s) est composée sur app.ironflock.com à partir des options deviceKey et appName.

Propriétés & cycle de vie de la connexion

PropriétéTypeDescription
is_connectedboolIndique si la connexion à la plateforme est active
connectionCrossbarConnectionL’instance de connexion sous-jacente (usage avancé)
filesFileStoreLe stockage de fichiers managé de l’app (voir Stockage de fichiers managé)
MéthodeDescription
run()Démarre la connexion et exécute mainFunc (bloquant), puis arrête la connexion
await start()Démarre la connexion de manière asynchrone. Les opérations émises avant lui l’attendent, dans la limite de leur propre attente (voir Fiabilité de la connexion)
await stop()Arrête la connexion et les connexions aux apps consommées, et annule la tâche principale (mainFunc). Les connexions quittent leurs realms et ne se reconnectent plus ensuite ; start() se connecte à nouveau. Toute opération qui attend encore une connexion, ou start(), échoue immédiatement, de même qu’un start() ou un connect_to_app encore en attente ; une opération émise après stop() attend le start() suivant (voir Fiabilité de la connexion)
await run_async()Démarre et maintient la connexion en cours d’exécution de manière asynchrone

run() et run_async() arrêtent la connexion dès que mainFunc se termine ou lève une exception ; une exception qu’elle lève est de nouveau levée après l’arrêt. Une mainFunc qui ne fait que s’abonner ou enregistrer des fonctions d’appareil doit donc continuer à s’exécuter, par exemple avec await asyncio.Event().wait(). mainFunc doit être une fonction asynchrone (async def). Sans mainFunc, run() et run_async() maintiennent la connexion ouverte jusqu’à la fin du processus.

Fiabilité de la connexion

Le SDK se reconnecte tout seul. Lorsque la connexion tombe, il réessaie jusqu’au retour du routeur, puis restaure chaque abonnement et chaque fonction d’appareil enregistrée — vous n’avez pas à vous réabonner après une reconnexion, et les handles obtenus restent valides : leur active indique si chacun est actuellement actif auprès du routeur (voir unsubscribe / unregister). Les abonnements aux tables transmettent en outre les lignes stockées pendant la coupure de la connexion (voir subscribeToTable) ; les messages de vos propres topics ne sont en revanche pas stockés : ce qui a été publié entre-temps est perdu. Si l’un d’eux ne peut pas être restauré, les autres le sont quand même, et celui qui a échoué est retenté tant que la session dure — après 1 seconde, en doublant jusqu’à 30 secondes, chaque attente variant de 15 % au plus — puis de nouveau après chaque reconnexion : le routeur l’avait accepté auparavant, son refus est donc passager, comme lorsque le contrôle d’identité d’une fonction d’appareil par la plateforme échoue brièvement en mode fermé. Le premier échec est journalisé comme avertissement, et la restauration est journalisée dès qu’elle réussit.

Les opérations sur les tables — écritures dans les tables, lectures d’historique, de séries et de secrets, ainsi que les lectures d’historique et de séries d’une app consommée — survivent aussi à un redémarrage de la plateforme, comme une mise à jour du routeur ou de la plateforme. Elles attendent jusqu’à reconnect_window (Python, en secondes) / reconnectWindowMs (JavaScript) / WithReconnectWindow (Go, un time.Duration), 60 secondes par défaut, le retour de la connexion ; une opération émise avant start() / Start attend celui-ci dans la limite de la même fenêtre. Elles réessaient aussi tant que le backend de données ne sert pas encore ses procédures : le routeur répond wamp.error.no_such_procedure, ou le backend de données refuse un appel arrivé avant son enregistrement avec “client has no handler for registration”. Un tel appel n’a jamais atteint de gestionnaire, de sorte qu’une nouvelle tentative n’écrit jamais une ligne deux fois ; toute autre erreur est levée telle quelle. La lecture de rattrapage d’un abonnement de table fait partie de ces lectures.

Les écritures dans les tables comprennent publishToTable / publish_to_table, publishRowsToTable / publish_rows_to_table et reportError / report_error : elles appellent les mêmes procédures d’ajout que appendToTable et appendRowsToTable, si bien qu’une ligne écrite pendant que le backend de données redémarre n’est pas perdue.

publish, subscribe, subscribeToTable / subscribe_to_table (y compris celui d’une app consommée) et registerDeviceFunction / register_device_function attendent eux aussi une connexion dans les limites de la fenêtre de reconnexion, si bien qu’une courte coupure ne les fait pas échouer. Ils ne sont pas retentés ; un abonnement ou un enregistrement est restauré après chaque reconnexion ultérieure.

Toutes les autres opérations — call, callDeviceFunction / call_device_function, setDeviceLocation / set_device_location, les appels de fichiers, ainsi que la découverte inter-apps et l’ouverture des connexions aux apps consommées — attendent une connexion jusqu’à 10 secondes et ne sont pas retentées. Une opération émise avant start() / Start attend celui-ci dans la limite de ces 10 secondes. Une opération qui n’obtient pas de connexion pendant son attente échoue avec une erreur qui indique la durée de l’attente en secondes : en Python, les méthodes d’IronFlock lèvent une RuntimeError dont la cause est une TimeoutError ; en JavaScript, c’est une Error ordinaire ; en Go, une erreur qui enveloppe wamp.ErrNotConnected. unsubscribe et unregister n’attendent pas du tout : ils prennent effet immédiatement, même pendant que la connexion est coupée (voir unsubscribe / unregister).

0 désactive la fenêtre : ces opérations se comportent alors comme toutes les autres opérations. Elles attendent une connexion pendant 10 secondes et ne sont pas retentées, si bien qu’un rattrapage juste après un redémarrage de la plateforme n’est pas retenté non plus. Une fenêtre négative, et en Python et en JavaScript aussi NaN ou une valeur infinie, est refusée à la création de l’objet IronFlock (voir Options du constructeur).

Une requête déjà envoyée lorsque sa session se ferme (le routeur la ferme, le lien tombe ou stop()) échoue immédiatement au lieu d’attendre une réponse, et n’est pas retentée : elle a peut-être atteint son gestionnaire. En Python, la cause de l’erreur est l’ApplicationError d’autobahn portant la raison de la fermeture (par ex. wamp.close.system_shutdown), ou TransportLost lorsque le lien est tombé. En JavaScript, c’est une WampError dont error est la raison de la fermeture : celle du routeur, wamp.close.goodbye_and_out après stop(), ou wamp.close.transport_lost lorsque le lien est tombé. En Go, une telle requête échoue avec une erreur qui enveloppe wamp.ErrNotConnected (wamp.ErrStopped après Stop).

Un cas particulier est un routeur joignable qui n’a pas encore de realm pour l’app. Lors d’une installation concurrente, le conteneur de l’app démarre avant que le backend de données ait provisionné son realm, si bien que les premières tentatives de connexion sont refusées avec wamp.error.no_such_realm ; le SDK réessaie toutes les quelques secondes et l’app démarre dès que le realm existe. Un realm toujours absent après une minute n’apparaîtra très probablement jamais — l’app a été supprimée ou n’a pas de backend de données pour ce stage —, le SDK ralentit donc à partir de là à une tentative toutes les deux minutes au lieu de solliciter le routeur chaque seconde indéfiniment. La cadence rapide revient dès que le realm apparaît, ou dès que le routeur lui-même disparaît puis revient.

stop() / Stop met fin immédiatement à toute attente. Une opération qui attend encore une connexion — ou, si elle a été émise avant start() / Start, qui attend celui-ci — échoue avec l’erreur d’arrêt du SDK, de même qu’une opération sur une table en pause entre deux tentatives : en Python, une ConnectionError indiquant que la connexion IronFlock a été arrêtée (les méthodes d’IronFlock la lèvent comme cause de leur RuntimeError), en JavaScript une ConnectionStoppedError, et en Go une erreur qui enveloppe wamp.ErrStopped. Un start() qui attend encore de rejoindre son realm pour la première fois — un realm qui n’existe pas encore, par exemple — échoue avec Connection to realm <realm> stopped before it opened ; en Go, Start renvoie une erreur qui enveloppe wamp.ErrStopped. Un connect_to_app / connectToApp qui ouvre encore la connexion à un fournisseur échoue avec Connection to app '<app>' (<stage>) aborted by stop() ou, si la connexion rejoint quand même le realm, celle-ci est fermée et l’appel échoue avec Connection to app '<app>' (<stage>) closed by stop(). En Go, ConnectToApp échoue avec ironflock: connection to app '<app>' (<stage>) aborted by Stop (ou closed by Stop). Un refus qui survient en même temps que stop() conserve son CrossAppAccessError. En Python et en JavaScript, plus rien n’est en cours de connexion lorsque stop() rend la main, et un routeur qui ne répond pas quand la connexion quitte le realm est déconnecté après 5 secondes ; le Stop de Go attend la sortie du realm dans les limites de ctx, puis ferme le socket, et Run lui accorde 10 secondes.

En Python et en JavaScript, l’instance peut être redémarrée après stop() : start() se connecte à nouveau, et une opération émise après stop() attend le start() suivant, comme le fait une opération émise avant le premier start(). Le Stop de Go est définitif : une opération émise après lui échoue immédiatement avec wamp.ErrStopped.

Le cas le plus délicat est une connexion qui meurt silencieusement : un NAT ou un proxy la coupe après une période d’inactivité sans prévenir ni l’une ni l’autre extrémité, ou un routeur redémarre derrière un répartiteur de charge. Rien n’arrive pour signaler la perte. Une application qui ne fait que publier s’en aperçoit à sa prochaine écriture en échec, mais une application qui ne fait que s’abonner n’écrit jamais : sans aide, elle reste morte jusqu’au redémarrage du conteneur.

Les trois SDK détectent cette situation. La détection est active par défaut et ne demande aucune configuration.

Le SDK Python utilise le keepalive WebSocket : il envoie un ping au routeur toutes les 30 secondes tant que le lien est inactif et attend un pong dans les 10 secondes. En l’absence de réponse, la connexion est abandonnée puis rétablie. Le trafic entrant vaut preuve de vie, si bien qu’une connexion active n’est jamais sondée — le ping ne part que lorsque le lien est réellement devenu silencieux.

Les connexions inter-applications ouvertes avec connectToApp / connect_to_app ne font généralement que s’abonner, ce qui est précisément le cas que cela protège. Elles sont couvertes automatiquement, et elles se reconnectent aussi après un refus du routeur, jusqu’à ce que le refus persiste (voir connectToApp).

Gestion des erreurs

Une méthode du SDK qui échoue — en cas d’arguments invalides, de connexion perdue ou de rejet par la plateforme — lève une exception (Python), rejette (JavaScript) ou renvoie une erreur (Go) avec un message indiquant l’opération, le topic et la raison : encadrez donc d’un bloc try les appels auxquels votre code doit survivre — en Go, vérifiez l’erreur renvoyée. Certains échecs n’atteignent pas votre code. Un message publié sur l’un de vos propres topics n’atteint aucun abonné hors service entre-temps. Une lecture de rattrapage qui échoue après une reconnexion est seulement journalisée (voir subscribeToTable).

try: rows = await ironflock.getHistory("sensordata", {"limit": 100}) except ValueError as e: # Invalid parameters — e.g. limit out of range, or a malformed filter print(f"Bad query: {e}") except RuntimeError as e: # Not connected, table not in the data-template, or the platform rejected the call print(f"Query failed: {e}")

En JavaScript, un refus du routeur ou d’un appelé est généralement une WampError — une sous-classe normale d’Error qui transporte en plus l’URI d’erreur WAMP dans error et la charge utile de l’erreur dans args / kwargs. Trois types d’appels échouent autrement : un appel d’historique, de séries, de révélation ou de vérification dont la procédure n’est pas enregistrée échoue avec une Error ordinaire qui l’indique et nomme les causes probables, et seulement après toute la fenêtre de reconnexion, car le SDK réessaie en cas de wamp.error.no_such_procedure ; un appel de fichiers échoue avec une FileStoreError (voir Erreurs de stockage de fichiers) ; et un appel inter-apps refusé, y compris le subscribeToTable d’une app consommée, échoue avec un CrossAppAccessError. Une opération à laquelle stop() met fin pendant qu’elle attend la connexion échoue avec une ConnectionStoppedError, une sous-classe exportée d’Error. Tout le reste (paramètres invalides, absence de connexion) est une Error ordinaire.

En Go, une opération qui échoue renvoie un *ironflock.OperationError dont le message indique l’opération et la cause, qu’il enveloppe ; errors.Is et errors.As voient à travers. Un refus du routeur ou d’un appelé est un *ironflock.WampError (*wamp.Error) avec URI, Args et Kwargs ; ironflock.WampURI(err) renvoie l’URI, ou "". Les autres erreurs sentinelles sont ironflock.ErrMissingConfig (une identité dont l’opération a besoin manque), ironflock.ErrAlreadyStarted et wamp.ErrStopped (une opération après Stop, ou une opération que Stop a interrompue).

Migration : les anciennes versions des SDK Python et JavaScript journalisaient un message et renvoyaient None / null lorsqu’un appel échouait. Elles lèvent désormais une erreur : un code de la forme if result is None: ne détecte donc plus les échecs — utilisez try / except (ou try / catch).

Refus du backend de données

Une opération que le backend de données refuse échoue avec l’une des URI d’erreur WAMP ci-dessous, la raison étant son premier argument. Les trois SDK les exportent sous forme de constantes ; Go les nomme dans le style Go (ironflock.URIRateLimited, ironflock.URIStorageFull, …). En Python, les méthodes d’IronFlock lèvent une RuntimeError dont la cause est l’ApplicationError d’autobahn (les lectures d’une app consommée lèvent directement l’ApplicationError), et wamp_error_uri(err) lit l’URI dans l’un comme dans l’autre cas. En JavaScript, l’erreur est une WampError dont error est l’URI. En Go, c’est un *ironflock.OperationError autour d’un *ironflock.WampError, et ironflock.WampURI(err) lit l’URI. Les SDK ne retentent aucun de ces refus. Toutes les écritures dans les tables les signalent, y compris les méthodes de publication : elles effectuent un ajout.

from datetime import datetime, timezone from ironflock import URI_STORAGE_FULL, wamp_error_uri row = {"tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5} try: await ironflock.append_to_table("sensordata", row) except RuntimeError as err: if wamp_error_uri(err) == URI_STORAGE_FULL: pending.append(row) # the appliance's disk is nearly full: append the row later else: raise
ConstanteRefusé
URI_RATE_LIMITEDrevealSecrets / reveal_secrets au-delà de 30, verifySecret / verify_secret au-delà de 120 appels par minute par identifiant d’app sur un appareil
URI_RESULT_TOO_LARGEUne lecture avec une seule ligne dépassant le budget de message de 8 Mio, ou un résultat dépassant le garde-fou de 1 Gio (les autres résultats volumineux arrivent en fragments, que le SDK réassemble). Ses arguments nommés indiquent les plus grosses colonnes : réduisez limit, resserrez timeRange, sélectionnez moins de columns ou élaguez avec columnPaths
URI_INVALID_LIMITUne lecture d’un transform avec un limit supérieur à 3000
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPSRequêtes de séries auxquelles la table ne peut pas répondre : pas de début, ou une fin qui ne lui est pas postérieure ; une métrique que la table ne peut pas agréger (colonne inexistante, colonne secrète, méthode que le type de la colonne n’accepte pas, tsp avec une méthode autre que COUNT) ; une colonne groupBy selon laquelle elle ne peut pas grouper ; plus de 50 000 lignes
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMNUn filtre sur une colonne secrète ; verifySecret / verify_secret sur une colonne qui n’est pas secrète. Une table sans colonnes secrètes n’a aucune fonction de révélation ni de vérification (voir revealSecrets)
URI_STORAGE_FULLUn ajout alors que le disque de l’appliance est presque plein (moins d’environ 3 Gio libres ; de nouveau accepté à partir de 4 Gio) — temporaire : ajoutez les lignes plus tard
URI_STORAGE_OVERUSAGEUn ajout au-delà de l’allocation de stockage du compte ; réessayer peu après n’aide pas
URI_ENTITY_KEY_CONFLICTUne ligne dont la clé d’entité (maintainLatestFlagFor) et le tsp appartiennent déjà à une autre ligne, dans le lot ou parmi les lignes stockées : donnez à chaque ligne son propre tsp
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTEDUne écriture qui conserve la valeur précédente d’un secret (__secret__) sans clé d’entité permettant de la retrouver ; une écriture d’une valeur déjà chiffrée

Un ajout que le backend de données refuse sans raison — wamp.error.runtime_error, sa réponse à une ligne sans tsp — n’est pas un refus typé : le message d’erreur du SDK pointe vers le tsp manquant et vers la table error-logs de l’app, où le backend de données consigne la cause.

Utilisation dans le navigateur (JavaScript uniquement)

Le SDK JavaScript fonctionne dans les navigateurs modernes. Comme les navigateurs n’ont pas de variables d’environnement, passez toute la configuration via le constructeur :

import { IronFlock } from "ironflock"; const ironflock = new IronFlock({ serialNumber: "device-serial-from-server", deviceKey: "12", appName: "MyWebApp", swarmKey: 10, appKey: 20, env: "PROD", }); await ironflock.start(); await ironflock.publishToTable("sensordata", [{ tsp: new Date().toISOString(), temperature: 22 }]);

Utilisez IronFlock.fromServer() pour récupérer la configuration depuis votre backend au lieu de coder en dur les identifiants :

const ironflock = await IronFlock.fromServer("/api/ironflock-config"); await ironflock.start();

Votre point de terminaison backend doit renvoyer un objet JSON contenant les options de connexion (serialNumber, deviceKey, appName, swarmKey, appKey, env).

Enregistrement de fonctions pour agents IA

Le SDK peut enregistrer des fonctions appelables par des agents IA. Enregistrez une procédure et référencez son topic dans votre ai-template.yml :

def get_sensor_reading(sensor_id): """Returns the latest reading from a sensor.""" reading = read_from_hardware(sensor_id) return { "sensor_id": sensor_id, "temperature": reading.temp, "humidity": reading.hum, "timestamp": reading.ts } await ironflock.register_device_function("sensors.get_latest", get_sensor_reading)

L’agent IA peut alors appeler cette fonction lorsqu’un utilisateur pose une question nécessitant des données de capteur en temps réel.

Pour relier le topic WAMP enregistré à un agent IA, référencez-le dans le .ironflock/ai-template.yml de votre app :

sensor_agent: tool_description: | Delegate to this agent when the user asks about sensor readings, live device data, or current environmental conditions. system_prompt: | You are a sensor data specialist. Use get_current to retrieve the latest reading from any sensor. Always include the unit in your response. main: true max_context_tokens: 30000 max_iterations: 5 tools: get_current: description: Returns the latest reading from a sensor. topic: sensors.get_latest parameters: sensor_id: type: string description: The sensor identifier to query. required: true

La valeur topic (sensors.get_latest) doit correspondre au nom passé à register_device_function / registerDeviceFunction / RegisterDeviceFunction dans votre code edge. IronFlock achemine automatiquement l’appel vers l’appareil où la fonction est enregistrée.

Pour la référence complète de ai-template.yml, consultez Définir des agents et des outils.

Variables d’environnement

Ces variables sont définies automatiquement par l’environnement d’exécution IronFlock dans les conteneurs d’applications :

VariableDescription
DEVICE_NAMENom d’affichage de l’appareil
DEVICE_SERIAL_NUMBERIdentifiant unique et immuable de l’appareil
DEVICE_KEYLa clé numérique de l’appareil. Le SDK construit à partir d’elle les URI des fonctions d’appareil et les labels des tunnels d’accès à distance, et l’envoie comme métadonnée avec les publications et les écritures dans les tables ; il ne l’utilise pas pour s’authentifier
SWARM_KEYIdentifiant du projet
APP_KEYIdentifiant de l’application
APP_NAMENom de l’application
ENVEnvironnement : DEV ou PROD
APP_AUTH_ID, APP_AUTH_SECRETLes identifiants propres à l’app, que le SDK présente au routeur. L’accès inter-apps les exige ; sans eux (agent de l’appareil plus ancien), le SDK se rabat sur le numéro de série de l’appareil
DEVICE_ENDPOINT_URLL’URL du routeur à laquelle le SDK se connecte, avec son chemin remplacé par /ws-ua-usr. Elle est prioritaire sur RESWARM_URL ; une URL de routeur passée en option (ironFlockUrl, WithURL en Go) l’emporte sur les deux
RESWARM_URLL’URL du studio, dont le SDK dérive l’URL du routeur lorsque DEVICE_ENDPOINT_URL n’est pas définie. Une URL de studio que le SDK ne connaît pas fait échouer start() / Start. Sans aucune des deux variables, le SDK se connecte à wss://cbw.ironflock.com/ws-ua-usr
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")

Les variables d’accès à distance (TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …) sont listées dans Configuration des ports.

L’agent de l’appareil reflète aussi ces valeurs dans /data/env, un fichier <NAME>.txt par valeur, et maintient ce miroir à jour pendant que l’app s’exécute. Pour deux types de valeurs, les SDK lisent d’abord ce miroir, afin qu’un changement parvienne à une app en cours d’exécution : les identifiants propres à l’app (APP_AUTH_ID, APP_AUTH_SECRET), relus à chaque tentative de connexion, de sorte que des identifiants dont l’agent effectue la rotation pendant que l’app s’exécute sont présentés à la reconnexion suivante ; et les ports distants (REMOTE_PORT_FOR_<port> et REMOTE_PORT_FOR_<port>_CLOUD), lus à chaque appel de getRemoteAccessUrlForPort. Toutes les autres valeurs — l’identité (DEVICE_KEY, DEVICE_NAME, …), ENV et les domaines de tunnel — proviennent de l’environnement avec lequel le conteneur a démarré (Go lit l’identité et ENV une seule fois, dans New). L’agent rend /data/env lisible par root uniquement : dans un conteneur qui s’exécute sous un autre utilisateur, le SDK voit les valeurs avec lesquelles le conteneur a démarré et journalise un avertissement une seule fois. En JavaScript, la lecture du miroir nécessite Node.js 22.3 ou une version ultérieure ; une version plus ancienne de Node.js ne voit que l’environnement.

Last updated on