Tutorials
Before We Start
So, you would like your robot to actually do something non-trivial?
Trivial? Ah, a sequence of timed actions - move forward 3s, rotate 90 degrees, move forward 3s, emit a greeting. This is open-loop and can be pre-programmed in a single script easily. Trivial. Shift gears!
Non-Trivial? Hmm, you’d like to dynamically plan navigational routes (waypoints), choose between actions depending on whether blocking obstacles are sensed, interrupt the current action if the battery is low … and this is just getting started. In short, decision making with priority interrupts and closed loops with peripheral systems (e.g. via sensing, HMI devices, web services). Now you’re talking!
Most roboticists will start scripting, but quickly run into a complexity barrier. They’ll often then reach for state machines which are great for control systems, but run into yet another complexity barrier attempting to handle priority interrupts and an exponentially increasing profusion of wires between states. Which brings you here, to behavour trees! Before we proceed though…
Where is the Robot? Ostensibly you’ll need one, at some point. More often than not though, it’s not available or it’s just not practical for rapid application development. Might be it’s only partially assembled, or new features are being developed in parallel (deadlines!). On the other hand, it may be available, but you cannot get enough time-share on the robot or it is not yet stable, resulting in a stream of unrelated issues lower down in the robotic stack that impede application development. So you make the sensible decision of moving to simulation.
Simulation or Mocked Robots? If you already have a robot simulation, it’s a great place to start. In the long run though, the investment of time to build a mock robot layer should, in most cases, pay itself off with a faster development cycle. Why? Testing an application is mostly about provoking and testing the many permutations and combinations o decision making. It’s not about the 20 minutes of travel from point A to point B in the building. With a mocked robot layer, you can emulate that travel at ludicrous speed and provide easy handles for mocking the problems that can arise.
So this is where the tutorials begin, with a very simple, mocked robot. They will then proceed to build up a behaviour tree application, one step at a time.
The Mock Robot
The tutorials here all run atop a very simple mock robot that encapsulates the following list of mocked components:
Battery
LED Strip
Docking Action Server
Move Base Action Server
Rotation Action Server
Safety Sensors Pipeline
Note
It should always be possible for the mock robot to be replaced by a gazebo simulated robot or the actual robot. Each of these underlying systems must implement exactly the same ROS API interface.
The tutorials take care of launching the mock robot, but it can be also launched on its own with:
$ ros2 launch py_trees_ros_tutorials mock_robot_launch.py
Tutorial 1 - Data Gathering
About
In this, the first of the tutorials, we start out with a behaviour that collects battery data from a subscriber and stores the result on the blackboard for other behaviours to utilise.
Data gathering up front via subscribers is a useful convention for a number of reasons:
Freeze incoming data for remaining behaviours in the tree tick so that decision making is consistent across the entire tree
Avoid redundantly invoking multiple subscribers to the same topic when not necessary
Python access to the blackboard is easier than ROS middleware handling
Typically data gatherers will be assembled underneath a parallel at or near the very root of the tree so they may always trigger their update() method and be processed before any decision making behaviours elsewhere in the tree.
Tree
$ py-trees-render -b py_trees_ros_tutorials.one_data_gathering.tutorial_create_root
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Tutorial One" [label="Tutorial One\n--SuccessOnAll(-)--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Tutorial One" -> Topics2BB;
Battery2BB [label=Battery2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Topics2BB -> Battery2BB;
Tasks [label=Tasks, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
"Tutorial One" -> Tasks;
"Flip Eggs" [label="Flip Eggs", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Tasks -> "Flip Eggs";
Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Tasks -> Idle;
subgraph {
label=children_of_Tasks;
rank=same;
"Flip Eggs" [label="Flip Eggs", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
subgraph {
label="children_of_Tutorial One";
rank=same;
Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
Tasks [label=Tasks, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
}
battery [label="battery: sensor_msgs.msg.B...", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
Battery2BB -> battery [color=blue, constraint=True];
battery_low_warning [label="battery_low_warning: False", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
Battery2BB -> battery_low_warning [color=blue, constraint=True];
}](_images/graphviz-5cd600fcb4d7c59ac70c5a693b047d88e67d016c.png)
1def tutorial_create_root() -> py_trees.behaviour.Behaviour:
2 """
3 Create a basic tree and start a 'Topics2BB' work sequence that
4 will become responsible for data gathering behaviours.
5
6 Returns:
7 the root of the tree
8 """
9 root = py_trees.composites.Parallel(
10 name="Tutorial One",
11 policy=py_trees.common.ParallelPolicy.SuccessOnAll(
12 synchronise=False
13 )
14 )
15
16 topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
17 battery2bb = py_trees_ros.battery.ToBlackboard(
18 name="Battery2BB",
19 topic_name="/battery/state",
20 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
21 threshold=30.0
22 )
23 priorities = py_trees.composites.Selector(name="Tasks", memory=False)
24 idle = py_trees.behaviours.Running(name="Idle")
25 flipper = py_trees.behaviours.Periodic(name="Flip Eggs", n=2)
26
27 root.add_child(topics2bb)
28 topics2bb.add_child(battery2bb)
29 root.add_child(priorities)
30 priorities.add_child(flipper)
31 priorities.add_child(idle)
32
33 return root
Along with the data gathering side, you’ll also notice the dummy branch for
priority jobs (complete with idle behaviour that is always
RUNNING). This configuration is typical
of the data gathering pattern.
Behaviours
The tree makes use of the py_trees_ros.battery.ToBlackboard behaviour.
This behaviour will cause the entire tree to tick over with
SUCCESS so long as there is data incoming.
If there is no data incoming, it will simply
block and prevent the rest of the tree from acting.
Running
# Launch the tutorial
$ ros2 launch py_trees_ros_tutorials tutorial_one_data_gathering_launch.py
# In a different shell, introspect the entire blackboard
$ py-trees-blackboard-watcher
# Or selectively get the battery percentage
$ py-trees-blackboard-watcher --list
$ py-trees-blackboard-watcher /battery.percentage
Tutorial 2 - Battery Check
About
Here we add the first decision. What to do if the battery is low? For this, we’ll get the mocked robot to flash a notification over it’s led strip.
Tree
$ py-trees-render -b py_trees_ros_tutorials.two_battery_check.tutorial_create_root
![digraph pastafarianism {
ordering=out;
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Tutorial Two" [fillcolor=gold, fontcolor=black, fontsize=9, label="Tutorial Two\nSuccessOnAll", shape=parallelogram, style=filled];
Topics2BB [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Topics2BB", shape=box, style=filled];
"Tutorial Two" -> Topics2BB;
Battery2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Battery2BB, shape=ellipse, style=filled];
Topics2BB -> Battery2BB;
Tasks [fillcolor=cyan, fontcolor=black, fontsize=9, label=Tasks, shape=octagon, style=filled];
"Tutorial Two" -> Tasks;
"Battery Low?" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Battery Low?", shape=ellipse, style=filled];
Tasks -> "Battery Low?";
FlashLEDs [fillcolor=gray, fontcolor=black, fontsize=9, label=FlashLEDs, shape=ellipse, style=filled];
"Battery Low?" -> FlashLEDs;
Idle [fillcolor=gray, fontcolor=black, fontsize=9, label=Idle, shape=ellipse, style=filled];
Tasks -> Idle;
"/battery_low_warning" -> "Battery Low?" [color=green, constraint=False, weight=0];
Battery2BB -> "/battery_low_warning" [color=blue, constraint=False, weight=0];
Battery2BB -> "/battery" [color=blue, constraint=False, weight=0];
subgraph Blackboard {
id=Blackboard;
label=Blackboard;
rank=sink;
"/battery_low_warning" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/battery_low_warning: False", shape=box, style=filled, width=0];
"/battery" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/battery: sensor_msgs.msg.B...", shape=box, style=filled, width=0];
}
}](_images/graphviz-967b0f7fc1cb82c6420cab3ea7c942688bffbdf1.png)
1 """
2 Create a basic tree with a battery to blackboard writer and a
3 battery check that flashes the LEDs on the mock robot if the
4 battery level goes low.
5
6 Returns:
7 the root of the tree
8 """
9 root = py_trees.composites.Parallel(
10 name="Tutorial Two",
11 policy=py_trees.common.ParallelPolicy.SuccessOnAll(
12 synchronise=False
13 )
14 )
15
16 topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
17 battery2bb = py_trees_ros.battery.ToBlackboard(
18 name="Battery2BB",
19 topic_name="/battery/state",
20 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
21 threshold=30.0
22 )
23 tasks = py_trees.composites.Selector("Tasks", memory=False)
24 flash_led_strip = behaviours.FlashLedStrip(
25 name="FlashLEDs",
26 colour="red"
27 )
28
29 def check_battery_low_on_blackboard(blackboard: py_trees.blackboard.Blackboard) -> bool:
30 return blackboard.battery_low_warning
31
32 battery_emergency = py_trees.decorators.EternalGuard(
33 name="Battery Low?",
34 condition=check_battery_low_on_blackboard,
35 blackboard_keys={"battery_low_warning"},
36 child=flash_led_strip
37 )
38 idle = py_trees.behaviours.Running(name="Idle")
39
40 root.add_child(topics2bb)
41 topics2bb.add_child(battery2bb)
42 root.add_child(tasks)
43 tasks.add_children([battery_emergency, idle])
44 return root
45
Here we’ve added a high priority branch for dealing with a low battery
that causes the hardware strip to flash. The py_trees.decorators.EternalGuard
enables a continuous check of the battery reading and subsequent termination of
the flashing strip as soon as the battery level has recovered sufficiently.
We could have equivalently made use of the py_trees.idioms.eternal_guard idiom,
which yields a more verbose, but explicit tree and would also allow direct use of
the py_trees.blackboard.CheckBlackboardVariable class as the conditional check.
Behaviours
This tree makes use of the py_trees_ros_tutorials.behaviours.FlashLedStrip behaviour.
1class FlashLedStrip(py_trees.behaviour.Behaviour):
2 """
3 This behaviour simply shoots a command off to the LEDStrip to flash
4 a certain colour and returns :attr:`~py_trees.common.Status.RUNNING`.
5 Note that this behaviour will never return with
6 :attr:`~py_trees.common.Status.SUCCESS` but will send a clearing
7 command to the LEDStrip if it is cancelled or interrupted by a higher
8 priority behaviour.
9
10 Publishers:
11 * **/led_strip/command** (:class:`std_msgs.msg.String`)
12
13 * colourised string command for the led strip ['red', 'green', 'blue']
14
15 Args:
16 name: name of the behaviour
17 topic_name : name of the battery state topic
18 colour: colour to flash ['red', 'green', blue']
19 """
20 def __init__(
21 self,
22 name: str,
23 topic_name: str="/led_strip/command",
24 colour: str="red"
25 ):
26 super(FlashLedStrip, self).__init__(name=name)
27 self.topic_name = topic_name
28 self.colour = colour
29
30 def setup(self, **kwargs):
31 """
32 Setup the publisher which will stream commands to the mock robot.
33
34 Args:
35 **kwargs (:obj:`dict`): look for the 'node' object being passed down from the tree
36
37 Raises:
38 :class:`KeyError`: if a ros2 node isn't passed under the key 'node' in kwargs
39 """
40 self.logger.debug("{}.setup()".format(self.qualified_name))
41 try:
42 self.node = kwargs['node']
43 except KeyError as e:
44 error_message = "didn't find 'node' in setup's kwargs [{}][{}]".format(self.qualified_name)
45 raise KeyError(error_message) from e # 'direct cause' traceability
46
47 self.publisher = self.node.create_publisher(
48 msg_type=std_msgs.String,
49 topic=self.topic_name,
50 qos_profile=py_trees_ros.utilities.qos_profile_latched()
51 )
52 self.feedback_message = "publisher created"
53
54 def update(self) -> py_trees.common.Status:
55 """
56 Annoy the led strip to keep firing every time it ticks over (the led strip will clear itself
57 if no command is forthcoming within a certain period of time).
58 This behaviour will only finish if it is terminated or priority interrupted from above.
59
60 Returns:
61 Always returns :attr:`~py_trees.common.Status.RUNNING`
62 """
63 self.logger.debug("%s.update()" % self.__class__.__name__)
64 self.publisher.publish(std_msgs.String(data=self.colour))
65 self.feedback_message = "flashing {0}".format(self.colour)
66 return py_trees.common.Status.RUNNING
67
68 def terminate(self, new_status: py_trees.common.Status):
69 """
70 Shoot off a clearing command to the led strip.
71
72 Args:
73 new_status: the behaviour is transitioning to this new status
74 """
75 self.logger.debug(
76 "{}.terminate({})".format(
77 self.qualified_name,
78 "{}->{}".format(self.status, new_status) if self.status != new_status else "{}".format(new_status)
79 )
80 )
81 self.publisher.publish(std_msgs.String(data=""))
82 self.feedback_message = "cleared"
This is a typical ROS behaviour that accepts a ROS node on setup. This delayed style is preferred since it allows simple construction of the behaviour, in a tree, sans all of the ROS plumbing - useful when rendering dot graphs of the tree without having a ROS runtime around.
The rest of the behaviour too, is fairly conventional:
ROS plumbing (i.e. the publisher) instantiated in setup()
Flashing notifications published in update()
The reset notification published when the behaviour is terminated
Running
$ ros2 launch py_trees_ros_tutorials tutorial_two_battery_check_launch.py
Then play with the battery slider in the qt dashboard to trigger the decision branching in the tree.
Tutorial 3 - Introspect the Blackboard
About
Tutorial three is a repeat of Tutorial 2 - Battery Check. The purpose of this
tutorial however is to introduce the tools provided to
allow introspection of the blackboard from ROS. Publishers and services
are provided by py_trees_ros.blackboard.Exchange
which is embedded in a py_trees_ros.trees.BehaviourTree. Interaction
with the exchange is over a set of services and dynamically created topics
via the the py-trees-blackboard-watcher command line utility.
Running
$ ros2 launch py_trees_ros_tutorials tutorial_three_introspect_the_blackboard_launch.py
In another shell:
# watch the entire board
$ py-trees-blackboard-watcher
# watch with the recent activity log (activity stream)
$ py-trees-blackboard-watcher --activity
# watch variables associated with behaviours on the most recent tick's visited path
$ py-trees-blackboard-watcher --visited
# list variables available to watch
$ py-trees-blackboard-watcher --list
# watch a simple variable (slide the battery level on the dashboard to trigger a change)
$ py-trees-blackboard-watcher /battery_low_warning
# watch a variable with nested attributes
$ py-trees-blackboard-watcher /battery.percentage
Tutorial 4 - Introspecting the Tree
About
Again, this is a repeat of Tutorial 2 - Battery Check. In addition to services and
topics for the blackboard, the
py_trees_ros.trees.BehaviourTree class provides services and topics
for introspection of the tree state itself as well as a command line utility,
py-trees-tree-watcher, to interact with these services and topics.
Running
Launch the tutorial:
$ ros2 launch py_trees_ros_tutorials tutorial_four_introspect_the_tree_launch.py
Using py-trees-tree-watcher on a private snapshot stream:
# stream the tree state on changes
$ py-trees-tree-watcher
# stream the tree state on changes with statistics
$ py-trees-tree-watcher -s
# stream the tree state on changes with most recent blackboard activity
$ py-trees-tree-watcher -a
# stream the tree state on changes with visited blackboard variables
$ py-trees-tree-watcher -b
# serialise to a dot graph (.dot/.png/.svg) and view in xdot if available
$ py-trees-tree-watcher --dot-graph
# not necessary here, but if there are multiple trees to choose from
$ py-trees-tree-watcher --namespace=/tree/snapshot_streams
Using py-trees-tree-watcher on the default snapshot stream (~/snapshots):
# enable the default snapshot stream
$ ros2 param set /tree default_snapshot_stream True
$ ros2 param set /tree default_snapshot_blackboard_data True
$ ros2 param set /tree default_snapshot_blackboard_activity True
# connect to the stream
$ py-trees-tree-watcher -a -s -b /tree/snapshots
Using py_trees_ros_viewer to configure and visualise the stream:
# install
$ sudo apt install ros-<rosdistro>-py-trees-ros-viewer
# start the viewer
$ py-trees-tree-viewer
Tutorial 5 - Action Clients
About
This tutorial inserts a task between emergency and fallback (idle) behaviours to perform some actual work - rotate 360 degrees in place to scan a room whilst simultaneously notifying the user (via flashing led strip) of it’s actions. The task is triggered from the qt dashboard.
The rotation is performed with a ROS action which are almost the defacto
means of interfacing with the control systems of ROS robots.
Here we introduce the py_trees_ros.actions.ActionClient
behaviour - a simple means of sequentially interacting with an action server such
that a goal always executes to completion or is cancelled before another
goal is sent (a client-side kind of preemption).
It also demonstrates the value of coordinating subsystems from the behaviour tree. In this case, both action controllers and notification subsystems are managed from the tree to perform a task. This frees control subsystems from having to be dependent on each other and simultaneously aware of higher level application logic. Instead, the application logic is centralised in one place, the tree, where it can be easily monitored, logged, and reconstructed in a slightly different form for another application without requiring changes in the underlying control subsystems.
Tree
$ py-trees-render -b py_trees_ros_tutorials.five_action_clients.tutorial_create_root
![digraph pastafarianism {
ordering=out;
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Tutorial Five" [fillcolor=gold, fontcolor=black, fontsize=9, label="Tutorial Five\nSuccessOnAll", shape=parallelogram, style=filled];
Topics2BB [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Topics2BB", shape=box, style=filled];
"Tutorial Five" -> Topics2BB;
Scan2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Scan2BB, shape=ellipse, style=filled];
Topics2BB -> Scan2BB;
Battery2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Battery2BB, shape=ellipse, style=filled];
Topics2BB -> Battery2BB;
Tasks [fillcolor=cyan, fontcolor=black, fontsize=9, label=Tasks, shape=octagon, style=filled];
"Tutorial Five" -> Tasks;
"Battery Low?" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Battery Low?", shape=ellipse, style=filled];
Tasks -> "Battery Low?";
"Flash Red" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Red", shape=ellipse, style=filled];
"Battery Low?" -> "Flash Red";
Scan [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Scan", shape=box, style=filled];
Tasks -> Scan;
"Scan?" [fillcolor=gray, fontcolor=black, fontsize=9, label="Scan?", shape=ellipse, style=filled];
Scan -> "Scan?";
"Preempt?" [fillcolor=cyan, fontcolor=black, fontsize=9, label="Preempt?", shape=octagon, style=filled];
Scan -> "Preempt?";
SuccessIsRunning [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=SuccessIsRunning, shape=ellipse, style=filled];
"Preempt?" -> SuccessIsRunning;
"Scan?*" [fillcolor=gray, fontcolor=black, fontsize=9, label="Scan?*", shape=ellipse, style=filled];
SuccessIsRunning -> "Scan?*";
Scanning [fillcolor=gold, fontcolor=black, fontsize=9, label="Scanning\nSuccessOnOne", shape=parallelogram, style=filled];
"Preempt?" -> Scanning;
Rotate [fillcolor=gray, fontcolor=black, fontsize=9, label=Rotate, shape=ellipse, style=filled];
Scanning -> Rotate;
"Flash Blue" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Blue", shape=ellipse, style=filled];
Scanning -> "Flash Blue";
Celebrate [fillcolor=gold, fontcolor=black, fontsize=9, label="Celebrate\nSuccessOnOne", shape=parallelogram, style=filled];
Scan -> Celebrate;
"Flash Green" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Green", shape=ellipse, style=filled];
Celebrate -> "Flash Green";
Pause [fillcolor=gray, fontcolor=black, fontsize=9, label=Pause, shape=ellipse, style=filled];
Celebrate -> Pause;
Idle [fillcolor=gray, fontcolor=black, fontsize=9, label=Idle, shape=ellipse, style=filled];
Tasks -> Idle;
"/goal_0d200292-24e9-4aef-9f08-a16147275b7e" -> Rotate [color=green, constraint=False, weight=0];
Rotate -> "/goal_0d200292-24e9-4aef-9f08-a16147275b7e" [color=blue, constraint=False, weight=0];
"/battery_low_warning" -> "Battery Low?" [color=green, constraint=False, weight=0];
Battery2BB -> "/battery_low_warning" [color=blue, constraint=False, weight=0];
Battery2BB -> "/battery" [color=blue, constraint=False, weight=0];
"/event_scan_button" -> "Scan?" [color=green, constraint=False, weight=0];
"/event_scan_button" -> "Scan?*" [color=green, constraint=False, weight=0];
Scan2BB -> "/event_scan_button" [color=blue, constraint=False, weight=0];
subgraph Blackboard {
id=Blackboard;
label=Blackboard;
rank=sink;
"/goal_0d200292-24e9-4aef-9f08-a16147275b7e" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/goal_0d200292-24e9-4aef-9f08-a16147275b7e: py_trees_ros_inte...", shape=box, style=filled, width=0];
"/battery_low_warning" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/battery_low_warning: False", shape=box, style=filled, width=0];
"/battery" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/battery: sensor_msgs.msg.B...", shape=box, style=filled, width=0];
"/event_scan_button" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/event_scan_button: -", shape=box, style=filled, width=0];
}
}](_images/graphviz-eacf99859950b2e1c5f175fabe8116ce6b029898.png)
1def tutorial_create_root() -> py_trees.behaviour.Behaviour:
2 """
3 Insert a task between battery emergency and idle behaviours that
4 controls a rotation action controller and notifications simultaenously
5 to scan a room.
6
7 Returns:
8 the root of the tree
9 """
10 root = py_trees.composites.Parallel(
11 name="Tutorial Five",
12 policy=py_trees.common.ParallelPolicy.SuccessOnAll(
13 synchronise=False
14 )
15 )
16
17 topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
18 scan2bb = py_trees_ros.subscribers.EventToBlackboard(
19 name="Scan2BB",
20 topic_name="/dashboard/scan",
21 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
22 variable_name="event_scan_button"
23 )
24 battery2bb = py_trees_ros.battery.ToBlackboard(
25 name="Battery2BB",
26 topic_name="/battery/state",
27 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
28 threshold=30.0
29 )
30 tasks = py_trees.composites.Selector(name="Tasks", memory=False)
31 flash_red = behaviours.FlashLedStrip(
32 name="Flash Red",
33 colour="red"
34 )
35
36 # Emergency Tasks
37 def check_battery_low_on_blackboard(blackboard: py_trees.blackboard.Blackboard) -> bool:
38 return blackboard.battery_low_warning
39
40 battery_emergency = py_trees.decorators.EternalGuard(
41 name="Battery Low?",
42 condition=check_battery_low_on_blackboard,
43 blackboard_keys={"battery_low_warning"},
44 child=flash_red
45 )
46 # Worker Tasks
47 scan = py_trees.composites.Sequence(name="Scan", memory=True)
48 is_scan_requested = py_trees.behaviours.CheckBlackboardVariableValue(
49 name="Scan?",
50 check=py_trees.common.ComparisonExpression(
51 variable="event_scan_button",
52 value=True,
53 operator=operator.eq
54 )
55 )
56 scan_preempt = py_trees.composites.Selector(name="Preempt?", memory=False)
57 is_scan_requested_two = py_trees.decorators.SuccessIsRunning(
58 name="SuccessIsRunning",
59 child=py_trees.behaviours.CheckBlackboardVariableValue(
60 name="Scan?",
61 check=py_trees.common.ComparisonExpression(
62 variable="event_scan_button",
63 value=True,
64 operator=operator.eq
65 )
66 )
67 )
68 scanning = py_trees.composites.Parallel(
69 name="Scanning",
70 policy=py_trees.common.ParallelPolicy.SuccessOnOne()
71 )
72 scan_rotate = py_trees_ros.actions.ActionClient(
73 name="Rotate",
74 action_type=py_trees_actions.Rotate,
75 action_name="rotate",
76 action_goal=py_trees_actions.Rotate.Goal(), # noqa
77 generate_feedback_message=lambda msg: "{:.2f}%%".format(msg.feedback.percentage_completed)
78 )
79 flash_blue = behaviours.FlashLedStrip(
80 name="Flash Blue",
81 colour="blue"
82 )
83 scan_celebrate = py_trees.composites.Parallel(
84 name="Celebrate",
85 policy=py_trees.common.ParallelPolicy.SuccessOnOne()
86 )
87 flash_green = behaviours.FlashLedStrip(name="Flash Green", colour="green")
88 scan_pause = py_trees.timers.Timer("Pause", duration=3.0)
89 # Fallback task
90 idle = py_trees.behaviours.Running(name="Idle")
91
92 root.add_child(topics2bb)
93 topics2bb.add_children([scan2bb, battery2bb])
94 root.add_child(tasks)
95 tasks.add_children([battery_emergency, scan, idle])
96 scan.add_children([is_scan_requested, scan_preempt, scan_celebrate])
97 scan_preempt.add_children([is_scan_requested_two, scanning])
98 scanning.add_children([scan_rotate, flash_blue])
99 scan_celebrate.add_children([flash_green, scan_pause])
100 return root
Data Gathering
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
Scan2BB [label=Scan2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Topics2BB -> Scan2BB;
Battery2BB [label=Battery2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Topics2BB -> Battery2BB;
}](_images/graphviz-79ce13b44ffbab4b225d96c8e6d6b2388983663d.png)
The Scan2BB behaviour collects incoming requests from the qt dashboard and drops them
onto the blackboard. This is your usual py_trees_ros.subscribers.EventToBlackboard
behaviour which will only register the result True on the blackboard if
there was an incoming message between the last and the current tick.
The Scanning Branch
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
Scan [label=Scan, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan?" [label="Scan?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scan -> "Scan?";
"Preempt?" [label="Preempt?", shape=octagon, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
Scan -> "Preempt?";
}](_images/graphviz-28e469379fb06ecfceb672821c736767dbc1e659.png)
The entire scanning branch is protected by a guard (the blackbox represents the lower part of the tree) which checks the blackboard to determine whether Scan2BB had recorded an incoming rqeuest. Once the scan event is received, this branch proceeds to work until it either finishes, or is pre-empted by the higher priority low battery branch.
Action Clients
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Preempt?" [label="Preempt?", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Preempt?" -> Scanning;
Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> Rotate;
"Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Flash Blue";
}](_images/graphviz-aff0d8925d44e46a2eeb94426f7f0b7ad00b9911.png)
This tree makes use of the py_trees_ros.actions.ActionClient
for the ‘Rotate’ behaviour.
Goal details are configured at construction and cannot be changed thereafter
New goals are sent on initialise()
Monitoring of feedback and result response occurs in update()
If the behaviour is interrupted, the goal will be cancelled in terminate()
This obviously places constraints on it’s usage. In particular, goal details cannot be assembled dynamically/elsewhere, nor can it send a new goal while a preceding goal is still active - the behaviour lifecycle forces it through terminate() before a new goal can be sent.
These constraints however, are fine in most situations and result in a very simple behaviour that almost always does what you need without perspiring inordinately on tree design ramifications.
Instantiating the action client, configured for rotations:
1 name="Scanning",
2 policy=py_trees.common.ParallelPolicy.SuccessOnOne()
3 )
4 scan_rotate = py_trees_ros.actions.ActionClient(
5 name="Rotate",
6 action_type=py_trees_actions.Rotate,
7 action_name="rotate",
The notification behaviour (FlashLedStrip) runs in parallel with the action. Composing in this manner from the behaviour tree centralises design concepts (in this case, notifications) and decouples the need for the control subsystems to be aware each other and the application logic.
A Kind of Preemption
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Preempt?" [label="Preempt?", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
SuccessIsRunning [label=SuccessIsRunning, shape=ellipse, style=filled, fillcolor=ghostwhite, fontsize=9, fontcolor=black];
"Preempt?" -> SuccessIsRunning;
"Scan?" [label="Scan?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
SuccessIsRunning -> "Scan?";
Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Preempt?" -> Scanning;
Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> Rotate;
"Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Flash Blue";
}](_images/graphviz-d0c39cacd0cb5fefdb6176cd33406deaf5f770f0.png)
The higher priority branch in the scanning action enables a kind of pre-emption on the scanning action from the client side. If a new request comes in, it will trigger the secondary scan event check, invalidating whatever scanning action was currently running. This will clear the led command and cancel the rotate action. On the next tick, the scan event check will fail (it was consumed on the last tick) and the scanning will restart.
The decorator is used to signal farther up in the tree that the action is still running, even when being preempted.
Note
This is not true pre-emption since it cancels the rotate action and restarts it. It is however, exactly the pattern that is required in many instances. If you are looking for more complex logic, e.g. enabling interactions with a manipulation action server with which you would like to leave pre-emptions up to the server, then this will require either decomposing the separate parts of the action client behaviour (i.e. separate send goal, monitoring and cancelling) into separate behaviours or construct a more complex behaviour that manages the entire process itself. PR’s welcome!
Handling Failure
If the rotate action should fail, then the whole branch will also fail,
subsequently dropping the robot back to its idle state. A failure
event could be generated by monitoring either the status of the ‘Scanning’
parallel or the py_trees.trees.BehaviourTree.tip() of the
tree and reacting to it’s state change.
Running
$ ros2 launch py_trees_ros_tutorials tutorial_five_action_clients_launch.py
Send scan requests from the qt dashboard.
Tutorial 6 - Context Switching
About
This tutorial inserts a context switching behaviour to run in tandem with the
scan rotation. A context switching behaviour will alter the runtime system
in some way when it is entered (i.e. in initialise())
and reset the runtime system to it’s original context
on terminate()). Refer to context switch
for more detail.
In this example it will enable a hypothetical safety sensor pipeline, necessary necessary for dangerous but slow moving rotational maneuvres not required for normal modes of travel (suppose we have a large rectangular robot that is ordinarily blind to the sides - it may need to take advantage of noisy sonars to the sides or rotate forward facing sensing into position before engaging).
Tree
$ py-trees-render -b py_trees_ros_tutorials.six_context_switching.tutorial_create_root
![digraph pastafarianism {
ordering=out;
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Tutorial Six" [fillcolor=gold, fontcolor=black, fontsize=9, label="Tutorial Six\nSuccessOnAll", shape=parallelogram, style=filled];
Topics2BB [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Topics2BB", shape=box, style=filled];
"Tutorial Six" -> Topics2BB;
Scan2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Scan2BB, shape=ellipse, style=filled];
Topics2BB -> Scan2BB;
Battery2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Battery2BB, shape=ellipse, style=filled];
Topics2BB -> Battery2BB;
Tasks [fillcolor=cyan, fontcolor=black, fontsize=9, label=Tasks, shape=octagon, style=filled];
"Tutorial Six" -> Tasks;
"Battery Low?" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Battery Low?", shape=ellipse, style=filled];
Tasks -> "Battery Low?";
"Flash Red" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Red", shape=ellipse, style=filled];
"Battery Low?" -> "Flash Red";
Scan [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Scan", shape=box, style=filled];
Tasks -> Scan;
"Scan?" [fillcolor=gray, fontcolor=black, fontsize=9, label="Scan?", shape=ellipse, style=filled];
Scan -> "Scan?";
"Preempt?" [fillcolor=cyan, fontcolor=black, fontsize=9, label="Preempt?", shape=octagon, style=filled];
Scan -> "Preempt?";
SuccessIsRunning [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=SuccessIsRunning, shape=ellipse, style=filled];
"Preempt?" -> SuccessIsRunning;
"Scan?*" [fillcolor=gray, fontcolor=black, fontsize=9, label="Scan?*", shape=ellipse, style=filled];
SuccessIsRunning -> "Scan?*";
Scanning [fillcolor=gold, fontcolor=black, fontsize=9, label="Scanning\nSuccessOnOne", shape=parallelogram, style=filled];
"Preempt?" -> Scanning;
"Context Switch" [fillcolor=gray, fontcolor=black, fontsize=9, label="Context Switch", shape=ellipse, style=filled];
Scanning -> "Context Switch";
Rotate [fillcolor=gray, fontcolor=black, fontsize=9, label=Rotate, shape=ellipse, style=filled];
Scanning -> Rotate;
"Flash Blue" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Blue", shape=ellipse, style=filled];
Scanning -> "Flash Blue";
Celebrate [fillcolor=gold, fontcolor=black, fontsize=9, label="Celebrate\nSuccessOnOne", shape=parallelogram, style=filled];
Scan -> Celebrate;
"Flash Green" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Green", shape=ellipse, style=filled];
Celebrate -> "Flash Green";
Pause [fillcolor=gray, fontcolor=black, fontsize=9, label=Pause, shape=ellipse, style=filled];
Celebrate -> Pause;
Idle [fillcolor=gray, fontcolor=black, fontsize=9, label=Idle, shape=ellipse, style=filled];
Tasks -> Idle;
"/goal_69f16b95-c094-4145-85e6-4c0c7477bb06" -> Rotate [color=green, constraint=False, weight=0];
Rotate -> "/goal_69f16b95-c094-4145-85e6-4c0c7477bb06" [color=blue, constraint=False, weight=0];
"/event_scan_button" -> "Scan?*" [color=green, constraint=False, weight=0];
"/event_scan_button" -> "Scan?" [color=green, constraint=False, weight=0];
Scan2BB -> "/event_scan_button" [color=blue, constraint=False, weight=0];
Battery2BB -> "/battery" [color=blue, constraint=False, weight=0];
"/battery_low_warning" -> "Battery Low?" [color=green, constraint=False, weight=0];
Battery2BB -> "/battery_low_warning" [color=blue, constraint=False, weight=0];
subgraph Blackboard {
id=Blackboard;
label=Blackboard;
rank=sink;
"/goal_69f16b95-c094-4145-85e6-4c0c7477bb06" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/goal_69f16b95-c094-4145-85e6-4c0c7477bb06: py_trees_ros_inte...", shape=box, style=filled, width=0];
"/event_scan_button" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/event_scan_button: -", shape=box, style=filled, width=0];
"/battery" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/battery: sensor_msgs.msg.B...", shape=box, style=filled, width=0];
"/battery_low_warning" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/battery_low_warning: False", shape=box, style=filled, width=0];
}
}](_images/graphviz-f8b12c77d283cfe315a6193e6876e9a8377adde2.png)
1def tutorial_create_root() -> py_trees.behaviour.Behaviour:
2 """
3 Insert a task between battery emergency and idle behaviours that
4 controls a rotation action controller and notifications simultaenously
5 to scan a room.
6
7 Returns:
8 the root of the tree
9 """
10 root = py_trees.composites.Parallel(
11 name="Tutorial Six",
12 policy=py_trees.common.ParallelPolicy.SuccessOnAll(
13 synchronise=False
14 )
15 )
16
17 topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
18 scan2bb = py_trees_ros.subscribers.EventToBlackboard(
19 name="Scan2BB",
20 topic_name="/dashboard/scan",
21 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
22 variable_name="event_scan_button"
23 )
24 battery2bb = py_trees_ros.battery.ToBlackboard(
25 name="Battery2BB",
26 topic_name="/battery/state",
27 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
28 threshold=30.0
29 )
30 tasks = py_trees.composites.Selector("Tasks", memory=False)
31 flash_red = behaviours.FlashLedStrip(
32 name="Flash Red",
33 colour="red"
34 )
35
36 # Emergency Tasks
37 def check_battery_low_on_blackboard(blackboard: py_trees.blackboard.Blackboard) -> bool:
38 return blackboard.battery_low_warning
39
40 battery_emergency = py_trees.decorators.EternalGuard(
41 name="Battery Low?",
42 condition=check_battery_low_on_blackboard,
43 blackboard_keys={"battery_low_warning"},
44 child=flash_red
45 )
46 # Worker Tasks
47 scan = py_trees.composites.Sequence(name="Scan", memory=True)
48 is_scan_requested = py_trees.behaviours.CheckBlackboardVariableValue(
49 name="Scan?",
50 check=py_trees.common.ComparisonExpression(
51 variable="event_scan_button",
52 value=True,
53 operator=operator.eq
54 )
55 )
56 scan_preempt = py_trees.composites.Selector(name="Preempt?", memory=False)
57 is_scan_requested_two = py_trees.decorators.SuccessIsRunning(
58 name="SuccessIsRunning",
59 child=py_trees.behaviours.CheckBlackboardVariableValue(
60 name="Scan?",
61 check=py_trees.common.ComparisonExpression(
62 variable="event_scan_button",
63 value=True,
64 operator=operator.eq
65 )
66 )
67 )
68 scanning = py_trees.composites.Parallel(
69 name="Scanning",
70 policy=py_trees.common.ParallelPolicy.SuccessOnOne()
71 )
72 scan_context_switch = behaviours.ScanContext("Context Switch")
73 scan_rotate = py_trees_ros.actions.ActionClient(
74 name="Rotate",
75 action_type=py_trees_actions.Rotate,
76 action_name="rotate",
77 action_goal=py_trees_actions.Rotate.Goal(),
78 generate_feedback_message=lambda msg: "{:.2f}%%".format(msg.feedback.percentage_completed)
79 )
80 flash_blue = behaviours.FlashLedStrip(
81 name="Flash Blue",
82 colour="blue"
83 )
84 scan_celebrate = py_trees.composites.Parallel(
85 name="Celebrate",
86 policy=py_trees.common.ParallelPolicy.SuccessOnOne()
87 )
88 flash_green = behaviours.FlashLedStrip(name="Flash Green", colour="green")
89 scan_pause = py_trees.timers.Timer("Pause", duration=3.0)
90 # Fallback task
91 idle = py_trees.behaviours.Running(name="Idle")
92
93 root.add_child(topics2bb)
94 topics2bb.add_children([scan2bb, battery2bb])
95 root.add_child(tasks)
96 tasks.add_children([battery_emergency, scan, idle])
97 scan.add_children([is_scan_requested, scan_preempt, scan_celebrate])
98 scan_preempt.add_children([is_scan_requested_two, scanning])
99 scanning.add_children([scan_context_switch, scan_rotate, flash_blue])
100 scan_celebrate.add_children([flash_green, scan_pause])
101 return root
Behaviour
The py_trees_ros_tutorials.behaviours.ScanContext is the
context switching behaviour constructed for this tutorial.
initialise(): trigger a sequence service calls to cache and set the /safety_sensors/enabled parameter to Trueupdate(): complete the chain of service calls & maintain the contextterminate(): reset the parameter to the cached value
Context Switching
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Context Switch";
Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> Rotate;
"Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Flash Blue";
}](_images/graphviz-d65fad747e75648c8e9374f3722903a39319c4b3.png)
On entry into the parallel, the ScanContext
behaviour will cache and switch
the safety sensors parameter. While in the parallel it will return with
RUNNING indefinitely. When the rotation
action succeeds or fails, it will terminate the parallel and subsequently
the ScanContext will terminate,
resetting the safety sensors parameter to it’s original value.
Running
# Launch the tutorial
$ ros2 launch py_trees_ros_tutorials tutorial_six_context_switching_launch.py
# In another shell, watch the parameter as a context switch occurs
$ watch -n 1 ros2 param get /safety_sensors enabled
# Trigger scan requests from the qt dashboard
Tutorial 7 - Docking, Cancelling, Failing
About
This tutorial adds additional complexity to the scanning application in order to introduce a few patterns typical of most applications - cancellations, recovery and result handling.
Specifically, there is now an undocking-move combination pre-scanning and a move-docking combination post-scanning. When cancelling, the robot should recover it’s initial state so it is ready to accept future requests. In this case, the robot must move home and dock, even when cancelled.
Additionally, the application should report out on it’s result upon completion.
Note
Preemption has been dropped from the application for simplicity. It could be reinserted, but care would be required to handle undocking and docking appropriately.
Tree
$ py-trees-render -b py_trees_ros_tutorials.seven_docking_cancelling_failing.tutorial_create_root
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Tutorial Seven" [label="Tutorial Seven\n--SuccessOnAll(-)--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Tutorial Seven" -> Topics2BB;
Scan2BB [label=Scan2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Topics2BB -> Scan2BB;
Cancel2BB [label=Cancel2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Topics2BB -> Cancel2BB;
Battery2BB [label=Battery2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Topics2BB -> Battery2BB;
subgraph {
label=children_of_Topics2BB;
rank=same;
Scan2BB [label=Scan2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Cancel2BB [label=Cancel2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Battery2BB [label=Battery2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
Tasks [label=Tasks, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
"Tutorial Seven" -> Tasks;
"Battery Low?" [label="Battery Low?", shape=ellipse, style=filled, fillcolor=ghostwhite, fontsize=9, fontcolor=black];
Tasks -> "Battery Low?";
"Flash Red" [label="Flash Red", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Battery Low?" -> "Flash Red";
Scan [label=Scan, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
Tasks -> Scan;
"Scan?" [label="Scan?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scan -> "Scan?";
"Scan or Die" [label="Scan or Die", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
Scan -> "Scan or Die";
"Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Die" -> "Ere we Go";
UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Ere we Go" -> UnDock;
"Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
"Ere we Go" -> "Scan or Be Cancelled";
"Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Be Cancelled" -> "Cancelling?";
"Cancel?" [label="Cancel?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Cancelling?" -> "Cancel?";
"Move Home" [label="Move Home", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Cancelling?" -> "Move Home";
"Result2BB\n'cancelled'" [label="Result2BB\n'cancelled'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Cancelling?" -> "Result2BB\n'cancelled'";
subgraph {
label="children_of_Cancelling?";
rank=same;
"Cancel?" [label="Cancel?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Home" [label="Move Home", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Result2BB\n'cancelled'" [label="Result2BB\n'cancelled'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
"Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Be Cancelled" -> "Move Out and Scan";
"Move Out" [label="Move Out", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Move Out";
Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Move Out and Scan" -> Scanning;
"Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Context Switch";
Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> Rotate;
"Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Flash Blue";
subgraph {
label=children_of_Scanning;
rank=same;
"Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
"Move Home*" [label="Move Home*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Move Home*";
"Result2BB\n'succeeded'" [label="Result2BB\n'succeeded'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Result2BB\n'succeeded'";
subgraph {
label="children_of_Move Out and Scan";
rank=same;
"Move Out" [label="Move Out", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Move Home*" [label="Move Home*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Result2BB\n'succeeded'" [label="Result2BB\n'succeeded'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
subgraph {
label="children_of_Scan or Be Cancelled";
rank=same;
"Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
}
Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Ere we Go" -> Dock;
Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Ere we Go" -> Celebrate;
"Flash Green" [label="Flash Green", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Celebrate -> "Flash Green";
Pause [label=Pause, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Celebrate -> Pause;
subgraph {
label=children_of_Celebrate;
rank=same;
"Flash Green" [label="Flash Green", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Pause [label=Pause, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
subgraph {
label="children_of_Ere we Go";
rank=same;
UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
}
Die [label=Die, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Die" -> Die;
Notification [label="Notification\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
Die -> Notification;
"Flash Red*" [label="Flash Red*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Notification -> "Flash Red*";
"Pause*" [label="Pause*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Notification -> "Pause*";
subgraph {
label=children_of_Notification;
rank=same;
"Flash Red*" [label="Flash Red*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Pause*" [label="Pause*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
"Result2BB\n'failed'" [label="Result2BB\n'failed'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Die -> "Result2BB\n'failed'";
subgraph {
label=children_of_Die;
rank=same;
Notification [label="Notification\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Result2BB\n'failed'" [label="Result2BB\n'failed'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
subgraph {
label="children_of_Scan or Die";
rank=same;
"Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
Die [label=Die, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
}
"Send Result" [label="Send Result", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scan -> "Send Result";
subgraph {
label=children_of_Scan;
rank=same;
"Scan?" [label="Scan?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Scan or Die" [label="Scan or Die", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
"Send Result" [label="Send Result", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Tasks -> Idle;
subgraph {
label=children_of_Tasks;
rank=same;
"Battery Low?" [label="Battery Low?", shape=ellipse, style=filled, fillcolor=ghostwhite, fontsize=9, fontcolor=black];
Scan [label=Scan, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
subgraph {
label="children_of_Tutorial Seven";
rank=same;
Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
Tasks [label=Tasks, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
}
event_scan_button [label="event_scan_button: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
event_scan_button -> "Scan?" [color=blue, constraint=False];
Scan2BB -> event_scan_button [color=blue, constraint=True];
event_cancel_button [label="event_cancel_button: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
event_cancel_button -> "Cancel?" [color=blue, constraint=False];
Cancel2BB -> event_cancel_button [color=blue, constraint=True];
battery [label="battery: sensor_msgs.msg.B...", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
Battery2BB -> battery [color=blue, constraint=True];
battery_low_warning [label="battery_low_warning: False", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
battery_low_warning -> "Battery Low?" [color=blue, constraint=False];
Battery2BB -> battery_low_warning [color=blue, constraint=True];
scan_result [label="scan_result: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
scan_result -> "Send Result" [color=blue, constraint=False];
"Result2BB\n'failed'" -> scan_result [color=blue, constraint=True];
"Result2BB\n'succeeded'" -> scan_result [color=blue, constraint=True];
"Result2BB\n'cancelled'" -> scan_result [color=blue, constraint=True];
}](_images/graphviz-689313d81ea9051143de869116f67e1b3715ab54.png)
1def tutorial_create_root() -> py_trees.behaviour.Behaviour:
2 """
3 Insert a task between battery emergency and idle behaviours that
4 controls a rotation action controller and notifications simultaenously
5 to scan a room.
6
7 Returns:
8 the root of the tree
9 """
10 root = py_trees.composites.Parallel(
11 name="Tutorial Seven",
12 policy=py_trees.common.ParallelPolicy.SuccessOnAll(
13 synchronise=False
14 )
15 )
16
17 topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
18 scan2bb = py_trees_ros.subscribers.EventToBlackboard(
19 name="Scan2BB",
20 topic_name="/dashboard/scan",
21 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
22 variable_name="event_scan_button"
23 )
24 cancel2bb = py_trees_ros.subscribers.EventToBlackboard(
25 name="Cancel2BB",
26 topic_name="/dashboard/cancel",
27 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
28 variable_name="event_cancel_button"
29 )
30 battery2bb = py_trees_ros.battery.ToBlackboard(
31 name="Battery2BB",
32 topic_name="/battery/state",
33 qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
34 threshold=30.0
35 )
36 tasks = py_trees.composites.Selector(name="Tasks", memory=False)
37 flash_red = behaviours.FlashLedStrip(
38 name="Flash Red",
39 colour="red"
40 )
41
42 # Emergency Tasks
43 def check_battery_low_on_blackboard(blackboard: py_trees.blackboard.Blackboard) -> bool:
44 return blackboard.battery_low_warning
45
46 battery_emergency = py_trees.decorators.EternalGuard(
47 name="Battery Low?",
48 condition=check_battery_low_on_blackboard,
49 blackboard_keys={"battery_low_warning"},
50 child=flash_red
51 )
52 # Worker Tasks
53 scan = py_trees.composites.Sequence(name="Scan", memory=True)
54 is_scan_requested = py_trees.behaviours.CheckBlackboardVariableValue(
55 name="Scan?",
56 check=py_trees.common.ComparisonExpression(
57 variable="event_scan_button",
58 value=True,
59 operator=operator.eq
60 )
61 )
62 scan_or_die = py_trees.composites.Selector(name="Scan or Die", memory=False)
63 die = py_trees.composites.Sequence(name="Die", memory=True)
64 failed_notification = py_trees.composites.Parallel(
65 name="Notification",
66 policy=py_trees.common.ParallelPolicy.SuccessOnOne()
67 )
68 failed_flash_green = behaviours.FlashLedStrip(name="Flash Red", colour="red")
69 failed_pause = py_trees.timers.Timer("Pause", duration=3.0)
70 result_failed_to_bb = py_trees.behaviours.SetBlackboardVariable(
71 name="Result2BB\n'failed'",
72 variable_name='scan_result',
73 variable_value='failed',
74 overwrite=True
75 )
76 ere_we_go = py_trees.composites.Sequence(name="Ere we Go", memory=True)
77 undock = py_trees_ros.actions.ActionClient(
78 name="UnDock",
79 action_type=py_trees_actions.Dock,
80 action_name="dock",
81 action_goal=py_trees_actions.Dock.Goal(dock=False),
82 generate_feedback_message=lambda msg: "undocking"
83 )
84 scan_or_be_cancelled = py_trees.composites.Selector(name="Scan or Be Cancelled", memory=False)
85 cancelling = py_trees.composites.Sequence(name="Cancelling?", memory=True)
86 is_cancel_requested = py_trees.behaviours.CheckBlackboardVariableValue(
87 name="Cancel?",
88 check=py_trees.common.ComparisonExpression(
89 variable="event_cancel_button",
90 value=True,
91 operator=operator.eq
92 )
93 )
94 move_home_after_cancel = py_trees_ros.actions.ActionClient(
95 name="Move Home",
96 action_type=py_trees_actions.MoveBase,
97 action_name="move_base",
98 action_goal=py_trees_actions.MoveBase.Goal(),
99 generate_feedback_message=lambda msg: "moving home"
100 )
101 result_cancelled_to_bb = py_trees.behaviours.SetBlackboardVariable(
102 name="Result2BB\n'cancelled'",
103 variable_name='scan_result',
104 variable_value='cancelled',
105 overwrite=True
106 )
107 move_out_and_scan = py_trees.composites.Sequence(name="Move Out and Scan", memory=True)
108 move_base = py_trees_ros.actions.ActionClient(
109 name="Move Out",
110 action_type=py_trees_actions.MoveBase,
111 action_name="move_base",
112 action_goal=py_trees_actions.MoveBase.Goal(),
113 generate_feedback_message=lambda msg: "moving out"
114 )
115 scanning = py_trees.composites.Parallel(
116 name="Scanning",
117 policy=py_trees.common.ParallelPolicy.SuccessOnOne()
118 )
119 scan_context_switch = behaviours.ScanContext("Context Switch")
120 scan_rotate = py_trees_ros.actions.ActionClient(
121 name="Rotate",
122 action_type=py_trees_actions.Rotate,
123 action_name="rotate",
124 action_goal=py_trees_actions.Rotate.Goal(),
125 generate_feedback_message=lambda msg: "{:.2f}%%".format(msg.feedback.percentage_completed)
126 )
127 scan_flash_blue = behaviours.FlashLedStrip(name="Flash Blue", colour="blue")
128 move_home_after_scan = py_trees_ros.actions.ActionClient(
129 name="Move Home",
130 action_type=py_trees_actions.MoveBase,
131 action_name="move_base",
132 action_goal=py_trees_actions.MoveBase.Goal(),
133 generate_feedback_message=lambda msg: "moving home"
134 )
135 result_succeeded_to_bb = py_trees.behaviours.SetBlackboardVariable(
136 name="Result2BB\n'succeeded'",
137 variable_name='scan_result',
138 variable_value='succeeded',
139 overwrite=True
140 )
141 celebrate = py_trees.composites.Parallel(
142 name="Celebrate",
143 policy=py_trees.common.ParallelPolicy.SuccessOnOne()
144 )
145 celebrate_flash_green = behaviours.FlashLedStrip(name="Flash Green", colour="green")
146 celebrate_pause = py_trees.timers.Timer("Pause", duration=3.0)
147 dock = py_trees_ros.actions.ActionClient(
148 name="Dock",
149 action_type=py_trees_actions.Dock,
150 action_name="dock",
151 action_goal=py_trees_actions.Dock.Goal(dock=True), # noqa
152 generate_feedback_message=lambda msg: "docking"
153 )
154
155 class SendResult(py_trees.behaviour.Behaviour):
156
157 def __init__(self, name: str):
158 super().__init__(name="Send Result")
159 self.blackboard = self.attach_blackboard_client(name=self.name)
160 self.blackboard.register_key(
161 key="scan_result",
162 access=py_trees.common.Access.READ
163 )
164
165 def update(self):
166 print(console.green +
167 "********** Result: {} **********".format(self.blackboard.scan_result) +
168 console.reset
169 )
170 return py_trees.common.Status.SUCCESS
171
172 send_result = SendResult(name="Send Result")
173
174 # Fallback task
175 idle = py_trees.behaviours.Running(name="Idle")
176
177 root.add_child(topics2bb)
178 topics2bb.add_children([scan2bb, cancel2bb, battery2bb])
179 root.add_child(tasks)
180 tasks.add_children([battery_emergency, scan, idle])
181 scan.add_children([is_scan_requested, scan_or_die, send_result])
182 scan_or_die.add_children([ere_we_go, die])
183 die.add_children([failed_notification, result_failed_to_bb])
184 failed_notification.add_children([failed_flash_green, failed_pause])
185 ere_we_go.add_children([undock, scan_or_be_cancelled, dock, celebrate])
186 scan_or_be_cancelled.add_children([cancelling, move_out_and_scan])
187 cancelling.add_children([is_cancel_requested, move_home_after_cancel, result_cancelled_to_bb])
188 move_out_and_scan.add_children([move_base, scanning, move_home_after_scan, result_succeeded_to_bb])
Succeeding
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Ere we Go" -> UnDock;
"Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
"Ere we Go" -> "Scan or Be Cancelled";
"Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
"Scan or Be Cancelled" -> "Cancelling?";
"Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Be Cancelled" -> "Move Out and Scan";
"Move Out" [label="Move Out", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Move Out";
Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Move Out and Scan" -> Scanning;
"Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Context Switch";
Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> Rotate;
"Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Flash Blue";
"Move Home" [label="Move Home", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Move Home";
"Result2BB\n'succeeded'" [label="Result2BB\n'succeeded'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Result2BB\n'succeeded'";
Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Ere we Go" -> Dock;
Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
"Ere we Go" -> Celebrate;
}](_images/graphviz-a11261c9bed65d40a872409ad4d5ac59011f29de.png)
Assuming everything works perfectly, then the subtree will sequentially progress to completion through undocking, move out, rotate, move home and docking actions as illustrated in the dot graph above. However, nothing ever works perfectly, so …
Failing
If any step of the ‘Ere we Go’ sequence fails the mock robot robot will simply stop, drop into the post-failure (‘Die’) subtree and commence post-failure actions. In this case this consists of both an alarm signal (flashing red) and communication of failure to the user (echoes to the screen, but could have been, for example, a middleware response to the user’s application).
These actions are merely post-failure notifications that would ostensibly result in manual (human assisted) recovery of the situation. To attempt an automated recovery, there are two options:
Global Recovery - use the blackboard as a means of transferring information about the failure from the relevant behaviour (UnDock, Move Out, Move Home, Dock) to the post-failure subtree. Introspect the data and determine the right course of action in the post-failure subtree.
Local Recovery - use a selector with each of the individual behaviours to immediately generate a recovery subtree specifically adapted to the behaviour that failed. This recovery subtree should also return
FAILUREso the parent sequence also returnsFAILURE. The ‘Die’ subtree is then merely for common post-failure actions (e.g. notification and response).
The latter is technically preferable as the decision logic is entirely visible in the tree connections, but it does cause an explosion in the scale of the tree and it’s maintenance.
Note
It is interesting to observe that although the application is considered to have
failed, the ‘Scan or Die’ operation will return with SUCCESS
after which post-failure actions will kick in.
Here, application failure is recorded in the ‘Result2BB’ behaviour which is later
transmitted back to the user in the final stages of the application.
Application failure is handled via the actions of behaviours, not the state of the tree.
Tip
Decision logic in the tree is for routing decision making, not routing application failure/success, nor logical errors. Overloading tree decision logic with more than one purpose will constrain your application design to the point of non-usefulness.
Cancelling
In this tutorial, the application listens continuously for cancellation requests and will cancel the operation if it is currently between undocking and docking actions.
Note
The approach demonstrated in this tutorial is simple, but sufficient as an example. Interactions are only one-way - from the user to the application. It neither prevents the user from requesting nor does it provide an informative response if the request is invalid (i.e. if the application is not running or already cancelling). It also falls short of caching and handling cancel requests across the entire application. These cases are easy to handle with additional logic in the tree - consider it a homework exercise :)
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
Scan2BB [label=Scan2BB, shape=ellipse, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
Topics2BB -> Scan2BB;
Cancel2BB [label=Cancel2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Topics2BB -> Cancel2BB;
Battery2BB [label=Battery2BB, shape=ellipse, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
Topics2BB -> Battery2BB;
}](_images/graphviz-b8d2f20cd92d706208cf948447f4374d3fec8a00.png)
Cancelling begins with catching incoming cancel requests:
Cancelling is a high priority subtree, but here we make sure that the post-cancelling workflow integrates with the non-cancelling workflow so that the robot returns to it’s initial location and state.
Results
As noted earlier, it is typically important to keep application result logic separate from the decision tree logic. To do so, the blackboard is used to record the application result and an application result agnostic behaviour is used to communicate the result back to the user in the final stage of the application’s lifecycle.
Running
# Launch the tutorial
$ ros2 launch py_trees_ros_tutorials tutorial_seven_docking_cancelling_failing_launch.py
# In another shell
$ py-trees-tree-watcher -b
# Trigger scan/cancel requests from the qt dashboard
Tutorial 8 - Dynamic Application Loading
About
The previous tutorial enables execution of a specific job upon request. You will inevitably grow the functionality of the robot beyond this and a very common use case for the trees is to switch the context of the robot between ‘applications’ - calibration, tests, demos, scheduled tasks from a fleet server, etc.
While these contexts could be entirely managed by the tree simultaneously, the exclusivity of the applications lends itself far more easily to the following paradigm:
Construct a tree on bringup for ticking over basic functionality while idling
Dynamically insert/prune application subtrees on demand, rejecting requests when already busy
This mirrors both the way smart phones operate (which also happens to be a reasonable mode of operation for robots due to similar resource contention arguments) and the conventional use of roslaunch files to bringup a core and later bootstrap / tear down application level processes on demand.
This tutorial uses a wrapper class around py_trees_ros.trees.BehaviourTree to handle:
Construction of the core tree
A job (application) request callback
Insertion of the application subtree in the request callback (if not busy)
Pruning of the application subtree in a post-tick handler (if finished)
A status report service for external clients of the tree
Note
Only the basics are demonstrated here, but you could imagine extensions to this class that would make it truly useful in an application driven robotics system - abstractions so application modules need not be known in advance, application subtrees delivered as python code, more detailed tree introspection in status reports (given it’s responsibility to be the decision making engine for the robot, it is the best snapshot of the robot’s current activity). You’re only limited by your imagination!
Core Tree (Dot Graph)
$ py-trees-render -b py_trees_ros_tutorials.eight_dynamic_application_loading.tutorial_create_root
![digraph pastafarianism {
ordering=out;
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
"Tutorial Eight" [fillcolor=gold, fontcolor=black, fontsize=9, label="Tutorial Eight\nSuccessOnAll", shape=parallelogram, style=filled];
Topics2BB [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Topics2BB", shape=box, style=filled];
"Tutorial Eight" -> Topics2BB;
Scan2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Scan2BB, shape=ellipse, style=filled];
Topics2BB -> Scan2BB;
Cancel2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Cancel2BB, shape=ellipse, style=filled];
Topics2BB -> Cancel2BB;
Battery2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Battery2BB, shape=ellipse, style=filled];
Topics2BB -> Battery2BB;
Tasks [fillcolor=cyan, fontcolor=black, fontsize=9, label=Tasks, shape=octagon, style=filled];
"Tutorial Eight" -> Tasks;
"Battery Low?" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Battery Low?", shape=ellipse, style=filled];
Tasks -> "Battery Low?";
"Flash Red" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Red", shape=ellipse, style=filled];
"Battery Low?" -> "Flash Red";
Idle [fillcolor=gray, fontcolor=black, fontsize=9, label=Idle, shape=ellipse, style=filled];
Tasks -> Idle;
Cancel2BB -> "/event_cancel_button" [color=blue, constraint=False, weight=0];
"/battery_low_warning" -> "Battery Low?" [color=green, constraint=False, weight=0];
Battery2BB -> "/battery_low_warning" [color=blue, constraint=False, weight=0];
Scan2BB -> "/event_scan_button" [color=blue, constraint=False, weight=0];
Battery2BB -> "/battery" [color=blue, constraint=False, weight=0];
subgraph Blackboard {
id=Blackboard;
label=Blackboard;
rank=sink;
"/event_cancel_button" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/event_cancel_button: -", shape=box, style=filled, width=0];
"/battery_low_warning" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/battery_low_warning: False", shape=box, style=filled, width=0];
"/event_scan_button" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/event_scan_button: -", shape=box, style=filled, width=0];
"/battery" [color=blue, fillcolor=white, fixedsize=False, fontcolor=blue, fontsize=8, height=0, label="/battery: sensor_msgs.msg.B...", shape=box, style=filled, width=0];
}
}](_images/graphviz-5001a61705d263f2d0a28a37975a96395d83aea1.png)
py_trees_ros_tutorials.eight_dynamic_application_loading.tutorial_create_root
Application Subtree (Dot Graph)
$ py-trees-render --with-blackboard-variables py_trees_ros_tutorials.eight_dynamic_application_loading.tutorial_create_scan_subtree
![digraph pastafarianism {
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
Scan [label=Scan, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Die" [label="Scan or Die", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
Scan -> "Scan or Die";
"Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Die" -> "Ere we Go";
UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Ere we Go" -> UnDock;
"Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
"Ere we Go" -> "Scan or Be Cancelled";
"Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Be Cancelled" -> "Cancelling?";
"Cancel?" [label="Cancel?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Cancelling?" -> "Cancel?";
"Move Home" [label="Move Home", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Cancelling?" -> "Move Home";
"Result2BB\n'cancelled'" [label="Result2BB\n'cancelled'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Cancelling?" -> "Result2BB\n'cancelled'";
subgraph {
label="children_of_Cancelling?";
rank=same;
"Cancel?" [label="Cancel?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Home" [label="Move Home", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Result2BB\n'cancelled'" [label="Result2BB\n'cancelled'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
"Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Be Cancelled" -> "Move Out and Scan";
"Move Out" [label="Move Out", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Move Out";
Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Move Out and Scan" -> Scanning;
"Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Context Switch";
Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> Rotate;
"Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning -> "Flash Blue";
subgraph {
label=children_of_Scanning;
rank=same;
"Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
"Move Home*" [label="Move Home*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Move Home*";
"Result2BB\n'succeeded'" [label="Result2BB\n'succeeded'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Move Out and Scan" -> "Result2BB\n'succeeded'";
subgraph {
label="children_of_Move Out and Scan";
rank=same;
"Move Out" [label="Move Out", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Move Home*" [label="Move Home*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Result2BB\n'succeeded'" [label="Result2BB\n'succeeded'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
subgraph {
label="children_of_Scan or Be Cancelled";
rank=same;
"Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
}
Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Ere we Go" -> Dock;
Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Ere we Go" -> Celebrate;
"Flash Green" [label="Flash Green", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Celebrate -> "Flash Green";
Pause [label=Pause, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Celebrate -> Pause;
subgraph {
label=children_of_Celebrate;
rank=same;
"Flash Green" [label="Flash Green", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Pause [label=Pause, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
subgraph {
label="children_of_Ere we Go";
rank=same;
UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
}
Die [label=Die, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
"Scan or Die" -> Die;
Notification [label="Notification\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
Die -> Notification;
"Flash Red" [label="Flash Red", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Notification -> "Flash Red";
"Pause*" [label="Pause*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Notification -> "Pause*";
subgraph {
label=children_of_Notification;
rank=same;
"Flash Red" [label="Flash Red", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
"Pause*" [label="Pause*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
"Result2BB\n'failed'" [label="Result2BB\n'failed'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Die -> "Result2BB\n'failed'";
subgraph {
label=children_of_Die;
rank=same;
Notification [label="Notification\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
"Result2BB\n'failed'" [label="Result2BB\n'failed'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
subgraph {
label="children_of_Scan or Die";
rank=same;
"Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
Die [label=Die, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
}
"Send Result" [label="Send Result", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
Scan -> "Send Result";
subgraph {
label=children_of_Scan;
rank=same;
"Scan or Die" [label="Scan or Die", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
"Send Result" [label="Send Result", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
}
scan_result [label="scan_result: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
scan_result -> "Send Result" [color=blue, constraint=False];
"Result2BB\n'failed'" -> scan_result [color=blue, constraint=True];
"Result2BB\n'succeeded'" -> scan_result [color=blue, constraint=True];
"Result2BB\n'cancelled'" -> scan_result [color=blue, constraint=True];
event_cancel_button [label="event_cancel_button: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
event_cancel_button -> "Cancel?" [color=blue, constraint=False];
}](_images/graphviz-9e81c4d86e92644ecadebc737587a867ef48c6e0.png)
py_trees_ros_tutorials.eight_dynamic_application_loading.tutorial_create_scan_subtree
Dynamic Application Tree (Class)
1 return scan
2
3
4class DynamicApplicationTree(py_trees_ros.trees.BehaviourTree):
5 """
1 and unloading of jobs.
2 """
3
4 def __init__(self):
5 """
6 Create the core tree and add post tick handlers for post-execution
7 management of the tree.
8 """
9 super().__init__(
10 root=tutorial_create_root(),
11 unicode_tree_debug=True
12 )
1 self.prune_application_subtree_if_done
2 )
3
4 def setup(self, timeout: float):
5 """
6 Setup the tree and connect additional application management / status
7 report subscribers and services.
8
9 Args:
10 timeout: time (s) to wait (use common.Duration.INFINITE to block indefinitely)
11 """
12 super().setup(timeout=timeout)
13 self._report_service = self.node.create_service(
14 srv_type=py_trees_srvs.StatusReport,
15 srv_name="~/report",
16 callback=self.deliver_status_report,
17 qos_profile=rclpy.qos.qos_profile_services_default
18 )
19 self._job_subscriber = self.node.create_subscription(
20 msg_type=std_msgs.Empty,
21 topic="/dashboard/scan",
1 qos_profile=py_trees_ros.utilities.qos_profile_unlatched()
2 )
3
4 def receive_incoming_job(self, msg: std_msgs.Empty):
5 """
6 Incoming job callback.
7
8 Args:
9 msg: incoming goal message
10
11 Raises:
12 Exception: be ready to catch if any of the behaviours raise an exception
13 """
14 if self.busy():
15 self.node.get_logger().warning("rejecting new job, last job is still active")
16 else:
17 scan_subtree = tutorial_create_scan_subtree()
18 try:
19 py_trees.trees.setup(
20 root=scan_subtree,
21 node=self.node
22 )
23 except Exception as e:
24 console.logerror(console.red + "failed to setup the scan subtree, aborting [{}]".format(str(e)) + console.reset)
1 response.report = "idle [last result: {}]".format(last_result)
2 return response
3
4 def prune_application_subtree_if_done(self, tree):
5 """
6 Check if a job is running and if it has finished. If so, prune the job subtree from the tree.
7 Additionally, make a status report upon introspection of the tree.
8 Args:
9 tree (:class:`~py_trees.trees.BehaviourTree`): tree to investigate/manipulate.
10 """
11 # executing
12 if self.busy():
13 job = self.priorities.children[-2]
14 # finished
15 if job.status == py_trees.common.Status.SUCCESS or job.status == py_trees.common.Status.FAILURE:
16 self.node.get_logger().info("{0}: finished [{1}]".format(job.name, job.status))
1 sys.exit(1)
2 self.insert_subtree(scan_subtree, self.priorities.id, 1)
3 self.node.get_logger().info("inserted job subtree")
4
5 def deliver_status_report(
6 self,
7 unused_request: py_trees_srvs.StatusReport.Request, # noqa
8 response: py_trees_srvs.StatusReport.Response # noqa
9 ):
10 """
11 Prepare a status report for an external service client.
12
13 Args:
14 unused_request: empty request message
15 """
16 # last result value or none
17 last_result = self.blackboard_exchange.blackboard.get(name="scan_result")
18 if self.busy():
19 response.report = "executing"
20 elif self.root.tip().has_parent_with_name("Battery Emergency"):
21 response.report = "battery [last result: {}]".format(last_result)
Note
In the code above, there is a conspicuous absence of thread locks. This is possible due to the use of ROS2’s single threaded executors to handle service and subscriber callbacks along with the tree’s tick tock that operates from within ROS2 timer callbacks. If using a behaviour tree, as is exemplified here, to handle robot application logic, you should never need to go beyond single threaded execution and thus avoid the complexity and bugs that come along with having to handle concurrency (this is a considerable improvement on the situation for ROS1).
Running
# Launch the tutorial
$ ros2 launch py_trees_ros_tutorials tutorial_eight_dynamic_application_loading_launch.py
# In another shell, catch the tree snapshots
$ py-trees-tree-watcher -b
# Trigger scan/cancel requests from the qt dashboard
Tutorial 9 - Bagging Trees
Coming soon…