State-machine structure, initial work.

This commit is contained in:
James Turner 2013-01-20 14:57:20 +01:00
parent bd71635c49
commit 22ea8ebe25
6 changed files with 889 additions and 2 deletions

View File

@ -23,6 +23,7 @@ set(HEADERS
exception.hxx
intern.hxx
subsystem_mgr.hxx
StateMachine.hxx
)
set(SOURCES
@ -36,7 +37,15 @@ set(SOURCES
commands.cxx
event_mgr.cxx
exception.cxx
subsystem_mgr.cxx
subsystem_mgr.cxx
StateMachine.cxx
)
simgear_component(structure structure "${SOURCES}" "${HEADERS}")
if(ENABLE_TESTS)
add_executable(test_state_machine state_machine_test.cxx)
target_link_libraries(test_state_machine SimGearCore)
add_test(test_state_machine ${EXECUTABLE_OUTPUT_PATH}/test_state_machine)
endif(ENABLE_TESTS)

View File

@ -11,6 +11,7 @@
# include <simgear_config.h>
#endif
#include <boost/foreach.hpp>
#include <simgear/compiler.h>
#include "SGBinding.hxx"
@ -23,6 +24,14 @@ SGBinding::SGBinding()
{
}
SGBinding::SGBinding(const std::string& commandName)
: _command(0),
_arg(0),
_setting(0)
{
_command_name = commandName;
}
SGBinding::SGBinding(const SGPropertyNode* node, SGPropertyNode* root)
: _command(0),
_arg(0),
@ -98,3 +107,10 @@ SGBinding::fire (double setting) const
fire();
}
}
void fireBindingList(const SGBindingList& aBindings)
{
BOOST_FOREACH(SGBinding_ptr b, aBindings) {
b->fire();
}
}

View File

@ -38,6 +38,12 @@ public:
*/
SGBinding ();
/**
* Convenience constructor.
*
* @param node The binding will be built from this node.
*/
SGBinding(const std::string& commandName);
/**
* Convenience constructor.
@ -119,7 +125,15 @@ private:
mutable SGPropertyNode_ptr _setting;
};
typedef std::vector<SGSharedPtr<SGBinding> > SGBindingList;
typedef SGSharedPtr<SGBinding> SGBinding_ptr;
typedef std::vector<SGBinding_ptr > SGBindingList;
typedef std::map<unsigned,SGBindingList> SGBindingMap;
/**
* fire every binding in a list, in sequence
*/
void fireBindingList(const SGBindingList& aBindings);
#endif

View File

