Skip to content

Commit 83e4fcd

Browse files
authored
Async/await and events executor in animation server (#704)
2 parents e5467bd + b8cbb34 commit 83e4fcd

File tree

3 files changed

+62
-17
lines changed

3 files changed

+62
-17
lines changed

bitbots_misc/bitbots_utils/bitbots_utils/utils.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
2-
from typing import Any
2+
from threading import Thread
3+
from typing import Any, Callable
34

45
import rclpy
56
import yaml
@@ -11,6 +12,7 @@
1112
from rcl_interfaces.srv import GetParameters, SetParameters
1213
from rclpy.node import Node
1314
from rclpy.parameter import PARAMETER_SEPARATOR_STRING, Parameter, parameter_value_to_python
15+
from rclpy.task import Future
1416

1517
# Create a new @nobeartype decorator disabling type-checking.
1618
nobeartype = beartype(conf=BeartypeConf(strategy=BeartypeStrategy.O0))
@@ -202,3 +204,43 @@ def parse_parameter_dict(*, namespace: str, parameter_dict: dict) -> list[Parame
202204
parameter = Parameter(name=full_param_name, value=param_value)
203205
parameters.append(parameter.to_parameter_msg())
204206
return parameters
207+
208+
209+
async def async_wait_for(node: Node, rel_time: float):
210+
"""
211+
ROS 2 does not provide an async sleep function, so we implement our own using a timer.
212+
This function will wait for the specified relative time in seconds.
213+
214+
:param node: The ROS2 node to create the timer on.
215+
:param rel_time: The relative time in seconds to wait.
216+
:return: None
217+
"""
218+
future = Future()
219+
rel_time = max(rel_time, 0.0)
220+
221+
def done_waiting():
222+
future.set_result(None)
223+
224+
timer = node.create_timer(rel_time, done_waiting, clock=node.get_clock())
225+
await future
226+
timer.cancel()
227+
node.destroy_timer(timer)
228+
229+
230+
async def async_run_thread(func: Callable[[], Any]) -> None:
231+
"""
232+
Allows the usage of blocking functions in an async context.
233+
234+
Spawns a thread to run the function and returns a Future that will be set when the function is done.
235+
"""
236+
future = Future()
237+
238+
def thread_func():
239+
try:
240+
future.set_result(func())
241+
except Exception as e:
242+
future.set_exception(e)
243+
244+
thread = Thread(target=thread_func)
245+
thread.start()
246+
await future

bitbots_motion/bitbots_animation_server/bitbots_animation_server/animation_node.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
import numpy as np
88
import rclpy
99
from bitbots_utils.transforms import quat2fused
10+
from bitbots_utils.utils import async_wait_for
1011
from rclpy.action import ActionServer, GoalResponse
1112
from rclpy.action.server import ServerGoalHandle
12-
from rclpy.callback_groups import ReentrantCallbackGroup
13-
from rclpy.duration import Duration
14-
from rclpy.executors import ExternalShutdownException, MultiThreadedExecutor
13+
from rclpy.executors import ExternalShutdownException
14+
from rclpy.experimental.events_executor import EventsExecutor
1515
from rclpy.node import Node
1616
from sensor_msgs.msg import Imu, JointState
1717
from simpleeval import simple_eval
@@ -65,11 +65,9 @@ def __init__(self):
6565
)
6666
traceback.print_exc()
6767

68-
callback_group = ReentrantCallbackGroup()
69-
7068
# Subscribers
71-
self.create_subscription(JointState, "joint_states", self.update_current_pose, 1, callback_group=callback_group)
72-
self.create_subscription(Imu, "imu/data", self.imu_callback, 1, callback_group=callback_group)
69+
self.create_subscription(JointState, "joint_states", self.update_current_pose, 1)
70+
self.create_subscription(Imu, "imu/data", self.imu_callback, 1)
7371

7472
# Service clients
7573
self.hcm_animation_mode = self.create_client(SetBool, "play_animation_mode")
@@ -79,7 +77,7 @@ def __init__(self):
7977

8078
# Action server for playing animations
8179
self._action_server = ActionServer(
82-
self, PlayAnimation, "animation", self.execute_cb, callback_group=callback_group, goal_callback=self.goal_cb
80+
self, PlayAnimation, "animation", self.execute_cb, goal_callback=self.goal_cb
8381
)
8482

8583
# Service to temporarily add an animation to the cache
@@ -125,7 +123,7 @@ def goal_cb(self, request: PlayAnimation.Goal) -> GoalResponse:
125123
# Everything is fine we are good to go
126124
return GoalResponse.ACCEPT
127125

128-
def execute_cb(self, goal: ServerGoalHandle) -> PlayAnimation.Result:
126+
async def execute_cb(self, goal: ServerGoalHandle) -> PlayAnimation.Result:
129127
"""This is called, when the action should be executed."""
130128

131129
# Convert goal id uuid to hashable tuple (custom UUID type)
@@ -165,16 +163,16 @@ def finish(successful: bool) -> PlayAnimation.Result:
165163

166164
# Send request to make the HCM to go into animation play mode
167165
num_tries = 0
168-
while rclpy.ok() and (not self.hcm_animation_mode.call(SetBool.Request(data=True)).success): # type: ignore[attr-defined]
166+
while rclpy.ok() and (not (await self.hcm_animation_mode.call_async(SetBool.Request(data=True))).success): # type: ignore[attr-defined]
169167
if num_tries >= 10:
170168
self.get_logger().error("Failed to request HCM to go into animation play mode")
171169
return finish(successful=False)
172170
num_tries += 1
173-
self.get_clock().sleep_for(Duration(seconds=0.1))
171+
await async_wait_for(self, 0.1)
174172

175173
# Make sure we have our current joint states
176174
while rclpy.ok() and self.current_joint_states is None:
177-
self.get_clock().sleep_for(Duration(seconds=0.1))
175+
await async_wait_for(self, 0.1)
178176

179177
# Create splines
180178
animator = SplineAnimator(
@@ -263,7 +261,9 @@ def finish(successful: bool) -> PlayAnimation.Result:
263261
if pose is None or (request.bounds and once and t > animator.get_keyframe_times()[request.end - 1]):
264262
# Animation is finished, tell it to the hcm (except if it is from the hcm)
265263
if not request.hcm:
266-
hcm_result: SetBool.Response = self.hcm_animation_mode.call(SetBool.Request(data=False))
264+
hcm_result: SetBool.Response = await self.hcm_animation_mode.call_async(
265+
SetBool.Request(data=False)
266+
)
267267
if not hcm_result.success:
268268
self.get_logger().error(f"Failed to finish animation on HCM. Reason: {hcm_result.message}")
269269

@@ -283,7 +283,10 @@ def finish(successful: bool) -> PlayAnimation.Result:
283283

284284
once = True
285285

286-
self.get_clock().sleep_until(last_time + Duration(seconds=0.02))
286+
execution_duration = (self.get_clock().now() - last_time).nanoseconds / 1e9
287+
288+
await async_wait_for(self, execution_duration + 0.02) # Wait for the next iteration
289+
287290
except (ExternalShutdownException, KeyboardInterrupt):
288291
sys.exit(0)
289292
return finish(successful=False)
@@ -339,7 +342,7 @@ def add_animation(self, request: AddAnimation.Request, response: AddAnimation.Re
339342
def main(args=None):
340343
rclpy.init(args=args)
341344
node = AnimationNode()
342-
ex = MultiThreadedExecutor(num_threads=10)
345+
ex = EventsExecutor()
343346
ex.add_node(node)
344347
try:
345348
ex.spin()

bitbots_motion/bitbots_hcm/bitbots_hcm/hcm_dsd/actions/change_motor_power.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,5 @@ def perform(self, reevaluate=False):
2626
if not self.blackboard.visualization_active and not self.blackboard.simulation_active:
2727
req = SetBool.Request()
2828
req.data = False
29-
self.blackboard.motor_switch_service.call(req)
29+
self.blackboard.motor_switch_service.call_async(req)
3030
return self.pop()

0 commit comments

Comments
 (0)