Skip to content

Commit 67e6995

Browse files
authored
Merge pull request #75 from compas-dev/fix/trajectory
Handle generic trajectory arguments on the ROS client trajectory follower
2 parents e8e6ac0 + da579dc commit 67e6995

File tree

11 files changed

+362
-164
lines changed

11 files changed

+362
-164
lines changed

.travis.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,14 @@ matrix:
99
before_install:
1010
- choco install python2 vcredist2008
1111
- choco install --ignore-dependencies vcpython27
12-
- pip install pypiwin32
1312
env:
1413
- PATH=/c/Python27:/c/Python27/Scripts:$PATH
1514
- TRAVIS_PYTHON_VERSION=2.7
1615
- name: "Python 3.7: Windows"
1716
os: windows
1817
language: shell
1918
before_install:
20-
- choco install python
21-
- pip install pypiwin32
19+
- choco install python3 --version=3.7.4
2220
env:
2321
- PATH=/c/Python37:/c/Python37/Scripts:$PATH
2422
- TRAVIS_PYTHON_VERSION=3.7

CHANGELOG.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,25 @@ Unreleased
1212

1313
**Changed**
1414

15+
* Fixed Python 2 vs Python 3 incompatibilities in ``compas_fab.sensors`` module
1516
* Changed example for loading PosConCM (includes parity argument, differs from PosCon3D)
1617
* Changed format ``compas_fab.sensors.baumer.PosConCM.set_flex_mount()``
1718
* Changed tasks.py to run ``invoke test``
18-
* Fixed Python 2 vs Python 3 incompatibilities
19-
19+
* Renamed ``compas_fab.backends.CancellableTask`` to ``compas_fab.backends.CancellableFutureResult``
20+
* 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()``
25-
27+
* 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

2932
* Removed ``compas_fab.sensors.baumer.PosConCM.get_live_monitor_data()``
33+
* Removed non-implemented methods from ``compas_fab.robots.Robot``: ``send_frame``, ``send_configuration``, ``send_trajectory``
3034

3135
**Fixed**
3236

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: 143 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@
77
from roslibpy.actionlib import Goal
88

99
from compas_fab.backends.ros.exceptions import RosError
10+
from compas_fab.backends.ros.messages import ExecuteTrajectoryFeedback
11+
from compas_fab.backends.ros.messages import ExecuteTrajectoryGoal
12+
from compas_fab.backends.ros.messages import ExecuteTrajectoryResult
13+
from compas_fab.backends.ros.messages import FollowJointTrajectoryFeedback
1014
from compas_fab.backends.ros.messages import FollowJointTrajectoryGoal
1115
from compas_fab.backends.ros.messages import FollowJointTrajectoryResult
1216
from compas_fab.backends.ros.messages import Header
13-
from compas_fab.backends.ros.messages import JointTrajectory
14-
from compas_fab.backends.ros.messages import JointTrajectoryPoint
17+
from compas_fab.backends.ros.messages import JointTrajectory as RosMsgJointTrajectory
18+
from compas_fab.backends.ros.messages import JointTrajectoryPoint as RosMsgJointTrajectoryPoint
19+
from compas_fab.backends.ros.messages import MoveItErrorCodes
20+
from compas_fab.backends.ros.messages import RobotTrajectory
1521
from compas_fab.backends.ros.messages import Time
1622
from compas_fab.backends.ros.planner_backend_moveit import MoveItPlanner
17-
from compas_fab.backends.tasks import CancellableTask
23+
from compas_fab.backends.tasks import CancellableFutureResult
1824

1925
__all__ = [
2026
'RosClient',
@@ -25,19 +31,29 @@
2531
}
2632

2733

28-
class CancellableRosAction(CancellableTask):
34+
class CancellableRosActionResult(CancellableFutureResult):
2935
def __init__(self, goal):
36+
super(CancellableRosActionResult, self).__init__()
3037
self.goal = goal
3138

