Merge branch 'pull-request/#1199-Added-support-for-transitions-on-state-enter-for-HFSM' into development

This commit is contained in:
John Wellbelove 2025-10-11 11:01:53 +01:00
commit 85678586c4
5 changed files with 464 additions and 45 deletions

View File

@ -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();
}

View File

@ -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<fsm_state_id_t>(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();
}
};

View File

@ -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

View File

@ -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 <array>
// 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<static_cast<uint8_t>(Events::ToS5)>
{
};
class ToS6Event : public etl::message<static_cast<uint8_t>(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<StateList> m_state_list;
};
class S1 : public etl::fsm_state<EntryTestSM, S1, EntryTestSM::S1>
{
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<uint8_t>(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<EntryTestSM, S2, EntryTestSM::S2>
{
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<uint8_t>(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<EntryTestSM, S3, EntryTestSM::S3, ToS5Event>
{
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<EntryTestSM, S4, EntryTestSM::S4>
{
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<uint8_t>(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<EntryTestSM, S5, EntryTestSM::S5, ToS6Event>
{
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<EntryTestSM, S6, EntryTestSM::S6>
{
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<etl::ifsm_state*, static_cast<uint8_t>(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

View File

@ -10282,6 +10282,7 @@
<ClCompile Include="..\test_expected.cpp" />
<ClCompile Include="..\test_function_traits.cpp" />
<ClCompile Include="..\test_hfsm_recurse_to_inner_state_on_start.cpp" />
<ClCompile Include="..\test_hfsm_transition_on_enter.cpp" />
<ClCompile Include="..\test_index_of_type.cpp" />
<ClCompile Include="..\test_intrusive_links.cpp" />
<ClCompile Include="..\test_macros.cpp" />