LoginSignup
0
1

【micro:bit ステートマシン】基礎と基本 ~状態遷移図で学ぶ状態遷移~

Last updated at Posted at 2024-02-24

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)
image.png

セットアップ手順
次の手順で、mstate拡張機能(pxt-mstate)をセットアップしてください。

  1. mstate拡張機能(HEX形式ファイル)をダウンロードします
    https://github.com/jp-rad/pxt-mstate/releases
  2. Microsoft MakeCode for micro:bit で新しいプロジェクトを作成します
    https://makecode.microbit.org/
  3. 拡張機能を開き、ファイルを読み込むボタンで、ダウンロードしたHEX形式ファイルを選択します(つづけるで確定)

mstate拡張機能の使い方

defineブロックで、ステートマシンの状態を定義していきます。
尚、サブステートマシンやサブ状態といった階層構造を直接的に定義できませんので、M0からM5までのステートマシンを使って、相互に連携させます。

image.png

ステート図の読み方・書き方
mstate拡張機能(pxt-mstate)のステートマシンをPlantUMLのステート・ダイアグラムとしてステート図に表現することができます。

image.png
を選択すると、ログとして出力されますので、出力された内容をコピーし、 https://www.plantuml.com/plantuml/ に貼り付けます。

ステート図(ステートマシン図、状態遷移図)の読み方や書き方は、統一モデリング言語(UML)の参考書を参照してください。また、本記事と参考書とを読み比べてmstate拡張機能(pxt-mstate)で出来ること、出来ないこと、実現方法の違いなどを確認してみてください。

▼おススメの参考書(UML入門書)
UMLモデリングの基本が簡潔にまとめられています。
かんたん UML入門 [改訂2版] – 2017/7/1

ステート図の出力
mstate拡張機能(pxt-mstate)で状態を定義すると、それをPlantUMLのWebサービスを使って、ステート図に表すことができます。
また、そのステート図の状態等に説明を補足する為、(UML) descriptionブロックを使っていますが、本記事ではその使い方の説明を省略しています。
尚、このブロックは、プログラムの動作に対して何も影響しません。ただし、文字列としてのメモリを消費しますので、HEX形式ファイルの容量オーバーでコンパイルできなかったり、micro:bit本体に転送できなかったりする場合があります。

容量オーバーの例
image.png

状態(ステート)

ステートマシンには、次の3つの状態の概念があり、状態から状態へと遷移します。

  • 初期疑似状態(Initial Pseudostate)
  • 状態(State)
  • 終了状態(FinalState)

初期疑似状態と終了状態
mstate拡張機能(pxt-mstate)の内部において、初期疑似状態と終了状態とは同じ状態です。また、その状態名は、""(空の文字列)です。

ステートマシンが初期疑似状態である場合に、startブロックを実行するとステートマシンが開始され、初期疑似状態から指定したデフォルト状態へと遷移します。

ステート図 ブロック・コード

image.png

ソースコード
JavaScript
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ブロックで""(空の文字列)を送ってもトリガーとしての遷移は行われません。

ステート図 ブロック・コード

image.png

