Skip to content

Commit fb3a6ae

Browse files
authored
Use the events executor for most suitable python nodes (#693)
2 parents 3a8b87e + 4ccc0bf commit fb3a6ae

File tree

18 files changed

+143
-61
lines changed

18 files changed

+143
-61
lines changed

bitbots_behavior/bitbots_blackboard/bitbots_blackboard/body_blackboard.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,19 @@ def __init__(self, node: Node, tf_buffer: tf2.BufferInterface):
3434
self.costmap = CostmapCapsule(self.node, self)
3535
self.pathfinding = PathfindingCapsule(self.node, self)
3636
self.team_data = TeamDataCapsule(self.node, self)
37+
38+
self.capsules = [
39+
self.misc,
40+
self.gamestate,
41+
self.animation,
42+
self.kick,
43+
self.world_model,
44+
self.costmap,
45+
self.pathfinding,
46+
self.team_data,
47+
]
48+
49+
def clear_cache(self):
50+
"""Clear the cache of all capsules."""
51+
for capsule in self.capsules:
52+
capsule.clear_cache()

bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import wraps
12
from typing import TYPE_CHECKING
23

34
from bitbots_utils.utils import nobeartype
@@ -7,10 +8,29 @@
78
from bitbots_blackboard.body_blackboard import BodyBlackboard
89

910

11+
def cached_capsule_function(method):
12+
"""Decorator to cache the result of a method."""
13+
cache_key = method.__name__
14+
15+
@wraps(method)
16+
def wrapper(self):
17+
if cache_key not in self._cache:
18+
self._cache[cache_key] = method(self)
19+
return self._cache[cache_key]
20+
21+
return wrapper
22+
23+
1024
class AbstractBlackboardCapsule:
1125
"""Abstract class for blackboard capsules."""
1226

1327
@nobeartype
1428
def __init__(self, node: Node, blackboard: "BodyBlackboard"):
1529
self._node = node
1630
self._blackboard = blackboard
31+
32+
self._cache: dict[str, object] = {}
33+
34+
def clear_cache(self):
35+
"""Clear the cache of this capsule."""
36+
self._cache.clear()

bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/team_data_capsule.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ros2_numpy import numpify
99
from std_msgs.msg import Float32
1010

11-
from bitbots_blackboard.capsules import AbstractBlackboardCapsule
11+
from bitbots_blackboard.capsules import AbstractBlackboardCapsule, cached_capsule_function
1212
from bitbots_msgs.msg import Strategy, TeamData
1313

1414

@@ -68,16 +68,22 @@ def __init__(self, node, blackboard):
6868
self.data_timeout: float = float(self._node.get_parameter("team_data_timeout").value)
6969
self.ball_max_covariance: float = float(self._node.get_parameter("ball_max_covariance").value)
7070

71+
@cached_capsule_function
72+
def time(self) -> Time:
73+
"""Returns the current time of the node, this is its own function so it can be cached during the decision loop."""
74+
return self._node.get_clock().now()
75+
7176
def is_valid(self, data: TeamData) -> bool:
7277
"""
7378
Checks if a team data message from a given robot is valid.
7479
Meaning it is not too old and the robot is not penalized.
7580
"""
7681
return (
77-
self._node.get_clock().now() - Time.from_msg(data.header.stamp) < Duration(seconds=self.data_timeout)
82+
self.time() - Time.from_msg(data.header.stamp) < Duration(seconds=self.data_timeout)
7883
and data.state != TeamData.STATE_PENALIZED
7984
)
8085

86+
@cached_capsule_function
8187
def is_goalie_handling_ball(self) -> bool:
8288
"""Returns true if the goalie is going to the ball."""
8389
data: TeamData
@@ -90,6 +96,7 @@ def is_goalie_handling_ball(self) -> bool:
9096
return True
9197
return False
9298

99+
@cached_capsule_function
93100
def is_team_mate_kicking(self) -> bool:
94101
"""Returns true if one of the players in the own team is kicking."""
95102
data: TeamData
@@ -190,6 +197,7 @@ def is_not_goalie(team_data: TeamData) -> bool:
190197
# Count valid team data infos (aka robots with valid team data)
191198
return sum(map(self.is_valid, team_data_infos))
192199

200+
@cached_capsule_function
193201
def get_is_goalie_active(self) -> bool:
194202
def is_a_goalie(team_data: TeamData) -> bool:
195203
return team_data.strategy.role == Strategy.ROLE_GOALIE
@@ -221,6 +229,7 @@ def teammate_ball_is_valid(self) -> bool:
221229
"""Returns true if a teammate has seen the ball accurately enough"""
222230
return self.get_teammate_ball() is not None
223231

232+
@cached_capsule_function
224233
def get_teammate_ball(self) -> Optional[PointStamped]:
225234
"""Returns the best ball from all teammates that satisfies a minimum ball precision"""
226235

bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/world_model_capsule.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
TransformStamped,
1111
)
1212
from rclpy.clock import ClockType
13-
from rclpy.duration import Duration
1413
from rclpy.time import Time
1514
from ros2_numpy import msgify, numpify
1615
from std_msgs.msg import Header
1716
from std_srvs.srv import Trigger
1817
from tf2_geometry_msgs import Point, PointStamped
1918
from tf_transformations import euler_from_quaternion
2019

