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)
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
Source Code (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.
If the water volume sensor detects empty when the state is not 4, the system should transition to state 4.
Source Code (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) |
---|---|---|
|
|
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
.
Sensor detection
The guard of state transition in s4 simulates the sensor directly, so this should be changed to be determined by waterLevel
.
Add detailed description
Added detailed explanations to make the state diagram easier to understand.
Blocks | PlantUML |
---|---|
|
Source Code (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.