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

このプログラムの処理としては、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;

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: 送信するメッセージ内容
Queue-Worker構成でWebサーバアクセスをする例
このExampleは、Producer(PRG)が値をQueueに追加し、Workerタスクが逐次処理を行うアーキテクチャを実装したものです。処理の大まかな構成は以下の通りです。Exampleのタスク構成は、Producer(PRG)が20msサイクルが設定される一方、WebClientWorkerは、200msサイクルの設定になっています。

Queue操作には、Element Collection Library(COL)を利用しています。
Web Clientへのアクセスには、この記事の主題であるWeb Client Libraryを利用しています。
以下、実際のコードを見ていきます。
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE WebClientWorkerState :
(
WAIT := 0,
SEND := 1
);
END_TYPE
Workerの状態を定義するENUMが定義されています。
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