【micro:bit編】状態遷移表からの実装（その２）

# 「カウントダウン終了」トリガー

[前回の実装では、](https://qiita.com/jp-96/items/e2a3f60220441569785f) 「カウントダウン終了」をステートマシン内部で判断するように実装していました。
今回、外部のタイマー機能を使って、設計通りに「カウントダウン終了」トリガーで遷移するように実装しました。

|前回|今回|
|----|----|
|ステートマシン内部で<br>「カウントダウン終了」を判断|外部のタイマー機能から<br>「カウントダウン終了」トリガーを送信|

**実装例（ブロック）**
![microbit-画面コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/cab86c73-4c1d-cfc3-5964-f4c5d25a4a7b.png)

<details><summary>ソースコード(JavaScript)</summary>

```js:JavaScript
function カウントダウン時間表示 (ミリ秒: number) {
    表示値秒 = Math.ceil(ミリ秒 / 1000)
    if (9 < 表示値秒) {
        basic.showLeds(`
            # . # # #
            # . # . #
            # . # . #
            # . # . #
            # . # # #
            `)
    } else if (0 < 表示値秒) {
        basic.showNumber(表示値秒)
    }
}
mstate.defineState(StateMachines.M1, "時間設定", function () {
    mstate.descriptionUml("- カウントダウン時間（設定）の表示")
    mstate.declareEntry(function () {
        if (1 == リセット済み) {
            if (10000 < タイマー設定値ミリ秒) {
                タイマー設定値ミリ秒 = 10000
            }
            時間設定ミリ秒 = タイマー設定値ミリ秒
        } else {
            時間設定ミリ秒 = タイマー設定値ミリ秒 - idletimer.millis(IdleTimer.Timer1)
        }
        カウントダウン時間表示(時間設定ミリ秒)
    })
    mstate.descriptionUml("/カウントダウン時間設定")
    mstate.declareCustomTransition("時間設定ボタン", ["時間設定"], function () {
        ボタン番号 = mstate.getTriggerArgs(StateMachines.M1)[0]
        if (0 == ボタン番号) {
            タイマー設定値ミリ秒 = 10000
            リセット済み = 1
        } else {
            時間設定ミリ秒 += 1000
            if (10000 < 時間設定ミリ秒) {
                時間設定ミリ秒 = 時間設定ミリ秒 % 1000
                時間設定ミリ秒 += 1000
            }
            if (1 == リセット済み) {
                タイマー設定値ミリ秒 = 時間設定ミリ秒
            } else {
                タイマー設定値ミリ秒 = 時間設定ミリ秒 + idletimer.millis(IdleTimer.Timer1)
            }
        }
        mstate.transitTo(StateMachines.M1, 0)
    })
    mstate.descriptionUml("/カウントダウン開始")
    mstate.declareCustomTransition("スタートストップボタン", ["カウントダウン"], function () {
        if (1 == リセット済み) {
            リセット済み = 0
            idletimer.start(IdleTimer.Timer1, idletimer.IntervalTimeoutTimerArgs(
            1000,
            タイマー設定値ミリ秒
            ))
        } else {
            idletimer.change(IdleTimer.Timer1, タイマー設定値ミリ秒)
            idletimer.resume(IdleTimer.Timer1)
        }
        mstate.transitTo(StateMachines.M1, 0)
    })
})
mstate.defineState(StateMachines.M1, "アラーム音出力", function () {
    mstate.descriptionUml("- ハートアイコン")
    mstate.declareEntry(function () {
        basic.showIcon(IconNames.Heart)
    })
    mstate.descriptionUml("/アラーム音停止")
    mstate.declareCustomTransition("スタートストップボタン", ["時間設定"], function () {
        music.stopAllSounds()
        リセット済み = 1
        mstate.transitTo(StateMachines.M1, 0)
    })
})
input.onButtonPressed(Button.A, function () {
    mstate.sendTriggerArgs(StateMachines.M1, "時間設定ボタン", [1])
})
input.onButtonPressed(Button.AB, function () {
    mstate.sendTriggerArgs(StateMachines.M1, "時間設定ボタン", [0])
})
input.onButtonPressed(Button.B, function () {
    mstate.sendTrigger(StateMachines.M1, "スタートストップボタン")
})
idletimer.onTimeout(IdleTimer.Timer1, function () {
    mstate.sendTrigger(StateMachines.M1, "カウントダウン終了")
})
idletimer.onInterval(IdleTimer.Timer1, function () {
    カウントダウン時間表示(タイマー設定値ミリ秒 - idletimer.millis(IdleTimer.Timer1))
})
mstate.defineState(StateMachines.M1, "カウントダウン", function () {
    mstate.descriptionUml("/カウントダウン停止")
    mstate.declareCustomTransition("スタートストップボタン", ["時間設定"], function () {
        idletimer.stop(IdleTimer.Timer1)
        if (!(idletimer.isTimeouted(IdleTimer.Timer1))) {
            mstate.transitTo(StateMachines.M1, 0)
        }
    })
    mstate.descriptionUml("/カウントダウン停止＆アラーム音出力")
    mstate.declareCustomTransition("カウントダウン終了", ["アラーム音出力"], function () {
        music._playDefaultBackground(music.builtInPlayableMelody(Melodies.PowerUp), music.PlaybackMode.LoopingInBackground)
        mstate.transitTo(StateMachines.M1, 0)
    })
})
let ボタン番号 = 0
let 時間設定ミリ秒 = 0
let 表示値秒 = 0
let リセット済み = 0
let タイマー設定値ミリ秒 = 0
タイマー設定値ミリ秒 = 10000
リセット済み = 1
mstate.exportUml(StateMachines.M1, "時間設定")
mstate.start(StateMachines.M1, "時間設定")

```

</details>

https://makecode.microbit.org/_9MWgDy1M1ixa

:::note info
IDLEタイマー

タイマー機能として、ユーザー定義の拡張機能であるIDLEタイマーを利用しています。

[IDLEタイマー - https://github.com/jp-rad/pxt-idle-timer](https://github.com/jp-rad/pxt-idle-timer)

:::


# 前回と今回との違い

外部のタイマー機能を使用したことにより、 [前回](https://qiita.com/jp-96/items/e2a3f60220441569785f) と 今回 とでは、次のような違いがあります。

* カウントダウンやカウントダウン表示をタイマー機能やそのインターバルイベントで行いました。
→ 「カウントダウン」状態でのソースコード量が減少しました。
* 「カウントダウン」状態でカウントダウンの終了を判断し、状態遷移するのではなく、タイマー機能のタイムアウトイベントで引き起こされる「カウントダウン終了」トリガーで状態遷移するようにしました。
* タイマー機能で経過時間を計測する為、「スタートストップボタン」トリガーの引数を削除しました。
* カウントダウンが不要なため、「最初だけ」の `settings` を削除しました(20msの指定)。

尚、今回、時間設定された設定値を保持し、タイムアウト後、最後の設定値で、再度、タイマーを開始できるようにしたり、途中で、時間を長くしたり短くしたりしても、ミリ秒単位で残り時間をカウントダウンできるようにしました。
その為、「時間設定」状態の時間設定のソースコードが複雑化しました。

<table><tr><th>前回</th><th>今回（その２）</th></tr><tr>
<td>

```plantuml
@startuml
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
state __M1__ {
[*] --> 時間設定
state 時間設定 : - カウントダウン時間（設定）の表示
時間設定 --> 時間設定 : 時間設定ボタン / カウントダウン時間設定
時間設定 --> カウントダウン : スタートストップボタン / カウントダウン開始
state アラーム音出力 : - ハートアイコン
アラーム音出力 --> 時間設定 : スタートストップボタン / アラーム音停止
state カウントダウン : - カウントダウン\n- カウントダウン時間の表示
カウントダウン --> 時間設定 : スタートストップボタン / カウントダウン停止
カウントダウン --> アラーム音出力 : [カウントダウン終了] / カウントダウン停止＆アラーム音出力
}
'
' generator: https://github.com/jp-rad/pxt-mstate
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
@enduml

```
</td><td>

```plantuml
@startuml
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
state __M1__ {
[*] --> 時間設定
state 時間設定 : - カウントダウン時間（設定）の表示
時間設定 --> 時間設定 : 時間設定ボタン / カウントダウン時間設定
時間設定 --> カウントダウン : スタートストップボタン / カウントダウン開始
state アラーム音出力 : - ハートアイコン
アラーム音出力 --> 時間設定 : スタートストップボタン / アラーム音停止
state カウントダウン
カウントダウン --> 時間設定 : スタートストップボタン / カウントダウン停止
カウントダウン --> アラーム音出力 : カウントダウン終了 / カウントダウン停止＆アラーム音出力
}
'
' generator: https://github.com/jp-rad/pxt-mstate
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
@enduml

```

</td></tr></table>

:::note info
**【拡張機能の追加手順】**
本記事では、次の２つの拡張機能を利用しています。
- **mstate (ver.0.8.3)**
https://github.com/jp-rad/pxt-mstate/releases/tag/0.8.3
- **idle-timer (ver.0.1.2)**
https://github.com/jp-rad/pxt-idle-timer/releases/tag/0.1.2

**【手順】**
1. それぞれのリンク先で、**hex形式ファイル** を選択して、ダウンロードする
1. MakeCodeで**1つずつ**拡張機能を追加する（それぞれの**hex形式ファイル**を選択する）
`⊕拡張機能` → `ファイルを読み込む` → `ファイルを選択` → `つづける`
⊕
:::


# 複雑さの改善1 - 変数の初期化

変数の初期化が、「最初に」と「時間設定」状態の２か所で実装されていますので、これを１か所で実装するように「初期化」状態と「再設定」状態とを追加します。

既定の設定時間を10秒にするために、少しややこしい工夫をしていますが、変数の初期化を１か所で行うようにしました。
これにより、「時間設定」状態では、時間設定とカウントダウン開始の実装に専念できます。

**状態遷移図**

```plantuml
@startuml
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
state __M1__ {
[*] --> 初期化
state 時間設定 : - カウントダウン時間（設定）の表示
時間設定 --> 初期化 : 時間設定ボタン [A+Bボタン]
時間設定 --> 時間設定 : 時間設定ボタン [Aボタン] / カウントダウン時間設定
時間設定 --> カウントダウン : スタートストップボタン / カウントダウン開始
state アラーム音出力 : - ハートアイコン
アラーム音出力 --> 再設定 : スタートストップボタン / アラーム音停止
state 初期化 : 設定時間=-1(不正な値)
初期化 --> 再設定
state 再設定 : タイマーの準備
再設定 --> 時間設定 : [不正な設定時間] / 設定時間=10s
再設定 --> 時間設定 : [それ以外]
state カウントダウン
カウントダウン --> 時間設定 : スタートストップボタン / カウントダウン停止
カウントダウン --> アラーム音出力 : カウントダウン終了 / カウントダウン停止＆アラーム音出力
}
'
' generator: https://github.com/jp-rad/pxt-mstate
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
@enduml

```

**実装例（ブロック）**
![microbit-画面コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/9aa9bf47-2132-9fb4-587d-2da692646d75.png)


<details><summary>ソースコード(JavaScript)</summary>

```js: JavaScript
function カウントダウン時間表示 (ミリ秒: number) {
    表示値秒 = Math.ceil(ミリ秒 / 1000)
    if (9 < 表示値秒) {
        basic.showLeds(`
            # . # # #
            # . # . #
            # . # . #
            # . # . #
            # . # # #
            `)
    } else if (0 < 表示値秒) {
        basic.showNumber(表示値秒)
    }
}
mstate.defineState(StateMachines.M1, "時間設定", function () {
    mstate.descriptionUml("- カウントダウン時間（設定）の表示")
    mstate.declareEntry(function () {
        if (1 == リセット済み) {
            時間設定ミリ秒 = タイマー設定値ミリ秒
        } else {
            時間設定ミリ秒 = タイマー設定値ミリ秒 - idletimer.millis(IdleTimer.Timer1)
        }
        カウントダウン時間表示(時間設定ミリ秒)
    })
    mstate.descriptionsUml(["A+Bボタン", "Aボタン/カウントダウン時間設定"])
    mstate.declareCustomTransition("時間設定ボタン", ["初期化", "時間設定"], function () {
        ボタン番号 = mstate.getTriggerArgs(StateMachines.M1)[0]
        if (0 == ボタン番号) {
            mstate.transitTo(StateMachines.M1, 0)
        } else {
            時間設定ミリ秒 += 1000
            if (10000 < 時間設定ミリ秒) {
                時間設定ミリ秒 = 時間設定ミリ秒 % 1000
                時間設定ミリ秒 += 1000
            }
            if (1 == リセット済み) {
                タイマー設定値ミリ秒 = 時間設定ミリ秒
            } else {
                タイマー設定値ミリ秒 = 時間設定ミリ秒 + idletimer.millis(IdleTimer.Timer1)
            }
            mstate.transitTo(StateMachines.M1, 1)
        }
    })
    mstate.descriptionUml("/カウントダウン開始")
    mstate.declareCustomTransition("スタートストップボタン", ["カウントダウン"], function () {
        if (1 == リセット済み) {
            リセット済み = 0
            idletimer.start(IdleTimer.Timer1, idletimer.IntervalTimeoutTimerArgs(
            1000,
            タイマー設定値ミリ秒
            ))
        } else {
            idletimer.change(IdleTimer.Timer1, タイマー設定値ミリ秒)
            idletimer.resume(IdleTimer.Timer1)
        }
        mstate.transitTo(StateMachines.M1, 0)
    })
})
mstate.defineState(StateMachines.M1, "アラーム音出力", function () {
    mstate.descriptionUml("- ハートアイコン")
    mstate.declareEntry(function () {
        basic.showIcon(IconNames.Heart)
    })
    mstate.descriptionUml("/アラーム音停止")
    mstate.declareCustomTransition("スタートストップボタン", ["再設定"], function () {
        music.stopAllSounds()
        mstate.transitTo(StateMachines.M1, 0)
    })
})
input.onButtonPressed(Button.A, function () {
    mstate.sendTriggerArgs(StateMachines.M1, "時間設定ボタン", [1])
})
mstate.defineState(StateMachines.M1, "初期化", function () {
    mstate.descriptionUml("設定時間=-1(不正な値)")
    mstate.declareEntry(function () {
        タイマー設定値ミリ秒 = -1
    })
    mstate.declareSimpleTransition("", "再設定")
})
mstate.defineState(StateMachines.M1, "再設定", function () {
    mstate.descriptionUml("タイマーの準備")
    mstate.declareEntry(function () {
        リセット済み = 1
    })
    mstate.descriptionsUml(["不正な設定時間/設定時間=10s", "それ以外"])
    mstate.declareCustomTransition("", ["時間設定", "時間設定"], function () {
        if (0 >= タイマー設定値ミリ秒 || 10000 < タイマー設定値ミリ秒) {
            タイマー設定値ミリ秒 = 10000
            mstate.transitTo(StateMachines.M1, 0)
        } else {
            mstate.transitTo(StateMachines.M1, 1)
        }
    })
})
input.onButtonPressed(Button.AB, function () {
    mstate.sendTriggerArgs(StateMachines.M1, "時間設定ボタン", [0])
})
input.onButtonPressed(Button.B, function () {
    mstate.sendTrigger(StateMachines.M1, "スタートストップボタン")
})
idletimer.onTimeout(IdleTimer.Timer1, function () {
    mstate.sendTrigger(StateMachines.M1, "カウントダウン終了")
})
idletimer.onInterval(IdleTimer.Timer1, function () {
    カウントダウン時間表示(タイマー設定値ミリ秒 - idletimer.millis(IdleTimer.Timer1))
})
mstate.defineState(StateMachines.M1, "カウントダウン", function () {
    mstate.descriptionUml("/カウントダウン停止")
    mstate.declareCustomTransition("スタートストップボタン", ["時間設定"], function () {
        idletimer.stop(IdleTimer.Timer1)
        if (!(idletimer.isTimeouted(IdleTimer.Timer1))) {
            mstate.transitTo(StateMachines.M1, 0)
        }
    })
    mstate.descriptionUml("/カウントダウン停止＆アラーム音出力")
    mstate.declareCustomTransition("カウントダウン終了", ["アラーム音出力"], function () {
        music._playDefaultBackground(music.builtInPlayableMelody(Melodies.PowerUp), music.PlaybackMode.LoopingInBackground)
        mstate.transitTo(StateMachines.M1, 0)
    })
})
let ボタン番号 = 0
let タイマー設定値ミリ秒 = 0
let 時間設定ミリ秒 = 0
let リセット済み = 0
let 表示値秒 = 0
mstate.exportUml(StateMachines.M1, "初期化")
mstate.start(StateMachines.M1, "初期化")

```

</details>

https://makecode.microbit.org/_X1jWCVH0D9b2

# 複雑さの改善2 - 時間設定（完全停止・一時停止）

「時間設定」状態では、 `リセット済み`変数の値によって、所々で条件分岐しています。この変数もまた`状態`ですので、「時間設定」状態を「時間設定」状態と「一時停止」状態とに分けて、状態遷移するようにしました（`リセット済み`変数の廃止）。

これにより、実装に埋もれていた **「`リセット済み` 変数の役割」** が、状態遷移図でも明確になりました。

```plantuml
@startuml
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
state __M1__ {
[*] --> 初期化
state 時間設定 : - カウントダウン時間（設定）の表示
時間設定 --> 初期化 : 時間設定ボタン [A+Bボタン]
時間設定 --> 時間設定 : 時間設定ボタン [Aボタン] / カウントダウン時間設定
時間設定 --> カウントダウン : スタートストップボタン / カウントダウン開始
state アラーム音出力 : - ハートアイコン
アラーム音出力 --> 再設定 : スタートストップボタン / アラーム音停止
state 初期化 : 設定時間=-1(不正な値)
初期化 --> 再設定
state 再設定
再設定 --> 時間設定 : [不正な設定時間] / 設定時間=10s
再設定 --> 時間設定 : [それ以外]
state カウントダウン
カウントダウン --> 一時停止 : スタートストップボタン / カウントダウン停止
カウントダウン --> アラーム音出力 : カウントダウン終了 / カウントダウン停止＆アラーム音出力
state 一時停止 : - カウントダウン時間（設定）の表示
一時停止 --> 初期化 : 時間設定ボタン [A+Bボタン]
一時停止 --> 一時停止 : 時間設定ボタン [Aボタン] / カウントダウン時間設定
一時停止 --> カウントダウン : スタートストップボタン / カウントダウン開始
}
'
' generator: https://github.com/jp-rad/pxt-mstate
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
@enduml
```

**実装例（ブロック）**
![microbit-画面コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/c6bea9dc-237f-88f3-eb22-3ff8fc7603d4.png)

<details><summary>ソースコード(JavaScript)</summary>

```js: JavaScript
function カウントダウン時間表示 (ミリ秒: number) {
    表示値秒 = Math.ceil(ミリ秒 / 1000)
    if (9 < 表示値秒) {
        basic.showLeds(`
            # . # # #
            # . # . #
            # . # . #
            # . # . #
            # . # # #
            `)
    } else if (0 < 表示値秒) {
        basic.showNumber(表示値秒)
    }
}
mstate.defineState(StateMachines.M1, "時間設定", function () {
    mstate.descriptionUml("- カウントダウン時間（設定）の表示")
    mstate.declareEntry(function () {
        時間設定ミリ秒 = タイマー設定値ミリ秒
        カウントダウン時間表示(時間設定ミリ秒)
    })
    mstate.descriptionsUml(["A+Bボタン", "Aボタン/カウントダウン時間設定"])
    mstate.declareCustomTransition("時間設定ボタン", ["初期化", "時間設定"], function () {
        ボタン番号 = mstate.getTriggerArgs(StateMachines.M1)[0]
        if (0 == ボタン番号) {
            mstate.transitTo(StateMachines.M1, 0)
        } else {
            時間設定ミリ秒 += 1000
            if (10000 < 時間設定ミリ秒) {
                時間設定ミリ秒 = 時間設定ミリ秒 % 1000
                時間設定ミリ秒 += 1000
            }
            タイマー設定値ミリ秒 = 時間設定ミリ秒
            mstate.transitTo(StateMachines.M1, 1)
        }
    })
    mstate.descriptionUml("/カウントダウン開始")
    mstate.declareCustomTransition("スタートストップボタン", ["カウントダウン"], function () {
        idletimer.start(IdleTimer.Timer1, idletimer.IntervalTimeoutTimerArgs(
        1000,
        タイマー設定値ミリ秒
        ))
        mstate.transitTo(StateMachines.M1, 0)
    })
})
mstate.defineState(StateMachines.M1, "アラーム音出力", function () {
    mstate.descriptionUml("- ハートアイコン")
    mstate.declareEntry(function () {
        basic.showIcon(IconNames.Heart)
    })
    mstate.descriptionUml("/アラーム音停止")
    mstate.declareCustomTransition("スタートストップボタン", ["再設定"], function () {
        music.stopAllSounds()
        mstate.transitTo(StateMachines.M1, 0)
    })
})
input.onButtonPressed(Button.A, function () {
    mstate.sendTriggerArgs(StateMachines.M1, "時間設定ボタン", [1])
})
mstate.defineState(StateMachines.M1, "初期化", function () {
    mstate.descriptionUml("設定時間=-1(不正な値)")
    mstate.declareEntry(function () {
        タイマー設定値ミリ秒 = -1
    })
    mstate.declareSimpleTransition("", "再設定")
})
mstate.defineState(StateMachines.M1, "再設定", function () {
    mstate.descriptionUml("タイマーの準備")
    mstate.descriptionsUml(["不正な設定時間/設定時間=10s", "それ以外"])
    mstate.declareCustomTransition("", ["時間設定", "時間設定"], function () {
        if (0 >= タイマー設定値ミリ秒 || 10000 < タイマー設定値ミリ秒) {
            タイマー設定値ミリ秒 = 10000
            mstate.transitTo(StateMachines.M1, 0)
        } else {
            mstate.transitTo(StateMachines.M1, 1)
        }
    })
})
input.onButtonPressed(Button.AB, function () {
    mstate.sendTriggerArgs(StateMachines.M1, "時間設定ボタン", [0])
})
input.onButtonPressed(Button.B, function () {
    mstate.sendTrigger(StateMachines.M1, "スタートストップボタン")
})
idletimer.onTimeout(IdleTimer.Timer1, function () {
    mstate.sendTrigger(StateMachines.M1, "カウントダウン終了")
})
idletimer.onInterval(IdleTimer.Timer1, function () {
    カウントダウン時間表示(タイマー設定値ミリ秒 - idletimer.millis(IdleTimer.Timer1))
})
mstate.defineState(StateMachines.M1, "カウントダウン", function () {
    mstate.descriptionUml("/カウントダウン停止")
    mstate.declareCustomTransition("スタートストップボタン", ["一時停止"], function () {
        idletimer.stop(IdleTimer.Timer1)
        if (!(idletimer.isTimeouted(IdleTimer.Timer1))) {
            mstate.transitTo(StateMachines.M1, 0)
        }
    })
    mstate.descriptionUml("/カウントダウン停止＆アラーム音出力")
    mstate.declareCustomTransition("カウントダウン終了", ["アラーム音出力"], function () {
        music._playDefaultBackground(music.builtInPlayableMelody(Melodies.PowerUp), music.PlaybackMode.LoopingInBackground)
        mstate.transitTo(StateMachines.M1, 0)
    })
})
mstate.defineState(StateMachines.M1, "一時停止", function () {
    mstate.descriptionUml("- カウントダウン時間（設定）の表示")
    mstate.declareEntry(function () {
        時間設定ミリ秒 = タイマー設定値ミリ秒 - idletimer.millis(IdleTimer.Timer1)
        カウントダウン時間表示(時間設定ミリ秒)
    })
    mstate.descriptionsUml(["A+Bボタン", "Aボタン/カウントダウン時間設定"])
    mstate.declareCustomTransition("時間設定ボタン", ["初期化", "一時停止"], function () {
        ボタン番号 = mstate.getTriggerArgs(StateMachines.M1)[0]
        if (0 == ボタン番号) {
            mstate.transitTo(StateMachines.M1, 0)
        } else {
            時間設定ミリ秒 += 1000
            if (10000 < 時間設定ミリ秒) {
                時間設定ミリ秒 = 時間設定ミリ秒 % 1000
                時間設定ミリ秒 += 1000
            }
            タイマー設定値ミリ秒 = 時間設定ミリ秒 + idletimer.millis(IdleTimer.Timer1)
            mstate.transitTo(StateMachines.M1, 1)
        }
    })
    mstate.descriptionUml("/カウントダウン開始")
    mstate.declareCustomTransition("スタートストップボタン", ["カウントダウン"], function () {
        idletimer.change(IdleTimer.Timer1, タイマー設定値ミリ秒)
        idletimer.resume(IdleTimer.Timer1)
        mstate.transitTo(StateMachines.M1, 0)
    })
})
let ボタン番号 = 0
let タイマー設定値ミリ秒 = 0
let 時間設定ミリ秒 = 0
let 表示値秒 = 0
mstate.exportUml(StateMachines.M1, "初期化")
mstate.start(StateMachines.M1, "初期化")

```

</details>

https://makecode.microbit.org/_KKEKtvRgaLYo

# 全体の見直し

状態遷移図とソースコード（ブロック）を全体的に見直します。
その結果、次の２つを見直しました。

1. 「時間設定」状態への遷移元は、「再設定」状態のみの為、「時間設定」状態に統合し、「再設定」状態を廃止しました。
1. 「時間設定」状態と「一時停止」状態とで、部分的に同一のコードがあるので、関数化しました。

```plantuml

@startuml
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
state __M1__ {
[*] --> 初期化
state 時間設定 : - 設定時間の検証\n- カウントダウン時間（設定）の表示
時間設定 --> 初期化 : 時間設定ボタン [A+Bボタン]
時間設定 --> 時間設定 : 時間設定ボタン [Aボタン] / カウントダウン時間設定
時間設定 --> カウントダウン : スタートストップボタン / カウントダウン開始
state アラーム音出力 : - ハートアイコン
アラーム音出力 --> 時間設定 : スタートストップボタン / アラーム音停止
state 初期化 : 設定時間=-1(不正な値)
初期化 --> 時間設定
state カウントダウン
カウントダウン --> 一時停止 : スタートストップボタン / カウントダウン停止
カウントダウン --> アラーム音出力 : カウントダウン終了 / カウントダウン停止＆アラーム音出力
state 一時停止 : - カウントダウン時間（設定）の表示
一時停止 --> 初期化 : 時間設定ボタン [A+Bボタン]
一時停止 --> 一時停止 : 時間設定ボタン [Aボタン] / カウントダウン時間設定
一時停止 --> カウントダウン : スタートストップボタン / カウントダウン開始
}
'
' generator: https://github.com/jp-rad/pxt-mstate
'
' PlantUML Web server:
' https://www.plantuml.com/plantuml/
'
@enduml

```

**実装例（ブロック）**
![microbit-画面コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/9b3ada6a-db7e-babf-9925-e31d764ecf39.png)

<details><summary>ソースコード(JavaScript)</summary>

```js: JavaScript
function カウントダウン時間表示 (ミリ秒: number) {
    表示値秒 = Math.ceil(ミリ秒 / 1000)
    if (9 < 表示値秒) {
        basic.showLeds(`
            # . # # #
            # . # . #
            # . # . #
            # . # . #
            # . # # #
            `)
    } else if (0 < 表示値秒) {
        basic.showNumber(表示値秒)
    }
}
mstate.defineState(StateMachines.M1, "時間設定", function () {
    mstate.descriptionsUml(["- 設定時間の検証", "- カウントダウン時間（設定）の表示"])
    mstate.declareEntry(function () {
        if (0 >= タイマー設定値ミリ秒 || 10000 < タイマー設定値ミリ秒) {
            タイマー設定値ミリ秒 = 10000
        }
        時間設定ミリ秒 = タイマー設定値ミリ秒
        カウントダウン時間表示(時間設定ミリ秒)
    })
    mstate.descriptionsUml(["A+Bボタン", "Aボタン/カウントダウン時間設定"])
    mstate.declareCustomTransition("時間設定ボタン", ["初期化", "時間設定"], function () {
        ボタン番号 = mstate.getTriggerArgs(StateMachines.M1)[0]
        if (0 == ボタン番号) {
            mstate.transitTo(StateMachines.M1, 0)
        } else {
            時間設定の加算()
            タイマー設定値ミリ秒 = 時間設定ミリ秒
            mstate.transitTo(StateMachines.M1, 1)
        }
    })
    mstate.descriptionUml("/カウントダウン開始")
    mstate.declareCustomTransition("スタートストップボタン", ["カウントダウン"], function () {
        idletimer.start(IdleTimer.Timer1, idletimer.IntervalTimeoutTimerArgs(
        1000,
        タイマー設定値ミリ秒
        ))
        mstate.transitTo(StateMachines.M1, 0)
    })
})
function 時間設定の加算 () {
    時間設定ミリ秒 += 1000
    if (10000 < 時間設定ミリ秒) {
        時間設定ミリ秒 = 時間設定ミリ秒 % 1000
        時間設定ミリ秒 += 1000
    }
}
mstate.defineState(StateMachines.M1, "アラーム音出力", function () {
    mstate.descriptionUml("- ハートアイコン")
    mstate.declareEntry(function () {
        basic.showIcon(IconNames.Heart)
    })
    mstate.descriptionUml("/アラーム音停止")
    mstate.declareCustomTransition("スタートストップボタン", ["時間設定"], function () {
        music.stopAllSounds()
        mstate.transitTo(StateMachines.M1, 0)
    })
})
input.onButtonPressed(Button.A, function () {
    mstate.sendTriggerArgs(StateMachines.M1, "時間設定ボタン", [1])
})
mstate.defineState(StateMachines.M1, "初期化", function () {
    mstate.descriptionUml("設定時間=-1(不正な値)")
    mstate.declareEntry(function () {
        タイマー設定値ミリ秒 = -1
    })
    mstate.declareSimpleTransition("", "時間設定")
})
input.onButtonPressed(Button.AB, function () {
    mstate.sendTriggerArgs(StateMachines.M1, "時間設定ボタン", [0])
})
input.onButtonPressed(Button.B, function () {
    mstate.sendTrigger(StateMachines.M1, "スタートストップボタン")
})
idletimer.onTimeout(IdleTimer.Timer1, function () {
    mstate.sendTrigger(StateMachines.M1, "カウントダウン終了")
})
idletimer.onInterval(IdleTimer.Timer1, function () {
    カウントダウン時間表示(タイマー設定値ミリ秒 - idletimer.millis(IdleTimer.Timer1))
})
mstate.defineState(StateMachines.M1, "カウントダウン", function () {
    mstate.descriptionUml("/カウントダウン停止")
    mstate.declareCustomTransition("スタートストップボタン", ["一時停止"], function () {
        idletimer.stop(IdleTimer.Timer1)
        if (!(idletimer.isTimeouted(IdleTimer.Timer1))) {
            mstate.transitTo(StateMachines.M1, 0)
        }
    })
    mstate.descriptionUml("/カウントダウン停止＆アラーム音出力")
    mstate.declareCustomTransition("カウントダウン終了", ["アラーム音出力"], function () {
        music._playDefaultBackground(music.builtInPlayableMelody(Melodies.PowerUp), music.PlaybackMode.LoopingInBackground)
        mstate.transitTo(StateMachines.M1, 0)
    })
})
mstate.defineState(StateMachines.M1, "一時停止", function () {
    mstate.descriptionUml("- カウントダウン時間（設定）の表示")
    mstate.declareEntry(function () {
        時間設定ミリ秒 = タイマー設定値ミリ秒 - idletimer.millis(IdleTimer.Timer1)
        カウントダウン時間表示(時間設定ミリ秒)
    })
    mstate.descriptionsUml(["A+Bボタン", "Aボタン/カウントダウン時間設定"])
    mstate.declareCustomTransition("時間設定ボタン", ["初期化", "一時停止"], function () {
        ボタン番号 = mstate.getTriggerArgs(StateMachines.M1)[0]
        if (0 == ボタン番号) {
            mstate.transitTo(StateMachines.M1, 0)
        } else {
            時間設定の加算()
            タイマー設定値ミリ秒 = 時間設定ミリ秒 + idletimer.millis(IdleTimer.Timer1)
            mstate.transitTo(StateMachines.M1, 1)
        }
    })
    mstate.descriptionUml("/カウントダウン開始")
    mstate.declareCustomTransition("スタートストップボタン", ["カウントダウン"], function () {
        idletimer.change(IdleTimer.Timer1, タイマー設定値ミリ秒)
        idletimer.resume(IdleTimer.Timer1)
        mstate.transitTo(StateMachines.M1, 0)
    })
})
let ボタン番号 = 0
let 時間設定ミリ秒 = 0
let タイマー設定値ミリ秒 = 0
let 表示値秒 = 0
mstate.exportUml(StateMachines.M1, "初期化")
mstate.start(StateMachines.M1, "初期化")

```

</details>

https://makecode.microbit.org/_dPMf0fcCY8v2


## 状態遷移表（設計変更）

これまでの設計変更を状態遷移表に反映しました。

- **「初期化」状態**を追加し、その状態へ遷移することで、タイマーのリセットを実現しました。
- 時間設定を１つの状態で行っていましたが、**タイマー開始前**と**一時停止中**とを状態で区別し、**「時間設定」状態と「一時停止」状態**のそれぞれで行うようにしました。

**状態遷移表**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/bcf16f91-bec0-468a-a954-4363489cd999.png)

<details><summary>（markdown）</summary>

<table>
<tr><th>キッチンタイマー</th><th>s</th>
    <th>初期化</th>
    <th>時間設定</th>
    <th>カウントダウン</th>
    <th>一時停止</th>
    <th>アラーム音出力</th>
</tr>
<tr><th>e</th>
<th></th>
    <th>0</th>
    <th>1</th>
    <th>2</th>
    <th>3</th>
    <th>4</th>
</tr>
<tr><th>(処理完了)</th>
<th>0</th>
    <td>(リセット)<br>-----<br>時間設定</td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
</tr>
<tr><th>時間設定ボタン<br>(Aボタン)</th>
<th>1.1</th>
    <td></td>
    <td>カウントダウン時間設定<br>-----<br>時間設定</td>
    <td></td>
    <td>カウントダウン時間設定<br>-----<br>時間設定</td>
    <td></td>
</tr>
<tr><th>時間設定ボタン<br>(A+Bボタン)</th>
<th>1.2</th>
    <td></td>
    <td>(なし)<br>-----<br>初期化</td>
    <td></td>
    <td>(なし)<br>-----<br>初期化</td>
    <td></td>
</tr>
<tr><th>スタートストップボタン</th>
<th>2</th>
    <td></td>
    <td>カウントダウン開始<br>-----<br>カウントダウン</td>
    <td>カウントダウン停止<br>-----<br>一時停止</td>
    <td>カウントダウン開始<br>-----<br>カウントダウン</td>
    <td>アラーム音停止<br>-----<br>時間設定</td>
</tr>
<tr><th>カウントダウン終了</th>
<th>3</th>
    <td></td>
    <td></td>
    <td>カウントダウン停止<br>アラーム音出力<br>-----<br>アラーム音出力</td>
    <td></td>
    <td></td>
</tr>
</table>

</details>

# まとめ

[前回の実装を元に、](https://qiita.com/jp-96/items/e2a3f60220441569785f) キッチンタイマーの見直しを行いました。

- 外部のタイマー機能を使って、設計通りに「カウントダウン終了」トリガーで遷移するように実装しました。
- 複雑さを改善する為、状態を追加したり、分けたりしました。
- 全体を見直し、状態遷移表に反映しました。

# リンク

https://qiita.com/jp-96/items/e2a3f60220441569785f