ソースコード
JavaScriptmstate.defineState(StateMachines.M0, "a", function () {
    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つの振る舞いとタイミング 説明
  1. ある状態へ遷移すると
    entryアクションを実行します
  2. entryの実行を完了すると
    doアクテビティを実行します
    (状態遷移を待つ)
  3. 状態遷移が行われようとすると(遷移前)
    exitアクションを実行します
  4. exitの実行を完了すると
    状態を抜け出し、状態遷移が行われます

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つの振る舞いが実行されることを確認してください。

ステート図 ブロック・コード

image.png

ソースコード
JavaScript
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""(空の文字列)を指定します。

ステート図 ブロック・コード

image.png

ソースコード
JavaScript
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引数の値を比較します。

JavaScript

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 ブロック

image.png

entryアクション-doアクテビティとして、繰り返し呼び出されます。
インターバル時間(ms) 繰り返し呼び出される間隔を指定します。
インターバール時間が0以下の場合、1回目だけが呼び出され、繰り返されません。
インターバル時間が短すぎると、呼び出しがスキップされることがあります。ただし、1回目は必ず呼び出されます(tickcount=0)。
tickcount引数 インターバル時間毎に加算されるカウンターを内部で保持していますが、その値がtickcount引数(変数)で渡されます。内部カウンターの値は、状態遷移時にリセットされ、0から始まります。 呼び出しがスキップされても、カウンターは内部で加算されていますので、tickcount引数の値が連続しない場合があります。ただし、1回目は必ず呼び出されます(tickcount=0)。

例1) 1回目だけの呼び出し

on stateブロックのインターバル時間(ms)に 0 以下の値を指定すると、1回目だけ呼び出され、繰り返されません。その時のtickcount引数の値は、0です。

ブロック・コード 説明

image.png

インターバル時間(ms)に0以下の値として、0を指定します。
ステートマシンを開始すると、状態aに遷移し、tickcount引数の値0が表示されます。
ボタンAで、トリガーeを送信すると、exitアクションで、Yes(チェック)アイコンが表示され、再び、状態aに遷移し、0が表示されます。

ソースコード
JavaScript
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引数で渡されます。

ブロック・コード 説明

image.png

インターバル時間(ms)に1000を指定します(1,000ms=1秒)。
ステートマシンを開始すると、状態aに遷移し、tickcount引数の値が1秒毎に繰り返し表示されます(0からのカウントアップ)。
ボタンAで、トリガーeを送信すると、exitアクションで、Yes(チェック)アイコンが表示された後、再び、状態aに遷移し、0からのカウントアップ表示が始まります。

カウントアップが連続しない
繰り返しのサンプルコードを実行して、動作を確認してみるとわかりますが、tickcount引数の値が10以上になったり、状態遷移でアイコン表示したりすると、カウントアップが連続しなくなります。これは、表示処理がインターバール時間以内に完了しない為、呼び出しのスキップが発生しているためです。

ソースコード
JavaScript
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引数の値を表示することでスキップの様子も確認できるのです。

ブロック・コード 説明

image.png

インターバル時間(ms)に500を指定します。
ステートマシンを開始すると、状態aに遷移し、tickcount引数の値が0からカウントアップ表示されますが、その表示は連続しておらず、スキップされた様子を確認できます。
ただし、1回目は必ず呼び出されます(tickcount=0)。

ソースコード
JavaScript
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以上)。

JavaScript
    basic.showNumber(counter, 1)

また、counterの値が2桁以上になると、スクロールが発生してしまうため、1桁目を表示するように工夫したりします(10で割った余り)。

JavaScript
    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)の実行処理とを適切にコーディングします。

ステート図 ブロック・コード

image.png

ソースコード
JavaScript
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端子をキーボードのシフトキーに見立てています)。

ステート図 ブロック・コード

image.png

ソースコード
JavaScript
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)、次のように処理が実行され、遷移の評価が行われます。

  1. 状態が遷移すると全てのon stateブロックが実行されます
    tickcount=0:entryアクションとdoアクティビィとして呼び出される)
  2. 最初の完了遷移(Completion Transition)が評価されます
  3. いずれかのon stateブロックが呼び出されます
    (例えば、3000msのブロックが、tickcount>0:doアクティビィとして呼び出される)
  4. 完了遷移(Completion Transition)が評価されます
  5. いずれかのon stateブロックが呼び出されます
    (例えば、3000msのブロックが、tickcount>0:doアクティビィとして呼び出される)
  6. 完了遷移(Completion Transition)が評価されます
  7. いずれかのon stateブロックが呼び出されます
    (例えば、7000msのブロックが、tickcount>0:doアクティビィとして呼び出される)
  8. 完了遷移(Completion Transition)が評価されます
  9. ボタンAでトリガーを送ります
  10. 対応するトリガーの遷移が評価されますが、完了遷移(Completion Transition)は評価されません
ステート図 ブロック・コード

image.png

ソースコード
JavaScript
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秒経過したら状態を遷移するようにしています。

ステート図 ブロック・コード

image.png

ソースコード
JavaScript
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

image.png

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1