Skip to content

Legacy Plans

Andrew Gresyk edited this page Apr 2, 2022 · 1 revision

Plans vs Planners

hfsm.dev doesn't have a planner and they are outside of the scope of the library, but it does support plans.

Plans

A plan is a sequence of tasks, represented in hfsm.dev by regions and states.

Code Readability

Often transitions within a region follow a linear pattern. Such regions can be referred to as 'sequences' and looping sequences are often called 'cycles'.

Using plans can make code more expressive and easy to read by co-locating all transition logic in one place.

using M = hfsm2::Machine;          // stateID

using FSM = M::Root<struct Apex,   //      0
                struct Red,        //      1
                struct Yellow,     //      2
                struct Green       //      3
            >;

struct Apex
    : FSM::State
{
    void enter(PlanControl& control) {
        // create an empty plan
        auto plan = control.plan();

        // loop yellow -> green -> yellow -> red
        plan.change<Red, Yellow>();
        plan.change<Yellow, Green>();
        plan.change<Green, Yellow>();
        plan.change<Yellow, Red>();
    }
};

struct Red    : FSM::State {
    void update(FullControl& control) { control.succeed(); }
};

struct Yellow : FSM::State {
    void update(FullControl& control) { control.succeed(); }
};

struct Green  : FSM::State {
    void update(FullControl& control) { control.succeed(); }
};

FSM::Instance fsm;

assert(fsm.isActive<Red>());

fsm.update();
assert(fsm.isActive<Yellow>());

fsm.update();
assert(fsm.isActive<Green>());

fsm.update();
assert(fsm.isActive<Yellow>());

fsm.update();
assert(fsm.isActive<Red>());

Notice how the cycle goes through Yellow twice in different directions. Without using plans, building this trivial FSM would require:

  • either an extra bool to track direction
  • or two Yellow variants for both directions: YellowDownward + YellowUpward

Leveraging plans to cycle states helps keep the client code clean and easy to follow.

Reactive => Proactive Decisionmaking

Traditionally, building multi-step behaviors has been a challenge for FSMs, as they can only reason one step ahead. Which is why they are often referred to as 'reactive' formalisms.

With plans, building complex behaviors becomes not only attainable, but also easy.

Design

Plans in the hierarchy are 'hosted' by the composite regions.

To continue to the next step in a plan, the current task reports 'successful execution'.

If a task reports 'execution failure', the plan in progress is stopped.

Regions can define plan execution reactions for both success and failure.

API Summary

Configuration

As hfsm.dev uses static data structures, the container for the plan tasks is preallocated.

The default capacity is twice the number of all the sub-states of composite regions in the FSM.

Config::TaskCapacityN<NCapacity> can be used to override it:

using M = hfsm2::MachineT<hfsm2::Config::TaskCapacityN<200>>;

using FSM = M::Root<S(Apex),
                //...

Accessing Plans

Own plan Any region's plan
PlanControl::plan() PlanControl::plan<TRegion>()

Read-only vs writable plans

Some state methods can only read existing plans, others can modify them, depening on the type of Control they have for an argument:

Read-only access Writable access
rank (const Control&)
utility(const Control&)
entryGuard(GuardControl&)
exitGuard (GuardControl&)

enter (PlanControl&)
reenter(PlanControl&)
exit (PlanControl&)

update(FullControl&)
react (FullControl&)

planSucceeded(FullControl&)
planFailed (FullControl&)

Success / failure reactions

Optional plan execution reactions can be defined by the regions hosting plans:

Event Reaction method
Plan success planSucceeded(FullControl&)
Plan failure planFailed(FullControl&)

Plan methods

Event Reaction method Example
Empty check explicit Plan::operator bool() const if (auto p = control.plan()) ...
Task iteration Plan::first() for (auto it = p.begin(); it; ++it) it->...
Remove all tasks Plan::clear()

Plan transitions

Plan transition methods take two arguments:

  • Origin task that hast to succeed to trigger the transition
  • Destination task

Semantically they follow the usual transition methods available in Control classes:

Fully templated Hybrid Non-templated
change <TOrigin, TDestination>()
restart <TOrigin, TDestination>()
resume <TOrigin, TDestination>()
utilize <TOrigin, TDestination>()
randomize<TOrigin, TDestination>()
schedule <TOrigin, TDestination>()
change <TOrigin>(destinationID)
restart <TOrigin>(destinationID)
resume <TOrigin>(destinationID)
utilize <TOrigin>(destinationID)
randomize<TOrigin>(destinationID)
schedule <TOrigin>(destinationID)
change (originID, destinationID)
restart (originID, destinationID)
resume (originID, destinationID)
utilize (originID, destinationID)
randomize(originID, destinationID)
schedule (originID, destinationID)

Detailed Demo

using M = hfsm2::Machine;                                 // stateID

