📜 ⬆️ ⬇️

Lightweight finite state machine with a tree autogenerator for embedded projects

image

Introduction


In my practice, there have often been situations where the use of a finite state machine was the most correct solution, but it had to be abandoned due to the urgency of development, the complexity of support, or for some other reason. In this post I would like to share with you a solution I developed that allows you to easily integrate state machines into your projects with the ability to visually display the tree structure.

The essence of the decision


Immediately make a reservation: this solution is designed to integrate into projects developed in C ++. Support for other languages ​​is currently missing (if needed, their support will be added in the future).

The solution is a bundle of two steps:

  1. In the PlantUML file, a project tree is created, written in the same style, after which a generator program is called that generates a C ++ file containing a tree of connections between the vertices of the graph. Rules for describing the tree are in the project description on github . To build, you need to have the library QT 5.10 and above.
  2. Support for the state machine is added to the required class.
    • In the class to which you need to add state machine support, an object of the template class fsm_class is created. The previously mentioned class is passed as a template parameter.
    • Also, static methods are added to the class that implement the finite state machine steps.
    • In a certain class method, the function of associating the state machine tree with its core is called, after which the method that triggers the passage through the graph is called.

Formulation of the problem


Consider the use of this solution on the example of recognizing an input message received from somewhere outside.
')
The message parameters are as follows:

  1. The message comes in the form of an array of characters of indefinite length, but not more than 512 characters (including the 0-terminator).
  2. The message ends with a 0-terminator.

It is reliably known that the message consists of a sequence of lines: “command” + “parameters”, separated by a space character. The string “command” can take the values ​​“read” or “set”, and “parameters” is required only if the “set” command is entered. In this case, the expected value for the int-variable.

