korken89 / smlang-rs

A State Machine Language DSL procedual macro for Rust
Apache License 2.0
202 stars 28 forks source link

Allow actions to return events. #17

Closed thejpster closed 2 years ago

thejpster commented 3 years ago

Brief example: consider radio that can be in the "RX", or "OFF" states. An "rx_packet" event in the "RX" state will cause the packet to be parsed (in the action). An "rx_packet" event in the "OFF" state should be ignored.

The received packet may be some command for the system, e.g. a "turn off now". I think that the easiest way to handle this is for the action to generate another event (EV_TURN_OFF), to be processed immediately.

This model can be described as as "feed the FSM an event and shake it until it stops rattling"; i.e. you run the FSM in a loop, first on the external event, and subsequently on the events generated by each action, until there is no new event generated. The events that can be generated by an action are finite, and known at compile time.

dzimmanck commented 2 years ago

@thejpster, the problem I see with this is that while each action can have a finite number of events, if two states have actions that generate events into each other, you have an endless loop. This is not in an of itself a problem. In fact, many state machines are intentionally endless loops, but I don't think it makes sense to allow such behavior in a single process_event() call. For your application, I think it makes more sense to have a process_event_task() in an asynchrnous framework such as RTIC. If your action returns an event, then you can simply re-spawn the process_event_task().

thejpster commented 2 years ago

I don't see how that solves the infinite loop problem - it just adds a yield point.

The answer, I think, is to design your FSM properly so it can't enter a (tight) infinite loop. Most intended loops will involve an external timeout event.

dzimmanck commented 2 years ago

@thejpster, just looked more at the macro and action syntax and think I understand your problem a little better. The existing API currently has no provision for generating an event from a event processing step, so an aync framework wrapped around wouldn't help either.

I think a work-around would be to embed an Option<Events> in each state that needs to be able to generate an event on entrance. After you process an event, you can then check to see if there is an event that needs to be processed in the state data. You could do this with a simple while let loop like this (pseudo-code):

while let event = Some(event){
    let state = sm.process_event(event).unwrap();
    let event = match state {
        State1(data) => data.event,
        State2(data) => data.event,
        _ => None,
    };
dzimmanck commented 2 years ago

@thejpster, I created an example that I call dominos.rs. I had to add some minor tweaks to the procedural macro to get it to work properly, but let me know if such a API style would work for your use case and I can work on a cleaner pull request.

//! An example of generating events with actions

#![deny(missing_docs)]

use smlang::statemachine;

/// We embed an optional event in the state data, allowing event propagation
#[derive(PartialEq, Copy, Clone, Debug)]
pub struct Data(Option<Events>);

statemachine! {
    transitions: {
        *D0 +  ToD1 / to_d2  = D1,
        D1(Data) +  ToD2 / to_d3  = D2,
        D2(Data) +  ToD3 / to_d4  = D3,
        D3(Data) +  ToD4 / to_d5  = D4,
        D4(Data) +  ToD5  = D5,
    }
}

/// Context
pub struct Context;

impl StateMachineContext for Context {
    fn to_d2(&mut self) -> Data {
        Data(Some(Events::ToD2))
    }

    fn to_d3(&mut self, _state_data: &Data) -> Data {
        Data(Some(Events::ToD3))
    }

    fn to_d4(&mut self, _state_data: &Data) -> Data {
        Data(Some(Events::ToD4))
    }

    fn to_d5(&mut self, _state_data: &Data) -> Data {
        Data(Some(Events::ToD5))
    }
}

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

    /// first event starts the dominos
    let mut event = Some(Events::ToD1);

    /// use a while let loop to let the events propagate and the dominos fall
    while let Some(e) = event {
        println!("We are in state --> {:?}", sm.state());
        let state = sm.process_event(e).unwrap();
        event = match state {
            States::D1(data) => data.0,
            States::D2(data) => data.0,
            States::D3(data) => data.0,
            States::D4(data) => data.0,
            _ => None,
        };
    }
}
dzimmanck commented 2 years ago

My example code works if you simply add a derive Clone/Copy to Events in the procedural macro, but unfortunately than requires any event data that anyone uses to also implement these traits, which may be an issue. It definitely breaks a lot of examples.

I think I could get it to work with references to events, but that would require State data to support lifetimes just like Event data, which is being tracked as a different issue (see issue #26 ).

dzimmanck commented 2 years ago

@thejpster,

I was able to get my example to work with no changes to the source code by manually implementing the Copy/Clone traits on events after the macro. I created pull request #27 which adds a working example called dominos.rs.

dzimmanck commented 2 years ago

@thejpster, I am going to close this issue because I believe the example I provided in dominos.rs (PR #27) illustrates how you can do this. If there is something about this method that does not work for your use case or if you think a dedicated API for such propagation is warranted, please open a new issue.