It seems that I'm really going over my time budget,
but no discussion of modern embedded programming and active objects in particular
would be complete without mentioning modern state machines. So, let me just only outline
how state machines fit with the overall picture and what problems they solve.
As I already mentioned, the biggest challenge of event-driven programming
is to maintain the context from one event to the next.
This is because in general, the proper reaction to an event depends not only on the event type
(that is an event signal), but also on the history of the past events.
This is much less of a problem in sequential systems, because you always know where in
the hardcoded sequence of events you are, so the context is maintained naturally and automatically.
As it turns out this automatic context management is quite expensive,
because it requires a private stack for each sequential thread, but it certainly is convenient.
In contrast, an event-driven system loses the stack context after every event, because the
code returns back to the event loop. Consequently, event-driven systems tend to use less stack space,
but instead require a different form of managing the context.
The most popular is the manual context management,
where programmers invent flags and variables, such as the isLedOn flag in your Blinky active object.
This approach seems to work initially, but is almost guaranteed to degenerate
into "spaghetti" code, which I'd like to quickly demonstrate with the Visual Basic
calculator example that used to ship with several versions of Visual Basic.
The simple 4-operation calculator obviously adds, subtracts, multiplies, and divides. But if you try
some less usual sequences of key-presses, you would discover many "corner cases".
One such "corner case" is the following sequence of operations:
2, -, -, -, 2, =.
The calculator crashes with "Run-time error 13".
One might argue that this is a meaningless sequence of events,
but at the same time it is also obvious that no user actions should crash the software like that.
So, the calculator apparently has
trouble correctly responding to events in different contexts.
But if you look at the code, you will find out that managing the context is
really the central concern here and most of the code is devoted to it.
Even though you might not be familiar with Visual Basic, I hope you can recognize a
bunch of variable declarations with the sole purpose to remember the current context.
Then, there are a bunch of subroutines each designed to handle a group of related events.
For example, the minus-key that confused the calculator before,
is handled in the subroutine Operator_Click(), which happens to be the most complex one.
Here you can see that the various flags and variables are checked in nontrivial conditions,
then modified and re-checked again, so that it is really hard to know
for sure which path through the code will be taken in a given situation.
Apparently, a 4-operation calculator is already
complex enough to reach the limit of such manual context management.
The real problem here is that the variables and flags
provide too much of redundant and poorly organized information.
But the improvised context management reflects the correct intuition of the software developers
that you don't need to remember the *whole* history, only certain *aspects* of it.
Specifically, you only need to remember the *relevant history*, which is only what influences
the *future* behavior of the system, and events don't contribute equally to that relevant history.
For example, an electronic controller for a
computer keyboard generates either lower-case or upper-case key codes.
This behavior depends on whether the Shift key has been pressed or released, but it doesn't
matter which other keys have been pressed or released, or how many of them were pressed.
Therefore, the whole relevant history of the keyboard can be reduced just to two
equivalence classes: the "normal" class, where the keys generate lower-case codes,
and the "shifted" class, where the same keys generate upper-case codes.
Which leads to the most important concept of "State".
"State" of a system is an *equivalence class* of past histories of a system,
all of which are equivalent in the sense that the future behavior of the system
given any of these past histories will be identical.
Thus the concept of "State" is the most efficient representation of the relevant history.
It is the minimum information that captures only the aspects relevant for the future behavior
and abstracts away all irrelevant aspects.
The concept of "State" naturally leads to the concept of "State Machine",
which is the set of all states, that is equivalence classes of past histories,
plus the rules for changing from one state to another.
These rules are called *transitions* and capture the fact that some events contribute to the
relevant history more than others. Those events contributing more trigger the state transitions.
A very nice aspect of the "State Machine" concept is its compelling graphical representation
in form of state diagrams. In UML, states are represented as rounded-corner rectangles
with the name of the state in the name compartment
and transitions are represented as arrows labeled with the triggering event.
Additionally, every state machine needs to have
an initial transition that points to the default state,
which is active when the state machine is created and before any events are dispatched to it.
As an example, let's build the state machine representing the
behavior of your BlinkyButton active object, but without the isLedOn flag.
The state machine starts in the "off" state.
Upon the entry to "off", the LED-zero is turned off and the time event is armed for the off-time.
When the time event expires, it posts the TIMEOUT event to the active object,
which triggers transition to the "on" state.
Upon the entry to "on", the LED-zero is turned on and the time event is armed for the on-time.
In the "on" state, the TIMEOUT event triggers transition to "off", which closes the cycle.
However, this is BlinkyButton state machine, so it must also handle the
BUTTON_PRESSED and BUTTON_RELEASED events.
These events are handled as so called internal transitions,
which just execute actions but don't trigger a change of state.
The BUTTON_PRESSED internal transition turns the LED-one on.
Similarly, the BUTTON_RELEASED internal transition turns the LED-one off.
But these events need to be handled also when the LED-zero
is on, which means that you need to repeat the same internal transitions in the "on" state.
Of course, such repetition is perhaps not a big deal in a toy example like BlinkyButton,
but in real-life projects repetitions like that quickly pile up to a clutter.
This phenomenon, known as "transition explosion"
renders the traditional state machines impractical for bigger projects.
But traditional state machines are no longer state of the art.
Modern hierarchical state machines provide a mechanism of hierarchical state nesting,
which has been exactly designed to eliminate the repetitions.
It works like this: states can nest in other states.
For example, both "off" and "on" states can nest in the state "active".
The meaning of such state nesting is that any behavior specified in the super-state
applies to all its sub-states.
With this in mind, you can now move the internal transitions BUTTON_PRESSED and BUTTON_RELEASED
up one level to the superstate "active", so that they apply to both its substates "off" and "on"
And because of that semantics of state nesting,
you can remove the repetition of these transitions from the substate "on".
So, now the state diagram of BlinkyButton is ready, but you can do more with it.
You can generate code from your hierarchical state machine automatically,
and you can build it for your embedded target.
The generated code is highly readable and fully traceable to the diagram.
"Traceable" means that every state machine element is represented in the code exactly once
and there is one-to-one correspondence between the diagram and code.
For example, here is the entry action to state off, which you can trace to your
model by copying the piece of code and using the special paste-link command in the model.
You can also go in the opposite direction. For example, you highlight the BUTTON_RELEASED
transition in the model, and use the copy-link command to place the link in the Clipboard.
Then you paste the link to the search box in your code editor to find the generated code.
Alright, so let's just build the generated code
and load it to the board.
As you can see the hierarchical state machine in the BlinkyButton active object works as designed.
You can also easily debug the code in terms of state machine elements,
for example, let's set a breakpoint on the BUTTON_RELEASED internal transition.
In summary, this session addressed two broad classes of challenges faced by
professional embedded software developers: concurrency issues and structural issues.
As far as concurrency architectures are concerned,
you have learned that "superloop" and RTOS are not the only games in town.
Experts on concurrency avoid the multitude of blocking mechanisms
and instead of a "naked" RTOS use the event-driven Active Object design pattern.
Active objects are beautifully complemented by the modern hierarchical state machines, which
provide a better alternative to the spaghetti code resulting from the "manual context management".
Both active objects and state machines raise the level of abstraction above the "naked" RTOS
and provide the *right* abstractions for applying graphical modeling and automatic code generation.
Welcome to the 21-st century!