#状态机 #DSL #状态转换

无需std smlang

一个无需std的状态机语言DSL

20个版本

0.8.0 2024年8月9日
0.7.0 2024年7月3日
0.6.0 2022年11月2日
0.5.1 2022年2月5日
0.1.2 2019年5月6日

嵌入式开发 中排名第 30

Download history 2210/week @ 2024-05-01 2455/week @ 2024-05-08 2539/week @ 2024-05-15 2371/week @ 2024-05-22 2421/week @ 2024-05-29 1851/week @ 2024-06-05 2573/week @ 2024-06-12 2245/week @ 2024-06-19 2501/week @ 2024-06-26 2692/week @ 2024-07-03 2714/week @ 2024-07-10 2629/week @ 2024-07-17 2183/week @ 2024-07-24 2630/week @ 2024-07-31 2587/week @ 2024-08-07 2722/week @ 2024-08-14

每月下载量 10,595
8 个包中使用(直接使用7个)8

MIT/Apache

59KB
200 代码行

smlang:Rust中一个no_std状态机语言DSL

Build Status Documentation

基于Boost-SML语法的状态机语言DSL。

smlang是一个过程宏库,创建了一个状态机语言DSL,其目的是简化状态机的使用,因为状态机很快就会变得过于复杂,难以编写和概述。

该库支持异步和非异步代码。

转换DSL

以下是DSL的示例。有关statemachine宏的完整说明,请参阅DSL文档

statemachine!{
    transitions: {
        *SrcState1 + Event1 [ guard1 ] / action1 = DstState2, // * denotes starting state
        SrcState2 + Event2 [ guard2 ] / action2 = DstState1,
    }
    // ...
}

其中guardaction是可选的,可以省略。一个guard是一个函数,当状态转换应该发生时返回Ok(true),否则转换不应该发生。在状态机转换期间运行action函数。

这意味着任何状态机都必须写成一系列转换。

DSL支持通配符和类似于Rust模式匹配的输入状态模式匹配

statemachine!{
    transitions: {
        *State1 | State3 + ToState2 = State2,
        State1 | State2 + ToState3 = State3,
        _ + ToState4 = State4,
        State4 + ToState1 = State1,
    }
    // ...
}

这相当于

statemachine!{
    transitions: {
        *State1 + ToState2 = State2,
        State3 + ToState2 = State2,

        State1 + ToState3 = State3,
        State2 + ToState3 = State3,

        State1 + ToState4 = State4,
        State2 + ToState4 = State4,
        State3 + ToState4 = State4,
        State4 + ToState4 = State4,

        State4 + ToState1 = State1,
    }
    // ...
}

请参阅examples/input_state_pattern_match.rs中的使用示例。

内部转换

DSL支持内部转换。内部转换允许接收事件并处理操作,然后保持在当前状态。可以明确指定内部转换,例如。

State2 + Event2 / event2_action = State2,

或者

State2 + Event2 / event2_action = _,

或者隐式地,通过省略目标状态包括'='。

State2 + Event2 / event2_action,

还可以定义通配符隐式(或使用'_'显式)内部转换。

statemachine! {
    transitions: {
        *State1 + Event2 = State2,
        State1 + Event3 = State3,
        State1 + Event4 = State4,
        
        _ + Event2 / event2_action,
    },
}

上面的示例演示了如何使Event2可接受任何状态,这些状态未由之前的任何转换覆盖,并执行处理它的操作。

这相当于

statemachine! {
    transitions: {
        *State1 + Event2 = State2,
        State1 + Event3 = State3,
        State1 + Event4 = State4,
        
        State2 + Event2 / event2_action = State2,
        State3 + Event2 / event2_action = State3,
        State4 + Event2 / event2_action = State4,
    },
}

参见以下测试:test_internal_transition_with_data()test_wildcard_states_and_internal_transitions() 以查看用法示例。

守卫表达式

方括号 [] 中的守卫表达式可以定义多个守卫函数的布尔表达式。例如

statemachine! {
  transitions: {
      *Init + Login(Entry) [valid_entry] / attempt = LoggedIn,
      Init + Login(Entry) [!valid_entry && !too_many_attempts] / attempt = Init,
      Init + Login(Entry) [!valid_entry && too_many_attempts] / attempt = LoginDenied,
      LoggedIn + Logout / reset = Init,
  }
}

守卫表达式可以由守卫函数名称以及它们与 &&、|| 和 ! 操作的组合组成。

相同状态和触发事件的多个守卫转换

支持对同一状态和触发事件的多个守卫转换(见上面示例)。在这种情况下,假设只有一个守卫被启用,以避免在哪个转换应该被选择上产生冲突。然而,如果有冲突并且多个守卫都被启用,则将选择在状态机定义中出现的第一个启用的转换。

状态机上下文

状态机需要一个上下文来定义。由 StateMachineContext 实现,它是由 statemachine! proc-macro 生成的,实现了守卫和动作,以及状态机中所有状态都可以访问的数据,并且这些数据在状态转换之间持续存在。

statemachine!{
    transitions: {
        State1 + Event1 = State2,
    }
    // ...
}

pub struct Context;

impl StateMachineContext for Context {}

fn main() {
    let mut sm = StateMachine::new(Context);

    // ...
}

