🫘

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を、以下のようなコードで呼び出します。

POU/プログラム/ProductionMonitor_FetchDemo
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キー機能が提供されたら内容を変更する可能性があります。

POU/プログラム/SupabaseClientServiceRunner
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で表示した生産モニタテーブル
SupabaseのTable Editorで表示した生産モニタテーブル

Sysmacプロジェクト

Sysmacプロジェクトは以下にあります。

https://github.com/kmu2030/SupabaseClientServiceLib

サンプルプロジェクトの使い方

サンプルプロジェクトの実行には、Supabase、サンプルプロジェクト、コントローラのそれぞれについて作業が必要です。Supabaseは予めアカウントを作成し、OrganizationとProjectを作成しておきます。

サンプルプロジェクトはDatabase操作を対象としたバックエンドだけです。フロントエンドはありません。サンプルプロジェクトは、以下のテーブルを操作します。RLSを有効にして簡潔なサービスキーによるアクセス制限を行います。

  • production_monitor : 生産モニタ
    生産情報を保持します。
  • production_task : 生産タスク
    生産タスクを保持します。

サンプルプロジェクトの使用には、最終的に以下の情報が必要です。

  • Supabaseのドメイン
  • SupabaseのAPIキー(anonキー)
  • 使用コントローラの型式
  • コントローラをインターネット接続するための設定
  • コントローラのセキュアソケットのセッションNo

作業は以下の手順で行います。

  1. Supabaseのセットアップ
  2. APIキーの確認
  3. サンプルプロジェクトを使用環境に合わせる
  4. サンプルプロジェクトのAPIクライアントサービスの設定を変更
  5. コントローラのセキュアソケット設定にTLSセッションを登録
  6. コントローラにサンプルプロジェクトを転送
1. Supabaseのセットアップ

リポジトリのセットアップSQL(setup_demo.sql)をSupabaseのSQL Editorで実行します。セットアップSQLをSQL EditorにコピーしたらDECLAREステートメントのinitial_service_keyを適当な値に変更します。HTTPリクエストのヘッダーとしてhttp_header_keyinitial_service_keyの値のペアを設定することでテーブルへのCRUD操作を行えるようになります。サービスキーによるアクセス制限です。

setup_demo.sql
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
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キーで置き換えます。

POU/プログラム/SupabaseClientServiceRunner
// Supabaseエンドポイントの登録
RegisterSupabaseEndpoint(
    // エンドポイントを識別する名称
    Name:='MachineInfo',
    // Supabaseのドメイン
    Domain:='YOUR_SUPABASE_DOMAIN',
    // Supabaseのanonキー
    ApiKey:='YOUR_ANON_KEY');

TLSセッションは、変更の理由が無ければ以下のままにします。

POU/プログラム/SupabaseClientServiceRunner
RegisterUnrestrictedTlsSession(
    TlsSessionName:='TLSSession0');
RegisterUnrestrictedTlsSession(
    TlsSessionName:='TLSSession1');

次に、POU/ファンクション/SetServiceCredentialToSupabaseQueryを編集します。サービスキーを1で設定した値にします。変更していなければ、以下のままにします。

POU/ファンクション/SetServiceCredentialToSupabaseQuery
SupabaseQuery_setHeader(
    Context:=Query,
    Key:='x-service-key',
    Value:='mykey');

このPOUは、APIクライアントサービスに含むものではありませんが、追加のアクセス制限として各プログラムで使用しています。

5. コントローラのセキュアソケット設定にTLSセッションを登録

コントローラに接続、プログラムモードに変更してセキュアソケット設定に4で指定したNoと同じNoのセッションを登録します。変更していなければ、IDが0と1のセッションを作成します。以下のように作業します。

セキュアソケット設定でのTLSセッションの登録
セキュアソケット設定での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で表示した生産モニタテーブル
SupabaseのTable Editorで表示した生産モニタテーブル

SQL Editorでクリエを生成し、Chartで表示すると以下のようになります。6秒毎、72時間のデータです。

SupabaseのSQL Editorで実行したクエリのChart表示
SupabaseのSQL Editorで実行したクエリのChart表示

生産タスク

生産タスクは、レコード取得とレコード更新をテストします。テーブルにレコードを作成しないと何も起こりません。セットアップSQLはいくつかのタスクを作成するので、それらが処理されると以下のようになります。

SupabaseのTable Editorで表示したタスクが処理された生産タスクテーブル
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クライアントサービスは、以下の手順で使用します。

  1. クエリの生成
  2. Fetchの生成
  3. 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クライアントサービスを使用する手順は以下です。ライブラリ操作を伴うため、必ず既存プロジェクトのコピーを作成します。

  1. プロジェクトでAPIクライアントサービスのライブラリと依存ライブラリを参照
    リポジトリのlib/が依存ライブラリです。
  2. サービス用変数をグローバル変数に登録
    gSupabaseClientServiceSingleton : SupabaseClientServiceSingletonContextを定義します。
  3. サービスランナー(SupabaseClientSingletonService FBを実行するプログラム)の作成
    サンプルプロジェクトを参考にしてください。
  4. サービスランナーをタスクに追加
    サービスランナーは、API呼び出し処理を実行するので、適切なタスク時間のタスクに登録します。プライマリタスクは不適です。
  5. ビルドしてエラーが無いことを確認
    ライブラリの不足やグローバル変数定義に誤りがあればエラーが出ます。
  6. メモリ使用状況を確認
    メモリ使用状況に変更が反映されているか確認します。

使用環境が整ったらAPIクライアントサービスを使用するプログラム、Supabaseプロジェクトを作成し、サービスランナーに必要な情報を記述します。大きなプロジェクトに対してこれらの操作を行うと時間がかかります。可能であれば小さなプロジェクトで必要な機能を開発し、動作テストまで済ませてから必要とするプロジェクトに統合することを検討してください。

実際に使用するには

Supabaseの理解、ロギング、フォールバックといった事項は別として以下が必要です。

  • サービス・ボットの認証手段あるいはアクセス制限の検討
    APIクライアントサービスは、ユーザー認証を行っていません。今回は、簡素なサービスキーを使用してアクセス制限を行いました。Supabaseは外部サービスを含め多くの認証方法を提供しています。使用可能な認証方法の確認と検証が必要です。SupabaseのAPI Keyについての新しい機能も使えるかもしれません。

まとめ

Supabaseにつないでしまえば、フロントエンドを提供しやすいだろうということでつなげました。手段の手軽さは、何事においても実践としての取り組みの敷居を下げてくれます。その点においてSupabaseには利点があるように思われます。折角の手軽なBaaSです。FAだからとよくあるようなダッシュボードを作成する必要はありません。ゲームエンジンを使用してワールドに興味深く状態を表現してもよいかもしれません。

大手のクラウドサービスプロバイダーであればサービスとして提供されるコンピューティングのレイヤーは多岐に渡り、FA向けのプロトコルも受け付けることができるのでSupabase以上のことが出来るのは確かです。Supabaseのサービスもどこかのプロバイダー上で運用されていると思いますが、他の手段もとれることに意味があります。

Discussion