77from roslibpy .actionlib import Goal
88
99from 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
1014from compas_fab .backends .ros .messages import FollowJointTrajectoryGoal
1115from compas_fab .backends .ros .messages import FollowJointTrajectoryResult
1216from 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
1521from compas_fab .backends .ros .messages import Time
1622from 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' ,
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
4258class 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