#状态机 #有限状态机 #硬件状态机 #状态图 #async-std #嵌入式

无std statig

用于设计事件驱动系统的分层状态机

6个版本

0.3.0 2023年3月25日
0.2.0 2023年1月5日
0.2.0-beta.02022年12月7日
0.1.0 2022年11月9日

#662算法

Download history 47/week @ 2024-03-14 83/week @ 2024-03-21 60/week @ 2024-03-28 102/week @ 2024-04-04 114/week @ 2024-04-11 154/week @ 2024-04-18 37/week @ 2024-04-25 22/week @ 2024-05-02 26/week @ 2024-05-09 21/week @ 2024-05-16 57/week @ 2024-05-23 74/week @ 2024-05-30 30/week @ 2024-06-06 205/week @ 2024-06-13 78/week @ 2024-06-20 65/week @ 2024-06-27

391 每月下载量

MIT 许可证

120KB
2K SLoC

statig

Current crates.io version Documentation Rust version CI

用于设计事件驱动系统的分层状态机。

特性

  • 分层状态机
  • 状态本地存储
  • 兼容#![no_std],状态机定义在只读存储器中,不进行堆内存分配。
  • (可选) 用于减少模板代码的宏。
  • 支持泛型。
  • 支持异步动作和处理程序(仅在std上)。

概览


Statig在行动

一个简单的闪烁状态机

┌─────────────────────────┐                   
│         Blinking        │◀─────────┐        
│    ┌───────────────┐    │          │        
│ ┌─▶│     LedOn     │──┐ │  ┌───────────────┐
│ │  └───────────────┘  │ │  │  NotBlinking  │
│ │  ┌───────────────┐  │ │  └───────────────┘
│ └──│     LedOff    │◀─┘ │          ▲        
│    └───────────────┘    │──────────┘        
└─────────────────────────┘                   
#[derive(Default)]
pub struct Blinky;

pub enum Event {
    TimerElapsed,
    ButtonPressed
}

#[state_machine(initial = "State::led_on()")]
impl Blinky {
    #[state(superstate = "blinking")]
    fn led_on(event: &Event) -> Response<State> {
        match event {
            Event::TimerElapsed => Transition(State::led_off()),
            _ => Super
        }
    }

    #[state(superstate = "blinking")]
    fn led_off(event: &Event) -> Response<State> {
        match event {
            Event::TimerElapsed => Transition(State::led_on()),
            _ => Super
        }
    }

    #[superstate]
    fn blinking(event: &Event) -> Response<State> {
        match event {
            Event::ButtonPressed => Transition(State::not_blinking()),
            _ => Super
        }
    }

    #[state]
    fn not_blinking(event: &Event) -> Response<State> {
        match event {
            Event::ButtonPressed => Transition(State::led_on()),
            _ => Super
        }
    }
}

fn main() {
    let mut state_machine = Blinky::default().state_machine();

    state_machine.handle(&Event::TimerElapsed);
    state_machine.handle(&Event::ButtonPressed);
}

(查看带有注释的完整代码的macro/blinky示例。或者查看no_macro/blinky以查看不使用宏的版本。


概念

状态

状态通过在impl块中编写方法,并添加#[state]属性到它们来定义。当将事件提交给状态机时,将调用与当前状态关联的方法来处理它。默认情况下,该事件映射到方法的event参数。

#[state]
fn led_on(event: &Event) -> Response<State> {
    Transition(State::led_off())
}

每个状态都必须返回一个Response。一个Response可以是以下三者之一

  • Handled:已处理事件。
  • Transition:转换到另一个状态。
  • Super:将事件延迟到父超级状态。

超级状态

超级状态允许您创建状态层次结构。状态可以通过返回 Super 响应来将事件延迟到其超级状态。

#[state(superstate = "blinking")]
fn led_on(event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => Transition(State::led_off()),
        Event::ButtonPressed => Super
    }
}

#[superstate]
fn blinking(event: &Event) -> Response<State> {
    match event {
        Event::ButtonPressed => Transition(State::not_blinking()),
        _ => Super
    }
}

超级状态本身也可以有超级状态。

动作

在转换过程中进入或离开状态时运行操作。

#[state(entry_action = "enter_led_on", exit_action = "exit_led_on")]
fn led_on(event: &Event) -> Response<State> {
    Transition(State::led_off())
}

#[action]
fn enter_led_on() {
    println!("Entered on");
}

#[action]
fn exit_led_on() {
    println!("Exited on");
}

共享存储

如果您的状态机实现的类型有任何字段,您可以在所有状态、超级状态或操作中访问它们。

#[state]
fn led_on(&mut self, event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => {
            self.led = false;
            Transition(State::led_off())
        }
        _ => Super
    }
}

或者,您还可以在进入操作中设置 led

#[action]
fn enter_led_off(&mut self) {
    self.led = false;
}

状态本地存储

