|
Jul
30
2010
Developing Well-Designed Packages for Robot Operating System (ROS), Part VIPart 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:
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:
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:
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:
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.
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:
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:
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! |
© 2011-2012 Codai, Inc. All Rights Reserved