Skip to Content
IoT-App-EntwicklungIronFlock SDK

IronFlock SDK

Das IronFlock SDK ermöglicht Ihren Edge-Anwendungen die Interaktion mit der IronFlock-Plattform. Es gibt es in drei Sprachen — Python, JavaScript und Go — mit demselben Funktionsumfang, unten nebeneinander dokumentiert. Es übernimmt die Authentifizierung automatisch, wenn es auf einem registrierten Gerät ausgeführt wird, und bietet Funktionen zum Veröffentlichen von Daten, Abfragen von Verlaufsdaten, Aufrufen von Remote-Prozeduren über Geräte hinweg sowie zum Aktualisieren von Gerätemetadaten.

SDKPaketVoraussetzung
Pythonironflock auf PyPIPython 3.8+
JavaScriptironflock auf npmNode.js 18+ oder moderner Browser
Gogithub.com/RecordEvolution/ironflock-goGo 1.27.2+

Installation

pip install ironflock

Oder fügen Sie ironflock zur requirements.txt Ihrer App hinzu.

Schnellstart

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()

Bei Verwendung innerhalb eines IronFlock-App-Containers liest das SDK die Verbindungsdaten automatisch aus der Umgebung — keine manuelle Konfiguration erforderlich. Jede Zeile, die Sie in eine Tabelle schreiben, trägt einen tsp, ihren Zeitstempel (siehe publishToTable).

Konstruktor-Optionen

ironflock = IronFlock( mainFunc=main, # async function to run after connecting serial_number="abc123", # override device serial (optional) reconnect_window=60, # seconds a table operation rides out a platform restart (optional) )
ParameterBeschreibung
mainFuncEine asynchrone Funktion, die ausgeführt wird, sobald die Verbindung hergestellt ist
serial_numberÜberschreibt die Geräte-Seriennummer, die mit Veröffentlichungen und Tabellenschreibvorgängen gesendet wird. Standardmäßig wird die Umgebungsvariable DEVICE_SERIAL_NUMBER verwendet. Die Identität, mit der sich die Verbindung authentifiziert, ändert sie nicht, solange der Geräte-Agent Zugangsdaten pro App injiziert
reconnect_windowSekunden, die eine Tabellenoperation nach einem Neustart auf die Rückkehr der Plattform wartet, bevor sie einen Fehler auslöst. Standardwert ist 60; 0 schaltet dies ab (siehe Verbindungszuverlässigkeit)

Daten veröffentlichen

publishToTable / publish_to_table

Veröffentlicht einen Datensatz in einer Fleet-Tabelle. Der Tabellenname muss einer in der data-template.yml Ihrer App definierten Tabelle entsprechen. Das SDK leitet die Daten automatisch an die richtige Projektdatenbank weiter.

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" })

Das Daten-Backend liest die Zeile aus dem ersten Argument: einem Dict in Python, dem ersten Element des Arrays in JavaScript, dem ersten positionalen Argument in Go (einer Map oder einem Struct). Zusätzliche Keyword-Argumente — in Go ein ironflock.Kwargs-Wert unter den Argumenten — werden nur für Spalten gelesen, die das Data Template ihnen zuordnet (kwargs.<name>).

Jede Zeile braucht einen tsp. Jede Tabelle hat die Pflichtspalte tsp, den Zeitstempel der Zeile — z. B. einen ISO-8601-String oder in Go ein time.Time, das als RFC 3339 in UTC gesendet wird —, den das Daten-Backend aus der Zeile liest, sofern das Data Template ihn nicht anderweitig zuordnet. Eine veröffentlichte Zeile ohne tsp verwirft es, und davon zeugt nur ein Eintrag in der error-logs-Tabelle der App — der bestätigte Publish sieht erfolgreich aus. Eine Zeile, die es mangels Speicherplatz ablehnt, wird sogar ohne einen solchen Eintrag verworfen. Verwenden Sie appendToTable / append_to_table, wenn Sie wissen müssen, ob eine Zeile gespeichert wurde.

Teilzeilen in Entitätstabellen. In einer Tabelle mit Entitätsschlüssel (maintainLatestFlagFor) genügt eine Zeile mit dem Entitätsschlüssel, tsp und den Spalten, die sich ändern. Eine Spalte, die die Zeile weglässt, behält den vorherigen Wert der Entität; eine als None / null / nil gesendete Spalte wird geleert. Ob ein Feld als weggelassen gilt, hängt davon ab, wie Sie die Zeile aufbauen:

  • Python: Ein Schlüssel mit dem Wert None wird als null gesendet und leert die Spalte. Entfernen Sie den Schlüssel aus dem Dict, um den alten Wert zu behalten.
  • JavaScript: Eine Eigenschaft mit dem Wert undefined fällt aus dem Payload heraus und behält daher den alten Wert. Setzen Sie sie auf null, um die Spalte zu leeren.
  • Go: Ein Struct-Feld mit omitempty oder omitzero im Tag wird weggelassen, wenn es leer ist, und behält daher den alten Wert. Sonst wird ein nil-Pointer-Feld, wie ein nil-Wert in einer ironflock.Row, als null gesendet und leert die Spalte.

Das gilt für alle vier Schreibmethoden. In einer Tabelle ohne Entitätsschlüssel wird eine Spalte, die die Zeile weglässt, als NULL gespeichert.

appendToTable / append_to_table

Fügt Daten über einen Remote Procedure Call anstelle von Pub/Sub an eine Fleet-Tabelle an. Verwenden Sie dies, wenn Sie eine Bestätigung benötigen, dass die Daten gespeichert wurden: Eine abgelehnte Zeile führt zu einem Fehler.

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

Die Zeile braucht einen tsp, wie bei publishToTable. Eine Zeile ohne tsp lehnt das Daten-Backend mit wamp.error.runtime_error und ohne Begründung ab; der Fehler des SDK ergänzt dann “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table”. Die typisierten Ablehnungen, etwa eine volle Festplatte auf einer Appliance, sind unter Ablehnungen durch das Daten-Backend aufgeführt.

publishRowsToTable / publish_rows_to_table

Veröffentlicht mehrere Zeilen in einer einzigen Nachricht (Bulk-Insert) in einer Fleet-Tabelle. Die Plattform fügt den gesamten Stapel atomar (Alles-oder-nichts) in einem einzigen Vorgang ein. Verwenden Sie dies für hochfrequente Daten, bei denen ein Roundtrip pro Zeile zu kostspielig wäre. Wie publishToTable erfolgt dies nach dem Fire-and-Forget-Prinzip — die Bestätigung bestätigt die Zustellung an den Router, nicht den Datenbank-Insert.

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}, ])

Das zweite Argument ist eine nicht leere Liste von einzufügenden Zeilenobjekten, jedes mit einem eigenen tsp — in Go ein Slice von Zeilen ([]ironflock.Row, []map[string]any oder ein Slice von Structs). Ein Stapel, den das Daten-Backend ablehnt — etwa weil einer Zeile ihr tsp fehlt —, wird wie bei publishToTable verworfen, ohne dass der Publisher davon erfährt.

appendRowsToTable / append_rows_to_table

Hängt mehrere Zeilen in einem einzigen Remote Procedure Call (Bulk-Insert) an eine Fleet-Tabelle an. Die Plattform fügt den gesamten Stapel atomar (Alles-oder-nichts) ein: Ist eine Zeile ungültig, wird der gesamte Stapel abgelehnt und nichts wird gespeichert. Bevorzugen Sie dies gegenüber publishRowsToTable / publish_rows_to_table, wenn Sie das Ergebnis des Inserts benötigen. Jede Zeile braucht einen eigenen tsp; die Ablehnungen sind die von appendToTable, einschließlich des Hinweises für einen ohne Begründung abgelehnten Stapel.

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

Meldet einen Anwendungsfehler in die error-logs-Tabelle Ihrer Fleet. Dies ist ein komfortabler Wrapper über publishToTable / appendToTable: Er versieht die Zeile mit source: "app", einem Schweregrad level und einem Zeitstempel und schreibt sie anschließend wie jede normale Tabellenzeile. Der Fehler landet in derselben error-logs-Tabelle, die auch fleetdb-Systemfehler verwenden (gekennzeichnet mit source: "system"), und ist somit mit getHistory abfragbar, mit subscribeToTable / subscribe_to_table streambar, in Board-Templates nutzbar und wird in Echtzeit auf transformed.error-logs zugestellt — ohne den System-Fehler-Toast der Plattform auszulösen.

# Fire-and-forget (default): publishes to the error-logs table await ironflock.report_error("Sensor read timed out", level="warn") # Pass an exception to capture its traceback (falls back to the message) try: risky_operation() except Exception as err: await ironflock.report_error(err) # Use the append RPC when you want to await the insert outcome, and show the # operator a friendlier line than the raw error await ironflock.report_error( "Calibration failed: offset 0.42 out of range", append=True, user_message="Please recalibrate the sensor", )

Die Zeile ist { tsp, msg, user_message, source: "app", level }: msg enthält den technischen Text (einen Traceback oder Stack), user_message die Zeile, die der Fehler-Toast des Boards anzeigt — die von Ihnen übergebene Benutzermeldung oder, ohne eine solche, die Meldung selbst.

Parameter:

ParameterTypBeschreibung
errorstr / string oder Exception / ErrorDie Fehlermeldung oder eine Exception, deren Traceback/Stack (oder Meldung) aufgezeichnet wird
levelstr / string, optionalSchweregrad: "error", "warn", "info" oder "debug". Standardwert ist "error"
appendbool / boolean, optionalBei true wird der Append-RPC verwendet (gibt das Ergebnis des Inserts zurück). Standardwert ist false (Fire-and-Forget-Publish)
tspstr / string, optionalÜberschreibt den Zeitstempel im ISO-8601-Format. Standardwert ist die aktuelle Zeit
user_message / userMessagestr / string, optionalText für den Bediener, gespeichert als user_message. Standardwert ist die aufgezeichnete Meldung

In Python werden die Optionen als Keyword-Argumente übergeben (report_error(error, level=..., append=..., tsp=..., user_message=...)); in JavaScript werden sie über ein Optionsobjekt übergeben (reportError(error, { level, append, tsp, userMessage })); in Go über ein Struct (ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), wobei errOrMsg ein error oder ein String ist, Level einer von ironflock.LevelError, LevelWarn, LevelInfo, LevelDebug, und das zurückgegebene *Result ist nil, sofern Append nicht gesetzt ist.

publish

Veröffentlicht eine Nachricht an ein eigenes WAMP-Topic. Verwenden Sie dies für benutzerdefinierte Nachrichten oder Ereignisse, die keiner Datenbanktabelle zugeordnet sind. Der Router reserviert einige Namen für die Plattform — siehe URIs, die eine App verwenden darf.

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

Historische Daten abfragen

getHistory

Ruft historische Daten aus einer Fleet-Tabelle oder einem Transform ab: die neuesten passenden Zeilen, aufsteigend nach tsp sortiert. Unterstützt Filterung, Zeitbereiche und Paginierung.

# 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"], })

Abfrageparameter:

