/* * Copyright (c) 2021 Project CHIP Authors * Copyright (c) 2021 SmartThings * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include namespace chip { namespace StateMachine { /** * An extension of the Optional class that removes the explicit requirement * for construction from a T value as a convenience to allow auto construction * of Optional. */ template class Optional : public chip::Optional { public: Optional(const T & value) : chip::Optional(value) {} Optional() : chip::Optional() {} }; /** * An extension of the Variant class offering pattern matching of State types * to dynamically dispatch execution of the required State interface methods: * Enter, Exit, GetName, LogTtransition. */ template struct VariantState : Variant { private: template void Enter(bool & ever) { if (!ever && chip::Variant::template Is()) { ever = true; chip::Variant::template Get().Enter(); } } template void Exit(bool & ever) { if (!ever && chip::Variant::template Is()) { ever = true; chip::Variant::template Get().Exit(); } } template void GetName(const char ** name) { if (name && !*name && chip::Variant::template Is()) { *name = chip::Variant::template Get().GetName(); } } template void LogTransition(bool & ever, const char * previous) { if (!ever && chip::Variant::template Is()) { ever = true; chip::Variant::template Get().LogTransition(previous); } } public: template static VariantState Create(Args &&... args) { VariantState instance; instance.template Set(std::forward(args)...); return instance; } void Enter() { bool ever = false; [](...) {}((this->template Enter(ever), 0)...); } void Exit() { bool ever = false; [](...) {}((this->template Exit(ever), 0)...); } const char * GetName() { const char * name = nullptr; [](...) {}((this->template GetName(&name), 0)...); return name; } void LogTransition(const char * previous) { bool ever = false; [](...) {}((this->template LogTransition(ever, previous), 0)...); } }; /** * The interface for dispatching events into the State Machine. * @tparam TEvent a variant holding the Events for the State Machine. */ template class Context { public: virtual ~Context() = default; /** * Dispatch an event to the current state. * @param evt a variant holding an Event for the State Machine. */ virtual void Dispatch(const TEvent & evt) = 0; }; /** * This is a functional approach to the State Machine design pattern. The design is * borrowed from http://www.vishalchovatiya.com/state-design-pattern-in-modern-cpp * and extended for this application. * * At a high-level, the purpose of a State Machine is to switch between States. Each * State handles Events. The handling of Events may lead to Transitions. The purpose * of this design pattern is to decouple States, Events, and Transitions. For instance, * it is desirable to remove knowledge of next/previous States from each individual * State. This allows adding/removing States with minimal code change and leads to a * simpler implementation. * * This State Machine design emulates C++17 features to achieve the functional approach. * Instead of using an enum or inheritance for the Events, the Events are defined as * structs and placed in a variant. Likewise, the States are all defined as structs and * placed in a variant. With the Events and States in two different variants, the * Transitions table uses the type introspction feature of the variant object to match a * given state and event to an optional new-state return. * * For event dispatch, the State Machine implements the Context interface. The Context * interface is passed to States to allow Dispatch() of events when needed. * * The State held in the TState must provide four methods to support calls from * the State Machine: * @code * struct State { * void Enter() { } * void Exit() { } * void LogTransition(const char *) { } * const char *GetName() { return ""; } * } * @endcode * * The TTransitions table type is implemented with an overloaded callable operator method * to match the combinations of State / Event variants that may produce a new-state return. * This allows the Transition table to define how each State responds to Events. Below is * an example of a Transitions table implemented as a struct: * * @code * struct Transitions { * using State = chip::StateMachine::VariantState; * chip::StateMachine::Optional operator()(State &state, Event &event) * { * if (state.Is() && event.Is()) * { * return State::Create(); * } * else if (state.Is() && event.Is()) * { * return State::Create(); * } * else * { * return {} * } * } * } * @endcode * * The rules for calling Dispatch from within the state machien are as follows: * * (1) Only the State::Enter method should call Dispatch. Calls from Exit or * LogTransition will cause an abort. * (2) The transitions table may return a new state OR call Dispatch, but must * never do both. Doing both will cause an abort. * * @tparam TState a variant holding the States. * @tparam TEvent a variant holding the Events. * @tparam TTransitions an object that implements the () operator for transitions. */ template class StateMachine : public Context { public: StateMachine(TTransitions & tr) : mCurrentState(tr.GetInitState()), mTransitions(tr), mSequence(0) {} ~StateMachine() override = default; void Dispatch(const TEvent & evt) override { ++mSequence; auto prev = mSequence; auto newState = mTransitions(mCurrentState, evt); if (newState.HasValue()) { auto oldState = mCurrentState.GetName(); mCurrentState.Exit(); mCurrentState = newState.Value(); mCurrentState.LogTransition(oldState); // It is impermissible to dispatch events from Exit() or // LogTransition(), or from the transitions table when a transition // has also been returned. Verify that this hasn't occurred. VerifyOrDie(prev == mSequence); mCurrentState.Enter(); } } TState GetState() { return mCurrentState; } private: TState mCurrentState; TTransitions & mTransitions; unsigned mSequence; }; } // namespace StateMachine } // namespace chip