📚

CODESYS Example Projectを調べてみた#13 (JSON and CSV Utility)

に公開

JSON and CSV Utilities SL Example

第13回の勉強会はリピータの方も参加していただきました。テーマは前回のWeb Clientに引き続いて、IIoT Library内のExampleから、JSON Utilities, CSV Utilitiesを見ていきました。

Setup手順

すでに前回の勉強会でダウンロード済みでしたので、C:\Users\<user_name>\CODESYS Examples\IIoT Libraries SL Examples\1.0.0.0\FilesにあるプロジェクトファイルからExampleを開きます。
まだダウンロードしていない場合は、こちらのリンクからダウンロードできます。

JSON Utilities

JSON Utilities ExampleのProjectには、'JSONArrayExample', 'JSONDataExample', 'JSONFileExample', 'JSONFindValueExample'の4種のアプリケーションが含まれています。これらは、 キーと値や配列の操作、ファイル入出力、値検索などの例を含んでいます。以下の節で詳しく見ていきましょう。

4つのExampleの意図

それぞれのアプリケーションで説明されている機能は、概ね以下の内容です。

JSONDataExample

JSONDataを使って「キーと値のペア」を生成し、JSON形式に整形する例です。PLC変数をJSONに変換する最も基本的な例です。

JSONDataFactory -> JSONData -> JSONByteArrayWriter の流れが基本パターンです。

PLC_PRG (抜粋)
PROGRAM PLC_PRG
VAR
	factory : JSON.JSONDataFactory;
	eDataFactoryError : FBF.ERROR; // Error code: 0: OK, 30103: no more memory	
	pJsonData : POINTER TO JSON.JSONData := factory.Create(eError => eDataFactoryError);

JSONDataFactoryは、JSON Utilities SL ライブラリに含まれるFunctionBlockです。指定の最大サイズのヒープ領域を確保するために使います。実際のメモリ確保は、createメソッドによって行います。

返り値は、JSONDataへのポインタ型です。JSONDataは、多くのメソッドを持つFunction Blockです。このあとも出てくるのでそこで詳しく説明します。

PLC_PRG (つづき)
	jsonArrayWriter : JSON.JSONByteArrayWriter; 
	diCounter : DINT;
	diMemory : DINT;
	wsJsonData : WSTRING(1000); 
	wsValue : WSTRING;
	xStart : BOOL := TRUE;
	iExample : INT := 1; // Default example 1
	fb_JBuilder : JSON.JSONBuilder;
	xValue : BOOL := TRUE;
	iValue : INT := 42;
	lrValue : LREAL := -0.444;
	wsArrValue : ARRAY [0..2] OF WSTRING := ["Reiten", "Golfen", "Lesen"];
END_VAR

IF xStart = TRUE AND pJsonData <> 0 THEN
	IF iExample = 1 THEN
		diCounter := 0;

JSONByteArrayWriterは、JSONDataオブジェクトに対して配列データを書き込むためのFunctionBlockです。

JSONBuilderは、JSONデータを作るHelper Function Blockです。SetKey, SetValue, SetKeyWithValueなどのメソッドを呼び出すことで簡単にJSON形式のデータを組み立てることができます。

このExampleでは、複数の方法でJSONデータを構築しています。下の例では、JSONDataのSetObjectメソッドを使って、IndexとParentIndexを設定します。そのIndexに対するKeyとValueをSetKeyメソッドとSetString, SetLInt, SetBoolなどを用いて設定します。

PLC_PRG (つづき)
		// Index of the root object is -1.
		pJsonData^.SetObject(diIndex := diCounter, diParentIndex := -1);
		diCounter := diCounter + 1;
		// The parent index of the key is the index of parent object.
		pJsonData^.SetKey(wsKey := "Key1", diParentIndex := 0, diIndex := diCounter);
		diCounter := diCounter + 1;
		// The parent index of the value is the index of corresponding key.
		pJsonData^.SetString(wsValue := "Value1", diIndex := diCounter, diParentIndex := diCounter - 1);

		diCounter := diCounter + 1;
		pJsonData^.SetKey(wsKey := "Key2", diParentIndex := 0, diIndex := diCounter);
		diCounter := diCounter + 1;
		pJsonData^.SetLInt(liValue := 77, diIndex := diCounter, diParentIndex := diCounter - 1);
		
		diCounter := diCounter + 1;
		pJsonData^.SetKey(wsKey := "Key3", diParentIndex := 0, diIndex := diCounter);
		diCounter := diCounter + 1;
		pJsonData^.SetBool(bValue := FALSE, diIndex := diCounter, diParentIndex := diCounter - 1);	
	END_IF

