Tutorial 7: Move Robots with MoveIt

Tutorial 7

This tutorial shows how to move the robots through service calls using the following steps:

  • Create a C++ class for interfacing with MoveIt (src/robot_commander.cpp).

    • Initialize a MoveGroupInterface object for each robot.

    • Create a service server for each robot. Service requests sent to these service servers will be used to move the robots to their home position.

  • Create a Python method that will be used to move the robot to its home position using a service client.

  • Call the Python method from the script tutorial_7.py to move the robots to their home positions.

Package Structure

Updates and additions that are specific to Tutorial 7 are highlighted in the tree below.

ariac_tutorials
├── CMakeLists.txt
├── package.xml
├── config
│   └── sensors.yaml
├── launch
│   └── robot_commander.launch.py
├── ariac_tutorials
│   ├── __init__.py
│   ├── utils.py
│   └── competition_interface.py
├── src
│   └── robot_commander.cpp
└── scripts
    ├── tutorial_1.py
    ├── tutorial_2.py
    ├── tutorial_3.py
    ├── tutorial_4.py
    ├── tutorial_5.py
    ├── tutorial_6.py
    └── tutorial_7.py

Updated/Created Files

Competition Interface

The competition interface used in this tutorial is shown in Listing 53.

Listing 53 competition_interface.py
  1import rclpy
  2from rclpy.time import Duration
  3from rclpy.node import Node
  4from rclpy.qos import qos_profile_sensor_data
  5from rclpy.parameter import Parameter
  6
  7from ariac_msgs.msg import (
  8    CompetitionState as CompetitionStateMsg,
  9    BreakBeamStatus as BreakBeamStatusMsg,
 10    AdvancedLogicalCameraImage as AdvancedLogicalCameraImageMsg,
 11    Part as PartMsg,
 12    PartPose as PartPoseMsg,
 13    Order as OrderMsg,
 14    AssemblyPart as AssemblyPartMsg,
 15    AGVStatus as AGVStatusMsg,
 16    AssemblyTask as AssemblyTaskMsg,
 17    VacuumGripperState,
 18)
 19
 20from ariac_msgs.srv import (
 21    MoveAGV,
 22    VacuumGripperControl
 23)
 24
 25from std_srvs.srv import Trigger
 26
 27from ariac_tutorials.utils import (
 28    multiply_pose,
 29    rpy_from_quaternion,
 30    rad_to_deg_str,
 31    AdvancedLogicalCameraImage,
 32    Order,
 33    KittingTask,
 34    CombinedTask,
 35    AssemblyTask,
 36    KittingPart
 37)
 38
 39class CompetitionInterface(Node):
 40    '''
 41    Class for a competition interface node.
 42
 43    Args:
 44        Node (rclpy.node.Node): Parent class for ROS nodes
 45
 46    Raises:
 47        KeyboardInterrupt: Exception raised when the user uses Ctrl+C to kill a process
 48    '''
 49    _competition_states = {
 50        CompetitionStateMsg.IDLE: 'idle',
 51        CompetitionStateMsg.READY: 'ready',
 52        CompetitionStateMsg.STARTED: 'started',
 53        CompetitionStateMsg.ORDER_ANNOUNCEMENTS_DONE: 'order_announcements_done',
 54        CompetitionStateMsg.ENDED: 'ended',
 55    }
 56    '''Dictionary for converting CompetitionState constants to strings'''
 57
 58    _part_colors = {
 59        PartMsg.RED: 'red',
 60        PartMsg.BLUE: 'blue',
 61        PartMsg.GREEN: 'green',
 62        PartMsg.ORANGE: 'orange',
 63        PartMsg.PURPLE: 'purple',
 64    }
 65    '''Dictionary for converting Part color constants to strings'''
 66
 67    _part_colors_emoji = {
 68        PartMsg.RED: '🟥',
 69        PartMsg.BLUE: '🟦',
 70        PartMsg.GREEN: '🟩',
 71        PartMsg.ORANGE: '🟧',
 72        PartMsg.PURPLE: '🟪',
 73    }
 74    '''Dictionary for converting Part color constants to emojis'''
 75
 76    _part_types = {
 77        PartMsg.BATTERY: 'battery',
 78        PartMsg.PUMP: 'pump',
 79        PartMsg.REGULATOR: 'regulator',
 80        PartMsg.SENSOR: 'sensor',
 81    }
 82    '''Dictionary for converting Part type constants to strings'''
 83
 84    _destinations = {
 85        AGVStatusMsg.KITTING: 'kitting station',
 86        AGVStatusMsg.ASSEMBLY_FRONT: 'front assembly station',
 87        AGVStatusMsg.ASSEMBLY_BACK: 'back assembly station',
 88        AGVStatusMsg.WAREHOUSE: 'warehouse',
 89    }
 90    '''Dictionary for converting AGVDestination constants to strings'''
 91
 92    _stations = {
 93        AssemblyTaskMsg.AS1: 'assembly station 1',
 94        AssemblyTaskMsg.AS2: 'assembly station 2',
 95        AssemblyTaskMsg.AS3: 'assembly station 3',
 96        AssemblyTaskMsg.AS4: 'assembly station 4',
 97    }
 98    '''Dictionary for converting AssemblyTask constants to strings'''
 99
