From 2d3b063df682bac6d6cd83026cb09dfc42f04c01 Mon Sep 17 00:00:00 2001 From: Jeremy Overesch <10420943+jovere@users.noreply.github.com> Date: Thu, 22 Apr 2021 02:40:53 -0500 Subject: [PATCH 1/6] Add hierarchical FSM capabilities to the FSM. (#374) In order to work properly, states with no change need to return ifsm_state::NO_CHANGE rather than their given state id. Otherwise, when an event isn't handled, it will return the parent state rather than the active state. Also, in this implementation, a state cannot return a different state during the on_enter_state() function. An assert has been added to check for that. --- include/etl/fsm.h | 163 +++-- include/etl/generators/fsm_generator.h | 131 ++++- include/etl/hfsm.h | 166 ++++++ meson.build | 1 + test/CMakeLists.txt | 1 + test/test_fsm.cpp | 2 +- test/test_hfsm.cpp | 785 +++++++++++++++++++++++++ 7 files changed, 1170 insertions(+), 79 deletions(-) create mode 100644 include/etl/hfsm.h create mode 100644 test/test_hfsm.cpp diff --git a/include/etl/fsm.h b/include/etl/fsm.h index 816a164f..7618d334 100644 --- a/include/etl/fsm.h +++ b/include/etl/fsm.h @@ -68,6 +68,7 @@ SOFTWARE. namespace etl { class fsm; + class hfsm; /// Allow alternative type for state id. #if !defined(ETL_FSM_STATE_ID_TYPE) @@ -79,6 +80,13 @@ namespace etl // For internal FSM use. typedef typename etl::larger_type::type fsm_internal_id_t; + template + class fsm_state; + //*************************************************************************** /// Base exception class for FSM. //*************************************************************************** @@ -144,6 +152,15 @@ namespace etl } }; + class fsm_state_enter_state_change_forbidden : public etl::fsm_exception + { + public: + fsm_state_enter_state_change_forbidden(string_type file_name_, numeric_type line_number_) + : etl::fsm_exception(ETL_ERROR_TEXT("fsm:enter state change in composite state forbidden", ETL_FSM_FILE_ID"E"), file_name_, line_number_) + { + } + }; + //*************************************************************************** /// Interface class for FSM states. //*************************************************************************** @@ -151,8 +168,19 @@ namespace etl { public: + // Pass this whenever no state change is desired. Specifically cast to + // Highest unsigned value of fsm_state_id_t. + static const fsm_state_id_t NO_CHANGE = static_cast(-1); + /// Allows ifsm_state functions to be private. friend class etl::fsm; + friend class etl::hfsm; + template + friend class etl::fsm_state; //******************************************* /// Gets the id for this state. @@ -162,6 +190,26 @@ namespace etl return state_id; } + //******************************************* + /// Adds a child to this state. + //******************************************* + void add_child(etl::ifsm_state& state) + { + ETL_ASSERT(state.p_parent == ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + state.p_parent = this; + } + + //******************************************* + /// Adds the initial child to this state. + //******************************************* + void add_initial_child(etl::ifsm_state& state) + { + ETL_ASSERT(p_default_active_child == ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + add_child(state); + p_active_child = &state; + p_default_active_child = &state; + } + protected: //******************************************* @@ -169,7 +217,10 @@ namespace etl //******************************************* ifsm_state(etl::fsm_state_id_t state_id_) : state_id(state_id_), - p_context(ETL_NULLPTR) + p_context(ETL_NULLPTR), + p_parent(ETL_NULLPTR), + p_active_child(ETL_NULLPTR), + p_default_active_child(ETL_NULLPTR) { } @@ -190,7 +241,7 @@ namespace etl virtual fsm_state_id_t process_event(const etl::imessage& message) = 0; - virtual fsm_state_id_t on_enter_state() { return state_id; } // By default, do nothing. + virtual fsm_state_id_t on_enter_state() { return NO_CHANGE; } // By default, do nothing. virtual void on_exit_state() {} // By default, do nothing. //******************************************* @@ -205,6 +256,15 @@ namespace etl // A pointer to the FSM context. etl::fsm* p_context; + // A pointer to the parent. + ifsm_state* p_parent; + + // A pointer to the active child. + ifsm_state* p_active_child; + + // A pointer to the default active child. + ifsm_state* p_default_active_child; + // Disabled. ifsm_state(const ifsm_state&); ifsm_state& operator =(const ifsm_state&); @@ -217,6 +277,7 @@ namespace etl { public: + friend etl::hfsm; using imessage_router::receive; //******************************************* @@ -240,6 +301,7 @@ namespace etl number_of_states = etl::fsm_state_id_t(size); ETL_ASSERT(number_of_states > 0, ETL_ERROR(etl::fsm_state_list_exception)); + ETL_ASSERT(number_of_states < ifsm_state::NO_CHANGE, ETL_ERROR(etl::fsm_state_list_exception)); for (etl::fsm_state_id_t i = 0; i < size; ++i) { @@ -257,26 +319,29 @@ namespace etl //******************************************* void start(bool call_on_enter_state = true) { - // Can only be started once. - if (p_state == ETL_NULLPTR) - { - p_state = state_list[0]; - ETL_ASSERT(p_state != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + // Can only be started once. + if (p_state == ETL_NULLPTR) + { + p_state = state_list[0]; + ETL_ASSERT(p_state != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); - if (call_on_enter_state) - { - etl::fsm_state_id_t next_state_id; - etl::ifsm_state* p_last_state; + if (call_on_enter_state) + { + etl::fsm_state_id_t next_state_id; + etl::ifsm_state* p_last_state; - do - { - p_last_state = p_state; - next_state_id = p_state->on_enter_state(); - p_state = state_list[next_state_id]; - - } while (p_last_state != p_state); - } - } + do + { + p_last_state = p_state; + next_state_id = p_state->on_enter_state(); + if (next_state_id != ifsm_state::NO_CHANGE) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_state = state_list[next_state_id]; + } + } while (p_last_state != p_state); + } + } } //******************************************* @@ -284,9 +349,11 @@ namespace etl //******************************************* void receive(const etl::imessage& message) ETL_OVERRIDE { - etl::fsm_state_id_t next_state_id = p_state->process_event(message); - ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + etl::fsm_state_id_t next_state_id = p_state->process_event(message); + if(next_state_id != ifsm_state::NO_CHANGE) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); etl::ifsm_state* p_next_state = state_list[next_state_id]; // Have we changed state? @@ -298,12 +365,14 @@ namespace etl p_state = p_next_state; next_state_id = p_state->on_enter_state(); - ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); - - p_next_state = state_list[next_state_id]; - + if(next_state_id != ifsm_state::NO_CHANGE) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_next_state = state_list[next_state_id]; + } } while (p_next_state != p_state); // Have we changed state again? } + } } using imessage_router::accepts; @@ -395,10 +464,10 @@ namespace etl // The definition for all 16 message types. //*************************************************************************** template + typename T1, typename T2, typename T3, typename T4, + typename T5, typename T6, typename T7, typename T8, + typename T9, typename T10, typename T11, typename T12, + typename T13, typename T14, typename T15, typename T16> class fsm_state : public ifsm_state { public: @@ -449,7 +518,7 @@ namespace etl case T14::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T15::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T16::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -513,7 +582,7 @@ namespace etl case T13::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T14::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T15::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -576,7 +645,7 @@ namespace etl case T12::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T13::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T14::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -638,7 +707,7 @@ namespace etl case T11::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T12::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T13::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -698,7 +767,7 @@ namespace etl case T10::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T11::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T12::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -757,7 +826,7 @@ namespace etl case T9::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T10::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T11::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -815,7 +884,7 @@ namespace etl case T8::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T9::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T10::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -872,7 +941,7 @@ namespace etl case T7::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T8::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T9::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -927,7 +996,7 @@ namespace etl case T6::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T7::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T8::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -981,7 +1050,7 @@ namespace etl case T5::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T6::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T7::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -1034,7 +1103,7 @@ namespace etl case T4::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T5::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T6::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -1086,7 +1155,7 @@ namespace etl case T3::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T4::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T5::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -1136,7 +1205,7 @@ namespace etl case T2::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T3::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T4::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -1185,7 +1254,7 @@ namespace etl case T1::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T2::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T3::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -1233,7 +1302,7 @@ namespace etl { case T1::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; case T2::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -1280,7 +1349,7 @@ namespace etl switch (event_id) { case T1::ID: new_state_id = static_cast(this)->on_event(static_cast(message)); break; - default: new_state_id = static_cast(this)->on_event_unknown(message); break; + default: new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); break; } return new_state_id; @@ -1319,7 +1388,7 @@ namespace etl etl::fsm_state_id_t process_event(const etl::imessage& message) { - return static_cast(this)->on_event_unknown(message); + return p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message); } }; } diff --git a/include/etl/generators/fsm_generator.h b/include/etl/generators/fsm_generator.h index d3c6eb8b..12a665c3 100644 --- a/include/etl/generators/fsm_generator.h +++ b/include/etl/generators/fsm_generator.h @@ -80,6 +80,7 @@ cog.outl("//******************************************************************** namespace etl { class fsm; + class hfsm; /// Allow alternative type for state id. #if !defined(ETL_FSM_STATE_ID_TYPE) @@ -91,6 +92,13 @@ namespace etl // For internal FSM use. typedef typename etl::larger_type::type fsm_internal_id_t; + template + class fsm_state; + //*************************************************************************** /// Base exception class for FSM. //*************************************************************************** @@ -156,6 +164,15 @@ namespace etl } }; + class fsm_state_enter_state_change_forbidden : public etl::fsm_exception + { + public: + fsm_state_enter_state_change_forbidden(string_type file_name_, numeric_type line_number_) + : etl::fsm_exception(ETL_ERROR_TEXT("fsm:enter state change in composite state forbidden", ETL_FSM_FILE_ID"E"), file_name_, line_number_) + { + } + }; + //*************************************************************************** /// Interface class for FSM states. //*************************************************************************** @@ -163,8 +180,19 @@ namespace etl { public: + // Pass this whenever no state change is desired. Specifically cast to + // Highest unsigned value of fsm_state_id_t. + static const fsm_state_id_t NO_CHANGE = static_cast(-1); + /// Allows ifsm_state functions to be private. friend class etl::fsm; + friend class etl::hfsm; + template + friend class etl::fsm_state; //******************************************* /// Gets the id for this state. @@ -174,6 +202,26 @@ namespace etl return state_id; } + //******************************************* + /// Adds a child to this state. + //******************************************* + void add_child(etl::ifsm_state& state) + { + ETL_ASSERT(state.p_parent == ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + state.p_parent = this; + } + + //******************************************* + /// Adds the initial child to this state. + //******************************************* + void add_initial_child(etl::ifsm_state& state) + { + ETL_ASSERT(p_default_active_child == ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + add_child(state); + p_active_child = &state; + p_default_active_child = &state; + } + protected: //******************************************* @@ -181,7 +229,10 @@ namespace etl //******************************************* ifsm_state(etl::fsm_state_id_t state_id_) : state_id(state_id_), - p_context(ETL_NULLPTR) + p_context(ETL_NULLPTR), + p_parent(ETL_NULLPTR), + p_active_child(ETL_NULLPTR), + p_default_active_child(ETL_NULLPTR) { } @@ -202,7 +253,7 @@ namespace etl virtual fsm_state_id_t process_event(const etl::imessage& message) = 0; - virtual fsm_state_id_t on_enter_state() { return state_id; } // By default, do nothing. + virtual fsm_state_id_t on_enter_state() { return NO_CHANGE; } // By default, do nothing. virtual void on_exit_state() {} // By default, do nothing. //******************************************* @@ -217,6 +268,15 @@ namespace etl // A pointer to the FSM context. etl::fsm* p_context; + // A pointer to the parent. + ifsm_state* p_parent; + + // A pointer to the active child. + ifsm_state* p_active_child; + + // A pointer to the default active child. + ifsm_state* p_default_active_child; + // Disabled. ifsm_state(const ifsm_state&); ifsm_state& operator =(const ifsm_state&); @@ -229,6 +289,7 @@ namespace etl { public: + friend etl::hfsm; using imessage_router::receive; //******************************************* @@ -252,6 +313,7 @@ namespace etl number_of_states = etl::fsm_state_id_t(size); ETL_ASSERT(number_of_states > 0, ETL_ERROR(etl::fsm_state_list_exception)); + ETL_ASSERT(number_of_states < ifsm_state::NO_CHANGE, ETL_ERROR(etl::fsm_state_list_exception)); for (etl::fsm_state_id_t i = 0; i < size; ++i) { @@ -269,26 +331,29 @@ namespace etl //******************************************* void start(bool call_on_enter_state = true) { - // Can only be started once. - if (p_state == ETL_NULLPTR) - { - p_state = state_list[0]; - ETL_ASSERT(p_state != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + // Can only be started once. + if (p_state == ETL_NULLPTR) + { + p_state = state_list[0]; + ETL_ASSERT(p_state != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); - if (call_on_enter_state) - { - etl::fsm_state_id_t next_state_id; - etl::ifsm_state* p_last_state; + if (call_on_enter_state) + { + etl::fsm_state_id_t next_state_id; + etl::ifsm_state* p_last_state; - do - { - p_last_state = p_state; - next_state_id = p_state->on_enter_state(); - p_state = state_list[next_state_id]; - - } while (p_last_state != p_state); - } - } + do + { + p_last_state = p_state; + next_state_id = p_state->on_enter_state(); + if (next_state_id != ifsm_state::NO_CHANGE) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_state = state_list[next_state_id]; + } + } while (p_last_state != p_state); + } + } } //******************************************* @@ -296,9 +361,11 @@ namespace etl //******************************************* void receive(const etl::imessage& message) ETL_OVERRIDE { - etl::fsm_state_id_t next_state_id = p_state->process_event(message); - ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + etl::fsm_state_id_t next_state_id = p_state->process_event(message); + if(next_state_id != ifsm_state::NO_CHANGE) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); etl::ifsm_state* p_next_state = state_list[next_state_id]; // Have we changed state? @@ -310,12 +377,14 @@ namespace etl p_state = p_next_state; next_state_id = p_state->on_enter_state(); - ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); - - p_next_state = state_list[next_state_id]; - + if(next_state_id != ifsm_state::NO_CHANGE) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_next_state = state_list[next_state_id]; + } } while (p_next_state != p_state); // Have we changed state again? } + } } using imessage_router::accepts; @@ -414,11 +483,11 @@ namespace etl cog.outl("template " % Handlers) + cog.outl("typename T%s>" % Handlers) cog.outl("class fsm_state : public ifsm_state") cog.outl("{") cog.outl("public:") @@ -458,7 +527,7 @@ namespace etl cog.out(" new_state_id = static_cast(this)->on_event(static_cast(message));" % n) cog.outl(" break;") cog.out(" default:") - cog.out(" new_state_id = static_cast(this)->on_event_unknown(message);") + cog.out(" new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message);") cog.outl(" break;") cog.outl(" }") cog.outl("") @@ -535,7 +604,7 @@ namespace etl cog.out(" new_state_id = static_cast(this)->on_event(static_cast(message));" % n) cog.outl(" break;") cog.out(" default:") - cog.out(" new_state_id = static_cast(this)->on_event_unknown(message);") + cog.out(" new_state_id = p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message);") cog.outl(" break;") cog.outl(" }") cog.outl("") @@ -584,7 +653,7 @@ namespace etl cog.outl("") cog.outl(" etl::fsm_state_id_t process_event(const etl::imessage& message)") cog.outl(" {") - cog.outl(" return static_cast(this)->on_event_unknown(message);") + cog.outl(" return p_parent ? p_parent->process_event(message) : static_cast(this)->on_event_unknown(message);") cog.outl(" }") cog.outl("};") ]]]*/ diff --git a/include/etl/hfsm.h b/include/etl/hfsm.h new file mode 100644 index 00000000..24475c40 --- /dev/null +++ b/include/etl/hfsm.h @@ -0,0 +1,166 @@ +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2021 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ + +#ifndef ETL_HFSM_INCLUDED +#define ETL_HFSM_INCLUDED + +#include "fsm.h" + +namespace etl +{ + + //*************************************************************************** + /// The HFSM class. + //*************************************************************************** + class hfsm : public etl::fsm + { + public: + + //******************************************* + /// Constructor. + //******************************************* + hfsm(etl::message_router_id_t id) + : fsm(id) + { + } + + using fsm::receive; + + //******************************************* + /// Top level message handler for the FSM. + //******************************************* + void receive(const etl::imessage& message) ETL_OVERRIDE + { + etl::fsm_state_id_t next_state_id = p_state->process_event(message); + + if(next_state_id != ifsm_state::NO_CHANGE) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + etl::ifsm_state* p_next_state = state_list[next_state_id]; + + // Have we changed state? + if(p_next_state != p_state) + { + etl::ifsm_state* p_root = common_ancestor(p_state, p_next_state); + do_exits(p_root, p_state); + + p_state = p_next_state; + + next_state_id = do_enters(p_root, p_next_state, true); + if(next_state_id != ifsm_state::NO_CHANGE) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_state = state_list[next_state_id]; + } + } + } + } + + static etl::ifsm_state* common_ancestor(etl::ifsm_state* p_one, etl::ifsm_state* p_two) + { + if(p_one == p_two) + { + return p_one; + } + + etl::ifsm_state* p_current_state = p_one; + + while (p_current_state->p_parent != ETL_NULLPTR) + { + if (p_current_state->p_parent == p_two) + { + return p_current_state->p_parent; + } + + p_current_state = p_current_state->p_parent; + } + + if(p_two->p_parent != ETL_NULLPTR) + { + return common_ancestor(p_one, p_two->p_parent); + } + + return ETL_NULLPTR; + } + + static void do_exits(const etl::ifsm_state* p_root, etl::ifsm_state* p_source) + { + etl::ifsm_state* p_current = p_source; + + // Iterate down to the lowest child + while(p_current->p_active_child != ETL_NULLPTR) + { + p_current = p_current->p_active_child; + } + + // Run exit state on all states up to the root + while (p_current != p_root) + { + p_current->on_exit_state(); + p_current = p_current->p_parent; + } + } + + static etl::fsm_state_id_t do_enters(const etl::ifsm_state* p_root, etl::ifsm_state* p_target, bool activate_default_children) + { + ETL_ASSERT(p_target != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + + // We need to go recursively up the tree if the target and root don't match + if (p_root != p_target && p_target->p_parent != ETL_NULLPTR) + { + if(p_target->p_parent != p_root) + { + // The parent we're calling shouldn't activate its defaults, or this state will be deactivated. + do_enters(p_root, p_target->p_parent, false); + } + + // Set us as our parent's active child + p_target->p_parent->p_active_child = p_target; + } + + etl::fsm_state_id_t next_state = p_target->on_enter_state(); + ETL_ASSERT(ifsm_state::NO_CHANGE == next_state, ETL_ERROR(etl::fsm_state_enter_state_change_forbidden)); + + // Activate default child if we need to activate any initial states in an active composite state. + if (activate_default_children) + { + while (p_target->p_default_active_child != ETL_NULLPTR) + { + p_target = p_target->p_default_active_child; + p_target->p_parent->p_active_child = p_target; + next_state = p_target->on_enter_state(); + ETL_ASSERT(ifsm_state::NO_CHANGE == next_state, ETL_ERROR(etl::fsm_state_enter_state_change_forbidden)); + } + next_state = p_target->get_state_id(); + } + + return next_state; + } + }; +} +#endif diff --git a/meson.build b/meson.build index f01d5a17..e5f24c8e 100644 --- a/meson.build +++ b/meson.build @@ -54,6 +54,7 @@ etl_test_sources = files( 'test/test_fnv_1.cpp', 'test/test_forward_list.cpp', 'test/test_fsm.cpp', + 'test/test_hfsm.cpp', 'test/test_functional.cpp', 'test/test_function.cpp', 'test/test_hash.cpp', diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 25c4bb03..e91d9b3c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -121,6 +121,7 @@ set(TEST_SOURCE_FILES test_functional.cpp test_gamma.cpp test_hash.cpp + test_hfsm.cpp test_histogram.cpp test_indirect_vector.cpp test_indirect_vector_external_buffer.cpp diff --git a/test/test_fsm.cpp b/test/test_fsm.cpp index 386f585d..cb10c994 100644 --- a/test/test_fsm.cpp +++ b/test/test_fsm.cpp @@ -224,7 +224,7 @@ namespace etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; - return STATE_ID; + return NO_CHANGE; } //*********************************** diff --git a/test/test_hfsm.cpp b/test/test_hfsm.cpp new file mode 100644 index 00000000..1b0ec0f0 --- /dev/null +++ b/test/test_hfsm.cpp @@ -0,0 +1,785 @@ +/****************************************************************************** +The MIT License(MIT) + +Embedded Template Library. +https://github.com/ETLCPP/etl +https://www.etlcpp.com + +Copyright(c) 2021 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files(the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +******************************************************************************/ + +#include "unit_test_framework.h" + +#include "etl/hfsm.h" +#include "etl/enum_type.h" +#include "etl/container.h" +#include "etl/packet.h" +#include "etl/queue.h" + +#include + +// This test implements the following state machine: +// +--------------------------------------------+ +// | | +// | O running | +// | | | +// O | v | +// | | +-----------+ +-----------+ | +// v | | |Timeout | | | +// +------+ Start | | windingUp +------->| atSpeed | | +// | idle +-------+--->| | | | | +// +------+ | +------+----+ +------+----+ | +// ^ ^ | | | | +// | | | Stop | | | +// | | | v |Stop | +// | | | +------------------+ | | +// | | Stopped | | | | | +// | +----------+--------+ windingDown |<---+ | +// | | | | | +// | EStop | +------------------+ | +// +-------------+ | +// | | +// +--------------------------------------------+ +// Created with asciiflow.com +namespace +{ + const etl::message_router_id_t MOTOR_CONTROL = 0; + + + //*************************************************************************** + // Events + struct EventId + { + enum enum_type + { + START, + STOP, + ESTOP, + STOPPED, + SET_SPEED, + RECURSIVE, + TIMEOUT, + UNSUPPORTED + }; + + ETL_DECLARE_ENUM_TYPE(EventId, etl::message_id_t) + ETL_ENUM_TYPE(START, "Start") + ETL_ENUM_TYPE(STOP, "Stop") + ETL_ENUM_TYPE(ESTOP, "E-Stop") + ETL_ENUM_TYPE(STOPPED, "Stopped") + ETL_ENUM_TYPE(SET_SPEED, "Set Speed") + ETL_ENUM_TYPE(RECURSIVE, "Recursive") + ETL_ENUM_TYPE(TIMEOUT, "Timeout") + ETL_ENUM_TYPE(UNSUPPORTED, "Unsupported") + ETL_END_ENUM_TYPE + }; + + //*********************************** + class Start : public etl::message + { + }; + + //*********************************** + class Stop : public etl::message + { + }; + + //*********************************** + class EStop : public etl::message + { + }; + + //*********************************** + class SetSpeed : public etl::message + { + public: + + SetSpeed(int speed_) : speed(speed_) {} + + const int speed; + }; + + //*********************************** + class Stopped : public etl::message + { + }; + + //*********************************** + class Recursive : public etl::message + { + }; + + //*********************************** + class Timeout : public etl::message + { + }; + + //*********************************** + class Unsupported : public etl::message + { + }; + + //*************************************************************************** + // States + struct StateId + { + enum enum_type + { + IDLE, + RUNNING, + WINDING_UP, + WINDING_DOWN, + AT_SPEED, + NUMBER_OF_STATES + }; + + ETL_DECLARE_ENUM_TYPE(StateId, etl::fsm_state_id_t) + ETL_ENUM_TYPE(IDLE, "Idle") + ETL_ENUM_TYPE(RUNNING, "Running") + ETL_ENUM_TYPE(WINDING_UP, "Winding Up") + ETL_ENUM_TYPE(WINDING_DOWN, "Winding Down") + ETL_ENUM_TYPE(AT_SPEED, "At Speed") + ETL_END_ENUM_TYPE + }; + + //*********************************** + // The motor control FSM. + //*********************************** + class MotorControl : public etl::hfsm + { + public: + + MotorControl() + : hfsm(MOTOR_CONTROL) + { + } + + //*********************************** + void Initialise(etl::ifsm_state** p_states, size_t size) + { + set_states(p_states, size); + ClearStatistics(); + } + + //*********************************** + void ClearStatistics() + { + startCount = 0; + stopCount = 0; + setSpeedCount = 0; + windUpCompleteCount = 0; + windUpStartCount = 0; + unknownCount = 0; + stoppedCount = 0; + isLampOn = false; + speed = 0; + } + + //*********************************** + void SetSpeedValue(int speed_) + { + speed = speed_; + } + + //*********************************** + void TurnRunningLampOn() + { + isLampOn = true; + } + + //*********************************** + void TurnRunningLampOff() + { + isLampOn = false; + } + + //*********************************** + template + void queue_recursive_message(const T& message) + { + messageQueue.emplace(message); + } + + typedef etl::largest Largest_t; + + typedef etl::packet Packet_t; + + etl::queue messageQueue; + + int startCount; + int stopCount; + int windUpCompleteCount; + int windUpStartCount; + int setSpeedCount; + int unknownCount; + int stoppedCount; + bool isLampOn; + int speed; + }; + + //*********************************** + // The idle state. + //*********************************** + class Idle : public etl::fsm_state + { + public: + + //*********************************** + etl::fsm_state_id_t on_event(const Start&) + { + ++get_fsm_context().startCount; + return StateId::RUNNING; + } + + //*********************************** + etl::fsm_state_id_t on_event(const Recursive&) + { + get_fsm_context().queue_recursive_message(Start()); + return StateId::IDLE; + } + + //*********************************** + etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + ++get_fsm_context().unknownCount; + return NO_CHANGE; + } + + //*********************************** + etl::fsm_state_id_t on_enter_state() + { + get_fsm_context().TurnRunningLampOff(); + return NO_CHANGE; + } + }; + + //*********************************** + // The running state. + //*********************************** + class Running : public etl::fsm_state + { + public: + + //*********************************** + etl::fsm_state_id_t on_event(const EStop& event) + { + ++get_fsm_context().stopCount; + + return StateId::IDLE; + } + + //*********************************** + etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + ++get_fsm_context().unknownCount; + return NO_CHANGE; + } + + //*********************************** + etl::fsm_state_id_t on_enter_state() + { + get_fsm_context().TurnRunningLampOn(); + + return NO_CHANGE; + } + }; + + //*********************************** + // The winding up state. + //*********************************** + class WindingUp : public etl::fsm_state + { + public: + + //*********************************** + etl::fsm_state_id_t on_event(const Stop&) + { + ++get_fsm_context().stopCount; + return StateId::WINDING_DOWN; + } + + //*********************************** + etl::fsm_state_id_t on_event(const Timeout&) + { + ++get_fsm_context().windUpCompleteCount; + return StateId::AT_SPEED; + } + + //*********************************** + etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + ++get_fsm_context().unknownCount; + return NO_CHANGE; + } + + etl::fsm_state_id_t on_enter_state() + { + ++get_fsm_context().windUpStartCount; + return NO_CHANGE; + } + }; + + //*********************************** + // The at speed state. + //*********************************** + class AtSpeed : public etl::fsm_state + { + public: + //*********************************** + etl::fsm_state_id_t on_event(const Stop&) + { + ++get_fsm_context().stopCount; + return StateId::WINDING_DOWN; + } + + //*********************************** + etl::fsm_state_id_t on_event(const SetSpeed& event) + { + ++get_fsm_context().setSpeedCount; + get_fsm_context().SetSpeedValue(event.speed); + return NO_CHANGE; + } + + //*********************************** + etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + ++get_fsm_context().unknownCount; + return NO_CHANGE; + } + }; + + //*********************************** + // The winding down state. + //*********************************** + class WindingDown : public etl::fsm_state + { + public: + + //*********************************** + etl::fsm_state_id_t on_event(const Stopped&) + { + ++get_fsm_context().stoppedCount; + return StateId::IDLE; + } + + //*********************************** + etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + ++get_fsm_context().unknownCount; + return NO_CHANGE; + } + }; + + // The states. + Idle idle; + Running running; + WindingUp windingUp; + WindingDown windingDown; + AtSpeed atSpeed; + + etl::ifsm_state* stateList[StateId::NUMBER_OF_STATES] = + { + &idle, &running, &windingUp, &windingDown, &atSpeed + }; + + MotorControl motorControl; + + SUITE(test_hfsm_states) + { + //************************************************************************* + TEST(test_hfsm) + { + etl::null_message_router nmr; + + CHECK(motorControl.is_producer()); + CHECK(motorControl.is_consumer()); + + running.add_initial_child(windingUp); + running.add_child(atSpeed); + running.add_child(windingDown); + + motorControl.Initialise(stateList, etl::size(stateList)); + motorControl.reset(); + motorControl.ClearStatistics(); + + CHECK(!motorControl.is_started()); + + // Start the FSM. + motorControl.start(false); + CHECK(motorControl.is_started()); + + // Now in Idle state. + + CHECK_EQUAL(StateId::IDLE, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::IDLE, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(false, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(0, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.windUpCompleteCount); + CHECK_EQUAL(0, motorControl.windUpStartCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(0, motorControl.unknownCount); + + // Send unhandled events. + motorControl.receive(Stop()); + motorControl.receive(Stopped()); + motorControl.receive(SetSpeed(10)); + + CHECK_EQUAL(StateId::IDLE, motorControl.get_state_id()); + CHECK_EQUAL(StateId::IDLE, motorControl.get_state().get_state_id()); + + CHECK_EQUAL(false, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(0, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(3, motorControl.unknownCount); + CHECK_EQUAL(0, motorControl.windUpCompleteCount); + CHECK_EQUAL(0, motorControl.windUpStartCount); + + // Send Start event. + motorControl.receive(Start()); + + // Now in WindingUp state. + + CHECK_EQUAL(StateId::WINDING_UP, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::WINDING_UP, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(3, motorControl.unknownCount); + CHECK_EQUAL(0, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + + // Send unhandled events. + motorControl.receive(Start()); + motorControl.receive(Stopped()); + + CHECK_EQUAL(StateId::WINDING_UP, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::WINDING_UP, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(5, motorControl.unknownCount); + CHECK_EQUAL(0, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + + // Send Timeout event + motorControl.receive(Timeout()); + + CHECK_EQUAL(StateId::AT_SPEED, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::AT_SPEED, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(5, motorControl.unknownCount); + CHECK_EQUAL(1, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + + // Send SetSpeed event. + motorControl.receive(SetSpeed(100)); + + // Still in at speed state. + + CHECK_EQUAL(StateId::AT_SPEED, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::AT_SPEED, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(1, motorControl.setSpeedCount); + CHECK_EQUAL(100, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(5, motorControl.unknownCount); + CHECK_EQUAL(1, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + + // Send Stop event. + motorControl.receive(Stop()); + + // Now in WindingDown state. + + CHECK_EQUAL(StateId::WINDING_DOWN, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::WINDING_DOWN, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(1, motorControl.setSpeedCount); + CHECK_EQUAL(100, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(1, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(5, motorControl.unknownCount); + CHECK_EQUAL(1, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + + // Send unhandled events. + motorControl.receive(Start()); + motorControl.receive(Stop()); + motorControl.receive(SetSpeed(100)); + + CHECK_EQUAL(StateId::WINDING_DOWN, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::WINDING_DOWN, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(1, motorControl.setSpeedCount); + CHECK_EQUAL(100, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(1, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(8, motorControl.unknownCount); + CHECK_EQUAL(1, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + + // Send Stopped event. + motorControl.receive(Stopped()); + + // Now in Idle state. + CHECK_EQUAL(StateId::IDLE, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::IDLE, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(false, motorControl.isLampOn); + CHECK_EQUAL(1, motorControl.setSpeedCount); + CHECK_EQUAL(100, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(1, motorControl.stopCount); + CHECK_EQUAL(1, motorControl.stoppedCount); + CHECK_EQUAL(8, motorControl.unknownCount); + CHECK_EQUAL(1, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + } + + //************************************************************************* + TEST(test_hfsm_emergency_stop_from_winding_up) + { + etl::null_message_router nmr; + + motorControl.Initialise(stateList, etl::size(stateList)); + motorControl.reset(); + motorControl.ClearStatistics(); + + CHECK(!motorControl.is_started()); + + // Start the FSM. + motorControl.start(false); + CHECK(motorControl.is_started()); + + // Now in Idle state. + + // Send Start event. + motorControl.receive(Start()); + + // Now in winding up state. + + CHECK_EQUAL(StateId::WINDING_UP, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::WINDING_UP, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(0, motorControl.unknownCount); + CHECK_EQUAL(0, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + + // Send emergency Stop event. + motorControl.receive(EStop()); + + // Now in Idle state. + CHECK_EQUAL(StateId::IDLE, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::IDLE, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(false, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(1, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(0, motorControl.unknownCount); + CHECK_EQUAL(0, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + } + + //************************************************************************* + TEST(test_hfsm_emergency_stop_from_at_speed) + { + etl::null_message_router nmr; + + motorControl.Initialise(stateList, etl::size(stateList)); + motorControl.reset(); + motorControl.ClearStatistics(); + + CHECK(!motorControl.is_started()); + + // Start the FSM. + motorControl.start(false); + CHECK(motorControl.is_started()); + + // Now in Idle state. + + // Send Start event. + motorControl.receive(Start()); + motorControl.receive(Timeout()); + + // Now in at speed state. + + CHECK_EQUAL(StateId::AT_SPEED, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::AT_SPEED, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(0, motorControl.unknownCount); + CHECK_EQUAL(1, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + + // Send emergency Stop event. + motorControl.receive(EStop()); + + // Now in Idle state. + CHECK_EQUAL(StateId::IDLE, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::IDLE, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(false, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(1, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(0, motorControl.unknownCount); + CHECK_EQUAL(1, motorControl.windUpCompleteCount); + CHECK_EQUAL(1, motorControl.windUpStartCount); + } + + //************************************************************************* + TEST(test_hfsm_recursive_event) + { + etl::null_message_router nmr; + + motorControl.Initialise(stateList, etl::size(stateList)); + motorControl.reset(); + motorControl.ClearStatistics(); + + motorControl.messageQueue.clear(); + + // Start the FSM. + motorControl.start(false); + + // Now in Idle state. + // Send Start event. + motorControl.receive(Recursive()); + + CHECK_EQUAL(1U, motorControl.messageQueue.size()); + + // Send the queued message. + motorControl.receive(motorControl.messageQueue.front().get()); + motorControl.messageQueue.pop(); + + // Now in winding up state. + + CHECK_EQUAL(StateId::WINDING_UP, int(motorControl.get_state_id())); + CHECK_EQUAL(StateId::WINDING_UP, int(motorControl.get_state().get_state_id())); + + CHECK_EQUAL(true, motorControl.isLampOn); + CHECK_EQUAL(0, motorControl.setSpeedCount); + CHECK_EQUAL(0, motorControl.speed); + CHECK_EQUAL(1, motorControl.startCount); + CHECK_EQUAL(0, motorControl.stopCount); + CHECK_EQUAL(0, motorControl.stoppedCount); + CHECK_EQUAL(0, motorControl.unknownCount); + } + + //************************************************************************* + TEST(test_hfsm_supported) + { + CHECK(motorControl.accepts(EventId::SET_SPEED)); + CHECK(motorControl.accepts(EventId::START)); + CHECK(motorControl.accepts(EventId::STOP)); + CHECK(motorControl.accepts(EventId::STOPPED)); + CHECK(motorControl.accepts(EventId::UNSUPPORTED)); + + CHECK(motorControl.accepts(SetSpeed(0))); + CHECK(motorControl.accepts(Start())); + CHECK(motorControl.accepts(Stop())); + CHECK(motorControl.accepts(Stopped())); + CHECK(motorControl.accepts(Unsupported())); + } + + //************************************************************************* + TEST(test_hfsm_no_states) + { + MotorControl mc; + + // No states. + etl::ifsm_state** stateList = nullptr; + + CHECK_THROW(mc.set_states(stateList, 0U), etl::fsm_state_list_exception); + } + + //************************************************************************* + TEST(test_hfsm_null_state) + { + MotorControl mc; + + // Null state. + etl::ifsm_state* stateList[StateId::NUMBER_OF_STATES] = + { + &idle, &running, &windingUp, &windingDown, nullptr + }; + + CHECK_THROW(mc.set_states(stateList, StateId::NUMBER_OF_STATES), etl::fsm_null_state_exception); + } + + //************************************************************************* + TEST(test_hfsm_incorrect_state_order) + { + MotorControl mc; + + // Incorrect order. + etl::ifsm_state* stateList[StateId::NUMBER_OF_STATES] = + { + &idle, &running, &windingDown, &windingUp, &atSpeed + }; + + CHECK_THROW(mc.set_states(stateList, StateId::NUMBER_OF_STATES), etl::fsm_state_list_order_exception); + } + }; +} From c5850a005b1f100fa25c36aba0535ccdd8b00985 Mon Sep 17 00:00:00 2001 From: John Wellbelove Date: Fri, 23 Apr 2021 11:58:58 +0100 Subject: [PATCH 2/6] Minor changes --- include/etl/fsm.h | 3 +++ include/etl/hfsm.h | 27 ++++++++++++++++++++------- test/vs2019/etl.vcxproj | 2 ++ test/vs2019/etl.vcxproj.filters | 6 ++++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/include/etl/fsm.h b/include/etl/fsm.h index 7618d334..4149d802 100644 --- a/include/etl/fsm.h +++ b/include/etl/fsm.h @@ -152,6 +152,9 @@ namespace etl } }; + //*************************************************************************** + /// Exception for forbidden state changes. + //*************************************************************************** class fsm_state_enter_state_change_forbidden : public etl::fsm_exception { public: diff --git a/include/etl/hfsm.h b/include/etl/hfsm.h index 24475c40..2738a6a2 100644 --- a/include/etl/hfsm.h +++ b/include/etl/hfsm.h @@ -58,13 +58,13 @@ namespace etl { etl::fsm_state_id_t next_state_id = p_state->process_event(message); - if(next_state_id != ifsm_state::NO_CHANGE) + if (next_state_id != ifsm_state::NO_CHANGE) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); etl::ifsm_state* p_next_state = state_list[next_state_id]; // Have we changed state? - if(p_next_state != p_state) + if (p_next_state != p_state) { etl::ifsm_state* p_root = common_ancestor(p_state, p_next_state); do_exits(p_root, p_state); @@ -72,7 +72,8 @@ namespace etl p_state = p_next_state; next_state_id = do_enters(p_root, p_next_state, true); - if(next_state_id != ifsm_state::NO_CHANGE) + + if (next_state_id != ifsm_state::NO_CHANGE) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); p_state = state_list[next_state_id]; @@ -81,11 +82,16 @@ namespace etl } } + private: + + //******************************************* + /// Return the common ancester of the two states. + //******************************************* static etl::ifsm_state* common_ancestor(etl::ifsm_state* p_one, etl::ifsm_state* p_two) { - if(p_one == p_two) + if (p_one == p_two) { - return p_one; + return p_one; } etl::ifsm_state* p_current_state = p_one; @@ -100,7 +106,7 @@ namespace etl p_current_state = p_current_state->p_parent; } - if(p_two->p_parent != ETL_NULLPTR) + if (p_two->p_parent != ETL_NULLPTR) { return common_ancestor(p_one, p_two->p_parent); } @@ -108,6 +114,9 @@ namespace etl return ETL_NULLPTR; } + //******************************************* + /// Exiting the state. + //******************************************* static void do_exits(const etl::ifsm_state* p_root, etl::ifsm_state* p_source) { etl::ifsm_state* p_current = p_source; @@ -126,6 +135,9 @@ namespace etl } } + //******************************************* + /// Entering the state. + //******************************************* static etl::fsm_state_id_t do_enters(const etl::ifsm_state* p_root, etl::ifsm_state* p_target, bool activate_default_children) { ETL_ASSERT(p_target != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); @@ -133,7 +145,7 @@ namespace etl // We need to go recursively up the tree if the target and root don't match if (p_root != p_target && p_target->p_parent != ETL_NULLPTR) { - if(p_target->p_parent != p_root) + if (p_target->p_parent != p_root) { // The parent we're calling shouldn't activate its defaults, or this state will be deactivated. do_enters(p_root, p_target->p_parent, false); @@ -156,6 +168,7 @@ namespace etl next_state = p_target->on_enter_state(); ETL_ASSERT(ifsm_state::NO_CHANGE == next_state, ETL_ERROR(etl::fsm_state_enter_state_change_forbidden)); } + next_state = p_target->get_state_id(); } diff --git a/test/vs2019/etl.vcxproj b/test/vs2019/etl.vcxproj index 37f1980a..80c7a26d 100644 --- a/test/vs2019/etl.vcxproj +++ b/test/vs2019/etl.vcxproj @@ -1326,6 +1326,7 @@ + @@ -4481,6 +4482,7 @@ + diff --git a/test/vs2019/etl.vcxproj.filters b/test/vs2019/etl.vcxproj.filters index a2429199..8b11f5cf 100644 --- a/test/vs2019/etl.vcxproj.filters +++ b/test/vs2019/etl.vcxproj.filters @@ -1098,6 +1098,9 @@ ETL\Maths + + ETL\Frameworks + @@ -2513,6 +2516,9 @@ Source Files + + Source Files + From 7f2ea864e00ea45597f39dbf47d7d699abaddc65 Mon Sep 17 00:00:00 2001 From: John Wellbelove Date: Sat, 24 Apr 2021 09:25:39 +0100 Subject: [PATCH 3/6] Minor changes & renames --- include/etl/fsm.h | 87 ++++++++++++++++++-------- include/etl/generators/fsm_generator.h | 82 +++++++++++++++++------- include/etl/hfsm.h | 14 +++-- test/test_fsm.cpp | 2 +- test/test_hfsm.cpp | 27 ++++---- 5 files changed, 145 insertions(+), 67 deletions(-) diff --git a/include/etl/fsm.h b/include/etl/fsm.h index 4149d802..d3f09847 100644 --- a/include/etl/fsm.h +++ b/include/etl/fsm.h @@ -153,13 +153,13 @@ namespace etl }; //*************************************************************************** - /// Exception for forbidden state changes. + /// Exception for forbidden state chages. //*************************************************************************** - class fsm_state_enter_state_change_forbidden : public etl::fsm_exception + class fsm_state_composite_state_change_forbidden : public etl::fsm_exception { public: - fsm_state_enter_state_change_forbidden(string_type file_name_, numeric_type line_number_) - : etl::fsm_exception(ETL_ERROR_TEXT("fsm:enter state change in composite state forbidden", ETL_FSM_FILE_ID"E"), file_name_, line_number_) + fsm_state_composite_state_change_forbidden(string_type file_name_, numeric_type line_number_) + : etl::fsm_exception(ETL_ERROR_TEXT("fsm:change in composite state forbidden", ETL_FSM_FILE_ID"E"), file_name_, line_number_) { } }; @@ -173,16 +173,16 @@ namespace etl // Pass this whenever no state change is desired. Specifically cast to // Highest unsigned value of fsm_state_id_t. - static const fsm_state_id_t NO_CHANGE = static_cast(-1); + static const fsm_state_id_t No_State_Change = static_cast(-1); /// Allows ifsm_state functions to be private. friend class etl::fsm; friend class etl::hfsm; template + typename, typename, typename, typename, + typename, typename, typename, typename, + typename, typename, typename, typename, + typename, typename, typename, typename > friend class etl::fsm_state; //******************************************* @@ -196,21 +196,56 @@ namespace etl //******************************************* /// Adds a child to this state. //******************************************* - void add_child(etl::ifsm_state& state) + void add_child_state(etl::ifsm_state& state) { ETL_ASSERT(state.p_parent == ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); state.p_parent = this; + + if (p_default_child == ETL_NULLPTR) + { + p_active_child = &state; + p_default_child = &state; + } } //******************************************* - /// Adds the initial child to this state. + /// Adds a list of child states. //******************************************* - void add_initial_child(etl::ifsm_state& state) + template + void set_child_states(etl::ifsm_state** state_list, TSize size) { - ETL_ASSERT(p_default_active_child == ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); - add_child(state); - p_active_child = &state; - p_default_active_child = &state; + p_active_child = ETL_NULLPTR; + p_default_child = ETL_NULLPTR; + + for (TSize i = 0; i < size; ++i) + { + ETL_ASSERT(state_list[i] != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + add_child_state(*state_list[i]); + } + } + + //******************************************* + /// Get the parent state for this state. + //******************************************* + etl::ifsm_state* get_parent_state() const + { + return p_parent; + } + + //******************************************* + /// Get the active child state for this state. + //******************************************* + etl::ifsm_state* get_active_child_state() const + { + return p_active_child; + } + + //******************************************* + /// Get the default child state for this state. + //******************************************* + etl::ifsm_state* get_default_child_state() const + { + return p_default_child; } protected: @@ -223,7 +258,7 @@ namespace etl p_context(ETL_NULLPTR), p_parent(ETL_NULLPTR), p_active_child(ETL_NULLPTR), - p_default_active_child(ETL_NULLPTR) + p_default_child(ETL_NULLPTR) { } @@ -244,7 +279,7 @@ namespace etl virtual fsm_state_id_t process_event(const etl::imessage& message) = 0; - virtual fsm_state_id_t on_enter_state() { return NO_CHANGE; } // By default, do nothing. + virtual fsm_state_id_t on_enter_state() { return No_State_Change; } // By default, do nothing. virtual void on_exit_state() {} // By default, do nothing. //******************************************* @@ -266,7 +301,7 @@ namespace etl ifsm_state* p_active_child; // A pointer to the default active child. - ifsm_state* p_default_active_child; + ifsm_state* p_default_child; // Disabled. ifsm_state(const ifsm_state&); @@ -304,7 +339,7 @@ namespace etl number_of_states = etl::fsm_state_id_t(size); ETL_ASSERT(number_of_states > 0, ETL_ERROR(etl::fsm_state_list_exception)); - ETL_ASSERT(number_of_states < ifsm_state::NO_CHANGE, ETL_ERROR(etl::fsm_state_list_exception)); + ETL_ASSERT(number_of_states < ifsm_state::No_State_Change, ETL_ERROR(etl::fsm_state_list_exception)); for (etl::fsm_state_id_t i = 0; i < size; ++i) { @@ -337,7 +372,7 @@ namespace etl { p_last_state = p_state; next_state_id = p_state->on_enter_state(); - if (next_state_id != ifsm_state::NO_CHANGE) + if (next_state_id != ifsm_state::No_State_Change) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); p_state = state_list[next_state_id]; @@ -354,7 +389,7 @@ namespace etl { etl::fsm_state_id_t next_state_id = p_state->process_event(message); - if(next_state_id != ifsm_state::NO_CHANGE) + if(next_state_id != ifsm_state::No_State_Change) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); etl::ifsm_state* p_next_state = state_list[next_state_id]; @@ -368,7 +403,7 @@ namespace etl p_state = p_next_state; next_state_id = p_state->on_enter_state(); - if(next_state_id != ifsm_state::NO_CHANGE) + if(next_state_id != ifsm_state::No_State_Change) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); p_next_state = state_list[next_state_id]; @@ -467,9 +502,9 @@ namespace etl // The definition for all 16 message types. //*************************************************************************** template class fsm_state : public ifsm_state { diff --git a/include/etl/generators/fsm_generator.h b/include/etl/generators/fsm_generator.h index 12a665c3..96ec2105 100644 --- a/include/etl/generators/fsm_generator.h +++ b/include/etl/generators/fsm_generator.h @@ -164,11 +164,14 @@ namespace etl } }; - class fsm_state_enter_state_change_forbidden : public etl::fsm_exception + //*************************************************************************** + /// Exception for forbidden state chages. + //*************************************************************************** + class fsm_state_composite_state_change_forbidden : public etl::fsm_exception { public: - fsm_state_enter_state_change_forbidden(string_type file_name_, numeric_type line_number_) - : etl::fsm_exception(ETL_ERROR_TEXT("fsm:enter state change in composite state forbidden", ETL_FSM_FILE_ID"E"), file_name_, line_number_) + fsm_state_composite_state_change_forbidden(string_type file_name_, numeric_type line_number_) + : etl::fsm_exception(ETL_ERROR_TEXT("fsm:change in composite state forbidden", ETL_FSM_FILE_ID"E"), file_name_, line_number_) { } }; @@ -182,16 +185,16 @@ namespace etl // Pass this whenever no state change is desired. Specifically cast to // Highest unsigned value of fsm_state_id_t. - static const fsm_state_id_t NO_CHANGE = static_cast(-1); + static const fsm_state_id_t No_State_Change = static_cast(-1); /// Allows ifsm_state functions to be private. friend class etl::fsm; friend class etl::hfsm; template + typename, typename, typename, typename, + typename, typename, typename, typename, + typename, typename, typename, typename, + typename, typename, typename, typename > friend class etl::fsm_state; //******************************************* @@ -205,21 +208,56 @@ namespace etl //******************************************* /// Adds a child to this state. //******************************************* - void add_child(etl::ifsm_state& state) + void add_child_state(etl::ifsm_state& state) { ETL_ASSERT(state.p_parent == ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); state.p_parent = this; + + if (p_default_child == ETL_NULLPTR) + { + p_active_child = &state; + p_default_child = &state; + } } //******************************************* - /// Adds the initial child to this state. + /// Adds a list of child states. //******************************************* - void add_initial_child(etl::ifsm_state& state) + template + void set_child_states(etl::ifsm_state** state_list, TSize size) { - ETL_ASSERT(p_default_active_child == ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); - add_child(state); - p_active_child = &state; - p_default_active_child = &state; + p_active_child = ETL_NULLPTR; + p_default_child = ETL_NULLPTR; + + for (TSize i = 0; i < size; ++i) + { + ETL_ASSERT(state_list[i] != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + add_child_state(*state_list[i]); + } + } + + //******************************************* + /// Get the parent state for this state. + //******************************************* + etl::ifsm_state* get_parent_state() const + { + return p_parent; + } + + //******************************************* + /// Get the active child state for this state. + //******************************************* + etl::ifsm_state* get_active_child_state() const + { + return p_active_child; + } + + //******************************************* + /// Get the default child state for this state. + //******************************************* + etl::ifsm_state* get_default_child_state() const + { + return p_default_child; } protected: @@ -232,7 +270,7 @@ namespace etl p_context(ETL_NULLPTR), p_parent(ETL_NULLPTR), p_active_child(ETL_NULLPTR), - p_default_active_child(ETL_NULLPTR) + p_default_child(ETL_NULLPTR) { } @@ -253,7 +291,7 @@ namespace etl virtual fsm_state_id_t process_event(const etl::imessage& message) = 0; - virtual fsm_state_id_t on_enter_state() { return NO_CHANGE; } // By default, do nothing. + virtual fsm_state_id_t on_enter_state() { return No_State_Change; } // By default, do nothing. virtual void on_exit_state() {} // By default, do nothing. //******************************************* @@ -275,7 +313,7 @@ namespace etl ifsm_state* p_active_child; // A pointer to the default active child. - ifsm_state* p_default_active_child; + ifsm_state* p_default_child; // Disabled. ifsm_state(const ifsm_state&); @@ -313,7 +351,7 @@ namespace etl number_of_states = etl::fsm_state_id_t(size); ETL_ASSERT(number_of_states > 0, ETL_ERROR(etl::fsm_state_list_exception)); - ETL_ASSERT(number_of_states < ifsm_state::NO_CHANGE, ETL_ERROR(etl::fsm_state_list_exception)); + ETL_ASSERT(number_of_states < ifsm_state::No_State_Change, ETL_ERROR(etl::fsm_state_list_exception)); for (etl::fsm_state_id_t i = 0; i < size; ++i) { @@ -346,7 +384,7 @@ namespace etl { p_last_state = p_state; next_state_id = p_state->on_enter_state(); - if (next_state_id != ifsm_state::NO_CHANGE) + if (next_state_id != ifsm_state::No_State_Change) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); p_state = state_list[next_state_id]; @@ -363,7 +401,7 @@ namespace etl { etl::fsm_state_id_t next_state_id = p_state->process_event(message); - if(next_state_id != ifsm_state::NO_CHANGE) + if(next_state_id != ifsm_state::No_State_Change) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); etl::ifsm_state* p_next_state = state_list[next_state_id]; @@ -377,7 +415,7 @@ namespace etl p_state = p_next_state; next_state_id = p_state->on_enter_state(); - if(next_state_id != ifsm_state::NO_CHANGE) + if(next_state_id != ifsm_state::No_State_Change) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); p_next_state = state_list[next_state_id]; diff --git a/include/etl/hfsm.h b/include/etl/hfsm.h index 2738a6a2..f4c1c5b1 100644 --- a/include/etl/hfsm.h +++ b/include/etl/hfsm.h @@ -36,6 +36,8 @@ namespace etl //*************************************************************************** /// The HFSM class. + /// Builds on the FSM class by overriding the receive function and adding + /// state hierarchy walking functions. //*************************************************************************** class hfsm : public etl::fsm { @@ -58,7 +60,7 @@ namespace etl { etl::fsm_state_id_t next_state_id = p_state->process_event(message); - if (next_state_id != ifsm_state::NO_CHANGE) + if (next_state_id != ifsm_state::No_State_Change) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); etl::ifsm_state* p_next_state = state_list[next_state_id]; @@ -73,7 +75,7 @@ namespace etl next_state_id = do_enters(p_root, p_next_state, true); - if (next_state_id != ifsm_state::NO_CHANGE) + if (next_state_id != ifsm_state::No_State_Change) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); p_state = state_list[next_state_id]; @@ -156,17 +158,17 @@ namespace etl } etl::fsm_state_id_t next_state = p_target->on_enter_state(); - ETL_ASSERT(ifsm_state::NO_CHANGE == next_state, ETL_ERROR(etl::fsm_state_enter_state_change_forbidden)); + ETL_ASSERT(ifsm_state::No_State_Change == next_state, ETL_ERROR(etl::fsm_state_composite_state_change_forbidden)); // Activate default child if we need to activate any initial states in an active composite state. if (activate_default_children) { - while (p_target->p_default_active_child != ETL_NULLPTR) + while (p_target->p_default_child != ETL_NULLPTR) { - p_target = p_target->p_default_active_child; + p_target = p_target->p_default_child; p_target->p_parent->p_active_child = p_target; next_state = p_target->on_enter_state(); - ETL_ASSERT(ifsm_state::NO_CHANGE == next_state, ETL_ERROR(etl::fsm_state_enter_state_change_forbidden)); + ETL_ASSERT(ifsm_state::No_State_Change == next_state, ETL_ERROR(etl::fsm_state_composite_state_change_forbidden)); } next_state = p_target->get_state_id(); diff --git a/test/test_fsm.cpp b/test/test_fsm.cpp index cb10c994..d23d9cf7 100644 --- a/test/test_fsm.cpp +++ b/test/test_fsm.cpp @@ -224,7 +224,7 @@ namespace etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; - return NO_CHANGE; + return No_State_Change; } //*********************************** diff --git a/test/test_hfsm.cpp b/test/test_hfsm.cpp index 1b0ec0f0..6fc9c3ba 100644 --- a/test/test_hfsm.cpp +++ b/test/test_hfsm.cpp @@ -260,14 +260,14 @@ namespace etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; - return NO_CHANGE; + return No_State_Change; } //*********************************** etl::fsm_state_id_t on_enter_state() { get_fsm_context().TurnRunningLampOff(); - return NO_CHANGE; + return No_State_Change; } }; @@ -290,7 +290,7 @@ namespace etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; - return NO_CHANGE; + return No_State_Change; } //*********************************** @@ -298,7 +298,7 @@ namespace { get_fsm_context().TurnRunningLampOn(); - return NO_CHANGE; + return No_State_Change; } }; @@ -327,13 +327,13 @@ namespace etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; - return NO_CHANGE; + return No_State_Change; } etl::fsm_state_id_t on_enter_state() { ++get_fsm_context().windUpStartCount; - return NO_CHANGE; + return No_State_Change; } }; @@ -355,14 +355,14 @@ namespace { ++get_fsm_context().setSpeedCount; get_fsm_context().SetSpeedValue(event.speed); - return NO_CHANGE; + return No_State_Change; } //*********************************** etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; - return NO_CHANGE; + return No_State_Change; } }; @@ -384,7 +384,7 @@ namespace etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; - return NO_CHANGE; + return No_State_Change; } }; @@ -400,6 +400,11 @@ namespace &idle, &running, &windingUp, &windingDown, &atSpeed }; + etl::ifsm_state* childStates[] = + { + &windingUp, &atSpeed, &windingDown + }; + MotorControl motorControl; SUITE(test_hfsm_states) @@ -412,9 +417,7 @@ namespace CHECK(motorControl.is_producer()); CHECK(motorControl.is_consumer()); - running.add_initial_child(windingUp); - running.add_child(atSpeed); - running.add_child(windingDown); + running.set_child_states(childStates, etl::size(childStates)); motorControl.Initialise(stateList, etl::size(stateList)); motorControl.reset(); From b01d58a21cf207efcf0beca4f37f7b7fe18621a4 Mon Sep 17 00:00:00 2001 From: John Wellbelove Date: Sun, 25 Apr 2021 14:20:29 +0100 Subject: [PATCH 4/6] Adjustments to code and optimisations --- include/etl/fsm.h | 36 ++++++++++--------- include/etl/hfsm.h | 90 ++++++++++++++++++++++++++++++---------------- test/test_fsm.cpp | 2 +- test/test_hfsm.cpp | 4 ++- 4 files changed, 83 insertions(+), 49 deletions(-) diff --git a/include/etl/fsm.h b/include/etl/fsm.h index d3f09847..3eb80641 100644 --- a/include/etl/fsm.h +++ b/include/etl/fsm.h @@ -173,7 +173,7 @@ namespace etl // Pass this whenever no state change is desired. Specifically cast to // Highest unsigned value of fsm_state_id_t. - static const fsm_state_id_t No_State_Change = static_cast(-1); + static ETL_CONSTANT fsm_state_id_t No_State_Change = etl::integral_limits::max; /// Allows ifsm_state functions to be private. friend class etl::fsm; @@ -389,27 +389,24 @@ namespace etl { etl::fsm_state_id_t next_state_id = p_state->process_event(message); - if(next_state_id != ifsm_state::No_State_Change) + if (have_changed_state(next_state_id)) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); etl::ifsm_state* p_next_state = state_list[next_state_id]; - // Have we changed state? - if (p_next_state != p_state) + do { - do - { - p_state->on_exit_state(); - p_state = p_next_state; + p_state->on_exit_state(); + p_state = p_next_state; - next_state_id = p_state->on_enter_state(); - if(next_state_id != ifsm_state::No_State_Change) - { - ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); - p_next_state = state_list[next_state_id]; - } - } while (p_next_state != p_state); // Have we changed state again? - } + next_state_id = p_state->on_enter_state(); + + if (have_changed_state(next_state_id)) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_next_state = state_list[next_state_id]; + } + } while (p_next_state != p_state); // Have we changed state again? } } @@ -493,6 +490,13 @@ namespace etl private: + //******************************************** + bool have_changed_state(etl::fsm_state_id_t next_state_id) const + { + return (next_state_id != p_state->get_state_id()) && + (next_state_id != ifsm_state::No_State_Change); + } + etl::ifsm_state* p_state; ///< A pointer to the current state. etl::ifsm_state** state_list; ///< The list of added states. etl::fsm_state_id_t number_of_states; ///< The number of states. diff --git a/include/etl/hfsm.h b/include/etl/hfsm.h index f4c1c5b1..43971b45 100644 --- a/include/etl/hfsm.h +++ b/include/etl/hfsm.h @@ -33,7 +33,6 @@ SOFTWARE. namespace etl { - //*************************************************************************** /// The HFSM class. /// Builds on the FSM class by overriding the receive function and adding @@ -87,54 +86,61 @@ namespace etl private: //******************************************* - /// Return the common ancester of the two states. + /// Return the first common ancester of the two states. //******************************************* - static etl::ifsm_state* common_ancestor(etl::ifsm_state* p_one, etl::ifsm_state* p_two) + static etl::ifsm_state* common_ancestor(etl::ifsm_state* p1, etl::ifsm_state* p2) { - if (p_one == p_two) + size_t depth1 = get_depth(p1); + size_t depth2 = get_depth(p2); + + // Align p1 and p2 to the same depth. + if (depth1 > depth2) { - return p_one; + p1 = align_depth(p1, depth1 - depth2); + } + else + { + p2 = align_depth(p2, depth2 - depth1); } - etl::ifsm_state* p_current_state = p_one; - - while (p_current_state->p_parent != ETL_NULLPTR) + // Now they're aligned they can step towards the root together. + while (p1 != p2) { - if (p_current_state->p_parent == p_two) - { - return p_current_state->p_parent; - } - - p_current_state = p_current_state->p_parent; + p1 = p1->p_parent; + p2 = p2->p_parent; } - if (p_two->p_parent != ETL_NULLPTR) - { - return common_ancestor(p_one, p_two->p_parent); - } - - return ETL_NULLPTR; + return p1; } //******************************************* - /// Exiting the state. + /// Find the depth of the state. //******************************************* - static void do_exits(const etl::ifsm_state* p_root, etl::ifsm_state* p_source) + static size_t get_depth(etl::ifsm_state* p) { - etl::ifsm_state* p_current = p_source; + size_t depth = 0; - // Iterate down to the lowest child - while(p_current->p_active_child != ETL_NULLPTR) + while (p != ETL_NULLPTR) { - p_current = p_current->p_active_child; + p = p->p_parent; + ++depth; } - // Run exit state on all states up to the root - while (p_current != p_root) + return depth; + } + + //******************************************* + /// Align the depths of the states. + //******************************************* + static etl::ifsm_state* align_depth(etl::ifsm_state* p, size_t offset) + { + while (offset != 0U) { - p_current->on_exit_state(); - p_current = p_current->p_parent; + p = p->p_parent; + --offset; } + + return p; } //******************************************* @@ -145,7 +151,8 @@ namespace etl ETL_ASSERT(p_target != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); // We need to go recursively up the tree if the target and root don't match - if (p_root != p_target && p_target->p_parent != ETL_NULLPTR) + if ((p_root != p_target) && + (p_target->p_parent != ETL_NULLPTR)) { if (p_target->p_parent != p_root) { @@ -176,6 +183,27 @@ namespace etl return next_state; } + + //******************************************* + /// Exiting the state. + //******************************************* + static void do_exits(const etl::ifsm_state* p_root, etl::ifsm_state* p_source) + { + etl::ifsm_state* p_current = p_source; + + // Iterate down to the lowest child + while (p_current->p_active_child != ETL_NULLPTR) + { + p_current = p_current->p_active_child; + } + + // Run exit state on all states up to the root + while (p_current != p_root) + { + p_current->on_exit_state(); + p_current = p_current->p_parent; + } + } }; } #endif diff --git a/test/test_fsm.cpp b/test/test_fsm.cpp index d23d9cf7..5e22d5a0 100644 --- a/test/test_fsm.cpp +++ b/test/test_fsm.cpp @@ -224,7 +224,7 @@ namespace etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; - return No_State_Change; + return StateId::IDLE; //No_State_Change; } //*********************************** diff --git a/test/test_hfsm.cpp b/test/test_hfsm.cpp index 6fc9c3ba..e081c9ca 100644 --- a/test/test_hfsm.cpp +++ b/test/test_hfsm.cpp @@ -327,6 +327,7 @@ namespace etl::fsm_state_id_t on_event_unknown(const etl::imessage&) { ++get_fsm_context().unknownCount; + return No_State_Change; } @@ -355,7 +356,8 @@ namespace { ++get_fsm_context().setSpeedCount; get_fsm_context().SetSpeedValue(event.speed); - return No_State_Change; + //return No_State_Change; + return this->get_state_id(); } //*********************************** From 86edd1a50464562e1947c7237d2cf46b9349ab71 Mon Sep 17 00:00:00 2001 From: John Wellbelove Date: Sun, 25 Apr 2021 14:21:40 +0100 Subject: [PATCH 5/6] Adjustments to code and optimisations --- include/etl/fsm.h | 4 +-- include/etl/generators/fsm_generator.h | 40 ++++++++++++++------------ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/include/etl/fsm.h b/include/etl/fsm.h index 3eb80641..6123539b 100644 --- a/include/etl/fsm.h +++ b/include/etl/fsm.h @@ -171,8 +171,8 @@ namespace etl { public: - // Pass this whenever no state change is desired. Specifically cast to - // Highest unsigned value of fsm_state_id_t. + // Pass this whenever no state change is desired. + // The highest unsigned value of fsm_state_id_t. static ETL_CONSTANT fsm_state_id_t No_State_Change = etl::integral_limits::max; /// Allows ifsm_state functions to be private. diff --git a/include/etl/generators/fsm_generator.h b/include/etl/generators/fsm_generator.h index 96ec2105..34fc133f 100644 --- a/include/etl/generators/fsm_generator.h +++ b/include/etl/generators/fsm_generator.h @@ -183,9 +183,9 @@ namespace etl { public: - // Pass this whenever no state change is desired. Specifically cast to - // Highest unsigned value of fsm_state_id_t. - static const fsm_state_id_t No_State_Change = static_cast(-1); + // Pass this whenever no state change is desired. + // The highest unsigned value of fsm_state_id_t. + static ETL_CONSTANT fsm_state_id_t No_State_Change = etl::integral_limits::max; /// Allows ifsm_state functions to be private. friend class etl::fsm; @@ -401,27 +401,24 @@ namespace etl { etl::fsm_state_id_t next_state_id = p_state->process_event(message); - if(next_state_id != ifsm_state::No_State_Change) + if (have_changed_state(next_state_id)) { ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); etl::ifsm_state* p_next_state = state_list[next_state_id]; - // Have we changed state? - if (p_next_state != p_state) + do { - do - { - p_state->on_exit_state(); - p_state = p_next_state; + p_state->on_exit_state(); + p_state = p_next_state; - next_state_id = p_state->on_enter_state(); - if(next_state_id != ifsm_state::No_State_Change) - { - ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); - p_next_state = state_list[next_state_id]; - } - } while (p_next_state != p_state); // Have we changed state again? - } + next_state_id = p_state->on_enter_state(); + + if (have_changed_state(next_state_id)) + { + ETL_ASSERT(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_next_state = state_list[next_state_id]; + } + } while (p_next_state != p_state); // Have we changed state again? } } @@ -505,6 +502,13 @@ namespace etl private: + //******************************************** + bool have_changed_state(etl::fsm_state_id_t next_state_id) const + { + return (next_state_id != p_state->get_state_id()) && + (next_state_id != ifsm_state::No_State_Change); + } + etl::ifsm_state* p_state; ///< A pointer to the current state. etl::ifsm_state** state_list; ///< The list of added states. etl::fsm_state_id_t number_of_states; ///< The number of states. From 43d16b1034b2ff47cf3d31b8b77ceab253a47e67 Mon Sep 17 00:00:00 2001 From: John Wellbelove Date: Sun, 25 Apr 2021 15:11:02 +0100 Subject: [PATCH 6/6] Adjustments to code and optimisations --- include/etl/hfsm.h | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/include/etl/hfsm.h b/include/etl/hfsm.h index 43971b45..134f18d1 100644 --- a/include/etl/hfsm.h +++ b/include/etl/hfsm.h @@ -53,7 +53,7 @@ namespace etl using fsm::receive; //******************************************* - /// Top level message handler for the FSM. + /// Top level message handler for the HFSM. //******************************************* void receive(const etl::imessage& message) ETL_OVERRIDE { @@ -93,17 +93,17 @@ namespace etl size_t depth1 = get_depth(p1); size_t depth2 = get_depth(p2); - // Align p1 and p2 to the same depth. + // Adjust p1 and p2 to the same depth. if (depth1 > depth2) { - p1 = align_depth(p1, depth1 - depth2); + p1 = adjust_depth(p1, depth1 - depth2); } else { - p2 = align_depth(p2, depth2 - depth1); + p2 = adjust_depth(p2, depth2 - depth1); } - // Now they're aligned they can step towards the root together. + // Now they're aligned to the same depth they can step towards the root together. while (p1 != p2) { p1 = p1->p_parent; @@ -132,7 +132,7 @@ namespace etl //******************************************* /// Align the depths of the states. //******************************************* - static etl::ifsm_state* align_depth(etl::ifsm_state* p, size_t offset) + static etl::ifsm_state* adjust_depth(etl::ifsm_state* p, size_t offset) { while (offset != 0U) { @@ -151,8 +151,7 @@ namespace etl ETL_ASSERT(p_target != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); // We need to go recursively up the tree if the target and root don't match - if ((p_root != p_target) && - (p_target->p_parent != ETL_NULLPTR)) + if ((p_root != p_target) && (p_target->p_parent != ETL_NULLPTR)) { if (p_target->p_parent != p_root) {