From 86b9e0903f88f6547bc4224594db095d3aecf640 Mon Sep 17 00:00:00 2001 From: David Ockey <2897027+ockeydockey@users.noreply.github.com> Date: Wed, 8 Oct 2025 02:43:09 -0500 Subject: [PATCH] Added support for transitions on state enter for HFSM (#1199) * Added support for transitions on state enter for HFSM * Fixed unit test build error * Comment clean-up * Changed FSM so that self-transitions can also transition "on enter" * Fixing C++03 build error for clang --- include/etl/fsm.h | 28 +- include/etl/hfsm.h | 116 ++++++-- test/CMakeLists.txt | 1 + test/test_hfsm_transition_on_enter.cpp | 363 +++++++++++++++++++++++++ 4 files changed, 463 insertions(+), 45 deletions(-) create mode 100644 test/test_hfsm_transition_on_enter.cpp diff --git a/include/etl/fsm.h b/include/etl/fsm.h index 66747ec7..6382884a 100644 --- a/include/etl/fsm.h +++ b/include/etl/fsm.h @@ -160,18 +160,6 @@ namespace etl } }; - //*************************************************************************** - /// Exception for forbidden state changes. - //*************************************************************************** - class fsm_state_composite_state_change_forbidden : public etl::fsm_exception - { - public: - 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_) - { - } - }; - //*************************************************************************** /// Exception for message received but not started. //*************************************************************************** @@ -330,7 +318,6 @@ namespace etl if (p_default_child == ETL_NULLPTR) { - p_active_child = &state; p_default_child = &state; } } @@ -481,7 +468,7 @@ namespace etl virtual void start(bool call_on_enter_state = true) { // Can only be started once. - if (p_state == ETL_NULLPTR) + if (!is_started()) { p_state = state_list[0]; ETL_ASSERT(p_state != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); @@ -589,7 +576,7 @@ namespace etl //******************************************* virtual void reset(bool call_on_exit_state = false) { - if ((p_state != ETL_NULLPTR) && call_on_exit_state) + if (is_started() && call_on_exit_state) { p_state->on_exit_state(); } @@ -636,6 +623,12 @@ namespace etl //******************************************* virtual etl::fsm_state_id_t process_state_change(etl::fsm_state_id_t next_state_id) { + if (is_self_transition(next_state_id)) + { + p_state->on_exit_state(); + next_state_id = p_state->on_enter_state(); + } + if (have_changed_state(next_state_id)) { ETL_ASSERT_OR_RETURN_VALUE(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception), p_state->get_state_id()); @@ -655,11 +648,6 @@ namespace etl } } while (p_next_state != p_state); // Have we changed state again? } - else if (is_self_transition(next_state_id)) - { - p_state->on_exit_state(); - p_state->on_enter_state(); - } return p_state->get_state_id(); } diff --git a/include/etl/hfsm.h b/include/etl/hfsm.h index d86ee39a..c3f2062c 100644 --- a/include/etl/hfsm.h +++ b/include/etl/hfsm.h @@ -60,18 +60,33 @@ namespace etl void start(bool call_on_enter_state = true) ETL_OVERRIDE { // Can only be started once. - if (p_state == ETL_NULLPTR) + if (!is_started()) { - p_state = state_list[0]; - ETL_ASSERT(p_state != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + etl::ifsm_state* p_first_state = state_list[0]; + ETL_ASSERT(p_first_state != ETL_NULLPTR, ETL_ERROR(etl::fsm_null_state_exception)); + p_state = p_first_state; if (call_on_enter_state) { - etl::fsm_state_id_t next_state = do_enters(ETL_NULLPTR, p_state, true); - - if (next_state != ifsm_state::No_State_Change) + do_enters_result result = do_enters(ETL_NULLPTR, p_first_state, true); + + if (result.active_state_id != ifsm_state::No_State_Change) { - p_state = state_list[next_state]; + // If the active_state_id is not No_State_Change, it means that an on_enter changed the target state. + // Set the state pointer as the active state to use it as the new origin for the transition to the + // updated target state. + ETL_ASSERT(result.active_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_state = state_list[result.active_state_id]; + + process_state_change(result.next_state_id); + } + else + { + if (have_changed_state(result.next_state_id)) + { + ETL_ASSERT(result.next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_state = state_list[result.next_state_id]; + } } } } @@ -83,7 +98,7 @@ namespace etl //******************************************* virtual void reset(bool call_on_exit_state = false) ETL_OVERRIDE { - if ((p_state != ETL_NULLPTR) && call_on_exit_state) + if (is_started() && call_on_exit_state) { do_exits(ETL_NULLPTR, p_state); } @@ -151,10 +166,21 @@ namespace etl return s; } + //******************************************* + /// Result of a "do_enters()" call + //******************************************* + struct do_enters_result + { + // State which is presently being targeted as the next state + etl::fsm_state_id_t next_state_id; + // State which was active when the on_enter triggered a state change + etl::fsm_state_id_t active_state_id; + }; + //******************************************* /// 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) + static do_enters_result 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)); @@ -163,8 +189,14 @@ namespace etl { 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); + // The parent we're calling shouldn't activate its defaults, or this state will be deactivated + do_enters_result result = do_enters(p_root, p_target->p_parent, false); + + // Short circuit the do enters if the parent state decided that a different state should be entered + if (result.active_state_id != ifsm_state::No_State_Change) + { + return result; + } } // Set us as our parent's active child @@ -172,9 +204,14 @@ namespace etl } etl::fsm_state_id_t next_state = p_target->on_enter_state(); - ETL_ASSERT(ifsm_state::No_State_Change == next_state, ETL_ERROR(etl::fsm_state_composite_state_change_forbidden)); + + // Short circuit the activation of any child states if the target state changed + if (next_state != ifsm_state::No_State_Change) + { + return {next_state, p_target->get_state_id()}; + } - // Activate default child if we need to activate any initial states in an active composite state. + // 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_child != ETL_NULLPTR) @@ -182,13 +219,19 @@ namespace etl 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_State_Change == next_state, ETL_ERROR(etl::fsm_state_composite_state_change_forbidden)); + + // Short circuit the activation of any child states if the target state changed + if (next_state != ifsm_state::No_State_Change) + { + return {next_state, p_target->get_state_id()}; + } } next_state = p_target->get_state_id(); } - return next_state; + // Wrapping No_State_Change in a static_cast gets rid of the "undefined reference" error when compiling on C++11 + return {next_state, static_cast(ifsm_state::No_State_Change)}; } //******************************************* @@ -217,7 +260,13 @@ namespace etl //******************************************* etl::fsm_state_id_t process_state_change(etl::fsm_state_id_t next_state_id) ETL_OVERRIDE { - if (have_changed_state(next_state_id)) + if (is_self_transition(next_state_id)) + { + p_state->on_exit_state(); + next_state_id = p_state->on_enter_state(); + } + + while (have_changed_state(next_state_id)) { ETL_ASSERT_OR_RETURN_VALUE(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception), p_state->get_state_id()); @@ -225,17 +274,34 @@ namespace etl etl::ifsm_state* p_root = common_ancestor(p_state, p_next_state); do_exits(p_root, p_state); - next_state_id = do_enters(p_root, p_next_state, true); - ETL_ASSERT_OR_RETURN_VALUE(next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception), p_state->get_state_id()); - p_state = state_list[next_state_id]; - } - else if (is_self_transition(next_state_id)) - { - p_state->on_exit_state(); - p_state->on_enter_state(); - } + do_enters_result result = do_enters(p_root, p_next_state, true); + next_state_id = result.next_state_id; + if (result.active_state_id != ifsm_state::No_State_Change) + { + // If the active_state_id is not No_State_Change, it means that an on_enter changed the target state. + // Set the state pointer as the active state to use it as the new origin for the transition to the + // updated target state. + ETL_ASSERT(result.active_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_state = state_list[result.active_state_id]; + ETL_ASSERT(result.next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_next_state = state_list[result.next_state_id]; + } + else if (result.next_state_id != ifsm_state::No_State_Change) + { + // If the next state is different, means that default children were activated. + // Assign both p_state and p_next_state to get out of the loop. + ETL_ASSERT(result.next_state_id < number_of_states, ETL_ERROR(etl::fsm_state_id_exception)); + p_state = state_list[result.next_state_id]; + p_next_state = state_list[result.next_state_id]; + } + else + { + p_state = p_next_state; + } + } + return p_state->get_state_id(); } }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 888fd12c..ab4121a6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -194,6 +194,7 @@ add_executable(etl_tests test_hash.cpp test_hfsm.cpp test_hfsm_recurse_to_inner_state_on_start.cpp + test_hfsm_transition_on_enter.cpp test_histogram.cpp test_index_of_type.cpp test_indirect_vector.cpp diff --git a/test/test_hfsm_transition_on_enter.cpp b/test/test_hfsm_transition_on_enter.cpp new file mode 100644 index 00000000..ff994c10 --- /dev/null +++ b/test/test_hfsm_transition_on_enter.cpp @@ -0,0 +1,363 @@ +/****************************************************************************** +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/string.h" +#include + +// entry && mode=RxEventDeviation +// ┌─────────────────────────────────────────────────────┐ +// │ │ +// * ┌────────▼─────────────┐ ToS6Event │ +// │ │ │ ┌────────────────────┐ │ +// │ │ s3 │ │ │ │ +// │ │ ┌─────────────────┐ │ │ ┌────────┼────────────┴──────────────┐ +// │ │ │ s2* │ │ │ │ s4 │ │ +// │ │ │ ┌────┐ ┌────┐ │ │ │ │ ┌────┴─────────────────────────┐ │ +// │ │ │ │ s1*│ │ s6 ◄─┼─┼─────┘ │ │ s5* │ │ +// │ │ │ │ │ │ │ │ │ entry && mode= │ ├──────────────────────────────┤ │ +// │ │ │ │ │ │ │ │ │ StartDeviation │ │ToS6Event && RxEventDuringTr./│ │ +// └──────┼──► │ │ │ │ ├─┼─────────────────┼───► receive(ToS5Event)** │ │ +// │ │ │ │ │ │ │ │ │ └─▲────────────────────────────┘ │ +// │ │ └┬───┘ └────┘ │ │ │ │ │ +// │ │ │ │ │ └─────┼──▲───────────────────────────┘ +// │ └──┼──────────────┘ │ │ │ +// │ │ │ │ │ +// └─────┼─┬──────────────┘ │ │ +// │ │ ToS5Event │ │ +// │ └──────────────────────────────────────┘ │ +// │ │ +// │ entry && mode=StartDeviationDefaultChild │ +// └───────────────────────────────────────────┘ +// +// * indicates default state +// ** Tests an exception for recursive receives +// Diagram created with asciiflow.com + +namespace +{ + class StateList; + + enum class Events : uint8_t + { + ToS5, + ToS6 + }; + + class ToS5Event : public etl::message(Events::ToS5)> + { + }; + + class ToS6Event : public etl::message(Events::ToS6)> + { + }; + + class EntryTestSM : public etl::hfsm + { + public: + EntryTestSM(); + + etl::string<30> test_data; + enum RunMode + { + Normal, + StartDeviation, + RxEventDeviation, + StartDeviationDefaultChild, + RxEventDuringTransition + }; + RunMode runMode = Normal; + + enum hfsmStates : uint8_t + { + S2, + S3, + S1, + S4, + S5, + S6, + Number_Of_States + }; + + private: + std::unique_ptr m_state_list; + }; + + class S1 : public etl::fsm_state + { + public: + etl::fsm_state_id_t on_enter_state() override + { + get_fsm_context().test_data += "E1"; + return (get_fsm_context().runMode == EntryTestSM::StartDeviationDefaultChild) + ? static_cast(EntryTestSM::S4) : No_State_Change; + } + + static etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + return No_State_Change; + } + + void on_exit_state() override + { + get_fsm_context().test_data += "X1"; + } + }; + + class S2 : public etl::fsm_state + { + public: + etl::fsm_state_id_t on_enter_state() override + { + get_fsm_context().test_data += "E2"; + return (get_fsm_context().runMode == EntryTestSM::StartDeviation) + ? static_cast(EntryTestSM::S5) : No_State_Change; + } + + static etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + return No_State_Change; + } + + void on_exit_state() override + { + get_fsm_context().test_data += "X2"; + } + }; + + class S3 : public etl::fsm_state + { + public: + etl::fsm_state_id_t on_enter_state() override + { + if(get_fsm_context().runMode == EntryTestSM::RxEventDuringTransition) + { + get_fsm_context().receive(ToS5Event{}); + } + get_fsm_context().test_data += "E3"; + return No_State_Change; + } + + static etl::fsm_state_id_t on_event(ToS5Event const& /*event*/) + { + return EntryTestSM::S5; + } + + static etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + return No_State_Change; + } + + void on_exit_state() override + { + get_fsm_context().test_data += "X3"; + } + }; + + class S4 : public etl::fsm_state + { + public: + etl::fsm_state_id_t on_enter_state() override + { + get_fsm_context().test_data += "E4"; + return (get_fsm_context().runMode == EntryTestSM::RxEventDeviation) + ? static_cast(EntryTestSM::S3) : No_State_Change; + } + + static etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + return No_State_Change; + } + + void on_exit_state() override + { + get_fsm_context().test_data += "X4"; + } + }; + + class S5 : public etl::fsm_state + { + public: + etl::fsm_state_id_t on_enter_state() override + { + get_fsm_context().test_data += "E5"; + return No_State_Change; + } + + etl::fsm_state_id_t on_event(ToS6Event const& /*event*/) + { + if (get_fsm_context().runMode == EntryTestSM::RxEventDuringTransition) + { + get_fsm_context().receive(ToS5Event{}); + } + return EntryTestSM::S6; + } + + static etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + return No_State_Change; + } + + void on_exit_state() override + { + get_fsm_context().test_data += "X5"; + } + }; + + class S6 : public etl::fsm_state + { + public: + etl::fsm_state_id_t on_enter_state() override + { + get_fsm_context().test_data += "E6"; + return No_State_Change; + } + + static etl::fsm_state_id_t on_event_unknown(const etl::imessage&) + { + return No_State_Change; + } + + void on_exit_state() override + { + get_fsm_context().test_data += "X6"; + } + }; + + class StateList + { + public: + StateList() + { + m_s3.add_child_state(m_s2); + m_s2.add_child_state(m_s1); + m_s2.add_child_state(m_s6); + + m_s4.add_child_state(m_s5); + } + + std::array(EntryTestSM::Number_Of_States)> stateList = { + &m_s2, &m_s3, &m_s1, &m_s4, &m_s5, &m_s6}; + + private: + // The states. + S1 m_s1; + S2 m_s2; + S3 m_s3; + S4 m_s4; + S5 m_s5; + S6 m_s6; + }; + + EntryTestSM::EntryTestSM() + : hfsm(0), m_state_list(new StateList{}) + { + set_states(m_state_list->stateList.data(), m_state_list->stateList.size()); + } + + SUITE(test_hfsm_transition_on_enter) + { + //************************************************************************* + TEST(start_regular) + { + EntryTestSM sm; + sm.start(true); + CHECK_EQUAL("E3E2E1", sm.test_data.c_str()); + } + + //************************************************************************* + TEST(start_deviation) + { + // Checks default child wanting to go to another state + EntryTestSM sm; + sm.runMode = EntryTestSM::StartDeviation; + sm.start(true); + CHECK_EQUAL("E3E2X2X3E4E5", sm.test_data.c_str()); + } + + //************************************************************************* + TEST(start_deviation_with_default_child_activation) + { + // Checks default child wanting to go to another state + EntryTestSM sm; + sm.runMode = EntryTestSM::StartDeviationDefaultChild; + sm.start(true); + CHECK_EQUAL("E3E2E1X1X2X3E4E5", sm.test_data.c_str()); + } + + //************************************************************************* + TEST(receive_event_normal) + { + EntryTestSM sm; + sm.start(true); + sm.test_data.clear(); + sm.receive(ToS5Event{}); + CHECK_EQUAL("X1X2X3E4E5", sm.test_data.c_str()); + } + + //************************************************************************* + TEST(receive_event_with_deviation) + { + // Test utilizes default child state entry + EntryTestSM sm; + sm.start(true); + sm.test_data.clear(); + sm.runMode = EntryTestSM::RxEventDeviation; + sm.receive(ToS5Event{}); + CHECK_EQUAL("X1X2X3E4X4E3E2E1", sm.test_data.c_str()); + } + + //************************************************************************* + TEST(go_to_non_default_state_and_back) + { + EntryTestSM sm; + // Skip enters so test data doesn't need to be cleared + sm.start(false); + sm.receive(ToS5Event{}); + sm.test_data.clear(); + sm.receive(ToS6Event{}); + CHECK_EQUAL("X5X4E3E2E6", sm.test_data.c_str()); + sm.test_data.clear(); + sm.receive(ToS5Event{}); + CHECK_EQUAL("X6X2X3E4E5", sm.test_data.c_str()); + } + + //************************************************************************* + TEST(reset_exits) + { + EntryTestSM sm; + sm.start(true); + sm.test_data.clear(); + sm.reset(true); + CHECK_EQUAL("X1X2X3", sm.test_data.c_str()); + } + } + +} // namespace