🔥

CODESYS Example Projectを調べてみた#12 (Web Client SL Library)

に公開

Web Client SL Library

第12回目にしてやっと勉強会に参加者が来てくれた今回は、Web Client SL LibraryのExample Projectを見ていきます。

このExampleは、CODESYS Application側からWebサーバーにアクセスする機能を提供するWeb Client SL Libraryの使い方について知ることができます。このライブラリは、IIoTライブラリの中の一つで、http, httpsを使ったWebサーバへのアクセスに利用します。IIoT Library Exampleの中には、Socket通信、MQTT通信の例や、INI File Utility, JSON Utilitiesなどよく使うファイル形式を扱うUtilityの例もあります。知ってると便利そうなので、これらも別の機会に見ておきたいですね。

Webサーバへアクセスする場合、認証が必要な場合があります。Web Clientライブラリは以下の認証に対応しています。

  • Basic認証
  • Digest認証
  • OAuth1
  • OAuth2

Web Clientライブラリは、それぞれの認証方法に応じたFunctionBlockを提供しています。

Setup手順

Exampleページ下部の「DOWNLOAD projects」からダウンロードします。このとき、IIoTのExample Projectsがまとめてダウンロードされるので、数分程度時間がかかります。

私の環境では、ホームディレクトリ以下のCODESYS Example¥IIoT Libraries SL Examples\1.0.0.0\Files\以下にprojectファイルがインストールされていました。その中のWeb Client SL Exampleをダブルクリックで開きます。

Project Fileのコンパイラバージョン(3.5.17.0)は、私の環境に合わせてバージョン(3.5.21.10)へ更新しました。


プロジェクトストレージ形式に関する確認がでてくるので、「はい」を選択しました。

最後に、Deviceツリーの右クリックから「デバイスの更新」を選択し、CODESYS Control WinV3 x64を選択して更新します。

Example Projectの内容

Example Projectの中には、認証方法に応じた4つのWeb Client使用例に加えて、Pushoverという通知サービスにメッセージを送る例、QueueとWorkerを使ってFlow制御する例も含まれています。

Web Serverへのアクセス例

通常のhttp/httpsでアクセスする場合は、WebClientExampleが対応しています。このExampleコードは、1つのPOUで構成され、CFCで作成されています。

// Program to send HTTP or HTTPS requests to a specified URL.
PROGRAM POU
VAR	
	webClient: WEB_CLIENT.WebClient;	// HTTP Client
	sURL : STRING(1024) := 'http://httpbin.org/get'; // URL
	xStart: BOOL; // Rising edge will send a request
	r_TRIG_0: R_TRIG; // Helper to reset the state.
	rsDone: RS;  // Helper to store xDone
	rsError: RS; // Helper to store xError
	xDone : BOOL; // TRUE if the request has been finished without errors
	xError : BOOL; // TRUE if the request has been finished with errors
	wsPostValue : WSTRING(100) := ""; // POST or PUT value
	wsAdditionalHeader : WSTRING(200) := "User-Agent: CODESYS_Webclient";
END_VAR

POU

このプログラムの処理としては、WebClient FBの入力として、URLの文字列、HTTPメソッド(ここではGET)などを設定し、xStartをトリガーとして、HTTPリクエストを実行します。

CFCの右側のRS(RSフリップフロップFB)と、R_TRIG(立ち上がり検出FB)は、Web ClientのDone, Errorのステータス保持に使われています。この部分はステータスの保持の定石なので覚えておくとよいかもしれません。

重要な要素は、WEB_CLIENT.WebClientです。これは、Web Client SL Libraryで提供されるFunction Blockです。

結果は、FBのhttpResultに保存されます。WebVisuなどで表示したり、JSONの処理系で処理することが可能です。httpResult構造体は、以下のようにステータス、ヘッダー、コンテンツごとにアクセス可能です。

TYPE HttpResult : STRUCT
    iStatus         : INT;   // HTTPステータスコード

    wsHeader        : WSTRING(g_udiMaxHeaderSize);   // レスポンスヘッダ(WSTRING)
    wsContent       : WSTRING(g_udiMaxResponseSize); // レスポンス本文(WSTRING)

    sHeader         : STRING(g_udiMaxHeaderSize);    // レスポンスヘッダ(STRING)
    sContent        : STRING(g_udiMaxResponseSize);  // レスポンス本文(STRING)

    diContentLength : DINT;  // sContent のバイト長