using FSM = M::Root<struct Apex,                          //      0
                M::Composite<struct PlanOwner,            //      1
                    struct StateTask,                     //      2
                    M::Composite<struct CompositeTask,    //      3
                        struct CT_Initial,                //      4
                        struct CT_Following               //      5
                    >,
                    M::Orthogonal<struct OrthogonalTask,  //      6
                        struct OT_1,                      //      7
                        struct OT_2                       //      8
                    >,
                    struct ReplanTask,                    //      9
                    M::Composite<struct SubPlanOwner,     //     10
                        struct SubTask_1,                 //     11
                        struct SubTask_2                  //     12
                    >,
                    struct End                            //     13
                >
            >;

struct Apex : FSM::State {};

struct PlanOwner
    : FSM::State
{
    void enter(PlanControl& control) {
        auto plan = control.plan();

        // build the plan by sequencing transitions
        // StateTask -> CompositeTask -> OrthogonalTask -> SubPlanOwner
        //
        // sequence links can be ordred arbitrarily
        // here - ordered linearly for readability
        plan.restart<StateTask, CompositeTask>();
        plan.change<CompositeTask, OrthogonalTask>();
        plan.change<OrthogonalTask, ReplanTask>();
        // skipping SubPlanOwner on purpose, see ReplanTask
        plan.change<ReplanTask, End>();
    }

    // optional plan execution result reactions
    // can be used to alter the execution flow
    //void planSucceeded(FullControl& control) {}

    // respond to plan failure below
    void planFailed(FullControl& control) {
        control.changeTo<End>();
    }
};

struct StateTask
    : FSM::State
{
    void update(FullControl& control) {
        control.succeed();
    }
};

struct CompositeTask : FSM::State {};

struct CT_Initial
    : FSM::State
{
    void update(FullControl& control) {
        // mark the task as successful
        control.changeTo<CT_Following>();
    }
};

struct CT_Following
    : FSM::State
{
    void update(FullControl& control) {
        // even though CompositeTask has no plan attached to it,
        // the sub-state can 'succeed' the entire region
        control.succeed();
    }
};

struct OrthogonalTask    : FSM::State {};

struct OT_1              : FSM::State {};

struct OT_2
    : FSM::State
{
    void update(FullControl& control) {
        // the task can also be marked successful
        // from its sub-state one level down
        control.succeed();
    }
};

struct ReplanTask
    : FSM::State
{
    void update(FullControl& control) {
        // parametrized version of PlanControl::plan() allows access to plans
        // hosted by any region
        auto plan = control.plan<PlanOwner>();
        assert(plan);

        // inspect the plan
        auto taskIterator = plan.first();
        assert(taskIterator);
        assert(taskIterator->origin == control.stateId<ReplanTask>());
        assert(taskIterator->destination == control.stateId<End>());

        // loop over plan task sequence
        for (auto it = plan.first(); it; ++it) {
            if (it->destination == control.stateId<End>())
                // and remove task links
                it.remove();
        }

        // when the plan is empty it is reported as 'invalid'
        assert(!plan);

        // plan can be explicitly cleared too
        plan.clear();

        // and re-populated
        plan.change<ReplanTask, SubPlanOwner>();

        control.succeed();
    }
};

struct SubPlanOwner
    : FSM::State
{
    // guard gets executed before the first sub-state activates implicitly ..
    void entryGuard(GuardControl& control) {
        // .. so the plan can start from the second sub-state
        control.changeTo<SubTask_2>();

        // and then continue in the reverse order
        control.plan().change<SubTask_2, SubTask_1>();
    }

    // without plan execution reactions, plan results
    // are propagated to the outer plan
    //
    //void planSucceeded(FullControl& control) {}
    //void planFailed(FullControl& control) {}
};

// these appear in the plan in reverse order
struct SubTask_1
    : FSM::State
{
    void update(FullControl& control) {
        // owner region's planFailed() gets called in response to plan failure
        control.fail();
    }
};

struct SubTask_2
    : FSM::State
{
    void update(FullControl& control) {
        // continue in reverse order, as defined by the plan in SubPlanOwner
        control.succeed();
    }
};

struct End
    : FSM::State
{};

FSM::Instance fsm;

assert(fsm.isActive<PlanOwner>());
assert(fsm.isActive<StateTask>());

fsm.update();
assert(fsm.isActive<CompositeTask>());
assert(fsm.isActive<CT_Initial>());

fsm.update();
assert(fsm.isActive<CompositeTask>());
assert(fsm.isActive<CT_Following>());

fsm.update();
assert(fsm.isActive<OrthogonalTask>());
assert(fsm.isActive<OT_1>());
assert(fsm.isActive<OT_2>());

fsm.update();
assert(fsm.isActive<ReplanTask>());

fsm.update();
assert(fsm.isActive<SubPlanOwner>());
assert(fsm.isActive<SubTask_2>());

fsm.update();
assert(fsm.isActive<SubPlanOwner>());
assert(fsm.isActive<SubTask_1>());

fsm.update();
assert(fsm.isActive<End>());

See Also

snippets/wiki_plans.cpp
test/test_plans.cpp