This exercise is designed to give you an introduction to sending and receiving ROS2 messages. If you run into any challenges, please reach out to the teaching team for help.
        Let's navigate to our directory within our intro_robo_ws for all of our in-class exercises and create a ROS2 package for today's coding exercises (to learn more about ROS2 package creation visit this ROS2 tutorial page). To do so, run the following commands in a terminal:
      
$ cd ~/intro_robo_ws/src
$ ros2 pkg create --build-type ament_python --license Apache-2.0 first_ros2_nodes --dependencies rclpy geometry_msgs std_msgs
      ros2 pkg create command means:
        --build-type ament_python [necessary] -- Specifies that this is a Python package using the ament build system (ROS2's build system)--license Apache-2.0 [optional] -- Sets the license for the package to Apache 2.0, which is a popular open-source licensefirst_ros2_nodes [necessary] -- The name of the package you're creating--dependencies rclpy geometry_msgs std_msgs [optional] -- Specifies the packages this new package depends on, if you don't specify them when creating the package, you can always add them later to the package.xml):
            rclpy: The ROS2 Python client librarygeometry_msgs: Contains message types for geometric data (like PointStamped)std_msgs: Contains standard message types (like String, Int32, etc.)
        Then be sure to build your workspace and source the install/setup.bash file.
      
$ cd ~/intro_robo_ws
$ colcon build --symlink-install --packages-select first_ros2_nodes
$ source install/local_setup.bash
      package.xml or setup.py files, or create a new message type within a package, you MUST re-build the workspace and source the setup file in order for your changes to take effect. 
        --symlink-install tag on the build, you will NOT need to re-build every time you edit your python scripts, which turns out to be pretty handy. 
      package.xml and setup.py files with the description of the package, maintainer information, license, and any dependencies: 
      package.xml:
          
        setup.py:
          description - description of the ROS package in 1 sentencemaintainer - your namemaintainer_email - your email addressentry_points - we'll leave this empty for now, but we'll be coming back to this later
        We're going to write Python code to construct a ROS2 node that publishes the location of a point in space (with x, y, and z coordinates) twice per second. Create a new python file named point_location_sender.py and ensure that it's full path is ~/intro_robo_ws/src/first_ros2_nodes/first_ros2_nodes/point_location_sender.py. To get started, put the following lines in your newly created point_location_sender.py:
      
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PointStamped
class PointLocationSender(Node):
    def __init__(self):
        super().__init__('send_point_location')
        self.publisher_ = self.create_publisher(PointStamped, '/my_point', 10)
        self.timer = self.create_timer(0.5, self.timer_callback)  # 2 Hz
    def timer_callback(self):
        msg = PointStamped()
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.header.frame_id = "odom"
        msg.point.x = 1.0
        msg.point.y = 2.0
        msg.point.z = 0.0
        self.publisher_.publish(msg)
        self.get_logger().info(f'Publishing: [{msg.point.x}, {msg.point.y}, {msg.point.z}]')
def main(args=None):
    rclpy.init(args=args)
    node = PointLocationSender()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()
if __name__ == '__main__':
    main()
    Let's break down each component of the code to better understand what's going on. Let's first take a look at the imports:
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PointStamped
    You'll need import rclpy and from rclpy.node import Node for each Python node you'll create. Then, for each message type you need, you'll need to import it. Since we're sending 3D point locations, we need to import PointStamped from the geometry_msgs.msg package.
Next, let's look at the main function:
def main(args=None):
    rclpy.init(args=args) # Initializes the ROS2 Python client library
    node = PointLocationSender() # Creates an instance of our custom node class
    rclpy.spin(node) # Keeps the node running 
    node.destroy_node() # Cleans up the node
    rclpy.shutdown() # Shuts down the ROS2 Python client library
    
      The main function handles ROS2 initialization, node creation, and cleanup (see the comments in the code above for more details). Importantly, this code uses rclpy.spin(), which enables the node to run indefinitely until you Ctrl+C (kill) it. This is helpful for this code example, however, you may not always want to use it, for example, if you want your code to run from start to finish and then exit itself.
    
Next, let's look at the __init__ method:
def __init__(self):
    super().__init__('send_point_location') # Initializes ROS2 node with unique name
    self.publisher_ = self.create_publisher(PointStamped, '/my_point', 10) # sets up ROS2 publisher
    self.timer = self.create_timer(0.5, self.timer_callback)  # Calls timer_callback every 0.5 seconds (2 Hz)
    
      Importantly, this part of the code initializes the ROS2 node with a unique name (no ROS2 nodes can exist with the same name on the same network). It also sets up the publisher that specifies the message type (PointStamped), on a specified topic name (/my_point), with a specified queue size (10). The queue size determines how many messages to store if they can't be sent immediately. Finally, we set up a timer that will call the timer_callback method every 0.5 seconds (2 Hz).
    
Finally, our last portion of the code, the timer_callback method, creates and publishes our PointStamped message:
def timer_callback(self):
    msg = PointStamped() # creates a new ROS2 message of type PointStamped
    msg.header.stamp = self.get_clock().now().to_msg()
    msg.header.frame_id = "odom"
    msg.point.x = 1.0
    msg.point.y = 2.0
    msg.point.z = 0.0
    self.publisher_.publish(msg) # publishes the message
    self.get_logger().info(f'Publishing: [{msg.point.x}, {msg.point.y}, {msg.point.z}]') # log to the console
    
      This code creates a new PointStamped message, fills it with data, and publishes it to the /my_point topic at a regular interval (2 Hz). To find out what format each kind of ROS2 message takes, you can either look at the documentation online (I found this by just Googling "PointStamped ROS2 message") or using the ROS2 command line tools (e.g., ros2 topic info /my_point and ros2 interface show /geometry_msgs/msg/PointStamped).
    
        Now that you've created and understand point_location_sender.py, you'll need to create an entry_point for this script in setup.py. Open up setup.py in the first_ros2_nodes package directory and add a line for your script to console_scripts, so your entry_points look like:
            
   entry_points={
       'console_scripts': [
           'sender = first_ros2_nodes.point_location_sender:main',
       ],
   },
      
      
        Since we've modified setup.py, we need to re-build our package for the changes to take effect. Run:
      
$ cd ~/intro_robo_ws
$ colcon build --symlink-install --packages-select first_ros2_nodes
$ source install/setup.bash
      Now, you can run your code:
$ ros2 run first_ros2_nodes sender
      
        To check that your ROS2 node is really sending PointStamped messages, open another terminal and run:
      
$ ros2 topic echo /my_point
      
        You should see your PointStamped messages printed to the console at a rate of 2 Hz.
      
        In addition to publishing ROS2 messages, it's equally important to understand how ROS2 nodes can "listen to" or subscribe to ROS2 messages that are published by other nodes. This is accomplished with ROS2 callback functions. We'll now write a second Python script to construct a ROS2 node that subscribes to our PointStamped messages on the /my_point ROS2 topic and prints them out in the terminal.
      
        Create a new Python script named point_location_receiver.py in the directory ~/intro_robo_ws/src/first_ros2_nodes/first_ros2_nodes. In this file, put the following code:
      
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PointStamped
class PointLocationReceiver(Node):
    def __init__(self):
        super().__init__('receive_point_location')
        self.subscription = self.create_subscription(
            PointStamped,
            '/my_point',
            self.listener_callback,
            10)
    def listener_callback(self, msg):
        self.get_logger().info(f'Received: [{msg.point.x:.2f}, {msg.point.y:.2f}, {msg.point.z:.2f}]')
def main(args=None):
    rclpy.init(args=args)
    node = PointLocationReceiver()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()
if __name__ == '__main__':
    main()
    
      Much of this code is similar to our point_location_sender.py code, so I'll highlight the key differences and new components: 
      
'receive_point_location', which is unique from 'send_point_location' of the other node we created.PointStamped), the topic name ('/my_point'), the callback function (self.listener_callback) that will be called when a new message is received, and the queue size (10).listener_callback function is where we define what to do when a new message is received. In this case, we're simply logging the received point's coordinates to the console.rclpy.spin(node), until we shut it down, so we can wait around and log any PointStamped messages on the /my_point topic that we receive.
        Just as before with the sender, you'll need to create an entry_point for this script in setup.py. Open up setup.py in the first_ros2_nodes package directory and add a line for your script to console_scripts, so your entry_points look like:
            
   entry_points={
       'console_scripts': [
           'sender = first_ros2_nodes.point_location_sender:main',
           'receiver = first_ros2_nodes.point_location_receiver:main',
       ],
   },
      
      
        Since we've modified setup.py, we need to re-build our package for the changes to take effect. Run:
      
$ cd ~/intro_robo_ws
$ colcon build --symlink-install --packages-select first_ros2_nodes
$ source install/setup.bash
      Now, you can run your code:
$ ros2 run first_ros2_nodes receiver
      
        With your sender running in one terminal and your receiver running in the other terminal, you should now be able to see the PointStamped messages being published by the sender and received by the receiver printed out in the terminal.
      
This coding exercise was based on the exercise presented in A Computational Introduction to Robotics course's Day 02 page and the Creating a package and Writing a simple publisher and subscriber (Python) ROS2 Humble tutorials.