├── .github └── FUNDING.yml ├── .gitignore ├── library.properties ├── library.json ├── keywords.txt ├── LICENSE ├── src ├── State.h ├── State.cpp ├── Transitions.h ├── SimpleFSM.h ├── Transitions.cpp └── SimpleFSM.cpp ├── examples ├── TimedTransitions │ ├── TimedTransitions.ino │ └── TimedTransitions.svg ├── Guards │ ├── Guards.ino │ └── Guards.svg ├── SimpleTransitions │ ├── SimpleTransitions.ino │ └── SimpleTransitions.svg ├── SimpleTransitionWithButton │ └── SimpleTransitionWithButton.ino ├── MixedTransitions │ ├── MixedTransitions.ino │ └── MixedTransitions.svg └── MixedTransitionsBrowser │ └── MixedTransitionsBrowser.ino ├── CHANGELOG.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: lennart0815 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MAC .dot folder 2 | .DS_Store 3 | # vs code files 4 | .vscode 5 | *.code-workspace 6 | #platformio 7 | platformio.ini 8 | .pio 9 | include 10 | # arduino_ci unit test binaries and artifacts 11 | *.bin 12 | *.bin.dSYM -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=SimpleFSM 2 | version=1.3.1 3 | author=Lennart Hennigs 4 | maintainer=Lennart Hennigs 5 | sentence=Arduino/ESP state machine library. 6 | paragraph=It allows you to quickly define a state machine. It uses callbacks and offers several convenience functions for tracking the machine's state. Tested with Arduino, ESP8266 and ESP32. 7 | category=Communication 8 | url=https://github.com/LennartHennigs/SimpleFSM 9 | architectures=* 10 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleFSM", 3 | "version": "1.3.1", 4 | "keywords": "State Machine, FSM, IoT", 5 | "description": "Arduino Library that allows you to quickly define a state machine. It uses callbacks and offers several convenience functions for tracking the machine's state. Tested with Arduino, ESP8266 and ESP32.", 6 | "homepage": "https://github.com/LennartHennigs/SimpleFSM", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/LennartHennigs/SimpleFSM" 10 | }, 11 | "authors": { 12 | "name": "Lennart Hennigs", 13 | "url": "https://lennarthennigs.de" 14 | }, 15 | "frameworks": "arduino", 16 | "platforms": "*" 17 | } 18 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | SimpleFSM KEYWORD1 2 | Transition KEYWORD1 3 | TimedTransition KEYWORD1 4 | State KEYWORD1 5 | CallbackFunction KEYWORD1 6 | GuardCondition KEYWORD1 7 | add KEYWORD2 8 | setInitialState KEYWORD2 9 | setFinishedHandler KEYWORD2 10 | setTransitionHandler KEYWORD2 11 | trigger KEYWORD2 12 | run KEYWORD2 13 | reset KEYWORD2 14 | getState KEYWORD2 15 | getPreviousState KEYWORD2 16 | isInState KEYWORD2 17 | isFinished KEYWORD2 18 | lastTransitioned KEYWORD2 19 | getID KEYWORD2 20 | getName KEYWORD2 21 | setName KEYWORD2 22 | getDotDefinition KEYWORD2 23 | setOnRunHandler KEYWORD2 24 | setGuardCondition KEYWORD2 25 | getEventID KEYWORD2 26 | setup KEYWORD2 27 | setName KEYWORD2 28 | setOnRunHandler KEYWORD2 29 | setOnEnterHandler KEYWORD2 30 | setOnStateHandler KEYWORD2 31 | setOnExitHandler KEYWORD2 32 | setAsFinal KEYWORD2 33 | isFinal KEYWORD2 34 | getTimedTransitionCount KEYWORD2 35 | getTransitionCount KEYWORD2 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2023 Lennart Hennigs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/State.h: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | 3 | #pragma once 4 | #ifndef STATE_H 5 | #define STATE_H 6 | 7 | ///////////////////////////////////////////////////////////////// 8 | 9 | #include "Arduino.h" 10 | 11 | ///////////////////////////////////////////////////////////////// 12 | 13 | typedef void (*CallbackFunction)(); 14 | typedef bool (*GuardCondition)(); 15 | 16 | ///////////////////////////////////////////////////////////////// 17 | 18 | class State { 19 | friend class SimpleFSM; 20 | 21 | public: 22 | State(); 23 | State(String name, CallbackFunction on_enter, CallbackFunction on_state = NULL, CallbackFunction on_exit = NULL, bool is_final = false); 24 | 25 | void setup(String name, CallbackFunction on_enter, CallbackFunction on_state = NULL, CallbackFunction on_exit = NULL, bool is_final = false); 26 | void setName(String name); 27 | void setOnEnterHandler(CallbackFunction f); 28 | void setOnStateHandler(CallbackFunction f); 29 | void setOnExitHandler(CallbackFunction f); 30 | void setAsFinal(bool final = true); 31 | 32 | int getID() const; 33 | bool isFinal() const; 34 | String getName() const; 35 | 36 | protected: 37 | int id; 38 | static int _next_id; 39 | 40 | String name = ""; 41 | CallbackFunction on_enter = NULL; 42 | CallbackFunction on_state = NULL; 43 | CallbackFunction on_exit = NULL; 44 | bool is_final = false; 45 | }; 46 | 47 | ///////////////////////////////////////////////////////////////// 48 | #endif 49 | ///////////////////////////////////////////////////////////////// 50 | -------------------------------------------------------------------------------- /examples/TimedTransitions/TimedTransitions.ino: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | /* 3 | This emulates a traffic light. 4 | Every 6 seconds the light will turn red, every 4 seconds green. 5 | While green, a "." is displayed 6 | */ 7 | ///////////////////////////////////////////////////////////////// 8 | 9 | #include "SimpleFSM.h" 10 | 11 | ///////////////////////////////////////////////////////////////// 12 | 13 | SimpleFSM fsm; 14 | 15 | ///////////////////////////////////////////////////////////////// 16 | 17 | void on_red() { 18 | Serial.println("\nState: RED"); 19 | } 20 | 21 | void on_green() { 22 | Serial.println("\nState: GREEN"); 23 | } 24 | 25 | void ongoing() { 26 | Serial.print("."); 27 | } 28 | ///////////////////////////////////////////////////////////////// 29 | 30 | 31 | State s[] = { 32 | State("red", on_red /*, ongoing */), 33 | State("green", on_green, ongoing) 34 | }; 35 | 36 | TimedTransition timedTransitions[] = { 37 | TimedTransition(&s[0], &s[1], 6000), 38 | TimedTransition(&s[1], &s[0], 4000), 39 | }; 40 | 41 | int num_timed = sizeof(timedTransitions) / sizeof(TimedTransition); 42 | 43 | ///////////////////////////////////////////////////////////////// 44 | 45 | void setup() { 46 | Serial.begin(9600); 47 | while (!Serial) { 48 | delay(300); 49 | } 50 | Serial.println(); 51 | Serial.println("SimpleFSM - Timed Transition (Simple traffic light)\n"); 52 | 53 | fsm.add(timedTransitions, num_timed); 54 | fsm.setInitialState(&s[0]); 55 | } 56 | 57 | ///////////////////////////////////////////////////////////////// 58 | 59 | void loop() { 60 | fsm.run(); 61 | } 62 | 63 | ///////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | - 6 | 7 | **Note:** Unreleased changes are checked in but not part of an official release (available through the Arduino IDE or PlatfomIO) yet. This allows you to test WiP features and give feedback to them. 8 | 9 | ## 1.3.1 - 2024-10-24 10 | 11 | - fixed Wrong type of memcopy argument #16 12 | 13 | ## 1.3.0 - 2023-10-14 14 | 15 | - Refactored `run()` 16 | - Updated `add()` – now multiple transition arrays can be added 17 | - Added `getTransitionCount()` and `getTimedTransitionCount()` functions as suggested in [PR #7](https://github.com/LennartHennigs/SimpleFSM/pull/7) 18 | - Fixed `TimedTransitions` constructor as mentioned in [#9](https://github.com/LennartHennigs/SimpleFSM/issues/9) 19 | - Fixed error in `State` constructor as mentioned in [#12](https://github.com/LennartHennigs/SimpleFSM/issues/12) 20 | - Fixed memory handling in `SimpleFSM` destructor as mentioned in [#8](https://github.com/LennartHennigs/SimpleFSM/issues/8) 21 | - Updated the button handlers in `MixedTransitions.ino`and `SimpleTransitionWithButton.ino` as mentioned in [#4](https://github.com/LennartHennigs/SimpleFSM/issues/4) 22 | 23 | 24 | ## 1.2.0 - 2022-12-19 25 | 26 | - Refactored code 27 | - Changed `bool add()` to `void add()` 28 | - Fixed bug, that the "ongoing state" was also called when a timed transition happens 29 | - Remove low-level memory allocation commands to remove compiler warnings mentioned in [#2](https://github.com/LennartHennigs/SimpleFSM/issues/2) 30 | - Updated examples (added explanation and some additional callbacks to show the state of the FSM) 31 | 32 | ## 1.1.0 - 2022-05-30 33 | 34 | - Added fix for ESP32 crashes as reported by [Erik](https://github.com/snowrodeo) in [#1](https://github.com/LennartHennigs/SimpleFSM/issues/1) 35 | 36 | ## 1.0.1 - 2022-05-07 37 | 38 | - Removed compile warnings for `SimpleFSM.cpp` 39 | - Updated `.gitignore` 40 | 41 | ## 1.0.0 - 2022-05-07 42 | 43 | - Initial release 44 | 45 | ## Note 46 | 47 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 48 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 49 | -------------------------------------------------------------------------------- /examples/Guards/Guards.ino: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | /* 3 | This example showcases how guards work. 4 | It emulates a time bomb count down. 5 | 6 | As long as the countdown is not zero the FSM stays in the counting state. 7 | When it reaches zero it goes to the exploding state. 8 | */ 9 | ///////////////////////////////////////////////////////////////// 10 | 11 | #include "SimpleFSM.h" 12 | 13 | ///////////////////////////////////////////////////////////////// 14 | 15 | int countdown = 6; 16 | 17 | SimpleFSM fsm; 18 | 19 | ///////////////////////////////////////////////////////////////// 20 | 21 | void counting() { 22 | Serial.println(countdown--); 23 | } 24 | 25 | void boom() { 26 | Serial.println("BOOM"); 27 | } 28 | 29 | bool not_zero_yet() { 30 | return !zero_yet(); 31 | } 32 | 33 | bool zero_yet() { 34 | return countdown == 0; 35 | } 36 | 37 | void finished() { 38 | Serial.println("THE END!"); 39 | } 40 | 41 | void tick() { 42 | Serial.println("TICK"); 43 | } 44 | 45 | 46 | ///////////////////////////////////////////////////////////////// 47 | 48 | State s[] = { 49 | State("counting", counting), 50 | State("exploding", boom) 51 | }; 52 | 53 | TimedTransition timedTransitions[] = { 54 | TimedTransition(&s[0], &s[0], 1000, NULL, "", not_zero_yet) 55 | TimedTransition(&s[0], &s[1], 1000, NULL, "", zero_yet), 56 | }; 57 | 58 | int num_timed = sizeof(timedTransitions) / sizeof(TimedTransition); 59 | 60 | ///////////////////////////////////////////////////////////////// 61 | 62 | void setup() { 63 | Serial.begin(9600); 64 | while (!Serial) { 65 | delay(300); 66 | } 67 | Serial.println(); 68 | Serial.println("SimpleFSM - Using Guard Condition (Time Bomb)\n"); 69 | 70 | fsm.add(timedTransitions, num_timed); 71 | // initial state 72 | fsm.setInitialState(&s[0]); 73 | // final state 74 | s[1].setAsFinal(true); 75 | fsm.setFinishedHandler(finished); 76 | } 77 | 78 | ///////////////////////////////////////////////////////////////// 79 | 80 | void loop() { 81 | fsm.run(1000, tick); 82 | } 83 | 84 | ///////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /src/State.cpp: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | #include "State.h" 3 | ///////////////////////////////////////////////////////////////// 4 | 5 | int State::_next_id = 0; 6 | 7 | ///////////////////////////////////////////////////////////////// 8 | 9 | State::State() { 10 | } 11 | 12 | ///////////////////////////////////////////////////////////////// 13 | 14 | State::State(String name, CallbackFunction on_enter, CallbackFunction on_state, CallbackFunction on_exit, bool is_final /* = false */) { 15 | setup(name, on_enter, on_state, on_exit, is_final); 16 | } 17 | 18 | ///////////////////////////////////////////////////////////////// 19 | 20 | void State::setup(String name, CallbackFunction on_enter, CallbackFunction on_state, CallbackFunction on_exit, bool is_final /* = false */) { 21 | this->name = name; 22 | this->on_enter = on_enter; 23 | this->on_state = on_state; 24 | this->on_exit = on_exit; 25 | this->id = _next_id++; 26 | this->is_final = is_final; 27 | } 28 | 29 | ///////////////////////////////////////////////////////////////// 30 | 31 | String State::getName() const { 32 | return name; 33 | } 34 | 35 | ///////////////////////////////////////////////////////////////// 36 | 37 | bool State::isFinal() const { 38 | return is_final; 39 | } 40 | 41 | ///////////////////////////////////////////////////////////////// 42 | 43 | void State::setName(String name) { 44 | this->name = name; 45 | } 46 | 47 | ///////////////////////////////////////////////////////////////// 48 | 49 | void State::setOnEnterHandler(CallbackFunction f) { 50 | this->on_enter = f; 51 | } 52 | 53 | ///////////////////////////////////////////////////////////////// 54 | 55 | void State::setOnStateHandler(CallbackFunction f) { 56 | this->on_state = f; 57 | } 58 | 59 | ///////////////////////////////////////////////////////////////// 60 | 61 | void State::setOnExitHandler(CallbackFunction f) { 62 | this->on_exit = f; 63 | } 64 | 65 | ///////////////////////////////////////////////////////////////// 66 | 67 | int State::getID() const { 68 | return id; 69 | } 70 | 71 | ///////////////////////////////////////////////////////////////// 72 | 73 | void State::setAsFinal(bool final /* = true */) { 74 | is_final = final; 75 | } 76 | 77 | ///////////////////////////////////////////////////////////////// 78 | -------------------------------------------------------------------------------- /examples/SimpleTransitions/SimpleTransitions.ino: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | /* 3 | This emulates an blinken light. 4 | Every four seconds will the light turn on or off. 5 | While the light is "on", a "." will be shown. 6 | */ 7 | 8 | ///////////////////////////////////////////////////////////////// 9 | 10 | #include "SimpleFSM.h" 11 | 12 | ///////////////////////////////////////////////////////////////// 13 | 14 | SimpleFSM fsm; 15 | 16 | ///////////////////////////////////////////////////////////////// 17 | 18 | void light_on() { 19 | Serial.println("Entering State: ON"); 20 | } 21 | 22 | void light_off() { 23 | Serial.println("Entering State: OFF"); 24 | } 25 | 26 | void exit_light_on() { 27 | Serial.println("\nLeaving State: ON "); 28 | } 29 | 30 | void exit_light_off() { 31 | Serial.println("\nLeaving State: OFF"); 32 | } 33 | 34 | void on_to_off() { 35 | Serial.println("ON -> OFF"); 36 | } 37 | 38 | void off_to_on() { 39 | Serial.println("OFF -> ON"); 40 | } 41 | 42 | void ongoing() { 43 | Serial.print("."); 44 | } 45 | 46 | ///////////////////////////////////////////////////////////////// 47 | 48 | State s[] = { 49 | State("on", light_on, ongoing, exit_light_on), 50 | State("off", light_off, ongoing, exit_light_off) 51 | }; 52 | 53 | enum triggers { 54 | light_switch_flipped = 1 55 | }; 56 | 57 | Transition transitions[] = { 58 | Transition(&s[0], &s[1], light_switch_flipped, on_to_off), 59 | Transition(&s[1], &s[0], light_switch_flipped, off_to_on) 60 | }; 61 | 62 | int num_transitions = sizeof(transitions) / sizeof(Transition); 63 | 64 | ///////////////////////////////////////////////////////////////// 65 | 66 | void setup() { 67 | Serial.begin(9600); 68 | while (!Serial) { 69 | delay(300); 70 | } 71 | Serial.println(); 72 | Serial.println(); 73 | Serial.println("SimpleFSM - Simple Transitions (Light Switch)\n"); 74 | 75 | fsm.add(transitions, num_transitions); 76 | fsm.setInitialState(&s[1]); 77 | } 78 | 79 | ///////////////////////////////////////////////////////////////// 80 | 81 | void loop() { 82 | fsm.run(); 83 | // flip the switch every 4 seconds 84 | // better than using delay() as this won't block the loop() 85 | if (fsm.lastTransitioned() > 4000) { 86 | fsm.trigger(light_switch_flipped); 87 | } 88 | } 89 | 90 | ///////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /src/Transitions.h: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | 3 | #pragma once 4 | #ifndef TRANSITIONS_H 5 | #define TRANSITIONS_H 6 | 7 | ///////////////////////////////////////////////////////////////// 8 | 9 | #include "Arduino.h" 10 | #include "State.h" 11 | 12 | ///////////////////////////////////////////////////////////////// 13 | 14 | typedef void (*CallbackFunction)(); 15 | typedef bool (*GuardCondition)(); 16 | 17 | ///////////////////////////////////////////////////////////////// 18 | // abstract parent class for Transition and TimedTransition 19 | 20 | class AbstractTransition { 21 | friend class SimpleFSM; 22 | 23 | public: 24 | AbstractTransition(); 25 | // to make this class an interface 26 | virtual ~AbstractTransition(){}; 27 | virtual int getID() const = 0; 28 | String getName() const; 29 | 30 | void setName(String name); 31 | void setOnRunHandler(CallbackFunction f); 32 | void setGuardCondition(GuardCondition f); 33 | 34 | protected: 35 | static int _next_id; 36 | int id = 0; 37 | String name = ""; 38 | State* from = NULL; 39 | State* to = NULL; 40 | CallbackFunction on_run_cb = NULL; 41 | GuardCondition guard_cb = NULL; 42 | }; 43 | 44 | ///////////////////////////////////////////////////////////////// 45 | 46 | class Transition : public AbstractTransition { 47 | friend class SimpleFSM; 48 | 49 | public: 50 | Transition(); 51 | Transition(State* from, State* to, int event_id, CallbackFunction on_run = NULL, String name = "", GuardCondition guard = NULL); 52 | 53 | void setup(State* from, State* to, int event_id, CallbackFunction on_run = NULL, String name = "", GuardCondition guard = NULL); 54 | 55 | int getID() const; 56 | int getEventID() const; 57 | 58 | protected: 59 | int event_id; 60 | }; 61 | 62 | ///////////////////////////////////////////////////////////////// 63 | 64 | class TimedTransition : public AbstractTransition { 65 | friend class SimpleFSM; 66 | 67 | public: 68 | TimedTransition(); 69 | TimedTransition(State* from, State* to, int interval, CallbackFunction on_run = NULL, String name = "", GuardCondition guard = NULL); 70 | 71 | void setup(State* from, State* to, int interval, CallbackFunction on_run = NULL, String name = "", GuardCondition guard = NULL); 72 | 73 | int getID() const; 74 | int getInterval() const; 75 | 76 | void reset(); 77 | 78 | protected: 79 | unsigned long start; 80 | unsigned long interval; 81 | }; 82 | ///////////////////////////////////////////////////////////////// 83 | #endif 84 | ///////////////////////////////////////////////////////////////// 85 | -------------------------------------------------------------------------------- /examples/SimpleTransitionWithButton/SimpleTransitionWithButton.ino: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | /* 3 | This emulates a light switch. 4 | The button click will turn on/off the light. 5 | While the light is "on", a "." will be shown. 6 | */ 7 | ///////////////////////////////////////////////////////////////// 8 | 9 | #include "SimpleFSM.h" 10 | #include "Button2.h" // https://github.com/LennartHennigs/Button2 11 | 12 | ///////////////////////////////////////////////////////////////// 13 | 14 | #define BUTTON_PIN 39 15 | 16 | ///////////////////////////////////////////////////////////////// 17 | 18 | SimpleFSM fsm; 19 | Button2 btn; 20 | 21 | ///////////////////////////////////////////////////////////////// 22 | 23 | void light_on() { 24 | Serial.println("Entering State: ON"); 25 | } 26 | 27 | void light_off() { 28 | Serial.println("Entering State: OFF"); 29 | } 30 | 31 | void exit_light_on() { 32 | Serial.println("Leaving State: ON "); 33 | } 34 | 35 | void exit_light_off() { 36 | Serial.println("Leaving State: OFF"); 37 | } 38 | 39 | void on_to_off() { 40 | Serial.println("ON -> OFF"); 41 | } 42 | 43 | void off_to_on() { 44 | Serial.println("OFF -> ON"); 45 | } 46 | 47 | void ongoing() { 48 | Serial.print("."); 49 | } 50 | 51 | ///////////////////////////////////////////////////////////////// 52 | 53 | State s[] = { 54 | State("on", light_on, ongoing, exit_light_on), 55 | State("off", light_off, NULL, exit_light_off) 56 | }; 57 | 58 | enum triggers { 59 | light_switch_flipped = 1 60 | }; 61 | 62 | Transition transitions[] = { 63 | Transition(&s[0], &s[1], light_switch_flipped, on_to_off), 64 | Transition(&s[1], &s[0], light_switch_flipped, off_to_on) 65 | }; 66 | 67 | int num_transitions = sizeof(transitions) / sizeof(Transition); 68 | 69 | ///////////////////////////////////////////////////////////////// 70 | 71 | void button_handler(Button2 &btn) { 72 | if (fsm.getState() == &s[0]) Serial.println(); 73 | 74 | Serial.println("BUTTON: I was flipped"); 75 | fsm.trigger(light_switch_flipped); 76 | } 77 | 78 | ///////////////////////////////////////////////////////////////// 79 | 80 | void setup() { 81 | Serial.begin(9600); 82 | while (!Serial) { 83 | delay(300); 84 | } 85 | Serial.println(); 86 | Serial.println(); 87 | Serial.println("SimpleFSM - Simple Transitions (Light Switch w/ Button)\n"); 88 | 89 | fsm.add(transitions, num_transitions); 90 | 91 | fsm.setInitialState(&s[1]); 92 | 93 | btn.begin(BUTTON_PIN); 94 | btn.setTapHandler(button_handler); 95 | } 96 | 97 | ///////////////////////////////////////////////////////////////// 98 | 99 | void loop() { 100 | fsm.run(1000); 101 | btn.loop(); 102 | } 103 | 104 | ///////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /src/SimpleFSM.h: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | 3 | #pragma once 4 | #ifndef SIMPLE_FSM_H 5 | #define SIMPLE_FSM_H 6 | 7 | ///////////////////////////////////////////////////////////////// 8 | 9 | #if defined(ARDUINO_ARCH_ESP32) || defined(ESP8266) 10 | #include 11 | #endif 12 | #include "Arduino.h" 13 | #include "State.h" 14 | #include "Transitions.h" 15 | 16 | ///////////////////////////////////////////////////////////////// 17 | 18 | typedef void (*CallbackFunction)(); 19 | typedef bool (*GuardCondition)(); 20 | 21 | ///////////////////////////////////////////////////////////////// 22 | 23 | class SimpleFSM { 24 | public: 25 | SimpleFSM(); 26 | SimpleFSM(State* initial_state); 27 | ~SimpleFSM(); 28 | 29 | void add(Transition t[], int size); 30 | void add(TimedTransition t[], int size); 31 | 32 | void setInitialState(State* state); 33 | void setFinishedHandler(CallbackFunction f); 34 | void setTransitionHandler(CallbackFunction f); 35 | 36 | bool trigger(int event_id); 37 | void run(int interval = 1000, CallbackFunction tick_cb = NULL); 38 | void reset(); 39 | 40 | int getTransitionCount() const; 41 | int getTimedTransitionCount() const; 42 | 43 | bool isFinished() const; 44 | State* getState() const; 45 | bool isInState(State* state) const; 46 | State* getPreviousState() const; 47 | unsigned long lastTransitioned() const; 48 | String getDotDefinition(); 49 | 50 | protected: 51 | int num_timed = 0; 52 | int num_standard = 0; 53 | Transition* transitions = NULL; 54 | TimedTransition* timed = NULL; 55 | 56 | bool is_initialized = false; 57 | bool is_finished = false; 58 | unsigned long last_run = 0; 59 | unsigned long last_transition = 0; 60 | 61 | State* inital_state = NULL; 62 | State* current_state = NULL; 63 | State* prev_state = NULL; 64 | CallbackFunction on_transition_cb = NULL; 65 | CallbackFunction finished_cb = NULL; 66 | 67 | String dot_definition = ""; 68 | 69 | bool _isDuplicate(const TimedTransition& transition, const TimedTransition* transitionArray, int arraySize) const; 70 | bool _isDuplicate(const Transition& transition, const Transition* transitionArray, int arraySize) const; 71 | 72 | bool _isTimeForRun(unsigned long now, int interval); 73 | void _handleTimedEvents(unsigned long now); 74 | 75 | bool _initFSM(); 76 | bool _transitionTo(AbstractTransition* transition); 77 | bool _changeToState(State* s, unsigned long now); 78 | 79 | void _addDotTransition(Transition& t); 80 | void _addDotTransition(TimedTransition& t); 81 | String _dot_transition(String from, String to, String label, String param); 82 | String _dot_inital_state(); 83 | String _dot_header(); 84 | String _dot_active_node(); 85 | }; 86 | 87 | ///////////////////////////////////////////////////////////////// 88 | #endif 89 | ///////////////////////////////////////////////////////////////// 90 | -------------------------------------------------------------------------------- /examples/MixedTransitions/MixedTransitions.ino: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | /* 3 | This example shows how to use regular and timed transitions. 4 | The FSM emulates a traffic light. 5 | 6 | When the button is pressed the light will turn green after a second. 7 | Otherwise the light will transition from red to green every 6 seconds. 8 | And from green to red after 4. 9 | */ 10 | ///////////////////////////////////////////////////////////////// 11 | 12 | #include "SimpleFSM.h" 13 | #include "Button2.h" // https://github.com/LennartHennigs/Button2 14 | 15 | ///////////////////////////////////////////////////////////////// 16 | 17 | #define BUTTON_PIN 39 18 | 19 | ///////////////////////////////////////////////////////////////// 20 | 21 | SimpleFSM fsm; 22 | Button2 btn; 23 | 24 | ///////////////////////////////////////////////////////////////// 25 | 26 | void on_red() { 27 | Serial.println("\nState: RED"); 28 | } 29 | 30 | void on_green() { 31 | Serial.println("\nState: GREEN"); 32 | } 33 | 34 | void ongoing() { 35 | Serial.print("."); 36 | } 37 | 38 | void on_button_press() { 39 | Serial.println("\nBUTTON!"); 40 | } 41 | 42 | ///////////////////////////////////////////////////////////////// 43 | 44 | State s[] = { 45 | State("red light", on_red, ongoing), 46 | State("green light", on_green, ongoing), 47 | State("button pressed", on_button_press, ongoing) 48 | }; 49 | 50 | enum triggers { 51 | button_was_pressed = 1 52 | }; 53 | 54 | Transition transitions[] = { 55 | Transition(&s[0], &s[2], button_was_pressed) 56 | }; 57 | 58 | TimedTransition timedTransitions[] = { 59 | TimedTransition(&s[0], &s[1], 6000), 60 | TimedTransition(&s[1], &s[0], 4000), 61 | TimedTransition(&s[2], &s[1], 1000) 62 | }; 63 | 64 | int num_transitions = sizeof(transitions) / sizeof(Transition); 65 | int num_timed = sizeof(timedTransitions) / sizeof(TimedTransition); 66 | 67 | ///////////////////////////////////////////////////////////////// 68 | 69 | void button_handler(Button2 &btn) { 70 | fsm.trigger(button_was_pressed); 71 | } 72 | 73 | ///////////////////////////////////////////////////////////////// 74 | 75 | void setup() { 76 | Serial.begin(9600); 77 | while (!Serial) { 78 | delay(300); 79 | } 80 | Serial.println(); 81 | Serial.println(); 82 | Serial.println("SimpleFSM - Mixed Transition (Traffic light with button for green)\n"); 83 | 84 | fsm.add(timedTransitions, num_timed); 85 | fsm.add(transitions, num_transitions); 86 | 87 | fsm.setInitialState(&s[0]); 88 | 89 | btn.begin(BUTTON_PIN); 90 | btn.setTapHandler(button_handler); 91 | } 92 | 93 | ///////////////////////////////////////////////////////////////// 94 | 95 | void loop() { 96 | fsm.run(); 97 | btn.loop(); 98 | } 99 | 100 | ///////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /examples/SimpleTransitions/SimpleTransitions.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 14 | on 15 | 16 | on 17 | 18 | 19 | 20 | off 21 | 22 | off 23 | 24 | 25 | 26 | on->off 27 | 28 | 29 | (ID=1) 30 | 31 | 32 | 33 | off->on 34 | 35 | 36 | (ID=1) 37 | 38 | 39 | 40 | 42 | 46 | 48 | 50 | 52 | 57 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/Guards/Guards.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 14 | counting 15 | 16 | counting 17 | 18 | 19 | 20 | counting->counting 21 | 22 | 23 | (1000ms) 24 | 25 | 26 | 27 | exploding 28 | 29 | exploding 30 | 31 | 32 | 33 | counting->exploding 34 | 35 | 36 | (1000ms) 37 | 38 | 39 | 40 | 42 | 46 | 48 | 50 | 52 | 57 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/TimedTransitions/TimedTransitions.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 14 | red 15 | 16 | red 17 | 18 | 19 | 20 | green 21 | 22 | green 23 | 24 | 25 | 26 | red->green 27 | 28 | 29 | (6000ms) 30 | 31 | 32 | 33 | green->red 34 | 35 | 36 | (4000ms) 37 | 38 | 39 | 40 | 42 | 46 | 48 | 50 | 52 | 57 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/Transitions.cpp: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | #include "Transitions.h" 3 | ///////////////////////////////////////////////////////////////// 4 | 5 | int AbstractTransition::_next_id = 0; 6 | 7 | ///////////////////////////////////////////////////////////////// 8 | 9 | AbstractTransition::AbstractTransition() { 10 | id = _next_id; 11 | _next_id++; 12 | } 13 | 14 | ///////////////////////////////////////////////////////////////// 15 | 16 | void AbstractTransition::setGuardCondition(GuardCondition f) { 17 | guard_cb = f; 18 | } 19 | 20 | ///////////////////////////////////////////////////////////////// 21 | 22 | String AbstractTransition::getName() const { 23 | return name; 24 | } 25 | 26 | ///////////////////////////////////////////////////////////////// 27 | 28 | void AbstractTransition::setName(String name) { 29 | this->name = name; 30 | } 31 | 32 | ///////////////////////////////////////////////////////////////// 33 | 34 | void AbstractTransition::setOnRunHandler(CallbackFunction f) { 35 | on_run_cb = f; 36 | } 37 | 38 | ///////////////////////////////////////////////////////////////// 39 | ///////////////////////////////////////////////////////////////// 40 | // Transition 41 | ///////////////////////////////////////////////////////////////// 42 | ///////////////////////////////////////////////////////////////// 43 | 44 | int Transition::getEventID() const { 45 | return event_id; 46 | } 47 | 48 | ///////////////////////////////////////////////////////////////// 49 | 50 | int Transition::getID() const { 51 | return id; 52 | } 53 | 54 | ///////////////////////////////////////////////////////////////// 55 | 56 | Transition::Transition() : event_id(0) {} 57 | 58 | ///////////////////////////////////////////////////////////////// 59 | 60 | Transition::Transition(State* from, State* to, int event_id, CallbackFunction on_run /* = NULL */, String name /* = "" */, GuardCondition guard /* = NULL */) { 61 | setup(from, to, event_id, on_run, name, guard); 62 | } 63 | 64 | ///////////////////////////////////////////////////////////////// 65 | 66 | void Transition::setup(State* from, State* to, int event_id, CallbackFunction on_run /* = NULL */, String name /* = "" */, GuardCondition guard /* = NULL */) { 67 | this->from = from; 68 | this->to = to; 69 | this->event_id = event_id; 70 | this->on_run_cb = on_run; 71 | this->name = name; 72 | this->guard_cb = guard; 73 | } 74 | 75 | ///////////////////////////////////////////////////////////////// 76 | ///////////////////////////////////////////////////////////////// 77 | // TimedTransition 78 | ///////////////////////////////////////////////////////////////// 79 | ///////////////////////////////////////////////////////////////// 80 | 81 | TimedTransition::TimedTransition() : start(0), interval(0) {} 82 | 83 | ///////////////////////////////////////////////////////////////// 84 | 85 | TimedTransition::TimedTransition(State* from, State* to, int interval, CallbackFunction on_run /* = NULL */, String name /* = "" */, GuardCondition guard /* = NULL */) : TimedTransition() { 86 | setup(from, to, interval, on_run, name, guard); 87 | } 88 | 89 | ///////////////////////////////////////////////////////////////// 90 | 91 | void TimedTransition::setup(State* from, State* to, int interval, CallbackFunction on_run /* = NULL */, String name /* = "" */, GuardCondition guard /* = NULL */) { 92 | this->from = from; 93 | this->to = to; 94 | this->interval = interval; 95 | this->on_run_cb = on_run; 96 | this->name = name; 97 | this->guard_cb = guard; 98 | } 99 | 100 | ///////////////////////////////////////////////////////////////// 101 | 102 | int TimedTransition::getInterval() const { 103 | return interval; 104 | } 105 | 106 | ///////////////////////////////////////////////////////////////// 107 | 108 | int TimedTransition::getID() const { 109 | return id; 110 | } 111 | 112 | ///////////////////////////////////////////////////////////////// 113 | 114 | void TimedTransition::reset() { 115 | start = 0; 116 | } 117 | 118 | ///////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /examples/MixedTransitionsBrowser/MixedTransitionsBrowser.ino: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | 3 | #include "SimpleFSM.h" 4 | #include "Button2.h" // https://github.com/LennartHennigs/Button2 5 | 6 | #if defined(ARDUINO_ARCH_ESP32) 7 | #include 8 | #include 9 | #include 10 | #elif defined(ARDUINO_ARCH_ESP8266) 11 | #include 12 | #include 13 | #include 14 | #else 15 | #error "you need an ESP32 or ESP8266" 16 | #endif 17 | 18 | ///////////////////////////////////////////////////////////////// 19 | 20 | #define BUTTON_PIN 39 21 | 22 | #define WIFI_SSID "SSID" 23 | #define WIFI_PASSWD "PWD" 24 | 25 | ///////////////////////////////////////////////////////////////// 26 | 27 | SimpleFSM fsm; 28 | Button2 btn; 29 | 30 | #if defined(ARDUINO_ARCH_ESP32) 31 | WebServer server(80); 32 | #else 33 | ESP8266WebServer server(80); 34 | #endif 35 | 36 | ///////////////////////////////////////////////////////////////// 37 | 38 | bool connectToWiFi(String ssid, String passwd, int timeout_after =-1) { 39 | WiFi.mode(WIFI_STA); 40 | WiFi.begin(ssid, passwd); 41 | // Wait for connection 42 | long now = millis(); 43 | while ((WiFi.status() != WL_CONNECTED) && ((timeout_after > -1) && (millis() < (now + timeout_after)))) { 44 | delay(500); 45 | } 46 | return (WiFi.status() == WL_CONNECTED); 47 | } 48 | 49 | ///////////////////////////////////////////////////////////////// 50 | 51 | void on_red() { 52 | Serial.println("\nState: RED"); 53 | } 54 | 55 | void on_green() { 56 | Serial.println("\nState: GREEN"); 57 | } 58 | 59 | void ongoing() { 60 | Serial.print("."); 61 | } 62 | 63 | void on_button_press() { 64 | Serial.println("\nBUTTON!"); 65 | } 66 | 67 | ///////////////////////////////////////////////////////////////// 68 | 69 | State s[] = { 70 | State("red light", on_red, ongoing), 71 | State("green light", on_green, ongoing), 72 | State("button pressed", on_button_press, ongoing) 73 | }; 74 | 75 | enum triggers { 76 | button_was_pressed = 1 77 | }; 78 | 79 | Transition transitions[] = { 80 | Transition(&s[0], &s[2], button_was_pressed) 81 | }; 82 | 83 | TimedTransition timedTransitions[] = { 84 | TimedTransition(&s[0], &s[1], 6000), 85 | TimedTransition(&s[1], &s[0], 4000), 86 | TimedTransition(&s[2], &s[1], 1000) 87 | }; 88 | 89 | int num_transitions = sizeof(transitions) / sizeof(Transition); 90 | int num_timed = sizeof(timedTransitions) / sizeof(TimedTransition); 91 | 92 | ///////////////////////////////////////////////////////////////// 93 | 94 | void button_handler(Button2 btn) { 95 | fsm.trigger(button_was_pressed); 96 | } 97 | 98 | ///////////////////////////////////////////////////////////////// 99 | 100 | void showGraph() { 101 | String message = ""; 102 | message += F("\n"); 103 | message += F("\n"); 104 | message += F("\n"); 105 | message += F("GraphVizArt\n"); 106 | message += F("\n"); 107 | message += F("\n"); 108 | message += F(""); 111 | message += F("\n"); 112 | message += F("\n"); 113 | server.send ( 200, F("text/html"), message); 114 | } 115 | 116 | ///////////////////////////////////////////////////////////////// 117 | 118 | void setup() { 119 | Serial.begin(9600); 120 | while (!Serial) { 121 | delay(300); 122 | } 123 | Serial.println(); 124 | Serial.println(); 125 | Serial.println("SimpleFSM - Mixed Transition (Traffic light with button for green)\n"); 126 | 127 | fsm.add(timedTransitions, num_timed); 128 | fsm.add(transitions, num_transitions); 129 | 130 | fsm.setInitialState(&s[0]); 131 | 132 | btn.begin(BUTTON_PIN); 133 | btn.setTapHandler(button_handler); 134 | 135 | if (!connectToWiFi(WIFI_SSID, WIFI_PASSWD, 10000)) { 136 | Serial.println(F("Could not connect to WiFi.")); 137 | } else { 138 | Serial.print(F("Open http://")); 139 | Serial.print(WiFi.localIP()); 140 | Serial.print(F(" in your browser.\n")); 141 | 142 | server.on( "/", showGraph); 143 | server.onNotFound([](){ 144 | server.send(404, F("text/plain"), F("File Not Found\n")); 145 | }); 146 | server.begin(); 147 | } 148 | server.handleClient(); 149 | } 150 | 151 | ///////////////////////////////////////////////////////////////// 152 | 153 | void loop() { 154 | fsm.run(); 155 | btn.loop(); 156 | server.handleClient(); 157 | } 158 | 159 | ///////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /examples/MixedTransitions/MixedTransitions.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 14 | red light 15 | 16 | red light 17 | 18 | 19 | 20 | green light 21 | 22 | green light 23 | 24 | 25 | 26 | red light->green light 27 | 28 | 29 | (6000ms) 30 | 31 | 32 | 33 | button pressed 34 | 35 | button pressed 36 | 37 | 38 | 39 | red light->button pressed 40 | 41 | 42 | (ID=1) 43 | 44 | 45 | 46 | green light->red light 47 | 48 | 49 | (4000ms) 50 | 51 | 52 | 53 | button pressed->green light 54 | 55 | 56 | (2000ms) 57 | 58 | 59 | 60 | 62 | 66 | 68 | 70 | 72 | 77 | 82 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleFSM 2 | 3 | Arduino/ESP library to simplify setting up and running a state machine. 4 | 5 | * Author: Lennart Hennigs () 6 | * Copyright (C) 2023 Lennart Hennigs. 7 | * Released under the MIT license. 8 | 9 | ## Description 10 | 11 | This library allows you to quickly setup a State Machine. Read here [what a state machine is](https://majenko.co.uk/blog/our-blog-1/the-finite-state-machine-26) and [why a state machine is neat for hardware projects](https://barrgroup.com/embedded-systems/how-to/state-machines-event-driven-systems?utm_source=pocket_mylist). 12 | 13 | It has been tested with Arduino, ESP8266 and ESP32 devices. 14 | 15 | ⚠️ To see the latest changes to the library please take a look at the [Changelog](https://github.com/LennartHennigs/SimpleFSM/blob/master/CHANGELOG.md). 16 | 17 | If you find this library helpful please consider giving it a ⭐️ at [GitHub](https://github.com/LennartHennigs/SimpleFSM) and/or [buy me a ☕️](https://ko-fi.com/lennart0815). Thanks! 18 | 19 | ### Features 20 | 21 | * Easy state definition 22 | * Allows for none, one or multiple final states 23 | * Event triggered transitions and (automatic) timed transitions 24 | * Possibility to define guard conditions for your transitions 25 | * Callback functions for... 26 | * State entry, staying, exit 27 | * Transition execution 28 | * Final state reached 29 | * the run interval of the state machine 30 | * Definition of an in_state interval 31 | * Functions for tracking the behavior and progress of the state machine 32 | * Creation of a `Graphviz` source file of your state machine definition 33 | * TBD: Storage of the current state on files with `LittleFS` or SD card storage (TBD) 34 | 35 | ⚠️ To see the latest changes to the library please take a look at the [Changelog](https://github.com/LennartHennigs/SimpleFSM/blob/master/CHANGELOG.md). 36 | 37 | ## How To Use 38 | 39 | * You first need to define a set of states for your state machine 40 | * Then, define transitions (regular or timed) for these state 41 | * Pass the transitions to your state machine 42 | * Define an initial state 43 | * ...and add the `run()` function in your loop 44 | * See [SimpleTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/MixedTransitions/MixedTransitions.ino) for a basic example 45 | 46 | ### Defining States 47 | 48 | * A finite state machine has a defined set of states 49 | * A state must have a name... 50 | * ...and can have callback functions for different events (when a state is entered, exited, or stays in a state) 51 | 52 | ```c++ 53 | State( 54 | String name, 55 | CallbackFunction on_enter, 56 | CallbackFunction on_state = NULL, 57 | CallbackFunction on_exit = NULL, 58 | bool is_final = false 59 | ); 60 | ``` 61 | 62 | * Most states will have the entry handler 63 | * The easiest way to define states is creating using an array, e.g. as shown in [MixedTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/MixedTransitions/MixedTransitions.ino): 64 | 65 | ```c++ 66 | State s[] = { 67 | State("red", on_red), 68 | State("green", on_green), 69 | State("BTN", on_button_press) 70 | }; 71 | ``` 72 | 73 | * The inital state **must** of the state machine must be defined – either via the constructor or the `setup()` function or via the `setInitialState()` function 74 | * None, one, or multiple states can be defined as an end state via `setAsFinal()` 75 | * For the full `State` class definition see [State.h](https://github.com/LennartHennigs/SimpleFSM/blob/master/src/State.h) 76 | 77 | ### Transitions 78 | 79 | * This library offers two types of Transitions, regular and timed ones 80 | * All transitions must have a from and and a to state 81 | * Transitions can have a callback function for when the transition is executed, a name, and a [guard condition](#guard-conditions) 82 | * You can add both types to a state machine, see [MixedTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/MixedTransitions/MixedTransitions.ino) for an example 83 | * See [Transitions.h](https://github.com/LennartHennigs/SimpleFSM/blob/master/src/Transitions.h) for the class definitions of `Transition` and `TimedTransition`. 84 | * Note: Both classes are based of an abstract class which is not to be used in your code. 85 | 86 | ### Regular Transitions 87 | 88 | ```c++ 89 | Transition( 90 | State* from, 91 | State* to, 92 | int event_id, 93 | CallbackFunction on_run = NULL, 94 | String name = "", 95 | GuardCondition guard = NULL 96 | ); 97 | ``` 98 | 99 | * Regular transitions must have a a set of states and a trigger (ID): 100 | 101 | ```c++ 102 | Transition transitions[] = { 103 | Transition(&s[0], &s[1], light_switch_flipped), 104 | Transition(&s[1], &s[0], light_switch_flipped) 105 | }; 106 | ``` 107 | 108 | * Define the triggers for your state machine in an enum (and set the first enum value to 1, to be safe): 109 | 110 | ```c++ 111 | enum triggers { 112 | light_switch_flipped = 1 113 | }; 114 | ``` 115 | 116 | * To call a trigger use the `trigger()`function: 117 | 118 | ```c++ 119 | fsm.trigger(light_switch_flipped); 120 | ``` 121 | 122 | * See [SimpleTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/SimpleTransitions/SimpleTransitions.ino) and [SimpleTransitionWithButtons.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/SimpleTransitionWithButton/SimpleTransitionWithButton.ino) for more details 123 | 124 | ### Timed Transitions 125 | 126 | ```c++ 127 | TimedTransition( 128 | State* from, 129 | State* to, 130 | int interval, 131 | CallbackFunction on_run = NULL, 132 | String name = "", 133 | GuardCondition guard = NULL 134 | ); 135 | ``` 136 | 137 | * Timed transitions are automatically executed after a certain amount of time has passed 138 | * The interval is defined in milliseconds: 139 | 140 | ```c++ 141 | TimedTransition timedTransitions[] = { 142 | TimedTransition(&s[0], &s[1], 6000), 143 | TimedTransition(&s[1], &s[0], 4000), 144 | TimedTransition(&s[2], &s[1], 2000) 145 | }; 146 | ``` 147 | 148 | * See [TimedTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/TimedTransitions/TimedTransitions.ino) for more details 149 | 150 | ### Guard Conditions 151 | 152 | * Guard conditions are evaluated before a transition takes places 153 | * Only if the guard condition is true the transition will be executed 154 | * Both regular and timed transitions can have guard conditions 155 | * You define guard conditions as functions: 156 | 157 | ```c++ 158 | TimedTransition timedTransitions[] = { 159 | TimedTransition(&s[0], &s[1], 1000, NULL, "", zero_yet), 160 | TimedTransition(&s[0], &s[0], 1000, NULL, "", not_zero_yet) 161 | }; 162 | 163 | bool not_zero_yet() { 164 | return countdown != 0; 165 | } 166 | 167 | bool zero_yet() { 168 | return countdown == 0; 169 | } 170 | ``` 171 | 172 | * See [Guards.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/Guards/Guards.ino) for a complete example 173 | 174 | ### In-State Interval 175 | 176 | * States can have up to three events... 177 | * when they enter a state 178 | * while they are in a state 179 | * ...and when they leave the state 180 | 181 | ```c++ 182 | State(String name, CallbackFunction on_enter, CallbackFunction on_state = NULL, CallbackFunction on_exit = NULL, bool is_final = false); 183 | ``` 184 | 185 | * To define how frequent the `on_state` event is called, pass an interval (in ms) to the `run()` function in your main `loop()`: 186 | 187 | ```c++ 188 | void run(int interval = 1000, CallbackFunction tick_cb = NULL); 189 | ``` 190 | 191 | * After this interval has passed the `on_state` function of the active state will be called (if it defined) 192 | * Also the `tick_cb`callback function will be called (if defined) in the `run()` call 193 | 194 | ### Helper functions 195 | 196 | * SimpleFSM provides a few functions to check on the state of the machine: 197 | 198 | ```c++ 199 | State* getState() const; 200 | State* getPreviousState() const; 201 | bool isInState(State* state) const; 202 | int lastTransitionedAt() const; 203 | bool isFinished() const; 204 | 205 | ``` 206 | 207 | ### GraphViz Generation 208 | 209 | * Use the function `getDotDefinition()` to get your state machine definition in the GraphViz [dot format](https://www.graphviz.org/doc/info/lang.html) 210 | * Here the output for the [MixedTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/MixedTransitions/MixedTransitions.ino) example: 211 | 212 | ```c++ 213 | digraph G { 214 | rankdir=LR; pad=0.5 215 | node [shape=circle fixedsize=true width=1.5]; 216 | "red light" -> "green light" [label=" (6000ms)"]; 217 | "green light" -> "red light" [label=" (4000ms)"]; 218 | "button pressed" -> "green light" [label=" (2000ms)"]; 219 | "red light" -> "button pressed" [label=" (ID=1)"]; 220 | "red light" [style=filled fontcolor=white fillcolor=black]; 221 | } 222 | ``` 223 | 224 | * You can use this visualize your state machine:\ 225 | ![MixedTransitions.ino example](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/MixedTransitions/MixedTransitions.svg?raw=true) 226 | 227 | * If the machine is running, the current state will be highlighted 228 | * Currently guard functions and end states are not shown in the graph 229 | * See [MixedTransitionsBrowser.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/MixedTransitionsBrowser/MixedTransitionsBrowser.ino) to learn how to run a webserver to show the Graphviz diagram of your state machine 230 | 231 | ## Class Definitions 232 | 233 | * [State.h](https://github.com/LennartHennigs/SimpleFSM/blob/master/src/State.h) 234 | * [Transitions.h](https://github.com/LennartHennigs/SimpleFSM/blob/master/src/Transitions.h) for the class definition of both transitions 235 | * [SimpleFSM](https://github.com/LennartHennigs/SimpleFSM/blob/master/src/SimpleFSM.h) 236 | 237 | ## Examples 238 | 239 | * [SimpleTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/SimpleTransitions/SimpleTransitions.ino) - only regular transitions and showcasing the different events 240 | * [SimpleTransitionWithButtons.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/SimpleTransitionWithButton/SimpleTransitionWithButton.ino) - event is now triggered via a hardware button 241 | * [TimedTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/TimedTransitions/TimedTransitions.ino) - showcasing timed transitions 242 | * [MixedTransitions.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/MixedTransitions/MixedTransitions.ino) - regular and timed transitions 243 | * [MixedTransitionsBrowser.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/MixedTransitionsBrowser/MixedTransitionsBrowser.ino) - creates a webserver to show the Graphviz diagram of the state machine 244 | * [Guards.ino](https://github.com/LennartHennigs/SimpleFSM/blob/master/examples/Guards/Guards.ino) - showing how to define guard functions 245 | 246 | ## Notes 247 | 248 | * This libary is heavily inspiried by the [Arduino-fsm](https://github.com/jonblack/arduino-fsm) library created by [Jon Black](https://github.com/jonblack). I initally used some of his as a base. Without Jon's work this library would not exist. 249 | * To see the latest changes to the library please take a look at the [Changelog](https://github.com/LennartHennigs/SimpleFSM/blob/master/CHANGELOG.md). 250 | * And if you find this library helpful, please consider giving it a star at [GitHub](https://github.com/LennartHennigs/SimpleFSM). Thanks! 251 | 252 | ## How To Install 253 | 254 | Open the Arduino IDE choose "Sketch > Include Library" and search for "SimpleFSM". 255 | Or download the ZIP archive (), and choose "Sketch > Include Library > Add .ZIP Library..." and select the downloaded file. 256 | 257 | ## License 258 | 259 | MIT License 260 | 261 | Copyright (c) 2023 Lennart Hennigs 262 | 263 | Permission is hereby granted, free of charge, to any person obtaining a copy 264 | of this software and associated documentation files (the "Software"), to deal 265 | in the Software without restriction, including without limitation the rights 266 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 267 | copies of the Software, and to permit persons to whom the Software is 268 | furnished to do so, subject to the following conditions: 269 | 270 | The above copyright notice and this permission notice shall be included in all 271 | copies or substantial portions of the Software. 272 | 273 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 274 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 275 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 276 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 277 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 278 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 279 | SOFTWARE. 280 | -------------------------------------------------------------------------------- /src/SimpleFSM.cpp: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////// 2 | #include "SimpleFSM.h" 3 | 4 | #include "State.h" 5 | #include "Transitions.h" 6 | ///////////////////////////////////////////////////////////////// 7 | 8 | SimpleFSM::SimpleFSM() { 9 | } 10 | 11 | ///////////////////////////////////////////////////////////////// 12 | /* 13 | * Constructor. 14 | */ 15 | 16 | SimpleFSM::SimpleFSM(State* initial_state) { 17 | SimpleFSM(); 18 | setInitialState(initial_state); 19 | } 20 | 21 | ///////////////////////////////////////////////////////////////// 22 | /* 23 | * Destructor. 24 | */ 25 | 26 | SimpleFSM::~SimpleFSM() { 27 | transitions = NULL; 28 | timed = NULL; 29 | } 30 | 31 | ///////////////////////////////////////////////////////////////// 32 | /* 33 | * Get the number of transitions. 34 | */ 35 | 36 | int SimpleFSM::getTransitionCount() const { 37 | return num_standard; 38 | } 39 | 40 | ///////////////////////////////////////////////////////////////// 41 | /* 42 | * Get the number of timed transitions. 43 | */ 44 | 45 | int SimpleFSM::getTimedTransitionCount() const { 46 | return num_timed; 47 | } 48 | 49 | ///////////////////////////////////////////////////////////////// 50 | /* 51 | * Reset the FSM to its initial state. 52 | */ 53 | 54 | void SimpleFSM::reset() { 55 | is_initialized = false; 56 | is_finished = false; 57 | last_run = 0; 58 | last_transition = 0; 59 | setInitialState(inital_state); 60 | current_state = NULL; 61 | prev_state = NULL; 62 | 63 | for (int i = 0; i < num_timed; i++) { 64 | timed[i].reset(); 65 | } 66 | } 67 | 68 | ///////////////////////////////////////////////////////////////// 69 | /* 70 | * Set the initial state. 71 | */ 72 | 73 | void SimpleFSM::setInitialState(State* state) { 74 | inital_state = state; 75 | } 76 | 77 | ///////////////////////////////////////////////////////////////// 78 | /* 79 | * Trigger an event. 80 | */ 81 | 82 | bool SimpleFSM::trigger(int event_id) { 83 | if (!is_initialized) _initFSM(); 84 | // Find the transition with the current state and given event 85 | for (int i = 0; i < num_standard; i++) { 86 | if (transitions[i].from == current_state && transitions[i].event_id == event_id) { 87 | return _transitionTo(&(transitions[i])); 88 | } 89 | } 90 | return false; 91 | } 92 | 93 | ///////////////////////////////////////////////////////////////// 94 | /* 95 | * Get the previous state. 96 | */ 97 | 98 | State* SimpleFSM::getPreviousState() const { 99 | return prev_state; 100 | } 101 | 102 | ///////////////////////////////////////////////////////////////// 103 | /* 104 | * Get the current state. 105 | */ 106 | 107 | State* SimpleFSM::getState() const { 108 | return current_state; 109 | } 110 | 111 | ///////////////////////////////////////////////////////////////// 112 | /* 113 | * Check if the FSM is in a given state. 114 | */ 115 | 116 | bool SimpleFSM::isInState(State* t) const { 117 | return t == current_state; 118 | } 119 | 120 | ///////////////////////////////////////////////////////////////// 121 | /* 122 | * Sets the transition handler. 123 | */ 124 | 125 | void SimpleFSM::setTransitionHandler(CallbackFunction f) { 126 | on_transition_cb = f; 127 | } 128 | 129 | ///////////////////////////////////////////////////////////////// 130 | /* 131 | * Add transitions to the FSM. 132 | * 133 | * @param t[] An array of transitions. 134 | * @param size The size of the array. 135 | */ 136 | 137 | void SimpleFSM::add(Transition newTransitions[], int size) { 138 | // Count the number of unique transitions 139 | int uniqueCount = 0; 140 | for (int i = 0; i < size; ++i) { 141 | if (!_isDuplicate(newTransitions[i], transitions, num_standard) && 142 | !_isDuplicate(newTransitions[i], newTransitions, i)) { 143 | uniqueCount++; 144 | } 145 | } 146 | // Allocate or expand storage for transitions with exact size 147 | Transition* temp = new Transition[num_standard + uniqueCount]; 148 | if (transitions != NULL) { 149 | memcpy((void*)temp, transitions, num_standard * sizeof(Transition)); 150 | delete[] transitions; 151 | } 152 | transitions = temp; 153 | // Check if memory allocation was successful 154 | if (transitions == NULL) { 155 | Serial.print("Out of storage"); 156 | abort(); 157 | } 158 | // Add new transitions, avoiding duplicates 159 | for (int i = 0; i < size; ++i) { 160 | if (!_isDuplicate(newTransitions[i], transitions, num_standard) && 161 | !_isDuplicate(newTransitions[i], newTransitions, i)) { 162 | transitions[num_standard] = newTransitions[i]; 163 | _addDotTransition(transitions[num_standard]); 164 | num_standard++; 165 | } 166 | } 167 | } 168 | 169 | ///////////////////////////////////////////////////////////////// 170 | /* 171 | * Add timed transitions to the FSM. 172 | * 173 | * @param t[] An array of timed transitions. 174 | * @param size The size of the array. 175 | */ 176 | 177 | void SimpleFSM::add(TimedTransition newTransitions[], int size) { 178 | // Count the number of unique transitions 179 | int uniqueCount = 0; 180 | for (int i = 0; i < size; ++i) { 181 | if (!_isDuplicate(newTransitions[i], timed, num_timed) && 182 | !_isDuplicate(newTransitions[i], newTransitions, i)) { 183 | uniqueCount++; 184 | } 185 | } 186 | // Allocate memory or expand existing storage with exact size 187 | TimedTransition* temp = new TimedTransition[num_timed + uniqueCount]; 188 | if (timed != NULL) { 189 | memcpy((void*)temp, timed, num_timed * sizeof(TimedTransition)); 190 | delete[] timed; 191 | } 192 | timed = temp; 193 | // Check memory allocation 194 | if (timed == NULL) { 195 | Serial.print("Out of storage"); 196 | abort(); 197 | } 198 | // Add new transitions while avoiding duplicates 199 | for (int i = 0; i < size; ++i) { 200 | if (!_isDuplicate(newTransitions[i], timed, num_timed) && 201 | !_isDuplicate(newTransitions[i], newTransitions, i)) { 202 | timed[num_timed] = newTransitions[i]; 203 | _addDotTransition(timed[num_timed]); 204 | num_timed++; 205 | } 206 | } 207 | } 208 | 209 | ///////////////////////////////////////////////////////////////// 210 | /* 211 | * Check if a timed transition is a duplicate. 212 | */ 213 | 214 | bool SimpleFSM::_isDuplicate(const TimedTransition& transition, const TimedTransition* transitionArray, int arraySize) const { 215 | for (int i = 0; i < arraySize; ++i) { 216 | if (transitionArray[i].from == transition.from && 217 | transitionArray[i].to == transition.to && 218 | transitionArray[i].interval == transition.interval) { 219 | return true; 220 | } 221 | } 222 | return false; 223 | } 224 | 225 | ///////////////////////////////////////////////////////////////// 226 | /* 227 | * Check if a transition is a duplicate. 228 | */ 229 | 230 | bool SimpleFSM::_isDuplicate(const Transition& transition, const Transition* transitionArray, int arraySize) const { 231 | for (int i = 0; i < arraySize; ++i) { 232 | if (transitionArray[i].from == transition.from && 233 | transitionArray[i].to == transition.to && 234 | transitionArray[i].event_id == transition.event_id) { 235 | return true; 236 | } 237 | } 238 | return false; 239 | } 240 | 241 | ///////////////////////////////////////////////////////////////// 242 | /* 243 | * Set the finished handler. 244 | */ 245 | 246 | void SimpleFSM::setFinishedHandler(CallbackFunction f) { 247 | finished_cb = f; 248 | } 249 | 250 | ///////////////////////////////////////////////////////////////// 251 | /* 252 | * Get the time since the last transition. 253 | */ 254 | 255 | unsigned long SimpleFSM::lastTransitioned() const { 256 | return (last_transition == 0) ? 0 : (millis() - last_transition); 257 | } 258 | 259 | ///////////////////////////////////////////////////////////////// 260 | /* 261 | * Check if the FSM is finished. 262 | */ 263 | 264 | bool SimpleFSM::isFinished() const { 265 | return is_finished; 266 | } 267 | 268 | ///////////////////////////////////////////////////////////////// 269 | /* 270 | * Run the FSM. 271 | * interval: The interval in milliseconds. 272 | * tick_cb: A callback function that is called on every tick. 273 | */ 274 | 275 | void SimpleFSM::run(int interval /* = 1000 */, CallbackFunction tick_cb /* = NULL */) { 276 | unsigned long now = millis(); 277 | // is the machine set up? 278 | if (!is_initialized) _initFSM(); 279 | // are we ok? 280 | if (current_state == NULL) return; 281 | // is it time? 282 | if (!_isTimeForRun(now, interval)) return; 283 | // are we done yet? 284 | if (is_finished) return; 285 | // save the time 286 | last_run = now; 287 | // go through the timed events 288 | _handleTimedEvents(now); 289 | // trigger the on_state event 290 | if (current_state->on_state != NULL) current_state->on_state(); 291 | // trigger the regular tick event 292 | if (tick_cb != NULL) tick_cb(); 293 | } 294 | 295 | ///////////////////////////////////////////////////////////////// 296 | 297 | bool SimpleFSM::_isTimeForRun(unsigned long now, int interval) { 298 | return now >= last_run + interval; 299 | } 300 | 301 | ///////////////////////////////////////////////////////////////// 302 | 303 | void SimpleFSM::_handleTimedEvents(unsigned long now) { 304 | for (int i = 0; i < num_timed; i++) { 305 | if (timed[i].from != current_state) continue; 306 | // start the transition timer 307 | if (timed[i].start == 0) { 308 | timed[i].start = now; 309 | continue; 310 | } 311 | // reached the interval? 312 | if (now - timed[i].start >= timed[i].interval) { 313 | if (_transitionTo(&timed[i])) { 314 | timed[i].start = 0; 315 | return; 316 | } 317 | } 318 | } 319 | } 320 | 321 | ///////////////////////////////////////////////////////////////// 322 | /* 323 | * Initialize the FSM. 324 | */ 325 | 326 | bool SimpleFSM::_initFSM() { 327 | if (is_initialized) return false; 328 | is_initialized = true; 329 | if (inital_state == NULL) return false; 330 | return _changeToState(inital_state, millis()); 331 | } 332 | 333 | ///////////////////////////////////////////////////////////////// 334 | /* 335 | * Change to a new state. 336 | */ 337 | 338 | bool SimpleFSM::_changeToState(State* s, unsigned long now) { 339 | if (s == NULL) return false; 340 | // set the new state 341 | prev_state = current_state; 342 | current_state = s; 343 | if (s->on_enter != NULL) s->on_enter(); 344 | // save the time 345 | last_run = now; 346 | last_transition = now; 347 | // is this the end? 348 | if (s->is_final && finished_cb != NULL) finished_cb(); 349 | if (s->is_final) is_finished = true; 350 | return true; 351 | } 352 | 353 | ///////////////////////////////////////////////////////////////// 354 | /* 355 | * Get the DOT definition of the FSM. 356 | */ 357 | 358 | String SimpleFSM::getDotDefinition() { 359 | return "digraph G {\n" + _dot_header() + dot_definition + _dot_active_node() + _dot_inital_state() + "}\n"; 360 | } 361 | 362 | ///////////////////////////////////////////////////////////////// 363 | 364 | bool SimpleFSM::_transitionTo(AbstractTransition* transition) { 365 | // empty parameter? 366 | if (transition->to == NULL) return false; 367 | // can I pass the guard 368 | if (transition->guard_cb != NULL && !transition->guard_cb()) return false; 369 | // trigger events 370 | if (transition->from->on_exit != NULL) transition->from->on_exit(); 371 | if (transition->on_run_cb != NULL) transition->on_run_cb(); 372 | if (on_transition_cb != NULL) on_transition_cb(); 373 | return _changeToState(transition->to, millis()); 374 | } 375 | 376 | ///////////////////////////////////////////////////////////////// 377 | 378 | String SimpleFSM::_dot_transition(String from, String to, String label, String param) { 379 | return "\t\"" + from + "\" -> \"" + to + "\"" + " [label=\"" + label + " (" + param + ")\"];\n"; 380 | } 381 | 382 | ///////////////////////////////////////////////////////////////// 383 | 384 | String SimpleFSM::_dot_inital_state() { 385 | return inital_state ? "\t\"" + inital_state->getName() + "\" [style=filled fontcolor=white fillcolor=black];\n\n" : ""; 386 | } 387 | 388 | ///////////////////////////////////////////////////////////////// 389 | 390 | String SimpleFSM::_dot_active_node() { 391 | return current_state ? "\t\"" + current_state->getName() + "\" [style=filled fontcolor=white];\n" : ""; 392 | } 393 | 394 | ///////////////////////////////////////////////////////////////// 395 | 396 | String SimpleFSM::_dot_header() { 397 | return "\trankdir=LR; pad=0.5\n\tnode [shape=circle fixedsize=true width=1.5];\n"; 398 | } 399 | 400 | ///////////////////////////////////////////////////////////////// 401 | 402 | void SimpleFSM::_addDotTransition(Transition& t) { 403 | dot_definition = dot_definition + _dot_transition(t.from->getName(), t.to->getName(), t.getName(), "ID=" + String(t.event_id)); 404 | } 405 | 406 | ///////////////////////////////////////////////////////////////// 407 | 408 | void SimpleFSM::_addDotTransition(TimedTransition& t) { 409 | dot_definition = dot_definition + _dot_transition(t.from->getName(), t.to->getName(), t.getName(), String(t.getInterval()) + "ms"); 410 | } 411 | 412 | ///////////////////////////////////////////////////////////////// 413 | --------------------------------------------------------------------------------