Skip to content
ionous edited this page Apr 6, 2016 · 4 revisions

Tutorial using a simple stopwatch as an example.

Introduction

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

Defining the Problem

Conceptually, this simple watch has just two buttons and a display. One button resets the timer, and the other acts as toggle to start and stop the watch. The display shows the elapsed time.

A simple mockup of the watch ( built in wxLua ) looks like this:

Watch

The Model

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 to model the watch:

C++ Lua
class Watch
{
    void tick( float delta );
    void reset_timer();
    float get_elapsed();
private:
    float time;
};
watch= { time =0 }

It's probably easy to imagine how you'd display the time from the model in the pictured stopwatch dialog. The bigger questions are: how do you handle the buttons and how do you handle updates? And that's just what the statechart is for. Keeping the behavior of the watch separate from its physical display and from its code model.

The Statechart

Here's where you might pull out a piece of paper and begin to sketch a statechart... or, you could just look at this handy UML diagram instead:

Watch chart

( In truth, especially with the lua version of hsm-statechart, I would probably skip the drawing and go straight to coding. I think you'll see why in a moment. )

Understanding the Chart

Let's look at the chart one state and one button at a time. We will then use that information as a guide towards the code.

Watch startup

The top most state here is called the Active state. When the chart enters the Active state: the entry event resets the watch time. This puts the timer at a good initial value.

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 out before we even get the watch home, this chart starts out Stopped.

Watch events

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 signaling may not be how a real quartz watch would work -- when the watch is turned off, the signaling would stop -- but we do it here to show that the program in which this watch is embedded doesn'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 the only handler of the Tick is in the body of Running. So, even though Tick is firing madly, the watch time only gets updated when the state 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.

The State Machine

In all flavors of hsm-statechart you need three things:

  1. A state chart implementation.
  2. A state machine to run the state chart.
  3. The events to send to the state machine.

Let's start with the middle one, then loop around to end with the chart itself.

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 makes accessing the watch model 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 }

Sending Events

The hsm-statechart code at it's core 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"

To create and send an event to the machine is a snap, in C it looks like:

  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 and Event Handling

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.

C Event Handling

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:

  1. a pointer to the machine: hsm
  2. the currently processing event: evt, which is the same event structure we declared above.
  3. the context data for the state: ctx, which in this chart is the watch data.

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 then 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.

Lua Event Handling

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.

Handling Reset

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, neither Running nor Stopped state has code handling evt_reset. That means their shared parent state -- Active -- will get a chance to handle the event instead.

So, let's see Active's 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;
}

The self-transition -- Active returning itself, depicted in our initial statechart diagram as the little curved arrow -- triggers one very specific behavior: the state exits, and then re-enters itself.

This re-entry is completely indistinguishable from the watch's initialization. The Active state's entry function is called, and then the machine moves into the Stopped state. This provides the behavior we want: the timer is reset.

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?

Implementing the Chart

The last and final step is to declare the chart as a whole: to show how the states interrelate and to define the hierarchy.

Chart of the C

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;
//    3. the state's initial child state, or zero if there are no children.
//
// the only difference b/t HSM_STATE_ENTER and HSM_STATE is that HSM_STATE_ENTER 
// allows a callback triggered whenever the state is entered...
// 
HSM_STATE_ENTER( ActiveState, HsmTopState, StoppedState );
    HSM_STATE( StoppedState, ActiveState, 0 );
    HSM_STATE( RunningState, ActiveState, 0 );

Believe it or not, we're 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, take a look at the sample code.

Lovely Lua

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.
    -- ( note: the watch object is sent by users of the chart )
    entry =
      function(watch) 
        watch.time=0
        return watch
      end,

    -- reset causes a self transition which re-enters active;
    -- this 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 updates the watch time:
      evt_tick =
        function(watch, time)
          watch.time= watch.time + time
        end,
    }
  }
}

Like the C version, the 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.