ステートマシン（という考え方）を使ってmicro:bitのLチカを機能改善する

# Lチカとステートマシン

（LEDをチカチカと表示させる）Lチカをコーディングをする際にフローチャートで設計するのがわかりやすい（とっつきやすい）と思いますが、ちょっと複雑なことをしようとすると、ソースコードがスパゲッティになってしまいます。そもそも「ソースコードがスパゲッティになる」原因は、フローチャートのような設計図がスパゲッティになってしまうからです（ループのネストと複雑な条件分岐）。

```plantuml
title Lチカのフローチャート
start
:blinkNext=1]
while (every 500ms)
if (0=blinkNext) then (yes)
  :blinkNext=1]
  :LEDを暗く]
else (no)
  :blinkNext=0]
  :LEDを明るく]
endif
endwhile
end
```

Lチカ自体は、フローチャートで表し、再利用できるように関数化しておくと良いと思いますが、全体的な振る舞いに対しては、ステートマシンという考え方を取り入れ、状態遷移図や状態遷移表を使って設計すると、より複雑な機能でもソースコードがスパゲッティになることを防げます。

:::note warn
**ステートマシン**
ステートマシンについては、次のキーワードで他の文献や資料を参照してください。

- ステートマシン
- 状態遷移図
- 状態遷移表
- UML State Machine Diagram
- 有限オートマトン
:::

状態遷移図や状態遷移表の設計を元に、状態変数や分岐処理を駆使したコーディングも可能ですが、一般的にはステートマシンと呼ばれるようなライブラリーを使用します。今回は、micro:bit向けのブロック型ステートマシンとして、[**Mstate拡張機能**](https://github.com/jp-rad/pxt-mstate)を使います。

:::note info
**Mstate拡張機能**
`Mstate拡張機能`はユーザー定義の拡張機能です。拡張機能の追加画面で、URLを入力して、追加できます。

`+拡張機能`
→ `拡張機能` (検索または、プロジェクトのURLを入力・・・)
→ `https://github.com/jp-rad/pxt-mstate`
:::

https://github.com/jp-rad/pxt-mstate

# Lチカの基本

LEDをチカチカと点滅するために、まずは、「Lチカのフローチャート」に従って、コーディングしました。
`blinkNext`の値に従って、LED画面の明るさを交互に切り替えています(関数`blinkLED`)。

**ブロック**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/0841a2de-fbcc-c707-a16b-721929974c87.png)

**JavaScript**
```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チカ

１つの状態のみで構成されるステートマシンをMstate拡張機能を使って次のようにコーディングできます。
尚、Mstate拡張機能のDoアクティビティでの振る舞いに注意してください。その状態へ状態遷移すると、Entryアクションを実行後、次の状態遷移が行われるまで、Doアクティビティを指定間隔毎に繰り返し実行します。

**ブロック**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/40fc977d-6fd1-74ee-82df-db192c0599f9.png)

**JavaScript**
```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")
```

**状態遷移図**

状態遷移図を使って次のように設計できます。

```plantuml

[*]-r->State1

State1 : entry/ LED表示
State1 : do/ LED点滅 (500ms)

```

`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`("*")へ遷移するようにコーディングしてみます。

**状態遷移図**
```plantuml

[*]-r->State1
State1-r->[*] : Trigger1
State1 : entry/ LED表示(大)
State1 : do/ LED点滅 (500ms)
State1 : exit/ LED表示(小)

```

**ブロック**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/a0a6759a-aee4-5186-eb48-eb8168b8ce22.png)

**JavaScript**
```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")で交互に状態遷移するようにします。

それを次の状態遷移図のように設計しました。

**状態遷移図**
```plantuml

[*]-->Slow
Slow-r->Fast: A
Fast-l->Slow: A

Slow : entry/ LED表示
Slow : do/ LED点滅 (500ms)

Fast : entry/ LED表示
Fast : do/ LED点滅 (200ms)

```

この設計でも期待する動作をしますが、状態遷移する度に`LED表示`が繰り返し呼び出されるのが気になります。
そこで、`LED表示`だけを行う状態（"On"）を追加し、次のように再設計しました。

**(再設計) 状態遷移図**
```plantuml

[*]-r->On
On-->Slow
Slow-r->Fast: A
Fast-l->Slow: A

On : entry/ LED表示
Slow : do/ LED点滅 (500ms)
Fast : do/ LED点滅 (200ms)

```

**ブロック**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/1352ce51-3cf6-1afb-aa31-b4041abd55eb.png)

**JavaScript**
```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チカを考えてみました。

1. micro:bitの電源を入れると、LEDは消灯状態となる（何も表示しない）
1. ボタンAを押すと、LEDが点灯する
1. ボタンAを押すたびに、点灯、遅い点滅、速い点滅の順にLED表示が切り替わる
1. ボタンBを押すと、LEDは消灯状態となる（何も表示しない）
1. ボタンA+Bを押すと、自動モードとなり、一定間隔でボタンAが押されているような動作をする
ただし、消灯状態では、自動モードにならない
1. ボタンBを押すと自動モードは解除され、通常モードに戻る（同時にLEDは消灯状態となる）

## 状態とトリガー

ステートマシンを使ったプログラミングを行うため、まずは、状態とトリガーを洗い出します。
ただし、この例では、LEDの表示状態とモード状態とが同時に現れるので、ステートマシンを２つに分けて考えます。

**ステートマシン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`による状態遷移から考えてみました。