@ -0,0 +1,456 @@
/* -*-c++-*-
*
* Copyright (C) 2013 James Turner
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
#ifdef HAVE_CONFIG_H
# include <simgear_config.h>
#endif
#include "StateMachine.hxx"
#include <cassert>
#include <set>
#include <boost/foreach.hpp>
#include <simgear/debug/logstream.hxx>
#include <simgear/structure/SGBinding.hxx>
#include <simgear/props/condition.hxx>
#include <simgear/timing/timestamp.hxx>
#include <simgear/structure/exception.hxx>
namespace simgear
{
typedef std::vector<StateMachine::State_ptr> StatePtrVec;
static void readBindingList(SGPropertyNode* desc, const std::string& name,
SGPropertyNode* root, SGBindingList& result)
{
BOOST_FOREACH(SGPropertyNode* b, desc->getChildren(name)) {
SGBinding* bind = new SGBinding;
bind->read(b, root);
result.push_back(bind);
}
}
///////////////////////////////////////////////////////////////////////////
class StateMachine::State::StatePrivate
{
public:
std::string _name;
SGBindingList _updateBindings,
_entryBindings,
_exitBindings;
};
///////////////////////////////////////////////////////////////////////////
class StateMachine::Transition::TransitionPrivate
{
public:
std::string _name;
SGBindingList _bindings;
std::set<State*> _sourceStates; ///< weak refs to source states
State* _target;
SGSharedPtr<SGCondition> _condition;
};
///////////////////////////////////////////////////////////////////////////
class StateMachine::StateMachinePrivate : public SGPropertyChangeListener
{
public:
StateMachinePrivate(StateMachine* p) : _p(p) { }
void computeEligibleTransitions()
{
_eligible.clear();
BOOST_FOREACH(Transition_ptr t, _transitions) {
if (t->applicableForState(_currentState)) {
_eligible.push_back(t.ptr());
}
}
}
StateMachine* _p;
State_ptr _currentState;
StatePtrVec _states;
std::vector<Transition_ptr> _transitions;
std::vector<Transition*> _eligible;
SGTimeStamp _timeInState;
bool _listenerLockout; ///< block our listener when self-updating props
virtual void valueChanged(SGPropertyNode* changed)
{
if (_listenerLockout) {
return;
}
if (changed == _currentStateIndex) {
State_ptr s = _p->stateByIndex(changed->getIntValue());
_p->changeToState(s);
} else if (changed == _currentStateName) {
_p->changeToStateName(changed->getStringValue());
}
}
// exposed properties
SGPropertyNode_ptr _root;
SGPropertyNode_ptr _currentStateIndex;
SGPropertyNode_ptr _currentStateName;
SGPropertyNode_ptr _timeInStateProp;
};
///////////////////////////////////////////////////////////////////////////
StateMachine::State::State(const std::string& aName) :
d(new StatePrivate)
{
d->_name = aName;
}
StateMachine::State::~State()
{
}
std::string StateMachine::State::name() const
{
return d->_name;
}
void StateMachine::State::update()
{
fireBindingList(d->_updateBindings);
}
void StateMachine::State::fireEntryBindings()
{
fireBindingList(d->_entryBindings);
}
void StateMachine::State::fireExitBindings()
{
fireBindingList(d->_exitBindings);
}
void StateMachine::State::addUpdateBinding(SGBinding* aBinding)
{
d->_updateBindings.push_back(aBinding);
}
void StateMachine::State::addEntryBinding(SGBinding* aBinding)
{
d->_entryBindings.push_back(aBinding);
}
void StateMachine::State::addExitBinding(SGBinding* aBinding)
{
d->_exitBindings.push_back(aBinding);
}
///////////////////////////////////////////////////////////////////////////
StateMachine::Transition::Transition(const std::string& aName, State* aTarget) :
d(new TransitionPrivate)
{
assert(aTarget);
d->_name = aName;
d->_target = aTarget;
}
StateMachine::Transition::~Transition()
{
}
StateMachine::State* StateMachine::Transition::target() const
{
return d->_target;
}
void StateMachine::Transition::addSourceState(State* aSource)
{
if (aSource == d->_target) { // should this be disallowed outright?
SG_LOG(SG_GENERAL, SG_WARN, d->_name << ": adding target state as source");
}
d->_sourceStates.insert(aSource);
}
bool StateMachine::Transition::applicableForState(State* aCurrent) const
{
return d->_sourceStates.count(aCurrent);
}
bool StateMachine::Transition::evaluate() const
{
return d->_condition->test();
}
void StateMachine::Transition::fireBindings()
{
fireBindingList(d->_bindings);
}
std::string StateMachine::Transition::name() const
{
return d->_name;
}
void StateMachine::Transition::setTriggerCondition(SGCondition* aCondition)
{
d->_condition = aCondition;
}
void StateMachine::Transition::addBinding(SGBinding* aBinding)
{
d->_bindings.push_back(aBinding);
}
///////////////////////////////////////////////////////////////////////////
StateMachine::StateMachine() :
d(new StateMachinePrivate(this))
{
d->_root = new SGPropertyNode();
}
StateMachine::~StateMachine()
{
}
void StateMachine::init()
{
d->_currentStateIndex = d->_root->getChild("current-index", 0, true);
d->_currentStateIndex->setIntValue(0);
d->_currentStateName = d->_root->getChild("current-name", 0, true);
d->_currentStateName->setStringValue("");
d->_currentStateIndex->addChangeListener(d.get());
d->_currentStateName->addChangeListener(d.get());
d->_timeInStateProp = d->_root->getChild("elapsed-time-msec", 0, true);
d->_timeInStateProp->setIntValue(0);
// TODO go to default state if found
d->computeEligibleTransitions();
}
void StateMachine::shutdown()
{
d->_currentStateIndex->removeChangeListener(d.get());
d->_currentStateName->removeChangeListener(d.get());
}
void StateMachine::innerChangeState(State_ptr aState, Transition_ptr aTrans)
{
d->_currentState->fireExitBindings();
// fire bindings before we change the state, hmmmm
if (aTrans) {
aTrans->fireBindings();
}
// update our private state and properties
d->_listenerLockout = true;
d->_currentState = aState;
d->_timeInState.stamp();
d->_currentStateName->setStringValue(d->_currentState->name());
d->_currentStateIndex->setIntValue(indexOfState(aState));
d->_timeInStateProp->setIntValue(0);
d->_listenerLockout = false;
// fire bindings
d->_currentState->fireEntryBindings();
d->_currentState->update();
d->computeEligibleTransitions();
}
void StateMachine::changeToState(State_ptr aState, bool aOnlyIfDifferent)
{
assert(aState != NULL);
if (std::find(d->_states.begin(), d->_states.end(), aState) == d->_states.end()) {
throw sg_exception("Requested change to state not in machine");
}
if (aOnlyIfDifferent && (aState == d->_currentState)) {
return;
}
innerChangeState(aState, NULL);
}
void StateMachine::changeToStateName(const std::string& aName, bool aOnlyIfDifferent)
{
State_ptr st = findStateByName(aName);
if (!st) {
throw sg_range_exception("unknown state:" + aName);
}
changeToState(st, aOnlyIfDifferent);
}
StateMachine::State_ptr StateMachine::state() const
{
return d->_currentState;
}
SGPropertyNode* StateMachine::root()
{
return d->_root;
}
void StateMachine::update(double aDt)
{
// do this first, for triggers which depend on time in current state
// (spring-loaded transitions)
d->_timeInStateProp->setIntValue(d->_timeInState.elapsedMSec());
Transition_ptr trigger;
BOOST_FOREACH(Transition* trans, d->_eligible) {
if (trans->evaluate()) {
if (trigger != Transition_ptr()) {
SG_LOG(SG_GENERAL, SG_WARN, "ambiguous transitions! "
<< trans->name() << " or " << trigger->name());
}
trigger = trans;
}
}
if (trigger != Transition_ptr()) {
SG_LOG(SG_GENERAL, SG_DEBUG, "firing transition:" << trigger->name());
innerChangeState(trigger->target(), trigger);
}
d->_currentState->update();
}
StateMachine::State_ptr StateMachine::findStateByName(const std::string& aName) const
{
BOOST_FOREACH(State_ptr sp, d->_states) {
if (sp->name() == aName) {
return sp;
}
}
SG_LOG(SG_GENERAL, SG_WARN, "unknown state:" << aName);
return State_ptr();
}
StateMachine::State_ptr StateMachine::stateByIndex(unsigned int aIndex) const
{
if (aIndex >= d->_states.size()) {
throw sg_range_exception("invalid state index, out of bounds");
}
return d->_states[aIndex];
}
int StateMachine::indexOfState(State_ptr aState) const
{
StatePtrVec::const_iterator it = std::find(d->_states.begin(), d->_states.end(), aState);
if (it == d->_states.end()) {
return -1;
}
return it - d->_states.begin();
}
StateMachine::State_ptr StateMachine::createState(const std::string& aName)
{
if (findStateByName(aName) != NULL) {
throw sg_range_exception("duplicate state name");
}
State_ptr st = new State(aName);
addState(st);
return st;
}
StateMachine::Transition_ptr
StateMachine::createTransition(const std::string& aName, State_ptr aTarget)
{
Transition_ptr t = new Transition(aName, aTarget);
addTransition(t);
return t;
}
StateMachine* StateMachine::createFromPlist(SGPropertyNode* desc, SGPropertyNode* root)
{
StateMachine* sm = new StateMachine;
BOOST_FOREACH(SGPropertyNode* stateDesc, desc->getChildren("state")) {
std::string nm = stateDesc->getStringValue("name");
State_ptr st(new State(nm));
readBindingList(stateDesc, "enter", root, st->d->_updateBindings);
readBindingList(stateDesc, "exit", root, st->d->_entryBindings);
readBindingList(stateDesc, "update", root, st->d->_exitBindings);
sm->addState(st);
} // of states iteration
BOOST_FOREACH(SGPropertyNode* tDesc, desc->getChildren("transition")) {
std::string nm = tDesc->getStringValue("name");
State_ptr target = sm->findStateByName(tDesc->getStringValue("target"));
SGCondition* cond = sgReadCondition(root, tDesc->getChild("condition"));
Transition_ptr t(new Transition(nm, target));
t->setTriggerCondition(cond);
BOOST_FOREACH(SGPropertyNode* src, desc->getChildren("source")) {
State_ptr srcState = sm->findStateByName(src->getStringValue());
t->addSourceState(srcState);
}
readBindingList(tDesc, "binding", root, t->d->_bindings);
sm->addTransition(t);
} // of states iteration
return sm;
}
void StateMachine::addState(State_ptr aState)
{
bool wasEmpty = d->_states.empty();
d->_states.push_back(aState);
if (wasEmpty) {
d->_currentState = aState;
}
}
void StateMachine::addTransition(Transition_ptr aTrans)
{
d->_transitions.push_back(aTrans);
}
} // of namespace simgear

View File

@ -0,0 +1,166 @@
/* -*-c++-*-
*
* Copyright (C) 2013 James Turner
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
#ifndef SIMGEAR_STATE_MACHINE_H
#define SIMGEAR_STATE_MACHINE_H
#include <memory>
#include <simgear/structure/SGReferenced.hxx>
#include <simgear/structure/SGSharedPtr.hxx>
// forward decls
class SGPropertyNode;
class SGBinding;
class SGCondition;
namespace simgear
{
class StateMachine : public SGReferenced
{
public:
StateMachine();
virtual ~StateMachine();
class State : public SGReferenced
{
public:
virtual ~State();
std::string name() const;
void addUpdateBinding(SGBinding* aBinding);
void addEntryBinding(SGBinding* aBinding);
void addExitBinding(SGBinding* aBinding);
private:
friend class StateMachine;
State(const std::string& name);
void fireExitBindings();
void fireEntryBindings();
void update();
class StatePrivate;
std::auto_ptr<StatePrivate> d;
};
class Transition : public SGReferenced
{
public:
virtual ~Transition();
std::string name() const;
/**
* The state we end in, after this transition fires
*/
State* target() const;
/**
* Add a state in which this transition is eligible to fire
*/
void addSourceState(State* aSource);
/**
* Specify the transition trigger condition. Takes ownership
*/
void setTriggerCondition(SGCondition* aCondition);
void addBinding(SGBinding* aBinding);
private:
friend class StateMachine;
Transition(const std::string& aName, State* aTarget);
/**
* predicate to determine if this transition can fire given a
* current state.
*/
bool applicableForState(State* aCurrent) const;
/**
* test if the transition should fire, based on current state
*/
bool evaluate() const;
void fireBindings();
class TransitionPrivate;
std::auto_ptr<TransitionPrivate> d;
};
typedef SGSharedPtr<State> State_ptr;
typedef SGSharedPtr<Transition> Transition_ptr;
/**
* create a state machine from a property list description
*/
static StateMachine* createFromPlist(SGPropertyNode* desc, SGPropertyNode* root);
SGPropertyNode* root();
void init();
void shutdown();
void update(double dt);
State_ptr state() const;
/**
* public API to force a change to a particular state.
* @param aOnlyIfDifferent - only make a transition if the new state is
* different from the current state. Otherwise, the existing state will
* be exited and re-entered.
*/
void changeToState(State_ptr aState, bool aOnlyIfDifferent=true);
/// wrapper to change state by looking up a name
void changeToStateName(const std::string& aName, bool aOnlyIfDifferent=true);
State_ptr findStateByName(const std::string& aName) const;
State_ptr stateByIndex(unsigned int aIndex) const;
int indexOfState(State_ptr aState) const;
// programatic creation
State_ptr createState(const std::string& aName);
Transition_ptr createTransition(const std::string& aName, State_ptr aTarget);
private:
void addState(State_ptr aState);
void addTransition(Transition_ptr aTrans);
void innerChangeState(State_ptr aState, Transition_ptr aTrans);
class StateMachinePrivate;
std::auto_ptr<StateMachinePrivate> d;
};
typedef SGSharedPtr<StateMachine> StateMachine_ptr;
} // of simgear namespace
#endif // of SIMGEAR_STATE_MACHINE_H

