Skip to Content

SDK IronFlock

SDK IronFlock pozwala aplikacjom brzegowym wchodzić w interakcję z platformą IronFlock. Jest dostępne w trzech językach — Python, JavaScript i Go — z tą samą funkcjonalnością, opisaną poniżej obok siebie. Automatycznie obsługuje uwierzytelnianie podczas działania na zarejestrowanym urządzeniu i zapewnia funkcje do publikowania danych, zapytywania historii, wywoływania zdalnych procedur na urządzeniach oraz aktualizowania metadanych urządzenia.

SDKPakietWymaga
Pythonironflock na PyPIPython 3.8+
JavaScriptironflock na npmNode.js 18+ lub nowoczesna przeglądarka
Gogithub.com/RecordEvolution/ironflock-goGo 1.27.2+

Instalacja

pip install ironflock

Lub dodaj ironflock do requirements.txt swojej aplikacji.

Szybki start

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

Podczas używania wewnątrz kontenera aplikacji IronFlock, SDK automatycznie odczytuje dane uwierzytelniające połączenia ze środowiska — nie jest wymagana ręczna konfiguracja. Każdy wiersz zapisywany do tabeli zawiera tsp, czyli swój znacznik czasu (zobacz publishToTable).

Opcje konstruktora

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) )
ParametrOpis
mainFuncFunkcja asynchroniczna, która uruchamia się po nawiązaniu połączenia
serial_numberNadpisuje numer seryjny urządzenia wysyłany wraz z publikacjami i zapisami do tabel. Domyślnie zmienna środowiskowa DEVICE_SERIAL_NUMBER. Nie zmienia tożsamości, z jaką uwierzytelnia się połączenie, dopóki agent urządzenia wstrzykuje poświadczenie per aplikacja
reconnect_windowLiczba sekund, przez które operacja na tabeli czeka na powrót platformy po restarcie, zanim zgłosi wyjątek. Domyślnie 60; 0 wyłącza oczekiwanie (zobacz Niezawodność połączenia)

Publikowanie danych

publishToTable / publish_to_table

Publikuje rekord danych do tabeli floty. Nazwa tabeli musi pasować do tabeli zdefiniowanej w data-template.yml aplikacji. SDK automatycznie kieruje dane do właściwej bazy danych projektu.

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

Zaplecze danych odczytuje wiersz z pierwszego argumentu: ze słownika (dict) w Pythonie, z pierwszego elementu tablicy w JavaScript, z pierwszego argumentu pozycyjnego w Go (mapy lub struktury). Dodatkowe argumenty nazwane — w Go wartość ironflock.Kwargs wśród argumentów — są odczytywane tylko dla kolumn, które szablon danych na nie mapuje (kwargs.<name>).

Każdy wiersz potrzebuje tsp. Każda tabela ma obowiązkową kolumnę tsp — znacznik czasu wiersza, np. łańcuch ISO 8601, a w Go time.Time, wysyłany jako RFC 3339 w UTC — którą zaplecze danych odczytuje z wiersza, chyba że szablon danych mapuje ją gdzie indziej. Opublikowany wiersz bez niej zaplecze danych porzuca, a jedynym śladem tego jest wpis w tabeli error-logs aplikacji — potwierdzona publikacja wygląda na udaną. Wiersz odrzucony z powodu braku miejsca na dane przepada nawet bez tego śladu. Użyj appendToTable / append_to_table, gdy musisz wiedzieć, czy wiersz został zapisany.

Częściowe wiersze w tabelach encji. W tabeli z kluczem encji (maintainLatestFlagFor) wiersz może zawierać tylko klucz encji, tsp i kolumny, które się zmieniają. Kolumna pominięta w wierszu zachowuje poprzednią wartość encji; kolumna wysłana jako None / null / nil zostaje wyczyszczona. To, czy pole liczy się jako pominięte, zależy od sposobu budowania wiersza:

  • Python: klucz o wartości None jest wysyłany jako null i czyści kolumnę. Usuń klucz ze słownika, aby zachować starą wartość.
  • JavaScript: właściwość o wartości undefined wypada z ładunku, więc zachowuje starą wartość. Ustaw ją na null, aby wyczyścić kolumnę.
  • Go: pole struktury z tagiem omitempty lub omitzero jest pomijane, gdy jest puste, więc zachowuje starą wartość. W przeciwnym razie pole wskaźnikowe nil, podobnie jak wartość nil w ironflock.Row, jest wysyłane jako null i czyści kolumnę.

Dotyczy to wszystkich czterech metod zapisu. W tabeli bez klucza encji kolumna pominięta w wierszu jest zapisywana jako NULL.

appendToTable / append_to_table

Dołącza dane do tabeli floty za pomocą zdalnego wywołania procedury zamiast pub/sub. Użyj tego, gdy potrzebujesz potwierdzenia, że dane zostały zapisane: odrzucony wiersz jest błędem.

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

Wiersz potrzebuje tsp, tak jak w publishToTable. Zaplecze danych odrzuca wiersz bez niego z błędem wamp.error.runtime_error, nie podając przyczyny; błąd SDK dodaje wtedy “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table”. Odmowy z określonym typem, takie jak pełny dysk na Appliance, są wymienione w sekcji Odmowy zaplecza danych.

publishRowsToTable / publish_rows_to_table

Publikuje wiele wierszy w jednej wiadomości (wstawianie zbiorcze) do tabeli floty. Platforma wstawia całą partię atomowo (wszystko albo nic) w jednej operacji. Użyj tego dla danych o wysokiej częstotliwości, gdzie jedno połączenie na wiersz byłoby zbyt kosztowne. Podobnie jak publishToTable, działa to w trybie „wyślij i zapomnij” — potwierdzenie potwierdza dostarczenie do routera, a nie wstawienie do bazy danych.

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

Drugim argumentem jest niepusta lista obiektów wierszy do wstawienia, każdy z własnym tsp — w Go wycinek (slice) wierszy ([]ironflock.Row, []map[string]any lub wycinek struktur). Partia, którą zaplecze danych odrzuci — na przykład dlatego, że któryś wiersz nie ma tsp — przepada bez słowa dla publikującego, tak jak w publishToTable.

appendRowsToTable / append_rows_to_table

Dołącza wiele wierszy w jednym zdalnym wywołaniu procedury (wstawianie zbiorcze) do tabeli floty. Platforma wstawia całą partię atomowo (wszystko albo nic): jeśli którykolwiek wiersz jest nieprawidłowy, cała partia zostaje odrzucona i nic nie jest zapisywane. Preferuj to zamiast publishRowsToTable / publish_rows_to_table, gdy potrzebujesz wyniku wstawienia. Każdy wiersz potrzebuje własnego tsp; odmowy są takie same jak w appendToTable, łącznie ze wskazówką dla partii odrzuconej bez podania przyczyny.

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

Zgłasza błąd aplikacji do tabeli error-logs twojej floty. Jest to wygodne opakowanie wokół publishToTable / appendToTable: oznacza wiersz wartością source: "app", poziomem ważności level oraz znacznikiem czasu, a następnie zapisuje go jak każdy normalny wiersz tabeli. Błąd trafia do tej samej tabeli error-logs, której używają błędy systemowe fleetdb (oznaczone source: "system"), więc można go odpytywać za pomocą getHistory, strumieniować za pomocą subscribeToTable / subscribe_to_table, używać w szablonach tablic oraz dostarczać w czasie rzeczywistym na transformed.error-logs — bez wyzwalania systemowego powiadomienia o błędzie platformy.

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

Wiersz ma postać { tsp, msg, user_message, source: "app", level }: msg zawiera tekst techniczny (traceback lub stos wywołań), a user_message — tekst, który wyświetla powiadomienie o błędzie (toast) na boardzie: przekazany przez ciebie komunikat dla użytkownika, a bez niego sam komunikat.

Parametry:

ParametrTypOpis
errorstr / string lub wyjątek / ErrorKomunikat błędu lub wyjątek, którego ślad stosu (traceback/stack) lub komunikat zostaje zapisany
levelstr / string, opcjonalnyPoziom ważności: "error", "warn", "info" lub "debug". Domyślnie "error"
appendbool / boolean, opcjonalnyGdy true, używa zdalnego wywołania append (zwraca wynik wstawienia). Domyślnie false (publikacja typu fire-and-forget)
tspstr / string, opcjonalnyNadpisanie znacznika czasu w formacie ISO-8601. Domyślnie bieżący czas
user_message / userMessagestr / string, opcjonalnyTekst przeznaczony dla operatora, zapisywany jako user_message. Domyślnie zapisywany komunikat

W Pythonie opcje są argumentami nazwanymi (report_error(error, level=..., append=..., tsp=..., user_message=...)); w JavaScript są przekazywane przez obiekt opcji (reportError(error, { level, append, tsp, userMessage })); w Go przez strukturę (ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), gdzie errOrMsg to error lub łańcuch, Level to jedna z wartości ironflock.LevelError, LevelWarn, LevelInfo, LevelDebug, a zwracany *Result ma wartość nil, chyba że ustawiono Append.

publish

Publikuje wiadomość do własnego tematu (topic) WAMP. Użyj tego do niestandardowych wiadomości lub zdarzeń, które nie odpowiadają tabeli bazy danych. Router rezerwuje niektóre nazwy dla platformy — zobacz Identyfikatory URI dozwolone dla aplikacji.

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

Zapytywanie historii

getHistory

Pobiera historyczne dane z tabeli floty lub transformacji: najnowsze pasujące wiersze, w kolejności rosnącej według tsp. Obsługuje filtrowanie, zakresy czasu oraz stronicowanie.

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

Parametry zapytania:

PoleTypOpis
limitint / numberMaksymalna liczba zwracanych wierszy (1–10 000, wymagane; 1–3 000 dla transformacji)
offsetint / numberPrzesunięcie dla stronicowania
timeRangelist / array[start, end] — łańcuchy daty i czasu ISO lub liczby milisekund epoki, nigdy pomieszane; None / null oznacza otwarty koniec. Starszy obiekt {"start": ..., "end": ...} jest konwertowany na tę parę
filterAndlist / arrayWpisy filtra łączone przez AND: warunki, grupy i/lub znacznik latest (patrz niżej)
columnslist / arrayKolumny do zwrócenia (opcjonalne). tsp, device_key oraz authid są zawsze dołączane; pomiń, aby otrzymać wszystkie kolumny
columnPathslist / arrayPrzycina kolumny json (opcjonalne): ścieżki JSON w notacji używanej przez filtry ("json_data.a.b", "json_data['key']"). Kolumna json wymieniona w columns wraca, zawierając tylko sumę swoich ścieżek; kolumna bez ścieżki tutaj wraca w całości. Ignorowane przez transformacje oraz przez zaplecza danych starsze niż fleetdb v1.0.52

