├── README.md
├── pi_trees
├── CHANGELOG.rst
├── CMakeLists.txt
└── package.xml
├── pi_trees_lib
├── CHANGELOG.rst
├── CMakeLists.txt
├── examples
│ ├── composite_tasks.py
│ ├── launch
│ │ └── patrol_tree.launch
│ ├── parallel_example.py
│ ├── patrol_tree.py
│ └── weighted_random_example.py
├── package.xml
├── setup.py
└── src
│ └── pi_trees_lib
│ ├── __init__.py
│ ├── pi_trees_lib.py
│ └── task_setup.py
└── pi_trees_ros
├── CHANGELOG.rst
├── CMakeLists.txt
├── package.xml
├── setup.py
└── src
└── pi_trees_ros
├── __init__.py
└── pi_trees_ros.py
/README.md:
--------------------------------------------------------------------------------
1 | **PLEASE NOTE: This code is depreciated and will not be maintained or developed further.**
2 |
3 | A Python/ROS package for implementing Behavior Trees.
4 |
--------------------------------------------------------------------------------
/pi_trees/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | Changelog for package pi_trees
3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | 0.1.2 (2016-09-25)
6 | ------------------
7 | * Updated dependencies in package.xml files
8 | * Contributors: Patrick Goebel
9 |
10 | 0.1.1 (2014-08-01)
11 | ------------------
12 | * Added blank line to end of CMakeLists.txt
13 | * Added initial CHANGELOG.rst files
14 | * Cleanup
15 | * Initial commit
16 | * Contributors: Patrick Goebel
17 |
--------------------------------------------------------------------------------
/pi_trees/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 2.8.3)
2 | project(pi_trees)
3 | find_package(catkin REQUIRED)
4 | catkin_metapackage()
5 |
--------------------------------------------------------------------------------
/pi_trees/package.xml:
--------------------------------------------------------------------------------
1 |
2 | pi_trees
3 | 0.1.1
4 |
5 | Behavior Trees for ROS
6 |
7 | Patrick Goebel
8 | BSD
9 | http://ros.org/wiki/pi_trees
10 | https://github.com/pirobot/pi_trees/issues
11 | Patrick Goebel
12 |
13 |
14 | catkin
15 |
16 |
17 | pi_trees_lib
18 | pi_trees_ros
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/pi_trees_lib/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | Changelog for package pi_trees_lib
3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | 0.1.2 (2016-09-25)
6 | ------------------
7 | * Added error message when number of random weights do not match number of children
8 | * Added weighted_random_example.py
9 | * Added weighted versions of random selector, sequence and iterator
10 | * Added composite_task.py example
11 | * Added try-except to print_tree function
12 | * Replaced print_tree function to fix some pathological cases
13 | * Fixed syntax errors in WaitTask
14 | * Merge branch 'indigo-devel' of https://github.com/pirobot/pi_trees into indigo-devel
15 | Fixed typo in WaitTask
16 | * Fixed reset_after behavior for Sequence and Selector tasks
17 | * typo for waittask
18 | * Added root level status (BEHAVE.status) to the main loop so that root node status would be colored appropriately
19 | * Removed old parallel tasks (commented out earlier)
20 | * Fixed ParallelOne and ParallelAll tasks to really execute each child task on every iteration
21 | * Fixed reset_after status for Sequence
22 | * Added status colors to root node for DOT graph
23 | * Added parallelogram shape for ParallelOne and ParallelAll tasks
24 | * Added task_setup.py for use with patrol_tree.py example
25 | * Added root node status color to display
26 | * Updated the patrol_tree.py sample script to check battery level only when not recharging
27 | * Changed rate from 10 to 5 for patrol_tree.launch example
28 | * Added patrol example to pi_trees_lib examples
29 | * Modified print_dot_tree() function to write dotfile only if tree has changed since last write
30 | * Fixed initial value of last_dot_tree from None to the empty string
31 | * Added pi_trees_viewer suppot to pi_trees_lib
32 | * Added reset_after option to other composite tasks
33 | * Added a few more task decorators and the reset_after option (defaults to False) to reset any task after execution if set True
34 | * Added ASCII symbols option to graph representation and improved the print_dot_tree() output
35 | * Added UntilFail and until_fail() decorators
36 | * Fixed bug in task_not() decorator and added invert alias
37 | * Fixed bug in TaskNot decorator and add Invert alias
38 | * Added limit() decorator
39 | * Added 1 to the displayed loop count for the loop() decorator to be consistent with the Limit decorator
40 | * Added Limit decorator
41 | * Added random versions of Sequence, Selector and Iterator
42 | * Added new CallbackTask to pi_trees_lib
43 | * Added new CallbackTask to pi_trees_lib
44 | * Added catkin_install_python line to CMakeLists.txt
45 | * Renamed scripts directory back to examples and parallel_tasks.py to parallel_example.py
46 | * Small reformatting of comment
47 | * Renamed examples directory to scripts and renamed counting.py example to parallel_tasks.py
48 | * Removed dependency on pygraph and pygraphviz for now
49 | * Updated dependencies in package.xml files
50 | * Contributors: Jack000, Patrick Goebel
51 |
52 | 0.1.1 (2014-08-01)
53 | ------------------
54 | * Added initial CHANGELOG.rst files
55 | * Removed print_dot_tree statement from counting.py example
56 | * Updates some comments
57 | * Updated counting.py example
58 | * Did some house cleaning
59 | * Cleanup
60 | * Initial commit
61 | * Contributors: Patrick Goebel
62 |
--------------------------------------------------------------------------------
/pi_trees_lib/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 2.8.3)
2 |
3 | project(pi_trees_lib)
4 |
5 | find_package(catkin REQUIRED)
6 |
7 | catkin_package()
8 |
9 | catkin_python_setup()
10 |
11 | catkin_install_python(PROGRAMS examples/parallel_example.py
12 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})
13 |
--------------------------------------------------------------------------------
/pi_trees_lib/examples/composite_tasks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from pi_trees_lib.pi_trees_lib import *
4 | import time
5 |
6 | class CompositeTasks():
7 | def __init__(self):
8 | # The root node
9 | BEHAVE = Sequence("behave")
10 |
11 | # Create a Selector composite task (returns SUCCESS as soon as any subtask returns SUCCESS)
12 | SELECTOR_TASK = Selector("Selector Count")
13 |
14 | # Create a Sequence composite task (returns FAULURE as soon as any subtask returns FAILURE)
15 | SEQUENCE_TASK = Sequence("Sequence Count")
16 |
17 | WAIT_TASK = WaitTask("Wait Task", 5)
18 |
19 | # Create three counting tasks
20 | COUNT2 = Count("Count+2", 1, 2, 1)
21 | COUNT5 = Count("Count-5", 5, 1, -1)
22 | COUNT16 = Count("Count+16", 1, 16, 1)
23 |
24 | # Add the tasks to the sequence composite task
25 | SEQUENCE_TASK.add_child(COUNT2)
26 | SEQUENCE_TASK.add_child(WAIT_TASK)
27 | SEQUENCE_TASK.add_child(COUNT5)
28 | SEQUENCE_TASK.add_child(COUNT16)
29 |
30 | # Add the tasks to the selector composite task
31 | SELECTOR_TASK.add_child(COUNT5)
32 | SELECTOR_TASK.add_child(COUNT2)
33 | SELECTOR_TASK.add_child(COUNT16)
34 |
35 | # Add the composite task to the root task
36 | BEHAVE.add_child(SEQUENCE_TASK)
37 | BEHAVE.add_child(SELECTOR_TASK)
38 |
39 | # Print a simple representation of the tree
40 | print "Behavior Tree Structure"
41 | print_tree(BEHAVE, use_symbols=True)
42 |
43 | # Run the tree
44 | while True:
45 | status = BEHAVE.run()
46 | if status == TaskStatus.SUCCESS:
47 | print "Finished running tree."
48 | break
49 |
50 | # A counting task that extends the base Task task
51 | class Count(Task):
52 | def __init__(self, name, start, stop, step, *args, **kwargs):
53 | super(Count, self).__init__(name, *args, **kwargs)
54 |
55 | self.name = name
56 | self.start = start
57 | self.stop = stop
58 | self.step = step
59 | self.count = self.start
60 | print "Creating task Count", self.start, self.stop, self.step
61 |
62 | def run(self):
63 | if abs(self.count - self.stop - self.step) <= 0:
64 | return TaskStatus.SUCCESS
65 | else:
66 | print self.name, self.count
67 | time.sleep(0.5)
68 | self.count += self.step
69 | if abs(self.count - self.stop - self.step) <= 0:
70 | return TaskStatus.SUCCESS
71 | else:
72 | return TaskStatus.RUNNING
73 |
74 |
75 | def reset(self):
76 | self.count = self.start
77 |
78 | if __name__ == '__main__':
79 | tree = CompositeTasks()
80 |
81 |
--------------------------------------------------------------------------------
/pi_trees_lib/examples/launch/patrol_tree.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/pi_trees_lib/examples/parallel_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from pi_trees_lib.pi_trees_lib import *
4 | import time
5 |
6 | class ParallelExample():
7 | def __init__(self):
8 | # The root node
9 | BEHAVE = Sequence("behave")
10 |
11 | # Create a ParallelOne composite task (returns SUCCESS as soon as any subtask returns SUCCESS)
12 | PARALLEL_TASKS = ParallelOne("Counting in Parallel")
13 |
14 | # Create three counting tasks
15 | COUNT2 = Count("Count+2", 1, 2, 1)
16 | COUNT5 = Count("Count-5", 5, 1, -1)
17 | COUNT16 = Count("Count+16", 1, 16, 1)
18 |
19 | # Add the tasks to the parallel composite task
20 | PARALLEL_TASKS.add_child(COUNT5)
21 | PARALLEL_TASKS.add_child(COUNT2)
22 | PARALLEL_TASKS.add_child(COUNT16)
23 |
24 | # Add the composite task to the root task
25 | BEHAVE.add_child(PARALLEL_TASKS)
26 |
27 | # Print a simple representation of the tree
28 | print "Behavior Tree Structure"
29 | print_tree(BEHAVE)
30 |
31 | # Run the tree
32 | while True:
33 | status = BEHAVE.run()
34 | if status == TaskStatus.SUCCESS:
35 | print "Finished running tree."
36 | break
37 |
38 | # A counting task that extends the base Task task
39 | class Count(Task):
40 | def __init__(self, name, start, stop, step, *args, **kwargs):
41 | super(Count, self).__init__(name, *args, **kwargs)
42 |
43 | self.name = name
44 | self.start = start
45 | self.stop = stop
46 | self.step = step
47 | self.count = self.start
48 | print "Creating task Count", self.start, self.stop, self.step
49 |
50 | def run(self):
51 | if abs(self.count - self.stop - self.step) <= 0:
52 | return TaskStatus.SUCCESS
53 | else:
54 | print self.name, self.count
55 | time.sleep(0.5)
56 | self.count += self.step
57 | if abs(self.count - self.stop - self.step) <= 0:
58 | return TaskStatus.SUCCESS
59 | else:
60 | return TaskStatus.RUNNING
61 |
62 |
63 | def reset(self):
64 | self.count = self.start
65 |
66 | if __name__ == '__main__':
67 | tree = ParallelExample()
68 |
69 |
--------------------------------------------------------------------------------
/pi_trees_lib/examples/patrol_tree.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import rospy
4 | from std_msgs.msg import Float32
5 | from geometry_msgs.msg import Twist
6 | from rbx2_msgs.srv import *
7 | from pi_trees_ros.pi_trees_ros import *
8 | from pi_trees_lib.task_setup import *
9 |
10 | class BlackBoard():
11 | def __init__(self):
12 | self.battery_level = None
13 | self.charging = None
14 |
15 | class Patrol():
16 | def __init__(self):
17 | rospy.init_node("patrol_tree")
18 |
19 | # Set the shutdown function (stop the robot)
20 | rospy.on_shutdown(self.shutdown)
21 |
22 | # Initialize a number of parameters and variables
23 | setup_task_environment(self)
24 |
25 | # Initialize the black board
26 | self.blackboard = BlackBoard()
27 |
28 | # How frequently do we "tic" the tree?
29 | rate = rospy.get_param('~rate', 10)
30 |
31 | # Convert tic rate to a ROS rate
32 | tic = rospy.Rate(rate)
33 |
34 | # Where should the DOT file be stored. Default location is $HOME/.ros/tree.dot
35 | dotfilepath = rospy.get_param('~dotfilepath', None)
36 |
37 | # Create a list to hold the move_base tasks
38 | MOVE_BASE_TASKS = list()
39 |
40 | n_waypoints = len(self.waypoints)
41 |
42 | # Create simple action navigation task for each waypoint
43 | for i in range(n_waypoints):
44 | goal = MoveBaseGoal()
45 | goal.target_pose.header.frame_id = 'map'
46 | goal.target_pose.header.stamp = rospy.Time.now()
47 | goal.target_pose.pose = self.waypoints[i % n_waypoints]
48 |
49 | move_base_task = SimpleActionTask("MOVE_BASE_TASK_" + str(i), "move_base", MoveBaseAction, goal, reset_after=False)
50 |
51 | MOVE_BASE_TASKS.append(move_base_task)
52 |
53 | # Set the docking station pose
54 | goal = MoveBaseGoal()
55 | goal.target_pose.header.frame_id = 'map'
56 | goal.target_pose.header.stamp = rospy.Time.now()
57 | goal.target_pose.pose = self.docking_station_pose
58 |
59 | # Assign the docking station pose to a move_base action task
60 | NAV_DOCK_TASK = SimpleActionTask("NAV_DOC_TASK", "move_base", MoveBaseAction, goal, reset_after=False)
61 |
62 | # Create the root node
63 | BEHAVE = Sequence("BEHAVE")
64 |
65 | # Create the "stay healthy" selector
66 | STAY_HEALTHY = Selector("STAY_HEALTHY")
67 |
68 | # Create the patrol loop decorator
69 | LOOP_PATROL = Loop("LOOP_PATROL", iterations=self.n_patrols)
70 |
71 | # Add the two subtrees to the root node in order of priority
72 | BEHAVE.add_child(STAY_HEALTHY)
73 | BEHAVE.add_child(LOOP_PATROL)
74 |
75 | # Create the patrol iterator
76 | PATROL = Iterator("PATROL")
77 |
78 | # Add the move_base tasks to the patrol task
79 | for task in MOVE_BASE_TASKS:
80 | PATROL.add_child(task)
81 |
82 | # Add the patrol to the loop decorator
83 | LOOP_PATROL.add_child(PATROL)
84 |
85 | # Add the battery check and recharge tasks to the "stay healthy" task
86 | with STAY_HEALTHY:
87 | # Monitor the fake battery level by subscribing to the /battery_level topic
88 | MONITOR_BATTERY = MonitorTask("MONITOR_BATTERY", "battery_level", Float32, self.monitor_battery)
89 |
90 | # Is the fake battery level below threshold?
91 | CHECK_BATTERY = CallbackTask("BATTERY_OK?", self.check_battery)
92 |
93 | # Set the fake battery level back to 100 using a ServiceTask
94 | CHARGE_COMPLETE = ServiceTask("CHARGE_COMPLETE", "/battery_simulator/set_battery_level", SetBatteryLevel, 100, result_cb=self.recharge_cb)
95 |
96 | # Sleep for the given interval to simulate charging
97 | CHARGING = RechargeRobot("CHARGING", interval=3, blackboard=self.blackboard)
98 |
99 | # Build the recharge sequence using inline construction
100 | RECHARGE = Sequence("RECHARGE", [NAV_DOCK_TASK, CHARGING, CHARGE_COMPLETE], reset_after=True)
101 |
102 | # Add the check battery and recharge tasks to the stay healthy selector
103 | STAY_HEALTHY.add_child(CHECK_BATTERY)
104 | STAY_HEALTHY.add_child(RECHARGE)
105 |
106 | # Display the tree before beginning execution
107 | print "Patrol Behavior Tree"
108 | print_tree(BEHAVE)
109 |
110 | # Run the tree
111 | while not rospy.is_shutdown():
112 | BEHAVE.status = BEHAVE.run()
113 | tic.sleep()
114 | print_dot_tree(BEHAVE, dotfilepath)
115 |
116 | def monitor_battery(self, msg):
117 | # Store the battery level as published on the fake battery level topic
118 | self.blackboard.battery_level = msg.data
119 | return True
120 |
121 | def check_battery(self):
122 | # Don't run the check if we are charging
123 | if self.blackboard.charging:
124 | return False
125 |
126 | if self.blackboard.battery_level is None:
127 | return None
128 | elif self.blackboard.battery_level < self.low_battery_threshold:
129 | rospy.loginfo("LOW BATTERY - level: " + str(int(self.blackboard.battery_level)))
130 | return False
131 | else:
132 | return True
133 |
134 | def recharge_cb(self, result):
135 | rospy.loginfo("BATTERY CHARGED!")
136 | self.blackboard.battery_level = 100
137 | self.blackboard.charging = False
138 | rospy.sleep(2)
139 | return True
140 |
141 | def shutdown(self):
142 | rospy.loginfo("Stopping the robot...")
143 | self.move_base.cancel_all_goals()
144 | self.cmd_vel_pub.publish(Twist())
145 | rospy.sleep(1)
146 |
147 | class RechargeRobot(Task):
148 | def __init__(self, name, interval=3, blackboard=None):
149 | super(RechargeRobot, self).__init__(name)
150 |
151 | self.name = name
152 | self.interval = interval
153 | self.blackboard = blackboard
154 |
155 | self.timer = 0
156 |
157 | def run(self):
158 | if self.timer == 0:
159 | rospy.loginfo("CHARGING THE ROBOT!")
160 |
161 | if self.timer < self.interval:
162 | self.timer += 0.1
163 | rospy.sleep(0.1)
164 | self.blackboard.charging = True
165 | return TaskStatus.RUNNING
166 | else:
167 | return TaskStatus.SUCCESS
168 |
169 | def reset(self):
170 | self.status = None
171 | self.timer = 0
172 |
173 | if __name__ == '__main__':
174 | tree = Patrol()
175 |
176 |
--------------------------------------------------------------------------------
/pi_trees_lib/examples/weighted_random_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from pi_trees_lib.pi_trees_lib import *
4 | import time
5 |
6 | class WeightedRandomExample():
7 | def __init__(self):
8 | # The root node
9 | BEHAVE = Sequence("behave")
10 |
11 | # Create a ParallelOne composite task (returns SUCCESS as soon as any subtask returns SUCCESS)
12 | WEIGHTED_RANDOM_TASKS = WeightedRandomSelector("Weighted Random Selector", weights=[3, 2, 1], reset_after=True)
13 |
14 | # Create three counting tasks
15 | TASK1 = Message("High_Probability", "Highest Probability Task")
16 | TASK2 = Message("Medium_Probability", "Medium Probability Task")
17 | TASK3 = Message("Low_Probability", "Lowest Probability Task")
18 |
19 | # Add the tasks to the parallel composite task
20 | WEIGHTED_RANDOM_TASKS.add_child(TASK1)
21 | WEIGHTED_RANDOM_TASKS.add_child(TASK2)
22 | WEIGHTED_RANDOM_TASKS.add_child(TASK3)
23 |
24 | LOOP = Loop("Loop_Forever", iterations=-1)
25 |
26 | LOOP.add_child(WEIGHTED_RANDOM_TASKS)
27 |
28 | # Add the composite task to the root task
29 | BEHAVE.add_child(LOOP)
30 |
31 | # Print a simple representation of the tree
32 | print "Behavior Tree Structure"
33 | print_tree(BEHAVE)
34 |
35 | # Run the tree
36 | while True:
37 | status = BEHAVE.run()
38 | if status == TaskStatus.SUCCESS:
39 | print "Finished running tree."
40 | break
41 |
42 | # A task to print a message
43 | class Message(Task):
44 | def __init__(self, name, message, *args, **kwargs):
45 | super(Message, self).__init__(name, *args, **kwargs)
46 |
47 | self.name = name
48 | self.message = message
49 | print "Creating message task: ", self.message
50 |
51 | def run(self):
52 | print self.message
53 | time.sleep(1)
54 |
55 | return TaskStatus.SUCCESS
56 |
57 |
58 | if __name__ == '__main__':
59 | tree = WeightedRandomExample()
60 |
61 |
--------------------------------------------------------------------------------
/pi_trees_lib/package.xml:
--------------------------------------------------------------------------------
1 |
2 | pi_trees_lib
3 | 0.1.1
4 | Python/ROS Library for Behavior Trees
5 | Patrick Goebel
6 | BSD
7 | http://ros.org/wiki/pi_trees
8 | https://github.com/pirobot/pi_trees/issues
9 | Patrick Goebel
10 |
11 | catkin
12 |
13 | rospy
14 |
15 |
16 |
17 | >
18 |
19 |
--------------------------------------------------------------------------------
/pi_trees_lib/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from distutils.core import setup
4 | from catkin_pkg.python_setup import generate_distutils_setup
5 |
6 | d = generate_distutils_setup(
7 | packages=['pi_trees_lib'],
8 | package_dir={'': 'src'},
9 | )
10 |
11 | setup(**d)
12 |
--------------------------------------------------------------------------------
/pi_trees_lib/src/pi_trees_lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pirobot/pi_trees/83738b675c3c197f4afb6c12f5b99b4e8444e707/pi_trees_lib/src/pi_trees_lib/__init__.py
--------------------------------------------------------------------------------
/pi_trees_lib/src/pi_trees_lib/pi_trees_lib.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import string
4 | from random import random, shuffle
5 |
6 | import os
7 |
8 | import pygraphviz as pgv
9 |
10 | from time import sleep
11 |
12 | class TaskStatus(object):
13 | """ A class for enumerating task statuses """
14 | FAILURE = 0
15 | SUCCESS = 1
16 | RUNNING = 2
17 |
18 | # A global value to track when the tree's status has changed. Used in the print_dot_tree() function.
19 | last_dot_tree = ''
20 |
21 | def weighted_choice(weights):
22 | rnd = random() * sum(weights)
23 | for i, w in enumerate(weights):
24 | rnd -= w
25 | if rnd < 0:
26 | return i
27 |
28 | def weighted_shuffle(a,w):
29 | w = list(w) # make a copy of w
30 | if len(a) != len(w):
31 | print("ERROR: Weighted_shuffle: Number of weights does not match number of children.")
32 | os._exit(0)
33 | return
34 |
35 | r = [0]*len(a) # contains the random shuffle
36 | for i in range(len(a)):
37 | j = weighted_choice(w)
38 | r[i]=a[j]
39 | w[j] = 0
40 | return r
41 |
42 | class Task(object):
43 | """ "The base Task class """
44 | def __init__(self, name, children=None, reset_after=False, announce=False, *args, **kwargs):
45 | self.name = name
46 | self.status = None
47 | self.reset_after = reset_after
48 | self._announce = announce
49 |
50 | if children is None:
51 | children = []
52 |
53 | self.children = children
54 |
55 | def run(self):
56 | pass
57 |
58 | def reset(self):
59 | for c in self.children:
60 | c.reset()
61 |
62 | self.status = None
63 |
64 | def add_child(self, c):
65 | self.children.append(c)
66 |
67 | def remove_child(self, c):
68 | self.children.remove(c)
69 |
70 | def prepend_child(self, c):
71 | self.children.insert(0, c)
72 |
73 | def insert_child(self, c, i):
74 | self.children.insert(i, c)
75 |
76 | def get_status(self):
77 | return self.status
78 |
79 | def set_status(self, s):
80 | self.status = s
81 |
82 | def announce(self):
83 | print("Executing task " + str(self.name))
84 |
85 | def get_type(self):
86 | return type(self)
87 |
88 | # These next two functions allow us to use the 'with' syntax
89 | def __enter__(self):
90 | return self.name
91 |
92 | def __exit__(self, exc_type, exc_val, exc_tb):
93 | if exc_type is not None:
94 | return False
95 | return True
96 |
97 | class Selector(Task):
98 | """ A selector runs each task in order until one succeeds,
99 | at which point it returns SUCCESS. If all tasks fail, a FAILURE
100 | status is returned. If a subtask is still RUNNING, then a RUNNING
101 | status is returned and processing continues until either SUCCESS
102 | or FAILURE is returned from the subtask.
103 | """
104 | def __init__(self, name, *args, **kwargs):
105 | super(Selector, self).__init__(name, *args, **kwargs)
106 |
107 | def run(self):
108 | for c in self.children:
109 |
110 | c.status = c.run()
111 |
112 | if c.status != TaskStatus.FAILURE:
113 | if c.status == TaskStatus.SUCCESS:
114 | if self.reset_after:
115 | self.reset()
116 | return TaskStatus.SUCCESS
117 | else:
118 | return c.status
119 | return c.status
120 |
121 | if self.reset_after:
122 | self.reset()
123 |
124 | return TaskStatus.FAILURE
125 |
126 | class Sequence(Task):
127 | """
128 | A sequence runs each task in order until one fails,
129 | at which point it returns FAILURE. If all tasks succeed, a SUCCESS
130 | status is returned. If a subtask is still RUNNING, then a RUNNING
131 | status is returned and processing continues until either SUCCESS
132 | or FAILURE is returned from the subtask.
133 | """
134 | def __init__(self, name, *args, **kwargs):
135 | super(Sequence, self).__init__(name, *args, **kwargs)
136 |
137 | def run(self):
138 | if self._announce:
139 | self.announce()
140 |
141 | for c in self.children:
142 |
143 | c.status = c.run()
144 |
145 | if c.status != TaskStatus.SUCCESS:
146 | if c.status == TaskStatus.FAILURE:
147 | if self.reset_after:
148 | self.reset()
149 | return TaskStatus.FAILURE
150 | return c.status
151 |
152 | if self.reset_after:
153 | self.reset()
154 |
155 | return TaskStatus.SUCCESS
156 |
157 | class RandomSelector(Task):
158 | """ A selector runs each task in random order until one succeeds,
159 | at which point it returns SUCCESS. If all tasks fail, a FAILURE
160 | status is returned. If a subtask is still RUNNING, then a RUNNING
161 | status is returned and processing continues until either SUCCESS
162 | or FAILURE is returned from the subtask.
163 | """
164 | def __init__(self, name, *args, **kwargs):
165 | super(RandomSelector, self).__init__(name, *args, **kwargs)
166 |
167 | self.shuffled = False
168 |
169 | def run(self):
170 | if not self.shuffled:
171 | shuffle(self.children)
172 | self.shuffled = True
173 |
174 | for c in self.children:
175 |
176 | c.status = c.run()
177 |
178 | if c.status != TaskStatus.FAILURE:
179 | if c.status == TaskStatus.SUCCESS:
180 | self.shuffled = False
181 |
182 | return c.status
183 |
184 | self.shuffled = False
185 |
186 | if self.reset_after:
187 | self.reset()
188 |
189 | return TaskStatus.FAILURE
190 |
191 | class RandomSequence(Task):
192 | """
193 | A sequence runs each task in random order until one fails,
194 | at which point it returns FAILURE. If all tasks succeed, a SUCCESS
195 | status is returned. If a subtask is still RUNNING, then a RUNNING
196 | status is returned and processing continues until either SUCCESS
197 | or FAILURE is returned from the subtask.
198 | """
199 | def __init__(self, name, *args, **kwargs):
200 | super(RandomSequence, self).__init__(name, *args, **kwargs)
201 |
202 | self.shuffled = False
203 |
204 | def run(self):
205 | if not self.shuffled:
206 | shuffle(self.children)
207 | self.shuffled = True
208 |
209 | for c in self.children:
210 |
211 | c.status = c.run()
212 |
213 | if c.status != TaskStatus.SUCCESS:
214 | if c.status == TaskStatus.FAILURE:
215 | self.shuffled = False
216 |
217 | return c.status
218 |
219 | self.shuffled = False
220 |
221 | if self.reset_after:
222 | self.reset()
223 |
224 | return TaskStatus.SUCCESS
225 |
226 | class WeightedRandomSequence(Task):
227 | """
228 | A sequence runs each task in random order until one fails,
229 | at which point it returns FAILURE. If all tasks succeed, a SUCCESS
230 | status is returned. If a subtask is still RUNNING, then a RUNNING
231 | status is returned and processing continues until either SUCCESS
232 | or FAILURE is returned from the subtask.
233 | """
234 | def __init__(self, name, weights, *args, **kwargs):
235 | super(WeightedRandomSequence, self).__init__(name, *args, **kwargs)
236 |
237 | self.shuffled = False
238 | self.weights = weights
239 |
240 | def run(self):
241 | if not self.shuffled:
242 | shuffled_children = weighted_shuffle(self.children, self.weights)
243 | self.shuffled = True
244 |
245 | for c in shuffled_children:
246 |
247 | c.status = c.run()
248 |
249 | if c.status != TaskStatus.SUCCESS:
250 | if c.status == TaskStatus.FAILURE:
251 | self.shuffled = False
252 |
253 | return c.status
254 |
255 | self.shuffled = False
256 |
257 | if self.reset_after:
258 | self.reset()
259 |
260 | return TaskStatus.SUCCESS
261 |
262 | class WeightedRandomSelector(Task):
263 | """ A selector runs each task in random order until one succeeds,
264 | at which point it returns SUCCESS. If all tasks fail, a FAILURE
265 | status is returned. If a subtask is still RUNNING, then a RUNNING
266 | status is returned and processing continues until either SUCCESS
267 | or FAILURE is returned from the subtask.
268 | """
269 | def __init__(self, name, weights, *args, **kwargs):
270 | super(WeightedRandomSelector, self).__init__(name, *args, **kwargs)
271 |
272 | self.shuffled = False
273 | self.weights = weights
274 |
275 | def run(self):
276 | if not self.shuffled:
277 | shuffled_children = weighted_shuffle(self.children, self.weights)
278 | self.shuffled = True
279 |
280 | for c in shuffled_children:
281 |
282 | c.status = c.run()
283 |
284 | if c.status != TaskStatus.FAILURE:
285 | if c.status == TaskStatus.SUCCESS:
286 | self.shuffled = False
287 |
288 | return c.status
289 |
290 | self.shuffled = False
291 |
292 | if self.reset_after:
293 | self.reset()
294 |
295 | return TaskStatus.FAILURE
296 |
297 | class Iterator(Task):
298 | """
299 | Iterate through all child tasks ignoring failure.
300 | """
301 | def __init__(self, name, *args, **kwargs):
302 | super(Iterator, self).__init__(name, *args, **kwargs)
303 |
304 | def run(self):
305 | for c in self.children:
306 |
307 | c.status = c.run()
308 |
309 | if c.status == TaskStatus.RUNNING:
310 | return c.status
311 |
312 | if self.reset_after:
313 | self.reset()
314 |
315 | return TaskStatus.SUCCESS
316 |
317 | class RandomIterator(Task):
318 | """
319 | Iterate through all child tasks randomly (without replacement) ignoring failure.
320 | """
321 | def __init__(self, name, *args, **kwargs):
322 | super(RandomIterator, self).__init__(name, *args, **kwargs)
323 |
324 | self.shuffled = False
325 |
326 | def run(self):
327 | if not self.shuffled:
328 | shuffle(self.children)
329 | self.shuffled = True
330 |
331 | for c in self.children:
332 |
333 | c.status = c.run()
334 |
335 | if c.status == TaskStatus.RUNNING:
336 | return c.status
337 |
338 | self.shuffled = False
339 |
340 | if self.reset_after:
341 | self.reset()
342 |
343 | return TaskStatus.SUCCESS
344 |
345 | class WeightedRandomIterator(Task):
346 | """
347 | Iterate through all child tasks randomly (without replacement) ignoring failure.
348 | """
349 | def __init__(self, name, weights, *args, **kwargs):
350 | super(WeightedRandomIterator, self).__init__(name, *args, **kwargs)
351 |
352 | self.shuffled = False
353 | self.weights = weights
354 |
355 | def run(self):
356 | if not self.shuffled:
357 | suffled_children = weighted_shuffle(self.children, self.weights)
358 | self.shuffled = True
359 |
360 | for c in suffled_children:
361 |
362 | c.status = c.run()
363 |
364 | if c.status == TaskStatus.RUNNING:
365 | return c.status
366 |
367 | self.shuffled = False
368 |
369 | if self.reset_after:
370 | self.reset()
371 |
372 | return TaskStatus.SUCCESS
373 |
374 | class ParallelOne(Task):
375 | """
376 | A parallel task runs each child task at (roughly) the same time.
377 | The ParallelOne task returns success as soon as any child succeeds.
378 | """
379 | def __init__(self, name, *args, **kwargs):
380 | super(ParallelOne, self).__init__(name, *args, **kwargs)
381 |
382 | self.failure = dict()
383 | self.index = 0
384 |
385 | def run(self):
386 | n_children = len(self.children)
387 |
388 | if self.index < n_children:
389 | child = self.children[self.index]
390 | child.status = child.run()
391 |
392 | if child.status != TaskStatus.SUCCESS:
393 | self.index += 1
394 |
395 | if child.status == TaskStatus.FAILURE:
396 | self.failure[child.name] = TaskStatus.FAILURE
397 |
398 | return TaskStatus.RUNNING
399 | else:
400 | if self.reset_after:
401 | self.reset()
402 | self.index = 0
403 | return TaskStatus.SUCCESS
404 |
405 | elif len(self.failure) == n_children:
406 | if self.reset_after:
407 | self.reset()
408 | return TaskStatus.FAILURE
409 | else:
410 | self.index = 0
411 | return TaskStatus.RUNNING
412 |
413 | def reset(self):
414 | super(ParallelOne, self).reset()
415 | self.failure = dict()
416 | self.index = 0
417 |
418 | class ParallelAll(Task):
419 | """
420 | A parallel task runs each child task at (roughly) the same time.
421 | The ParallelAll task requires all subtasks to succeed for it to succeed.
422 | """
423 | def __init__(self, name, *args, **kwargs):
424 | super(ParallelAll, self).__init__(name, *args, **kwargs)
425 |
426 | self.success = dict()
427 | self.index = 0
428 |
429 | def run(self):
430 | n_children = len(self.children)
431 |
432 | if self.index < n_children:
433 | child = self.children[self.index]
434 | child.status = child.run()
435 |
436 | if child.status != TaskStatus.FAILURE:
437 | self.index += 1
438 |
439 | if child.status == TaskStatus.SUCCESS:
440 | self.success[child.name] = TaskStatus.SUCCESS
441 |
442 | return TaskStatus.RUNNING
443 | else:
444 | if self.reset_after:
445 | self.reset()
446 | return TaskStatus.FAILURE
447 |
448 | elif len(self.success) == n_children:
449 | if self.reset_after:
450 | self.reset()
451 | return TaskStatus.SUCCESS
452 | else:
453 | self.index = 0
454 | return TaskStatus.RUNNING
455 |
456 | def reset(self):
457 | super(ParallelAll, self).reset()
458 | self.success = dict()
459 | self.index = 0
460 |
461 | class Loop(Task):
462 | """
463 | Loop over one or more subtasks for the given number of iterations
464 | Use the value -1 to indicate a continual loop.
465 | """
466 | def __init__(self, name, announce=True, *args, **kwargs):
467 | super(Loop, self).__init__(name, *args, **kwargs)
468 |
469 | self.iterations = kwargs['iterations']
470 | self._announce = announce
471 | self.loop_count = 0
472 | self.name = name
473 | print("Loop iterations: " + str(self.iterations))
474 |
475 | def run(self):
476 |
477 | c = self.children[0]
478 |
479 | while self.iterations == -1 or self.loop_count < self.iterations:
480 | c.status = c.run()
481 |
482 | status = c.status
483 |
484 | self.status = status
485 |
486 | if status == TaskStatus.SUCCESS or status == TaskStatus.FAILURE:
487 | self.loop_count += 1
488 |
489 | if self._announce:
490 | print(self.name + " COMPLETED " + str(self.loop_count) + " LOOP(S)")
491 |
492 | c.reset()
493 |
494 | return status
495 |
496 | class Limit(Task):
497 | """
498 | Limit the number of times a task can execute
499 | """
500 | def __init__(self, name, announce=True, *args, **kwargs):
501 | super(Limit, self).__init__(name, *args, **kwargs)
502 |
503 | self.max_executions = kwargs['max_executions']
504 | self._announce = announce
505 | self.execution_count = 0
506 | self.name = name
507 | print("Limit number of executions to: " + str(self.max_executions))
508 |
509 | def run(self):
510 | c = self.children[0]
511 |
512 | if self.execution_count >= self.max_executions:
513 |
514 | if self._announce:
515 | print(self.name + " reached maximum number (" + str(self.max_executions) + ") of executions.")
516 |
517 | if self.reset_after:
518 | self.reset()
519 |
520 | return TaskStatus.FAILURE
521 |
522 | c.status = c.run()
523 |
524 | if c.status != TaskStatus.RUNNING:
525 | self.execution_count += 1
526 |
527 | return c.status
528 |
529 | def reset(self):
530 | c = self.children[0]
531 | c.reset()
532 | self.execution_count = 0
533 | self.status = None
534 |
535 | class IgnoreFailure(Task):
536 | """
537 | Always return either RUNNING or SUCCESS.
538 | """
539 | def __init__(self, name, *args, **kwargs):
540 | super(IgnoreFailure, self).__init__(name, *args, **kwargs)
541 |
542 | def run(self):
543 |
544 | for c in self.children:
545 |
546 | c.status = c.run()
547 |
548 | if c.status == TaskStatus.FAILURE:
549 | return TaskStatus.SUCCESS
550 | else:
551 | return c.status
552 |
553 | return TaskStatus.SUCCESS
554 |
555 |
556 | class AlwaysFail(Task):
557 | """
558 | Always return FAILURE
559 | """
560 | def __init__(self, name, *args, **kwargs):
561 | super(AlwaysFail, self).__init__(name, *args, **kwargs)
562 |
563 | def run(self):
564 |
565 | for c in self.children:
566 |
567 | c.status = c.run()
568 |
569 | return c.status
570 |
571 | return TaskStatus.FAILURE
572 |
573 | class AlwaysSucceed(Task):
574 | """
575 | Always return SUCCESS
576 | """
577 | def __init__(self, name, *args, **kwargs):
578 | super(AlwaysSucceed, self).__init__(name, *args, **kwargs)
579 |
580 | def run(self):
581 |
582 | for c in self.children:
583 |
584 | c.status = c.run()
585 |
586 | return c.status
587 |
588 | return TaskStatus.SUCCESS
589 |
590 |
591 | class Invert(Task):
592 | """
593 | Turn SUCCESS into FAILURE and vice-versa
594 | """
595 | def __init__(self, name, *args, **kwargs):
596 | super(Invert, self).__init__(name, *args, **kwargs)
597 |
598 | def run(self):
599 |
600 | for c in self.children:
601 |
602 | c.status = c.run()
603 |
604 | if c.status == TaskStatus.FAILURE:
605 | return TaskStatus.SUCCESS
606 |
607 | elif c.status == TaskStatus.SUCCESS:
608 | return TaskStatus.FAILURE
609 |
610 | else:
611 | return c.status
612 |
613 | # Alias TaskNot to Invert for backwards compatibility
614 | TaskNot = Invert
615 |
616 | class UntilFailure(Task):
617 | """
618 | Continue executing a task until it fails or max_attempts is reached.
619 | """
620 | def __init__(self, name, max_attempts=-1, *args, **kwargs):
621 | super(UntilFailure, self).__init__(name, *args, **kwargs)
622 |
623 | self.max_attempts = max_attempts
624 | self.attempts = 0
625 |
626 | def run(self):
627 | c = self.children[0]
628 |
629 | while self.attempts < self.max_attempts or self.max_attempts == -1:
630 |
631 | c.status = c.run()
632 |
633 | if c.status == TaskStatus.FAILURE:
634 | return TaskStatus.SUCCESS
635 |
636 | if c.status == TaskStatus.SUCCESS:
637 | self.attempts += 1
638 | c.reset()
639 |
640 | return TaskStatus.RUNNING
641 |
642 | return TaskStatus.FAILURE
643 |
644 | def reset(self):
645 | c = self.children[0]
646 | c.reset()
647 | self.attempts = 0
648 |
649 | # Alias UntilFail to UntilFailure for backwards compatibility
650 | UntilFail = UntilFailure
651 |
652 | class UntilSuccess(Task):
653 | """
654 | Continue executing a task until it succeeds or max_attempts is reached.
655 | """
656 | def __init__(self, name, max_attempts=-1, *args, **kwargs):
657 | super(UntilSuccess, self).__init__(name, *args, **kwargs)
658 |
659 | self.max_attempts = max_attempts
660 | self.attempts = 0
661 |
662 | def run(self):
663 | c = self.children[0]
664 |
665 | while self.attempts < self.max_attempts or self.max_attempts == -1:
666 |
667 | c.status = c.run()
668 |
669 | if c.status == TaskStatus.SUCCESS:
670 | return TaskStatus.SUCCESS
671 |
672 | if c.status == TaskStatus.FAILURE:
673 | self.attempts += 1
674 | c.reset()
675 |
676 | return TaskStatus.RUNNING
677 |
678 | if self.reset_after:
679 | self.reset()
680 |
681 | return TaskStatus.FAILURE
682 |
683 | def reset(self):
684 | c = self.children[0]
685 | c.reset()
686 | self.attempts = 0
687 |
688 | class AutoRemoveSequence(Task):
689 | """
690 | Remove each successful subtask from a sequence
691 | """
692 | def __init__(self, name, *args, **kwargs):
693 | super(AutoRemoveSequence, self).__init__(name, *args, **kwargs)
694 |
695 | def run(self):
696 | for c in self.children:
697 | c.status = c.run()
698 |
699 | if c.status == TaskStatus.FAILURE:
700 | return TaskStatus.FAILURE
701 |
702 | if c.statuss == TaskStatus.RUNNING:
703 | return TaskStatus.RUNNING
704 |
705 | try:
706 | self.children.remove(self.children[0])
707 | except:
708 | return TaskStatus.FAILURE
709 |
710 | return TaskStatus.SUCCESS
711 |
712 | class CallbackTask(Task):
713 | """
714 | Turn any callback function (cb) into a task
715 | """
716 | def __init__(self, name, cb=None, cb_args=[], cb_kwargs={}, **kwargs):
717 | super(CallbackTask, self).__init__(name, cb=None, cb_args=[], cb_kwargs={}, **kwargs)
718 |
719 | self.name = name
720 | self.cb = cb
721 | self.cb_args = cb_args
722 | self.cb_kwargs = cb_kwargs
723 |
724 | def run(self):
725 | status = self.cb(*self.cb_args, **self.cb_kwargs)
726 |
727 | if status is None:
728 | self.status = TaskStatus.RUNNING
729 |
730 | elif status:
731 | self.status = TaskStatus.SUCCESS
732 |
733 | else:
734 | self.status = TaskStatus.FAILURE
735 |
736 | return self.status
737 |
738 | def reset(self):
739 | self.status = None
740 |
741 | class WaitTask(Task):
742 | """
743 | This is a *blocking* wait task. The interval argument is in seconds.
744 | """
745 | def __init__(self, name, interval, *args, **kwargs):
746 | super(WaitTask, self).__init__(name, interval, *args, **kwargs)
747 |
748 | self.interval = interval
749 |
750 | def run(self):
751 | sleep(self.interval)
752 |
753 | return TaskStatus.SUCCESS
754 |
755 | class loop(Task):
756 | """
757 | Loop over one or more subtasks a given number of iterations
758 | """
759 | def __init__(self, task, iterations=-1):
760 | new_name = task.name + "_loop_" + str(iterations)
761 | super(loop, self).__init__(new_name)
762 |
763 | self.iterations = iterations
764 | self.old_run = task.run
765 | self.old_reset = task.reset
766 | self.old_children = task.children
767 | self.loop_count = 0
768 |
769 | print("Loop iterations: " + str(self.iterations))
770 |
771 | def run(self):
772 | if self.iterations != -1 and self.loop_count >= self.iterations:
773 | return TaskStatus.SUCCESS
774 |
775 | print("Loop " + str(self.loop_count + 1))
776 |
777 | while True:
778 | self.status = self.old_run()
779 |
780 | if self.status == TaskStatus.SUCCESS:
781 | break
782 | else:
783 | return self.status
784 |
785 | self.old_reset()
786 | self.loop_count += 1
787 |
788 | class limit(Task):
789 | """
790 | Limit a task to the given number of executions
791 | """
792 | def __init__(self, task, max_executions=-1):
793 | new_name = task.name + "_limit_" + str(max_executions)
794 | super(limit, self).__init__(new_name)
795 |
796 | self.max_executions = max_executions
797 | self.old_run = task.run
798 | self.old_reset = task.reset
799 | self.old_children = task.children
800 | self.execution_count = 0
801 |
802 | print("Limit number of executions to: " + str(self.max_executions))
803 |
804 | def run(self):
805 | if self.max_executions != -1 and self.execution_count >= self.max_executions:
806 | self.execution_count = 0
807 |
808 | if self._announce:
809 | print(self.name + " reached maximum number (" + str(self.max_executions) + ") of executions.")
810 |
811 | return TaskStatus.FAILURE
812 |
813 | while True:
814 | self.status = self.old_run()
815 |
816 | if self.status == TaskStatus.SUCCESS:
817 | break
818 | else:
819 | return self.status
820 |
821 | self.old_reset()
822 | self.execution_count += 1
823 |
824 | class ignore_failure(Task):
825 | """
826 | Always return either RUNNING or SUCCESS.
827 | """
828 | def __init__(self, task):
829 | new_name = task.name + "_ignore_failure"
830 | super(ignore_failure, self).__init__(new_name)
831 |
832 | self.old_run = task.run
833 |
834 | def run(self):
835 | while True:
836 | self.status = self.old_run()
837 |
838 | if self.status == TaskStatus.FAILURE:
839 | return TaskStatus.SUCCESS
840 | else:
841 | return self.status
842 |
843 | class ignore_success(Task):
844 | """
845 | Always return FAILURE or RUNNING
846 | """
847 | def __init__(self, task):
848 | new_name = task.name + "_ignore_success"
849 | super(ignore_success, self).__init__(new_name)
850 |
851 | self.old_run = task.run
852 |
853 | def run(self):
854 | while True:
855 | self.status = self.old_run()
856 |
857 | if self.status == TaskStatus.SUCCESS:
858 | return TaskStatus.FAILURE
859 | else:
860 | return self.status
861 |
862 | class task_not(Task):
863 | """
864 | Turn SUCCESS into FAILURE and vice-versa
865 | """
866 | def __init__(self, task):
867 | new_name = task.name + "_not"
868 | super(task_not, self).__init__(new_name)
869 |
870 | self.old_run = task.run
871 |
872 | def run(self):
873 | while True:
874 | self.status = self.old_run()
875 |
876 | if self.status == TaskStatus.FAILURE:
877 | return TaskStatus.SUCCESS
878 |
879 | elif self.status == TaskStatus.SUCCESS:
880 | return TaskStatus.FAILURE
881 |
882 | else:
883 | return self.status
884 |
885 | # Alias task_not to invert which seems more intuitive
886 | invert = task_not
887 |
888 | class until_fail(Task):
889 | """
890 | Execute a task until it fails
891 | """
892 | def __init__(self, task):
893 | new_name = task.name + "_until_fail"
894 | super(until_fail, self).__init__(new_name)
895 |
896 | self.old_run = task.run
897 |
898 | def run(self):
899 | while True:
900 | self.status = self.old_run()
901 |
902 | if self.status == TaskStatus.FAILURE:
903 | break
904 |
905 | else:
906 | return self.status
907 |
908 | return TaskStatus.SUCCESS
909 |
910 | class always_fail(Task):
911 | """
912 | Execute a task but always return FAILTURE
913 | """
914 | def __init__(self, task):
915 | new_name = task.name + "_always_fail"
916 | super(always_fail, self).__init__(new_name)
917 |
918 | self.old_run = task.run
919 |
920 | def run(self):
921 | while True:
922 | self.old_run()
923 |
924 | self.status = TaskStatus.FAILURE
925 |
926 | return TaskStatus.FAILURE
927 |
928 |
929 | def print_tree(tree, indent=0, use_symbols=False):
930 | """
931 | Print an ASCII representation of the tree
932 | """
933 | if use_symbols:
934 | if indent == 0:
935 | print_tree_symbol(tree, indent)
936 | indent += 1
937 |
938 | for c in tree.children:
939 | print_tree_symbol(c, indent)
940 |
941 | try:
942 | if c.children != []:
943 | print_tree(c, indent+1, use_symbols)
944 | except:
945 | pass
946 | else:
947 | for c in tree.children:
948 | print " " * indent, "-->", c.name
949 |
950 | try:
951 | if c.children != []:
952 | print_tree(c, indent + 1)
953 | except:
954 | pass
955 |
956 | def print_tree_symbol(c, indent):
957 | """
958 | Use ASCII symbols to represent Sequence, Selector, Task, etc.
959 | """
960 | if isinstance(c, Selector):
961 | print " " * indent, "--?",
962 | elif isinstance(c, (Sequence, Iterator)):
963 | print " " * indent, "-->",
964 | elif isinstance(c, (RandomSequence, RandomIterator, WeightedRandomSequence, WeightedRandomIterator)):
965 | print " " * indent, "~~>",
966 | elif isinstance(c, (RandomSelector, WeightedRandomSelector)):
967 | print " " * indent, "~~?",
968 | elif isinstance(c, ParallelOne):
969 | print " " * indent, "==?",
970 | elif isinstance(c, ParallelAll):
971 | print " " * indent, "==>",
972 | elif isinstance(c, Loop):
973 | print " " * indent, "<->",
974 | elif isinstance(c, Invert):
975 | print " " * indent, "--!",
976 | else:
977 | print " " * indent, "--|",
978 |
979 | print c.name
980 |
981 | def print_phpsyntax_tree(tree):
982 | """
983 | Print an output compatible with ironcreek.net/phpSyntaxTree
984 | """
985 | for c in tree.children:
986 | print "[" + string.replace(c.name, "_", "."),
987 | if c.children != []:
988 | print_phpsyntax_tree(c),
989 | print "]",
990 |
991 | def print_dot_tree(root, dotfilepath=None):
992 | """
993 | Print an output compatible with the DOT synatax and Graphiz
994 | """
995 | gr = pgv.AGraph(strict=True, directed=True, rotate='0', bgcolor='white', ordering="out")
996 | gr.node_attr['fontsize'] = '9'
997 | gr.node_attr['color'] = 'black'
998 |
999 | if dotfilepath is None:
1000 | dotfilepath = os.path.expanduser('~') + '/.ros/tree.dot'
1001 |
1002 | global last_dot_tree
1003 |
1004 | # Add the root node
1005 | gr.add_node(root.name)
1006 | node = gr.get_node(root.name)
1007 |
1008 | if root.status == TaskStatus.RUNNING:
1009 | node.attr['fillcolor'] = 'yellow'
1010 | node.attr['style'] = 'filled'
1011 | node.attr['border'] = 'bold'
1012 | elif root.status == TaskStatus.SUCCESS:
1013 | node.attr['color'] = 'green'
1014 | elif root.status == TaskStatus.FAILURE:
1015 | node.attr['color'] = 'red'
1016 | else:
1017 | node.attr['color'] = 'black'
1018 |
1019 | def add_edges(root):
1020 | for c in root.children:
1021 | if isinstance(c, (Sequence, Iterator, RandomSequence, RandomIterator, WeightedRandomSequence, WeightedRandomIterator)):
1022 | gr.add_node(c.name, shape="cds")
1023 | elif isinstance(c, (Selector, RandomSelector, WeightedRandomSelector)):
1024 | gr.add_node(c.name, shape="diamond")
1025 | elif isinstance(c, (ParallelOne, ParallelAll)):
1026 | gr.add_node(c.name, shape="parallelogram")
1027 | elif isinstance(c, Invert):
1028 | gr.add_node(c.name, shape="house")
1029 | else:
1030 | gr.add_node(c.name)
1031 |
1032 | gr.add_edge((root.name, c.name))
1033 | node = gr.get_node(c.name)
1034 |
1035 | if c.status == TaskStatus.RUNNING:
1036 | node.attr['fillcolor'] = 'yellow'
1037 | node.attr['style'] = 'filled'
1038 | node.attr['border'] = 'bold'
1039 | elif c.status == TaskStatus.SUCCESS:
1040 | node.attr['color'] = 'green'
1041 | elif c.status == TaskStatus.FAILURE:
1042 | node.attr['color'] = 'red'
1043 | else:
1044 | node.attr['color'] = 'black'
1045 |
1046 | if c.children != []:
1047 | add_edges(c)
1048 |
1049 | add_edges(root)
1050 |
1051 | current_dot_tree = gr.string()
1052 |
1053 | if current_dot_tree != last_dot_tree:
1054 | gr.write(dotfilepath)
1055 | last_dot_tree = gr.string()
1056 |
--------------------------------------------------------------------------------
/pi_trees_lib/src/pi_trees_lib/task_setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import rospy
4 | import actionlib
5 | from actionlib import GoalStatus
6 | from geometry_msgs.msg import Pose, Point, Quaternion, Twist
7 | from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal, MoveBaseActionFeedback
8 | from tf.transformations import quaternion_from_euler
9 | from visualization_msgs.msg import Marker
10 | from math import pi
11 | from collections import OrderedDict
12 |
13 | def setup_task_environment(self):
14 | # How big is the square we want the robot to patrol?
15 | self.square_size = rospy.get_param("~square_size", 1.0) # meters
16 |
17 | # Set the low battery threshold (between 0 and 100)
18 | self.low_battery_threshold = rospy.get_param('~low_battery_threshold', 50)
19 |
20 | # How many times should we execute the patrol loop
21 | self.n_patrols = rospy.get_param("~n_patrols", 2) # meters
22 |
23 | # How long do we have to get to each waypoint?
24 | self.move_base_timeout = rospy.get_param("~move_base_timeout", 10) #seconds
25 |
26 | # Initialize the patrol counter
27 | self.patrol_count = 0
28 |
29 | # Subscribe to the move_base action server
30 | self.move_base = actionlib.SimpleActionClient("move_base", MoveBaseAction)
31 |
32 | rospy.loginfo("Waiting for move_base action server...")
33 |
34 | # Wait up to 60 seconds for the action server to become available
35 | self.move_base.wait_for_server(rospy.Duration(60))
36 |
37 | rospy.loginfo("Connected to move_base action server")
38 |
39 | # Create a list to hold the target quaternions (orientations)
40 | quaternions = list()
41 |
42 | # First define the corner orientations as Euler angles
43 | euler_angles = (pi/2, pi, 3*pi/2, 0)
44 |
45 | # Then convert the angles to quaternions
46 | for angle in euler_angles:
47 | q_angle = quaternion_from_euler(0, 0, angle, axes='sxyz')
48 | q = Quaternion(*q_angle)
49 | quaternions.append(q)
50 |
51 | # Create a list to hold the waypoint poses
52 | self.waypoints = list()
53 |
54 | # Append each of the four waypoints to the list. Each waypoint
55 | # is a pose consisting of a position and orientation in the map frame.
56 | self.waypoints.append(Pose(Point(0.0, 0.0, 0.0), quaternions[3]))
57 | self.waypoints.append(Pose(Point(self.square_size, 0.0, 0.0), quaternions[0]))
58 | self.waypoints.append(Pose(Point(self.square_size, self.square_size, 0.0), quaternions[1]))
59 | self.waypoints.append(Pose(Point(0.0, self.square_size, 0.0), quaternions[2]))
60 |
61 | # Create a mapping of room names to waypoint locations
62 | room_locations = (('hallway', self.waypoints[0]),
63 | ('living_room', self.waypoints[1]),
64 | ('kitchen', self.waypoints[2]),
65 | ('bathroom', self.waypoints[3]))
66 |
67 | # Store the mapping as an ordered dictionary so we can visit the rooms in sequence
68 | self.room_locations = OrderedDict(room_locations)
69 |
70 | # Where is the docking station?
71 | self.docking_station_pose = (Pose(Point(0.5, 0.5, 0.0), Quaternion(0.0, 0.0, 0.0, 1.0)))
72 |
73 | # Initialize the waypoint visualization markers for RViz
74 | init_waypoint_markers(self)
75 |
76 | # Set a visualization marker at each waypoint
77 | for waypoint in self.waypoints:
78 | p = Point()
79 | p = waypoint.position
80 | self.waypoint_markers.points.append(p)
81 |
82 | # Set a marker for the docking station
83 | init_docking_station_marker(self)
84 |
85 | # Publisher to manually control the robot (e.g. to stop it)
86 | self.cmd_vel_pub = rospy.Publisher('cmd_vel', Twist, queue_size=5)
87 |
88 | rospy.loginfo("Starting Tasks")
89 |
90 | # Publish the waypoint markers
91 | self.marker_pub.publish(self.waypoint_markers)
92 | rospy.sleep(1)
93 | self.marker_pub.publish(self.waypoint_markers)
94 |
95 | # Publish the docking station marker
96 | self.docking_station_marker_pub.publish(self.docking_station_marker)
97 | rospy.sleep(1)
98 |
99 | def init_waypoint_markers(self):
100 | # Set up our waypoint markers
101 | marker_scale = 0.2
102 | marker_lifetime = 0 # 0 is forever
103 | marker_ns = 'waypoints'
104 | marker_id = 0
105 | marker_color = {'r': 1.0, 'g': 0.7, 'b': 1.0, 'a': 1.0}
106 |
107 | # Define a marker publisher.
108 | self.marker_pub = rospy.Publisher('waypoint_markers', Marker, queue_size=5)
109 |
110 | # Initialize the marker points list.
111 | self.waypoint_markers = Marker()
112 | self.waypoint_markers.ns = marker_ns
113 | self.waypoint_markers.id = marker_id
114 | self.waypoint_markers.type = Marker.CUBE_LIST
115 | self.waypoint_markers.action = Marker.ADD
116 | self.waypoint_markers.lifetime = rospy.Duration(marker_lifetime)
117 | self.waypoint_markers.scale.x = marker_scale
118 | self.waypoint_markers.scale.y = marker_scale
119 | self.waypoint_markers.color.r = marker_color['r']
120 | self.waypoint_markers.color.g = marker_color['g']
121 | self.waypoint_markers.color.b = marker_color['b']
122 | self.waypoint_markers.color.a = marker_color['a']
123 |
124 | self.waypoint_markers.header.frame_id = 'odom'
125 | self.waypoint_markers.header.stamp = rospy.Time.now()
126 | self.waypoint_markers.points = list()
127 |
128 | def init_docking_station_marker(self):
129 | # Define a marker for the charging station
130 | marker_scale = 0.3
131 | marker_lifetime = 0 # 0 is forever
132 | marker_ns = 'waypoints'
133 | marker_id = 0
134 | marker_color = {'r': 0.7, 'g': 0.7, 'b': 0.0, 'a': 1.0}
135 |
136 | self.docking_station_marker_pub = rospy.Publisher('docking_station_marker', Marker, queue_size=5)
137 |
138 | self.docking_station_marker = Marker()
139 | self.docking_station_marker.ns = marker_ns
140 | self.docking_station_marker.id = marker_id
141 | self.docking_station_marker.type = Marker.CYLINDER
142 | self.docking_station_marker.action = Marker.ADD
143 | self.docking_station_marker.lifetime = rospy.Duration(marker_lifetime)
144 | self.docking_station_marker.scale.x = marker_scale
145 | self.docking_station_marker.scale.y = marker_scale
146 | self.docking_station_marker.scale.z = 0.02
147 | self.docking_station_marker.color.r = marker_color['r']
148 | self.docking_station_marker.color.g = marker_color['g']
149 | self.docking_station_marker.color.b = marker_color['b']
150 | self.docking_station_marker.color.a = marker_color['a']
151 |
152 | self.docking_station_marker.header.frame_id = 'odom'
153 | self.docking_station_marker.header.stamp = rospy.Time.now()
154 | self.docking_station_marker.pose = self.docking_station_pose
155 |
--------------------------------------------------------------------------------
/pi_trees_ros/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | Changelog for package pi_trees_ros
3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | 0.1.2 (2016-09-25)
6 | ------------------
7 | * Tweaked behavior of SimpleActionTask when task is preempted so that the task will continue when control returns to that branch of the tree
8 | * Tweaked comments in SimpleActionTask
9 | * Fixed SimpleActionTask to better handle goal actions that do not succeed (e.g. aborted, preempted, etc.)
10 | * Tweaked done_cb behavior for SimpleActionTask so that a user-defined done_cb is appended to the default_done_cb instead of replacing it
11 | * adding sys import to resolve error in ServiceTask
12 | * Updated dependencies in package.xml files
13 | * Contributors: Patrick Goebel, maxfolley
14 |
15 | 0.1.1 (2014-08-01)
16 | ------------------
17 | * Added initial CHANGELOG.rst files
18 | * Deleted obsolete examples directory from pi_trees_ros
19 | * Updates some comments
20 | * Uncommented feedback_cb for SimpleActionTask
21 | * Did some house cleaning
22 | * Cleanup
23 | * Initial commit
24 | * Contributors: Patrick Goebel
25 |
--------------------------------------------------------------------------------
/pi_trees_ros/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 2.8.3)
2 |
3 | project(pi_trees_ros)
4 |
5 | find_package(catkin REQUIRED)
6 |
7 | catkin_package()
8 |
9 | catkin_python_setup()
10 |
--------------------------------------------------------------------------------
/pi_trees_ros/package.xml:
--------------------------------------------------------------------------------
1 |
2 | pi_trees_ros
3 | 0.1.1
4 |
5 | ROS wrappers for the pi_trees_lib Library
6 |
7 | Patrick Goebel
8 | BSD
9 | http://ros.org/wiki/pi_trees
10 | https://github.com/pirobot/pi_trees/issues
11 | Patrick Goebel
12 |
13 | catkin
14 |
15 |
16 | rospy
17 | actionlib
18 | actionlib_msgs
19 | pi_trees_lib
20 |
21 |
22 |
--------------------------------------------------------------------------------
/pi_trees_ros/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from distutils.core import setup
4 | from catkin_pkg.python_setup import generate_distutils_setup
5 |
6 | d = generate_distutils_setup(
7 | packages=['pi_trees_ros'],
8 | package_dir={'': 'src'},
9 | )
10 |
11 | setup(**d)
12 |
--------------------------------------------------------------------------------
/pi_trees_ros/src/pi_trees_ros/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pirobot/pi_trees/83738b675c3c197f4afb6c12f5b99b4e8444e707/pi_trees_ros/src/pi_trees_ros/__init__.py
--------------------------------------------------------------------------------
/pi_trees_ros/src/pi_trees_ros/pi_trees_ros.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import rospy
4 | import actionlib
5 | from actionlib_msgs.msg import GoalStatus
6 | from pi_trees_lib.pi_trees_lib import *
7 | import sys
8 |
9 | class MonitorTask(Task):
10 | """
11 | Turn a ROS subscriber into a Task.
12 | """
13 | def __init__(self, name, topic, msg_type, msg_cb, wait_for_message=True, timeout=5):
14 | super(MonitorTask, self).__init__(name)
15 |
16 | self.topic = topic
17 | self.msg_type = msg_type
18 | self.timeout = timeout
19 | self.msg_cb = msg_cb
20 |
21 | rospy.loginfo("Subscribing to topic " + topic)
22 |
23 | if wait_for_message:
24 | try:
25 | rospy.wait_for_message(topic, msg_type, timeout=self.timeout)
26 | rospy.loginfo("Connected.")
27 | except:
28 | rospy.loginfo("Timed out waiting for " + topic)
29 |
30 | # Subscribe to the given topic with the given callback function executed via run()
31 | rospy.Subscriber(self.topic, self.msg_type, self._msg_cb)
32 |
33 | def _msg_cb(self, msg):
34 | self.set_status(self.msg_cb(msg))
35 |
36 | def run(self):
37 | return self.status
38 |
39 | class ServiceTask(Task):
40 | """
41 | Turn a ROS service into a Task.
42 | """
43 | def __init__(self, name, service, service_type, request, result_cb=None, wait_for_service=True, timeout=5):
44 | super(ServiceTask, self).__init__(name)
45 |
46 | self.result = None
47 | self.request = request
48 | self.timeout = timeout
49 | self.result_cb = result_cb
50 |
51 | rospy.loginfo("Connecting to service " + service)
52 |
53 | if wait_for_service:
54 | rospy.loginfo("Waiting for service")
55 | rospy.wait_for_service(service, timeout=self.timeout)
56 | rospy.loginfo("Connected.")
57 |
58 | # Create a service proxy
59 | self.service_proxy = rospy.ServiceProxy(service, service_type)
60 |
61 | def run(self):
62 | try:
63 | result = self.service_proxy(self.request)
64 | if self.result_cb is not None:
65 | self.result_cb(result)
66 | return TaskStatus.SUCCESS
67 | except:
68 | rospy.logerr(sys.exc_info())
69 | return TaskStatus.FAILURE
70 |
71 | def reset(self):
72 | self.status = None
73 |
74 | class SimpleActionTask(Task):
75 | """
76 | Turn a ROS action into a Task.
77 | """
78 | def __init__(self, name, action, action_type, goal, rate=5, connect_timeout=10, result_timeout=30, reset_after=False, active_cb=None, done_cb=None, feedback_cb=None):
79 | super(SimpleActionTask, self).__init__(name)
80 |
81 | self.action = action
82 | self.goal = goal
83 | self.tick = 1.0 / rate
84 | self.rate = rospy.Rate(rate)
85 |
86 | self.result = None
87 | self.connect_timeout = connect_timeout
88 | self.result_timeout = result_timeout
89 | self.reset_after = reset_after
90 |
91 | self.final_status = None
92 |
93 | if done_cb:
94 | self.user_done_cb = done_cb
95 | else:
96 | self.user_done_cb = None
97 |
98 | self.done_cb = self.default_done_cb
99 |
100 | if active_cb == None:
101 | active_cb = self.default_active_cb
102 | self.active_cb = active_cb
103 |
104 | if feedback_cb == None:
105 | feedback_cb = self.default_feedback_cb
106 | self.feedback_cb = feedback_cb
107 |
108 | self.action_started = False
109 | self.action_finished = False
110 | self.goal_status_reported = False
111 | self.time_so_far = 0.0
112 |
113 | # Goal state return values
114 | self.goal_states = ['PENDING', 'ACTIVE', 'PREEMPTED',
115 | 'SUCCEEDED', 'ABORTED', 'REJECTED',
116 | 'PREEMPTING', 'RECALLING', 'RECALLED',
117 | 'LOST']
118 |
119 | self.retry_goal_states = [GoalStatus.PREEMPTED]
120 |
121 | rospy.loginfo("Connecting to action " + action)
122 |
123 | # Subscribe to the base action server
124 | self.action_client = actionlib.SimpleActionClient(action, action_type)
125 |
126 | rospy.loginfo("Waiting for action server...")
127 |
128 | # Wait up to timeout seconds for the action server to become available
129 | try:
130 | self.action_client.wait_for_server(rospy.Duration(self.connect_timeout))
131 | except:
132 | rospy.loginfo("Timed out connecting to the action server " + action)
133 |
134 | rospy.loginfo("Connected to action server")
135 |
136 | def run(self):
137 | # Send the goal
138 | if not self.action_started:
139 | rospy.loginfo("Sending " + str(self.name) + " goal to action server...")
140 | self.action_client.send_goal(self.goal, done_cb=self.done_cb, active_cb=self.active_cb, feedback_cb=self.feedback_cb)
141 | self.action_started = True
142 | self.activate_time = rospy.Time.now()
143 |
144 | ''' We cannot use the wait_for_result() method here as it will block the entire
145 | tree so we break it down in time slices of duration 1 / rate.
146 | '''
147 | if not self.action_finished:
148 | self.time_so_far += self.tick
149 | self.rate.sleep()
150 | if self.time_so_far > self.result_timeout:
151 | self.action_client.cancel_goal()
152 | rospy.loginfo("Timed out achieving goal")
153 | self.action_finished = True
154 | return TaskStatus.FAILURE
155 | else:
156 | return TaskStatus.RUNNING
157 | else:
158 | # Check the final goal status returned by default_done_cb
159 | if self.goal_status == GoalStatus.SUCCEEDED:
160 | self.status = TaskStatus.SUCCESS
161 |
162 | # This case handles PREEMPTED
163 | elif self.goal_status in self.retry_goal_states:
164 | self.status = TaskStatus.RUNNING
165 | self.action_started = False
166 | self.action_finished = False
167 | self.time_so_far = 0
168 |
169 | # Otherwise, consider the task to have failed
170 | else:
171 | self.status = TaskStatus.FAILURE
172 |
173 | # Store the final status before we reset
174 | self.final_status = self.status
175 |
176 | # Reset the task if the reset_after flag is True
177 | if self.reset_after:
178 | self.reset()
179 |
180 | self.action_client.wait_for_result(rospy.Duration(10))
181 | return self.final_status
182 |
183 | def default_done_cb(self, result_state, result):
184 | """Goal Done Callback
185 | This callback resets the active flags and reports the duration of the action.
186 | Also, if the user has defined a result_cb, it is called here before the
187 | method returns.
188 | """
189 | self.goal_status = result_state
190 | self.action_finished = True
191 |
192 | if not self.goal_status_reported:
193 | self._duration = rospy.Time.now() - self.activate_time
194 |
195 | rospy.loginfo("Action " + self.name + " terminated after "\
196 | + str(self._duration.to_sec()) + " seconds with result "\
197 | + self.goal_states[self.action_client.get_state()] + ".")
198 |
199 | self.goal_status_reported = True
200 |
201 | if self.user_done_cb:
202 | self.user_done_cb(result_state, result)
203 |
204 | def default_active_cb(self):
205 | pass
206 |
207 | def default_feedback_cb(self, msg):
208 | pass
209 |
210 | def reset(self):
211 | rospy.logdebug("RESETTING " + str(self.name))
212 | self.action_started = False
213 | self.action_finished = False
214 | self.goal_status_reported = False
215 | self.status = self.final_status
216 | self.time_so_far = 0.0
217 | super(SimpleActionTask, self).reset()
218 |
--------------------------------------------------------------------------------