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
+