有时您有仅在特定状态中存在的数据。您可以将此数据添加为状态处理器的输入,而不是将其添加到共享存储中,并可能需要解包 Option<T>

#[state]
fn led_on(counter: &mut u32, event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => {
            *counter -= 1;
            if *counter == 0 {
                Transition(State::led_off())
            } else {
                Handled
            }
        }
        Event::ButtonPressed => Transition(State::led_on(10))
    }
}

counter 仅在 led_on 状态中可用,但也可以在其超级状态和操作中访问。

上下文

当状态机在更大的系统中使用时,有时需要传递外部可变上下文。默认情况下,此上下文映射到方法的方法参数 context

#[state]
fn led_on(context: &mut Context, event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => {
            context.do_something();
            Handled
        }
        _ => Super
    }
}

然后您将需要使用 handle_with_context 方法向状态机提交事件。

state_machine.handle_with_context(&Event::TimerElapsed, &mut context);

检查

为了日志记录目的,您可以在状态机执行过程中特定点调用两个回调。

  • on_dispatch 在将事件分发到特定状态或超级状态之前调用。
  • on_transition 在转换发生后调用。
#[state_machine(
    initial = "State::on()",
    on_dispatch = "Self::on_dispatch",
    on_transition = "Self::on_transition",
    state(derive(Debug)),
    superstate(derive(Debug))
)]
impl Blinky {
    ...
}

impl Blinky {
    fn on_transition(&mut self, source: &State, target: &State) {
        println!("transitioned from `{:?}` to `{:?}`", source, target);
    }

    fn on_dispatch(&mut self, state: StateOrSuperstate<Blinky>, event: &Event) {
        println!("dispatched `{:?}` to `{:?}`", event, state);
    }
}

异步

所有处理程序和操作都可以是异步的。(目前仅在 std 上可用,并需要启用 async 功能)。

#[state_machine(initial = "State::led_on()")]
impl Blinky {
    #[state]
    async fn led_on(event: &Event) -> Response<State> {
        match event {
            Event::TimerElapsed => Transition(State::led_off()),
            _ => Super
        }
    }
}

然后,#[state_machine] 宏将自动检测正在使用异步函数,并生成异步状态机。

async fn main() {
    let mut state_machine = Blinky::default().state_machine();

    state_machine.handle(&Event::TimerElapsed).await;
    state_machine.handle(&Event::ButtonPressed).await;
}

实现

大多数实现细节都由 #[state_machine] 宏处理,但了解幕后发生的事情总是很有价值。此外,您会发现生成的代码实际上非常简单,可以很容易地手动编写,所以如果您更喜欢避免使用宏,这是完全可以实现的。

statig 的目标是表示层次状态机。概念上,层次状态机可以被视为树。

                          ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐             
                                    Top                       
                          └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘             
                                     │                        
                        ┌────────────┴────────────┐           
                        │                         │           
             ┌─────────────────────┐   ╔═════════════════════╗
             │      Blinking       │   ║     NotBlinking     ║
             │─────────────────────│   ╚═════════════════════╝
             │ counter: &'a usize  │                          
             └─────────────────────┘                          
                        │                                     
           ┌────────────┴────────────┐                        
           │                         │                        
╔═════════════════════╗   ╔═════════════════════╗             
║        LedOn        ║   ║        LedOff       ║             
║─────────────────────║   ║─────────────────────║             
║ counter: usize      ║   ║ counter: usize      ║             
╚═════════════════════╝   ╚═════════════════════╝

树边缘的节点称为叶子状态,在 statig 中由枚举表示。如果数据仅在特定状态中存在,我们可以赋予该状态对数据的所有权。这被称为“状态本地存储”。例如,counter 仅存在于 LedOnLedOff 状态中。

enum State {
    LedOn { counter: usize },
    LedOff { counter: usize },
    NotBlinking
}

Blinking 之类的状态称为超级状态。它们定义了子状态的共享行为。超级状态也由枚举表示,但它们不拥有自己的数据,而是从底层状态借用。

enum Superstate<'sub> {
    Blinking { counter: &'sub usize }
}

状态与其处理程序之间的关联通过 StateSuperstate 特性中的 call_handler() 方法表达。

impl statig::State<Blinky> for State {
    fn call_handler(&mut self, blinky: &mut Blinky, event: &Event) -> Response<Self> {
        match self {
            State::LedOn { counter } => blinky.led_on(counter, event),
            State::LedOff { counter } => blinky.led_off(counter, event),
            State::NotBlinking => blinky.not_blinking(event)
        }
    }
}

impl statig::Superstate<Blinky> for Superstate {
    fn call_handler(&mut self, blinky: &mut Blinky, event: &Event) -> Response<Self> {
        match self {
            Superstate::Blinking { counter } => blinky.blinking(counter, event),
        }
    }
}

状态与其操作之间的关联以类似的方式表达。

impl statig::State<Blinky> for State {
    
    ...