W Go są to pola Limit, Offset, TimeRange, FilterAnd, Columns i ColumnPaths struktury ironflock.TableQueryParams. TimeRange to *ironflock.TimeRange{Start, End}: każda granica to time.Time, łańcuch ISO 8601, liczba milisekund epoki albo nil dla otwartej granicy (ironflock.Between, Since i Until budują zakres z wartości time.Time). FilterAnd to []ironflock.Filter. Starsza forma obiektowa nie istnieje.

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

Każdy warunek filtra to obiekt z kluczami column, operator i value — w Go budowany za pomocą ironflock.Where(column, operator, value), ironflock.IsNull(column) i ironflock.IsNotNull(column). Operatory są rozpoznawane bez względu na wielkość liter, z pominięciem otaczających spacji, i wysyłane w postaci podanej powyżej — jedynej, którą akceptuje zaplecze danych — więc "like" wychodzi jako "LIKE", także wewnątrz grup. Operator spoza listy jest wysyłany w podanej postaci, z ostrzeżeniem. IS NULL i IS NOT NULL nie przyjmują value i są jedynym sposobem pytania o NULL: każdy inny operator wyklucza wiersz, w którym kolumna ma wartość NULL, dokładnie jak w SQL. IN i NOT IN przyjmują listę (w Go wycinek). LIKE rozróżnia wielkość liter; aby dopasowywać bez jej rozróżniania, użyj ILIKE.

Łączenie przez OR. Wpisy filterAnd są łączone przez AND. Wpis grupy łączy własne wpisy — warunki, znacznik latest lub kolejne grupy — jednym operatorem, "AND" albo "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}, ], } ]

Zaplecze danych starsze niż grupy (fleetdb przed v1.2.0) odrzuca taki kształt, zamiast stosować jego część, więc kończy się głośnym błędem, a nie po cichu zwraca wiersze, które filtr wyklucza.

Odczytywanie aktualnych wartości. Wpis {"latest": true} w filterAnd (ironflock.Latest() w Go) nie jest warunkiem filtra, lecz przełącznikiem trybu: zaplecze danych zwraca tylko najnowszy wiersz dla każdej encji, wyznaczony w SQL na podstawie klucza encji, który tabela deklaruje za pomocą maintainLatestFlagFor. Tabela bez klucza encji zwraca swój pojedynczy, najnowszy wiersz.

Pozostałe warunki łączą się ze znacznikiem zgodnie z oczekiwaniami: warunki na kolumnach klucza encji zawężają to, które encje są zwracane, natomiast wszystkie inne warunki oraz timeRange są stosowane do wynikowych najnowszych wierszy. Dlatego połączenie {"latest": true} z filtrem na kolumnie deleted ukrywa usunięte encje, zamiast przywracać ich poprzedni wiersz.

Wcześniejsze wersje IronFlock przechowywały fizyczną kolumnę latest_flag. Kolumna ta już nie istnieje — starszy filtr latest_flag = true jest nadal akceptowany i traktowany jak ten znacznik, ale nowy kod powinien używać {"latest": true}. Znacznik latest nie jest dostępny w getSeriesHistory.

Zakresy czasu. Zaplecze danych odczytuje granicę podaną jako łańcuch za pomocą Date z JavaScriptu, więc zapisuj datę lub datę z czasem w formacie ISO 8601 (2026-07-01, 2026-07-01T12:30:15.250Z) i oddzielaj datę od czasu literą T. SDK odrzucają spację między datą a czasem w latach 0–99, które Date odczytuje błędnie: "0001-01-01 00:00:00" — to, co zwraca str(datetime.min) w Pythonie — zostałoby odczytane jako rok 2001. Czas bez strefy jest odczytywany jako UTC. Granica liczbowa to milisekundy epoki i musi być skończona: NaN i ±Infinity dotarłyby do zaplecza danych jako otwarta granica i po cichu usunęłyby filtr czasu, dlatego SDK odrzucają je przed wysłaniem. W Go granica time.Time jest wysyłana jako RFC 3339 w UTC, a granica w postaci łańcucha musi mieć jedną z form ISO 8601, które Date odczytuje poprawnie — datę albo datę z godziną z opcjonalnym Z lub przesunięciem względem UTC (+02:00 lub +0200); inne łańcuchy, takie jak format podstawowy 20260701T123000Z, przesunięcie podane tylko w godzinach czy dzień, którego miesiąc nie ma (2026-02-30), kończą się błędem ironflock.ErrInvalidArgument, zanim cokolwiek zostanie wysłane.

Transformacje. Transformację (widok SQL szablonu danych, zobacz Tabele transformacyjne) odczytuje się inaczej niż tabelę. Zaplecze danych stosuje wyłącznie limit — co najwyżej 3000; większy jest odrzucany z URI_INVALID_LIMIT — offset i filterAnd. Ignoruje timeRange, columns, columnPaths oraz znacznik latest, a filtr na kolumnie, której transformacja nie deklaruje (transformacja nie ma niejawnych tsp, device_key ani authid), pomija, co poszerza wynik. Jej wiersze przychodzą we własnej kolejności widoku, odwróconej: najnowsze wiersze, od najstarszego, dostajesz tylko wtedy, gdy widok sortuje od najnowszych. Zakres czasu transformacji ograniczaj w jej SQL.

Duże wyniki. Wynik zbyt duży na jedną wiadomość zaplecza danych (8 MiB JSON) przychodzi w częściach, które SDK składa w pamięci: otrzymujesz wiersze pojedynczego odczytu, w tej samej kolejności. Części, które się nie sumują — brakująca lub zduplikowana albo liczba wierszy niezgodna z podsumowaniem zaplecza danych — powodują niepowodzenie odczytu zamiast zwrócenia części wierszy. Pojedynczy wiersz powyżej 8 MiB lub wynik powyżej zabezpieczenia 1 GiB zaplecza danych jest odrzucany z URI_RESULT_TOO_LARGE: zmniejsz limit, zawęź timeRange, wybierz mniej kolumn w columns albo przytnij je za pomocą columnPaths.

getSeriesHistory / get_series_history

Pobiera zredukowane (down-sampled) dane szeregów czasowych z tabeli floty: kolumny agregowane w przedziałach czasu (np. średnie godzinowe). Idealne do wykresów obejmujących długie zakresy czasu. Dostępne dla tabel (nie dla transformacji), na zapleczach danych od 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"

Parametry zapytania:

PoleTypOpis
metricslist / arrayAgregaty obliczane dla każdego przedziału, co najmniej jeden: {"ref": ..., "method": ...}. ref to kolumna albo ścieżka JSON wewnątrz kolumny json ("json_data.temp"); method to "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" lub "LAST". Każda metryka wraca pod nazwą "<METHOD>:<ref>", która musi zmieścić się w 63 bajtach
bucketMsint / numberSzerokość przedziału w pełnych milisekundach, co najmniej 1000 (opcjonalne). Pomiń, aby podzielić zakres czasu na limit przedziałów
limitint / numberBudżet przedziałów (1–10 000) — nie limit wierszy
timeRangelist / array[start, end] — łańcuchy daty i czasu ISO lub liczby epoch-ms. Wymagane, z początkiem; None / null jako koniec oznacza „teraz”. Koniec musi wypadać po początku
groupBylist / arrayKolumny, które dzielą każdy przedział na wiersz na grupę (opcjonalne)
filterAndlist / arrayWarunki i grupy łączone przez AND, jak w getHistory (opcjonalne). Znacznik latest jest tutaj odrzucany, także wewnątrz grupy; aby odczytać aktualne wartości, użyj getHistory

Wiersze. Każdy wiersz to jeden przedział — jednej grupy, gdy użyto groupBy. tsp to początek przedziału w milisekundach epoki, każda kolumna groupBy wraca pod własną nazwą, a każda metryka pod "<METHOD>:<ref>" (SeriesMetric.column w Pythonie, seriesColumn(metric) w JavaScript, SeriesMetric.Column() w Go), np. row["AVG:temperature"]. Wiersze są posortowane rosnąco według tsp; przedziału bez wierszy nie ma w wyniku (bez wypełniania luk). AVG i SUM przyjmują kolumny liczbowe, MIN i MAX także łańcuchy i znaczniki czasu; FIRST i LAST dają wartość najwcześniejszego i najpóźniejszego wiersza przedziału; COUNT liczy wartości różne od null (dla tsp: wiersze). Duży wynik jest składany z części, tak jak w getHistory.

Przedziały i otwarty koniec. limit liczy przedziały, nie wiersze. Bez bucketMs zakres czasu jest dzielony na limit przedziałów. Z nim zaplecze danych poszerza przedziały do wielokrotności bucketMs, gdy zakres mieści ich więcej niż limit — a otwarty koniec sięga aż do własnego „teraz” zaplecza danych, nieco późniejszego niż twoje. Ostatnie 24 godziny z otwartym końcem obejmują 25 przedziałów godzinnych, więc limit: 24 daje przedziały dwugodzinne. Ogranicz koniec, jak w przykładzie, albo dopuść jeden przedział więcej: przy bucketMs i otwartym końcu może wrócić limit + 1 przedziałów.

SDK sprawdzają przed wysłaniem to, co zaplecze danych by odrzuciło: co najmniej jedną metrykę, niepuste ref, znane metody, nazwy kolumn mieszczące się w 63 bajtach, początek, koniec po początku oraz bucketMs jako liczbę całkowitą wynoszącą co najmniej 1000 (w Go Bucket wynoszący co najmniej 1 sekundę, w pełnych milisekundach). Zaplecze danych odrzuca to, na co nie potrafi odpowiedzieć, z URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY lub URI_SERIES_TOO_MANY_GROUPS (ponad 50 000 wierszy) — zobacz Odmowy zaplecza danych.