END_STRUCT
END_TYPE

//g_udiMaxHeaderSize や g_udiMaxResponseSize は、Web Client SL ライブラリ内で定義された定数です。
//wsHeader/wsContent と sHeader/sContent の両方を持つのは、Unicode 対応(WSTRING)と従来のマルチバイト文字列(STRING)の両方に対応するためです。

認証付きのWebサーバへアクセスする場合の例はHTTPBasicAuthExample, HTTPDigestAuthExampleが対応しています。

Basic認証とDigest認証の場合

Basic認証もDigest認証もユーザー名とパスワードを使って認証します。それぞれの認証方式で利用するFBが異なりますが、INPUTで渡すデータは共通しています。FBのwsLoginにWSTRING型でユーザー名を設定し、wsPasswordにWSTRING型でパスワードを設定します。リクエストの結果は、認証なしの場合と同様、HttpResultから利用可能です。

OAuth2の場合

OAuth2.0は、ユーザーのパスワードを渡さずにクライアントアプリがユーザーに代わって別サービスのデータや機能へアクセスできるようにする「認可」機能を提供します。ユーザーの「認証」はOIDCなどで行います。

OAuth2.0には複数の認可フローがありますが、このExampleでは2-legged OAuth2認証を実装しており、Client Credentials Flow, Resource Owner Password Flow をサポートしています。Client Credentials Flowは、サービス間通信やバックエンド処理に使用され、ユーザー認証不要でアプリケーション認証のみ行います。Resource Owner Password Flowは、自社開発アプリなど信頼できるクライアントに対して使用します。セキュリティリスクが高いため、推奨されないようです。

このExampleは、CODESYS の WEB_CLIENT.OAuth2WebClient を使って、OAuth 2.0の“リソースオーナー・パスワード認証(ROPC)” でアクセストークンを取得し、そのトークンでリソースAPIにHTTPアクセスする最小構成です。OAuth2WebClientを用いることで、アクセストークンの自動取得・管理・更新をすることができます。

以下で実際のコードを見ていきます。CFCの部分はSTに変更しています。

PROGRAM POU
VAR	
	webClient: WEB_CLIENT.OAuth2WebClient;	// OAuth2 Web Client
	sAuthorizationServerURL : STRING(1024) := 'http://brentertainment.com/oauth2/lockdin/token'; // The URL of the authorization server
	sURL : STRING(1024) := 'http://brentertainment.com/oauth2/lockdin/resource'; // The URL of the resource server
	oAuth2ClientCredentials : WEB_CLIENT.OAuth2RessourceOwnerCredentials := (
		wsClientId := "demoapp",  // OAuth2: client_id 
		wsClientSecret := "demopass", // OAuth2: client_secret. HIDE THIS VALUE IN YOUR APPLICATION!!!
		wsUsername := "demouser", // OAuth2: user_name 
		wsPassword := "testpass", // OAuth2: password. HIDE THIS VALUE IN YOUR APPLICATION!!!
		wsScope := "" // OAuth2: scope (optional) 
	);
	xStart: BOOL; // Rising edge sends a request
	r_TRIG_0: R_TRIG; // Helper to reset the state.
	rsDone: RS;  // Helper to store xDone
	rsError: RS; // Helper to store xError
	xDone : BOOL; // TRUE if the request has been finished without errors
	xError : BOOL; // TRUE if the request has been finished with errors
	wsPostValue : WSTRING(100) := ""; // POST or PUT value	
END_VAR

// エッジ検出
r_TRIG_0(CLK := xStart);

// OAuth2 Web Client実行
webClient(
    xExecute := xStart,
    sAuthorizationServerURL := sAuthorizationServerURL,
    sURL := sURL,
    eRequestType := WEB_CLIENT.REQUEST_TYPE.GET,
    itfOAuth2Credentials := oAuth2ClientCredentials,
    udiTimeOut := 10_000_000 //10sec
);

// 状態管理
rsDone(SET := webClient.xDone, RESET := r_TRIG_0.Q);
xDone := rsDone.Q;

rsError(SET := webClient.xError, RESET := r_TRIG_0.Q);
xError := rsError.Q;

sequenceDiagram

