├── .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 | 39 | 40 | 41 | 0 42 | 0 43 | 551 44 | 29 45 | 46 | 47 | 48 | false 49 | 50 | 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 | --------------------------------------------------------------------------------