Przestarzała forma. Wydania SDK do 1.9.0 włącznie wysyłały metrics jako nazwy kolumn plus method na najwyższym poziomie ({"metrics": ["temperature"], "method": "AVG", ...}), co zaplecza danych odrzucają od fleetdb v1.0.58. Ta forma nadal działa: SDK wysyła jedną metrykę {ref, method} na kolumnę, ostrzega (DeprecationWarning w Pythonie, ostrzeżenie w konsoli raz na proces w JavaScript) i zmienia nazwy kolumn wyniku z powrotem na nazwy kolumn, więc stary kod nadal odczytuje row["temperature"] — chyba że wiersz ma już kolumnę o tej nazwie, jak tsp dla COUNT z tsp. Przejdź na metryki {ref, method} i odczytuj row["AVG:temperature"]. SDK dla Go ma tylko aktualną formę: metodę dla każdej metryki (SeriesMetric).

Subskrybowanie danych

subscribeToTable / subscribe_to_table

Subskrybuje aktualizacje tabeli floty w czasie rzeczywistym. Handler jest wywoływany za każdym razem, gdy zaplecze danych zapisze nowy wiersz, a jako pierwszy argument otrzymuje ten wiersz w zapisanej postaci — z wartościami o typach kolumn z szablonu danych i z zamaskowanymi tajnymi kolumnami. Wiersze zapisane przez ścieżkę wstawiania zbiorczego (publishRowsToTable / appendRowsToTable) są dostarczane do twojego handlera pojedynczo, więc kod handlera pozostaje taki sam niezależnie od tego, jak dane zostały zapisane.

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)

Transformacja nie ma własnych wierszy: przy każdym takcie swojego harmonogramu zaplecze danych publikuje cały widok — co najwyżej 3000 wierszy, w odwróconej kolejności widoku — jako jedno zdarzenie, którego pierwszym argumentem jest lista wierszy. event_rows(args[0]) w Pythonie, eventRows(args) w JavaScript i ev.Rows() w Go odczytują oba kształty: wiersz tabeli daje [row], lista transformacji — tę listę ([] dla pustego widoku), a wszystko inne — None / undefined / nil. W Go ev.Row() ma wartość nil dla zdarzenia transformacji.

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

Subskrybuje własny temat (topic) WAMP do niestandardowej komunikacji w czasie rzeczywistym. Do wierszy tabel używaj subscribeToTable / subscribe_to_table: router odrzuca subskrypcje surowych tematów zapisu tabel (zobacz Identyfikatory URI dozwolone dla aplikacji).

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

Tajne kolumny

Kolumna zadeklarowana jako secret: true w szablonie danych aplikacji jest szyfrowana w spoczynku. Żadna zwykła ścieżka odczytu nie zwraca wartości jawnej, nawet aplikacji, która ją zapisała:

Ścieżka odczytuJak wygląda tajna kolumna
getHistory, subscribeToTable / subscribe_to_table i wszystko inne przechodzące przez router komunikatówsymbol zastępczy __secret__, eksportowany jako SECRET_PLACEHOLDER (ironflock.SecretPlaceholder w Go)
Dostęp SQL (login Postgres w FleetDB Access)przechowywany szyfrogram, ifsec:1:…
revealSecrets / reveal_secretsodszyfrowana wartość jawna

NULL jest przekazywany jako NULL na każdej ścieżce, więc „ustawione, ale ukryte” nadal daje się odróżnić od „nigdy nie zapisane”.

W tabeli encji wzorzec odczyt–modyfikacja–zapis jest bezpieczny: ponowne opublikowanie wiersza, który wciąż zawiera symbol zastępczy __secret__ — lub całkowite pominięcie tajnej kolumny — zachowuje przechowywany sekret zamiast go nadpisywać, a jawny null go czyści. Dokładne reguły opisano w sekcji Tajne kolumny w przewodniku po zapleczu danych.

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

Obie poniższe funkcje można wywołać wyłącznie z kontenerów samej aplikacji. Router komunikatów odmawia do nich dostępu każdej roli przeglądarkowej oraz aplikacjom konsumującym — i właśnie dlatego uchwyt ConsumedApp nie ma ich odpowiednika: tajne kolumny innej aplikacji przychodzą jako __secret__ i takie pozostają.

revealSecrets / reveal_secrets

Odczytuje wiersze jednej z twoich własnych tabel z odszyfrowanymi tajnymi kolumnami.

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

Parametry zapytania są takie same jak w getHistory, z jedną różnicą: limit nie może przekroczyć 100. Ten pułap istnieje po to, aby jedno wywołanie nie odszyfrowało całej tabeli, a zaplecze danych odrzuca większą wartość, zamiast po cichu ją przycinać.

Zaplecze danych obsługuje co najwyżej 30 ujawnień na minutę na poświadczenie aplikacji na urządzeniu; powyżej tej liczby wywołanie kończy się błędem URI_RATE_LIMITED. Ujawnij sekret raz i go zachowaj, zamiast robić to przy każdym użyciu. Tabela bez tajnych kolumn jest odrzucana z URI_NOT_A_SECRET_COLUMN, a filtr na tajnej kolumnie — z URI_SECRET_COLUMN.

verifySecret / verify_secret

Sprawdza kandydującą wartość względem tajnej kolumny, nie ujawniając jej — odpowiednik sprawdzania hasła dla funkcji ujawniającej. Porównanie działa wewnątrz zaplecza danych, w stałym czasie, a z powrotem wraca wyłącznie odpowiedź.

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

Parametry:

ParametrTypOpis
tablenamestr / stringTabela, względem której następuje sprawdzenie
columnstr / stringTajna kolumna do porównania
candidatestr / stringTestowana wartość jawna. Porównywana po stronie serwera i nigdy nie zapisywana do tabeli
query_params / queryParamsSecretQueryParamsKtóre wiersze biorą udział. Domyślnie najnowszy wiersz; limit nie może przekroczyć 100

W Go parametry są łańcuchami pozycyjnymi po kontekście, a zapytanie to *ironflock.TableQueryParams (nil oznacza najnowszy wiersz).

JavaScript zwraca { match, checked }, a Go — *ironflock.SecretVerifyResult z polami Match i Checked; Python zwraca zwykłą wartość bool. Tam, gdzie masz checked, warto z niej skorzystać: oddziela ona błędną wartość kandydującą (checked > 0) od selektora, który nie dopasował żadnego wiersza (checked === 0) — a są to bardzo różne błędy do zgłoszenia użytkownikowi.

Ponieważ każdy wiersz jest szyfrowany pod własną losową wartością, dwa wiersze przechowujące ten sam sekret zapisują różne szyfrogramy. Właśnie dlatego filtr = na tajnej kolumnie nie może działać — zaplecze danych odrzuca każdy filtr na takiej kolumnie z URI_SECRET_COLUMN — i dlatego istnieje ta funkcja.

Zaplecze danych obsługuje co najwyżej 120 sprawdzeń na minutę na poświadczenie aplikacji na urządzeniu; powyżej tej liczby wywołanie kończy się błędem URI_RATE_LIMITED. Przy sprawdzaniu przy każdym żądaniu (np. klucza API) krótko buforuj wynik pozytywny. Kolumna, która nie jest tajna, jest odrzucana z URI_NOT_A_SECRET_COLUMN.

Dostęp do danych między aplikacjami

Odczytuj dane floty innej aplikacji z poziomu własnej aplikacji, w obrębie tego samego projektu. Twoja aplikacja musi zadeklarować aplikację dostawcy w sekcji consumes: własnego pliku data-template.yml (lub posiadać uprawnienie wieloznaczne, opisane poniżej), a użytkownik projektu musi przyznać dostęp. Dostęp jest tylko do odczytu: możesz odpytywać historię oraz subskrybować w czasie rzeczywistym wiersze tabel i transformacji (transforms) udostępnianych przez dostawcę, ale nie możesz do nich zapisywać. Połączenia z konsumowanymi aplikacjami są buforowane per aplikacja i zamykane automatycznie, gdy twoja instancja zostaje zatrzymana.

Połączenie wymaga poświadczenia per aplikacja, które wstrzykuje agent urządzenia (APP_AUTH_ID, APP_AUTH_SECRET). Platforma odrzuca starsze poświadczenie urządzenia — awaryjnie używany numer seryjny aplikacji, która nie dostała poświadczenia per aplikacja, np. pod starszym agentem urządzenia — w realmie innej aplikacji (NOT_AUTHORIZED, z komunikatem, który to wyjaśnia), chyba że aplikacja dostawcy działa na tym samym urządzeniu: wtedy poświadczenie dołącza do jej realmu z pełnymi uprawnieniami tej aplikacji, a nie tylko do odczytu.

Jeśli twoja aplikacja posiada wieloznaczne (wildcard) uprawnienie (consumes: [{ app: "*" }]), możesz dynamicznie wykrywać i otwierać dostawców za pomocą listConsumableApps / list_consumable_apps oraz connectToAllApps / connect_to_all_apps (poniżej).

connectToApp / connect_to_app

Otwiera połączenie tylko do odczytu z zapleczem danych innej aplikacji i zwraca uchwyt (handle). Uchwyt udostępnia getHistory / get_history, subscribeToTable / subscribe_to_table oraz getSeriesHistory / get_series_history (tylko tabele) — te same zapytania i subskrypcje, których używasz we własnych tabelach, z tymi samymi parametrami i wynikami — a także close oraz udostępnione katalogi 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)

Parametry:

ParametrTypOpis
app_name / appNamestr / stringNazwa aplikacji dostawcy, zadeklarowana w twojej sekcji consumes:
stagestr / string, opcjonalnyEtap (stage) dostawcy: "dev" lub "prod". Domyślnie etap twojej własnej aplikacji
on_error / onErrorcallable, opcjonalnyWywoływany z obiektem CrossAppAccessError, gdy dostęp do otwartego połączenia zostanie odmówiony na dobre (np. gdy uprawnienie zostanie później cofnięte — poniżej opisano, kiedy odmowa jest uznawana za ostateczną)

