383 lines
16 KiB
Python
383 lines
16 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# License: BSD
|
|
# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
|
|
#
|
|
##############################################################################
|
|
# Documentation
|
|
##############################################################################
|
|
|
|
"""
|
|
The core behaviour template. All behaviours, standalone and composite, inherit
|
|
from this class.
|
|
"""
|
|
|
|
##############################################################################
|
|
# Imports
|
|
##############################################################################
|
|
|
|
import re
|
|
import typing
|
|
import uuid
|
|
|
|
from . import blackboard
|
|
from . import common
|
|
from . import logging
|
|
|
|
##############################################################################
|
|
# Behaviour BluePrint
|
|
##############################################################################
|
|
|
|
|
|
class Behaviour(object):
|
|
"""
|
|
Defines the basic properties and methods required of a node in a
|
|
behaviour tree. When implementing your own behaviour,
|
|
subclass this class.
|
|
|
|
Args:
|
|
name: the behaviour name, defaults to auto-generating from the class name
|
|
|
|
Raises:
|
|
TypeError: if the provided name is not a string
|
|
|
|
Attributes:
|
|
~py_trees.behaviours.Behaviour.id (:class:`uuid.UUID`): automagically generated unique identifier for the behaviour
|
|
~py_trees.behaviours.Behaviour.name (:obj:`str`): the behaviour name
|
|
~py_trees.behaviours.Behaviour.blackboards (typing.List[py_trees.blackboard.Client]): collection of attached blackboard clients
|
|
~py_trees.behaviours.Behaviour.status (:class:`~py_trees.common.Status`): the behaviour status (:data:`~py_trees.common.Status.INVALID`, :data:`~py_trees.common.Status.RUNNING`, :data:`~py_trees.common.Status.FAILURE`, :data:`~py_trees.common.Status.SUCCESS`)
|
|
~py_trees.behaviours.Behaviour.parent (:class:`~py_trees.behaviour.Behaviour`): a :class:`~py_trees.composites.Composite` instance if nested in a tree, otherwise None
|
|
~py_trees.behaviours.Behaviour.children ([:class:`~py_trees.behaviour.Behaviour`]): empty for regular behaviours, populated for composites
|
|
~py_trees.behaviours.Behaviour.logger (:class:`logging.Logger`): a simple logging mechanism
|
|
~py_trees.behaviours.Behaviour.feedback_message(:obj:`str`): improve debugging with a simple message
|
|
~py_trees.behaviours.Behaviour.blackbox_level (:class:`~py_trees.common.BlackBoxLevel`): a helper variable for dot graphs and runtime gui's to collapse/explode entire subtrees dependent upon the blackbox level.
|
|
|
|
.. seealso::
|
|
* :ref:`Skeleton Behaviour Template <skeleton-behaviour-include>`
|
|
* :ref:`The Lifecycle Demo <py-trees-demo-behaviour-lifecycle-program>`
|
|
* :ref:`The Action Behaviour Demo <py-trees-demo-action-behaviour-program>`
|
|
|
|
"""
|
|
def __init__(
|
|
self,
|
|
name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED
|
|
):
|
|
if not name or name == common.Name.AUTO_GENERATED:
|
|
name = self.__class__.__name__
|
|
if not isinstance(name, str):
|
|
raise TypeError("a behaviour name should be a string, but you passed in {}".format(type(name)))
|
|
self.id = uuid.uuid4() # used to uniquely identify this node (helps with removing children from a tree)
|
|
self.name: str = name
|
|
self.blackboards: typing.List[blackboard.Client] = []
|
|
self.qualified_name = "{}/{}".format(self.__class__.__qualname__, self.name) # convenience
|
|
self.status = common.Status.INVALID
|
|
self.iterator = self.tick()
|
|
self.parent: typing.Optional[Behaviour] = None # will get set if a behaviour is added to a composite
|
|
self.children: typing.List[Behaviour] = [] # only set by composite behaviours
|
|
self.logger = logging.Logger(name)
|
|
self.feedback_message = "" # useful for debugging, or human readable updates, but not necessary to implement
|
|
self.blackbox_level = common.BlackBoxLevel.NOT_A_BLACKBOX
|
|
|
|
############################################
|
|
# User Customisable Callbacks
|
|
############################################
|
|
|
|
def setup(self, **kwargs):
|
|
"""
|
|
.. note:: User Customisable Callback
|
|
|
|
Subclasses may override this method for any one-off delayed construction &
|
|
validation that is necessary prior to ticking the tree. Such construction is best
|
|
done here rather than in __init__ so that trees can be instantiated on the fly for
|
|
easy rendering to dot graphs without imposing runtime requirements (e.g. establishing
|
|
a middleware connection to a sensor or a driver to a serial port).
|
|
|
|
Equally as important, executing methods which validate the configuration of
|
|
behaviours will increase confidence that your tree will successfully tick
|
|
without logical software errors before actually ticking. This is useful both
|
|
before a tree's first tick and immediately after any modifications to a tree
|
|
has been made between ticks.
|
|
|
|
.. tip::
|
|
|
|
Faults are notified to the user of the behaviour via exceptions.
|
|
Choice of exception to use is left to the user.
|
|
|
|
.. warning::
|
|
|
|
The kwargs argument is for distributing objects at runtime to behaviours
|
|
before ticking. For example, a simulator instance with which behaviours can
|
|
interact with the simulator's python api, a ros2 node for setting up
|
|
communications. Use sparingly, as this is not proof against keyword conflicts
|
|
amongst disparate libraries of behaviours.
|
|
|
|
Args:
|
|
**kwargs (:obj:`dict`): distribute arguments to this
|
|
behaviour and in turn, all of it's children
|
|
|
|
Raises:
|
|
Exception: if this behaviour has a fault in construction or configuration
|
|
|
|
.. seealso:: :meth:`py_trees.behaviour.Behaviour.shutdown`
|
|
"""
|
|
pass
|
|
|
|
def initialise(self):
|
|
"""
|
|
.. note:: User Customisable Callback
|
|
|
|
Subclasses may override this method to perform any necessary initialising/clearing/resetting
|
|
of variables when when preparing to enter this behaviour if it was not previously
|
|
:data:`~py_trees.common.Status.RUNNING`. i.e. Expect this to trigger more than once!
|
|
"""
|
|
pass
|
|
|
|
def terminate(self, new_status):
|
|
"""
|
|
.. note:: User Customisable Callback
|
|
|
|
Subclasses may override this method to clean up. It will be triggered when a behaviour either
|
|
finishes execution (switching from :data:`~py_trees.common.Status.RUNNING`
|
|
to :data:`~py_trees.common.Status.FAILURE` || :data:`~py_trees.common.Status.SUCCESS`)
|
|
or it got interrupted by a higher priority branch (switching to
|
|
:data:`~py_trees.common.Status.INVALID`). Remember that the :meth:`~py_trees.behaviour.Behaviour.initialise` method
|
|
will handle resetting of variables before re-entry, so this method is about
|
|
disabling resources until this behaviour's next tick. This could be a indeterminably
|
|
long time. e.g.
|
|
|
|
* cancel an external action that got started
|
|
* shut down any tempoarary communication handles
|
|
|
|
Args:
|
|
new_status (:class:`~py_trees.common.Status`): the behaviour is transitioning to this new status
|
|
|
|
.. warning:: Do not set `self.status = new_status` here, that is automatically handled
|
|
by the :meth:`~py_trees.behaviour.Behaviour.stop` method. Use the argument purely for introspection purposes (e.g.
|
|
comparing the current state in `self.status` with the state it will transition to in
|
|
`new_status`.
|
|
"""
|
|
pass
|
|
|
|
def update(self):
|
|
"""
|
|
.. note:: User Customisable Callback
|
|
|
|
Returns:
|
|
:class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
|
|
|
|
Subclasses may override this method to perform any logic required to
|
|
arrive at a decision on the behaviour's new status. It is the primary worker function called on
|
|
by the :meth:`~py_trees.behaviour.Behaviour.tick` mechanism.
|
|
|
|
.. tip:: This method should be almost instantaneous and non-blocking
|
|
|
|
"""
|
|
return common.Status.INVALID
|
|
|
|
def shutdown(self):
|
|
"""
|
|
.. note:: User Customisable Callback
|
|
|
|
Subclasses may override this method for any custom destruction of infrastructure
|
|
usually brought into being in :meth:`~py_trees.behaviour.Behaviour.setup`.
|
|
|
|
Raises:
|
|
Exception: of whatever flavour the child raises when errors occur on destruction
|
|
|
|
.. seealso:: :meth:`py_trees.behaviour.Behaviour.setup`
|
|
"""
|
|
pass
|
|
|
|
############################################
|
|
# Private Methods - use inside a behaviour
|
|
############################################
|
|
|
|
def attach_blackboard_client(
|
|
self,
|
|
name: str=None,
|
|
namespace: str=None
|
|
) -> blackboard.Client:
|
|
"""
|
|
Create and attach a blackboard to this behaviour.
|
|
|
|
Args:
|
|
name: human-readable (not necessarily unique) name for the client
|
|
namespace: sandbox the client to variables behind this namespace
|
|
|
|
Returns:
|
|
a handle to the attached blackboard client
|
|
"""
|
|
if name is None:
|
|
count = len(self.blackboards)
|
|
name = self.name if (count == 0) else self.name + "-{}".format(count)
|
|
new_blackboard = blackboard.Client(
|
|
name=name,
|
|
namespace=namespace
|
|
)
|
|
self.blackboards.append(new_blackboard)
|
|
return new_blackboard
|
|
|
|
############################################
|
|
# Public - lifecycle API
|
|
############################################
|
|
|
|
def setup_with_descendants(self):
|
|
"""
|
|
Iterates over this child, it's children (it's children's children, ...)
|
|
calling the user defined :meth:`~py_trees.behaviour.Behaviour.setup`
|
|
on each in turn.
|
|
"""
|
|
for child in self.children:
|
|
for node in child.iterate():
|
|
node.setup()
|
|
self.setup()
|
|
|
|
def tick_once(self):
|
|
"""
|
|
A direct means of calling tick on this object without
|
|
using the generator mechanism.
|
|
"""
|
|
# no logger necessary here...it directly relays to tick
|
|
for unused in self.tick():
|
|
pass
|
|
|
|
def tick(self):
|
|
"""
|
|
This function is a generator that can be used by an iterator on
|
|
an entire behaviour tree. It handles the logic for deciding when to
|
|
call the user's :meth:`~py_trees.behaviour.Behaviour.initialise` and :meth:`~py_trees.behaviour.Behaviour.terminate` methods as well as making the
|
|
actual call to the user's :meth:`~py_trees.behaviour.Behaviour.update` method that determines the
|
|
behaviour's new status once the tick has finished. Once done, it will
|
|
then yield itself (generator mechanism) so that it can be used as part of
|
|
an iterator for the entire tree.
|
|
|
|
.. code-block:: python
|
|
|
|
for node in my_behaviour.tick():
|
|
print("Do something")
|
|
|
|
.. note::
|
|
|
|
This is a generator function, you must use this with *yield*. If you need a direct call,
|
|
prefer :meth:`~py_trees.behaviour.Behaviour.tick_once` instead.
|
|
|
|
Yields:
|
|
:class:`~py_trees.behaviour.Behaviour`: a reference to itself
|
|
|
|
.. warning:: Override this method only in exceptional circumstances, prefer overriding :meth:`~py_trees.behaviour.Behaviour.update` instead.
|
|
|
|
"""
|
|
self.logger.debug("%s.tick()" % (self.__class__.__name__))
|
|
if self.status != common.Status.RUNNING:
|
|
self.initialise()
|
|
# don't set self.status yet, terminate() may need to check what the current state is first
|
|
new_status = self.update()
|
|
if new_status not in list(common.Status):
|
|
self.logger.error("A behaviour returned an invalid status, setting to INVALID [%s][%s]" % (new_status, self.name))
|
|
new_status = common.Status.INVALID
|
|
if new_status != common.Status.RUNNING:
|
|
self.stop(new_status)
|
|
self.status = new_status
|
|
yield self
|
|
|
|
def iterate(self, direct_descendants=False):
|
|
"""
|
|
Generator that provides iteration over this behaviour and all its children.
|
|
To traverse the entire tree:
|
|
|
|
.. code-block:: python
|
|
|
|
for node in my_behaviour.iterate():
|
|
print("Name: {0}".format(node.name))
|
|
|
|
Args:
|
|
direct_descendants (:obj:`bool`): only yield children one step away from this behaviour.
|
|
|
|
Yields:
|
|
:class:`~py_trees.behaviour.Behaviour`: one of it's children
|
|
"""
|
|
for child in self.children:
|
|
if not direct_descendants:
|
|
for node in child.iterate():
|
|
yield node
|
|
else:
|
|
yield child
|
|
yield self
|
|
|
|
def visit(self, visitor):
|
|
"""
|
|
This is functionality that enables external introspection into the behaviour. It gets used
|
|
by the tree manager classes to collect information as ticking traverses a tree.
|
|
|
|
Args:
|
|
visitor (:obj:`object`): the visiting class, must have a run(:class:`~py_trees.behaviour.Behaviour`) method.
|
|
"""
|
|
visitor.run(self)
|
|
|
|
def stop(self, new_status=common.Status.INVALID):
|
|
"""
|
|
Args:
|
|
new_status (:class:`~py_trees.common.Status`): the behaviour is transitioning to this new status
|
|
|
|
This calls the user defined :meth:`~py_trees.behaviour.Behaviour.terminate` method and also resets the
|
|
generator. It will finally set the new status once the user's :meth:`~py_trees.behaviour.Behaviour.terminate`
|
|
function has been called.
|
|
|
|
.. warning:: Override this method only in exceptional circumstances, prefer overriding :meth:`~py_trees.behaviour.Behaviour.terminate` instead.
|
|
"""
|
|
self.logger.debug("%s.stop(%s)" % (self.__class__.__name__, "%s->%s" % (self.status, new_status) if self.status != new_status else "%s" % new_status))
|
|
self.terminate(new_status)
|
|
self.status = new_status
|
|
self.iterator = self.tick()
|
|
|
|
############################################
|
|
# Public - introspection API
|
|
############################################
|
|
def has_parent_with_name(self, name):
|
|
"""
|
|
Searches through this behaviour's parents, and their parents, looking for
|
|
a behaviour with the same name as that specified.
|
|
|
|
Args:
|
|
name (:obj:`str`): name of the parent to match, can be a regular expression
|
|
|
|
Returns:
|
|
bool: whether a parent was found or not
|
|
"""
|
|
pattern = re.compile(name)
|
|
b = self
|
|
while b.parent is not None:
|
|
if pattern.match(b.parent.name) is not None:
|
|
return True
|
|
b = b.parent
|
|
return False
|
|
|
|
def has_parent_with_instance_type(self, instance_type):
|
|
"""
|
|
Moves up through this behaviour's parents looking for
|
|
a behaviour with the same instance type as that specified.
|
|
|
|
Args:
|
|
instance_type (:obj:`str`): instance type of the parent to match
|
|
|
|
Returns:
|
|
bool: whether a parent was found or not
|
|
"""
|
|
b = self
|
|
while b.parent is not None:
|
|
if isinstance(b.parent, instance_type):
|
|
return True
|
|
b = b.parent
|
|
return False
|
|
|
|
def tip(self):
|
|
"""
|
|
Get the *tip* of this behaviour's subtree (if it has one) after it's last
|
|
tick. This corresponds to the the deepest node that was running before the
|
|
subtree traversal reversed direction and headed back to this node.
|
|
|
|
Returns:
|
|
:class:`~py_trees.behaviour.Behaviour` or :obj:`None`: child behaviour, itself or :obj:`None` if its status is :data:`~py_trees.common.Status.INVALID`
|
|
"""
|
|
return self if self.status != common.Status.INVALID else None
|