21-
from bitbots_blackboard.capsules import AbstractBlackboardCapsule
20+
from bitbots_blackboard.capsules import AbstractBlackboardCapsule, cached_capsule_function
2221

2322

2423
class WorldModelTFError(Exception): ...
@@ -27,9 +26,6 @@ class WorldModelTFError(Exception): ...
2726
class WorldModelPositionTFError(WorldModelTFError): ...
2827

2928

30-
class WorldModelBallTFError(WorldModelTFError): ...
31-
32-
3329
class WorldModelCapsule(AbstractBlackboardCapsule):
3430
"""Provides information about the world model."""
3531

@@ -78,10 +74,12 @@ def __init__(self, node, blackboard):
7874
### Ball ###
7975
############
8076

77+
@cached_capsule_function
8178
def ball_seen_self(self) -> bool:
8279
"""Returns true we are reasonably sure that we have seen the ball"""
8380
return all(np.diag(self._ball_covariance) < self.ball_max_covariance)
8481

82+
@cached_capsule_function
8583
def ball_has_been_seen(self) -> bool:
8684
"""Returns true if we or a teammate are reasonably sure that we have seen the ball"""
8785
return self.ball_seen_self() or self._blackboard.team_data.teammate_ball_is_valid()
@@ -91,6 +89,7 @@ def get_ball_position_xy(self) -> tuple[float, float]:
9189
ball = self.get_best_ball_point_stamped()
9290
return ball.point.x, ball.point.y
9391

92+
@cached_capsule_function
9493
def get_best_ball_point_stamped(self) -> PointStamped:
9594
"""
9695
Returns the best ball, either its own ball has been in the ball_lost_lost time
@@ -113,21 +112,25 @@ def get_best_ball_point_stamped(self) -> PointStamped:
113112
self.debug_publisher_which_ball.publish(Header(stamp=own_ball.header.stamp, frame_id="own_ball_map"))
114113
return own_ball
115114

115+
@cached_capsule_function
116116
def get_ball_position_uv(self) -> tuple[float, float]:
117117
"""
118118
Returns the ball position relative to the robot in the base_footprint frame.
119119
U and V are returned, where positive U is forward, positive V is to the left.
120120
"""
121121
ball = self.get_best_ball_point_stamped()
122-
try:
123-
ball_bfp = self._blackboard.tf_buffer.transform(
124-
ball, self.base_footprint_frame, timeout=Duration(seconds=0.2)
125-
).point
126-
except tf2.ExtrapolationException as e:
127-
self._node.get_logger().warn(str(e))
128-
self._node.get_logger().error("Severe transformation problem concerning the ball!")
129-
raise WorldModelBallTFError("Could not transform ball to base_footprint frame") from e
130-
return ball_bfp.x, ball_bfp.y
122+
assert ball.header.frame_id == self.map_frame, "Ball needs to be in the map frame"
123+
our_pose = self.get_current_position_transform()
124+
assert our_pose.header.frame_id == self.map_frame, "Our pose needs to be in the map frame"
125+
# Construct the homogeneous transformation matrix for the ball position
126+
ball_transform = np.eye(4)
127+
ball_transform[0, 3] = ball.point.x
128+
ball_transform[1, 3] = ball.point.y
129+
# Get the homogeneous transformation matrix for the robot's current position
130+
our_pose_transform = numpify(our_pose.transform)
131+
# Calculate the relative position of the ball to the robot
132+
relative_transform = np.linalg.inv(our_pose_transform) @ ball_transform
133+
return relative_transform[0, 3], relative_transform[1, 3]
131134

132135
def get_ball_distance(self) -> float:
133136
"""
@@ -171,9 +174,7 @@ def forget_ball(self) -> None:
171174
"""
172175
Forget that we saw a ball
173176
"""
174-
result: Trigger.Response = self.reset_ball_filter.call(Trigger.Request())
175-
if not result.success:
176-
self._node.get_logger().error(f"Ball filter reset failed with: '{result.message}'")
177+
self.reset_ball_filter.call_async(Trigger.Request())
177178