FeldTypBeschreibung
limitint / numberMaximale Anzahl zurückgegebener Zeilen (1–10.000, erforderlich; 1–3.000 bei einem Transform)
offsetint / numberOffset für Paginierung
timeRangelist / array[start, end] — ISO-Datetime-Strings oder Epoch-Millisekunden-Zahlen, nie gemischt; None / null für ein offenes Ende. Ein Legacy-Objekt {"start": ..., "end": ...} wird in das Paar umgewandelt
filterAndlist / arrayUND-verknüpfte Filtereinträge: Bedingungen, Gruppen und/oder der latest-Marker (siehe unten)
columnslist / arrayZurückzugebende Spalten (optional). tsp, device_key und authid sind immer enthalten; weglassen für alle Spalten
columnPathslist / arrayBeschneidet json-Spalten (optional): JSON-Pfade in der Notation, die auch Filter verwenden ("json_data.a.b", "json_data['key']"). Eine in columns aufgeführte json-Spalte enthält dann nur die Vereinigung ihrer Pfade; eine Spalte ohne Pfad hier kommt vollständig zurück. Wird von Transforms und von Daten-Backends vor fleetdb v1.0.52 ignoriert

In Go sind dies die Felder Limit, Offset, TimeRange, FilterAnd, Columns und ColumnPaths von ironflock.TableQueryParams. TimeRange ist ein *ironflock.TimeRange{Start, End}: jede Grenze ein time.Time, ein ISO-8601-String, eine Zahl in Epoch-Millisekunden oder nil für ein offenes Ende (ironflock.Between, Since und Until bilden einen aus time.Time-Werten). FilterAnd ist ein []ironflock.Filter. Eine veraltete Objektform gibt es nicht.

Filteroperatoren: =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL

Jede Filterbedingung ist ein Objekt mit den Schlüsseln column, operator und value — in Go gebildet mit ironflock.Where(column, operator, value), ironflock.IsNull(column) und ironflock.IsNotNull(column). Operatoren werden unabhängig von Groß- und Kleinschreibung erkannt, umgebende Leerzeichen werden ignoriert, und gesendet werden sie in der oben aufgeführten Form — der einzigen, die das Daten-Backend akzeptiert —, sodass "like" als "LIKE" hinausgeht, auch innerhalb von Gruppen. Ein Operator außerhalb der Liste wird unverändert und mit einer Warnung gesendet. IS NULL und IS NOT NULL erwarten keinen value und sind die einzige Möglichkeit, nach NULL zu fragen: Jeder andere Operator schließt eine Zeile aus, deren Spalte NULL ist, genau wie in SQL. IN und NOT IN erwarten eine Liste (in Go ein Slice). LIKE unterscheidet Groß- und Kleinschreibung; verwenden Sie ILIKE für einen Vergleich ohne Beachtung der Groß- und Kleinschreibung.

Mit OR kombinieren. Die Einträge von filterAnd werden UND-verknüpft. Ein Gruppeneintrag verknüpft seine eigenen Einträge — Bedingungen, den latest-Marker oder weitere Gruppen — mit einem einzigen Operator, "AND" oder "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}, ], } ]

Ein Daten-Backend, das noch keine Gruppen kennt (fleetdb vor v1.2.0), lehnt diese Form ab, statt nur einen Teil davon anzuwenden; der Aufruf schlägt also lautstark fehl, statt stillschweigend Zeilen zurückzugeben, die der Filter ausschließt.

Aktuelle Werte lesen. Ein Eintrag {"latest": true} in filterAnd (ironflock.Latest() in Go) ist keine Filterbedingung, sondern ein Modusschalter: Das Daten-Backend gibt nur die neueste Zeile pro Entität zurück, in SQL abgeleitet aus dem Entitätsschlüssel, den die Tabelle mit maintainLatestFlagFor deklariert. Eine Tabelle ohne Entitätsschlüssel gibt ihre einzelne aktuellste Zeile zurück.

Andere Bedingungen lassen sich wie erwartet mit dem Marker kombinieren: Bedingungen auf Spalten des Entitätsschlüssels schränken ein, welche Entitäten zurückgegeben werden, während alle übrigen Bedingungen und timeRange auf die resultierenden neuesten Zeilen angewendet werden. Die Kombination von {"latest": true} mit einem deleted-Filter blendet gelöschte Entitäten also aus, statt deren vorherige Zeile wieder auftauchen zu lassen.

Frühere Versionen von IronFlock speicherten eine physische Spalte latest_flag. Diese existiert nicht mehr — ein alter Filter latest_flag = true wird weiterhin akzeptiert und als Marker behandelt, neuer Code sollte aber {"latest": true} verwenden. Der latest-Marker steht in getSeriesHistory nicht zur Verfügung.

Zeitbereiche. Das Daten-Backend liest eine Zeitgrenze als String mit JavaScripts Date; schreiben Sie daher ein ISO-8601-Datum oder eine ISO-8601-Datums- und Uhrzeitangabe (2026-07-01, 2026-07-01T12:30:15.250Z) und trennen Sie Datum und Uhrzeit mit T. Die SDKs lehnen ein Leerzeichen zwischen Datum und Uhrzeit in den Jahren 0–99 ab, weil Date es falsch liest: "0001-01-01 00:00:00" — was Pythons str(datetime.min) liefert — würde als 2001 gelesen. Eine Uhrzeit ohne Zeitzone wird als UTC gelesen. Eine numerische Grenze ist in Epoch-Millisekunden angegeben und muss endlich sein: NaN und ±Infinity kämen beim Daten-Backend als offene Grenze an und ließen den Zeitfilter stillschweigend wegfallen, daher lehnen die SDKs sie vor dem Senden ab. In Go wird eine time.Time-Grenze als RFC 3339 in UTC gesendet, und eine String-Grenze muss eine der ISO-8601-Formen sein, die Date korrekt liest — ein Datum oder ein Datum mit Uhrzeit und optionalem Z oder UTC-Offset (+02:00 oder +0200); andere Strings, etwa das Basisformat 20260701T123000Z, ein Offset nur in Stunden oder ein Tag, den der Monat nicht hat (2026-02-30), schlagen mit ironflock.ErrInvalidArgument fehl, bevor etwas gesendet wird.

Transforms. Ein Transform (eine SQL-View eines Data Templates, siehe Transformationstabellen) wird anders gelesen als eine Tabelle. Das Daten-Backend wendet nur limit — höchstens 3000, ein größerer Wert wird mit URI_INVALID_LIMIT abgelehnt —, offset und filterAnd an. Es ignoriert timeRange, columns, columnPaths und den latest-Marker und verwirft einen Filter auf eine Spalte, die der Transform nicht deklariert (ein Transform hat keine impliziten Spalten tsp, device_key oder authid), wodurch das Ergebnis größer wird. Seine Zeilen kommen in der eigenen Reihenfolge der View, umgekehrt: Die neuesten Zeilen, die ältesten zuerst, erhalten Sie nur, wenn die View die neuesten zuerst sortiert. Begrenzen Sie den Zeitbereich eines Transforms in seinem SQL.

Große Ergebnisse. Ein Ergebnis, das für eine Nachricht des Daten-Backends zu groß ist (8 MiB JSON), kommt in Chunks an, die das SDK im Speicher wieder zusammensetzt: Sie erhalten die Zeilen eines einzigen Lesevorgangs in derselben Reihenfolge. Chunks, die nicht aufgehen — einer fehlt oder ist doppelt, oder die Zeilenzahl weicht von der Zusammenfassung des Daten-Backends ab —, lassen den Lesevorgang fehlschlagen, statt einen Teil der Zeilen zurückzugeben. Eine einzelne Zeile über 8 MiB oder ein Ergebnis über der 1-GiB-Schutzgrenze des Daten-Backends wird mit URI_RESULT_TOO_LARGE abgelehnt: Senken Sie limit, verengen Sie timeRange, wählen Sie weniger columns aus oder beschneiden Sie mit columnPaths.

getSeriesHistory / get_series_history

Ruft heruntergerechnete Zeitreihendaten aus einer Fleet-Tabelle ab: Spalten, die pro Zeitfenster aggregiert werden (z. B. stündliche Durchschnittswerte). Ideal für Diagramme über lange Zeiträume. Für Tabellen verfügbar (nicht für Transforms), auf Daten-Backends ab 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"

Abfrageparameter:

FeldTypBeschreibung
metricslist / arrayDie pro Zeitfenster berechneten Aggregate, mindestens eines: {"ref": ..., "method": ...}. ref ist eine Spalte oder ein JSON-Pfad in eine json-Spalte ("json_data.temp"); method ist "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" oder "LAST". Jede Metrik kommt unter "<METHOD>:<ref>" zurück, was in 63 Bytes passen muss
bucketMsint / numberBreite eines Zeitfensters in ganzen Millisekunden, mindestens 1000 (optional). Ohne Angabe wird der Zeitbereich in limit Zeitfenster aufgeteilt
limitint / numberDas Budget an Zeitfenstern (1–10.000) — kein Zeilenlimit
timeRangelist / array[start, end] — ISO-Datetime-Strings oder Epoch-ms-Zahlen. Erforderlich, mit einem Start; None / null als Ende bedeutet „jetzt“. Das Ende muss nach dem Start liegen
groupBylist / arraySpalten, die jedes Zeitfenster in eine Zeile pro Gruppe aufteilen (optional)
filterAndlist / arrayUND-verknüpfte Bedingungen und Gruppen, wie bei getHistory (optional). Der latest-Marker wird hier abgelehnt, auch innerhalb einer Gruppe; verwenden Sie getHistory, um aktuelle Werte zu lesen

Zeilen. Jede Zeile ist ein Zeitfenster — mit groupBy das Zeitfenster einer Gruppe. tsp ist der Beginn des Zeitfensters in Epoch-Millisekunden, jede groupBy-Spalte kommt unter ihrem eigenen Namen zurück und jede Metrik unter "<METHOD>:<ref>" (SeriesMetric.column in Python, seriesColumn(metric) in JavaScript, SeriesMetric.Column() in Go), z. B. row["AVG:temperature"]. Die Zeilen sind aufsteigend nach tsp sortiert; ein Zeitfenster ohne Zeilen fehlt (keine Lückenfüllung). AVG und SUM nehmen numerische Spalten, MIN und MAX auch Strings und Zeitstempel; FIRST und LAST liefern den Wert der frühesten bzw. spätesten Zeile des Zeitfensters; COUNT zählt die Werte, die nicht NULL sind (bei tsp: die Zeilen). Ein großes Ergebnis wird wie bei getHistory aus Chunks zusammengesetzt.

Zeitfenster und ein offenes Ende. limit zählt Zeitfenster, nicht Zeilen. Ohne bucketMs wird der Zeitbereich in limit Zeitfenster aufgeteilt. Mit bucketMs verbreitert das Daten-Backend die Zeitfenster auf ein Vielfaches von bucketMs, wenn der Bereich mehr als limit davon enthält — und ein offenes Ende reicht bis zum eigenen Jetzt des Daten-Backends, ein wenig nach Ihrem. Die letzten 24 Stunden mit offenem Ende enthalten 25 einstündige Zeitfenster, sodass limit: 24 zweistündige liefert. Begrenzen Sie das Ende wie im Beispiel oder lassen Sie ein Zeitfenster mehr zu: Mit bucketMs und offenem Ende können limit + 1 Zeitfenster zurückkommen.

