Sub-machine: establishSession

In this section, we look at adding actions and transitions for the sub-machine, establishSession.

The Design

establishSession, with events and states looks like this:

event MESSAGE_RECEIVED;

machine establishSession
{
   event ESTABLISH_SESSION_REQUEST
         , STEP0_RESPONSE
         , STEP1_RESPONSE
         , parent::MESSAGE_RECEIVED
         ;

   state IDLE
         , AWAITING_RESPONSE
         ;

}

The ESTABLISH_SESSION_REQUEST event is expected only in the IDLE state. When it is received, the machine sends the STEP0 message and moves to the AWAITING_RESPONSE state to receive the STEP0_RESPONSE.

event ESTABLISH_SESSION_REQUEST;
state IDLE, AWAITING_RESPONSE;

/** Start the session establishment process. */
action sendStep0Message[ESTABLISH_SESSION_REQUEST, IDLE]  transition AWAITING_RESPONSE;

sendStep0Message returns noEvent;

Obligingly, while in the AWAITING_RESPONSE state the MESSAGE_RECEIVED event is shared from the parent machine. establishSession must parse that message to see whether it is a step0 or step1 response.

event MESSAGE_RECEIVED, STEP0_RESPONSE, STEP1_RESPONSE;
state AWAITING_RESPONSE;

/** Parse the incoming message */
action parseMessage[MESSAGE_RECEIVED, AWAITING_RESPONSE];

parseMessage returns STEP0_RESPONSE, STEP1_RESPONSE, noEvent;

When the STEP0_RESPONSE is found, the step1 message is sent, and the machine remains in the AWAITING_RESPONSE state to await the next MESSAGE_RECEIVED event. When received, that message will again be parsed.

States are used to disambiguate actions which must be different upon subsequent receptions of a single event. It might be thought that since the MESSAGE_RECEIVED event is received twice, that the machine should move to a different state to await the event. This is not necessary, though, since the action is the same in each case: the message is parsed. Because the parsing yields a unique event for each message type, there is no ambiguity as to the action which must be taken.

Any temptation to design with the expectation that the second MESSAGE_RECEIVED event will be the expected STEP1_RESPONSE event should be effectively resisted. This example is the “happy path” to session establishment; it could very well be in the real world that an error indication may be received from the peer. Remaining in the AWAITING_RESPONSE state until the received message is parsed makes for the easist approache to designing for the “unhappy path.”

event STEP0_RESPONSE;
state AWAITING_RESPONSE;

/** Continue session establisment */
action sendStep1Message[STEP0_RESPONSE, AWAITING_RESPONSE];

sendStep1Message returns noEvent;

When the STEP1_RESPONSE is found, the machine notifies the parent that the session is established and returns to the IDLE state.

event SESSION_ESTABLISHED;

/** Notify parent that session is established. */
action notifyParent[STEP1_RESPONSE, AWAITING_RESPONSE] transition IDLE;

notifyParent     returns parent::SESSION_ESTABLISHED;

Remember that the parent machine begins to have messages send by the sendMessage sub-machine (the existance of which this machine is ignorant) once the session is established. Because of that, the parent will receive its MESSAGE_RECEIVED event when ACKs are sent from the peer. These events will be shared with this sub-machine, but will not be acted upon in the IDLE state. This highlights an important design principle, namely that sub-machines must return to a neutral state when their task is complete. For sub-machines such as this, which expect to be called more than once, that state is usually the initial state. For machines which expect to act only once, though, it may be required that they enter a done state, in which they react to no events.

A use-case for this last example might be a sub-machine which is acting as a co-routine to accomplish a long calculation. Once the calculation is finished, the machine must quiesce, even though the periodic event driving the machine may continue to be shared to it.

The full establishSession machine looks like this:

event MESSAGE_RECEIVED, SESSION_ESTABLISHED;

machine establishSession
{
   event ESTABLISH_SESSION_REQUEST
         , STEP0_RESPONSE
         , STEP1_RESPONSE
         , parent::MESSAGE_RECEIVED
         ;

   state IDLE
         , AWAITING_RESPONSE
         ;

   /** Start the session establishment process. */
   action sendStep0Message[ESTABLISH_SESSION_REQUEST, IDLE]  transition AWAITING_RESPONSE;

   /** Parse the incoming message */
   action parseMessage[MESSAGE_RECEIVED, AWAITING_RESPONSE];

   /** Continue session establisment */
   action sendStep1Message[STEP0_RESPONSE, AWAITING_RESPONSE];

   /** Notify parent that session is established. */
   action notifyParent[STEP1_RESPONSE, AWAITING_RESPONSE] transition IDLE;

   sendStep0Message returns noEvent;
   sendStep1Message returns noEvent;
   parseMessage     returns STEP0_RESPONSE, STEP1_RESPONSE, noEvent;
   notifyParent     returns parent::SESSION_ESTABLISHED;

}

The Generated Code

As mentioned in a previous section, the command line, fsm -tc --generate-weak-fns=false hsmCommunicator.fsm, produces the following files:

Source files:

  • hsmCommunicator.c

  • establishSession.c

  • sendMessage.c