OAuth2.0の認可処理に必要な入力は、インターフェース型のIOAuth2Credentialsです。設定するデータは、インターフェース型を実装したOAuth2ClientCredentialsとOAuth2RessourceOwnerCredentialsとで切り替えられるようになっています。

Pushoverへのメッセージ送信例

Pushoverは、Android、iPhone、iPad、デスクトップ(Android WearやApple Watchにも対応)でリアルタイムの通知を受信できるサービスで、様々なWebサイト、サービス、アプリケーションからの無制限のプッシュ通知を受け取ることができます。

Pushoverへメッセージを送るには、認証なしのWebClient FBを使用し、POSTメソッドで以下の値を送ります。wsPostValueは、HTTPリクエストのBodyに埋め込まれる値です。

wsPostValue : WSTRING(255) := "token=YOUR_TOKEN&user=YOUR_USER&message=YOUR_MESSAGE";

形式: URL-encoded形式(application/x-www-form-urlencoded)
必須パラメータ:
token: アプリケーショントークン
user: ユーザーキー
message: 送信するメッセージ内容

送信先は https://api.pushover.net/1/messages.json、HTTPS必須かつPOST(application/x-www-form-urlencoded)。TLS検証は有効化して運用してください。

Queue-Worker構成でWebサーバアクセスをする例

このExampleは、Producer(PRG)が値をQueueに追加し、Workerタスクが逐次処理を行うアーキテクチャを実装したものです。処理の大まかな構成は以下の通りです。Exampleのタスク構成は、Producer(PRG)が20msサイクルが設定される一方、WebClientWorkerは、200msサイクルの設定になっています。

プログラム構成

Queue操作には、Element Collection Library(COL)を利用しています。
Web Clientへのアクセスには、この記事の主題であるWeb Client Libraryを利用しています。

以下、実際のコードを見ていきます。

WebClientWorkerState(ENUM)
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE WebClientWorkerState :
(
	WAIT := 0,
	SEND := 1
);
END_TYPE

Workerの状態を定義するENUMが定義されています。

PROG
PROGRAM PRG
VAR
	wstringElementFactory : COL.WStringElementFactory; // Factory to create WStringElements 
	itfWStringElement : COL.IWStringElement; // Element interface for WSTRINGs 
	queueFactory : COL.QueueFactory; // Queue factory
	queue: COL.IQueue := queueFactory.Create(udiMaxElements := 5000); // Creates a queue with max 5000 elements
	
	wskey : WSTRING := "SampleValue="; // Sample post data key
	wsValue : WSTRING(3); // Sample post data value
	i : INT;
	eError : COL.COLLECTION_ERROR;
END_VAR

IF i < 100 THEN
	// Add 100 samples to the queue. The execution of the queue will be done in the WebClientWorker-task.
	// Prepare example post data and add the data to the queue.
	wsValue :=  INT_TO_WSTRING(i);
	Stu.StrConcatW(pstFrom := ADR(wsValue), pstTo := ADR(wskey), iBufferSize := UINT_TO_INT(SIZEOF(wskey)));
	itfWStringElement := wstringElementFactory.Create(wsValue := wsValue);
	eError := queue.AddElement(itfElement := itfWStringElement);
END_IF
i := i + 1;

Queueの生成は、queue: COL.IQueue := queueFactory.Create(udiMaxElements := 5000);の行で行っています。Queue以外もインターフェース型で変数を定義して、FactoryクラスのCreateメソッドで実際のメモリ領域を作る流れは共通なので、覚えておくと便利だと思います。

作成したQueueへの要素作成は、itfWStringElement := wstringElementFactory.Create(wsValue := wsValue);で行い、追加は、eError := queue.AddElement(itfElement := itfWStringElement);の行で行っています。

上記のリクエスト作成は比較的軽量な処理ですが、実際の外部Webサーバへのアクセスは重い処理になります。その部分を以下のWebClientWorkerタスクに分離しています。

PROGRAM WebClientWorker
VAR
	webClient: WEB_CLIENT.WebClient;
	sURL : STRING(1024) := 'http://httpbin.org/post'; // The url of the server (example)
	wsPostValue : WSTRING;
	itfElement : COL.IElement;
	itfWStringElement : COL.IWStringElement;
	xResult : BOOL;
	wsValue : WSTRING;
	eState : WebClientWorkerState;
	xSendError : BOOL;
	itfInstanceBase : FBF.IInstance;
END_VAR