Die SDKs prüfen vor dem Senden, was das Daten-Backend ablehnen würde: mindestens eine Metrik, nicht leere Refs, bekannte Methoden, Spaltennamen innerhalb von 63 Bytes, einen Start, ein Ende nach dem Start und bucketMs als ganze Zahl von mindestens 1000 (in Go Bucket von mindestens 1 Sekunde, in ganzen Millisekunden). Was es nicht beantworten kann, lehnt das Daten-Backend mit URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY oder URI_SERIES_TOO_MANY_GROUPS (mehr als 50.000 Zeilen) ab — siehe Ablehnungen durch das Daten-Backend.

Veraltete Form. SDK-Versionen bis 1.9.0 sendeten metrics als Spaltennamen plus ein method auf oberster Ebene ({"metrics": ["temperature"], "method": "AVG", ...}), was Daten-Backends seit fleetdb v1.0.58 ablehnen. Diese Form funktioniert weiterhin: Das SDK sendet eine {ref, method}-Metrik pro Spalte, warnt (eine DeprecationWarning in Python, eine Konsolenwarnung einmal pro Prozess in JavaScript) und benennt die Ergebnisspalten zurück in die Spaltennamen, sodass alter Code weiterhin row["temperature"] liest — es sei denn, die Zeile hat bereits eine Spalte dieses Namens, etwa tsp bei COUNT von tsp. Steigen Sie auf {ref, method}-Metriken um und lesen Sie row["AVG:temperature"]. Das Go-SDK kennt nur die aktuelle Form: eine Methode pro Metrik (SeriesMetric).

Daten abonnieren

subscribeToTable / subscribe_to_table

Abonniert Echtzeit-Updates einer Fleet-Tabelle. Der Handler wird immer dann aufgerufen, wenn das Daten-Backend eine neue Zeile speichert, und erhält als erstes Argument die Zeile so, wie sie gespeichert wurde — Werte typisiert gemäß den Spalten des Data Templates, geheime Spalten maskiert. Zeilen, die über den Bulk-Insert-Pfad (publishRowsToTable / appendRowsToTable) geschrieben werden, werden Ihrem Handler einzeln zugestellt, sodass der Handler-Code unabhängig davon, wie die Daten geschrieben wurden, gleich bleibt.

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)

Ein Transform hat keine eigenen Zeilen: Bei jedem Takt seines Zeitplans veröffentlicht das Daten-Backend die gesamte View — höchstens 3000 Zeilen, in umgekehrter Reihenfolge der View — als ein Ereignis, dessen erstes Argument die Liste der Zeilen ist. event_rows(args[0]) in Python, eventRows(args) in JavaScript und ev.Rows() in Go lesen beide Formen: Die Zeile einer Tabelle ergibt [row], die Liste eines Transforms die Liste ([] bei einer leeren View), alles andere None / undefined / nil. In Go ist ev.Row() beim Ereignis eines Transforms nil.

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

Abonniert ein eigenes WAMP-Topic für benutzerdefinierte Echtzeit-Kommunikation. Für Tabellenzeilen verwenden Sie subscribeToTable / subscribe_to_table: Der Router lehnt Subscriptions auf die rohen Schreib-Topics der Tabellen ab (siehe URIs, die eine App verwenden darf).

def on_alert(*args, **kwargs): print("Alert received:", args, kwargs) await ironflock.subscribe("com.myapp.alerts", on_alert)

Geheime Spalten

Eine Spalte, die in der data-template.yml der App mit secret: true deklariert ist, wird verschlüsselt gespeichert. Kein gewöhnlicher Lesepfad gibt den Klartext zurück, nicht einmal an die App, die ihn geschrieben hat:

LesepfadWie eine geheime Spalte aussieht
getHistory, subscribeToTable / subscribe_to_table und alles Weitere über den Message Routerder Platzhalter __secret__, exportiert als SECRET_PLACEHOLDER (ironflock.SecretPlaceholder in Go)
SQL-Zugriff (der Postgres-Login FleetDB Access)das gespeicherte Chiffrat, ifsec:1:…
revealSecrets / reveal_secretsder entschlüsselte Klartext

NULL wird auf jedem Pfad als NULL durchgereicht, sodass „gesetzt, aber verborgen“ von „nie geschrieben“ unterscheidbar bleibt.

Auf einer Entitätstabelle ist Read-Modify-Write sicher: Eine Zeile erneut zu veröffentlichen, die noch den Platzhalter __secret__ trägt — oder die geheime Spalte ganz wegzulassen — behält das gespeicherte Geheimnis, statt es zu überschreiben, und ein explizites null löscht es. Die genauen Regeln finden Sie unter Geheime Spalten im Daten-Backend-Leitfaden.

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__"}]

Beide nachfolgenden Funktionen sind ausschließlich aus den eigenen Containern der App heraus aufrufbar. Der Message Router verweigert sie jeder Browser-Rolle und allen konsumierenden Apps — deshalb hat ein ConsumedApp-Handle auch keine Entsprechung dazu: Die geheimen Spalten einer fremden App kommen geschwärzt an und bleiben es auch.

revealSecrets / reveal_secrets

Liest Zeilen einer Ihrer eigenen Tabellen mit entschlüsselten geheimen Spalten.

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

Die Abfrageparameter sind dieselben wie bei getHistory, mit einem Unterschied: limit darf 100 nicht überschreiten. Diese Obergrenze verhindert, dass ein einzelner Aufruf eine ganze Tabelle entschlüsselt, und das Daten-Backend weist einen größeren Wert zurück, statt ihn stillschweigend zu kappen.

Das Daten-Backend beantwortet höchstens 30 Reveal-Aufrufe pro Minute und App-Zugangsdaten auf einem Gerät; darüber hinaus schlägt der Aufruf mit URI_RATE_LIMITED fehl. Lesen Sie ein Geheimnis einmal entschlüsselt aus und behalten Sie es, statt es bei jeder Verwendung erneut abzurufen. Eine Tabelle ohne geheime Spalten wird mit URI_NOT_A_SECRET_COLUMN abgelehnt, ein Filter auf eine geheime Spalte mit URI_SECRET_COLUMN.

verifySecret / verify_secret

Prüft einen Kandidatenwert gegen eine geheime Spalte, ohne sie preiszugeben — das Passwortprüfungs-Gegenstück zur Reveal-Funktion. Der Vergleich läuft innerhalb des Daten-Backends, in konstanter Zeit, und zurück kommt nur die Antwort.

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

Parameter:

ParameterTypBeschreibung
tablenamestr / stringDie Tabelle, gegen die geprüft wird
columnstr / stringDie zu vergleichende geheime Spalte
candidatestr / stringDer zu prüfende Klartext. Wird serverseitig verglichen und niemals in die Tabelle geschrieben
query_params / queryParamsSecretQueryParamsWelche Zeilen teilnehmen. Standardmäßig die neueste Zeile; limit darf 100 nicht überschreiten

In Go sind die Parameter positionale Strings nach dem Kontext, und die Abfrage ist ein *ironflock.TableQueryParams (nil für die neueste Zeile).

JavaScript gibt { match, checked } zurück und Go ein *ironflock.SecretVerifyResult mit Match und Checked; Python gibt einen einfachen bool zurück. Wo Ihnen checked zur Verfügung steht, lohnt es sich, den Wert zu nutzen: Er trennt einen falschen Kandidaten (checked > 0) von einem Selektor, auf den überhaupt keine Zeile passte (checked === 0) — zwei sehr unterschiedliche Fehler, die einem Benutzer auch unterschiedlich gemeldet werden sollten.

Da jede Zeile unter einem eigenen Zufallswert verschlüsselt wird, speichern zwei Zeilen mit demselben Geheimnis unterschiedliche Chiffrate. Genau deshalb kann ein =-Filter auf einer geheimen Spalte nicht funktionieren — das Daten-Backend lehnt jeden Filter auf einer solchen Spalte mit URI_SECRET_COLUMN ab —, und genau deshalb gibt es diese Funktion.

Das Daten-Backend beantwortet höchstens 120 Prüfungen pro Minute und App-Zugangsdaten auf einem Gerät; darüber hinaus schlägt der Aufruf mit URI_RATE_LIMITED fehl. Für eine Prüfung bei jeder Anfrage (etwa eines API-Schlüssels) speichern Sie ein positives Ergebnis kurz zwischen. Eine Spalte, die keine geheime Spalte ist, wird mit URI_NOT_A_SECRET_COLUMN abgelehnt.

App-übergreifender Datenzugriff

Lesen Sie die Fleet-Daten einer anderen App aus Ihrer eigenen App heraus – innerhalb desselben Projekts. Ihre App muss die bereitstellende App im Abschnitt consumes: ihrer eigenen data-template.yml deklarieren (oder die Wildcard-Consume-Berechtigung besitzen, siehe unten), und der Projektbenutzer muss den Zugriff gewähren. Der Zugriff ist schreibgeschützt: Sie können den Verlauf abfragen und Zeilen der vom Anbieter freigegebenen Tabellen und Transforms in Echtzeit abonnieren, aber nicht in sie schreiben. Verbindungen zu konsumierten Apps werden pro App zwischengespeichert und automatisch geschlossen, wenn Ihre Instanz stoppt.

Die Verbindung benötigt die Zugangsdaten pro App, die der Geräte-Agent injiziert (APP_AUTH_ID, APP_AUTH_SECRET). Die Plattform lehnt die alten Gerätezugangsdaten — den Seriennummern-Fallback einer App, die keine eigenen Zugangsdaten erhalten hat, z. B. unter einem älteren Geräte-Agenten — auf dem Realm einer anderen App ab (NOT_AUTHORIZED, mit einer Meldung, die genau das sagt), es sei denn, die bereitstellende App läuft auf demselben Gerät: Dann tritt die Verbindung deren Realm mit den vollen eigenen Rechten dieser App bei statt schreibgeschützt.

Wenn Ihre App die Wildcard-Berechtigung besitzt (consumes: [{ app: "*" }]), können Sie Anbieter dynamisch mit listConsumableApps / list_consumable_apps und connectToAllApps / connect_to_all_apps (siehe unten) auffinden und öffnen.

connectToApp / connect_to_app

Öffnet eine schreibgeschützte Verbindung zum Data Backend einer anderen App und gibt ein Handle zurück. Das Handle stellt getHistory / get_history, subscribeToTable / subscribe_to_table und getSeriesHistory / get_series_history (nur Tabellen) bereit – dasselbe Abfragen und Abonnieren wie bei Ihren eigenen Tabellen, mit denselben Parametern und Ergebnissen – sowie close und die freigegebenen Kataloge tables / transforms.

# 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)

Parameter:

