Writing long running ActionNodes
Overview
There are two different types of long running ActionNodes
.
First, there are the ActionNodes
that require more ticks to complete. In this case typically in the
on_tick
callback a function (action) is triggered and/or a status in the ‘world’ is checked.
However, the execution time of one tick of the on_tick
function has to be fast. Otherwise this affects the
execution of the careBT execution engine. Thus, this programming model should be used if a function
needs to be triggered or a check of a state needs to be executed periodically and the execution
time is fast.
Second, there are the ActionNodes
that need to call long running asynchronous functions (actions). In this case
the node is put into the SUSPENDED
state to suppress further calls of the on_tick
callback. When
initiating the asynchronous action a ‘result’ callback should be registered that can then take over and
change the state of the node accordingly. In the following example this asynchronous action is ‘simulated’
with a Python timer.
Create a multi-tick ActionNode
Create a file named longrun_actions.py
with following content.
Or use the provided file: longrun_actions.py
1from threading import Timer
2
3from carebt import ActionNode
4from carebt import NodeStatus
5
6
7class AddTwoNumbersMultiTickAction(ActionNode):
8 """The `AddTwoNumbersMultiTickAction` example node.
9
10 The `AddTwoNumbersMultiTickAction` demonstrates how it looks like when a
11 `ActionNode` requires more ticks to complete. To make things simple the
12 amount of ticks required to complete the action is provided as input
13 parameter.
14
15 Input Parameters
16 ----------------
17 ?ticks : int
18 Number of ticks requiered to complete
19 ?x : int
20 The first value
21 ?y : int
22 The second value
23
24 Output Parameters
25 -----------------
26 ?z : int
27 The sum of ?x and ?y
28
29 """
30
31 def __init__(self, bt_runner):
32 super().__init__(bt_runner, '?ticks ?x ?y => ?z')
33
34 def on_init(self) -> None:
35 self._tick_count = 1
36
37 def on_tick(self) -> None:
38 if(self._tick_count < self._ticks):
39 print('AddTwoNumbersMultiTickAction: (tick_count = '
40 + f'{self._tick_count}/{self._ticks})')
41 self._tick_count += 1
42 self.set_status(NodeStatus.RUNNING)
43 else:
44 self._z = self._x + self._y
45 print(f'AddTwoNumbersMultiTickAction: DONE {self._x} + {self._y} = {self._z}')
46 self.set_status(NodeStatus.SUCCESS)
The code explained
The AddTwoNumbersMultiTickAction
node is implemented as a Python class which inherits from ActionNode
.
class AddTwoNumbersMultiTickAction(ActionNode):
The constructor (__init__
) of the AddTwoNumbersMultiTickAction
defines the signature that
the node has three input parameters (?ticks ?x ?y) and one output parameter (?z). The
input parameter ?ticks is used to specify how many ticks the calculation should take. The remaining
parameters are the same as for the AddTwoNumbersAction
.
def __init__(self, bt_runner):
super().__init__(bt_runner, '?ticks ?x ?y => ?z')
In the on_init
function the internal _tick_count variable is initialized to one.
def on_init(self) -> None:
self._tick_count = 1
In the on_tick
function it is checked whether the internal _tick_count has reached the provided
?tick limit or not. In case the limit is reached the other two input parameters are added,
the result is bound to the output parameter and the node is set so SUCCESS
. In case the _tick_count
limit is not reached a message is printed on standard output. The node remains in status RUNNING
and thus,
it is ticked again.
def on_tick(self) -> None:
if(self._tick_count < self._ticks):
print('AddTwoNumbersMultiTickAction: (tick_count = '
+ f'{self._tick_count}/{self._ticks})')
self._tick_count += 1
self.set_status(NodeStatus.RUNNING)
else:
self._z = self._x + self._y
print(f'AddTwoNumbersMultiTickAction: DONE {self._x} + {self._y} = {self._z}')
self.set_status(NodeStatus.SUCCESS)
Run the example
Start the Python interpreter and run the AddTwoNumbersMultiTickAction
node:
>>> import carebt
>>> from carebt.examples.longrun_actions import AddTwoNumbersMultiTickAction
>>> bt_runner = carebt.BehaviorTreeRunner()
>>> bt_runner.run(AddTwoNumbersMultiTickAction, '1 4 7 => ?result')
AddTwoNumbersMultiTickAction: DONE 4 + 7 = 11
>>> bt_runner.run(AddTwoNumbersMultiTickAction, '5 4 7 => ?result')
AddTwoNumbersMultiTickAction: (tick_count = 1/5)
AddTwoNumbersMultiTickAction: (tick_count = 2/5)
AddTwoNumbersMultiTickAction: (tick_count = 3/5)
AddTwoNumbersMultiTickAction: (tick_count = 4/5)
AddTwoNumbersMultiTickAction: DONE 4 + 7 = 11
>>> bt_runner.run(AddTwoNumbersMultiTickAction, '9 4 7 => ?result')
AddTwoNumbersMultiTickAction: (tick_count = 1/9)
AddTwoNumbersMultiTickAction: (tick_count = 2/9)
AddTwoNumbersMultiTickAction: (tick_count = 3/9)
AddTwoNumbersMultiTickAction: (tick_count = 4/9)
AddTwoNumbersMultiTickAction: (tick_count = 5/9)
AddTwoNumbersMultiTickAction: (tick_count = 6/9)
AddTwoNumbersMultiTickAction: (tick_count = 7/9)
AddTwoNumbersMultiTickAction: (tick_count = 8/9)
AddTwoNumbersMultiTickAction: DONE 4 + 7 = 11
Create a multi-tick ActionNode with timeout
Add the following content to longrun_actions.py
.
Or use the provided file: longrun_actions.py
1class AddTwoNumbersMultiTickActionWithTimeout(ActionNode):
2 """The `AddTwoNumbersMultiTickActionWithTimeout` example node.
3
4 The `AddTwoNumbersMultiTickActionWithTimeout` adds a timeout to the
5 `AddTwoNumbersMultiTickAction`.
6
7 Input Parameters
8 ----------------
9 ?ticks : int
10 Number of ticks requiered to complete
11 ?x : int
12 The first value
13 ?y : int
14 The second value
15
16 Output Parameters
17 -----------------
18 ?z : int
19 The sum of ?x and ?y
20
21 """
22
23 def __init__(self, bt_runner):
24 super().__init__(bt_runner, '?ticks ?x ?y => ?z')
25
26 def on_init(self) -> None:
27 self._tick_count = 1
28 # without throttling / with timeout
29 self.set_timeout(500)
30 # with throttling and timeout
31 # self.set_throttle_ms(1000)
32 # self.set_timeout(5000)
33
34 def on_tick(self) -> None:
35 if(self._tick_count < self._ticks):
36 print('AddTwoNumbersMultiTickActionWithTimeout: (tick_count = '
37 + f'{self._tick_count}/{self._ticks})')
38 self._tick_count += 1
39 self.set_status(NodeStatus.RUNNING)
40 else:
41 self._z = self._x + self._y
42 print('AddTwoNumbersMultiTickActionWithTimeout: '
43 + f'DONE {self._x} + {self._y} = {self._z}')
44 self.set_status(NodeStatus.SUCCESS)
45
46 def on_timeout(self) -> None:
47 print('AddTwoNumbersMultiTickActionWithTimeout: on_timeout')
48 self.abort()
49 self.set_contingency_message('TIMEOUT')
50
51 def on_abort(self) -> None:
52 print('AddTwoNumbersMultiTickActionWithTimeout: on_abort')
The code explained
The AddTwoNumbersMultiTickActionWithTimeout
introduces a timeout and throttling to
the AddTwoNumbersMultiTickAction
.
In the on_init
function the internal _tick_count variable is initialized to one and a timeout
is specified for the node which expires after 500 ms. In case the timeout expires the on_timeout
callback is called. In the second variant (which is commented out) throttling is set to 1000 ms.
This ensures that the ticks of the node are omitted and not forwarded until 1000 ms have passed. Thus,
the on_tick
function is called each 1000ms. Furthermore the timeout is set to 5000 ms, that the
timeout is greater than the throttling.
def on_init(self) -> None:
self._tick_count = 1
# without throttling / with timeout
self.set_timeout(500)
# with throttling and timeout
# self.set_throttle_ms(1000)
# self.set_timeout(5000)
The on_timeout
function is called is case the specified timeout timer expires. In this example,
it is implemented that the current node is aborted and the contingency-message is set to ‘TIMEOUT’.
def on_timeout(self) -> None:
print('AddTwoNumbersMultiTickActionWithTimeout: on_timeout')
self.abort()
self.set_contingency_message('TIMEOUT')
The on_abort
function is called in case that the node is aborted. This function is the place
to do some cleanup which needs to be done in case the ‘running’ actions (resources) are aborted.
In this example only a message is printed on standard output.
def on_abort(self) -> None:
print('AddTwoNumbersMultiTickActionWithTimeout: on_abort')
Run the example
Start the Python interpreter and run the AddTwoNumbersMultiTickAction
node:
>>> import carebt
>>> from carebt.examples.longrun_actions import AddTwoNumbersMultiTickActionWithTimeout
>>> bt_runner = carebt.BehaviorTreeRunner()
>>> bt_runner.run(AddTwoNumbersMultiTickActionWithTimeout, '1 4 7 => ?result')
AddTwoNumbersMultiTickActionWithTimeout: DONE 4 + 7 = 11
>>> bt_runner.run(AddTwoNumbersMultiTickActionWithTimeout, '3 4 7 => ?result')
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 1/3)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 2/3)
AddTwoNumbersMultiTickActionWithTimeout: DONE 4 + 7 = 11
>>> bt_runner.run(AddTwoNumbersMultiTickActionWithTimeout, '15 4 7 => ?result')
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 1/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 2/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 3/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 4/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 5/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 6/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 7/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 8/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 9/15)
AddTwoNumbersMultiTickActionWithTimeout: (tick_count = 10/15)
AddTwoNumbersMultiTickActionWithTimeout: on_timeout
AddTwoNumbersMultiTickActionWithTimeout: on_abort
2021-12-01 20:29:17 WARN ---------------------------------------------------
2021-12-01 20:29:17 WARN bt execution finished
2021-12-01 20:29:17 WARN status: NodeStatus.ABORTED
2021-12-01 20:29:17 WARN contingency-message: TIMEOUT
2021-12-01 20:29:17 WARN ---------------------------------------------------
Hint
Change the comments to enable throttling and increase the timeout to 5000 ms to also test this feature.
Create an asynchronous ActionNode
Add the following content to longrun_actions.py
.
Or use the provided file: longrun_actions.py
1class AddTwoNumbersLongRunningAction(ActionNode):
2 """The `AddTwoNumbersLongRunningAction` example node.
3
4 The `AddTwoNumbersLongRunningAction` demonstrates how it looks like when a
5 `ActionNode` executes an asynchronous function. To make things simple the
6 asynchronous function is implemented with a simple Python timer and
7 the amount of milliseconds the asynchronous function requires to complete
8 is provided as input parameter.
9
10 Input Parameters
11 ----------------
12 ?calctime : int (ms)
13 Milliseconds requiered to complete
14 ?x : int
15 The first value
16 ?y : int
17 The second value
18
19 Output Parameters
20 -----------------
21 ?z : int
22 The sum of ?x and ?y
23
24 """
25
26 def __init__(self, bt_runner):
27 super().__init__(bt_runner, '?calctime ?x ?y => ?z')
28
29 def on_init(self) -> None:
30 print(f'AddTwoNumbersLongRunningAction: calculating {self._calctime} ms ...')
31 self.set_status(NodeStatus.SUSPENDED)
32 Timer(self._calctime / 1000, self.done_callback).start()
33
34 def on_tick(self) -> None:
35 print('AddTwoNumbersLongRunningAction: on_tick')
36
37 def done_callback(self) -> None:
38 self._z = self._x + self._y
39 print(f'AddTwoNumbersLongRunningAction: DONE {self._x} + {self._y} = {self._z}')
40 self.set_status(NodeStatus.SUCCESS)
The code explained
In the on_init
function the node status is set to SUSPENDED
and a Python timer is implemented
to ‘simulate’ an asynchronous action. This timer is set to ?calctime and the done_callback
is
registered which is called as soon as the timer has expired.
def on_init(self) -> None:
print(f'AddTwoNumbersLongRunningAction: calculating {self._calctime} ms ...')
self.set_status(NodeStatus.SUSPENDED)
Timer(self._calctime / 1000, self.done_callback).start()
In the on_tick
function a print statement is implemented to demonstrate that the on_tick
function
is never called in this example as the node is directly set to SUSPENDED
. The on_tick
function
could also be removed in this case!
def on_tick(self) -> None:
print('AddTwoNumbersLongRunningAction: on_tick')
In the done_callback
the calculation is performed, the result is bound to the output parameter and
the status of the node is set to SUCCESS
.
def done_callback(self) -> None:
self._z = self._x + self._y
print(f'AddTwoNumbersLongRunningAction: DONE {self._x} + {self._y} = {self._z}')
self.set_status(NodeStatus.SUCCESS)
Run the example
Start the Python interpreter and run the AddTwoNumbersLongRunningAction
node:
>>> import carebt
>>> from carebt.examples.longrun_actions import AddTwoNumbersLongRunningAction
>>> bt_runner = carebt.BehaviorTreeRunner()
>>> bt_runner.run(AddTwoNumbersLongRunningAction, '2000 4 7 => ?result')
AddTwoNumbersLongRunningAction: calculating 2000 ms ...
AddTwoNumbersLongRunningAction: DONE 4 + 7 = 11