以下の部分では、fb_JBuilderを使ってJSONデータを構築しています。fb_JBuilderを初期化し、SetKeyWithValueメソッドを用いてKeyとValueを設定する流れです。

PRG (つづき)
	// This example recreates the test.json file
	IF iExample = 2 THEN
		// Initialize fb and set root object
		fb_JBuilder(pJsonData := pJsonData, diRootObj => diCounter);
		wsValue := "Xema";
		// JSON:    "Herausgeber": "Xema", 
		fb_JBuilder.SetKeyWithValue("Herausgeber", wsValue, diParentIndex := diCounter);
		wsValue := "1234-5678-9012-3456";
		// JSON:   "Nummer": "1234-5678-9012-3456", 
		fb_JBuilder.SetKeyWithValue("Nummer", wsValue, diCounter);
		// JSON:   "Deckung": -4.44e-1, 
		fb_JBuilder.SetKeyWithValue("Deckung", lrValue, diCounter);
		wsValue := "EURO";
		// JSON:    "Waehrung": "EURO", 
		fb_JBuilder.SetKeyWithValue("Waehrung", wsValue, diCounter);
		// Set key with object, and override diParent. JSON:    "Inhaber": {
		diCounter := fb_JBuilder.SetKeyWithObject("Inhaber", diCounter);
		wsValue := "\ $"Mustermann\$"";
		// JSON:       "Name": "\ \"Mustermann\\"", 
		fb_JBuilder.SetKeyWithValue("Name", wsValue, diCounter);
		wsValue := "Max";
		// JSON:       "Vorname": "Max", 
		fb_JBuilder.SetKeyWithValue("Vorname", wsValue, diCounter);
		// JSON:       "maennlich": true, 
		fb_JBuilder.SetKeyWithValue("maennlich", xValue, diCounter);
		// Save parent to use after it after the array again
		diMemory := diCounter;

値に配列を渡す場合は、SetKeyWithArrayメソッドを使います。

PLC_PRG (つづき)
		// JSON:       "Hobbys":
		diCounter := fb_JBuilder.SetKeyWithArray("Hobbys", diCounter);
		// JSON:         ["Reiten", "Golfen", "Lesen"],
		fb_JBuilder.SetValue(wsArrValue[0], diCounter);
		fb_JBuilder.SetValue(wsArrValue[1], diCounter);
		fb_JBuilder.SetValue(wsArrValue[2], diCounter);
		// Use saved parent. JSON:       "Alter": 42, 
		fb_JBuilder.SetKeyWithValue("Alter", iValue, diMemory);
		// JSON:       "Kinder": 
		// JSON:         [],
		fb_JBuilder.SetKeyWithArray("Kinder", diMemory);
		// JSON:       "Partner": null
		fb_JBuilder.SetKeyWithValueNull("Partner", diMemory);
	END_IF
	xStart := FALSE;
END_IF

最終的にjsonArrayWriterで先ほど構築したJSON Dataを元にJSON Stringを作成します。

// Generate the JSON String. The value will be written to the variable wsJsonData.
jsonArrayWriter(xExecute := TRUE, pwData := ADR(wsJsonData), udiSize := SIZEOF(wsJsonData), jsonData := pJsonData^);

JSONArray Example

こちらは、JSON配列を生成・読み書きする基本例です。PLCデータを配列形式でJSONに変換して可視化します。JSONDataFactory -> JSONData -> JSONByteArrayWriterでメモリ領域確保、JSON Dataオブジェクトへのデータ設定、JSON String化の流れは先ほどの例と共通です。プログラムの処理はCFCで書かれていて、ハードコーディングされたWSTRINGのJSONを読み込んで、エレメントごとの表示やJSON Stringへの変換を行っています。

