How the application is built - FontysAtWork/ESA-PROJ GitHub Wiki

C++

The application is written in C++ and is divided in several classes. We chose C++ as programming language for several reasons:

  • C++ has an easy structure which can be splitted in seperate files
  • C++ and ROS are compatible
  • Creating user interfaces is possible in several ways
  • C++ allows us to create threads.
  • The project group has experience with C++

Qt interface

Todo

ROS node

The ROS node is the part of the user application that communicates with the robot using the ROS framework. Before you continue reading we recommend to study how ROS works. This information can be found on the ROS wiki.

To initialize the ROS node we have to connect to the ROS core. This can be done using the ROS Environment Variables or by setting the master_url and host_url manually. Next we have to start the node using ros::start(). Now we can set the node-handlers which allows us to publish and subscribe to certain topics.

bool QNode::Init() {
...
}

bool QNode::Init(const std::string &master_url, const std::string &host_url) {
...
}

This is the main loop containing the ROS features. At the moment this only retrieves the position of the robot. Using a tf::TransformListener subscribed to the /map and the /base_link topics we are able to achieve this.

void QNode::run() {
...
}

To make it possible to get the robot position the GetRobotPosition() function can be used. This returns a geometry_msgs::Pose message.

geometry_msgs::Pose QNode::GetRobotPosition() {
...
}

The Panic() function is called when the user pushes the Panic button in the user interface. This will send commands to the robot to stop moving and kills the running processes.

void QNode::Panic() {
...
}

To move the robot to a selected position on the map we have to send a geometry_msgs::PoseStamped to the /move_base_simple/goal topic. We stored the position in the map as a vector containing geometry_msgs::Pose so we have to convert this to a geometry_msgs::PoseStamped.

void QNode::MoveRobotToPose(geometry_msgs::Pose pos) {
...
}

Yaml

Write

Within the gui there are two things that you're able to write to yaml files, these are the markers and the lines. This is done in a separate class called YamlWriter. This class has 4 functions in it, two for each of the type of data to save. Both the lines and yaml are written to the files in a same manner and only the actual data differs.

The main function called from the outside with the list opens the file. After opening the file it iterates through the list and adds the first line to the yaml file. This first line contains the type of data that it is, for marker adding the type of marker it is. After initializing the data it calls the function to create the text for a single marker or line. In this function it adds all of the data that is written to the file in a single string, all data separated by new lines. The function returns this text and it writes this text after the initial line. After going through all of the items in the list it closes the file.

Read

For reading the yaml file the program uses libyaml. This is a yaml parser written for c but it can be used within c++. This was chosen because it is easy to use and the main library for parsing in c++, libyaml-cpp wasn't compiling and certain parts weren't working at all.

Loading a yaml into the program happens with the yaml parser. This separates all of the data into KeyDataPairs. This is a struct created for containing Keys and the data associated with these keys. A key is the name of the data while the data part is the actual data.

After initializing the parser it scans the parser for any tokens that it finds. These tokens are the basis of the yaml build up. The programs scans the parser for tokens of the key type. After it finds a key it creates a key data pair and adds the key to this. After it has found a key it parsed the next token with the dataparser. This data can be of different types so it parses the data according to the type of token that it finds. This could be just a single entry of data, it could be a key used as data or an array of data. It adds this data to the KeyDataPair. After parsing the data it adds this to the list of KeyDataPairs.

After parsing all of the tokens in the file it closes the file, saving the KeyDataPairs in the list it has. When a new file is loaded this data is lost.

For parsing the KeyDataPairs into usable data a function has to be created which parses the list of KeyDataPairs into the correct data format. This has been done for all of the three ways that a yaml can be loaded into the program. This is just iterating through the list to find the appropriate KeyDataPairs before creating the correct data types.

Marker

The Marker class is used to store marker data in a simple way. It contains the information we want to store and some methods to retrieve the data. The robot position is stored as a geometry_msgs::Pose. We also store the name of the marker as a std::string and the type of the marker as an enum MarkerType.

enum MarkerType {
    Navigation = 0,
    Workspace = 1,
    Robot = 2
};

Map config