The solution of the problem


  1. Create an empty directory and add the fsm kernel module and generator program there .

    git submodule add git@github.com:Vadimatorik/module_fsm.git git submodule add git@github.com:Vadimatorik/plantuml_to_fsm_tree_generator.git 
  2. Create a folder user_code and in it files user_string_parsing.cpp and user_string_parsing.h , which will contain the description of the class engaged in the analysis of the incoming message.
  3. We write the preparation of our class.

     class user_string_parsing_class { public: user_string_parsing_class(); void start_parsing ( char* string ); private: ///     . char* p = nullptr; /// ,      . int data = 0; ///  . fsm_class< user_string_parsing_class > fsm; }; 
  4. Create a tree.pu file in the user_code folder, which will contain the description of the tree of our state machine
  5. We describe our state machine:

     @startuml [*] --> start state "team_search" as start { start:     start:  . } start --> fts: 0 start --> arg1: 1 start --> arg2: 2 state "team_search_fail" as fts { fts:   fts:   . fts:    fts: . } state "set_param" as arg1 { arg1:   . } state "read_param" as arg2 { arg2:   . } arg1 --> spd: 0 arg1 --> spn: 1 state "set_param_done" as spd { spd:    , spd:     . } state "set_param_fail" as spn { spn:    , spn:     . } @enduml 

    The tree has the following form:

    image
  6. Add static methods to our class that are vertices of the graph. For a shorter entry, we declare them via define. Our user_string_parsing.h file will look like this:

     #pragma once #include "fsm.h" #define HANDLER_USPC_FSM_STEP(NAME_STEP)\ static int NAME_STEP ( const fsm_step< user_string_parsing_class >* previous_step,\ user_string_parsing_class* obj ) class user_string_parsing_class { public: user_string_parsing_class(); void start_parsing ( char* string ); HANDLER_USPC_FSM_STEP( fsm_step_func_team_search ); HANDLER_USPC_FSM_STEP( fsm_step_func_team_search_fail ); HANDLER_USPC_FSM_STEP( fsm_step_func_set_param ); HANDLER_USPC_FSM_STEP( fsm_step_func_read_param ); HANDLER_USPC_FSM_STEP( fsm_step_func_set_param_done ); HANDLER_USPC_FSM_STEP( fsm_step_func_set_param_fail ); private: ///     . char* p = nullptr; /// ,      . int data = 0; ///  . fsm_class< user_string_parsing_class > fsm; }; 
  7. We implement class methods.

     ///    : ///   +    + _fsm_step /// user_string_parsing_class_ + team_search + _fsm_step extern const fsm_step< user_string_parsing_class > \ user_string_parsing_class_team_search_fsm_step; user_string_parsing_class::user_string_parsing_class() { ///         . this->fsm.relinking( &user_string_parsing_class_team_search_fsm_step, this ); } void user_string_parsing_class::start_parsing ( char* string ) { ///    ,   . this->p = string; ///   . this->fsm.start(); } 
  8. Implement the vertices of the graph:

     #define FSM_F_USPC \ const fsm_step< user_string_parsing_class >* \ previous_step, user_string_parsing_class* obj int user_string_parsing_class::fsm_step_func_team_search ( FSM_F_USPC ) { ( void )previous_step; if ( strncmp( obj->p, "set", sizeof( "set" ) - 1 ) == 0 ) { obj->p += sizeof( "set" ); return 1; } if ( strcmp( obj->p, "read" ) == 0 ) { return 2; } return 0; } int user_string_parsing_class::fsm_step_func_team_search_fail ( FSM_F_USPC ) { ( void )previous_step; ( void )obj; cout << "Command search fail! Available commands: set, read." << endl << endl; return 0; } int user_string_parsing_class::fsm_step_func_set_param ( FSM_F_USPC ) { ( void )previous_step; int r; r = sscanf( obj->p, "%d", &obj->data ); return ( r == 1 ) ? 0 : 1; } int user_string_parsing_class::fsm_step_func_read_param ( FSM_F_USPC ) { ( void )previous_step; cout << "Data = " << obj->data << endl; return 0; } int user_string_parsing_class::fsm_step_func_set_param_done ( FSM_F_USPC ) { ( void )previous_step; ( void )obj; cout << "The parameter done set!" << endl << endl; return 0; } int user_string_parsing_class::fsm_step_func_set_param_fail ( FSM_F_USPC ) { ( void )previous_step; ( void )obj; cout << "The parameter must be int!" << endl << endl; return 0; } 
  9. Create an object of our class in the main program (the main.cpp file) and skip over several tests:

     #include <fstream> #include <stdint.h> #include <string.h> #include "user_string_parsing.h" using namespace std; ///      . char buf[512]; ///     . void get_data ( char* b, int n ) { switch( n ) { case 0: memcpy( b, "fsdfhsd", sizeof("fsdfhsd") ); break; case 1: memcpy( b, "read", sizeof("read") ); break; case 2: memcpy( b, "set", sizeof("set") ); break; case 3: memcpy( b, "set sfdf", sizeof("set sfdf") ); break; case 4: memcpy( b, "set 21", sizeof("set 21") ); break; case 5: memcpy( b, "read", sizeof("read") ); break; } } int main ( void ) { user_string_parsing_class usp; for ( int l = 0; l < 6; l++ ) { get_data( buf, l ); usp.start_parsing( buf ); } return 0; } 
  10. Let's write a makefile that will have separate goals for building the tree generator program, as well as our program using the previously compiled generator.

     CPP := g++ CPP_FLAGS := -O0 -g3 -Werror -Wall -Wextra -std=c++1z LDFLAGS := -O0 -g3 -Werror -Wall -Wextra PU = plantuml_to_fsm_tree_generator/build/plantuml_to_fsm_tree_generator #       user_code. USER_CPP_FILE := $(shell find user_code/ -maxdepth 5 -type f -name "*.cpp" ) USER_DIR := $(shell find user_code/ -maxdepth 5 -type d -name "*" ) USER_PATH := $(addprefix -I, $(USER_DIR)) USER_OBJ_FILE := $(addprefix build/obj/, $(USER_CPP_FILE)) USER_OBJ_FILE := $(patsubst %.cpp, %.o, $(USER_OBJ_FILE)) PROJECT_PATH += $(USER_PATH) PROJECT_OBJ_FILE += $(USER_OBJ_FILE) FSM_PU_FILE = $(shell find user_code/ -maxdepth 5 -type f -name "*.pu" ) FSM_CPP_FILE += $(patsubst %.pu, %.cpp, $(FSM_PU_FILE)) FSM_OBJ_FILE += $(patsubst %.pu, build/obj/%.o, $(FSM_PU_FILE)) PROJECT_OBJ_FILE += $(FSM_OBJ_FILE) include module_fsm/makefile %.cpp: %.pu @echo [PU] $< @$(PU) $< $@ user_string_parsing_class user_string_parsing.h build/obj/%.o: %.cpp @echo [CPP] $< @mkdir -p $(dir $@) @$(CPP) $(CPP_FLAGS) \ $(PROJECT_PATH) \ -c $< -o $@ all: $(PROJECT_OBJ_FILE) @$(CPP) $(PROJECT_OBJ_FILE) -o build/example @size --format=Berkeley build/example clean: @rm -R build/ @echo Clean all! pfsm_build: mkdir -p plantuml_to_fsm_tree_generator/build cd plantuml_to_fsm_tree_generator/build && qmake -qt=qt5 .. && make pfsm_clean: cd plantuml_to_fsm_tree_generator/ && rm -R build pfsm_rebuild: cd plantuml_to_fsm_tree_generator/ && rm -R build mkdir plantuml_to_fsm_tree_generator/build cd plantuml_to_fsm_tree_generator/build && qmake -qt=qt5 .. && make 
  11. Let's compile our project. First, the generator program, and then the project.

     make pfsm_build make all 

The example described in the article can be downloaded and tried to build from here .
For all inaccuracies, typos, errors, suggestions, please write in personal messages.

Source: https://habr.com/ru/post/350880/


All Articles