CODESYSでのステートマシン実装方法の比較

## 初めに
私はFAの制御設計としてステートマシン図が一番書きやすいと考え、ステートマシン図を作成して設計しています。
もちろん必要があれば、その他にシーケンス図などを作成することもあります。
ただ、シーケンス図は他人向け（例えばソフトを外注する場合など）には登場人物や処理内容を明確にするためには有効ですが、実装前の設計としては細かくなりすぎて時間がかかり、シリアルでない処理が書きづらいといった欠点もあります。
そのため、自身で設計して自身で実装する場合においては、ステートマシン図ほどの粒度がちょうどよいかと思っています。

TwinCAT/C++を使用する時にはC++向けのステートマシン用ライブラリの[boost/sml](https://github.com/boost-ext/sml)などが使用できました（※1）。しかし、CODESYS向け（IEC61131-3言語向け）にはオープンソース文化も少なく、有名なものがありません。
そのため、まずは公式で用意された方法として、以下方法でのステートマシン実装方法の比較を行いました。
- ①ST（ライブラリ未使用）
- ②LD
- ③SFC
- ④CODESYS UML

※1 TwinCAT/C++は使用できるSTLが限られているため（[参考](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_c/1298172427.html&id=)）使用できるライブラリにも制限がありますが、[boost/sml](https://github.com/boost-ext/sml)は問題なく使用できました。

## 実装するステートマシン例

今回実装するステートマシンとして、ある程度の量がないとそれぞれの手法のメリデメも出づらいかと思い、下図のようなコンベヤで流れてきたワークを上下反転させる装置を想定することとします。
![反転機イメージ図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067155/a0aaff12-b2df-0e46-a2b5-746fbe64dcfe.png)


ステートマシン図として表すと下図の通りです。
![StateMachineDiagram_反転機.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067155/9bd65fb8-f541-3a76-f325-b9ccdfac112d.png)

このステートマシン図は[PlantUML](https://plantuml.com/ja/)で書いています。
今回の記事の趣旨とは関係ありませんが、PlantUMLはテキストベースでUMLの図が書けて、Gitでも管理でき、便利です。参考までにPlantUMLのソースコードも張っておきます。

```python
@startuml StateMachineDiagram_反転機
    skinparam defaultFontName "Yu Gothic UI"
    title 反転機
    hide empty description

    ' 全体状態遷移
    state "原点復帰待ち" as WaitingHoming
    WaitingHoming : entry / 全駆動即時停止
    state "原点復帰中" as Homing
    Homing : entry / 原点復帰動作開始
    state "待機中" as Idling
    
    '立上後動作
    [*] --> WaitingHoming
    WaitingHoming --> Homing : 原点復帰ボタン押下後
    Homing --> Idling : 原点復帰完了後

    '異常・非常停止時
    Auto --> WaitingHoming : 非常停止時
    Idling --> WaitingHoming : 非常停止時
    Homing --> WaitingHoming : 非常停止時

    ' 自動モード状態遷移
    Idling --> Auto : 運転ボタン押下時
    state  Auto {
        
        state "停止中_ワークなし" as Stopped_withoutWork
        state "停止中_ワークあり" as Stopped_withWork
        state ExistWork_A <<choice>>

        state "ワーク送り出し中" as SendingWork
        SendingWork : entry / コンベヤ正転開始
        SendingWork : exit / コンベヤ停止
        state "ワーク受け入れ中" as ReceivingWork
        ReceivingWork : entry / コンベヤ正転開始
        ReceivingWork : exit / コンベヤ停止
        state "ワーク反転中" as FlippingWork
        FlippingWork : entry / 反転機正転開始
        FlippingWork : exit / 反転機停止
        state "反転戻し中" as ResettingFlipped
        ResettingFlipped : entry / 反転機逆転開始
        ResettingFlipped : exit / 反転機停止
        
        ' 初期遷移
        [*] --> ExistWork_A
        ExistWork_A -->Stopped_withoutWork : ワークなし
        ExistWork_A --> FlippingWork : ワークあり

        ' ワーク受け入れ
        Stopped_withoutWork --> ReceivingWork : 前工程のワーク送り出し開始時
        ReceivingWork --> FlippingWork : 受け入れ完了後
        FlippingWork --> Stopped_withWork : 反転完了後

        ' ワーク送り出し
        Stopped_withWork --> SendingWork : 次工程への送り出し可能時
        SendingWork --> ResettingFlipped : ワーク送り出し完了後
        ResettingFlipped --> Stopped_withoutWork : 反転戻し完了後
        
        ' 停止ボタン動作
        Stopped_withWork --> Idling : 停止ボタン押下時
        Stopped_withoutWork --> Idling : 停止ボタン押下時
    }
    state "自動モード" as Auto

@enduml
```

## 実装例

### ①ST言語

よく使われる手法かと思いますが、Enum＋CASE OFを使用し、標準的なST言語としての実装しました。
ステートマシン図とセットで見れば読みづらくはないですが、Entryを遷移元で実装して重複の実装となっている部分など気になる部分があります。

E_States(ENUM)

```pascal
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE E_States :
(
	Initial := 0,
	WaitingHoming,			//原点復帰待ち
	Homing,					//原点復帰中
	Idling,					//待機中
	Auto,					//自動モード
	Stopped_withoutWork,	//停止中_ワークなし
	Stopped_withWork,		//停止中_ワークあり
	SendingWork,			//ワーク送り出し中	
	ReceivingWork,			//ワーク受け入れ中
	FlippingWork,			//ワーク反転中
	ResettingFlipped		//反転戻し中
);
END_TYPE

```

PRG_ST(PRG)

```pascal
PROGRAM PRG_ST
VAR
	currentState : E_States;
END_VAR

//非常停止時
IF FlippingMachine.isEmergencyStopped THEN
	FlippingMachine.StopAll();
	currentState := E_States.WaitingHoming;
END_IF

CASE currentState OF
	//初期状態
	E_States.Initial:
		currentState := E_States.WaitingHoming;
		
	//原点復帰待ち
	E_States.WaitingHoming:
		IF FlippingMachine.homingButton THEN
			FlippingMachine.StartHoming();
			currentState := E_States.Homing;
		END_IF
		
	//原点復帰中
	E_States.Homing:
		IF FlippingMachine.isCompleteHoming THEN
			currentState := E_States.Idling;
		END_IF
		
	//待機中
	E_States.Idling:
		IF FlippingMachine.runButton THEN
			currentState := E_States.Auto;
		END_IF
		
	//自動モード
	E_States.Auto:
		IF FlippingMachine.existWork THEN
			FlippingMachine.StartFlipping();
			currentState := E_States.FlippingWork;
		ELSE
			currentState := E_States.Stopped_withoutWork;
		END_IF
		
	//停止中_ワークなし
	E_States.Stopped_withoutWork:
		IF FlippingMachine.stopButton THEN
			currentState := E_States.Idling;
		ELSIF FlippingMachine.PrevProcess_isWorkSending THEN
			FlippingMachine.RunConveyor();
			currentState := E_States.ReceivingWork;
		END_IF
		
	//停止中_ワークあり
	E_States.Stopped_withWork:
		IF FlippingMachine.stopButton THEN
			currentState := E_States.Idling;
		ELSIF FlippingMachine.NextProcess_isWaitingWork THEN
			FlippingMachine.RunConveyor();
			currentState := E_States.SendingWork;
		END_IF
		
	//ワーク送り出し中
	E_States.SendingWork:
		IF FlippingMachine.isCompleteSending THEN
			FlippingMachine.StopConveyor();
			FlippingMachine.StartResettingFlipped();
			currentState := E_States.ResettingFlipped;
		END_IF
		
	//ワーク受け入れ中
	E_States.ReceivingWork:
		IF FlippingMachine.isCompleteReceiving THEN
			FlippingMachine.StopConveyor();
			FlippingMachine.StartFlipping();
			currentState := E_States.FlippingWork;
		END_IF
		
	//ワーク反転中
	E_States.FlippingWork:
		IF FlippingMachine.isCompleteFlipping THEN
			FlippingMachine.StopFlipping();
			currentState := E_States.Stopped_withWork;
		END_IF
		
	//反転戻し中
	E_States.ResettingFlipped:
		IF FlippingMachine.isCompleteResettingFlipped THEN
			FlippingMachine.StopFlipping();
			currentState := E_States.Stopped_withoutWork;
		END_IF
		
END_CASE
```

### ②LDでの実装

PLCではよく使用されるLD（ラダー・ダイアグラム）での実装例です。
HMIで表示ができるようになったりと、最近少し話題となったCODESYSのLD2で実装してみました。

私は、普段はラダーをほとんど書かないため、変な実装があったらすみません。
今回はST同様にENUMを使用しましたが、ENUMは使用せずに一つ一つの状態をBOOLで表したほうがラダーっぽいかもしれません。
各状態をBOOLで表すと使用する変数は増えますが、立ち上がり、立下りでのEntry、Exit実装はしやすそうです。

また、STではEntry、Exit相当の処理をそれぞれ必要なタイミングで一度呼び出すようにしていましたが、ラダーでは有効な間運転させる信号を立てるイメージで実装しています。
IT的な観点では無駄な処理をさせないためにも必要なタイミングのみで必要な処理をさせることが多いと思いますが、OT的（もといラダー的）には有効な間同じ処理をさせ続けるような書き方が一般的かと思うので、そちらに倣っています。

ラダーは量が増えると見づらくなる印象がありますが、きちんとコメントを書けば、可読性に関してはSTと遜色なく感じます。
ラダー自体リレー値を保持して（ステートフルに）順次処理をしていくため、ステートマシン図との相性はいいのかもしれません。

（スクショをPowerPointでつなぎ合わせて画像を作ったが、ラダー全体を画像化するいい方法ないでしょうか？）
![LDSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067155/05925ba7-9d68-07cc-3e28-e5c7c8181fe0.png)


### ③SFCでの実装

SFC自体そこまで詳しくないですが、ステートマシン図の状態が順次遷移していく感じがSFCにも近いように思い、SFCで実装できないか確認してみました。
SFCで書ければ、IEC-61131-3準拠のため、Codesysに依存せず書くこともできそうです。

試行錯誤した結果として、SFCでは順次のステップ遷移のみを前提としており、場合分けで状態が分岐するような処理を書けませんでした。もし、SFCでステートマシン図相当が書ける場合には、書き方をどなたか教えていただけると幸いです。

（書きかけたSFC）
![SFCSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067155/3eb759ae-70d0-57eb-e338-75e788757f3f.png)


### ④CODESYS UMLの使用

CODESYSには、公式にステートマシン図やクラス図が書ける[CODESYS UML](https://store.codesys.com/en/codesys-uml.html)という機能が用意されています。
CODESYS UMLは[CODESYS Professional Developer Edition](https://www.codesys.com/products/codesys-engineering/professional-developer-edition.html)の機能の一部のため、課金をしないと使えません。今回は30日間のデモ版で実装しています。
実は、今回この記事を書いた一番の目的は、これを試して比較してみたくて書きました。
CODESYSのOEMのTwinCATでも同様の機能[TF1910](https://www.beckhoff.com/ja-jp/products/automation/twincat/tfxxxx-twincat-3-functions/tf1xxx-system/tf1910.html)があります。

CODESYS UMLを書いてみた結果として、元々書いていたステートマシン図をそのまま図にできるため、とても楽でした。操作もマウスで直感的に操作できるため、初めてでも簡単にできました。
正直CODESYS UMLを使うなら、事前にPlantUML等でステートマシン図を描かず、直接実装するのもありだと思います。

ただ、今回実装したもののタスクに紐づけることができずうまくテストはできませんでした。サイクリックタスクには紐づかないのかとも思いましたが、タスクの種類を色々変えてもタスク化できず。。
[公式ヘルプ](https://content.helpme-codesys.com/en/CODESYS%20UML/f_uml_sc.html)も見ましたがよくわからず、どなたかやり方わかる人がいたら教えてください。。
![UMLSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067155/d96b1e2e-5dcb-886a-40a4-66dc821eae20.png)


## 終わりに

今回反転機のステートマシン図を題材として、①ST（ライブラリ未使用）、②LD、③SFC、④CODESYS UMLで実装し、比較してみました。

実装のしやすさとしては、以下の通りでした。
④CODESYS UML　＞　①ST（ライブラリ未使用）≒②LD
※③SFCはそもそも実装できない

ただ、④CODESYS UMLは[CODESYS Professional Developer Edition](https://www.codesys.com/products/codesys-engineering/professional-developer-edition.html)の一部であり、使用するためには550ユーロ/year・userを払い続けなければなりません。
組織としてCODESYSをゴリゴリ使い、CODESYS Gitなども使うのであれば、CODESYS Professional Developer Editionの契約を検討するのもありかとは思います。ただ、長期的な保守も考えて、CODESYS Professional Developer Editionの機能を使用する決断を、私は今行えません。

[FB_CaseStateMachine](https://github.com/FellowWithLaptop/FB_CaseStateMachine)など、ST言語でもステートマシン用のオープンソースを公開してくれている人もいるため、なるべくCODESYS UMLを使わずにきれいに実装できる方法は引き続き検討していきたいと思います。
[FB_CaseStateMachine](https://github.com/FellowWithLaptop/FB_CaseStateMachine)はパッと見た限り、`REPEAT`を使用した呪文のようなものが必要だったり、STで実装しようと思うとこうなるのか～というのが今のところの感想です（それでも素の`CASE OF`を使用する実装に比べるとスマートに実装できそうですが！）。
他方法も含め、時間があるときにもっとじっくり見てみたいと思います。