Episode 5 Statechart Framework – event
In the previous episode, we introduced a simple statechart framework and learned how to represent leaf states with a Javascript enumeration. We also learned how to assign entry and exit actions to a leaf state such that they are automatically executed whenever the state is entered or exited.
But what causes a state to be entered or exited in the first place? That is events. An event tells a state machine something has happened and depending on its current state the event may trigger a transition.
In statecharts, the definition of transition is very broad. It covers any responses a state machine makes in reaction to an event. There are two main types of transitions:
(1) External transition – A transition exiting the current state and entering a different or the same state. In our timer example, the transition from RUNNING to PAUSED upon the B_PRESSED event is an example of an external transition, so is the transition from STOPPED back to itself upon B_PRESSED. The latter is a special case called self-transition.
When an external transition is triggered, exit actions of any states exited and entry actions of any states entered along the transition path are executed. This involves finding the least common ancestor (LCA) of the source and target states of the transition. Doing so efficiently can be tricky and often calls for a dedicated library (e.g. QP, xstate) or code generator (e.g. Qt, scxmlcc). For simplicity our framework only supports entry and exit actions in leaf states. This limited support is very simple to implement yet very useful in many practical applications.
(2) Internal transition – A transition that does not leave the current state, and therefore no exit and entry actions are carried out. The TIMER_INTERVAL event inside the RUNNING state is an example of internal transitions. The actions associated with the event are executed, i.e. the LedCount variable is incremented, and the remainingTime variable is subtracted by intervalMs, etc. However the system stays in the RUNNING state throughout.
Every transition, internal or external, is denoted by a label in the following format:
EVENT_NAME [guard_condition] / action_list
EVENT_NAME identifies the type of the triggering event, such as B_PRESSED or TIMER_INTERVAL.
guard_condition is a logical expression which determines if a transition is enabled or not when a triggering event arrives. A transition is only enabled when the guard_condition is evaluated to true. For example, in the ROOT state, we have these two internal transitions:
Upon the TIMER_FLASH event, the first transition is only enabled when the flag flashOn is set, while the second one is only enabled when flashOn is cleared. Working together they caused the LEDs to blink at 1 Hz (TIMER_FLASH arrives every 500ms when the timer is started). Since these transitions are defined in the ROOT state, they apply to all substates.
action_list is a comma-separated list of activities to be performed when the transition is triggered. An action_list can be empty, meaning that there are no activities to accompany the transition. Otherwise it may contain a description of actions to be performed, a list of functions to be called or events to be generated. In the first TIMER_FLASH example above, the actions are clearing the flashOn flag and calling the function display(0) to turn off all LEDs.
In our framework, we represent events with an enum named Evt:
enum Evt {
EVT_START,
// TIMER events
TIMER_FLASH,
TIMER_INTERVAL,
TIMER_STOP,
// INTERNAL events
A_PRESSED,
B_PRESSED,
TIMEOUT,
}
This list contains all events mentioned in the statechart. Our framework provides an event interface which provides the following API methods:
event.raise(evt, param) – To send an internal event to itself.
event.on(evt, handler) – To specify a handler function for the specified event.
The listing below shows the handling of the TIMER_FLASH event in the ROOT state. Notice how we use an “if… else…” block for guard conditions:
event.on(Evt.TIMER_FLASH, () => {
if (!flashOn) {
flashOn = true
display(ledCount)
} else {
flashOn = false
display(0)
}
})
The next example shows the handling of the B_PRESSED event. Since the handling differs depending on what the current state is, we use an “if… else if… else” block to distinguish the states of STOPPED, RUNNING or PAUSED. In this case, the order of state checking does not matter since there is no nesting relationship among these states. Otherwise the order of state checking goes from the most nested state to the least nested one, which matches the order of precedence of transitions in statecharts.
event.on(Evt.B_PRESSED, () => {
if (inMainStopped()) {
ledCount = ledCount % 25 + 1
state.transit(Region.MAIN, MainState.STOPPED)
} else if (inMainRunning()) {
state.transit(Region.MAIN, MainState.PAUSED)
} else if (inMainPaused()) {
state.transit(Region.MAIN, MainState.RUNNING)
}
})
Helper functions, like isMainStopped(), provide shorthands to check states and define the state hierarchy (see inMainStarted()):
function inMainStopped() { return state.isIn(Region.MAIN, MainState.STOPPED) }
function inMainRunning() { return state.isIn(Region.MAIN, MainState.RUNNING) }
function inMainPaused() { return state.isIn(Region.MAIN, MainState.PAUSED) }
function inMainTimedOut(){ return state.isIn(Region.MAIN, MainState.TIMED_OUT) }
function inMainStarted() { return inMainRunning() || inMainPaused() ||
inMainTimedOut() }
In the next episode, we will show the internal workings of the event and state interfaces, and see how everything works together.
Copyright (C) 2019 Gallium Studio LLC.