3239
def cancel(self):
33-
"""Attempt to cancel the task.
40+
"""Attempt to cancel the action.
3441
3542
If the task is currently being executed and cannot be cancelled
3643
then the method will return ``False``, otherwise the call will
3744
be cancelled and the method will return ``True``.
3845
"""
46+
if self.done:
47+
raise Exception('Already completed action cannot be cancelled')
48+
49+
if self.goal.is_finished:
50+
return False
51+
3952
self.goal.cancel()
4053

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

4258
class RosClient(Ros):
4359
"""Interface to use ROS as backend via the **rosbridge**.
@@ -81,7 +97,6 @@ def __init__(self, host='localhost', port=9090, is_secure=False, planner_backend
8197
planner_backend_type = PLANNER_BACKENDS[planner_backend]
8298
self.__class__ = type('RosClient_' + planner_backend_type.__name__, (planner_backend_type, RosClient), {})
8399

84-
85100
def __enter__(self):
86101
self.run()
87102
self.connect()
@@ -227,7 +242,7 @@ def remove_attached_collision_mesh(self, id):
227242
def get_configuration(self):
228243
pass
229244

230-
def follow_configurations(self, callback, joint_names, configurations, timesteps, timeout=None):
245+
def follow_configurations(self, callback, joint_names, configurations, timesteps, timeout=60000):
231246

232247
if len(configurations) != len(timesteps):
233248
raise ValueError("%d configurations must have %d timesteps, but %d given." % (
@@ -239,21 +254,44 @@ def follow_configurations(self, callback, joint_names, configurations, timesteps
239254
points = []
240255
num_joints = len(configurations[0].values)
241256
for config, time in zip(configurations, timesteps):
242-
pt = JointTrajectoryPoint(positions=config.values, velocities=[
257+
pt = RosMsgJointTrajectoryPoint(positions=config.values, velocities=[
243258
0]*num_joints, time_from_start=Time(secs=(time)))
244259
points.append(pt)
245260

246-
joint_trajectory = JointTrajectory(
261+
joint_trajectory = RosMsgJointTrajectory(
247262
Header(), joint_names, points) # specify header necessary?
248-
return self.follow_joint_trajectory(callback, joint_trajectory, timeout)
263+
return self.follow_joint_trajectory(joint_trajectory=joint_trajectory, callback=callback, timeout=timeout)
264+
265+
def _convert_to_ros_trajectory(self, joint_trajectory):
266+
"""Converts a ``compas_fab.robots.JointTrajectory`` into a ROS Msg joint trajectory."""
267+
268+
# For backwards-compatibility, accept ROS Msg directly as well and simply do not modify
269+
if isinstance(joint_trajectory, RosMsgJointTrajectory):
270+
return joint_trajectory
271+
272+
trajectory = RosMsgJointTrajectory()
273+
trajectory.joint_names = joint_trajectory.joint_names
274+
275+
for point in joint_trajectory.points:
276+
ros_point = RosMsgJointTrajectoryPoint(
277+
positions=point.positions,
278+
velocities=point.velocities,
279+
accelerations=point.accelerations,
280+
effort=point.effort,
281+
time_from_start=Time(
282+
point.time_from_start.secs, point.time_from_start.nsecs),
283+
)
284+
trajectory.points.append(ros_point)
285+
286+
return trajectory
249287

250288
def follow_joint_trajectory(self, joint_trajectory, action_name='/joint_trajectory_action', callback=None, errback=None, feedback_callback=None, timeout=60000):
251289
"""Follow the joint trajectory as computed by MoveIt planner.
252290
253291
Parameters
254292
----------
255-
joint_trajectory : JointTrajectory
256-
Joint trajectory message as computed by MoveIt planner
293+
joint_trajectory : :class:`compas_fab.robots.JointTrajectory`
294+
Instance of joint trajectory. Note: for backwards compatibility, this supports a ROS Msg being passed as well.
257295
action_name : string
258296
ROS action name, defaults to ``/joint_trajectory_action`` but some drivers need ``/follow_joint_trajectory``.
259297
callback : callable
@@ -273,33 +311,112 @@ def follow_joint_trajectory(self, joint_trajectory, action_name='/joint_trajecto
273311
An instance of a cancellable tasks.
274312
"""
275313

276-
trajectory_goal = FollowJointTrajectoryGoal(
277-
trajectory=joint_trajectory)
314+
joint_trajectory = self._convert_to_ros_trajectory(joint_trajectory)
315+
trajectory_goal = FollowJointTrajectoryGoal(trajectory=joint_trajectory)
278316

279-
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):
280322
result = FollowJointTrajectoryResult.from_msg(msg)
281-
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)
282338

283339
def handle_failure(error):
284340
errback(error)
285341

286-
action_client = ActionClient(self, action_name,
287-
'control_msgs/FollowJointTrajectoryAction')
342+
goal.on('result', handle_result)
288343

344+
if feedback_callback:
345+
goal.on('feedback', handle_feedback)
346+
347+
if errback:
348+
goal.on('timeout', lambda: handle_failure(
349+
RosError('Action Goal timeout', -1)))
350+
351+
goal.send(timeout=timeout)
352+
353+
return action_result
354+
355+
def execute_joint_trajectory(self, joint_trajectory, action_name='/execute_trajectory', callback=None, errback=None, feedback_callback=None, timeout=60000):
356+
"""Execute a joint trajectory via the MoveIt infrastructure.
357+
358+
Note
359+
----
360+
This method does not support Multi-DOF Joint Trajectories.
361+
362+
Parameters
363+
----------
364+
joint_trajectory : :class:`compas_fab.robots.JointTrajectory`
365+
Instance of joint trajectory.
366+
callback : callable
367+
Function to be invoked when the goal is completed, requires
368+
one positional parameter ``result``.
369+
action_name : string
370+
ROS action name, defaults to ``/execute_trajectory``.
371+
errback : callable
372+
Function to be invoked in case of error or timeout, requires
373+
one position parameter ``exception``.
374+
feedback_callback : callable
375+
Function to be invoked during execution to provide feedback.
376+
timeout : int
377+
Timeout for goal completion in milliseconds.
378+
379+
Returns
380+
-------
381+
:class:`CancellableFutureResult`
382+
An instance of a cancellable future result.
383+
"""
384+
385+
joint_trajectory = self._convert_to_ros_trajectory(joint_trajectory)
386+
trajectory = RobotTrajectory(joint_trajectory=joint_trajectory)
387+
trajectory_goal = ExecuteTrajectoryGoal(trajectory=trajectory)
388+
389+
action_client = ActionClient(self, action_name, 'moveit_msgs/ExecuteTrajectoryAction')
289390
goal = Goal(action_client, Message(trajectory_goal.msg))
391+
action_result = CancellableRosActionResult(goal)
392+
393+
def handle_result(msg):
394+
result = ExecuteTrajectoryResult.from_msg(msg)
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)
404+
405+
def handle_feedback(msg):
406+
feedback = ExecuteTrajectoryFeedback.from_msg(msg)
407+
feedback_callback(feedback)
290408

291-
if callback:
292-
goal.on('result', lambda result: handle_result(
293-
result, action_client))
409+
def handle_failure(error):
410+
errback(error)
411+
412+
goal.on('result', handle_result)
294413

295414
if feedback_callback:
296-
goal.on('feedback', feedback_callback)
415+
goal.on('feedback', handle_feedback)
297416

298417
if errback:
299-
goal.on('timeout', lambda: handle_failure(
300-
RosError("Action Goal timeout", -1)))
418+
goal.on('timeout', lambda: handle_failure(RosError('Action Goal timeout', -1)))
301419

302420
goal.send(timeout=timeout)
303421

304-
return CancellableRosAction(goal)
305-
422+
return action_result

0 commit comments

Comments
 (0)