Skip to content

Latest commit

 

History

History
120 lines (80 loc) · 9.86 KB

ros_package.md

File metadata and controls

120 lines (80 loc) · 9.86 KB

Developing a ROS Package

[TOC]

Back to tutorial contents

ROS2 Packages

In the core tutorial, the focus was on the controller source code within your Python files. In Adding Perception we very briefly touched on launch files, but will did not go into much detail as to what is the rest of a ROS2 package - and this has some knock-on effects when you want to implement new functionality!

Inspecting the package

Inspecting the fenswood_drone_controller source folder we see a few more folders and files.

  1. fenswood_drone_controller - the folder which contains your source code, it is considered a python package and therefore should have an __init__.py file.
  2. launch - the folder which contains all of your launch files for various versions of your controller.
  3. resource - a Python ROS2 package specific folder which contains the executable your controller gets compiled into.
  4. Dockerfile - not a part of a ROS2 package, this is Starling specific, see a later tutorial for more details.
  5. package.xml - This is an xml file which ROS2 reads to find details of your package. This includes the packages and libraries that your project depends on. (It is not used here, but there is a tool called rosdep which reads this file and is often used to automatically find dependencies )
  6. setup.cfg - This is an autogenerated file which defines where to install this package.
  7. setup.py - The main configuration file for ROS2 Python packages. It defines the name of your ROS2 nodes within this package, as well as specifies which files get copied to the compiled version of the package.

"Where" is my ROS2 package and package compilation

In the previous tutorials, implementing and writing the Python probably felt like previous times you may have used interpreted languages. You make a change, then you run your script and you can see your changes. In fact, as part of Starling, we wanted it to feel like that to make it easier to pick up and use.

However, it is actually slightly more complicated when it comes to ROS2 nodes. There is a hidden step in which your ROS2 nodes are compiled, even if you are writing Python where you don't traditionally compile anything!

Note: In our Starling system, the compilation auomatically happens when you run docker-compose -f ... up --build.

The process of compilation takes your ROS2 package and compresses it into a set of executables which run your program and copies it into a install folder. Whenever the package is run, it then runs from the executables within the build folder.

This has some knock on effects:

  1. Writing code without compiling it won't actually change the existing executables!
  2. If you have static data in folders within the package, such as images or configuration files, your executable might complain that it can't find them when you run it!
    • This is because you haven't told the compiler to also copy your new files.
    • You have to add your new files into the data_files list in setup.py. The syntax is to add a tuple of ("share/" + package_name, ['A list of', 'my extra files']).
  3. If you create a new ros node, it will not immediately be available for running or use in a launch file.
    • You have to add your ros node as a new entry_point in setup.py
  4. Similarly if you create a new launch file but it doesn't shown up, make sure the file has name *.launch.* and is in the launch folder.
    • See the setup.py file again.

Note: As we will see in the container tutorial, all of this is not immediately obvious as its all inside the application container. See Exercises.

Multi-File Nodes

So far all of the ros-nodes have been implemented using a single control file. In this file you are sending Mavlink commands to the drone, keeping track of the navigation as well as keeping track of the higher level mission all at the same time! As you grow your application, you may start finding the maintenance of a monolithic single file source challenging.

Often we want to organise our code by some abstract notion, like functionality or purpose. For instance we want one module which just takes care of sending and receiving commands for the drone, such that if we change the overall mission, we don't accidentally break the drone communication code.

One example of a similar concept is given in perception where we create an extra node whose sole job is to monitor the camera inputs. We could have put that into the controller itself rather than having an extra node, and incuring the extra costs that might be involved in creating a new node.

An alternative to multi file nodes would be to make use of a new ROS2 feature called composition. We have not explored it in Starling so far, but feel free to give it a go too.

Implementing multi-file nodes

Fortunately it's almost as simply as normal Python to refactor your source into multiple files. However, there are a number of important differences, let us walk through the core changes showing in the multi_part_controller_mission.py file.

  • The source code in fenswood_drone_controller is registered as a python package. There are a number of effects of this, but the main one being that when you import your sub-file in your main file, you have to add a .:
from .multi_part_controller_drone_only import DroneController
     ^
  • If your sub-file requires use of ROS2 node functionality (e.g. creating clients, parameters etc), you will need to pass a reference to the ROS2 node into the sub class.

This is a little more challenging to explain - as this source code we're implementing here still represents the same single ROS2 node, all of the publishers, subscribers and other ROS2 features have to be created by the same node instance - no matter in which source file/ sub class we use. Therefore if we wish to create - say - a subscriber to mavros/state in the drone controller subclass, the DroneController will need access to the node to create the subscriber. The only way to do this is to pass in the rosnode in DroneController's constructor:

class DroneController():
    def __init__(self, node):
        self.node = node            # store the rosnode object
        ...
        self.node.create_subscription(State, '/vehicle_1/mavros/state', self.state_callback, 10)

"Wait", I hear you ask, aren't functions like create_subscription a part of the node itself, how do I pass the node to DroneController ... in fact where even is "the node"? Rewind to when we introduced objects and classes we created the FenswoodDroneController as a child of the ROS Node class. This means that FenswoodDroneController has all the properties of the Node and therefore is itself the node!

Then how does a class refer to itself - through the self keyword of course! In Python the self is actually a reference to itself. So you can pass self to functions if you want the function to have access or change the state of self, just like passing any other variable to a function.

Therefore in order to make multiple-source files work, we create a new controller property which represents the drone using the DroneController created previously. We then pass self into the drone to represent the ros node. This passes the node reference in so that DroneController then has access to node functions such as create_subscription!

from .multi_part_controller_drone_only import DroneController

class FenswoodDroneController(Node):

    def __init__(self):
        super().__init__('controller')

        # Create object representing drone
        self.drone = DroneController(self)

This can then be repeated each time you wish to refactor into multiple source files. This may feel weird and self-referential, but it works!

Note: Watch that the names of node parameters dont clash over the files, and that you don't create multiple publishers, subscribers and services that point to the same topic. Since these are all part of the same node, the node only likes unique things.

If you find yourself needing to create multiple of the same subscriber, for example, you may need to reconsider whether your refactoring.

You can run this example using the following. Note that this is not yet tested so feel free to submit a Pull Request if it doesn't fully work yet!

docker-compose -f 7_multi_part/docker-compose.yml up --build

Exercises

  1. Let us inspect your ROS2 package and what it's like after compilation.
    1. First run docker-compose.yml up in one terminal.
    2. Open up another terminal and use the docker ps to find the container id of your application container and docker exec -it <container id> bash to go inside the container. See this tutorial for more details.
    3. Once inside the container, you should be in the /ros_ws directory. Running ls -al you should see a number of directories. Your source code should be in src. Have a look in the other folders.
    4. In the ROS2 workspace root (i.e. /ros_ws) you can recompile yourself manually. First enable ROS2 by running source /opt/ros/foxy/setup.bash, then run colcon build.
    5. While there you can also try and launch some of the other controllers you have been working on!
  2. Lets say you want to add a new folder config as your code needs to read a configuration file named fenswood.yaml (It can just contain a single field).
    1. Add the reading of this configuration file into your source code with some logging.
    2. Modify the setup.py file so that your application can read your configuration when run.
  3. Try and pull out the drone related functionality into its own source file, while still remaining part of the same rosnode.