Jeśli dostęp zostanie odmówiony lub użyty nieprawidłowo, zgłaszany (Python) / rzucany (JavaScript) / zwracany (Go, jako *ironflock.CrossAppAccessError, który sprawdzasz za pomocą errors.As) jest CrossAppAccessError z polem code (Code w Go, ze stałymi takimi jak ironflock.CodeNoGrant): NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (zapytanie, które filtruje — także wewnątrz grupy — lub wybiera kolumnę oznaczoną przez dostawcę jako tajna) lub NOT_AUTHORIZED (uprawnienie zostało cofnięte albo w danej chwili nie dało się go zweryfikować — albo twoja aplikacja przedstawiła starsze poświadczenie urządzenia, o czym informuje komunikat).

Odmowa w czasie, gdy connectToApp / connect_to_app czeka, od razu kończy wywołanie niepowodzeniem. Gdy uchwyt jest już otwarty, odmowa jest uznawana za ostateczną dopiero wtedy, gdy się utrzymuje — 3 lub więcej odmów od ostatniego aktywnego połączenia (zamknięcie sesji przez router z powodu uwierzytelniania liczy się jako jedna, podobnie jak każde odrzucone ponowne połączenie), z czego pierwsza co najmniej 60 sekund temu — ponieważ platforma odmawia w ten sam sposób, gdy przez chwilę nie może zweryfikować dostępu. Do tego czasu uchwyt ponawia łączenie i wraca do działania, jeśli ponowne połączenie się powiedzie. Gdy odmowa jest ostateczna, wywoływane jest on_error / onError / OnError, a uchwyt zostaje usunięty z bufora: następne connectToApp / connect_to_app / ConnectToApp otwiera nowe połączenie. Od tego momentu odczyty i subskrypcje uchwytu od razu kończą się niepowodzeniem z błędem, z którym wywołano on_error / onError — w Go z błędem opakowującym wamp.ErrStopped.

W Pythonie stage i on_error są argumentami nazwanymi; w JavaScript są przekazywane przez obiekt opcji (connectToApp(appName, { stage, onError })); w Go przez strukturę (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), gdzie OnError to func(*ironflock.CrossAppAccessError). W Go ctx ogranicza tylko oczekiwanie wywołującego: współbieżne wywołania dla tej samej aplikacji współdzielą jedną próbę, a Close(ctx) zamyka uchwyt.

listConsumableApps / list_consumable_apps

Wyświetla listę wszystkich niepublicznych dostawców w projekcie — prymityw wykrywania dla aplikacji posiadających wieloznaczne uprawnienie do konsumpcji (consumes: [{ app: "*" }] w twoim pliku data-template.yml, przyznane przez użytkownika projektu). Wykonuje pojedyncze wywołanie i nie otwiera żadnych połączeń: wyrenderuj zwrócone katalogi w selektorze, a następnie wywołaj connectToApp / connect_to_app dla tych, które chcesz otworzyć — albo connectToAllApps / connect_to_all_apps, aby otworzyć je wszystkie naraz.

Uwaga: Zadeklaruj uprawnienie w pliku data-template.yml swojej aplikacji i ujmij * w cudzysłów — samo * jest aliasem YAML i nie zostanie sparsowane:

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

Każdy wpis opisuje jednego dostawcę:

PoleTypOpis
appstr / stringNazwa aplikacji dostawcy
provider_app_keyint / numberKlucz aplikacji dostawcy (app key)
stagesdict / objectKatalog per etap { dev?, prod? }; etap jest obecny tylko wtedy, gdy dostawca ma dla niego zaplecze danych. Każdy katalog zawiera niepubliczne tables oraz transforms, które udostępnia

W Go każdy wpis to ironflock.ConsumedAppInfo z polami App, ProviderAppKey i Stages.Dev / Stages.Prod (*StageCatalog z Tables i Transforms, nil dla brakującego etapu); Catalog(stage) zwraca katalog po nazwie etapu.

Zgłaszany (Python) / rzucany (JavaScript) / zwracany (Go) jest CrossAppAccessError z code: NO_GRANT, jeśli twoja aplikacja nie posiada uprawnienia wieloznacznego.

connectToAllApps / connect_to_all_apps

Otwiera uchwyty tylko do odczytu dla wszystkich niepublicznych dostawców w projekcie w jednym wywołaniu (tylko dla konsumentów wieloznacznych). Wylicza dostawców za pomocą listConsumableApps / list_consumable_apps i otwiera każdego z nich, pomijając tych, którzy nie mają zaplecza danych dla żądanego etapu. Każdy uchwyt jest buforowany pod tym samym kluczem co connectToApp / connect_to_app, więc późniejsze wywołanie connectToApp(name) zwraca już otwarty uchwyt. Zwrócone uchwyty są zamykane razem, gdy twoja instancja zostaje zatrzymana.

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)

Parametry:

ParametrTypOpis
stagestr / string, opcjonalnyEtap (stage) dostawcy: "dev" lub "prod". Domyślnie etap twojej własnej aplikacji
continue_on_error / continueOnErrorbool / boolean, opcjonalnyGdy true (wartość domyślna), dostawca, którego nie udało się otworzyć, jest zgłaszany do on_error / onError i pomijany w wyniku. Gdy false, pierwszy błąd jest zgłaszany/rzucany
on_error / onErrorcallable, opcjonalnyWywoływany z każdym dostawcą, którego nie udało się otworzyć (gdy continue_on_error / continueOnError ma wartość true), oraz z obiektem CrossAppAccessError, jeśli dostęp do już otwartego połączenia zostanie później odmówiony na dobre (np. gdy uprawnienie zostanie cofnięte)

Zwraca pomyślnie otwarte uchwyty dostawców (ten sam typ uchwytu co connectToApp / connect_to_app). Zgłaszany (Python) / rzucany (JavaScript) / zwracany (Go) jest CrossAppAccessError z code: NO_GRANT, jeśli twoja aplikacja nie posiada uprawnienia wieloznacznego.

W Pythonie stage, on_error i continue_on_error są argumentami nazwanymi; w JavaScript są przekazywane przez obiekt opcji (connectToAllApps({ stage, onError, continueOnError })); w Go przez strukturę (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), gdzie OnError to func(error), a StopOnError: true odpowiada continueOnError: false.

Zarządzane przechowywanie plików

Każde zaplecze danych aplikacji otrzymuje obok swoich tabel prywatny magazyn obiektów, dostępny przez właściwość files (w Go ifl.Files(), *filestore.FileStore, który można pobrać jeszcze przed Start). Używaj go do obrazów, plików PDF, klatek z kamery, obrazów firmware — wszystkiego, co nie należy do wiersza tabeli. Nie jest wymagana żadna konfiguracja: aplikacja bez sekcji files: w swoim szablonie danych i tak otrzymuje jedną przestrzeń nazw o nazwie default (szablon, który deklaruje własne przestrzenie nazw, zastępuje ją — zobacz Przestrzenie nazw).

Ta sekcja to dokumentacja referencyjna po stronie klienta. Stronę platformy — deklarowanie przestrzeni nazw, przydziałów i retencji, udostępnianie plików między aplikacjami, poświadczenia S3 per aplikacja oraz zachowanie on-premises — opisuje strona Przechowywanie plików.

Kluczowa idea jest taka, że zapisanie obiektu zwraca ci od razu trwały adres URL, który możesz wpisać wprost do kolumny tabeli, dzięki czemu widget na panelu może go wyrenderować bez żadnej dodatkowej pracy:

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)

Ten adres URL nigdy nie wygasa, ale nie jest linkiem publicznym: pozostaje czytelny wyłącznie dla uwierzytelnionego żądającego, który posiada uprawnienie READ do tego zaplecza danych, a proxy uwierzytelniające sprawdza to ponownie przy każdym żądaniu. Dlatego można go bezpiecznie przechowywać w bazie danych.

Przestrzenie nazw

Przestrzeń nazw to prefiks klucza, z którym powiązane są zasady — retencja, reguły udostępniania, dozwolone typy zawartości. Nie jest to osobny bucket; wszystkie przestrzenie nazw aplikacji znajdują się w jej jednym obszarze przechowywania. Deklaruj nową tylko wtedy, gdy zbiór obiektów wymaga innych reguł; w przeciwnym razie pozostań w default i porządkuj obiekty ścieżkami kluczy w rodzaju 2026/03/part-1.jpg.

Przestrzenie nazw deklarujesz w data-template.yml. Zadeklarowanie którejkolwiek zastępuje niejawną default: wymień też przestrzeń o nazwie default, w przeciwnym razie każde wywołanie bez przestrzeni nazw zakończy się błędem 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 }

Zwróć uwagę, że budżet deklaruje się raz dla całej aplikacji, a nie osobno dla każdej przestrzeni nazw. Przestrzeń nazw to tylko prefiks klucza wewnątrz jednego obszaru przechowywania aplikacji, więc budżet per prefiks nie miałby czego egzekwować. maxObjectBytes jest ustawiany per przestrzeń nazw — ogranicza pojedynczy obiekt, a nie sumę: 100 MiB, jeśli go nie ustawiono, najwyżej 5 GiB (powyżej OBJECT_TOO_LARGE).

Każda z poniższych metod przyjmuje przestrzeń nazw jako argument opcjonalny, a domyślnie używa default; pusta przestrzeń nazw ("") również oznacza default.

Zapisywanie i odczytywanie obiektów

# 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")
MetodaOpis
put(key, data, …)Zapisuje obiekt (bytes w Pythonie, Uint8Array w JavaScript, []byte w Go). Zwraca metadane obiektu wraz z jego url
get(key, namespace?)Zwraca zawartość obiektu. Obiekt powyżej limitu inline przychodzi bezpośrednio z magazynu obiektów albo — tam, gdzie to niemożliwe — w zakresach bajtów przez router (zobacz Duże obiekty)
put_file(key, path, …) / get_to_file(key, path, …)Python i Go (PutFile / GetToFile; Go strumieniuje też z io.Reader za pomocą PutReader i do io.Writer za pomocą GetTo). Zapis z pliku lokalnego lub do pliku lokalnego. Strumieniuje na ścieżce dużych obiektów
delete(key, namespace?)Usuwa obiekt
copy(key, to, …)Kopiuje obiekt, opcjonalnie do innej przestrzeni nazw
move(key, to, …)Kopiowanie, a następnie usunięcie. Nie jest atomowe — usługa nie ma operacji przeniesienia, więc nieudane usunięcie pozostawia obie kopie

JavaScript nie ma pomocników operujących na ścieżkach plików, ponieważ pakiet dostarcza jedną kompilację zarówno dla Node, jak i dla przeglądarki — pliki lokalne odczytuj i zapisuj samodzielnie za pomocą fs.

