1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CODESYSで任意の構造体でのMap(HashTable)実装

Last updated at Posted at 2025-01-25

対象読者

  • 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を実装済みのIIntElementIStringElement型の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も作成してます。

example-ss.png

前章の基本型を使用する場合においても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を使用するメリットを感じられるのではないでしょうか!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?