diff --git a/examples/worlds/default.sdf b/examples/worlds/default.sdf index 934f985c81..f359666e1f 100644 --- a/examples/worlds/default.sdf +++ b/examples/worlds/default.sdf @@ -17,18 +17,6 @@ 0.001 1.0 - - - - - - true diff --git a/examples/worlds/shapes.sdf b/examples/worlds/shapes.sdf index 4715d18c7d..5dec565b81 100644 --- a/examples/worlds/shapes.sdf +++ b/examples/worlds/shapes.sdf @@ -8,23 +8,6 @@ Try moving a model: --> - - 0.001 - 1.0 - - - - - - - - 1.0 1.0 1.0 0.8 0.8 0.8 diff --git a/include/ignition/gazebo/CMakeLists.txt b/include/ignition/gazebo/CMakeLists.txt index f6a29455ec..65544b91eb 100644 --- a/include/ignition/gazebo/CMakeLists.txt +++ b/include/ignition/gazebo/CMakeLists.txt @@ -1,3 +1,5 @@ ign_install_all_headers() add_subdirectory(components) + +install (FILES server.config playback_server.config DESTINATION ${IGN_DATA_INSTALL_DIR}) diff --git a/include/ignition/gazebo/ServerConfig.hh b/include/ignition/gazebo/ServerConfig.hh index 6a1de2c4dd..abb0283cde 100644 --- a/include/ignition/gazebo/ServerConfig.hh +++ b/include/ignition/gazebo/ServerConfig.hh @@ -351,6 +351,24 @@ namespace ignition /// \param[in] _info Information about the plugin to load. public: void AddPlugin(const PluginInfo &_info); + /// \brief Add multiple plugins to the simulation + /// \param[in] _info List of Information about the plugin to load. + public: void AddPlugins(const std::list &_plugins); + + /// \brief Generate PluginInfo for Log recording based on the + /// internal state of this ServerConfig object: + /// \sa UseLogRecord + /// \sa LogRecordPath + /// \sa LogRecordResources + /// \sa LogRecordCompressPath + /// \sa LogRecordTopics + public: PluginInfo LogRecordPlugin() const; + + /// \brief Generate PluginInfo for Log playback based on the + /// internal state of this ServerConfig object: + /// \sa LogPlaybackPath + public: PluginInfo LogPlaybackPlugin() const; + /// \brief Get all the plugins that should be loaded. /// \return A list of all the plugins specified via /// AddPlugin(const PluginInfo &). @@ -372,6 +390,45 @@ namespace ignition /// \brief Private data pointer private: std::unique_ptr dataPtr; }; + + /// \brief Parse plugins from XML configuration file. + /// \param[in] _fname Absolute path to the configuration file to parse. + /// \return A list of all of the plugins found in the configuration file + std::list + IGNITION_GAZEBO_VISIBLE + parsePluginsFromFile(const std::string &_fname); + + /// \brief Parse plugins from XML configuration string. + /// \param[in] _str XML configuration content to parse + /// \return A list of all of the plugins found in the configuration string. + std::list + IGNITION_GAZEBO_VISIBLE + parsePluginsFromString(const std::string &_str); + + /// \brief Load plugin information, following ordering. + /// + /// This method is used when no plugins are found in an SDF + /// file to load either a default or custom set of plugins. + /// + /// The following order is used to resolve: + /// 1. Config file located at IGN_GAZEBO_SERVER_CONFIG_PATH environment + /// variable. + /// * If IGN_GAZEBO_SERVER_CONFIG_PATH is set but empty, no plugins + /// are loaded. + /// 2. File at ${IGN_HOMEDIR}/.ignition/gazebo/server.config + /// 3. File at ${IGN_DATA_INSTALL_DIR}/server.config + /// + /// If any of the above files exist but are empty, resolution + /// stops and the plugin list will be empty. + /// + // + /// \param[in] _isPlayback Is the server in playback mode. If so, fallback + /// to playback_server.config. + // + /// \return A list of plugins to load, based on above ordering + std::list + IGNITION_GAZEBO_VISIBLE + loadPluginInfo(bool _isPlayback = false); } } } diff --git a/include/ignition/gazebo/Util.hh b/include/ignition/gazebo/Util.hh index 39d790f7f9..ca058f3a08 100644 --- a/include/ignition/gazebo/Util.hh +++ b/include/ignition/gazebo/Util.hh @@ -146,6 +146,9 @@ namespace ignition /// `` const std::string kSdfPathEnv{"SDF_PATH"}; + /// \breif Environment variable holding server config paths. + const std::string kServerConfigPathEnv{"IGN_GAZEBO_SERVER_CONFIG_PATH"}; + /// \brief Environment variable holding paths to custom rendering engine /// plugins. const std::string kRenderPluginPathEnv{"IGN_GAZEBO_RENDER_ENGINE_PATH"}; diff --git a/include/ignition/gazebo/config.hh.in b/include/ignition/gazebo/config.hh.in index fb16fddef7..050a0304a4 100644 --- a/include/ignition/gazebo/config.hh.in +++ b/include/ignition/gazebo/config.hh.in @@ -15,6 +15,7 @@ #define IGNITION_GAZEBO_GUI_CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/${IGN_DATA_INSTALL_DIR}/gui" #define IGNITION_GAZEBO_SYSTEM_CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/${IGN_DATA_INSTALL_DIR}/systems" +#define IGNITION_GAZEBO_SERVER_CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/${IGN_DATA_INSTALL_DIR}" #define IGN_GAZEBO_PLUGIN_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${IGN_LIB_INSTALL_DIR}/ign-${IGN_DESIGNATION}-${PROJECT_VERSION_MAJOR}/plugins" #define IGN_GAZEBO_GUI_PLUGIN_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${IGN_LIB_INSTALL_DIR}/ign-${IGN_DESIGNATION}-${PROJECT_VERSION_MAJOR}/plugins/gui" #define IGN_GAZEBO_WORLD_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${IGN_DATA_INSTALL_DIR}/worlds" diff --git a/include/ignition/gazebo/playback_server.config b/include/ignition/gazebo/playback_server.config new file mode 100644 index 0000000000..2551fda3ea --- /dev/null +++ b/include/ignition/gazebo/playback_server.config @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/include/ignition/gazebo/server.config b/include/ignition/gazebo/server.config new file mode 100644 index 0000000000..5de18a66fc --- /dev/null +++ b/include/ignition/gazebo/server.config @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1c029e2900..078c1b4bcf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -75,6 +75,7 @@ set (gtest_sources SdfEntityCreator_TEST.cc SdfGenerator_TEST.cc Server_TEST.cc + ServerConfig_TEST.cc SimulationRunner_TEST.cc System_TEST.cc SystemLoader_TEST.cc diff --git a/src/Server.cc b/src/Server.cc index 9fb9492de8..bb188ef993 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -59,53 +59,14 @@ std::string findFuelResourceSdf(const std::string &_path) /// \brief This struct provides access to the default world. struct DefaultWorld { - /// \brief Get the default plugins as a string. - /// \return An SDF string that contains the default plugins. - public: static std::string &DefaultPlugins(const ServerConfig &_config) - { - std::vector pluginsV = { - { - std::string("" - }, - { - std::string("" - } - }; - - // The set of default gazebo plugins. - if (_config.LogPlaybackPath().empty()) - { - pluginsV.push_back(std::string(""); - } - - // Playback plugin - else - { - pluginsV.push_back(std::string("" + - _config.LogPlaybackPath() + ""); - } - - static std::string plugins = std::accumulate(pluginsV.begin(), - pluginsV.end(), std::string("")); - return plugins; - } - /// \brief Get the default world as a string. + /// Plugins will be loaded from the server.config file. /// \return An SDF string that contains the default world. - public: static std::string &World(const ServerConfig &_config) + public: static std::string &World() { static std::string world = std::string("" "" "") + - DefaultPlugins(_config) + "" ""; @@ -211,7 +172,7 @@ Server::Server(const ServerConfig &_config) ignmsg << "Loading default world.\n"; // Load an empty world. /// \todo(nkoenig) Add a "AddWorld" function to sdf::Root. - errors = this->dataPtr->sdfRoot.LoadSdfString(DefaultWorld::World(_config)); + errors = this->dataPtr->sdfRoot.LoadSdfString(DefaultWorld::World()); } if (!errors.empty()) diff --git a/src/ServerConfig.cc b/src/ServerConfig.cc index fbb61135ca..97b738c4ea 100644 --- a/src/ServerConfig.cc +++ b/src/ServerConfig.cc @@ -14,13 +14,18 @@ * limitations under the License. * */ +#include "ignition/gazebo/ServerConfig.hh" + +#include + #include #include #include #include #include #include -#include "ignition/gazebo/ServerConfig.hh" + +#include "ignition/gazebo/Util.hh" using namespace ignition; using namespace gazebo; @@ -230,7 +235,8 @@ class ignition::gazebo::ServerConfigPrivate plugins(_cfg->plugins), networkRole(_cfg->networkRole), networkSecondaries(_cfg->networkSecondaries), - seed(_cfg->seed) { } + seed(_cfg->seed), + logRecordTopics(_cfg->logRecordTopics) { } // \brief The SDF file that the server should load public: std::string sdfFile = ""; @@ -550,6 +556,104 @@ void ServerConfig::AddPlugin(const ServerConfig::PluginInfo &_info) this->dataPtr->plugins.push_back(_info); } +///////////////////////////////////////////////// +ServerConfig::PluginInfo +ServerConfig::LogPlaybackPlugin() const +{ + auto entityName = "*"; + auto entityType = "world"; + auto pluginName = "ignition::gazebo::systems::LogPlayback"; + auto pluginFilename = "ignition-gazebo-log-system"; + + sdf::ElementPtr playbackElem; + playbackElem = std::make_shared(); + playbackElem->SetName("plugin"); + + if (!this->LogPlaybackPath().empty()) + { + sdf::ElementPtr pathElem = std::make_shared(); + pathElem->SetName("path"); + playbackElem->AddElementDescription(pathElem); + pathElem = playbackElem->GetElement("path"); + pathElem->AddValue("string", "", false, ""); + pathElem->Set(this->LogPlaybackPath()); + } + + return ServerConfig::PluginInfo(entityName, + entityType, + pluginFilename, + pluginName, + playbackElem); +} + +///////////////////////////////////////////////// +ServerConfig::PluginInfo +ServerConfig::LogRecordPlugin() const +{ + auto entityName = "*"; + auto entityType = "world"; + auto pluginName = "ignition::gazebo::systems::LogRecord"; + auto pluginFilename = std::string("ignition-gazebo") + + IGNITION_GAZEBO_MAJOR_VERSION_STR + "-log-system"; + + sdf::ElementPtr recordElem; + + recordElem = std::make_shared(); + recordElem->SetName("plugin"); + + if (!this->LogRecordPath().empty()) + { + sdf::ElementPtr pathElem = std::make_shared(); + pathElem->SetName("path"); + recordElem->AddElementDescription(pathElem); + pathElem = recordElem->GetElement("path"); + pathElem->AddValue("string", "", false, ""); + pathElem->Set(this->LogRecordPath()); + } + + // Set whether to record resources + sdf::ElementPtr resourceElem = std::make_shared(); + resourceElem->SetName("record_resources"); + recordElem->AddElementDescription(resourceElem); + resourceElem = recordElem->GetElement("record_resources"); + resourceElem->AddValue("bool", "false", false, ""); + resourceElem->Set(this->LogRecordResources() ? true : false); + + // Set whether to compress + sdf::ElementPtr compressElem = std::make_shared(); + compressElem->SetName("compress"); + recordElem->AddElementDescription(compressElem); + compressElem = recordElem->GetElement("compress"); + compressElem->AddValue("bool", "false", false, ""); + compressElem->Set(this->LogRecordCompressPath().empty() ? false : + true); + + // Set compress path + sdf::ElementPtr cPathElem = std::make_shared(); + cPathElem->SetName("compress_path"); + recordElem->AddElementDescription(cPathElem); + cPathElem = recordElem->GetElement("compress_path"); + cPathElem->AddValue("string", "", false, ""); + cPathElem->Set(this->LogRecordCompressPath()); + + // If record topics specified, add in SDF + for (const std::string &topic : this->LogRecordTopics()) + { + sdf::ElementPtr topicElem = std::make_shared(); + topicElem->SetName("record_topic"); + recordElem->AddElementDescription(topicElem); + topicElem = recordElem->AddElement("record_topic"); + topicElem->AddValue("string", "false", false, ""); + topicElem->Set(topic); + } + + return ServerConfig::PluginInfo(entityName, + entityType, + pluginFilename, + pluginName, + recordElem); +} + ///////////////////////////////////////////////// const std::list &ServerConfig::Plugins() const { @@ -587,3 +691,230 @@ const std::vector &ServerConfig::LogRecordTopics() const { return this->dataPtr->logRecordTopics; } + +///////////////////////////////////////////////// +void copyElement(sdf::ElementPtr _sdf, const tinyxml2::XMLElement *_xml) +{ + _sdf->SetName(_xml->Value()); + if (_xml->GetText() != nullptr) + _sdf->AddValue("string", _xml->GetText(), "1"); + + for (const tinyxml2::XMLAttribute *attribute = _xml->FirstAttribute(); + attribute; attribute = attribute->Next()) + { + _sdf->AddAttribute(attribute->Name(), "string", "", 1, ""); + _sdf->GetAttribute(attribute->Name())->SetFromString( + attribute->Value()); + } + + // Iterate over all the child elements + const tinyxml2::XMLElement *elemXml = nullptr; + for (elemXml = _xml->FirstChildElement(); elemXml; + elemXml = elemXml->NextSiblingElement()) + { + sdf::ElementPtr element(new sdf::Element); + element->SetParent(_sdf); + + copyElement(element, elemXml); + _sdf->InsertElement(element); + } +} + +///////////////////////////////////////////////// +std::list +parsePluginsFromDoc(const tinyxml2::XMLDocument &_doc) +{ + auto ret = std::list(); + auto root = _doc.RootElement(); + if (root == nullptr) + { + ignerr << "No element found when parsing plugins\n"; + return ret; + } + + auto plugins = root->FirstChildElement("plugins"); + if (plugins == nullptr) + { + ignerr << "No element found when parsing plugins\n"; + return ret; + } + + const tinyxml2::XMLElement *elem{nullptr}; + + // Note, this was taken from ign-launch, where this type of parsing happens. + // Process all the plugins. + for (elem = plugins->FirstChildElement("plugin"); elem; + elem = elem->NextSiblingElement("plugin")) + { + // Get the plugin's name + const char *nameStr = elem->Attribute("name"); + std::string name = nameStr == nullptr ? "" : nameStr; + if (name.empty()) + { + ignerr << "Plugin is missing the name attribute. " + << "Skipping this plugin.\n"; + continue; + } + + // Get the plugin's filename + const char *fileStr = elem->Attribute("filename"); + std::string file = fileStr == nullptr ? "" : fileStr; + if (file.empty()) + { + ignerr << "A Plugin with name[" << name << "] is " + << "missing the filename attribute. Skipping this plugin.\n"; + continue; + } + + // Get the plugin's entity name attachment information. + const char *entityNameStr = elem->Attribute("entity_name"); + std::string entityName = entityNameStr == nullptr ? "" : entityNameStr; + if (entityName.empty()) + { + ignerr << "A Plugin with name[" << name << "] and " + << "filename[" << file << "] is missing the entity_name attribute. " + << "Skipping this plugin.\n"; + continue; + } + + // Get the plugin's entity type attachment information. + const char *entityTypeStr = elem->Attribute("entity_type"); + std::string entityType = entityTypeStr == nullptr ? "" : entityTypeStr; + if (entityType.empty()) + { + ignerr << "A Plugin with name[" << name << "] and " + << "filename[" << file << "] is missing the entity_type attribute. " + << "Skipping this plugin.\n"; + continue; + } + + // Create an SDF element of the plugin + sdf::ElementPtr sdf(new sdf::Element); + copyElement(sdf, elem); + + // Add the plugin to the server config + ret.push_back({entityName, entityType, file, name, sdf}); + } + return ret; +} + +///////////////////////////////////////////////// +std::list +ignition::gazebo::parsePluginsFromFile(const std::string &_fname) +{ + tinyxml2::XMLDocument doc; + doc.LoadFile(_fname.c_str()); + return parsePluginsFromDoc(doc); +} + +///////////////////////////////////////////////// +std::list +ignition::gazebo::parsePluginsFromString(const std::string &_str) +{ + tinyxml2::XMLDocument doc; + doc.Parse(_str.c_str()); + return parsePluginsFromDoc(doc); +} + + +///////////////////////////////////////////////// +std::list +ignition::gazebo::loadPluginInfo(bool _isPlayback) +{ + std::list ret; + + // 1. Check contents of environment variable + std::string envConfig; + bool configSet = ignition::common::env(gazebo::kServerConfigPathEnv, + envConfig); + + if (configSet) + { + if (ignition::common::exists(envConfig)) + { + // Parse configuration stored in environment variable + ret = ignition::gazebo::parsePluginsFromFile(envConfig); + if (ret.empty()) + { + // This may be desired behavior, but warn just in case. + // Some users may want to defer all loading until later + // during runtime. + ignwarn << gazebo::kServerConfigPathEnv + << " set but no plugins found\n"; + } + igndbg << "Loaded (" << ret.size() << ") plugins from file " << + "[" << envConfig << "]\n"; + + return ret; + } + else + { + // This may be desired behavior, but warn just in case. + // Some users may want to defer all loading until late + // during runtime. + ignwarn << gazebo::kServerConfigPathEnv + << " set but no file found," + << " no plugins loaded\n"; + return ret; + } + } + + std::string configFilename; + if (_isPlayback) + { + configFilename = "playback_server.config"; + } + else + { + configFilename = "server.config"; + } + + std::string defaultConfig; + ignition::common::env(IGN_HOMEDIR, defaultConfig); + defaultConfig = ignition::common::joinPaths(defaultConfig, ".ignition", + "gazebo", configFilename); + + if (!ignition::common::exists(defaultConfig)) + { + auto installedConfig = ignition::common::joinPaths( + IGNITION_GAZEBO_SERVER_CONFIG_PATH, + configFilename); + + if (!ignition::common::exists(installedConfig)) + { + ignerr << "Failed to copy installed config [" << installedConfig + << "] to default config [" << defaultConfig << "]." + << "(file " << installedConfig << " doesn't exist)" + << std::endl; + return ret; + } + else if (!ignition::common::copyFile(installedConfig, defaultConfig)) + { + ignerr << "Failed to copy installed config [" << installedConfig + << "] to default config [" << defaultConfig << "]." + << std::endl; + return ret; + } + else + { + ignmsg << "Copied installed config [" << installedConfig + << "] to default config [" << defaultConfig << "]." + << std::endl; + } + } + + ret = ignition::gazebo::parsePluginsFromFile(defaultConfig); + + if (ret.empty()) + { + // This may be desired behavior, but warn just in case. + ignwarn << "Loaded config: [" << defaultConfig + << "], but no plugins found\n"; + } + + igndbg << "Loaded (" << ret.size() << ") plugins from file " << + "[" << defaultConfig << "]\n"; + + return ret; +} + diff --git a/src/ServerConfig_TEST.cc b/src/ServerConfig_TEST.cc new file mode 100644 index 0000000000..733a55ef37 --- /dev/null +++ b/src/ServerConfig_TEST.cc @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2020 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include +#include +#include +#include + +using namespace ignition; +using namespace gazebo; + +////////////////////////////////////////////////// +TEST(ParsePluginsFromString, Valid) +{ + std::string config = R"( + + + + 0.123 + + + 987 + + + 456 + + + )"; + + auto plugins = parsePluginsFromString(config); + ASSERT_EQ(3u, plugins.size()); + + auto plugin = plugins.begin(); + + EXPECT_EQ("default", plugin->EntityName()); + EXPECT_EQ("world", plugin->EntityType()); + EXPECT_EQ("TestWorldSystem", plugin->Filename()); + EXPECT_EQ("ignition::gazebo::TestWorldSystem", plugin->Name()); + + plugin = std::next(plugin, 1); + + EXPECT_EQ("box", plugin->EntityName()); + EXPECT_EQ("model", plugin->EntityType()); + EXPECT_EQ("TestModelSystem", plugin->Filename()); + EXPECT_EQ("ignition::gazebo::TestModelSystem", plugin->Name()); + + plugin = std::next(plugin, 1); + + EXPECT_EQ("default::box::link_1::camera", plugin->EntityName()); + EXPECT_EQ("sensor", plugin->EntityType()); + EXPECT_EQ("TestSensorSystem", plugin->Filename()); + EXPECT_EQ("ignition::gazebo::TestSensorSystem", plugin->Name()); +} + +////////////////////////////////////////////////// +TEST(ParsePluginsFromString, Invalid) +{ + std::string config = R"( + + + 0.123 + + )"; + + auto plugins = parsePluginsFromString(config); + ASSERT_EQ(0u, plugins.size()); + + auto plugins2 = parsePluginsFromString(""); + ASSERT_EQ(0u, plugins2.size()); +} + +////////////////////////////////////////////////// +TEST(ParsePluginsFromFile, Valid) +{ + auto config = common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "server_valid.config"); + + auto plugins = parsePluginsFromFile(config); + ASSERT_EQ(3u, plugins.size()); + + auto plugin = plugins.begin(); + + EXPECT_EQ("default", plugin->EntityName()); + EXPECT_EQ("world", plugin->EntityType()); + EXPECT_EQ("TestWorldSystem", plugin->Filename()); + EXPECT_EQ("ignition::gazebo::TestWorldSystem", plugin->Name()); + + plugin = std::next(plugin, 1); + + EXPECT_EQ("box", plugin->EntityName()); + EXPECT_EQ("model", plugin->EntityType()); + EXPECT_EQ("TestModelSystem", plugin->Filename()); + EXPECT_EQ("ignition::gazebo::TestModelSystem", plugin->Name()); + + plugin = std::next(plugin, 1); + + EXPECT_EQ("default::box::link_1::camera", plugin->EntityName()); + EXPECT_EQ("sensor", plugin->EntityType()); + EXPECT_EQ("TestSensorSystem", plugin->Filename()); + EXPECT_EQ("ignition::gazebo::TestSensorSystem", plugin->Name()); +} + +////////////////////////////////////////////////// +TEST(ParsePluginsFromFile, Invalid) +{ + auto config = common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "server_invalid.config"); + + // Valid file without valid content + auto plugins = parsePluginsFromFile(config); + ASSERT_EQ(0u, plugins.size()); + + // Invalid file + auto plugins2 = parsePluginsFromFile("/foo/bar/baz"); + ASSERT_EQ(0u, plugins2.size()); +} + +////////////////////////////////////////////////// +TEST(ParsePluginsFromFile, DefaultConfig) +{ + // Note: This test validates that that the default + // configuration always parses. + // If more systems are added, then the number needs + // to be adjusted below. + auto config = common::joinPaths(PROJECT_SOURCE_PATH, + "include", "ignition", "gazebo", "server.config"); + + auto plugins = parsePluginsFromFile(config); + ASSERT_EQ(3u, plugins.size()); +} + +////////////////////////////////////////////////// +TEST(ParsePluginsFromFile, PlaybackConfig) +{ + // Note: This test validates that that the default + // configuration always parses. + // If more systems are added, then the number needs + // to be adjusted below. + auto config = common::joinPaths(PROJECT_SOURCE_PATH, + "include", "ignition", "gazebo", "playback_server.config"); + + auto plugins = parsePluginsFromFile(config); + ASSERT_EQ(2u, plugins.size()); +} + +////////////////////////////////////////////////// +TEST(LoadPluginInfo, FromEmptyEnv) +{ + // Set environment to something that doesn't exist + ASSERT_TRUE(common::setenv(gazebo::kServerConfigPathEnv, "foo")); + auto plugins = loadPluginInfo(); + + EXPECT_EQ(0u, plugins.size()); + EXPECT_TRUE(common::unsetenv(gazebo::kServerConfigPathEnv)); +} + +////////////////////////////////////////////////// +TEST(LoadPluginInfo, FromValidEnv) +{ + auto validPath = common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "server_valid2.config"); + + ASSERT_TRUE(common::setenv(gazebo::kServerConfigPathEnv, validPath)); + + auto plugins = loadPluginInfo(); + ASSERT_EQ(2u, plugins.size()); + + auto plugin = plugins.begin(); + + EXPECT_EQ("*", plugin->EntityName()); + EXPECT_EQ("world", plugin->EntityType()); + EXPECT_EQ("TestWorldSystem", plugin->Filename()); + EXPECT_EQ("ignition::gazebo::TestWorldSystem", plugin->Name()); + + plugin = std::next(plugin, 1); + + EXPECT_EQ("box", plugin->EntityName()); + EXPECT_EQ("model", plugin->EntityType()); + EXPECT_EQ("TestModelSystem", plugin->Filename()); + EXPECT_EQ("ignition::gazebo::TestModelSystem", plugin->Name()); + + EXPECT_TRUE(common::unsetenv(gazebo::kServerConfigPathEnv)); +} + +////////////////////////////////////////////////// +TEST(ServerConfig, GenerateRecordPlugin) +{ + ServerConfig config; + config.SetUseLogRecord(true); + config.SetLogRecordPath("foo/bar"); + config.SetLogRecordResources(true); + + auto plugin = config.LogRecordPlugin(); + EXPECT_EQ(plugin.EntityName(), "*"); + EXPECT_EQ(plugin.EntityType(), "world"); + EXPECT_EQ(plugin.Name(), "ignition::gazebo::systems::LogRecord"); +} + diff --git a/src/ServerPrivate.cc b/src/ServerPrivate.cc index 4b15c260fa..7792222754 100644 --- a/src/ServerPrivate.cc +++ b/src/ServerPrivate.cc @@ -183,204 +183,168 @@ bool ServerPrivate::Run(const uint64_t _iterations, } ////////////////////////////////////////////////// -void ServerPrivate::AddRecordPlugin(const ServerConfig &_config) +sdf::ElementPtr GetRecordPluginElem(sdf::Root &_sdfRoot) { - const sdf::World *sdfWorld = this->sdfRoot.WorldByIndex(0); - sdf::ElementPtr worldElem = sdfWorld->Element(); + sdf::ElementPtr rootElem = _sdfRoot.Element(); - // Check if there is already a record plugin specified - if (worldElem->HasElement("plugin")) + if (rootElem->HasElement("world")) { - sdf::ElementPtr pluginElem = worldElem->GetElement("plugin"); - while (pluginElem != nullptr) + sdf::ElementPtr worldElem = rootElem->GetElement("world"); + + if (worldElem->HasElement("plugin")) { - sdf::ParamPtr pluginName = pluginElem->GetAttribute("name"); - sdf::ParamPtr pluginFileName = pluginElem->GetAttribute("filename"); + sdf::ElementPtr pluginElem = worldElem->GetElement("plugin"); - if (pluginName != nullptr && pluginFileName != nullptr) + while (pluginElem != nullptr) { - // Found a logging plugin - if (pluginFileName->GetAsString().find( - LoggingPlugin::LoggingPluginSuffix()) != std::string::npos) + sdf::ParamPtr pluginName = pluginElem->GetAttribute("name"); + sdf::ParamPtr pluginFileName = pluginElem->GetAttribute("filename"); + + if (pluginName != nullptr && pluginFileName != nullptr) { - // If record plugin already specified in SDF, and record flags are - // specified on command line, replace SDF parameters with those on - // command line. (If none specified on command line, use those in - // SDF.) - if (pluginName->GetAsString() == LoggingPlugin::RecordPluginName()) + // Found a logging plugin + if (pluginFileName->GetAsString().find( + LoggingPlugin::LoggingPluginSuffix()) != std::string::npos) { - std::string recordPath = _config.LogRecordPath(); - std::string cmpPath = _config.LogRecordCompressPath(); - - // Set record path - if (!_config.LogRecordPath().empty()) + if (pluginName->GetAsString() == LoggingPlugin::RecordPluginName()) { - bool overwriteSdf = false; - // If is specified in SDF, check whether to replace it - if (pluginElem->HasElement("path") && - !pluginElem->Get("path").empty()) - { - // If record path came from command line, overwrite SDF - if (_config.LogIgnoreSdfPath()) - { - overwriteSdf = true; - } - // TODO(anyone) In Ignition-D, remove this. will be - // permanently ignored in favor of common::ignLogDirectory(). - // Always overwrite SDF. - // Otherwise, record path is same as the default timestamp log - // path. Take the path in SDF . - // Deprecated. - else - { - ignwarn << "--record-path is not specified on command line. " - << " is specified in SDF. Will record to . " - << "Console will be logged to [" << ignLogDirectory() - << "]. Note: In Ignition-D, will be ignored, and " - << "all recordings will be written to default console log " - << "path if no path is specified on command line.\n"; - overwriteSdf = false; - - // Take in SDF - recordPath = pluginElem->Get("path"); - - // Update path for compressed file to match record path - cmpPath = std::string(recordPath); - if (!std::string(1, cmpPath.back()).compare( - ignition::common::separator(""))) - { - // Remove the separator at end of path - cmpPath = cmpPath.substr(0, cmpPath.length() - 1); - } - cmpPath += ".zip"; - } - } - else - { - overwriteSdf = true; - } - - if (overwriteSdf) - { - sdf::ElementPtr pathElem = std::make_shared(); - pathElem->SetName("path"); - pluginElem->AddElementDescription(pathElem); - pathElem = pluginElem->GetElement("path"); - pathElem->AddValue("string", "", false, ""); - pathElem->Set(recordPath); - } + return pluginElem; } + } + } - // If resource flag specified on command line, replace in SDF - if (_config.LogRecordResources()) - { - sdf::ElementPtr resourceElem = std::make_shared(); - resourceElem->SetName("record_resources"); - pluginElem->AddElementDescription(resourceElem); - resourceElem = pluginElem->GetElement("record_resources"); - resourceElem->AddValue("bool", "false", false, ""); - resourceElem->Set(_config.LogRecordResources() - ? true : false); - } + pluginElem = pluginElem->GetNextElement(); + } + } + } + return nullptr; +} - // If compress flag specified on command line, replace in SDF - if (!_config.LogRecordCompressPath().empty()) - { - sdf::ElementPtr compressElem = std::make_shared(); - compressElem->SetName("compress"); - pluginElem->AddElementDescription(compressElem); - compressElem = pluginElem->GetElement("compress"); - compressElem->AddValue("bool", "false", false, ""); - compressElem->Set(true); - - sdf::ElementPtr cPathElem = std::make_shared(); - cPathElem->SetName("compress_path"); - pluginElem->AddElementDescription(cPathElem); - cPathElem = pluginElem->GetElement("compress_path"); - cPathElem->AddValue("string", "", false, ""); - cPathElem->Set(cmpPath); - } +////////////////////////////////////////////////// +void ServerPrivate::AddRecordPlugin(const ServerConfig &_config) +{ + auto recordPluginElem = GetRecordPluginElem(this->sdfRoot); + bool sdfUseLogRecord = (recordPluginElem != nullptr); + + bool hasRecordPath {false}; + bool hasCompressPath {false}; + bool hasRecordResources {false}; + bool hasCompress {false}; + bool hasRecordTopics {false}; + + std::string sdfRecordPath; + std::string sdfCompressPath; + bool sdfRecordResources; + bool sdfCompress; + std::vector sdfRecordTopics; + + if (sdfUseLogRecord) + { + std::tie(sdfRecordPath, hasRecordPath) = + recordPluginElem->Get("path", ""); + std::tie(sdfCompressPath, hasCompressPath) = + recordPluginElem->Get("compress_path", ""); + std::tie(sdfRecordResources, hasRecordResources) = + recordPluginElem->Get("record_resources", false); + std::tie(sdfCompress, hasCompress) = + recordPluginElem->Get("compress", false); + + hasRecordTopics = recordPluginElem->HasElement("record_topic"); + if (hasRecordTopics) + { + sdf::ElementPtr recordTopicElem = + recordPluginElem->GetElement("record_topic"); + while (recordTopicElem) + { + auto topic = recordTopicElem->Get(); + sdfRecordTopics.push_back(topic); + } - // If record topics specified, add in SDF - for (const std::string &topic : _config.LogRecordTopics()) - { - sdf::ElementPtr topicElem = std::make_shared(); - topicElem->SetName("record_topic"); - pluginElem->AddElementDescription(topicElem); - topicElem = pluginElem->AddElement("record_topic"); - topicElem->AddValue("string", "false", false, ""); - topicElem->Set(topic); - } + recordTopicElem = recordTopicElem->GetNextElement(); + } - return; - } + // Remove from SDF + recordPluginElem->RemoveFromParent(); + recordPluginElem->Reset(); + } - // If playback plugin also specified, do not add a record plugin - if (pluginName->GetAsString() == LoggingPlugin::PlaybackPluginName()) + // Set the config based on what is in the SDF: + if (hasRecordPath) + this->config.SetLogRecordPath(sdfRecordPath); + if (hasCompressPath) + this->config.SetLogRecordCompressPath(sdfCompressPath); + if (hasRecordResources) + this->config.SetLogRecordResources(sdfRecordResources); + + if (hasRecordTopics) + { + this->config.ClearLogRecordTopics(); + for (auto topic : sdfRecordTopics) + { + this->config.AddLogRecordTopic(topic); + } + } + + if (!_config.LogRecordPath().empty() && hasRecordPath) + { + if (hasRecordPath) + { + // If record path came from command line, overwrite SDF + if (_config.LogIgnoreSdfPath()) + { + this->config.SetLogRecordPath(_config.LogRecordPath()); + } + // TODO(anyone) In Ignition-D, remove this. will be + // permanently ignored in favor of common::ignLogDirectory(). + // Always overwrite SDF. + // Otherwise, record path is same as the default timestamp log + // path. Take the path in SDF . + // Deprecated. + else + { + ignwarn << "--record-path is not specified on command line. " + << " is specified in SDF. Will record to . " + << "Console will be logged to [" << ignLogDirectory() + << "]. Note: In Ignition-D, will be ignored, and " + << "all recordings will be written to default console log " + << "path if no path is specified on command line.\n"; + + // In the case that the --compress flag is set, then + // this field will be populated with just the file extension + if(_config.LogRecordCompressPath() == ".zip") + { + sdfCompressPath = std::string(sdfRecordPath); + if (!std::string(1, sdfCompressPath.back()).compare( + ignition::common::separator(""))) { - ignwarn << "Both record and playback are specified. " - << "Ignoring record.\n"; - return; + // Remove the separator at end of path + sdfCompressPath = sdfCompressPath.substr(0, + sdfCompressPath.length() - 1); } + sdfCompressPath += ".zip"; + this->config.SetLogRecordCompressPath(sdfCompressPath); } } - - pluginElem = pluginElem->GetNextElement(); + } + else + { + this->config.SetLogRecordPath(_config.LogRecordPath()); } } - // A record plugin is not already specified in SDF. Add one. - sdf::ElementPtr recordElem = worldElem->AddElement("plugin"); - sdf::ParamPtr recordName = recordElem->GetAttribute("name"); - recordName->SetFromString(LoggingPlugin::RecordPluginName()); - sdf::ParamPtr recordFileName = recordElem->GetAttribute("filename"); - recordFileName->SetFromString(LoggingPlugin::LoggingPluginFileName()); + if (_config.LogRecordResources()) + this->config.SetLogRecordResources(true); - // Add custom record path - if (!_config.LogRecordPath().empty()) - { - sdf::ElementPtr pathElem = std::make_shared(); - pathElem->SetName("path"); - recordElem->AddElementDescription(pathElem); - pathElem = recordElem->GetElement("path"); - pathElem->AddValue("string", "", false, ""); - pathElem->Set(_config.LogRecordPath()); - } + if (_config.LogRecordCompressPath() != ".zip") + this->config.SetLogRecordCompressPath(_config.LogRecordCompressPath()); - // Set whether to record resources - sdf::ElementPtr resourceElem = std::make_shared(); - resourceElem->SetName("record_resources"); - recordElem->AddElementDescription(resourceElem); - resourceElem = recordElem->GetElement("record_resources"); - resourceElem->AddValue("bool", "false", false, ""); - resourceElem->Set(_config.LogRecordResources() ? true : false); - - // Set whether to compress - sdf::ElementPtr compressElem = std::make_shared(); - compressElem->SetName("compress"); - recordElem->AddElementDescription(compressElem); - compressElem = recordElem->GetElement("compress"); - compressElem->AddValue("bool", "false", false, ""); - compressElem->Set(_config.LogRecordCompressPath().empty() ? false : - true); - - // Set compress path - sdf::ElementPtr cPathElem = std::make_shared(); - cPathElem->SetName("compress_path"); - recordElem->AddElementDescription(cPathElem); - cPathElem = recordElem->GetElement("compress_path"); - cPathElem->AddValue("string", "", false, ""); - cPathElem->Set(_config.LogRecordCompressPath()); - - // If record topics specified, add in SDF - for (const std::string &topic : _config.LogRecordTopics()) + if (_config.LogRecordTopics().size()) { - sdf::ElementPtr topicElem = std::make_shared(); - topicElem->SetName("record_topic"); - recordElem->AddElementDescription(topicElem); - topicElem = recordElem->AddElement("record_topic"); - topicElem->AddValue("string", "false", false, ""); - topicElem->Set(topic); + this->config.ClearLogRecordTopics(); + for (auto topic : _config.LogRecordTopics()) + { + this->config.AddLogRecordTopic(topic); + } } } diff --git a/src/Server_TEST.cc b/src/Server_TEST.cc index b057678e28..d5d9c37f45 100644 --- a/src/Server_TEST.cc +++ b/src/Server_TEST.cc @@ -34,6 +34,7 @@ #include "ignition/gazebo/SystemLoader.hh" #include "ignition/gazebo/Server.hh" #include "ignition/gazebo/Types.hh" +#include "ignition/gazebo/Util.hh" #include "ignition/gazebo/test_config.hh" #include "plugins/MockSystem.hh" @@ -220,8 +221,8 @@ TEST_P(ServerFixture, ServerConfigSensorPlugin) { // Start server ServerConfig serverConfig; - serverConfig.SetSdfFile(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/air_pressure.sdf"); + serverConfig.SetSdfFile(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "air_pressure.sdf")); sdf::ElementPtr sdf(new sdf::Element); sdf->SetName("plugin"); @@ -229,7 +230,8 @@ TEST_P(ServerFixture, ServerConfigSensorPlugin) "ignition::gazebo::TestSensorSystem", true); sdf->AddAttribute("filename", "string", "libTestSensorSystem.so", true); - serverConfig.AddPlugin({"air_pressure_model::link::air_pressure_sensor", + serverConfig.AddPlugin({ + "air_pressure_sensor::air_pressure_model::link::air_pressure_sensor", "sensor", "libTestSensorSystem.so", "ignition::gazebo::TestSensorSystem", sdf}); @@ -238,6 +240,7 @@ TEST_P(ServerFixture, ServerConfigSensorPlugin) // The simulation runner should not be running. EXPECT_FALSE(*server.Running(0)); + EXPECT_EQ(2u, *server.SystemCount()); // Run the server igndbg << "Run server" << std::endl; @@ -316,6 +319,7 @@ TEST_P(ServerFixture, ServerConfigLogRecord) serverConfig.SetLogRecordPath(logPath); gazebo::Server server(serverConfig); + EXPECT_EQ(0u, *server.IterationCount()); EXPECT_EQ(3u, *server.EntityCount()); EXPECT_EQ(4u, *server.SystemCount()); diff --git a/src/SimulationRunner.cc b/src/SimulationRunner.cc index 1984408f12..79e4a1f34e 100644 --- a/src/SimulationRunner.cc +++ b/src/SimulationRunner.cc @@ -19,8 +19,9 @@ #include -#include "ignition/common/Profiler.hh" +#include +#include "ignition/common/Profiler.hh" #include "ignition/gazebo/components/Model.hh" #include "ignition/gazebo/components/Name.hh" #include "ignition/gazebo/components/Sensor.hh" @@ -29,6 +30,7 @@ #include "ignition/gazebo/Events.hh" #include "ignition/gazebo/SdfEntityCreator.hh" #include "ignition/gazebo/Util.hh" + #include "network/NetworkManagerPrimary.hh" #include "SdfGenerator.hh" @@ -155,6 +157,21 @@ SimulationRunner::SimulationRunner(const sdf::World *_world, // Load the active levels this->levelMgr->UpdateLevelsState(); + // Load any additional plugins from the Server Configuration + this->LoadServerPlugins(this->serverConfig.Plugins()); + + // If we have reached this point and no systems have been loaded, then load + // a default set of systems. + if (this->systems.empty() && this->pendingSystems.empty()) + { + ignmsg << "No systems loaded from SDF, loading defaults" << std::endl; + bool isPlayback = !this->serverConfig.LogPlaybackPath().empty(); + auto plugins = ignition::gazebo::loadPluginInfo(isPlayback); + this->LoadServerPlugins(plugins); + } + + this->LoadLoggingPlugins(this->serverConfig); + // World control transport::NodeOptions opts; if (this->networkMgr) @@ -732,50 +749,46 @@ void SimulationRunner::Step(const UpdateInfo &_info) } ////////////////////////////////////////////////// -void SimulationRunner::LoadPlugins(const Entity _entity, - const sdf::ElementPtr &_sdf) +void SimulationRunner::LoadPlugin(const Entity _entity, + const std::string &_fname, + const std::string &_name, + const sdf::ElementPtr &_sdf) { - sdf::ElementPtr pluginElem = _sdf->GetElement("plugin"); - while (pluginElem) + std::optional system; { - // No error message for the 'else' case of the following 'if' statement - // because SDF create a default element even if it's not - // specified. An error message would result in spamming - // the console. \todo(nkoenig) Fix SDF should so that elements are not - // automatically added. - if (pluginElem->Get("filename") != "__default__" && - pluginElem->Get("name") != "__default__") + std::lock_guard lock(this->systemLoaderMutex); + system = this->systemLoader->LoadPlugin(_fname, _name, _sdf); + } + + // System correctly loaded from library, try to configure + if (system) + { + auto systemConfig = system.value()->QueryInterface(); + if (systemConfig != nullptr) { - std::optional system; - { - std::lock_guard lock(this->systemLoaderMutex); - system = this->systemLoader->LoadPlugin(pluginElem); - } - if (system) - { - auto systemConfig = system.value()->QueryInterface(); - if (systemConfig != nullptr) - { - systemConfig->Configure(_entity, pluginElem, - this->entityCompMgr, - this->eventMgr); - } - this->AddSystem(system.value()); - igndbg << "Loaded system [" << pluginElem->Get("name") - << "] for entity [" << _entity << "]" << std::endl; - } + systemConfig->Configure(_entity, _sdf, + this->entityCompMgr, + this->eventMgr); } - pluginElem = pluginElem->GetNextElement("plugin"); + this->AddSystem(system.value()); + igndbg << "Loaded system [" << _name + << "] for entity [" << _entity << "]" << std::endl; } +} +////////////////////////////////////////////////// +void SimulationRunner::LoadServerPlugins( + const std::list &_plugins) +{ // \todo(nkoenig) Remove plugins from the server config after they have // been added. We might not want to do this if we want to support adding // the same plugin to multiple entities, for example via a regex // expression. // // Check plugins from the ServerConfig for matching entities. - for (const ServerConfig::PluginInfo &plugin : this->serverConfig.Plugins()) + + for (const ServerConfig::PluginInfo &plugin : _plugins) { // \todo(anyone) Type + name is not enough to uniquely identify an entity // \todo(louise) The runner shouldn't care about specific components, this @@ -789,8 +802,16 @@ void SimulationRunner::LoadPlugins(const Entity _entity, } else if ("world" == plugin.EntityType()) { - entity = this->entityCompMgr.EntityByComponents( - components::Name(plugin.EntityName()), components::World()); + // Allow wildcard for world name + if (plugin.EntityName() == "*") + { + entity = this->entityCompMgr.EntityByComponents(components::World()); + } + else + { + entity = this->entityCompMgr.EntityByComponents( + components::Name(plugin.EntityName()), components::World()); + } } else if ("sensor" == plugin.EntityType()) { @@ -830,29 +851,59 @@ void SimulationRunner::LoadPlugins(const Entity _entity, << plugin.EntityType() << "]" << std::endl; } - // Skip plugins that do not match the provided entity - if (entity != _entity) - continue; - std::optional system; + if (kNullEntity != entity) { - std::lock_guard lock(this->systemLoaderMutex); - system = this->systemLoader->LoadPlugin(plugin.Filename(), plugin.Name(), - nullptr); + this->LoadPlugin(entity, plugin.Filename(), plugin.Name(), plugin.Sdf()); } + } +} + +////////////////////////////////////////////////// +void SimulationRunner::LoadLoggingPlugins(const ServerConfig &_config) +{ + std::list plugins; + + if(_config.UseLogRecord() && !_config.LogPlaybackPath().empty()) + { + ignwarn << + "Both recording and playback are specified, defaulting to playback\n"; + } + + if(!_config.LogPlaybackPath().empty()) + { + auto playbackPlugin = _config.LogPlaybackPlugin(); + plugins.push_back(playbackPlugin); + } + else if(_config.UseLogRecord()) + { + auto recordPlugin = _config.LogRecordPlugin(); + plugins.push_back(recordPlugin); + } + + this->LoadServerPlugins(plugins); +} - if (system) +////////////////////////////////////////////////// +void SimulationRunner::LoadPlugins(const Entity _entity, + const sdf::ElementPtr &_sdf) +{ + sdf::ElementPtr pluginElem = _sdf->GetElement("plugin"); + while (pluginElem) + { + auto filename = pluginElem->Get("filename"); + auto name = pluginElem->Get("name"); + // No error message for the 'else' case of the following 'if' statement + // because SDF create a default element even if it's not + // specified. An error message would result in spamming + // the console. \todo(nkoenig) Fix SDF should so that elements are not + // automatically added. + if (filename != "__default__" && name != "__default__") { - auto systemConfig = system.value()->QueryInterface(); - if (systemConfig != nullptr) - { - systemConfig->Configure(_entity, plugin.Sdf(), this->entityCompMgr, - this->eventMgr); - } - this->AddSystem(system.value()); - igndbg << "Loaded system [" << plugin.Name() - << "] for entity [" << _entity << "]" << std::endl; + this->LoadPlugin(_entity, filename, name, pluginElem); } + + pluginElem = pluginElem->GetNextElement("plugin"); } } diff --git a/src/SimulationRunner.hh b/src/SimulationRunner.hh index 775487f4fa..faf8e3518b 100644 --- a/src/SimulationRunner.hh +++ b/src/SimulationRunner.hh @@ -170,12 +170,32 @@ namespace ignition /// \brief Publish current world statistics. public: void PublishStats(); + /// \brief Load system plugin for a given entity. + /// \param[in] _entity Entity + /// \param[in] _fname Filename of the plugin library + /// \param[in] _name Name of the plugin + /// \param[in] _sdf SDF element (content of plugin tag) + public: void LoadPlugin(const Entity _entity, + const std::string &_fname, + const std::string &_name, + const sdf::ElementPtr &_sdf); + /// \brief Load system plugins for a given entity. /// \param[in] _entity Entity /// \param[in] _sdf SDF element public: void LoadPlugins(const Entity _entity, const sdf::ElementPtr &_sdf); + /// \brief Load server plugins for a given entity. + /// \param[in] _config Configuration to load plugins from. + /// plugins based on the _config contents + public: void LoadServerPlugins( + const std::list &_plugins); + + /// \brief Load logging/playback plugins + /// \param[in] _config Configuration to load plugins from. + public: void LoadLoggingPlugins(const ServerConfig &_config); + /// \brief Get whether this is running. When running is true, /// then simulation is stepping forward. /// \return True if the server is running. diff --git a/src/SimulationRunner_TEST.cc b/src/SimulationRunner_TEST.cc index 52795298b0..64b75f6c1f 100644 --- a/src/SimulationRunner_TEST.cc +++ b/src/SimulationRunner_TEST.cc @@ -16,6 +16,8 @@ */ #include +#include + #include #include #include @@ -26,6 +28,7 @@ #include #include + #include "ignition/gazebo/test_config.hh" #include "ignition/gazebo/components/CanonicalLink.hh" #include "ignition/gazebo/components/ChildLinkName.hh" @@ -48,6 +51,7 @@ #include "ignition/gazebo/components/Wind.hh" #include "ignition/gazebo/components/World.hh" #include "ignition/gazebo/Events.hh" +#include "ignition/gazebo/Util.hh" #include "ignition/gazebo/config.hh" #include "SimulationRunner.hh" @@ -81,8 +85,8 @@ class SimulationRunnerTest : public ::testing::TestWithParam { common::Console::SetVerbosity(4); - setenv("IGN_GAZEBO_SYSTEM_PLUGIN_PATH", - (std::string(PROJECT_BINARY_PATH) + "/lib").c_str(), 1); + common::setenv("IGN_GAZEBO_SYSTEM_PLUGIN_PATH", + common::joinPaths(PROJECT_BINARY_PATH, "lib")); } }; @@ -107,8 +111,8 @@ TEST_P(SimulationRunnerTest, CreateEntities) { // Load SDF file sdf::Root root; - root.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/shapes.sdf"); + root.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "shapes.sdf")); ASSERT_EQ(1u, root.WorldCount()); @@ -520,8 +524,8 @@ TEST_P(SimulationRunnerTest, CreateLights) { // Load SDF file sdf::Root root; - root.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/lights.sdf"); + root.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "lights.sdf")); ASSERT_EQ(1u, root.WorldCount()); @@ -790,8 +794,8 @@ TEST_P(SimulationRunnerTest, CreateJointEntities) { // Load SDF file sdf::Root root; - root.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/demo_joint_types.sdf"); + root.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "demo_joint_types.sdf")); ASSERT_EQ(1u, root.WorldCount()); @@ -931,8 +935,8 @@ TEST_P(SimulationRunnerTest, Time) { // Load SDF file sdf::Root root; - root.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/shapes.sdf"); + root.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "shapes.sdf")); ASSERT_EQ(1u, root.WorldCount()); @@ -1053,8 +1057,8 @@ TEST_P(SimulationRunnerTest, LoadPlugins) { // Load SDF file sdf::Root root; - root.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/plugins.sdf"); + root.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "plugins.sdf")); ASSERT_EQ(1u, root.WorldCount()); @@ -1135,13 +1139,156 @@ TEST_P(SimulationRunnerTest, LoadPlugins) #endif } +///////////////////////////////////////////////// +TEST_P(SimulationRunnerTest, LoadServerNoPlugins) +{ + sdf::Root rootWithout; + rootWithout.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "plugins_empty.sdf")); + ASSERT_EQ(1u, rootWithout.WorldCount()); + + // ServerConfig will fall back to environment variable + auto config = common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "server_valid2.config"); + ASSERT_EQ(true, common::setenv(gazebo::kServerConfigPathEnv, config)); + ServerConfig serverConfig; + + // Create simulation runner + auto systemLoader = std::make_shared(); + SimulationRunner runner(rootWithout.WorldByIndex(0), systemLoader, + serverConfig); + + ASSERT_EQ(2u, runner.SystemCount()); +} + +///////////////////////////////////////////////// +TEST_P(SimulationRunnerTest, LoadServerConfigPlugins) +{ + sdf::Root rootWithout; + rootWithout.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "plugins_empty.sdf")); + ASSERT_EQ(1u, rootWithout.WorldCount()); + + // Create a server configuration with plugins + // No fallback expected + auto config = common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "server_valid.config"); + + auto plugins = parsePluginsFromFile(config); + ASSERT_EQ(3u, plugins.size()); + + ServerConfig serverConfig; + for (auto plugin : plugins) + { + serverConfig.AddPlugin(plugin); + } + + // Create simulation runner + auto systemLoader = std::make_shared(); + SimulationRunner runner(rootWithout.WorldByIndex(0), systemLoader, + serverConfig); + + // Get world entity + Entity worldId{kNullEntity}; + runner.EntityCompMgr().Each([&]( + const ignition::gazebo::Entity &_entity, + const ignition::gazebo::components::World *_world)->bool + { + EXPECT_NE(nullptr, _world); + worldId = _entity; + return true; + }); + EXPECT_NE(kNullEntity, worldId); + + // Get model entity + Entity modelId{kNullEntity}; + runner.EntityCompMgr().Each([&]( + const ignition::gazebo::Entity &_entity, + const ignition::gazebo::components::Model *_model)->bool + { + EXPECT_NE(nullptr, _model); + modelId = _entity; + return true; + }); + EXPECT_NE(kNullEntity, modelId); + + // Get sensor entity + Entity sensorId{kNullEntity}; + runner.EntityCompMgr().Each([&]( + const ignition::gazebo::Entity &_entity, + const ignition::gazebo::components::Sensor *_sensor)->bool + { + EXPECT_NE(nullptr, _sensor); + sensorId = _entity; + return true; + }); + EXPECT_NE(kNullEntity, sensorId); + + // Check component registered by world plugin + std::string worldComponentName{"WorldPluginComponent"}; + auto worldComponentId = ignition::common::hash64(worldComponentName); + + EXPECT_TRUE(runner.EntityCompMgr().HasComponentType(worldComponentId)); + EXPECT_TRUE(runner.EntityCompMgr().EntityHasComponentType(worldId, + worldComponentId)); + + // Check component registered by model plugin + std::string modelComponentName{"ModelPluginComponent"}; + auto modelComponentId = ignition::common::hash64(modelComponentName); + + EXPECT_TRUE(runner.EntityCompMgr().HasComponentType(modelComponentId)); + EXPECT_TRUE(runner.EntityCompMgr().EntityHasComponentType(modelId, + modelComponentId)); + + // Check component registered by sensor plugin + std::string sensorComponentName{"SensorPluginComponent"}; + auto sensorComponentId = ignition::common::hash64(sensorComponentName); + + EXPECT_TRUE(runner.EntityCompMgr().HasComponentType(sensorComponentId)); + EXPECT_TRUE(runner.EntityCompMgr().EntityHasComponentType(sensorId, + sensorComponentId)); + + // Clang re-registers components between tests. If we don't unregister them + // beforehand, the new plugin tries to create a storage type from a previous + // plugin, causing a crash. + // Is this only a problem with GTest, or also during simulation? How to + // reproduce? Maybe we need to test unloading plugins, but we have no API for + // it yet. + #if defined (__clang__) + components::Factory::Instance()->Unregister(worldComponentId); + components::Factory::Instance()->Unregister(modelComponentId); + components::Factory::Instance()->Unregister(sensorComponentId); + #endif +} + +///////////////////////////////////////////////// +TEST_P(SimulationRunnerTest, LoadPluginsDefault) +{ + sdf::Root rootWithout; + rootWithout.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "plugins_empty.sdf")); + ASSERT_EQ(1u, rootWithout.WorldCount()); + + // Load the default config, but not through the default code path. + // The user may have modified their local config. + auto config = common::joinPaths(PROJECT_SOURCE_PATH, + "include", "ignition", "gazebo", "server.config"); + ASSERT_TRUE(common::setenv(gazebo::kServerConfigPathEnv, config)); + + // Create simulation runner + auto systemLoader = std::make_shared(); + SimulationRunner runner(rootWithout.WorldByIndex(0), systemLoader); + ASSERT_EQ(3u, runner.SystemCount()); + common::unsetenv(gazebo::kServerConfigPathEnv); +} + ///////////////////////////////////////////////// TEST_P(SimulationRunnerTest, LoadPluginsEvent) { // Load SDF file without plugins sdf::Root rootWithout; - rootWithout.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/shapes.sdf"); + rootWithout.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "shapes.sdf")); ASSERT_EQ(1u, rootWithout.WorldCount()); // Create simulation runner @@ -1175,8 +1322,8 @@ TEST_P(SimulationRunnerTest, LoadPluginsEvent) // Load SDF file with plugins sdf::Root rootWith; - rootWith.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/plugins.sdf"); + rootWith.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "plugins.sdf")); ASSERT_EQ(1u, rootWith.WorldCount()); // Emit plugin loading event @@ -1233,8 +1380,8 @@ TEST_P(SimulationRunnerTest, GuiInfo) { // Load SDF file sdf::Root root; - root.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/shapes.sdf"); + root.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "shapes.sdf")); ASSERT_EQ(1u, root.WorldCount()); @@ -1270,8 +1417,8 @@ TEST_P(SimulationRunnerTest, GenerateWorldSdf) { // Load SDF file sdf::Root root; - root.Load(std::string(PROJECT_SOURCE_PATH) + - "/test/worlds/shapes.sdf"); + root.Load(common::joinPaths(PROJECT_SOURCE_PATH, + "test", "worlds", "shapes.sdf")); ASSERT_EQ(1u, root.WorldCount()); diff --git a/src/cmd/cmdgazebo.rb.in b/src/cmd/cmdgazebo.rb.in index 594e360763..0baa713665 100755 --- a/src/cmd/cmdgazebo.rb.in +++ b/src/cmd/cmdgazebo.rb.in @@ -156,6 +156,7 @@ COMMANDS = { 'gazebo' => " resources such as worlds and models. \n\n"\ " IGN_GAZEBO_SYSTEM_PLUGIN_PATH Colon separated paths used to \n"\ " locate system plugins. \n\n"\ + " IGN_GAZEBO_SERVER_CONFIG_PATH Path to server configuration file. \n\n"\ " IGN_GUI_PLUGIN_PATH Colon separated paths used to locate GUI \n"\ " plugins. \n\n"\ " IGN_GAZEBO_NETWORK_ROLE Participant role used in a distributed \n"\ diff --git a/src/systems/log/LogRecord.cc b/src/systems/log/LogRecord.cc index ccdcf7b8ce..9cc417088f 100644 --- a/src/systems/log/LogRecord.cc +++ b/src/systems/log/LogRecord.cc @@ -57,6 +57,8 @@ #include "ignition/gazebo/components/Pose.hh" #include "ignition/gazebo/components/SourceFilePath.hh" #include "ignition/gazebo/components/Visual.hh" +#include "ignition/gazebo/components/World.hh" + #include "ignition/gazebo/Util.hh" using namespace ignition; @@ -279,16 +281,6 @@ bool LogRecordPrivate::Start(const std::string &_logPath, common::createDirectories(this->logPath); } - // Go up to root of SDF, to record entire SDF file - sdf::ElementPtr sdfRoot = this->sdf->GetParent(); - while (sdfRoot->GetParent() != nullptr) - { - sdfRoot = sdfRoot->GetParent(); - } - - // Construct message with SDF string - this->sdfMsg.set_data(sdfRoot->ToString("")); - // Use directory basename as topic name, to be able to retrieve at playback std::string sdfTopic = "/" + common::basename(this->logPath) + "/sdf"; this->sdfPub = this->node.Advertise(sdfTopic, this->sdfMsg.GetTypeName()); @@ -306,9 +298,6 @@ bool LogRecordPrivate::Start(const std::string &_logPath, } ignmsg << "Recording to log file [" << dbPath << "]" << std::endl; - // Use ign-transport directly - sdf::ElementPtr sdfWorld = sdfRoot->GetElement("world"); - // Add default topics if no topics were specified. std::string dynPoseTopic = "/world/" + this->worldName + "/dynamic_pose/info"; @@ -665,8 +654,28 @@ void LogRecord::PostUpdate(const UpdateInfo &_info, // Publish only once if (!this->dataPtr->sdfPublished) { - this->dataPtr->sdfPub.Publish(this->dataPtr->sdfMsg); - this->dataPtr->sdfPublished = true; + // Construct message with SDF string + auto worldEntity = _ecm.EntityByComponents(components::World()); + if (worldEntity == kNullEntity) + { + ignerr << "Could not find the world entity\n"; + } + else + { + auto worldSdfComp = _ecm.Component(worldEntity); + if (worldSdfComp == nullptr || worldSdfComp->Data().Element() == nullptr) + { + ignerr << "Could not load world SDF data\n"; + } + else + { + this->dataPtr->sdfMsg.set_data( + worldSdfComp->Data().Element()->ToString("")); + + this->dataPtr->sdfPub.Publish(this->dataPtr->sdfMsg); + this->dataPtr->sdfPublished = true; + } + } } // TODO(louise) Use the SceneBroadcaster's topic once that publishes diff --git a/test/worlds/plugins_empty.sdf b/test/worlds/plugins_empty.sdf new file mode 100644 index 0000000000..ac6128380c --- /dev/null +++ b/test/worlds/plugins_empty.sdf @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/test/worlds/server_invalid.config b/test/worlds/server_invalid.config new file mode 100644 index 0000000000..d0371fc23f --- /dev/null +++ b/test/worlds/server_invalid.config @@ -0,0 +1,13 @@ + + + + 0.123 + + + diff --git a/test/worlds/server_valid.config b/test/worlds/server_valid.config new file mode 100644 index 0000000000..7c55928937 --- /dev/null +++ b/test/worlds/server_valid.config @@ -0,0 +1,28 @@ + + + + + 0.123 + + + 987 + + + 456 + + + diff --git a/test/worlds/server_valid2.config b/test/worlds/server_valid2.config new file mode 100644 index 0000000000..e52be4382b --- /dev/null +++ b/test/worlds/server_valid2.config @@ -0,0 +1,21 @@ + + + + + 0.123 + + + 987 + + + diff --git a/test/worlds/shapes.sdf b/test/worlds/shapes.sdf index 7c4ebffeb3..ac254fa80b 100644 --- a/test/worlds/shapes.sdf +++ b/test/worlds/shapes.sdf @@ -5,18 +5,6 @@ 0.001 1.0 - - - - - - diff --git a/tutorials.md.in b/tutorials.md.in index a7f1c5be81..6930c6c746 100644 --- a/tutorials.md.in +++ b/tutorials.md.in @@ -16,6 +16,7 @@ Ignition @IGN_DESIGNATION_CAP@ library and how to use the library effectively. * \subpage physics "Physics engines": Loading different physics engines. * \subpage battery "Battery": Keep track of battery charge on robot models. * \subpage gui_config "GUI configuration": Customizing your layout. +* \subpage server_config "Server configuration": Customizing what system plugins are loaded. * \subpage debugging "Debugging": Information about debugging Gazebo. * \subpage pointcloud "Converting a Point Cloud to a 3D Model": Turn point cloud data into 3D models for use in simulations. * \subpage meshtofuel "Importing a Mesh to Fuel": Build a model directory around a mesh so it can be added to the Ignition Fuel app. diff --git a/tutorials/files/server_config/camera_env.gif b/tutorials/files/server_config/camera_env.gif new file mode 100644 index 0000000000..409b0c2b5c Binary files /dev/null and b/tutorials/files/server_config/camera_env.gif differ diff --git a/tutorials/files/server_config/camera_no_env.gif b/tutorials/files/server_config/camera_no_env.gif new file mode 100644 index 0000000000..d7a1fda7f2 Binary files /dev/null and b/tutorials/files/server_config/camera_no_env.gif differ diff --git a/tutorials/files/server_config/default_server.gif b/tutorials/files/server_config/default_server.gif new file mode 100644 index 0000000000..0230eb529a Binary files /dev/null and b/tutorials/files/server_config/default_server.gif differ diff --git a/tutorials/files/server_config/from_sdf.png b/tutorials/files/server_config/from_sdf.png new file mode 100644 index 0000000000..391ebe9240 Binary files /dev/null and b/tutorials/files/server_config/from_sdf.png differ diff --git a/tutorials/files/server_config/from_sdf_no_plugins.gif b/tutorials/files/server_config/from_sdf_no_plugins.gif new file mode 100644 index 0000000000..1e631bdb5a Binary files /dev/null and b/tutorials/files/server_config/from_sdf_no_plugins.gif differ diff --git a/tutorials/files/server_config/modified_default_config.gif b/tutorials/files/server_config/modified_default_config.gif new file mode 100644 index 0000000000..da620c130e Binary files /dev/null and b/tutorials/files/server_config/modified_default_config.gif differ diff --git a/tutorials/server_config.md b/tutorials/server_config.md new file mode 100644 index 0000000000..42fdec843d --- /dev/null +++ b/tutorials/server_config.md @@ -0,0 +1,265 @@ +\page server_config Server Configuration + +Most functionality on Ignition Gazebo is provided by plugins, which means that +users can choose exactly what functionality is available to their simulations. +Even running the physics engine is optional. This gives users great control +and makes sure only what's crucial for a given simulation is loaded. + +This tutorial will go over how to specify what system plugins to be loaded for +a simulation. + +## How to load plugins + +There are a few places where the plugins can be defined: + +1. `` elements inside an SDF file. +2. File path defined by the `IGN_GAZEBO_SERVER_CONFIG_PATH` environment variable. +3. The default configuration file at `$HOME/.ignition/gazebo/server.config` \* + +Each of the items above takes precedence over the ones below it. For example, +if a the SDF file has any `` elements, then the +`IGN_GAZEBO_SERVER_CONFIG_PATH` variable is ignored. And the default configuration +file is only loaded if no plugins are passed through the SDF file or the +environment variable. + +> \* For log-playback, the default file is +> `$HOME/.ignition/gazebo/playback_gui.config` + +## Try it out + +### Default configuration + +Let's try this in practice. First, let's open Ignition Gazebo without passing +any arguments: + +`ign gazebo` + +You should see an empty world with several systems loaded by default, such as +physics, the scene broadcaster (which keeps the GUI updated), and the system that +handles user commands like translating models. Try for example inserting a simple +shape into simulation and pressing "play": + +* the shape is inserted correctly because the user commands system is loaded; +* the shape falls due to gravity because the physics system is loaded; +* and you can see the shape falling through the GUI because the scene +broadcaster is loaded. + +@image html files/server_config/default_server.gif + +By default, you're loading this file: + +`$HOME/.ignition/gazebo/server.config` + +That file is created the first time you load Ignition Gazebo. Once it is +created, Ignition will never write to it again unless you delete it. This +means that you can customize it with your preferences and they will be applied +every time Ignition is started! + +Let's try customizing it: + +1. Open this file with your favorite editor: + + `$HOME/.ignition/gazebo/server.config` + +2. Remove the `` block for the physics system + +3. Reload Gazebo: + + `ign gazebo` + +Now insert a shape and press play: it shouldn't fall because physics wasn't +loaded. + +@image html files/server_config/modified_default_config.gif + +You'll often want to restore default settings or to use the latest default +provided by Ignition (when you update to a newer version for example). In +that case, just delete that file, and the next time Gazebo is started a new file +will be created with default values: + +`rm $HOME/.ignition/gazebo/server.config` + +### SDF + +Let's try overriding the default configuration from an SDF file. Open your +favorite editor and save this file as `fuel_preview.sdf`: + +``` + + + + + + + + + + + + 3D View + false + docked + + + ogre2 + scene + 1.0 1.0 1.0 + 0.4 0.6 1.0 + 8.3 7 7.8 0 0.5 -2.4 + + + + + + https://fuel.ignitionrobotics.org/1.0/OpenRobotics/models/Sun + + + + https://fuel.ignitionrobotics.org/1.0/OpenRobotics/models/Construction Cone + + + + +``` + +Now let's load this world: + +`ign gazebo -r /fuel_preview.sdf` + +Notice how the application has only one system plugin loaded, the scene +broadcaster, as defined on the SDF file above. Physics is not loaded, so even +though the simulation is running (started with `-r`), the cone doesn't fall +with gravity. + +@image html files/server_config/from_sdf.png + +If you delete the `` element from the file above and reload it, you'll +see the same model loaded with the default plugins, so it will fall. + +@image html files/server_config/from_sdf_no_plugins.gif + +### Environment variable + +It's often inconvenient to embed your plugins directly into every SDF file. +But you also don't want to be editing the default config file every time you +want to start with a different set of plugins. That's why Gazebo also supports +loading configuration files from an environment variable. + +Let's start by saving this simple world with a camera sensor as +`simple_camera.sdf`: + +``` + + + + + + + + + + 3D View + false + docked + + + ogre2 + scene + 1.0 1.0 1.0 + 0.4 0.6 1.0 + 8.3 7 7.8 0 0.5 -2.4 + + + + + floating + + + + + + + https://fuel.ignitionrobotics.org/1.0/OpenRobotics/models/Sun + + + + 0 0 1 0 0 0 + https://fuel.ignitionrobotics.org/1.0/OpenRobotics/models/Gazebo + + + + true + 20 0 1.0 0 0.0 3.14 + + 0.05 0.05 0.05 0 0 0 + + + + 0.1 0.1 0.1 + + + + + + 1.047 + + 320 + 240 + + + 30 + + + + + + +``` + +Then load the `simple_camera.sdf` world: + +`ign gazebo -r /simple_camera.sdf` + +You'll see a world with a camera and a cone. If you refresh the image display +plugin, it won't show any image topics. That's because the default server +configuration doesn't include the sensors system, which is necessary for +rendering-based sensors to generate data. + +@image html files/server_config/camera_no_env.gif + +Now let's create a custom configuration file in +`$HOME/.ignition/gazebo/rendering_sensors_server.config` that has the sensors +system: + +``` + + + + + + ogre + + + +``` + +And point the environment variable to that file: + +`export IGN_GAZEBO_SERVER_CONFIG_PATH=$HOME/.ignition/gazebo/rendering_sensors_server.config` + +Now when we launch the simulation again, refreshing the image display will +show the camera topic, and we can see the camera data. +One interesting thing to notice is that on the camera view, there's no grid and +the background color is the default grey, instead of the blue color set on the +GUI `GzScene` plugin. + +@image html files/server_config/camera_env.gif +