-
Notifications
You must be signed in to change notification settings - Fork 0
StopWatch
Tutorial using a simple stopwatch as an example.
The stopwatch samples included in the source are modeled after the boost statechart tutorial. A similar tutorial exits for Apache's scxml.
This tutorial is meant to demonstrate:
- basic state declaration
- simple event handling
- simple hierarchical charts
- different ways of using events and context data
It shows how all this is done in both C and Lua. I'll assume you maybe know a little bit about state charts, but you don't have to be any sort of statemachine expert. ( Is there such a person? )
Conceptually, this simple stopwatch has just two buttons and a dial. One button resets the timer, and the other is a toggle to start and stop the watch. The dial simply shows the elapsed time.
A simple mockup of the watch ( built in wxLua ) looks like this:
In general practice, it's good to separate your models -- the logical objects of your application -- from their behavior. This makes both sides more flexible and reusable. In this example, the following snippets are all we really need for this simple watch.
C++ | Lua |
---|---|
class Watch { void tick( float delta ); void reset_timer(); float get_elapsed(); private: float time; }; |
watch= { time =0 } |
It's hopefully easy to imagine how you'd display the above model in the pictured stopwatch dialog. The big questions are: how do you handle those buttons, and how do you handle updates? And that of course, is what the statechart is for. Keeping the logic of the watch separate from its display, and from its model.
Here's where you might pull out a piece of paper and begin to sketch a statechart to support all this. Or, you could just look at this handy UML diagram instead:
( In truth, especially with the lua version of hsm-statechart
, I probably would skip the drawing stage, and go straight to the code. I think you'll see why in a moment. )
Let's look at the watch chart one state, and one button at a time, and use that as a guide towards the code.
The top-most state here is called the Active
state. In this case, when the chart enters the Active
state, the entry
event resets the watch time. This puts the watch in a good state to start with.
In UML statecharts, after entry
has occurred, a state then enters its initial child state (if any). The initial state is marked always marked with an empty circle and an arrow.
Since we don't want our batteries to be Running
down before we even get the watch home, this chart starts out Stopped
.
The entry
event is one of the predefined UML statechart events, and although it doesn't appear here, exit
is another. For this chart, the remaining three events are all custom events: created specifically to handle the watch logic. They are:evt_toggle, evt_reset, and evt_tick.
evt_toggle
will get triggered whenever the user presses the watch's toggle button, and evt_reset
when they press the watch's reset button. evt_tick
, on the other hand, we'll just send continuously to the watch. This continuous signalling may not be how a real quartz watch would work, but we do it here to show that the outside world -- the code systems that this watch is embedded in -- don't need to know which state the watch is in. The outside code acts generically with respect to the watch, and the watch logic makes up its own mind what to do.
Do these events and their behavior as described in the machine make sense?
Well, you can see in the middle of the diagram that evt_toggle moves the machine to Running
while in Stopped
, and to Stopped
while in Running
. That's the behavior we wanted from the outset. Also, notice, that only the body of Running
handles the Tick. So, even though Tick is firing madly, the watch time is only updated while the machine is Running
.
That seems about right then. Let's hold off on evt_reset for a second, and look at how to implement what we've got so far in code.
In all flavors of hsm-statechart
you need three things:
- A chart
- A machine to run the chart.
- The events to send to the machine.
Let's start with the middle one, then loop around to end with the chart.
In the C implementation there are actually two different machine "classes" -- the core hsm_machine_t
, and a slightly weightier ( by 8 bytes! ) extension hsm_context_machine_t
. The context machine allows states to have optional per state data, and provides an easy way to give the whole chart access to external data. While we don't absolutely need the context machine for this simple example, it does make accessing the watch object easier, so we'll go ahead and use it.
Watch watch; // an instance of our watch model.
hsm_context_machine_t machine; // our statemachine.
WatchContext ctx= { 0, 0, &watch }; // access to the watch by the machine;
// 0,0 are system defaults.
// initialize the statemachine, pass our watch context data.
hsm_machine hsm= HsmMachineWithContext( &machine, &ctx.ctx );
// start the machine, handing it the topmost state.
// we'll see the state definitions in just a minute....
HsmStart( hsm, ActiveState() );
Watch context in the above is just a small struct wrapping the watch data, it looks like:
typedef struct watch_context WatchContext;
struct watch_context {
hsm_context_t ctx; // a required system defined structure ( 8 bytes )
Watch * watch;
};
The lua interface is just as easy, except every machine is automatically a context machine, and you pass the chart instead of the top state.
require 'hsm_statechart'
-- create a simple representation of a watch
local watch= { time = 0 }
-- create a state machine with the chart, and the watch as context
local hsm= hsm_statechart.new{ stop_watch_chart, context= watch }
The hsm-statechart
code is very flexible. At it's core it has a C based implementation and other layers are built up around that. Applications can define events pretty much however they want. In the following examples, I'll use enums for the C side, but note: the lua side uses strings. Really, anything goes.
C++ | Lua |
---|---|
enum watch_events { WATCH_RESET_PRESSED, WATCH_TOGGLE_PRESSED, WATCH_TICK, }; typedef struct hsm_event_rec WatchEvent; struct hsm_event_rec { enum watch_events type; }; |
-- in lua you don't have to predeclare events -- they're simply strings: "evt_reset", "evt_toggle", "evt_tick" |
while ( HsmIsRunning( hsm ) ) {
// get a key from the keyboard
const int ch= PlatformGetKey();
// turn a '1' into the reset button,
// turn a '2' into the toggle button
const WatchEvents events[]= { WATCH_RESET_PRESSED, WATCH_TOGGLE_PRESSED };
const int index= ch-'1';
if ((index >=0) && (index < sizeof(events)/sizeof(WatchEvents))) {
const WatchEvent evt= { events[index] };
HsmSignalEvent( hsm, &evt );
}
Lua is again very much the same as the C-implementation ( the chart, on the other hand, as you will shortly see, is radically simpler ):
-- lookup table for mapping keyboard to events
local key_to_event = { ["1"]='evt_reset',
["2"]='evt_toggle' }
-- keep going until the watch breaks
while hsm:is_running() do
local key= string.char( platform.get_key() )
-- change it into an event
local event= key_to_event[key]
-- send it to the statemachine
if event then
hsm:signal( event )
end
end
The tick event is just an extension of the basic watch event which adds the elapsed time.
typedef struct tick_event TickEvent;
struct tick_event {
WatchEvent core;
int time;
};
// once per loop, or after some accumulated time, issue a tick
const int elapsed_time= 1; // vanish the coin in the usual fashion.
const WatchEvent watchevt= { WATCH_TICK, elapsed_time };
HsmSignalEvent( hsm, &watchevt );
You can have as many different kinds of events and associated event data structures as your machine needs.
In Lua, you can use tables to pass event structures, but you don't need to if you don't want. Just pass one or more additional parameters to the signal function:
local elapsed_time= 2; -- how fast is coding in lua? twice as fast.
hsm:signal( "evt_tick", elapsed_time )
-- an example of multiple parameters, *not* how this chart works.
hsm:signal( "evt_tick", seconds, milliseconds, microseconds )
-- an example of table, also *not* how this chart works.
hsm:signal( "evt_tick", { seconds=5, days=1, years=23 } )
States handle the events sent to the machine. The "active" state gets the first crack at handling every event. If it doesn't respond to the event, then the parent gets a chance.
On the C-side ( is that commonly known as the shore? ) the most basic way of implementing a state's event handler is to define a function. That one function will receive all events for that state, and with enum based events, you simply write a switch with case statements for each event.
Stopped is the simplest state, so lets start there:
// The naming convention can be changed by redefining some macros
// by default, it's <name of state>Event
hsm_state StoppedStateEvent( hsm_status status )
{
// return NULL by default to indicate unhandled events
hsm_state ret=NULL;
switch (status->evt->type) {
// but, hen the 'toggle' button gets pressed...
case WATCH_TOGGLE_PRESSED:
// transition over to the running state
ret= RunningState();
break;
}
return ret;
}
The hsm_status
object is a const *
with three members:
It's those last two items that make it possible to implement the running state:
hsm_state RunningStateEvent( hsm_status status )
{
// by default this function does nothing....
hsm_state ret=NULL;
switch (status->evt->type) {
// but, when the 'toggle' button gets pressed....
case WATCH_TOGGLE_PRESSED:
// transition back to the stopped state
ret= StoppedState();
break;
// also, when a 'tick' is sent, update our timer
case WATCH_TICK:
{
// our event is the tick event
TickEvent* tick= (TickEvent*)status->evt;
// our context is the watch object
Watch* watch=((WatchContext*)status->ctx)->watch;
// tick by this much time
TickTime ( watch, tick->time );
// indicate to the statemachine that we've take care of this event
// this stops the event from being sent to our parent
ret= HsmStateHandled();
}
break;
}
return ret;
}
Notice that in the WATCH_TICK case, the code not only updated the watch, it also returned a special value: HsmStateHandled
in order to stop the event from moving up the chart. You should return handled
anytime you've done something as a result of the event, but don't want a transition to a new state to occur.
As a side note: if you're ever tempted to write a little bit of logic for a child state event, and return NULL to allow the parent state to also handle the event -- you probably want to refactor the statechart. It works in code, but it's not how statecharts are intended to function.
In Lua, things are a little different. States aren't represented as functions, they are represented as tables. The event handlers are simply keys in those tables. Let's see how it works.
For simple event handlers like Stopped
, where all you need to do is simply transition to a new state, you simply provide an entry in the state table mapping the event name to the name of the next state.
Stopped, in it's entirety, looks like this:
-- while stopped:
stopped = {
-- toggle starts the watch running.
evt_toggle = 'running'
}
Short and sweet, no?
In Lua, of course, you can store anything in a table, and that includes functions. For Running, therefore, all we need to do is map the toggle event to the stopped state, and then map the tick event to a function which updates the time.
-- while the watch is running:
running = {
-- the toggle button stops the watch
evt_toggle = 'stopped',
-- the tick of time updates the watch
evt_tick
function(watch, time)
watch.time= watch.time + time
end,
}
hsm-statechart
does all the behind the scenes magic necessary to make declaring states in Lua as easy as possible.
The one event we held off on was evt_reset
. For this watch: we want the reset button to both clear the timer, and stop the timer.
As you've seen in the previous code, no state has code handling evt_reset
. That means the parent state of both 'Runningand
Stopped` will get a chance to handle it.
In the chart, Active
is that parent state. It contains both Running
and Stopped
. So, let's see its event handler:
hsm_state ActiveStateEvent( hsm_status status )
{
// by default this function does nothing....
hsm_state ret=NULL;
switch ( status->evt->type ) {
// but, whenever the reset button is pressed...
case WATCH_RESET_PRESSED:
// it transitions to itself.
ret= ActiveState();
break;
}
return ret;
}
That self-transition, the little curved arrow in the statechart diagram at the start of the tutorial, triggers one very specific behavior. The state exits, and then re-enters itself. This re-entry is completely indistinguishable from when the watch was started. The Active state's entry function is called, and then the machine moves into the Stopped state. This behavior, therefore, provides exactly what we want.
For completeness, sake here is Active's entry function:
hsm_context ActiveStateEnter( hsm_status status )
{
Watch* watch=((WatchContext*)status->ctx)->watch;
watch->time=0; // or maybe, ResetTime( watch ) to be a bit cleaner.
return status->ctx;
}
So, that's all nice and everything, but how do these functions actually get called at the right time?
The last and final step is to declare the chart as a whole: to show how the states interrelate, and define how the hierarchy works.
In the core hsm-statechart API, hierarchy is defined via macros. These macros also connect up the logic to call all the functions we've defined above. The chart in it's entirety looks like this:
// all of the HSM_STATE* macros take:
// 1. the state being declared;
// 2. the state's parent state;
// 3. an initial child state to move into first, or zero.
//
// the only difference b/t HSM_STATE_ENTER and HSM_STATE is that HSM_ENTER
// declares a callback that gets called whenever the state gets entered...
//
HSM_STATE_ENTER( ActiveState, HsmTopState, StoppedState );
HSM_STATE( StoppedState, ActiveState, 0 );
HSM_STATE( RunningState, ActiveState, 0 );
Believe it or not, we are finally done. We've seen the machine that uses this chart, we've defined all of our events, and triggered them at appropriate times, and we've also implemented all the handlers which get called for each state and each event.
If you want to see the complete source for this example, just take a look at the watch1_enum_events.c sample code, included in the source, and also: here.
In Lua, the chart is -- just like the states themselves -- a table. When you define the states, you are actually simultaneously defining the chart.
There's really no way to break it down into parts, so here it is, finally, all at once:
local stop_watch_chart= {
-- each state is represented as a table
-- active is the top-most state
active= {
-- entry to the active state clears the watch timer
-- the watch is provided by machines using this chart
entry
function(watch)
watch.time=0
return watch
end,
-- reset causes a self transition which re-enters active
-- and clears the time no matter which state the machine is in
evt_reset = 'active',
-- the active state is stopped by default:
init = 'stopped',
-- while the watch is stopped:
stopped = {
-- the toggle button starts the watch running
evt_toggle = 'running',
},
-- while the watch is running:
running = {
-- the toggle button stops the watch
evt_toggle = 'stopped',
-- the tick of time updates the watch
evt_tick
function(watch, time)
watch.time= watch.time + time
end,
}
}
}
Like the C version, tthe complete sample is included in the source, and it's also here.
The wxLua version has the exact same statechart, and the same watch model, but uses a dialog to gather input from the user, and display the watch's time.