Domain-Driven Design in Embedded Systems

What is domain-driven design? A software design approach that focuses on modeling the software architecture to match the real-world system it operates in, domain-driven design creates a common language between the customer and the developer — and has the software framework reflect the language.

Because the customer and the software developers share a common language when implementing domain-driven design, there is less chance of misunderstanding between the customer and the developers as they iterate on the design and implementation of the system. And because the software design resembles the common language, functionality is easier to verify and changes are easier to implement.

Domain-driven design is generally applied to large, complex systems. Eric Evans, in his book, “Domain-Driven Design: Tackling Complexity in the Heart of Software,” concentrates his examples in this space. However, the approach is just as applicable to embedded systems for creating expressive, well-described, more easily modifiable software systems.

Take, for example, the control system for a robot competing in robot-sumo.

a black sumobot equipped with sensors as an example of domain-driven design in embedded systems

A Domain-Driven Design Example: Robot-Sumo

Robot-sumo is a competition in which two robots — often called “sumobots” — attempt to push each other out of a circular ring. The ring is one color (usually black), with a border around the outer edge in a contrasting color (usually white) so that contestants know when they are approaching the edge of the ring. The sumobots are fully autonomous and must use their onboard sensors to locate their opponent and push them out of the ring.

The example sumobot has a downward-facing line sensor array at the front of the robot for detecting the edge of the ring, and three proximity sensors — one each on the front, left and right sides — for finding its opponent. Depending on whether the sumobot has found its opponent, the sumobot may act differently for the same sensor inputs.

This sounds suspiciously like a state machine (because it is), but before any software developers start busting out the switch statements, take a step back. Domain-driven design attempts to create a common language for the project. In the case of the sumobot, that language is a statechart. Domain-driven design also tries to get the software architecture to look as much like the common language as possible, which means actually modeling states instead of keeping track of states as enumerated values. In object-oriented design, that translates to states as objects with member functions to handle the events. In non-object-oriented implementations, that could mean function pointers or relying on multiple dispatch capabilities of the language, if they exist. For the sake of this example, we will stick with an object-oriented implementation because it is easier to visualize.

First Iteration Sumobot

For the first iteration of the sumobot, the robot’s behavior is described as “the robot should spin around in place until it finds its opponent, then it should charge toward it at full speed. If the robot loses sight of its opponent, it should start spinning again to look for it. For now, the robot will not worry about the edge of the ring.” Because everyone involved in the project either knows how, or has been taught, to properly read a statechart (the common language for the project), the following state machine is drawn and developers and customers agree that it can represent the desired functionality:

diagram showing sumobot state machine containing two states: the Search state and the Attack state, as well as what triggers each response: proximity lost (spin) and proximity detected (attack)

The state machine contains two states — the Search state and the Attack state. Transitioning to the Search state causes the robot to spin in place. While in the Search state, if the front proximity detector sees something, the state machine transitions to the Attack state. Transitioning to the Attack state causes the sumobot to stop spinning and move forward at full speed. If the front proximity detector stops seeing something while in the Attack state, the state machine transitions back to the Search state, causing the robot to stop moving forward and resume spinning in place.

To make the code look like the state machine, the following classes are needed:

sumobot classes and state machine

There are two state objects — SearchState and AttackState — which are derived from a base State class. Similarly, there is a ProximityEvent object that is derived from a base Event class. Finally, there is a StateMachine class that acts as an interface to the state machine.

Each state class implements an event handler method that takes a ProximityEvent as an argument. The SearchState event handler will command the sumobot to stop spinning, start moving forward as quickly as possible and transition to the AttackState if the ProximityEvent indicates it sees the opponent.

// Pseudocode for StateSearch::handle_event
SearchState::handle_event(ProximityEvent const & e) {
if (e.front == detected) {
sumobot.stop();
sumobot.move_forward(RAMMING_SPEED);
state_machine.transition_to_state(attack_state);
}
}
The AttackState event handler will stop the sumobot, have it resume spinning and transition back to the SearchState if the ProximityEvent indicates it no longer sees the opponent.
// Pseudocode for AttackState::handle_event
AttackState::handle_event(ProximityEvent const & e) {
if (e.front != detected) {
sumobot.stop();
sumobot.spin();
state_machine.transition_to_state(search_state);
}
}

The clarity of intent reflected in the statechart is matched in the code. There is a one-to-one relationship between states and event handling on the statechart — and their implementation in the code.

Second Iteration Sumobot