Header files:

  • hsmCommunicator_priv.h

  • hsmCommunicator.h

  • hsmCommunicator_submach.h

  • hsmCommunicator_events.h

  • establishSession_priv.h

  • sendMessage_priv.h

In this section, we look at only the files related to this sub-machine, i.e. the ones beginning with establishSession.

Being a sub-machine, establishSession has no function to call it directly from the outside world, nor does it publish its own events. Thus, neither establishSession.h nor establishSession_events.h are needed.

Note

Should establishSession have also been a parent machine, having at least one sub-machine, it would have needed the establishSession_submach.h file.

Also because it is a sub-machine of a machine having actions which return events, the FSM function for establishSession must return an event. But, because it has no sub-machines of its own, its FSM structure does not have a sub-machine interface block array. So, we find the following in establishSession_priv.h:

typedef HSM_COMMUNICATOR_EVENT (*ESTABLISH_SESSION_FSM)(FSM_TYPE_PTR,HSM_COMMUNICATOR_EVENT);

struct _establishSession_struct_ {
    ESTABLISH_SESSION_STATE            state;
    HSM_COMMUNICATOR_EVENT             event;
    ESTABLISH_SESSION_STATE_FN   const (*statesArray)[establishSession_numStates];
    ESTABLISH_SESSION_FSM              fsm;
};

As with the top-level, this header contains everything needed by the action functions file(s). Convenience macros are re-defined as necessary to fit the needs of this machine.

In the source file, establishSession must provide the sub-fsm interface block needed by its parent.

HSM_COMMUNICATOR_EVENT THIS(sub_machine_fn)(HSM_COMMUNICATOR_EVENT e)
{
    return establishSessionFSM(pestablishSession,e);
}

HSM_COMMUNICATOR_SUB_FSM_IF hsmCommunicator_establishSession_sub_fsm_if =
{
    .subFSM = THIS(sub_machine_fn)
    , .first_event = THIS(firstEvent)
    , .last_event = THIS(noEvent)
};

The structure needed for each event shared from the parent must also be provided.

HSM_COMMUNICATOR_SHARED_EVENT_STR establishSession_share_hsmCommunicator_MESSAGE_RECEIVED_str = {
    .event               = THIS(MESSAGE_RECEIVED)
    , .psub_fsm_if         = &hsmCommunicator_establishSession_sub_fsm_if
};

The FSM function implementation is closer to that of a flat FSM, needing only to pass on any event returned from an action function, when that event does not belong to this machine.

HSM_COMMUNICATOR_EVENT establishSessionFSM(pESTABLISH_SESSION pfsm, HSM_COMMUNICATOR_EVENT event)
{
    HSM_COMMUNICATOR_EVENT e = event;

    while ((e != THIS(noEvent))
            && (e >= THIS(firstEvent))
          )
    {

#ifdef HSM_COMMUNICATOR_ESTABLISH_SESSION_DEBUG
        if ((EVENT_IS_NOT_EXCLUDED_FROM_LOG(e))
                && (e >= THIS(firstEvent))
                && (e < THIS(noEvent))
           )
        {
            DBG_PRINTF("event: %s; state: %s"
                       ,ESTABLISH_SESSION_EVENT_NAMES[e - THIS(firstEvent)]
                       ,ESTABLISH_SESSION_STATE_NAMES[pfsm->state]
                      );
        }
#endif

        /* This is read-only data to facilitate error reporting in action functions */
        pfsm->event = e;

        if ((e >= THIS(firstEvent))
                && (e < THIS(noEvent))
           )
        {
            e = ((* (*pfsm->statesArray)[pfsm->state])(pfsm,e));
        }

    }

    return e == THIS(noEvent) ? PARENT(noEvent) : e;
}

Note that a local noEvent will stop the loop, but must be transformed to the parent’s noEvent in order to be returned (otherwise, the parent would hand it back!).

Also note that should establishSession itself had had sub-machines, the conditional would have had an else block and a findAndRunSubmachine function.

The check on the event range for both the while and if constructs serves to capture local events. In our example, recall that the parseMessage action will return the local STEP0_RESPONSE and STEP1_RESPONSE events; these will fall in the range allowed in the loop and the conditional.

HSM_COMMUNICATOR_EVENT UFMN(parseMessage)(FSM_TYPE_PTR pfsm)
{
    DBG_PRINTF("%s", __func__);
    (void) pfsm;
    static bool first = true;

    return first ? (first = false, THIS(STEP0_RESPONSE)) : THIS(STEP1_RESPONSE);
}

(This simplistic action function serves only to illustrate our point, of course.)

Our notifyParent action illustrates meaningfull communication back to the parent.

HSM_COMMUNICATOR_EVENT UFMN(notifyParent)(FSM_TYPE_PTR pfsm)
{
    DBG_PRINTF("%s", __func__);
    (void) pfsm;
    return PARENT(SESSION_ESTABLISHED);
}

The parent SESSION_ESTABLISED event falls outside of our range check (being below THIS(firstEvent) - see hsmCommunicator_events.h), and will thus end the loop. Since it is not equal to our local noEvent, it will not be transformed, but will be returned unchanged to the parent FSM function. That function will see it as one of its own events and will act on it as directed by its own state chart.