100    _gripper_states = {
101        True: 'enabled',
102        False: 'disabled'
103    }
104    '''Dictionary for converting VacuumGripperState constants to strings'''
105
106    def __init__(self):
107        super().__init__('competition_interface')
108
109        sim_time = Parameter(
110            "use_sim_time",
111            rclpy.Parameter.Type.BOOL,
112            True
113        )
114
115        self.set_parameters([sim_time])
116
117        # Service client for starting the competition
118        self._start_competition_client = self.create_client(Trigger, '/ariac/start_competition')
119
120        # Subscriber to the competition state topic
121        self._competition_state_sub = self.create_subscription(
122            CompetitionStateMsg,
123            '/ariac/competition_state',
124            self._competition_state_cb,
125            10)
126
127        # Store the state of the competition
128        self._competition_state: CompetitionStateMsg = None
129
130        # Subscriber to the break beam status topic
131        self._break_beam0_sub = self.create_subscription(
132            BreakBeamStatusMsg,
133            '/ariac/sensors/breakbeam_0/status',
134            self._breakbeam0_cb,
135            qos_profile_sensor_data)
136
137        # Store the number of parts that crossed the beam
138        self._conveyor_part_count = 0
139
140        # Store whether the beam is broken
141        self._object_detected = False
142
143        # Subscriber to the logical camera topic
144        self._advanced_camera0_sub = self.create_subscription(
145            AdvancedLogicalCameraImageMsg,
146            '/ariac/sensors/advanced_camera_0/image',
147            self._advanced_camera0_cb,
148            qos_profile_sensor_data)
149
150        # Store each camera image as an AdvancedLogicalCameraImage object
151        self._camera_image: AdvancedLogicalCameraImage = None
152
153        # Subscriber to the order topic
154        self.orders_sub = self.create_subscription(
155            OrderMsg,
156            '/ariac/orders',
157            self._orders_cb,
158            10)
159
160        # Flag for parsing incoming orders
161        self._parse_incoming_order = False
162
163        # List of orders
164        self._orders = []
165
166        # Subscriber to the floor gripper state topic
167        self._floor_robot_gripper_state_sub = self.create_subscription(
168            VacuumGripperState,
169            '/ariac/floor_robot_gripper_state',
170            self._floor_robot_gripper_state_cb,
171            qos_profile_sensor_data)
172
173        # Service client for turning on/off the vacuum gripper on the floor robot
174        self._floor_gripper_enable = self.create_client(
175            VacuumGripperControl,
176            "/ariac/floor_robot_enable_gripper")
177
178        # Attribute to store the current state of the floor robot gripper
179        self._floor_robot_gripper_state = VacuumGripperState()
180
181        # Service client for moving the floor robot to the home position
182        self._move_floor_robot_home = self.create_client(
183            Trigger, '/competitor/move_floor_robot_home')
184
185        # Service client for moving the ceiling robot to the home position
186        self._move_ceiling_robot_home = self.create_client(
187            Trigger, '/competitor/move_ceiling_robot_home')
188
189    @property
190    def orders(self):
191        return self._orders
192
193    @property
194    def camera_image(self):
195        return self._camera_image
196
197    @property
198    def conveyor_part_count(self):
199        return self._conveyor_part_count
200
201    @property
202    def parse_incoming_order(self):
203        return self._parse_incoming_order
204
205    @parse_incoming_order.setter
206    def parse_incoming_order(self, value):
207        self._parse_incoming_order = value
208
209    def _orders_cb(self, msg: Order):
210        '''Callback for the topic /ariac/orders
211        Arguments:
212            msg -- Order message
213        '''
214        order = Order(msg)
215        self._orders.append(order)
216        if self._parse_incoming_order:
217            self.get_logger().info(self._parse_order(order))
218
219    def _advanced_camera0_cb(self, msg: AdvancedLogicalCameraImageMsg):
220        '''Callback for the topic /ariac/sensors/advanced_camera_0/image
221
222        Arguments:
223            msg -- AdvancedLogicalCameraImage message
224        '''
225        self._camera_image = AdvancedLogicalCameraImage(msg.part_poses,
226                                                        msg.tray_poses,
227                                                        msg.sensor_pose)
228
229    def _breakbeam0_cb(self, msg: BreakBeamStatusMsg):
230        '''Callback for the topic /ariac/sensors/breakbeam_0/status
231
232        Arguments:
233            msg -- BreakBeamStatusMsg message
234        '''
235        if not self._object_detected and msg.object_detected:
236            self._conveyor_part_count += 1
237
238        self._object_detected = msg.object_detected
239
240    def _competition_state_cb(self, msg: CompetitionStateMsg):
241        '''Callback for the topic /ariac/competition_state
242        Arguments:
243            msg -- CompetitionState message
244        '''
245        # Log if competition state has changed
246        if self._competition_state != msg.competition_state:
247            state = CompetitionInterface._competition_states[msg.competition_state]
248            self.get_logger().info(f'Competition state is: {state}', throttle_duration_sec=1.0)
249
250        self._competition_state = msg.competition_state
251
252    def _floor_robot_gripper_state_cb(self, msg: VacuumGripperState):
253        '''Callback for the topic /ariac/floor_robot_gripper_state
254
255        Arguments:
256            msg -- VacuumGripperState message
257        '''
258        self._floor_robot_gripper_state = msg
259
260    def start_competition(self):
261        '''Function to start the competition.
262        '''
263        self.get_logger().info('Waiting for competition to be ready')
264
265        if self._competition_state == CompetitionStateMsg.STARTED:
266            return
267        # Wait for competition to be ready
268        while self._competition_state != CompetitionStateMsg.READY:
269            try:
270                rclpy.spin_once(self)
271            except KeyboardInterrupt:
272                return
273
274        self.get_logger().info('Competition is ready. Starting...')
275
276        # Check if service is available
277        if not self._start_competition_client.wait_for_service(timeout_sec=3.0):
278            self.get_logger().error('Service \'/ariac/start_competition\' is not available.')
279            return
280
281        # Create trigger request and call starter service
282        request = Trigger.Request()
283        future = self._start_competition_client.call_async(request)
284
285        # Wait until the service call is completed
286        rclpy.spin_until_future_complete(self, future)
287
288        if future.result().success:
289            self.get_logger().info('Started competition.')
290        else:
291            self.get_logger().warn('Unable to start competition')
292
293    def parse_advanced_camera_image(self, image: AdvancedLogicalCameraImage) -> str:
294        '''
295        Parse an AdvancedLogicalCameraImage message and return a string representation.
296        '''
297
298        if len(image._part_poses) == 0:
299            return 'No parts detected'
300
301        output = '\n\n'
302        for i, part_pose in enumerate(image._part_poses):
303            part_pose: PartPoseMsg
304            output += '==========================\n'
305            part_color = CompetitionInterface._part_colors[part_pose.part.color].capitalize()
306            part_color_emoji = CompetitionInterface._part_colors_emoji[part_pose.part.color]
307            part_type = CompetitionInterface._part_types[part_pose.part.type].capitalize()
308            output += f'Part {i+1}: {part_color_emoji} {part_color} {part_type}\n'
309            output += '--------------------------\n'
310            output += 'Camera Frame\n'
311            output += '--------------------------\n'
312
313            output += '  Position:\n'
314            output += f'    x: {part_pose.pose.position.x:.3f} (m)\n'
315            output += f'    y: {part_pose.pose.position.y:.3f} (m)\n'
316            output += f'    z: {part_pose.pose.position.z:.3f} (m)\n'
317
318            roll, pitch, yaw = rpy_from_quaternion(part_pose.pose.orientation)
319            output += '  Orientation:\n'
320            output += f'    roll: {rad_to_deg_str(roll)}\n'
321            output += f'    pitch: {rad_to_deg_str(pitch)}\n'
322            output += f'    yaw: {rad_to_deg_str(yaw)}\n'
323
324            part_world_pose = multiply_pose(image._sensor_pose, part_pose.pose)
325            output += '--------------------------\n'
326            output += 'World Frame\n'
327            output += '--------------------------\n'
328
329            output += '  Position:\n'
330            output += f'    x: {part_world_pose.position.x:.3f} (m)\n'
331            output += f'    y: {part_world_pose.position.y:.3f} (m)\n'
332            output += f'    z: {part_world_pose.position.z:.3f} (m)\n'
333
334            roll, pitch, yaw = rpy_from_quaternion(part_world_pose.orientation)
335            output += '  Orientation:\n'
336            output += f'    roll: {rad_to_deg_str(roll)}\n'
337            output += f'    pitch: {rad_to_deg_str(pitch)}\n'
338            output += f'    yaw: {rad_to_deg_str(yaw)}\n'
339
340            output += '==========================\n\n'
341
342        return output
343
344    def _parse_kitting_task(self, kitting_task: KittingTask):
345        '''
346        Parses a KittingTask object and returns a string representation.
347        Args:
348            kitting_task (KittingTask): KittingTask object to parse
349        Returns:
350            str: String representation of the KittingTask object
351        '''
352        output = 'Type: Kitting\n'
353        output += '==========================\n'
354        output += f'AGV: {kitting_task.agv_number}\n'
355        output += f'Destination: {CompetitionInterface._destinations[kitting_task.destination]}\n'
356        output += f'Tray ID: {kitting_task.tray_id}\n'
357        output += 'Products:\n'
358        output += '==========================\n'
359
360        quadrants = {1: "Quadrant 1: -",
361                    2: "Quadrant 2: -",
362                    3: "Quadrant 3: -",
363                    4: "Quadrant 4: -"}
364
365        for i in range(1, 5):
366            product: KittingPart
367            for product in kitting_task.parts:
368                if i == product.quadrant:
369                    part_color = CompetitionInterface._part_colors[product.part.color].capitalize()
370                    part_color_emoji = CompetitionInterface._part_colors_emoji[product.part.color]
371                    part_type = CompetitionInterface._part_types[product.part.type].capitalize()
372                    quadrants[i] = f'Quadrant {i}: {part_color_emoji} {part_color} {part_type}'
373        output += f'\t{quadrants[1]}\n'
374        output += f'\t{quadrants[2]}\n'
375        output += f'\t{quadrants[3]}\n'
376        output += f'\t{quadrants[4]}\n'
377
378        return output
379
380    def _parse_assembly_task(self, assembly_task: AssemblyTask):
381        '''
382        Parses an AssemblyTask object and returns a string representation.
383
384        Args:
385            assembly_task (AssemblyTask): AssemblyTask object to parse
386
387        Returns:
388            str: String representation of the AssemblyTask object
389        '''
390        output = 'Type: Assembly\n'
391        output += '==========================\n'
392        if len(assembly_task.agv_numbers) == 1:
393            output += f'AGV: {assembly_task.agv_number[0]}\n'
394        elif len(assembly_task.agv_numbers) == 2:
395            output += f'AGV(s): [{assembly_task.agv_numbers[0]}, {assembly_task.agv_numbers[1]}]\n'
396        output += f'Station: {self._stations[assembly_task.station].title()}\n'
397        output += 'Products:\n'
398        output += '==========================\n'
399
400        product: AssemblyPartMsg
401        for product in assembly_task.parts:
402            part_color = CompetitionInterface._part_colors[product.part.color].capitalize()
403            part_color_emoji = CompetitionInterface._part_colors_emoji[product.part.color]
404            part_type = CompetitionInterface._part_types[product.part.type].capitalize()
405
406            output += f'Part: {part_color_emoji} {part_color} {part_type}\n'
407
408            output += '  Position:\n'
409            output += f'    x: {product.assembled_pose.pose.position.x:.3f} (m)\n'
410            output += f'    y: {product.assembled_pose.pose.position.y:.3f} (m)\n'
411            output += f'    z: {product.assembled_pose.pose.position.z:.3f} (m)\n'
412
413            roll, pitch, yaw = rpy_from_quaternion(product.assembled_pose.pose.orientation)
414            output += '  Orientation:\n'
415            output += f'    roll: {rad_to_deg_str(roll)}\n'
416            output += f'    pitch: {rad_to_deg_str(pitch)}\n'
417            output += f'    yaw: {rad_to_deg_str(yaw)}\n'
418
419            output += f'  Install direction:\n'
420            output += f'    x: {product.install_direction.x:.1f}\n'
421            output += f'    y: {product.install_direction.y:.1f}\n'
422            output += f'    z: {product.install_direction.z:.1f}\n'
423
424        return output
425
426    def _parse_combined_task(self, combined_task: CombinedTask):
427        '''
428        Parses a CombinedTask object and returns a string representation.
429
430        Args:
431            combined_task (CombinedTask): CombinedTask object to parse
432
433        Returns:
434            str: String representation of the CombinedTask object
435        '''
436
437        output = 'Type: Combined\n'
438        output += '==========================\n'
439        output += f'Station: {self._stations[combined_task.station].title()}\n'
440        output += 'Products:\n'
441        output += '==========================\n'
442
443        product: AssemblyPartMsg
444        for product in combined_task.parts:
445            part_color = CompetitionInterface._part_colors[product.part.color].capitalize()
446            part_color_emoji = CompetitionInterface._part_colors_emoji[product.part.color]
447            part_type = CompetitionInterface._part_types[product.part.type].capitalize()
448
449            output += f'Part: {part_color_emoji} {part_color} {part_type}\n'
450
451            output += '  Position:\n'
452            output += f'    x: {product.assembled_pose.pose.position.x:.3f} (m)\n'
453            output += f'    y: {product.assembled_pose.pose.position.y:.3f} (m)\n'
454            output += f'    z: {product.assembled_pose.pose.position.z:.3f} (m)\n'
455
456            roll, pitch, yaw = rpy_from_quaternion(product.assembled_pose.pose.orientation)
457            output += '  Orientation:\n'
458            output += f'    roll: {rad_to_deg_str(roll)}\n'
459            output += f'    pitch: {rad_to_deg_str(pitch)}\n'
460            output += f'    yaw: {rad_to_deg_str(yaw)}\n'
461
462            output += f'  Install direction:\n'
463            output += f'    x: {product.install_direction.x:.1f}\n'
464            output += f'    y: {product.install_direction.y:.1f}\n'
465            output += f'    z: {product.install_direction.z:.1f}\n'
466
467        return output
468
469    def _parse_order(self, order: Order):
470        '''Parse an order message and return a string representation.
471        Args:
472            order (Order) -- Order message
473        Returns:
474            String representation of the order message
475        '''
476        output = '\n\n==========================\n'
477        output += f'Received Order: {order.order_id}\n'
478        output += f'Priority: {order.order_priority}\n'
479
480        if order.order_type == OrderMsg.KITTING:
481            output += self._parse_kitting_task(order.order_task)
482        elif order.order_type == OrderMsg.ASSEMBLY:
483            output += self._parse_assembly_task(order.order_task)
484        elif order.order_type == OrderMsg.COMBINED:
485            output += self._parse_combined_task(order.order_task)
486        else:
487            output += 'Type: Unknown\n'
488        return output
489
490    def lock_agv_tray(self, num):
491        '''
492        Lock the tray of an AGV and parts on the tray. This will prevent tray and parts from moving during transport.
493        Args:
494            num (int):  AGV number
495        Raises:
496            KeyboardInterrupt: Exception raised when the user presses Ctrl+C
497        '''
498
499        # Create a client to send a request to the `/ariac/agv{num}_lock_tray` service
500        tray_locker = self.create_client(
501            Trigger,
502            f'/ariac/agv{num}_lock_tray'
503        )
504
505        # Build the request
506        request = Trigger.Request()
507        # Send the request
508        future = tray_locker.call_async(request)
509
510        # Wait for the response
511        try:
512            rclpy.spin_until_future_complete(self, future)
513        except KeyboardInterrupt as kb_error:
514            raise KeyboardInterrupt from kb_error
515
516        # Check the response
517        if future.result().success:
518            self.get_logger().info(f'Locked AGV{num}\'s tray')
519        else:
520            self.get_logger().warn('Unable to lock tray')
521
522    def move_agv_to_station(self, num, station):
523        '''
524        Move an AGV to an assembly station.
525        Args:
526            num (int): AGV number
527            station (int): Assembly station number
528        Raises:
529            KeyboardInterrupt: Exception raised when the user presses Ctrl+C
530        '''
531
532        # Create a client to send a request to the `/ariac/move_agv` service.
533        mover = self.create_client(
534            MoveAGV,
535            f'/ariac/move_agv{num}')
536
537        # Create a request object.
538        request = MoveAGV.Request()
539
540        # Set the request location.
541        if station in [AssemblyTaskMsg.AS1, AssemblyTaskMsg.AS3]:
542            request.location = MoveAGV.Request.ASSEMBLY_FRONT
543        else:
544            request.location = MoveAGV.Request.ASSEMBLY_BACK
545
546        # Send the request.
547        future = mover.call_async(request)
548
549        # Wait for the server to respond.
550        try:
551            rclpy.spin_until_future_complete(self, future)
552        except KeyboardInterrupt as kb_error:
553            raise KeyboardInterrupt from kb_error
554
555        # Check the result of the service call.
556        if future.result().success:
557            self.get_logger().info(f'Moved AGV{num} to {self._stations[station]}')
558        else:
559            self.get_logger().warn(future.result().message)
560
561    def set_floor_robot_gripper_state(self, state):
562        '''Set the gripper state of the floor robot.
563
564        Arguments:
565            state -- True to enable, False to disable
566
567        Raises:
568            KeyboardInterrupt: Exception raised when the user presses Ctrl+C
569        '''
570        if self._floor_robot_gripper_state.enabled == state:
571            self.get_logger().warn(f'Gripper is already {self._gripper_states[state]}')
572            return
573
574        request = VacuumGripperControl.Request()
575        request.enable = state
576
577        future = self._floor_gripper_enable.call_async(request)
578
579        try:
580            rclpy.spin_until_future_complete(self, future)
581        except KeyboardInterrupt as kb_error:
582            raise KeyboardInterrupt from kb_error
583
584        if future.result().success:
585            self.get_logger().info(f'Changed gripper state to {self._gripper_states[state]}')
586        else:
587            self.get_logger().warn('Unable to change gripper state')
588
589    def wait(self, duration):
590        '''Wait for a specified duration.
591
592        Arguments:
593            duration -- Duration to wait in seconds
594
595        Raises:
596            KeyboardInterrupt: Exception raised when the user presses Ctrl+C
597        '''
598        start = self.get_clock().now()
599
600        while self.get_clock().now() <= start + Duration(seconds=duration):
601            try:
602                rclpy.spin_once(self)
603            except KeyboardInterrupt as kb_error:
604                raise KeyboardInterrupt from kb_error
605
606    def move_robot_home(self, robot_name):
607        '''Move one of the robots to its home position.
608
609        Arguments:
610            robot_name -- Name of the robot to move home
611        '''
612        request = Trigger.Request()
613
614        if robot_name == 'floor_robot':
615            if not self._move_floor_robot_home.wait_for_service(timeout_sec=1.0):
616                self.get_logger().error('Robot commander node not running')
617                return
618
619            future = self._move_floor_robot_home.call_async(request)
620
621        elif robot_name == 'ceiling_robot':
622            if not self._move_ceiling_robot_home.wait_for_service(timeout_sec=1.0):
623                self.get_logger().error('Robot commander node not running')
624                return
625            future = self._move_ceiling_robot_home.call_async(request)
626        else:
627            self.get_logger().error(f'Robot name: ({robot_name}) is not valid')
628            return
629
630        # Wait until the service call is completed
631        rclpy.spin_until_future_complete(self, future)
632
633        if future.result().success:
634            self.get_logger().info(f'Moved {robot_name} to home position')
635        else:
636            self.get_logger().warn(future.result().message)

