[TOC]
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 fenswood_drone_controller
source folder we see a few more folders and files.
fenswood_drone_controller
- the folder which contains your source code, it is considered a python package and therefore should have an__init__.py
file.launch
- the folder which contains all of your launch files for various versions of your controller.resource
- a Python ROS2 package specific folder which contains the executable your controller gets compiled into.Dockerfile
- not a part of a ROS2 package, this is Starling specific, see a later tutorial for more details.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 calledrosdep
which reads this file and is often used to automatically find dependencies )setup.cfg
- This is an autogenerated file which defines where to install this package.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.
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:
- Writing code without compiling it won't actually change the existing executables!
- 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 insetup.py
. The syntax is to add a tuple of("share/" + package_name, ['A list of', 'my extra files'])
.
- 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
insetup.py
- You have to add your ros node as a new
- Similarly if you create a new launch file but it doesn't shown up, make sure the file has name
*.launch.*
and is in thelaunch
folder.- See the
setup.py
file again.
- See the
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.
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.
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
- Let us inspect your ROS2 package and what it's like after compilation.
- First run
docker-compose.yml
up in one terminal. - Open up another terminal and use the
docker ps
to find the container id of your application container anddocker exec -it <container id> bash
to go inside the container. See this tutorial for more details. - Once inside the container, you should be in the
/ros_ws
directory. Runningls -al
you should see a number of directories. Your source code should be insrc
. Have a look in the other folders. - In the ROS2 workspace root (i.e.
/ros_ws
) you can recompile yourself manually. First enable ROS2 by runningsource /opt/ros/foxy/setup.bash
, then runcolcon build
. - While there you can also try and launch some of the other controllers you have been working on!
- First run
- Lets say you want to add a new folder
config
as your code needs to read a configuration file namedfenswood.yaml
(It can just contain a single field).- Add the reading of this configuration file into your source code with some logging.
- Modify the
setup.py
file so that your application can read your configuration when run.
- Try and pull out the drone related functionality into its own source file, while still remaining part of the same rosnode.