W Pythonie operacje na plikach w put_file i get_to_file działają poza pętlą zdarzeń. get_to_file tworzy brakujące katalogi nadrzędne i zapisuje plik w miejscu docelowym, gdy tylko pierwsze bajty obiektu są dostępne: odrzucony odczyt (brak obiektu, brak autoryzacji) pozostawia ścieżkę nietkniętą, a odczyt, który zawiedzie w połowie, pozostawia to, co zdążyło zostać zapisane. FIFO jest zapisywane w miarę napływania strumienia obiektu, a jego otwarcie czeka na czytelnika, więc FIFO, którego nikt nie czyta, blokuje wywołanie (nie pętlę zdarzeń); asyncio.wait_for pozwala z niego zrezygnować. put_file odczytuje FIFO do końca przed zapisaniem.

W Go GetToFile również tworzy brakujące katalogi nadrzędne, ale pobiera dane do pliku tymczasowego obok pliku docelowego i po zakończeniu zmienia jego nazwę na docelową: nieudane lub anulowane pobieranie nigdy nie pozostawia częściowego pliku i zachowuje poprzedni. Dowiązania symboliczne są rozwiązywane (zapisywany jest plik, do którego prowadzą, a dowiązania pozostają); plik zastępczy otrzymuje, na ile pozwalają uprawnienia procesu, właściciela, grupę i bity uprawnień starego pliku. FIFO lub urządzenie jest zapisywane w miejscu, w miarę napływania strumienia obiektu. Dla pliku docelowego, obok którego nie da się utworzyć nowego pliku, np. w systemie plików tylko do odczytu lub w pseudo-systemie plików, użyj GetTo z plikiem, który otworzysz samodzielnie.

put przyjmuje: content_type / contentType (typ MIME; przestrzeń nazw może ograniczać dozwolone wartości) oraz namespace. W Pythonie są to argumenty nazwane; w JavaScript trafiają do obiektu opcji; w Go są to opcje (filestore.ContentType(…), filestore.Namespace(…)).

Wyświetlanie listy i inspekcja

MetodaOpis
list(…)Jedna strona listingu usługi plików, domyślnie w stylu folderów: objects zawiera obiekty, których klucze kontynuują prefiks bez /, a prefixes — foldery poniżej. delimiter równy "" listuje płasko — każdy obiekt pod prefiksem, także w podfolderach. Zwraca objects, prefixes, is_truncated / isTruncated / IsTruncated oraz cursor, który przekazujesz z powrotem po następną stronę
iter(…) / iterate(…)Asynchroniczny iterator po każdym obiekcie pod danym prefiksem, także w podfolderach, stronicujący automatycznie. W Pythonie nazywa się iter, w JavaScript iterate, w Go Iter (iter.Seq2[*ObjectInfo, error] dla pętli range)
stat(key, namespace?)Metadane jednego obiektu bez przesyłania jego zawartości
exists(key, namespace?)Czy obiekt istnieje
namespaces()Przestrzenie nazw, których ta aplikacja może używać
usage(…)Ile magazynu zajmuje aplikacja — patrz niżej
catalog()Przestrzenie nazw wraz z limitami i przydziałami wystawionymi przez serwer. Pobierane raz i buforowane: catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) pobiera je ponownie

Foldery. Prefiks zakończony na / wskazuje folder: iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) zwraca 2026/a.txt i 2026/sub/c.txt, ale nie 2026-notes.txt. list nie potrafi otworzyć folderu po nazwie, ponieważ usługa plików odrzuca prefiks listingu zakończony na / (FleetFiles v0.2 odpowiada INTERNAL): list z prefiksem 2026 zwraca 2026-notes.txt wśród obiektów i folder 2026/ wśród prefiksów. Usługa przechowuje klucze w postaci normalizacji Unicode C, więc prefiks folderu również podawaj w NFC; iteracja, która natrafi na klucz pod inną postacią prefiksu, kończy się błędem, zamiast pomijać każdy klucz.

Obiekty są opisywane tymi samymi polami we wszystkich trzech SDK, w stylu nazewnictwa właściwym dla danego języka: namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 oraz url — w Go są to pola Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256 i URL struktury filestore.ObjectInfo.

Wykorzystanie magazynu i przydział

usage odpowiada prosto z magazynu obiektów w jednym wywołaniu, więc sumy są dokładne, a nie doliczane przez 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}

W Go są to pola SizeBytes, ObjectCount, QuotaBytes, FreeBytes i PerNamespace struktury filestore.StorageUsage.

PoleZnaczenie
size_bytes / sizeBytesAktualnie przechowywane bajty
object_count / objectCountLiczba przechowywanych obiektów
quota_bytes / quotaBytesEgzekwowany budżet. 0 oznacza brak limitu
free_bytes / freeBytesPozostałe bajty. -1 oznacza brak limitu — zgłoszenie tu 0 czytałoby się jako „pełny”
per_namespace / perNamespaceBajty w rozbiciu na przestrzenie nazw. Obecne tylko wtedy, gdy poprosisz o szczegółowe rozbicie

Rozbicie na przestrzenie nazw jest domyślnie wyłączone, ponieważ magazyn nie potrafi na nie odpowiedzieć wprost: rozlicza się per obszar przechowywania, a przestrzeń nazw to tylko prefiks, więc SDK musi wylistować każdą przestrzeń nazw i zsumować rozmiary. Proś o nie wtedy, gdy naprawdę go potrzebujesz, a nie na gorącej ścieżce.

Pojawiają się dwa różne przydziały i warto je od siebie odróżniać. catalog() raportuje oba:

PoleZnaczenie
quota_bytes / quotaBytesTo, co jest faktycznie egzekwowane, odczytane z magazynu obiektów — ustawienie użytkownika projektu
suggested_quota_bytes / suggestedQuotaBytesTo, o co poprosił szablon danych aplikacji. 0, jeśli o nic nie prosił

W Go Catalog(ctx) zwraca *filestore.Catalog z polami QuotaBytes i SuggestedQuotaBytes.

Różnią się zawsze wtedy, gdy użytkownik podniósł lub obniżył budżet aplikacji — i właśnie dlatego wartość egzekwowana jest odczytywana z magazynu, a nie z szablonu: ponowne wdrożenie aplikacji nie może po cichu zresetować wyboru użytkownika. Interfejs może pokazywać obie („aplikacja sugeruje X, Ty ustawiłeś Y”). Egzekwowanie zawsze korzysta z tej pierwszej. Użytkownik może zmienić budżet w dowolnej chwili, a catalog() raportuje go według stanu z ostatniego pobrania: aktualną wartość odczytuj z usage().

Udostępnianie obiektów

Istnieją dwa rodzaje linków, a różnica między nimi ma znaczenie:

MetodaCzas życiaKto może go odczytać
url(key, …)TrwałyWyłącznie uwierzytelniony żądający z uprawnieniem READ do tego zaplecza danych — sprawdzanym ponownie przy każdym żądaniu. Można go bezpiecznie przechowywać w kolumnie tabeli
share_url / shareUrl / ShareURLWygasający (domyślnie 15 min, ograniczany przez serwer)Każdy, kto ma ten link. Nic nie sprawdza ponownie uprawnień w chwili jego użycia

share_url / shareUrl to poświadczenie na okaziciela: przekaż je osobie, która potrzebuje tymczasowego dostępu, i nie przechowuj go w bazie danych. Do wszystkiego, co renderuje panel, używaj url.

url zwraca None / undefined / "" (Go) tam, gdzie wdrożenie nie ma krawędzi HTTP (na przykład Appliance działający po zwykłym HTTP) — to sygnał, aby sięgnąć po get. Przekazanie etag obiektu jako argumentu version (w Go filestore.Version(etag)) pozwala przeglądarkom buforować odpowiedź jako niezmienną.