Code Explanation

The competition interface from Tutorial 6 was augmented with the components described below.

  • Instance Variables

    • _move_floor_robot_home: Service client for moving the floor robot to the home position.

    • _move_ceiling_robot_home: Service client for moving the ceiling robot to the home position.

  • Instance Methods

    • move_robot_home(self, robot_name): public method is used in the main function to move each robot to its home position. The method takes the name of the robot to move as an argument. The method then calls the appropriate service client to move the robot to its home position.

Robot Commander

The RobotCommander class used in this tutorial is shown in Listing 54.

Listing 54 robot_commander.cpp
  1#include <rclcpp/rclcpp.hpp>
  2
  3#include <std_srvs/srv/trigger.hpp>
  4
  5#include <moveit/move_group_interface/move_group_interface.h>
  6#include <moveit/planning_scene_interface/planning_scene_interface.h>
  7
  8class RobotCommander : public rclcpp::Node
  9{
 10public:
 11RobotCommander();
 12~RobotCommander();
 13
 14private:
 15// MoveIt Interfaces
 16moveit::planning_interface::MoveGroupInterface floor_robot_;
 17moveit::planning_interface::MoveGroupInterface ceiling_robot_;
 18
 19// ROS Services
 20rclcpp::Service<std_srvs::srv::Trigger>::SharedPtr floor_robot_move_home_srv_;
 21rclcpp::Service<std_srvs::srv::Trigger>::SharedPtr ceiling_robot_move_home_srv_;
 22
 23// Service Callbacks
 24void FloorRobotMoveHome(
 25    std_srvs::srv::Trigger::Request::SharedPtr req,
 26    std_srvs::srv::Trigger::Response::SharedPtr res);
 27
 28void CeilingRobotMoveHome(
 29    std_srvs::srv::Trigger::Request::SharedPtr req,
 30    std_srvs::srv::Trigger::Response::SharedPtr res);
 31};
 32
 33RobotCommander::RobotCommander()
 34: Node("robot_commander"),
 35floor_robot_(std::shared_ptr<rclcpp::Node>(std::move(this)), "floor_robot"),
 36ceiling_robot_(std::shared_ptr<rclcpp::Node>(std::move(this)), "ceiling_robot")
 37{
 38// Use upper joint velocity and acceleration limits
 39floor_robot_.setMaxAccelerationScalingFactor(1.0);
 40floor_robot_.setMaxVelocityScalingFactor(1.0);
 41
 42ceiling_robot_.setMaxAccelerationScalingFactor(1.0);
 43ceiling_robot_.setMaxVelocityScalingFactor(1.0);
 44
 45// Register services
 46floor_robot_move_home_srv_ = create_service<std_srvs::srv::Trigger>(
 47    "/competitor/move_floor_robot_home",
 48    std::bind(
 49    &RobotCommander::FloorRobotMoveHome, this,
 50    std::placeholders::_1, std::placeholders::_2));
 51
 52ceiling_robot_move_home_srv_ = create_service<std_srvs::srv::Trigger>(
 53    "/competitor/move_ceiling_robot_home",
 54    std::bind(
 55    &RobotCommander::CeilingRobotMoveHome, this,
 56    std::placeholders::_1, std::placeholders::_2));
 57}
 58
 59RobotCommander::~RobotCommander()
 60{
 61    floor_robot_.~MoveGroupInterface();
 62    ceiling_robot_.~MoveGroupInterface();
 63}
 64
 65void RobotCommander::FloorRobotMoveHome(
 66    std_srvs::srv::Trigger::Request::SharedPtr req,
 67    std_srvs::srv::Trigger::Response::SharedPtr res)
 68{
 69    (void)req; // remove unused parameter warning
 70    floor_robot_.setNamedTarget("home");
 71
 72    moveit::planning_interface::MoveGroupInterface::Plan plan;
 73    bool success = static_cast<bool>(floor_robot_.plan(plan));
 74
 75    if (success) {
 76        if (static_cast<bool>(floor_robot_.execute(plan))) {
 77            res->success = true;
 78        } else {
 79            res->success = false;
 80            res->message = "Trajectory execution failed";
 81        }
 82    } else {
 83        res->message = "Unable to generate trajectory";
 84        res->success = false;
 85    }
 86}
 87
 88void RobotCommander::CeilingRobotMoveHome(
 89    std_srvs::srv::Trigger::Request::SharedPtr req,
 90    std_srvs::srv::Trigger::Response::SharedPtr res)
 91{
 92    (void)req; // remove unused parameter warning
 93    ceiling_robot_.setNamedTarget("home");
 94
 95    moveit::planning_interface::MoveGroupInterface::Plan plan;
 96    bool success = static_cast<bool>(ceiling_robot_.plan(plan));
 97
 98    if (success) {
 99        if (static_cast<bool>(ceiling_robot_.execute(plan))) {
100            res->success = true;
101        } else {
102            res->success = false;
103            res->message = "Trajectory execution failed";
104        }
105    } else {
106        res->message = "Unable to generate trajectory";
107        res->success = false;
108    }
109}
110
111int main(int argc, char *argv[])
112{
113    rclcpp::init(argc, argv);
114    auto robot_commander = std::make_shared<RobotCommander>();
115    rclcpp::spin(robot_commander);
116    rclcpp::shutdown();
117}

