🍳

CASEステートマシンによるファイルストリーム

2024/11/20に公開

はじめに

本記事は、PLC(Programmable Logic Controller)のソフトウェア開発に従事、または関心のある方で、Structured Text(ST)による開発に興味がある方向けです。OMRON社のSysmac Studioを使用します。

CASEステートマシンの使用例として、ファイルストリームを挙げます。プロジェクトは以下より入手してください。コントローラは不要ですが、SDカード命令のエラーを確認したい場合は必要です。

https://github.com/kmu2030/CaseStateMachineExample_FileStreamReader

ファイルストリームは、SDカード命令を実行するFBと、ストリームを使用するためのFUNから成ります。関連するFBとFUNを介在する構造体をリングバッファとして、いい具合にデータを出し入れします。

FileStreamReader FB

FileStreamReaderの役割は、ファイルからコンテンツを読み出し、ストリーム(Context)に供給することです。ストリームは構造体で、バッファであると同時にFBと使用コード間の連絡役と考えてください。変数定義は省略します。"i"を内部変数プレフィックスとして使用していますが、SDカード命令は読み取れると思います。まずは、状態変数(iState)の変化を追いながら、処理の流れを追ってみてください。日常的に触れるSTに比べると分量が多いかもしれませんが、多くは値の設定、SDカード命令の実行とその結果待ちです。

Done := FALSE;
IF Execute AND NOT Busy THEN
	Busy := TRUE;
	
	iState := STATE_INIT;
END_IF;

