Frontend Code for Robotic System with State Management - Part 2

June 19, 2023

In the previous article, I wrote on the importance of state management while defining two different types of events which trigger state transitions.

Now, this article delves into existing patterns (library) and their characteristics: Boost MSM (opens in a new tab) (c++), Behavior tree (opens in a new tab) (c++), and Redux (opens in a new tab) (JS).

Boost MSM

MSM is a robust state machine library in C++ that provides a structured approach to state management, adhering to the UML standard. It offers several advantages for developers:

1. Strength

Structure for UML standard

Boost MSM allows developers to model state transitions and behaviors according to the Unified Modeling Language (UML) standard. This enables clear and concise representation of states, events, and actions.

Flexibility and extensibility

Boost MSM offers a high degree of flexibility, allowing developers to customize state machines to suit their specific needs. It provides various hooks and callbacks that enable fine-grained control over state transitions and actions. Support for complex state machines: Boost MSM excels in handling complex state machines with multiple states, nested states, and hierarchical structures. It provides mechanisms for defining substate machines, composite states, and orthogonal regions, enabling the representation of intricate system behaviors.

Thread safety

Boost MSM provides built-in thread safety mechanisms, making it suitable for concurrent or multi-threaded applications. It allows developers to define thread-safe state machines and handle synchronization and communication between threads effectively.

Integration with other Boost libraries

Boost MSM seamlessly integrates with other Boost libraries, leveraging their functionalities and enhancing the overall development experience. This integration allows developers to combine the power of Boost MSM with other tools and utilities available in the Boost ecosystem.

2. Weakness

Difficulty in obtaining the active state

One drawback of Boost MSM lies in its design, as there is no clean way to retrieve the currently active state. Let us see an official example (opens in a new tab) of boost msm (I wrote some comments for explanation):