W Pythonie i Go cloud_url(key, …) / CloudURL(ctx, key, …) zwraca trwały adres URL przez chmurowy tunel plików Appliance (https://i<instanceKey>-files.<cloud-edge>): osiągalny z internetu, dopóki Appliance jest online, z taką samą autoryzacją jak url. Zwraca None ("" w Go) we wdrożeniach hostowanych w chmurze (gdzie url jest już osiągalny z internetu), tam, gdzie nie ma krawędzi HTTP, oraz gdy udostępnianie w chmurze zaplecza danych jest wyłączone. Użytkownik może je przełączyć w dowolnej chwili, dlatego cloud_url / CloudURL ponownie odczytuje katalog, gdy zbuforowany jest starszy niż 30 sekund.

upload_url / uploadUrl / UploadURL tworzy wygasający adres URL, który przyjmuje bezpośrednie przesłanie danych, i zwraca url, method, headers oraz expires_in / expiresIn (w Go *filestore.UploadTarget z polami URL, Method, Headers i ExpiresIn). Wyślij nagłówki w otrzymanej postaci, aby obiekt został zapisany z typem zawartości ustalonym przy tworzeniu adresu. Typy zawartości i limit rozmiaru przestrzeni nazw, zadeklarowany size oraz przydział są sprawdzane w chwili tworzenia adresu URL, a nie jego użycia: adres jest podpisany tylko dla hosta, więc każdy, kto go posiada, może zapisać pod jego kluczem dowolną zawartość aż do wygaśnięcia. Podobnie jak share_url / shareUrl jest to poświadczenie na okaziciela — takie, które pozwala zapisywać.

Duże obiekty

SDK wybiera sposób transferu automatycznie, na podstawie rozmiaru — nie ma tu nic do konfigurowania:

Rozmiar obiektuSposób transferu
Do limitu inline (obecnie 6 MiB)Pojedyncze wywołanie przez router wiadomości
PowyżejBezpośrednio do magazynu obiektów przez HTTPS, z pominięciem routera
Powyżej, odczyt tam, gdzie nie ma ścieżki bezpośredniejW zakresach bajtów przez router wiadomości

Dokładny limit serwer podaje w czasie działania jako inline_max_bytes / inlineMaxBytes / InlineMaxBytes w catalog(), dzięki czemu można go podnieść bez wydawania nowej wersji SDK.

Ścieżka bezpośrednia wymaga wdrożenia, które potrafi wstępnie podpisywać (presign) adresy URL, oraz urządzenia z dostępem do hosta magazynu obiektów, a nie tylko do routera. Tam, gdzie brakuje któregoś z nich — katalog mówi, że wdrożenie nie potrafi podpisywać (domyślnie Appliance on-premises), utworzenie adresu URL do odczytu zwraca NOT_SUPPORTED albo magazyn obiektów jest nieosiągalny, zanim dotarł jakikolwiek bajt — get (oraz get_to_file w Pythonie, GetTo i GetToFile w Go) odczytują obiekt w zakresach bajtów przez router (FleetFiles v0.1.33 i nowsze), każdy co najwyżej o wielkości limitu inline. Każdy zakres to osobny odczyt: obiekt zastąpiony w trakcie takiego odczytu powoduje jego niepowodzenie z INTERNAL (“object changed while it was read”). Zapisy nie mają takiej ścieżki zastępczej.

Dla zapisów pozostają dwa górne pułapy i oba zgłaszają TOO_LARGE z przyczyną wskazującą, na który z nich trafiłeś:

  • 5 GiB — limit pojedynczego przesłania w magazynie obiektów. Przesyłanie wieloczęściowe (multipart) nie jest jeszcze zaimplementowane.
  • Limit inline tam, gdzie nie ma bezpośredniego punktu końcowego — wdrożenie, które nie potrafi podpisywać adresów (domyślnie Appliance), w ogóle nie może zapisać większego obiektu. Nie pomoże tu ani ponowienie próby, ani mniejszy fragment, i komunikat wprost o tym informuje.

Dwa niepowodzenia ścieżki bezpośredniej częste w terenie mają własne kody, zamiast wyglądać jak problemy z autoryzacją: PRESIGN_UNREACHABLE (proxy przepuszczające wyłącznie ruch do routera — odczyt przechodzi na zakresy, zapis kończy się błędem) oraz CLOCK_SKEW (magazyn obiektów odrzuca żądania rozbieżne w czasie o więcej niż 15 minut — sprawdź NTP na urządzeniu).

Błędy przechowywania plików

Niepowodzenie usługi plików lub bezpośredniego transferu zgłasza (Python) / rzuca (JavaScript) / zwraca (Go) FileStoreError, który niesie stabilny code oraz czytelny dla człowieka reason. Rozgałęziaj logikę na podstawie code, nigdy na podstawie reason. Błędy plików lokalnych, anulowane wywołanie i błędy WAMP na poziomie routera inne niż zmapowane poniżej są zgłaszane bez zmian.

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
KodZnaczenie
NOT_AUTHORIZEDWywołujący nie może wykonać tej operacji
NO_SUCH_NAMESPACEPrzestrzeń nazw nie jest zadeklarowana w szablonie danych
NO_SUCH_OBJECTKlucz nie istnieje
TOO_LARGEPrzekracza limit transferu w pojedynczym wywołaniu
OBJECT_TOO_LARGEPrzekracza własny maxObjectBytes przestrzeni nazw
QUOTA_EXCEEDEDMagazyn plików jest pełny
CONTENT_TYPE_NOT_ALLOWEDPrzestrzeń nazw ogranicza contentTypes
INVALID_KEYNieprawidłowy klucz lub prefiks (patrz niżej; FleetFiles v0.2 nadal zgłasza go jako INTERNAL)
INVALID_RANGEZakres bajtów poza obiektem; SDK nigdy o taki nie proszą
NOT_SUPPORTEDZaplecze nie potrafi tego wykonać
NOT_AVAILABLETo wdrożenie nie ma usługi plików — albo jeszcze jej nie ma: po restarcie platformy lub gdy usługa plików rejestruje swoje procedury. Wywołanie nigdzie nie zostało wykonane i można je powtórzyć
PRESIGN_UNREACHABLEMagazyn obiektów nie jest osiągalny bezpośrednio (proxy?)
CLOCK_SKEWZegar urządzenia jest zbyt rozbieżny
INTERNALWszystko pozostałe — także obiekt, który zmienił się podczas odczytu w zakresach

Nowszy serwer może wprowadzić kody, których to wydanie SDK nie zna. Są one przekazywane dalej jako code, zamiast być sprowadzane do jednej wartości, więc traktuj nierozpoznaną wartość jako ogólne niepowodzenie.

Wywołania plikowe czekają na połączenie (do 10 sekund w Pythonie i Go, 6 w JavaScript) i nie są ponawiane: w przeciwieństwie do operacji na tabelach nie przeczekują restartu platformy, więc w jego trakcie wywołanie może zakończyć się błędem NOT_AVAILABLE, dopóki usługa plików ponownie się nie zarejestruje. Takie wywołanie nigdzie nie zostało wykonane, więc twoja aplikacja może je powtórzyć.

Klucze. O kluczach rozstrzyga usługa plików; SDK wysyłają klucze i prefiksy w podanej postaci. Nieprawidłowy klucz lub prefiks — pusty; z / na początku lub na końcu (poza prefiksem folderu dla iter / iterate); z pustym segmentem, segmentem . lub .. albo segmentem kończącym się na . lub zaczynającym się albo kończącym spacją; z jednym ze znaków \ : * ? " < > | ^ ~, znakiem sterującym lub znakiem formatującym Unicode; powyżej 900 bajtów, 20 segmentów lub 255 bajtów na segment; albo pod zarezerwowanym prefiksem _sys/ — jest odrzucany z INVALID_KEY przez usługę plików, która klasyfikuje błędy kluczy, a przez FleetFiles v0.2 z INTERNAL. Uważaj na znaczniki czasu: 2026-10-09T12:00:00Z nie jest prawidłowym segmentem klucza z powodu :.

W Pythonie FileStoreError jest importowany z ironflock.filestore; w JavaScript jest eksportowany z katalogu głównego pakietu (import { FileStoreError } from "ironflock"); w Go jest to *filestore.Error (także ironflock.FileStoreError), z kodami jako stałymi, np. filestore.CodeQuotaExceeded.

Komunikacja między urządzeniami

registerDeviceFunction / register_device_function

Rejestruje procedurę, którą mogą wywoływać inne urządzenia z tą samą aplikacją (w tym samym etapie), a także akcje widgetów na panelu. SDK buduje za ciebie pełny identyfikator URI <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>, gdzie STAGE to DEV lub PROD. Router nie przyjmuje od aplikacji żadnej innej rejestracji i wyłącznie rejestracje pojedyncze (single) (zobacz Identyfikatory URI dozwolone dla aplikacji). Rejestracja jest przywracana po każdym ponownym połączeniu.

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

register() jest aliasem dla register_device_function().

callDeviceFunction / call_device_function

Wywołuje procedurę zarejestrowaną przez inne urządzenie z tą samą aplikacją. SDK automatycznie składa pełny temat (topic) WAMP, używając klucza docelowego urządzenia: <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic> — ten sam identyfikator URI, który registerDeviceFunction / register_device_function rejestruje na urządzeniu docelowym.

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

call

Wywołuje zdalną procedurę, używając pełnego identyfikatora URI WAMP. Aplikacja nie może zarejestrować nazwy takiej jak some.full.wamp.topic, więc taka nazwa dociera wyłącznie do procedur, które usługi platformy rejestrują w realmie danych aplikacji, takich jak procedury zaplecza danych i przechowywania plików. Aby wywołać funkcję zarejestrowaną przez inne urządzenie twojej aplikacji, użyj callDeviceFunction / call_device_function.

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

Identyfikatory URI dozwolone dla aplikacji

Kontener twojej aplikacji dołącza do własnego realmu danych, realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>, z rolą routera app. Router sprawdza tam każdą publikację, subskrypcję, wywołanie i rejestrację względem listy dozwolonych akcji tej roli. Własne metody SDK mieszczą się w tej liście; reguły mają znaczenie, gdy przekazujesz własne identyfikatory URI do publish, subscribe lub call albo korzystasz bezpośrednio z connection (Connection() w Go). Odrzucona akcja kończy się błędem wamp.error.not_authorized (w Go zwraca go ironflock.WampURI(err)).

URI w twoim własnym realmieDozwoloneUżywane przez
Twoje własne nazwy, np. com.myapp.statuspublikowanie, subskrypcja (także subskrypcje prefiksowe), wywołanie — nigdy rejestracjapublish, subscribe, call
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table>publikowanie — nigdy subskrypcjapublishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table
transformed.<table>, transformed.bulk.<table>tylko subskrypcjasubscribeToTable / subscribe_to_table
databackend.errors.…, files.events…tylko subskrypcjaZdarzenia błędów i plików zaplecza danych
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>rejestracja (single), wywołanie, publikowanie — nigdy subskrypcjaregisterDeviceFunction / register_device_function, callDeviceFunction / call_device_function
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.…wywołanie (wywołuje je SDK)Metody tabel, historii, sekretów i plików
sys.appaccess.list, sys.appaccess.resolvewywołanielistConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps
wamp.session.getwywołanieHeartbeat SDK dla JavaScriptu

Nazwy ze sprawdzaniem tożsamości. Nazwy zaczynające się od cyfry — tematy zapisu tabel i identyfikatory URI funkcji urządzenia — przechodzą też sprawdzenie tożsamości przez platformę, które wiąże je z SWARM_KEY twojego realmu. Rejestracja jest akceptowana tylko dla identyfikatora URI funkcji twojego własnego urządzenia z SWARM_KEY, APP_KEY i etapem (stage) twojego realmu; rejestracja prefiksowa lub wieloznaczna — tylko wtedy, gdy ustala te cztery segmenty. Publikowanie i wywołanie wymagają tylko SWARM_KEY twojego realmu: wiersz opublikowany do tematu tabeli z innym APP_KEY zostaje przyjęty, ale nie trafia do żadnego zaplecza danych i przepada. Pozwól, aby te identyfikatory URI budowało SDK.

Nazwy zarezerwowane. Router odrzuca sys. z wyjątkiem dwóch powyższych procedur międzyaplikacyjnych oraz wamp. z wyjątkiem wamp.session.get. Pozostałe nazwy z tabeli należą do platformy: nie zaczynaj własnych nazw od cyfry, bulk., transformed., databackend.errors., files., append., appendBulk., history. ani secret.. Każdy inny pierwszy segment to własna przestrzeń tematów twojej aplikacji.

Nikt nie subskrybuje surowych tematów tabel. Przenoszą one wiersz dokładnie w wysłanej postaci, łącznie z jawną wartością tajnych kolumn, które zaplecze danych szyfruje dopiero przy zapisie wiersza. subscribeToTable / subscribe_to_table odczytuje zamiast tego zapisane wiersze z transformed.<table>, z typami i z zamaskowanymi tajnymi kolumnami.

Dane innych aplikacji są tylko do odczytu. W realmie dostawcy twoja aplikacja (rola routera app_reader) może jedynie subskrybować transformed.… i files.events… oraz wywoływać history.transformed.… i files.read.…, poza heartbeatem wamp.session.get — bez publikowania, bez rejestracji, bez zapisów i bez procedur sekretów.

Metadane urządzenia

setDeviceLocation / set_device_location

Prosi platformę o zaktualizowanie lokalizacji GPS urządzenia, którą mapy IronFlock pokazują w czasie rzeczywistym.

Obecnie niedostępne: żadna usługa platformy nie odpowiada jeszcze na to wywołanie w realmie danych aplikacji, więc kończy się ono błędem wamp.error.no_such_procedure. Do tego czasu przechowuj lokalizacje we własnej tabeli.

await ironflock.set_device_location(long=8.6821, lat=50.1109)
ParametrZakres
long-180 do 180
lat-90 do 90

Historia lokalizacji nie jest przechowywana. Aby śledzić lokalizację w czasie, utwórz dedykowaną tabelę i użyj publish_to_table / publishToTable.

getRemoteAccessUrlForPort

Zwraca adres URL, pod którym port zadeklarowany w pliku port-template.yml aplikacji jest osiągalny, gdy jego tunel jest aktywny.

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>"
ParametrTypOpis
portint / numberPort zadeklarowany w port-template.yml
protocolstr / string, opcjonalnyProtokół portu: "http" (domyślny), "https", "tcp" lub "udp", bez względu na wielkość liter; otaczające spacje są pomijane, a pusty protokół oznacza "http"
remote_port_environment / remotePortEnvironmentstr / string, opcjonalnyTylko tcp/udp: zmienna przechowująca port zdalny, zamiast REMOTE_PORT_FOR_<port> — własne remote_port_environment portu z szablonu, którego potrzebuje agent urządzenia w wersji starszej niż 0.19.8

Porty http i https są serwowane pod etykietą tunelu <device_key>-<app name, lower case>-<port> (porty https z prefiksem platformy secure-) w domenie tuneli wstrzykiwanej przez agenta urządzenia (TUNNEL_DOMAIN), zawsze jako adres URL https://. Na urządzeniu należącym do Appliance on-premises (agent wstrzykuje INSTANCE_KEY) zwracana jest zamiast tego trasa przekazywana przez chmurę https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN>.

Adres URL tcp lub udp zawiera publiczny port przydzielony przez tunel, który agent urządzenia ogłasza jako REMOTE_PORT_FOR_<port> (agent 0.19.8 i nowsze): <protocol>://<TUNNEL_DOMAIN>:<remote port>. Na urządzeniu Appliance preferowany jest port dostępny z internetu, REMOTE_PORT_FOR_<port>_CLOUD, dopóki agent go ogłasza; zanim się pojawi i ponownie, gdy agent go usunie (przekazywanie przez chmurę wyłączone), zwracany jest lokalny adres URL Appliance. Port 0 nie został jeszcze przydzielony.

Wartości są odczytywane na bieżąco z /data/env, więc wywołuj metodę ponownie, zamiast buforować wynik. Niektóre przypadki, których adres URL nie obsłuży:

  • Agent urządzenia tuneluje porty wyłącznie instalacji produkcyjnych (PROD). W kontenerze deweloperskim (DEV) adres URL http(s) jest adresem instalacji PROD tej samej aplikacji na tym urządzeniu, a tcp/udp nie dają żadnego; do aplikacji DEV docieraj zamiast tego przez adres LAN urządzenia (DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>).
  • Na urządzeniu brzegowym Appliance zainstalowanego bez domeny (tryb prosty) agent ustawia TUNNEL_DOMAIN na localhost, więc składane tam adresy URL wskazują na localhost: taki Appliance serwuje porty aplikacji na portach swojego hosta, czego adres URL tunelu nie potrafi wyrazić.
  • Nieustawione lub puste domeny tuneli domyślnie przyjmują wartość app.ironflock.com. Platforma obecnie nie ustawia CLOUD_TUNNEL_DOMAIN: adresy URL Appliance używają app.ironflock.com, chyba że aplikacja sama ustawi tę zmienną w swoich ustawieniach środowiska.
  • W kontenerze działającym jako użytkownik inny niż root wartości są tymi, z którymi kontener wystartował (zobacz Zmienne środowiskowe).

Metoda zwraca None / null (w Go "" i false), gdy adresu URL nie da się złożyć: brakuje klucza urządzenia lub nazwy aplikacji, port tcp/udp nie ma jeszcze ogłoszonego portu zdalnego (lub ma 0), protokół jest nieznany albo etykieta nie jest prawidłową etykietą DNS (ponad 63 znaki lub nazwa aplikacji z kropką). W przeglądarce nie ma środowiska: adres URL http(s) jest składany na app.ironflock.com z opcji deviceKey i appName.

Właściwości i cykl życia połączenia

WłaściwośćTypOpis
is_connectedboolCzy połączenie z platformą jest aktywne
connectionCrossbarConnectionBazowa instancja połączenia (zastosowania zaawansowane)
MetodaOpis
run()Uruchamia połączenie i wykonuje mainFunc (blokujące)
await start()Uruchamia połączenie asynchronicznie
await stop()Zatrzymuje połączenie i połączenia z konsumowanymi aplikacjami oraz anuluje uruchomione zadania. Połączenia opuszczają swoje realmy i nie łączą się potem ponownie; start() łączy od nowa. Wywołanie start() lub connect_to_app, które wciąż czeka, kończy się niepowodzeniem (zobacz Niezawodność połączenia)
await run_async()Uruchamia i utrzymuje połączenie działające asynchronicznie

Niezawodność połączenia

SDK samo nawiązuje połączenie na nowo. Gdy połączenie padnie, ponawia próby aż router wróci, a następnie odtwarza wszystkie subskrypcje i wszystkie zarejestrowane funkcje urządzenia — po ponownym połączeniu nie musisz subskrybować od nowa. Jeśli którejś z nich nie da się odtworzyć, pozostałe i tak zostaną odtworzone, a ta nieudana jest ponawiana, dopóki trwa sesja — po 1 sekundzie, z podwajaniem do 30 sekund i z każdym oczekiwaniem zmienianym losowo o maksymalnie 15 % — oraz ponownie po każdym ponownym połączeniu: router już ją wcześniej zaakceptował, więc jego odmowa jest przejściowa, jak np. sprawdzenie tożsamości funkcji urządzenia przez platformę, które przez chwilę kończy się odmową (fail closed). Pierwsze niepowodzenie jest logowane jako ostrzeżenie, a odtworzenie — gdy się powiedzie.

Operacje na tabelach — zapisy, odczyty historii, serii i sekretów oraz odczyty historii konsumowanej aplikacji — również przeczekują restart platformy, taki jak aktualizacja routera lub platformy. Czekają do reconnect_window (Python, w sekundach) / reconnectWindowMs (JavaScript) / WithReconnectWindow (Go, time.Duration), domyślnie 60 sekund, aż połączenie wróci, i ponawiają próby, dopóki backend danych nie obsługuje jeszcze swoich procedur: router odpowiada wamp.error.no_such_procedure albo backend danych odrzuca wywołanie, które wyprzedziło jego rejestrację, z komunikatem “client has no handler for registration”. Takie wywołanie nigdy nie dotarło do handlera, więc ponowienie nigdy nie zapisze wiersza dwukrotnie; każdy inny błąd jest zgłaszany od razu. Wywołania plikowe nie są ponawiane (zobacz Błędy przechowywania plików). Żądanie wysłane już w chwili, gdy zamyka się jego sesja (zamyka ją router, zrywa się łącze albo zamyka ją stop()), od razu kończy się niepowodzeniem, zamiast czekać na odpowiedź, i nie jest ponawiane: mogło dotrzeć do swojego handlera. W Go takie żądanie — oraz każda operacja, która w czasie swojego oczekiwania nie znajdzie połączenia — kończy się błędem opakowującym wamp.ErrNotConnected (wamp.ErrStopped po Stop). Pozostałe operacje w Go (Publish, Subscribe, Call, funkcje urządzenia, wykrywanie aplikacji) czekają na połączenie do 10 sekund i nie są ponawiane.

Szczególnym przypadkiem jest osiągalny router, który nie ma jeszcze realmu dla aplikacji. Przy równoczesnej instalacji kontener aplikacji startuje, zanim backend danych zdąży utworzyć jej realm, więc pierwsze próby dołączenia są odrzucane z wamp.error.no_such_realm; SDK ponawia próbę co kilka sekund i aplikacja rusza w chwili, gdy realm się pojawi. Realm, którego wciąż brakuje po minucie, najprawdopodobniej nigdy się nie pojawi — aplikacja została usunięta albo nie ma backendu danych dla tego stage’a — dlatego od tego momentu SDK zwalnia do jednej próby co dwie minuty, zamiast w nieskończoność odpytywać router co sekundę. Szybkie tempo wraca, gdy tylko realm się pojawi albo gdy sam router zniknie i wróci.

stop() kończy każde oczekiwanie na połączenie. Wywołanie start(), które wciąż czeka na pierwsze dołączenie — na przykład do realmu, który jeszcze nie istnieje — kończy się błędem Connection to realm <realm> stopped before it opened. Wywołanie connect_to_app / connectToApp, które wciąż otwiera połączenie z dostawcą, kończy się błędem Connection to app '<app>' (<stage>) aborted by stop() albo, gdy połączenie mimo to dołączy, zostaje ono zamknięte, a wywołanie kończy się błędem Connection to app '<app>' (<stage>) closed by stop(). Odmowa, która zbiegnie się w czasie ze stop(), zachowuje swój CrossAppAccessError. Gdy stop() zwraca sterowanie, żadne połączenie nie jest już nawiązywane; router, który nie odpowie na opuszczenie realmu, zostaje odcięty po 5 sekundach.

W Go Stop(ctx) również kończy te oczekiwania, z błędami opakowującymi wamp.ErrStopped: wywołanie Start, które wciąż czeka, zwraca taki błąd, a ConnectToApp, które wciąż otwiera połączenie, kończy się błędem ironflock: connection to app '<app>' (<stage>) aborted by Stop (lub closed by Stop, gdy połączenie mimo to dołączy). Stop czeka na opuszczenie realmu w ramach ctx, a następnie zamyka gniazdo; Run daje mu na to 10 sekund.

Trudniejszy przypadek to połączenie, które umiera po cichu: bezczynne połączenie zostaje przerwane przez NAT lub proxy bez powiadomienia którejkolwiek ze stron albo router restartuje się za load balancerem. Nie przychodzi nic, co sygnalizowałoby utratę. Aplikacja, która tylko publikuje, dowie się o tym przy następnym nieudanym zapisie, ale aplikacja, która tylko subskrybuje, nigdy nie zapisuje — bez wsparcia pozostanie martwa aż do restartu kontenera.

Wszystkie trzy SDK to wykrywają. Wykrywanie jest domyślnie włączone i nie wymaga konfiguracji.

SDK dla Pythona korzysta z keepalive WebSocket: co 30 sekund wysyła ping do routera, gdy łącze jest bezczynne, i oczekuje ponga w ciągu 10 sekund. Jeśli go nie ma, połączenie zostaje zerwane i nawiązane ponownie. Ruch przychodzący liczy się jako dowód życia, więc obciążone połączenie nigdy nie jest sondowane — ping wychodzi tylko wtedy, gdy łącze faktycznie ucichło.

Połączenia międzyaplikacyjne otwarte przez connectToApp / connect_to_app zwykle tylko subskrybują, a to dokładnie ten przypadek, który jest tu chroniony. Są objęte automatycznie, a po odmowie routera również łączą się ponownie, dopóki odmowa się nie utrwali (zobacz connectToApp).

Obsługa błędów

Każda metoda SDK zgłasza niepowodzenie głośno: przy nieprawidłowych argumentach, utraconym połączeniu lub odrzuceniu przez platformę zgłasza wyjątek (Python), odrzuca obietnicę (JavaScript) lub zwraca błąd (Go) z komunikatem wskazującym operację, temat (topic) oraz przyczynę. Nic nie jest po cichu pomijane, więc opakuj w blok try te wywołania, które mają przetrwać błąd — w Go sprawdzaj zwrócony błąd.

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

W JavaScript niepowodzenia pochodzące z platformy są instancjami WampError — zwykłej podklasy Error, która dodatkowo przenosi identyfikator URI błędu WAMP w polu error oraz ładunek błędu w polach args / kwargs. Wszystko pozostałe (nieprawidłowe parametry, brak połączenia) to zwykły Error.

W Go nieudana operacja zwraca *ironflock.OperationError, którego komunikat wskazuje operację i przyczynę, którą opakowuje; errors.Is i errors.As widzą przez niego. Odmowa routera lub wywoływanego to *ironflock.WampError (*wamp.Error) z polami URI, Args i Kwargs; ironflock.WampURI(err) zwraca identyfikator URI albo "". Kolejne błędy wartownicze to ironflock.ErrMissingConfig (brakuje tożsamości, której operacja potrzebuje), ironflock.ErrAlreadyStarted oraz wamp.ErrStopped (po Stop).

Migracja: starsze wersje SDK dla Pythona i JavaScriptu zapisywały komunikat w logu i zwracały None / null, gdy wywołanie się nie powiodło. Teraz zamiast tego zgłaszają błąd, więc kod w rodzaju if result is None: nie wykrywa już niepowodzeń — użyj try / except (lub try / catch).

Odmowy zaplecza danych

Operacja, której zaplecze danych odmawia, kończy się jednym z poniższych identyfikatorów URI błędów WAMP, z przyczyną jako pierwszym argumentem. Wszystkie trzy SDK eksportują je jako stałe; Go nazywa je w stylu Go (ironflock.URIRateLimited, ironflock.URIStorageFull, …). W Pythonie metody IronFlock zgłaszają RuntimeError, którego przyczyną jest ApplicationError z autobahn (odczyty konsumowanej aplikacji zgłaszają bezpośrednio ApplicationError), a wamp_error_uri(err) odczytuje identyfikator URI z każdego z nich. W JavaScript błędem jest WampError, którego error to identyfikator URI. W Go jest to *ironflock.OperationError opakowujący *ironflock.WampError, a ironflock.WampURI(err) odczytuje identyfikator URI. SDK nie ponawiają żadnego z nich. Opublikowany wiersz, który zaplecze danych odrzuci, przepada bez błędu — zgłaszają to tylko metody append.

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
StałaOdrzucane
URI_RATE_LIMITEDrevealSecrets / reveal_secrets powyżej 30, verifySecret / verify_secret powyżej 120 wywołań na minutę na poświadczenie aplikacji na urządzeniu
URI_RESULT_TOO_LARGEOdczyt z pojedynczym wierszem przekraczającym budżet wiadomości 8 MiB lub wynik powyżej zabezpieczenia 1 GiB (inne duże wyniki przychodzą w częściach, które SDK składa). Jego argumenty nazwane wskazują największe kolumny: zmniejsz limit, zawęź timeRange, wybierz mniej kolumn w columns albo przytnij je za pomocą columnPaths
URI_INVALID_LIMITOdczyt transformacji z limit powyżej 3000
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPSZapytania o serie, na które tabela nie potrafi odpowiedzieć: brak początku albo koniec, który nie wypada po nim; metryka, której tabela nie potrafi zagregować (brak takiej kolumny, tajna kolumna, metoda nieobsługiwana przez typ kolumny, tsp z metodą inną niż COUNT); kolumna groupBy, według której nie można grupować; ponad 50 000 wierszy
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMNFiltr na tajnej kolumnie; verifySecret / verify_secret na kolumnie, która nie jest tajna, lub revealSecrets / reveal_secrets tabeli bez tajnych kolumn
URI_STORAGE_FULLDołączanie, gdy dysk Appliance jest prawie pełny (poniżej ok. 3 GiB wolnego miejsca; ponownie przyjmowane od 4 GiB) — stan przejściowy: dołącz wiersze później
URI_STORAGE_OVERUSAGEDołączanie ponad przydział magazynu konta; szybkie ponowienie nie pomoże
URI_ENTITY_KEY_CONFLICTWiersz, którego klucz encji (maintainLatestFlagFor) i tsp ma już inny wiersz, w partii lub zapisany: nadaj każdemu wierszowi własny tsp
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTEDZapis, który zachowuje poprzednią wartość sekretu (__secret__) bez klucza encji, po którym można ją znaleźć; zapis wartości już zaszyfrowanej

Dołączenie, którego zaplecze danych odmawia bez podania przyczyny — wamp.error.runtime_error, jego odpowiedź na wiersz bez tsp — nie jest odmową z określonym typem: komunikat błędu SDK wskazuje na brakujący tsp oraz na tabelę error-logs aplikacji, w której zaplecze danych zapisuje przyczynę.

Użycie w przeglądarce (tylko JavaScript)

SDK JavaScript działa w nowoczesnych przeglądarkach. Ponieważ przeglądarki nie mają zmiennych środowiskowych, przekaż całą konfigurację przez 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 }]);

