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 の流れが基本パターンです。
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です。このあとも出てくるのでそこで詳しく説明します。
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などを用いて設定します。
// 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を設定する流れです。
// 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メソッドを使います。
// 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への変換を行っています。
// 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ファイルを読み書きする例です。JSONFileReaderとJSONFileWriterを使ってres/`フォルダ内で読み書きを行っています。先ほどと同様にCFCで処理が記載されています。違いは、プログラムにハードコードされていたJSONデータがファイル渡しになっている点です。これまで同様pJSONDataをFactory経由で作成し、それをJSONFileReaderに渡してFile中のデータを設定します。
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に使用しています。
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に書き出しています。
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内で宣言されています。
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ファイルはタスクに設定されている処理です。
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
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型の変数をそのまま渡して使います。
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

よくあるハマりどころ
-
保存先パス:
resはPLC側のファイルシステム(例: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