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