Lチカとステートマシン
(LEDをチカチカと表示させる)Lチカをコーディングをする際にフローチャートで設計するのがわかりやすい(とっつきやすい)と思いますが、ちょっと複雑なことをしようとすると、ソースコードがスパゲッティになってしまいます。そもそも「ソースコードがスパゲッティになる」原因は、フローチャートのような設計図がスパゲッティになってしまうからです(ループのネストと複雑な条件分岐)。
Lチカ自体は、フローチャートで表し、再利用できるように関数化しておくと良いと思いますが、全体的な振る舞いに対しては、ステートマシンという考え方を取り入れ、状態遷移図や状態遷移表を使って設計すると、より複雑な機能でもソースコードがスパゲッティになることを防げます。
ステートマシン
ステートマシンについては、次のキーワードで他の文献や資料を参照してください。
- ステートマシン
- 状態遷移図
- 状態遷移表
- UML State Machine Diagram
- 有限オートマトン
状態遷移図や状態遷移表の設計を元に、状態変数や分岐処理を駆使したコーディングも可能ですが、一般的にはステートマシンと呼ばれるようなライブラリーを使用します。今回は、micro:bit向けのブロック型ステートマシンとして、Mstate拡張機能を使います。
Mstate拡張機能
Mstate拡張機能
はユーザー定義の拡張機能です。拡張機能の追加画面で、URLを入力して、追加できます。
+拡張機能
→ 拡張機能
(検索または、プロジェクトのURLを入力・・・)
→ https://github.com/jp-rad/pxt-mstate
Lチカの基本
LEDをチカチカと点滅するために、まずは、「Lチカのフローチャート」に従って、コーディングしました。
blinkNext
の値に従って、LED画面の明るさを交互に切り替えています(関数blinkLED
)。
JavaScript
function blinkLED () {
if (0 == blinkNext) {
blinkNext = 1
led.setBrightness(100)
} else {
blinkNext = 0
led.setBrightness(255)
}
}
let blinkNext = 0
blinkNext = 1
basic.showIcon(IconNames.Heart)
loops.everyInterval(500, function () {
blinkLED()
})
Mstate拡張機能を使ったLチカ
1つの状態のみで構成されるステートマシンをMstate拡張機能を使って次のようにコーディングできます。
尚、Mstate拡張機能のDoアクティビティでの振る舞いに注意してください。その状態へ状態遷移すると、Entryアクションを実行後、次の状態遷移が行われるまで、Doアクティビティを指定間隔毎に繰り返し実行します。
JavaScript
function blinkLED () {
if (0 == blinkNext) {
blinkNext = 1
led.setBrightness(100)
} else {
blinkNext = 0
led.setBrightness(255)
}
}
let blinkNext = 0
mstate.declareEntry("State1", function (prev) {
blinkNext = 1
basic.showIcon(IconNames.Heart)
})
mstate.declareDo("State1", 500, function () {
blinkLED()
})
mstate.start("State1")
状態遷移図
状態遷移図を使って次のように設計できます。
mstate.declareEntry("State1", ...)
が、図中State1
のentry/ LED表示
に対応し、
mstate.declareDo("State1", 500, ...)
が、図中State1
のdo/ LED点滅 (500ms)
(500ms毎の繰り返し)に対応しています。
また、mstate.start("State1")
が、図中State1
(デフォルトステート)からの開始を意味します。
Mstateブロックの書き方
最初だけブロック内にすべてのブロックを配置しましたが、状態名称定義ブロック(defineStateName()
)内に配置することをお勧めします。状態別にEntryアクション宣言ブロック、Doアクティビティ宣言ブロック、Exitアクション宣言ブロック、トランジション宣言ブロックをまとめておくことが可能です。また、STATE
変数を参照すれば、状態名称の入力ミスを減らすこともできます。
例えば、次の状態遷移図を設計し、Aボタンでステートマシンを開始し、BボタンでFinal
("*")へ遷移するようにコーディングしてみます。
状態遷移図
JavaScript
let blinkNext = 0
function blinkLED () {
if (0 == blinkNext) {
blinkNext = 1
led.setBrightness(100)
} else {
blinkNext = 0
led.setBrightness(255)
}
}
mstate.defineStateName("State1", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
blinkNext = 1
basic.showIcon(IconNames.Heart)
})
mstate.declareDo(STATE, 500, function () {
blinkLED()
})
mstate.declareExit(STATE, function (next) {
basic.showIcon(IconNames.SmallHeart)
})
mstate.declareTransition(STATE, "*", "Trigger1")
})
input.onButtonPressed(Button.A, function () {
mstate.start("State1")
})
input.onButtonPressed(Button.B, function () {
mstate.fire("Trigger1")
})
点滅スピードの切り替え
次に点滅スピードを切り替えられるようにしてみます。Doアクティビティの繰り返し間隔は指定可能ですが、実行中に動的に変更することはできません(ハードコーディング)。そこで、遅い点滅の状態(500ms)と速い点滅の状態(200ms)とをそれぞれ宣言し、トリガー("A")で交互に状態遷移するようにします。
それを次の状態遷移図のように設計しました。
状態遷移図
この設計でも期待する動作をしますが、状態遷移する度にLED表示
が繰り返し呼び出されるのが気になります。
そこで、LED表示
だけを行う状態("On")を追加し、次のように再設計しました。
(再設計) 状態遷移図
JavaScript
function blinkLED () {
if (0 == blinkNext) {
blinkNext = 1
led.setBrightness(100)
} else {
blinkNext = 0
led.setBrightness(255)
}
}
mstate.defineStateName("On", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
blinkNext = 0
basic.showIcon(IconNames.Heart)
})
mstate.declareTransition(STATE, "Slow", "")
})
mstate.defineStateName("Slow", function (STATE) {
mstate.declareDo(STATE, 500, function () {
blinkLED()
})
mstate.declareTransition(STATE, "Fast", "A")
})
mstate.defineStateName("Fast", function (STATE) {
mstate.declareDo(STATE, 200, function () {
blinkLED()
})
mstate.declareTransition(STATE, "Slow", "A")
})
input.onButtonPressed(Button.A, function () {
mstate.fire("A")
})
let blinkNext = 0
mstate.start("On")
ちょっと複雑なLチカ
もうちょっと複雑なLチカを考えてみました。
- micro:bitの電源を入れると、LEDは消灯状態となる(何も表示しない)
- ボタンAを押すと、LEDが点灯する
- ボタンAを押すたびに、点灯、遅い点滅、速い点滅の順にLED表示が切り替わる
- ボタンBを押すと、LEDは消灯状態となる(何も表示しない)
- ボタンA+Bを押すと、自動モードとなり、一定間隔でボタンAが押されているような動作をする
ただし、消灯状態では、自動モードにならない - ボタンBを押すと自動モードは解除され、通常モードに戻る(同時にLEDは消灯状態となる)
状態とトリガー
ステートマシンを使ったプログラミングを行うため、まずは、状態とトリガーを洗い出します。
ただし、この例では、LEDの表示状態とモード状態とが同時に現れるので、ステートマシンを2つに分けて考えます。
ステートマシン1-LED表示状態
状態(1) | 説明 |
---|---|
(初期状態) | micro:bitが起動していない(電源オフ) |
Off | LEDは消灯状態で何も表示されていない |
On | LEDは点灯状態である(ハートアイコンを表示) |
Slow | LEDは遅い点滅を繰り返す(500ms間隔) |
Fast | LEDは速い点滅を繰り返す(200ms間隔) |
トリガー
トリガー(1) | 説明 |
---|---|
(電源オン) | 電源が入れられた |
A | ボタンAが押された |
B | ボタンBが押された |
ステートマシン2-モード
状態(2) | 説明 |
---|---|
通常モード | ボタンで表示を操作できる |
自動モード | 一定間隔で点灯・点滅が切り替わる |
トリガー
トリガー(2) | 説明 |
---|---|
A+B | ボタンA+Bが押された |
トリガーAから考える
まずは、トリガーA
による状態遷移から考えてみました。
状態遷移図
- Offへ(●-->Off)
電源オンで、初期状態(●印)から状態Off
へ遷移します。この状態Off
に遷移した時(entry)、LEDを消灯にしています。 - Onへ(Off-->On)
ボタンAが押された際に、トリガーA
を発生させることにより、状態がOff
からOn
へと遷移します。On
に遷移した時(entry)、「点滅状態初期化」で、点滅の明るさを暗くした状態から始めるように、ここで初期化します。その後、LEDを点灯にしています。 - Slowへ(On-->Slow)
さらにボタンAが押された際に、トリガーA
を発生させることにより、状態がOn
からSlow
へと遷移します。LEDを点滅させるために、その明るさを暗くしたり明るくしたりします。状態On
で「点滅状態初期化」がされているため、暗い状態から始まります。
実際には、do
で500ms毎に明るさを切り替える処理(LED点滅
の呼び出し)を繰り返します(トリガーA
が発生するまで)。 - Fastへ(Slow-->Fast)
またさらに、トリガーA
を発生させることにより、状態がSlow
からFast
へと遷移しますが、Fast
では、entry
がありません。これは、明るさの状態(変数の値)をSlow
から引き継ぐ為です。そして、do
では、200ms毎に明るさを切り替える処理を繰り返します。 - 再びOnへ(Fast-->On)
繰り返し、トリガーA
を発生させることにより、状態がFast
からOn
へと再び遷移しますが、遷移する前に、Fast
のexit
が実行されます。タイミングによっては、LEDが暗くなっている場合があるために、最大の明るさに戻しています。
尚、状態遷移図を状態遷移表に書き換えることもできます。
状態遷移表
状態 | アクション | (電源オン) | A |
---|---|---|---|
● | Offへ | - | |
Off | entry/ - LED消灯 |
- | Onへ |
On | entry/ - 点滅状態初期化 - LED点灯 |
- | Slowへ |
Slow | do/ - LED点滅(500ms) |
- | Fastへ |
Fast | do/ - LED点滅(200ms) exit/ - 明るさ最大化 |
- | Onへ |
状態遷移表のままに、コーディング(宣言)します(ブロックは省略)。
JavaScript
function blinkLED () {
if (0 == blinkNext) {
blinkNext = 1
led.setBrightness(100)
} else {
blinkNext = 0
led.setBrightness(255)
}
}
mstate.defineStateName("Off", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
basic.clearScreen()
})
mstate.declareTransition(STATE, "On", "A")
})
mstate.defineStateName("On", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
blinkNext = 0
basic.showIcon(IconNames.Heart)
})
mstate.declareTransition(STATE, "Slow", "A")
})
mstate.defineStateName("Slow", function (STATE) {
mstate.declareDo(STATE, 500, function () {
blinkLED()
})
mstate.declareTransition(STATE, "Fast", "A")
})
mstate.defineStateName("Fast", function (STATE) {
mstate.declareDo(STATE, 200, function () {
blinkLED()
})
mstate.declareExit(STATE, function (next) {
led.setBrightness(255)
})
mstate.declareTransition(STATE, "On", "A")
})
input.onButtonPressed(Button.A, function () {
mstate.fire("A")
})
let blinkNext = 0
mstate.start("Off")
トリガーBを追加する
次にトリガーB
による状態遷移を追加して考えます。
状態On
、または、状態Slow
、または、状態Fast
から状態Off
へと状態遷移するようにします。
状態遷移図
状態遷移表
状態 | アクション | (電源オン) | A | B |
---|---|---|---|---|
● | Offへ | - | - | |
Off | entry/ - LED消灯 |
- | Onへ | - |
On | entry/ - 点滅状態初期化 - LED点灯 |
- | Slowへ | Offへ |
Slow | do/ - LED点滅(500ms) |
- | Fastへ | Offへ |
Fast | do/ - LED点滅(200ms) exit/ - 明るさ最大化 |
- | Onへ | Offへ |
JavaScript
function blinkLED () {
if (0 == blinkNext) {
blinkNext = 1
led.setBrightness(100)
} else {
blinkNext = 0
led.setBrightness(255)
}
}
mstate.defineStateName("Off", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
basic.clearScreen()
})
mstate.declareTransition(STATE, "On", "A")
})
mstate.defineStateName("On", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
blinkNext = 0
basic.showIcon(IconNames.Heart)
})
mstate.declareTransition(STATE, "Slow", "A")
mstate.declareTransition(STATE, "Off", "B")
})
mstate.defineStateName("Slow", function (STATE) {
mstate.declareDo(STATE, 500, function () {
blinkLED()
})
mstate.declareTransition(STATE, "Fast", "A")
mstate.declareTransition(STATE, "Off", "B")
})
mstate.defineStateName("Fast", function (STATE) {
mstate.declareDo(STATE, 200, function () {
blinkLED()
})
mstate.declareExit(STATE, function (next) {
led.setBrightness(255)
})
mstate.declareTransition(STATE, "On", "A")
mstate.declareTransition(STATE, "Off", "B")
})
input.onButtonPressed(Button.A, function () {
mstate.fire("A")
})
input.onButtonPressed(Button.B, function () {
mstate.fire("B")
})
let blinkNext = 0
mstate.start("Off")
【不具合】LED点灯が暗くなることがある
トリガーBを追加したことにより、「LED点灯が暗くなることがある」という不具合が発生します。
状態Slow
では、LEDの明るさが暗い場合があります。もし暗いままで、状態Off
へ状態遷移し、その後、状態On
に状態遷移しても、LEDの明るさは暗いままです。
状態Fast
の場合は、Exitアクションで明るさを最大化していますので、この不具合の原因にはなりません。
したがって、「明るさ最大化」を状態On
のEntryアクションへ移動することにしました。
状態遷移図
状態遷移表
状態 | アクション | (電源オン) | A | B |
---|---|---|---|---|
● | Offへ | - | - | |
Off | entry/ - LED消灯 |
- | Onへ | - |
On | entry/ - 点滅状態初期化 - 明るさ最大化 - LED点灯 |
- | Slowへ | Offへ |
Slow | do/ - LED点滅(500ms) |
- | Fastへ | Offへ |
Fast | do/ - LED点滅(200ms) |
- | Onへ | Offへ |
JavaScript
function blinkLED () {
if (0 == blinkNext) {
blinkNext = 1
led.setBrightness(100)
} else {
blinkNext = 0
led.setBrightness(255)
}
}
mstate.defineStateName("Off", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
basic.clearScreen()
})
mstate.declareTransition(STATE, "On", "A")
})
mstate.defineStateName("On", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
led.setBrightness(255)
blinkNext = 0
basic.showIcon(IconNames.Heart)
})
mstate.declareTransition(STATE, "Slow", "A")
mstate.declareTransition(STATE, "Off", "B")
})
mstate.defineStateName("Slow", function (STATE) {
mstate.declareDo(STATE, 500, function () {
blinkLED()
})
mstate.declareTransition(STATE, "Fast", "A")
mstate.declareTransition(STATE, "Off", "B")
})
mstate.defineStateName("Fast", function (STATE) {
mstate.declareDo(STATE, 200, function () {
blinkLED()
})
mstate.declareTransition(STATE, "On", "A")
mstate.declareTransition(STATE, "Off", "B")
})
input.onButtonPressed(Button.A, function () {
mstate.fire("A")
})
input.onButtonPressed(Button.B, function () {
mstate.fire("B")
})
let blinkNext = 0
mstate.start("Off")
自動モードへの対応
最後に、自動モードへの対応を行います。ただし、現状のMstate拡張機能では、ステートマシンを1つしか宣言できない為、通常モード・自動モードのステートマシンを追加できません。
Mstate拡張機能のM
micro:bitのMのようにも思えますが、"MODOKI"のMです。Mstate拡張機能では、完全なステートマシンが実装されているわけではありません。
そこで、mode
変数で状態を管理し、自動モードの機能を実現しました。
JavaScriptのソースコードだけを示しておきます。
JavaScript
mstate.defineStateName("On", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
led.setBrightness(255)
blinkNext = 0
basic.showIcon(IconNames.Heart)
})
mstate.declareTransition(STATE, "Slow", "A")
mstate.declareTransition(STATE, "Slow", "A+B")
mstate.declareTransition(STATE, "Off", "B")
})
function blinkLED () {
if (0 == blinkNext) {
blinkNext = 1
led.setBrightness(100)
} else {
blinkNext = 0
led.setBrightness(255)
}
}
input.onButtonPressed(Button.A, function () {
mstate.fire("A")
})
input.onButtonPressed(Button.AB, function () {
mode = 1
})
input.onButtonPressed(Button.B, function () {
mstate.fire("B")
})
mstate.defineStateName("Slow", function (STATE) {
mstate.declareDo(STATE, 500, function () {
blinkLED()
})
mstate.declareTransition(STATE, "Fast", "A")
mstate.declareTransition(STATE, "Fast", "A+B")
mstate.declareTransition(STATE, "Off", "B")
})
mstate.defineStateName("Fast", function (STATE) {
mstate.declareDo(STATE, 200, function () {
blinkLED()
})
mstate.declareTransition(STATE, "On", "A")
mstate.declareTransition(STATE, "On", "A+B")
mstate.declareTransition(STATE, "Off", "B")
})
mstate.defineStateName("Off", function (STATE) {
mstate.declareEntry(STATE, function (prev) {
basic.clearScreen()
})
mstate.declareExit(STATE, function (next) {
mode = 0
})
mstate.declareTransition(STATE, "On", "A")
})
let mode = 0
let blinkNext = 0
mstate.start("Off")
loops.everyInterval(3000, function () {
if (1 == mode) {
mstate.fire("A+B")
}
})
おわりに
- Lチカを題材にMstate拡張機能を使いました
- 状態遷移図や状態遷移表が設計できれば、Mstate拡張機能で宣言するだけでコーディングできます
- ステートマシン(という考え方)を使ってmicro:bitのLチカを機能改善しました
- 機能改善の中で、新たな不具合が発生することがありますが、簡単に解決できました
最終成果物(Lチカ-ステートマシン)