    fn call_entry_action(&mut self, blinky: &mut Blinky) {
        match self {
            State::LedOn { counter } => blinky.enter_led_on(counter),
            State::LedOff { counter } => blinky.enter_led_off(counter),
            State::NotBlinking => blinky.enter_not_blinking()
        }
    }

    fn call_exit_action(&mut self, blinky: &mut Blinky) {
        match self {
            State::LedOn { counter } => blinky.exit_led_on(counter),
            State::LedOff { counter } => blinky.exit_led_off(counter),
            State::NotBlinking => blinky.exit_not_blinking()
        }
    }
}

impl statig::Superstate<Blinky> for Superstate {

    ...

    fn call_entry_action(&mut self, blinky: &mut Blinky) {
        match self {
            Superstate::Blinking { counter } => blinky.enter_blinking(counter),
        }
    }

    fn call_exit_action(&mut self, blinky: &mut Blinky) {
        match self {
            Superstate::Blinking { counter } => blinky.exit_blinking(counter),
        }
    }
}

状态及其超级状态之间的树结构通过 StateSuperstate 特性的 superstate 方法表达。

impl statig::State<Blinky> for State {

    ...

    fn superstate(&mut self) -> Option<Superstate<'_>> {
        match self {
            State::LedOn { counter } => Some(Superstate::Blinking { counter }),
            State::LedOff { counter } => Some(Superstate::Blinking { counter }),
            State::NotBlinking => None
        }
    }
}

impl<'sub> statig::Superstate<Blinky> for Superstate<'sub> {

    ...

    fn superstate(&mut self) -> Option<Superstate<'_>> {
        match self {
            Superstate::Blinking { .. } => None
        }
    }
}

当事件到达时,statig 首先将事件分发到当前叶状态。如果该状态返回一个 Super 响应,则将其分发到该状态的父状态,父状态随后返回自己的响应。每次将事件推迟到父状态时,statig 都会在图中向上遍历,直到达到 Top 状态。这是一个隐式的父状态,会认为每个事件都已处理。

如果返回的响应是 Transition,则 statig 会通过遍历图从当前源状态到目标状态,执行转换序列,并选择最短路径。当路径从源状态向上时,经过的每个状态都会执行其 退出动作。然后,当向下移动时,经过的每个状态都会执行其 进入动作

例如,从 LedOn 状态转换到 NotBlinking 状态的转换序列如下

  1. 退出 LedOn 状态
  2. 退出 Blinking 状态
  3. 进入 NotBlinking 状态

相比之下,从 LedOn 状态转换到 LedOff 状态的转换序列如下

  1. 退出 LedOn 状态
  2. 进入 LedOff 状态

我们不会执行 Blinking 的退出或进入动作,因为这个父状态在 LedOnLedOff 状态之间是共享的。

进入和退出动作也可以访问状态本地存储,但请注意,退出动作操作的是源状态的状态本地存储,而进入动作操作的是目标状态的状态本地存储。

例如,在 LedOn 的退出动作中更改 counter 的值,对 LedOff 状态中的 counter 的值没有影响。

最后,在用于共享存储的类型上实现了 StateMachine 特性。

impl IntoStateMachine for Blinky {
    type State = State;

    type Superstate<'sub> = Superstate<'sub>;

    type Event<'evt> = Event;

    type Context<'ctx> = Context;

    const INITIAL: State = State::off(10);
}

常见问题解答

这个 #[state_machine] 宏对我的代码做了什么?😨

简短的回答:什么也没做。 #[state_machine] 只是对底层的 impl 块进行解析,并根据其内容派生一些代码并将其添加到你的源文件中。你的代码仍然存在,没有任何改变。实际上,#[state_machine] 可以是一个派生宏,但截至目前,Rust 只允许派生宏用于枚举和结构体。如果你想看看生成的代码是什么样子,请查看带有宏的测试 没有宏的测试

这与使用类型状态模式相比有什么优势?

我认为它们有不同的用途。类型状态模式(typestate pattern)对于设计API非常有用,因为它能够通过使每个状态成为一个独特的类型来在编译时强制执行操作的合法性。但statig是为了模拟一个动态系统而设计的,其中事件来自外部,操作的顺序是在运行时确定的。更具体地说,这意味着状态机将处于一个循环中,从队列中读取事件并使用handle()方法提交给状态机。如果我们想用使用类型状态模式的状态机做同样的事情,我们不得不使用枚举来封装所有不同的状态,并将事件匹配到这些状态的操作上。这意味着额外的模板代码,而几乎没有什么好处,因为操作的顺序是未知的,因此不能在编译时进行检查。另一方面,statig让我们能够创建状态层次结构,我认为这对于状态机变得复杂时非常有价值。


致谢

这个库的想法来源于阅读书籍《Practical UML Statecharts in C/C++》。如果你想要学习如何使用状态机来设计复杂系统,我强烈推荐这本书。

依赖项

~0–7.5MB
~49K SLoC