PLC_PRG
// This program shows how to read and write JSON arrays.
PROGRAM PLC_PRG
VAR
	// JSON data container
	jsonDataFactory : JSON.JSONDataFactory; // Factory to create a JSONData function block. 
	eDataFactoryError : FBF.ERROR; // Error code: 0: OK, 30103: no more memory
	pJSONData : POINTER TO JSON.JSONData := jsonDataFactory.Create(eError => eDataFactoryError); // Result of Create is a pointer to JSONData
	
	// Reader
	JSONByteArrayReader_0: JSON.JSONByteArrayReader;	
	xRead : BOOL;
	// Demo data
	wsData : WSTRING(1024) := "{
  $0022Herausgeber$0022: $0022Xema$0022,
  $0022Nummer$0022: $00221234-5678-9012-3456$0022,
  $0022Deckung$0022: -0.444,
  $0022Waehrung$0022: $0022EURO$0022,
  $0022Inhaber$0022: 
  {
    $0022Name$0022: $0022Mustermann$0022,
    $0022Vorname$0022: $0022Max$0022,
    $0022maennlich$0022: true,
    $0022Hobbys$0022: [ $0022Reiten$0022, $0022Golfen$0022, $0022Lesen$0022 ],
    $0022Alter$0022: 42,
    $0022Kinder$0022: [],
    $0022Partner$0022: null
  }
}";
	
	// Writer
	JSONByteArrayWriter_0: JSON.JSONByteArrayWriter;
	wsWriteData : WSTRING(1024) := ""; // The result of the Writer
	xWrite : BOOL;
	
	// Helper array for visu
	awsData :  ARRAY[0..3, 0..500] OF WSTRING(JSON.GParams.g_diMaxStringSize);	
	element2StringArray : Element2StringArray; // Function block to convert the sample data into an array of WSTRINGs (only for visualization)
END_VAR

JSON.GParams.g_diMaxStringSizeは、JSON Utilities SLライブラリParameterの1つです。最大のSTRINGSサイズを指定するパラメータです。

Element2StringArrayは、JSONの要素をWSTRINGの配列に変換するヘルパーFunction Blockです。CODESYS Editorの左下のPOUを選択するか、Go To Definitionからソースコードを確認できます。

JSONFileExample

3つ目の例は、JSONファイルを読み書きする例です。JSONFileReaderJSONFileWriterを使ってres/`フォルダ内で読み書きを行っています。先ほどと同様にCFCで処理が記載されています。違いは、プログラムにハードコードされていたJSONデータがファイル渡しになっている点です。これまで同様pJSONDataをFactory経由で作成し、それをJSONFileReaderに渡してFile中のデータを設定します。

PLC_PRG (抜粋、CFC部分は省略)
PROGRAM PLC_PRG
VAR
	// JSON data container
	// 省略
	
	// Reader
	JSONFileReader_0: JSON.JSONFileReader; 
	wsFilenameRead : STRING(255) := 'res/test.json';
	xReadFile : BOOL;
	
	// Writer
	JSONFileWriter_0: JSON.JSONFileWriter;
	wsFilenameWrite : STRING(255) := 'res/testWrite.json';
	xWriteFile : BOOL;
	
	// Helper array for visu
	awsData :  ARRAY[0..3, 0..500] OF WSTRING(JSON.GParams.g_diMaxStringSize);
	element2StringArray : Element2StringArray; // Function block to convert the sample data into an array of WSTRINGs (only for visualization)
END_VAR

JSONFindValueExample

最後の例は、JSON内の特定のキーを検索し、その値を取得する例です。先程の例と同様にFindFirstValueByKey FBを利用します。FindFirstValueByKeyは、Inputに検索キー(WSTRING)と検索開始Indexを指定することで、検索キーに対応する値(JSONElement型)を取得できます。CFCコード内では、JSONElementToString関数を呼び出し、JSONElement型のデータをWSTRING型に変換してVisuに使用しています。

PLC_PRG (抜粋、CFCは省略)
PROGRAM PLC_PRG
VAR
	sFileName : STRING(255) := 'res/test.json';	
	// Factory経由でJSONDataを用意
	// 省略

	JSONFileReader_0: JSON.JSONFileReader;
	xStart : BOOL;
	xFindValue : BOOL;
	wsKey : WSTRING := "Vorname";
	element : JSON.JSONElement;
	FindFirstValueByKey_0: JSON.FindFirstValueByKey;
	wsValue: WSTRING(JSON.GParams.g_diMaxStringSize);
END_VAR

CSV Utility

CSV Utilities SL Exampleには、CSVReaderExample, CSVWriterCFCExample, CSVWriterSTExample`の3つのアプリケーションが含まれています。