CASE eState OF
	
	WebClientWorkerState.WAIT : 
		// Wait for incoming values
		IF PRG.queue.CountElements() > 0 THEN
			// Get head of the queue
			PRG.queue.Peek(itfElement => itfElement);
			// Cast IElement to IWStringElement
			xResult := __QUERYINTERFACE(itfElement, itfWStringElement);
			IF xResult THEN
				wsValue := itfWStringElement.WStringValue;
				eState := WebClientWorkerState.SEND;
			END_IF
		END_IF
		
	WebClientWorkerState.SEND :
		// Send 
		webClient(	xExecute := TRUE, 
			udiTimeOut := 10000000, 
			sURL := sURL,
			pwsPostValue := ADR(wsValue),
			eRequestType := WEB_CLIENT.REQUEST_TYPE.POST, 
			eContentType := WEB_CLIENT.CONTENT_TYPE.APPLICATION_FORM);
		IF webClient.xDone THEN			
			xSendError := FALSE;
			IF webClient.httpResult.iStatus = 200 THEN // Response is ok
				PRG.queue.Poll(itfElement => itfElement); // Remove the head element from queue
				// Cast the element to FBF.IInstance and call Dispose() to free memory. 
				xResult := __QUERYINTERFACE(itfElement, itfInstanceBase);
				IF xResult THEN
					itfInstanceBase.Dispose();
				END_IF
				eState := WebClientWorkerState.WAIT;
			ELSE
				xSendError := TRUE;
				eState := WebClientWorkerState.WAIT;
			END_IF
			webClient( xExecute := FALSE);
		ELSIF webClient.xError THEN
			xSendError := TRUE;
			eState := WebClientWorkerState.WAIT;
			webClient( xExecute := FALSE);
		END_IF
END_CASE

このタスクの処理は、ステートマシンで実装されています。WAITステートでQueueからデータを取得し、SENDステートに遷移します。SENDステートでは、Web Clientでリクエストを送信し、レスポンスが200かどうかチェックします。正常にリクエストが処理されると、Queueの中身をCAA FB Factory Libraryで提供されるインターフェース型にキャストして、メモリを開放します。

共有オブジェクトのQueueは、ProducerとWorkerの両方のタスクから参照されます。このExampleでは、PRG.queue.Peek(itfElement => itfElement)のように、直接PRGを指定しています。本番では、グローバルFBに集約するなどした方がよさそうです。

Peekメソッドは、Elementを削除しないので、送信成功時にPRG.queue.Poll(itfElement => itfElement); で削除するようになっています。QueueからElementが削除されても、Createメソッドで動的に確保したメモリは残った状態なので、それを削除しているのが、以下の部分です。

xResult := __QUERYINTERFACE(itfElement, itfInstanceBase);
IF xResult THEN
  itfInstanceBase.Dispose(); // メモリ解放
END_IF

__QUERYINTERFACEは、2つのインターフェース型の型変換を行う演算子で、TRUEかFALSEを返します。

このあたりは、COLやFBFの定石処理の1つです。Element Collections Exampleも参照してください。

学びと気づき

  • Web Client SLの基本パターン
    xExecute は立ち上がりエッジで発火し、xDone / xError は1サイクルだけ立つ。R_TRIG と RS で“実行→終了検知→初期化”を安定化できる。
    udiTimeOut は μs単位。実ネットワークを想定した十分な値(例:10_000_000=10秒)にする。

  • レスポンスの取り扱い(HttpResult)
    可読表示は wsHeader/wsContent(WSTRING)、バイト長は diContentLength を使うと良い。ログやJSON処理系に渡す経路を最初に決めておくと後工程が楽。

  • 認証の実務ポイント
    Basic/Digest は wsLogin / wsPassword を入れるだけで流用可能。
    OAuth2 は OAuth2WebClient + IOAuth2Credentials 実装(ClientCredentials/ResourceOwner)。ROPCはデモ用途に留め、可能なら Client Credentials へ。秘密情報はハードコード禁止。

まとめ

  • Web Client SLで外部Webサーバにアクセス可能。認証が必要なものも対応するFBがある。
  • Queueでフロー制御する場合、Peek→送信→Poll→Dispose の流れと xExecute の正しいライフサイクルが重要。
  • 本番では 再試行戦略・レート制御・TLS検証・秘密管理・監視 を必ず追加。

Discussion