micro:bitで使えるステートマシン・ブロック
mstate拡張機能(pxt-mstate)は、Microsoft MakeCode for micro:bitでステートマシンによるコーディングを実現できるブロックです。
本記事は、ver.0.10.0 を元にしています(2024/03/30編集)。
リリースページ:pxt-mstate.0.10.0.hex
セットアップ手順
次の手順で、mstate拡張機能(pxt-mstate)をセットアップしてください。
- mstate拡張機能(HEX形式ファイル)をダウンロードします
→ https://github.com/jp-rad/pxt-mstate/releases -
Microsoft MakeCode for micro:bit で新しいプロジェクトを作成します
→ https://makecode.microbit.org/ -
拡張機能
を開き、ファイルを読み込む
ボタンで、ダウンロードしたHEX形式ファイルを選択します(つづける
で確定)
mstate拡張機能の使い方
define
ブロックで、ステートマシンの状態を定義していきます。
尚、サブステートマシンやサブ状態といった階層構造を直接的に定義できませんので、M0
からM5
までのステートマシンを使って、相互に連携させます。
ステート図の読み方・書き方
mstate拡張機能(pxt-mstate)のステートマシンをPlantUMLのステート・ダイアグラムとしてステート図に表現することができます。
を選択すると、ログとして出力されますので、出力された内容をコピーし、 https://www.plantuml.com/plantuml/ に貼り付けます。
ステート図(ステートマシン図、状態遷移図)の読み方や書き方は、統一モデリング言語(UML)の参考書を参照してください。また、本記事と参考書とを読み比べてmstate拡張機能(pxt-mstate)で出来ること、出来ないこと、実現方法の違いなどを確認してみてください。
▼おススメの参考書(UML入門書)
UMLモデリングの基本が簡潔にまとめられています。
かんたん UML入門 [改訂2版] – 2017/7/1
状態(ステート)
ステートマシンには、次の3つの状態の概念があり、状態から状態へと遷移します。
- 初期疑似状態(Initial Pseudostate)
- 状態(State)
- 終了状態(FinalState)
初期疑似状態と終了状態
mstate拡張機能(pxt-mstate)の内部において、初期疑似状態と終了状態とは同じ状態です。また、その状態名は、""
(空の文字列)です。
ステートマシンが初期疑似状態である場合に、start
ブロックを実行するとステートマシンが開始され、初期疑似状態から指定したデフォルト状態へと遷移します。
ステート図 | ブロック・コード |
---|---|
|
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.onTrigger("", [""], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
input.onButtonPressed(Button.A, function () {
mstate.start(StateMachines.M0, "a")
})
mstate.exportUml(StateMachines.M0, "a", ModeExportUML.StateDiagram)
ステートマシンの開始と終了
初期疑似状態からデフォルト状態(図中の状態a
)へ遷移(開始)するには、start
ブロックを実行します。
終了状態へ遷移(終了)するには、on trigger
ブロックのtargets
リストの要素に""
(空の文字列)を設定します。traverse
ブロックで、その要素の添え字を指定します(0番目から数える)。
遷移(トランジション)
ステートマシン内では、トリガー(trigger)により、遷移元の状態から遷移先の状態(終了状態を含む)へと遷移します。
- 遷移(transition)
- トリガー(trigger)
- 遷移元の状態(source)
- 遷移先の状態(target)
ステートマシンにトリガー(trigger)を送るには、send
ブロックを実行します。
トリガーを受けて、遷移するには、遷移元の状態定義(define
ブロック)の内側に、on trigger
ブロックを配置し、トリガー(trigger)と遷移先(target)とを指定します。
さらに、on trigger
ブロックの内側(処理部)に、traverse
ブロックで何番目の遷移先へ遷移するのかを指定します(0番目から始まる添え字)。
完了時遷移(Completion Transition)
on trigger
ブロックのtrigger
に""
(空の文字列)を指定すると、完了時遷移(Completion Transition)が行われます。
尚、この完了時遷移(Completion Transition)は、mstate拡張機能(pxt-mstate)によってコントロールされている為、send
ブロックで""
(空の文字列)を送ってもトリガーとしての遷移は行われません。
ステート図 | ブロック・コード |
---|---|
|
ソースコード
mstate.onTrigger("e", ["b"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
mstate.defineState(StateMachines.M0, "b", function () {
mstate.onTrigger("e", [""], function () {
mstate.traverse(StateMachines.M0, 0)
})
mstate.onTrigger("A+B", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
input.onButtonPressed(Button.A, function () {
mstate.start(StateMachines.M0, "a")
})
input.onButtonPressed(Button.AB, function () {
mstate.send(StateMachines.M0, "A+B")
})
input.onButtonPressed(Button.B, function () {
mstate.send(StateMachines.M0, "e")
})
mstate.exportUml(StateMachines.M0, "a", ModeExportUML.StateDiagram)
状態の振る舞い(Behavior)
ステートマシンの状態には、3つの振る舞い(Behavior)があり、それぞれの振る舞いがそれぞれのタイミングで実行されます。
- entry - 状態に入った時(entryアクション)
- doActivity - その状態にある時(doアクティビティ)
- exit - 状態から出る前(exitアクション)
3つの振る舞いとタイミング | 説明 |
---|---|
|
|
mstate拡張機能(pxt-mstate) では、on state
ブロックとon exit
ブロックの2つを使って、これら3つの振る舞いをコーディングします。
entryアクションとdoアクティビティの実現方法は独特です
mstate拡張機能(pxt-mstate) では、entryアクションとdoアクティビティの実現方法は独特であり、entryアクションとdoアクティビティとを一つにしたon state
ブロックを使ってコーディングします。詳しくは「on stateブロックの使い方」を参照してください。
次のブロック・コードでは、これら3つの振る舞いがどのようなタイミングで実行されるのかを確認できます。
ボタンAで、ステートマシンM0
を開始後、トリガーe
(ボタンB)によって、状態a
と 状態b
とで交互に状態遷移します。
また、トリガーself
(ボタンA+B)によって、自分自身の状態へ状態遷移しますが、その際にも3つの振る舞いが実行されることを確認してください。
ステート図 | ブロック・コード |
---|---|
|
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.descriptionUml("entry/ A表示")
mstate.descriptionUml("do/ ハート表示")
mstate.onState(1000, function (tickcount) {
if (0 == tickcount) {
basic.showString("A")
} else {
basic.showIcon(IconNames.Heart)
}
})
mstate.onExit(function () {
basic.showIcon(IconNames.Happy)
})
mstate.onTrigger("e", ["b"], function () {
mstate.traverse(StateMachines.M0, 0)
})
mstate.onTrigger("self", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
mstate.defineState(StateMachines.M0, "b", function () {
mstate.descriptionUml("entry/ B表示")
mstate.descriptionUml("do/ カウンター表示")
mstate.onState(1000, function (tickcount) {
if (0 == tickcount) {
basic.showString("B")
} else {
basic.showNumber(tickcount)
}
})
mstate.onExit(function () {
basic.showIcon(IconNames.Sad)
})
mstate.onTrigger("e", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
mstate.onTrigger("self", ["b"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
input.onButtonPressed(Button.A, function () {
mstate.start(StateMachines.M0, "a")
})
input.onButtonPressed(Button.AB, function () {
mstate.send(StateMachines.M0, "self")
})
input.onButtonPressed(Button.B, function () {
mstate.send(StateMachines.M0, "e")
})
mstate.exportUml(StateMachines.M0, "a", ModeExportUML.StateDiagram)
mstate拡張機能(pxt-mstate)における振る舞い
状態における3つの振る舞いに加え、遷移においても振る舞いがあり、エフェクト(effect
)と呼びます。
mstate拡張機能(pxt-mstate)では、振る舞いを2つのタイプで分類しています。
振る舞い | タイプ | 対象 | 項目 | 呼び方 |
---|---|---|---|---|
Behavior | Action | 状態 | entry | entryアクション |
exit | exitアクション | |||
遷移 | effect | エフェクト | ||
Activity | 状態 | doActivity | doアクティビティ |
タイプとして分類したアクション(Action)とアクテビティ(Activity)には、その振る舞い(Behavior)が、単発的か継続的かの違いがあります。
ただし、mstate拡張機能(pxt-mstate)では、entryアクションとdoアクテビティとをon state
ブロックで実現しています。
完了遷移(Completion Transition)
トリガーによる遷移の他に、完了遷移(Completion Transition)と呼ばれる遷移もあります。それは、ある状態への遷移でentryアクションとdoアクティビティの一連の処理が行われ、その一連の処理が完了した直後に行われる遷移です。
完了遷移(Completion Transition)を行うには、transition
ブロックのtrigger
に""
(空の文字列)を指定します。
ステート図 | ブロック・コード |
---|---|
|
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.descriptionUml("entry/ A表示")
mstate.descriptionUml("do/ ハート表示")
mstate.onState(1000, function (tickcount) {
if (0 == tickcount) {
basic.showString("A")
} else {
basic.showIcon(IconNames.Heart)
}
})
mstate.onExit(function () {
basic.showIcon(IconNames.Happy)
})
mstate.onTrigger("e", ["b"], function () {
mstate.traverse(StateMachines.M0, 0)
})
mstate.onTrigger("A+B", ["c"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
mstate.defineState(StateMachines.M0, "b", function () {
mstate.descriptionUml("entry/ B表示")
mstate.descriptionUml("do/ カウンター表示")
mstate.onState(1000, function (tickcount) {
if (0 == tickcount) {
basic.showString("B")
} else {
basic.showNumber(tickcount)
}
})
mstate.onExit(function () {
basic.showIcon(IconNames.Sad)
})
mstate.onTrigger("e", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
mstate.onTrigger("A+B", ["c"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
input.onButtonPressed(Button.A, function () {
mstate.start(StateMachines.M0, "a")
})
mstate.defineState(StateMachines.M0, "c", function () {
mstate.descriptionUml("(完了遷移)")
mstate.onState(0, function (tickcount) {
basic.showString("C")
})
mstate.onTrigger("", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
input.onButtonPressed(Button.AB, function () {
mstate.send(StateMachines.M0, "A+B")
})
input.onButtonPressed(Button.B, function () {
mstate.send(StateMachines.M0, "e")
})
mstate.exportUml(StateMachines.M0, "a", ModeExportUML.StateDiagram)
on state
ブロックの使い方
mstate拡張機能(pxt-mstate) では、entryアクションとdoアクティビティの実現方法は独特であり、entryアクションとdoアクティビティとを一つにしたon state
ブロックを使ってコーディングします。
on state
ブロックの処理部は、指定したインターバル時間(ms)毎に繰り返し呼び出されます。状態遷移時にリセットされ、0
から始まるカウンターを内部で保持しており、インターバル時間(ms)毎に加算されます。そのカウンターの値がtickcount
引数で渡されます。
on state
ブロックの処理部で、tickcount
の値が0
の場合に、entryアクションもしくはdoアクテビティとしての処理を実行するようにコーディングし、tickcount
の値が正の値の場合は、doアクテビティとしての処理を実行するようにコーディングします。
尚、インターバル時間(ms)を0
以下に指定すると、tickcount=0
である1回目だけが呼び出され、繰り返されません。
tickcount
引数の値比較
on state
ブロックの呼び出しがスキップされることを考慮して、tickcount
引数の値を比較します。
if (0 == tickcount) {
basic.showIcon(IconNames.Heart)
} else if (10 > tickcount) {
basic.showNumber(tickcount)
} else {
basic.showIcon(IconNames.Yes)
}
また、インターバル時間(ms)が短すぎたり、処理部の実行時間が長すぎたりすると、2回目以降の呼び出しで、その呼び出しがスキップされるかもしれません(tickcount=0
である1回目は必ず呼び出されます)。スキップされても、カウンターの値は、インターバール時間毎に内部で加算されていますので、tickcount
引数の値と比較する際は、このことを考慮してください(等しいかどうかだけで比較しないようにします)。
項目 | 説明 | 備考 |
---|---|---|
on state ブロック | entryアクション-doアクテビティとして、繰り返し呼び出されます。 | |
インターバル時間(ms) | 繰り返し呼び出される間隔を指定します。 インターバール時間が 0 以下の場合、1回目だけが呼び出され、繰り返されません。 |
インターバル時間が短すぎると、呼び出しがスキップされることがあります。ただし、1回目は必ず呼び出されます(tickcount=0 )。 |
tickcount 引数 |
インターバル時間毎に加算されるカウンターを内部で保持していますが、その値がtickcount 引数(変数)で渡されます。内部カウンターの値は、状態遷移時にリセットされ、0から始まります。 |
呼び出しがスキップされても、カウンターは内部で加算されていますので、tickcount 引数の値が連続しない場合があります。ただし、1回目は必ず呼び出されます(tickcount=0 )。 |
例1) 1回目だけの呼び出し
on state
ブロックのインターバル時間(ms)に 0
以下の値を指定すると、1回目だけ呼び出され、繰り返されません。その時のtickcount
引数の値は、0
です。
ブロック・コード | 説明 |
---|---|
インターバル時間(ms)に0以下の値として、 |
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.onState(0, function (tickcount) {
basic.showNumber(tickcount)
})
mstate.onExit(function () {
basic.showIcon(IconNames.Yes)
})
mstate.onTrigger("e", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
input.onButtonPressed(Button.A, function () {
mstate.send(StateMachines.M0, "e")
})
mstate.start(StateMachines.M0, "a")
例2) 繰り返し
インターバル時間(ms)に0
より大きい値を指定すると、そのインターバル時間(ms)毎にon state
ブロックが呼び出されようとします(呼び出しのスキップあり)。
on state
ブロック毎に、それぞれ独立したカウンターを内部に持っており、状態遷移後、0
から加算されていきます。そのカウンターの値は、on state
ブロックのtickcount
引数で渡されます。
ブロック・コード | 説明 |
---|---|
インターバル時間(ms)に |
カウントアップが連続しない
繰り返しのサンプルコードを実行して、動作を確認してみるとわかりますが、tickcount
引数の値が10以上になったり、状態遷移でアイコン表示したりすると、カウントアップが連続しなくなります。これは、表示処理がインターバール時間以内に完了しない為、呼び出しのスキップが発生しているためです。
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.onState(1000, function (tickcount) {
basic.showNumber(tickcount)
})
mstate.onExit(function () {
basic.showIcon(IconNames.Yes)
})
mstate.onTrigger("e", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
input.onButtonPressed(Button.A, function () {
mstate.send(StateMachines.M0, "e")
})
mstate.start(StateMachines.M0, "a")
例3) スキップされた繰り返し
on state
ブロックの内部カウンターは、インターバル時間(ms)毎に必ず加算されますが、インターバル時間(ms)が短すぎるとon state
ブロックの処理部の呼び出しがスキップされることもあります。
例えば、数を表示
ブロックを500ms毎に呼び出そうとした場合、表示される数が連続せず、呼び出しがスキップされた様子を確認できます。これは、数を表示
ブロックにおいて、その数を一定時間表示しようとして、内部で一時停止(600ms以上)の処理が実行されているからです。
on state
ブロックの処理部の呼び出しがスキップされても、内部のカウンターは、インターバル時間(ms)毎に加算される為、tickcount
引数の値を表示することでスキップの様子も確認できるのです。
ブロック・コード | 説明 |
---|---|
インターバル時間(ms)に |
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.onState(500, function (tickcount) {
basic.showNumber(tickcount)
})
mstate.onExit(function () {
basic.showIcon(IconNames.Yes)
})
mstate.onTrigger("e", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
input.onButtonPressed(Button.A, function () {
mstate.send(StateMachines.M0, "e")
})
mstate.start(StateMachines.M0, "a")
数の表示をすばやくする方法
数の表示
ブロックは、デフォルトでその処理に600ms以上の時間を要します。これをすばやくするには、JavaScriptで第2引数(interval
引数)に小さい値を指定します(1以上)。
basic.showNumber(counter, 1)
また、counterの値が2桁以上になると、スクロールが発生してしまうため、1桁目を表示するように工夫したりします(10で割った余り)。
basic.showNumber(counter % 10, 1)
尚、第2引数に0(ゼロ)を指定すると、micro:bit v1の実機では、異常終了してしまいますので、1以上の値を指定してください。micro:bit v2の実機やMakeCodeのシミュレーターでは、0(ゼロ)を指定しても異常終了しないようです。
遷移におけるガードとエフェクト
遷移には、トリガーによる遷移と完了遷移(Completion Transition)とがありますが、どちらの遷移も、ガード(guard)による条件でその遷移を抑制することができます。また、遷移に伴い、エフェクト(effect)と呼ばれるアクションを実行することも可能です。
これらを実現するには、on trigger
ブロックの内部(処理部)で、ガード(guard)やエフェクト(effect)をコーディングします。
遷移の評価で遷移の許可を忘れずに
on trigger
ブロックの内部(処理部)で、その遷移を評価し、その結果として遷移させる場合、traverse
ブロックで遷移先の添え字を指定し、遷移を許可します。遷移先の添え字を指定しなかったり、配列に対して無効な添え字を指定したりすると、遷移が抑止されます。
on trigger
ブロックの内部(処理部)で、ガード(guard)による条件を満たしているかを判定します。これを遷移の評価と呼んでいます。その条件を満たしていれば、traverse
ブロックを実行することで遷移を許可します。その条件を満たしていなければ、traverse
ブロックを実行せずに遷移を抑止します。1つのon trigger
ブロックに複数の遷移先を配列で定義できますので、遷移の評価で求められた遷移先の添え字をtraverse
ブロックの引数に指定します。
また、遷移の評価において、ガード(guard)による条件を満たしていれば、その分岐処理の中で、エフェクト(effect)を実行するようにコーディングします。尚、mstate拡張機能(pxt-mstate) は、エフェクト(effect)を実行するかどうかについて関与しませんので、on trigger
ブロックの内部(処理部)でガード(guard)による分岐処理とエフェクト(effect)の実行処理とを適切にコーディングします。
ステート図 | ブロック・コード |
---|---|
|
ソースコード
mstate.defineState(StateMachines.M0, "偶数", function () {
mstate.onState(0, function (tickcount) {
basic.showIcon(IconNames.Square)
})
mstate.onTrigger("e", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
mstate.defineState(StateMachines.M0, "a", function () {
mstate.descriptionUml("余りの計算\\n(奇数・偶数)")
mstate.onState(1000, function (tickcount) {
basic.showNumber(tickcount)
余り = tickcount % 2
})
mstate.descriptionUml("余り=1")
mstate.descriptionUml("余り<>1")
mstate.onTrigger("e", ["奇数", "偶数"], function () {
if (1 == 余り) {
mstate.traverse(StateMachines.M0, 0)
} else {
mstate.traverse(StateMachines.M0, 1)
}
})
})
input.onButtonPressed(Button.A, function () {
mstate.send(StateMachines.M0, "e")
})
mstate.defineState(StateMachines.M0, "奇数", function () {
mstate.onState(0, function (tickcount) {
basic.showIcon(IconNames.Diamond)
})
mstate.onTrigger("e", ["a"], function () {
mstate.traverse(StateMachines.M0, 0)
})
})
let 余り = 0
mstate.start(StateMachines.M0, "a")
mstate.exportUml(StateMachines.M0, "a", ModeExportUML.StateDiagram)
トリガーの引数
send
ブロックでトリガーを送る際に、引数args
(数値の配列)を渡すことができます。
これまで、ボタンAやボタンB、ボタンA+Bのそれぞれのイベントに対応した別々のトリガーを送っていましたが、もしこれが、パソコンのキーボードだったらいくつのトリガーが必要でしょうか。トリガーの引数args
を使えば、1つのトリガーを用意し、その引数args
にキーの番号を渡すことで、遷移の評価において、その引数args
の値を取得し、どのキーが押されたのかを判断することができます。
トリガーのタイムラグ
トリガーを送信してから遷移の評価が起こるまで(on trigger
ブロックの処理部が実行されるまで)、そこにはタイムラグがあります。ステートマシンが何らか別の振る舞いを処理している場合、その振る舞いが完了するまで、送信されたトリガーは保留にされるからです。
例えば、ボタンでスタートとストップできるストップウォッチを作成しようとした場合、稼働時間(ミリ秒)
ブロックを使って稼働時間を取得するかと思いますが、ボタンが押された直後の稼働時間をトリガーの引数で渡す必要があります。そうすれば、トリガーのタイムラグがあったとしても、スタートとストップが押された直後の稼働時間(スタート)と稼働時間(ストップ)とで経過時間を計算できます。
次の例では、ボタンAやボタンB、ボタンA+Bが操作された際に、トリガーe
を送るとともに、引数args
の0番目の値に対象のボタン番号(1:A, 2;B, 3:A+B)を、1番目の値にP0端子のデジタル入力値(0, 1)を渡しています。
渡された引数args
は、args
ブロックで数値の配列として取得できますので(on trigger
ブロック内でのみ取得が可能)、その値を元にアルファベットを表示しています(P0端子をキーボードのシフトキーに見立てています)。
ステート図 | ブロック・コード |
---|---|
|
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.descriptionUml("トリガー引数で\\nエフェクト(内部遷移)")
mstate.onTrigger("e", [], function () {
ボタン = mstate.args(StateMachines.M0)[0]
P0値 = mstate.args(StateMachines.M0)[1]
if (1 == ボタン) {
if (0 == P0値) {
basic.showString("a")
} else {
basic.showString("A")
}
} else if (2 == ボタン) {
if (0 == P0値) {
basic.showString("b")
} else {
basic.showString("B")
}
} else if (3 == ボタン) {
if (0 == P0値) {
basic.showString("c")
} else {
basic.showString("C")
}
} else {
basic.showIcon(IconNames.No)
}
})
})
input.onButtonPressed(Button.A, function () {
mstate.send(StateMachines.M0, "e", [1, pins.digitalReadPin(DigitalPin.P0)])
})
input.onButtonPressed(Button.AB, function () {
mstate.send(StateMachines.M0, "e", [3, pins.digitalReadPin(DigitalPin.P0)])
})
input.onButtonPressed(Button.B, function () {
mstate.send(StateMachines.M0, "e", [2, pins.digitalReadPin(DigitalPin.P0)])
})
let ボタン = 0
let P0値 = 0
P0値 = pins.digitalReadPin(DigitalPin.P0)
mstate.start(StateMachines.M0, "a")
mstate.exportUml(StateMachines.M0, "a", ModeExportUML.StateDiagram)
内部遷移と自己遷移(外部遷移)
トリガーの引数を確認するために、on trigger
ブロックで遷移しない遷移をコーディングしましたが、これを内部遷移と呼びます。また、これとは別に自己遷移と呼ばれる遷移(外部遷移)もあります。結果的にどちらも元の状態のままに見えますが遷移の有無に違いがあります。
- 内部遷移 - 遷移しない為、exitアクション等が行われない
- 自己遷移 - 遷移する為、exitアクション、entryアクションが行われる
この内部遷移を使えば、トリガー等に反応して、エフェクトだけを実行することが可能です。また、mstate拡張機能(pxt-mstate)では、階層構造を直接的に定義できませんので、この内部遷移のエフェクトにおいて、他のステートマシンを操作することが想定されます。
on state
ブロックと完了遷移(Completion Transition)
ある状態に遷移すると、entryアクションとdoアクテビティとが実行された直後に完了遷移(Completion Transition)に対する遷移の評価が行われます。mstate拡張機能(pxt-mstate)では、さらに、2回目以降のdoアクテビティが実行された直後にも完了遷移(Completion Transition)に対する遷移の評価が繰り返されます。
The run-to-completion paradigm
統一モデリング言語(UML)の仕様書では、ステートマシン自体を動作させるための処理のサイクルについても説明しており、それをrun-to-completion パラダイム(paradigm)と呼んでいます。また、そのパラダイムにおけるそれぞれの処理をrun-to-completion ステップ(step)と呼んでいます。
また、パラダイムの中には、待機ポイント(wait point)と呼ばれる安定した状況(state configuration)があります。トリガー等のイベントが発生しても、前のイベントに対応する処理が実行されていたりすれば、待機ポイントになるまでそのイベントに対応する処理の実行は保留とされます。
つまり、そのステートマシンにおいて、複数のイベントに対応する処理が同時に実行されることはありません。
インターバル時間の異なるon state
ブロックが複数ある場合(例えば、0msと3000ms、7000ms)、次のように処理が実行され、遷移の評価が行われます。
- 状態が遷移すると全ての
on state
ブロックが実行されます
(tickcount=0
:entryアクションとdoアクティビィとして呼び出される) - 最初の完了遷移(Completion Transition)が評価されます
- いずれかの
on state
ブロックが呼び出されます
(例えば、3000msのブロックが、tickcount>0
:doアクティビィとして呼び出される) - 完了遷移(Completion Transition)が評価されます
- いずれかの
on state
ブロックが呼び出されます
(例えば、3000msのブロックが、tickcount>0
:doアクティビィとして呼び出される) - 完了遷移(Completion Transition)が評価されます
- いずれかの
on state
ブロックが呼び出されます
(例えば、7000msのブロックが、tickcount>0
:doアクティビィとして呼び出される) - 完了遷移(Completion Transition)が評価されます
- ボタンAでトリガーを送ります
- 対応するトリガーの遷移が評価されますが、完了遷移(Completion Transition)は評価されません
ステート図 | ブロック・コード |
---|---|
|
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.descriptionUml("entry/ 1を設定")
mstate.onState(0, function (tickcount) {
評価事由 = 1
})
mstate.descriptionUml("do/")
mstate.descriptionUml("1. 3秒毎に2を設定")
mstate.onState(3000, function (tickcount) {
if (tickcount > 0) {
評価事由 = 2
}
})
mstate.descriptionUml("2. 7秒毎に3を設定")
mstate.onState(7000, function (tickcount) {
if (tickcount > 0) {
評価事由 = 3
}
})
mstate.descriptionUml("評価/")
mstate.descriptionUml("- 完了遷移の評価")
mstate.onTrigger("", [], function () {
basic.showNumber(評価事由)
basic.clearScreen()
評価事由 = 0
})
mstate.descriptionUml("- トリガーによる遷移の評価")
mstate.onTrigger("e", [], function () {
basic.showString("e")
basic.clearScreen()
})
})
input.onButtonPressed(Button.A, function () {
mstate.start(StateMachines.M0, "a")
})
input.onButtonPressed(Button.B, function () {
mstate.send(StateMachines.M0, "e")
})
let 評価事由 = 0
mstate.exportUml(StateMachines.M0, "a", ModeExportUML.StateDiagram)
アフター5の実現
所定の時間が経過したら(タイムアウトしたら)、何らかのアクションを行ったり、状態を遷移させたりしたい場合があります。
次の例では、doアクテビティと完了遷移とを組み合わせて、5秒経過したら状態を遷移するようにしています。
ステート図 | ブロック・コード |
---|---|
|
ソースコード
mstate.defineState(StateMachines.M0, "a", function () {
mstate.onState(5000, function (tickcount) {
if (0 == tickcount) {
basic.showIcon(IconNames.Heart)
}
after5 = tickcount
})
mstate.descriptionUml("after 5s")
mstate.onTrigger("", ["b"], function () {
if (0 < after5) {
mstate.traverse(StateMachines.M0, 0)
}
})
})
mstate.defineState(StateMachines.M0, "b", function () {
mstate.onState(5000, function (tickcount) {
if (0 == tickcount) {
basic.showIcon(IconNames.SmallHeart)
}
after5 = tickcount
})
mstate.descriptionUml("after 5s")
mstate.onTrigger("", ["a"], function () {
if (0 < after5) {
mstate.traverse(StateMachines.M0, 0)
}
})
})
let after5 = 0
mstate.start(StateMachines.M0, "a")
mstate.exportUml(StateMachines.M0, "a", ModeExportUML.StateDiagram)
「後日、こちらからご連絡差し上げます」
これは、ものごとをお断りをする際の常套句であり、ハリウッドの原則(Hollywood Principle)とも言うようです("Don't Call Us, We'll Call You.")。
プログラミングにおけるフレームワークにおいてもハリウッドの原則が適用されています。しかし、その意図するところは異なり、フレームワークからは所定の条件と順序で適切かつ確実に処理が呼び出されます。
mstate拡張機能(pxt-mstate)もそうですが、フレームワークの活用で堅牢なシステムの構築が可能です。
まとめ
ステートマシンの基礎と基本として、ステート図とコードを示しながらmstate拡張機能(pxt-mstate)の使い方を中心に説明しました。
mstate拡張機能(pxt-mstate)というフレームワークを利用すれば、ステートマシンという考え方で設計し、コーディングすることができます。単にアイコン表示やカウント表示をする機能であれば、ステートマシンの必要性は感じないかもしれません。それでも、ステート図で表現するとその振る舞いをより理解できるようになると思います。
また、組み込み系のmicro:bitプログラミングにおいては、その振る舞いが複雑化し、フローチャートだけでは上手く表現しきれない場面があります。そんな時、ステート図も用いると、要求を実現させた不具合の少ないコーディングが可能になると信じています。
【ステートマシン版】Flashing Heart