The robot commander node is a C++ node which initializes the MoveGroupInterface for the floor and ceiling robots.

  • The move group floor_robot is used for the floor robot. This group allows control of the arm and its displacement on the linear rail.

  • The move group ceiling_robot is used for the ceiling robot. This group allows control of the arm and the torso of the robot on the rails.

The node also hosts two services:

  • /competitor/move_floor_robot_home calls the method FloorRobotMoveHome() which moves the floor robot to its home position.

  • /competitor/move_ceiling_robot_home calls the method CeilingRobotMoveHome() which moves the ceiling robot to its home position.

Launch File

The RobotCommander node and the MoveIt node is started using the launch file shown in Listing 55. Nodes that use the MoveGroupInterface need to be launched with specific ROS parameters. A python function in the ariac_moveit_config module generate_parameters has been added to easily create those parameters. The launch file ariac_robots_moveit.launch.py is also called to start the move group node.

Listing 55 robot_commander.launch.py
 1from launch import LaunchDescription
 2from launch_ros.actions import Node
 3from launch.launch_description_sources import PythonLaunchDescriptionSource
 4from launch.actions import IncludeLaunchDescription
 5from launch_ros.substitutions import FindPackageShare
 6
 7from ariac_moveit_config.parameters import generate_parameters
 8
 9