参见示例 examples/context.rs 以查看用法示例。

状态数据

任何状态都可以关联一些数据。

pub struct MyStateData(pub u32);

statemachine!{
    transitions: {
        State1(MyStateData) + Event1 = State2,
    }
    // ...
}

参见示例 examples/state_with_data.rs 以查看用法示例。

如果起始状态包含数据,则在创建新机器时必须提供这些数据。

pub struct MyStateData(pub u32);

statemachine!{
    transitions: {
        State2 + Event2 / action = State1(MyStateData),
        *State1(MyStateData) + Event1 = State2,
        // ...
    }
    // ...
}

// ...

let mut sm = StateMachine::new(Context, MyStateData(42));

状态数据还可以关联生命周期,由 statemachine! 宏捕获并添加到 States 枚举和 StateMachine 结构中。这意味着以下也将工作

pub struct MyStateData<'a>(&'a u32);

statemachine! {
    transitions: {
        *State1 + Event1 / action = State2,
        State2(MyStateData<'a>) + Event2 = State1,
        // ...
    }
    // ...
}

参见示例 examples/state_with_reference_data.rs 以查看用法示例。

事件数据

可以将数据与事件一起传递到 guardaction 中。

pub struct MyEventData(pub u32);

statemachine!{
    transitions: {
        State1 + Event1(MyEventData) [guard] = State2,
    }
    // ...
}

事件数据还可以关联生命周期,由 statemachine! 宏捕获并添加到 Events 枚举中。这意味着以下也将工作

pub struct MyEventData<'a>(pub &'a u32);

statemachine!{
    transitions: {
        State1 + Event1(MyEventData<'a>) [guard1] = State2,
        State1 + Event2(&'a [u8]) [guard2] = State3,
    }
    // ...
}

参见示例 examples/event_with_data.rs 以查看用法示例。

守卫和动作语法

参见示例 examples/guard_action_syntax.rs 以查看用法示例。

异步守卫和动作

守卫和动作都可以可选地使用 async

use smlang::{async_trait, statemachine};

statemachine! {
    transitions: {
        *State1 + Event1 [guard1] / async action1 = State2,
        State2 + Event2 [async guard2] / action2 = State3,
    }
}


pub struct Context {
    // ...
}

impl StateMachineContext for Context {
    async fn action1(&mut self) -> () {
        // ...
    }

    async fn guard2(&mut self) -> Result<(), ()> {
        // ...
    }

    fn guard1(&mut self) -> Result<(), ()> {
        // ...
    }

    fn action2(&mut self) -> () {
        // ...
    }
}

参见示例 examples/async.rs 以查看用法示例。

状态机示例

以下是将 UML 转换为状态机语言 DSL 的状态机的一些示例。每个示例的运行版本都可在 examples 文件夹中找到。这些 .png 图像是通过 graphviz 功能生成的。

线性状态机

alt text

DSL 实现

statemachine!{
    transitions: {
        *State1 + Event1 = State2,
        State2 + Event2 = State3,
    }
}

此示例可在 ex1.rs 中找到。

循环状态机

alt text

DSL 实现

statemachine!{
    transitions: {
        *State1 + Event1 = State2,
        State2 + Event2 = State3,
        State3 + Event3 = State2,
    }
}

此示例可在 ex2.rs 中找到。

使用守卫和动作

alt text

DSL 实现

statemachine!{
    transitions: {
        *State1 + Event1 [guard] / action = State2,
    }
}

此示例可在 ex3.rs 中找到。

在转换中使用入口和退出函数

状态机将为所有状态创建一个 on_entry_on_exit_ 函数。如果未使用,它们将被编译器优化掉。示例可以在 on_entry_on_exit_generic 中找到。

转换回调

状态机将为每个转换调用一个转换回调。此函数将旧状态和新状态作为参数。示例可以在 dominos 中找到。

辅助函数

自动推导状态和事件的某些特性

derive_eventsderive_states 字段设置为特质数组,分别为 EventsStates 枚举添加推导表达式。要推导 Display,请使用 derive_more::Display

use core::Debug;
use derive_more::Display;
// ...
statemachine!{
    derive_states: [Debug, Display],
    derive_events: [Debug, Display],
    transitions: {
        *State1 + Event1 = State2,
    }
}

// ...

println!("Current state: {}", sm.state().unwrap());
println!("Expected state: {}", States::State1);
println!("Sending event: {}", Events::Event1);

// ...

用于记录事件、守卫、动作和状态转换的钩子

StateMachineContext 特质定义(并提供默认、无操作实现)了对于每个事件、守卫、动作和状态转换调用的函数。您可以提供自己的实现,以连接到您首选的日志机制。

fn log_process_event(&self, current_state: &States, event: &Events) {}
fn log_guard(&self, guard: &'static str, result: &Result<(), ()>) {}
fn log_action(&self, action: &'static str) {}
fn log_state_change(&self, new_state: &States) {}

请参阅 examples/state_machine_logger.rs,了解使用 derive_statesderive_events 推导 Debug 实现以轻松记录的示例。

贡献者

按字母顺序排列的贡献者列表


许可

许可如下

任选其一。

依赖关系

~1.5MB
~36K SLoC