From 0ce0d706078291c384f9f2b435c8ad0bd7054ba0 Mon Sep 17 00:00:00 2001 From: Laszlo Kiss Date: Fri, 27 Nov 2015 13:42:59 -0700 Subject: [PATCH] Initial commit. --- FSM/AsynchronousFSM.cc | 543 ++++++++++++++++++++++++++++++ FSM/FiniteStateMachine.hh | 690 ++++++++++++++++++++++++++++++++++++++ FSM/SynchronousFSM.cc | 277 +++++++++++++++ FSM/main.cc | 23 ++ LICENSE | 15 +- README.md | 34 +- 6 files changed, 1572 insertions(+), 10 deletions(-) create mode 100644 FSM/AsynchronousFSM.cc create mode 100644 FSM/FiniteStateMachine.hh create mode 100644 FSM/SynchronousFSM.cc create mode 100644 FSM/main.cc diff --git a/FSM/AsynchronousFSM.cc b/FSM/AsynchronousFSM.cc new file mode 100644 index 0000000..930e90b --- /dev/null +++ b/FSM/AsynchronousFSM.cc @@ -0,0 +1,543 @@ +// Copyright (c) 2015 Delta Prime, Inc. +// +// 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. + +// A very simple sample function and appropriate classes to show how an +// asynchronous finite state machine (FSM) may be implemented. + +#include +#include + +#include +#include + +#include "FiniteStateMachine.hh" + + +// The namespace where the FiniteStateMachine lives. +// +using namespace Core; + +// A very basic audio recorder model which can be stopped, playing or recording. +// It can also skip to the next recording. + +namespace AudioRecorder +{ + /** + * The subject matter information as the name implies holds the domain + * specific information that the state machine is operating on. This is + * not strictly necessary, but it can be helpful. + */ + struct SubjectMatterInformation + { + using SongName = std::string; + using SongDuration = int; + + std::deque< std::tuple< SongName, SongDuration > > songs; + + int current_song; // the one that is about to play (or is playing) + int next_song_number; // for numbering recordings + + SubjectMatterInformation() : current_song( 0 ), next_song_number( 1 ) { } + }; + + + /** + * The state machine of the AudioPlayer object. This is the class in which + * activity actually occurs. + */ + class Machine + : public FiniteStateMachine<> + { + public : + + Machine(); + virtual ~Machine(); + + /** + * Starts the thread on which this machine is executing. + */ + void StartThread(); + + /** + * Stops the thread. + */ + void StopThread(); + + + /** + * Override to allow this machine to post the event to the thread in a + * safe manner. This is what makes the machine async capable. + */ + void EventDeliveryMethod( const Event & event ) override; + + + // The calls that the FSM receives from the outside world. They must be + // converted to appropriate events and delivered to the current state. + + /** + * Called by the external system when it needs to activate the player. + */ + void PushPlay() { PostEvent( START_PLAYING_EVENT ); } + + + /** + * Called by the external system when it needs to deactivate the player. + */ + void PushStop() { PostEvent( STOP_PLAYING_EVENT ); } + + + /** + * Called by the external system to start recording a song. + */ + void PushRecord() { PostEvent( START_RECORDING_EVENT ); } + + + /** + * Called by the external system to skip to the next song. + */ + void PushSkipForward() { PostEvent( ADVANCE_TO_NEXT_EVENT ); } + + + private : + + // The state machine's event set. + // + enum + { + START_PLAYING_EVENT, + STOP_PLAYING_EVENT, + START_RECORDING_EVENT, + ADVANCE_TO_NEXT_EVENT, + TIMEOUT_EVENT + }; + + + private : + // The domain specific information block. + // + SubjectMatterInformation info; + + // These store the state numbers associated with the state objects that + // are to be installed. + // + StateID STOPPED_STATE; + StateID PLAYING_STATE; + StateID RECORDING_STATE; + + // The states supported by this machine. + // + State stopped; + State playing; + State recording; + + // These two form the basis of the Boost asynch I/O framework, which is + // good for event driven work. + // + boost::asio::io_service io; + boost::asio::io_service::work work; + + // The thread in which the recorder is operating. + // + std::thread thread; + + // Use a timer to pretend that songs are playing for some variable amount + // of time. + // + boost::asio::deadline_timer song_timer; + + + private : + + // These methods set the entry/exit function objects and install the + // event handlers and transitions. + // + void ConfigureStoppedState(); + void ConfigurePlayingState(); + void ConfigureRecordingState(); + + // The methods that actually perform the functions requested by the + // external system, but in the context of the thread. + // + // The work is being done in the FSM not in the individual states. + // These methods implement algorithms that the states' event handlers can + // call on event arrival. + // + void PerformPlay(); + void PerformStop(); + void PerformRecord(); + void PerformSkip(); + + // A utility method to go to the next song. + // + void AdvanceToNextSong(); + + // Accomodate the operating convention of the boost deadline timer. + // + void PerformTimeout( const boost::system::error_code & error ) + { + if ( error != boost::asio::error::operation_aborted ) PostEvent( TIMEOUT_EVENT ); + } + }; +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +AudioRecorder::Machine::Machine() + : io() + , work( io ) + , thread() + , song_timer( io ) +{ + // The songs to play. + // + info.songs.push_back( { "Heartbeat Song", 2 } ); + info.songs.push_back( { "One Hot Mess", 3 } ); + info.songs.push_back( { "Cool", 2 } ); + + // The supported states must be registered. + // + STOPPED_STATE = RegisterState( & stopped ); + PLAYING_STATE = RegisterState( & playing ); + RECORDING_STATE = RegisterState( & recording ); + + // Establish the starting state, this is important. + // + SetInitialState( STOPPED_STATE ); + + // Configure the states' event handlers and transitions. + // + ConfigureStoppedState(); + ConfigurePlayingState(); + ConfigureRecordingState(); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +AudioRecorder::Machine::~Machine() +{ +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::StartThread() +{ + try + { + this->thread = std::thread( [this](){ this->io.run(); } ); + } + catch ( const std::system_error & e ) + { + std::cout << "ERROR: Failed to spawn thread. [" << e.code() << "] " << e.what(); + throw; + } +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::StopThread() +{ + io.stop(); + if ( thread.joinable() ) + { + thread.join(); + } +} + + +//------------------------------------------------------------------------------ +// The event here is posted to the thread for execution. +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::EventDeliveryMethod( const Event & event ) +{ + io.post( + [this, event]() + { + FiniteStateMachine<>::EventDeliveryMethod( event ); + } + ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::ConfigureStoppedState() +{ + stopped.SetEntryFunction( + [this]( const Event & ) + { + std::cout << "=== Special entry code executed for state: 'STOPPED' ===" << std::endl; + } + ); + + stopped.SetExitFunction( + [this]( const Event & ) + { + std::cout << "=== Special exit code executed for state: 'STOPPED' ===" << std::endl; + } + ); + + stopped + .SetEventHandler( + STOP_PLAYING_EVENT, + [this]( const Event & ) + { + std::cout << "Already stopped, nothing to do." << std::endl; + } + ) + .SetEventHandler( + ADVANCE_TO_NEXT_EVENT, + [this]( const Event & ) + { + std::cout << "Stopped, can't skip." << std::endl; + } + ) + .SetTransition( START_PLAYING_EVENT, PLAYING_STATE ) + .SetTransition( START_RECORDING_EVENT, RECORDING_STATE ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::ConfigurePlayingState() +{ + playing.SetEntryFunction( + [this]( const Event & ) + { + PerformPlay(); + } + ); + + playing.SetExitFunction( + [this]( const Event & ) + { + song_timer.cancel(); + } + ); + + playing + .SetEventHandler( + START_PLAYING_EVENT, + [this]( const Event & ) + { + std::cout << "Already playing, nothing to do." << std::endl; + } + ) + .SetEventHandler( + START_RECORDING_EVENT, + [this]( const Event & ) + { + std::cout << "Playing, can't record." << std::endl; + } + ) + .SetEventHandler( + ADVANCE_TO_NEXT_EVENT, + [this]( const Event & ) + { + std::cout << "Skipping..." << std::endl; + PerformSkip(); + PerformPlay(); + } + ) + .SetEventHandler( + TIMEOUT_EVENT, + [this]( const Event & ) + { + AdvanceToNextSong(); + PerformPlay(); + } + ) + .SetTransition( STOP_PLAYING_EVENT, STOPPED_STATE ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::ConfigureRecordingState() +{ + recording.SetEntryFunction( + [this]( const Event & ) + { + PerformRecord(); + } + ); + + recording.SetExitFunction( + [this]( const Event & ) + { + song_timer.cancel(); + } + ); + + recording + .SetEventHandler( + START_PLAYING_EVENT, + [this]( const Event & event ) + { + std::cout << "Recording, can't play." << std::endl; + } + ) + .SetEventHandler( + START_RECORDING_EVENT, + [this]( const Event & event ) + { + std::cout << "Already recording, nothing to do." << std::endl; + } + ) + .SetEventHandler( + ADVANCE_TO_NEXT_EVENT, + [this]( const Event & ) + { + std::cout << "Recording, can't skip." << std::endl; + } + ) + .SetEventHandler( + TIMEOUT_EVENT, + [this]( const Event & event ) + { + PerformRecord(); + } + ) + .SetTransition( STOP_PLAYING_EVENT, STOPPED_STATE ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::PerformPlay() +{ + std::cout << "Playing '" << std::get<0>(info.songs[info.current_song]) << "'." << std::endl; + + const int duration = std::get<1>(info.songs[info.current_song]); + + song_timer.expires_from_now( boost::posix_time::seconds(duration) ); + song_timer.async_wait( + boost::bind( + & AudioRecorder::Machine::PerformTimeout, + this, + boost::asio::placeholders::error + ) + ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::PerformStop() +{ + std::cout << "Stopped playing." << std::endl; +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::PerformRecord() +{ + std::ostringstream ostr; + ostr << "Recorded Song " << info.next_song_number++; + const int duration = 2 + (info.next_song_number % 2); + info.songs.push_back( { ostr.str(), duration } ); + + std::cout << "Recording '" << ostr.str() << "'." << std::endl; + + song_timer.expires_from_now( boost::posix_time::seconds(duration) ); + song_timer.async_wait( + boost::bind( + & AudioRecorder::Machine::PerformTimeout, + this, + boost::asio::placeholders::error + ) + ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::PerformSkip() +{ + song_timer.cancel(); + AdvanceToNextSong(); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioRecorder::Machine::AdvanceToNextSong() +{ + info.current_song++; + if ( info.current_song > (info.songs.size() - 1) ) + { + info.current_song = 0; + } +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +RunAsynchronousFSM() +{ + std::cout << "<<< Asynchronous FSM Sample Started >>>" << std::endl << std::endl; + + AudioRecorder::Machine audio_recorder; + + audio_recorder.StartThread(); + + sleep( 2 ); + audio_recorder.PushStop(); + sleep( 2 ); + audio_recorder.PushPlay(); + sleep( 10 ); + audio_recorder.PushSkipForward(); + sleep( 5 ); + audio_recorder.PushRecord(); + sleep( 1 ); + audio_recorder.PushStop(); + audio_recorder.PushRecord(); + sleep( 5 ); + audio_recorder.PushPlay(); + sleep( 2 ); + audio_recorder.PushStop(); + sleep( 2 ); + + audio_recorder.PushPlay(); + sleep( 15 ); + audio_recorder.PushStop(); + sleep( 1 ); + + audio_recorder.StopThread(); + + std::cout << std::endl << "<<< Asynchronous FSM Sample Finished >>>" << std::endl << std::endl; +} + diff --git a/FSM/FiniteStateMachine.hh b/FSM/FiniteStateMachine.hh new file mode 100644 index 0000000..7754c3e --- /dev/null +++ b/FSM/FiniteStateMachine.hh @@ -0,0 +1,690 @@ +#pragma once + +// Copyright (c) 2015 Delta Prime, Inc. +// +// 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. + +/** + * The definition of a Finite State Machine (FSM) and corresponding state + * object. + * + * These classes allow the user to create state machines fairly simply. + * + * The states are very simple, they include a dispatch table for the events + * to which they respond and they can optionally have entry and exit + * methods. The Finite State Machine proper is responsible for actually + * driving the state machine. To keep things simple that class can also be the + * one that implements domain specific methods which the state's event handlers + * call on. + * + * There are two event queues, one external and one internal. The external + * event queue is generally filled by events generted on behalf of the clients + * of the FSM. The internal event queue allows the FSM to perform multiple + * transitions in response to one external event and therefore has higher + * priority. + * + * @author: Laszlo Kiss + * @since: 5/1/2015 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + + +// A namespace to partition the FSM class from the rest of the system. +// +namespace Core +{ + /** + * The base class for any finite state machine implementation. + * The Event template parameter is a type that may function as a key in + * a std::map<> and which supports automatic type conversion to 'int'. + * The HANDLER template parameter specifies the function object that is + * to be used as the event handler. + */ + template < + typename EVENT = int, + typename HANDLER = std::function< void ( const EVENT & ) > + > + class FiniteStateMachine + { + public : + /** + * A convenience typedef. + */ + using Event = EVENT; + + + /** + * An event handler is simply a function object that is called to handle + * an event. + */ + using EventHandler = HANDLER; + + + /** + * The EntryFunction is called when entering a new state. + */ + using EntryFunction = std::function< void ( const Event & ) >; + + + /** + * The ExitFunction is called when leaving the current state. + */ + using ExitFunction = std::function< void ( const Event & ) >; + + + /** + * An alternate mechanism to refer to a state object. + */ + using StateID = int; + + + /** + * The exception thrown when the state id number is out of bounds. + */ + class Exception + : public std::runtime_error + { + public : + Exception( const std::string & why = "" ) + : std::runtime_error( why ) + { } + }; + + + /** + * The base class of all State objects compatible with this FSM + * implementation. + */ + class State + { + public : + /** + * Default constructor sets all members to their default values. + */ + State() + : entry_function() + , exit_function() + , dispatch_table() + { } + + + /** + * Sets the function the FSM calls when the state is entered. + * + * @param efun The entry function object to set. + * @return The old entry function or nullptr if none. + */ + virtual EntryFunction SetEntryFunction( + const EntryFunction & efun + ) + { + EntryFunction old_function{}; + std::swap( old_function, entry_function ); + entry_function = efun; + return std::move( old_function ); + } + + + /** + * Sets the function the FSM calls when the state is exited. + * + * @param efun The exit function object to set. + * @return The old exit function or nullptr if none. + */ + virtual ExitFunction SetExitFunction( + const ExitFunction & efun + ) + { + ExitFunction old_function{}; + std::swap( old_function, exit_function ); + exit_function = efun; + return std::move( old_function ); + } + + + /** + * Sets the state transition to take when a particular event arrives. + * + * @param event The event that is to be handled. + * @param state The state to which to transition. + * @return The reference to this State object. + */ + virtual State & SetTransition( + const Event & event, + StateID state + ) + { + // Indicates that the event has already been configured in this + // state. A single event can't have multiple functions. + // + assert( dispatch_table.find( event ) == dispatch_table.end() ); + + if ( state < LowestValidStateID ) + { + throw Exception("Out of bounds state id number."); + } + transition_table.insert( { event, state } ); + return *this; + } + + + /** + * Replaces a an existing transition with another. + * This is a 'fancy' capability that should be used with caution. + * + * @param event The event that is to be handled. + * @param handler The handler function object to replace the existing one. + * @return the old state that was replaced or SentinelStateID indicating an error. + */ + virtual StateID ReplaceTransition( + const Event & event, + StateID state + ) + { + StateID old_state{ SentinelStateID }; + auto dit = transition_table.find( event ); + if ( dit != transition_table.end() ) + { + std::swap( dit->second, old_state ); + transition_table[event] = state; + } + return old_state; + } + + + /** + * Retrieves the target state if a transition was installed for the + * provided event. + * + * @param event The event for which the target state is needed. + * @return the StateID of the target state or SentinelStateID if there is no such. + */ + inline StateID TransitionForEvent( + const Event & event + ) const + { + StateID target{ SentinelStateID }; + auto dit = transition_table.find( event ); + if ( dit != transition_table.end() ) + { + target = dit->second; + } + return target; + } + + + /** + * Sets the event handler function the FSM calls when an event needs + * to be handled. + * + * @param event The event that is to be handled. + * @param handler The handler function object to set. + * @return The reference to this State object. + */ + virtual State & SetEventHandler( + const Event & event, + const EventHandler & handler + ) + { + // Indicates that the event has already been configured in this + // state. A single event can't have multiple functions. + // + assert( transition_table.find( event ) == transition_table.end() ); + dispatch_table.insert( { event, handler } ); + return *this; + } + + + /** + * Replaces a potentially existing event handler function with another. + * This is a 'fancy' capability that should be used with caution. + * + * @param event The event that is to be handled. + * @param handler The handler function object to replace the existing one. + * @return The event handler that was replaced. + */ + virtual EventHandler ReplaceEventHandler( + const Event & event, + const EventHandler & handler + ) + { + EventHandler old_handler{}; + auto dit = dispatch_table.find( event ); + if ( dit != dispatch_table.end() ) + { + std::swap( dit->second, old_handler ); + dispatch_table[event] = handler; + } + return std::move( old_handler ); + } + + + /** + * Called by the FiniteStateMachine class when it is dispatching an + * event to the state. The state returns the corresponding event + * handler which is subsequently called to handle the event. + * Note that the state object is simply a container for these functions. + * All activity takes place in the event handler (not the State) in the + * context of the FiniteStateMachine's DeliverNextEvent() method. + * + * @return The EventHandler for the specified event or nullptr if no such. + */ + inline EventHandler HandlerForEvent( + const Event & event + ) const + { + EventHandler handler{}; + auto dit = dispatch_table.find( event ); + if ( dit != dispatch_table.end() ) + { + handler = dit->second; + } + return std::move( handler ); + } + + + /** + * Called by the FiniteStateMachine when a state transition is + * taking place and the state is becoming the new current state. + * + * @return The EntryFunction of the state. + */ + inline EntryFunction Entry() const { return entry_function; } + + + /** + * Called by the FinitieStateMachine when a state transition is + * taking place and the state is becoming the previous state. + * @return The ExitFunction of the state. + */ + inline ExitFunction Exit() const { return exit_function; } + + + private : + /// The mechanism used to associate an event handler with an event in + /// the state. + /// + using DispatchTable = std::map< Event, EventHandler >; + + /// Describes what state transition to take when an event arrives for + /// the state. Note that the DispatchTable and the TransitionTable + /// are mutually exclusive (the same event can't be in both). + /// + using TransitionTable = std::map< Event, StateID >; + + + private : + EntryFunction entry_function; + ExitFunction exit_function; + DispatchTable dispatch_table; + TransitionTable transition_table; + }; + + + public : + /// A value that may be returned when requesting the current state and + /// the initial state has not yet been set. + /// + static const StateID SentinelStateID{ -1 }; + + + public : + /** + * Default constructor. + */ + FiniteStateMachine() + : previous_state( SentinelStateID ) + , current_state( SentinelStateID ) + { } + + + /** + * The destructor. + */ + virtual ~FiniteStateMachine() = default; + + + /** + * Registers the passed in state object with the FSM. The state is + * associated with an identifier that is static for the life of the FSM. + * The passed in state object remains the property and responsibility of + * the caller. + * + * @param state The state object to register with the FSM. + * @return the state identifier chosen for the state. + */ + virtual StateID RegisterState( + State *state + ) + { + int state_number = static_cast( state_table.size() ); + state_table.push_back( state ); + return state_number; + } + + + /** + * The primary work horse method of the FSM which posts (enqueues) the + * provided event on the normal priority event queue. + * + * @param event The event that is to be queued for execution by the state + * machine. + */ + inline void PostEvent( + const Event & event + ) + { + EventDeliveryMethod( event ); + } + + + /** + * The secondary work horse method of the FSM when it is necessary to + * post a high priority event (internal event) that should be executed before + * any of the normal priority events. Enables the FSM to make multiple + * state transitions between normal priority events (thus the term internal). + * + * @param event The internal event that is to be queued for execution by the + * state machine. + */ + inline void PostInternalEvent( + const Event & event + ) + { + internal_events.push_back( event ); + } + + + /** + * @return the current state identifier of the FSM. + */ + inline StateID CurrentState() const + { + return current_state; + } + + + /** + * @return the previous state identifier of the FSM. + */ + inline StateID PreviousState() const + { + return previous_state; + } + + + /** + * Allows the outright setting of the current state w/o executing any + * events or entry/exit functions. + * + * @param new_state The new state that should become the FSM's current state. + */ + void SetInitialState( + StateID new_state + ) + { + if ( new_state > (StateID) state_table.size() || new_state < LowestValidStateID ) + { + throw Exception("Out of bounds state id number."); + } + + current_state = new_state; + } + + + protected : + /// Events are queued to the state machine and stored in instances of this + /// type. + /// + using Events = std::deque< Event >; + + /// The states registered for this state machine are held in this type. + /// The raw pointer here is used only as a reference and the FSM must + /// assure that the lifetime of States are longer than the state table. + /// + using StateTable = std::vector< State * >; + + + protected : + /// A boundary number that identifies the lowest numeric value for a + /// a state number. + /// + static const StateID LowestValidStateID{ 0 }; + + + protected : + + /** + * The method that actually performs the event delivery. It is meant to + * help with the asynchronous version of this class. In that case a + * proper replacement should be defined that delivers the event to the + * appropriate thread context. + */ + virtual void EventDeliveryMethod( const Event & event ) + { + events.push_back( event ); + while ( DeliverNextEvent() ); + } + + /** + * The handler installed here catches any events that the current + * state does not handle. If not set, undefined events for a particular + * state are simply ignored. If defined, then it is executed if the + * state has no event handler defined. + */ + inline void InstallDefaultEventHandler( const EventHandler & handler ) + { + default_handler = handler; + } + + /** + * @return true if there are events waiting to be executed. + */ + inline bool HaveEventsToProcess() const + { + return ! (internal_events.empty() && events.empty()); + } + + + /** + * The driving function that consumes and executes the queued up events. + * Priority is given to 'internal' events. Only a single event at a time + * is executed. + * In a synchronous FSM the method should be called from 'interface' + * methods that are called by foreign objects only (just after the + * corresponding event is posted - see examples). + * + * @return true if there are more events available to process. + */ + bool DeliverNextEvent() + { + // Indicates that the intial state has not been set via the + // SetInitialState() method. + // + assert( current_state != SentinelStateID ); + + if ( ! HaveEventsToProcess() ) + { + return false; + } + + if ( ! internal_events.empty() ) + { + Event ev( internal_events.front() ); + State *state = state_table[current_state]; + + assert( state != nullptr ); + + StateID target = state->TransitionForEvent( ev ); + if ( target != SentinelStateID ) + { + TransitionToState( target, ev ); + } + else + { + EventHandler handler = state->HandlerForEvent( ev ); + if ( handler != nullptr ) + handler( ev ); + else if ( default_handler != nullptr ) + default_handler( ev ); + } + internal_events.pop_front(); + } + else if ( ! events.empty() ) + { + Event ev( events.front() ); + State *state = state_table[current_state]; + + assert( state != nullptr ); + + StateID target = state->TransitionForEvent( ev ); + if ( target != SentinelStateID ) + { + TransitionToState( target, ev ); + } + else + { + EventHandler handler( state->HandlerForEvent( ev ) ); + if ( handler != nullptr ) + handler( ev ); + else if ( default_handler != nullptr ) + default_handler( ev ); + } + events.pop_front(); + } + + return HaveEventsToProcess(); + } + + + /** + * @return the number of events queued for execution in the external event queue. + */ + inline size_t ExternalEventBacklog() const + { + return events.size(); + } + + + /** + * @return the number of events queued for execution in the internal event queue. + */ + inline size_t InternalEventBacklog() const + { + return internal_events.size(); + } + + + /** + * Remove and discard all events from the external event queue. + */ + inline void PurgeExternalEvents() + { + events.clear(); + } + + + /** + * Remove and discard all events from the internal event queue. + */ + inline void PurgeInternalEvents() + { + internal_events.clear(); + } + + + private : + StateID previous_state; /// The state prior to the current one. + StateID current_state; /// The index into the state table. + StateTable state_table; /// The registered states. + Events events; /// External events queued up. + Events internal_events; /// Internal events queued up. + + /// Set to handle the case when the current state has no suitable event + /// handler + /// + EventHandler default_handler; + + + private : + /** + * The mechanism for performing state transitions. If there is an existing + * non-null state then it's exit function is called (if not null). Once + * the new state is set the entry function of the new state is called (if + * not null). + * + * @param new_state The state to which the FSM should transition to. + * @param event The event that caused the state transition. + * + * @return the previous state (from which the transition was taken). + */ + StateID TransitionToState( + StateID new_state, + const Event & event + ) + { + // Indicates that the intial state has not been set via the + // SetInitialState() method. + // + assert( current_state != SentinelStateID ); + + if ( new_state == current_state ) + { + return current_state; // no transition necessary + } + + if ( new_state > (StateID) state_table.size() || new_state < LowestValidStateID ) + { + std::ostringstream ostr; + ostr << "Out of bounds state id number '" << new_state + << "' on transition from state '" << current_state + << "' via event '" << (int)event << "'."; + throw Exception( ostr.str() ); + } + + ExitFunction on_exit( state_table[current_state]->Exit() ); + if ( on_exit != nullptr ) + { + on_exit( event ); + } + + previous_state = current_state; + current_state = new_state; + + EntryFunction on_entry( state_table[current_state]->Entry() ); + if ( on_entry != nullptr ) + { + on_entry( event ); + } + + return previous_state; + } + + }; + +} + diff --git a/FSM/SynchronousFSM.cc b/FSM/SynchronousFSM.cc new file mode 100644 index 0000000..0139a53 --- /dev/null +++ b/FSM/SynchronousFSM.cc @@ -0,0 +1,277 @@ +// Copyright (c) 2015 Delta Prime, Inc. +// +// 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. + +// A very simple sample function and appropriate classes to show how a +// synchronous finite state machine (FSM) may be implemented. + +#include + +#include "FiniteStateMachine.hh" + + +// The namespace where the FiniteStateMachine lives. +// +using namespace Core; + + +// A very basic audio player model which can be stopped or playing a song. + +namespace AudioPlayer +{ + /** + * The subject matter information as the name implies holds the domain + * specific information that the state machine is operating on. This is + * not strictly necessary, but it can be helpful. + */ + struct SubjectMatterInformation + { + std::string song; /// The song to play. + }; + + + /** + * The state machine of the AudioPlayer object. This is the class in which + * activity actually occurs. + */ + class Machine + : public FiniteStateMachine<> + { + public : + + Machine(); + virtual ~Machine(); + + // The calls that the FSM receives from the outside world. They must be + // converted to appropriate events and delivered to the current state. + + /** + * Called by the external system when it needs to activate the player. + */ + void PushPlay(); + + /** + * Called by the external system when it needs to deactivate the player. + */ + void PushStop(); + + + private : + + // The state machine's event set. + // + enum + { + START_PLAYING_EVENT, + STOP_PLAYING_EVENT + }; + + + private : + // The domain specific information block. + // + SubjectMatterInformation info; + + // These store the state numbers associated with the state objects that + // are to be installed. + // + StateID STOPPED_STATE; + StateID PLAYING_STATE; + + // The states supported by this machine. + // + State stopped; + State playing; + + + private : + // These methods set the entry/exit function objects and install the + // event handlers and transitions. + // + void ConfigureStoppedState(); + void ConfigurePlayingState(); + + // The work is being done in the FSM not in the individual states. + // These methods implement algorithms that the states' event handlers can + // call on event arrival. + // + void PerformPlay(); + void PerformStop(); + }; +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +AudioPlayer::Machine::Machine() +{ + // The single song this player plays. + // + info.song = "It's a Wonderful World"; + + // The supported states must be registered. + // + STOPPED_STATE = RegisterState( & stopped ); + PLAYING_STATE = RegisterState( & playing ); + + // Establish the starting state, this is important. + // + SetInitialState( STOPPED_STATE ); + + // Configure the states' event handlers and transitions. + // + ConfigureStoppedState(); + ConfigurePlayingState(); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +AudioPlayer::Machine::~Machine() +{ +} + + +//------------------------------------------------------------------------------ +// This method is always called by a foreign object, so it is necessary to +// call the DeliverNextEvent() method as the last step. This is what drives +// the state machine and this is what makes the state machine 'synchronous'. +//------------------------------------------------------------------------------ +void +AudioPlayer::Machine::PushPlay() +{ + // The event to post that will be delivered momentarily. + // + PostEvent( START_PLAYING_EVENT ); + + // Event deliveries occur here until all (internal) events have been + // processed. The only reason this method can be called here is because the + // internals of this machine never call this method. + // + while ( DeliverNextEvent() ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioPlayer::Machine::PushStop() +{ + PostEvent( STOP_PLAYING_EVENT ); + while ( DeliverNextEvent() ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioPlayer::Machine::ConfigureStoppedState() +{ + stopped.SetEntryFunction( + [this]( const Event & ) + { + PerformStop(); + } + ); + + stopped.SetExitFunction( + [this]( const Event & ) + { + std::cout << "=== Special exit code executed for state: 'STOPPED' ===" << std::endl; + } + ); + + stopped + .SetEventHandler( + STOP_PLAYING_EVENT, + [this]( const Event & ) + { + std::cout << "Already stopped, nothing to do." << std::endl; + } + ) + .SetTransition( START_PLAYING_EVENT, PLAYING_STATE ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioPlayer::Machine::ConfigurePlayingState() +{ + playing.SetEntryFunction( + [this]( const Event & ) + { + PerformPlay(); + } + ); + + playing.SetExitFunction( + [this]( const Event & ) + { + std::cout << "=== Special exit code executed for state: 'PLAYING' ===" << std::endl; + } + ); + + playing + .SetEventHandler( + START_PLAYING_EVENT, + [this]( const Event & ) + { + std::cout << "Already playing, nothing to do." << std::endl; + } + ) + .SetTransition( STOP_PLAYING_EVENT, STOPPED_STATE ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioPlayer::Machine::PerformPlay() +{ + std::cout << "Playing '" << info.song << "'." << std::endl; +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +AudioPlayer::Machine::PerformStop() +{ + std::cout << "Stopped playing." << std::endl; +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void +RunSynchronousFSM() +{ + std::cout << "<<< Synchronous FSM Sample Started >>>" << std::endl << std::endl; + + AudioPlayer::Machine audio_player; + + audio_player.PushStop(); + audio_player.PushPlay(); + audio_player.PushPlay(); + audio_player.PushStop(); + + std::cout << std::endl << "<<< Synchronous FSM Sample Finished >>>" << std::endl << std::endl; +} + diff --git a/FSM/main.cc b/FSM/main.cc new file mode 100644 index 0000000..bb43182 --- /dev/null +++ b/FSM/main.cc @@ -0,0 +1,23 @@ +#include +#include + +/** + * A sample function that exercises a fictional FSM based on a rudimentary + * audio player. + */ +extern void RunSynchronousFSM(); + +/** + * A sample function that exercises a fictional asynchronous FSM based on a + * an audio recorder. + */ +extern void RunAsynchronousFSM(); + + +int main ( int argc, const char * argv[] ) +{ + RunSynchronousFSM(); + RunAsynchronousFSM(); + return 0; +} + diff --git a/LICENSE b/LICENSE index 20efd1b..21713a2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,4 @@ -The MIT License (MIT) - -Copyright (c) 2015 +Copyright (c) 2015 Delta Prime, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,14 +7,13 @@ 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 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 +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. - +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 9059eb5..9b11627 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,34 @@ # FiniteStateMachine -A simple, header only, finite state machine implementation for C++. + +Simple state machine classes in C++. + +The state machine may have any number of states and events. There can be two types +of Events, external and internal. External events are generated by the 'outside world' +or users of the state machine. Internal events may be generated by the state machinery +itself and are considered high priority (thus serviced before external events). Their +use is optional. + +Each state may have an optional entry and exit method in addition to the event handler +methods. + +See: https://en.wikipedia.org/wiki/Finite-state_machine + +## Install +Place in a convenient include directory and include in a C++11 source file. + +##Features +* Header only. +* No dependencies. +* Supports both synchronous and asynchronous use (examples included). + +## Tested on: +* Mac OS X 10.10 (Xcode 7 - Apple LLVM version 7.0.0 (clang-700.1.76)) + +## Alternatives +* https://github.com/makulik/sttcl +* https://github.com/eglimi/cppfsm +* http://www.boost.org/doc/libs/1_58_0/libs/statechart/doc/index.html +* http://www.boost.org/doc/libs/1_58_0/libs/msm/doc/HTML/index.html + +## License +* MIT