Warning: file() [function.file]: Couldn't resolve host name in /home/sharpr6/public_html/wp-content/themes/starscape/code/starscape.php on line 419

Warning: file(http://upgrade.starscapetheme.com/check.php?v=1.5.6&b=http%3A%2F%2Fwww.sharprobotica.com) [function.file]: failed to open stream: operation failed in /home/sharpr6/public_html/wp-content/themes/starscape/code/starscape.php on line 419

Warning: strlen() expects parameter 1 to be string, array given in /home/sharpr6/public_html/wp-content/themes/starscape/code/starscape.php on line 450
Developing Well-Designed Packages for Robot Operating System (ROS), Part VI | SharpRobotica.com - Sharp ideas for the software side of robotics

Developing Well-Designed Packages for Robot Operating System (ROS), Part VI

datePosted on 07:22, July 30th, 2010 by Billy McCafferty

Part VI: Adding a UI Layer to the Package

As the last and final chapter to this series of posts (Part I, II, III, IV, V), we’ll be adding a basic UI layer to facilitate user interaction with the underlying layers of our package. Specifically, a UI will be developed to allow the user (e.g., you) to start and stop the laser reporting application service via a wxWidgets interface. If you’re new to wxWidgets, it really is a terrific open-source UI package with very helpful online tutorials, a thriving community, and a very helpful book, Cross-Platform GUI Programming with wxWidgets – certainly a good reference to add to the bookshelf. Arguably, the sample code discussed below is very simplistic and only touches upon wxWidgets; with that said, it should demonstrate how to put the basics in place and to see how the UI layer interacts with the other layers of the package.

Developing a UI layer with wxWidgets is quite straight forward; the UI itself is made up of two primary elements: a wxApp which is used to initialize the UI and a wxFrame which serves as the primary window. For the task at hand, the wxApp in the UI layer will be used to perform three primary tasks, in the order listed:

  1. Initialize ROS,
  2. Initialize application services and dependencies for those services (e.g., message endpoints), and
  3. Create the initial frame/window; application services will be passed to the frame to enable wiring up UI events to the respective application service functions.

As a rule of thumb, the UI layer should only communicate to the rest of the package elements via the application services layer. E.g., the UI layer should not be invoking functions directly on domain objects found within ladar_reporter_core; instead, it should call tasks exposed by the application services layer which then coordinates and delegates activity to lower levels.

Before we delve deeper, as a reminder of what the overall class diagram looks like, as developed over the previous posts, review the class diagram found within Part V. The current objective will be to add the UI layer, as illustrated in the package diagram found within Part I. To cut to the chase and download the end result of this post, click here.

Show me the code!

1. Setup the Package Skeleton, Domain Layer, Application Services Layer, and Message Endpoint Layer

If not done already, follow the steps in Part II, III, IV, and V to get everything in place. (Or simply download the source from Part V to skip all the action packed steps leading up to this post.)

2. Install wxWidgets

Download and install wxWidgets. Instructions for Ubuntu and Debian may be found at http://wiki.wxpython.org/InstallingOnUbuntuOrDebian.

3. Define the UI events that the user may raise

Create an enum class at src/ui/UiEvents.hpp to define UI events as follows:

// UiEvents.hpp
 
#ifndef GUARD_UiEvents
#define GUARD_UiEvents
 
namespace ladar_reporter_ui
{
  enum UiEventType
  {
    UI_EVENT_Quit = 1,
    UI_EVENT_StartReporting = 2,
    UI_EVENT_StopReporting = 3
  };
}
 
#endif /* GUARD_UiEvents */

As suggested by the enum values, the user will be able to start the reporting process, stop it, and quit the application altogether.

4. Create the wxWidgets application header class

Create src/ui/LadarReporterApp.hpp containing the following code:

// LadarReporterApp.hpp
 
#include <boost/shared_ptr.hpp>
#include <ros/ros.h>
#include "LaserScanEndpoint.hpp"
#include "LaserScanReportingService.hpp"
 
namespace ladar_reporter_ui
{
  class LadarReporterApp : public wxApp
  {
    public:
      virtual bool OnInit();
      virtual int OnExit();
 
    private:
      void InitializeRos();
      void InitializeApplicationServices();
      void CreateMainWindow();
 
      char** _argvForRos;
      ros::NodeHandlePtr _nodeHandlePtr;
 
      // Application services and dependencies.
      // Stored as pointers to postpone creation until ready to initialize.
      boost::shared_ptr<ladar_reporter_core::ILaserScanEndpoint> _laserScanEndpoint;
      boost::shared_ptr<ladar_reporter_application_services::LaserScanReportingService> _laserScanReportingService;
  };
}

A few notes:

  • LadarReporterApp inherits from wxApp to create the “main” equivalent for wxWidgets.
  • OnInit() and OnExit() are events called by wxWidgets when the UI is created and upon exiting (obviously).
  • InitializeRos(), InitializeApplicationServices(), and CreateMainWindow() lay out the three primary tasks previously described.
  • _nodeHandlePtr will hold our initialized reference to ROS during the lifetime of the UI.
  • Finally, the application services and dependencies which will be leveraged by the UI are declared. While _laserScanEndpoint won’t be used by the UI directly, it’ll need to be injected into the constructor of _laserScanReportingService and will need to be kept alive so as to continue advertising on its respective topic.

5. Create the wxWidgets application implementation class

Create src/ui/LadarReporterApp.cpp containing the following code:

// LadarReporterApp.cpp
 
#include <wx/wx.h>
#include "LadarReporterApp.hpp"
#include "LadarReporterFrame.hpp"
#include "LaserScanEndpoint.hpp"
#include "UiEvents.hpp"
 
using namespace ladar_reporter_application_services;
using namespace ladar_reporter_core;
using namespace ladar_reporter_message_endpoints;
 
// Inform wxWidgets what to use as the wxApp
IMPLEMENT_APP(ladar_reporter_ui::LadarReporterApp);
// Implements LadarReporterApp& wxGetApp() globally
DECLARE_APP(ladar_reporter_ui::LadarReporterApp);
 
namespace ladar_reporter_ui
{
  bool LadarReporterApp::OnInit() {
    // Order of initialization functions is critical:
    // 1) ROS must be initialized before message endpoint(s) can advertise
    InitializeRos();
 
    // 2) Application services must be initialized before being passed to UI
    InitializeApplicationServices();
 
    // 3) UI can be created with properly initialized ROS and application services
    CreateMainWindow();
 
    return true;
  }
 
  int LadarReporterApp::OnExit() {
    for (int i = 0; i < argc; ++i) {
      free(_argvForRos[i]);
    }
 
    delete [] _argvForRos;
 
    return 0;
  }
 
  void LadarReporterApp::InitializeRos() {
    // create our own copy of argv, with regular char*s.
    _argvForRos =  new char*[argc];
 
    for (int i = 0; i < argc; ++i) {
      _argvForRos[i] = strdup( wxString( argv[i] ).mb_str() );
    }
 
    ros::init(argc, _argvForRos, "ladar_reporter");
    _nodeHandlePtr.reset(new ros::NodeHandle);
  }
 
  void LadarReporterApp::InitializeApplicationServices() {
    _laserScanEndpoint = boost::shared_ptr<ILaserScanEndpoint>(
      new LaserScanEndpoint());
 
    _laserScanReportingService = boost::shared_ptr<LaserScanReportingService>(
      new LaserScanReportingService(_laserScanEndpoint));
  }
 
  void LadarReporterApp::CreateMainWindow() {
    LadarReporterFrame * frame = new LadarReporterFrame(
      _laserScanReportingService, _("Ladar Reporter"), wxPoint(50, 50), wxSize(450, 200));
 
    frame->Connect( ladar_reporter_ui::UI_EVENT_Quit, wxEVT_COMMAND_MENU_SELECTED,
      (wxObjectEventFunction) &LadarReporterFrame::OnQuit );
    frame->Connect( ladar_reporter_ui::UI_EVENT_StartReporting, wxEVT_COMMAND_MENU_SELECTED,
      (wxObjectEventFunction) &LadarReporterFrame::OnStartReporting );
    frame->Connect( ladar_reporter_ui::UI_EVENT_StopReporting, wxEVT_COMMAND_MENU_SELECTED,
      (wxObjectEventFunction) &LadarReporterFrame::OnStopReporting );
 
    frame->Show();
    SetTopWindow(frame);
  }
}

The direction for this class was taken from wxWidgets online tutorials along with reviewing the ROS turtlesim package, which is a real treasure trove for seeing how a much more sophisticated ROS UI is put together. (If you have not already, I strongly suggest you review the turtlesim code in detail.)

6. Create the wxWidgets frame header class

Now that the wxWidgets application is in place, the frame, representing the UI window itself, needs to be developed. Accordingly, create src/ui/LadarReporterFrame.hpp containing the following code:

// LadarReporterFrame.hpp
 
#ifndef GUARD_LadarReporterFrame
#define GUARD_LadarReporterFrame
 
#include <wx/wx.h>
#include "LaserScanReportingService.hpp"
 
namespace ladar_reporter_ui
{  
  class LadarReporterFrame : public wxFrame
  {
    public:
        LadarReporterFrame(
          boost::shared_ptr<ladar_reporter_application_services::LaserScanReportingService> laserScanReportingService, 
          const wxString& title, const wxPoint& pos, const wxSize& size);
        void OnQuit(wxCommandEvent& event);
        void OnStartReporting(wxCommandEvent& event);
        void OnStopReporting(wxCommandEvent& event);
 
    private:
        boost::shared_ptr<ladar_reporter_application_services::LaserScanReportingService> _laserScanReportingService;
  };
}
 
#endif /* GUARD_LadarReporterFrame */

There are a couple of interesting bits in the header:

  • The frame header declares the events which will be handled; e.g., OnQuit.
  • It accepts and stores an instance of the LaserScanReportingService in order to invoke the application service layer in response to user interaction.

7. Create the wxWidgets frame implementation class

Create src/ui/LadarReporterFrame.cpp containing the following code:

// LadarReporterFrame.cpp
 
#include "LadarReporterFrame.hpp"
#include "UiEvents.hpp"
 
using namespace ladar_reporter_application_services;
 
namespace ladar_reporter_ui
{
  LadarReporterFrame::LadarReporterFrame(
    boost::shared_ptr<LaserScanReportingService> laserScanReportingService, 
    const wxString& title, const wxPoint& pos, const wxSize& size)
      : wxFrame( NULL, -1, title, pos, size ), _laserScanReportingService(laserScanReportingService)
  {
    wxMenuBar *menuBar = new wxMenuBar;
    wxMenu *menuAction = new wxMenu;
 
    menuAction->Append( UI_EVENT_StartReporting, _("&Start Reporting") );
    menuAction->AppendSeparator();
    menuAction->Append( UI_EVENT_StopReporting, _("S&top Reporting") );
    menuAction->AppendSeparator();
    menuAction->Append( UI_EVENT_Quit, _("E&xit") );
 
    menuBar->Append(menuAction, _("&Action") );
 
    SetMenuBar(menuBar);
    CreateStatusBar();
    SetStatusText( _("Ready to begin reporting") );
  }
 
  void LadarReporterFrame::OnStartReporting(wxCommandEvent& WXUNUSED(event)) {
    _laserScanReportingService->beginReporting();
 
    SetStatusText( _("Laser scan reporting is running") );
  }
 
  void LadarReporterFrame::OnStopReporting(wxCommandEvent& WXUNUSED(event)) {
    _laserScanReportingService->stopReporting();
 
    SetStatusText( _("Laser scan reporting has been stopped") );
  }
 
  void LadarReporterFrame::OnQuit(wxCommandEvent& WXUNUSED(event)) {
    Close(true);
  }
}

A few implementation notes:

  • The LadarReporterFrame() constructor initializes the menubar for the window along with other rendering details.
  • Each of the respective events are defined, invoking the application services layer when applicable. As discussed previously, the UI should interact with the rest of the package layers via the application services, as demonstrated above.

There’s obviously a lot of wxWidgets related information which I am glossing over which is beyond the scope of these posts. The wxWidgets documentation referenced earlier should fill in any remaining gaps.

8. Configure CMake to Include the Header and Implementation

With the header and implementation classes completed for the both the wxWidgets application and frame, we need to make a couple of minor modifications to CMake for their inclusion in the build.

  1. Open /ladar_reporter/CMakeLists.txt. Under the commented line #rosbuild_gensrv(), add inclusions for wxWidgets as follows:
    find_package(wxWidgets REQUIRED)
    include(${wxWidgets_USE_FILE})
    include_directories( ${wxWidgets_INCLUDE_DIRS} )
  2. With /ladar_reporter/CMakeLists.txt still open, add a line to the include_directories statement for the following directory in order to include the wxWidgets application and frame headers:
    "src/ui"
  3. Open /ladar_reporter/src/CMakeLists.txt. Add an inclusion for the ui directory at the end of the file:
    add_subdirectory(ui)
  4. Now, in order to create the UI executable itself, a new CMake file is needed under /ladar_reporter/src/ui. Accordingly, create a new CMakeLists.txt under /ladar_reporter/src/ui, containing the following:
    rosbuild_add_executable(
      ladar_reporter_node
      LadarReporterApp.cpp
      LadarReporterFrame.cpp
    )
    
    target_link_libraries(
      ladar_reporter_node
      ladar_reporter_application_services
      ladar_reporter_message_endpoints
      # Important that core comes after application_services due to direction of dependencies
      ladar_reporter_core
      ${wxWidgets_LIBRARIES}
    )

9. Add a ROS wxWidgets Dependency to manifest.xml

Since the package will be leveraging wxWidgets, a dependency needs to be added for the package to find and use this, accordingly:

  • Under /ladar_reporter, open manifest.xml and add the following line just before the closing </package> tag:
    <rosdep name="wxwidgets"/>

10. Build and try out the UI Functionality

We are now ready to try everything out. While it is generally possible to write unit tests for the UI layer, personal experience has shown that the UI changes too frequently to make such unit tests worth while. UI unit tests quickly become a maintenance headache and do not provide much more value than what the existing unit tests have already proven; i.e., we’ve already verified through unit tests that the heart of our package – the domain objects, the message endpoints, and the application services – are all working as expected…the UI is now “simply” the final touch. Enough babble, let’s see this baby in action:

  1. Build the application by running make within a terminal at the root of the package (at /ladar_reporter).
  2. Open a new terminal and run roscore to begin ROS
  3. Open a third terminal window and run rostopic echo /laser_report to observe any laser reports published to ROS
  4. In the terminal that you’ve been building in…
    make
    cd bin
    ./ladar_reporter_tests

    Wait a second, that’s not showing anything new…that’s right! Always run your unit tests after making changes, even if you haven’t added any new tests, to make sure that you haven’t broken any existing code.

  5. Still within the bin folder – in the terminal – run ./ladar_reporter_node. A window should pop-up allowing you to select an “Action” menu to start and stop the reporting service along with quitting the application altogether. You can see the laser reports going out to ROS via the third terminal window that was opened.
  6. Click the “Quit” menu item when you just can’t handle any more excitement.

Well, that about wraps it up, we started by laying out our architecture and systematically tackling each layer of the package with proper separation of concerns and unit testing to make sure we were doing what we said we were doing. As demonstrated with the layering approach that we developed, higher layers (e.g., application services and core) didn’t depend on lower layers (e.g., message endpoints and the ROS API). In fact, when possible, the lower layers actually depended on interfaces defined in the higher layers; e.g., the message endpoint implemented an interface defined in the higher core layer. (Although the class diagrams show core on the bottom, it’s actually reflecting the dependency inversion that was introduced.) This dependency inversion enabled a clean separation of concerns while allowing us to unit test the various layers in isolation of each other.

I sincerely hope that this series has shed some light on how to properly architect a ROS package. While this series did not go into a granular level of detail with respect to using ROS and wxWidgets, it should have provided a good starting point for developing a solid package. The techniques described in this series have been honed over many years by demi-gods of development (e.g., Martin Fowler, Robert Martin, Kent Beck, Ward Cunningham, and many others) and continue to prove their value in enabling the development of maintainable, extensible applications which are enjoyable to work on. While ROS may be relatively new, the tried and trued lessons of professional development are quite timeless indeed.

As always, your feedback, questions, comments, suggestions, and even rebuttals are most welcome. To delve a bit further into many of the patterns oriented topics discussed, I recommend reading Gregor Hohpe’s Enterprise Integration Patterns and Robert Martin’s Agile Software Development, Principles, Patterns, and Practices. And obviously, for anything ROS related, you’ll want to keep reading everything you can at http://www.ros.org/wiki/ (and here at sharprobotica.com, of course)!

Enjoy!
Billy McCafferty

Download the source for this article.

categoryPosted in Architecture, ROS | printPrint

Leave a Reply

Name: (required)
Email: (required) (will not be published)
Website:
Comment:
*

© 2011-2014 Codai, Inc. All Rights Reserved