├── .gitignore
├── .project
├── .pydevproject
├── .readthedocs.yaml
├── .settings
└── org.eclipse.core.resources.prefs
├── CHANGELOG.rst
├── LICENSE
├── README.md
├── doc
├── Makefile
├── changelog.rst
├── conf.py
├── dot
│ ├── tutorial-eight-application-subtree.dot
│ ├── tutorial-eight-core-tree.dot
│ ├── tutorial-five-action-client.dot
│ ├── tutorial-five-action-clients.dot
│ ├── tutorial-five-data-gathering.dot
│ ├── tutorial-five-preemption.dot
│ ├── tutorial-five-scan-branch.dot
│ ├── tutorial-one-data-gathering.dot
│ ├── tutorial-seven-cancel2bb.dot
│ ├── tutorial-seven-docking-cancelling-failing.dot
│ ├── tutorial-seven-ere-we-go.dot
│ ├── tutorial-seven-failing.dot
│ ├── tutorial-six-context-switching-subtree.dot
│ ├── tutorial-six-context-switching.dot
│ └── tutorial-two-battery-check.dot
├── examples
│ ├── five_action_client.py
│ ├── five_data_gathering.py
│ ├── five_preemption.py
│ ├── five_scan_branch.py
│ ├── seven_cancel_blackboard.py
│ ├── seven_cancelling.py
│ ├── seven_failing.py
│ ├── seven_result.py
│ ├── seven_succeeding.py
│ └── six_context_switch.py
├── faq.rst
├── images
│ ├── tutorial-eight-dynamic-application-loading.png
│ ├── tutorial-five-action-clients.png
│ ├── tutorial-four-introspect-the-tree.gif
│ ├── tutorial-four-py-trees-ros-viewer.png
│ ├── tutorial-one-data-gathering.gif
│ ├── tutorial-seven-cancelling.svg
│ ├── tutorial-seven-docking-cancelling-failing.png
│ ├── tutorial-seven-failure_paths.svg
│ ├── tutorial-seven-result.svg
│ ├── tutorial-six-context-switching.png
│ ├── tutorial-three-introspect-the-blackboard.gif
│ └── tutorial-two-battery-check.png
├── index.rst
├── modules.rst
├── requirements.txt
├── terminology.rst
├── tutorials.rst
└── venv.bash
├── launch
├── mock_robot_launch.py
├── tutorial_eight_dynamic_application_loading_launch.py
├── tutorial_five_action_clients_launch.py
├── tutorial_four_introspect_the_tree_launch.py
├── tutorial_one_data_gathering_launch.py
├── tutorial_seven_docking_cancelling_failing_launch.py
├── tutorial_six_context_switching_launch.py
├── tutorial_three_introspect_the_blackboard_launch.py
└── tutorial_two_battery_check_launch.py
├── package.xml
├── py_trees_ros_tutorials
├── __init__.py
├── behaviours.py
├── eight_dynamic_application_loading.py
├── five_action_clients.py
├── mock
│ ├── __init__.py
│ ├── actions.py
│ ├── battery.py
│ ├── dashboard.py
│ ├── dock.py
│ ├── gui
│ │ ├── __init__.py
│ │ ├── configuration_group_box.py
│ │ ├── configuration_group_box.ui
│ │ ├── configuration_group_box_ui.py
│ │ ├── dashboard_group_box.py
│ │ ├── dashboard_group_box.ui
│ │ ├── dashboard_group_box_ui.py
│ │ ├── gen.bash
│ │ ├── main_window.py
│ │ ├── main_window.qrc
│ │ ├── main_window.ui
│ │ ├── main_window_rc.py
│ │ ├── main_window_ui.py
│ │ └── tuxrobot.png
│ ├── launch.py
│ ├── led_strip.py
│ ├── move_base.py
│ ├── rotate.py
│ └── safety_sensors.py
├── one_data_gathering.py
├── seven_docking_cancelling_failing.py
├── six_context_switching.py
├── two_battery_check.py
└── version.py
├── resources
└── py_trees_ros_tutorials
├── setup.cfg
├── setup.py
├── testies
└── tests
├── README.md
├── __init__.py
├── test_actions.py
└── test_led_strip.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *doc/html
3 | *egg-info
4 | .README.md.html
5 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | py_trees_ros_tutorials
4 |
5 |
6 | py_trees
7 | py_trees_ros
8 | py_trees_ros_interfaces
9 |
10 |
11 |
12 | org.python.pydev.PyDevBuilder
13 |
14 |
15 |
16 |
17 |
18 | org.python.pydev.pythonNature
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.pydevproject:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | /${PROJECT_DIR_NAME}
9 |
10 |
11 |
12 |
13 |
14 | python interpreter
15 |
16 |
17 | python3
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the version of Python and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.10"
12 | apt_packages:
13 | - graphviz
14 |
15 | # Build documentation in the docs/ directory with Sphinx
16 | sphinx:
17 | configuration: doc/conf.py
18 | fail_on_warning: true
19 |
20 | # Build your docs in additional formats such as PDF and ePub
21 | # [], all, or - epub, - pdf
22 | formats: []
23 |
24 | python:
25 | install:
26 | - requirements: doc/requirements.txt
27 | # Unfortunately can only do this from pip
28 | # - method: setuptools
29 | # path: .
30 | # extra_requirements:
31 | # - docs
32 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.core.resources.prefs:
--------------------------------------------------------------------------------
1 | eclipse.preferences.version=1
2 | encoding//doc/conf.py=utf-8
3 | encoding//doc/examples/five_action_client.py=utf-8
4 | encoding//doc/examples/five_preemption.py=utf-8
5 | encoding//doc/examples/five_scan_branch.py=utf-8
6 | encoding//doc/examples/seven_cancel_blackboard.py=utf-8
7 | encoding//doc/examples/seven_cancelling.py=utf-8
8 | encoding//doc/examples/seven_failing.py=utf-8
9 | encoding//doc/examples/seven_result.py=utf-8
10 | encoding//doc/examples/seven_succeeding.py=utf-8
11 | encoding//doc/examples/six_context_switch.py=utf-8
12 | encoding//launch/mock_robot_launch.py=utf-8
13 | encoding//launch/tutorial_eight_dynamic_application_loading_launch.py=utf-8
14 | encoding//launch/tutorial_five_action_clients_launch.py=utf-8
15 | encoding//launch/tutorial_four_introspect_the_tree_launch.py=utf-8
16 | encoding//launch/tutorial_one_data_gathering_launch.py=utf-8
17 | encoding//launch/tutorial_seven_docking_cancelling_failing_launch.py=utf-8
18 | encoding//launch/tutorial_six_context_switching_launch.py=utf-8
19 | encoding//launch/tutorial_three_introspect_the_blackboard_launch.py=utf-8
20 | encoding//launch/tutorial_two_battery_check_launch.py=utf-8
21 | encoding//py_trees_ros_tutorials/eight_dynamic_application_loading.py=utf-8
22 | encoding//py_trees_ros_tutorials/five_action_clients.py=utf-8
23 | encoding//py_trees_ros_tutorials/mock/actions.py=utf-8
24 | encoding//py_trees_ros_tutorials/mock/battery.py=utf-8
25 | encoding//py_trees_ros_tutorials/mock/dashboard.py=utf-8
26 | encoding//py_trees_ros_tutorials/mock/dock.py=utf-8
27 | encoding//py_trees_ros_tutorials/mock/gui/configuration_group_box.py=utf-8
28 | encoding//py_trees_ros_tutorials/mock/gui/configuration_group_box_ui.py=utf-8
29 | encoding//py_trees_ros_tutorials/mock/gui/dashboard_group_box.py=utf-8
30 | encoding//py_trees_ros_tutorials/mock/gui/main_window.py=utf-8
31 | encoding//py_trees_ros_tutorials/mock/gui/main_window_rc.py=utf-8
32 | encoding//py_trees_ros_tutorials/mock/launch.py=utf-8
33 | encoding//py_trees_ros_tutorials/mock/led_strip.py=utf-8
34 | encoding//py_trees_ros_tutorials/mock/move_base.py=utf-8
35 | encoding//py_trees_ros_tutorials/mock/rotate.py=utf-8
36 | encoding//py_trees_ros_tutorials/mock/safety_sensors.py=utf-8
37 | encoding//py_trees_ros_tutorials/one_data_gathering.py=utf-8
38 | encoding//py_trees_ros_tutorials/seven_docking_cancelling_failing.py=utf-8
39 | encoding//py_trees_ros_tutorials/six_context_switching.py=utf-8
40 | encoding//py_trees_ros_tutorials/two_battery_check.py=utf-8
41 | encoding//tests/test_actions.py=utf-8
42 | encoding//tests/test_led_strip.py=utf-8
43 | encoding/testies=utf-8
44 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | =========
2 | Changelog
3 | =========
4 |
5 | 2.3.0 (2025-01-11)
6 | ------------------
7 | * [tutorials] fix: grammar mistake (`#51 `_)
8 | * [doc] some fresh dot files
9 | * [mock] update for new shutdown handling for humble
10 | * [docs] update intersphinx releases to latest py_trees releases
11 | * [tutorials] refactor for explicit composite arguments
12 | * [mock] bugfix signal type mismatch for charging status
13 | * Contributors: Daniel Stonier, Humaney
14 |
15 | 2.1.0 (2020-08-02)
16 | ------------------
17 | * [infra] api updates for py_trees 2.1.x and ros2/foxy compatibility
18 | * [infra] spelling fix for tutorial eight
19 |
20 | 2.0.2 (2020-05-14)
21 | ------------------
22 | * [launch] tutorials seven, eight - not five
23 |
24 | 2.0.1 (2019-12-30)
25 | ------------------
26 | * [launch] switch to ros2 launch (not ros2 run)
27 | * [launch] dashing/eloquent handling of emulate_tty
28 | * [tests] moved from unittest to pytest
29 | * [tutorials] catch and log appropriately when ctrl-c engaged in setup
30 | * [tutorials] catch the setup specific TimedOutError so other errors are easy to diagnose
31 |
32 | 1.0.5 (2019-10-04)
33 | ------------------
34 | * [tests] shorten time to send cancel request to avoid accidental success too early
35 |
36 | 1.0.4 (2019-10-03)
37 | ------------------
38 | * [tutorials] updated for new blackboard (with tracking) changes
39 |
40 | 1.0.3 (2019-08-16)
41 | ------------------
42 | * [infra] add ament_index file to be installed
43 |
44 | 1.0.2 (2019-06-27)
45 | ------------------
46 | * [infra] script installation bugfix
47 |
48 | 1.0.1 (2019-06-26)
49 | ------------------
50 | * [infra] various minor release bugfixes
51 |
52 | 1.0.0 (2019-06-22)
53 | ------------------
54 | * [tutorials] all tutorials except for bagging upgraded for ROS2 dashing
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # Software License Agreement (BSD License)
2 | #
3 | # Copyright (c) 2019 Daniel Stonier
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions
8 | # are met:
9 | #
10 | # * Redistributions of source code must retain the above copyright
11 | # notice, this list of conditions and the following disclaimer.
12 | # * Redistributions in binary form must reproduce the above
13 | # copyright notice, this list of conditions and the following
14 | # disclaimer in the documentation and/or other materials provided
15 | # with the distribution.
16 | # * Neither the name of Yujin Robot nor the names of its
17 | # contributors may be used to endorse or promote products derived
18 | # from this software without specific prior written permission.
19 | #
20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 | # POSSIBILITY OF SUCH DAMAGE.
32 |
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PyTrees Tutorials for ROS
2 |
3 | Tutorials for usage of PyTrees on ROS 2 and, more generally, behaviour trees for
4 | robotics applications.
5 |
6 | ## Documentation
7 |
8 | Documentation and tutorials are on ReadTheDocs.
9 |
10 | * [devel](https://py-trees-ros-tutorials.readthedocs.io/en/devel/)
11 | * [release-2.3.x](https://py-trees-ros-tutorials.readthedocs.io/en/release-2.3.x/)
12 |
13 | ## PyTrees ROS Ecosystem
14 |
15 | Refer to [py_trees_ros/README.md](https://github.com/splintered-reality/py_trees_ros/blob/devel/README.md) for more information on the PyTrees packages in the ROS ecosystem.
16 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | @echo "This makefile is for building documentation"
3 | @echo "in the virtual environment via '. ./venv.bash'"
4 | @echo ""
5 | @echo "Valid targets:"
6 | @echo ""
7 | @echo " docs: build the sphinx documentation in ./html"
8 | @echo " clean: clean out the html directory"
9 |
10 | # use -vv for more verbosity
11 | docs:
12 | sphinx-build -v -b html . html
13 |
14 | clean:
15 | rm -rf html
16 |
17 | .PHONY: docs clean
18 |
--------------------------------------------------------------------------------
/doc/changelog.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CHANGELOG.rst
2 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-eight-application-subtree.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | Scan [label=Scan, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
6 | "Scan or Die" [label="Scan or Die", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
7 | Scan -> "Scan or Die";
8 | "Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
9 | "Scan or Die" -> "Ere we Go";
10 | UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
11 | "Ere we Go" -> UnDock;
12 | "Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
13 | "Ere we Go" -> "Scan or Be Cancelled";
14 | "Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
15 | "Scan or Be Cancelled" -> "Cancelling?";
16 | "Cancel?" [label="Cancel?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
17 | "Cancelling?" -> "Cancel?";
18 | "Move Home" [label="Move Home", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
19 | "Cancelling?" -> "Move Home";
20 | "Result2BB\n'cancelled'" [label="Result2BB\n'cancelled'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
21 | "Cancelling?" -> "Result2BB\n'cancelled'";
22 | subgraph {
23 | label="children_of_Cancelling?";
24 | rank=same;
25 | "Cancel?" [label="Cancel?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
26 | "Move Home" [label="Move Home", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
27 | "Result2BB\n'cancelled'" [label="Result2BB\n'cancelled'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
28 | }
29 |
30 | "Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
31 | "Scan or Be Cancelled" -> "Move Out and Scan";
32 | "Move Out" [label="Move Out", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
33 | "Move Out and Scan" -> "Move Out";
34 | Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
35 | "Move Out and Scan" -> Scanning;
36 | "Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
37 | Scanning -> "Context Switch";
38 | Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
39 | Scanning -> Rotate;
40 | "Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
41 | Scanning -> "Flash Blue";
42 | subgraph {
43 | label=children_of_Scanning;
44 | rank=same;
45 | "Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
46 | Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
47 | "Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
48 | }
49 |
50 | "Move Home*" [label="Move Home*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
51 | "Move Out and Scan" -> "Move Home*";
52 | "Result2BB\n'succeeded'" [label="Result2BB\n'succeeded'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
53 | "Move Out and Scan" -> "Result2BB\n'succeeded'";
54 | subgraph {
55 | label="children_of_Move Out and Scan";
56 | rank=same;
57 | "Move Out" [label="Move Out", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
58 | Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
59 | "Move Home*" [label="Move Home*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
60 | "Result2BB\n'succeeded'" [label="Result2BB\n'succeeded'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
61 | }
62 |
63 | subgraph {
64 | label="children_of_Scan or Be Cancelled";
65 | rank=same;
66 | "Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
67 | "Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
68 | }
69 |
70 | Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
71 | "Ere we Go" -> Dock;
72 | Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
73 | "Ere we Go" -> Celebrate;
74 | "Flash Green" [label="Flash Green", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
75 | Celebrate -> "Flash Green";
76 | Pause [label=Pause, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
77 | Celebrate -> Pause;
78 | subgraph {
79 | label=children_of_Celebrate;
80 | rank=same;
81 | "Flash Green" [label="Flash Green", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
82 | Pause [label=Pause, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
83 | }
84 |
85 | subgraph {
86 | label="children_of_Ere we Go";
87 | rank=same;
88 | UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
89 | "Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
90 | Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
91 | Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
92 | }
93 |
94 | Die [label=Die, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
95 | "Scan or Die" -> Die;
96 | Notification [label="Notification\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
97 | Die -> Notification;
98 | "Flash Red" [label="Flash Red", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
99 | Notification -> "Flash Red";
100 | "Pause*" [label="Pause*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
101 | Notification -> "Pause*";
102 | subgraph {
103 | label=children_of_Notification;
104 | rank=same;
105 | "Flash Red" [label="Flash Red", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
106 | "Pause*" [label="Pause*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
107 | }
108 |
109 | "Result2BB\n'failed'" [label="Result2BB\n'failed'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
110 | Die -> "Result2BB\n'failed'";
111 | subgraph {
112 | label=children_of_Die;
113 | rank=same;
114 | Notification [label="Notification\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
115 | "Result2BB\n'failed'" [label="Result2BB\n'failed'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
116 | }
117 |
118 | subgraph {
119 | label="children_of_Scan or Die";
120 | rank=same;
121 | "Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
122 | Die [label=Die, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
123 | }
124 |
125 | "Send Result" [label="Send Result", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
126 | Scan -> "Send Result";
127 | subgraph {
128 | label=children_of_Scan;
129 | rank=same;
130 | "Scan or Die" [label="Scan or Die", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
131 | "Send Result" [label="Send Result", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
132 | }
133 |
134 | scan_result [label="scan_result: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False];
135 | scan_result -> "Send Result" [color=blue, constraint=False];
136 | "Result2BB\n'failed'" -> scan_result [color=blue, constraint=True];
137 | "Result2BB\n'succeeded'" -> scan_result [color=blue, constraint=True];
138 | "Result2BB\n'cancelled'" -> scan_result [color=blue, constraint=True];
139 | 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];
140 | event_cancel_button -> "Cancel?" [color=blue, constraint=False];
141 | }
142 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-eight-core-tree.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | ordering=out;
3 | graph [fontname="times-roman"];
4 | node [fontname="times-roman"];
5 | edge [fontname="times-roman"];
6 | "Tutorial Eight" [fillcolor=gold, fontcolor=black, fontsize=9, label="Tutorial Eight\nSuccessOnAll", shape=parallelogram, style=filled];
7 | Topics2BB [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Topics2BB", shape=box, style=filled];
8 | "Tutorial Eight" -> Topics2BB;
9 | Scan2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Scan2BB, shape=ellipse, style=filled];
10 | Topics2BB -> Scan2BB;
11 | Cancel2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Cancel2BB, shape=ellipse, style=filled];
12 | Topics2BB -> Cancel2BB;
13 | Battery2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Battery2BB, shape=ellipse, style=filled];
14 | Topics2BB -> Battery2BB;
15 | Tasks [fillcolor=cyan, fontcolor=black, fontsize=9, label=Tasks, shape=octagon, style=filled];
16 | "Tutorial Eight" -> Tasks;
17 | "Battery Low?" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Battery Low?", shape=ellipse, style=filled];
18 | Tasks -> "Battery Low?";
19 | "Flash Red" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Red", shape=ellipse, style=filled];
20 | "Battery Low?" -> "Flash Red";
21 | Idle [fillcolor=gray, fontcolor=black, fontsize=9, label=Idle, shape=ellipse, style=filled];
22 | Tasks -> Idle;
23 | Cancel2BB -> "/event_cancel_button" [color=blue, constraint=False, weight=0];
24 | "/battery_low_warning" -> "Battery Low?" [color=green, constraint=False, weight=0];
25 | Battery2BB -> "/battery_low_warning" [color=blue, constraint=False, weight=0];
26 | Scan2BB -> "/event_scan_button" [color=blue, constraint=False, weight=0];
27 | Battery2BB -> "/battery" [color=blue, constraint=False, weight=0];
28 | subgraph Blackboard {
29 | id=Blackboard;
30 | label=Blackboard;
31 | rank=sink;
32 | "/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];
33 | "/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];
34 | "/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];
35 | "/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];
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-five-action-client.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | "Preempt?" [label="Preempt?", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
6 | Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
7 | "Preempt?" -> Scanning;
8 | Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
9 | Scanning -> Rotate;
10 | "Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
11 | Scanning -> "Flash Blue";
12 | }
13 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-five-action-clients.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | ordering=out;
3 | graph [fontname="times-roman"];
4 | node [fontname="times-roman"];
5 | edge [fontname="times-roman"];
6 | "Tutorial Five" [fillcolor=gold, fontcolor=black, fontsize=9, label="Tutorial Five\nSuccessOnAll", shape=parallelogram, style=filled];
7 | Topics2BB [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Topics2BB", shape=box, style=filled];
8 | "Tutorial Five" -> Topics2BB;
9 | Scan2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Scan2BB, shape=ellipse, style=filled];
10 | Topics2BB -> Scan2BB;
11 | Battery2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Battery2BB, shape=ellipse, style=filled];
12 | Topics2BB -> Battery2BB;
13 | Tasks [fillcolor=cyan, fontcolor=black, fontsize=9, label=Tasks, shape=octagon, style=filled];
14 | "Tutorial Five" -> Tasks;
15 | "Battery Low?" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Battery Low?", shape=ellipse, style=filled];
16 | Tasks -> "Battery Low?";
17 | "Flash Red" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Red", shape=ellipse, style=filled];
18 | "Battery Low?" -> "Flash Red";
19 | Scan [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Scan", shape=box, style=filled];
20 | Tasks -> Scan;
21 | "Scan?" [fillcolor=gray, fontcolor=black, fontsize=9, label="Scan?", shape=ellipse, style=filled];
22 | Scan -> "Scan?";
23 | "Preempt?" [fillcolor=cyan, fontcolor=black, fontsize=9, label="Preempt?", shape=octagon, style=filled];
24 | Scan -> "Preempt?";
25 | SuccessIsRunning [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=SuccessIsRunning, shape=ellipse, style=filled];
26 | "Preempt?" -> SuccessIsRunning;
27 | "Scan?*" [fillcolor=gray, fontcolor=black, fontsize=9, label="Scan?*", shape=ellipse, style=filled];
28 | SuccessIsRunning -> "Scan?*";
29 | Scanning [fillcolor=gold, fontcolor=black, fontsize=9, label="Scanning\nSuccessOnOne", shape=parallelogram, style=filled];
30 | "Preempt?" -> Scanning;
31 | Rotate [fillcolor=gray, fontcolor=black, fontsize=9, label=Rotate, shape=ellipse, style=filled];
32 | Scanning -> Rotate;
33 | "Flash Blue" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Blue", shape=ellipse, style=filled];
34 | Scanning -> "Flash Blue";
35 | Celebrate [fillcolor=gold, fontcolor=black, fontsize=9, label="Celebrate\nSuccessOnOne", shape=parallelogram, style=filled];
36 | Scan -> Celebrate;
37 | "Flash Green" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Green", shape=ellipse, style=filled];
38 | Celebrate -> "Flash Green";
39 | Pause [fillcolor=gray, fontcolor=black, fontsize=9, label=Pause, shape=ellipse, style=filled];
40 | Celebrate -> Pause;
41 | Idle [fillcolor=gray, fontcolor=black, fontsize=9, label=Idle, shape=ellipse, style=filled];
42 | Tasks -> Idle;
43 | "/goal_0d200292-24e9-4aef-9f08-a16147275b7e" -> Rotate [color=green, constraint=False, weight=0];
44 | Rotate -> "/goal_0d200292-24e9-4aef-9f08-a16147275b7e" [color=blue, constraint=False, weight=0];
45 | "/battery_low_warning" -> "Battery Low?" [color=green, constraint=False, weight=0];
46 | Battery2BB -> "/battery_low_warning" [color=blue, constraint=False, weight=0];
47 | Battery2BB -> "/battery" [color=blue, constraint=False, weight=0];
48 | "/event_scan_button" -> "Scan?" [color=green, constraint=False, weight=0];
49 | "/event_scan_button" -> "Scan?*" [color=green, constraint=False, weight=0];
50 | Scan2BB -> "/event_scan_button" [color=blue, constraint=False, weight=0];
51 | subgraph Blackboard {
52 | id=Blackboard;
53 | label=Blackboard;
54 | rank=sink;
55 | "/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];
56 | "/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];
57 | "/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];
58 | "/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];
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-five-data-gathering.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
6 | Scan2BB [label=Scan2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
7 | Topics2BB -> Scan2BB;
8 | Battery2BB [label=Battery2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
9 | Topics2BB -> Battery2BB;
10 | }
11 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-five-preemption.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | "Preempt?" [label="Preempt?", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
6 | SuccessIsRunning [label=SuccessIsRunning, shape=ellipse, style=filled, fillcolor=ghostwhite, fontsize=9, fontcolor=black];
7 | "Preempt?" -> SuccessIsRunning;
8 | "Scan?" [label="Scan?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
9 | SuccessIsRunning -> "Scan?";
10 | Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
11 | "Preempt?" -> Scanning;
12 | Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
13 | Scanning -> Rotate;
14 | "Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
15 | Scanning -> "Flash Blue";
16 | }
17 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-five-scan-branch.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | Scan [label=Scan, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
6 | "Scan?" [label="Scan?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
7 | Scan -> "Scan?";
8 | "Preempt?" [label="Preempt?", shape=octagon, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
9 | Scan -> "Preempt?";
10 | }
11 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-one-data-gathering.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | "Tutorial One" [label="Tutorial One\n--SuccessOnAll(-)--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
6 | Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
7 | "Tutorial One" -> Topics2BB;
8 | Battery2BB [label=Battery2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
9 | Topics2BB -> Battery2BB;
10 | Tasks [label=Tasks, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
11 | "Tutorial One" -> Tasks;
12 | "Flip Eggs" [label="Flip Eggs", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
13 | Tasks -> "Flip Eggs";
14 | Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
15 | Tasks -> Idle;
16 | subgraph {
17 | label=children_of_Tasks;
18 | rank=same;
19 | "Flip Eggs" [label="Flip Eggs", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
20 | Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
21 | }
22 |
23 | subgraph {
24 | label="children_of_Tutorial One";
25 | rank=same;
26 | Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
27 | Tasks [label=Tasks, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
28 | }
29 |
30 | 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];
31 | Battery2BB -> battery [color=blue, constraint=True];
32 | 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];
33 | Battery2BB -> battery_low_warning [color=blue, constraint=True];
34 | }
35 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-seven-cancel2bb.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | Topics2BB [label=Topics2BB, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
6 | Scan2BB [label=Scan2BB, shape=ellipse, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
7 | Topics2BB -> Scan2BB;
8 | Cancel2BB [label=Cancel2BB, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
9 | Topics2BB -> Cancel2BB;
10 | Battery2BB [label=Battery2BB, shape=ellipse, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
11 | Topics2BB -> Battery2BB;
12 | }
13 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-seven-ere-we-go.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | "Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
6 | UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
7 | "Ere we Go" -> UnDock;
8 | "Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
9 | "Ere we Go" -> "Scan or Be Cancelled";
10 | "Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
11 | "Scan or Be Cancelled" -> "Cancelling?";
12 | "Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
13 | "Scan or Be Cancelled" -> "Move Out and Scan";
14 | "Move Out" [label="Move Out", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
15 | "Move Out and Scan" -> "Move Out";
16 | Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
17 | "Move Out and Scan" -> Scanning;
18 | "Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
19 | Scanning -> "Context Switch";
20 | Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
21 | Scanning -> Rotate;
22 | "Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
23 | Scanning -> "Flash Blue";
24 | "Move Home" [label="Move Home", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
25 | "Move Out and Scan" -> "Move Home";
26 | "Result2BB\n'succeeded'" [label="Result2BB\n'succeeded'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
27 | "Move Out and Scan" -> "Result2BB\n'succeeded'";
28 | Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
29 | "Ere we Go" -> Dock;
30 | Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
31 | "Ere we Go" -> Celebrate;
32 | }
33 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-seven-failing.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | "Scan or Die" [label="Scan or Die", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
6 | "Ere we Go" [label="Ere we Go", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
7 | "Scan or Die" -> "Ere we Go";
8 | UnDock [label=UnDock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
9 | "Ere we Go" -> UnDock;
10 | "Scan or Be Cancelled" [label="Scan or Be Cancelled", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black];
11 | "Ere we Go" -> "Scan or Be Cancelled";
12 | "Cancelling?" [label="Cancelling?", shape=box, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
13 | "Scan or Be Cancelled" -> "Cancelling?";
14 | "Move Out and Scan" [label="Move Out and Scan", shape=box, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
15 | "Scan or Be Cancelled" -> "Move Out and Scan";
16 | Dock [label=Dock, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
17 | "Ere we Go" -> Dock;
18 | Celebrate [label="Celebrate\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gray20, fontsize=9, fontcolor=dodgerblue];
19 | "Ere we Go" -> Celebrate;
20 | Die [label=Die, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black];
21 | "Scan or Die" -> Die;
22 | Notification [label="Notification\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
23 | Die -> Notification;
24 | "Flash Red" [label="Flash Red", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
25 | Notification -> "Flash Red";
26 | Pause [label=Pause, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
27 | Notification -> Pause;
28 | "Result2BB\n'failed'" [label="Result2BB\n'failed'", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
29 | Die -> "Result2BB\n'failed'";
30 | }
31 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-six-context-switching-subtree.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | graph [fontname="times-roman"];
3 | node [fontname="times-roman"];
4 | edge [fontname="times-roman"];
5 | Scanning [label="Scanning\n--SuccessOnOne--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black];
6 | "Context Switch" [label="Context Switch", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
7 | Scanning -> "Context Switch";
8 | Rotate [label=Rotate, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
9 | Scanning -> Rotate;
10 | "Flash Blue" [label="Flash Blue", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black];
11 | Scanning -> "Flash Blue";
12 | }
13 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-six-context-switching.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | ordering=out;
3 | graph [fontname="times-roman"];
4 | node [fontname="times-roman"];
5 | edge [fontname="times-roman"];
6 | "Tutorial Six" [fillcolor=gold, fontcolor=black, fontsize=9, label="Tutorial Six\nSuccessOnAll", shape=parallelogram, style=filled];
7 | Topics2BB [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Topics2BB", shape=box, style=filled];
8 | "Tutorial Six" -> Topics2BB;
9 | Scan2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Scan2BB, shape=ellipse, style=filled];
10 | Topics2BB -> Scan2BB;
11 | Battery2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Battery2BB, shape=ellipse, style=filled];
12 | Topics2BB -> Battery2BB;
13 | Tasks [fillcolor=cyan, fontcolor=black, fontsize=9, label=Tasks, shape=octagon, style=filled];
14 | "Tutorial Six" -> Tasks;
15 | "Battery Low?" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Battery Low?", shape=ellipse, style=filled];
16 | Tasks -> "Battery Low?";
17 | "Flash Red" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Red", shape=ellipse, style=filled];
18 | "Battery Low?" -> "Flash Red";
19 | Scan [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Scan", shape=box, style=filled];
20 | Tasks -> Scan;
21 | "Scan?" [fillcolor=gray, fontcolor=black, fontsize=9, label="Scan?", shape=ellipse, style=filled];
22 | Scan -> "Scan?";
23 | "Preempt?" [fillcolor=cyan, fontcolor=black, fontsize=9, label="Preempt?", shape=octagon, style=filled];
24 | Scan -> "Preempt?";
25 | SuccessIsRunning [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=SuccessIsRunning, shape=ellipse, style=filled];
26 | "Preempt?" -> SuccessIsRunning;
27 | "Scan?*" [fillcolor=gray, fontcolor=black, fontsize=9, label="Scan?*", shape=ellipse, style=filled];
28 | SuccessIsRunning -> "Scan?*";
29 | Scanning [fillcolor=gold, fontcolor=black, fontsize=9, label="Scanning\nSuccessOnOne", shape=parallelogram, style=filled];
30 | "Preempt?" -> Scanning;
31 | "Context Switch" [fillcolor=gray, fontcolor=black, fontsize=9, label="Context Switch", shape=ellipse, style=filled];
32 | Scanning -> "Context Switch";
33 | Rotate [fillcolor=gray, fontcolor=black, fontsize=9, label=Rotate, shape=ellipse, style=filled];
34 | Scanning -> Rotate;
35 | "Flash Blue" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Blue", shape=ellipse, style=filled];
36 | Scanning -> "Flash Blue";
37 | Celebrate [fillcolor=gold, fontcolor=black, fontsize=9, label="Celebrate\nSuccessOnOne", shape=parallelogram, style=filled];
38 | Scan -> Celebrate;
39 | "Flash Green" [fillcolor=gray, fontcolor=black, fontsize=9, label="Flash Green", shape=ellipse, style=filled];
40 | Celebrate -> "Flash Green";
41 | Pause [fillcolor=gray, fontcolor=black, fontsize=9, label=Pause, shape=ellipse, style=filled];
42 | Celebrate -> Pause;
43 | Idle [fillcolor=gray, fontcolor=black, fontsize=9, label=Idle, shape=ellipse, style=filled];
44 | Tasks -> Idle;
45 | "/goal_69f16b95-c094-4145-85e6-4c0c7477bb06" -> Rotate [color=green, constraint=False, weight=0];
46 | Rotate -> "/goal_69f16b95-c094-4145-85e6-4c0c7477bb06" [color=blue, constraint=False, weight=0];
47 | "/event_scan_button" -> "Scan?*" [color=green, constraint=False, weight=0];
48 | "/event_scan_button" -> "Scan?" [color=green, constraint=False, weight=0];
49 | Scan2BB -> "/event_scan_button" [color=blue, constraint=False, weight=0];
50 | Battery2BB -> "/battery" [color=blue, constraint=False, weight=0];
51 | "/battery_low_warning" -> "Battery Low?" [color=green, constraint=False, weight=0];
52 | Battery2BB -> "/battery_low_warning" [color=blue, constraint=False, weight=0];
53 | subgraph Blackboard {
54 | id=Blackboard;
55 | label=Blackboard;
56 | rank=sink;
57 | "/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];
58 | "/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];
59 | "/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];
60 | "/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];
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/doc/dot/tutorial-two-battery-check.dot:
--------------------------------------------------------------------------------
1 | digraph pastafarianism {
2 | ordering=out;
3 | graph [fontname="times-roman"];
4 | node [fontname="times-roman"];
5 | edge [fontname="times-roman"];
6 | "Tutorial Two" [fillcolor=gold, fontcolor=black, fontsize=9, label="Tutorial Two\nSuccessOnAll", shape=parallelogram, style=filled];
7 | Topics2BB [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Topics2BB", shape=box, style=filled];
8 | "Tutorial Two" -> Topics2BB;
9 | Battery2BB [fillcolor=gray, fontcolor=black, fontsize=9, label=Battery2BB, shape=ellipse, style=filled];
10 | Topics2BB -> Battery2BB;
11 | Tasks [fillcolor=cyan, fontcolor=black, fontsize=9, label=Tasks, shape=octagon, style=filled];
12 | "Tutorial Two" -> Tasks;
13 | "Battery Low?" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Battery Low?", shape=ellipse, style=filled];
14 | Tasks -> "Battery Low?";
15 | FlashLEDs [fillcolor=gray, fontcolor=black, fontsize=9, label=FlashLEDs, shape=ellipse, style=filled];
16 | "Battery Low?" -> FlashLEDs;
17 | Idle [fillcolor=gray, fontcolor=black, fontsize=9, label=Idle, shape=ellipse, style=filled];
18 | Tasks -> Idle;
19 | "/battery_low_warning" -> "Battery Low?" [color=green, constraint=False, weight=0];
20 | Battery2BB -> "/battery_low_warning" [color=blue, constraint=False, weight=0];
21 | Battery2BB -> "/battery" [color=blue, constraint=False, weight=0];
22 | subgraph Blackboard {
23 | id=Blackboard;
24 | label=Blackboard;
25 | rank=sink;
26 | "/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];
27 | "/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];
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/doc/examples/five_action_client.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros
6 | import py_trees_ros_interfaces.action as py_trees_actions
7 | import py_trees_ros_tutorials
8 |
9 | if __name__ == '__main__':
10 |
11 | scan_preempt = py_trees.composites.Selector(name="Preempt?", memory=False)
12 | scanning = py_trees.composites.Parallel(
13 | name="Scanning",
14 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
15 | )
16 | scan_rotate = py_trees_ros.actions.ActionClient(
17 | name="Rotate",
18 | action_type=py_trees_actions.Rotate,
19 | action_name="rotate",
20 | action_goal=py_trees_actions.Rotate.Goal(),
21 | generate_feedback_message=lambda msg: "{:.2f}%%".format(msg.percentage_completed)
22 | )
23 | flash_blue = py_trees_ros_tutorials.behaviours.FlashLedStrip(
24 | name="Flash Blue",
25 | colour="blue"
26 | )
27 |
28 | scan_preempt.add_children([scanning])
29 | scanning.add_children([scan_rotate, flash_blue])
30 | py_trees.display.render_dot_tree(
31 | scan_preempt,
32 | py_trees.common.string_to_visibility_level("all"))
33 |
--------------------------------------------------------------------------------
/doc/examples/five_data_gathering.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import py_trees
4 | import py_trees_ros
5 |
6 | if __name__ == '__main__':
7 | root = py_trees.composites.Sequence(name="Topics2BB", memory=True)
8 |
9 | scan2bb = py_trees_ros.subscribers.EventToBlackboard(
10 | name="Scan2BB",
11 | topic_name="/dashboard/scan",
12 | variable_name="event_scan_button"
13 | )
14 | battery2bb = py_trees_ros.battery.ToBlackboard(
15 | name="Battery2BB",
16 | topic_name="/battery/state",
17 | threshold=30.0
18 | )
19 | root.add_children([scan2bb, battery2bb])
20 | py_trees.display.render_dot_tree(
21 | root,
22 | py_trees.common.string_to_visibility_level("all"))
23 |
--------------------------------------------------------------------------------
/doc/examples/five_preemption.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros
6 | import py_trees_ros_interfaces.action as py_trees_actions
7 | import py_trees_ros_tutorials
8 |
9 | if __name__ == '__main__':
10 |
11 | scan_preempt = py_trees.composites.Selector(name="Preempt?", memory=False)
12 | is_scan_requested_two = py_trees.decorators.SuccessIsRunning(
13 | name="SuccessIsRunning",
14 | child=py_trees.blackboard.CheckBlackboardVariable(
15 | name="Scan?",
16 | variable_name='event_scan_button',
17 | expected_value=True
18 | )
19 | )
20 | scanning = py_trees.composites.Parallel(
21 | name="Scanning",
22 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
23 | )
24 | scan_rotate = py_trees_ros.actions.ActionClient(
25 | name="Rotate",
26 | action_type=py_trees_actions.Rotate,
27 | action_name="rotate",
28 | action_goal=py_trees_actions.Rotate.Goal(),
29 | generate_feedback_message=lambda msg: "{:.2f}%%".format(msg.percentage_completed)
30 | )
31 | flash_blue = py_trees_ros_tutorials.behaviours.FlashLedStrip(
32 | name="Flash Blue",
33 | colour="blue"
34 | )
35 |
36 | scan_preempt.add_children([is_scan_requested_two, scanning])
37 | scanning.add_children([scan_rotate, flash_blue])
38 | py_trees.display.render_dot_tree(
39 | scan_preempt,
40 | py_trees.common.string_to_visibility_level("all"))
41 |
--------------------------------------------------------------------------------
/doc/examples/five_scan_branch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 |
6 | if __name__ == '__main__':
7 |
8 | scan = py_trees.composites.Sequence(name="Scan", memory=True)
9 | is_scan_requested = py_trees.blackboard.CheckBlackboardVariable(
10 | name="Scan?",
11 | variable_name='event_scan_button',
12 | expected_value=True
13 | )
14 | scan_preempt = py_trees.composites.Selector(name="Preempt?", memory=False)
15 | scan_preempt.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
16 |
17 | scan.add_children([is_scan_requested, scan_preempt])
18 | py_trees.display.render_dot_tree(
19 | scan,
20 | py_trees.common.string_to_visibility_level("detail"))
21 |
--------------------------------------------------------------------------------
/doc/examples/seven_cancel_blackboard.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros
6 | import py_trees_ros_interfaces.action as py_trees_actions
7 | import py_trees_ros_tutorials
8 |
9 | if __name__ == '__main__':
10 |
11 | topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
12 | scan2bb = py_trees_ros.subscribers.EventToBlackboard(
13 | name="Scan2BB",
14 | topic_name="/dashboard/scan",
15 | variable_name="event_scan_button"
16 | )
17 | scan2bb.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
18 | cancel2bb = py_trees_ros.subscribers.EventToBlackboard(
19 | name="Cancel2BB",
20 | topic_name="/dashboard/cancel",
21 | variable_name="event_cancel_button"
22 | )
23 | battery2bb = py_trees_ros.battery.ToBlackboard(
24 | name="Battery2BB",
25 | topic_name="/battery/state",
26 | threshold=30.0
27 | )
28 | battery2bb.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
29 | topics2bb.add_children([scan2bb, cancel2bb, battery2bb])
30 |
31 | py_trees.display.render_dot_tree(
32 | topics2bb,
33 | py_trees.common.string_to_visibility_level("detail")
34 | )
35 |
--------------------------------------------------------------------------------
/doc/examples/seven_cancelling.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros
6 | import py_trees_ros_interfaces.action as py_trees_actions # noqa
7 | import py_trees_ros_tutorials
8 |
9 | if __name__ == '__main__':
10 |
11 | ere_we_go = py_trees.composites.Sequence(name="Ere we Go", memory=True)
12 | undock = py_trees_ros.actions.ActionClient(
13 | name="UnDock",
14 | action_type=py_trees_actions.Dock,
15 | action_name="dock",
16 | action_goal=py_trees_actions.Dock.Goal(dock=False),
17 | generate_feedback_message=lambda msg: "undocking"
18 | )
19 | scan_or_be_cancelled = py_trees.composites.Selector(name="Scan or Be Cancelled", memory=False)
20 | cancelling = py_trees.composites.Sequence(name="Cancelling?", memory=True)
21 | is_cancel_requested = py_trees.blackboard.CheckBlackboardVariable(
22 | name="Cancel?",
23 | variable_name='event_cancel_button',
24 | expected_value=True
25 | )
26 | move_home_after_cancel = py_trees_ros.actions.ActionClient(
27 | name="Move Home",
28 | action_type=py_trees_actions.MoveBase,
29 | action_name="move_base",
30 | action_goal=py_trees_actions.MoveBase.Goal(),
31 | generate_feedback_message=lambda msg: "moving home"
32 | )
33 | result_cancelled_to_bb = py_trees.blackboard.SetBlackboardVariable(
34 | name="Result2BB\n'cancelled'",
35 | variable_name='scan_result',
36 | variable_value='cancelled'
37 | )
38 | move_out_and_scan = py_trees.composites.Sequence(name="Move Out and Scan", memory=True)
39 | move_base = py_trees_ros.actions.ActionClient(
40 | name="Move Out",
41 | action_type=py_trees_actions.MoveBase,
42 | action_name="move_base",
43 | action_goal=py_trees_actions.MoveBase.Goal(),
44 | generate_feedback_message=lambda msg: "moving out"
45 | )
46 | scanning = py_trees.composites.Parallel(
47 | name="Scanning",
48 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
49 | )
50 | scanning.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
51 | move_home_after_scan = py_trees_ros.actions.ActionClient(
52 | name="Move Home",
53 | action_type=py_trees_actions.MoveBase,
54 | action_name="move_base",
55 | action_goal=py_trees_actions.MoveBase.Goal(),
56 | generate_feedback_message=lambda msg: "moving home"
57 | )
58 | result_succeeded_to_bb = py_trees.blackboard.SetBlackboardVariable(
59 | name="Result2BB\n'succeeded'",
60 | variable_name='scan_result',
61 | variable_value='succeeded'
62 | )
63 | celebrate = py_trees.composites.Parallel(
64 | name="Celebrate",
65 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
66 | )
67 | celebrate_flash_green = py_trees_ros_tutorials.behaviours.FlashLedStrip(name="Flash Green", colour="green")
68 | celebrate_pause = py_trees.timers.Timer("Pause", duration=3.0)
69 | dock = py_trees_ros.actions.ActionClient(
70 | name="Dock",
71 | action_type=py_trees_actions.Dock,
72 | action_name="dock",
73 | action_goal=py_trees_actions.Dock.Goal(dock=True),
74 | generate_feedback_message=lambda msg: "docking"
75 | )
76 | ere_we_go.add_children([undock, scan_or_be_cancelled, dock, celebrate])
77 | scan_or_be_cancelled.add_children([cancelling, move_out_and_scan])
78 | cancelling.add_children([is_cancel_requested, move_home_after_cancel, result_cancelled_to_bb])
79 | move_out_and_scan.add_children([move_base, scanning, move_home_after_scan, result_succeeded_to_bb])
80 | celebrate.add_children([celebrate_flash_green, celebrate_pause])
81 |
82 | py_trees.display.render_dot_tree(
83 | ere_we_go,
84 | py_trees.common.string_to_visibility_level("detail")
85 | )
86 |
--------------------------------------------------------------------------------
/doc/examples/seven_failing.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros
6 | import py_trees_ros_interfaces.action as py_trees_actions
7 | import py_trees_ros_tutorials
8 |
9 | if __name__ == '__main__':
10 |
11 | scan_or_die = py_trees.composites.Selector(name="Scan or Die", memory=False)
12 | die = py_trees.composites.Sequence(name="Die", memory=True)
13 | failed_notification = py_trees.composites.Parallel(
14 | name="Notification",
15 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
16 | )
17 | failed_flash_green = py_trees_ros_tutorials.behaviours.FlashLedStrip(
18 | name="Flash Red",
19 | colour="red"
20 | )
21 | failed_pause = py_trees.timers.Timer("Pause", duration=3.0)
22 | result_failed_to_bb = py_trees.blackboard.SetBlackboardVariable(
23 | name="Result2BB\n'failed'",
24 | variable_name='scan_result',
25 | variable_value='failed'
26 | )
27 | ere_we_go = py_trees.composites.Sequence(name="Ere we Go", memory=True)
28 | undock = py_trees_ros.actions.ActionClient(
29 | name="UnDock",
30 | action_type=py_trees_actions.Dock,
31 | action_name="dock",
32 | action_goal=py_trees_actions.Dock.Goal(dock=False),
33 | generate_feedback_message=lambda msg: "undocking"
34 | )
35 | scan_or_be_cancelled = py_trees.composites.Selector(name="Scan or Be Cancelled", memory=False)
36 | cancelling = py_trees.composites.Sequence(name="Cancelling?", memory=True)
37 | cancelling.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
38 | move_out_and_scan = py_trees.composites.Sequence(name="Move Out and Scan", memory=True)
39 | move_out_and_scan.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
40 | celebrate = py_trees.composites.Parallel(
41 | name="Celebrate",
42 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
43 | )
44 | celebrate.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
45 | dock = py_trees_ros.actions.ActionClient(
46 | name="Dock",
47 | action_type=py_trees_actions.Dock,
48 | action_name="dock",
49 | action_goal=py_trees_actions.Dock.Goal(dock=True),
50 | generate_feedback_message=lambda msg: "docking"
51 | )
52 |
53 | scan_or_die.add_children([ere_we_go, die])
54 | die.add_children([failed_notification, result_failed_to_bb])
55 | failed_notification.add_children([failed_flash_green, failed_pause])
56 | ere_we_go.add_children([undock, scan_or_be_cancelled, dock, celebrate])
57 | scan_or_be_cancelled.add_children([cancelling, move_out_and_scan])
58 |
59 | py_trees.display.render_dot_tree(
60 | scan_or_die,
61 | py_trees.common.string_to_visibility_level("detail")
62 | )
63 |
--------------------------------------------------------------------------------
/doc/examples/seven_result.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros
6 | import py_trees.console as console
7 | import py_trees_ros_interfaces.action as py_trees_actions # noqa
8 | import py_trees_ros_tutorials
9 |
10 | if __name__ == '__main__':
11 |
12 | # Worker Tasks
13 | scan = py_trees.composites.Sequence(name="Scan", memory=True)
14 | is_scan_requested = py_trees.behaviours.CheckBlackboardVariableValue(
15 | name="Scan?",
16 | variable_name='event_scan_button',
17 | expected_value=True
18 | )
19 | scan_or_die = py_trees.composites.Selector(name="Scan or Die", memory=False)
20 | die = py_trees.composites.Sequence(name="Die", memory=True)
21 | failed_notification = py_trees.composites.Parallel(
22 | name="Notification",
23 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
24 | )
25 | failed_notification.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
26 | result_failed_to_bb = py_trees.behaviours.SetBlackboardVariable(
27 | name="Result2BB\n'failed'",
28 | variable_name='scan_result',
29 | variable_value='failed'
30 | )
31 | ere_we_go = py_trees.composites.Sequence(name="Ere we Go", memory=True)
32 | undock = py_trees_ros.actions.ActionClient(
33 | name="UnDock",
34 | action_type=py_trees_actions.Dock,
35 | action_name="dock",
36 | action_goal=py_trees_actions.Dock.Goal(dock=False),
37 | generate_feedback_message=lambda msg: "undocking"
38 | )
39 | scan_or_be_cancelled = py_trees.composites.Selector(name="Scan or Be Cancelled", memory=False)
40 | cancelling = py_trees.composites.Sequence(name="Cancelling?", memory=True)
41 | is_cancel_requested = py_trees.behaviours.CheckBlackboardVariableValue(
42 | name="Cancel?",
43 | variable_name='event_cancel_button',
44 | expected_value=True
45 | )
46 | move_home_after_cancel = py_trees_ros.actions.ActionClient(
47 | name="Move Home",
48 | action_type=py_trees_actions.MoveBase,
49 | action_name="move_base",
50 | action_goal=py_trees_actions.MoveBase.Goal(),
51 | generate_feedback_message=lambda msg: "moving home"
52 | )
53 | result_cancelled_to_bb = py_trees.behaviours.SetBlackboardVariable(
54 | name="Result2BB\n'cancelled'",
55 | variable_name='scan_result',
56 | variable_value='cancelled'
57 | )
58 | move_out_and_scan = py_trees.composites.Sequence(name="Move Out and Scan", , memory=True)
59 | move_base = py_trees_ros.actions.ActionClient(
60 | name="Move Out",
61 | action_type=py_trees_actions.MoveBase,
62 | action_name="move_base",
63 | action_goal=py_trees_actions.MoveBase.Goal(),
64 | generate_feedback_message=lambda msg: "moving out"
65 | )
66 | scanning = py_trees.composites.Parallel(
67 | name="Scanning",
68 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
69 | )
70 | scanning.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
71 | move_home_after_scan = py_trees_ros.actions.ActionClient(
72 | name="Move Home",
73 | action_type=py_trees_actions.MoveBase,
74 | action_name="move_base",
75 | action_goal=py_trees_actions.MoveBase.Goal(),
76 | generate_feedback_message=lambda msg: "moving home"
77 | )
78 | result_succeeded_to_bb = py_trees.behaviours.SetBlackboardVariable(
79 | name="Result2BB\n'succeeded'",
80 | variable_name='scan_result',
81 | variable_value='succeeded'
82 | )
83 | celebrate = py_trees.composites.Parallel(
84 | name="Celebrate",
85 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
86 | )
87 | celebrate.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
88 | dock = py_trees_ros.actions.ActionClient(
89 | name="Dock",
90 | action_type=py_trees_actions.Dock,
91 | action_name="dock",
92 | action_goal=py_trees_actions.Dock.Goal(dock=True),
93 | generate_feedback_message=lambda msg: "docking"
94 | )
95 |
96 | class SendResult(py_trees.behaviour.Behaviour):
97 |
98 | def __init__(self, name: str):
99 | super().__init__(name="Send Result")
100 | self.blackboard.register_key("scan_result", read=True)
101 |
102 | def update(self):
103 | print(console.green +
104 | "********** Result: {} **********".format(self.blackboard.scan_result) +
105 | console.reset
106 | )
107 | return py_trees.common.Status.SUCCESS
108 |
109 | send_result = SendResult(name="Send Result")
110 |
111 | # Fallback task
112 |
113 | scan.add_children([is_scan_requested, scan_or_die, send_result])
114 | scan_or_die.add_children([ere_we_go, die])
115 | die.add_children([failed_notification, result_failed_to_bb])
116 | ere_we_go.add_children([undock, scan_or_be_cancelled, dock, celebrate])
117 | scan_or_be_cancelled.add_children([cancelling, move_out_and_scan])
118 | cancelling.add_children([is_cancel_requested, move_home_after_cancel, result_cancelled_to_bb])
119 | move_out_and_scan.add_children([move_base, scanning, move_home_after_scan, result_succeeded_to_bb])
120 |
121 | py_trees.display.render_dot_tree(
122 | scan,
123 | py_trees.common.string_to_visibility_level("detail")
124 | )
125 |
--------------------------------------------------------------------------------
/doc/examples/seven_succeeding.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros
6 | import py_trees_ros_interfaces.action as py_trees_actions
7 | import py_trees_ros_tutorials
8 |
9 | if __name__ == '__main__':
10 |
11 | ere_we_go = py_trees.composites.Sequence(name="Ere we Go", memory=True)
12 | undock = py_trees_ros.actions.ActionClient(
13 | name="UnDock",
14 | action_type=py_trees_actions.Dock,
15 | action_name="dock",
16 | action_goal=py_trees_actions.Dock.Goal(dock=False),
17 | generate_feedback_message=lambda msg: "undocking"
18 | )
19 | scan_or_be_cancelled = py_trees.composites.Selector(name="Scan or Be Cancelled", memory=False)
20 | cancelling = py_trees.composites.Sequence(name="Cancelling?", memory=True)
21 | cancelling.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
22 | move_out_and_scan = py_trees.composites.Sequence(name="Move Out and Scan", memory=True)
23 | move_base = py_trees_ros.actions.ActionClient(
24 | name="Move Out",
25 | action_type=py_trees_actions.MoveBase,
26 | action_name="move_base",
27 | action_goal=py_trees_actions.MoveBase.Goal(),
28 | generate_feedback_message=lambda msg: "moving out"
29 | )
30 | scanning = py_trees.composites.Parallel(
31 | name="Scanning",
32 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
33 | )
34 | scan_context_switch = py_trees_ros_tutorials.behaviours.ScanContext("Context Switch")
35 | scan_rotate = py_trees_ros.actions.ActionClient(
36 | name="Rotate",
37 | action_type=py_trees_actions.Rotate,
38 | action_name="rotate",
39 | action_goal=py_trees_actions.Rotate.Goal(),
40 | generate_feedback_message=lambda msg: "{:.2f}%%".format(msg.percentage_completed)
41 | )
42 | scan_flash_blue = py_trees_ros_tutorials.behaviours.FlashLedStrip(name="Flash Blue", colour="blue")
43 | move_home_after_scan = py_trees_ros.actions.ActionClient(
44 | name="Move Home",
45 | action_type=py_trees_actions.MoveBase,
46 | action_name="move_base",
47 | action_goal=py_trees_actions.MoveBase.Goal(),
48 | generate_feedback_message=lambda msg: "moving home"
49 | )
50 | result_succeeded_to_bb = py_trees.blackboard.SetBlackboardVariable(
51 | name="Result2BB\n'succeeded'",
52 | variable_name='scan_result',
53 | variable_value='succeeded'
54 | )
55 | celebrate = py_trees.composites.Parallel(
56 | name="Celebrate",
57 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
58 | )
59 | celebrate.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
60 | dock = py_trees_ros.actions.ActionClient(
61 | name="Dock",
62 | action_type=py_trees_actions.Dock,
63 | action_name="dock",
64 | action_goal=py_trees_actions.Dock.Goal(dock=True),
65 | generate_feedback_message=lambda msg: "docking"
66 | )
67 |
68 | ere_we_go.add_children([undock, scan_or_be_cancelled, dock, celebrate])
69 | scan_or_be_cancelled.add_children([cancelling, move_out_and_scan])
70 | move_out_and_scan.add_children([move_base, scanning, move_home_after_scan, result_succeeded_to_bb])
71 | scanning.add_children([scan_context_switch, scan_rotate, scan_flash_blue])
72 |
73 | py_trees.display.render_dot_tree(
74 | ere_we_go,
75 | py_trees.common.string_to_visibility_level("detail")
76 | )
77 |
--------------------------------------------------------------------------------
/doc/examples/six_context_switch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros
6 | import py_trees_ros_interfaces.action as py_trees_actions
7 | import py_trees_ros_tutorials
8 |
9 | if __name__ == '__main__':
10 |
11 | scanning = py_trees.composites.Parallel(
12 | name="Scanning",
13 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
14 | )
15 | scan_context_switch = py_trees_ros_tutorials.behaviours.ScanContext("Context Switch")
16 | scan_rotate = py_trees_ros.actions.ActionClient(
17 | name="Rotate",
18 | action_type=py_trees_actions.Rotate,
19 | action_name="rotate",
20 | action_goal=py_trees_actions.Rotate.Goal(),
21 | generate_feedback_message=lambda msg: "{:.2f}%%".format(msg.percentage_completed)
22 | )
23 | flash_blue = py_trees_ros_tutorials.behaviours.FlashLedStrip(
24 | name="Flash Blue",
25 | colour="blue"
26 | )
27 |
28 | scanning.add_children([scan_context_switch, scan_rotate, flash_blue])
29 | py_trees.display.render_dot_tree(
30 | scanning,
31 | py_trees.common.string_to_visibility_level("all")
32 | )
33 |
--------------------------------------------------------------------------------
/doc/faq.rst:
--------------------------------------------------------------------------------
1 | .. _faq-section-label:
2 |
3 | FAQ
4 | ===
5 |
6 | ROS related frequently asked questions.
7 |
8 | .. seealso:: The :ref:`py_trees:faq-section-label` from the py_trees package.
9 |
10 | Parameter/Remap Proliferation
11 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 |
13 | You can imagine once you have 50+ re-usable behaviours in a tree
14 | that the need for remapping of topics, services and parameters in the behaviour tree
15 | launch description will become exceedingly large. In these situations it is more convenient
16 | to load parameters for these remappings in a structured way on the parameter server
17 | (loaded from a single yaml). This centralises your application configuration and additionally
18 | exposes that configuration at runtime which will assist with debugging. Sanity...
19 |
20 | On the parameter server, such configuration might look like:
21 |
22 | .. code-block:: python
23 |
24 | /tree/topics/odom /gopher/odom
25 | /tree/topics/pose /gopher/pose
26 | /tree/services/get_global_costmap /move_base/global/get_costmap
27 | /tree/parameters/max_speed /trajectory_controller/max_speed
28 |
29 | In code, highlighting re-usability of the remappings across multiple behaviours:
30 |
31 | .. code-block:: python
32 |
33 | odometry_topic=self.node.get_parameter_or(name="~topics/odom", alternative_value="/odom")
34 | pose_topic=self.node.get_parameter(name="~topics/pose", alternative_value="/pose")
35 | move_base = my_behaviours.MoveBaseClient(odometry_topic, pose_topic)
36 | odometry_foo = my_behvaiours.OdometryFoo(odometry_topic)
37 |
38 |
39 | Continuous Tick-Tock?
40 | ^^^^^^^^^^^^^^^^^^^^^
41 |
42 | Even though the behaviour tree provides a continuous tick-tock method,
43 | you can set your own pace. This can be useful if you wish to vary the tick
44 | duration, or to tick only when an external trigger is received (a common
45 | trick to minimise cpu usage in games). For example:
46 |
47 | .. code-block:: python
48 |
49 | ...
50 | while rclpy.ok():
51 | rclpy.spin_once(timeout_sec=0.1)
52 | if some_external_trigger:
53 | tree.tick_once()
54 |
55 | Triggering based on logic inside the tree however, is much more challenging
56 | as this is almost a chicken and egg situation (tick only when an event
57 | fires, but events are typically embedded in the decision making tree itself).
58 | UE4 has an implementation that has crafted mechanisms for this which go beyond
59 | basic behaviour tree concepts - if you have such a need, it's likely you'll
60 | have to extend py_trees to meet the needs of your own use case.
61 |
62 | Control-Level Decision Making
63 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
64 |
65 | Our first use case never intended to utilise behaviour trees for decision making
66 | typically considered internal to control subsystems. A good example of such is the
67 | approach logic for a docking maneuvre. Another is the recovery behaviours for
68 | navigation, which start to access sound/light notifications, specialised sensing
69 | contexts as well as specialised maneuvres. Note that neither of these require
70 | low-latency for their decision logic. Nonetheless, it was surprising
71 | to find the control engineers moving the logic from internal state machines to
72 | the behaviour trees at a higher level.
73 |
74 | In hindsight, this makes good sense. With the robot's decision making logic
75 | landing in one place, logging, debugging and visualising the state of the robot
76 | became simpler and could make use of a single set of tools. A growing library
77 | of shared and reusable patterns sped up the development cycle.
78 | It also liberated subsystems from having to co-ordinate other subsystems (e.g. the
79 | navigation system when engaging in recovery behaviours).
80 |
--------------------------------------------------------------------------------
/doc/images/tutorial-eight-dynamic-application-loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-eight-dynamic-application-loading.png
--------------------------------------------------------------------------------
/doc/images/tutorial-five-action-clients.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-five-action-clients.png
--------------------------------------------------------------------------------
/doc/images/tutorial-four-introspect-the-tree.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-four-introspect-the-tree.gif
--------------------------------------------------------------------------------
/doc/images/tutorial-four-py-trees-ros-viewer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-four-py-trees-ros-viewer.png
--------------------------------------------------------------------------------
/doc/images/tutorial-one-data-gathering.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-one-data-gathering.gif
--------------------------------------------------------------------------------
/doc/images/tutorial-seven-docking-cancelling-failing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-seven-docking-cancelling-failing.png
--------------------------------------------------------------------------------
/doc/images/tutorial-six-context-switching.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-six-context-switching.png
--------------------------------------------------------------------------------
/doc/images/tutorial-three-introspect-the-blackboard.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-three-introspect-the-blackboard.gif
--------------------------------------------------------------------------------
/doc/images/tutorial-two-battery-check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/doc/images/tutorial-two-battery-check.png
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. py_trees documentation master file, created by
2 | sphinx-quickstart on Thu Jul 30 16:43:58 2015.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | PyTrees ROS Tutorials
7 | =====================
8 |
9 | This package is home to tutorials that incrementally walk through the
10 | development of a behaviour tree application tested against a mocked robot
11 | control layer using the **py_trees** and **py_trees_ros** packages.
12 |
13 | .. seealso::
14 |
15 | * `py_trees@github`_
16 | * :ref:`py_trees@read-the-docs `
17 | * `py_trees_ros@github`_
18 | * :ref:`py_trees_ros@read-the-docs `
19 |
20 | .. toctree::
21 | :maxdepth: 2
22 | :caption: Guide
23 |
24 | tutorials
25 | faq
26 | terminology
27 |
28 | .. toctree::
29 | :maxdepth: 1
30 | :caption: Reference
31 |
32 | modules
33 | changelog
34 |
35 | Indices and tables
36 | ==================
37 |
38 | * :ref:`genindex`
39 | * :ref:`modindex`
40 | * :ref:`search`
41 |
42 |
43 | .. _py_trees@github: https://github.com/splintered-reality/py_trees
44 | .. _py_trees_ros@github: https://github.com/splintered-reality/py_trees_ros
45 |
--------------------------------------------------------------------------------
/doc/modules.rst:
--------------------------------------------------------------------------------
1 | .. _modules-section-label:
2 |
3 | Module API
4 | ==========
5 |
6 | py_trees_ros_tutorials
7 | ----------------------
8 |
9 | .. automodule:: py_trees_ros_tutorials
10 | :synopsis: tutorials for py_trees in ros
11 |
12 | py_trees_ros_tutorials.behaviours
13 | ---------------------------------
14 |
15 | .. automodule:: py_trees_ros_tutorials.behaviours
16 | :members:
17 | :show-inheritance:
18 | :synopsis: behaviours for the tutorials
19 |
20 | py_trees_ros_tutorials.mock
21 | ---------------------------
22 |
23 | .. automodule:: py_trees_ros_tutorials.mock
24 | :synopsis: utilities and components for mocking a robot
25 |
26 | py_trees_ros_tutorials.mock.actions
27 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | .. automodule:: py_trees_ros_tutorials.mock.actions
29 | :members:
30 | :show-inheritance:
31 | :synopsis: reusable action clients for testing mock components
32 |
33 | py_trees_ros_tutorials.mock.battery
34 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 |
36 | .. automodule:: py_trees_ros_tutorials.mock.battery
37 | :members:
38 | :show-inheritance:
39 | :synopsis: mock the state of a battery component
40 |
41 | py_trees_ros_tutorials.mock.dock
42 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43 |
44 | .. automodule:: py_trees_ros_tutorials.mock.dock
45 | :members:
46 | :show-inheritance:
47 | :synopsis: mock a docking controller
48 |
49 | py_trees_ros_tutorials.mock.launch
50 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
51 |
52 | .. automodule:: py_trees_ros_tutorials.mock.launch
53 | :members:
54 | :show-inheritance:
55 | :synopsis: a python launcher for all mock robot processes
56 |
57 | py_trees_ros_tutorials.mock.led_strip
58 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 |
60 | .. automodule:: py_trees_ros_tutorials.mock.led_strip
61 | :members:
62 | :show-inheritance:
63 | :synopsis: mock a led strip notification server
64 |
65 | py_trees_ros_tutorials.mock.move_base
66 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
67 |
68 | .. automodule:: py_trees_ros_tutorials.mock.move_base
69 | :members:
70 | :show-inheritance:
71 | :synopsis: mock the ROS navistack move base
72 |
73 | py_trees_ros_tutorials.mock.rotate
74 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
75 |
76 | .. automodule:: py_trees_ros_tutorials.mock.rotate
77 | :members:
78 | :show-inheritance:
79 | :synopsis: mock a very simple rotation action server
80 |
81 | py_trees_ros_tutorials.mock.safety_sensors
82 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83 |
84 | .. automodule:: py_trees_ros_tutorials.mock.safety_sensors
85 | :members:
86 | :show-inheritance:
87 | :synopsis: mock a safety sensor pipeline, requires context switching
88 |
89 | py_trees_ros_tutorials.version
90 | ------------------------------
91 |
92 | .. automodule:: py_trees_ros_tutorials.version
93 | :members:
94 | :show-inheritance:
95 | :synopsis: package version number for users of the package
96 |
97 |
--------------------------------------------------------------------------------
/doc/requirements.txt:
--------------------------------------------------------------------------------
1 | ##############################################################################
2 | #
3 | # Requirements for sphinx documentation environment. Most dependencies
4 | # are mocked.
5 | #
6 | # This file is discovered by the root level
7 | # .readthedocs.yaml (rtd build) and docenv.bash (local build)
8 | ##############################################################################
9 |
10 | Sphinx
11 | sphinx-argparse
12 | sphinx_rtd_theme
13 | sphinx-autodoc-typehints
14 | py_trees>=2
15 |
--------------------------------------------------------------------------------
/doc/terminology.rst:
--------------------------------------------------------------------------------
1 | Terminology
2 | ===========
3 |
4 | .. glossary::
5 |
6 | block
7 | blocking
8 | A behaviour is sometimes referred to as a 'blocking' behaviour. Technically, the execution
9 | of a behaviour should be non-blocking (i.e. the tick part), however when it's progress from
10 | 'RUNNING' to 'FAILURE/SUCCESS' takes more than one tick, we say that the behaviour itself
11 | is blocking. In short, `blocking == RUNNING`.
12 |
13 | context switch
14 | Very often a task or sequence of tasks will require a context switch of
15 | the runtime system. For example, enabling additional sensing and
16 | processing pipelines in order to navigate a staircase. The context switch is
17 | typically the modification of a few dynamic parameters and/or service calls
18 | to configure runtime nodes to behave in a different manner. The key
19 | requirements for a context switch is to cache the original context,
20 | hold the configuration throughout the context and then reset the context to
21 | the cached state upon completion. This falls naturally into a behaviour's
22 | :meth:`~py_trees.behaviour.Behaviour.initialise()`,
23 | :meth:`~py_trees.behaviour.Behaviour.update()` and
24 | :meth:`~py_trees.behaviour.Behaviour.terminate()` modalities. To ensure
25 | it activates at the appropriate time, drop it into a parallel alongside
26 | the activity that requires the context switch.
27 |
28 | .. seealso:: :ref:`tutorial-six`
29 |
30 | data gathering
31 | Caching events, notifications, or incoming data arriving asynchronously on the blackboard.
32 | This is a fairly common practice for behaviour trees which exist inside a complex system.
33 | In the ROS world, it is most likely you will catch data coming in on subscribers in this way.
34 |
35 | In most cases, data gathering is done at the front end of your tree under a parallel
36 | directly alongside your priority work selector.
37 |
38 | .. seealso:: :ref:`tutorial-one`
39 |
40 | mock
41 | mocking
42 | A very useful paradigm to accelerate development and testing of your behaviour trees
43 | is to mock your robot with very simple stubs that provide the same ROS API as the real
44 | robot. The actual behaviour underneath that ROS API need only be very roughly connected
45 | to the real thing.
46 |
47 | .. note:: The key here is to test the decision making in your behaviour tree.
48 |
49 | In most cases this has very little to do with the kinematics, dynamics or sensor
50 | fidelity of a full simulation.
51 |
52 | Mocking the bits and pieces takes far less time and you'll also be able to
53 | insert handles that can help you force decision making to
54 | branch to where you want to test. For example, using dynamic reconfigure in
55 | :class:`py_trees_ros.mock.battery.Battery` to abruptly force charging/discharging and at
56 | varying rates. Additionally, if you set up the mock well, you'll find it executes far faster
57 | than a full simulation (you can montage - no need to endure travel time).
58 |
59 | t will also make your web team happier (for apps that sit astride the behaiviour tree).
60 | These apps typically require thorough testing of decision making branches that are not
61 | often traversed e.g. battery low recovery handling, or cancelling procedures.
62 | This is far easier to do in a mock. They'll also appreciate not having to setup the
63 | entire infrastructure necessary for a dynamic simulation.
64 |
--------------------------------------------------------------------------------
/doc/tutorials.rst:
--------------------------------------------------------------------------------
1 | .. _tutorials-section:
2 |
3 | Tutorials
4 | =========
5 |
6 | Before We Start
7 | ---------------
8 |
9 | So, you would like your robot to actually do something non-trivial?
10 |
11 | **Trivial?** Ah, a sequence of timed actions - move forward 3s,
12 | rotate 90 degrees, move forward 3s, emit a greeting. This is open-loop
13 | and can be pre-programmed in a single script easily.
14 | Trivial. Shift gears!
15 |
16 | **Non-Trivial?** Hmm, you'd like to dynamically plan navigational
17 | routes (waypoints), choose between actions depending on whether
18 | blocking obstacles are sensed, interrupt the current action
19 | if the battery is low ... and this is just getting started.
20 | In short, *decision making* with *priority interrupts* and
21 | *closed loops* with peripheral systems (e.g. via sensing,
22 | HMI devices, web services). Now you're talking!
23 |
24 | Most roboticists will start scripting, but
25 | quickly run into a complexity barrier. They'll often then reach for
26 | state machines which are great for control systems, but run into
27 | yet another complexity barrier attempting to handle priority interrupts
28 | and an exponentially increasing profusion of wires between states. Which
29 | brings you here, to behavour trees! Before we proceed though...
30 |
31 | **Where is the Robot?** Ostensibly you'll need one, at some point.
32 | More often than not though, it's not available or it's just
33 | not practical for rapid application development.
34 | Might be it's only partially assembled, or new
35 | features are being developed in parallel (deadlines!). On the other hand,
36 | it may be available, but you cannot get enough time-share on the robot or
37 | it is not yet stable, resulting in a stream of unrelated issues lower down
38 | in the robotic stack that impede application development. So you make
39 | the sensible decision of moving to simulation.
40 |
41 | **Simulation or Mocked Robots?** If you already have a robot simulation,
42 | it's a great place to start. In the long run though, the investment
43 | of time to build a mock robot layer should, in most cases, pay itself off
44 | with a faster development cycle. Why? Testing an application is mostly
45 | about provoking and testing the many permutations and combinations o
46 | decision making. It's not about the 20 minutes of travel from point A to
47 | point B in the building. With a mocked robot layer, you can emulate
48 | that travel at ludicrous speed and provide easy handles for mocking the
49 | problems that can arise.
50 |
51 | So this is where the tutorials begin, with a very simple, mocked robot. They will
52 | then proceed to build up a behaviour tree application, one step at a time.
53 |
54 | The Mock Robot
55 | --------------
56 |
57 | The tutorials here all run atop a very simple :term:`mock` robot that
58 | encapsulates the following list of mocked components:
59 |
60 | * Battery
61 | * LED Strip
62 | * Docking Action Server
63 | * Move Base Action Server
64 | * Rotation Action Server
65 | * Safety Sensors Pipeline
66 |
67 | .. note::
68 |
69 | It should always be possible for the :term:`mock` robot to be replaced
70 | by a gazebo simulated robot or the actual robot. Each
71 | of these underlying systems must implement exactly the same
72 | ROS API interface.
73 |
74 | The tutorials take care of launching the mock robot, but it can be also
75 | launched on its own with:
76 |
77 | .. code-block:: bash
78 |
79 | $ ros2 launch py_trees_ros_tutorials mock_robot_launch.py
80 |
81 | .. _tutorial-one:
82 |
83 | Tutorial 1 - Data Gathering
84 | ---------------------------
85 |
86 | .. automodule:: py_trees_ros_tutorials.one_data_gathering
87 | :synopsis: data gathering with the battery to blackboard behaviour
88 |
89 | .. _tutorial-two:
90 |
91 | Tutorial 2 - Battery Check
92 | --------------------------
93 |
94 | .. automodule:: py_trees_ros_tutorials.two_battery_check
95 | :synopsis: adding a low battery check, with LED notification to the tree
96 |
97 | .. _tutorial-three:
98 |
99 | Tutorial 3 - Introspect the Blackboard
100 | --------------------------------------
101 |
102 | About
103 | ^^^^^
104 |
105 | Tutorial three is a repeat of :ref:`tutorial-two`. The purpose of this
106 | tutorial however is to introduce the tools provided to
107 | allow introspection of the blackboard from ROS. Publishers and services
108 | are provided by :class:`py_trees_ros.blackboard.Exchange`
109 | which is embedded in a :class:`py_trees_ros.trees.BehaviourTree`. Interaction
110 | with the exchange is over a set of services and dynamically created topics
111 | via the the :ref:`py-trees-blackboard-watcher` command line utility.
112 |
113 | Running
114 | ^^^^^^^
115 |
116 | .. code-block:: bash
117 |
118 | $ ros2 launch py_trees_ros_tutorials tutorial_three_introspect_the_blackboard_launch.py
119 |
120 | In another shell:
121 |
122 | .. code-block:: bash
123 |
124 | # watch the entire board
125 | $ py-trees-blackboard-watcher
126 | # watch with the recent activity log (activity stream)
127 | $ py-trees-blackboard-watcher --activity
128 | # watch variables associated with behaviours on the most recent tick's visited path
129 | $ py-trees-blackboard-watcher --visited
130 | # list variables available to watch
131 | $ py-trees-blackboard-watcher --list
132 | # watch a simple variable (slide the battery level on the dashboard to trigger a change)
133 | $ py-trees-blackboard-watcher /battery_low_warning
134 | # watch a variable with nested attributes
135 | $ py-trees-blackboard-watcher /battery.percentage
136 |
137 | .. image:: images/tutorial-three-introspect-the-blackboard.gif
138 |
139 | .. _tutorial-four:
140 |
141 | Tutorial 4 - Introspecting the Tree
142 | -----------------------------------
143 |
144 | About
145 | ^^^^^
146 |
147 | Again, this is a repeat of :ref:`tutorial-two`. In addition to services and
148 | topics for the blackboard, the
149 | :class:`py_trees_ros.trees.BehaviourTree` class provides services and topics
150 | for introspection of the tree state itself as well as a command line utility,
151 | :ref:`py-trees-tree-watcher`, to interact with these services and topics.
152 |
153 | .. note:
154 |
155 | The tree watcher by default requests a hidden stream to be configured and
156 | opened for it's private use. On request, you can redirect the watcher to
157 | an already open stream (for example, the default stream which would
158 | typically be used for logging purposes).
159 |
160 | .. note:
161 |
162 | The tip of the tree, i.e. the behaviour which redirects the decision
163 | making flow of the tree back to the root, is highlighted in bold.
164 |
165 | Running
166 | ^^^^^^^
167 |
168 | Launch the tutorial:
169 |
170 | .. code-block:: bash
171 |
172 | $ ros2 launch py_trees_ros_tutorials tutorial_four_introspect_the_tree_launch.py
173 |
174 | Using ``py-trees-tree-watcher`` on a private snapshot stream:
175 |
176 | .. code-block:: bash
177 |
178 | # stream the tree state on changes
179 | $ py-trees-tree-watcher
180 | # stream the tree state on changes with statistics
181 | $ py-trees-tree-watcher -s
182 | # stream the tree state on changes with most recent blackboard activity
183 | $ py-trees-tree-watcher -a
184 | # stream the tree state on changes with visited blackboard variables
185 | $ py-trees-tree-watcher -b
186 | # serialise to a dot graph (.dot/.png/.svg) and view in xdot if available
187 | $ py-trees-tree-watcher --dot-graph
188 | # not necessary here, but if there are multiple trees to choose from
189 | $ py-trees-tree-watcher --namespace=/tree/snapshot_streams
190 |
191 | .. image:: images/tutorial-four-introspect-the-tree.gif
192 |
193 | Using ``py-trees-tree-watcher`` on the default snapshot stream (``~/snapshots``):
194 |
195 | .. code-block:: bash
196 |
197 | # enable the default snapshot stream
198 | $ ros2 param set /tree default_snapshot_stream True
199 | $ ros2 param set /tree default_snapshot_blackboard_data True
200 | $ ros2 param set /tree default_snapshot_blackboard_activity True
201 | # connect to the stream
202 | $ py-trees-tree-watcher -a -s -b /tree/snapshots
203 |
204 | Using `py_trees_ros_viewer`_ to configure and visualise the stream:
205 |
206 | .. code-block:: bash
207 |
208 | # install
209 | $ sudo apt install ros--py-trees-ros-viewer
210 | # start the viewer
211 | $ py-trees-tree-viewer
212 |
213 | .. image:: images/tutorial-four-py-trees-ros-viewer.png
214 |
215 | .. _py_trees_ros_viewer: https://github.com/splintered-reality/py_trees_ros_viewer
216 |
217 | .. _tutorial-five:
218 |
219 | Tutorial 5 - Action Clients
220 | ---------------------------
221 |
222 | .. automodule:: py_trees_ros_tutorials.five_action_clients
223 | :synopsis: prioritised work, action_clients and preemptions
224 |
225 | .. _tutorial-six:
226 |
227 | Tutorial 6 - Context Switching
228 | ------------------------------
229 |
230 | .. automodule:: py_trees_ros_tutorials.six_context_switching
231 | :synopsis: switching the context while scanning
232 |
233 | .. _tutorial-seven:
234 |
235 | Tutorial 7 - Docking, Cancelling, Failing
236 | -----------------------------------------
237 |
238 | .. automodule:: py_trees_ros_tutorials.seven_docking_cancelling_failing
239 | :synopsis: docking, cancelling and failing
240 |
241 | Tutorial 8 - Dynamic Application Loading
242 | ----------------------------------------
243 |
244 | .. automodule:: py_trees_ros_tutorials.eight_dynamic_application_loading
245 | :synopsis: dynamically inserting/pruning application subtrees
246 |
247 | Tutorial 9 - Bagging Trees
248 | --------------------------
249 |
250 | Coming soon...
--------------------------------------------------------------------------------
/doc/venv.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Script for setting up the development environment.
4 | #source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
5 |
6 | NAME=py_trees_ros_tutorials
7 |
8 | ##############################################################################
9 | # Colours
10 | ##############################################################################
11 |
12 | BOLD="\e[1m"
13 |
14 | CYAN="\e[36m"
15 | GREEN="\e[32m"
16 | RED="\e[31m"
17 | YELLOW="\e[33m"
18 |
19 | RESET="\e[0m"
20 |
21 | padded_message ()
22 | {
23 | line="........................................"
24 | printf "%s %s${2}\n" ${1} "${line:${#1}}"
25 | }
26 |
27 | pretty_header ()
28 | {
29 | echo -e "${BOLD}${1}${RESET}"
30 | }
31 |
32 | pretty_print ()
33 | {
34 | echo -e "${GREEN}${1}${RESET}"
35 | }
36 |
37 | pretty_warning ()
38 | {
39 | echo -e "${YELLOW}${1}${RESET}"
40 | }
41 |
42 | pretty_error ()
43 | {
44 | echo -e "${RED}${1}${RESET}"
45 | }
46 |
47 | ##############################################################################
48 | # Methods
49 | ##############################################################################
50 |
51 | install_package ()
52 | {
53 | PACKAGE_NAME=$1
54 | dpkg -s ${PACKAGE_NAME} > /dev/null
55 | if [ $? -ne 0 ]; then
56 | sudo apt-get -q -y install ${PACKAGE_NAME} > /dev/null
57 | else
58 | pretty_print " $(padded_message ${PACKAGE_NAME} "found")"
59 | return 0
60 | fi
61 | if [ $? -ne 0 ]; then
62 | pretty_error " $(padded_message ${PACKAGE_NAME} "failed")"
63 | return 1
64 | fi
65 | pretty_warning " $(padded_message ${PACKAGE_NAME} "installed")"
66 | return 0
67 | }
68 |
69 | ##############################################################################
70 |
71 | install_package virtualenvwrapper || return
72 |
73 | # To use the installed python3
74 | VERSION="--python=/usr/bin/python3"
75 | # To use a specific version
76 | # VERSION="--python=python3.6"
77 |
78 | if [ "${VIRTUAL_ENV}" == "" ]; then
79 | workon ${NAME}
80 | result=$?
81 | if [ $result -eq 1 ]; then
82 | mkvirtualenv ${VERSION} ${NAME}
83 | fi
84 | if [ $result -eq 127 ]; then
85 | pretty_error "Failed to find virtualenvwrapper aliases: 1) re-log or 2) source virtualenvwrapper.sh in your shell's .rc"
86 | return 1
87 | fi
88 | fi
89 |
90 | # Get all dependencies for doc generation
91 | # pip install -e .[docs]
92 | pip install -r requirements.txt
93 |
94 | # NB: this automagically nabs install_requires
95 | python ../setup.py develop
96 |
97 | echo ""
98 | echo "Leave the virtual environment with 'deactivate'"
99 | echo ""
100 | echo "I'm grooty, you should be too."
101 | echo ""
102 |
103 |
--------------------------------------------------------------------------------
/launch/mock_robot_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | The mocked robot, for use with the tutorials.
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.mock.launch
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | A ros2 launch script for the mock robot
27 | """
28 | return py_trees_ros_tutorials.mock.launch.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/launch/tutorial_eight_dynamic_application_loading_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | Tutorial 8 - Dynamic Application Loading
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.eight_dynamic_application_loading as tutorial
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | Launch description for the tutorial.
27 | """
28 | return tutorial.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/launch/tutorial_five_action_clients_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | Tutorial 5 - Action Clients
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.five_action_clients as tutorial
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | Launch description for the tutorial.
27 | """
28 | return tutorial.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/launch/tutorial_four_introspect_the_tree_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | Tutorial 4 - Introspect the Tree
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.two_battery_check as tutorial
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | Launch description for the tutorial.
27 | """
28 | return tutorial.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/launch/tutorial_one_data_gathering_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | The mocked robot, for use with the tutorials.
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.one_data_gathering as tutorial
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | A ros2 launch script for the mock robot
27 | """
28 | return tutorial.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/launch/tutorial_seven_docking_cancelling_failing_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | Tutorial 7 - Docking, Cancelling & Failing
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.seven_docking_cancelling_failing as tutorial
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | Launch description for the tutorial.
27 | """
28 | return tutorial.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/launch/tutorial_six_context_switching_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | Tutorial 6 - Context Switching
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.six_context_switching as tutorial
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | Launch description for the tutorial.
27 | """
28 | return tutorial.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/launch/tutorial_three_introspect_the_blackboard_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | Tutorial 3 - Introspect the Blackboard
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.two_battery_check as tutorial
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | Launch description for the tutorial.
27 | """
28 | return tutorial.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/launch/tutorial_two_battery_check_launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 | """
11 | Tutorial 2 - Battery Check
12 | """
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees_ros_tutorials.two_battery_check as tutorial
18 |
19 | ##############################################################################
20 | # Launch Service
21 | ##############################################################################
22 |
23 |
24 | def generate_launch_description():
25 | """
26 | Launch description for the tutorial.
27 | """
28 | return tutorial.generate_launch_description()
29 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | py_trees_ros_tutorials
5 | 2.3.0
6 |
7 | Tutorials for py_trees on ROS2.
8 |
9 |
10 | Daniel Stonier
11 | Daniel Stonier
12 | Sebastian Castro
13 |
14 | BSD
15 |
16 | https://py-trees-ros-tutorials.readthedocs.io/en/release-2.0.x/
17 | https://github.com/splintered-reality/py_trees_ros_tutorials
18 | https://github.com/splintered-reality/py_trees_ros_tutorials/issues
19 |
20 | python3-setuptools
21 | pyqt5-dev-tools
22 | qttools5-dev-tools
23 |
24 | python3-pytest
25 |
26 |
29 |
30 |
31 | action_msgs
32 | geometry_msgs
33 | py_trees
34 | py_trees_ros
35 | py_trees_ros_interfaces
36 | python3-qt5-bindings
37 | rcl_interfaces
38 | rclpy
39 | sensor_msgs
40 | std_msgs
41 |
42 |
43 | py_trees
44 | py_trees_ros
45 | py_trees_ros_interfaces
46 | rcl_interfaces
47 | rclpy
48 | std_msgs
49 |
50 |
51 | launch
52 | launch_ros
53 | ros2launch
54 | ros2param
55 | ros2run
56 | ros2service
57 | ros2topic
58 |
59 |
60 | action_msgs
61 | py_trees
62 | py_trees_ros
63 | rclpy
64 |
65 |
66 | ament_python
67 |
68 |
69 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # License: BSD
3 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
4 | #
5 | ##############################################################################
6 | # Documentation
7 | ##############################################################################
8 |
9 | """
10 | A mock robot and tutorials for py_trees on ROS2.
11 | """
12 |
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | from . import behaviours
18 | from . import mock
19 |
20 | from . import one_data_gathering
21 | from . import two_battery_check
22 | from . import five_action_clients
23 | from . import six_context_switching
24 | from . import seven_docking_cancelling_failing
25 | from . import eight_dynamic_application_loading
26 |
27 | ##############################################################################
28 | # Version
29 | ##############################################################################
30 |
31 | from .version import __version__
32 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/behaviours.py:
--------------------------------------------------------------------------------
1 | #
2 | # License: BSD
3 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
4 | #
5 | ##############################################################################
6 | # Documentation
7 | ##############################################################################
8 |
9 | """
10 | Behaviours for the tutorials.
11 | """
12 |
13 | ##############################################################################
14 | # Imports
15 | ##############################################################################
16 |
17 | import py_trees
18 | import py_trees_ros
19 | import rcl_interfaces.msg as rcl_msgs
20 | import rcl_interfaces.srv as rcl_srvs
21 | import rclpy
22 | import std_msgs.msg as std_msgs
23 |
24 | ##############################################################################
25 | # Behaviours
26 | ##############################################################################
27 |
28 |
29 | class FlashLedStrip(py_trees.behaviour.Behaviour):
30 | """
31 | This behaviour simply shoots a command off to the LEDStrip to flash
32 | a certain colour and returns :attr:`~py_trees.common.Status.RUNNING`.
33 | Note that this behaviour will never return with
34 | :attr:`~py_trees.common.Status.SUCCESS` but will send a clearing
35 | command to the LEDStrip if it is cancelled or interrupted by a higher
36 | priority behaviour.
37 |
38 | Publishers:
39 | * **/led_strip/command** (:class:`std_msgs.msg.String`)
40 |
41 | * colourised string command for the led strip ['red', 'green', 'blue']
42 |
43 | Args:
44 | name: name of the behaviour
45 | topic_name : name of the battery state topic
46 | colour: colour to flash ['red', 'green', blue']
47 | """
48 | def __init__(
49 | self,
50 | name: str,
51 | topic_name: str="/led_strip/command",
52 | colour: str="red"
53 | ):
54 | super(FlashLedStrip, self).__init__(name=name)
55 | self.topic_name = topic_name
56 | self.colour = colour
57 |
58 | def setup(self, **kwargs):
59 | """
60 | Setup the publisher which will stream commands to the mock robot.
61 |
62 | Args:
63 | **kwargs (:obj:`dict`): look for the 'node' object being passed down from the tree
64 |
65 | Raises:
66 | :class:`KeyError`: if a ros2 node isn't passed under the key 'node' in kwargs
67 | """
68 | self.logger.debug("{}.setup()".format(self.qualified_name))
69 | try:
70 | self.node = kwargs['node']
71 | except KeyError as e:
72 | error_message = "didn't find 'node' in setup's kwargs [{}][{}]".format(self.qualified_name)
73 | raise KeyError(error_message) from e # 'direct cause' traceability
74 |
75 | self.publisher = self.node.create_publisher(
76 | msg_type=std_msgs.String,
77 | topic=self.topic_name,
78 | qos_profile=py_trees_ros.utilities.qos_profile_latched()
79 | )
80 | self.feedback_message = "publisher created"
81 |
82 | def update(self) -> py_trees.common.Status:
83 | """
84 | Annoy the led strip to keep firing every time it ticks over (the led strip will clear itself
85 | if no command is forthcoming within a certain period of time).
86 | This behaviour will only finish if it is terminated or priority interrupted from above.
87 |
88 | Returns:
89 | Always returns :attr:`~py_trees.common.Status.RUNNING`
90 | """
91 | self.logger.debug("%s.update()" % self.__class__.__name__)
92 | self.publisher.publish(std_msgs.String(data=self.colour))
93 | self.feedback_message = "flashing {0}".format(self.colour)
94 | return py_trees.common.Status.RUNNING
95 |
96 | def terminate(self, new_status: py_trees.common.Status):
97 | """
98 | Shoot off a clearing command to the led strip.
99 |
100 | Args:
101 | new_status: the behaviour is transitioning to this new status
102 | """
103 | self.logger.debug(
104 | "{}.terminate({})".format(
105 | self.qualified_name,
106 | "{}->{}".format(self.status, new_status) if self.status != new_status else "{}".format(new_status)
107 | )
108 | )
109 | self.publisher.publish(std_msgs.String(data=""))
110 | self.feedback_message = "cleared"
111 |
112 |
113 | class ScanContext(py_trees.behaviour.Behaviour):
114 | """
115 | Alludes to switching the context of the runtime system for a scanning
116 | action. Technically, it reaches out to the mock robots safety sensor
117 | dynamic parameter, switches it off in :meth:`initialise()` and maintains
118 | that for the the duration of the context before returning it to
119 | it's original value in :meth:`terminate()`.
120 |
121 | Args:
122 | name (:obj:`str`): name of the behaviour
123 | """
124 | def __init__(self, name):
125 | super().__init__(name=name)
126 |
127 | self.cached_context = None
128 |
129 | def setup(self, **kwargs):
130 | """
131 | Setup the ros2 communications infrastructure.
132 |
133 | Args:
134 | **kwargs (:obj:`dict`): look for the 'node' object being passed down from the tree
135 |
136 | Raises:
137 | :class:`KeyError`: if a ros2 node isn't passed under the key 'node' in kwargs
138 | """
139 | self.logger.debug("%s.setup()" % self.__class__.__name__)
140 |
141 | # ros2 node
142 | try:
143 | self.node = kwargs['node']
144 | except KeyError as e:
145 | error_message = "didn't find 'node' in setup's kwargs [{}][{}]".format(self.qualified_name)
146 | raise KeyError(error_message) from e # 'direct cause' traceability
147 |
148 | # parameter service clients
149 | self.parameter_clients = {
150 | 'get_safety_sensors': self.node.create_client(
151 | rcl_srvs.GetParameters,
152 | '/safety_sensors/get_parameters'
153 | ),
154 | 'set_safety_sensors': self.node.create_client(
155 | rcl_srvs.SetParameters,
156 | '/safety_sensors/set_parameters'
157 | )
158 | }
159 | for name, client in self.parameter_clients.items():
160 | if not client.wait_for_service(timeout_sec=3.0):
161 | raise RuntimeError("client timed out waiting for server [{}]".format(name))
162 |
163 | def initialise(self):
164 | """
165 | Reset the cached context and trigger the chain of get/set parameter
166 | calls involved in changing the context.
167 |
168 | .. note::
169 |
170 | Completing the chain of service calls here
171 | (with `rclpy.spin_until_future_complete(node, future)`)
172 | is not possible if this behaviour is encapsulated inside, e.g.
173 | a tree tick activated by a ros2 timer callback, since it is
174 | already part of a scheduled job in a spinning node. It will
175 | just deadlock.
176 |
177 | Prefer instead to chain a sequence of events that will be
178 | completed over a span of ticks instead of at best, blocking
179 | here and at worst, falling into deadlock.
180 |
181 | """
182 | self.logger.debug("%s.initialise()" % self.__class__.__name__)
183 | self.cached_context = None
184 | # kickstart get/set parameter chain
185 | self._send_get_parameter_request()
186 |
187 | def update(self) -> py_trees.common.Status:
188 | """
189 | Complete the chain of calls begun in :meth:`initialise()` and then
190 | maintain the context (i.e. :class:`py_trees.behaviour.Behaviour` and
191 | return :data:`~py_trees.common.Status.RUNNING`).
192 | """
193 | self.logger.debug("%s.update()" % self.__class__.__name__)
194 | all_done = False
195 |
196 | # wait for get_parameter to return
197 | if self.cached_context is None:
198 | if self._process_get_parameter_response():
199 | self._send_set_parameter_request(value=True)
200 | return py_trees.common.Status.RUNNING
201 |
202 | # wait for set parameter to return
203 | if not all_done:
204 | if self._process_set_parameter_response():
205 | all_done = True
206 | return py_trees.common.Status.RUNNING
207 |
208 | # just spin around, wait for an interrupt to trigger terminate
209 | return py_trees.common.Status.RUNNING
210 |
211 | def terminate(self, new_status: py_trees.common.Status):
212 | """
213 | Reset the parameters back to their original (cached) values.
214 |
215 | Args:
216 | new_status: the behaviour is transitioning to this new status
217 | """
218 | self.logger.debug("%s.terminate(%s)" % (self.__class__.__name__, "%s->%s" % (self.status, new_status) if self.status != new_status else "%s" % new_status))
219 | if (
220 | new_status == py_trees.common.Status.INVALID and
221 | self.cached_context is not None
222 | ):
223 | self._send_set_parameter_request(value=self.cached_context)
224 | # don't worry about the response, no chance to catch it anyway
225 |
226 | def _send_get_parameter_request(self):
227 | request = rcl_srvs.GetParameters.Request() # noqa
228 | request.names.append("enabled")
229 | self.get_parameter_future = self.parameter_clients['get_safety_sensors'].call_async(request)
230 |
231 | def _process_get_parameter_response(self) -> bool:
232 | if not self.get_parameter_future.done():
233 | return False
234 | if self.get_parameter_future.result() is None:
235 | self.feedback_message = "failed to retrieve the safety sensors context"
236 | self.node.get_logger().error(self.feedback_message)
237 | # self.node.get_logger().info('Service call failed %r' % (future.exception(),))
238 | raise RuntimeError(self.feedback_message)
239 | if len(self.get_parameter_future.result().values) > 1:
240 | self.feedback_message = "expected one parameter value, got multiple [{}]".format("/safety_sensors/enabled")
241 | raise RuntimeError(self.feedback_message)
242 | value = self.get_parameter_future.result().values[0]
243 | if value.type != rcl_msgs.ParameterType.PARAMETER_BOOL: # noqa
244 | self.feedback_message = "expected parameter type bool, got [{}]{}]".format(value.type, "/safety_sensors/enabled")
245 | self.node.get_logger().error(self.feedback_message)
246 | raise RuntimeError(self.feedback_message)
247 | self.cached_context = value.bool_value
248 | return True
249 |
250 | def _send_set_parameter_request(self, value: bool):
251 | request = rcl_srvs.SetParameters.Request() # noqa
252 | parameter = rcl_msgs.Parameter()
253 | parameter.name = "enabled"
254 | parameter.value.type = rcl_msgs.ParameterType.PARAMETER_BOOL # noqa
255 | parameter.value.bool_value = value
256 | request.parameters.append(parameter)
257 | self.set_parameter_future = self.parameter_clients['set_safety_sensors'].call_async(request)
258 |
259 | def _process_set_parameter_response(self) -> bool:
260 | if not self.get_parameter_future.done():
261 | return False
262 | if self.set_parameter_future.result() is not None:
263 | self.feedback_message = "reconfigured the safety sensors context"
264 | else:
265 | self.feedback_message = "failed to reconfigure the safety sensors context"
266 | self.node.get_logger().error(self.feedback_message)
267 | # self.node.get_logger().info('service call failed %r' % (future.exception(),))
268 | return True
269 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # License: BSD
3 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
4 | #
5 | ##############################################################################
6 | # Documentation
7 | ##############################################################################
8 |
9 | """
10 | A mocked robot for use in the tutorials.
11 | """
12 | ##############################################################################
13 | # Imports
14 | ##############################################################################
15 |
16 | from . import actions
17 | from . import battery
18 | from . import dock
19 | from . import move_base
20 | from . import rotate
21 | from . import launch
22 | from . import led_strip
23 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/battery.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Mock the state of a battery component.
13 | """
14 |
15 |
16 | ##############################################################################
17 | # Imports
18 | ##############################################################################
19 |
20 | import argparse
21 | import py_trees_ros
22 | import rclpy
23 | import rclpy.parameter
24 | import sensor_msgs.msg as sensor_msgs
25 | import sys
26 |
27 | ##############################################################################
28 | # Class
29 | ##############################################################################
30 |
31 |
32 | class Battery(object):
33 | """
34 | Mocks the processed battery state for a robot (/battery/sensor_state).
35 |
36 | Node Name:
37 | * **battery**
38 |
39 | Publishers:
40 | * **~state** (:class:`sensor_msgs.msg.BatteryState`)
41 |
42 | * full battery state information
43 |
44 | Dynamic Parameters:
45 | * **~charging_percentage** (:obj:`float`)
46 |
47 | * one-shot setter of the current battery percentage
48 | * **~charging** (:obj:`bool`)
49 |
50 | * charging or discharging
51 | * **~charging_increment** (:obj:`float`)
52 |
53 | * the current charging/discharging increment
54 |
55 | On startup it is in a DISCHARGING state and updates every 200ms.
56 | Use the ``dashboard`` to dynamically reconfigure parameters.
57 | """
58 | def __init__(self):
59 | # node
60 | self.node = rclpy.create_node(
61 | node_name="battery",
62 | parameter_overrides=[
63 | rclpy.parameter.Parameter('charging_percentage', rclpy.parameter.Parameter.Type.DOUBLE, 100.0),
64 | rclpy.parameter.Parameter('charging_increment', rclpy.parameter.Parameter.Type.DOUBLE, 0.1),
65 | rclpy.parameter.Parameter('charging', rclpy.parameter.Parameter.Type.BOOL, False),
66 | ],
67 | automatically_declare_parameters_from_overrides=True
68 | )
69 |
70 | # publishers
71 | not_latched = False # latched = True
72 | self.publishers = py_trees_ros.utilities.Publishers(
73 | self.node,
74 | [
75 | ('state', "~/state", sensor_msgs.BatteryState, not_latched),
76 | ]
77 | )
78 |
79 | # initialisations
80 | self.battery = sensor_msgs.BatteryState()
81 | self.battery.header.stamp = rclpy.clock.Clock().now().to_msg()
82 | self.battery.voltage = float('nan')
83 | self.battery.current = float('nan')
84 | self.battery.charge = float('nan')
85 | self.battery.capacity = float('nan')
86 | self.battery.design_capacity = float('nan')
87 | self.battery.percentage = 100.0
88 | self.battery.power_supply_health = sensor_msgs.BatteryState.POWER_SUPPLY_HEALTH_GOOD
89 | self.battery.power_supply_technology = sensor_msgs.BatteryState.POWER_SUPPLY_TECHNOLOGY_LION
90 | self.battery.power_supply_status = sensor_msgs.BatteryState.POWER_SUPPLY_STATUS_FULL
91 | self.battery.present = True
92 | self.battery.location = ""
93 | self.battery.serial_number = ""
94 |
95 | self.timer = self.node.create_timer(
96 | timer_period_sec=0.2,
97 | callback=self.update_and_publish
98 | )
99 |
100 | def update_and_publish(self):
101 | """
102 | Timer callback that processes the battery state update and publishes.
103 | """
104 | # parameters
105 | charging = self.node.get_parameter("charging").value
106 | charging_increment = self.node.get_parameter("charging_increment").value
107 | charging_percentage = self.node.get_parameter("charging_percentage").value
108 |
109 | # update state
110 | if charging:
111 | charging_percentage = min(100.0, charging_percentage + charging_increment)
112 | if charging_percentage % 5.0 < 0.1:
113 | self.node.get_logger().debug("Charging...{:.1f}%%".format(charging_percentage))
114 | else:
115 | charging_percentage = max(0.0, charging_percentage - charging_increment)
116 | if charging_percentage % 2.5 < 0.1:
117 | self.node.get_logger().debug("Discharging...{:.1f}%%".format(charging_percentage))
118 |
119 | # update parameters (TODO: need a guard?)
120 | self.node.set_parameters([
121 | rclpy.parameter.Parameter(
122 | 'charging_percentage',
123 | rclpy.parameter.Parameter.Type.DOUBLE,
124 | float(charging_percentage)
125 | )
126 | ])
127 |
128 | # publish
129 | self.battery.header.stamp = rclpy.clock.Clock().now().to_msg()
130 | charging_percentage = min(100.0, charging_percentage)
131 | self.battery.percentage = charging_percentage
132 | if charging_percentage == 100.0:
133 | self.battery.power_supply_status = sensor_msgs.BatteryState.POWER_SUPPLY_STATUS_FULL
134 | elif charging:
135 | self.battery.power_supply_status = sensor_msgs.BatteryState.POWER_SUPPLY_STATUS_CHARGING
136 | else:
137 | self.battery.power_supply_status = sensor_msgs.BatteryState.POWER_SUPPLY_STATUS_DISCHARGING
138 | self.publishers.state.publish(msg=self.battery)
139 |
140 | def shutdown(self):
141 | """
142 | Cleanup ROS components.
143 | """
144 | # currently complains with:
145 | # RuntimeWarning: Failed to fini publisher: rcl node implementation is invalid, at /tmp/binarydeb/ros-dashing-rcl-0.7.5/src/rcl/node.c:462
146 | # Q: should rlcpy.shutdown() automagically handle descruction of nodes implicitly?
147 | self.node.destroy_node()
148 |
149 |
150 | def main():
151 | """
152 | Entry point for the mock batttery node.
153 | """
154 | parser = argparse.ArgumentParser(description='Mock the state of a battery component')
155 | command_line_args = rclpy.utilities.remove_ros_args(args=sys.argv)[1:]
156 | parser.parse_args(command_line_args)
157 | rclpy.init() # picks up sys.argv automagically internally
158 | battery = Battery()
159 | try:
160 | rclpy.spin(battery.node)
161 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
162 | pass
163 | finally:
164 | battery.shutdown()
165 | rclpy.try_shutdown()
166 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/dock.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Mocks a docking controller
13 | """
14 |
15 |
16 | ##############################################################################
17 | # Imports
18 | ##############################################################################
19 |
20 | import argparse
21 | import py_trees_ros.mock.actions
22 | import py_trees_ros_interfaces.action as py_trees_actions
23 | import rclpy
24 | import sys
25 |
26 | ##############################################################################
27 | # Class
28 | ##############################################################################
29 |
30 |
31 | class Dock(py_trees_ros.mock.actions.GenericServer):
32 | """
33 | Simple action server that docks/undocks depending on the instructions
34 | in the goal requests.
35 |
36 | Node Name:
37 | * **docking_controller**
38 |
39 | Action Servers:
40 | * **/dock** (:class:`py_trees_ros_interfaces.action.Dock`)
41 |
42 | * docking/undocking control
43 |
44 | Args:
45 | duration: mocked duration of a successful docking/undocking action
46 | """
47 | def __init__(self, duration: float=2.0):
48 | super().__init__(
49 | node_name="docking_controller",
50 | action_name="dock",
51 | action_type=py_trees_actions.Dock,
52 | generate_feedback_message=self.generate_feedback_message,
53 | goal_received_callback=self.goal_received_callback,
54 | duration=duration
55 | )
56 |
57 | def goal_received_callback(self, goal):
58 | """
59 | Set the title of the action depending on whether a docking
60 | or undocking action was requestions ('Dock'/'UnDock')
61 | """
62 | if goal.dock:
63 | self.title = "Dock"
64 | else:
65 | self.title = "UnDock"
66 |
67 | def generate_feedback_message(self) -> py_trees_actions.Dock.Feedback:
68 | """
69 | Create a feedback message that populates the percent completed.
70 |
71 | Returns:
72 | :class:`py_trees_actions.Dock_Feedback`: the populated feedback message
73 | """
74 | msg = py_trees_actions.Dock.Feedback(
75 | percentage_completed=self.percent_completed
76 | )
77 | return msg
78 |
79 |
80 | def main():
81 | """
82 | Entry point for the mocked docking controller.
83 | """
84 | parser = argparse.ArgumentParser(description='Mock a docking controller')
85 | command_line_args = rclpy.utilities.remove_ros_args(args=sys.argv)[1:]
86 | parser.parse_args(command_line_args)
87 | rclpy.init() # picks up sys.argv automagically internally
88 | docking = Dock()
89 |
90 | executor = rclpy.executors.MultiThreadedExecutor(num_threads=4)
91 | executor.add_node(docking.node)
92 |
93 | try:
94 | executor.spin()
95 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
96 | pass
97 | finally:
98 | docking.abort()
99 | # caveat: often broken, with multiple spin_once or shutdown, error is the
100 | # mysterious:
101 | # The following exception was never retrieved: PyCapsule_GetPointer
102 | # called with invalid PyCapsule object
103 | executor.shutdown() # finishes all remaining work and exits
104 | rclpy.try_shutdown()
105 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # License: BSD
3 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
4 | #
5 | ##############################################################################
6 | # Documentation
7 | ##############################################################################
8 |
9 | """
10 | Generated qt modules for the mock robot dashboard.
11 | """
12 | ##############################################################################
13 | # Imports
14 | ##############################################################################
15 |
16 | from . import main_window
17 | from . import configuration_group_box
18 | from . import dashboard_group_box
19 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/configuration_group_box.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Launch a qt dashboard for the tutorials.
13 | """
14 | ##############################################################################
15 | # Imports
16 | ##############################################################################
17 |
18 | import PyQt5.QtWidgets as qt_widgets
19 | import PyQt5.QtCore as qt_core
20 |
21 | from . import configuration_group_box_ui
22 |
23 | ##############################################################################
24 | # Helpers
25 | ##############################################################################
26 |
27 |
28 | class ConfigurationGroupBox(qt_widgets.QGroupBox):
29 | """
30 | Convenience class that Designer can use to promote
31 | elements for layouts in applications.
32 | """
33 |
34 | change_battery_percentage = qt_core.pyqtSignal(float, name="changeBatteryPercentage")
35 | change_battery_charging_status = qt_core.pyqtSignal(bool, name="changeBatteryChargingStatus")
36 | change_safety_sensors_enabled = qt_core.pyqtSignal(bool, name="safetySensorsEnabled")
37 |
38 | def __init__(self, parent):
39 | super().__init__(parent)
40 | self.ui = configuration_group_box_ui.Ui_ConfigurationGroupBox()
41 | self.ui.setupUi(self)
42 |
43 | self.ui.battery_charging_check_box.clicked.connect(
44 | self.battery_charging_status_checkbox_clicked
45 | )
46 | self.ui.battery_percentage_slider.sliderReleased.connect(
47 | self.battery_percentage_slider_updated
48 | )
49 | self.ui.safety_sensors_enabled_check_box.clicked.connect(
50 | self.safety_sensors_enabled_checkbox_clicked
51 | )
52 |
53 | def set_battery_percentage(self, percentage):
54 | if not self.ui.battery_percentage_slider.isSliderDown():
55 | self.ui.battery_percentage_slider.setValue(int(percentage))
56 |
57 | def set_charging_status(self, charging_status):
58 | self.ui.battery_charging_check_box.setChecked(charging_status)
59 |
60 | def set_safety_sensors_enabled(self, enabled_status: bool):
61 | self.ui.safety_sensors_enabled_check_box.setChecked(enabled_status)
62 |
63 | def battery_percentage_slider_updated(self):
64 | percentage = self.ui.battery_percentage_slider.value()
65 | self.change_battery_percentage.emit(percentage)
66 |
67 | def battery_charging_status_checkbox_clicked(self, checked):
68 | self.change_battery_charging_status.emit(checked)
69 |
70 | def safety_sensors_enabled_checkbox_clicked(self, checked):
71 | self.change_safety_sensors_enabled.emit(checked)
72 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/configuration_group_box.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | ConfigurationGroupBox
4 |
5 |
6 |
7 | 0
8 | 0
9 | 400
10 | 300
11 |
12 |
13 |
14 | GroupBox
15 |
16 |
17 | Configuration
18 |
19 |
20 | -
21 |
22 |
23 | Battery
24 |
25 |
26 |
-
27 |
28 |
29 | Charging
30 |
31 |
32 |
33 | -
34 |
35 |
36 | 100
37 |
38 |
39 | 50
40 |
41 |
42 | Qt::Horizontal
43 |
44 |
45 | QSlider::NoTicks
46 |
47 |
48 |
49 | -
50 |
51 |
52 | Qt::Vertical
53 |
54 |
55 |
56 | 20
57 | 41
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | -
66 |
67 |
68 | Safety Sensors
69 |
70 |
71 |
-
72 |
73 |
74 | Enabled
75 |
76 |
77 |
78 | -
79 |
80 |
81 | Qt::Vertical
82 |
83 |
84 |
85 | 20
86 | 97
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/configuration_group_box_ui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'configuration_group_box.ui'
4 | #
5 | # Created by: PyQt5 UI code generator 5.10.1
6 | #
7 | # WARNING! All changes made in this file will be lost!
8 |
9 | from PyQt5 import QtCore, QtGui, QtWidgets
10 |
11 | class Ui_ConfigurationGroupBox(object):
12 | def setupUi(self, ConfigurationGroupBox):
13 | ConfigurationGroupBox.setObjectName("ConfigurationGroupBox")
14 | ConfigurationGroupBox.resize(400, 300)
15 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(ConfigurationGroupBox)
16 | self.verticalLayout_2.setObjectName("verticalLayout_2")
17 | self.battery_group_box = QtWidgets.QGroupBox(ConfigurationGroupBox)
18 | self.battery_group_box.setObjectName("battery_group_box")
19 | self.verticalLayout = QtWidgets.QVBoxLayout(self.battery_group_box)
20 | self.verticalLayout.setObjectName("verticalLayout")
21 | self.battery_charging_check_box = QtWidgets.QCheckBox(self.battery_group_box)
22 | self.battery_charging_check_box.setObjectName("battery_charging_check_box")
23 | self.verticalLayout.addWidget(self.battery_charging_check_box)
24 | self.battery_percentage_slider = QtWidgets.QSlider(self.battery_group_box)
25 | self.battery_percentage_slider.setMaximum(100)
26 | self.battery_percentage_slider.setSliderPosition(50)
27 | self.battery_percentage_slider.setOrientation(QtCore.Qt.Horizontal)
28 | self.battery_percentage_slider.setTickPosition(QtWidgets.QSlider.NoTicks)
29 | self.battery_percentage_slider.setObjectName("battery_percentage_slider")
30 | self.verticalLayout.addWidget(self.battery_percentage_slider)
31 | spacerItem = QtWidgets.QSpacerItem(20, 41, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
32 | self.verticalLayout.addItem(spacerItem)
33 | self.verticalLayout_2.addWidget(self.battery_group_box)
34 | self.safety_sensors_group_box = QtWidgets.QGroupBox(ConfigurationGroupBox)
35 | self.safety_sensors_group_box.setObjectName("safety_sensors_group_box")
36 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.safety_sensors_group_box)
37 | self.verticalLayout_3.setObjectName("verticalLayout_3")
38 | self.safety_sensors_enabled_check_box = QtWidgets.QCheckBox(self.safety_sensors_group_box)
39 | self.safety_sensors_enabled_check_box.setObjectName("safety_sensors_enabled_check_box")
40 | self.verticalLayout_3.addWidget(self.safety_sensors_enabled_check_box)
41 | spacerItem1 = QtWidgets.QSpacerItem(20, 97, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
42 | self.verticalLayout_3.addItem(spacerItem1)
43 | self.verticalLayout_2.addWidget(self.safety_sensors_group_box)
44 |
45 | self.retranslateUi(ConfigurationGroupBox)
46 | QtCore.QMetaObject.connectSlotsByName(ConfigurationGroupBox)
47 |
48 | def retranslateUi(self, ConfigurationGroupBox):
49 | _translate = QtCore.QCoreApplication.translate
50 | ConfigurationGroupBox.setWindowTitle(_translate("ConfigurationGroupBox", "GroupBox"))
51 | ConfigurationGroupBox.setTitle(_translate("ConfigurationGroupBox", "Configuration"))
52 | self.battery_group_box.setTitle(_translate("ConfigurationGroupBox", "Battery"))
53 | self.battery_charging_check_box.setText(_translate("ConfigurationGroupBox", "Charging"))
54 | self.safety_sensors_group_box.setTitle(_translate("ConfigurationGroupBox", "Safety Sensors"))
55 | self.safety_sensors_enabled_check_box.setText(_translate("ConfigurationGroupBox", "Enabled"))
56 |
57 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/dashboard_group_box.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Launch a qt dashboard for the tutorials.
13 | """
14 | ##############################################################################
15 | # Imports
16 | ##############################################################################
17 |
18 | import PyQt5.QtCore as qt_core
19 | import PyQt5.QtWidgets as qt_widgets
20 | import threading
21 |
22 | from . import dashboard_group_box_ui
23 |
24 | ##############################################################################
25 | # Helpers
26 | ##############################################################################
27 |
28 |
29 | class DashboardGroupBox(qt_widgets.QGroupBox):
30 | """
31 | Convenience class that Designer can use to promote
32 | elements for layouts in applications.
33 | """
34 | def __init__(self, parent):
35 | super(DashboardGroupBox, self).__init__(parent)
36 | self.ui = dashboard_group_box_ui.Ui_DashboardGroupBox()
37 | self.ui.setupUi(self)
38 | self.stylesheets = {
39 | "scan_push_button": self.ui.scan_push_button.styleSheet(),
40 | "cancel_push_button": self.ui.cancel_push_button.styleSheet(),
41 | "led_strip_label": self.ui.led_strip_label.styleSheet()
42 | }
43 |
44 | self.led_strip_lock = threading.Lock()
45 | self.led_strip_flashing = False
46 | self.led_strip_on_count = 1
47 | self.led_strip_colour = "grey"
48 | self.set_led_strip_label_colour(self.led_strip_colour)
49 | self.led_strip_timer = qt_core.QTimer()
50 | self.led_strip_timer.timeout.connect(self.led_strip_timer_callback)
51 | self.led_strip_timer.start(500) # ms
52 |
53 | def led_strip_timer_callback(self):
54 | with self.led_strip_lock:
55 | if self.led_strip_flashing:
56 | if self.led_strip_on_count > 0:
57 | self.led_strip_on_count = 0
58 | self.set_led_strip_label_colour("none")
59 | else:
60 | self.led_strip_on_count += 1
61 | self.set_led_strip_label_colour(self.led_strip_colour)
62 | else: # solid
63 | self.led_strip_on_count = 1
64 | self.set_led_strip_label_colour(self.led_strip_colour)
65 |
66 | def set_led_strip_colour(self, colour):
67 | with self.led_strip_lock:
68 | self.led_strip_colour = colour
69 | self.led_strip_flashing = False if self.led_strip_colour == "grey" else True
70 |
71 | def set_cancel_push_button_colour(self, val):
72 | background_colour = "green" if val else "none"
73 | self.ui.cancel_push_button.setStyleSheet(
74 | self.stylesheets["cancel_push_button"] + "\n" +
75 | "background-color: {}".format(background_colour)
76 | )
77 |
78 | def set_scan_push_button_colour(self, val):
79 | print("style: {}".format(self.ui.scan_push_button.styleSheet()))
80 | background_colour = "green" if val else "none"
81 | self.ui.scan_push_button.setStyleSheet(
82 | self.stylesheets["scan_push_button"] + "\n" +
83 | "background-color: {}".format(background_colour)
84 | )
85 |
86 | def set_led_strip_label_colour(self, colour):
87 | # background-color doesn't line up with the qframe panel border
88 | # border-radius wipes out the qframe styledpanel raised border
89 | #
90 | # Q: How to get the background fill colour, to be merely
91 | # embedded in the qframe StyledPanel|Raised style?
92 | #
93 | # Workaround: just set the text colour
94 | self.ui.led_strip_label.setStyleSheet(
95 | self.stylesheets["led_strip_label"] + "\n" +
96 | "color: {};".format(colour)
97 | )
98 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/dashboard_group_box.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | DashboardGroupBox
4 |
5 |
6 |
7 | 0
8 | 0
9 | 400
10 | 300
11 |
12 |
13 |
14 | Dashboard
15 |
16 |
17 | Dashboard
18 |
19 |
20 | -
21 |
22 |
23 |
24 | 0
25 | 0
26 |
27 |
28 |
29 | font-size: 30pt;
30 |
31 |
32 | Scan
33 |
34 |
35 |
36 | -
37 |
38 |
39 |
40 | 0
41 | 0
42 |
43 |
44 |
45 | false
46 |
47 |
48 | font-size: 30pt;
49 |
50 |
51 | Cancel
52 |
53 |
54 | false
55 |
56 |
57 |
58 | -
59 |
60 |
61 |
62 | 0
63 | 0
64 |
65 |
66 |
67 | false
68 |
69 |
70 | font-size: 30pt;
71 |
72 |
73 | QFrame::StyledPanel
74 |
75 |
76 | QFrame::Raised
77 |
78 |
79 | Led Strip
80 |
81 |
82 | Qt::AlignCenter
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/dashboard_group_box_ui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'dashboard_group_box.ui'
4 | #
5 | # Created by: PyQt5 UI code generator 5.10.1
6 | #
7 | # WARNING! All changes made in this file will be lost!
8 |
9 | from PyQt5 import QtCore, QtGui, QtWidgets
10 |
11 | class Ui_DashboardGroupBox(object):
12 | def setupUi(self, DashboardGroupBox):
13 | DashboardGroupBox.setObjectName("DashboardGroupBox")
14 | DashboardGroupBox.resize(400, 300)
15 | self.verticalLayout = QtWidgets.QVBoxLayout(DashboardGroupBox)
16 | self.verticalLayout.setObjectName("verticalLayout")
17 | self.scan_push_button = QtWidgets.QPushButton(DashboardGroupBox)
18 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding)
19 | sizePolicy.setHorizontalStretch(0)
20 | sizePolicy.setVerticalStretch(0)
21 | sizePolicy.setHeightForWidth(self.scan_push_button.sizePolicy().hasHeightForWidth())
22 | self.scan_push_button.setSizePolicy(sizePolicy)
23 | self.scan_push_button.setStyleSheet("font-size: 30pt;")
24 | self.scan_push_button.setObjectName("scan_push_button")
25 | self.verticalLayout.addWidget(self.scan_push_button)
26 | self.cancel_push_button = QtWidgets.QPushButton(DashboardGroupBox)
27 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding)
28 | sizePolicy.setHorizontalStretch(0)
29 | sizePolicy.setVerticalStretch(0)
30 | sizePolicy.setHeightForWidth(self.cancel_push_button.sizePolicy().hasHeightForWidth())
31 | self.cancel_push_button.setSizePolicy(sizePolicy)
32 | self.cancel_push_button.setAutoFillBackground(False)
33 | self.cancel_push_button.setStyleSheet("font-size: 30pt;")
34 | self.cancel_push_button.setFlat(False)
35 | self.cancel_push_button.setObjectName("cancel_push_button")
36 | self.verticalLayout.addWidget(self.cancel_push_button)
37 | self.led_strip_label = QtWidgets.QLabel(DashboardGroupBox)
38 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
39 | sizePolicy.setHorizontalStretch(0)
40 | sizePolicy.setVerticalStretch(0)
41 | sizePolicy.setHeightForWidth(self.led_strip_label.sizePolicy().hasHeightForWidth())
42 | self.led_strip_label.setSizePolicy(sizePolicy)
43 | self.led_strip_label.setAutoFillBackground(False)
44 | self.led_strip_label.setStyleSheet("font-size: 30pt;")
45 | self.led_strip_label.setFrameShape(QtWidgets.QFrame.StyledPanel)
46 | self.led_strip_label.setFrameShadow(QtWidgets.QFrame.Raised)
47 | self.led_strip_label.setAlignment(QtCore.Qt.AlignCenter)
48 | self.led_strip_label.setObjectName("led_strip_label")
49 | self.verticalLayout.addWidget(self.led_strip_label)
50 |
51 | self.retranslateUi(DashboardGroupBox)
52 | QtCore.QMetaObject.connectSlotsByName(DashboardGroupBox)
53 |
54 | def retranslateUi(self, DashboardGroupBox):
55 | _translate = QtCore.QCoreApplication.translate
56 | DashboardGroupBox.setWindowTitle(_translate("DashboardGroupBox", "Dashboard"))
57 | DashboardGroupBox.setTitle(_translate("DashboardGroupBox", "Dashboard"))
58 | self.scan_push_button.setText(_translate("DashboardGroupBox", "Scan"))
59 | self.cancel_push_button.setText(_translate("DashboardGroupBox", "Cancel"))
60 | self.led_strip_label.setText(_translate("DashboardGroupBox", "Led Strip"))
61 |
62 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/gen.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Script for setting up the development environment.
4 | #source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
5 |
6 | NAME=py_trees
7 |
8 | ##############################################################################
9 | # Colours
10 | ##############################################################################
11 |
12 | BOLD="\e[1m"
13 |
14 | CYAN="\e[36m"
15 | GREEN="\e[32m"
16 | RED="\e[31m"
17 | YELLOW="\e[33m"
18 |
19 | RESET="\e[0m"
20 |
21 | padded_message ()
22 | {
23 | line="........................................"
24 | printf "%s %s${2}\n" ${1} "${line:${#1}}"
25 | }
26 |
27 | pretty_header ()
28 | {
29 | echo -e "${BOLD}${1}${RESET}"
30 | }
31 |
32 | pretty_print ()
33 | {
34 | echo -e "${GREEN}${1}${RESET}"
35 | }
36 |
37 | pretty_warning ()
38 | {
39 | echo -e "${YELLOW}${1}${RESET}"
40 | }
41 |
42 | pretty_error ()
43 | {
44 | echo -e "${RED}${1}${RESET}"
45 | }
46 |
47 | ##############################################################################
48 | # Methods
49 | ##############################################################################
50 |
51 | install_package ()
52 | {
53 | PACKAGE_NAME=$1
54 | dpkg -s ${PACKAGE_NAME} > /dev/null
55 | if [ $? -ne 0 ]; then
56 | sudo apt-get -q -y install ${PACKAGE_NAME} > /dev/null
57 | else
58 | pretty_print " $(padded_message ${PACKAGE_NAME} "found")"
59 | return 0
60 | fi
61 | if [ $? -ne 0 ]; then
62 | pretty_error " $(padded_message ${PACKAGE_NAME} "failed")"
63 | return 1
64 | fi
65 | pretty_warning " $(padded_message ${PACKAGE_NAME} "installed")"
66 | return 0
67 | }
68 |
69 | generate_ui ()
70 | {
71 | NAME=$1
72 | pyuic5 --from-imports -o ${NAME}_ui.py ${NAME}.ui
73 | if [ $? -ne 0 ]; then
74 | pretty_error " $(padded_message ${NAME} "failed")"
75 | return 1
76 | fi
77 | pretty_print " $(padded_message ${NAME} "generated")"
78 | return 0
79 | }
80 |
81 | generate_qrc ()
82 | {
83 | NAME=$1
84 | pyrcc5 -o ${NAME}_rc.py ${NAME}.qrc
85 | if [ $? -ne 0 ]; then
86 | pretty_error " $(padded_message ${NAME} "failed")"
87 | return 1
88 | fi
89 | pretty_print " $(padded_message ${NAME} "generated")"
90 | return 0
91 | }
92 |
93 | ##############################################################################
94 |
95 | echo ""
96 |
97 | echo -e "${CYAN}Dependencies${RESET}"
98 | install_package pyqt5-dev-tools || return
99 |
100 | echo ""
101 |
102 | echo -e "${CYAN}Generating UIs${RESET}"
103 | generate_ui configuration_group_box
104 | generate_ui dashboard_group_box
105 | generate_ui main_window
106 |
107 | echo ""
108 |
109 | echo -e "${CYAN}Generating QRCs${RESET}"
110 | generate_qrc main_window
111 |
112 | echo ""
113 | echo "I'm grooty, you should be too."
114 | echo ""
115 |
116 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/main_window.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Launch a qt dashboard for the tutorials.
13 | """
14 | ##############################################################################
15 | # Imports
16 | ##############################################################################
17 |
18 | import PyQt5.QtCore as qt_core
19 | import PyQt5.QtWidgets as qt_widgets
20 |
21 | from . import main_window_ui
22 |
23 | ##############################################################################
24 | # Helpers
25 | ##############################################################################
26 |
27 |
28 | class MainWindow(qt_widgets.QMainWindow):
29 |
30 | request_shutdown = qt_core.pyqtSignal(name="requestShutdown")
31 |
32 | def __init__(self):
33 | super().__init__()
34 | self.ui = main_window_ui.Ui_MainWindow()
35 | self.ui.setupUi(self)
36 |
37 | def closeEvent(self, unused_event):
38 | self.request_shutdown.emit()
39 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/main_window.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | tuxrobot.png
4 |
5 |
6 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/main_window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 |
7 | 0
8 | 0
9 | 551
10 | 356
11 |
12 |
13 |
14 | Robot Mock
15 |
16 |
17 |
18 | :/images/tuxrobot.png:/images/tuxrobot.png
19 |
20 |
21 |
22 | -
23 |
24 |
25 | Dashboard
26 |
27 |
28 |
29 | -
30 |
31 |
32 | Configuration
33 |
34 |
35 |
36 |
37 |
38 |
51 |
52 |
53 |
54 |
55 | DashboardGroupBox
56 | QGroupBox
57 | py_trees_ros_tutorials.mock.gui.dashboard_group_box
58 | 1
59 |
60 |
61 | ConfigurationGroupBox
62 | QGroupBox
63 | py_trees_ros_tutorials.mock.gui.configuration_group_box
64 | 1
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/main_window_ui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'main_window.ui'
4 | #
5 | # Created by: PyQt5 UI code generator 5.10.1
6 | #
7 | # WARNING! All changes made in this file will be lost!
8 |
9 | from PyQt5 import QtCore, QtGui, QtWidgets
10 |
11 | class Ui_MainWindow(object):
12 | def setupUi(self, MainWindow):
13 | MainWindow.setObjectName("MainWindow")
14 | MainWindow.resize(551, 356)
15 | icon = QtGui.QIcon()
16 | icon.addPixmap(QtGui.QPixmap(":/images/tuxrobot.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
17 | MainWindow.setWindowIcon(icon)
18 | self.central_layout = QtWidgets.QWidget(MainWindow)
19 | self.central_layout.setObjectName("central_layout")
20 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.central_layout)
21 | self.horizontalLayout.setObjectName("horizontalLayout")
22 | self.dashboard_group_box = DashboardGroupBox(self.central_layout)
23 | self.dashboard_group_box.setTitle("Dashboard")
24 | self.dashboard_group_box.setObjectName("dashboard_group_box")
25 | self.horizontalLayout.addWidget(self.dashboard_group_box)
26 | self.configuration_group_box = ConfigurationGroupBox(self.central_layout)
27 | self.configuration_group_box.setObjectName("configuration_group_box")
28 | self.horizontalLayout.addWidget(self.configuration_group_box)
29 | MainWindow.setCentralWidget(self.central_layout)
30 | self.menubar = QtWidgets.QMenuBar(MainWindow)
31 | self.menubar.setGeometry(QtCore.QRect(0, 0, 551, 29))
32 | self.menubar.setDefaultUp(False)
33 | self.menubar.setObjectName("menubar")
34 | MainWindow.setMenuBar(self.menubar)
35 | self.statusbar = QtWidgets.QStatusBar(MainWindow)
36 | self.statusbar.setObjectName("statusbar")
37 | MainWindow.setStatusBar(self.statusbar)
38 |
39 | self.retranslateUi(MainWindow)
40 | QtCore.QMetaObject.connectSlotsByName(MainWindow)
41 |
42 | def retranslateUi(self, MainWindow):
43 | _translate = QtCore.QCoreApplication.translate
44 | MainWindow.setWindowTitle(_translate("MainWindow", "Robot Mock"))
45 | self.configuration_group_box.setTitle(_translate("MainWindow", "Configuration"))
46 |
47 | from py_trees_ros_tutorials.mock.gui.configuration_group_box import ConfigurationGroupBox
48 | from py_trees_ros_tutorials.mock.gui.dashboard_group_box import DashboardGroupBox
49 | from . import main_window_rc
50 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/gui/tuxrobot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/py_trees_ros_tutorials/mock/gui/tuxrobot.png
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/launch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Launch the mock robot.
13 | """
14 | ##############################################################################
15 | # Imports
16 | ##############################################################################
17 |
18 | import typing
19 |
20 | import launch
21 | import launch_ros.actions
22 |
23 | ##############################################################################
24 | # Helpers
25 | ##############################################################################
26 |
27 |
28 | def generate_launch_nodes() -> typing.List[launch_ros.actions.Node]:
29 | """
30 | Generate an action node for launch.
31 |
32 | Returns:
33 | a list of the mock robot ros nodes as actions for launch
34 | """
35 | launch_nodes = []
36 | for node_name in ['battery', 'dashboard', 'docking_controller',
37 | 'led_strip', 'move_base', 'rotation_controller',
38 | 'safety_sensors']:
39 | executable = "mock-{}".format(node_name.replace('_', '-'))
40 | launch_nodes.append(
41 | launch_ros.actions.Node(
42 | package='py_trees_ros_tutorials',
43 | name=node_name,
44 | executable=executable,
45 | output='screen',
46 | emulate_tty=True
47 | )
48 | )
49 | launch_nodes.append(
50 | launch.actions.LogInfo(msg=["Bob the robot, at your service. Need a colander?"])
51 | )
52 | return launch_nodes
53 |
54 |
55 | def generate_launch_description() -> launch.LaunchDescription:
56 | """
57 | Launch the mock robot (i.e. launch all mocked components).
58 |
59 | Returns:
60 | the launch description
61 | """
62 |
63 | return launch.LaunchDescription(generate_launch_nodes())
64 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/led_strip.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Mock a hardware LED strip.
13 | """
14 |
15 |
16 | ##############################################################################
17 | # Imports
18 | ##############################################################################
19 |
20 | import argparse
21 | import functools
22 | import math
23 | import py_trees.console as console
24 | import py_trees_ros
25 | import rclpy
26 | import std_msgs.msg as std_msgs
27 | import sys
28 | import threading
29 | import uuid
30 |
31 | ##############################################################################
32 | # Class
33 | ##############################################################################
34 |
35 |
36 | class LEDStrip(object):
37 | """
38 | Emulates command/display of an led strip so that it flashes various colours.
39 |
40 | Node Name:
41 | * **led_strip**
42 |
43 | Publishers:
44 | * **~display** (:class:`std_msgs.msg.String`)
45 |
46 | * colourised string display of the current led strip state
47 |
48 | Subscribers:
49 | * **~command** (:class:`std_msgs.msg.String`)
50 |
51 | * send it a colour to express, it will flash this for the next 3 seconds
52 | """
53 | _pattern = '*'
54 | _pattern_width = 60 # total width of the pattern to be output
55 | _pattern_name_spacing = 4 # space between pattern and the name of the pattern
56 |
57 | def __init__(self):
58 | self.node = rclpy.create_node("led_strip")
59 | self.command_subscriber = self.node.create_subscription(
60 | msg_type=std_msgs.String,
61 | topic='~/command',
62 | callback=self.command_callback,
63 | qos_profile=py_trees_ros.utilities.qos_profile_unlatched()
64 | )
65 | self.display_publisher = self.node.create_publisher(
66 | msg_type=std_msgs.String,
67 | topic="~/display",
68 | qos_profile=py_trees_ros.utilities.qos_profile_latched()
69 | )
70 | self.duration_sec = 3.0
71 | self.last_text = ''
72 | self.last_uuid = None
73 | self.lock = threading.Lock()
74 | self.flashing_timer = None
75 |
76 | def _get_display_string(self, width: int, label: str="Foo") -> str:
77 | """
78 | Display the current state of the led strip as a formatted
79 | string.
80 |
81 | Args:
82 | width: the width of the pattern
83 | label: display this in the centre of the pattern rather
84 | than the pattern name
85 | """
86 | # top and bottom of print repeats the pattern as many times as possible
87 | # in the space specified
88 | top_bottom = LEDStrip._pattern * int(width / len(LEDStrip._pattern))
89 | # space for two halves of the pattern on either side of the pattern name
90 | mid_pattern_space = (width - len(label) - self._pattern_name_spacing * 2) / 2
91 |
92 | # pattern for the mid line
93 | mid = LEDStrip._pattern * int(mid_pattern_space / len(LEDStrip._pattern))
94 |
95 | # total length of the middle line with pattern, spacing and name
96 | mid_len = len(mid) * 2 + self._pattern_name_spacing * 2 + len(label)
97 |
98 | # patterns won't necessarily match up with the width, so need to deal
99 | # with extra space. Odd numbers of extra space handled by putting more
100 | # spaces on the right side
101 | extra_space = width - mid_len
102 | extra_left_space = int(math.floor(extra_space / 2.0))
103 | extra_right_space = int(math.ceil(extra_space / 2.0))
104 |
105 | # left and right parts of the mid line to go around the name
106 | left = mid + ' ' * (self._pattern_name_spacing + extra_left_space)
107 | right = ' ' * (self._pattern_name_spacing + extra_right_space) + mid
108 |
109 | return '\n' + top_bottom + '\n' + left + label.replace('_', ' ') + right + '\n' + top_bottom
110 |
111 | def generate_led_text(self, colour: bool) -> str:
112 | """
113 | Generate a formatted string representation of the the current state of the led strip.
114 |
115 | Args:
116 | colour: use shell escape sequences for colour, matching the specified text colour label
117 | """
118 | if not colour:
119 | return ""
120 | else:
121 | text = self._get_display_string(self._pattern_width, label=colour)
122 |
123 | # map colour names in message to console colour escape sequences
124 | console_colour_map = {
125 | 'grey': console.dim + console.white,
126 | 'red': console.red,
127 | 'green': console.green,
128 | 'yellow': console.yellow,
129 | 'blue': console.blue,
130 | 'purple': console.magenta,
131 | 'white': console.white
132 | }
133 |
134 | coloured_text = console_colour_map[colour] + console.blink + text + console.reset
135 | return coloured_text
136 |
137 | def command_callback(self, msg: std_msgs.String):
138 | """
139 | If the requested state is different from the existing state, update and
140 | restart a periodic timer to affect the flashing effect.
141 |
142 | Args:
143 | msg (:class:`std_msgs.msg.String`): incoming command message
144 | """
145 | with self.lock:
146 | text = self.generate_led_text(msg.data)
147 | # don't bother publishing if nothing changed.
148 | if self.last_text != text:
149 | self.node.get_logger().info("{}".format(text))
150 | self.last_text = text
151 | self.last_uuid = uuid.uuid4()
152 | self.display_publisher.publish(std_msgs.String(data=msg.data))
153 | if self.flashing_timer is not None:
154 | self.flashing_timer.cancel()
155 | self.node.destroy_timer(self.flashing_timer)
156 | # TODO: convert this to a one-shot once rclpy has the capability
157 | # Without oneshot, it will keep triggering, but do nothing while
158 | # it has the uuid check
159 | self.flashing_timer = self.node.create_timer(
160 | timer_period_sec=self.duration_sec,
161 | callback=functools.partial(
162 | self.cancel_flashing,
163 | this_uuid=self.last_uuid
164 | )
165 | )
166 |
167 | def cancel_flashing(self, this_uuid: uuid.UUID):
168 | """
169 | If the notification identified by the given uuid is still relevant (i.e.
170 | new command requests haven't come in) then publish an update with an
171 | empty display message.
172 |
173 | Args:
174 | this_uuid: the uuid of the notification to cancel
175 | """
176 | with self.lock:
177 | if self.last_uuid == this_uuid:
178 | # We're still relevant, publish and make us irrelevant
179 | self.display_publisher.publish(std_msgs.String(data=""))
180 | self.last_text = ""
181 | self.last_uuid = uuid.uuid4()
182 |
183 | def shutdown(self):
184 | """
185 | Cleanup ROS components.
186 | """
187 | self.node.destroy_node()
188 |
189 |
190 | def main():
191 | """
192 | Entry point for the mock led strip.
193 | """
194 | parser = argparse.ArgumentParser(description='Mock an led strip')
195 | command_line_args = rclpy.utilities.remove_ros_args(args=sys.argv)[1:]
196 | parser.parse_args(command_line_args)
197 | rclpy.init(args=sys.argv)
198 | led_strip = LEDStrip()
199 | try:
200 | rclpy.spin(led_strip.node)
201 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
202 | pass
203 | finally:
204 | rclpy.try_shutdown()
205 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/move_base.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Mocks a simple action server that rotates the robot 360 degrees.
13 | """
14 |
15 |
16 | ##############################################################################
17 | # Imports
18 | ##############################################################################
19 |
20 | import argparse
21 | import geometry_msgs.msg as geometry_msgs
22 | import py_trees_ros.mock.actions
23 | import py_trees_ros_interfaces.action as py_trees_actions
24 | import rclpy
25 | import sys
26 |
27 | ##############################################################################
28 | # Class
29 | ##############################################################################
30 |
31 |
32 | class MoveBase(py_trees_ros.mock.actions.GenericServer):
33 | """
34 | Simulates a move base style interface.
35 |
36 | Node Name:
37 | * **move_base_controller**
38 |
39 | Action Servers:
40 | * **/move_base** (:class:`py_trees_ros_interfaces.action.MoveBase`)
41 |
42 | * point to point move base action
43 |
44 | Args:
45 | duration: mocked duration of a successful action
46 | """
47 | def __init__(self, duration=None):
48 | super().__init__(
49 | node_name="move_base_controller",
50 | action_name="move_base",
51 | action_type=py_trees_actions.MoveBase,
52 | generate_feedback_message=self.generate_feedback_message,
53 | duration=duration
54 | )
55 | self.pose = geometry_msgs.PoseStamped()
56 | self.pose.pose.position = geometry_msgs.Point(x=0.0, y=0.0, z=0.0)
57 |
58 | def generate_feedback_message(self) -> py_trees_actions.MoveBase.Feedback:
59 | """
60 | Do a fake pose incremenet and populate the feedback message.
61 |
62 | Returns:
63 | :class:`py_trees_actions.MoveBase.Feedback`: the populated feedback message
64 | """
65 | # actually doesn't go to the goal right now...
66 | # but we could take the feedback from the action
67 | # and increment this to that proportion
68 | # self.odometry.pose.pose.position.x += 0.01
69 | self.pose.pose.position.x += 0.01
70 | msg = py_trees_actions.MoveBase.Feedback() # .Feedback() is more proper, but indexing can't find it
71 | msg.base_position = self.pose
72 | return msg
73 |
74 |
75 | def main():
76 | """
77 | Entry point for the mock move base node.
78 | """
79 | parser = argparse.ArgumentParser(description='Mock a docking controller')
80 | command_line_args = rclpy.utilities.remove_ros_args(args=sys.argv)[1:]
81 | parser.parse_args(command_line_args)
82 |
83 | rclpy.init() # picks up sys.argv automagically internally
84 | move_base = MoveBase()
85 | executor = rclpy.executors.MultiThreadedExecutor(num_threads=4)
86 | executor.add_node(move_base.node)
87 |
88 | try:
89 | executor.spin()
90 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
91 | pass
92 | finally:
93 | move_base.abort()
94 | move_base.shutdown()
95 | # caveat: often broken, with multiple spin_once or shutdown, error is the
96 | # mysterious:
97 | # The following exception was never retrieved: PyCapsule_GetPointer
98 | # called with invalid PyCapsule object
99 | executor.shutdown() # finishes all remaining work and exits
100 | rclpy.try_shutdown()
101 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/rotate.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Mocks a simple action server that rotates the robot 360 degrees.
13 | """
14 |
15 |
16 | ##############################################################################
17 | # Imports
18 | ##############################################################################
19 |
20 | import argparse
21 | import math
22 | import py_trees_ros.mock.actions
23 | import py_trees_ros_interfaces.action as py_trees_actions
24 | import rclpy
25 | import sys
26 |
27 | ##############################################################################
28 | # Class
29 | ##############################################################################
30 |
31 |
32 | class Rotate(py_trees_ros.mock.actions.GenericServer):
33 | """
34 | Simple server that controls a full rotation of the robot.
35 |
36 | Node Name:
37 | * **rotation_controller**
38 |
39 | Action Servers:
40 | * **/rotate** (:class:`py_trees_ros_interfaces.action.Dock`)
41 |
42 | * motion primitives - rotation server
43 |
44 | Args:
45 | rotation_rate (:obj:`float`): rate of rotation (rad/s)
46 | """
47 | def __init__(self, rotation_rate: float=1.57):
48 | super().__init__(node_name="rotation_controller",
49 | action_name="rotate",
50 | action_type=py_trees_actions.Rotate,
51 | generate_feedback_message=self.generate_feedback_message,
52 | duration=2.0 * math.pi / rotation_rate
53 | )
54 |
55 | def generate_feedback_message(self):
56 | """
57 | Create a feedback message that populates the percent completed.
58 |
59 | Returns:
60 | :class:`py_trees_actions.Rotate.Feedback`: the populated feedback message
61 | """
62 | # TODO: send some feedback message
63 | msg = py_trees_actions.Rotate.Feedback() # Rotate.Feedback() works, but the indexer can't find it
64 | msg.percentage_completed = self.percent_completed
65 | msg.angle_rotated = 2*math.pi*self.percent_completed/100.0
66 | return msg
67 |
68 |
69 | def main():
70 | """
71 | Entry point for the mock rotation controller node.
72 | """
73 | parser = argparse.ArgumentParser(description='Mock a rotation controller')
74 | command_line_args = rclpy.utilities.remove_ros_args(args=sys.argv)[1:]
75 | parser.parse_args(command_line_args)
76 | rclpy.init() # picks up sys.argv automagically internally
77 | rotation = Rotate()
78 |
79 | executor = rclpy.executors.MultiThreadedExecutor(num_threads=4)
80 | executor.add_node(rotation.node)
81 |
82 | try:
83 | executor.spin()
84 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
85 | pass
86 | finally:
87 | rotation.abort()
88 | # caveat: often broken, with multiple spin_once or shutdown, error is the
89 | # mysterious:
90 | # The following exception was never retrieved: PyCapsule_GetPointer
91 | # called with invalid PyCapsule object
92 | executor.shutdown() # finishes all remaining work and exits
93 | rclpy.try_shutdown()
94 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/mock/safety_sensors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | Mocks a battery provider.
13 | """
14 |
15 |
16 | ##############################################################################
17 | # Imports
18 | ##############################################################################
19 |
20 | import argparse
21 | import rclpy
22 | import rclpy.parameter
23 | import sys
24 |
25 | ##############################################################################
26 | # Class
27 | ##############################################################################
28 |
29 |
30 | class SafetySensors(object):
31 | """
32 | Mocks the ability to enable/disable a safety sensor processing pipeline.
33 | This emulates a component which needs to be enabled contextually so that
34 | cpu resources can be efficiently optimised or to resolve contextual
35 | conflicts in the usage of the sensors.
36 |
37 | Node Name:
38 | * **safety_sensors**
39 |
40 | Dynamic Parameters:
41 | * **~enable** (:obj:`bool`)
42 |
43 | * enable/disable the safety sensor pipeline (default: False)
44 |
45 | Use the ``dashboard`` to dynamically reconfigure the parameters.
46 | """
47 | def __init__(self):
48 | # node
49 | self.node = rclpy.create_node(
50 | "safety_sensors",
51 | parameter_overrides=[
52 | rclpy.parameter.Parameter('enabled', rclpy.parameter.Parameter.Type.BOOL, False),
53 | ],
54 | automatically_declare_parameters_from_overrides=True
55 | )
56 |
57 | def shutdown(self):
58 | """
59 | Cleanup ROS components.
60 | """
61 | # currently complains with:
62 | # RuntimeWarning: Failed to fini publisher: rcl node implementation is invalid, at /tmp/binarydeb/ros-dashing-rcl-0.7.5/src/rcl/node.c:462
63 | # Q: should rlcpy.shutdown() automagically handle descruction of nodes implicitly?
64 | self.node.destroy_node()
65 |
66 |
67 | def main():
68 | """
69 | Entry point for the mock safety sensors node.
70 | """
71 | parser = argparse.ArgumentParser(description='Mock the safety sensors')
72 | command_line_args = rclpy.utilities.remove_ros_args(args=sys.argv)[1:]
73 | parser.parse_args(command_line_args)
74 | rclpy.init() # picks up sys.argv automagically internally
75 | safety_sensors = SafetySensors()
76 | try:
77 | rclpy.spin(safety_sensors.node)
78 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
79 | pass
80 | finally:
81 | safety_sensors.shutdown()
82 | rclpy.try_shutdown()
83 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/one_data_gathering.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | About
13 | ^^^^^
14 |
15 | In this, the first of the tutorials, we start out with a behaviour that
16 | collects battery data from a subscriber and stores the result on the
17 | blackboard for other behaviours to utilise.
18 |
19 | Data gathering up front via subscribers is a useful convention for
20 | a number of reasons:
21 |
22 | * Freeze incoming data for remaining behaviours in the tree tick so that decision making is consistent across the entire tree
23 | * Avoid redundantly invoking multiple subscribers to the same topic when not necessary
24 | * Python access to the blackboard is easier than ROS middleware handling
25 |
26 | Typically data gatherers will be assembled underneath a parallel at or near
27 | the very root of the tree so they may always trigger their update() method
28 | and be processed before any decision making behaviours elsewhere in the tree.
29 |
30 | Tree
31 | ^^^^
32 |
33 | .. code-block:: bash
34 |
35 | $ py-trees-render -b py_trees_ros_tutorials.one_data_gathering.tutorial_create_root
36 |
37 | .. graphviz:: dot/tutorial-one-data-gathering.dot
38 | :align: center
39 |
40 | .. literalinclude:: ../py_trees_ros_tutorials/one_data_gathering.py
41 | :language: python
42 | :linenos:
43 | :lines: 121-153
44 | :caption: one_data_gathering.py#tutorial_create_root
45 |
46 | Along with the data gathering side, you'll also notice the dummy branch for
47 | priority jobs (complete with idle behaviour that is always
48 | :attr:`~py_trees.common.Status.RUNNING`). This configuration is typical
49 | of the :term:`data gathering` pattern.
50 |
51 | Behaviours
52 | ^^^^^^^^^^
53 |
54 | The tree makes use of the :class:`py_trees_ros.battery.ToBlackboard` behaviour.
55 |
56 | This behaviour will cause the entire tree to tick over with
57 | :attr:`~py_trees.common.Status.SUCCESS` so long as there is data incoming.
58 | If there is no data incoming, it will simply
59 | :term:`block` and prevent the rest of the tree from acting.
60 |
61 |
62 | Running
63 | ^^^^^^^
64 |
65 | .. code-block:: bash
66 |
67 | # Launch the tutorial
68 | $ ros2 launch py_trees_ros_tutorials tutorial_one_data_gathering_launch.py
69 | # In a different shell, introspect the entire blackboard
70 | $ py-trees-blackboard-watcher
71 | # Or selectively get the battery percentage
72 | $ py-trees-blackboard-watcher --list
73 | $ py-trees-blackboard-watcher /battery.percentage
74 |
75 | .. image:: images/tutorial-one-data-gathering.gif
76 | """
77 |
78 | ##############################################################################
79 | # Imports
80 | ##############################################################################
81 |
82 | import launch
83 | import launch_ros
84 | import py_trees
85 | import py_trees_ros.trees
86 | import py_trees.console as console
87 | import rclpy
88 | import sys
89 |
90 | from . import mock
91 |
92 | ##############################################################################
93 | # Launcher
94 | ##############################################################################
95 |
96 |
97 | def generate_launch_description():
98 | """
99 | Launcher for the tutorial.
100 |
101 | Returns:
102 | the launch description
103 | """
104 | return launch.LaunchDescription(
105 | mock.launch.generate_launch_nodes() +
106 | [
107 | launch_ros.actions.Node(
108 | package='py_trees_ros_tutorials',
109 | executable="tree-data-gathering",
110 | output='screen',
111 | emulate_tty=True,
112 | )
113 | ]
114 | )
115 |
116 | ##############################################################################
117 | # Tutorial
118 | ##############################################################################
119 |
120 |
121 | def tutorial_create_root() -> py_trees.behaviour.Behaviour:
122 | """
123 | Create a basic tree and start a 'Topics2BB' work sequence that
124 | will become responsible for data gathering behaviours.
125 |
126 | Returns:
127 | the root of the tree
128 | """
129 | root = py_trees.composites.Parallel(
130 | name="Tutorial One",
131 | policy=py_trees.common.ParallelPolicy.SuccessOnAll(
132 | synchronise=False
133 | )
134 | )
135 |
136 | topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
137 | battery2bb = py_trees_ros.battery.ToBlackboard(
138 | name="Battery2BB",
139 | topic_name="/battery/state",
140 | qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
141 | threshold=30.0
142 | )
143 | priorities = py_trees.composites.Selector(name="Tasks", memory=False)
144 | idle = py_trees.behaviours.Running(name="Idle")
145 | flipper = py_trees.behaviours.Periodic(name="Flip Eggs", n=2)
146 |
147 | root.add_child(topics2bb)
148 | topics2bb.add_child(battery2bb)
149 | root.add_child(priorities)
150 | priorities.add_child(flipper)
151 | priorities.add_child(idle)
152 |
153 | return root
154 |
155 |
156 | def tutorial_main():
157 | """
158 | Entry point for the demo script.
159 | """
160 | rclpy.init(args=None)
161 | root = tutorial_create_root()
162 | tree = py_trees_ros.trees.BehaviourTree(
163 | root=root,
164 | unicode_tree_debug=True
165 | )
166 | try:
167 | tree.setup(node_name="foo", timeout=15.0)
168 | except py_trees_ros.exceptions.TimedOutError as e:
169 | console.logerror(console.red + "failed to setup the tree, aborting [{}]".format(str(e)) + console.reset)
170 | tree.shutdown()
171 | rclpy.try_shutdown()
172 | sys.exit(1)
173 | except KeyboardInterrupt:
174 | # not a warning, nor error, usually a user-initiated shutdown
175 | console.logerror("tree setup interrupted")
176 | tree.shutdown()
177 | rclpy.try_shutdown()
178 | sys.exit(1)
179 |
180 | tree.tick_tock(period_ms=1000.0)
181 |
182 | try:
183 | rclpy.spin(tree.node)
184 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
185 | pass
186 | finally:
187 | tree.shutdown()
188 | rclpy.try_shutdown()
189 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/six_context_switching.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | About
13 | ^^^^^
14 |
15 | This tutorial inserts a context switching behaviour to run in tandem with the
16 | scan rotation. A context switching behaviour will alter the runtime system
17 | in some way when it is entered (i.e. in :meth:`~py_trees.behaviour.Behaviour.initialise`)
18 | and reset the runtime system to it's original context
19 | on :meth:`~py_trees.behaviour.Behaviour.terminate`). Refer to :term:`context switch`
20 | for more detail.
21 |
22 | In this example it will enable a hypothetical safety sensor pipeline, necessary
23 | necessary for dangerous but slow moving rotational maneuvres not required for
24 | normal modes of travel (suppose we have a large rectangular robot that is
25 | ordinarily blind to the sides - it may need to take advantage of noisy
26 | sonars to the sides or rotate forward facing sensing into position before
27 | engaging).
28 |
29 |
30 | Tree
31 | ^^^^
32 |
33 | .. code-block:: bash
34 |
35 | $ py-trees-render -b py_trees_ros_tutorials.six_context_switching.tutorial_create_root
36 |
37 | .. graphviz:: dot/tutorial-six-context-switching.dot
38 | :align: center
39 |
40 | .. literalinclude:: ../py_trees_ros_tutorials/six_context_switching.py
41 | :language: python
42 | :linenos:
43 | :lines: 132-232
44 | :caption: six_context_switching.py#tutorial_create_root
45 |
46 | Behaviour
47 | ---------
48 |
49 | The :class:`py_trees_ros_tutorials.behaviours.ScanContext` is the
50 | context switching behaviour constructed for this tutorial.
51 |
52 | * :meth:`~py_trees_ros_tutorials.behaviours.ScanContext.initialise()`: trigger a sequence service calls to cache and set the /safety_sensors/enabled parameter to True
53 | * :meth:`~py_trees_ros_tutorials.behaviours.ScanContext.update()`: complete the chain of service calls & maintain the context
54 | * :meth:`~py_trees_ros_tutorials.behaviours.ScanContext.terminate()`: reset the parameter to the cached value
55 |
56 |
57 | Context Switching
58 | -----------------
59 |
60 | .. graphviz:: dot/tutorial-six-context-switching-subtree.dot
61 | :align: center
62 |
63 | On entry into the parallel, the :class:`~py_trees_ros_tutorials.behaviours.ScanContext`
64 | behaviour will cache and switch
65 | the safety sensors parameter. While in the parallel it will return with
66 | :data:`~py_trees.common.Status.RUNNING` indefinitely. When the rotation
67 | action succeeds or fails, it will terminate the parallel and subsequently
68 | the :class:`~py_trees_ros_tutorials.behaviours.ScanContext` will terminate,
69 | resetting the safety sensors parameter to it's original value.
70 |
71 | Running
72 | ^^^^^^^
73 |
74 | .. code-block:: bash
75 |
76 | # Launch the tutorial
77 | $ ros2 launch py_trees_ros_tutorials tutorial_six_context_switching_launch.py
78 | # In another shell, watch the parameter as a context switch occurs
79 | $ watch -n 1 ros2 param get /safety_sensors enabled
80 | # Trigger scan requests from the qt dashboard
81 |
82 | .. image:: images/tutorial-six-context-switching.png
83 | """
84 |
85 | ##############################################################################
86 | # Imports
87 | ##############################################################################
88 |
89 | import operator
90 | import sys
91 |
92 | import launch
93 | import launch_ros
94 | import py_trees
95 | import py_trees_ros.trees
96 | import py_trees.console as console
97 | import py_trees_ros_interfaces.action as py_trees_actions # noqa
98 | import rclpy
99 |
100 | from . import behaviours
101 | from . import mock
102 |
103 | ##############################################################################
104 | # Launcher
105 | ##############################################################################
106 |
107 |
108 | def generate_launch_description():
109 | """
110 | Launcher for the tutorial.
111 |
112 | Returns:
113 | the launch description
114 | """
115 | return launch.LaunchDescription(
116 | mock.launch.generate_launch_nodes() +
117 | [
118 | launch_ros.actions.Node(
119 | package='py_trees_ros_tutorials',
120 | executable="tree-context-switching",
121 | output='screen',
122 | emulate_tty=True,
123 | )
124 | ]
125 | )
126 |
127 | ##############################################################################
128 | # Tutorial
129 | ##############################################################################
130 |
131 |
132 | def tutorial_create_root() -> py_trees.behaviour.Behaviour:
133 | """
134 | Insert a task between battery emergency and idle behaviours that
135 | controls a rotation action controller and notifications simultaenously
136 | to scan a room.
137 |
138 | Returns:
139 | the root of the tree
140 | """
141 | root = py_trees.composites.Parallel(
142 | name="Tutorial Six",
143 | policy=py_trees.common.ParallelPolicy.SuccessOnAll(
144 | synchronise=False
145 | )
146 | )
147 |
148 | topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
149 | scan2bb = py_trees_ros.subscribers.EventToBlackboard(
150 | name="Scan2BB",
151 | topic_name="/dashboard/scan",
152 | qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
153 | variable_name="event_scan_button"
154 | )
155 | battery2bb = py_trees_ros.battery.ToBlackboard(
156 | name="Battery2BB",
157 | topic_name="/battery/state",
158 | qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
159 | threshold=30.0
160 | )
161 | tasks = py_trees.composites.Selector("Tasks", memory=False)
162 | flash_red = behaviours.FlashLedStrip(
163 | name="Flash Red",
164 | colour="red"
165 | )
166 |
167 | # Emergency Tasks
168 | def check_battery_low_on_blackboard(blackboard: py_trees.blackboard.Blackboard) -> bool:
169 | return blackboard.battery_low_warning
170 |
171 | battery_emergency = py_trees.decorators.EternalGuard(
172 | name="Battery Low?",
173 | condition=check_battery_low_on_blackboard,
174 | blackboard_keys={"battery_low_warning"},
175 | child=flash_red
176 | )
177 | # Worker Tasks
178 | scan = py_trees.composites.Sequence(name="Scan", memory=True)
179 | is_scan_requested = py_trees.behaviours.CheckBlackboardVariableValue(
180 | name="Scan?",
181 | check=py_trees.common.ComparisonExpression(
182 | variable="event_scan_button",
183 | value=True,
184 | operator=operator.eq
185 | )
186 | )
187 | scan_preempt = py_trees.composites.Selector(name="Preempt?", memory=False)
188 | is_scan_requested_two = py_trees.decorators.SuccessIsRunning(
189 | name="SuccessIsRunning",
190 | child=py_trees.behaviours.CheckBlackboardVariableValue(
191 | name="Scan?",
192 | check=py_trees.common.ComparisonExpression(
193 | variable="event_scan_button",
194 | value=True,
195 | operator=operator.eq
196 | )
197 | )
198 | )
199 | scanning = py_trees.composites.Parallel(
200 | name="Scanning",
201 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
202 | )
203 | scan_context_switch = behaviours.ScanContext("Context Switch")
204 | scan_rotate = py_trees_ros.actions.ActionClient(
205 | name="Rotate",
206 | action_type=py_trees_actions.Rotate,
207 | action_name="rotate",
208 | action_goal=py_trees_actions.Rotate.Goal(),
209 | generate_feedback_message=lambda msg: "{:.2f}%%".format(msg.feedback.percentage_completed)
210 | )
211 | flash_blue = behaviours.FlashLedStrip(
212 | name="Flash Blue",
213 | colour="blue"
214 | )
215 | scan_celebrate = py_trees.composites.Parallel(
216 | name="Celebrate",
217 | policy=py_trees.common.ParallelPolicy.SuccessOnOne()
218 | )
219 | flash_green = behaviours.FlashLedStrip(name="Flash Green", colour="green")
220 | scan_pause = py_trees.timers.Timer("Pause", duration=3.0)
221 | # Fallback task
222 | idle = py_trees.behaviours.Running(name="Idle")
223 |
224 | root.add_child(topics2bb)
225 | topics2bb.add_children([scan2bb, battery2bb])
226 | root.add_child(tasks)
227 | tasks.add_children([battery_emergency, scan, idle])
228 | scan.add_children([is_scan_requested, scan_preempt, scan_celebrate])
229 | scan_preempt.add_children([is_scan_requested_two, scanning])
230 | scanning.add_children([scan_context_switch, scan_rotate, flash_blue])
231 | scan_celebrate.add_children([flash_green, scan_pause])
232 | return root
233 |
234 |
235 | def tutorial_main():
236 | """
237 | Entry point for the demo script.
238 | """
239 | rclpy.init(args=None)
240 | root = tutorial_create_root()
241 | tree = py_trees_ros.trees.BehaviourTree(
242 | root=root,
243 | unicode_tree_debug=True
244 | )
245 | try:
246 | tree.setup(timeout=15)
247 | except py_trees_ros.exceptions.TimedOutError as e:
248 | console.logerror(console.red + "failed to setup the tree, aborting [{}]".format(str(e)) + console.reset)
249 | tree.shutdown()
250 | rclpy.try_shutdown()
251 | sys.exit(1)
252 | except KeyboardInterrupt:
253 | # not a warning, nor error, usually a user-initiated shutdown
254 | console.logerror("tree setup interrupted")
255 | tree.shutdown()
256 | rclpy.try_shutdown()
257 | sys.exit(1)
258 |
259 | tree.tick_tock(period_ms=1000.0)
260 |
261 | try:
262 | rclpy.spin(tree.node)
263 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
264 | pass
265 | finally:
266 | tree.shutdown()
267 | rclpy.try_shutdown()
268 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/two_battery_check.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
6 | #
7 | ##############################################################################
8 | # Documentation
9 | ##############################################################################
10 |
11 | """
12 | About
13 | ^^^^^
14 |
15 | Here we add the first decision. What to do if the battery is low? For this,
16 | we’ll get the mocked robot to flash a notification over it’s led strip.
17 |
18 | Tree
19 | ^^^^
20 |
21 | .. code-block:: bash
22 |
23 | $ py-trees-render -b py_trees_ros_tutorials.two_battery_check.tutorial_create_root
24 |
25 | .. graphviz:: dot/tutorial-two-battery-check.dot
26 | :align: center
27 |
28 | .. literalinclude:: ../py_trees_ros_tutorials/two_battery_check.py
29 | :language: python
30 | :linenos:
31 | :lines: 122-166
32 | :caption: two_battery_check.py#tutorial_create_root
33 |
34 | Here we’ve added a high priority branch for dealing with a low battery
35 | that causes the hardware strip to flash. The :class:`py_trees.decorators.EternalGuard`
36 | enables a continuous check of the battery reading and subsequent termination of
37 | the flashing strip as soon as the battery level has recovered sufficiently.
38 | We could have equivalently made use of the :class:`py_trees.idioms.eternal_guard` idiom,
39 | which yields a more verbose, but explicit tree and would also allow direct use of
40 | the :class:`py_trees.blackboard.CheckBlackboardVariable` class as the conditional check.
41 |
42 | Behaviours
43 | ^^^^^^^^^^
44 |
45 | This tree makes use of the :class:`py_trees_ros_tutorials.behaviours.FlashLedStrip` behaviour.
46 |
47 | .. literalinclude:: ../py_trees_ros_tutorials/behaviours.py
48 | :language: python
49 | :linenos:
50 | :lines: 29-110
51 | :caption: behaviours.py#FlashLedStrip
52 |
53 | This is a typical ROS behaviour that accepts a ROS node on setup. This delayed style is
54 | preferred since it allows simple construction of the behaviour, in a tree, sans all of the
55 | ROS plumbing - useful when rendering dot graphs of the tree without having a ROS runtime
56 | around.
57 |
58 | The rest of the behaviour too, is fairly conventional:
59 |
60 | * ROS plumbing (i.e. the publisher) instantiated in setup()
61 | * Flashing notifications published in update()
62 | * The reset notification published when the behaviour is terminated
63 |
64 | Running
65 | ^^^^^^^
66 |
67 | .. code-block:: bash
68 |
69 | $ ros2 launch py_trees_ros_tutorials tutorial_two_battery_check_launch.py
70 |
71 | Then play with the battery slider in the qt dashboard to trigger the decision
72 | branching in the tree.
73 |
74 | .. image:: images/tutorial-two-battery-check.png
75 | """
76 |
77 | ##############################################################################
78 | # Imports
79 | ##############################################################################
80 |
81 | import launch
82 | import launch_ros
83 | import py_trees
84 | import py_trees_ros.trees
85 | import py_trees.console as console
86 | import rclpy
87 | import sys
88 |
89 | from . import behaviours
90 | from . import mock
91 |
92 | ##############################################################################
93 | # Launcher
94 | ##############################################################################
95 |
96 |
97 | def generate_launch_description():
98 | """
99 | Launcher for the tutorial.
100 |
101 | Returns:
102 | the launch description
103 | """
104 | return launch.LaunchDescription(
105 | mock.launch.generate_launch_nodes() +
106 | [
107 | launch_ros.actions.Node(
108 | package='py_trees_ros_tutorials',
109 | executable="tree-battery-check",
110 | output='screen',
111 | emulate_tty=True,
112 | )
113 | ]
114 | )
115 |
116 | ##############################################################################
117 | # Tutorial
118 | ##############################################################################
119 |
120 |
121 | def tutorial_create_root() -> py_trees.behaviour.Behaviour:
122 | """
123 | Create a basic tree with a battery to blackboard writer and a
124 | battery check that flashes the LEDs on the mock robot if the
125 | battery level goes low.
126 |
127 | Returns:
128 | the root of the tree
129 | """
130 | root = py_trees.composites.Parallel(
131 | name="Tutorial Two",
132 | policy=py_trees.common.ParallelPolicy.SuccessOnAll(
133 | synchronise=False
134 | )
135 | )
136 |
137 | topics2bb = py_trees.composites.Sequence(name="Topics2BB", memory=True)
138 | battery2bb = py_trees_ros.battery.ToBlackboard(
139 | name="Battery2BB",
140 | topic_name="/battery/state",
141 | qos_profile=py_trees_ros.utilities.qos_profile_unlatched(),
142 | threshold=30.0
143 | )
144 | tasks = py_trees.composites.Selector("Tasks", memory=False)
145 | flash_led_strip = behaviours.FlashLedStrip(
146 | name="FlashLEDs",
147 | colour="red"
148 | )
149 |
150 | def check_battery_low_on_blackboard(blackboard: py_trees.blackboard.Blackboard) -> bool:
151 | return blackboard.battery_low_warning
152 |
153 | battery_emergency = py_trees.decorators.EternalGuard(
154 | name="Battery Low?",
155 | condition=check_battery_low_on_blackboard,
156 | blackboard_keys={"battery_low_warning"},
157 | child=flash_led_strip
158 | )
159 | idle = py_trees.behaviours.Running(name="Idle")
160 |
161 | root.add_child(topics2bb)
162 | topics2bb.add_child(battery2bb)
163 | root.add_child(tasks)
164 | tasks.add_children([battery_emergency, idle])
165 | return root
166 |
167 |
168 | def tutorial_main():
169 | """
170 | Entry point for the demo script.
171 | """
172 | rclpy.init(args=None)
173 | root = tutorial_create_root()
174 | tree = py_trees_ros.trees.BehaviourTree(
175 | root=root,
176 | unicode_tree_debug=True
177 | )
178 | try:
179 | tree.setup(timeout=15)
180 | except py_trees_ros.exceptions.TimedOutError as e:
181 | console.logerror(console.red + "failed to setup the tree, aborting [{}]".format(str(e)) + console.reset)
182 | tree.shutdown()
183 | rclpy.try_shutdown()
184 | sys.exit(1)
185 | except KeyboardInterrupt:
186 | # not a warning, nor error, usually a user-initiated shutdown
187 | console.logerror("tree setup interrupted")
188 | tree.shutdown()
189 | rclpy.try_shutdown()
190 | sys.exit(1)
191 |
192 | tree.tick_tock(period_ms=1000.0)
193 |
194 | try:
195 | rclpy.spin(tree.node)
196 | except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
197 | pass
198 | finally:
199 | tree.shutdown()
200 | rclpy.try_shutdown()
201 |
--------------------------------------------------------------------------------
/py_trees_ros_tutorials/version.py:
--------------------------------------------------------------------------------
1 | #
2 | # License: BSD
3 | # https://github.com/splintered-reality/py_trees_ros_tutorials/raw/devel/LICENSE
4 | #
5 | ##############################################################################
6 | # Documentation
7 | ##############################################################################
8 |
9 | """
10 | Version number accessible to users of the package.
11 | """
12 |
13 | ##############################################################################
14 | # Version
15 | ##############################################################################
16 |
17 | __version__ = '2.1.0'
18 |
--------------------------------------------------------------------------------
/resources/py_trees_ros_tutorials:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splintered-reality/py_trees_ros_tutorials/572fdfff24cfec612790b9cfa05971d291ea6506/resources/py_trees_ros_tutorials
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [pep8]
2 | max-line-length=299
3 |
4 |
5 | [aliases]
6 | test=pytest
7 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 |
5 | from distutils import log
6 | from setuptools import find_packages, setup
7 | from setuptools.command.develop import develop
8 | from setuptools.command.install import install
9 |
10 | package_name = 'py_trees_ros_tutorials'
11 |
12 |
13 | # This is somewhat dodgy as it will escape any override from, e.g. the command
14 | # line or a setup.cfg configuration. It does however, get us around the problem
15 | # of setup.cfg influencing requirements install on rtd installs
16 | #
17 | # TODO: should be a way of detecting whether scripts_dir has been influenced
18 | # from outside
19 | def redirect_install_dir(command_subclass):
20 |
21 | original_run = command_subclass.run
22 |
23 | def modified_run(self):
24 | try:
25 | old_script_dir = self.script_dir # develop
26 | except AttributeError:
27 | old_script_dir = self.install_scripts # install
28 | # TODO: A more intelligent way of stitching this together...
29 | # Warning: script_dir is typically a 'bin' path alongside the
30 | # lib path, if ever that is somewhere wildly different, this
31 | # will break.
32 | # Note: Consider making use of self.prefix, but in some cases
33 | # that is mislading, e.g. points to /usr when actually
34 | # everything goes to /usr/local
35 | new_script_dir = os.path.abspath(
36 | os.path.join(
37 | old_script_dir, os.pardir, 'lib', package_name
38 | )
39 | )
40 | log.info("redirecting scripts")
41 | log.info(" from: {}".format(old_script_dir))
42 | log.info(" to: {}".format(new_script_dir))
43 | if hasattr(self, "script_dir"):
44 | self.script_dir = new_script_dir # develop
45 | else:
46 | self.install_scripts = new_script_dir # install
47 | original_run(self)
48 |
49 | command_subclass.run = modified_run
50 | return command_subclass
51 |
52 |
53 | @redirect_install_dir
54 | class OverrideDevelop(develop):
55 | pass
56 |
57 |
58 | @redirect_install_dir
59 | class OverrideInstall(install):
60 | pass
61 |
62 |
63 | def gather_launch_files():
64 | data_files = []
65 | for root, unused_subdirs, files in os.walk('launch'):
66 | destination = os.path.join('share', package_name, root)
67 | launch_files = []
68 | for file in files:
69 | pathname = os.path.join(root, file)
70 | launch_files.append(pathname)
71 | data_files.append((destination, launch_files))
72 | return data_files
73 |
74 |
75 | setup(
76 | cmdclass={
77 | 'develop': OverrideDevelop,
78 | 'install': OverrideInstall
79 | },
80 | name=package_name,
81 | # also update package.xml (version and website url), version.py and conf.py
82 | version='2.3.0',
83 | packages=find_packages(exclude=['tests*', 'docs*', 'launch*']),
84 | data_files=[
85 | ('share/' + package_name, ['package.xml']),
86 | ('share/ament_index/resource_index/packages', [
87 | 'resources/py_trees_ros_tutorials']),
88 | ] + gather_launch_files(),
89 | package_data={'py_trees_ros_tutorials': ['mock/gui/*']},
90 | install_requires=[], # it's all lies (c.f. package.xml, but no use case for this yet)
91 | extras_require={},
92 | author='Daniel Stonier',
93 | maintainer='Daniel Stonier , Sebastian Castro ',
94 | url='https://github.com/splintered-reality/py_trees_ros_tutorials',
95 | keywords=['ROS', 'ROS2', 'behaviour-trees'],
96 | zip_safe=True,
97 | classifiers=[
98 | 'Intended Audience :: Developers',
99 | 'License :: OSI Approved :: BSD License',
100 | 'Programming Language :: Python',
101 | 'Topic :: Scientific/Engineering :: Artificial Intelligence',
102 | 'Topic :: Software Development :: Libraries'
103 | ],
104 | description=(
105 | "Tutorials for py_trees on ROS2."
106 | ),
107 | long_description=(
108 | "Tutorials demonstrating usage of py_trees in ROS and more generally,"
109 | "behaviour trees for robotics."
110 | ),
111 | license='BSD',
112 | # test_suite="tests"
113 | # tests_require=['nose', 'pytest', 'flake8', 'yanc', 'nose-htmloutput']
114 | entry_points={
115 | 'console_scripts': [
116 | # Mocks
117 | 'mock-battery = py_trees_ros_tutorials.mock.battery:main',
118 | 'mock-dashboard = py_trees_ros_tutorials.mock.dashboard:main',
119 | 'mock-docking-controller = py_trees_ros_tutorials.mock.dock:main',
120 | 'mock-led-strip = py_trees_ros_tutorials.mock.led_strip:main',
121 | 'mock-move-base = py_trees_ros_tutorials.mock.move_base:main',
122 | 'mock-rotation-controller = py_trees_ros_tutorials.mock.rotate:main',
123 | 'mock-safety-sensors = py_trees_ros_tutorials.mock.safety_sensors:main',
124 | # Mock Tests
125 | 'mock-dock-client = py_trees_ros_tutorials.mock.actions:dock_client',
126 | 'mock-move-base-client = py_trees_ros_tutorials.mock.actions:move_base_client',
127 | 'mock-rotate-client = py_trees_ros_tutorials.mock.actions:rotate_client',
128 | # Tutorial Nodes
129 | 'tree-data-gathering = py_trees_ros_tutorials.one_data_gathering:tutorial_main',
130 | 'tree-battery-check = py_trees_ros_tutorials.two_battery_check:tutorial_main',
131 | 'tree-action-clients = py_trees_ros_tutorials.five_action_clients:tutorial_main',
132 | 'tree-context-switching = py_trees_ros_tutorials.six_context_switching:tutorial_main',
133 | 'tree-docking-cancelling-failing = py_trees_ros_tutorials.seven_docking_cancelling_failing:tutorial_main',
134 | 'tree-dynamic-application-loading = py_trees_ros_tutorials.eight_dynamic_application_loading:tutorial_main',
135 | ],
136 | },
137 | )
138 |
--------------------------------------------------------------------------------
/testies:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import py_trees
5 | import py_trees_ros_tutorials
6 | import rclpy
7 | import rcl_interfaces.srv as rcl_srvs
8 | import time
9 |
10 | behaviour = py_trees_ros_tutorials.behaviours.ScanContext(
11 | name="ContextSwitch"
12 | )
13 |
14 | rclpy.init()
15 | print("create node")
16 | node = rclpy.create_node("scan_context")
17 |
18 | # time.sleep(1.0)
19 |
20 | # print("Create client")
21 | # client = node.create_client(
22 | # rcl_srvs.GetParameters,
23 | # '/safety_sensors/get_parameters'
24 | # )
25 | # ready = client.wait_for_service(timeout_sec=3.0)
26 | # if not ready:
27 | # raise RuntimeError('Wait for service timed out')
28 | #
29 | # print("Create request")
30 | # request = rcl_srvs.GetParameters.Request()
31 | # request.names.append("enabled")
32 | # future = client.call_async(request)
33 | # print("Future: %s" % future.__dict__)
34 | # rclpy.spin_until_future_complete(node, future)
35 | # print("Retrieived")
36 | # if future.result() is not None:
37 | # node.get_logger().info(
38 | # 'Result of /safety_sensors/enabled: {}'.format(future.result().enabled)
39 | # )
40 | # feedback_message = "retrieved the safety sensors context"
41 | # cached_context = future.result().enabled
42 | # else:
43 | # feedback_message = "failed to retrieve the safety sensors context"
44 | # node.get_logger().error(feedback_message)
45 | # node.get_logger().info('Service call failed %r' % (future.exception(),))
46 |
47 | behaviour.setup(node=node)
48 | behaviour.initialise()
49 | print("Initialised")
50 | time.sleep(5.0)
51 | behaviour.terminate(new_status=py_trees.common.Status.INVALID)
52 |
53 | rclpy.shutdown()
54 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # Executing Tests
2 |
3 | ```bash
4 | # run all tests in the current directory
5 | $ pytest-3
6 | # run all tests with full stdout (-s / --capture=no)
7 | $ pytest-3 -s
8 | # run a single test
9 | $ pytest-3 -s test_alakazam.py
10 | # run using setuptools
11 | $ python3 setup.py test
12 | ```
13 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import unittest
3 |
4 |
5 | class ImportTest(unittest.TestCase):
6 | def test_import(self) -> None:
7 | """
8 | This test serves to make the buildfarm happy in Python 3.12 and later.
9 | See https://github.com/colcon/colcon-core/issues/678 for more information.
10 | """
11 | assert importlib.util.find_spec("py_trees_ros_tutorials")
12 |
--------------------------------------------------------------------------------
/tests/test_actions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
6 | #
7 |
8 | ##############################################################################
9 | # Imports
10 | ##############################################################################
11 |
12 | import action_msgs.msg as action_msgs # GoalStatus
13 | import py_trees
14 | import py_trees.console as console
15 | import rclpy
16 | import rclpy.executors
17 | import time
18 |
19 | import py_trees_ros_tutorials.mock as mock
20 |
21 | ##############################################################################
22 | # Helpers
23 | ##############################################################################
24 |
25 |
26 | def assert_banner():
27 | print(console.green + "----- Asserts -----" + console.reset)
28 |
29 |
30 | def assert_details(text, expected, result):
31 | print(console.green + text +
32 | "." * (40 - len(text)) +
33 | console.cyan + "{}".format(expected) +
34 | console.yellow + " [{}]".format(result) +
35 | console.reset)
36 |
37 |
38 | def setup_module(module):
39 | console.banner("ROS Init")
40 | rclpy.init()
41 |
42 |
43 | def teardown_module(module):
44 | console.banner("ROS Shutdown")
45 | rclpy.shutdown()
46 |
47 |
48 | def timeout():
49 | return 3.0
50 |
51 |
52 | def number_of_iterations():
53 | return 100
54 |
55 | ##############################################################################
56 | # Success
57 | ##############################################################################
58 |
59 |
60 | def generic_success_test(
61 | title,
62 | server,
63 | client
64 | ):
65 | console.banner(title)
66 |
67 | executor = rclpy.executors.MultiThreadedExecutor(num_threads=4)
68 | executor.add_node(server.node)
69 | executor.add_node(client.node)
70 |
71 | # Send goal and await future
72 | client.setup()
73 | goal_future = client.send_goal()
74 | start_time = time.monotonic()
75 | while (time.monotonic() - start_time) < timeout():
76 | executor.spin_once(timeout_sec=0.1)
77 | if goal_future.result() is not None:
78 | break
79 | assert_banner()
80 | assert_details("goal_future.result()", "!None", goal_future.result())
81 | assert(goal_future.result() is not None)
82 | print("goal_future.result().accepted.....True [{}]".format(
83 | goal_future.result().accepted)
84 | )
85 | assert(goal_future.result().accepted)
86 | goal_handle = goal_future.result()
87 |
88 | # Await goal result future
89 | result_future = goal_handle.get_result_async()
90 | start_time = time.monotonic()
91 | while (time.monotonic() - start_time) < timeout():
92 | executor.spin_once(timeout_sec=0.1)
93 | if result_future.done():
94 | break
95 |
96 | assert_banner()
97 | assert_details("result_future.done()", "True", result_future.done())
98 | assert(result_future.done())
99 | assert_details(
100 | "result_future.result().status",
101 | "STATUS_SUCCEEDED",
102 | client.status_strings[result_future.result().status]
103 | )
104 | assert(result_future.result().status ==
105 | action_msgs.GoalStatus.STATUS_SUCCEEDED) # noqa
106 | executor.shutdown()
107 | server.shutdown()
108 | client.shutdown()
109 |
110 |
111 | def test_move_base_success():
112 | generic_success_test(
113 | title="MoveBase Success",
114 | server=mock.move_base.MoveBase(duration=0.5),
115 | client=mock.actions.MoveBaseClient())
116 |
117 |
118 | def test_dock_success():
119 | generic_success_test(
120 | title="Dock Success",
121 | server=mock.dock.Dock(duration=0.5),
122 | client=mock.actions.DockClient())
123 |
124 |
125 | def test_rotate_success():
126 | generic_success_test(
127 | title="Rotate Success",
128 | server=mock.rotate.Rotate(rotation_rate=3.14),
129 | client=mock.actions.RotateClient())
130 |
131 | ##############################################################################
132 | # Preemption
133 | ##############################################################################
134 |
135 |
136 | def generic_preemption_test(
137 | title,
138 | server,
139 | client
140 | ):
141 | console.banner(title)
142 |
143 | executor = rclpy.executors.MultiThreadedExecutor(num_threads=4)
144 | executor.add_node(server.node)
145 | executor.add_node(client.node)
146 |
147 | # Send goal and await future
148 | client.setup()
149 | goal_future = client.send_goal()
150 | start_time = time.monotonic()
151 | while (time.monotonic() - start_time) < timeout():
152 | executor.spin_once(timeout_sec=0.1)
153 | if goal_future.result() is not None:
154 | break
155 | assert_banner()
156 | assert_details("goal_future.result()", "!None", goal_future.result())
157 | assert(goal_future.result() is not None)
158 | print("goal_future.result().accepted.....True [{}]".format(
159 | goal_future.result().accepted)
160 | )
161 | assert(goal_future.result().accepted)
162 | goal_handle = goal_future.result()
163 |
164 | # preempt with another goal
165 | unused_next_goal_future = client.action_client.send_goal_async(
166 | client.action_type.Goal()
167 | )
168 |
169 | # Await preempted goal result future
170 | result_future = goal_handle.get_result_async()
171 | start_time = time.monotonic()
172 | while (time.monotonic() - start_time) < timeout():
173 | executor.spin_once(timeout_sec=0.1)
174 | if result_future.done():
175 | break
176 |
177 | assert_banner()
178 | assert_details("result_future.done()", "True", result_future.done())
179 | assert(result_future.done())
180 | assert_details(
181 | "result_future.result().status",
182 | "STATUS_ABORTED",
183 | client.status_strings[result_future.result().status]
184 | )
185 | assert(result_future.result().status ==
186 | action_msgs.GoalStatus.STATUS_ABORTED) # noqa
187 |
188 | # Somewhat uncertain how this shutdown works.
189 | # The action server shutdown calls action_server.destroy()
190 | # which destroys goal handles and removes the action server
191 | # as a waitable on the node queue, however....there still
192 | # exists unexplicable mysteries
193 | #
194 | # - if executor.shutdown() is first, why doesn't it wait?
195 | # - why does the action server itself hang around till the
196 | # execute() task is done/aborted instead of crashing?
197 | executor.shutdown()
198 | server.shutdown()
199 | client.shutdown()
200 |
201 |
202 | def test_dock_preemption():
203 | generic_preemption_test(
204 | title="Dock Preemption",
205 | server=mock.dock.Dock(duration=1.5),
206 | client=mock.actions.DockClient())
207 |
208 | ##############################################################################
209 | # Cancel
210 | ##############################################################################
211 |
212 |
213 | def generic_cancel_test(
214 | title,
215 | server,
216 | client
217 | ):
218 | console.banner(title)
219 |
220 | executor = rclpy.executors.MultiThreadedExecutor(num_threads=4)
221 | executor.add_node(server.node)
222 | executor.add_node(client.node)
223 |
224 | # Send goal and await future
225 | client.setup()
226 | goal_future = client.send_goal()
227 | start_time = time.monotonic()
228 | while (time.monotonic() - start_time) < timeout():
229 | executor.spin_once(timeout_sec=0.1)
230 | if goal_future.result() is not None:
231 | break
232 | assert_banner()
233 | assert_details("goal_future.result()", "!None", goal_future.result())
234 | assert(goal_future.result() is not None)
235 | print("goal_future.result().accepted.....True [{}]".format(
236 | goal_future.result().accepted)
237 | )
238 | assert(goal_future.result().accepted)
239 | goal_handle = goal_future.result()
240 |
241 | # cancel
242 | _timer = client.node.create_timer(0.05, client.send_cancel_request)
243 |
244 | # Await preempted goal result future
245 | # Note: it's going to spam cancel requests, but that's ok
246 | # even desirable to make sure it doesn't flake out
247 | result_future = goal_handle.get_result_async()
248 | start_time = time.monotonic()
249 | while (time.monotonic() - start_time) < timeout():
250 | executor.spin_once(timeout_sec=0.1)
251 | if result_future.done():
252 | _timer.cancel()
253 | client.node.destroy_timer(_timer)
254 | break
255 |
256 | assert_banner()
257 | assert_details("result_future.done()", "True", result_future.done())
258 | assert(result_future.done())
259 | assert_details(
260 | "result_future.result().status",
261 | "STATUS_CANCELED",
262 | client.status_strings[result_future.result().status]
263 | )
264 | assert(result_future.result().status ==
265 | action_msgs.GoalStatus.STATUS_CANCELED) # noqa
266 |
267 | executor.shutdown()
268 | server.shutdown()
269 | client.shutdown()
270 |
271 |
272 | def test_dock_cancel():
273 | generic_cancel_test(
274 | title="Dock Cancel",
275 | server=mock.dock.Dock(duration=0.5),
276 | client=mock.actions.DockClient())
277 |
--------------------------------------------------------------------------------
/tests/test_led_strip.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # License: BSD
5 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
6 | #
7 |
8 | ##############################################################################
9 | # Imports
10 | ##############################################################################
11 |
12 | import py_trees
13 | import py_trees.console as console
14 | import py_trees_ros_tutorials
15 | import rclpy
16 | import rclpy.executors
17 |
18 | ##############################################################################
19 | # Helpers
20 | ##############################################################################
21 |
22 |
23 | def assert_banner():
24 | print(console.green + "----- Asserts -----" + console.reset)
25 |
26 |
27 | def assert_details(text, expected, result):
28 | print(console.green + text +
29 | "." * (40 - len(text)) +
30 | console.cyan + "{}".format(expected) +
31 | console.yellow + " [{}]".format(result) +
32 | console.reset)
33 |
34 |
35 | def setup_module(module):
36 | console.banner("ROS Init")
37 | rclpy.init()
38 |
39 |
40 | def teardown_module(module):
41 | console.banner("ROS Shutdown")
42 | rclpy.shutdown()
43 |
44 |
45 | def timeout():
46 | return 3.0
47 |
48 |
49 | def number_of_iterations():
50 | return 40
51 |
52 | ##############################################################################
53 | # Tests
54 | ##############################################################################
55 |
56 |
57 | def test_led_strip():
58 | console.banner("Client Success")
59 |
60 | mock_led_strip = py_trees_ros_tutorials.mock.led_strip.LEDStrip()
61 | tree_node = rclpy.create_node("tree")
62 | flash_led_strip = py_trees_ros_tutorials.behaviours.FlashLedStrip(name="Flash")
63 | flash_led_strip.setup(node=tree_node)
64 |
65 | executor = rclpy.executors.MultiThreadedExecutor(num_threads=4)
66 | executor.add_node(mock_led_strip.node)
67 | executor.add_node(tree_node)
68 |
69 | assert_banner()
70 |
71 | # send flashing led
72 | spin_iterations = 0
73 | while spin_iterations < number_of_iterations() and flash_led_strip.colour not in mock_led_strip.last_text:
74 | flash_led_strip.tick_once()
75 | executor.spin_once(timeout_sec=0.05)
76 | spin_iterations += 1
77 |
78 | assert_details("flashing", flash_led_strip.colour, flash_led_strip.colour if flash_led_strip.colour in mock_led_strip.last_text else mock_led_strip.last_text)
79 | assert(flash_led_strip.colour in mock_led_strip.last_text)
80 |
81 | # cancel
82 | flash_led_strip.stop(new_status=py_trees.common.Status.INVALID)
83 | spin_iterations = 0
84 | while spin_iterations < number_of_iterations() and mock_led_strip.last_text:
85 | executor.spin_once(timeout_sec=0.05)
86 | spin_iterations += 1
87 |
88 | assert_details("cancelled", "", mock_led_strip.last_text)
89 | assert("" == mock_led_strip.last_text)
90 |
91 | executor.shutdown()
92 | tree_node.destroy_node()
93 | mock_led_strip.node.destroy_node()
94 |
--------------------------------------------------------------------------------