ParameterTypBeschreibung
app_name / appNamestr / stringName der bereitstellenden App, wie in Ihrem consumes:-Abschnitt deklariert
stagestr / string, optionalStage des Anbieters: "dev" oder "prod". Standardmäßig die Stage Ihrer eigenen App
on_error / onErrorCallback, optionalWird mit einem CrossAppAccessError aufgerufen, wenn der geöffneten Verbindung der Zugriff endgültig verweigert wird (z. B. wenn die Berechtigung später widerrufen wird — wann eine Verweigerung als endgültig gilt, steht unten)

Bei verweigertem oder falsch verwendetem Zugriff wird ein CrossAppAccessError ausgelöst (Python) bzw. geworfen (JavaScript) bzw. zurückgegeben (Go, ein *ironflock.CrossAppAccessError, den Sie mit errors.As prüfen) mit einem code-Feld (Code in Go, mit Konstanten wie ironflock.CodeNoGrant): NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (eine Abfrage, die auf eine vom Anbieter als geheim markierte Spalte filtert — auch innerhalb einer Gruppe — oder sie auswählt) oder NOT_AUTHORIZED (die Berechtigung wurde widerrufen oder konnte gerade nicht überprüft werden — oder Ihre App hat die alten Gerätezugangsdaten vorgelegt; dann sagt die Meldung genau das).

Eine Verweigerung, während connectToApp / connect_to_app wartet, lässt den Aufruf sofort fehlschlagen. Ist ein Handle erst geöffnet, gilt eine Verweigerung nur dann als endgültig, wenn sie anhält — 3 oder mehr Ablehnungen, seit die Verbindung zuletzt stand (das Schließen der Session durch den Router aus einem Authentifizierungsgrund zählt als eine, ebenso jede abgelehnte Neuverbindung), die erste vor mindestens 60 Sekunden —, denn die Plattform lehnt genauso ab, während sie den Zugriff kurzzeitig nicht überprüfen kann. Bis dahin verbindet sich das Handle immer wieder neu und erholt sich, wenn eine Neuverbindung gelingt. Ist die Verweigerung endgültig, wird on_error / onError / OnError aufgerufen und das Handle verworfen: Der nächste Aufruf von connectToApp / connect_to_app / ConnectToApp öffnet eine neue Verbindung. Lesevorgänge und Subscriptions des Handles schlagen von da an sofort fehl, mit dem Fehler, den on_error / onError erhalten hat — in Go mit einem Fehler, der wamp.ErrStopped umhüllt.

In Python sind stage und on_error Keyword-Argumente; in JavaScript werden sie über ein Optionsobjekt übergeben (connectToApp(appName, { stage, onError })); in Go über ein Struct (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), wobei OnError eine func(*ironflock.CrossAppAccessError) ist. In Go begrenzt ctx nur das Warten des Aufrufers: Gleichzeitige Aufrufe für dieselbe App teilen sich einen Versuch, und Close(ctx) schließt ein Handle.

listConsumableApps / list_consumable_apps

Listet jeden nicht privaten Anbieter im Projekt auf – das Discovery-Primitiv für Apps, die die Wildcard-Consume-Berechtigung besitzen (consumes: [{ app: "*" }] in Ihrer data-template.yml, gewährt durch den Projektbenutzer). Sie führt einen einzigen Aufruf aus und öffnet keine Verbindungen: Stellen Sie die zurückgegebenen Kataloge in einem Auswahlmenü dar und rufen Sie dann connectToApp / connect_to_app für die gewünschten auf – oder connectToAllApps / connect_to_all_apps, um alle auf einmal zu öffnen.

Hinweis: Deklarieren Sie die Berechtigung in der data-template.yml Ihrer App und setzen Sie das * in Anführungszeichen – ein bloßes * ist ein YAML-Alias und kann nicht geparst werden:

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"]

Jeder Eintrag beschreibt einen Anbieter:

FeldTypBeschreibung
appstr / stringName der bereitstellenden App
provider_app_keyint / numberDer App-Schlüssel des Anbieters
stagesdict / objectKatalog pro Stage { dev?, prod? }; eine Stage ist nur vorhanden, wenn der Anbieter dafür ein Data Backend besitzt. Jeder Katalog enthält die nicht privaten tables und transforms, die er freigibt

In Go ist jeder Eintrag ein ironflock.ConsumedAppInfo mit App, ProviderAppKey und Stages.Dev / Stages.Prod (ein *StageCatalog mit Tables und Transforms, nil für eine fehlende Stage); Catalog(stage) liefert einen davon über seinen Namen.

Es wird ein CrossAppAccessError mit code: NO_GRANT ausgelöst (Python) bzw. geworfen (JavaScript) bzw. zurückgegeben (Go), wenn Ihre App keine Wildcard-Berechtigung besitzt.

connectToAllApps / connect_to_all_apps

Öffnet in einem einzigen Aufruf schreibgeschützte Handles zu jedem nicht privaten Anbieter im Projekt (nur für Wildcard-Consumer). Zählt die Anbieter über listConsumableApps / list_consumable_apps auf und öffnet jeden einzelnen, wobei alle ohne Data Backend für die angeforderte Stage übersprungen werden. Jedes Handle wird unter demselben Schlüssel wie bei connectToApp / connect_to_app zwischengespeichert, sodass ein späteres connectToApp(name) das bereits vorgewärmte Handle zurückgibt. Die zurückgegebenen Handles werden gemeinsam geschlossen, wenn Ihre Instanz stoppt.

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)

Parameter:

ParameterTypBeschreibung
stagestr / string, optionalStage des Anbieters: "dev" oder "prod". Standardmäßig die Stage Ihrer eigenen App
continue_on_error / continueOnErrorbool / boolean, optionalBei true (Standardwert) wird ein Anbieter, der nicht geöffnet werden kann, an on_error / onError gemeldet und aus dem Ergebnis ausgelassen. Bei false wird der erste Fehler ausgelöst/geworfen
on_error / onErrorcallable, optionalWird mit jedem Anbieter aufgerufen, der nicht geöffnet werden konnte (solange continue_on_error / continueOnError true ist), und mit einem CrossAppAccessError, wenn einer bereits geöffneten Verbindung später der Zugriff endgültig verweigert wird (z. B. wenn die Berechtigung widerrufen wird)

Gibt die erfolgreich geöffneten Anbieter-Handles zurück (derselbe Handle-Typ wie bei connectToApp / connect_to_app). Es wird ein CrossAppAccessError mit code: NO_GRANT ausgelöst (Python) bzw. geworfen (JavaScript) bzw. zurückgegeben (Go), wenn Ihre App keine Wildcard-Berechtigung besitzt.

In Python sind stage, on_error und continue_on_error Keyword-Argumente; in JavaScript werden sie über ein Optionsobjekt übergeben (connectToAllApps({ stage, onError, continueOnError })); in Go über ein Struct (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), wobei OnError eine func(error) ist und StopOnError: true continueOnError: false entspricht.

Verwalteter Dateispeicher

Jedes App-Data-Backend erhält neben seinen Tabellen einen privaten Objektspeicher, erreichbar über die Eigenschaft files (ifl.Files() in Go, ein *filestore.FileStore, der schon vor Start abgerufen werden kann). Verwenden Sie ihn für Bilder, PDFs, Kamerabilder, Firmware-Blobs — alles, was nicht in eine Tabellenzeile gehört. Es ist keine Einrichtung nötig: Auch eine App ohne files:-Abschnitt in ihrer data-template.yml erhält einen Namespace mit dem Namen default (ein Template, das eigene Namespaces deklariert, ersetzt ihn — siehe Namespaces).

Dieser Abschnitt ist die clientseitige Referenz. Für die Plattformseite — das Deklarieren von Namespaces, Kontingenten und Aufbewahrung, das Teilen von Dateien zwischen Apps, S3-Zugangsdaten pro App und das Verhalten auf On-Premises-Appliances — siehe Dateispeicher.

Der zentrale Gedanke: Das Ablegen eines Objekts liefert Ihnen eine permanente URL zurück, die Sie direkt in eine Tabellenspalte schreiben können, sodass ein Dashboard-Widget sie ohne weiteres Zutun darstellen kann:

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)

Diese URL läuft nie ab, ist aber kein öffentlicher Link: Sie bleibt nur für einen authentifizierten Anfragenden lesbar, der READ-Zugriff auf dieses Data Backend besitzt, und ein Auth-Proxy prüft das bei jeder Anfrage erneut. Sie kann daher gefahrlos in der Datenbank gespeichert werden.

Namespaces

Ein Namespace ist ein Schlüsselpräfix, das Richtlinien mitführt — Aufbewahrung, Freigaberegeln, erlaubte Content-Types. Er ist kein separater Bucket; jeder Namespace einer App liegt innerhalb des einen Speicherbereichs dieser App. Deklarieren Sie einen eigenen Namespace nur, wenn eine Menge von Objekten andere Regeln benötigt; bleiben Sie andernfalls in default und organisieren Sie Ihre Objekte über Schlüsselpfade wie 2026/03/part-1.jpg.

Namespaces deklarieren Sie in der data-template.yml. Sobald Sie welche deklarieren, ersetzen diese den impliziten default: Führen Sie auch einen Namespace namens default auf, sonst schlägt jeder Aufruf ohne Namespace mit NO_SUCH_NAMESPACE fehl.

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 }

Beachten Sie, dass das Budget einmal für die gesamte App deklariert wird, nicht pro Namespace. Ein Namespace ist lediglich ein Schlüsselpräfix innerhalb des einen Speicherbereichs der App, sodass es nichts gibt, wogegen sich ein Budget pro Präfix durchsetzen ließe. maxObjectBytes gilt dagegen pro Namespace — es begrenzt ein einzelnes Objekt, keine Summe: 100 MiB, sofern nicht gesetzt, höchstens 5 GiB (OBJECT_TOO_LARGE darüber).

Jede der folgenden Methoden nimmt den Namespace als optionales Argument entgegen und verwendet standardmäßig default; ein leerer Namespace ("") bedeutet ebenfalls default.

Objekte ablegen und lesen

# 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")
MethodeBeschreibung
put(key, data, …)Legt ein Objekt ab (bytes in Python, Uint8Array in JavaScript, []byte in Go). Gibt die Metadaten des Objekts inklusive seiner url zurück
get(key, namespace?)Gibt den Inhalt des Objekts zurück. Ein Objekt über dem Inline-Limit kommt direkt aus dem Objektspeicher oder, wo das nicht möglich ist, in Byte-Bereichen über den Router (siehe Große Objekte)
put_file(key, path, …) / get_to_file(key, path, …)Python und Go (PutFile / GetToFile; Go streamt außerdem mit PutReader aus einem io.Reader und mit GetTo in einen io.Writer). Aus einer lokalen Datei ablegen bzw. in eine lokale Datei schreiben. Streamt auf dem Pfad für große Objekte
delete(key, namespace?)Löscht ein Objekt
copy(key, to, …)Kopiert ein Objekt, optional in einen anderen Namespace
move(key, to, …)Kopieren und anschließend löschen. Nicht atomar — der Dienst kennt kein Move-Verb, daher hinterlässt ein fehlgeschlagenes Löschen beide Kopien

