LoginSignup
0
0

How to code state machines more simply using mstate extension with micro:bit

Posted at

Simple State Machine

The following video illustrates an example of coding a state machine on the BBC micro:bit. This is a rare and valuable video explaining state machine coding on the micro:bit.

Source Code (JavaScript)
JavaScript
let pumpStopTime = 0
let pumpStartTime = 0
let PUMP_ON_TIME = 5000
let PUMP_OFF_TIME = 3000
let pumpState = 0
pins.digitalWritePin(DigitalPin.P1, 0)
basic.forever(function () {
    if (pumpState == 0) {
        if (input.buttonIsPressed(Button.A)) {
            pumpState = 1
            basic.showNumber(1)
            pumpStartTime = input.runningTime()
            pins.digitalWritePin(DigitalPin.P1, 1)
        }
    }
    if (pumpState == 1) {
        if (input.runningTime() > pumpStartTime + PUMP_ON_TIME) {
            basic.showNumber(2)
            pumpState = 2
            pins.digitalWritePin(DigitalPin.P1, 0)
            pumpStopTime = input.runningTime()
        }
    }
    if (pumpState == 2) {
        if (input.runningTime() > pumpStopTime + PUMP_OFF_TIME) {
            basic.showNumber(3)
            pumpState = 3
        }
    }
    if (pumpState == 3) {
        basic.showString("Cycle Complete")
        pumpState = 0
    }
    if (pumpState == 4) {
        basic.showString("Water Empty")
        if (!(input.buttonIsPressed(Button.B))) {
            pumpState = 0
            basic.showIcon(IconNames.Yes)
        }
    }
})
basic.forever(function () {
    if (input.buttonIsPressed(Button.B)) {
        if (pumpState != 4) {
            pumpState = 4
            basic.showNumber(4)
            pins.digitalWritePin(DigitalPin.P1, 0)
        }
    }
})

Using mstate Extension

Typically, the forever and if constructs are utilized in coding state machines. Libraries, such as the mstate extension, simplify the coding process and enhance comprehensibility.

This article uses pxt-mstate ver. 0.10.0.
https://github.com/jp-rad/pxt-mstate/releases/tag/0.10.0

Step1 - Normal Operation

In normal operation, pumpState transitions from 0,1,2,3,0.
The mstate extension allows you to define the state as follows:

Blocks

image.png