CASE iState OF
	// 初期化状態
	// FBを初期化し、パラメータを固定します。
	0: // STATE_INIT
		iFileOpen.Execute := FALSE;
		iFileClose.Execute := FALSE;
		iFileSeek.Execute := FALSE;
		iFileRead.Execute := FALSE;
		
		iPath := Path;
		iBufferSize := SizeOfAry(Context.Buffer);
		// バッファの半分未満を読み込み閾値とします。
		iReadThreshold := iBufferSize / 2;
		// 例外時は200サイクル待機します。
		iRetryTick := 200;
		
		Clear(Context);
		
		iState := STATE_WAIT;
		
	// 処理待機状態
	10: // STATE_WAIT
		IF NOT Execute THEN
			iState := STATE_DONE;
		ELSE
			// バッファ残量が閾値未満になったら、読み出し処理を実行します。
			IF (Context.WriteSize - Context.ReadSize) < iReadThreshold THEN
				iReturnState := STATE_WAIT;
				
				iState := STATE_READ;
			END_IF;
		END_IF;

	// 例外状態
	// 時間をおいてから再度試みます。
	15: // STATE_EXCEPTION
		iWaitTick := iRetryTick;
		
		Inc(iState);
	16:
		Dec(iWaitTick);
		IF iWaitTick < 1 THEN
			iState := STATE_WAIT;
		END_IF;

	// ファイル末尾到達状態
	20: // STATE_EOF
		IF NOT Execute THEN
			iState := STATE_DONE;
		END_IF;
		
	// エラー状態
	30: // STATE_ERROR
		Context.Error := TRUE;
		Context.ErrorID := iErrorID;
		Context.ErrorIDEx := iErrorIDEx;
		
		Inc(iState);
	31:
		IF NOT Execute THEN
			iState := STATE_DONE;
		END_IF;
	
	// 読み出し処理実行状態
	// ファイルアクセス関連の定型処理が殆どです。
	50: // STATE_READ
		Clear(iError);
		Clear(iErrorID);
		Clear(iErrorIDEx);
	
		iFileOpen.FileName := iPath;
		iFileOpen.Mode := _eFOPEN_MODE#_READ_EXIST;
		iFileOpen.Execute := TRUE;
		
		Inc(iState);
	51:
		IF iFileOpen.Done OR iFileOpen.Error THEN
			iFileOpen.Execute := FALSE;
			
			IF iFileOpen.Error THEN
				iError := TRUE;
				iErrorID := iFileOpen.ErrorID;
				iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16)
					OR WORD_TO_DWORD(iErrorID);
				
				CASE WORD_TO_UINT(iErrorID) OF
					16#1405,16#140B: iReturnState := STATE_EXCEPTION;
				ELSE
					iReturnState := STATE_ERROR;
				END_CASE;
				
				iState := STATE_READ_DONE;
			ELSE	
				iFD := iFileOpen.FileID;
			
				iState := iState + 4;
			END_IF;
		END_IF;
	55:
		iFileSeek.FileID := iFD;
		iFileSeek.Origin := _eFSEEK_ORIGIN#_SEEK_SET;
		iFileSeek.Offset := Context.WriteSize;
		iFileSeek.Execute := TRUE;
		
		Inc(iState);
	56:
		IF iFileSeek.Done OR iFileSeek.Error THEN
			iFileSeek.Execute := FALSE;
			
			IF iFileSeek.Error THEN
				iError := TRUE;
				iErrorID := iFileSeek.ErrorID;
				iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16)
					OR WORD_TO_DWORD(iErrorID);
				
				CASE WORD_TO_UINT(iErrorID) OF
					16#1405: iReturnState := STATE_EXCEPTION;
				ELSE
					iReturnState := STATE_ERROR;
				END_CASE;
				
				iState := STATE_READ_CLOSE;
			ELSE		
				iState := iState + 4;
			END_IF;
		END_IF;
 	// ストリームのバッファがリングメモリであるとして読み出します。
	60:
		iHead := SEL(Context.Head = iBufferSize, Context.Head, UINT#0);
		iTail := Context.Tail;
		iReadSize := 0;
		iEnd := FALSE;
		
		Inc(iState);
	61:
		IF iHead >= iTail THEN
			iFileRead.Size := iBufferSize - iHead;
		ELSE
			iFileRead.Size := iTail - iHead;
		END_IF;
		
		// 読み出しサイズがゼロで実行しても意味がありません。
		IF iFileRead.Size > 0 THEN
			iFileRead.FileID := iFD;
			iFileRead.Execute := TRUE;
			
			Inc(iState);
		ELSE
			iState := iState + 2;
		END_IF;
	62:
		IF iFileRead.Done OR iFileRead.Error THEN
			iFileRead.Execute := FALSE;
			
			IF iFileRead.Error THEN
				iError := TRUE;
				iErrorID := iFileRead.ErrorID;
				iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16)
					OR WORD_TO_DWORD(iErrorID);
				
				CASE WORD_TO_UINT(iErrorID) OF
					16#1405: iReturnState := STATE_EXCEPTION;
				ELSE
					iReturnState := STATE_ERROR;
				END_CASE;
				
				iState := STATE_READ_CLOSE;
			ELSE
				iReadSize := iReadSize + iFileRead.ReadSize;
				iHead := iHead + iFileRead.ReadSize;
				iHead := SEL(iHead = iBufferSize, iHead, UINT#0);
				iEnd := iFileRead.EOF;
				
				IF NOT iEnd AND (iHead < iTail) THEN
					Dec(iState);
				ELSE
					Inc(iState);
				END_IF;
			END_IF;
		END_IF;
	63:
		Context.Head := iHead;
		Context.WriteSize := Context.WriteSize + iReadSize;
		Context.End := iEnd;
		
		IF iEnd THEN
			iReturnState := STATE_EOF;
		END_IF;
		
		iState := STATE_READ_CLOSE;
	// ファイルリソース解放
	// ファイルリソースの解放は、いずれかの処理でエラーが生じた場合も念のため行うため、
	// 明示的に遷移できる(状態値の定数を定義)ようにします。
	65: // STATE_READ_CLOSE
		iFileClose.FileID := iFD;
		iFileClose.Execute := TRUE;
		
		Inc(iState);
	66:
		IF iFileClose.Done OR iFileClose.Error THEN
			iFileClose.Execute := FALSE;
			
			IF iFileClose.Error AND NOT iError THEN
				iError := TRUE;
				iErrorID := iFileClose.ErrorID;
				iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16)
					OR WORD_TO_DWORD(iErrorID);
			END_IF;
			
			iState := STATE_READ_DONE;
		END_IF;
	// 読み出し処理完了状態
	// ファイルオープンに失敗した場合、遷移するため明示的に遷移できるようにします。
	69: // STATE_READ_DONE
		iState := iReturnState;

	// FB完了状態
	1000: // STATE_DONE
		Context.End := TRUE;
		
		Busy := FALSE;
		Done := TRUE;
		
		Inc(iState);