Użyj IronFlock.fromServer(), aby pobrać konfigurację z twojego zaplecza (backend) zamiast wpisywać dane uwierzytelniające na stałe:

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

Twój punkt końcowy zaplecza powinien zwrócić obiekt JSON z opcjami połączenia (serialNumber, deviceKey, appName, swarmKey, appKey, env).

Rejestrowanie funkcji agentów AI

SDK może rejestrować funkcje, które mogą być wywoływane przez agentów AI. Zarejestruj procedurę i odwołaj się do jej tematu (topic) w pliku 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)

Agent AI może następnie wywołać tę funkcję, gdy użytkownik zada pytanie wymagające bieżących danych z czujnika.

Aby połączyć zarejestrowany temat (topic) WAMP z agentem AI, odwołaj się do niego w pliku .ironflock/ai-template.yml swojej aplikacji:

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

Wartość topic (sensors.get_latest) musi pasować do nazwy przekazanej do register_device_function / registerDeviceFunction / RegisterDeviceFunction w twoim kodzie brzegowym. IronFlock automatycznie kieruje wywołanie do urządzenia, na którym zarejestrowana jest funkcja.

Pełną dokumentację pliku ai-template.yml znajdziesz w Definiowanie agentów i narzędzi.

Zmienne środowiskowe

