1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-07-29

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)。

ブロック
image.png

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アクティビティを指定間隔毎に繰り返し実行します。

ブロック
image.png

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", ...)が、図中State1entry/ LED表示に対応し、
mstate.declareDo("State1", 500, ...)が、図中State1do/ LED点滅 (500ms)(500ms毎の繰り返し)に対応しています。
また、mstate.start("State1")が、図中State1(デフォルトステート)からの開始を意味します。

Mstateブロックの書き方

最初だけブロック内にすべてのブロックを配置しましたが、状態名称定義ブロック(defineStateName())内に配置することをお勧めします。状態別にEntryアクション宣言ブロックDoアクティビティ宣言ブロックExitアクション宣言ブロックトランジション宣言ブロックをまとめておくことが可能です。また、STATE変数を参照すれば、状態名称の入力ミスを減らすこともできます。

例えば、次の状態遷移図を設計し、Aボタンでステートマシンを開始し、BボタンでFinal("*")へ遷移するようにコーディングしてみます。

状態遷移図

ブロック
image.png

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")を追加し、次のように再設計しました。

(再設計) 状態遷移図

ブロック
image.png

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は消灯状態となる(何も表示しない)
  2. ボタンAを押すと、LEDが点灯する
  3. ボタンAを押すたびに、点灯、遅い点滅、速い点滅の順にLED表示が切り替わる
  4. ボタンBを押すと、LEDは消灯状態となる(何も表示しない)
  5. ボタンA+Bを押すと、自動モードとなり、一定間隔でボタンAが押されているような動作をする
    ただし、消灯状態では、自動モードにならない
  6. ボタン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による状態遷移から考えてみました。

状態遷移図

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

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?