JavaScript besitzt keine Helfer für Dateipfade, weil das Paket einen einzigen Build für Node und den Browser ausliefert — lesen und schreiben Sie lokale Dateien selbst mit fs.

In Python läuft die Dateiarbeit von put_file und get_to_file außerhalb der Event-Loop. get_to_file legt fehlende übergeordnete Verzeichnisse an und schreibt die Datei an Ort und Stelle, sobald die ersten Bytes des Objekts vorliegen: Ein abgelehnter Lesevorgang (kein solches Objekt, nicht autorisiert) lässt den Pfad unberührt, während ein Lesevorgang, der mittendrin fehlschlägt, das bis dahin Geschriebene hinterlässt. Ein FIFO wird beschrieben, während das Objekt hereinströmt, und sein Öffnen wartet auf einen Leser, sodass ein FIFO, den niemand liest, den Aufruf blockiert (nicht die Event-Loop); mit asyncio.wait_for lässt er sich aufgeben. put_file liest einen FIFO bis zum Ende, bevor es ihn speichert.

In Go legt auch GetToFile fehlende übergeordnete Verzeichnisse an, lädt aber in eine temporäre Datei neben dem Ziel herunter und benennt sie an ihren Platz um, sobald der Download vollständig ist: Ein fehlgeschlagener oder abgebrochener Download hinterlässt nie eine unvollständige Datei und behält die vorherige. Symbolischen Links wird gefolgt (die Datei, auf die sie verweisen, wird geschrieben, und die Links bleiben bestehen); die Ersatzdatei erhält, soweit der Prozess das darf, Besitzer, Gruppe und Berechtigungsbits der alten Datei. Ein FIFO oder ein Gerät wird an Ort und Stelle beschrieben, während das Objekt hereinströmt. Für ein Ziel, neben dem keine neue Datei angelegt werden kann, etwa auf einem schreibgeschützten oder einem Pseudo-Dateisystem, verwenden Sie GetTo mit einer Datei, die Sie selbst öffnen.

put akzeptiert: content_type / contentType (den MIME-Typ; der Namespace kann einschränken, welche erlaubt sind) und namespace. In Python sind dies Keyword-Argumente; in JavaScript stehen sie in einem Optionsobjekt; in Go sind es Optionen (filestore.ContentType(…), filestore.Namespace(…)).

Auflisten und inspizieren

MethodeBeschreibung
list(…)Eine Seite der Auflistung des Dateidienstes, standardmäßig ordnerartig: objects enthält die Objekte, deren Schlüssel das Präfix ohne / fortsetzen, prefixes die Ordner darunter. Ein delimiter von "" listet flach — jedes Objekt unterhalb des Präfixes, auch in Unterordnern. Gibt objects, prefixes, is_truncated / isTruncated / IsTruncated und einen cursor zurück, den Sie für die nächste Seite wieder übergeben
iter(…) / iterate(…)Asynchroner Iterator über jedes Objekt unterhalb eines Präfixes, auch in Unterordnern, mit automatischer Paginierung. In Python heißt er iter, in JavaScript iterate, in Go Iter (ein iter.Seq2[*ObjectInfo, error] für eine range-Schleife)
stat(key, namespace?)Metadaten zu einem Objekt, ohne dessen Inhalt zu übertragen
exists(key, namespace?)Ob ein Objekt existiert
namespaces()Die Namespaces, die diese App verwenden darf
usage(…)Wie viel Speicher die App belegt — siehe unten
catalog()Namespaces sowie die vom Server vorgegebenen Limits und Kontingente. Wird einmal abgerufen und zwischengespeichert: catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) ruft ihn erneut ab

Ordner. Ein Präfix, das auf / endet, bezeichnet einen Ordner: iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) liefert 2026/a.txt und 2026/sub/c.txt, aber nicht 2026-notes.txt. list kann einen Ordner nicht über seinen Namen öffnen, weil der Dateidienst ein Auflistungspräfix ablehnt, das auf / endet (FleetFiles v0.2 antwortet mit INTERNAL): list mit dem Präfix 2026 liefert 2026-notes.txt unter den Objekten und den Ordner 2026/ unter den Präfixen. Der Dienst speichert Schlüssel in der Unicode-Normalisierungsform C, geben Sie ein Ordnerpräfix daher ebenfalls in NFC an; eine Iteration, die auf einen Schlüssel unter einer anderen Form des Präfixes trifft, schlägt fehl, statt jeden Schlüssel zu überspringen.

Objekte werden in allen drei SDKs durch dieselben Felder beschrieben, jeweils in der Namenskonvention der Sprache: namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 und url — in Go die Felder Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256 und URL von filestore.ObjectInfo.

Speicherbelegung und Kontingent

usage beantwortet die Frage in einem einzigen Aufruf direkt aus dem Objektspeicher, sodass die Summen exakt sind und nicht vom SDK zusammengezählt werden:

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}

In Go sind die Felder SizeBytes, ObjectCount, QuotaBytes, FreeBytes und PerNamespace von filestore.StorageUsage.

FeldBedeutung
size_bytes / sizeBytesAktuell gespeicherte Bytes
object_count / objectCountAnzahl der gespeicherten Objekte
quota_bytes / quotaBytesDas durchgesetzte Budget. 0 bedeutet unbegrenzt
free_bytes / freeBytesVerbleibende Bytes. -1 bedeutet unbegrenzt — eine 0 würde hier als „voll“ gelesen
per_namespace / perNamespaceBytes pro Namespace. Nur vorhanden, wenn Sie die detaillierte Aufschlüsselung anfordern

Die Aufschlüsselung pro Namespace ist standardmäßig abgeschaltet, weil der Speicher sie nicht direkt beantworten kann: Er rechnet pro Speicherbereich ab, und ein Namespace ist nur ein Präfix, sodass das SDK jeden Namespace auflisten muss, um die Größen aufzusummieren. Fordern Sie sie an, wenn Sie sie brauchen, aber nicht auf einem heißen Pfad.

Es tauchen zwei verschiedene Kontingente auf, und es lohnt sich, sie auseinanderzuhalten. catalog() meldet beide:

FeldBedeutung
quota_bytes / quotaBytesWas tatsächlich durchgesetzt wird, aus dem Objektspeicher gelesen — die Einstellung des Projektnutzers
suggested_quota_bytes / suggestedQuotaBytesWas das Data Template der App angefordert hat. 0, wenn es nichts angefordert hat

In Go liefert Catalog(ctx) einen *filestore.Catalog mit QuotaBytes und SuggestedQuotaBytes.

Sie unterscheiden sich immer dann, wenn ein Nutzer das Budget der App erhöht oder gesenkt hat. Genau deshalb wird der durchgesetzte Wert aus dem Speicher gelesen und nicht aus dem Template — ein erneutes Deployment der App darf die Entscheidung eines Nutzers nicht stillschweigend zurücksetzen. Eine Oberfläche kann beide anzeigen („die App schlägt X vor, Sie haben Y eingestellt“). Durchgesetzt wird immer der erste Wert. Der Nutzer kann das Budget jederzeit ändern, und catalog() meldet es mit dem Stand seines letzten Abrufs: Lesen Sie den aktuellen Wert aus usage().

Objekte freigeben

Es gibt zwei Arten von Links, und der Unterschied ist wesentlich:

MethodeLebensdauerWer kann ihn lesen
url(key, …)PermanentNur ein authentifizierter Anfragender mit READ auf diesem Data Backend — bei jeder Anfrage erneut geprüft. Kann gefahrlos in einer Tabellenspalte gespeichert werden
share_url / shareUrl / ShareURLAblaufend (standardmäßig 15 Minuten, serverseitig begrenzt)Jeder, der den Link besitzt. Bei seiner Verwendung prüft nichts die Autorisierung erneut

share_url / shareUrl ist eine Bearer-Berechtigung: Geben Sie den Link an eine Person weiter, die vorübergehend Zugriff benötigt, und speichern Sie ihn nicht in der Datenbank. Verwenden Sie url für alles, was ein Dashboard darstellt.

url gibt None / undefined / "" (Go) zurück, wenn das Deployment keinen HTTP-Edge besitzt (zum Beispiel eine Appliance mit reinem HTTP) — das ist das Signal, auf get zurückzufallen. Wird der etag eines Objekts als version-Argument übergeben (filestore.Version(etag) in Go), können Browser die Antwort unveränderlich zwischenspeichern.