Source Code (JavaScript)
JavaScript
mstate.defineState(StateMachines.M0, "s3", function () {
    mstate.onState(0, function (tickcount) {
        basic.showString("Cycle Complete")
    })
    mstate.onTrigger("", ["s0"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
})
input.onButtonPressed(Button.A, function () {
    mstate.send(StateMachines.M0, "e")
})
mstate.defineState(StateMachines.M0, "s1", function () {
    mstate.onState(5000, function (tickcount) {
        timeoutedCount = tickcount
    })
    mstate.onTrigger("", ["s2"], function () {
        if (0 < timeoutedCount) {
            basic.showNumber(2)
            mstate.traverse(StateMachines.M0, 0)
            pins.digitalWritePin(DigitalPin.P1, 0)
        }
    })
})
mstate.defineState(StateMachines.M0, "s2", function () {
    mstate.onState(3000, function (tickcount) {
        timeoutedCount = tickcount
    })
    mstate.onTrigger("", ["s3"], function () {
        if (0 < timeoutedCount) {
            basic.showNumber(3)
            mstate.traverse(StateMachines.M0, 0)
        }
    })
})
mstate.defineState(StateMachines.M0, "s0", function () {
    mstate.onTrigger("e", ["s1"], function () {
        mstate.traverse(StateMachines.M0, 0)
        basic.showNumber(1)
        pins.digitalWritePin(DigitalPin.P1, 1)
    })
})
let timeoutedCount = 0
mstate.start(StateMachines.M0, "s0")

Step2 - Exception Handling

If the water is empty (pumpState is 4), the pump must be stopped.

image.png

If the water volume sensor detects empty when the state is not 4, the system should transition to state 4.

image.png

Source Code (JavaScript)
JavaScript
mstate.defineState(StateMachines.M0, "s3", function () {
    mstate.onState(0, function (tickcount) {
        basic.showString("Cycle Complete")
    })
    mstate.onTrigger("", ["s0"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
    mstate.onTrigger("empty", ["s4"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
})
input.onButtonPressed(Button.A, function () {
    mstate.send(StateMachines.M0, "e")
})
mstate.defineState(StateMachines.M0, "s1", function () {
    mstate.onState(5000, function (tickcount) {
        timeoutedCount = tickcount
    })
    mstate.onTrigger("", ["s2"], function () {
        if (0 < timeoutedCount) {
            basic.showNumber(2)
            mstate.traverse(StateMachines.M0, 0)
            pins.digitalWritePin(DigitalPin.P1, 0)
        }
    })
    mstate.onTrigger("empty", ["s4"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
})
mstate.defineState(StateMachines.M0, "s2", function () {
    mstate.onState(3000, function (tickcount) {
        timeoutedCount = tickcount
    })
    mstate.onTrigger("", ["s3"], function () {
        if (0 < timeoutedCount) {
            basic.showNumber(3)
            mstate.traverse(StateMachines.M0, 0)
        }
    })
    mstate.onTrigger("empty", ["s4"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
})
mstate.defineState(StateMachines.M0, "s0", function () {
    mstate.onTrigger("e", ["s1"], function () {
        mstate.traverse(StateMachines.M0, 0)
        basic.showNumber(1)
        pins.digitalWritePin(DigitalPin.P1, 1)
    })
    mstate.onTrigger("empty", ["s4"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
})
mstate.defineState(StateMachines.M0, "s4", function () {
    mstate.onState(100, function (tickcount) {
        if (0 == tickcount) {
            pins.digitalWritePin(DigitalPin.P1, 0)
        }
        basic.showString("Water Empty")
    })
    mstate.onTrigger("", ["s0"], function () {
        if (!(input.buttonIsPressed(Button.B))) {
            mstate.traverse(StateMachines.M0, 0)
            basic.showIcon(IconNames.Yes)
        }
    })
})
let waterLevel = 0
let timeoutedCount = 0
mstate.start(StateMachines.M0, "s0")
basic.forever(function () {
    if (input.buttonIsPressed(Button.B)) {
        if (0 != waterLevel) {
            waterLevel = 0
            mstate.send(StateMachines.M0, "empty")
        }
    } else {
        waterLevel = 1
    }
})

Refactoring

To refactor, output to a state diagram to see the overall structure.
The mstate extension has the ability to output state diagrams in PlantUML syntax.

PlantUML

Place an exportUML block inside the on start block. The PlantUML syntax will be output to the simulator's log (Show data Simulator), which will be drawn by the PlantUML Web server.

PlantUML Web server: https://www.plantuml.com/plantuml/

It is also possible to omit the arrow notation to s4 with the description block. This way, the normal transitions of operations are easier to understand.

Blocks PlantUML (as description)

image.png

Effect to Entry

The first step is to change each action performed as an effect to be performed as an entry.
In the mstate extension, to make it run as an entry, it evaluates whether the tickcount is 0.

image.png

Sensor detection

The guard of state transition in s4 simulates the sensor directly, so this should be changed to be determined by waterLevel.

image.png

Add detailed description

Added detailed explanations to make the state diagram easier to understand.

Blocks PlantUML

image.png

Source Code (JavaScript)
JavaScript
mstate.defineState(StateMachines.M0, "s3", function () {
    mstate.descriptionUml("entry/ \"Cycle Complete\"")
    mstate.onState(0, function (tickcount) {
        basic.showNumber(3)
        basic.showString("Cycle Complete")
    })
    mstate.onTrigger("", ["s0"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
    mstate.descriptionUml(":")
    mstate.onTrigger("empty", ["s4"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
})
input.onButtonPressed(Button.A, function () {
    mstate.send(StateMachines.M0, "e")
})
mstate.defineState(StateMachines.M0, "s1", function () {
    mstate.descriptionUml("entry/ moter on")
    mstate.onState(5000, function (tickcount) {
        if (0 == tickcount) {
            pins.digitalWritePin(DigitalPin.P1, 1)
            basic.showNumber(1)
        }
        timeoutedCount = tickcount
    })
    mstate.descriptionUml("after 5s")
    mstate.onTrigger("", ["s2"], function () {
        if (0 < timeoutedCount) {
            mstate.traverse(StateMachines.M0, 0)
        }
    })
    mstate.descriptionUml(":")
    mstate.onTrigger("empty", ["s4"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
})
mstate.defineState(StateMachines.M0, "s2", function () {
    mstate.descriptionUml("entry/ moter off")
    mstate.onState(3000, function (tickcount) {
        if (0 == tickcount) {
            pins.digitalWritePin(DigitalPin.P1, 0)
            basic.showNumber(2)
        }
        timeoutedCount = tickcount
    })
    mstate.descriptionUml("after 3s")
    mstate.onTrigger("", ["s3"], function () {
        if (0 < timeoutedCount) {
            mstate.traverse(StateMachines.M0, 0)
        }
    })
    mstate.descriptionUml(":")
    mstate.onTrigger("empty", ["s4"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
})
mstate.defineState(StateMachines.M0, "s0", function () {
    mstate.onTrigger("e", ["s1"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
    mstate.descriptionUml(":")
    mstate.onTrigger("empty", ["s4"], function () {
        mstate.traverse(StateMachines.M0, 0)
    })
    mstate.descriptionUml("waiting")
})
mstate.defineState(StateMachines.M0, "s4", function () {
    mstate.descriptionUml("entry/ moter off")
    mstate.descriptionUml("do/ \"Water Empty\"")
    mstate.onState(100, function (tickcount) {
        if (0 == tickcount) {
            pins.digitalWritePin(DigitalPin.P1, 0)
        }
        basic.showString("Water Empty")
    })
    mstate.descriptionUml("waterLevel <> 0/show icon")
    mstate.onTrigger("", ["s0"], function () {
        if (0 != waterLevel) {
            mstate.traverse(StateMachines.M0, 0)
            basic.showIcon(IconNames.Yes)
        }
    })
})
let waterLevel = 0
let timeoutedCount = 0
mstate.start(StateMachines.M0, "s0")
mstate.exportUml(StateMachines.M0, "s0", ModeExportUML.StateDiagram)
basic.forever(function () {
    if (input.buttonIsPressed(Button.B)) {
        if (0 != waterLevel) {
            waterLevel = 0
            mstate.send(StateMachines.M0, "empty")
        }
    } else {
        waterLevel = 1
    }
})

Conclusion

The state machines are useful if you can define a finite number of states and events that will trigger transitions to them. With the mstate extension, you can develop simpler and more robust projects.

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