mdeloof / statig

Hierarchical state machines for designing event-driven systems
https://crates.io/crates/statig
MIT License
560 stars 18 forks source link

Accessing hardware peripherals from states #4

Closed shanemmattner closed 1 year ago

shanemmattner commented 1 year ago

Can anyone give a small example of accessing a GPIO or some other peripheral from a state? I got the example blinky working on an ESP32-C3, but I can't get an LED to blink.

mdeloof commented 1 year ago

Hi! The example below should work:

use std::thread::sleep;
use std::time::Duration;

use anyhow::Result;
use esp_idf_hal::gpio;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_hal::prelude::Peripherals;
use statig::prelude::*;

enum Event {
    TimerElapsed,
}

struct Blinky {
    led: PinDriver<'static, gpio::Gpio27, gpio::Output>,
}

impl Blinky {
    fn new(led: PinDriver<'static, gpio::Gpio27, gpio::Output>) -> Self {
        Self { led }
    }
}

#[state_machine(initial = "State::on()")]
impl Blinky {
    #[action]
    fn enter_on(&mut self) {
        self.led.set_high().unwrap();
    }

    #[state(entry_action = "enter_on")]
    fn on(event: &Event) -> Response<State> {
        match event {
            Event::TimerElapsed => Transition(State::off()),
        }
    }

    #[action]
    fn enter_off(&mut self) {
        self.led.set_low().unwrap();
    }

    #[state(entry_action = "enter_off")]
    fn off(&mut self, event: &Event) -> Response<State> {
        match event {
            Event::TimerElapsed => Transition(State::on()),
        }
    }
}

fn main() -> Result<()> {
    esp_idf_sys::link_patches();

    // Config logging.
    esp_idf_svc::log::EspLogger::initialize_default();

    let dp = Peripherals::take().unwrap();
    let led = PinDriver::output(dp.pins.gpio27).unwrap();

    let mut blinky = Blinky::new(led).state_machine().init();

    loop {
        sleep(Duration::from_millis(500));
        blinky.handle(&Event::TimerElapsed);
    }
}

I noticed that you also asked the question on the Rust user forum and I wanted to add some comments on the answer that was given there. It's important to understand that event handlers such as led_on and led_off are called when an event arrives in the associated state. So here for instance the led_on handler is called when an event arrives in the LedOn state:

fn led_on(&mut self, event: &Event) -> Response<State> {
    self.led.set_high();
    match event {
        Event::TimerElapsed => Transition(State::LedOff),
        _ => Super,
    }
}

The thing is, if you set the led to high inside the led_on event handler and then decide to transition to LedOff, the led will be turned on in the LedOff state and vice versa, which is probably not what you'd expect. 😅

So instead you could do this:

fn led_on(&mut self, event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => {
            self.led.set_low();
            Transition(State::LedOff)
        }
        _ => Super,
    }
}

Here we set the led to low just before transitioning to the LedOff state, which will give the desired behaviour. However, this is not ideal as you have to make sure you set the led to low every time you transition to the LedOff state. This is not really a problem in a simple example as this, but in larger state machines it can be easy to forget.

To solve this we can use a feature of statecharts called actions. These run when entering (or leaving) states during a transition, so if we set the led there it will always be done when entering the On state.

#[action]
fn enter_on(&mut self) {
    self.led.set_high().unwrap();
}

#[state(entry_action = "enter_on")]
fn on(event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => Transition(State::off()),
    }
}
shanemmattner commented 1 year ago

Thank you @mdeloof! I appreciate your response and added commentary on what I thought was a working solution!