Te zmienne są ustawiane automatycznie przez środowisko uruchomieniowe IronFlock wewnątrz kontenerów aplikacji:

ZmiennaOpis
DEVICE_NAMEWyświetlana nazwa urządzenia
DEVICE_SERIAL_NUMBERUnikalny, niezmienny identyfikator urządzenia
DEVICE_KEYKlucz urządzenia do uwierzytelniania
SWARM_KEYIdentyfikator projektu
APP_KEYIdentyfikator aplikacji
APP_NAMENazwa aplikacji
ENVŚrodowisko: DEV lub PROD
APP_AUTH_ID, APP_AUTH_SECRETWłasne poświadczenie aplikacji, które SDK przedstawia routerowi. Wymaga go dostęp między aplikacjami; bez niego (starszy agent urządzenia) SDK awaryjnie używa numeru seryjnego urządzenia
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")

Zmienne zdalnego dostępu (TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …) są wymienione w Konfigurowanie portów.

Agent urządzenia odzwierciedla też te wartości w /data/env, po jednym pliku <NAME>.txt na każdą, i utrzymuje tę kopię aktualną podczas działania aplikacji; SDK odczytują ją w pierwszej kolejności, więc wartości zmieniające się w czasie działania, takie jak port zdalnego dostępu, docierają do działającej aplikacji. Poświadczenie per aplikacja jest odczytywane ponownie przy każdej próbie połączenia, więc poświadczenie, które agent rotuje podczas działania aplikacji, zostaje przedstawione przy następnym ponownym połączeniu. Agent udostępnia /data/env do odczytu tylko użytkownikowi root: w kontenerze działającym jako inny użytkownik SDK widzi wartości, z którymi kontener wystartował, i jednorazowo zapisuje ostrzeżenie w logu.

Last updated on