有限状态机TinyFSM使用指南

2023-10-19 13:50

本文主要是介绍有限状态机TinyFSM使用指南,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

有限状态机TinyFSM使用指南

贺志国
2022-7-14

项目网址:https://github.com/digint/tinyfsm

一、类图

                                       .......
+--------------------------------------:  T  :
| tinyfsm::FsmList                     :.....:
+-----------------------------------------|
| [+] set_initial_state()     <<static>>  |
| [+] reset()                 <<static>>  |
| [+] enter()                 <<static>>  |
| [+] start()                 <<static>>  |
| [+] dispatch(Event)         <<static>>  |
+-----------------------------------------+.......
+--------------------------------------:  T  :
| tinyfsm::Fsm                         :.....:
+-----------------------------------------|
| [+] state<S>()              <<static>>  |
| [+] set_initial_state()     <<static>>  |
| [+] reset()                 <<static>>  |
| [+] enter()                 <<static>>  |
| [+] start()                 <<static>>  |
| [+] dispatch(Event)         <<static>>  |
| [#] transit<S>()                        |
| [#] transit<S>(Action)                  |
| [#] transit<S>(Action, Condition)       |
+-----------------------------------------+#||+---------------------+| MyFSM               |+---------------------+| [+] entry()         || [+] exit()          || [+] react(EventX)   || [+] react(EventY)   || ...                 |+---------------------+#|+-------------+-------------+|             |             |+---------+   +---------+   +---------+| State_A |   | State_B |   | ...     |+---------+   +---------+   +---------+[#]  protected
[+]  public
[-]  private

二、使用说明

TinyFSM是一个基于模板撰写的头文件库, 要求编译器支持C++11标准(GCC的编译选项为-std=c++11,Ubuntu 16.04以上操作系统自带的GCC默认支持)。使用时,只需将头文件tinyfsm.hpp复制到项目合适的位置,然后使用如下语句包含该头文件即可使用。

#include <tinyfsm.hpp>

TinyFSM不依赖于RTTI(“Runtime Type Information”的缩写,表示运行时类型信息)、异常或者任何外部库。如果编译时C++标准库(即STL库)都不想使用,请在编译时添加-DTINYFSM_NOSTDLIB选项,在链接时添加-nostdlib选项。

下面以Elevator(电梯)项目为例,对使用TinyFSM实现状态机进行详细地阐述。Elevator是一个同时使用两个有限状态机的较复杂示例。

2.1 需求分析

一般而言,一部电梯拥有:

  1. “Call”(呼叫)按钮,即每层楼召唤电梯(上升或下降)的按钮;

  2. “Floor Sensor”(楼层传感器)按钮,即”1“、”2“、”3“等楼层按钮;

  3. “Alarm”(报警)按钮。

说明:当然还有“Open Door(开门)”和“Close Door(关门)”按钮,该项目未提及,大家可以尝试自行添加。

同时,一部电梯包含轿厢(Cabin)和马达(Motor)两大部件。

其中,Cabin的状态包括(后文直接使用Elevator代替Cabin):

  1. 状态: Idle(空闲)
  2. 状态: Moving(移动)
  3. 状态: Panic(恐慌,即故障状态)

Motor的状态包括:

  1. 状态: Stopped(停止)
  2. 状态: Up(上升)
  3. 状态: Down(下降)

特别强调一个良好的状态机必须避免循环依赖。以Elevator项目为例:Elevator可以向Motor发送事件,但Motor永远不会向Elevator发送事件。这样就只存在单向事件传输,否则就是循环依赖。一旦出现循环依赖,系统将变得不可控!

2.2 状态迁移图
  1. Elevator的状态迁移图如下(椭圆表示状态,箭头表示事件):
    在这里插入图片描述

  2. Motor的状态迁移图如下(椭圆表示状态,箭头表示事件):

  3. 在这里插入图片描述

2.3 代码分析
2.3.1 声明事件类

声明状态机需要监听的事件类。事件类从tinyfsm::Event类派生得到,示例代码如下:

    struct FloorEvent : tinyfsm::Event{int floor;};struct Call        : FloorEvent { };struct FloorSensor : FloorEvent { };struct Alarm       : tinyfsm::Event { };

上述代码中,我们定义了三个事件:CallFloorSensorAlarm。这三个事件类的对象将会作为实参传给State类的react()成员函数。该类中,我们定义了一个成员变量floor,用于存储CallFloorSensors事件中触发的楼层数字。

2.3.2 声明状态机类

声明状态机类。状态机类从tinyfsm::Fsm<T> 模板类派生得到,其中的T是状态机自身的类型名。

在状态机类中,需要声明如下公有函数:

  • react():为每个事件均声明一个react()函数 ;
  • entry() ;
  • exit() 。

示例代码如下:

    class Elevator: public tinyfsm::Fsm<Elevator>{public:/* default reaction for unhandled events */void react(tinyfsm::Event const &) { };virtual void react(Call        const &);virtual void react(FloorSensor const &);void         react(Alarm       const &);virtual void entry(void) { };  /* entry actions in some states */void         exit(void)  { };  /* no exit actions */};

注意:为事件声明的react()函数可以是virtual,也可以不是virtual,如果不是virtual,则不需要查找vtable(虚函数表),调用效率会更高,但会失去运行时多态的灵活性。

2.3.3 声明状态类

声明状态机的状态类。状态类均从状态机类(即2.3.2节声明的状态机类)派生得到。注意状态类是隐式实例化的。如果大家想在多个状态机中复用状态,需要将其声明为模板类(参考 examples/api/multiple_switch.cpp)。示例代码如下:

    class Panic: public Elevator{void entry() override;};class Moving: public Elevator{void react(FloorSensor const &) override;};class Idle: public Elevator{void entry() override;void react(Call const & e) override;};

该示例中,我们声明了三个状态类:PanicMovingIdle。注意在Elevator示例项目中,并未将这三个类分开声明(即放在不同的头文件中)。因为这几个状态比较简单,过分强调独立性会造成头文件满天飞。当然,如果每个状态内部比较复杂,则需要为其创建一个单独的头文件。

2.3.4 实现动作和对事件的响应

大多数情形下,事件响应函数包含下列一个或多个步骤:

  • 改变一些局部数据;
  • 给其他状态机发送事件;
  • 转移到其他不同的状态。

重要:
在事件响应函数中确保状态迁移动作函数transit<>() 在最后一行调用!

重要:
entry()/exit()动作中永远不要调用状态迁移动作函数transit<>()

示例代码如下:

    void Idle::entry() {send_event(MotorStop());}void Idle::react(Call const & e) {dest_floor = e.floor;if(dest_floor == current_floor)return;/* lambda function used for transition action */auto action = [] { if(dest_floor > current_floor)send_event(MotorUp());else if(dest_floor < current_floor)send_event(MotorDown());};transit<Moving>(action);};

该示例中,我们使用一个Lambda函数作为状态迁移动作函数。transit<>()函数执行如下动作:

  1. 调用当前状态的exit()函数;
  2. 如果提供了状态迁移动作函数(如本例中的Lambda函数),调用该函数;
  3. 改变当前状态到新状态;
  4. 调用新状态的entry()函数。

注意,可以将条件函数传递给transit<>()函数。

2.3.5 定义初始状态

一般情况下,使用宏FSM_INITIAL_STATE(fsm, state) 来定义状态机中的初始状态(或者说是“起始状态”) 。示例代码如下:

    FSM_INITIAL_STATE(Elevator, Idle)

上述宏将状态机Elevator的当前状态设置为Idle。实际上,在该宏内部为Fsm<Elevator>::set_initial_state()定义了一个模板特化,并设置当前状态为Idle。宏FSM_INITIAL_STATE(fsm, state)的定义如下所示:

#define FSM_INITIAL_STATE(_FSM, _STATE)                               \
namespace tinyfsm {                                                   \template<> void Fsm< _FSM >::set_initial_state(void) {              \current_state_ptr = &_state_instance< _STATE >::value;            \}                                                                   \
}
2.3.6 自定义初始化

特定情况下,可能需要自定义初始化,可通过在状态机类中重写(override)reset() 成员函数来达成。如果需要重新初始化所有状态变量,可通过调用
tinyfsm::StateList<MyStates...>::reset()函数来实现。示例代码如下:

    class Switch : public tinyfsm::Fsm<Switch>{public: static void reset(void) {tinyfsm::StateList<Off, On>::reset();  // reset all statesmyvar = 0;...}...}

如果确定重写初始化函数,要确保当前状态已被正确设置,否则在状态机类中会出现对于空指针的解引用

2.3.7 使用FsmList分发事件

大家可能已经注意到,在Elevator项目示例中多次出现对send_event()函数的调用。注意:该函数不是TinyFSM提供的。事件分发可通过多种方式实现,TinyFSM的策略是留给大家自行实现。Elevator项目借助send_event()函数来实现直接的事件分发,没有使用事件队列。该方法的优点在于执行速度非常快,因为不需要RTTI(“Runtime Type Information”的缩写,表示运行时类型信息),调用哪个函数来分发事件在编译时就已确定。另一方面,设计状态机时要特别留意避免出现事件循环(即A事件依赖B事件,B事件经过几层转折后又依赖A事件),一旦出现事件循环,证明状态机的设计是完全错误的。
fsmlist.hpp中的事件分发函数send_event():

    typedef tinyfsm::FsmList<Motor, Elevator> fsm_list;template<typename E>void send_event(E const & event){fsm_list::template dispatch<E>(event);}

如上所示,send_event()将事件分发到list中的所有状态机对象。该方法完成不会带来调用性能的损失,因为在状态机类的声明中,默认的react()被定义为一个空函数。当然缺点是灵活性有所降低。理解这点很重要。

初始化状态转移日志如下所示:

FsmList<Motor, Elevator>::start()Motor::set_initial_state()Motor::current_state = StoppedElevator::set_initial_state()Elevator::current_state = IdleMotor::enter()Motor:Stopped->entry()cout << "Motor: stopped"               <-- HEREMotor::direction = 0Elevator::enter()Elevator:Idle->entry()send_event(MotorStop)Motor::react(MotorStop)Motor:Stopped->transit<Stopped>Motor:Stopped->exit()Motor::current_state = StoppedMotor:Stopped->entry()cout << "Motor: stopped"   <-- HEREMotor::direction = 0Elevator::react(MotorStop)

这篇关于有限状态机TinyFSM使用指南的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/240218

相关文章

Maven使用指南的笔记

文档索引 Maven in 5 Minutes 篇幅很短,快速上手,不求甚解。 执行如下命令,创建项目的基础配置。 mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1

正规式与有限自动机例题

答案:D 知识点: 正规式 正规集 举例 ab 字符串ab构成的集合 {ab} a|b 字符串a,b构成的集合 {a,b} a^* 由0或者多个a构成的字符串集合 {空,a,aa,aaa,aaaa····} (a|b)^* 所有字符a和b构成的串的集合 {空,a,b,ab,aab,aba,aaab····} a(a|b)^* 以a为首字符的a,b字符串的集

STM32F4按键状态机--单击、双击、长按

STM32F4按键状态机--单击、双击、长按 一、状态机的三要素二、使用状态机原因2.1资源占用方面2.2 执行效率方面:2.3 按键抖动方面: 三、状态机实现3.1 状态机分析3.1 程序实现 百度解析的状态机概念如下 状态机由状态寄存器和组合逻辑电路构成,能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作、完成特定操作的控制中心。有限状态机简写为FSM(

OWASP ZAP2.4.3使用指南(中文版)

OWASP ZAP是一款开源的web安全工具,它简单易用,与burp suite相似,主要功能包含了:代理、数据拦截修改、主动扫描、被动扫描、主动攻击、爬虫、fuzzing、渗透测试等。在国外安全圈和渗透测试领域应用非常广泛,在youtube上有许多关于ZAP的视频资料。与burp suite相比,前者是一款商业渗透测试工具,部分功能不能使用,国内的大部分使用者都使用的破解版,而ZAP是开源免费的

Circuitjs 在线电路模拟器使用指南

Circuitjs 是一款 web 在线电路模拟器, 可以在浏览器上方便地模拟各种模拟或数字的电路, 用户无需安装各种软件, 生成的电路也支持在线分享给其它用户. 网址是 https://cc.xiaogd.net/. 当前版本为 v2.9.0 cc 为 circuit 的简写, 也即是电路的意思. 版本说明 在模拟器右侧栏增加了版本的说明. 另外, 在 菜单--关于 的弹出窗

Linux IPC 资源管理:ipcs和 ipcrm使用指南

文章目录 0. 引言1. IPC 资源概述2. 查询 IPC 资源2.1 使用 `ipcs` 查询 IPC 资源2.2 查询特定 IPC 资源2.3 查询系统 IPC 参数 3. 修改 IPC 系统参数4. 清除 IPC 资源5. 实践应用5.1 查询用户的消息队列5.2 查找未被清理的消息队列 0. 引言 进程间通信(IPC)允许不同的进程共享数据或进行同步操作。Linux

EventBus-Vue事件总线解析与使用指南

前言         在Vue.js中,组件通信是开发过程中非常场景的需求。根据不同的场景和需求,Vue提供了多种组件通信方式。比如父子组件通信、兄弟组件通信、跨代组件通信等。当应用程序中两个组件或者说页面之间没有引入和被引入的关系的时,或者说他们之间嵌套的结果复杂的时候,我们可以考虑如何传递数据呢?         其中适用的通信方式有 Vuex、provide 和 inject、Event

Vuex Module Decorators 使用指南

vuex-module-decorators 是基于 TypeScript 的 Vuex 装饰器库,它允许我们使用装饰器模式编写模块化的 Vuex 代码,使得代码更加简洁和可维护。对于 Vue.js 项目,尤其是使用 TypeScript 的项目,vuex-module-decorators 提供了一种更优雅的方式来组织 Vuex store。 在本文中,我们将介绍如何使用 vuex-modul

Android中的JNI使用指南一

一、简介 JNI全称是Java Native Interface(Java本地接口)单词首字母的缩写,本地接口就是指用C和C++开发的接口。由于JNI是JVM规范中的一部份,因此可以将我们写的JNI程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码。    开发JNI程序会受到系统环境的限制,因为用C/C++语言写出来的代码或模块,编译过

apicloud+Vue.js使用指南(源码+设计思路)

源码: <html><head><!-- 头部代码省略 --></head><body><div id="app"><div class="title"><div :style="{'height':(top+'px')}"></div><div @click="look">{{authorInfo}}</div></div></div></body><script type="t