CSVWriterCFCExample(CFCでの書き込み)

まずは、CSVWriterCFCExampleを見ていきます。このExampleでは、FBをつないで処理を可視化する内容です。CSV.Initは、CSV Utility SLで提供されているCSVWriter初期化用のFunction Blockです。グローバル変数でCSV.CSVWriterが定義してあり、それを初期化するのに使います。初期化後にCSVWriterに対して、AddLine, AddStringなどを呼び出してデータを追加していきます。最終的にWriteFile Function Blockを使ってFileに書き出しています。

WriteValues
PROGRAM WriteValues
VAR
	sDir			: STRING := 'res';
	sFileName 		: STRING := 'CSVWriterCFCExampleData.csv';

	csvInit			: CSV.Init;
	blink			: BLINK;
	ctu				: CTU;	
	csvAddNewLine	: CSV.NewLine;
	csvWriteFile	: CSV.WriteFile;
	csvAddString	: CSV.AddSTRING;
	csvAddWord		: CSV.AddWord;
	xAddLineAndSave	: BOOL;
END_VAR

CSVWriterSTExample(STでの書き込み)

CSVWriterSTExampleは、GVL, DataObject(FB), GetTime(FUN), Sequential_Add_Values(PRG), Sequential_Save(PRG)など複数のファイルで構成されています。CSVWriterは、GVL内で宣言されています。

GVL
VAR_GLOBAL
	g_data	: DataObject;
	g_csv	: csv.CSVWriter;
END_VAR

DataObjectは、AddValueとSaveという2つのメソッドを持つFunction Blockです。AddValueメソッドは、g_csv.AddLTIME, g_csv.AddUDINTなどのメソッドを用いて値を設定しています。また、g_csv.NewLIne(), g_csv.NewFile()なども利用されています。Saveメソッドでは、g_csv.InitSave()メソッドによる初期化、g_csv.Save()によるBufferデータのファイルへの書き出しが実装されています。

残り2つのPRGファイルはタスクに設定されている処理です。

Sequential_Save
PROGRAM Sequential_Save
VAR
	xInit 			: BOOL;
	iSave 			: INT;
	udiFileSize 	        : __XWORD;
	
	sFileName 		: STRING:= 'dataFileSeq.csv';(* This name can be adapted *)
	sDirectoryPath     	: STRING := 'res';(* This path need to be adapted to an existing directory.*)
	
	eSaveResult		: CSV.CSV_ERROR; (* error happened by saving *)
	eError 			: CSV.CSV_ERROR;
END_VAR

IF NOT xInit THEN
	eError := g_csv.InitSave( sDirectoryPath:=sDirectoryPath, sFileName:=sFileName);
	IF eError <> CSV.CSV_ERROR.NO_ERROR THEN
		iSave := 32767;
	END_IF
	xInit := TRUE;
END_IF


CASE iSave OF
		
	0:
		(* Save the data from the buffer*)
		IF g_csv.Save() <> CSV.CSV_ERROR.NO_ERROR THEN
			iSave := 32767;
		END_IF
		(* When the file is bigger, than 8000 Byte, a new file with an generated name is created.*)
		udiFileSize :=  g_csv.GetFileSize(peError := ADR(eError));
		IF eError = CSV.CSV_ERROR.NO_ERROR AND udiFileSize > 8000 THEN
			g_csv.NewFile('');
		END_IF
		
		IF  eError <> CSV.CSV_ERROR.NO_ERROR THEN
			iSave := 32767;
		END_IF

	32767: (* Error *);
	
	
