Tutorial 9 — Hierarchical States
Real-world state machines quickly outgrow a flat list of states. SMACC2 supports hierarchical state grouping through mode states and super states, as demonstrated in the sm_three_some reference state machine. In this tutorial you will learn how to create mode states, super states with inner states, and loop patterns.
Mode States
A mode state is a top-level grouping of states, analogous to an operating mode. The sm_three_some state machine has two modes:
// mode_states/ms_run.hpp
struct MsRun : smacc2::SmaccState<MsRun, SmThreeSome, StState1>
{
using SmaccState::SmaccState;
};
// mode_states/ms_recover.hpp
struct MsRecover : smacc2::SmaccState<MsRecover, SmThreeSome>
{
using SmaccState::SmaccState;
typedef mpl::list<
Transition<EvToDeep, MsRun::deep_history>
>reactions;
};
Key points:
SmaccState<MsRun, SmThreeSome, StState1>— the third template parameter is the initial child state. When the machine entersMsRun, it immediately entersStState1.States inside
MsRundeclare their parent asMsRun, notSmThreeSome.MsRecovertransitions toMsRun::deep_history, which returns to the last active state insideMsRun— deep history preserves the full state hierarchy.
The state machine declaration uses the mode state as its initial state:
struct SmThreeSome
: public smacc2::SmaccStateMachineBase<SmThreeSome, MsRun>
{
using SmaccStateMachineBase::SmaccStateMachineBase;
virtual void onInitialize() override
{
this->createOrthogonal<OrTimer>();
this->createOrthogonal<OrKeyboard>();
this->createOrthogonal<OrSubscriber>();
}
};
Super States
A super state groups related inner states into a reusable, self-contained unit. Super states use a namespace to prevent name collisions between inner states:
// superstates/ss_superstate_1.hpp
namespace SS1
{
// Forward declarations
class StiState1;
class StiState2;
class StiState3;
struct Ss1 : smacc2::SmaccState<Ss1, MsRun, StiState1>
{
using SmaccState::SmaccState;
typedef mpl::list<
Transition<EvLoopEnd<Ss1>, SS2::Ss2>
>reactions;
static void staticConfigure()
{
configure_orthogonal<OrTimer, CbTimerCountdownOnce>(10);
configure_orthogonal<OrSubscriber, CbWatchdogSubscriberBehavior>();
configure_orthogonal<OrKeyboard, CbDefaultKeyboardBehavior>();
}
int iteration_count = 0;
static constexpr int total_iterations() { return 5; }
void runtimeConfigure() { iteration_count = 0; }
};
} // namespace SS1
Key points:
SmaccState<Ss1, MsRun, StiState1>— parent isMsRun, initial inner state isStiState1.The namespace
SS1prevents inner state names (StiState1, etc.) from colliding with those in other super states.iteration_countandtotal_iterations()support the loop pattern.The super state’s transition
EvLoopEnd<Ss1>fires when the inner loop completes.
Inner States
Inner states belong to their super state:
// states/inner_states/sti_state_1.hpp
namespace SS1
{
struct StiState1 : smacc2::SmaccState<StiState1, Ss1>
{
using SmaccState::SmaccState;
typedef mpl::list<
Transition<EvLoopContinue<Ss1>, StiState2, CONTINUELOOP>
>reactions;
bool loopWhileCondition()
{
auto & ss = this->context<Ss1>();
return ss.iteration_count++ < ss.total_iterations();
}
void onEntry()
{
RCLCPP_INFO(getLogger(), "Loop iteration: %d",
this->context<Ss1>().iteration_count);
checkWhileLoopConditionAndThrowEvent(&StiState1::loopWhileCondition);
}
};
} // namespace SS1
The loop pattern:
StiState1::onEntry()callscheckWhileLoopConditionAndThrowEvent().This evaluates
loopWhileCondition()— if true, it postsEvLoopContinue, transitioning toStiState2.The inner states cycle:
StiState1 → StiState2 → StiState3 → StiState1.When
loopWhileCondition()returns false (after 5 iterations),EvLoopEndis posted.The super state’s transition table matches
EvLoopEnd<Ss1>and exits the super state.
Accessing Super State Context
Inner states access their super state’s data through this->context<Ss1>():
auto & ss = this->context<Ss1>();
int count = ss.iteration_count;
This returns a reference to the live super state instance, so you can read and write its member variables.
Accessing Super State Context from Client Behaviors
this->context<Ss>() is a Boost Statechart method available only inside state classes. Client behaviors need a different approach — navigate up the hierarchy via getParentState():
void onEntry() override
{
auto * ss = dynamic_cast<SsMission *>(
this->getCurrentState()->getParentState());
ss->initialPosition = Position2D{1.0, 2.0};
}
getCurrentState() returns the active leaf state as ISmaccState *, and getParentState() returns its parent — the live superstate instance. The dynamic_cast recovers the concrete type so you can access its members.
The sm_data_sharing_2 reference state machine demonstrates this pattern end-to-end: SsMission holds initialPosition and targetPosition fields, and three client behaviors (CbStoreData1, CbStoreData2, CbProcessData) read and write them across state transitions using getParentState(). See the source code.
Hierarchy Overview
The sm_three_some hierarchy looks like this:
SmThreeSome
├── MsRun (initial)
│ ├── StState1 (initial child of MsRun)
│ ├── StState2
│ ├── StState3
│ ├── StState4
│ ├── SS1::Ss1 (super state)
│ │ ├── StiState1 → StiState2 → StiState3 (loop × 5)
│ │ └── EvLoopEnd → SS2::Ss2
│ └── SS2::Ss2 (super state)
│ ├── StiState1 → StiState2 → StiState3 (loop × 5)
│ └── EvLoopEnd → StState4
└── MsRecover
└── EvToDeep → MsRun::deep_history
Transitions can cross hierarchy levels. A regular state inside MsRun can transition to a super state (StState3 → SS1::Ss1), and a super state can transition out to another super state (SS1::Ss1 → SS2::Ss2) or a regular state (SS2::Ss2 → StState4).
Summary
You learned:
Mode states group states into operational modes (
MsRun,MsRecover)Super states group inner states with namespaces for isolation
The loop pattern:
checkWhileLoopConditionAndThrowEvent()+EvLoopContinue/EvLoopEnd``this->context<Ss>()`` accesses parent super state data
Deep history (
MsRun::deep_history) restores the full state hierarchy on recovery
Next Steps
In Tutorial 10 — Multi-Stage Missions you will combine all these concepts into a complete multi-stage autonomous mission.