10def generate_launch_description():
11
12    # Robot Commander Node
13    robot_commander = Node(
14        package="ariac_tutorials",
15        executable="robot_commander",
16        output="screen",
17        parameters=generate_parameters()
18    )
19
20    # MoveIt node
21    moveit = IncludeLaunchDescription(
22        PythonLaunchDescriptionSource(
23            [FindPackageShare("ariac_moveit_config"), "/launch", "/ariac_robots_moveit.launch.py"]
24        )
25    )
26
27    return LaunchDescription([robot_commander, moveit])

Executable

Listing 56 tutorial_7.py
#!/usr/bin/env python3
'''
To test this script, run the following commands in separate terminals:

- ros2 launch ariac_tutorials robot_commander.launch.py
- ros2 run ariac_tutorials move_robots.py
- ros2 launch ariac_gazebo ariac.launch.py competitor_pkg:=ariac_tutorials trial_name:=tutorials
'''

import rclpy
from ariac_tutorials.competition_interface import CompetitionInterface


def main(args=None):
    rclpy.init(args=args)
    interface = CompetitionInterface()
    interface.start_competition()

    interface.move_robot_home("floor_robot")
    interface.move_robot_home("ceiling_robot")

    interface.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Code Explanation

This executable does the following:

  • Initialize the ROS client library.

  • Create an instance of the class CompetitionInterface as a ROS node.

  • Start the competition.

  • The call to move_robot_home("floor_robot") sends a service request to /competitor/move_floor_robot_home.

  • The call to move_robot_home("ceiling_robot") sends a service request to /competitor/move_ceiling_robot_home