END_CASE
Sequential_Add_Values
PROGRAM Sequential_Add_Values
VAR	
	iAdd			: ULINT;
	eAddResult		: CSV.ERROR;									
	eAddError 		: CSV.ERROR;
	dtAddErrorTime 	: DATE_AND_TIME;
END_VAR

VAR_OUTPUT
	ERROR: INT;
END_VAR

(* Altogether 800 lines with different values are written into several csv files.*)

IF iAdd <= 800 THEN
	iAdd := iAdd + 1;
END_IF

(* Add 800 lines into the file*)
IF iAdd >= 0 AND iAdd <= 800 THEN
	 	eAddResult := g_csv.AddLTIME(LTIME());
		ErrorCheck();
		
		eAddResult :=  g_csv.AddBOOL(TRUE);
		ErrorCheck();

		eAddResult := g_csv.AddDINT(-1568745896);
		ErrorCheck();

		eAddResult := g_csv.AddTIME(TIME());
		ErrorCheck();
		
		eAddResult := g_csv.AddULINT(iAdd);
		ErrorCheck();
		
		eAddResult := g_csv.NewLine();
		ErrorCheck();
END_IF

ErrorCheck()は、Sequential_Add_Valuesに設定されたActionです。

CSVReaderExample(読み込み)

最後は、CSVデータをファイルから読み込む例です。このアプリケーションでは、NextElement、NextLine, Allの3種類の読み取り例が実装されています。CSVReaderInitが初期化用のFunction Blockです。後段のNextElement(FB), NextLine(FB), ReadAll(FB)には、CSVReaderInit型の変数をそのまま渡して使います。

Prog
PROGRAM Prog
VAR
	sFileName			: STRING := 'res/CSVReader.csv';	

	// 'NextElement' example
	csvReaderInit		: CSV.CSVReaderInit;
	nextElement		: CSV.NextElement;
	xNextElement	 	: BOOL;
	sElement		: STRING(200);
	psElement	     : POINTER TO STRING := ADR(sElement);
	
	// 'NextLine' example
	nextLine			: CSV.NextLine;
	csvReaderInit2		     : CSV.CSVReaderInit;
	xNextLine	 		: BOOL;
	sLine				: STRING(200);
	psLine				: POINTER TO STRING := ADR(sLine);
	
	// 'ReadAll' example
	csvReaderInit3		    : CSV.CSVReaderInit;
	xRead				: BOOL;
	readAll				: CSV.ReadAll;	
	saDataArray			: ARRAY[0..2, 0..10] OF STRING(100);
	asDataArray			: ARRAY[0..32] OF STRING(100);
	pasDataArray		    : POINTER TO ARRAY[0..32] OF STRING(100) := ADR(asDataArray);
	dataConvert			: ConvertDataArray;
	R_TRIG_0			: R_TRIG;
END_VAR

よくあるハマりどころ

  • 保存先パスresPLC側のファイルシステム(例:PlcLogic/res)。存在しなければ作成してください。
  • Init/InitSaveの回数:基本1回。毎周期呼ぶとハンドルやバッファが不安定になります。
  • 区切り文字CSVWriterCFCExample';' セミコロン。Excelで読み込む際の区切り設定に注意。
  • バッファ溢れCSV.ERROR.END_OF_BUFFER一時的な溢れSave() で解消。連発するなら周期や1行の列数を調整。
  • サイズローテーションGetFileSize() → 閾値超えで NewFile('')。ログ運用に便利。

まとめ

今回は、IIoT Exampleの中にあるJSONとCSVデータを扱うExampleを見てきました。JSONについては、Factoryからオブジェクトを作成し、ライブラリで用意されたメソッドを使ってデータの設定、JSON Stringの生成を行いました。CSVでは、JSON Utilityと異なり、WriterをInit FBで初期化したあと、操作用FBに渡してデータを処理する流れでした。以下の表に特徴をまとめておきます。

ライブラリ 目的 主要FB(例)
JSON Utilities 構造化データの保存/交換 AddKeyValue, WriteFile, FindValue
CSV Utility SL 記録/表データの入出力 InitSave, AddXXXX, NewLine, Save, ReadAll

Discussion