Developing a Controller Using Design Patterns

By John Kramer

This whitepaper details a practical example of how to use software design patterns in modeling real-world embedded systems applications. By illustrating how to apply design patterns during development of a garage door controller, we demonstrate the important benefits of state design patterns:

  • Software Modularity
  • Code Re-usability
  • Improved Maintainability
  • Lower Costs
  • Faster Time-to-Market

View the source code for this controller.

A Garage Door Controller - Introduction
In the following example, we illustrate design patterns as they are used to develop a simplified garage door controller: from design to software. All sample code is in Java, although other object-oriented languages such as C++ can be used.

The main focus of this document is on the design of an embedded systems application using object-oriented design patterns. Topics such as the choice of wireless protocol to use for communications between the remote device and the controller, garage door motor size and sensor interfaces, are another topic altogether. While we use a garage door controller as an example, this example is not intended for use in designing specific garage door controllers.

A familiarity with general software design principles and guidelines for embedded systems applications is assumed. A basic knowledge of the Unified Modeling Language (UML), specifically the constructs used on class diagrams is also helpful.

Why Use Design Patterns for Your Embedded Application/Controller?
Although the use of design patterns is an established methodology in software design, its benefits are often poorly understood when compared to conventional programming styles. This confusion begins with the formal definition for a software design pattern: a general reusable solution to a commonly recurring problem. This definition is too generic and abstract to reveal anything useful about design patterns.

In an effort to better understand why design patterns are used, it is perhaps better to examine their various features. A software design pattern consists of several software classes and their relationships. Classes consist of a name, attributes (variables) and methods (functions); relationships include inheritance, composition, aggregation and/or references. A software design pattern is a combination of one or more classes and their relationships, and it is this combination that facilitates the grouping of software code in such a way that significant benefits can be achieved.

The greatest benefits to using software design patterns are modularity and better code re-usability. These, of course, all lead to code that is easier to maintain, lower costs and faster time to market.

Controller Specifications
Most modern garage doors have a button to toggle the door up and down. If the door is down and the button is pushed, the door will start to open. If the door is open and the button is pushed, the door will start to close. It should also be possible for the user to reverse the direction of the door by pressing the button while the door is moving (four-year-olds love to perform this operation repeatedly to the annoyance of their parents!).

Furthermore, if the door reaches its final position (either at the top or bottom), the controller should signal the motor to stop driving the door.

Most garage doors also have a sensor beam located at the bottom of the door. In the event that someone or something stands directly underneath the door while it is closing, the door should open back up to prevent a collision and possible bodily injury.

Controller Design
There are, of course, ultimately many ways one could choose to design the garage door controller. For most engineers, an intuitive solution involves using a state machine to govern the behavior of the garage door under the various situations described. Two obvious states are "OPEN" and "CLOSED", and these indicate the status of the garage door under the stationary conditions implied by their names.

A third state is needed to describe the motion of the garage door. It would be tempting to call this state "MOVING", but such a state cannot capture which direction the door is moving. Among other reasons, it would be important to know this information to determine whether tripping the sensor would require any action as described above. Thus, it is necessary to utilize two states to describe the garage door motion. These will be called "OPENING" and "CLOSING". The final state machine is shown below:

state
Figure 1: Garage Door State Machine

The transition labeled 'tripSensor' is included as a safety feature to prevent objects or people from being crushed by a closing garage door. The transition 'motorStopped' indicates the door cannot travel any further in the direction it is moving, and has signaled the motor to stop driving it. The transition 'pushButton' is analogous to a human user pushing their garage door opener button to either open or close their garage door.

This is an overly simplistic view of a garage door controller. The safety feature for tripping the sensor is attractive to those with children or people who have a tendency to leave things directly underneath the door (especially their cars)!

But what happens when power is lost and the door is ajar? There is no state here to model error conditions such as lost power, nor is there any recovery mechanism. Since there are alternative design methods for handling the loss of power that do not involve the use of a state machine (i.e. flagging diagnostics), a solution for handling the loss of power in this design is intentionally omitted.

