Wzorzec aplikacji
Ta strona pokazuje, jak zbudować aplikację IronFlock, aby korzystała z platformy zgodnie z jej założeniami. Idea w jednym zdaniu: twoje kontenery działają na każdym urządzeniu i przechowują cały stan aplikacji w jej tabelach, panel jest interfejsem użytkownika aplikacji, a te dwie części nigdy nie komunikują się ze sobą bezpośrednio. Interfejs webowy serwowany przez samo urządzenie dociera do użytkowników przez tunel dostępu zdalnego i pojawia się wewnątrz panelu.
Poszczególne elementy są szczegółowo opisane na osobnych stronach. Ta strona pokazuje, jak łączą się w całość.
Części aplikacji
Aplikacja działa jednocześnie na wielu urządzeniach, zwykle na komputerach przemysłowych stojących przy maszynach. Składa się z następujących części:
| Część | Czym jest | Definiowana w |
|---|---|---|
| Kod brzegowy | Kontener lub stos Docker Compose na każdym urządzeniu, na którym zainstalowano aplikację | Dockerfile lub docker-compose.yml |
| SDK | Jedyne połączenie między twoim kodem a platformą: tabele, aktualizacje w czasie rzeczywistym, funkcje urządzenia, pliki | SDK IronFlock (Python, JavaScript, Go) |
| Tabele | Stan aplikacji, we własnej bazie danych projektu | data-template.yml |
| Panel | Centralny interfejs użytkownika aplikacji, powiązany na żywo z tabelami, wspólny dla wszystkich urządzeń | board-template.yml |
| Parametry | Ustawienia jednego urządzenia lub grupy urządzeń | env-template.yml |
| Dostęp zdalny | Tunele do portów, na których twoje kontenery serwują np. interfejs webowy | port-template.yml |
| Agenty AI | Agenty dla asystenta IronFlock, które używają twoich funkcji urządzenia jako narzędzi | ai-template.yml |
Gdy użytkownik instaluje twoją aplikację w projekcie, IronFlock tworzy tabele aplikacji w bazie danych tego projektu, instaluje jej panel i otwiera prywatny obszar komunikatów (realm), który w czasie rzeczywistym łączy kontenery aplikacji na wszystkich urządzeniach, jej tabele i jej panel. SDK dołącza do tego realmu z tożsamością wstrzykiwaną przez agenta urządzenia, więc twój kod nie potrzebuje ani adresu, ani danych uwierzytelniających.
Zasady
- Tabele są stanem aplikacji. Wszystko, czego potrzebuje panel, inne urządzenie, zrestartowany kontener lub agent AI, jest przechowywane w tabeli. Kontener trzyma tylko to, co może odtworzyć z tabel, np. kopię swoich ustawień w pamięci.
- Części komunikują się przez tabele, a nie bezpośrednio ze sobą. Urządzenie zapisuje to, co mierzy, i stan, w którym się znajduje, a panel to pokazuje. Panel zapisuje decyzje użytkowników, a urządzenia je stosują. Żadna ze stron nie wywołuje drugiej, więc panel działa, gdy urządzenia są offline, a urządzenia pracują dalej, gdy nikt nie ma otwartego panelu.
- Jedno źródło zapisów na tabelę. Do każdej tabeli zapisują albo urządzenia (każde własne dane), albo panel, albo jedna usługa. Wskaż, które z nich, w
descriptiontabeli. Dwa źródła zapisujące te same wiersze nadpisują sobie nawzajem zmiany. - Pożądany stan, a nie polecenia. Aby zmienić to, co robią urządzenia, zapisz w tabeli stan docelowy (limit, wartość zadaną, recepturę, harmonogram) i pozwól każdemu urządzeniu zastosować najnowszy wiersz. Urządzenie, które było offline, zastosuje go po powrocie, a dwukrotne zastosowanie niczego nie zmienia.
- Zapisuj przy zmianie, śledź na żywo, nigdy nie odpytuj. Zapisuj wiersz encji, gdy zmieni się jedna z jej wartości, a pomiary — z ich częstotliwością próbkowania. Wszystko, co odczytuje dane, śledzi tabele w czasie rzeczywistym: powiązania danych panelu, subskrypcje tabel na urządzeniach, połączenia WebSocket w interfejsie webowym urządzenia. Timer, który cyklicznie odczytuje tabelę od nowa, zawsze oznacza błędny projekt.
- Panel jest interfejsem użytkownika; interfejs webowy urządzenia go uzupełnia. Serwuj stronę webową z urządzenia tylko dla tego, czego panel nie potrafi pokazać: własnego HMI maszyny, wideo, widoku na żywo o wysokiej częstotliwości, ekranu operatora przy maszynie. Panel ją osadza.
Gdzie trafia każdy rodzaj danych
| Dane | Gdzie | Zapisywane przez | Odczytywane przez |
|---|---|---|---|
| Odczyty w czasie | Tabela z tsp | publishToTable, publishRowsToTable | Wykresy z oknami czasowymi i agregacją |
| Aktualny stan urządzenia, maszyny lub zlecenia | Tabela encji (maintainLatestFlagFor) | appendToTable: wiersz z kluczem, tsp i zmienionymi kolumnami | Filtr latest |
| Limity, wartości zadane, receptury ustawiane przez użytkowników | Tabela zapisywana przez formularz panelu | Formularz | Każde urządzenie, jak pokazano poniżej |
| Rekordy, które użytkownicy tworzą, edytują i usuwają | Tabela encji z kolumną deleted | Formularz, który przy usuwaniu ustawia flagę | Listy i strony edycji na panelu |
| Błędy i komunikaty dla operatorów | Tabela error-logs | reportError z userMessage | Widget powiadomień (toast) na panelu |
| Obrazy, dokumenty, nagrania | Magazyn plików aplikacji | files.put | Adres URL pliku w kolumnie, wyświetlany przez widget |
| Hasła i klucze API | Kolumna secret: true | Dowolny zapis do tabeli | revealSecrets, verifySecret, wyłącznie we własnych kontenerach aplikacji |
| Instalacja jednego urządzenia: adres maszyny, port szeregowy | Parametry aplikacji | Ustawienia aplikacji dla urządzenia lub grupy | Zmienne środowiskowe |
| Komunikaty między częściami aplikacji, potrzebne tylko w chwili dostarczenia | Własne tematy (topics) | publish | subscribe |
| Żądanie, które wymaga natychmiastowej odpowiedzi jednego urządzenia | Funkcja urządzenia | registerDeviceFunction | callDeviceFunction, akcje widgetów, narzędzia AI |
Dołączać czy publikować? appendToTable czeka na restartujące się zaplecze danych i informuje cię, gdy odrzuci ono wiersz. publishToTable czeka tylko na połączenie, więc wiersz opublikowany podczas restartu zaplecza danych przepada. Zmiany stanu i rekordy dołączaj, a odczyty o wysokiej częstotliwości publikuj.
Parametry czy tabela ustawień? Parametry należą do urządzenia lub grupy urządzeń: wprowadza je w ustawieniach aplikacji ten, kto instaluje lub konfiguruje urządzenie, a twój kod odczytuje je ze swojego środowiska. Używaj ich do tego, co różni się między instalacjami. To, co użytkownicy zmieniają w trakcie działania aplikacji — limity, receptury, harmonogramy, wszystko, co dotyczy wielu urządzeń — należy umieścić w tabeli zapisywanej przez panel. To w panelu użytkownicy pracują z aplikacją, każde urządzenie od razu uwzględnia zmianę, a tabela przechowuje historię tego, kto, co i kiedy zmienił.
Śledzenie tego, co zapisuje panel
Urządzenie stosuje stan zapisany przez panel, np. limit ustawiony na panelu, w trzech krokach:
- Zasubskrybuj tabelę, zanim ją odczytasz, aby żaden wiersz nie przepadł między odczytem a subskrypcją.
- Odczytaj najnowszy wiersz raz przy starcie, z filtrem
latest, i zastosuj go. - Stosuj wiersz tylko wtedy, gdy jest nowszy (
tsp) niż obowiązujący. Wiersz zapisany akurat w chwili startu programu może przyjść zarówno z odczytu, jak i z subskrypcji.
Python
await ironflock.subscribe_to_table("settings", lambda row, *args, **kwargs: apply(row))
rows = await ironflock.getHistory("settings", {"limit": 1, "filterAnd": [{"latest": True}]})
for row in rows or []:
apply(row)Nic więcej nie trzeba, także w przypadku urządzeń, które bywają offline. Połączeniem zajmuje się SDK, a nie twój kod: łączy się ponownie, odtwarza twoje subskrypcje i funkcje urządzenia, a następnie dostarcza do każdej subskrypcji tabeli wiersze zapisane w czasie, gdy urządzenie było offline, od najstarszego, przed wierszami przychodzącymi na żywo. Limit zapisany, gdy urządzenie było offline, dociera do niego zaraz po jego powrocie. Nadrabianie zaległości opiera się na tsp: dostarczane są wiersze nowsze od najnowszego, który otrzymał twój handler, najwyżej 10 000 najnowszych. Odczyt wykonywany w ramach nadrabiania zaległości czeka na zaplecze danych jak każdy odczyt historii, w granicach okna ponownego połączenia; gdy się nie powiedzie, te wiersze nie zostają dostarczone, a w logu zapisywane jest ostrzeżenie. Nadrabianie zaległości jest częścią SDK od wersji 1.10.0.
Polecenia, funkcje urządzenia i tematy
Preferuj stan. Większość tego, co użytkownicy „wydają jako polecenia”, to w rzeczywistości pożądany stan: wartość zadana, stan włączony lub wyłączony, receptura. Zapisuj to w tabeli, jak powyżej.
Polecenia, które mają zostać wykonane tylko raz, np. wydrukowanie etykiety lub uruchomienie cyklu czyszczenia, to wiersze z identyfikatorem w tabeli zapisywanej przez panel. Urządzenie zapisuje identyfikatory wykonanych poleceń we własnej tabeli, więc polecenie otrzymane dwukrotnie wykonuje się tylko raz.
Funkcje urządzenia to procedury, które urządzenie rejestruje za pomocą registerDeviceFunction. Używaj ich, gdy wywołujący potrzebuje odpowiedzi urządzenia od razu:
- Agenty AI: narzędzie w
ai-template.ymlwywołuje funkcję na urządzeniu wybranym przez asystenta. Zobacz Definiowanie agentów i narzędzi. - Widgety Przełącznik i SCADA na panelu wywołują je, gdy urządzenie jest online.
- Twoje własne kontenery i urządzenia wywołują się nawzajem za pomocą
callDeviceFunction.
Zwracaj z funkcji urządzenia czytelny tekst lub niewielki JSON, a spodziewane niepowodzenie (wyłączoną maszynę) zwracaj jako wynik, zamiast zgłaszać błąd.
Tematy przenoszą między częściami twojej aplikacji komunikaty potrzebne tylko w chwili dostarczenia: publish i subscribe na nazwach we własnej przestrzeni nazw, np. myapp.events. Panel nie może subskrybować tematów: wszystko, co pokazuje, musi znajdować się w tabeli. Komunikaty opublikowane, gdy urządzenie jest offline, nie są przechowywane, więc do wszystkiego, co musi dotrzeć, używaj tabeli.
Kontener na urządzeniu
Co agent urządzenia daje twojemu kontenerowi i czego od niego oczekuje:
- Tożsamość: agent ustawia
DEVICE_KEY,DEVICE_NAME,DEVICE_SERIAL_NUMBER,SWARM_KEY,APP_KEY,APP_NAME,ENV(DEVlubPROD),TUNNEL_DOMAIN,DEVICE_LAN_IPiDEVICE_PORT_FOR_<port>, a do tego własne poświadczenie aplikacji dla SDK. SDK odczytuje to, czego potrzebuje; nigdy nie wpisuj żadnej z tych wartości na sztywno w kodzie. - Parametry: instalacja produkcyjna otrzymuje wartości ustawione przez użytkowników, a dla pozostałych parametrów ich
defaultValue. Uruchomienie deweloperskie otrzymuje wyłącznie wartości domyślne. Nadawaj parametrom konkretne nazwy (PLC_HOST, a nieHOST). - Przechowywanie danych: aplikacja z pojedynczym obrazem otrzymuje katalog
/data, prywatny dla aplikacji i zachowywany między restartami i aktualizacjami (usuwany razem z aplikacją), oraz/shared, współdzielony przez wszystkie aplikacje na urządzeniu. Mimo to przechowuj stan aplikacji w tabelach, a tych katalogów używaj do plików należących do urządzenia, takich jak pobrany model lub pamięć podręczna. - Sprzęt: kontener z pojedynczym obrazem działa w trybie uprzywilejowanym, z dostępem do
/devurządzenia. - Logi: wszystko, co twój program wypisuje na stdout i stderr, jest logiem aplikacji w IronFlock. Wypisuj bez buforowania (
python -u). - Start: 30 sekund po uruchomieniu kontener musi nadal działać, inaczej start uznaje się za nieudany.
- Awarie: gdy aplikacja produkcyjna zakończy działanie, agent uruchamia ją ponownie, czekając dłużej po każdym niepowodzeniu. Uruchomienie deweloperskie nie jest restartowane. Spodziewane błędy, np. czujnik, który nie odpowiada, obsługuj w programie, a przy błędach, po których nie da się wrócić do normalnej pracy, pozwól mu się zakończyć.
- Zatrzymanie: twój program jest głównym procesem kontenera (PID 1), który ignoruje sygnał zatrzymania SIGTERM, chyba że go obsługuje. Niech program kończy działanie po otrzymaniu SIGTERM, inaczej zatrzymanie aplikacji będzie trwało, dopóki Docker nie zabije procesu. Uruchamiaj program w formie exec (
CMD ["python", "-u", "main.py"]), a nie przez powłokę. - Uruchomienia deweloperskie: uruchomienie aplikacji ze środowiska IDE w chmurze to uruchomienie deweloperskie (
ENVma wartośćDEV). Zapisuje ono do deweloperskich tabel aplikacji, otrzymuje wartości domyślne parametrów i nie dostaje żadnych tuneli dostępu zdalnego.
Docker Compose
Użyj pliku docker-compose.yml, gdy twoja aplikacja uruchamia kilka procesów albo dołącza obok twojego kodu usługi firm trzecich, takie jak broker MQTT lub baza danych. Podstawy opisano na stronie Docker i Compose. W porównaniu z pojedynczym obrazem:
- Każda usługa otrzymuje wstrzyknięte środowisko. Agent daje każdej usłudze własny plik środowiskowy ze zmiennymi wymienionymi powyżej; napisany przez ciebie
env_file:zostaje zastąpiony. Wyrażenie${VAR}w pliku jest rozwijane na podstawie wstrzykniętych wartości. - Brak
/data. Usługi nie dostają katalogu aplikacji. Przechowuj pliki w nazwanych wolumenach: przetrwają zatrzymania i aktualizacje, a są kasowane, gdy aplikacja zostanie usunięta z urządzenia. - Opublikowane porty są przemapowywane. Każdy opublikowany port dostaje wolny port hosta na wszystkich interfejsach urządzenia, a
DEVICE_PORT_FOR_<port>podaje twojemu kodowi rzeczywisty port. Wport-template.ymlużywaj portu hosta w takiej postaci, w jakiej zapisano go w pliku compose. Publikuj tylko to, do czego użytkownicy mają mieć dostęp, a usługi niech komunikują się ze sobą przez sieć compose po nazwie usługi. Port hosta zapisany jako${VAR}nie jest zarządzany: nie jest przemapowywany i nie dostaje tunelu. - Nic nie jest dodawane. Agent nie przyznaje usługom compose żadnych uprawnień ani urządzeń. Zadeklaruj
privileged,deviceslubdevice_cgroup_rulessamodzielnie. Wpisdevices:wskazuje sprzęt, który musi istnieć, inaczej kontener się nie uruchomi;device_cgroup_rulesrazem z wolumenem/devdziała na każdym urządzeniu. - Bez
profiles:. Usługa z profilami jest pomijana w wydaniu. - Ta sama reguła startu. W ciągu 30 sekund każdy kontener musi działać lub zakończyć się z kodem 0, a co najmniej jeden musi działać. Długo działającym usługom nadaj
restart: unless-stopped.
Interfejsy webowe na urządzeniu
Serwer webowy w twoim kontenerze staje się osiągalny z dowolnego miejsca przez tunel dostępu zdalnego swojego portu, a panel go wyświetla:
- Serwuj na stałym porcie (nasłuchując na
0.0.0.0) i zadeklaruj go wport-template.ymlzprotocol: http. Instalacja produkcyjna włącza tunel i serwuje go przez HTTPS, łącznie z połączeniami WebSocket. - Zapisz adres URL tunelu w tabeli. Metoda SDK
getRemoteAccessUrlForPort(port)zwraca ten adres. Przy starcie zapisz go w wierszu urządzenia w tabeli urządzeń. Tunele mają tylko instalacje produkcyjne, więc uruchomienie deweloperskie nie zapisuje adresu URL. - Osadź go w panelu. Powiąż adres URL widgetu Osadź z tą kolumną, na stronie przeznaczonej dla jednego urządzenia, która filtruje dane według urządzenia. W UI Builder jest to powiązanie danych jak każde inne.
Co do samej strony:
- Kto może ją otworzyć: tunel http jest prywatny. Mogą go otworzyć tylko osoby zalogowane do IronFlock, które mają dostęp do urządzenia, chyba że użytkownik przełączy port na publiczny. W sieci samego urządzenia strona jest otwarta dla wszystkich, więc nie pokazuj tam niczego, czego nie powinna widzieć sieć zakładu, a dla wszystkiego, co zmienia stan maszyny, wymagaj własnego logowania.
- Względne adresy URL: ładuj zasoby, wywołuj swoje API i otwieraj połączenia WebSocket względem strony, aby działała w sieci urządzenia, przez tunel i za reverse proxy.
- Wypychaj, nigdy nie odpytuj: wysyłaj aktualizacje przez WebSocket.
- Żadnych poświadczeń platformy w przeglądarce: strona komunikuje się z własnym serwerem w kontenerze, a to ten serwer utrzymuje połączenie SDK.
Aby zamiast własnej strony pokazać webowy HMI maszyny z sieci zakładu, udostępnij go przez reverse proxy ze swojego kontenera. Zobacz Osadzanie zewnętrznych interfejsów HMI.
Wiele urządzeń
- Każdy wiersz wskazuje swoje urządzenie: IronFlock zapisuje w
device_keyklucz urządzenia, które zapisało wiersz. Jako klucza tabel per urządzenie używajdevice_key, nazwę urządzenia zapisuj w kolumnie zpath: kwargs.DEVICE_NAMEi dziel wykresy według tej nazwy. - Lista urządzeń i strona dla każdego urządzenia: nawigacja boczna zbudowana z tabeli urządzeń oraz jedna strona, która działa dla każdego urządzenia i filtruje swoje widgety według urządzenia wskazanego w trasie. Zobacz Budowanie paneli.
- Ustawienia per urządzenie lub per maszyna: jako klucza tabeli ustawień użyj kolumny, która wskazuje cel (wiersze zapisywane przez panel nie mają własnego urządzenia), i wypełniaj ją w formularzu na stronie celu. Każde urządzenie stosuje najnowszy wiersz dla własnego klucza.
- Usunięte urządzenie zachowuje swoje wiersze. Jeśli użytkownicy mają móc usuwać je z list, dodaj do tabeli urządzeń kolumnę
deleted, a do panelu formularz, który ją ustawia.
Lista kontrolna
- Cały stan znajduje się w tabelach, z których każda ma jedno źródło zapisów wskazane w swoim opisie, a każda kolumna zapisywana przez kod jest zadeklarowana w
data-template.yml. - Każdy wiersz ma
tsp, a wiersze stanu są zapisywane przy zmianie, tylko z kluczem i zmienionymi kolumnami. - Stan zapisywany przez panel jest śledzony w trzech opisanych wyżej krokach, z SDK 1.10.0 lub nowszym, a kod nigdy nie zajmuje się połączeniem ani jego sesjami.
- Zapisy do tabel w długo działających pętlach obsługują błąd zgłaszany przez zapis, gdy platforma jest nieosiągalna od minuty, a spodziewane niepowodzenia są zgłaszane przez
reportErrorzuserMessage. - Program kończy działanie po otrzymaniu SIGTERM, startuje w formie exec i loguje bez buforowania.
- Nic nie odpytuje: panel wiąże dane, urządzenia subskrybują, interfejsy webowe wypychają aktualizacje.
- Każdy port serwowany przez twoją aplikację jest zadeklarowany w
port-template.yml, jego adres URL pochodzi zgetRemoteAccessUrlForPorti jest zapisywany w tabeli, a panel osadza go stamtąd. - Aplikacja compose nie ma
env_file:,profiles:ani portów hosta${VAR}, przechowuje pliki w nazwanych wolumenach i publikuje tylko porty potrzebne użytkownikom.
Zacznij od aplikacji startowej
Wtyczka IronFlock dla Claude Code zawiera aplikację startową zgodną z tym wzorcem: program, który na każdym urządzeniu odczytuje dane z maszyny, przechowuje stan i odczyty każdego urządzenia w tabelach, stosuje limit ustawiony przez użytkowników na panelu i serwuje widok na żywo, który panel osadza na stronie każdego urządzenia. Claude Code tworzy na jej podstawie nową aplikację i dostosowuje ją do twojego przypadku użycia.