diff --git a/doc/cmd_yarprobotinterface.dox b/doc/cmd_yarprobotinterface.dox
index 7737d436a3d..26436d96a4a 100644
--- a/doc/cmd_yarprobotinterface.dox
+++ b/doc/cmd_yarprobotinterface.dox
@@ -1,5 +1,5 @@
/**
-\page yarprobotinterface yarprobotinterface: start all the devices required by a robot
+\page yarprobotinterface yarprobotinterface: Start multiple YARP devices as specified in an xml file.
\ingroup yarp_tools
@@ -7,4 +7,37 @@
\section yarprobotinterface_intro Description
+The yarprobotinterface is a command line tool that is useful to launch multiple YARP device at once.
+
+Its name derives from the fact that the first and main use of the yarprobotinterface was used as the
+main program to provide a network "interface", via YARP Network Server Wrappers (NWS) devices, to a robot.
+
+However, the yarprobotinterface can be used to launch YARP devices of any kind. In a sense, is an extension of the
+yarpdev command, that instead can be used only to launch one or two devices, while yarprobotinterface can launch an
+arbitrary number of YARP devices.
+
+The details of the xml format of the files loaded by yarprobotinterface are documented in \ref yarp_robotinterface_xml_config_files .
+
+\section yarprobotinterface_parameters Parameters
+
+`--config ./configdir/config.xml`
+- Specify the path of the `.xml` file to load and that
+ describes the YARP devices to launch.
+
+`--portprefix portprefix`
+- If specified, this values override the portprefix attribute
+ of the robot element of the xml file.
+
+`--verbose`
+- If this option is specified, enable verbose output of the xml parser.
+
+`--dryrun`
+- If this option is specified, then xml file is only loaded without actually opening devices.
+ This option is useful to validate if xml files are well formed.
+
+\section yarprobotinterface_conf_file Configuration Files
+
+yarprobotinterface loads the xml file from the location specified in the `--config` option.
+
+
*/
diff --git a/doc/release/master/robotinterface_portprefix.md b/doc/release/master/robotinterface_portprefix.md
new file mode 100644
index 00000000000..ffca61eac84
--- /dev/null
+++ b/doc/release/master/robotinterface_portprefix.md
@@ -0,0 +1,5 @@
+robotinterface_portprefix {#master}
+--------------
+
+* Modify libYARP_robotinterface and yarprobotinterface to support
+ passing using ${portprefix} in parameters (#2819).
diff --git a/doc/robotinterface_all.dox b/doc/robotinterface_all.dox
new file mode 100644
index 00000000000..968c75a0bf0
--- /dev/null
+++ b/doc/robotinterface_all.dox
@@ -0,0 +1,49 @@
+/**
+\defgroup robointerface_all yarp::robotinterface YARP RobotInterface library
+
+The `libYARP_robotinterface` library is useful to programatically launch YARP devices
+from C++ code using the same xml files used with the \ref yarprobotinterface "yarprobotinterface tool",
+that are described in \ref yarp_robotinterface_xml_config_files .
+
+An example of use of this library is:
+\code
+// Load the XML configuration file
+std::string pathToXmlConfigurationFile = ...;
+yarp::robotinterface::XMLReader reader;
+yarp::robotinterface::XMLReaderResult result = reader.getRobotFromFile(pathToXmlConfigurationFile);
+
+if (!result.isParsingSuccessful) {
+ // Handle error
+ // ...
+}
+
+// Optional: specify externally created devices to which robotinterface's devices can attach
+// It is assumed that the devices contained in externalDriverList will remain valid and open until
+// result.robot.enterPhase(yarp::robotinterface::ActionPhaseShutdown) will be called
+// bool ok = result.robot.setExternalDevices(externalDriverList);
+
+// Enter the startup phase, that will open all the devices and call attach if necessary
+bool ok = result.robot.enterPhase(yarp::robotinterface::ActionPhaseStartup);
+
+if (!ok) {
+ // Handle error
+ // ...
+}
+
+
+// At this point, the system is running and the thread that called the
+// enterPhase methods does not need to do anything else
+
+// This code need to be executed when you want to close the robot
+// Close robot, that will close all the devices and call detach if necessary
+ok = result.robot.enterPhase(yarp::robotinterface::ActionPhaseInterrupt1);
+ok = ok && result.robot.enterPhase(yarp::robotinterface::ActionPhaseShutdown);
+if (!ok) {
+ // Handle error
+ // ...
+}
+\endcode
+
+
+*/
+
diff --git a/doc/yarp_robotinterface_xml_config_files.dox b/doc/yarp_robotinterface_xml_config_files.dox
new file mode 100644
index 00000000000..a08cad113b9
--- /dev/null
+++ b/doc/yarp_robotinterface_xml_config_files.dox
@@ -0,0 +1,89 @@
+/**
+\page yarp_robotinterface_xml_config_files YARP robotinterface XML files
+
+\tableofcontents
+
+This tutorial covers how to write and read XML files that are used by \ref yarprobotinterface "yarprobotinterface tool"
+and by the \ref robointerface_all "libYARP_robotinterface C++ library".
+
+\section yarp_robotinterface_xml_config_files_basics A minimal XML file
+
+Here is a minimal config file, let's call it "config.xml":
+\code
+
+
+
+
+
+
+
+
+ 3
+
+
+
+
+ ${portprefix}/body
+ 0.01
+
+
+ fake_motor_device
+
+
+
+
+
+
+\endcode
+
+This configuration files create two devices:
+ * One `fake_motor_device`, that creates a fake motor control board.
+ * One `fake_motor_nws_yarp`, that creates a Network Wrapper Server (NWS) that exposes `fake_motor_device` functionality over YARP ports.
+
+\section yarp_config_file_reference Reference documentation of XML format.
+
+\subsection robot_element robot Element
+
+The `robot` element is the root element of the XML file. It contains the following attributes:
+* `name`: The name of the `robotinterface` instance.
+* `portprefix`: The portprefix to be used by the port created by the `robotinterface` instance. It can be used as `${portprefix}` when specifying a parameter. It must start with a `/`.
+* `build`: Not used.
+
+\subsection devices_element devices Element
+
+The `devices` element is a child element of `robot` element.
+
+It is a collector of YARP devices that are spawned by the `robotinterface` instance.
+
+\subsection device_element device Element
+
+The `device` element is a child element of `devices` element.
+
+It is used to specify a YARP device that is spawned by the `robotinterface`. It contains the following attributes:
+* `name`: The name of the specific instance of YARP device that is created.
+* `type`: The name of the type of YARP device to instantiate.
+
+\subsection group_element group Element
+
+The `group` element is a child element of `device` or `action` element.
+
+It is a collector of parameters under a specific group name.
+
+\subsection param_element param Element
+
+The `param` element is a child element of `device`, `action` or `group` element.
+
+This element it is used to specify a specific configuration parameter. It contain the following attributes:
+* `name`: The name (i.e. key) of the attribute.
+
+The inner text of the element represents the **value** of the parameter.
+If the inner text contains the string ${portprefix}, it will be substituted with the portprefix parameter specified
+in the `portprefix` attribute of the `robot` element.
+
+
+\subsection action_element action Element
+
+This element still needs to be documented.
+
+
+*/
diff --git a/src/libYARP_robotinterface/src/yarp/robotinterface/XMLReader.h b/src/libYARP_robotinterface/src/yarp/robotinterface/XMLReader.h
index 1d2cd62ca38..5d4741e4894 100644
--- a/src/libYARP_robotinterface/src/yarp/robotinterface/XMLReader.h
+++ b/src/libYARP_robotinterface/src/yarp/robotinterface/XMLReader.h
@@ -18,7 +18,9 @@
namespace yarp::robotinterface {
/**
- * Result of the parsing of XMLReader.
+ * \ingroup robointerface_all
+ *
+ * Result of the parsing of yarp::robotinterface::XMLReader.
*/
class YARP_robotinterface_API XMLReaderResult
{
@@ -41,6 +43,11 @@ class YARP_robotinterface_API XMLReaderResult
Robot robot;
};
+/**
+ * \ingroup robointerface_all
+ *
+ * Class to read an XML file.
+ */
class YARP_robotinterface_API XMLReader
{
public:
diff --git a/src/libYARP_robotinterface/src/yarp/robotinterface/impl/XMLReaderFileV3.cpp b/src/libYARP_robotinterface/src/yarp/robotinterface/impl/XMLReaderFileV3.cpp
index e6082a0bff6..de97675a695 100644
--- a/src/libYARP_robotinterface/src/yarp/robotinterface/impl/XMLReaderFileV3.cpp
+++ b/src/libYARP_robotinterface/src/yarp/robotinterface/impl/XMLReaderFileV3.cpp
@@ -57,7 +57,7 @@ class yarp::robotinterface::impl::XMLReaderFileV3::Private
yarp::robotinterface::ActionList readActionsTag(TiXmlElement* actionsElem, yarp::robotinterface::XMLReaderResult& result);
bool PerformInclusions(TiXmlNode* pParent, const std::string& parent_fileName, const std::string& current_path);
-
+ void ReplaceAllStrings(std::string& str, const std::string& from, const std::string& to);
XMLReaderFileV3* const parent;
#ifdef USE_DTD
@@ -65,7 +65,7 @@ class yarp::robotinterface::impl::XMLReaderFileV3::Private
#endif
bool verbose_output;
- const yarp::os::Searchable* config;
+ yarp::os::Property config;
std::string curr_filename;
unsigned int minorVersion;
unsigned int majorVersion;
@@ -252,9 +252,16 @@ yarp::robotinterface::XMLReaderResult yarp::robotinterface::impl::XMLReaderFileV
result.robot.build() = static_cast(tmp);
#endif
- if (robotElem->QueryStringAttribute("portprefix", &result.robot.portprefix()) != TIXML_SUCCESS) {
- SYNTAX_WARNING(robotElem->Row()) << R"("robot" element should contain the "portprefix" attribute. Using "name" attribute)";
- result.robot.portprefix() = result.robot.name();
+ // If portprefix is already present in config we use that one
+ if (!config.check("portprefix"))
+ {
+ if (robotElem->QueryStringAttribute("portprefix", &result.robot.portprefix()) != TIXML_SUCCESS) {
+ SYNTAX_WARNING(robotElem->Row()) << R"("robot" element should contain the "portprefix" attribute. Using "name" attribute)";
+ result.robot.portprefix() = result.robot.name();
+ }
+ config.put("portprefix",result.robot.portprefix());
+ } else {
+ result.robot.portprefix() = config.find("portprefix").asString();
}
// FIXME DTD >= 4 Make this the default behaviour
@@ -410,6 +417,17 @@ yarp::robotinterface::ParamList yarp::robotinterface::impl::XMLReaderFileV3::Pri
return yarp::robotinterface::ParamList();
}
+void yarp::robotinterface::impl::XMLReaderFileV3::Private::ReplaceAllStrings(std::string& str, const std::string& from, const std::string& to)
+{
+ if(from.empty())
+ return;
+ size_t start_pos = 0;
+ while((start_pos = str.find(from, start_pos)) != std::string::npos) {
+ str.replace(start_pos, from.length(), to);
+ start_pos += to.length();
+ }
+}
+
yarp::robotinterface::Param yarp::robotinterface::impl::XMLReaderFileV3::Private::readParamTag(TiXmlElement* paramElem,
yarp::robotinterface::XMLReaderResult& result)
@@ -437,14 +455,24 @@ yarp::robotinterface::Param yarp::robotinterface::impl::XMLReaderFileV3::Private
return yarp::robotinterface::Param();
}
+ // First process extern-name
std::string extern_name;
- if (paramElem->QueryStringAttribute("extern-name", &extern_name) == TIXML_SUCCESS && config && config->check(extern_name)) {
+ if (paramElem->QueryStringAttribute("extern-name", &extern_name) == TIXML_SUCCESS && config.check(extern_name)) {
// FIXME Check DTD >= 3.1
- param.value() = config->find(extern_name).toString();
+ param.value() = config.find(extern_name).toString();
} else {
param.value() = valueText;
}
+ // After process ${portprefix}
+ std::string paramValueBefore = param.value();
+ std::string paramValueAfter = paramValueBefore;
+ std::string portprefix = config.find("portprefix").toString();
+ ReplaceAllStrings(paramValueAfter, "${portprefix}", portprefix);
+ param.value() = paramValueAfter;
+
+
+
// yDebug() << param;
return param;
}
@@ -732,10 +760,10 @@ yarp::robotinterface::XMLReaderResult yarp::robotinterface::impl::XMLReaderFileV
const yarp::os::Searchable& config,
bool verb)
{
- mPriv->config = &config;
+ mPriv->config.fromString(config.toString());
mPriv->verbose_output = verb;
auto ret = mPriv->readRobotFromFile(filename);
- mPriv->config = nullptr;
+ mPriv->config.clear();
return ret;
}
@@ -743,10 +771,10 @@ yarp::robotinterface::XMLReaderResult yarp::robotinterface::impl::XMLReaderFileV
const yarp::os::Searchable& config,
bool verb)
{
- mPriv->config = &config;
+ mPriv->config.fromString(config.toString());
mPriv->verbose_output = verb;
auto ret = mPriv->readRobotFromString(xmlString);
- mPriv->config = nullptr;
+ mPriv->config.clear();
return ret;
}
diff --git a/src/yarprobotinterface/Module.cpp b/src/yarprobotinterface/Module.cpp
index adf29fbcbd5..05709c81b0f 100644
--- a/src/yarprobotinterface/Module.cpp
+++ b/src/yarprobotinterface/Module.cpp
@@ -144,7 +144,19 @@ bool yarprobotinterface::Module::configure(yarp::os::ResourceFinder& rf)
mPriv->robot.setAllowDeprecatedDevices(rf.check("allow-deprecated-devices"));
mPriv->robot.setDryRun(dryrun);
- std::string rpcPortName("/" + getName() + "/yarprobotinterface");
+ std::string portprefix = mPriv->robot.portprefix();
+ if (portprefix[0] != '/') {
+ yWarning() <<
+ "*************************************************************************************\n"
+ "* yarprobotinterface 'portprefix' parameter does not follow convention, *\n"
+ "* it MUST start with a leading '/' since it is used as the full prefix port name *\n"
+ "* name: full port prefix name with leading '/', e.g. /robotName *\n"
+ "* A temporary automatic fix will be done for you, but please fix your config file *\n"
+ "*************************************************************************************";
+ portprefix = "/" + portprefix;
+ }
+
+ std::string rpcPortName(portprefix + "/yarprobotinterface");
mPriv->rpcPort.open(rpcPortName);
attach(mPriv->rpcPort);
diff --git a/tests/libYARP_robotinterface/RobotinterfaceTest.cpp b/tests/libYARP_robotinterface/RobotinterfaceTest.cpp
index 7c35b08e24c..5eb95a46da1 100644
--- a/tests/libYARP_robotinterface/RobotinterfaceTest.cpp
+++ b/tests/libYARP_robotinterface/RobotinterfaceTest.cpp
@@ -30,6 +30,7 @@ TEST_CASE("robotinterface::ParamTest", "[yarp::robotinterface]")
}
};
+
// Dummy device used in "Check valid robot file with two devices" test
namespace yarp::dev {
class RobotInterfaceTestMockDriver;
@@ -330,6 +331,117 @@ TEST_CASE("robotinterface::XMLReaderTest", "[yarp::robotinterface]")
CHECK(globalState.mockDriverParamValue == "theparam_alt");
}
+ SECTION("Check valid robot file with portprefix passed via xml")
+ {
+ // Reset test flags
+ globalState.reset();
+
+ // Add dummy devices to YARP drivers factory
+ yarp::dev::Drivers::factory().add(new yarp::dev::DriverCreatorOf("robotinterface_test_mock_device", "", "RobotInterfaceTestMockDriver"));
+
+ // Load empty XML configuration file
+ std::string XMLString = "\n"
+ "\n"
+ "\n"
+ " \n"
+ " \n"
+ " ${portprefix}/SomethingInTheMiddle${portprefix} \n"
+ " \n"
+ " \n"
+ "\n";
+
+ yarp::robotinterface::XMLReader reader;
+ yarp::os::Property config;
+ yarp::robotinterface::XMLReaderResult result = reader.getRobotFromString(XMLString, config);
+
+ // Check parsing fails on empty string
+ CHECK(result.parsingIsSuccessful);
+
+ // Verify that only one device has been loaded
+ CHECK(result.robot.devices().size() == 1);
+
+ // Verify that the devices were not opened and the attach was not called
+ CHECK(!globalState.mockDriverWasOpened);
+ CHECK(!globalState.mockDriverWasClosed);
+ CHECK(globalState.mockDriverParamValue.empty());
+
+ // Start the robot (open the device and call "attach" actions)
+ bool ok = result.robot.enterPhase(yarp::robotinterface::ActionPhaseStartup);
+ CHECK(ok);
+
+ // Check that the device was opened and attach called
+ CHECK(globalState.mockDriverWasOpened);
+ CHECK(!globalState.mockDriverWasClosed);
+ CHECK(globalState.mockDriverParamValue == "/RobotWithOneDevice/SomethingInTheMiddle/RobotWithOneDevice");
+
+ // Stop the robot
+ ok = result.robot.enterPhase(yarp::robotinterface::ActionPhaseInterrupt1);
+ CHECK(ok);
+ ok = result.robot.enterPhase(yarp::robotinterface::ActionPhaseShutdown);
+ CHECK(ok);
+
+ // Check that the device was closed and detach called
+ CHECK(globalState.mockDriverWasOpened);
+ CHECK(globalState.mockDriverWasClosed);
+ CHECK(globalState.mockDriverParamValue == "/RobotWithOneDevice/SomethingInTheMiddle/RobotWithOneDevice");
+ }
+
+ SECTION("Check valid robot file with portprefix overriden via configuration")
+ {
+ // Reset test flags
+ globalState.reset();
+
+ // Add dummy devices to YARP drivers factory
+ yarp::dev::Drivers::factory().add(new yarp::dev::DriverCreatorOf("robotinterface_test_mock_device", "", "RobotInterfaceTestMockDriver"));
+
+ // Load empty XML configuration file
+ std::string XMLString = "\n"
+ "\n"
+ "\n"
+ " \n"
+ " \n"
+ " ${portprefix}/SomethingInTheMiddle${portprefix} \n"
+ " \n"
+ " \n"
+ "\n";
+
+ yarp::robotinterface::XMLReader reader;
+ yarp::os::Property config;
+ config.put("portprefix","/RobotWithOneDeviceAlternativePrefix");
+ yarp::robotinterface::XMLReaderResult result = reader.getRobotFromString(XMLString, config);
+
+ // Check parsing fails on empty string
+ CHECK(result.parsingIsSuccessful);
+
+ // Verify that only one device has been loaded
+ CHECK(result.robot.devices().size() == 1);
+
+ // Verify that the devices were not opened and the attach was not called
+ CHECK(!globalState.mockDriverWasOpened);
+ CHECK(!globalState.mockDriverWasClosed);
+ CHECK(globalState.mockDriverParamValue.empty());
+
+ // Start the robot (open the device and call "attach" actions)
+ bool ok = result.robot.enterPhase(yarp::robotinterface::ActionPhaseStartup);
+ CHECK(ok);
+
+ // Check that the device was opened and attach called
+ CHECK(globalState.mockDriverWasOpened);
+ CHECK(!globalState.mockDriverWasClosed);
+ CHECK(globalState.mockDriverParamValue == "/RobotWithOneDeviceAlternativePrefix/SomethingInTheMiddle/RobotWithOneDeviceAlternativePrefix");
+
+ // Stop the robot
+ ok = result.robot.enterPhase(yarp::robotinterface::ActionPhaseInterrupt1);
+ CHECK(ok);
+ ok = result.robot.enterPhase(yarp::robotinterface::ActionPhaseShutdown);
+ CHECK(ok);
+
+ // Check that the device was closed and detach called
+ CHECK(globalState.mockDriverWasOpened);
+ CHECK(globalState.mockDriverWasClosed);
+ CHECK(globalState.mockDriverParamValue == "/RobotWithOneDeviceAlternativePrefix/SomethingInTheMiddle/RobotWithOneDeviceAlternativePrefix");
+ }
+
SECTION("Check valid robot file with two devices")
{
// Reset test flags