Transitioning the Controller from Design to Software
At this point, many embedded software engineers would code up a standard state machine solution. Enumerated constants would be used to designate the states. Additionally, a new thread would be spawned off that executed in a forever repeating loop. This loop would wait for events, and then take action on the event based on which state is current. The code for this state machine would like this:

#define STATE_OPEN	1
#define STATE_CLOSED	2
#define STATE_OPENING	3
#define STATE_CLOSING	4

#define EVENT_MOTORSTOPPED	1
#define EVENT_PUSHBUTTON	2
#define EVENT_TRIPSENSOR	3

int state = STATE_CLOSED;

// This code runs forever looking for events!
StateMachine::void run() {
    GarageDoorControllerEvent newEvent;
    while( 1 == 1 ) {
        dequeueEvent( &newEvent );
        switch( newEvent->getType() ) {
            case EVENT_MOTORSTOPPED:
                if( state == OPENING ) { state = OPEN; }
                else if( state == CLOSING ) { state = CLOSED; }              
                break;

            case EVENT_PUSHBUTTON:
                if( state == CLOSED ) { state = OPENING; }
                else if( state == OPEN ) { state = CLOSING; }
                else if( state == CLOSING ) { state = OPENING; }
                else if( state == OPENING ) { state = CLOSING; }
                break;

            case EVENT_TRIPSENSOR:
                if( state == CLOSING ) { state = OPENING; }
                break;                  
       
            default:
                // Not a valid event, either discard or flag an error?!
                break;
        }

        // Don’t be a CPU hog, share with others and let them have a chance!
        sleep( 50 );
    }
}
				

NOTE: The above code omits details regarding threading, mutual exclusion for access of the state variable, and queuing mechanism for the stored events.

This method of straightforward coding, however, has some significant drawbacks. The change or addition of any events and/or transitions is cumbersome and rather error prone. Re-testing is required for this since the code has to change.

Using the Design Pattern Called 'State' A wonderful software design pattern exists for the modeling of state machines. The pattern is called State. It can be used to model any state machine one can conceive. A diagram of the State pattern is shown below:

sate
Figure 2:  State Design Pattern UML Class Diagram

The State pattern can easily be mapped into the problem domain of a state machine. Simply, each state in the state machine design becomes a ConcreteState subclass of an abstract State class (it can also be implemented as an interface for the Java inclined). Each transition in the state machine design becomes a method in the abstract State class, and the implementation of each transition is placed inside the overridden concrete subclass methods.

Applying the above description to our garage door opener state machine design yields an abstract class GarageDoorControllerState with the three abstract methods pushButton, tripSensor and motorStopped. It also yields four subclasses of the GarageDoorControllerState called Closed, Closing, Open and Opening. The context object is largely template-driven and simply created for simplicity of usage. The final class diagram is shown below:

astset
Figure 3:  State Pattern Applied to Garage Door Design

The Benefits of the State Design Pattern
This design definitely has advantages over the previous implementation. For one, each state is encapsulated inside its own class. This means that, if there are changes, there is a minimum amount of recompilation and testing to perform.

This design also isolates the state transitions from any code that needs to be done in conjunction with the transition. The StateContext object handles things such as engaging motor control or reading the sensors to determine if an object is present underneath the door. Another huge advantage to this approach involves the use of auto-generated tools. Many tools, such as UML Studio, Rhapsody, Together, Poseidon and others, allow for the creation of UML design patterns and have a feature that automatically generates the code. This allows the engineer to focus on the actual design and less on the implementation details. All in all, this approach allows for greater accuracy and understanding of the actual design and results in faster time to market.

One potential disadvantage exists with this approach, particularly in C++ where memory allocation becomes an issue. The constant use of the new and delete operators used while changing states could become a problem, depending upon the operating system's memory allocation policies. Constantly allocating and de-allocating memory could result in memory fragmentation. The extent of this depends upon the underlying memory allocation scheme in the operating system.

View the source code for this controller.