NXからSupabaseを使う
はじめに
本記事は、PLC(Programmable Logic Controller)向けのソフトウェア開発に従事、または関心のある方で、Structured Text(ST)による開発に興味がある方向けです。OMRON社のSysmac Studioとコントローラ(NX1またはNX5)、Supabaseを使用します。
今回は、Supabase REST APIを使用してNXからSupabaseを使います。今回もユーザーに詳細は意識させないようにし、簡潔にSupabaseのDatabase、Edge functionsとGraphQLのREST APIを使用できるようにします。Supabase REST API clientサービス(以下、APIクライアントサービス)を提供するFBが適当なタスクで動作していればSupabase REST APIを使用できます。
PostgRESTのクライアントを作成しようと考えたのですが、せっかくなのでSupabaseにしました。開発者向けのBaaSとして手軽でローカル環境という選択肢も存在し、クライアント向けの機能が揃っているので使えるのではないかという判断です。目的はkintone同様、目的に特化したアプリケーションを展開し、目的を達したら破棄するという弾力的な運用を可能にすることです。kintoneとは異なり完全なBaaSなので、アプリケーションとして効果を上げたのであればそのまま展開してサービスとして提供することも視野に入れることができます。
API呼び出しはkintone向けのAPIクライアント同様です。Database操作はクエリ生成が複雑なので細かなPOUの提供を避け、操作とキーバリューのクエリパラメータを指定する少数のPOUを提供します。実際には、RPCかEdge functionsをクライアント向けのエンドポイントとし、Databaseを直接操作することはあまりないのではないかと思います。用途によってはGraphQLも良さそうです。ユーザーはSupabase REST APIを、以下のようなコードで呼び出します。
100:
// クエリの生成。
SupabaseQuery_init(iQuery);
SPQ_FROM('production_monitor', iQuery);
SPQ_BULK_INSERT(iRows, 0, iRowSize, 'text/csv', iQuery);
// デモプログラムではSupabaseのanonキーとは異なる独自のサービスキーを設定。
SetServiceCredentialToSupabaseQuery(iQuery);
// Fetchの生成。
SupabaseFetch_new(
Context:=iFetchContext,
EndpointName:=SUPABASE_ENDPOINT_NAME,
Query:=iQuery);
Inc(iState);
101:
CASE Supabase_fetch(iFetchContext) OF
ATS_RESOLVED:
SupabaseFetch_getStatusCode(
Context:=iFetchContext,
StatusCode=>iStatusCode);
iState := iReturnState;
ATS_REJECTED:
SupabaseFetch_getStatusCode(
Context:=iFetchContext,
StatusCode=>iStatusCode);
SupabaseFetch_getResponseBodyAsStr(
Context:=iFetchContext,
Body=>iRespBody);
SupabaseFetch_getError(
Context:=iFetchContext,
Error=>iError,
ErrorID=>iErrorID,
ErrorIDEx=>iErrorIDEx);
iState := iReturnState;
END_CASE;
以下がAPI呼び出し処理を行うPOUです。Supabaseの新しいAPIキー機能が提供されたら内容を変更する可能性があります。
CASE iState OF
// STATE_INIT
0:
// 設定初期化
InitSupabaseClientServiceSettings(
// 排他制御キー
LockKey:=17);
// Supabaseエンドポイントの登録
RegisterSupabaseEndpoint(
// エンドポイントを識別する名称
Name:='MachineInfo',
// Supabaseのドメイン
Domain:='YOUR_SUPABASE_DOMAIN',
// Supabaseのanonキー
ApiKey:='YOUR_ANON_KEY');
// 無制約TLSセッションの登録
RegisterUnrestrictedTlsSession(
TlsSessionName:='TLSSession0');
RegisterUnrestrictedTlsSession(
TlsSessionName:='TLSSession1');
Inc(iState);
1:
IF iCtrlReload THEN
ReloadSupabaseClientService();
iCtrlReload := FALSE;
ELSE
EnableSupabaseClientService();
iService.Enable := TRUE;
END_IF;
iState := STATE_ACTIVE;
// STATE_ACTIVE
10:
// オンラインによる手動操作用
IF iCtrlEnable THEN
EnableSupabaseClientService();
iCtrlEnable := FALSE;
ELSIF iCtrlDisable THEN
DisableSupabaseClientService();
iCtrlDisable := FALSE;
ELSIF iCtrlReload THEN
iState := STATE_INIT;
END_IF;
END_CASE;
iService();
サンプルプロジェクトを編集してコントローラで実行すると、Supabaseのテーブルに以下のようにレコードを作成します。このテーブルは装置の生産状態を保持します。
SupabaseのTable Editorで表示した生産モニタテーブル
Sysmacプロジェクト
Sysmacプロジェクトは以下にあります。
サンプルプロジェクトの使い方
サンプルプロジェクトの実行には、Supabase、サンプルプロジェクト、コントローラのそれぞれについて作業が必要です。Supabaseは予めアカウントを作成し、OrganizationとProjectを作成しておきます。
サンプルプロジェクトはDatabase操作を対象としたバックエンドだけです。フロントエンドはありません。サンプルプロジェクトは、以下のテーブルを操作します。RLSを有効にして簡潔なサービスキーによるアクセス制限を行います。
-
production_monitor : 生産モニタ
生産情報を保持します。 -
production_task : 生産タスク
生産タスクを保持します。
サンプルプロジェクトの使用には、最終的に以下の情報が必要です。
- Supabaseのドメイン
- SupabaseのAPIキー(anonキー)
- 使用コントローラの型式
- コントローラをインターネット接続するための設定
- コントローラのセキュアソケットのセッションNo
作業は以下の手順で行います。
- Supabaseのセットアップ
- APIキーの確認
- サンプルプロジェクトを使用環境に合わせる
- サンプルプロジェクトのAPIクライアントサービスの設定を変更
- コントローラのセキュアソケット設定にTLSセッションを登録
- コントローラにサンプルプロジェクトを転送
1. Supabaseのセットアップ
リポジトリのセットアップSQL(setup_demo.sql)をSupabaseのSQL Editorで実行します。セットアップSQLをSQL EditorにコピーしたらDECLAREステートメントのinitial_service_key
を適当な値に変更します。HTTPリクエストのヘッダーとしてhttp_header_key
とinitial_service_key
の値のペアを設定することでテーブルへのCRUD操作を行えるようになります。サービスキーによるアクセス制限です。
DECLARE
http_header_key TEXT := 'x-service-key';
initial_service_key TEXT := 'mykey';
service_keys_table TEXT := 'service_keys';
production_monitor_table TEXT := 'production_monitor';
production_task_table TEXT := 'production_task';
上記であれば、x-service-key: mykey
をHTTPヘッダーに設定することで、操作が行えるようになります。セットアップSQLは、関連要素のクリーンアップを行うので問題が生じたら再実行することで初期状態にすることができます。詳細はセットアップSQLを確認してください。
2. APIキーの確認
SupabaseのSettings/API KeysでAPIキーを確認して控えます。
SupabaseのSettings/API Keys
3. サンプルプロジェクトを使用環境に合わせる
サンプルプロジェクトを使用環境に合わせます。以下の変更が必要です。
-
コントローラの型式
使用するコントローラの型式に変更します。 -
コントローラのネットワーク設定
使用環境でインターネット接続可能な設定とします。DNSは特別な理由が無ければ、"1.1.1.1"のようなパブリックDNSを使用します。
4. サンプルプロジェクトのAPIクライアントサービスの設定を変更
POU/プログラム/SupabaseClientServiceRunner
を編集します。各RegisterSupabaseEndpointの引数をSupabaseの環境に合わせます。Supabaseのドメイン、APIキー(anonキー)を変更します。YOUR_SUPABASE_DOMAIN
をSupabaseのプロジェクトIDを含むドメインで、YOUR_ANON_KEY
を2で控えたAPIキーで置き換えます。
// Supabaseエンドポイントの登録
RegisterSupabaseEndpoint(
// エンドポイントを識別する名称
Name:='MachineInfo',
// Supabaseのドメイン
Domain:='YOUR_SUPABASE_DOMAIN',
// Supabaseのanonキー
ApiKey:='YOUR_ANON_KEY');
TLSセッションは、変更の理由が無ければ以下のままにします。
RegisterUnrestrictedTlsSession(
TlsSessionName:='TLSSession0');
RegisterUnrestrictedTlsSession(
TlsSessionName:='TLSSession1');
次に、POU/ファンクション/SetServiceCredentialToSupabaseQuery
を編集します。サービスキーを1で設定した値にします。変更していなければ、以下のままにします。
SupabaseQuery_setHeader(
Context:=Query,
Key:='x-service-key',
Value:='mykey');
このPOUは、APIクライアントサービスに含むものではありませんが、追加のアクセス制限として各プログラムで使用しています。
5. コントローラのセキュアソケット設定にTLSセッションを登録
コントローラに接続、プログラムモードに変更してセキュアソケット設定に4で指定したNoと同じNoのセッションを登録します。変更していなければ、IDが0と1のセッションを作成します。以下のように作業します。
セキュアソケット設定でのTLSセッションの登録
6. コントローラにサンプルプロジェクトを転送
コントローラにサンプルプロジェクトを転送し、運転モードに切り替えます。ネットワークエラーが発生していないか確認します。ネットワークエラーが発生している場合、エラー原因を取り除きます。
エラーが発生した場合、以下の可能性があります。
-
TLSセッションIDとTLSセッション名の不一致
4で指定したTLSセッション名の番号と5で指定したTLSセッションIDが一致していることを確認します。 -
ドメインの不一致
4で指定した登録しドメインが一致していることを確認します。 -
APIキーの不一致
4で指定したAPIキーがSupabaseのAPIキーに一致ていることを確認します。 -
インターネットに接続できないか、名前解決ができない
Supabaseドメインまでのルートをtracertで確認します。 -
Supabaseで障害が発生している
Supabase Statusを確認します。
サンプルプロジェクトの実行
サンプルプロジェクトのテーブルとプログラムは以下のように対応します。
機能 | テーブル | POU/プログラム |
---|---|---|
生産モニタ | production_monitor | ProductionMonitor_FetchDemo |
生産タスク | production_task | ProductionTaskWatcher_FetchDemo |
生産モニタは、プログラムが動作すれば勝手にレコードを作成し始めます。生産タスクは、テーブルにレコードを作成しておく必要があります。
生産モニタ
生産モニタは、一定間隔でのSupabaseへのレコード作成をテストします。一度に複数のレコードを作成するBulk Insertを使用します。動作に問題がなければ、以下のようにテーブルにレコードを作成します。
SupabaseのTable Editorで表示した生産モニタテーブル
SQL Editorでクリエを生成し、Chartで表示すると以下のようになります。6秒毎、72時間のデータです。
SupabaseのSQL Editorで実行したクエリのChart表示
生産タスク
生産タスクは、レコード取得とレコード更新をテストします。テーブルにレコードを作成しないと何も起こりません。セットアップSQLはいくつかのタスクを作成するので、それらが処理されると以下のようになります。
SupabaseのTable Editorで表示したタスクが処理された生産タスクテーブル
APIクライアントサービスの機能と使用手順
APIクライアントサービスの機能は、Database、Edge functionsとGraphQLに限ります。Storage、Auth、Realtimeは提供していません。構成は、kintone REST API clientサービスに同等ですが、HTTPリクエストを作るためのヘルパーPOUが異なります。kintoneは必要とするHTTPリクエストのパターンが少数でしたが、Supabaseはクエリパラメータの生成に加え、意図したレスポンスを得るためのHTTPヘッダーの設定も必要になるので、HTTPリクエストのパターンが膨大です。
そこで、ユーザーにクエリを生成するためのヘルパーPOUを提供し、ユーザーがクエリ(SupabaseQuery)を構築し、そのクエリからHTTPリクエストを生成するようにします。クエリはSQLに関連した名称のPOUを、Supabaseの公式クライアントに似た順序で呼び出して構築します。フィルタのオペレータのような細かなPOUは提供しません。返って読みづらくなるためです。
APIクライアントサービスは、以下の手順で使用します。
- クエリの生成
- Fetchの生成
- Fetchの実行
1. クエリの生成
APIクライアントサービスの使用は、以下のようなクエリの生成から始まります。クエリは、Supabase REST APIを呼び出すのに必要な情報を集約する構造体です。
// 最初に構造体を初期化する。
SupabaseQuery_init(iQuery);
SPQ_FROM('table', iQuery);
SPQ_BULK_INSERT(iRows, 0, iRowSize, 'text/csv', iQuery);
クエリの構築には、以下のPOUを使用します。
POU | 機能 |
---|---|
SPQ_SCHEMA | スキーマを指定します。 |
SPQ_FROM | テーブルを指定します。 |
SPQ_SELECT | レコード取得を指定します。 |
SPQ_INSERT | 単一レコード作成を指定します。 |
SPQ_BULK_INSERT | 複数レコード作成を指定します。 |
SPQ_UPDATE | レコード更新を指定します。 |
SPQ_UPSERT | Upsertによるレコード更新を指定します。 |
SPQ_SINGLE_UPSERT | Upsertによる単一レコード更新を指定します。 |
SPQ_DELETE | レコード削除を指定します。 |
SPQ_FILTER | フィルタを指定します。 |
SPQ_PARAM | 任意のURLクエリパラメータを指定します。 |
SPQ_RPC | PostgreSQL関数呼び出しを指定します。 |
SPQ_FUNCTION | Edge functions呼び出しを指定します。 |
SPQ_GRAPHQL | GraphQLを指定します。 |
SupabaseQuery_addPreference | 任意のプリファレンスを追加します。 |
SupabaseQuery_setHeader | 任意のHTTPヘッダーを設定します。 |
単一クエリに対して複数回実行できるPOUとそうではないPOUがありますが、単一SQL文の組み立てを考えれば、予測がつくと思います。
2. Fetchの生成
クエリを生成したら、そのクエリからAPIクライアントサービスで処理可能なFetchを生成します。Fetchを生成するPOUは一つだけです。以下のように生成します。
SupabaseFetch_new(
Context:=iFetchContext,
EndpointName:=SUPABASE_ENDPOINT_NAME,
Query:=iQuery);
3. Fetchの実行
Fetchを生成しただけでは、APIクライアントサービスのリソースは消費しません。以下のように実行してAPIクライアントサービスに処理させます。
CASE Supabase_fetch(iFetchContext) OF
ATS_RESOLVED:
SupabaseFetch_getStatusCode(
Context:=iFetchContext,
StatusCode=>iStatusCode);
iState := iReturnState;
ATS_REJECTED:
SupabaseFetch_getStatusCode(
Context:=iFetchContext,
StatusCode=>iStatusCode);
SupabaseFetch_getResponseBodyAsStr(
Context:=iFetchContext,
Body=>iRespBody);
SupabaseFetch_getError(
Context:=iFetchContext,
Error=>iError,
ErrorID=>iErrorID,
ErrorIDEx=>iErrorIDEx);
iState := iReturnState;
END_CASE;
レスポンスを文字列として取得する場合、P_PRGERフラグを確認してエラーが発生していないか確認します。レコード取得のレスポンスは、容易に文字列型の最大長を超えるためです。レスポンスが大きい場合、UTF-8のバイト列として処理します。
Fetchの実行と結果に関する情報は、以下のPOUを使用して取得します。
POU | 機能 |
---|---|
SupabaseFetch_getStatusCode | API呼び出しのHTTPレスポンスステータスコードを取得します。 |
SupabaseFetch_getResponseHeader | API呼び出しのHTTPレスポンスヘッダーを取得します。 |
SupabaseFetch_getResponseBody | API呼び出しのHTTPレスポンスボディをバイト列として取得します。 |
SupabaseFetch_getResponseBodyAsStr | API呼び出しのHTTPレスポンスボディを文字列として取得します。 |
SupabaseFetch_getError | API呼び出しで発生したエラーを取得します。 |
SupabaseFetch_getResponse | API呼び出しのHTTPレスポンス情報を集約したレスポンス構造体(SupabaseRestApiResponse)を取得します。 |
SupabaseFetch_getRequest | API呼び出しのHTTPリクエスト情報を集約したリクエスト構造体(SupabaseRestApiRequest)を取得します。 |
SupabaseRestApiResponse_getStatusCode | レスポンス構造体からHTTPレスポンスステータスコードを取得します。 |
SupabaseRestApiResponse_getHeader | レスポンス構造体からHTTPレスポンスヘッダーを取得します。 |
SupabaseRestApiResponse_getBody | レスポンス構造体からHTTPレスポンスボディをバイト列として取得します。 |
SupabaseRestApiResponse_getBodyAsStr | レスポンス構造体からHTTPレスポンスボディを文字列として取得します。 |
SupabaseRestApiRequest_getHttpMethod | リクエスト構造体からHTTPメソッドを取得します。 |
SupabaseRestApiRequest_getUrl | リクエスト構造体からUrlを取得します。 |
SupabaseRestApiRequest_getHeader | リクエスト構造体からHTTPリクエストヘッダーを取得します。 |
SupabaseRestApiRequest_getBody | リクエスト構造体からHTTPリクエストボディをバイト列として取得します。 |
SupabaseFetch_getError
が返すエラーコード(ErrorID)は以下です。
ErrorID | 内容 |
---|---|
0x1001 | 意図しないTCPコネクションの切断。 |
0x1003 | 成功ではない(非200番台)のHTTPレスポンスステータスコード。 |
0x2000 | APIクライアントサービスのリロードによるタスク破棄。 |
0x2001 | APIクライアントサービスのシャットダウンによるタスク破棄。 |
0x2002 | SupabaseEndpointが無効。 |
既存プロジェクトで使用するには
既存プロジェクトでAPIクライアントサービスを使用する手順は以下です。ライブラリ操作を伴うため、必ず既存プロジェクトのコピーを作成します。
-
プロジェクトでAPIクライアントサービスのライブラリと依存ライブラリを参照
リポジトリのlib/が依存ライブラリです。 -
サービス用変数をグローバル変数に登録
gSupabaseClientServiceSingleton : SupabaseClientServiceSingletonContext
を定義します。 -
サービスランナー(SupabaseClientSingletonService FBを実行するプログラム)の作成
サンプルプロジェクトを参考にしてください。 -
サービスランナーをタスクに追加
サービスランナーは、API呼び出し処理を実行するので、適切なタスク時間のタスクに登録します。プライマリタスクは不適です。 -
ビルドしてエラーが無いことを確認
ライブラリの不足やグローバル変数定義に誤りがあればエラーが出ます。 -
メモリ使用状況を確認
メモリ使用状況に変更が反映されているか確認します。
使用環境が整ったらAPIクライアントサービスを使用するプログラム、Supabaseプロジェクトを作成し、サービスランナーに必要な情報を記述します。大きなプロジェクトに対してこれらの操作を行うと時間がかかります。可能であれば小さなプロジェクトで必要な機能を開発し、動作テストまで済ませてから必要とするプロジェクトに統合することを検討してください。
実際に使用するには
Supabaseの理解、ロギング、フォールバックといった事項は別として以下が必要です。
-
サービス・ボットの認証手段あるいはアクセス制限の検討
APIクライアントサービスは、ユーザー認証を行っていません。今回は、簡素なサービスキーを使用してアクセス制限を行いました。Supabaseは外部サービスを含め多くの認証方法を提供しています。使用可能な認証方法の確認と検証が必要です。SupabaseのAPI Keyについての新しい機能も使えるかもしれません。
まとめ
Supabaseにつないでしまえば、フロントエンドを提供しやすいだろうということでつなげました。手段の手軽さは、何事においても実践としての取り組みの敷居を下げてくれます。その点においてSupabaseには利点があるように思われます。折角の手軽なBaaSです。FAだからとよくあるようなダッシュボードを作成する必要はありません。ゲームエンジンを使用してワールドに興味深く状態を表現してもよいかもしれません。
大手のクラウドサービスプロバイダーであればサービスとして提供されるコンピューティングのレイヤーは多岐に渡り、FA向けのプロトコルも受け付けることができるのでSupabase以上のことが出来るのは確かです。Supabaseのサービスもどこかのプロバイダー上で運用されていると思いますが、他の手段もとれることに意味があります。
Discussion