アプリの設計図
このページでは、プラットフォームを本来の想定どおりに活用するIronFlockアプリの作り方を説明します。考え方をひと言でいえば、コンテナはすべてのデバイスで動作してアプリの状態をすべてアプリのテーブルに保持し、ボードはアプリのユーザーインターフェースであり、両者が直接やり取りすることは決してない、ということです。デバイス自身が提供するWeb UIは、リモートアクセストンネルを通じてユーザーに届き、ボードの中に表示されます。
個々の要素については、それぞれのページで詳しく説明しています。このページでは、それらがどのように組み合わさるかを示します。
アプリの構成要素
アプリは多数のデバイス上で同時に動作します。これらのデバイスは、多くの場合、機械のそばに設置された産業用PCです。アプリは次の要素で構成されます:
| 要素 | 内容 | 定義場所 |
|---|---|---|
| エッジコード | アプリがインストールされた各デバイス上で動作するコンテナ、またはDocker Composeスタック | Dockerfileまたはdocker-compose.yml |
| SDK | コードとプラットフォームをつなぐ唯一の接点:テーブル、リアルタイム更新、デバイス関数、ファイル | IronFlock SDK(Python、JavaScript、Go) |
| テーブル | プロジェクト独自のデータベースに保持される、アプリの状態 | data-template.yml |
| ボード | テーブルにライブでバインドされた、全デバイス共通のアプリの中心的なユーザーインターフェース | board-template.yml |
| パラメータ | 1台のデバイスまたはデバイスグループの設定 | env-template.yml |
| リモートアクセス | コンテナが提供するポート(Web UIなど)へのトンネル | port-template.yml |
| AIエージェント | デバイス関数をツールとして使う、IronFlockアシスタント向けのエージェント | ai-template.yml |
ユーザーがプロジェクトにアプリをインストールすると、IronFlockはそのプロジェクトのデータベースにアプリのテーブルを作成し、ボードをインストールして、全デバイス上のアプリのコンテナ、テーブル、ボードをリアルタイムに結ぶプライベートメッセージレルムを開きます。SDKはデバイスエージェントが注入する識別情報を使ってこのレルムに参加するため、コード側でアドレスや認証情報を用意する必要はありません。
ルール
- テーブルがアプリの状態です。ボード、他のデバイス、再起動したコンテナ、AIエージェントが必要とするものは、すべてテーブルに保存します。コンテナが保持するのは、設定のメモリ上のコピーのように、テーブルから再構築できるものだけです。
- 各部分は互いに直接ではなく、テーブルを介してやり取りします。デバイスは測定した値と自身の状態を書き込み、ボードはそれを表示します。ボードはユーザーが決めたことを書き込み、デバイスはそれを適用します。どちらも相手を呼び出さないため、デバイスがオフラインでもボードは動作し、誰もボードを開いていなくてもデバイスは動作し続けます。
- 書き込み元はテーブルごとに1つです。各テーブルに書き込むのは、デバイス(各デバイスが自身について)、ボード、1つのサービスのいずれかです。どれが書き込むかは、テーブルの
descriptionに記載してください。同じ行に2つの書き込み元があると、互いの変更を上書きしてしまいます。 - コマンドではなく、望ましい状態を書き込みます。デバイスの動作を変えるには、目標(閾値、設定値、レシピ、スケジュール)をテーブルに書き込み、各デバイスに最新の行を適用させます。オフラインだったデバイスは復帰時にそれを適用し、2回適用しても何も変わりません。
- 変更時に書き込み、ライブで追従し、ポーリングは決してしません。エンティティの行は値のいずれかが変化したときに、測定値はそのサンプリングレートで書き込みます。読み取る側はすべて、テーブルにリアルタイムで追従します。具体的には、ボードのバインディング、デバイス上のテーブル購読、デバイスのWeb UI内のWebSocketです。テーブルを読み直すタイマーは、どんな場合でも誤った設計です。
- UIはボードであり、デバイスのWeb UIはそれを補うものです。デバイスからWebページを提供するのは、ボードでは表示できないもの、つまり機械固有のHMI、動画、高頻度のライブビュー、機械のそばのオペレーター画面に限ります。そのページはボードに埋め込みます。
データの種類ごとの置き場所
| データ | 置き場所 | 書き込み元 | 読み取り側 |
|---|---|---|---|
| 時系列の測定値 | tspを持つテーブル | publishToTable、publishRowsToTable | 時間ウィンドウと集計を使うチャート |
| デバイス、機械、生産オーダーの現在の状態 | エンティティテーブル(maintainLatestFlagFor) | appendToTable:キー、tsp、変更されたカラムを含む行 | latestフィルター |
| ユーザーが設定する閾値、設定値、レシピ | ボードのフォームが書き込むテーブル | フォーム | 後述のとおり、すべてのデバイス |
| ユーザーが作成・編集・削除するレコード | deletedカラムを持つエンティティテーブル | 削除時にフラグを立てるフォーム | ボード上の一覧ページと編集ページ |
| オペレーター向けのエラーと通知 | error-logsテーブル | userMessageを指定したreportError | ボードのトーストウィジェット |
| 画像、ドキュメント、録画・録音 | アプリのファイルストレージ | files.put | カラムに保存したファイルのURL(ウィジェットで表示) |
| パスワードとAPIキー | secret: trueのカラム | 任意のテーブル書き込み | revealSecrets、verifySecret(アプリ自身のコンテナでのみ) |
| 1台のデバイスの設置情報:機械のアドレス、シリアルポート | アプリパラメータ | デバイスまたはグループのアプリ設定 | 環境変数 |
| 後で誰も必要としない、アプリの各部分間のメッセージ | 独自のトピック | publish | subscribe |
| 特定のデバイスから今すぐ応答が必要なリクエスト | デバイス関数 | registerDeviceFunction | callDeviceFunction、ウィジェットのアクション、AIツール |
追加と公開の使い分け。appendToTableは再起動中のデータバックエンドを待ち、データバックエンドが行を拒否した場合はそれを知らせます。publishToTableは接続しか待たないため、データバックエンドの再起動中に公開された行は失われます。状態の変化とレコードには追加を、高頻度の測定値には公開を使います。
パラメータと設定テーブルの使い分け。パラメータはデバイスまたはデバイスグループに属します。デバイスを設置・設定する人がアプリ設定で値を指定し、コードはそれを環境変数から読み取ります。パラメータは、設置ごとに異なるものに使います。アプリの稼働中にユーザーが変更するもの — 閾値、レシピ、スケジュールなど、多くのデバイスに適用されるもの — は、ボードが書き込むテーブルに置きます。ユーザーがアプリを操作する場所はボードであり、変更はすべてのデバイスにすぐ反映され、誰がいつ何を変更したかの履歴がテーブルに残ります。
ボードが書き込む内容への追従
デバイスは、ボードで設定された閾値など、ボードが書き込んだ状態を次の3ステップで適用します:
- テーブルを読み取る前に、まずテーブルを購読します。こうすることで、読み取りと購読の間に届いた行を取りこぼしません。
- 起動時に一度、
latestフィルターを使って最新の行を読み取り、適用します。 - 適用中の行より(
tspで比較して)新しい場合にのみ、行を適用します。プログラムの起動とちょうど同時に保存された行は、読み取りと購読の両方から届くことがあります。
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)これだけで、オフラインになるデバイスにも対応できます。接続の管理はSDKが担うため、コードで扱う必要はありません。SDKは再接続し、購読とデバイス関数を復元したうえで、デバイスがオフラインの間に保存された行を、ライブで届く行より先に、古い順に各テーブル購読へ配信します。デバイスがオフラインの間に保存された閾値は、デバイスが復帰するとすぐに届きます。この取りこぼし分の配信(キャッチアップ)はtspに基づき、ハンドラが受け取った最新の行より新しい行のうち、最新の最大10,000行を配信します。キャッチアップの読み取りは、ほかの履歴の読み取りと同様に、最大で再接続ウィンドウの間データバックエンドを待ちます。読み取りに失敗した場合、それらの行は配信されず、警告がログに記録されます。キャッチアップはSDKのバージョン1.10.0から含まれています。
コマンド、デバイス関数、トピック
状態を優先します。ユーザーが「コマンド」として出すものの多くは、実際には望ましい状態です。たとえば設定値、オンかオフか、レシピなどです。これらは前述のとおりテーブルに書き込みます。
ラベルの印刷や洗浄サイクルの開始のように一度だけ実行する必要があるコマンドは、ボードが書き込むテーブル内の、IDを持つ行として表します。デバイスは実行済みのIDを自身のテーブルに記録するため、同じコマンドを2回受け取っても実行されるのは1回だけです。
デバイス関数は、デバイスがregisterDeviceFunctionで登録するプロシージャです。呼び出し側がデバイスの応答をすぐに必要とする場合に使います:
- AIエージェント:
ai-template.ymlのツールが、アシスタントの選んだデバイス上で関数を呼び出します。AIエージェント & ツールを参照してください。 - ボード上のスイッチウィジェットとSCADAウィジェットは、デバイスがオンラインの間、デバイス関数を呼び出します。
- 独自のコンテナやデバイスは、
callDeviceFunctionで互いを呼び出します。
デバイス関数からは、読みやすいテキストか小さなJSONを返してください。想定内の失敗(電源が切れている機械など)は、エラーを送出するのではなく結果として返します。
トピックは、後で誰も必要としない、アプリの各部分間のメッセージを運びます。myapp.eventsのような独自の名前空間内の名前に対して、publishとsubscribeを使います。ボードはトピックを購読できないため、ボードに表示するものはすべてテーブルに置く必要があります。デバイスがオフラインの間に公開されたメッセージは保存されないため、確実に届ける必要があるものにはテーブルを使ってください。
デバイス上のコンテナ
デバイスエージェントがコンテナに提供するものと、コンテナに求めるものは次のとおりです:
- 識別情報:エージェントは
DEVICE_KEY、DEVICE_NAME、DEVICE_SERIAL_NUMBER、SWARM_KEY、APP_KEY、APP_NAME、ENV(DEVまたはPROD)、TUNNEL_DOMAIN、DEVICE_LAN_IP、DEVICE_PORT_FOR_<port>に加えて、SDK用のアプリ自身の認証情報を設定します。必要な値はSDKが読み取ります。これらの値はいずれもハードコードしないでください。 - パラメータ:本番インストールでは、ユーザーが設定した値を受け取り、それ以外のパラメータについては各パラメータの
defaultValueを受け取ります。開発モードでの実行では、デフォルト値のみを受け取ります。パラメータには具体的な名前を付けてください(HOSTではなくPLC_HOST)。 - ストレージ:単一イメージのアプリには、アプリ専用で再起動や更新後も保持される
/data(アプリとともに削除されます)と、デバイス上のすべてのアプリで共有される/sharedが用意されます。それでもアプリの状態はテーブルに保持し、これらのフォルダは、ダウンロードしたモデルやキャッシュなど、デバイスに属するファイルに使ってください。 - ハードウェア:単一イメージのコンテナは特権モードで実行され、デバイスの
/devを利用できます。 - ログ:プログラムがstdoutとstderrに出力するものはすべて、IronFlockにおけるアプリのログになります。バッファリングせずに出力してください(
python -u)。 - 起動:コンテナは起動から30秒後に実行中でなければなりません。そうでない場合、起動は失敗とみなされます。
- クラッシュ:本番アプリが終了すると、エージェントはそれを再起動し、失敗のたびに待ち時間を延ばします。開発モードでの実行は再起動されません。応答しないセンサーのような想定内の失敗はプログラム内で処理し、回復できないエラーではプログラムを終了させてください。
- 停止:プログラムはコンテナのメインプロセス(PID 1)であり、PID 1は停止シグナルSIGTERMを自分で処理しない限り無視します。SIGTERMを受け取ったら終了するようにしてください。そうしないと、アプリの停止はDockerがプロセスを強制終了するまで待たされます。プログラムはシェル経由ではなく、exec形式(
CMD ["python", "-u", "main.py"])で起動してください。 - 開発モードでの実行:クラウドIDEからアプリを実行すると、開発モードでの実行になります(
ENVはDEV)。この場合、アプリは開発用テーブルに書き込み、パラメータのデフォルト値を受け取ります。リモートアクセストンネルは使用できません。
Docker Compose
アプリが複数のプロセスを実行する場合や、MQTTブローカーやデータベースなどのサードパーティサービスを独自のコードと一緒に同梱する場合は、docker-compose.ymlを使います。基本についてはDockerとComposeを参照してください。単一イメージとの違いは次のとおりです:
- すべてのサービスに、注入された環境が渡されます。エージェントは各サービスに、上記の変数を含む専用の環境ファイルを渡します。自分で記述した
env_file:は置き換えられます。ファイル内の${VAR}は、注入された値で解決されます。 /dataはありません。サービスにはアプリフォルダが用意されません。ファイルは名前付きボリュームに保存してください。名前付きボリュームは停止や更新後も保持され、アプリがデバイスから削除されると削除されます。- 公開ポートは再マッピングされます。公開された各ポートには、デバイスのすべてのインターフェース上で空いているホストポートが割り当てられ、実際のポートは
DEVICE_PORT_FOR_<port>でコードに伝えられます。port-template.ymlでは、composeファイルに記述したとおりのホストポートを使います。ユーザーがアクセスすべきポートだけを公開し、サービス同士はcomposeネットワーク上でサービス名を使って通信させてください。${VAR}と記述したホストポートは管理対象外となり、再マッピングもトンネルも行われません。 - 何も追加されません。エージェントはcomposeサービスに特権もデバイスも付与しません。
privileged、devices、device_cgroup_rulesは自分で宣言してください。devices:のエントリは存在が必須のハードウェアを指定するもので、そのハードウェアがなければコンテナは起動しません。/devボリュームと組み合わせたdevice_cgroup_rulesは、どのデバイスでも機能します。 profiles:は使えません。プロファイルを持つサービスはリリースから除外されます。- 起動ルールは同じです。30秒以内に、すべてのコンテナが実行中であるか終了コード0で終了しており、かつ少なくとも1つが実行中でなければなりません。長時間実行するサービスには
restart: unless-stoppedを指定してください。
デバイス上のWeb UI
コンテナ内のWebサーバーは、そのポートのリモートアクセストンネルを通じてどこからでもアクセスできるようになり、ボードに表示されます:
- 固定ポートで提供し(
0.0.0.0にバインド)、port-template.ymlでprotocol: httpを指定して宣言します。本番インストールではトンネルがオンになり、WebSocketも含めてHTTPSで提供されます。 - トンネルのURLをテーブルに書き込みます。URLはSDKの
getRemoteAccessUrlForPort(port)で取得できます。起動時に、デバイステーブルのそのデバイスの行に書き込みます。トンネルがあるのは本番インストールだけなので、開発モードでの実行ではURLは書き込まれません。 - ボードに埋め込みます。デバイスで絞り込むデバイスごとのページで、EmbedウィジェットのURLをそのカラムにバインドします。UI Builderでは、これも他と同じデータバインディングです。
ページ自体については、次の点に注意してください:
- 開けるユーザー:httpトンネルはプライベートです。ユーザーがポートをPublicに切り替えない限り、IronFlockにサインインしていてデバイスへのアクセス権を持つユーザーだけが開けます。一方、デバイス自身のネットワーク上では、ページは誰でも開けます。そのため、工場ネットワークに見せてはならないものはそこに表示せず、機械の状態を変更する操作には独自のログインを必須にしてください。
- 相対URL:アセットの読み込み、APIの呼び出し、WebSocketの接続は、ページからの相対URLで行います。こうすることで、デバイスのネットワーク上でも、トンネル経由でも、リバースプロキシの背後でも動作します。
- プッシュし、ポーリングはしない:更新はWebSocketで送信します。
- ブラウザにプラットフォームの認証情報を置かない:ページはコンテナ内の自身のサーバーと通信し、SDKの接続はそのサーバーが保持します。
独自のページではなく、工場ネットワーク上にある機械のWeb HMIを表示するには、コンテナからそのHMIをリバースプロキシします。外部HMIの埋め込みを参照してください。
複数のデバイス
- すべての行に書き込み元のデバイスが記録されます:IronFlockは、行を書き込んだデバイスのキーを
device_keyに保存します。デバイスごとのテーブルはdevice_keyをキーにし、デバイス名はpath: kwargs.DEVICE_NAMEを指定したカラムに保存して、チャートはその名前で分割します。 - デバイス一覧とデバイスごとのページ:デバイステーブルから生成するサイドナビゲーションと、すべてのデバイスに使える1つのページを用意し、そのページのウィジェットをルートで指定されたデバイスで絞り込みます。ボードの構築を参照してください。
- デバイスごと・機械ごとの設定:設定テーブルは、対象を示すカラムをキーにします(ボードが書き込む行には固有のデバイスがありません)。そのカラムは、対象のページにあるフォームで入力します。各デバイスは、自身のキーに対応する最新の行を適用します。
- 離脱したデバイスの行は残ります。ユーザーが一覧からそのデバイスを削除できるようにするには、デバイステーブルに
deletedカラムを、ボードにそれを設定するフォームを追加します。
チェックリスト
- すべての状態がテーブルにあり、各テーブルの説明に唯一の書き込み元が明記され、コードが書き込むすべてのカラムが
data-template.ymlで宣言されています。 - すべての行に
tspがあり、状態の行は変更時に、キーと変更されたカラムだけで書き込まれています。 - ボードが書き込む状態に、SDK 1.10.0以降を使って前述の3ステップで追従しており、コードが接続やそのセッションを扱うことはありません。
- 長時間実行されるループ内のテーブル書き込みでは、プラットフォームに1分間到達できなかった場合に書き込みが送出するエラーを処理しており、想定内の失敗は
reportErrorとuserMessageで報告しています。 - プログラムがSIGTERMで終了し、exec形式で起動し、バッファリングせずにログを出力しています。
- ポーリングは一切ありません。ボードはバインドし、デバイスは購読し、Web UIはプッシュします。
- アプリが提供する各ポートが
port-template.ymlで宣言され、そのURLはgetRemoteAccessUrlForPortから取得されてテーブルに書き込まれ、ボードはそこからページを埋め込んでいます。 - composeアプリに
env_file:、profiles:、${VAR}形式のホストポートがなく、ファイルは名前付きボリュームに保存され、ユーザーが必要とするポートだけが公開されています。
スターターアプリから始める
Claude Code用のIronFlockプラグインには、この設計図に沿ったスターターアプリが含まれています。すべてのデバイスで機械からデータを読み取り、各デバイスの状態と測定値をテーブルに保持し、ユーザーがボードで設定した閾値を適用し、ボードが各デバイスのページに埋め込むライブビューを提供するプログラムです。Claude Codeはこれをもとに新しいアプリを作成し、ユースケースに合わせて調整します。