For the second iteration of the sumobot, it is decided to incorporate edge detection and a behavior change that sees the robot sit at the edge of the ring and wait to ambush its opponent. If the opponent is detected to the side while the sumobot is waiting in ambush, the sumobot will find a new place to hide. If at any time the opponent appears in front of the sumobot, the sumobot will attack. While attacking, if the sumobot loses track of its opponent, it will find a new hiding spot. The following state machine is drawn and agreed upon to represent the desired functionality:

sumobot state machine

The state machine now contains three states — the Hide state and Ambush state, which are new, and the Attack state, which is retained from the first iteration. To reduce boilerplate in code, actions on transitions to states are consolidated into state entry functions that will be called whenever the machine transitions to a different state. The Hide state initiates a search for a new ambush position by turning a random amount and moving forward. If the edge of the ring is detected, it transitions to the Ambush state. If the opponent is detected in front of the sumobot, it transitions to the Attack state.

Upon entering the Ambush state, the sumobot stops, turns around to face the ring interior and waits until it sees its opponent. If the opponent appears to either side, the sumobot transitions to the Hide state. If the opponent appears in front, the sumobot transitions to the Attack state.

Upon entering the Attack state, the sumobot charges forward. If it loses sight of the opponent to its front, the sumobot transitions to the Hide state.

Because the implementation of the state machine matches the description of the state machine, changes are modular and well contained.

sumobot state machine second iteration

A new event class is added for edge detection, as well as two new state classes — HideState and AmbushState. SearchState from the first iteration is removed, but AttackState is retained. Each state implements a state entry function that will be called by StateMachine::transition_to_state().

HideState has an entry method and two event handlers, corresponding to the “on entry” and the events Hide state handles on the statechart. When HideState is entered, the sumobot turns a random amount, then starts moving forward. On an EdgeEvent, HideState transitions to AmbushState. On a ProximityEvent, if the opponent is detected to the front, HideState transitions to AttackState, otherwise it is ignored.

// Pseudocode for HideState
HideState::on_entry() {
sumobot.turn(rand(0, 359));
sumobot.move_forward(HIDING_SPEED);
}

HideState::handle_event(EdgeEvent const & e) {
state_machine.transition_to_state(ambush_state);
}

HideState::handle_event(ProximityEvent const & e) {
if (e.front == detected) {
state_machine.transition_to_state(attack_state);
}
}

AmbushState has an entry method and handles ProximityEvents, corresponding to the “on entry” and the events Ambush state handles on the statechart. When AmbushState is entered, the sumobot stops and turns 180 degrees to face into the ring. If the ProximityEvent indicates the opponent is detected to the front of the sumobot, AmbushState transitions to AttackState. If the ProximityEvent indicates the opponent is detected to either side, AmbushState transitions to HideState.

// Pseudocode for AmbushState
AmbushState::on_entry() {
sumobot.stop();
sumobot.turn(180);
}

AmbushState::handle_event(ProximityEvent const & e) {
if (e.front == detected) {
state_machine.transition_to_state(attack_state);
}
else if (e.left == detected e.right == detected) {
state_machine.transition_to_state(hide_state);
}
}

AttackState’s handle_event is changed to transition to HideState instead of the now-deleted SearchState if the ProximityEvent indicates that the opponent is no longer in front of it. An entry method is also added to get the sumobot to attack the opponent.

// Pseudocode for AttackState
AttackState::on_entry() {
sumobot.move_forward(RAMMING_SPEED);
}

AttackState::handle_event(ProximityEvent const & e) {
if (e.front != detected) {
state_machine.transition_to_state(hide_state);
}
}

AmbushState and AttackState only handle ProximityEvent, so EdgeEvents are ignored.

Check out this blog about embedded software engineering services for answers to common FAQs.

Concluding with Domain-Driven Design Fundamentals

When the conversation surrounds domain-driven design fundamentals, having a clear, effective, common language that is understood by all parties to describe a software system enables better communication of ideas. This domain-driven approach can reduce misunderstandings between customers and developers. Developing a software framework that closely resembles the common language allows for easier-to-understand implementations of the desires described by the common language. This results in readable, maintainable and testable code. Domain-driven design provides a template for developing a common language and software framework that is applicable not only to enterprise software but embedded systems as well.

At Cardinal Peak, we are experienced in leveraging a domain-driven strategy to design and develop embedded system design from end to end. In fact, we likely have expertise with your target technology and have successfully launched innovations that meet the needs of many different markets. If you need a talented, experienced product design and development team to help build your next innovation, reach out to our team!