Build Instructions

Updates and additions that are specific to Tutorial 7 are highlighted in the CMakeLists.txt below.

Listing 57 CMakeLists.txt
 1cmake_minimum_required(VERSION 3.8)
 2project(ariac_tutorials)
 3
 4if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
 5add_compile_options(-Wall -Wextra -Wpedantic)
 6endif()
 7
 8find_package(ament_cmake REQUIRED)
 9find_package(ament_cmake_python REQUIRED)
10find_package(rclcpp REQUIRED)
11find_package(rclpy REQUIRED)
12find_package(ariac_msgs REQUIRED)
13find_package(orocos_kdl REQUIRED)
14find_package(moveit_ros_planning_interface REQUIRED)
15
16# Install the config directory to the package share directory
17install(DIRECTORY
18config
19DESTINATION share/${PROJECT_NAME}
20)
21
22# Install Python modules
23ament_python_install_package(${PROJECT_NAME} SCRIPTS_DESTINATION lib/${PROJECT_NAME})
24
25# Install Python executables
26install(PROGRAMS
27scripts/tutorial_1.py
28scripts/tutorial_2.py
29scripts/tutorial_3.py
30scripts/tutorial_4.py
31scripts/tutorial_5.py
32scripts/tutorial_6.py
33scripts/tutorial_7.py
34DESTINATION lib/${PROJECT_NAME}
35)
36
37# Install the config directory to the package share directory
38install(DIRECTORY
39config
40launch
41DESTINATION share/${PROJECT_NAME}
42)
43
44# Install C++ executables
45add_executable(robot_commander
46src/robot_commander.cpp)
47
48ament_target_dependencies(robot_commander
49rclcpp
50moveit_ros_planning_interface
51ariac_msgs)
52
53install(TARGETS
54robot_commander
55DESTINATION lib/${PROJECT_NAME})
56
57
58ament_package()

Run the Executable

  • In terminal 1, run the following commands:

    cd ~/ariac_ws
    . install/setup.bash
    ros2 launch ariac_tutorials robot_commander.launch.py
    

    The launch command starts the robot commander node and move it.

  • In terminal 2, run the following commands:

    cd ~/ariac_ws
    . install/setup.bash
    ros2 run ariac_tutorials tutorial_7.py
    

    The last command starts the competition interface node and sends the service requests to move the robots to their home positions.

  • In terminal 3, run the following commands:

    cd ~/ariac_ws
    colcon build
    . install/setup.bash
    ros2 launch ariac_gazebo ariac.launch.py trial_name:=tutorial competitor_pkg:=ariac_tutorials
    

    The last command starts the environment.

Outputs

The output of the above commands show both robots moving to their home positions in Gazebo.

../_images/tutorial7_output.jpg