|
| 1 | +Building a Custom RViz Panel |
| 2 | +============================ |
| 3 | + |
| 4 | +This tutorial is for people who would like to work within the RViz environment to either display or interact with some data in a two-dimensional environment. |
| 5 | + |
| 6 | +In this tutorial you will learn how to do three things within RViz: |
| 7 | + |
| 8 | +* Create a new QT panel within RViz. |
| 9 | +* Create a topic subscriber within RViz that can monitor messages published on that topic and display them within the RViz panel. |
| 10 | +* Create a topic publisher such button presses within RViz publish to an output topic in ROS. |
| 11 | + |
| 12 | +All of the code for this tutorial can be found in `this repository <https://github.com/MetroRobots/rviz_panel_tutorial>`__. |
| 13 | + |
| 14 | +Boilerplate Code |
| 15 | +---------------- |
| 16 | + |
| 17 | +Header File |
| 18 | +^^^^^^^^^^^ |
| 19 | + |
| 20 | +Here are the contents of ``demo_panel.hpp`` |
| 21 | + |
| 22 | +.. code-block:: c++ |
| 23 | + |
| 24 | + #ifndef RVIZ_PANEL_TUTORIAL__DEMO_PANEL_HPP_ |
| 25 | + #define RVIZ_PANEL_TUTORIAL__DEMO_PANEL_HPP_ |
| 26 | + |
| 27 | + #include <rviz_common/panel.hpp> |
| 28 | + |
| 29 | + namespace rviz_panel_tutorial |
| 30 | + { |
| 31 | + class DemoPanel |
| 32 | + : public rviz_common::Panel |
| 33 | + { |
| 34 | + Q_OBJECT |
| 35 | + public: |
| 36 | + explicit DemoPanel(QWidget * parent = 0); |
| 37 | + ~DemoPanel() override; |
| 38 | + }; |
| 39 | + } // namespace rviz_panel_tutorial |
| 40 | + |
| 41 | + #endif // RVIZ_PANEL_TUTORIAL__DEMO_PANEL_HPP_ |
| 42 | + |
| 43 | +* We're extending the `rviz_common::Panel <https://github.com/ros2/rviz/blob/9a94bdf2f5f92ccdac4037c9268b95940845d609/rviz_common/include/rviz_common/panel.hpp#L46>`__ class. |
| 44 | +* `For reasons outside the scope of this tutorial <https://doc.qt.io/qt-5/moc.html>`__, you need the ``Q_OBJECT`` macro in there to get the QT parts of the GUI to work. |
| 45 | +* We start by declaring just a constructor and destructor, implemented in the cpp file. |
| 46 | + |
| 47 | +Source File |
| 48 | +^^^^^^^^^^^ |
| 49 | + |
| 50 | +``demo_panel.cpp`` |
| 51 | + |
| 52 | +.. code-block:: c++ |
| 53 | + |
| 54 | + #include <rviz_panel_tutorial/demo_panel.hpp> |
| 55 | + |
| 56 | + namespace rviz_panel_tutorial |
| 57 | + { |
| 58 | + DemoPanel::DemoPanel(QWidget* parent) : Panel(parent) |
| 59 | + { |
| 60 | + } |
| 61 | + |
| 62 | + DemoPanel::~DemoPanel() = default; |
| 63 | + } // namespace rviz_panel_tutorial |
| 64 | + |
| 65 | + #include <pluginlib/class_list_macros.hpp> |
| 66 | + PLUGINLIB_EXPORT_CLASS(rviz_panel_tutorial::DemoPanel, rviz_common::Panel) |
| 67 | + |
| 68 | +* Overriding the constructor and deconstructor are not strictly necessary, but we can do more with them later. |
| 69 | +* In order for RViz to find our plugin, we need this ``PLUGINLIB`` invocation in our code (as well as other things below). |
| 70 | + |
| 71 | +package.xml |
| 72 | +^^^^^^^^^^^ |
| 73 | + |
| 74 | +We need the following dependencies in our package.xml: |
| 75 | + |
| 76 | +.. code-block:: xml |
| 77 | +
|
| 78 | + <depend>pluginlib</depend> |
| 79 | + <depend>rviz_common</depend> |
| 80 | +
|
| 81 | +rviz_common_plugins.xml |
| 82 | +^^^^^^^^^^^^^^^^^^^^^^^ |
| 83 | + |
| 84 | +.. code-block:: xml |
| 85 | +
|
| 86 | + <library path="demo_panel"> |
| 87 | + <class type="rviz_panel_tutorial::DemoPanel" base_class_type="rviz_common::Panel"> |
| 88 | + <description></description> |
| 89 | + </class> |
| 90 | + </library> |
| 91 | +
|
| 92 | +* This is standard ``pluginlib`` code. |
| 93 | + |
| 94 | + * The library ``path`` is the name of the library we'll assign in the CMake. |
| 95 | + * The class should match the ``PLUGINLIB`` invocation from above. |
| 96 | + |
| 97 | +* We'll come back to the description later, I promise. |
| 98 | + |
| 99 | +CMakeLists.txt |
| 100 | +^^^^^^^^^^^^^^ |
| 101 | + |
| 102 | +Add the following lines to the top of the standard boilerplate. |
| 103 | + |
| 104 | +.. code-block:: cmake |
| 105 | +
|
| 106 | + find_package(ament_cmake_ros REQUIRED) |
| 107 | + find_package(pluginlib REQUIRED) |
| 108 | + find_package(rviz_common REQUIRED) |
| 109 | +
|
| 110 | + set(CMAKE_AUTOMOC ON) |
| 111 | + qt5_wrap_cpp(MOC_FILES |
| 112 | + include/rviz_panel_tutorial/demo_panel.hpp |
| 113 | + ) |
| 114 | +
|
| 115 | + add_library(demo_panel src/demo_panel.cpp ${MOC_FILES}) |
| 116 | + target_include_directories(demo_panel PUBLIC |
| 117 | + $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> |
| 118 | + $<INSTALL_INTERFACE:include> |
| 119 | + ) |
| 120 | + ament_target_dependencies(demo_panel |
| 121 | + pluginlib |
| 122 | + rviz_common |
| 123 | + ) |
| 124 | + install(TARGETS demo_panel |
| 125 | + EXPORT export_rviz_panel_tutorial |
| 126 | + ARCHIVE DESTINATION lib |
| 127 | + LIBRARY DESTINATION lib |
| 128 | + RUNTIME DESTINATION bin |
| 129 | + ) |
| 130 | + install(DIRECTORY include/ |
| 131 | + DESTINATION include |
| 132 | + ) |
| 133 | + install(FILES rviz_common_plugins.xml |
| 134 | + DESTINATION share/${PROJECT_NAME} |
| 135 | + ) |
| 136 | + ament_export_include_directories(include) |
| 137 | + ament_export_targets(export_rviz_panel_tutorial) |
| 138 | + pluginlib_export_plugin_description_file(rviz_common rviz_common_plugins.xml) |
| 139 | +
|
| 140 | +
|
| 141 | +* To generate the proper Qt files, we need to |
| 142 | + |
| 143 | + * Turn ``CMAKE_AUTOMOC`` on. |
| 144 | + * Wrap the headers by calling ``qt5_wrap_cpp`` with each header that has ``Q_OBJECT`` in it. |
| 145 | + * Include the ``MOC_FILES`` in the library alongside our other cpp files. |
| 146 | + |
| 147 | +* A lot of the other code ensures that the plugin portion works. |
| 148 | + Namely, calling ``pluginlib_export_plugin_description_file`` is essential to getting RViz to find your new plugin. |
| 149 | + |
| 150 | +Testing it out |
| 151 | +^^^^^^^^^^^^^^ |
| 152 | + |
| 153 | +Compile your code, source your workspace and run ``rviz2``. |
| 154 | + |
| 155 | +In the top Menu bar, there should be a "Panels" menu. |
| 156 | +Select "Add New Panel" from that menu. |
| 157 | + |
| 158 | +.. image:: images/Select0.png |
| 159 | + :target: images/Select0.png |
| 160 | + :alt: screenshot of Add New Panel dialog |
| 161 | + |
| 162 | +A dialog will pop up showing all the panels accessible in your ROS environment, grouped into folders based on their ROS package. |
| 163 | +Create a new instance of your panel by either double clicking on its name, or selecting it and clicking OK. |
| 164 | + |
| 165 | +This should create a new panel in your RViz window, albeit with nothing but a title bar with the name of your panel. |
| 166 | + |
| 167 | +.. image:: images/RViz0.png |
| 168 | + :target: images/RViz0.png |
| 169 | + :alt: screenshot of the whole RViz window showing the new simple panel |
| 170 | + |
| 171 | +Filling in the Panel |
| 172 | +-------------------- |
| 173 | +We're going to update our panel with some very basic ROS/QT interaction. |
| 174 | +What we will do, roughly, is access the ROS node from within RViz that can both subscribe and publish to ROS topics. |
| 175 | +We will use our subscriber to monitor an ``/input`` topic within ROS and display the published ``String`` values in the widget. |
| 176 | +We use our publisher to map button presses within RViz to messages published on a ROS topic named ``/output`` . |
| 177 | + |
| 178 | +Updated Header File |
| 179 | +^^^^^^^^^^^^^^^^^^^ |
| 180 | + |
| 181 | +Update ``demo_panel.hpp`` to include the following includes and class Body. |
| 182 | + |
| 183 | +.. code-block:: c++ |
| 184 | + |
| 185 | + #include <rviz_common/panel.hpp> |
| 186 | + #include <rviz_common/ros_integration/ros_node_abstraction_iface.hpp> |
| 187 | + #include <std_msgs/msg/string.hpp> |
| 188 | + #include <QLabel> |
| 189 | + #include <QPushButton> |
| 190 | + |
| 191 | + namespace rviz_panel_tutorial |
| 192 | + { |
| 193 | + class DemoPanel : public rviz_common::Panel |
| 194 | + { |
| 195 | + Q_OBJECT |
| 196 | + public: |
| 197 | + explicit DemoPanel(QWidget * parent = 0); |
| 198 | + ~DemoPanel() override; |
| 199 | + |
| 200 | + void onInitialize() override; |
| 201 | + |
| 202 | + protected: |
| 203 | + std::shared_ptr<rviz_common::ros_integration::RosNodeAbstractionIface> node_ptr_; |
| 204 | + rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_; |
| 205 | + rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_; |
| 206 | + |
| 207 | + void topicCallback(const std_msgs::msg::String & msg); |
| 208 | + |
| 209 | + QLabel* label_; |
| 210 | + QPushButton* button_; |
| 211 | + |
| 212 | + private Q_SLOTS: |
| 213 | + void buttonActivated(); |
| 214 | + }; |
| 215 | + } // namespace rviz_panel_tutorial |
| 216 | + |
| 217 | +* On the ROS side, we declare an abstract node pointer, which we will use to create interfaces to the wider ROS ecosystem. |
| 218 | + We have a subscriber which will allow us to take information from ROS and use it in RViz. |
| 219 | + The publisher allows us to publish information/events from within RViz and make them available in ROS. |
| 220 | + We also have methods an initialization method for setting up the ROS components (``onInitialize``) and a callback for the subscriber (``topicCallback``). |
| 221 | +* On the QT side, we declare a label and a button, as well as a callback for the button (``buttonActivated``). |
| 222 | + |
| 223 | +Updated Source File |
| 224 | +^^^^^^^^^^^^^^^^^^^ |
| 225 | + |
| 226 | +Update ``demo_panel.cpp`` to have the following contents: |
| 227 | + |
| 228 | +.. code-block:: c++ |
| 229 | + |
| 230 | + #include <rviz_panel_tutorial/demo_panel.hpp> |
| 231 | + #include <QVBoxLayout> |
| 232 | + #include <rviz_common/display_context.hpp> |
| 233 | + |
| 234 | + namespace rviz_panel_tutorial |
| 235 | + { |
| 236 | + |
| 237 | + DemoPanel::DemoPanel(QWidget* parent) : Panel(parent) |
| 238 | + { |
| 239 | + // Create a label and a button, displayed vertically (the V in VBox means vertical) |
| 240 | + const auto layout = new QVBoxLayout(this); |
| 241 | + // Create a button and a label for the button |
| 242 | + label_ = new QLabel("[no data]"); |
| 243 | + button_ = new QPushButton("GO!"); |
| 244 | + // Add those elements to the GUI layout |
| 245 | + layout->addWidget(label_); |
| 246 | + layout->addWidget(button_); |
| 247 | + |
| 248 | + // Connect the event of when the button is released to our callback, |
| 249 | + // so pressing the button results in the buttonActivated callback being called. |
| 250 | + QObject::connect(button_, &QPushButton::released, this, &DemoPanel::buttonActivated); |
| 251 | + } |
| 252 | + |
| 253 | + DemoPanel::~DemoPanel() = default; |
| 254 | + |
| 255 | + void DemoPanel::onInitialize() |
| 256 | + { |
| 257 | + // Access the abstract ROS Node and |
| 258 | + // in the process lock it for exclusive use until the method is done. |
| 259 | + node_ptr_ = getDisplayContext()->getRosNodeAbstraction().lock(); |
| 260 | + |
| 261 | + // Get a pointer to the familiar rclcpp::Node for making subscriptions/publishers |
| 262 | + // (as per normal rclcpp code) |
| 263 | + rclcpp::Node::SharedPtr node = node_ptr_->get_raw_node(); |
| 264 | + |
| 265 | + // Create a String publisher for the output |
| 266 | + publisher_ = node->create_publisher<std_msgs::msg::String>("/output", 10); |
| 267 | + |
| 268 | + // Create a String subscription and bind it to the topicCallback inside this class. |
| 269 | + subscription_ = node->create_subscription<std_msgs::msg::String>("/input", 10, std::bind(&DemoPanel::topicCallback, this, std::placeholders::_1)); |
| 270 | + } |
| 271 | + |
| 272 | + // When the subscriber gets a message, this callback is triggered, |
| 273 | + // and then we copy its data into the widget's label |
| 274 | + void DemoPanel::topicCallback(const std_msgs::msg::String & msg) |
| 275 | + { |
| 276 | + label_->setText(QString(msg.data.c_str())); |
| 277 | + } |
| 278 | + |
| 279 | + // When the widget's button is pressed, this callback is triggered, |
| 280 | + // and then we publish a new message on our topic. |
| 281 | + void DemoPanel::buttonActivated() |
| 282 | + { |
| 283 | + auto message = std_msgs::msg::String(); |
| 284 | + message.data = "Button clicked!"; |
| 285 | + publisher_->publish(message); |
| 286 | + } |
| 287 | + |
| 288 | + } // namespace rviz_panel_tutorial |
| 289 | + |
| 290 | + #include <pluginlib/class_list_macros.hpp> |
| 291 | + |
| 292 | + PLUGINLIB_EXPORT_CLASS(rviz_panel_tutorial::DemoPanel, rviz_common::Panel) |
| 293 | + |
| 294 | +Testing with ROS |
| 295 | +^^^^^^^^^^^^^^^^ |
| 296 | +Compile and launch RViz2 with your panel again. You should see your label and button in the panel now. |
| 297 | + |
| 298 | +.. image:: images/RViz1.png |
| 299 | + :target: images/RViz1.png |
| 300 | + :alt: screenshot of the RViz panel in its default state |
| 301 | + |
| 302 | +To change the label, we simply have to publish a message on the ``/input`` topic, which you can do with this command: |
| 303 | + |
| 304 | +.. code-block:: bash |
| 305 | +
|
| 306 | + ros2 topic pub /input std_msgs/msg/String "{data: 'Please be kind.'}" |
| 307 | +
|
| 308 | +Since the widget is subscribed to this topic, it will trigger the callback and change the text of the label. |
| 309 | + |
| 310 | +.. image:: images/RViz2.png |
| 311 | + :target: images/RViz2.png |
| 312 | + :alt: screenshot of the RViz panel with custom string message displayed |
| 313 | + |
| 314 | + |
| 315 | +Pressing the button will publish a message, which you can see by echoing the ``/output`` topic, like with this command. |
| 316 | + |
| 317 | +.. code-block:: bash |
| 318 | +
|
| 319 | + ros2 topic echo /output |
| 320 | +
|
| 321 | +
|
| 322 | +Cleanup |
| 323 | +------- |
| 324 | + |
| 325 | +Now its time to clean it up a bit. |
| 326 | +This makes things look nicer and be a little easier to use, but aren't strictly required. |
| 327 | + |
| 328 | +First, you should update the description of your plugin in ``rviz_common_plugins.xml`` |
| 329 | + |
| 330 | +We also add an icon for the plugin at ``icons/classes/DemoPanel.png``. |
| 331 | +The folder is hardcoded, and the filename should match the name from the plugin declaration (or the name of the class if not specified). |
| 332 | + |
| 333 | +We need to install the image file in the CMake. |
| 334 | + |
| 335 | +.. code-block:: cmake |
| 336 | +
|
| 337 | + install(FILES icons/classes/DemoPanel.png |
| 338 | + DESTINATION share/${PROJECT_NAME}/icons/classes |
| 339 | + ) |
| 340 | +
|
| 341 | +Now when you add the panel, it should show up with an icon and description. |
| 342 | + |
| 343 | +.. image:: images/Select1.png |
| 344 | + :target: images/Select1.png |
| 345 | + :alt: screenshot of Add New Panel dialog with our custom icon and description |
| 346 | + |
| 347 | +The panel will also have an updated icon. |
| 348 | + |
| 349 | +.. image:: images/RViz3.png |
| 350 | + :target: images/RViz3.png |
| 351 | + :alt: screenshot of the RViz panel with custom icon |
0 commit comments