END_CASE;

iFileOpen();
iFileClose();
iFileSeek();
iFileRead(ReadBuf:=Context.Buffer[iHead]);
初期化状態
	// 初期化状態
	// FBを初期化し、パラメータを固定します。
	0: // STATE_INIT
		iFileOpen.Execute := FALSE;
		iFileClose.Execute := FALSE;
		iFileSeek.Execute := FALSE;
		iFileRead.Execute := FALSE;
		
		iPath := Path;
		iBufferSize := SizeOfAry(Context.Buffer);
		// バッファの半分未満を読み込み閾値とします。
		iReadThreshold := iBufferSize / 2;
		// 例外時は200サイクル待機します。
		iRetryTick := 200;
		
		Clear(Context);
		
		iState := STATE_WAIT;

使用するFBを初期化し、ストリーム処理中に参照するパラメータを固定します。ストリーム処理は、複数サイクルに及びます。処理が複数サイクルに及ぶFBで、入力変数を使用箇所にて直接参照すると、処理中の入力変数の変化により、意図しない動作となる可能性があります。入出力変数は、処理によってその値を変化させる目的がある場合は問題ありません。しかし、任意長配列を使用する、または、サイズが大きいため参照にする目的である場合、その変化を意図していない場合もあります。

処理待機状態
	// 処理待機状態
	10: // STATE_WAIT
		IF NOT Execute THEN
			iState := STATE_DONE;
		ELSE
			// バッファ残量が閾値未満になったら、読み出し処理を実行します。
			IF (Context.WriteSize - Context.ReadSize) < iReadThreshold THEN
				iReturnState := STATE_WAIT;
				
				iState := STATE_READ;
			END_IF;
		END_IF;
		
	// 例外状態
	// 時間をおいてから再度試みます。
	15: // STATE_EXCEPTION
		iWaitTick := iRetryTick;
		
		Inc(iState);
	16:
		Dec(iWaitTick);
		IF iWaitTick < 1 THEN
			iState := STATE_WAIT;
		END_IF;

処理待機状態(10)では、FBの終了信号(Execute=False)とストリームのバッファ残量を監視します。FBの実行制御は、FBのパラメータ以外で行うこともできますが、FBには標準的なパラメータとそれに係る振る舞いがあります。標準的な振る舞いに則ることは、開発者(自分自身)の負担軽減につながるため、できる限り実装します。ここでは、それに則りFBの終了信号を監視し、終了信号があればストリーム処理を終了します。FB自体のパラメータによる制御は、それ以外による制御に優先するものとします。FBの実行制御を担うコードはその実行制御に責務を負っているためです。

例えば、システムの終了処理に向けた一連のリソース解放の為、関連するFBの終了を確認するとします。頑なに処理を継続するFBはどうでしょうか。あるいは、終了したように見えて処理を続けていたらどうでしょうか。変数を介して制御を行うFBは、その変数を介して処理結果を使用しているコードがシステムの終了処理に係る制御への責務も負うことが望ましいでしょうか。そのような場合、使用者は何らかのリソース管理や複雑な手順を他に移譲(面倒を見てもらう)し、使える時だけ使いたいという状況を求めるのではないでしょうか。これは、使用者が怠惰なのではなく、希少なリソース(ファイルアクセス、ソケットなど)を必要とする処理の実装負荷軽減、ソフトウェアの品質維持を考えれば、妥当な要求です。そもそも、その使用者のコードは、システム終了時に実行していないかもしれません。では、FBがリソース管理を行うものとして、そのFBを実行するコードの要求はどのようなものでしょうか。リソース解放に複雑な手順を必要とせず、FBを終了したらリソースが解放される、標準的な終了手順を求めるでしょう。

これらは、希少なリソースが関連するFBに限ったものではありません。"リソース"を"責務"と読みかえれば、全てのFBが対象となります。責務を果たしたFBは、他の実行中コードからすれば意識の外に置いても問題になりません。但し、責務を果たしたとするサインが、確かにそれを意味してる場合に限られます。FBの実装では、標準的な信号を定義して標準的な出力を行い、かつ、FB内もそのような状態とすることでそれを満たすことになります。

