Skip to content

Commit eeaefb2

Browse files
committed
Refactor cancellable task into future results
1 parent 1da319b commit eeaefb2

File tree

4 files changed

+125
-35
lines changed

4 files changed

+125
-35
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ Unreleased
1616
* Changed example for loading PosConCM (includes parity argument, differs from PosCon3D)
1717
* Changed format ``compas_fab.sensors.baumer.PosConCM.set_flex_mount()``
1818
* Changed tasks.py to run ``invoke test``
19+
* Renamed ``compas_fab.backends.CancellableTask`` to ``compas_fab.backends.CancellableFutureResult``
1920
* ROS client: changed joint trajectory follower (``follow_joint_trajectory``) to support generic ``JointTrajectory`` arguments.
21+
* ROS client: changed return type of trajectory execution methods to ``CancellableFutureResult``
2022

2123
**Added**
2224

2325
* Added ``compas_fab.sensors.baumer.PosCon3D.reset()``
2426
* Added ``compas_fab.sensors.baumer.PosConCM.reset()``
2527
* ROS client: added support for MoveIt! execution action via `client.execute_joint_trajectory`.
28+
* Added ``compas_fab.backends.FutureResult`` class to deal with long-running async tasks
2629

2730
**Removed**
2831

src/compas_fab/backends/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@
2626
RosClient
2727
RosFileServerLoader
2828
29-
Tasks
30-
-----
29+
Long-running tasks
30+
------------------
3131
3232
.. autosummary::
3333
:toctree: generated/
3434
:nosignatures:
3535
36-
CancellableTask
36+
FutureResult
37+
CancellableFutureResult
3738
3839
Exceptions
3940
----------

src/compas_fab/backends/ros/client.py

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@
1010
from compas_fab.backends.ros.messages import ExecuteTrajectoryFeedback
1111
from compas_fab.backends.ros.messages import ExecuteTrajectoryGoal
1212
from compas_fab.backends.ros.messages import ExecuteTrajectoryResult
13+
from compas_fab.backends.ros.messages import FollowJointTrajectoryFeedback
1314
from compas_fab.backends.ros.messages import FollowJointTrajectoryGoal
1415
from compas_fab.backends.ros.messages import FollowJointTrajectoryResult
1516
from compas_fab.backends.ros.messages import Header
1617
from compas_fab.backends.ros.messages import JointTrajectory as RosMsgJointTrajectory
1718
from compas_fab.backends.ros.messages import JointTrajectoryPoint as RosMsgJointTrajectoryPoint
19+
from compas_fab.backends.ros.messages import MoveItErrorCodes
1820
from compas_fab.backends.ros.messages import RobotTrajectory
1921
from compas_fab.backends.ros.messages import Time
2022
from compas_fab.backends.ros.planner_backend_moveit import MoveItPlanner
21-
from compas_fab.backends.tasks import CancellableTask
23+
from compas_fab.backends.tasks import CancellableFutureResult
2224

2325
__all__ = [
2426
'RosClient',
@@ -29,19 +31,29 @@
2931
}
3032

3133

32-
class CancellableRosAction(CancellableTask):
34+
class CancellableRosActionResult(CancellableFutureResult):
3335
def __init__(self, goal):
36+
super(CancellableRosActionResult, self).__init__()
3437
self.goal = goal
3538

3639
def cancel(self):
37-
"""Attempt to cancel the task.
40+
"""Attempt to cancel the action.
3841
3942
If the task is currently being executed and cannot be cancelled
4043
then the method will return ``False``, otherwise the call will
4144
be cancelled and the method will return ``True``.
4245
"""
46+
if self.done:
47+
raise Exception('Already completed action cannot be cancelled')
48+
49+
if self.goal.is_finished:
50+
return False
51+
4352
self.goal.cancel()
4453

54+
# NOTE: Check if we can output more meaning results than just "we tried to cancel"
55+
return True
56+
4557

4658
class RosClient(Ros):
4759
"""Interface to use ROS as backend via the **rosbridge**.
@@ -302,32 +314,43 @@ def follow_joint_trajectory(self, joint_trajectory, action_name='/joint_trajecto
302314
joint_trajectory = self._convert_to_ros_trajectory(joint_trajectory)
303315
trajectory_goal = FollowJointTrajectoryGoal(trajectory=joint_trajectory)
304316

305-
def handle_result(msg, client):
317+
action_client = ActionClient(self, action_name, 'control_msgs/FollowJointTrajectoryAction')
318+
goal = Goal(action_client, Message(trajectory_goal.msg))
319+
action_result = CancellableRosActionResult(goal)
320+
321+
def handle_result(msg):
306322
result = FollowJointTrajectoryResult.from_msg(msg)
307-
callback(result)
323+
if result.error_code != FollowJointTrajectoryResult.SUCCESSFUL:
324+
ros_error = RosError(
325+
'Follow trajectory failed={}'.format(result.error_string),
326+
int(result.error_code))
327+
action_result._set_error_result(ros_error)
328+
if errback:
329+
errback(ros_error)
330+
else:
331+
action_result._set_result(result)
332+
if callback:
333+
callback(result)
334+
335+
def handle_feedback(msg):
336+
feedback = FollowJointTrajectoryFeedback.from_msg(msg)
337+
feedback_callback(feedback)
308338

309339
def handle_failure(error):
310340
errback(error)
311341

312-
action_client = ActionClient(self, action_name,
313-
'control_msgs/FollowJointTrajectoryAction')
314-
315-
goal = Goal(action_client, Message(trajectory_goal.msg))
316-
317-
if callback:
318-
goal.on('result', lambda result: handle_result(
319-
result, action_client))
342+
goal.on('result', handle_result)
320343

321344
if feedback_callback:
322-
goal.on('feedback', feedback_callback)
345+
goal.on('feedback', handle_feedback)
323346

324347
if errback:
325348
goal.on('timeout', lambda: handle_failure(
326-
RosError("Action Goal timeout", -1)))
349+
RosError('Action Goal timeout', -1)))
327350

