対象読者
- CODESYSでMap(HashTable)の実装方法を知りたい方
- オブジェクト指向プログラミング(OOP)の基礎(インターフェースの実装やポリモーフィズム)を理解している方
- CODESYSまたはTwinCATでST言語を使用した開発経験のある方
余談
最近CODESYSにはまっています。
オブジェクト指向を使いながら近代的なプログラミング言語のようにPLCプログラムを書くことができ、TwinCATと比べても価格も安いためです。
TwinCATには以下のようなメリットがあり、価格を取るか、メリットをとるかは、装置規模や導入台数によって変わってくると思います。
- POU毎にテキストファイルで保存されてGit管理がしやすい
- ADSが使えてPC側との通信がしやすい
- IDEがVisual StudioでCODESYSと比べて軽量かつ使いやすい
- ハードウェア(Bekckhoff IPC/EPC)のラインナップが多い
- トライアルのランタイムライセンスが1週間継続(CODESYSは2時間ごとに切れる)し、実機ではない開発端末でのテストがしやすい
当記事記載の背景
最近CODESYSで実装したプログラムで、STRING型のIDをKeyに構造体のワーク情報を管理したい場合がありました。
C++やC#といったIT向けのプログラミング言語においては、このようなケースでは一般的にKey-Value形式のCollectionのMap(C#やPythonではDictionaryという名前)を使用します。
三菱電機やキーエンスといったクラシックPLCでこのような機能を実装したい場合には、配列で持たせて、値を取り出したい場合には毎回走査するといった方法になるのでしょうか(クラシックPLCに詳しくないため、もし他に良い方法があれば教えてください)。
ただしその場合には値を取り出すごとにO(N)の処理が走り、処理効率が悪く、ワークの種類数によっては目標とするスキャンタイムに収まらなくなることも出てきます。
CODESYSではC++やC#同様にCollectionやMapがライブラリとして提供されています。
具体的な(インターフェースではない)FBとしてはHashTableが提供されています。
当記事では INTやSTRINGという基本的な型をKey,Valueとする場合と、独自の構造体(STRUCT)をValueとする場合の実装方法を説明します。
基本型をKey,Valueとする場合のHashTableの実装
HashTableの基本的な使い方についてはこちらのExample: Element Collectionsを見るのが一番わかりやすいと思います。
基本型をKey,Valueとする場合はExample: Element CollectionsのSimpleHashTableExampleにある通りのため、ソースコードを引用し、簡単な解説を行います。
// This example shows how to add key value pairs to hash tables and how to get a value by key.
PROGRAM PLC_PRG
VAR
HashTableFactory : COL.HashTableFactory; // Factory to create a hash table on heap memory.
itfMap : COL.IMap := HashTableFactory.Create(udiMaxElements := 100); // Creates a hashtable on heap memory. NOTE: Collections that have been created with this Factory are not online change safe.
StringElementFactory : COL.StringElementFactory; // Factory to create StringElements on heap memory.
state : ActionState := ActionState.IDLE;
sKey : STRING := 'Enter a unique key';
sValue : STRING := 'Enter a value';
sKeyFind : STRING;
itfStringKey : COL.IStringElement;
itfIInstance : FBF.IInstance;
sValueFound : STRING;
eError : COL.COLLECTION_ERROR;
aStringValues : ARRAY[0..99] OF STRING;
iterator : COL.ListIterator;
itfElement : COL.IElement;
itfStringElement : COL.IStringElement;
i : INT;
xResult: BOOL;
END_VAR
CASE state OF
ActionState.IDLE:
// Idle
ActionState.ADD_ELEMENT:
// Add key value pair
eError := itfMap.AddKeyValuePair(itfKey := StringElementFactory.Create(sValue := sKey), itfValue := StringElementFactory.Create(sValue := sValue));
IF eError = COL.COLLECTION_ERROR.NO_ERROR THEN
state := ActionState.UPDATE_VISU;
ELSE
state := ActionState.IDLE;
END_IF;
ActionState.FIND_ELEMENT_BY_KEY:
// Finds a value by key
itfStringKey := StringElementFactory.Create(sValue := sKeyFind);
eError := itfMap.GetElementByKey(itfKey := itfStringKey, itfValue => itfElement);
xResult := __QUERYINTERFACE(itfStringKey, itfIInstance);
IF xResult THEN
// Remove the temporary key
itfIInstance.Dispose();
END_IF
// Set the found value
sValueFound := '';
xResult := __QUERYINTERFACE(itfElement, itfStringElement);
IF xResult THEN
sValueFound := itfStringElement.StringValue;
END_IF
IF eError = COL.COLLECTION_ERROR.NO_ERROR THEN
state := ActionState.UPDATE_VISU;
ELSE
state := ActionState.IDLE;
END_IF;
ActionState.UPDATE_VISU:
// Clear visu array
FOR i := 0 TO 9 DO
aStringValues[i] := '';
END_FOR
eError := itfMap.Keys(itfIterator := iterator);
i := 0;
WHILE iterator.HasNext() DO
iterator.Next(itfElement => itfElement);
// Cast IElement to IStringElement
__QUERYINTERFACE(itfElement, itfStringElement);
aStringValues[i] := itfStringElement.StringValue;
i := i + 1;
END_WHILE
state := ActionState.IDLE;
END_CASE
Exampleから重要な部分を抜粋して説明します。
まず以下で必要なFacotry FBの定義と、HashTableの作成を行っています。
HashTableFactory : COL.HashTableFactory; //HashTable自体のFactory FB
itfMap : COL.IMap := HashTableFactory.Create(udiMaxElements := 100); //100個の要素を持つHashTableのインスタンス生成
StringElementFactory : COL.StringElementFactory; //StringElement型のインスタンスを生成するためのFactory FB
ポイントとして、このCollectionライブラリの要素にSTRING型やINT型の値を直接入れることはできず、Key,ValueともIElement型を実装(Implements)したFBとする必要があります。
INT型やSTRINGといった基本型はIElementを実装済みのIIntElementやIStringElement型のFB(ITF)を生成してくれるFactoryメソッドが用意されているため、これを使用すれば比較的簡単に実装できます。
HashTableへの要素の追加は以下にて実施しています。
eError := itfMap.AddKeyValuePair(itfKey := StringElementFactory.Create(sValue := sKey), itfValue := StringElementFactory.Create(sValue := sValue));
Key, Valueとも追加時に動的にFBのインスタンスをFactoryで生成して追加してます。
追加済みの要素の取得は以下で実施しています。
itfStringKey := StringElementFactory.Create(sValue := sKeyFind);
eError := itfMap.GetElementByKey(itfKey := itfStringKey, itfValue => itfElement);
xResult := __QUERYINTERFACE(itfStringKey, itfIInstance);
IF xResult THEN
// Remove the temporary key
itfIInstance.Dispose();
END_IF
// Set the found value
sValueFound := '';
xResult := __QUERYINTERFACE(itfElement, itfStringElement);
IF xResult THEN
sValueFound := itfStringElement.StringValue;
END_IF
追加と比べて取得は少々複雑です。
GetElementByKeyメソッドの返り値はIElement型(コード内ではitfElementという変数)となります。IElement型のままではオブジェクト指向的な意味で抽象度が高すぎて目的の値を取り出せないため、__QUERYINTERFACEでIStringElement型(コード内ではitfStringElementという変数)に変換しています。
こうすることで取得したいSTRING型の値を取り出せます。
独自作成の構造体をValueとする場合のHashTableの実装
サンプル
当記事のサンプルはこちらのGitHubに置いています。
当記事の内容のみではなく、動作がわかりやすくするためにHashTableを活用した製品登録・編集用のVisualizationも作成してます。
前章の基本型を使用する場合においてもC#やPythonといったIT向けの言語に比べるとなかなか手間ですが、独自作成の構造体を使用する場合には正直かなりの手間が発生します。
前章までの用意されている基本型の場合と比べ、独自の構造体を使用する場合には以下が必要となります。
- IElementをImplementsした独自Elementの実装
- 独自Element型のFBインスタンスを動的に生成するFactoryの実装
実装のイメージとしては、Example: Element CollectionsのSimpleHashTableExampleとElementExampleを組み合わせます。
今回はKeyをSTRING型、Valueを以下の独自の構造体として追加したい場合でご説明します。
TYPE ST_Product :
STRUCT
//製品情報
id : STRING; //製品ID
name : STRING; //製品名
width : UINT; //幅
length : UINT; //長さ
height : UINT; //高さ
END_STRUCT
END_TYPE
IElementをImplementsした独自Elementの実装
インターフェースの実装
実態がないインターフェースの定義のみのためシンプルに以下の通りです。
PRG_ProductSetting.state <> E_ProductSettingState.Edit
PROPERTY ProductValue : ST_Product
実態のFBの実装
インターフェースは不要で実態のみの実装にできるとよいですが、FACTORYや値取得時に__QUERYINTERFACEを使用する仕様上、それぞれの実装が必要なようです。
// Demo element with two integer values.
// An element must implement an interface which extends |COL.IElement|.
// This function block extends FBF.InstanceBase to enable dynamic creation via FBFactory.
{attribute 'no_explicit_call' := 'Explicit calls not allowed.'}
FUNCTION_BLOCK FB_ProductElement EXTENDS FBF.InstanceBase IMPLEMENTS IProductElement
VAR_INPUT CONSTANT
_product : ST_Product;
END_VAR
VAR_OUTPUT
END_VAR
VAR
END_VAR
// Compares this element with itfElement.
// Returns 0 if the elements are equal, < 0 if the element is less than itfElement,
// > 0 if the element is greater than itfElement.
// This method will be called from sorted collections (e.g. |COL.SortedList|) to sort the elements.
// IMPORTANT: The underlying value to be compared with MUST NOT be changed during the lifecycle of the object.
METHOD ElementCompareTo : INT
VAR_INPUT
(* The element to compare*)
itfElement : COL.IElement;
END_VAR
VAR
element : IProductElement;
xResult : BOOL;
elementProduct : ST_Product;
END_VAR
// We use integer iInt1 for sorting.
xResult := __QUERYINTERFACE(itfElement, element);
IF xResult THEN
elementProduct := element.ProductValue;
IF _product.id < elementProduct.id THEN
ElementCompareTo := -1;
ELSIF _product.id > elementProduct.Id THEN
ElementCompareTo := 1;
ELSE
ElementCompareTo := 0;
END_IF
ELSE
ElementCompareTo := -1;
END_IF
// Returns true of this element and itfElement are equal.
// This method will be called from the function block |COL.HashTable| to find an element by key if the hashcode of an element collides with an other element.
// IMPORTANT: The underlying value to be compared with MUST NOT be changed during the lifecycle of the object.
METHOD ElementEquals : BOOL
VAR_INPUT
(* The element to compare*)
itfElement : COL.IElement;
END_VAR
VAR
element : IProductElement;
xResult : BOOL;
elementProduct : ST_Product;
thisProduct : ST_Product;
END_VAR
xResult := __QUERYINTERFACE(itfElement, element);
elementProduct := element.ProductValue;
thisProduct := ProductValue;
IF xResult THEN
ElementEquals := thisProduct.id = elementProduct.id;
ELSE
ElementEquals := FALSE;
END_IF
// Returns the hashcode of this element.
// This method is used by hash based collections (e.g. |Hashtable|) to get the hashcode from a element.
// IMPORTANT: The value of the method MUST NOT be changed during the lifecycle of the object.
METHOD ElementHashCode : LINT
VAR
p : POINTER TO STRING;
END_VAR
p := ADR(_product.id);
ElementHashCode := COL.HashCodeFromString(p);
// This methode is called from the factory to give a chance
// to initialize the new fb instance with the content of itfData
METHOD prvInstInit : FBF.ERROR
VAR_INPUT
itfData : FBF.IData;
END_VAR
VAR
pData : POINTER TO FB_Product;
xOk : BOOL;
thisProduct : ST_Product;
END_VAR
SUPER^.prvInstInit(itfData);
xOk := __QUERYPOINTER(itfData, pData);
IF xOk THEN
ProductValue := pData^.info;
//END_IF
PROPERTY ProductValue : ST_Product
//Get
ProductValue := _product;
//Set
_product := ProductValue;
実際に比較など行わない場合には、ElementCompareTo、ElementEquals、ElementHashCodeの実装は適当でも良いと思います。
Factoryの実装
このソースについて、正直私は全ては理解できていないです。。
必要な部分のみサンプルから修正して実装しています。
// IElement factory for INT values
{attribute 'enable_dynamic_creation'}
FUNCTION_BLOCK FB_ProductElementFactory EXTENDS FBF.FactoryBase
VAR
END_VAR
SUPER^();
(* Creates a IntElement *)
METHOD Create : IProductElement
VAR_INPUT
inputData : ST_Product;
END_VAR
VAR_OUTPUT
eError : FBF.ERROR;
END_VAR
VAR
hInst : CAA.HANDLE;
itfInst : FBF.IInstance;
xOk : BOOL;
itfData : FB_Product;
pInstance : POINTER TO FB_ProductElement;
END_VAR
(* !!! This code must not be subject to change !!! *)
itfData(info:=inputData);
hInst := SUPER^.prvAllocInstMem(eError => eError);
pInstance := SUPER^.prvGetInstPointer(hInst);
IF pInstance <> 0 AND eError = FBF.ERROR.NO_ERROR THEN
{implicit on}
pInstance^.__vfInit();
pInstance^.FB_Init(TRUE, FALSE);
{implicit off}
xOk := __QUERYINTERFACE(pInstance^, itfInst);
IF xOk THEN
xOk := SUPER^.prvInstInit(itfInst, hInst, itfData, eError=>eError);
ELSE
eError := FBF.ERROR.WRONG_INTERFACE;
END_IF
IF NOT xOk THEN
SUPER^.prvFreeInstMem(hInst);
Create := CAA.gc_pNULL;
ELSE
__QUERYINTERFACE(pInstance^, Create);
END_IF
END_IF
prvInstCount、prvInstPoolExtendsFactor、prvInstSizeはサンプルのままのため省略します。
Factoryで動的にCreateする実態のために以下のFB実装が必要です。構造体を直接生成できたらよかったですが、FBF.InstanceDataを継承する必要があるようで(※2)、別途作成が必要です。
※2 CODESYSは構造体はPOUではなくDUTというデータを扱うものであり、C++のようにclass(FB)のような役割を持たせられません。
{attribute 'enable_dynamic_creation'}
{attribute 'hide'}
FUNCTION_BLOCK FB_Product EXTENDS FBF.InstanceData
VAR_INPUT
info : ST_PRODUCT;
END_VAR
(* !! Do not add any executable code here !! *)
SUPER^();
使用方法例
要素の追加
PROGRAM PRG_ProductSetting
VAR
HashTableFactory : COL.HashTableFactory; // Factory to create a hash table on heap memory.
productMap : COL.IMap3 := HashTableFactory.Create(udiMaxElements := 100); // Creates a hashtable on heap memory. NOTE: Collections that have been created with this Factory are not online change safe.
productElementFactory : FB_ProductElementFactory;
eError : COL.COLLECTION_ERROR;
StringElementFactory : COL.StringElementFactory; // Factory to create StringElements on heap memory.
keyElement : COL. IStringElement;
END_VAR
keyElement := StringElementFactory.Create(sValue := visuProduct.id);
eError := productMap.AddKeyValuePair(itfKey := keyElement, itfValue := productElementFactory.Create(visuProduct));
作成したFB_ProductElementFactoryを使用してIProductElement型の要素としてproductMapに追加しています。
値の取得
METHOD GetProductInfo : ST_Product
VAR_INPUT
id : STRING;
END_VAR
VAR
itfStringKey : COL.IStringElement;
itfIInstance : FBF.IInstance;
eError : COL.COLLECTION_ERROR;
itfElement : COL.IElement;
itfProductElement : IProductElement;
StringElementFactory : COL.StringElementFactory; // Factory to create StringElements on heap memory.
xResult: BOOL;
END_VAR
//MapからProductインスタンスを取得
itfStringKey := StringElementFactory.Create(sValue := id);
eError := productMap.GetElementByKey(itfKey := itfStringKey, itfValue => itfElement);
IF eError <> COL.COLLECTION_ERROR.NO_ERROR THEN
RETURN;
END_IF
//キーの確認
xResult := __QUERYINTERFACE(itfStringKey, itfIInstance);
IF xResult THEN
// Remove the temporary key
itfIInstance.Dispose();
END_IF
//IElement形式からIProductElementに変換
xResult := __QUERYINTERFACE(itfElement, itfProductElement);
IF xResult THEN
GetProductInfo := itfProductElement.ProductValue;
END_IF
Mapから直接取得できる型はIElement型のため、__QUERYINTERFACEでIProductElement型に変換することで値を取り出せます。
最後に
独自構造体をValueとするHashMapの実装はExample: Element Collectionsの組み合わせではありますが、独自のElement実装方法など、きちんと理解して動作させるまでに結構時間がかかりました。そのため、同様のことをしたい人の理解を促進できるように、今回記事にして皆さんに共有した次第です。
Pythonはほとんど型を意識せずに使用できますし、C#ではジェネリクスを使用して簡単に独自classのDictionaryを実装できます。そのため、IT向け言語に比べるとCODESYSでのMapの実装はかなり面倒に感じます。
しかし、Mapの実装は従来のクラシックPLCではできない実装であり、Mapを使いこなせればソフトウェアPLCであるCODESYSを使用するメリットを感じられるのではないでしょうか!