In Python und Go liefert cloud_url(key, …) / CloudURL(ctx, key, …) die permanente URL über den Cloud-Datei-Tunnel einer Appliance (https://i<instanceKey>-files.<cloud-edge>): aus dem Internet erreichbar, solange die Appliance online ist, mit derselben Autorisierung wie url. Sie gibt None ("" in Go) zurück bei in der Cloud gehosteten Deployments (wo url bereits aus dem Internet erreichbar ist), wo es keinen HTTP-Edge gibt, und solange die Cloud-Freigabe des Daten-Backends ausgeschaltet ist. Der Nutzer kann die Freigabe jederzeit umschalten, daher liest cloud_url / CloudURL den Katalog neu, wenn der zwischengespeicherte älter als 30 Sekunden ist.

upload_url / uploadUrl / UploadURL erzeugt eine ablaufende URL, die einen direkten Upload entgegennimmt, und gibt url, method, headers sowie expires_in / expiresIn zurück (in Go ein *filestore.UploadTarget mit URL, Method, Headers und ExpiresIn). Senden Sie die Header unverändert mit, damit das Objekt mit dem beim Erzeugen festgelegten Content-Type gespeichert wird. Die Content-Types und die Größenobergrenze des Namespace, eine deklarierte size und das Kontingent werden beim Erzeugen der URL geprüft, nicht bei ihrer Verwendung: Die URL ist nur für den Host signiert, sodass jeder, der sie besitzt, bis zu ihrem Ablauf beliebige Inhalte unter ihren Schlüssel schreiben kann. Wie share_url / shareUrl ist sie eine Bearer-Berechtigung — eine, die schreibt.

Große Objekte

Das SDK wählt den Transportweg automatisch anhand der Größe — es gibt nichts zu konfigurieren:

ObjektgrößeÜbertragungsweg
Bis zum Inline-Limit (derzeit 6 MiB)Ein einzelner Aufruf über den Message Router
DarüberDirekt in den Objektspeicher über HTTPS, unter Umgehung des Routers
Darüber, beim Lesen ohne direkten PfadIn Byte-Bereichen über den Message Router

Das genaue Limit meldet der Server zur Laufzeit als inline_max_bytes / inlineMaxBytes / InlineMaxBytes in catalog(), sodass es ohne ein SDK-Release angehoben werden kann.

Der direkte Pfad setzt ein Deployment voraus, das URLs vorsignieren kann, und ein Gerät, das den Host des Objektspeichers erreicht, nicht nur den Router. Wo eines davon fehlt — der Katalog meldet, dass das Deployment nicht vorsignieren kann (standardmäßig bei einer On-Premises-Appliance), das Erzeugen der Lese-URL antwortet mit NOT_SUPPORTED, oder der Objektspeicher ist nicht erreichbar, bevor ein Byte angekommen ist —, lesen get (und Pythons get_to_file, Gos GetTo und GetToFile) das Objekt stattdessen in Byte-Bereichen über den Router (FleetFiles v0.1.33 und neuer), jeder höchstens so groß wie das Inline-Limit. Jeder Bereich ist ein eigener Lesevorgang: Ein Objekt, das während eines solchen Lesens ersetzt wird, lässt es mit INTERNAL fehlschlagen (“object changed while it was read”). Schreibvorgänge haben keinen solchen Fallback.

Für Schreibvorgänge bleiben zwei Obergrenzen bestehen, und beide melden TOO_LARGE mit einem Grund, der benennt, welche davon Sie erreicht haben:

  • 5 GiB — das Limit des Objektspeichers für einen einzelnen Upload. Multipart-Upload ist noch nicht implementiert.
  • Das Inline-Limit, wo es keinen direkten Endpunkt gibt — ein Deployment, das nicht vorsignieren kann (standardmäßig eine Appliance), kann ein größeres Objekt überhaupt nicht speichern. Kein Retry und keine kleinere Chunk-Größe hilft, und die Meldung sagt das auch.

Zwei im Feld häufige Fehler des direkten Pfads erhalten eigene Codes, statt wie Autorisierungsprobleme auszusehen: PRESIGN_UNREACHABLE (ein Proxy, der nur den Router zulässt — ein Lesevorgang weicht auf Byte-Bereiche aus, ein Schreibvorgang schlägt fehl) und CLOCK_SKEW (der Objektspeicher weist Anfragen zurück, die mehr als 15 Minuten abweichen — prüfen Sie NTP auf dem Gerät).

Fehler beim Dateispeicher

Ein Fehler des Dateidienstes oder einer direkten Übertragung löst einen FileStoreError aus (Python) bzw. wirft ihn (JavaScript) bzw. gibt ihn zurück (Go), der einen stabilen code und einen menschenlesbaren reason trägt. Verzweigen Sie über code, niemals über reason. Lokale Dateifehler, ein abgebrochener Aufruf und WAMP-Fehler auf Router-Ebene außer den unten zugeordneten werden unverändert ausgelöst.

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
CodeBedeutung
NOT_AUTHORIZEDDer Aufrufer darf diese Operation nicht ausführen
NO_SUCH_NAMESPACEDer Namespace ist in der data-template.yml nicht deklariert
NO_SUCH_OBJECTDer Schlüssel existiert nicht
TOO_LARGEÜberschreitet das Übertragungslimit für einen einzelnen Aufruf
OBJECT_TOO_LARGEÜberschreitet das maxObjectBytes des jeweiligen Namespace
QUOTA_EXCEEDEDDer Dateispeicher ist voll
CONTENT_TYPE_NOT_ALLOWEDDer Namespace schränkt contentTypes ein
INVALID_KEYEin fehlerhafter Schlüssel oder ein fehlerhaftes Präfix (siehe unten; FleetFiles v0.2 meldet dies noch als INTERNAL)
INVALID_RANGEEin Byte-Bereich außerhalb des Objekts; die SDKs fordern nie einen solchen an
NOT_SUPPORTEDDas Backend kann dies nicht
NOT_AVAILABLEDieses Deployment besitzt keinen Dateidienst — oder noch nicht: nach einem Plattform-Neustart oder während der Dateidienst seine Prozeduren registriert. Der Aufruf wurde nirgends ausgeführt und darf wiederholt werden
PRESIGN_UNREACHABLEDer Objektspeicher ist nicht direkt erreichbar (ein Proxy?)
CLOCK_SKEWDie Geräteuhr weicht zu stark ab
INTERNALAlles Übrige — auch ein Objekt, das sich während eines Lesens in Byte-Bereichen geändert hat

Ein neuerer Server kann Codes einführen, die dieses SDK-Release noch nicht kennt. Sie werden unverändert als code durchgereicht statt zusammengefasst zu werden; behandeln Sie einen unbekannten Wert daher als generischen Fehler.

Dateiaufrufe warten auf die Verbindung (bis zu 10 Sekunden in Python und Go, 6 in JavaScript) und werden nicht wiederholt: Anders als Tabellenoperationen überbrücken sie keinen Plattform-Neustart, sodass ein Aufruf währenddessen mit NOT_AVAILABLE fehlschlagen kann, bis sich der Dateidienst wieder registriert hat. Ein solcher Aufruf wurde nirgends ausgeführt, Ihre App darf ihn also wiederholen.

Schlüssel. Maßgeblich für Schlüssel ist der Dateidienst; die SDKs senden Schlüssel und Präfixe unverändert. Ein fehlerhafter Schlüssel oder ein fehlerhaftes Präfix — leer; mit führendem oder abschließendem / (abgesehen vom Ordnerpräfix von iter / iterate); mit einem leeren, .- oder ..-Segment oder einem Segment, das auf . endet oder mit einem Leerzeichen beginnt oder endet; mit einem der Zeichen \ : * ? " < > | ^ ~, einem Steuerzeichen oder einem Unicode-Formatzeichen; über 900 Bytes, 20 Segmente oder 255 Bytes pro Segment; oder unter dem reservierten Präfix _sys/ — wird von einem Dateidienst, der Schlüsselfehler klassifiziert, mit INVALID_KEY abgelehnt und von FleetFiles v0.2 mit INTERNAL. Achten Sie auf Zeitstempel: 2026-10-09T12:00:00Z ist wegen des : kein gültiges Schlüsselsegment.

In Python wird FileStoreError aus ironflock.filestore importiert; in JavaScript wird er aus dem Paket-Root exportiert (import { FileStoreError } from "ironflock"); in Go ist es *filestore.Error (auch ironflock.FileStoreError), mit den Codes als Konstanten wie filestore.CodeQuotaExceeded.

Geräteübergreifende Kommunikation

registerDeviceFunction / register_device_function

Registriert eine Prozedur, die andere Geräte aufrufen können, auf denen dieselbe App (in derselben Stage) läuft, ebenso Aktionen von Dashboard-Widgets. Das SDK bildet die vollständige URI <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic> für Sie, wobei STAGE DEV oder PROD ist. Der Router akzeptiert von einer App keine andere Registrierung und nur Einzelregistrierungen (siehe URIs, die eine App verwenden darf). Die Registrierung wird nach jeder Neuverbindung wiederhergestellt.

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

register() ist ein Alias für register_device_function().

callDeviceFunction / call_device_function

Ruft eine Prozedur auf, die ein anderes Gerät derselben App registriert hat. Das SDK erstellt das vollständige WAMP-Topic automatisch anhand des Schlüssels des Zielgeräts: <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic> — dieselbe URI, die registerDeviceFunction / register_device_function auf dem Zielgerät registriert.

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

call

Ruft eine Remote-Prozedur über eine vollständige WAMP-URI auf. Eine App kann einen Namen wie some.full.wamp.topic nicht registrieren, daher erreicht ein solcher Name nur Prozeduren, die Plattformdienste auf dem Daten-Realm der App registrieren, etwa die des Daten-Backends und des Dateispeichers. Um eine Funktion aufzurufen, die ein anderes Gerät Ihrer App registriert hat, verwenden Sie callDeviceFunction / call_device_function.

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

URIs, die eine App verwenden darf

Ihr App-Container tritt seinem eigenen Daten-Realm realm-<SWARM_KEY>-<APP_KEY>-<dev|prod> mit der Router-Rolle app bei. Der Router prüft dort jedes Veröffentlichen, Abonnieren, Aufrufen und Registrieren gegen die Allow-List dieser Rolle. Die eigenen Methoden des SDK bleiben innerhalb dieser Liste; die Regeln sind relevant, wenn Sie eigene URIs an publish, subscribe oder call übergeben oder die connection direkt verwenden (Connection() in Go). Eine abgelehnte Aktion schlägt mit wamp.error.not_authorized fehl (in Go liefert ironflock.WampURI(err) diese URI).

URI auf Ihrem eigenen RealmErlaubtVerwendet von
Eigene Namen, z. B. com.myapp.statusveröffentlichen, abonnieren (auch per Präfix), aufrufen — nie registrierenpublish, subscribe, call
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table>veröffentlichen — nie abonnierenpublishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table
transformed.<table>, transformed.bulk.<table>nur abonnierensubscribeToTable / subscribe_to_table
databackend.errors.…, files.events…nur abonnierenDie Fehler- und Datei-Ereignisse des Daten-Backends
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>registrieren (einzeln), aufrufen, veröffentlichen — nie abonnierenregisterDeviceFunction / register_device_function, callDeviceFunction / call_device_function
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.…aufrufen (das SDK ruft sie auf)Methoden für Tabellen, Verlauf, geheime Spalten und Dateien
sys.appaccess.list, sys.appaccess.resolveaufrufenlistConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps
wamp.session.getaufrufenDer Heartbeat des JavaScript-SDK

Identitätsgeprüfte Namen. Namen, die mit einer Ziffer beginnen — die Schreib-Topics der Tabellen und die URIs der Gerätefunktionen —, durchlaufen zusätzlich die Identitätsprüfung der Plattform, die sie an den Swarm-Key Ihres Realms bindet. Eine Registrierung wird nur für eine Funktions-URI Ihres eigenen Geräts mit Swarm-Key, App-Key und Stage Ihres Realms akzeptiert; eine Präfix- oder Wildcard-Registrierung nur, wenn sie diese vier Segmente festlegt. Veröffentlichen und Aufrufen benötigen nur den Swarm-Key Ihres Realms: Eine Zeile, die an ein Tabellen-Topic mit einem anderen App-Key veröffentlicht wird, wird angenommen, erreicht aber kein Daten-Backend und geht verloren. Lassen Sie das SDK diese URIs bilden.

Reservierte Namen. Der Router lehnt sys. ab, abgesehen von den beiden app-übergreifenden Prozeduren oben, und wamp., abgesehen von wamp.session.get. Die übrigen Namen in der Tabelle gehören der Plattform: Beginnen Sie eigene Namen nicht mit einer Ziffer, bulk., transformed., databackend.errors., files., append., appendBulk., history. oder secret.. Jedes andere erste Segment ist der eigene Topic-Raum Ihrer App.

Niemand abonniert rohe Tabellen-Topics. Sie transportieren die Zeile genau so, wie sie gesendet wurde, einschließlich des Klartexts geheimer Spalten, die das Daten-Backend erst beim Speichern der Zeile verschlüsselt. subscribeToTable / subscribe_to_table liest stattdessen die gespeicherten Zeilen aus transformed.<table>, typisiert und mit maskierten geheimen Spalten.

Daten anderer Apps sind schreibgeschützt. Auf dem Realm eines Anbieters darf Ihre App (Router-Rolle app_reader) neben dem Heartbeat wamp.session.get nur transformed.… und files.events… abonnieren und history.transformed.… und files.read.… aufrufen — kein Veröffentlichen, kein Registrieren, keine Schreibvorgänge und keine Prozeduren für geheime Spalten.

Gerätemetadaten

setDeviceLocation / set_device_location

Bittet die Plattform, den GPS-Standort des Geräts zu aktualisieren, den IronFlock-Karten in Echtzeit anzeigen.

Derzeit nicht verfügbar: Noch beantwortet kein Plattformdienst diesen Aufruf auf dem Daten-Realm der App, daher schlägt er mit wamp.error.no_such_procedure fehl. Halten Sie Standorte bis dahin in einer eigenen Tabelle.

await ironflock.set_device_location(long=8.6821, lat=50.1109)
ParameterBereich
long-180 bis 180
lat-90 bis 90

Der Standortverlauf wird nicht gespeichert. Um den Standort über die Zeit zu verfolgen, erstellen Sie eine eigene Tabelle und verwenden Sie publish_to_table / publishToTable.

getRemoteAccessUrlForPort

Gibt die URL zurück, unter der ein in der port-template.yml der App deklarierter Port erreichbar ist, sobald sein Tunnel aktiv ist.

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>"
ParameterTypBeschreibung
portint / numberDer Port, wie in port-template.yml deklariert
protocolstr / string, optionalDas Protokoll des Ports: "http" (Standard), "https", "tcp" oder "udp", unabhängig von Groß- und Kleinschreibung; umgebende Leerzeichen werden ignoriert, und ein leeres gilt als "http"
remote_port_environment / remotePortEnvironmentstr / string, optionalNur tcp/udp: die Variable, die den entfernten Port enthält, anstelle von REMOTE_PORT_FOR_<port> — das eigene remote_port_environment des Ports aus dem Template, das Geräte-Agenten vor 0.19.8 benötigen

http- und https-Ports werden unter dem Tunnel-Label <device_key>-<app name, lower case>-<port> (https-Ports mit dem Plattform-Präfix secure-) auf der Tunnel-Domain ausgeliefert, die der Geräte-Agent injiziert (TUNNEL_DOMAIN), immer als https://-URL. Auf einem Gerät, das zu einer On-Premises-Appliance gehört (der Agent injiziert INSTANCE_KEY), wird stattdessen die über die Cloud weitergeleitete Route https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN> zurückgegeben.

Eine tcp- oder udp-URL enthält den öffentlichen Port, den der Tunnel zugewiesen hat und den der Geräte-Agent als REMOTE_PORT_FOR_<port> bekanntgibt (Agent 0.19.8 und neuer): <protocol>://<TUNNEL_DOMAIN>:<remote port>. Auf dem Gerät einer Appliance wird der aus dem Internet erreichbare Port REMOTE_PORT_FOR_<port>_CLOUD bevorzugt, solange der Agent ihn bekanntgibt; bevor er erscheint, und wieder, sobald der Agent ihn entfernt hat (Cloud-Weiterleitung ausgeschaltet), wird die Appliance-lokale URL zurückgegeben. Ein Port 0 ist noch nicht zugewiesen.

Die Werte werden live aus /data/env gelesen; rufen Sie die Methode daher erneut auf, statt das Ergebnis zwischenzuspeichern. Einige Fälle kann die URL nicht abdecken:

  • Der Geräte-Agent tunnelt nur die Ports von Produktionsinstallationen (PROD). In einem Entwicklungscontainer (DEV) ist die http(s)-URL die der PROD-Installation derselben App auf diesem Gerät, und tcp/udp liefern keine; erreichen Sie eine DEV-App stattdessen über die LAN-Adresse des Geräts (DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>).
  • Auf einem Edge-Gerät einer ohne Domain installierten Appliance (Modus „Einfaches HTTP“) setzt der Agent TUNNEL_DOMAIN auf localhost, sodass die dort gebildeten URLs auf localhost zeigen: Eine solche Appliance stellt App-Ports auf ihren Host-Ports bereit, was eine Tunnel-URL nicht ausdrücken kann.
  • Nicht gesetzte oder leere Tunnel-Domains fallen auf app.ironflock.com zurück. Die Plattform setzt CLOUD_TUNNEL_DOMAIN derzeit nicht: Die URLs einer Appliance verwenden app.ironflock.com, sofern die App diese Variable nicht selbst in ihren Umgebungseinstellungen setzt.
  • In einem Container, der unter einem anderen Benutzer als root läuft, sind die Werte die, mit denen der Container gestartet wurde (siehe Umgebungsvariablen).

Die Methode gibt None / null (in Go "" und false) zurück, wenn sich die URL nicht bilden lässt: Geräteschlüssel oder App-Name fehlen, für einen tcp/udp-Port ist noch kein entfernter Port bekanntgegeben (oder er ist 0), das Protokoll ist unbekannt, oder das Label ist kein gültiges DNS-Label (über 63 Zeichen oder ein App-Name mit Punkt). Im Browser gibt es keine Umgebung: Die http(s)-URL wird auf app.ironflock.com aus den Optionen deviceKey und appName gebildet.

Verbindungseigenschaften & Lebenszyklus

EigenschaftTypBeschreibung
is_connectedboolOb die Plattformverbindung aktiv ist
connectionCrossbarConnectionDie zugrunde liegende Verbindungsinstanz (für fortgeschrittene Nutzung)
MethodeBeschreibung
run()Startet die Verbindung und führt mainFunc aus (blockierend)
await start()Startet die Verbindung asynchron
await stop()Stoppt die Verbindung und die Verbindungen zu konsumierten Apps und bricht laufende Aufgaben ab. Die Verbindungen verlassen ihre Realms und verbinden sich danach nicht neu; start() verbindet von Neuem. Ein noch wartendes start() oder connect_to_app schlägt fehl (siehe Verbindungszuverlässigkeit)
await run_async()Startet die Verbindung und hält sie asynchron aufrecht

Verbindungszuverlässigkeit

Das SDK verbindet sich selbstständig neu. Bricht die Verbindung ab, wird so lange erneut versucht, bis der Router wieder erreichbar ist; danach werden alle Subscriptions und alle registrierten Gerätefunktionen wiederhergestellt — Sie müssen nach einer Neuverbindung nichts erneut abonnieren. Lässt sich eine davon nicht wiederherstellen, werden die übrigen dennoch wiederhergestellt, und die fehlgeschlagene wird erneut versucht, solange die Session besteht — nach 1 Sekunde, mit Verdopplung bis 30 Sekunden, jede Wartezeit um bis zu 15 % variiert — und erneut nach jeder Neuverbindung: Der Router hat sie zuvor akzeptiert, ihre Ablehnung ist also vorübergehend, etwa weil die Identitätsprüfung der Plattform für eine Gerätefunktion kurzzeitig im Fail-Closed-Modus ablehnt. Der erste Fehlschlag wird als Warnung protokolliert, und die Wiederherstellung wird protokolliert, sobald sie gelingt.

Tabellenoperationen — Schreibvorgänge, Lesevorgänge von Verlauf, Serien und geheimen Spalten sowie die Verlaufslesevorgänge einer konsumierten App — überbrücken außerdem einen Plattform-Neustart, etwa ein Router- oder Plattform-Update. Sie warten bis zu reconnect_window (Python, in Sekunden) / reconnectWindowMs (JavaScript) / WithReconnectWindow (Go, eine time.Duration), standardmäßig 60 Sekunden, auf die Rückkehr der Verbindung und versuchen es erneut, solange das Daten-Backend seine Prozeduren noch nicht bereitstellt: Der Router antwortet mit wamp.error.no_such_procedure, oder das Daten-Backend lehnt einen Aufruf, der seiner Registrierung zuvorgekommen ist, mit “client has no handler for registration” ab. Ein solcher Aufruf hat nie einen Handler erreicht, daher schreibt ein erneuter Versuch nie eine Zeile doppelt; jeder andere Fehler wird unverändert ausgelöst. Dateiaufrufe werden nicht wiederholt (siehe Fehler beim Dateispeicher). Eine Anfrage, die bereits gesendet ist, wenn ihre Session geschlossen wird (der Router schließt sie, die Verbindung bricht ab oder stop()), schlägt sofort fehl, statt auf eine Antwort zu warten, und wird nicht wiederholt: Sie hat ihren Handler möglicherweise erreicht. In Go schlägt eine solche Anfrage — und jede Operation, die innerhalb ihrer Wartezeit keine Verbindung findet — mit einem Fehler fehl, der wamp.ErrNotConnected umhüllt (wamp.ErrStopped nach Stop). Die übrigen Operationen in Go (Publish, Subscribe, Call, die Gerätefunktionen, die app-übergreifende Erkennung) warten bis zu 10 Sekunden auf eine Verbindung und werden nicht wiederholt.

Ein Sonderfall ist ein erreichbarer Router, der für die App noch kein Realm hat. Beim gleichzeitigen Installieren startet der App-Container, bevor das Daten-Backend sein Realm bereitgestellt hat, sodass die ersten Joins mit wamp.error.no_such_realm abgewiesen werden; das SDK versucht es alle paar Sekunden erneut, und die App ist da, sobald das Realm da ist. Ein Realm, das nach einer Minute noch immer fehlt, wird höchstwahrscheinlich nie erscheinen — die App wurde gelöscht oder hat für diese Stage kein Daten-Backend —, daher verlangsamt das SDK von da an auf einen Versuch alle zwei Minuten, statt den Router für immer jede Sekunde anzufragen. Der schnelle Takt kehrt zurück, sobald das Realm erscheint oder der Router selbst wegfällt und wiederkommt.

stop() beendet jedes Warten auf eine Verbindung. Ein start(), das noch auf seinen ersten Join wartet — etwa auf ein Realm, das es noch nicht gibt —, schlägt mit Connection to realm <realm> stopped before it opened fehl. Ein connect_to_app / connectToApp, das noch die Verbindung zu einer bereitstellenden App öffnet, schlägt mit Connection to app '<app>' (<stage>) aborted by stop() fehl; tritt die Verbindung trotzdem ihrem Realm bei, wird sie geschlossen, und der Aufruf schlägt mit Connection to app '<app>' (<stage>) closed by stop() fehl. Eine Verweigerung, die gleichzeitig mit stop() eintrifft, behält ihren CrossAppAccessError. Wenn stop() zurückkehrt, baut nichts mehr eine Verbindung auf; ein Router, der auf das Verlassen des Realms nicht antwortet, wird nach 5 Sekunden getrennt.

In Go beendet Stop(ctx) diese Wartevorgänge ebenfalls, mit Fehlern, die wamp.ErrStopped umhüllen: Ein noch wartendes Start gibt ihn zurück, und ein ConnectToApp, das noch eine Verbindung öffnet, schlägt mit ironflock: connection to app '<app>' (<stage>) aborted by Stop fehl (oder closed by Stop, wenn die Verbindung ihrem Realm trotzdem beigetreten ist). Stop wartet innerhalb von ctx auf das Verlassen des Realms und schließt dann den Socket; Run gibt ihm 10 Sekunden.

Schwieriger ist eine Verbindung, die stillschweigend stirbt: Ein NAT oder Proxy kappt sie im Leerlauf, ohne eine der beiden Seiten zu informieren, oder ein Router startet hinter einem Load Balancer neu. Es trifft nichts ein, was den Verlust signalisiert. Eine App, die nur veröffentlicht, merkt es beim nächsten fehlschlagenden Schreibvorgang — eine App, die nur abonniert, schreibt jedoch nie und merkt es daher gar nicht: Sie bleibt tot, bis der Container neu startet.

Alle drei SDKs erkennen dies. Die Erkennung ist standardmäßig aktiv und benötigt keine Konfiguration.

Das Python-SDK nutzt WebSocket-Keepalive: Es sendet alle 30 Sekunden einen Ping an den Router, solange die Verbindung im Leerlauf ist, und erwartet innerhalb von 10 Sekunden einen Pong. Bleibt dieser aus, wird die Verbindung verworfen und neu aufgebaut. Eingehender Datenverkehr gilt als Lebenszeichen, sodass eine ausgelastete Verbindung nie geprüft wird — der Ping geht nur raus, wenn die Verbindung tatsächlich still geworden ist.

Mit connectToApp / connect_to_app geöffnete app-übergreifende Verbindungen sind typischerweise reine Subscribe-Verbindungen — genau der Fall, den dies schützt. Sie sind automatisch abgedeckt und verbinden sich auch dann neu, wenn der Router sie ablehnt, bis die Ablehnung anhält (siehe connectToApp).

Fehlerbehandlung

Jede SDK-Methode schlägt lautstark fehl: Bei ungültigen Argumenten, einer abgerissenen Verbindung oder einer Ablehnung durch die Plattform löst sie eine Exception aus (Python), wird das Promise abgelehnt (JavaScript) bzw. gibt sie einen Fehler zurück (Go) — mit einer Meldung, die die Operation, das Topic und den Grund nennt. Nichts wird stillschweigend verschluckt; verpacken Sie Aufrufe, die überleben sollen, daher in einen try-Block — in Go prüfen Sie den zurückgegebenen Fehler.

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}")

