You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

457 lines
19 KiB

  1. # Example 12
  2. For this example, we will send follow joint trajectory commands for the head camera to search and locate an ArUco tag. In this instance, a Stretch robot will try to locate the docking station's ArUco tag.
  3. ## Modifying Stretch Marker Dictionary YAML File
  4. When defining the ArUco markers on Stretch, hello robot utilizes a YAML file, [stretch_marker_dict.yaml](https://github.com/hello-robot/stretch_ros2/blob/humble/stretch_core/config/stretch_marker_dict.yaml), that holds the information about the markers. A further breakdown of the YAML file can be found in our [Aruco Marker Detection](aruco_marker_detection.md) tutorial.
  5. Below is what needs to be included in the [stretch_marker_dict.yaml](https://github.com/hello-robot/stretch_ros2/blob/humble/stretch_core/config/stretch_marker_dict.yaml) file so the [detect_aruco_markers](https://github.com/hello-robot/stretch_ros2/blob/humble/stretch_core/stretch_core/detect_aruco_markers.py) node can find the docking station's ArUco tag.
  6. ```yaml
  7. '245':
  8. 'length_mm': 88.0
  9. 'use_rgb_only': False
  10. 'name': 'docking_station'
  11. 'link': None
  12. ```
  13. ## Getting Started
  14. Begin by running the stretch driver launch file.
  15. ```{.bash .shell-prompt}
  16. ros2 launch stretch_core stretch_driver.launch.py
  17. ```
  18. To activate the RealSense camera and publish topics to be visualized, run the following launch file in a new terminal.
  19. ```{.bash .shell-prompt}
  20. ros2 launch stretch_core d435i_high_resolution.launch.py
  21. ```
  22. Next, run the stretch ArUco launch file which will bring up the [detect_aruco_markers](https://github.com/hello-robot/stretch_ros2/blob/humble/stretch_core/stretch_core/detect_aruco_markers.py) node. In a new terminal, execute:
  23. ```{.bash .shell-prompt}
  24. ros2 launch stretch_core stretch_aruco.launch.py
  25. ```
  26. Within this tutorial package, there is an [RViz config file](https://github.com/hello-robot/stretch_tutorials/blob/humble/rviz/aruco_detector_example.rviz) with the topics for the transform frames in the Display tree. You can visualize these topics and the robot model by running the command below in a new terminal.
  27. ```{.bash .shell-prompt}
  28. ros2 run rviz2 rviz2 -d /home/hello-robot/ament_ws/src/stretch_tutorials/rviz/aruco_detector_example.rviz
  29. ```
  30. Then run the [aruco_tag_locator.py](https://github.com/hello-robot/stretch_tutorials/blob/humble/stretch_ros_tutorials/aruco_tag_locator.py) node. In a new terminal, execute:
  31. ```{.bash .shell-prompt}
  32. cd ament_ws/src/stretch_tutorials/stretch_ros_tutorials/
  33. python3 aruco_tag_locator.py
  34. ```
  35. <p align="center">
  36. <img src="https://raw.githubusercontent.com/hello-robot/stretch_tutorials/noetic/images/aruco_locator.gif"/>
  37. </p>
  38. ### The Code
  39. ```python
  40. #!/usr/bin/env python3
  41. # Import modules
  42. import rclpy
  43. import time
  44. import tf2_ros
  45. from tf2_ros import TransformException
  46. from rclpy.time import Time
  47. from math import pi
  48. # Import hello_misc script for handling trajectory goals with an action client
  49. import hello_helpers.hello_misc as hm
  50. # We're going to subscribe to a JointState message type, so we need to import
  51. # the definition for it
  52. from sensor_msgs.msg import JointState
  53. # Import the FollowJointTrajectory from the control_msgs.action package to
  54. # control the Stretch robot
  55. from control_msgs.action import FollowJointTrajectory
  56. # Import JointTrajectoryPoint from the trajectory_msgs package to define
  57. # robot trajectories
  58. from trajectory_msgs.msg import JointTrajectoryPoint
  59. # Import TransformStamped from the geometry_msgs package for the publisher
  60. from geometry_msgs.msg import TransformStamped
  61. class LocateArUcoTag(hm.HelloNode):
  62. """
  63. A class that actuates the RealSense camera to find the docking station's
  64. ArUco tag and returns a Transform between the `base_link` and the requested tag.
  65. """
  66. def __init__(self):
  67. """
  68. A function that initializes the subscriber and other needed variables.
  69. :param self: The self reference.
  70. """
  71. # Initialize the inhereted hm.Hellonode class
  72. hm.HelloNode.__init__(self)
  73. hm.HelloNode.main(self, 'aruco_tag_locator', 'aruco_tag_locator', wait_for_first_pointcloud=False)
  74. # Initialize subscriber
  75. self.joint_states_sub = self.create_subscription(JointState, '/stretch/joint_states', self.joint_states_callback, 1)
  76. # Initialize publisher
  77. self.transform_pub = self.create_publisher(TransformStamped, 'ArUco_transform', 10)
  78. # Initialize the variable that will store the joint state positions
  79. self.joint_state = None
  80. # Provide the min and max joint positions for the head pan. These values
  81. # are needed for sweeping the head to search for the ArUco tag
  82. self.min_pan_position = -3.8
  83. self.max_pan_position = 1.50
  84. # Define the number of steps for the sweep, then create the step size for
  85. # the head pan joint
  86. self.pan_num_steps = 10
  87. self.pan_step_size = abs(self.min_pan_position - self.max_pan_position)/self.pan_num_steps
  88. # Define the min tilt position, number of steps, and step size
  89. self.min_tilt_position = -0.75
  90. self.tilt_num_steps = 3
  91. self.tilt_step_size = pi/16
  92. # Define the head actuation rotational velocity
  93. self.rot_vel = 0.5 # radians per sec
  94. def joint_states_callback(self, msg):
  95. """
  96. A callback function that stores Stretch's joint states.
  97. :param self: The self reference.
  98. :param msg: The JointState message type.
  99. """
  100. self.joint_state = msg
  101. def send_command(self, command):
  102. '''
  103. Handles single joint control commands by constructing a FollowJointTrajectoryGoal
  104. message and sending it to the trajectory_client created in hello_misc.
  105. :param self: The self reference.
  106. :param command: A dictionary message type.
  107. '''
  108. if (self.joint_state is not None) and (command is not None):
  109. # Extract the string value from the `joint` key
  110. joint_name = command['joint']
  111. # Set trajectory_goal as a FollowJointTrajectory.Goal and define
  112. # the joint name
  113. trajectory_goal = FollowJointTrajectory.Goal()
  114. trajectory_goal.trajectory.joint_names = [joint_name]
  115. # Create a JointTrajectoryPoint message type
  116. point = JointTrajectoryPoint()
  117. # Check to see if `delta` is a key in the command dictionary
  118. if 'delta' in command:
  119. # Get the current position of the joint and add the delta as a
  120. # new position value
  121. joint_index = self.joint_state.name.index(joint_name)
  122. joint_value = self.joint_state.position[joint_index]
  123. delta = command['delta']
  124. new_value = joint_value + delta
  125. point.positions = [new_value]
  126. # Check to see if `position` is a key in the command dictionary
  127. elif 'position' in command:
  128. # extract the head position value from the `position` key
  129. point.positions = [command['position']]
  130. # Set the rotational velocity
  131. point.velocities = [self.rot_vel]
  132. # Assign goal position with updated point variable
  133. trajectory_goal.trajectory.points = [point]
  134. # Specify the coordinate frame that we want (base_link) and set the time to be now.
  135. trajectory_goal.trajectory.header.stamp = self.get_clock().now().to_msg()
  136. trajectory_goal.trajectory.header.frame_id = 'base_link'
  137. # Make the action call and send the goal. The last line of code waits
  138. # for the result
  139. self.trajectory_client.send_goal(trajectory_goal)
  140. def find_tag(self, tag_name='docking_station'):
  141. """
  142. A function that actuates the camera to search for a defined ArUco tag
  143. marker. Then the function returns the pose.
  144. :param self: The self reference.
  145. :param tag_name: A string value of the ArUco marker name.
  146. :returns transform: The docking station's TransformStamped message.
  147. """
  148. # Create dictionaries to get the head in its initial position
  149. pan_command = {'joint': 'joint_head_pan', 'position': self.min_pan_position}
  150. self.send_command(pan_command)
  151. tilt_command = {'joint': 'joint_head_tilt', 'position': self.min_tilt_position}
  152. self.send_command(tilt_command)
  153. # Nested for loop to sweep the joint_head_pan and joint_head_tilt in increments
  154. for i in range(self.tilt_num_steps):
  155. for j in range(self.pan_num_steps):
  156. # Update the joint_head_pan position by the pan_step_size
  157. pan_command = {'joint': 'joint_head_pan', 'delta': self.pan_step_size}
  158. self.send_command(pan_command)
  159. # Give time for system to do a Transform lookup before next step
  160. time.sleep(0.2)
  161. # Use a try-except block
  162. try:
  163. now = Time()
  164. # Look up transform between the base_link and requested ArUco tag
  165. transform = self.tf_buffer.lookup_transform('base_link',
  166. tag_name,
  167. now)
  168. self.get_logger().info("Found Requested Tag: \n%s", transform)
  169. # Publish the transform
  170. self.transform_pub.publish(transform)
  171. # Return the transform
  172. return transform
  173. except TransformException as ex:
  174. continue
  175. # Begin sweep with new tilt angle
  176. pan_command = {'joint': 'joint_head_pan', 'position': self.min_pan_position}
  177. self.send_command(pan_command)
  178. tilt_command = {'joint': 'joint_head_tilt', 'delta': self.tilt_step_size}
  179. self.send_command(tilt_command)
  180. time.sleep(0.25)
  181. # Notify that the requested tag was not found
  182. self.get_logger().info("The requested tag '%s' was not found", tag_name)
  183. def main(self):
  184. """
  185. Function that initiates the issue_command function.
  186. :param self: The self reference.
  187. """
  188. # Create a StaticTranformBoradcaster Node. Also, start a Tf buffer that
  189. # will store the tf information for a few seconds.Then set up a tf listener, which
  190. # will subscribe to all of the relevant tf topics, and keep track of the information
  191. self.static_broadcaster = tf2_ros.StaticTransformBroadcaster(self)
  192. self.tf_buffer = tf2_ros.Buffer()
  193. self.listener = tf2_ros.TransformListener(self.tf_buffer, self)
  194. # Give the listener some time to accumulate transforms
  195. time.sleep(1.0)
  196. # Notify Stretch is searching for the ArUco tag with `get_logger().info()`
  197. self.get_logger().info('Searching for docking ArUco tag')
  198. # Search for the ArUco marker for the docking station
  199. pose = self.find_tag("docking_station")
  200. def main():
  201. try:
  202. # Instantiate the `LocateArUcoTag()` object
  203. node = LocateArUcoTag()
  204. # Run the `main()` method
  205. node.main()
  206. node.new_thread.join()
  207. except:
  208. node.get_logger().info('Interrupt received, so shutting down')
  209. node.destroy_node()
  210. rclpy.shutdown()
  211. if __name__ == '__main__':
  212. main()
  213. ```
  214. ### The Code Explained
  215. Now let's break the code down.
  216. ```python
  217. #!/usr/bin/env python3
  218. ```
  219. Every Python ROS [Node](http://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Nodes/Understanding-ROS2-Nodes.html) will have this declaration at the top. The first line makes sure your script is executed as a Python3 script.
  220. ```python
  221. import rclpy
  222. import time
  223. import tf2_ros
  224. from tf2_ros import TransformException
  225. from rclpy.time import Time
  226. from math import pi
  227. import hello_helpers.hello_misc as hm
  228. from sensor_msgs.msg import JointState
  229. from control_msgs.action import FollowJointTrajectory
  230. from trajectory_msgs.msg import JointTrajectoryPoint
  231. from geometry_msgs.msg import TransformStamped
  232. ```
  233. You need to import `rclpy` if you are writing a ROS [Node](http://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Nodes/Understanding-ROS2-Nodes.html). Import other python modules needed for this node. Import the `FollowJointTrajectory` from the [control_msgs.action](http://wiki.ros.org/control_msgs) package to control the Stretch robot. Import `JointTrajectoryPoint` from the [trajectory_msgs](https://github.com/ros2/common_interfaces/tree/humble/trajectory_msgs) package to define robot trajectories. The [hello_helpers](https://github.com/hello-robot/stretch_ros2/tree/humble/hello_helpers) package consists of a module that provides various Python scripts used across [stretch_ros](https://github.com/hello-robot/stretch_ros2). In this instance, we are importing the `hello_misc` script.
  234. ```python
  235. def __init__(self):
  236. # Initialize the inhereted hm.Hellonode class
  237. hm.HelloNode.__init__(self)
  238. hm.HelloNode.main(self, 'aruco_tag_locator', 'aruco_tag_locator', wait_for_first_pointcloud=False)
  239. # Initialize subscriber
  240. self.joint_states_sub = self.create_subscription(JointState, '/stretch/joint_states', self.joint_states_callback, 1)
  241. # Initialize publisher
  242. self.transform_pub = self.create_publisher(TransformStamped, 'ArUco_transform', 10)
  243. # Initialize the variable that will store the joint state positions
  244. self.joint_state = None
  245. ```
  246. The `LocateArUcoTag` class inherits the `HelloNode` class from `hm` and is instantiated.
  247. Set up a subscriber with `self.create_subscription(JointState, '/stretch/joint_states', self.joint_states_callback, 1)`. We're going to subscribe to the topic `stretch/joint_states`, looking for `JointState` messages. When a message comes in, ROS is going to pass it to the function `joint_states_callback()` automatically.
  248. `self.create_publisher(TransformStamped, 'ArUco_transform', 10)` declares that your node is publishing to the `ArUco_transform` topic using the message type `TransformStamped`. The `10` argument limits the amount of queued messages if any subscriber is not receiving them fast enough.
  249. ```python
  250. self.min_pan_position = -4.10
  251. self.max_pan_position = 1.50
  252. self.pan_num_steps = 10
  253. self.pan_step_size = abs(self.min_pan_position - self.max_pan_position)/self.pan_num_steps
  254. ```
  255. Provide the minimum and maximum joint positions for the head pan. These values are needed for sweeping the head to search for the ArUco tag. We also define the number of steps for the sweep, then create the step size for the head pan joint.
  256. ```python
  257. self.min_tilt_position = -0.75
  258. self.tilt_num_steps = 3
  259. self.tilt_step_size = pi/16
  260. ```
  261. Set the minimum position of the tilt joint, the number of steps, and the size of each step.
  262. ```python
  263. self.rot_vel = 0.5 # radians per sec
  264. ```
  265. Define the head actuation rotational velocity.
  266. ```python
  267. def joint_states_callback(self, msg):
  268. self.joint_state = msg
  269. ```
  270. The `joint_states_callback()` function stores Stretch's joint states.
  271. ```python
  272. def send_command(self, command):
  273. if (self.joint_state is not None) and (command is not None):
  274. joint_name = command['joint']
  275. trajectory_goal = FollowJointTrajectory.Goal()
  276. trajectory_goal.trajectory.joint_names = [joint_name]
  277. point = JointTrajectoryPoint()
  278. ```
  279. Assign `trajectory_goal` as a `FollowJointTrajectory.Goal` message type. Then extract the string value from the `joint` key. Also, assign `point` as a `JointTrajectoryPoint` message type.
  280. ```python
  281. if 'delta' in command:
  282. joint_index = self.joint_state.name.index(joint_name)
  283. joint_value = self.joint_state.position[joint_index]
  284. delta = command['delta']
  285. new_value = joint_value + delta
  286. point.positions = [new_value]
  287. ```
  288. Check to see if `delta` is a key in the command dictionary. Then get the current position of the joint and add the delta as a new position value.
  289. ```python
  290. elif 'position' in command:
  291. point.positions = [command['position']]
  292. ```
  293. Check to see if `position` is a key in the command dictionary. Then extract the position value.
  294. ```python
  295. point.velocities = [self.rot_vel]
  296. trajectory_goal.trajectory.points = [point]
  297. trajectory_goal.trajectory.header.stamp = self.get_clock().now().to_msg()
  298. trajectory_goal.trajectory.header.frame_id = 'base_link'
  299. self.trajectory_client.send_goal(trajectory_goal)
  300. ```
  301. Then `trajectory_goal.trajectory.points` is defined by the positions set in `point`. Specify the coordinate frame that we want (*base_link*) and set the time to be now. Make the action call and send the goal.
  302. ```python
  303. def find_tag(self, tag_name='docking_station'):
  304. pan_command = {'joint': 'joint_head_pan', 'position': self.min_pan_position}
  305. self.send_command(pan_command)
  306. tilt_command = {'joint': 'joint_head_tilt', 'position': self.min_tilt_position}
  307. self.send_command(tilt_command)
  308. ```
  309. Create a dictionary to get the head in its initial position for its search and send the commands with the `send_command()` function.
  310. ```python
  311. for i in range(self.tilt_num_steps):
  312. for j in range(self.pan_num_steps):
  313. pan_command = {'joint': 'joint_head_pan', 'delta': self.pan_step_size}
  314. self.send_command(pan_command)
  315. time.sleep(0.5)
  316. ```
  317. Utilize a nested for loop to sweep the pan and tilt in increments. Then update the `joint_head_pan` position by the `pan_step_size`. Use `time.sleep()` function to give time to the system to do a Transform lookup before the next step.
  318. ```python
  319. try:
  320. now = Time()
  321. transform = self.tf_buffer.lookup_transform('base_link',
  322. tag_name,
  323. now)
  324. self.get_logger().info("Found Requested Tag: \n%s", transform)
  325. self.transform_pub.publish(transform)
  326. return transform
  327. except TransformException as ex:
  328. continue
  329. ```
  330. Use a try-except block to look up the transform between the *base_link* and the requested ArUco tag. Then publish and return the `TransformStamped` message.
  331. ```python
  332. pan_command = {'joint': 'joint_head_pan', 'position': self.min_pan_position}
  333. self.send_command(pan_command)
  334. tilt_command = {'joint': 'joint_head_tilt', 'delta': self.tilt_step_size}
  335. self.send_command(tilt_command)
  336. time.sleep(.25)
  337. ```
  338. Begin sweep with new tilt angle.
  339. ```python
  340. def main(self):
  341. self.static_broadcaster = tf2_ros.StaticTransformBroadcaster(self)
  342. self.tf_buffer = tf2_ros.Buffer()
  343. self.listener = tf2_ros.TransformListener(self.tf_buffer, self)
  344. time.sleep(1.0)
  345. ```
  346. Create a StaticTranformBoradcaster Node. Also, start a tf buffer that will store the tf information for a few seconds. Then set up a tf listener, which will subscribe to all of the relevant tf topics, and keep track of the information. Include `time.sleep(1.0)` to give the listener some time to accumulate transforms.
  347. ```python
  348. self.get_logger().info('Searching for docking ArUco tag')
  349. pose = self.find_tag("docking_station")
  350. ```
  351. Notice Stretch is searching for the ArUco tag with a `self.get_logger().info()` function. Then search for the ArUco marker for the docking station.
  352. ```python
  353. def main():
  354. try:
  355. node = LocateArUcoTag()
  356. node.main()
  357. node.new_thread.join()
  358. except:
  359. node.get_logger().info('Interrupt received, so shutting down')
  360. node.destroy_node()
  361. rclpy.shutdown()
  362. ```
  363. Instantiate the `LocateArUcoTag()` object and run the `main()` method.