例外状態(15)は、一時的なファイルリソースの不足など、時間がたてば正常に読み出し処理を行える状態で、処理待機状態(10)の延長と考えられます。適当なフラグを使用して処理待機状態(10)に組み込むこともできそうですが、分かれています。FBの終了信号の監視が重複しているので、組み込みたくなります。内部状態と考えると、さらにそうしたくなるかもしれません。これは実装上の理由によります。ネストを浅くし、特定の内部状態までの形式的なコード実行を避ける目的があります。

終末状態
	// ファイル末尾到達状態
	20: // STATE_EOF
		IF NOT Execute THEN
			iState := STATE_DONE;
		END_IF;
		
	// エラー状態
	30: // STATE_ERROR
		Context.Error := TRUE;
		Context.ErrorID := iErrorID;
		Context.ErrorIDEx := iErrorIDEx;
		
		Inc(iState);
	31:
		IF NOT Execute THEN
			iState := STATE_DONE;
		END_IF;

ストリーム処理の終末状態です。ファイル末尾到達か、致命的なエラー発生でストリーム処理は終了します。どちらの状態でも、ファイルリソースは解放状態にあります。希少なリソースはできる限り占有しないようにします。

読み出し処理実行状態
	// 読み出し処理実行状態
	// ファイルアクセス関連の定型処理が殆どです。
	50: // STATE_READ
		Clear(iError);
		Clear(iErrorID);
		Clear(iErrorIDEx);
	
		iFileOpen.FileName := iPath;
		iFileOpen.Mode := _eFOPEN_MODE#_READ_EXIST;
		iFileOpen.Execute := TRUE;
		
		Inc(iState);
	51:
		IF iFileOpen.Done OR iFileOpen.Error THEN
			iFileOpen.Execute := FALSE;
			
			IF iFileOpen.Error THEN
				iError := TRUE;
				iErrorID := iFileOpen.ErrorID;
				iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16)
					OR WORD_TO_DWORD(iErrorID);
				
				CASE WORD_TO_UINT(iErrorID) OF
					16#1405,16#140B: iReturnState := STATE_EXCEPTION;
				ELSE
					iReturnState := STATE_ERROR;
				END_CASE;
				
				iState := STATE_READ_DONE;
			ELSE	
				iFD := iFileOpen.FileID;
			
				iState := iState + 4;
			END_IF;
		END_IF;

読み出し処理は、ファイルオープンから始まります。非同期FBは、完了に少なくとも1サイクル必要になると考えてください。この2ステップの構造は頻出です。ネットワーク命令、モーション制御命令でも使用します。最初のステップで、FBを実行(Execute=True)し、次のステップで完了を監視(Done=True、または、Error=True)します。FBが完了したら、必ず1サイクルは非実行(Execute=False)とします。組み込みFBの実行時エラーは何らかの物理的要因、または、システム異常による場合があるため、外部で参照できるようにし、判断と対処を使用者に移譲します。エラー情報には発生個所を特定する情報を含めます。

ファイル読み出し位置へ移動
	55:
		iFileSeek.FileID := iFD;
		iFileSeek.Origin := _eFSEEK_ORIGIN#_SEEK_SET;
		iFileSeek.Offset := Context.WriteSize;
		iFileSeek.Execute := TRUE;
		
		Inc(iState);
	56:
		IF iFileSeek.Done OR iFileSeek.Error THEN
			iFileSeek.Execute := FALSE;
			
			IF iFileSeek.Error THEN
				iError := TRUE;
				iErrorID := iFileSeek.ErrorID;
				iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16)
					OR WORD_TO_DWORD(iErrorID);
				
				CASE WORD_TO_UINT(iErrorID) OF
					16#1405: iReturnState := STATE_EXCEPTION;
				ELSE
					iReturnState := STATE_ERROR;
				END_CASE;
				
				iState := STATE_READ_CLOSE;
			ELSE		
				iState := iState + 4;
			END_IF;
		END_IF;

ファイル位置指示子を合わせます。ストリーム処理は、メモリ展開できないサイズのファイルを扱えるようにするため、どこまでファイルを読んだかを記憶し、メモリへの読み出しはその位置から開始するようにします。