In JavaScript sind Fehler, die von der Plattform stammen, WampError-Instanzen — eine gewöhnliche Unterklasse von Error, die zusätzlich die WAMP-Fehler-URI in error und die Fehlerdaten in args / kwargs transportiert. Alles andere (fehlerhafte Parameter, keine Verbindung) ist ein einfacher Error.

In Go gibt eine fehlgeschlagene Operation einen *ironflock.OperationError zurück, dessen Meldung die Operation und die Ursache nennt, die er umhüllt; errors.Is und errors.As sehen durch ihn hindurch. Eine Ablehnung durch den Router oder einen Aufgerufenen ist ein *ironflock.WampError (*wamp.Error) mit URI, Args und Kwargs; ironflock.WampURI(err) liefert die URI oder "". Weitere Sentinel-Fehler sind ironflock.ErrMissingConfig (eine Identitätsangabe, die die Operation benötigt, fehlt), ironflock.ErrAlreadyStarted und wamp.ErrStopped (nach Stop).

Migration: Ältere Versionen des Python- und des JavaScript-SDK protokollierten eine Meldung und gaben None / null zurück, wenn ein Aufruf fehlschlug. Sie lösen jetzt stattdessen einen Fehler aus, sodass Code der Form if result is None: Fehlschläge nicht mehr erkennt — verwenden Sie try / except (bzw. try / catch).