View File

@ -0,0 +1,226 @@
#ifdef HAVE_CONFIG_H
# include <simgear_config.h>
#endif
#ifdef NDEBUG
// Always enable DEBUG mode in test application, otherwise "assert" test
// statements have no effect and don't actually test anything (catch 17 ;-) ).
#undef NDEBUG
#endif
#include <simgear/compiler.h>
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include "StateMachine.hxx"
#include <simgear/structure/SGBinding.hxx>
#include <simgear/structure/exception.hxx>
#include <simgear/props/condition.hxx>
#include <simgear/props/props.hxx>
#include <simgear/props/props_io.hxx>
#include <simgear/structure/commands.hxx>
using std::string;
using std::cout;
using std::cerr;
using std::endl;
// SGCondition subclass we can trivially manipulate from test code.
class DummyCondition : public SGCondition
{
public:
DummyCondition(): _state(false) { }
virtual bool test() const
{
return _state;
}
bool _state;
};
static int dummy_cmd_state = 0;
bool dummyCommand(const SGPropertyNode* arg)
{
++dummy_cmd_state;
return true;
}
#define COMPARE(a, b) \
if ((a) != (b)) { \
cerr << "failed:" << #a << " != " << #b << endl; \
cerr << "\tgot:'" << a << "'" << endl; \
exit(1); \
}
#define VERIFY(a) \
if (!(a)) { \
cerr << "failed:" << #a << endl; \
exit(1); \
}
using namespace simgear;
#define BUILD_MACHINE_1() \
StateMachine_ptr sm(new StateMachine); \
StateMachine::State_ptr stateA = sm->createState("a"); \
StateMachine::State_ptr stateB = sm->createState("b"); \
StateMachine::State_ptr stateC = sm->createState("c"); \
\
DummyCondition* trigger1 = new DummyCondition; \
StateMachine::Transition_ptr t1 = sm->createTransition(">b", stateB); \
t1->addSourceState(stateA); \
t1->setTriggerCondition(trigger1); \
\
DummyCondition* trigger2 = new DummyCondition; \
StateMachine::Transition_ptr t2 = sm->createTransition(">c", stateC); \
t2->addSourceState(stateB); \
t2->setTriggerCondition(trigger2); \
\
DummyCondition* trigger3 = new DummyCondition; \
StateMachine::Transition_ptr t3 = sm->createTransition(">a", stateA); \
t3->addSourceState(stateC); \
t3->addSourceState(stateB); \
t3->setTriggerCondition(trigger3); \
sm->init();
void testBasic()
{
BUILD_MACHINE_1();
////////////////////////////////////////////
COMPARE(sm->state()->name(), "a");
COMPARE(sm->indexOfState(stateA), 0);
COMPARE(sm->findStateByName("c"), stateC);
sm->changeToState(stateC);
COMPARE(sm->state(), stateC);
trigger3->_state = true;
sm->update(1.0);
COMPARE(sm->state()->name(), "a");
trigger3->_state = false;
trigger1->_state = true;
sm->update(1.0);
trigger1->_state = false;
COMPARE(sm->state()->name(), "b");
trigger3->_state = true;
sm->update(1.0);
COMPARE(sm->state()->name(), "a");
trigger3->_state = false;
trigger1->_state = true;
sm->update(1.0);
trigger1->_state = false;
COMPARE(sm->state()->name(), "b");
trigger2->_state = true;
sm->update(1.0);
trigger2->_state = false;
COMPARE(sm->state()->name(), "c");
//////////////////////////////////////////
COMPARE(sm->root()->getIntValue("current-index"), 2);
COMPARE(sm->root()->getStringValue("current-name"), string("c"));
sm->root()->setStringValue("current-name", "b");
COMPARE(sm->state()->name(), "b");
////////////////////////////////////////
COMPARE(sm->findStateByName("foo"), NULL);
COMPARE(sm->indexOfState(StateMachine::State_ptr()), -1);
COMPARE(sm->stateByIndex(1), stateB);
try {
sm->stateByIndex(44);
VERIFY(false && "should have raised an exception");
} catch (sg_exception& e){
// expected!
}
}
void testBindings()
{
SGCommandMgr* cmdMgr = SGCommandMgr::instance();
cmdMgr->addCommand("dummy", dummyCommand);
BUILD_MACHINE_1();
t2->addBinding(new SGBinding("dummy"));
stateA->addEntryBinding(new SGBinding("dummy"));
stateA->addExitBinding(new SGBinding("dummy"));
stateC->addEntryBinding(new SGBinding("dummy"));
////////////////////////
COMPARE(sm->state()->name(), "a");
trigger1->_state = true;
sm->update(1.0);
trigger1->_state = false;
COMPARE(sm->state()->name(), "b");
COMPARE(dummy_cmd_state, 1); // exit state A
trigger2->_state = true;
sm->update(1.0);
trigger2->_state = false;
COMPARE(dummy_cmd_state, 3); // fire transition 2, enter state C
dummy_cmd_state = 0;
sm->changeToState(stateA);
COMPARE(dummy_cmd_state, 1); // enter state A
trigger1->_state = true;
sm->update(1.0);
trigger1->_state = false;
COMPARE(dummy_cmd_state, 2); // exit state A
////////////////////////
t3->addBinding(new SGBinding("dummy"));
t3->addBinding(new SGBinding("dummy"));
t3->addBinding(new SGBinding("dummy"));
sm->changeToStateName("b");
dummy_cmd_state = 0;
trigger3->_state = true;
sm->update(1.0);
trigger3->_state = false;
COMPARE(dummy_cmd_state, 4); // three transition bindings, enter A
}
void testParse()
{
const char* xml = "<?xml version=\"1.0\"?>"
"<PropertyList>"
"<state>"
"<name>one</name>"
"</state>"
"<state>"
"<name>two</name>"
"</state>"
"</PropertyList>";
SGPropertyNode* desc = new SGPropertyNode;
readProperties(xml, strlen(xml), desc);
SGPropertyNode_ptr root(new SGPropertyNode);
StateMachine_ptr sm = StateMachine::createFromPlist(desc, root);
VERIFY(sm->findStateByName("one") != NULL);
VERIFY(sm->findStateByName("two") != NULL);
}
int main(int argc, char* argv[])
{
testBasic();
testBindings();
testParse();
cout << __FILE__ << ": All tests passed" << endl;
return EXIT_SUCCESS;
}