Tutorial 7: Move Robots with MoveIt
Tutorial 7
Prerequisites: Introduction to Tutorials and Tutorial 6
Source Code: https://github.com/jaybrecht/ariac_tutorials/tree/tutorial_7
Switch Branch:
cd ~/ariac_ws/src/ariac_tutorials git switch 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.
Updated/Created Files
Competition Interface
The competition interface used in this tutorial is shown in Listing 53.
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.
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.
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
#!/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.
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.
