diff --git a/include/ignition/gazebo/SystemLoader.hh b/include/ignition/gazebo/SystemLoader.hh index 592027394a..2ba237bcd3 100644 --- a/include/ignition/gazebo/SystemLoader.hh +++ b/include/ignition/gazebo/SystemLoader.hh @@ -17,6 +17,7 @@ #ifndef IGNITION_GAZEBO_SYSTEMLOADER_HH_ #define IGNITION_GAZEBO_SYSTEMLOADER_HH_ +#include #include #include #include @@ -61,7 +62,8 @@ namespace ignition /// \brief Load and instantiate system plugin from name/filename. /// \param[in] _filename Shared library filename to load plugin from. - /// \param[in] _name Class name to be instantiated. + /// \param[in] _name Class name to be instantiated. If empty, the first + /// plugin in the shared library will be loaded. /// \param[in] _sdf SDF Element describing plugin instance to be loaded. /// \returns Shared pointer to system instance or nullptr. /// \note This will be deprecated in Gazebo 7 (Garden), please the use @@ -81,6 +83,10 @@ namespace ignition /// \returns A pretty string public: std::string PrettyStr() const; + /// \brief Get the plugin search paths used for loading system plugins + /// \return Paths to search for plugins + public: std::list PluginPaths() const; + /// \brief Pointer to private data. private: std::unique_ptr dataPtr; }; diff --git a/src/ServerConfig.cc b/src/ServerConfig.cc index 00e31e9e8b..201b9eaaa2 100644 --- a/src/ServerConfig.cc +++ b/src/ServerConfig.cc @@ -91,6 +91,7 @@ class ignition::gazebo::ServerConfig::PluginInfoPrivate /// \brief XML elements associated with this plugin public: sdf::ElementPtr sdf = nullptr; + }; ////////////////////////////////////////////////// diff --git a/src/SystemLoader.cc b/src/SystemLoader.cc index fbf7ae020e..fd8556cb7f 100644 --- a/src/SystemLoader.cc +++ b/src/SystemLoader.cc @@ -41,8 +41,7 @@ class ignition::gazebo::SystemLoaderPrivate public: explicit SystemLoaderPrivate() = default; ////////////////////////////////////////////////// - public: bool InstantiateSystemPlugin(const sdf::Plugin &_sdfPlugin, - ignition::plugin::PluginPtr &_gzPlugin) + public: std::list PluginPaths() const { ignition::common::SystemPaths systemPaths; systemPaths.SetPluginPathEnv(pluginPathEnv); @@ -52,9 +51,24 @@ class ignition::gazebo::SystemLoaderPrivate std::string homePath; ignition::common::env(IGN_HOMEDIR, homePath); - systemPaths.AddPluginPaths(homePath + "/.ignition/gazebo/plugins"); + systemPaths.AddPluginPaths(common::joinPaths( + homePath, ".ignition", "gazebo", "plugins")); systemPaths.AddPluginPaths(IGN_GAZEBO_PLUGIN_INSTALL_DIR); + return systemPaths.PluginPaths(); + } + + ////////////////////////////////////////////////// + public: bool InstantiateSystemPlugin(const sdf::Plugin &_sdfPlugin, + ignition::plugin::PluginPtr &_gzPlugin) + { + std::list paths = this->PluginPaths(); + common::SystemPaths systemPaths; + for (const auto &p : paths) + { + systemPaths.AddPluginPaths(p); + } + auto pathToLib = systemPaths.FindSharedLibrary(_sdfPlugin.Filename()); if (pathToLib.empty()) { @@ -85,7 +99,11 @@ class ignition::gazebo::SystemLoaderPrivate return false; } - _gzPlugin = this->loader.Instantiate(_sdfPlugin.Name()); + // use the first plugin name in the library if not specified + std::string pluginToInstantiate = _sdfPlugin.Name().empty() ? + pluginName : _sdfPlugin.Name(); + + _gzPlugin = this->loader.Instantiate(pluginToInstantiate); if (!_gzPlugin) { ignerr << "Failed to load system plugin [" << _sdfPlugin.Name() << @@ -129,6 +147,12 @@ SystemLoader::SystemLoader() ////////////////////////////////////////////////// SystemLoader::~SystemLoader() = default; +////////////////////////////////////////////////// +std::list SystemLoader::PluginPaths() const +{ + return this->dataPtr->PluginPaths(); +} + ////////////////////////////////////////////////// void SystemLoader::AddSystemPluginPath(const std::string &_path) { @@ -141,11 +165,10 @@ std::optional SystemLoader::LoadPlugin( const std::string &_name, const sdf::ElementPtr &_sdf) { - if (_filename == "" || _name == "") + if (_filename == "") { ignerr << "Failed to instantiate system plugin: empty argument " - "[(filename): " << _filename << "] " << - "[(name): " << _name << "]." << std::endl; + "[(filename): " << _filename << "] " << std::endl; return {}; } @@ -175,18 +198,16 @@ std::optional SystemLoader::LoadPlugin( { ignition::plugin::PluginPtr plugin; - if (_plugin.Filename() == "" || _plugin.Name() == "") + if (_plugin.Filename() == "") { ignerr << "Failed to instantiate system plugin: empty argument " - "[(filename): " << _plugin.Filename() << "] " << - "[(name): " << _plugin.Name() << "]." << std::endl; + "[(filename): " << _plugin.Filename() << "] " << std::endl; return {}; } auto ret = this->dataPtr->InstantiateSystemPlugin(_plugin, plugin); if (ret && plugin) return plugin; - return {}; } diff --git a/src/SystemLoader_TEST.cc b/src/SystemLoader_TEST.cc index 8ca53aff47..d1c8b38bd6 100644 --- a/src/SystemLoader_TEST.cc +++ b/src/SystemLoader_TEST.cc @@ -27,6 +27,7 @@ #include "ignition/gazebo/test_config.hh" // NOLINT(build/include) using namespace ignition; +using namespace gazebo; ///////////////////////////////////////////////// TEST(SystemLoader, Constructor) @@ -67,3 +68,38 @@ TEST(SystemLoader, EmptyNames) auto system = sm.LoadPlugin(plugin); ASSERT_FALSE(system.has_value()); } + +///////////////////////////////////////////////// +TEST(SystemLoader, PluginPaths) +{ + SystemLoader sm; + + // verify that there should exist some default paths + std::list paths = sm.PluginPaths(); + unsigned int pathCount = paths.size(); + EXPECT_LT(0u, pathCount); + + // Add test path and verify that the loader now contains this path + auto testBuildPath = common::joinPaths( + std::string(PROJECT_BINARY_PATH), "lib"); + sm.AddSystemPluginPath(testBuildPath); + paths = sm.PluginPaths(); + + // Number of paths should increase by 1 + EXPECT_EQ(pathCount + 1, paths.size()); + + // verify newly added paths exists + bool hasPath = false; + for (const auto &s : paths) + { + // the returned path string may not be exact match due to extra '/' + // appended at the end of the string. So use absPath to generate + // a path string that matches the format returned by joinPaths + if (common::absPath(s) == testBuildPath) + { + hasPath = true; + break; + } + } + EXPECT_TRUE(hasPath); +} diff --git a/src/SystemManager.cc b/src/SystemManager.cc index 8440364fc8..ff3bc2f65b 100644 --- a/src/SystemManager.cc +++ b/src/SystemManager.cc @@ -15,6 +15,11 @@ * */ +#include +#include + +#include + #include "ignition/gazebo/components/SystemPluginInfo.hh" #include "ignition/gazebo/Conversions.hh" #include "SystemManager.hh" @@ -34,11 +39,15 @@ SystemManager::SystemManager(const SystemLoaderPtr &_systemLoader, transport::NodeOptions opts; opts.SetNameSpace(_namespace); this->node = std::make_unique(opts); - std::string entitySystemService{"entity/system/add"}; - this->node->Advertise(entitySystemService, + std::string entitySystemAddService{"entity/system/add"}; + this->node->Advertise(entitySystemAddService, &SystemManager::EntitySystemAddService, this); ignmsg << "Serving entity system service on [" - << "/" << entitySystemService << "]" << std::endl; + << "/" << entitySystemAddService << "]" << std::endl; + + std::string entitySystemInfoService{"system/info"}; + this->node->Advertise(entitySystemInfoService, + &SystemManager::EntitySystemInfoService, this); } ////////////////////////////////////////////////// @@ -215,6 +224,51 @@ bool SystemManager::EntitySystemAddService(const msgs::EntityPlugin_V &_req, return true; } +////////////////////////////////////////////////// +bool SystemManager::EntitySystemInfoService(const msgs::Empty &, + msgs::EntityPlugin_V &_res) +{ + // loop through all files in paths and populate the list of + // plugin libraries. + std::list paths = this->systemLoader->PluginPaths(); + std::set filenames; + for (const auto &p : paths) + { + if (common::exists(p)) + { + for (common::DirIter file(p); + file != common::DirIter(); ++file) + { + std::string current(*file); + std::string filename = common::basename(current); + if (common::isFile(current) && + (common::EndsWith(filename, ".so") || + common::EndsWith(filename, ".dll") || + common::EndsWith(filename, ".dylib"))) + { + // remove extension and lib prefix + size_t extensionIndex = filename.rfind("."); + std::string nameWithoutExtension = + filename.substr(0, extensionIndex); + if (common::StartsWith(nameWithoutExtension, "lib")) + { + nameWithoutExtension = nameWithoutExtension.substr(3); + } + filenames.insert(nameWithoutExtension); + } + } + } + } + + for (const auto &fn : filenames) + { + auto plugin = _res.add_plugins(); + plugin->set_filename(fn); + } + + return true; +} + ////////////////////////////////////////////////// void SystemManager::ProcessPendingEntitySystems() { diff --git a/src/SystemManager.hh b/src/SystemManager.hh index c23ee38cee..a7bf1484ee 100644 --- a/src/SystemManager.hh +++ b/src/SystemManager.hh @@ -142,6 +142,14 @@ namespace ignition private: bool EntitySystemAddService(const msgs::EntityPlugin_V &_req, msgs::Boolean &_res); + /// \brief Callback for entity info system service. + /// \param[in] _req Empty request message + /// \param[out] _res Response containing a list of plugin names + /// and filenames + /// \return True if request received. + private: bool EntitySystemInfoService(const msgs::Empty &_req, + msgs::EntityPlugin_V &_res); + /// \brief All the systems. private: std::vector systems; diff --git a/src/gui/plugins/component_inspector/ComponentInspector.cc b/src/gui/plugins/component_inspector/ComponentInspector.cc index 4185b826fb..0432159a8e 100644 --- a/src/gui/plugins/component_inspector/ComponentInspector.cc +++ b/src/gui/plugins/component_inspector/ComponentInspector.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -69,6 +70,7 @@ #include "ignition/gazebo/components/Volume.hh" #include "ignition/gazebo/components/WindMode.hh" #include "ignition/gazebo/components/World.hh" +#include "ignition/gazebo/config.hh" #include "ignition/gazebo/EntityComponentManager.hh" #include "ignition/gazebo/gui/GuiEvents.hh" @@ -119,9 +121,35 @@ namespace ignition::gazebo /// \brief Handles all system info components. public: std::unique_ptr systemInfo; + + /// \brief A list of system plugin human readable names. + public: QStringList systemNameList; + + /// \brief Maps plugin display names to their filenames. + public: std::unordered_map systemMap; }; } +// Helper to remove a prefix from a string if present +void removePrefix(const std::string &_prefix, std::string &_s) +{ + auto id = _s.find(_prefix); + if (id != std::string::npos) + { + _s = _s.substr(_prefix.length()); + } +} + +// Helper to remove a suffix from a string if present +void removeSuffix(const std::string &_suffix, std::string &_s) +{ + auto id = _s.find(_suffix); + if (id != std::string::npos && id + _suffix.length() == _s.length()) + { + _s.erase(id, _suffix.length()); + } +} + using namespace ignition; using namespace gazebo; @@ -896,7 +924,6 @@ void ComponentInspector::Update(const UpdateInfo &, auto comp = _ecm.Component(this->dataPtr->entity); if (comp) { - this->SetType("material"); setData(item, comp->Data()); } } @@ -1268,6 +1295,106 @@ transport::Node &ComponentInspector::TransportNode() return this->dataPtr->node; } +///////////////////////////////////////////////// +void ComponentInspector::QuerySystems() +{ + msgs::Empty req; + msgs::EntityPlugin_V res; + bool result; + unsigned int timeout = 5000; + std::string service{"/world/" + this->dataPtr->worldName + + "/system/info"}; + if (!this->dataPtr->node.Request(service, req, timeout, res, result)) + { + ignerr << "Unable to query available systems." << std::endl; + return; + } + + this->dataPtr->systemNameList.clear(); + this->dataPtr->systemMap.clear(); + for (const auto &plugin : res.plugins()) + { + if (plugin.filename().empty()) + { + ignerr << "Received empty plugin name. This shouldn't happen." + << std::endl; + continue; + } + + // Remove common prefixes and suffixes + auto humanReadable = plugin.filename(); + removePrefix("ignition-gazebo-", humanReadable); + removePrefix("ignition-gazebo" + + std::string(IGNITION_GAZEBO_MAJOR_VERSION_STR) + "-", humanReadable); + removeSuffix("-system", humanReadable); + removeSuffix("system", humanReadable); + removeSuffix("-plugin", humanReadable); + removeSuffix("plugin", humanReadable); + + // Replace - with space, capitalize + std::replace(humanReadable.begin(), humanReadable.end(), '-', ' '); + humanReadable[0] = std::toupper(humanReadable[0]); + + this->dataPtr->systemMap[humanReadable] = plugin.filename(); + this->dataPtr->systemNameList.push_back( + QString::fromStdString(humanReadable)); + } + this->dataPtr->systemNameList.sort(); + this->dataPtr->systemNameList.removeDuplicates(); + this->SystemNameListChanged(); +} + +///////////////////////////////////////////////// +QStringList ComponentInspector::SystemNameList() const +{ + return this->dataPtr->systemNameList; +} + +///////////////////////////////////////////////// +void ComponentInspector::SetSystemNameList(const QStringList &_list) +{ + this->dataPtr->systemNameList = _list; +} + +///////////////////////////////////////////////// +void ComponentInspector::OnAddSystem(const QString &_name, + const QString &_filename, const QString &_innerxml) +{ + auto filenameStr = _filename.toStdString(); + auto it = this->dataPtr->systemMap.find(filenameStr); + if (it == this->dataPtr->systemMap.end()) + { + ignerr << "Internal error: failed to find [" << filenameStr + << "] in system map." << std::endl; + return; + } + + msgs::EntityPlugin_V req; + auto ent = req.mutable_entity(); + ent->set_id(this->dataPtr->entity); + auto plugin = req.add_plugins(); + std::string name = _name.toStdString(); + std::string filename = this->dataPtr->systemMap[filenameStr]; + std::string innerxml = _innerxml.toStdString(); + plugin->set_name(name); + plugin->set_filename(filename); + plugin->set_innerxml(innerxml); + + msgs::Boolean res; + bool result; + unsigned int timeout = 5000; + std::string service{"/world/" + this->dataPtr->worldName + + "/entity/system/add"}; + if (!this->dataPtr->node.Request(service, req, timeout, res, result)) + { + ignerr << "Error adding new system to entity: " + << this->dataPtr->entity << "\n" + << "Name: " << name << "\n" + << "Filename: " << filename << "\n" + << "Inner XML: " << innerxml << std::endl; + } +} + // Register this plugin IGNITION_ADD_PLUGIN(ignition::gazebo::ComponentInspector, ignition::gui::Plugin) diff --git a/src/gui/plugins/component_inspector/ComponentInspector.hh b/src/gui/plugins/component_inspector/ComponentInspector.hh index a4800795e2..9e8735a928 100644 --- a/src/gui/plugins/component_inspector/ComponentInspector.hh +++ b/src/gui/plugins/component_inspector/ComponentInspector.hh @@ -212,6 +212,14 @@ namespace gazebo NOTIFY NestedModelChanged ) + /// \brief System display name list + Q_PROPERTY( + QStringList systemNameList + READ SystemNameList + WRITE SetSystemNameList + NOTIFY SystemNameListChanged + ) + /// \brief Constructor public: ComponentInspector(); @@ -372,6 +380,28 @@ namespace gazebo /// \return Transport node public: transport::Node &TransportNode(); + /// \brief Query system plugin info. + public: Q_INVOKABLE void QuerySystems(); + + /// \brief Get the system plugin display name list + /// \return A list of names that are potentially system plugins + public: Q_INVOKABLE QStringList SystemNameList() const; + + /// \brief Set the system plugin display name list + /// \param[in] _systempFilenameList A list of system plugin display names + public: Q_INVOKABLE void SetSystemNameList( + const QStringList &_systemNameList); + + /// \brief Notify that system plugin display name list has changed + signals: void SystemNameListChanged(); + + /// \brief Callback when a new system is to be added to an entity + /// \param[in] _name Name of system + /// \param[in] _filename Filename of system + /// \param[in] _innerxml Inner XML content of the system + public: Q_INVOKABLE void OnAddSystem(const QString &_name, + const QString &_filename, const QString &_innerxml); + /// \internal /// \brief Pointer to private data. private: std::unique_ptr dataPtr; diff --git a/src/gui/plugins/component_inspector/ComponentInspector.qml b/src/gui/plugins/component_inspector/ComponentInspector.qml index 21f43e9506..14bbee7153 100644 --- a/src/gui/plugins/component_inspector/ComponentInspector.qml +++ b/src/gui/plugins/component_inspector/ComponentInspector.qml @@ -15,9 +15,9 @@ * */ import QtQuick 2.9 -import QtQuick.Controls 1.4 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 +import QtQuick.Dialogs 1.1 import QtQuick.Layouts 1.3 import QtQuick.Controls.Styles 1.4 import IgnGazebo 1.0 as IgnGazebo @@ -216,6 +216,27 @@ Rectangle { } } + ToolButton { + id: addSystemButton + checkable: false + text: "\u002B" + contentItem: Text { + text: addSystemButton.text + color: "#b5b5b5" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + visible: (entityType == "model" || entityType == "visual" || + entityType == "sensor" || entityType == "world") + ToolTip.text: "Add a system to this entity" + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + onClicked: { + addSystemDialog.open() + } + } + + Label { id: entityLabel text: 'Entity ' + ComponentInspector.entity @@ -227,6 +248,111 @@ Rectangle { } } + Dialog { + id: addSystemDialog + modal: false + focus: true + title: "Add System" + closePolicy: Popup.CloseOnEscape + width: parent.width + + ColumnLayout { + width: parent.width + GridLayout { + columns: 2 + columnSpacing: 30 + Text { + text: "Name" + Layout.row: 0 + Layout.column: 0 + } + + TextField { + id: nameField + selectByMouse: true + Layout.row: 0 + Layout.column: 1 + Layout.fillWidth: true + Layout.minimumWidth: 250 + onTextEdited: { + addSystemDialog.updateButtonState(); + } + placeholderText: "Leave empty to load first plugin" + } + + Text { + text: "Filename" + Layout.row: 1 + Layout.column: 0 + } + + ComboBox { + id: filenameCB + Layout.row: 1 + Layout.column: 1 + Layout.fillWidth: true + Layout.minimumWidth: 250 + model: ComponentInspector.systemNameList + currentIndex: 0 + onCurrentIndexChanged: { + if (currentIndex < 0) + return; + addSystemDialog.updateButtonState(); + } + ToolTip.visible: hovered + ToolTip.delay: tooltipDelay + ToolTip.text: currentText + } + } + + Text { + id: innerxmlLabel + text: "Inner XML" + } + + Flickable { + id: innerxmlFlickable + Layout.minimumHeight: 300 + Layout.fillWidth: true + Layout.fillHeight: true + + flickableDirection: Flickable.VerticalFlick + TextArea.flickable: TextArea { + id: innerxmlField + wrapMode: Text.WordWrap + selectByMouse: true + textFormat: TextEdit.PlainText + font.pointSize: 10 + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + } + } + } + + footer: DialogButtonBox { + id: buttons + standardButtons: Dialog.Ok | Dialog.Cancel + } + + onOpened: { + ComponentInspector.QuerySystems(); + addSystemDialog.updateButtonState(); + } + + onAccepted: { + ComponentInspector.OnAddSystem(nameField.text.trim(), + filenameCB.currentText.trim(), innerxmlField.text.trim()) + } + + function updateButtonState() { + buttons.standardButton(Dialog.Ok).enabled = + (filenameCB.currentText.trim() != '') + } + } + + + ListView { anchors.top: header.bottom anchors.bottom: parent.bottom diff --git a/test/integration/entity_system.cc b/test/integration/entity_system.cc index d4ea7ce57c..338cf4e685 100644 --- a/test/integration/entity_system.cc +++ b/test/integration/entity_system.cc @@ -158,6 +158,54 @@ TEST_P(EntitySystemTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(PublishCmd)) "/model/vehicle/cmd_vel"); } +///////////////////////////////////////////////// +TEST_P(EntitySystemTest, SystemInfo) +{ + // Start server + ServerConfig serverConfig; + const auto sdfFile = std::string(PROJECT_SOURCE_PATH) + + "/test/worlds/empty.sdf"; + serverConfig.SetSdfFile(sdfFile); + + Server server(serverConfig); + EXPECT_FALSE(server.Running()); + EXPECT_FALSE(*server.Running(0)); + + // get info on systems available + msgs::Empty req; + msgs::EntityPlugin_V res; + bool result; + unsigned int timeout = 5000; + std::string service{"/world/empty/system/info"}; + transport::Node node; + EXPECT_TRUE(node.Request(service, req, timeout, res, result)); + EXPECT_TRUE(result); + + // verify plugins are not empty + EXPECT_FALSE(res.plugins().empty()); + + // check for a few known plugins that we know should exist in gazebo + std::set knownPlugins; + knownPlugins.insert("user-commands-system"); + knownPlugins.insert("physics-system"); + knownPlugins.insert("scene-broadcaster-system"); + knownPlugins.insert("sensors-system"); + + for (const auto &plugin : res.plugins()) + { + for (const auto &kp : knownPlugins) + { + if (plugin.filename().find(kp) != std::string::npos) + { + knownPlugins.erase(kp); + break; + } + } + } + // verify all known plugins are found and removed from the set + EXPECT_TRUE(knownPlugins.empty()); +} + // Run multiple times INSTANTIATE_TEST_SUITE_P(ServerRepeat, EntitySystemTest, ::testing::Range(1, 2)); diff --git a/test/worlds/diff_drive_no_plugin.sdf b/test/worlds/diff_drive_no_plugin.sdf index 1792199086..1a70e4eff1 100644 --- a/test/worlds/diff_drive_no_plugin.sdf +++ b/test/worlds/diff_drive_no_plugin.sdf @@ -10,6 +10,10 @@ filename="ignition-gazebo-physics-system" name="ignition::gazebo::systems::Physics"> + + true