Sysmac Studioで簡単な初期化システムを作る
はじめに
本記事は、PLC(Programmable Logic Controller)向けのソフトウェア開発に従事、または関心のある方で、Structured Text(ST)による開発に興味がある方向けです。OMRON社のSysmac Studioを使用します。
今回は、初期化システムを作ります。systemdには程遠いですが、コントローラ立ち上げ時に指定した一連のプログラムを起動する初期化システムです。多くの環境は、起動時に構成情報に応じて動作プログラムを決定し、目的に対して必要十分な機能だけを提供することができます。これを目指します。命令語リファレンスのプログラム制御命令にも構成によって動作プログラムを変更する例が挙げられています。意図は異なりますが、行うことは類似しています。
初期化システムは、一連のプログラムの集合をモードとし、個々のプログラムをモードユニットとしています。モードセットは、モードの集合です。これらを宣言的なファンクションで定義し、選択したモードをファンクションで有効化して初期化システムは役割を終えます。関連するPOUは全てファンクションで定義してあるため、任意のPOUで使用することができます。以下は、サンプルプログラムです。
IF P_First_Run THEN
SPEC_USE_MQTT := TRUE;
SPEC_USE_KINTONE := TRUE;
ModeManager_init(Context:=iModeManager,
// モードマネージャが動作するプログラムの名称。
Name:='ProgramLoader',
// モード有効化成功時にモードマネージャが動作するプログラムを停止するかどうか。
// デフォルトはTRUE(停止)。
OnSuccessStop:=TRUE,
// モード有効化失敗時にモードマネージャが動作するプログラムを停止するかどうか。
// デフォルトはTRUE(停止)。
OnFailStop:=FALSE,
// モード有効化失敗時にコントローラにアラームログを残すかどうか。
// デフォルトはFALSE(残さない)。
OnFailAlarm:=TRUE,
// ユーザーログのアラームコード。
// デフォルトは1。
AlarmCode:=1);
ModeSet(Context:=iModeManager);
Mode(Context:=iModeManager,
// モードの識別子。
Name:='modeselect',
// デフォルトモードとするかどうか。
// デフォルトはFALSE(デフォルトにしない)。
Default:=TRUE);
ModeUnit(Context:=iModeManager,
// モードユニットであるプログラムの名称。
Name:='DeviceIn',
// P_First_Runを有効とするかどうか。
// デフォルトはTRUE(有効)。
FirstRun:=TRUE,
// モード有効化失敗時にプログラムを停止するかどうか。
// デフォルトはTRUE(停止)。
OnFailStop:=TRUE);
ModeUnit(Context:=iModeManager, Name:='DeviceOut');
ModeUnit(Context:=iModeManager, Name:='TelnetService');
Mode(Context:=iModeManager, Name:='setup');
ModeUnit(Context:=iModeManager, Name:='DeviceIn');
ModeUnit(Context:=iModeManager, Name:='DeviceOut');
ModeUnit(Context:=iModeManager, Name:='Machine1Setup');
ModeUnit(Context:=iModeManager, Name:='Machine2Setup');
ModeUnit(Context:=iModeManager, Name:='TelnetService');
Mode(Context:=iModeManager, Name:='run');
ModeUnit(Context:=iModeManager, Name:='DeviceIn');
ModeUnit(Context:=iModeManager, Name:='DeviceOut');
ModeUnit(Context:=iModeManager, Name:='Machine1Ctrl');
ModeUnit(Context:=iModeManager, Name:='Machine2Ctrl');
IF SPEC_USE_MQTT THEN
ModeUnit(Context:=iModeManager, Name:='MqttMachineStatSender');
END_IF;
IF SPEC_USE_KINTONE THEN
ModeUnit(Context:=iModeManager, Name:='KintoneClientService');
ModeUnit(Context:=iModeManager, Name:='KintoneAlarmCollector');
END_IF;
Mode(Context:=iModeManager, Name:='diagnosis');
ModeUnit(Context:=iModeManager, Name:='DeviceIn');
ModeUnit(Context:=iModeManager, Name:='DeviceOut');
ModeUnit(Context:=iModeManager, Name:='DiagnosisCtrl');
ModeUnit(Context:=iModeManager, Name:='Machine1Diagnosis');
ModeUnit(Context:=iModeManager, Name:='Machine2Diagnosis');
ModeUnit(Context:=iModeManager, Name:='DiagnosisReporter');
ModeUnit(Context:=iModeManager, Name:='TelnetService');
IF SPEC_USE_MQTT THEN
ModeUnit(Context:=iModeManager, Name:='MqttMachineStatSender');
END_IF;
IF SPEC_USE_KINTONE THEN
ModeUnit(Context:=iModeManager, Name:='KintoneClientService');
ModeUnit(Context:=iModeManager, Name:='KintoneAlarmCollector');
END_IF;
ModeSetClose(Context:=iModeManager);
// モードを選択する。
ModeManager_selectMode(Context:=iModeManager, Name:=RUN_MODE);
END_IF;
// 選択中のモードを有効化する。
ModeManager_activate(Context:=iModeManager);
// 自身でコントローラにログを残したい場合。
(*
IF ModeManager_activate(Context:=iModeManager) THEN
IF ModeManager_hasError(Context:=iModeManager) THEN
ModeManager_getAlarmInfo(Context:=iModeManager,
ErrorID=>iErrorID,
RunMode=>iRunMode);
SetAlarm(Code:=1, Info1:=iErrorID, Info2:=iRunMode);
END_IF;
END_IF;
*)
ModeファンクションのNameで指定している値からそれぞれが何を意図しているかの予想がつくと思います。モードを選択するRUN_MODE変数は、グローバルの保持変数です。値をdiagnosis
として実行すると、プログラムの実行状態は以下のようになります。
diagnosisモードに関連した一連のプログラムだけを実行します。プログラム間の実行についての述語評価機能、依存グラフの構築とそれに対応したプログラム起動のような機能を追加するとsystemdらしくなりますが、初期化プロセスが複雑になる要因にもなります。
システム起動後の変更を目的とし、systemdに対するsystemctlのような機能を作成することもできますが、実行中プログラムの停止は慎重を要する操作です。しかし、NX/NJはコントローラ運転中のEtherCATスレーブの離脱と参加、EIPデバイスのタグデータリンクコネクションの開始と停止が可能なので、ホットな状態での構成変更が有り得ることを考えると必要であるかもしれません。
Sysmacプロジェクト
以下に、Sysmacプロジェクトがあります。今回は、開発用プロジェクトにサンプルプログラムをバンドルしています。
実装
初期化システムのPOUはいずれも難しくありません。分量のあるモードを有効化するファンクションを確認します。プログラム制御命令でモードに対応したプログラムの実行と停止を行います。プログラム制御命令は、リファレンスの記載とは異なる振る舞いをするので注意が必要です。
iState := Context.State;
CASE iState OF
// STATE_INIT
0:
IF Context.Error THEN
iState := STATE_DONE;
ELSE
// PrgStatusは、FALSEを返さない。
iOk := FALSE;
iOk := PrgStatus(PrgName:=Context.Name);
IF NOT iOk THEN
Context.Error := TRUE;
Context.ErrorID := WORD#16#2000;
iState := STATE_DONE;
ELSE
Inc(iState);
END_IF;
END_IF;
1:
iRunMode := Context.RunMode;
iMode := Context.Modes[iRunMode];
i := iMode.Head;
j := iMode.Head + iMode.Num;
iOk := TRUE;
WHILE i < j DO
// PrgStartは、FALSEを返さない。
iOk := FALSE;
iOk := PrgStart(PrgName:=Context.ModeUnits[i].Name,
isFirstRun:=Context.ModeUnits[i].FirstRun);
IF iOk THEN
Context.ModeUnitStatuses[i].Activating := TRUE;
ELSE
iErrorID := SHL(UINT_TO_WORD(iRunMode), 8) OR UINT_TO_WORD(i);
EXIT;
END_IF;
Inc(i);
END_WHILE;
IF NOT iOk THEN
i := iMode.Head;
j := iMode.Head + iMode.Num;
WHILE i < j DO
IF Context.ModeUnitStatuses[i].Activating
AND Context.ModeUnits[i].OnFailStop
THEN
PrgStop(PrgName:=Context.ModeUnits[i].Name);
Context.ModeUnitStatuses[i].Activating := FALSE;
END_IF;
Inc(i);
END_WHILE;
Context.Error := TRUE;
Context.ErrorID := iErrorID;
iState := STATE_DONE;
ELSE
Inc(iState);
END_IF;
2:
iRunMode := Context.RunMode;
iMode := Context.Modes[iRunMode];
i := iMode.Head;
j := iMode.Head + iMode.Num;
iOk := TRUE;
WHILE i < j DO
// PrgStatusは、FALSEを返さない。
iOk := FALSE;
iOk := PrgStatus(PrgName:=Context.ModeUnits[i].Name);
IF iOk THEN
Context.ModeUnitStatuses[i].Active := TRUE;
Context.ModeUnitStatuses[i].Activating := FALSE;
ELSE
iErrorID := SHL(UINT_TO_WORD(iRunMode), 8) OR UINT_TO_WORD(i);
EXIT;
END_IF;
Inc(i);
END_WHILE;
IF NOT iOk THEN
i := iMode.Head;
j := iMode.Head + iMode.Num;
WHILE i < j DO
IF Context.ModeUnitStatuses[i].Active
AND Context.ModeUnits[i].OnFailStop
THEN
PrgStop(PrgName:=Context.ModeUnits[i].Name);
Context.ModeUnitStatuses[i].Active := FALSE;
Context.ModeUnitStatuses[i].Activating := FALSE;
END_IF;
Inc(i);
END_WHILE;
Context.Error := TRUE;
Context.ErrorID := iErrorID;
iState := STATE_DONE;
ELSE
iState := STATE_DONE;
END_IF;
// STATE_DONE
1000:
IF Context.Error THEN
IF Context.OnFailAlarm THEN
SetAlarm(Code:=Context.AlarmCode,
Info1:=Context.ErrorID,
Info2:=Context.RunMode);
END_IF;
IF Context.OnFailStop THEN
PrgStop(Context.Name);
END_IF;
ELSE
IF Context.OnSuccessStop THEN
PrgStop(Context.Name);
END_IF;
END_IF;
ModeManager_activate := TRUE;
Inc(iState);
END_CASE;
Context.State := iState;
淡々とモードのプログラムを実行し、すべて起動できたら成功、起動できなかったら起動済みプログラムを設定に応じて停止させるかそのままとし、最後に自身が動作するプログラムも設定に応じて停止させるかそのままにします。
プログラム制御命令であるPrgStart
、PrgStop
、PrgStatus
の使用は注意が必要です。いずれも戻り値に意味があり、BOOLを返すことになっていますが、FALSEが返ってきません。そのため、戻り値を受ける変数は直前にFALSEで初期化しておく必要があります。
まとめ
今回は初期化システムを作りました。初期化システムと呼ぶことが憚られるぐらい内容も作りも素直です。素直さを活かしてサブシステムの初期化にも使用する二段構成や、外部からシステム構成を与えて柔軟に構成する為のツールにすることもできます。
初期化システムは、個々のプログラムの役割が明確で、機能の選択が起動するプログラムの取捨選択で実現できる状態にあるとき有効に機能します。そのためには、条件分岐による実行制御、フラグによる処理の切り分けを減らすように設計することが必要です。構造としては、条件分岐による実行制御の条件式の評価と処理コードが構造的に分離した状態です。
頭を捻る必要はありません。装置からの視点ではなく、装置を流れるワークからの視点で生産プロセスを観察することです。装置としてのシステム構成と生産プロセスとしてのシステム構成は異なります。生産プロセスとしてのシステム構成が見えてくると、関数型言語で流れるようにコードを記述する感覚で、構成要素から機能を組み上げるための、機能の切り出し方が見えてくると思います。
関数型言語のメタファーとして工場のラインが挙げられることがありますが、これはラインそのものに適用します。生産管理論には生産プロセスは情報の転写であるという考え方もあるので、生産プロセスを関数型言語のように連続したデータ処理として見ることも的外れなわけではありません。STでFunctionが第一級オブジェクトではないことがつくづく悔やまれます。
Discussion