やりたいこと
MATLAB Scriptを用いてStateflowを生成したい。通常のSimulinkのブロック配置と異なり、Stateflowは自由に描ける分自動化が難しいのではと考えていた。
ところが、Mathworksさんはそんなニーズに応えてくれるAPIを用意してくれているので気合があればできることがわかった。
環境
今回お試しする環境は下記のとおり
- MATLAB Version 9.12(R2022a)
- Simulink Version 10.5(R2022a)
- Stateflow Version 10.6 (R2022a)
参考
とにかくMathworksさんのヘルプは充実している。
これと各関連するオブジェクトのページを読めば自ずとスクリプトが完成する(はず)。
- Chart:
https://jp.mathworks.com/help/stateflow/api/stateflow.chart.html - State:
https://jp.mathworks.com/help/stateflow/api/stateflow.state.html - Transition:
https://jp.mathworks.com/help/stateflow/api/stateflow.transition.html - Data:
https://jp.mathworks.com/help/stateflow/api/stateflow.data.html
目標:簡易信号機を作る
仕様は以下の通り
- 起動停止(OFF)と起動中(ON)の状態をもつ
- ONの状態の中に青点灯中(BLUE)と赤点灯中(RED)の状態を持つ
- OFFとONは外からの入力(power)の値により決定する
- BLUEとREDは時間経過で交互に行き来する
(ホントはBLUEの中にJunctionの練習としてblink処理を入れたかったけど、API的にはやること同じなので割愛した)
できたもの
スクリプト
流れは以下のような感じ
- SimulinkモデルにStateflowダイアグラム(Chart)を配置する
- State(OFF/ON/BLUE/RED)を配置する
- 初期遷移を設定する
- 各状態間の遷移を設定する
- Symbolを追加(power/outputと定数定義)
1. SimulinkモデルにStateflowダイアグラム(Chart)を配置する
% 名前をつくってSimulinkを開く
modelName = 'state_test';
chartName = 'STATE_FLLOW';
sf_path = [modelName, '/', chartName];
open_system(modelName);
% Chartブロックを配置する
add_block('sflib/Chart/', sf_path);
ch = find(sfroot, "-isa", "Stateflow.Chart", Path = sf_path);
ポイントはChartがsflib/Chartに存在するということ。通常のsimulink blockの配置とは異なることに気づかず躓いた。しかしながらさすが我らがMathworksさん、ヘルプにサンプルがあり無事解決。
2. State(OFF/ON/BLUE/RED)を配置する
% OFF Stateを配置する
sts_off = Stateflow.State(ch);
sts_off.Name = 'OFF';
sts_label = sprintf("OFF\n entry:output = off;");
sts_off.LabelString = sts_label;
sts_off.Position = [10 50 180 600];
% ON Stateを配置する
sts_on = Stateflow.State(ch);
sts_on.Name = 'ON';
sts_label = sprintf("ON\n");
sts_on.LabelString = sts_label;
sts_on.Position = [380 50 560 600];
% ON Stateの中にBLUEとREDの子Stateを配置する
sts_blue = Stateflow.State(sts_on);
sts_blue.Name = 'BLUE';
sts_label = sprintf("BLUE\n entry: output = blue;");
sts_blue.LabelString = sts_label;
sts_blue.Position = [400 130 500 200];
sts_red = Stateflow.State(sts_on);
sts_red.Name = 'RED';
sts_label = sprintf("RED\n entry: output = red;");
sts_red.LabelString = sts_label;
sts_red.Position = [400 430 500 150]; %[Left Top Width Height]
ポイントは各Stateに対するentryやduringの実行指示は文字列で指定できるということ。
ついでにPositionの指定がsimulink blockの時と異なり、[左上のx座標 左上のy座標, width, height]なことに注意!! Simulinkの通常のBlockだと[左上x座標 左上y座標, 右下x座標, 右下y座標]なので違いに躓く。
3. 初期遷移を設定する
% 初期遷移を設定する(MainState)
dt_off = Stateflow.Transition(ch);
dt_off.Destination = sts_off;
dt_off.DestinationOClock = 0;
dt_off.SourceEndPoint = dt_off.DestinationEndPoint-[0 30];
dt_off.SourceOClock = 6;
dt_off.MidPoint = dt_off.DestinationEndPoint-[0 20];
% 初期遷移を設定する(OnState)
dt_on = Stateflow.Transition(sts_on);
dt_on.Destination = sts_blue;
dt_on.DestinationOClock = 11;
dt_on.SourceEndPoint = dt_on.DestinationEndPoint-[0 30];
dt_on.SourceOClock = 6;
dt_on.MidPoint = dt_on.DestinationEndPoint-[0 20];
ポイントというか面白かったところがSourceOClockやDestinationOClockだ。
これは遷移の接続箇所を指定する方法で、OClockのとおり時計の針の位置をイメージして設定する。SourceOClock=6
なら6時(真下)に、DestinationOClock=11
なら11時方向に。
(グラフィカルでプログラミングできるStateflowを言語で実現するスマートな方法に感服である!)
あとMidPointは接続時に経由したい座標であり、これを設定することで意図した場所に矢印が描かれる。
(これも良く考えられている!!)
4. 各状態間の遷移を設定する
% State間を接続する(off->on)
dt_OffToOn = Stateflow.Transition(ch);
dt_OffToOn.Source = sts_off;
dt_OffToOn.SourceOClock = 2;
dt_OffToOn.Destination = sts_on;
dt_OffToOn.DestinationOClock = 10;
dt_OffToOn.MidPoint = dt_OffToOn.DestinationEndPoint-[20 0];
dt_OffToOn.LabelString = "[power==true]";
labelXPos = ((dt_OffToOn.DestinationEndPoint(1)-dt_OffToOn.SourceEndPoint(1))/2)+dt_OffToOn.SourceEndPoint(1) - 30; %-20 is label width
dt_OffToOn.LabelPosition = [ labelXPos dt_OffToOn.DestinationEndPoint(2)-20 30 20];
% State間を接続する(on->off)
dt_OnToOff = Stateflow.Transition(ch);
dt_OnToOff.Source = sts_on;
dt_OnToOff.SourceOClock = 8;
dt_OnToOff.Destination = sts_off;
dt_OnToOff.DestinationOClock = 4;
dt_OnToOff.MidPoint = dt_OnToOff.SourceEndPoint-[20 0];
dt_OnToOff.LabelString = "[power==false]";
labelXPos = ((dt_OnToOff.DestinationEndPoint(1)-dt_OnToOff.SourceEndPoint(1))/2)+dt_OnToOff.SourceEndPoint(1) - 30; %-30 is label width
dt_OnToOff.LabelPosition = [ labelXPos dt_OnToOff.DestinationEndPoint(2)-20 30 20];
% State間を接続する(blue->red)
dt_BlueToRed = Stateflow.Transition(sts_on);
dt_BlueToRed.Source = sts_blue;
dt_BlueToRed.SourceOClock = 7;
dt_BlueToRed.Destination = sts_red;
dt_BlueToRed.DestinationOClock = 11;
dt_BlueToRed.MidPoint = dt_BlueToRed.SourceEndPoint+[0 20];
dt_BlueToRed.LabelString = "[after(10,sec)]";
labelYPos = ((dt_BlueToRed.DestinationEndPoint(2)-dt_BlueToRed.SourceEndPoint(2))/2)+dt_BlueToRed.SourceEndPoint(2) - 30;
dt_BlueToRed.LabelPosition = [dt_BlueToRed.DestinationEndPoint(1)+5 labelYPos 30 20];
% State間を接続する(red->blue)
dt_RedToBlue = Stateflow.Transition(sts_on);
dt_RedToBlue.Source = sts_red;
dt_RedToBlue.SourceOClock = 1;
dt_RedToBlue.Destination = sts_blue;
dt_RedToBlue.DestinationOClock = 5;
dt_RedToBlue.MidPoint = dt_RedToBlue.DestinationEndPoint+[0 20];
dt_RedToBlue.LabelString = "[after(10,sec)]";
labelYPos = ((dt_RedToBlue.SourceEndPoint(2)-dt_RedToBlue.DestinationEndPoint(2))/2)+dt_RedToBlue.DestinationEndPoint(2) - 30;
dt_RedToBlue.LabelPosition = [dt_RedToBlue.DestinationEndPoint(1)+5 labelYPos 30 20];
ここは初期遷移とほぼ同じ。Sourceに遷移元のStateを指定することで初期遷移(●)ではなくなる。
遷移の条件式はStateと同様に文字列指定であるが綺麗に見せるために位置計算のアルゴリズムを考える必要がありそうだ。
5. Symbolを追加(power/outputと定数定義)
% Symbolを設定する
data_output = Stateflow.Data(ch);
data_output.Name = 'output';
data_output.Scope = 'Output';
data_output.Port = 1;
data_output.DataType = 'uint8';
data_output.Props.InitialValue = 'uint8(0)';
% Symbolを設定する(power);
data_power = Stateflow.Data(ch);
data_power.Name = 'power';
data_power.Scope = 'Input';
data_power.Port = 1;
data_power.DataType = 'boolean';
data_off = Stateflow.Data(ch);
data_off.Name = 'off';
data_off.Scope = 'Constant';
data_off.Props.InitialValue = 'uint8(0)';
data_off.DataType = 'uint8';
data_blue = Stateflow.Data(ch);
data_blue.Name = 'blue';
data_blue.Scope = 'Constant';
data_blue.Props.InitialValue = 'uint8(1)';
data_blue.DataType = 'uint8';
data_red = Stateflow.Data(ch);
data_red.Name = 'red';
data_red.Scope = 'Constant';
data_red.Props.InitialValue = 'uint8(2)';
data_red.DataType = 'uint8';
最後にChartへのinport/outportや内部で使う定数を定義している。
ここもHelpをみると色々な設定項目があるので必要に応じて設定すればよい。
シミュレート
Chartブロックの中身をいっさい手を加えずにシミュレートすることができた。
(FLLOWのスペル間違えました。)
以上のスクリプトでStateflowをMATLAB Scriptから生成できてしまう。
所感
Stateflowは設計をしながら実装するうえでとても便利である一方、
設計済みで同様なパターンを量産しようと大変である。
MATLAB ScriptでSimulinkのブロックを自動配置できるようにStateflowも自動配置化することで大事な設計に時間を割くことができそうだ。
あと文中に書いたが時計のイメージで接続位置を指定するAPIがとにかく頭よかった!!