328351
goal.send(timeout=timeout)
329352

330-
return CancellableRosAction(goal)
353+
return action_result
331354

332355
def execute_joint_trajectory(self, joint_trajectory, action_name='/execute_trajectory', callback=None, errback=None, feedback_callback=None, timeout=60000):
333356
"""Execute a joint trajectory via the MoveIt infrastructure.
@@ -355,17 +378,29 @@ def execute_joint_trajectory(self, joint_trajectory, action_name='/execute_traje
355378
356379
Returns
357380
-------
358-
:class:`CancellableTask`
359-
An instance of a cancellable tasks.
381+
:class:`CancellableFutureResult`
382+
An instance of a cancellable future result.
360383
"""
361384

362385
joint_trajectory = self._convert_to_ros_trajectory(joint_trajectory)
363386
trajectory = RobotTrajectory(joint_trajectory=joint_trajectory)
364387
trajectory_goal = ExecuteTrajectoryGoal(trajectory=trajectory)
365388

389+
action_client = ActionClient(self, action_name, 'moveit_msgs/ExecuteTrajectoryAction')
390+
goal = Goal(action_client, Message(trajectory_goal.msg))
391+
action_result = CancellableRosActionResult(goal)
392+
366393
def handle_result(msg):
367394
result = ExecuteTrajectoryResult.from_msg(msg)
368-
callback(result)
395+
if result.error_code != MoveItErrorCodes.SUCCESS:
396+
ros_error = RosError('Execute trajectory failed. Message={}'.format(result.error_code.human_readable), int(result.error_code))
397+
action_result._set_error_result(ros_error)
398+
if errback:
399+
errback(ros_error)
400+
else:
401+
action_result._set_result(result)
402+
if callback:
403+
callback(result)
369404

370405
def handle_feedback(msg):
371406
feedback = ExecuteTrajectoryFeedback.from_msg(msg)
@@ -374,18 +409,14 @@ def handle_feedback(msg):
374409
def handle_failure(error):
375410
errback(error)
376411

377-
action_client = ActionClient(self, action_name, 'moveit_msgs/ExecuteTrajectoryAction')
378-
goal = Goal(action_client, Message(trajectory_goal.msg))
379-
380-
if callback:
381-
goal.on('result', handle_result)
412+
goal.on('result', handle_result)
382413

383414
if feedback_callback:
384415
goal.on('feedback', handle_feedback)
385416

386417
if errback:
387-
goal.on('timeout', lambda: handle_failure(RosError("Action Goal timeout", -1)))
418+
goal.on('timeout', lambda: handle_failure(RosError('Action Goal timeout', -1)))
388419

389420
goal.send(timeout=timeout)
390421

391-
return CancellableRosAction(goal)
422+
return action_result

src/compas_fab/backends/tasks.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,74 @@
22
from __future__ import division
33
from __future__ import print_function
44

5+
import threading
6+
57
__all__ = [
6-
'CancellableTask',
8+
'FutureResult',
9+
'CancellableFutureResult'
710
]
811

912

10-
class CancellableTask(object):
11-
"""Preemptable task represents a long-running operation that can be cancelled."""
13+
class FutureResult(object):
14+
"""Represents a future result value.
15+
16+
Futures are the result of asynchronous operations
17+
but allow to explicitely control when to block and wait
18+
for its completion."""
19+
20+
def __init__(self):
21+
self.done = False
22+
self.value = None
23+
self.error = None
24+
self.event = threading.Event()
25+
26+
def result(self, timeout=None):
27+
"""Return the feedback value returned by the instruction.
28+
29+
If the instruction has not yet returned feedback, it will wait
30+
up to ``timeout`` seconds. If the ``timeout`` expires, the method
31+
will raise an exception.
32+
"""
33+
if not self.done:
34+
if not self.event.wait(timeout):
35+
raise Exception('Timeout: future result not available')
36+
37+
if self.error:
38+
raise self.error
39+
40+
return self.value
41+
42+
def _set_result(self, message):
43+
self.done = True
44+
self.value = message
45+
self.error = None
46+
self.event.set()
47+
48+
def _set_error_result(self, error):
49+
"""Mark the future as failed.
50+
51+
Parameters
52+
----------
53+
error : Exception
54+
An exception instance.
55+
"""
56+
self.done = True
57+
self.value = None
58+
self.error = error
59+
self.event.set()
60+
61+
62+
class CancellableFutureResult(FutureResult):
63+
"""Represents a future result of a long-running asynchronous operation that can be cancelled."""
64+
65+
def __init__(self):
66+
super(CancellableFutureResult, self).__init__()
1267

1368
def cancel(self):
14-
"""Attempt to cancel the task.
69+
"""Attempt to cancel the operation.
1570
16-
If the task is currently being executed and cannot be cancelled
71+
If the operation is currently being executed and cannot be cancelled
1772
then the method will return ``False``, otherwise the call will
1873
be cancelled and the method will return ``True``.
1974
"""
20-
raise NotImplementedError('Concrete tasks need to provide an implementation')
75+
raise NotImplementedError('Needs concrete implementation')

0 commit comments

Comments
 (0)