178179
########
179180
# Goal #
@@ -218,6 +219,7 @@ def get_map_based_opp_goal_right_post_uv(self) -> tuple[float, float]:
218219
# Pose #
219220
########
220221

222+
@cached_capsule_function
221223
def get_current_position(self) -> tuple[float, float, float]:
222224
"""
223225
Returns the current position on the field as determined by the localization.
@@ -228,6 +230,7 @@ def get_current_position(self) -> tuple[float, float, float]:
228230
theta = euler_from_quaternion(numpify(transform.transform.rotation))[2]
229231
return transform.transform.translation.x, transform.transform.translation.y, theta
230232

233+
@cached_capsule_function
231234
def get_current_position_pose_stamped(self) -> PoseStamped:
232235
"""
233236
Returns the current position as determined by the localization as a PoseStamped
@@ -241,6 +244,7 @@ def get_current_position_pose_stamped(self) -> PoseStamped:
241244
),
242245
)
243246

247+
@cached_capsule_function
244248
def get_current_position_transform(self) -> TransformStamped:
245249
"""
246250
Returns the current position as determined by the localization as a TransformStamped

bitbots_behavior/bitbots_body_behavior/bitbots_body_behavior/body_behavior.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from geometry_msgs.msg import PoseWithCovarianceStamped, Twist
99
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
1010
from rclpy.duration import Duration
11-
from rclpy.executors import MultiThreadedExecutor
11+
from rclpy.experimental.events_executor import EventsExecutor
1212
from rclpy.node import Node
1313
from soccer_vision_3d_msgs.msg import RobotArray
1414

@@ -85,6 +85,7 @@ def loop(self):
8585
self.counter = (self.counter + 1) % blackboard.config["time_to_ball_divider"]
8686
if self.counter == 0:
8787
blackboard.pathfinding.calculate_time_to_ball()
88+
blackboard.clear_cache()
8889
except Exception as e:
8990
import traceback
9091

@@ -97,12 +98,11 @@ def main(args=None):
9798
node = Node("body_behavior", automatically_declare_parameters_from_overrides=True)
9899
body_dsd = BodyDSD(node)
99100
node.create_timer(1 / 60.0, body_dsd.loop, callback_group=MutuallyExclusiveCallbackGroup(), clock=node.get_clock())
100-
# Number of executor threads is the number of MutuallyExclusiveCallbackGroups + 2 threads needed by the tf listener and executor
101-
multi_executor = MultiThreadedExecutor(num_threads=12)
102-
multi_executor.add_node(node)
103101

102+
executor = EventsExecutor()
103+
executor.add_node(node)
104104
try:
105-
multi_executor.spin()
105+
executor.spin()
106106
except KeyboardInterrupt:
107107
pass
108108

bitbots_misc/bitbots_teleop/bitbots_teleop/joy_node.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import rclpy
66
from geometry_msgs.msg import Twist
77
from rclpy.duration import Duration
8+
from rclpy.experimental.events_executor import EventsExecutor
89
from rclpy.node import Node
910
from sensor_msgs.msg import Joy
1011

@@ -240,6 +241,12 @@ def joy_cb(self, msg: Joy):
240241
def main():
241242
rclpy.init(args=None)
242243
node = JoyNode()
243-
rclpy.spin(node)
244+
245+
executor = EventsExecutor()
246+
executor.add_node(node)
247+
try:
248+
executor.spin()
249+
except KeyboardInterrupt:
250+
pass
251+
244252
node.destroy_node()
245-
rclpy.shutdown()

bitbots_misc/bitbots_tts/bitbots_tts/tts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ament_index_python import get_package_prefix
1111
from rcl_interfaces.msg import Parameter, SetParametersResult
1212
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
13-
from rclpy.executors import MultiThreadedExecutor
13+
from rclpy.experimental.events_executor import EventsExecutor
1414
from rclpy.node import Node
1515
from rclpy.publisher import Publisher
1616

@@ -121,7 +121,7 @@ def speak_cb(self, msg: Audio) -> None:
121121
def main(args=None):
122122
rclpy.init(args=args)
123123
node = Speaker()
124-
executor = MultiThreadedExecutor(num_threads=2)
124+
executor = EventsExecutor()
125125
executor.add_node(node)
126126
try:
127127
executor.spin()

bitbots_motion/bitbots_hcm/bitbots_hcm/humanoid_control_module.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from dynamic_stack_decider.dsd import DSD
1313
from rcl_interfaces.msg import Parameter as ParameterMsg
1414
from rclpy.duration import Duration
15-
from rclpy.executors import MultiThreadedExecutor
15+
from rclpy.experimental.events_executor import EventsExecutor
1616
from rclpy.node import Node
1717
from rclpy.parameter import Parameter
1818
from rclpy.serialization import deserialize_message
@@ -54,9 +54,9 @@ def __init__(self, use_sim_time, simulation_active, visualization_active):
5454
)
5555

5656
# Create own executor for Python part
57-
multi_executor = MultiThreadedExecutor(num_threads=10)
58-
multi_executor.add_node(self.node)
59-
self.spin_thread = threading.Thread(target=multi_executor.spin, args=(), daemon=True)
57+
executor = EventsExecutor()
58+
executor.add_node(self.node)
59+
self.spin_thread = threading.Thread(target=executor.spin, args=(), daemon=True)
6060
self.spin_thread.start()
6161

6262
# Otherwise messages will get lost, bc the init is not finished

bitbots_navigation/bitbots_localization_handler/bitbots_localization_handler/localization_handler.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from game_controller_hl_interfaces.msg import GameState
77
from geometry_msgs.msg import PoseWithCovarianceStamped
88
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup
9-
from rclpy.executors import MultiThreadedExecutor
9+
from rclpy.experimental.events_executor import EventsExecutor
1010
from rclpy.node import Node
1111
from sensor_msgs.msg import Imu
1212

@@ -65,12 +65,12 @@ def main(args=None):
6565
node.create_timer(1 / 25.0, dsd.update, callback_group=MutuallyExclusiveCallbackGroup())
6666

6767
# Create executor
68-
multi_executor = MultiThreadedExecutor(4)
69-
multi_executor.add_node(node)
68+
executor = EventsExecutor()
69+
executor.add_node(node)
7070

7171
# Spin the executor
7272
try:
73-
multi_executor.spin()
73+
executor.spin()
7474
except KeyboardInterrupt:
7575
pass
7676

bitbots_navigation/bitbots_path_planning/bitbots_path_planning/path_planning.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from geometry_msgs.msg import PointStamped, PoseStamped, PoseWithCovarianceStamped, Twist
55
from nav_msgs.msg import Path
66
from rclpy.duration import Duration
7+
from rclpy.experimental.events_executor import EventsExecutor
78
from std_msgs.msg import Bool, Empty
89
from visualization_msgs.msg import MarkerArray
910

@@ -74,7 +75,11 @@ def main(args=None):
7475
rclpy.init(args=args)
7576
node = PathPlanning()
7677

77-
rclpy.spin(node)
78+
executor = EventsExecutor()
79+
executor.add_node(node)
80+
try:
81+
executor.spin()
82+
except KeyboardInterrupt:
83+
pass
7884

7985
node.destroy_node()
80-
rclpy.shutdown()

0 commit comments

Comments
 (0)