ファイルからバッファへ読み出し
// ストリームのバッファがリングメモリであるとして読み出します。
	60:
		iHead := SEL(Context.Head = iBufferSize, Context.Head, UINT#0);
		iTail := Context.Tail;
		iReadSize := 0;
		iEnd := FALSE;
		
		Inc(iState);
	61:
		IF iHead >= iTail THEN
			iFileRead.Size := iBufferSize - iHead;
		ELSE
			iFileRead.Size := iTail - iHead;
		END_IF;
		
		// 読み出しサイズがゼロで実行しても意味がありません。
		IF iFileRead.Size > 0 THEN
			iFileRead.FileID := iFD;
			iFileRead.Execute := TRUE;
			
			Inc(iState);
		ELSE
			iState := iState + 2;
		END_IF;
	62:
		IF iFileRead.Done OR iFileRead.Error THEN
			iFileRead.Execute := FALSE;
			
			IF iFileRead.Error THEN
				iError := TRUE;
				iErrorID := iFileRead.ErrorID;
				iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16)
					OR WORD_TO_DWORD(iErrorID);
				
				CASE WORD_TO_UINT(iErrorID) OF
					16#1405: iReturnState := STATE_EXCEPTION;
				ELSE
					iReturnState := STATE_ERROR;
				END_CASE;
				
				iState := STATE_READ_CLOSE;
			ELSE
				iReadSize := iReadSize + iFileRead.ReadSize;
				iHead := iHead + iFileRead.ReadSize;
				iHead := SEL(iHead = iBufferSize, iHead, UINT#0);
				iEnd := iFileRead.EOF;
				
				IF NOT iEnd AND (iHead < iTail) THEN
					Dec(iState);
				ELSE
					Inc(iState);
				END_IF;
			END_IF;
		END_IF;
	63:
		Context.Head := iHead;
		Context.WriteSize := Context.WriteSize + iReadSize;
		Context.End := iEnd;
		
		IF iEnd THEN
			iReturnState := STATE_EOF;
		END_IF;

ストリームのバッファへの読み出しを行います。60で読み出し処理に関係するパラメータの固定、61-62で読み出し処理、63でストリームの値を更新しています。パラメータの固定を行うのは、ファイルからの読み出しが1回ではない可能性があるためです。これは、バッファをリングメモリとみなしているためです。バッファ配列の末尾まで使用すると、先頭に戻ります。また、バッファは、読み出しと書き込みの指示子があり、バッファへの書き込みは、読み出しの指示子を越えないようにする必要があります。読み出しの指示子を越えて書き込みを行うと、未読み出しのデータを上書きしてしまいます。

FBの内部バッファを使用することもできます。最大長のBYTE型配列を内部変数に用意し、一度そのバッファに読み出した後、ストリームにコピーします。保守性も処理効率もそちらの方がよいでしょう。修正できるようであれば、そのようにしましょう。

ファイルリソース解放
	// ファイルリソース解放
	// ファイルリソースの解放は、いずれかの処理でエラーが生じた場合も行うため、
	// 明示的に遷移できる(状態値の定数を定義)ようにします。
	65: // STATE_READ_CLOSE
		iFileClose.FileID := iFD;
		iFileClose.Execute := TRUE;
		
		Inc(iState);
	66:
		IF iFileClose.Done OR iFileClose.Error THEN
			iFileClose.Execute := FALSE;
			
			IF iFileClose.Error AND NOT iError THEN
				iError := TRUE;
				iErrorID := iFileClose.ErrorID;
				iErrorIDEx := SHL(UINT_TO_DWORD(iState), 16)
					OR WORD_TO_DWORD(iErrorID);
			END_IF;
			
			iState := STATE_READ_DONE;
		END_IF;
	// 読み出し処理完了状態
	// ファイルオープンに失敗した場合、遷移するため明示的に遷移できるようにします。
	69: // STATE_READ_DONE
		iState := iReturnState;

ファイルリソースを解放します。ファイルリソースの解放は、読み出し処理よりも重要です。読み出し処理自体の失敗は、ストリームの使用コードとその関連コードに影響を与えますが、ファイルリソース解放の失敗は、システム全体に偶発的な障害を引き起こす可能性があります。処理の責務を果たすことは重要ですが、システムを破綻させないことはさらに重要です。

読み出し処理完了状態
	// 読み出し処理完了状態
	// ファイルオープンに失敗した場合、遷移するため明示的に遷移できるようにします。
	69: // STATE_READ_DONE
		iState := iReturnState;

一連の読み出し処理を終えたら、次の状態に遷移します。ここでは、遷移を実行するだけです。一連の処理の末尾に次の状態を決定する節を設けることもできますが、ここでは各処理内にその責務を持たせています。末尾節で遷移先を決定しようとすると、各処理の結果のうち、遷移に必要な情報を保持しておく必要があります。エラーに関する情報は保持しているため、できないことはありませんが、その内容を精査することになります。遷移先の決定が複雑であれば、一元的に決定する節を設けることが妥当かもしれませんが、そうでなければ、構造的な合理性と引き換えに不要な冗長性を持ち込むだけです。

また、一連の処理が関連しているとはいえ、より凝集していたほうが変更に強くなる可能性もあります。状態遷移の決定が複雑に思えたとしても、個々の処理で遷移先が決定(次の処理に任せるという移譲も含む)するようであれば、一つ一つは単純な判断に収まるかもしれません。早期リターンを思い起こしてください。多少の処理は行ったとしても、区切りよく処理を継続するか否かの評価を行うことで、コードの複雑さと不要な処理を避けることができます。

POU末尾でのFB実行
...
END_CASE;

iFileOpen();
iFileClose();
iFileSeek();
iFileRead(ReadBuf:=Context.Buffer[iHead]);

内部で使用しているFBを最後に実行します。これは、サイクル中のFB実行を1回にすることを強制するためのパターンです。FBによっては、目的によって引数とする変数の型を変える必要があるため、このパターンを採用できないものもありますが、多くのFBに適用できます。組み込みFBには、サイクル中の実行が1回であることを暗黙裡に求めるものがあります。独自FBでサイクル中の多回実行を許容したものでない限り、サイクル中のFB実行は1回とします。

Mainプログラム

Mainプログラムは、FileStreamReaderの使用例です。CASEステートマシンを手続き的に使用しています。また、いくつかのストリームFUNを使用しています。

CASE iState OF
	// ファイルリーダー初期化
	0:
		iFileStreamReader.Execute := FALSE;
		
		Inc(iState);
	1:
	 	// 99999文字のLorem ipsumのファイルです。
		iFileReader.Path := '/loremipsum.txt';
		iFileReader.Execute := TRUE;
				
		Inc(iState);
		
	// ストリーム処理
	2:
		// ストリームでエラーが発生すると実行します。
		IF Stream_OnError(Context:=iContext) THEN
			iMessage := 'Fatal error...';
			Stream_ErrorInfo(Context:=iContext,
				ErrorID=>iErrorID, ErrorIDEx=>iErrorIDEx);
			
			iState := iState + 7;
		// ストリームが終了すると実行します。
		ELSIF Stream_OnEnd(Context:=iContext) THEN
			iMessage := 'Done!!';
			
			iState := iState + 7;
		// ストリームにデータが存在したら実行します。
		ELSIF Stream_OnData(
			   Context:=iContext,
			   Out:=iBuffer,
			   OutHead:=0,
			   OutSize=>iReadSize)
		THEN
			iMessage := AryToString(In:=iBuffer[0], Size:=iReadSize);
			iWaitTick := 10;
			
			Inc(iState);
		END_IF;
	3: // ストリーム処理に必要ではありませんが、無いとすぐに終了してしまいます。
		Dec(iWaitTick);
		IF iWaitTick < 1 THEN
			Dec(iState);
		END_IF;
	
	// ストリーム処理終了
	9:
		iFileStreamReader.Execute := FALSE;
		
		Inc(iState);
END_CASE;

iFileStreamReader(Context:=iContext);
ファイルリーダー初期化
	// ファイルリーダー初期化
	0:
		iFileStreamReader.Execute := FALSE;
		
		Inc(iState);
	1:
	 	// 99999文字のLorem ipsumのファイルです。
		iFileReader.Path := '/loremipsum.txt';
		iFileReader.Execute := TRUE;
				
		Inc(iState);

FBは実行前に、少なくとも1サイクルは非実行(Execute=False)で実行します。非実行での実行は、FBの内部状態を初期状態、あるいは、処理を開始可能な状態にするものと考えてください。組み込みFBでは必須です。独自FBも同様の振る舞いをするよう実装します。

ストリーム処理
	// ストリーム処理
	2:
		// ストリームでエラーが発生すると実行します。
		IF Stream_OnError(Context:=iContext) THEN
			iMessage := 'Fatal error...';
			Stream_ErrorInfo(Context:=iContext,
				ErrorID=>iErrorID, ErrorIDEx=>iErrorIDEx);
			
			iState := iState + 7;
		// ストリームが終了すると実行します。
		ELSIF Stream_OnEnd(Context:=iContext) THEN
			iMessage := 'Done!!';
			
			iState := iState + 7;
		// ストリームにデータが存在したら実行します。
		ELSIF Stream_OnData(
			   Context:=iContext,
			   Out:=iBuffer,
			   OutHead:=0,
			   OutSize=>iReadSize)
		THEN
			iMessage := AryToString(In:=iBuffer[0], Size:=iReadSize);
			iWaitTick := 10;
			
			Inc(iState);
		END_IF;
	3: // ストリーム処理に必要ではありませんが、無いとすぐに終了してしまいます。
		Dec(iWaitTick);
		IF iWaitTick < 1 THEN
			Dec(iState);
		END_IF;

ストリームのデータを消費します。イベントをトラップする記述となるようにFUNを実装しています。ストリームは、リングバッファです。その構造に対して、どのような使い勝手を提供するかは開発者次第です。FileStreamReaderは、バッファ残量に応じて自動でデータをファイルから読み出し、ストリームに供給します。使用者のコードからFileStreamReaderへの信号は、データの消費です。コメント記述もありますが、FUN名から意味をつかめるのではないでしょうか。

ストリーム終了処理
	// ストリーム処理終了
	9:
		iFileStreamReader.Execute := FALSE;
		
		Inc(iState);

標準的な終了処理です。FileStreamReaderの非実行状態(Busy=False)を確認すれば、再使用も可能です。

まとめ

ファイルストリームを題材にCASEステートマシンの使用例を確認しましたが、CASEステートマシン以外の設計に関連する内容が多くなりました。これは、CASEステートマシンが設計上の骨組みしか提供しないことを示しています。CASEステートマシンは、実装に関連した設計手段ないし、プラクティスを展開する場を提供するものです。また、組み込みFBの使用法や、振る舞いなどシステム固有の内容にも触れました。開発対象によらず概ね同様の振る舞いをしますが、開発対象についての理解も不可欠であることを示しています。

いわゆるストリーム処理を実装できることは体感できたと思います。重要なのは、ファイルのコンテンツを簡単に使用できることではなく、単純なSDカード命令では使用できない、大きなサイズのファイルのコンテンツを簡単に使用できるということです。そして、その骨組みがCASEステートマシンだということです。CASEステートマシン以外の内容が多いように感じられるかもしれませんが、それが可能なのはその場があることによります。

CASEステートマシンは、特別強力な構造ではありません。実践的な使用には、確認する中で個々に触れたCASEステートマシン以外の内容に不足があるという課題が残っています。また、一般的な言語で有用なプラクティスの適用は、やや変更して適用する必要性や、場合によってはあるプラクティスの理念をPLCという環境ではどのように展開するかを一から考えなければならないことについての内容の不足も課題として残っています。あるいは、EhterCATネットワークの構築、その上でのアクチュエータやセンサの使用方法、IO-Linkデバイスの概念を含めての利活用、EIPデバイスの使用、OPC UA機能といったFA寄りの内容の不足も課題であるかもしれません。

私が考える限りでも、これをもってCASEステートマシンの十分な参考になるとは言えない多くの課題がありますが、何を実装するにしても決定的に不足しているものを、先に少しだけ対処します。ソフトウェアテストのツールがありません。CASEステートマシンでテストフレームワークを実装しましょう。

Discussion