Ablehnungen durch das Daten-Backend

Eine Operation, die das Daten-Backend ablehnt, schlägt mit einer der folgenden WAMP-Fehler-URIs fehl, mit dem Grund als erstem Argument. Alle drei SDKs exportieren sie als Konstanten; Go benennt sie im Go-Stil (ironflock.URIRateLimited, ironflock.URIStorageFull, …). In Python lösen die Methoden von IronFlock einen RuntimeError aus, dessen Ursache der ApplicationError von autobahn ist (Lesevorgänge einer konsumierten App lösen den ApplicationError selbst aus), und wamp_error_uri(err) liest die URI aus beiden. In JavaScript ist der Fehler ein WampError, dessen error die URI ist. In Go ist es ein *ironflock.OperationError um einen *ironflock.WampError, und ironflock.WampURI(err) liest die URI. Die SDKs wiederholen keine dieser Ablehnungen. Eine veröffentlichte Zeile, die das Daten-Backend ablehnt, wird ohne Fehler verworfen — nur die Append-Methoden melden sie.

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
KonstanteAbgelehnt wird
URI_RATE_LIMITEDrevealSecrets / reveal_secrets über 30, verifySecret / verify_secret über 120 Aufrufe pro Minute und App-Zugangsdaten auf einem Gerät
URI_RESULT_TOO_LARGEEin Lesevorgang mit einer einzelnen Zeile über dem Nachrichtenbudget von 8 MiB oder einem Ergebnis über der 1-GiB-Schutzgrenze (andere große Ergebnisse kommen in Chunks an, die das SDK wieder zusammensetzt). Seine Keyword-Argumente nennen die größten Spalten: Senken Sie limit, verengen Sie timeRange, wählen Sie weniger columns aus oder beschneiden Sie mit columnPaths
URI_INVALID_LIMITEin Lesevorgang eines Transforms mit einem limit über 3000
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPSSerienabfragen, die die Tabelle nicht beantworten kann: kein Start oder ein Ende, das nicht danach liegt; eine Metrik, die die Tabelle nicht aggregieren kann (keine solche Spalte, eine geheime Spalte, eine Methode, die der Typ der Spalte nicht zulässt, tsp mit einer anderen Methode als COUNT); eine groupBy-Spalte, nach der sie nicht gruppieren kann; mehr als 50.000 Zeilen
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMNEin Filter auf eine geheime Spalte; verifySecret / verify_secret auf eine Spalte, die nicht geheim ist, oder revealSecrets / reveal_secrets einer Tabelle ohne geheime Spalten
URI_STORAGE_FULLEin Append, während die Festplatte der Appliance fast voll ist (unter etwa 3 GiB frei; ab 4 GiB wieder angenommen) — vorübergehend: Hängen Sie die Zeilen später an
URI_STORAGE_OVERUSAGEEin Append über das Speicherkontingent des Kontos hinaus; ein baldiger neuer Versuch hilft nicht
URI_ENTITY_KEY_CONFLICTEine Zeile, deren Entitätsschlüssel (maintainLatestFlagFor) und tsp bereits eine andere Zeile hat, im Stapel oder gespeichert: Geben Sie jeder Zeile ihren eigenen tsp
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTEDEin Schreibvorgang, der den bisherigen Wert eines Geheimnisses beibehält (__secret__), ohne einen Entitätsschlüssel, über den er sich finden lässt; ein Schreibvorgang mit einem bereits verschlüsselten Wert

Ein Append, das das Daten-Backend ohne Begründung ablehnt — wamp.error.runtime_error, seine Antwort auf eine Zeile ohne tsp —, ist keine typisierte Ablehnung: Die Fehlermeldung des SDK verweist auf den fehlenden tsp und auf die error-logs-Tabelle der App, in der das Daten-Backend die Ursache protokolliert.

Browser-Nutzung (nur JavaScript)

Das JavaScript SDK funktioniert in modernen Browsern. Da Browser keine Umgebungsvariablen haben, übergeben Sie alle Konfigurationsoptionen über den Konstruktor:

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 }]);

Verwenden Sie IronFlock.fromServer(), um die Konfiguration von Ihrem Backend abzurufen, anstatt Zugangsdaten fest einzuprogrammieren:

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

Ihr Backend-Endpunkt sollte ein JSON-Objekt mit den Verbindungsoptionen (serialNumber, deviceKey, appName, swarmKey, appKey, env) zurückgeben.

AI-Agent-Funktionen registrieren

Das SDK kann Funktionen registrieren, die von AI-Agents aufgerufen werden können. Registrieren Sie eine Prozedur und referenzieren Sie deren Topic in Ihrer 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)

Der AI-Agent kann diese Funktion dann aufrufen, wenn ein Benutzer eine Frage stellt, die aktuelle Sensordaten erfordert.

Um das registrierte WAMP-Topic mit einem AI-Agenten zu verbinden, referenzieren Sie es in der .ironflock/ai-template.yml Ihrer App:

sensor_agent: tool_description: | Delegiere an diesen Agenten, wenn der Benutzer nach Sensormesswerten, Live-Gerätedaten oder aktuellen Umgebungsbedingungen fragt. system_prompt: | Du bist ein Sensordaten-Spezialist. Verwende get_current, um den aktuellen Messwert eines beliebigen Sensors abzurufen. Gib in deiner Antwort immer die Einheit an. main: true max_context_tokens: 30000 max_iterations: 5 tools: get_current: description: Gibt den aktuellen Messwert eines Sensors zurück. topic: sensors.get_latest parameters: sensor_id: type: string description: Der abzufragende Sensor-Bezeichner. required: true

Der Wert von topic (sensors.get_latest) muss mit dem Namen übereinstimmen, der bei register_device_function / registerDeviceFunction / RegisterDeviceFunction im Edge-Code angegeben wurde. IronFlock leitet den Aufruf automatisch an das Gerät weiter, auf dem die Funktion registriert ist.

Die vollständige Referenz für ai-template.yml finden Sie unter Agents & Tools definieren.

Umgebungsvariablen

Diese Variablen werden von der IronFlock-Laufzeitumgebung automatisch innerhalb von App-Containern gesetzt:

VariableBeschreibung
DEVICE_NAMEAnzeigename des Geräts
DEVICE_SERIAL_NUMBEREindeutiger, unveränderlicher Gerätebezeichner
DEVICE_KEYGeräteschlüssel für die Authentifizierung
SWARM_KEYProjektbezeichner
APP_KEYApp-Bezeichner
APP_NAMEApp-Name
ENVUmgebung: DEV oder PROD
APP_AUTH_ID, APP_AUTH_SECRETDie eigenen Zugangsdaten der App, die das SDK dem Router vorlegt. Der app-übergreifende Zugriff setzt sie voraus; ohne sie (bei einem älteren Geräte-Agenten) greift das SDK auf die Seriennummer des Geräts zurück
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")

Die Variablen für den Fernzugriff (TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …) sind unter Ports konfigurieren aufgeführt.

Der Geräte-Agent spiegelt diese Werte außerdem nach /data/env, je eine Datei <NAME>.txt, und hält die Spiegelung aktuell, solange die App läuft; die SDKs lesen sie zuerst, sodass Werte, die sich zur Laufzeit ändern, etwa ein Fernzugriffsport, eine laufende App erreichen. Die Zugangsdaten pro App werden bei jedem Verbindungsversuch neu gelesen, sodass Zugangsdaten, die der Agent rotiert, während die App läuft, bei der nächsten Neuverbindung vorgelegt werden. Der Agent macht /data/env nur für root lesbar: In einem Container, der unter einem anderen Benutzer läuft, sieht das SDK die Werte, mit denen der Container gestartet wurde, und protokolliert einmalig eine Warnung.

Last updated on