**状態遷移図**
```plantuml

state Off
state On
state Slow
state Fast

[*] -r-> Off

Off --> On : A
On --> Slow : A
Slow -r-> Fast : A
Fast -r-> On : A

'On --> Off : B
'Slow -l-> Off : B
'Fast -r-> Off : B

state Off : entry/ \n - LED消灯
state On : entry/ \n - 点滅状態初期化 \n - LED点灯
state Slow : do/ \n - LED点滅（500ms）
state Fast : do/ \n - LED点滅（200ms）
state Fast : exit/ \n - 明るさ最大化

```

1. Offへ(●-->Off)
電源オンで、初期状態（●印）から状態`Off`へ遷移します。この状態`Off`に遷移した時(entry)、LEDを消灯にしています。
1. Onへ(Off-->On)
ボタンAが押された際に、トリガー`A`を発生させることにより、状態が`Off`から`On`へと遷移します。`On`に遷移した時(entry)、「点滅状態初期化」で、点滅の明るさを暗くした状態から始めるように、ここで初期化します。その後、LEDを点灯にしています。
1. Slowへ(On-->Slow)
さらにボタンAが押された際に、トリガー`A`を発生させることにより、状態が`On`から`Slow`へと遷移します。LEDを点滅させるために、その明るさを暗くしたり明るくしたりします。状態`On`で「点滅状態初期化」がされているため、暗い状態から始まります。
実際には、`do`で500ms毎に明るさを切り替える処理（`LED点滅`の呼び出し）を繰り返します（トリガー`A`が発生するまで）。
1. Fastへ(Slow-->Fast)
またさらに、トリガー`A`を発生させることにより、状態が`Slow`から`Fast`へと遷移しますが、`Fast`では、`entry`がありません。これは、明るさの状態（変数の値）を`Slow`から引き継ぐ為です。そして、`do`では、200ms毎に明るさを切り替える処理を繰り返します。
1. 再びOnへ(Fast-->On)
繰り返し、トリガー`A`を発生させることにより、状態が`Fast`から`On`へと再び遷移しますが、遷移する前に、`Fast`の`exit`が実行されます。タイミングによっては、LEDが暗くなっている場合があるために、最大の明るさに戻しています。

尚、状態遷移図を状態遷移表に書き換えることもできます。

**状態遷移表**
|状態|アクション|(電源オン)|A|
|----|----|----|----|
|●||Offへ|-|
|Off|entry/<br> - LED消灯|-|Onへ|
|On|entry/<br> - 点滅状態初期化<br> - LED点灯|-|Slowへ|
|Slow|do/<br> - LED点滅（500ms）|-|Fastへ|
|Fast|do/<br> - LED点滅（200ms）<br>exit/<br> - 明るさ最大化|-|Onへ|

状態遷移表のままに、コーディング（宣言）します（ブロックは省略）。

**JavaScript**
```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`へと状態遷移するようにします。

**状態遷移図**
```plantuml


state Off
state On
state Slow
state Fast

[*] -r-> Off

Off --> On : A
On --> Slow : A
Slow -r-> Fast : A
Fast -r-> On : A

On --> Off : B
Slow --> Off : B
Fast --> Off : B

state Off : entry/ \n - LED消灯
state On : entry/ \n - 点滅状態初期化 \n - LED点灯
state Slow : do/ \n - LED点滅（500ms）
state Fast : do/ \n - LED点滅（200ms）
state Fast : exit/ \n - 明るさ最大化

```

**状態遷移表**
|状態|アクション|(電源オン)|A|B|
|----|----|----|----|----|
|●||Offへ|-|-|
|Off|entry/<br> - LED消灯|-|Onへ|-|
|On|entry/<br> - 点滅状態初期化<br> - LED点灯|-|Slowへ|Offへ|
|Slow|do/<br> - LED点滅（500ms）|-|Fastへ|Offへ|
|Fast|do/<br> - LED点滅（200ms）<br>exit/<br> - 明るさ最大化|-|Onへ|Offへ|

**JavaScript**
```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アクションへ移動することにしました。

**状態遷移図**
```plantuml

state Off
state On
state Slow
state Fast

[*] -r-> Off

Off --> On : A
On --> Slow : A
Slow -r-> Fast : A
Fast --> On : A

On --> Off : B
Slow --> Off : B
Fast --> Off : B

state Off : entry/ \n - LED消灯
state On : entry/ \n - 点滅状態初期化 \n - 明るさ最大化 \n - LED点灯
state Slow : do/ \n - LED点滅（500ms）
state Fast : do/ \n - LED点滅（200ms）

```

**状態遷移表**
|状態|アクション|(電源オン)|A|B|
|----|----|----|----|----|
|●||Offへ|-|-|
|Off|entry/<br> - LED消灯|-|Onへ|-|
|On|entry/<br> - 点滅状態初期化<br> - 明るさ最大化<br> - LED点灯<br>|-|Slowへ|Offへ|
|Slow|do/<br> - LED点滅（500ms）|-|Fastへ|Offへ|
|Fast|do/<br> - LED点滅（200ms）|-|Onへ|Offへ|

**JavaScript**
```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拡張機能では、ステートマシンを１つしか宣言できない為、通常モード・自動モードのステートマシンを追加できません。

:::note info
**M**state拡張機能の**M**
micro:bitの**M**のようにも思えますが、"MODOKI"の**M**です。Mstate拡張機能では、完全なステートマシンが実装されているわけではありません。
:::

そこで、`mode`変数で状態を管理し、自動モードの機能を実現しました。

JavaScriptのソースコードだけを示しておきます。

**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チカ-ステートマシン）**

https://makecode.microbit.org/_3fRC4xAXeYox