To store the map configuration we retrieved from the YAML file we use the MapConfig class. This contains a std::string containing the name, a geometry_msgs::Pose with the origin of the map and the resolution, negate, occupied threshold and the free threshold as double. The values can be retrieved with some methods.

CMake

Because we want to combine Qt and ROS we have to create our own CMakeLists.txt to compile the code. We use the ROS compiler catkin_make and have to link the Qt library to this compiler.

Do not change this unless you fully understand what you are doing!

The CMakeList is made for 2 different ROS versions:

  • ROS Indigo, running on Ubuntu 14.04, using Qt4
  • ROS Kinetic, running on Ubuntu 16.04, using Qt5

Currently the robot is running on Indigo but to make the application future-proof we decided to test the application on a Kinetic machine.

ROS Indigo

To find the required libraries we have to tell CMake to link to the right files, using find_package CMake will look for the right package and link it to the compiler.

find_package(catkin REQUIRED COMPONENTS qt_build tf roscpp)

We have to initialize the ROS components, this can be done using the catkin_package command.

catkin_package()

In ROS Indigo we can use the component qt_build which enables us to use the rosbuild_prepare_qt4 macro. This will initialize the Qt instances. Next we can use the QT4_ADD_RESOURCES, QT4_WRAP_UI and QT4_WRAP_CPP macros which compiles the Qt specific files.

rosbuild_prepare_qt4(QtCore QtGui)
QT4_ADD_RESOURCES(QT_RESOURCES_CPP ${QT_RESOURCES})
QT4_WRAP_UI(QT_FORMS_HPP ${QT_FORMS})
QT4_WRAP_CPP(QT_MOC_HPP ${QT_MOC})

Now we compiled all the ROS and Qt specific files we can create and install an executable using the standard CMake method.

add_executable(map_marker ${QT_SOURCES} ${QT_RESOURCES_CPP} ${QT_FORMS_HPP} ${QT_MOC_HPP})
target_link_libraries(map_marker ${QT_LIBRARIES} ${catkin_LIBRARIES} yaml)
install(TARGETS map_marker RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})

ROS Kinetic

In ROS Kinetic we have a different structure to compile the application. We don't have the rosbuild_prepare_qt4 macro in this version so we have to do some things manually. First we have to setCMAKE_INCLUDE_CURRENT_DIR to ON, this will include each folder which is accessed by the compiler to the includes. This is necessary to find the generated header files which are created when making the Qt forms.

set(CMAKE_INCLUDE_CURRENT_DIR ON)

Next we have to manually find the required packages and include them. We use the find_package macro and then check if the package is found, if it's not found we return an error message.

find_package(catkin REQUIRED COMPONENTS tf roscpp)
include_directories(${catkin_INCLUDE_DIRS})

if(NOT catkin_FOUND)
	message(FATAL_ERROR "find_package(catkin) failed...")
endif()

We have to initialize the ROS components, this can be done using the catkin_package command.

catkin_package()

Next we add some Qt specific definitions and flags.

add_definitions(${Qt5Widgets_DEFINITIONS})
set(CMAKE_CXX_FLAGS "${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}")

We can use the qt5_add_resources, qt5_wrap_ui and qt5_wrap_cpp macros which compiles the Qt specific files.

qt5_add_resources(QT_RESOURCES_CPP ${QT_RESOURCES})
qt5_wrap_ui(QT_FORMS_HPP ${QT_FORMS})
qt5_wrap_cpp(QT_MOC_HPP ${QT_MOC})

Now we've compiled all the ROS and Qt specific files we can create and install an executable using the standard CMake method. We've added each library individually to have a nice overview.

add_executable(map_marker ${QT_SOURCES} ${QT_RESOURCES_CPP} ${QT_FORMS_HPP} ${QT_MOC_HPP})

target_link_libraries(map_marker ${catkin_LIBRARIES})
target_link_libraries(map_marker Qt5::Widgets)
target_link_libraries(map_marker ${PYTHON_LIBRARIES})
target_link_libraries(map_marker ${Boost_LIBRARIES})
target_link_libraries(map_marker yaml)

install(TARGETS map_marker RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})