Technical introduction
Introduction
The basic class from where all behavior tree nodes and state machines are derived is a TickingState. Each TickingState (see API for detailed definition) is defined by the following methods:
-
entry(self,blackboard:Blackboard): executed when the state is entered
-
doo(self,blackboard:Blackboard): execute while the state is running. The state can take a longer time but should regularly yield by returning TICKING. Is implemented by derived classes.
- exit(self): is execute when the state exits, note that it does not has the blackboard as argument. Will even be called when the other methods return an exception. Is implemented by derived classes
- reset(self): Resets the state (i.e. calls exit() when appropriate and ensures that the next time, entry() will be called)
- accept(self, visitor:Visitor): calls the visitor that you pass as an argument and possibly iterates over its children, e.g. to generate a visual representation of the behavior tree.
There is an additional execute member function that typically should not be touched.
WARNING
A TickingState is low-overhead and synchronous. It is expected to return quickly, and if further processing is needed to return TICK. If there is code in doo and entry that blocks, the execution of the whole BeTFSM tree blocks, including concurrent TickingStates. For blocking code, the TickingState should create its own thread.
When using the state one calls it with the () operator. This calls then the execute() method that calls the entry,doo,exit methods appropriately, according to the figure above.
The visitor pattern is used to be able to generically travers the hierarchy of states. the accept method of a TickingState calls the visitor appropriately. Visitor is defined here
Blackboard
All TickingStates can read and write from a common blackboard of type Blackboard: TypeAlias = Dict[str, Dict|any], i.e. a hierarchically organized common storage where all TickingStates can update information or get information from.
Parameters
When defining new TickingStates, remember to distinguish between construction-time (arguments given to constructor) and run-time (passed using a certain location in the blackboard) parameters. Typically the location (not the value!) of the run-time parameters is passed as parameters to the constructor, such that the TickingState is easily reusable in different applications.
Predefined
There are a series of predefined outcome strings. Other outcome strings can be defined, but it is recommended to use the predefined outcomes, as long as it fits your semantics. This helps interoperability of different nodes in the behavior tree.
- SUCCEED = "succeeded" # everything is fine, continue as normal
- CANCEL = "canceled" # voluntary stop, deliberatly provoked, e.g. reacting to cancel request of an action
- TIMEOUT = "timeout" # some operation times out.
Reserved
These outcome names are reserved and have a specific meaning enforced by the framework.
- ABORT = "aborted" # involuntary stop, e.g. due to exception raised, communication failure,... Allows the state machine to deal with exceptions.
- TICKING = "ticking" # only use this to yield and expecting to be called back the next tick
- CONTINUE = "continue" # only used in the entry() method of TickingState, to signal tht you want to directly continue with Doo(). Don't use it anywhere else
Summary of states and state-machines
This section explains the basics of BeTFSM. You find more detailed information in the sections BeTFSM, BeTFSM ROS2, BeTFSM eTaSL which contain a full API-documentation.
Sometimes states have underlying states, e.g. a state-machine or a sequence. Typically they can be specfied in two ways:
- a list of children in the constructor. This is handy when the substates are also simple to construct. If there is only one underlying state, the constructor argument is typically called
state, if there are multiple underlying states, the constructor argument is typically calledchildren. - by subclassing the state (e.g. Sequence), and calling
add_state()in the constructor of this subclass. This is handy when the definition of the substates is a bit more complex.
In some cases, more information needs to be given and only the add_state approach is applicable (e.g. TickingStateMachine).
The state-machine state
TickingStateMachine implements a basic state machine. You can add nodes using add_state. In this call you also specify the transitions between states. These transitions are specified by mapping an outcome of the state to the name of one of the states in the state machine.
The constructor takes a (instance) name of the state machine ands its allowable outcomes
Behavior-tree like states
The most important behavior-tree like states are Sequence,Fallback, ConcurrentSequence,ConcurrentFallback, and Repeat
The main difference is that for a typical behavior-tree implementation the outcomes can only be RUNNING, SUCCESS or FAILURE. Here, there can be more types of outcome. The mapping to traditional behavior trees is explained below, detailed behavior is documented in the diagrams in the API-documentation.
-
Fallback (or Any(success)): Implements a behaviortree-like Fallback node (concurrently executed):
- other outcome is success
- CANCEL outcome is failure
- TICKING outcome is running
Finishes if any has success. Success is defined by an outcome different from CANCEL. In other words, success can be differentiated by different outcomes.
-
Sequence (or All(success)). Implements a behaviortree-like Sequence node (concurrently executed):
- SUCCEED outcome is success,
- any other outcome is failure
- TICKING outcome is running
Finishes if all have success. Success is defined by an outcome SUCCEED. In other words, failure can be differentiated by different outcomes.
ConcurrentSequence and ConcurrentFallback are basically the same as Sequence and Fallback but execute concurrently: at each tick they go to their complete list of states and follow the logic of sequence or fallback. e.g. in a ConcurrentSequence the states are executed concurrently, but within one tick, they are executed in the order specified.
Concurrent executes also its children concurrently (calling them in sequence for each tick). Concurrent stops executing when any child returns any outcome different from TICKING. See API for description fo detailed behavior.
The Repeat state has one underlying state and repeats this state for a given number of times.
Related to conditions
- waitFor waits until a condition is satisfied. This condition is given by a callback. Note that this callback can be defined using Python's lambda
- WaitForever waits forever, you probably want something to be running in parallel with this.
- While continues to execute the underlying state while checking the given condition. It finishes with CANCEL when the condition returns false, it finishes also when the underlying state finishes and returns the outcome of the underlying state.
Related to output
-
Message is a state to quickly return send message to the log. Its arguments are either a string or either a callback function. Since the string is specified at construction time, the callback function is handy when you want to return something depending on the actual state while running. Python's lambda could be useful to specify the callback.
-
LogBlackboard logs the blackboard or a part of the blackboard. The location to log is given by a list of strings.
Example:
LogBlackboard|(["output","move_home"])
blackboard["output]["move_home"]
Related to timing
- TimedWait waits for the given duration and returns SUCCEED.
- TimedRepeat repeats the underlying state for a given number of time. The time between two repetitions is specified.
- Timeout executes the underlying state at long as its outcome is TICKING. It finishes when the outcome is not ticking and returns this outcome. It also finishes when the given duration is exceeded and returns TIMEOUT.
Related to ROS 2 services, life cycle and topics
To manage file locations in a ROS2 environment a function expand_ref is provided that expands references to ROS2 packages (or more preciselly ament packages) (using $[packagename]) or environmental variables (using ${environmental_variable})
-
ServiceClient creates a TickingState that calls a ROS2 service and generates an outcome when the service returns back. While waiting, it continues to tick. Subclasses need to implement two methods
fill_in_requestto fill in the service request, most probably using information from the blackboard, andprocess_resultto process the result from the service request, most probably putting some information in the blackboard. -
LifeCycle manages the lifecycle of some other ROS2 node. The node is constructed with service_name, transition (see ROS2 Lifecycle ), timeout and the transition is requested during execution of the state.
Related to eTaSL
Defining your own states
To implement a TickingState, you have to implement:
def entry(self, blackboard: Blackboard) -> strcalled when execute() is called for the first time. returns an outcome but has one additional outcome CONTINUE that indicates its preference to directly calldoo()after its return, without a tick.def doo(self, blackboard: Blackboard) -> strcalled for the duration of the task, as long as you return TICKINGdef exit(self) -> str: should callreturn super().exit()at the end, is guaranteed to be called after the last time that execute() was called.def reset(self)->None:should callsuper().exit()at the end: to reset the state and its children.def accept(self, visitor:Visitor)callspreon the visitor, calls accept(visitor) on all its children, and then callsposton the visitor.
Keep in mind that the reset and accept methods still need to be defined when the state has children (otherwise
the default implementations are sufficient)
Warning
Forgetting to implement these methods can lead to silent failures. Even worse, the state will work the first time, but not the second time.
Warning
Outside code that repeately calls a state-machine has to call reset itself before calling the state-machine. This method
calls the reset method of all its children and calls the reset method of its superclass TickingState
Facilitating implementation of a TickingState
To facilitate the definition of tickingStates, you can use Python generators, for this
the Generator class is defined. Generator is a generic TickingState that implements methods entry, doo and exit.
Users can define new TickingStates by defining the method co_execute(self, blackboard:Blackboard). This method is a co-routine and can regularly
yield control using a yield <outcome> statement. This makes it easy to specify a TickingState.
For common usecases, further (generic) specializations are provided by:
- GeneratorWithState for a Generator with one child state
- GeneratorWithList for a Generator with a (ordered) list of childs.
These implementations implement correctly reset and accept for you and manage the child nodes (TickingStates) of
your TickingState. Most of the other nodes are implemented using these auxiliary classes.
Recommendation
Look at a few implementations such as WaitFor,Repeat, Concurrent. This will help making your own nodes.
Often it is sufficient to implement the constructor and the co_execute(self,blackboard) method and yield when
you want to yield control to BeTFSM, and come back at the same location the next tick of BeTFSM.