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.
| SDK | Pakiet | Wymaga |
|---|---|---|
| Python | ironflock na PyPI | Python 3.8+ |
| JavaScript | ironflock na npm | Node.js 18+ lub nowoczesna przeglądarka |
| Go | github.com/RecordEvolution/ironflock-go | Go 1.27.2+ |
Instalacja
Python
pip install ironflockLub dodaj ironflock do requirements.txt swojej aplikacji.
Szybki start
Python
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
Python
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)
)| Parametr | Opis |
|---|---|
mainFunc | Funkcja asynchroniczna, która uruchamia się po nawiązaniu połączenia |
serial_number | Nadpisuje 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_window | Liczba 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.
Python
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
Nonejest wysyłany jako null i czyści kolumnę. Usuń klucz ze słownika, aby zachować starą wartość. - JavaScript: właściwość o wartości
undefinedwypada z ładunku, więc zachowuje starą wartość. Ustaw ją nanull, aby wyczyścić kolumnę. - Go: pole struktury z tagiem
omitemptylubomitzerojest pomijane, gdy jest puste, więc zachowuje starą wartość. W przeciwnym razie pole wskaźnikowenil, podobnie jak wartośćnilwironflock.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.
Python
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.
Python
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.
Python
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.
Python
# 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:
| Parametr | Typ | Opis |
|---|---|---|
error | str / string lub wyjątek / Error | Komunikat błędu lub wyjątek, którego ślad stosu (traceback/stack) lub komunikat zostaje zapisany |
level | str / string, opcjonalny | Poziom ważności: "error", "warn", "info" lub "debug". Domyślnie "error" |
append | bool / boolean, opcjonalny | Gdy true, używa zdalnego wywołania append (zwraca wynik wstawienia). Domyślnie false (publikacja typu fire-and-forget) |
tsp | str / string, opcjonalny | Nadpisanie znacznika czasu w formacie ISO-8601. Domyślnie bieżący czas |
user_message / userMessage | str / string, opcjonalny | Tekst 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})), gdzieerrOrMsgtoerrorlub łańcuch,Levelto jedna z wartościironflock.LevelError,LevelWarn,LevelInfo,LevelDebug, a zwracany*Resultma wartośćnil, chyba że ustawionoAppend.
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.
Python
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.
Python
# 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:
| Pole | Typ | Opis |
|---|---|---|
limit | int / number | Maksymalna liczba zwracanych wierszy (1–10 000, wymagane; 1–3 000 dla transformacji) |
offset | int / number | Przesunięcie dla stronicowania |
timeRange | list / 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ę |
filterAnd | list / array | Wpisy filtra łączone przez AND: warunki, grupy i/lub znacznik latest (patrz niżej) |
columns | list / array | Kolumny do zwrócenia (opcjonalne). tsp, device_key oraz authid są zawsze dołączane; pomiń, aby otrzymać wszystkie kolumny |
columnPaths | list / array | Przycina 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":
Python
# 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.
Python
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:
| Pole | Typ | Opis |
|---|---|---|
metrics | list / array | Agregaty 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 |
bucketMs | int / number | Szerokość przedziału w pełnych milisekundach, co najmniej 1000 (opcjonalne). Pomiń, aby podzielić zakres czasu na limit przedziałów |
limit | int / number | Budżet przedziałów (1–10 000) — nie limit wierszy |
timeRange | list / 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 |
groupBy | list / array | Kolumny, które dzielą każdy przedział na wiersz na grupę (opcjonalne) |
filterAnd | list / array | Warunki 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
metricsjako nazwy kolumn plusmethodna 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 (DeprecationWarningw 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 odczytujerow["temperature"]— chyba że wiersz ma już kolumnę o tej nazwie, jaktspdlaCOUNTztsp. Przejdź na metryki{ref, method}i odczytujrow["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.
Python
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.
Python
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).
Python
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 odczytu | Jak wygląda tajna kolumna |
|---|---|
getHistory, subscribeToTable / subscribe_to_table i wszystko inne przechodzące przez router komunikatów | symbol 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_secrets | odszyfrowana 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.
Python
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.
Python
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ź.
Python
ok = await ironflock.verify_secret(
"credentials", "api_token", submitted_token,
{"limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}]},
)
if ok:
...Parametry:
| Parametr | Typ | Opis |
|---|---|---|
tablename | str / string | Tabela, względem której następuje sprawdzenie |
column | str / string | Tajna kolumna do porównania |
candidate | str / string | Testowana wartość jawna. Porównywana po stronie serwera i nigdy nie zapisywana do tabeli |
query_params / queryParams | SecretQueryParams | Któ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.
Python
# 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:
| Parametr | Typ | Opis |
|---|---|---|
app_name / appName | str / string | Nazwa aplikacji dostawcy, zadeklarowana w twojej sekcji consumes: |
stage | str / string, opcjonalny | Etap (stage) dostawcy: "dev" lub "prod". Domyślnie etap twojej własnej aplikacji |
on_error / onError | callable, opcjonalny | Wywoł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
stageion_errorsą argumentami nazwanymi; w JavaScript są przekazywane przez obiekt opcji (connectToApp(appName, { stage, onError })); w Go przez strukturę (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), gdzieOnErrortofunc(*ironflock.CrossAppAccessError). W Goctxogranicza tylko oczekiwanie wywołującego: współbieżne wywołania dla tej samej aplikacji współdzielą jedną próbę, aClose(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.ymlswojej aplikacji i ujmij*w cudzysłów — samo*jest aliasem YAML i nie zostanie sparsowane:
consumes:
- app: "*"Python
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ę:
| Pole | Typ | Opis |
|---|---|---|
app | str / string | Nazwa aplikacji dostawcy |
provider_app_key | int / number | Klucz aplikacji dostawcy (app key) |
stages | dict / object | Katalog 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.
Python
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:
| Parametr | Typ | Opis |
|---|---|---|
stage | str / string, opcjonalny | Etap (stage) dostawcy: "dev" lub "prod". Domyślnie etap twojej własnej aplikacji |
continue_on_error / continueOnError | bool / boolean, opcjonalny | Gdy 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 / onError | callable, opcjonalny | Wywoł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_erroricontinue_on_errorsą argumentami nazwanymi; w JavaScript są przekazywane przez obiekt opcji (connectToAllApps({ stage, onError, continueOnError })); w Go przez strukturę (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), gdzieOnErrortofunc(error), aStopOnError: trueodpowiadacontinueOnError: 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:
Python
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
Python
# 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")| Metoda | Opis |
|---|---|
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
| Metoda | Opis |
|---|---|
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:
Python
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.
| Pole | Znaczenie |
|---|---|
size_bytes / sizeBytes | Aktualnie przechowywane bajty |
object_count / objectCount | Liczba przechowywanych obiektów |
quota_bytes / quotaBytes | Egzekwowany budżet. 0 oznacza brak limitu |
free_bytes / freeBytes | Pozostałe bajty. -1 oznacza brak limitu — zgłoszenie tu 0 czytałoby się jako „pełny” |
per_namespace / perNamespace | Bajty 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:
| Pole | Znaczenie |
|---|---|
quota_bytes / quotaBytes | To, co jest faktycznie egzekwowane, odczytane z magazynu obiektów — ustawienie użytkownika projektu |
suggested_quota_bytes / suggestedQuotaBytes | To, 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:
| Metoda | Czas życia | Kto może go odczytać |
|---|---|---|
url(key, …) | Trwały | Wyłą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 / ShareURL | Wygasają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 obiektu | Sposób transferu |
|---|---|
| Do limitu inline (obecnie 6 MiB) | Pojedyncze wywołanie przez router wiadomości |
| Powyżej | Bezpośrednio do magazynu obiektów przez HTTPS, z pominięciem routera |
| Powyżej, odczyt tam, gdzie nie ma ścieżki bezpośredniej | W 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.
Python
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| Kod | Znaczenie |
|---|---|
NOT_AUTHORIZED | Wywołujący nie może wykonać tej operacji |
NO_SUCH_NAMESPACE | Przestrzeń nazw nie jest zadeklarowana w szablonie danych |
NO_SUCH_OBJECT | Klucz nie istnieje |
TOO_LARGE | Przekracza limit transferu w pojedynczym wywołaniu |
OBJECT_TOO_LARGE | Przekracza własny maxObjectBytes przestrzeni nazw |
QUOTA_EXCEEDED | Magazyn plików jest pełny |
CONTENT_TYPE_NOT_ALLOWED | Przestrzeń nazw ogranicza contentTypes |
INVALID_KEY | Nieprawidłowy klucz lub prefiks (patrz niżej; FleetFiles v0.2 nadal zgłasza go jako INTERNAL) |
INVALID_RANGE | Zakres bajtów poza obiektem; SDK nigdy o taki nie proszą |
NOT_SUPPORTED | Zaplecze nie potrafi tego wykonać |
NOT_AVAILABLE | To 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_UNREACHABLE | Magazyn obiektów nie jest osiągalny bezpośrednio (proxy?) |
CLOCK_SKEW | Zegar urządzenia jest zbyt rozbieżny |
INTERNAL | Wszystko 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
FileStoreErrorjest importowany zironflock.filestore; w JavaScript jest eksportowany z katalogu głównego pakietu (import { FileStoreError } from "ironflock"); w Go jest to*filestore.Error(takżeironflock.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.
Python
def add(a, b):
return a + b
await ironflock.register_device_function("com.myapp.add", add)
register()jest aliasem dlaregister_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.
Python
result = await ironflock.call_device_function(
42, # target device key
"com.myapp.add", # procedure name
args=[3, 5] # arguments
)
print(result) # 8call
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.
Python
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 realmie | Dozwolone | Używane przez |
|---|---|---|
Twoje własne nazwy, np. com.myapp.status | publikowanie, subskrypcja (także subskrypcje prefiksowe), wywołanie — nigdy rejestracja | publish, subscribe, call |
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table> | publikowanie — nigdy subskrypcja | publishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table |
transformed.<table>, transformed.bulk.<table> | tylko subskrypcja | subscribeToTable / subscribe_to_table |
databackend.errors.…, files.events… | tylko subskrypcja | Zdarzenia błędów i plików zaplecza danych |
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name> | rejestracja (single), wywołanie, publikowanie — nigdy subskrypcja | registerDeviceFunction / 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.resolve | wywołanie | listConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps |
wamp.session.get | wywołanie | Heartbeat 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.
Python
await ironflock.set_device_location(long=8.6821, lat=50.1109)| Parametr | Zakres |
|---|---|
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.
Python
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>"| Parametr | Typ | Opis |
|---|---|---|
port | int / number | Port zadeklarowany w port-template.yml |
protocol | str / string, opcjonalny | Protokół 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 / remotePortEnvironment | str / string, opcjonalny | Tylko 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_DOMAINnalocalhost, 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 ustawiaCLOUD_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
Python
| Właściwość | Typ | Opis |
|---|---|---|
is_connected | bool | Czy połączenie z platformą jest aktywne |
connection | CrossbarConnection | Bazowa instancja połączenia (zastosowania zaawansowane) |
| Metoda | Opis |
|---|---|
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.
Python
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_appzwykle 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 (zobaczconnectToApp).
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.
Python
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 rodzajuif result is None:nie wykrywa już niepowodzeń — użyjtry/except(lubtry/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.
Python
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ła | Odrzucane |
|---|---|
URI_RATE_LIMITED | revealSecrets / 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_LARGE | Odczyt 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_LIMIT | Odczyt transformacji z limit powyżej 3000 |
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPS | Zapytania 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_COLUMN | Filtr na tajnej kolumnie; verifySecret / verify_secret na kolumnie, która nie jest tajna, lub revealSecrets / reveal_secrets tabeli bez tajnych kolumn |
URI_STORAGE_FULL | Dołą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_OVERUSAGE | Dołączanie ponad przydział magazynu konta; szybkie ponowienie nie pomoże |
URI_ENTITY_KEY_CONFLICT | Wiersz, 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_REJECTED | Zapis, 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:
Python
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: trueWartość 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:
| Zmienna | Opis |
|---|---|
DEVICE_NAME | Wyświetlana nazwa urządzenia |
DEVICE_SERIAL_NUMBER | Unikalny, niezmienny identyfikator urządzenia |
DEVICE_KEY | Klucz urządzenia do uwierzytelniania |
SWARM_KEY | Identyfikator projektu |
APP_KEY | Identyfikator aplikacji |
APP_NAME | Nazwa aplikacji |
ENV | Środowisko: DEV lub PROD |
APP_AUTH_ID, APP_AUTH_SECRET | Wł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 |
Python
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.