boot msm example
 
  // events are defined outside of state machine
  struct play {};
  struct end_pause {};
  struct stop {};
  struct pause {};
  struct open_close {};
 
  ...
 
  // front-end: define the FSM structure
  struct player_ : public msm::front::state_machine_def<player_>
  {
      ...
 
      // The list of FSM states: these are define inside of state machine !!
      struct Empty : public msm::front::state<>
      {
        ...
      };
      struct Open : public msm::front::state<>
      {
        ...
      };
 

Then, what is the recommended way to identify the currently active state? First of all, as per Active state section of this document (opens in a new tab):

The backend also offers a way to know which state is active, though you will normally only need this for debugging purposes. If what you need simply is doing something with the active state, internal transitions or visitors are a better alternative. If you need to know what state is active, const int* current_state() will return an array of state ids. Please refer to the internals section to know how state ids are generated.

Thus, for example, we might have to manually identify the id of states by this kind (opens in a new tab) of method.

This becomes problematic when we are handling MonitorEvent (opens in a new tab).

Assume that we want do the following:

MyMachine msm_machine_;
while(true){
  auto e = DetectSomeEvent()
  msm_machine_.process_event(e)
  ...
}

When writing DetectSomeEvent(), we might want to detect an event based on current state, because detecting an event can be computationally demanding.

State based event computation
Event DetectSomeEvent() {
  auto current_state = msm_machine_.get_state() // this line is not possible !!
  switch (current_state) {
    case StateA:
    case StateB:
      // perform event detection code
 
  }
}

Lack of graphical flow chart support

Boost MSM does not provide native support for creating graphical flow charts. Developers may find it challenging to visualize the state transitions, potentially impeding the understanding of complex state machines. Also, there is not handy tool to create such table as below (I really want to ask how the authors are writing this kind of table even while using formatters..):

struct transition_table : mpl::vector<
      //    Start     Event         Next      Action/Guard
      //  +---------+-------------+---------+---------------------+----------------------+
    a_row2 < Stopped , play        , Playing , Stopped , &Stopped::start_playback         >,
    a_row2 < Stopped , open_close  , Open    , Empty   , &Empty::open_drawer              >,
      _row < Stopped , stop        , Stopped                                              >,
      //  +---------+-------------+---------+---------------------+----------------------+
    a_row2 < Open    , open_close  , Empty   , Open    , &Open::close_drawer              >,
      //  +---------+-------------+---------+---------------------+----------------------+
    ...
      //  +---------+-------------+---------+---------------------+----------------------+
        > {};

BehaviorTree.CPP

Although behavior trees are not state machines, they are often employed for flow management in robotics and game design. I want to you study the comparison between state machine vs behavior tree (opens in a new tab). As this aricle focuses on library aspect, I will skip this comparison.

Let's explore the advantages and disadvantages of using behavior trees:

1. Strength

Subtree inclusion

Behavior trees excel in modularizing complex behaviors through the concept of sub trees. This allows for the reuse and composition of smaller, manageable behavior modules.

Groot description language

Behavior trees can be described using a domain-specific language (DSL) called Groot (opens in a new tab). This language simplifies the creation and modification of behavior tree structures, enhancing readability and maintainability.

Fig. Behavior tree used in nav2

2. Weakness

Readability issues with growing trees

As behavior trees grow larger and more complex, their readability can diminish. It can become cumbersome to comprehend the entire tree structure, making it harder to identify and troubleshoot issues. In contrast, a few lines of code may be more readable than following numerous blocks and arrows.

Learning curve and terminology

Understanding behavior tree concepts and associated terminology may require some initial effort. Developers unfamiliar with behavior trees may need time to grasp the various nodes, their behaviors, and how they interact.

Redux

When it comes to state management in front-end development, Redux stands out as a popular choice. It offers a flexible and efficient approach to managing application state, providing developers with a range of advantages.

Simplicity of Reducers (strength of boost msm)

One of the notable strengths of Redux is the simplicity of its reducers (function that computes a new state from previous state and action). Reducers are pure functions responsible for handling state transitions based on dispatched actions. Typically consisting of switch statements or if-else conditions, reducers offer a straightforward and intuitive way to define state transitions. Let's consider an example (I will use the code structure in this repo (opens in a new tab)):

src/reducers/cartReducers.js
export const cartReducer = (state = { cartItems: [], shippingAddress: {} }, action) => {
  switch (action.type) {
    case CART_ADD_ITEM:
      const item = action.payload
      const existItem = state.cartItems.find(x => x.product === item.product)
 
      if (existItem) {
        return {
          ...state,
          cartItems: state.cartItems.map(x =>
              x.product === existItem.product ? item : x)
        }
 
      } else {
        return {
          ...state,
          cartItems: [...state.cartItems, item]
        }
      }
 
    case CART_REMOVE_ITEM:
      return {
        ...state,
        cartItems: state.cartItems.filter(x => x.product !== action.payload)
      }
 

This reducer is dispatched from its wrapper (opens in a new tab) as below:

export const addToCart = (id, qty) => async (dispatch, getState) => {
  const { data } = await axios.get(`/api/products/${id}`);
 
  dispatch({
    type: CART_ADD_ITEM,
    payload: {
      product: data._id,
      name: data.name,
      image: data.image,
      price: data.price,
      countInStock: data.countInStock,
      qty,
    },
  });
  localStorage.setItem("cartItems", JSON.stringify(getState().cart.cartItems));
};

Modularization through Multiple Reducers (strength of behavior tree)

Redux allows developers to modularize their state management through the use of multiple reducers. Each reducer can handle a specific portion of the global state, promoting code organization and maintainability. This approach is akin to the subtree insertion capability offered by behavior trees. Let's consider an example (opens in a new tab):

store.js
const reducer = combineReducers({
  productList: productListReducer,
  productDetails: productDetailsReducer,
  productDelete: productDeleteReducer,
  productCreate: productCreateReducer,
  productUpdate: productUpdateReducer,
  productReviewCreate: productReviewCreateReducer,
  productTopRated: productTopRatedReducer,
 
  cart: cartReducer,
  userLogin: userLoginReducer,
  userRegister: userRegisterReducer,
  userDetails: userDetailsReducer,
  userUpdateProfile: userUpdateProfileReducer,
  userList: userListReducer,
  userDelete: userDeleteReducer,
  userUpdate: userUpdateReducer,
 
  orderCreate: orderCreateReducer,
  orderDetails: orderDetailsReducer,
  orderPay: orderPayReducer,
  orderListMy: orderListMyReducer,
  orderList: orderListReducer,
  orderDeliver: orderDeliverReducer,
});

Global State Accessibility (overcome weakness of msm)

Another strength of Redux lies in its global state accessibility. The state is defined globally within the store, making it easily accessible throughout the application.

// Timer callback
function timerCallback() {
  const currentState = store.getState().cart.status;
  if (currentState === "ready") {
    // Perform action relevant to the "ready" state
    // ...
  }
}
 
// Set up timer
setInterval(timerCallback, 1000);

Summary

The simplicity of reducers, modularization through multiple reducers, and global state accessibility empower developers to build scalable and maintainable applications, by overcoming the weakness of Boost msm and Behavior tree. By leveraging Redux's strengths, developers can streamline their state management process, enhance code readability, and efficiently handle events based on the current state. With Redux at their disposal, front-end developers can unlock the full potential of their applications while maintaining a robust and maintainable codebase.

However, it is important to note that Redux, as it stands, is not directly applicable to C++ due to the fundamental language differences between the two. Redux relies heavily on JavaScript's functional programming paradigm and its dynamic nature, which may not translate seamlessly to the statically-typed and object-oriented nature of C++.

Adapting Redux concepts to C++ requires careful consideration of the language's features and design patterns, ensuring a suitable approach is chosen for the specific needs of C++ development. This may involve building custom state management libraries or leveraging existing C++ libraries that provide similar functionality.