diff --git a/CMakeLists.txt b/CMakeLists.txt index a2366ac98..515295fc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(urcl src/comm/tcp_socket.cpp src/comm/tcp_server.cpp src/control/reverse_interface.cpp + src/control/script_reader.cpp src/control/script_sender.cpp src/control/trajectory_point_interface.cpp src/control/script_command_interface.cpp diff --git a/doc/architecture.rst b/doc/architecture.rst index 01d7ce3c0..fe608dbfe 100644 --- a/doc/architecture.rst +++ b/doc/architecture.rst @@ -13,6 +13,7 @@ well as a couple of standalone modules to directly use subsets of the library's architecture/reverse_interface architecture/rtde_client architecture/script_command_interface + architecture/script_reader architecture/script_sender architecture/trajectory_point_interface architecture/ur_driver diff --git a/doc/architecture/script_reader.rst b/doc/architecture/script_reader.rst new file mode 100644 index 000000000..3c0c9b41e --- /dev/null +++ b/doc/architecture/script_reader.rst @@ -0,0 +1,156 @@ +:github_url: https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/doc/architecture/script_reader.rst + +.. _script_reader: + +ScriptReader +============ + +Script code used by the :ref:`script_sender` is read from a file. That script code might have to be +dynamically modified based on some configuration input. For example, if the script code contains +connections to a remote PC, that PC's IP address might be configured by the user. Another example +would be to include certain parts of the script code only if the robot's software version supports +that. + +For that purpose the ``ScriptReader`` class is provided. It reads the script code from a file and +performs the following substitutions: + +- Replaces variables in the form of ``{{variable_name}}`` with the value of the variable + from a provided dictionary. +- Includes other script files using the directive ``{% include file_name %}``. The included file is + read from the same directory as the main script file. Nested includes are also possible. +- Use conditionals in order to add certain parts of the script code only if a condition matches. + +The supported substitutions use a basic implementation of the `Jinja2 templating engine` syntax. + +.. note:: + One special literal is defined for **version information**. Use a software version prefixed with a + ``v`` character, e.g. ``v10.8.0`` to encode a software version. Version information entries can be + compared with each other. + + **Do not** wrap version information into quotes, as this will be interpreted as a string. + +Example +------- + +Given two script files: + +.. literalinclude:: ../../tests/resources/example_urscript_main.urscript + :caption: tests/resources/example_urscript_main.urscript + :linenos: + :lineno-match: + +.. literalinclude:: ../../tests/resources/example_urscript_feature.urscript + :language: python + :caption: tests/resources/example_urscript_feature.urscript + :linenos: + :lineno-match: + +The dictionary entry for ``feature_name`` is "torque control". + +Depending on the ``SOFTWARE_VERSION`` entry in the dictionary passed to the +``ScriptReader``, the script code will be read as follows: + +Given ``SOFTWARE_VERSION = v5.21.0``, the script code will be: + +.. code-block:: python + + popup("The cool new feature is not supported on Software version 5.23.0") + + +Given ``SOFTWARE_VERSION = v5.23.0``, the script code will be: + +.. code-block:: python + + textmsg("torque control is a very cool feature!") + +Supported Data +-------------- + +Data dictionary (C++ side) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The data dictionary supports the following types + +- ``str``: A string value, e.g. "Hello World" +- ``int``: An integer value, e.g. 42 +- ``double``: A floating point value, e.g. 3.14 +- ``bool``: A boolean value, e.g. ``true`` or ``false`` +- ``VersionInformation``: A version information value, e.g. ``VersionInformation::fromString("10.8.0")`` + +Script code side +~~~~~~~~~~~~~~~~ + +Variable replacements +^^^^^^^^^^^^^^^^^^^^^ + +Variable replacements in the script code are done using the syntax ``{{ variable_name }}``. For +this to work, the variable ``variable_name`` has to be defined in the data dictionary passed to the +``ScriptReader``. + +The expression ``{{ variable_name }}`` will be replaced with the string representation of the +variable's content. + +- If the variable is a string, it has to be wrapped into quotes in the script + code. e.g. + + .. code-block:: python + + textmsg("{{ log_message }}") + + +- Boolean variables will be replaced with the string ``True`` or ``False``. +- Numeric variables (integer and floating point) will be replaced with the string representation generated by + `std::to_string() `_. +- Version information variables will be replaced with the string representation similar to + ``10.7.0.0`` + +Boolean expressions +^^^^^^^^^^^^^^^^^^^ + +Boolean expressions have to follow one of two possible syntax variations + +- Direct evaluation of a boolean variable from the data dictionary: + + .. code-block:: + + boolean_variable_name + +- Comparison of a variable with a value using an operator. + + .. code-block:: + + variable_name operator value + + The operator has to be one of ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``. On the lefthand side + of the operator there has to be a variable from the data dictionary. The right hand side can be + either a variable name or a value. If the right hand side is a variable name, it has to be + defined in the data dictionary as well. Values will be parsed as follows: + + - Strings: Wrapped in quotes, e.g. ``"Hello World"`` or ``'Universal Robots'``. + - Numerical values such as ``42``, ``3.14``, ``-1``, ``1e-12``. + - Boolean values: See below. + - Version information: Prefixed with a ``v`` character, e.g. ``v10.8.0``, ``v5.23.0``. + +Boolean values parsing +^^^^^^^^^^^^^^^^^^^^^^ + +Boolean values can be parsed from the following strings: + +- ``true``, ``True``, ``TRUE`` +- ``on``, ``On``, ``ON`` +- ``yes``, ``Yes``, ``YES`` +- ``1`` +- ``false``, ``False``, ``FALSE`` +- ``off``, ``Off``, ``OFF`` +- ``no``, ``No``, ``NO`` +- ``0`` + +Conditional blocks +^^^^^^^^^^^^^^^^^^ + +Conditional blocks have to be started with a ``{% if condition %}`` directive and closed with a +``{% endif %}`` directive. The condition can be any boolean expression as described above. + +The ``{% elif condition %}`` and ``{% else %}`` directives can be used to add alternative paths. + +Conditional blocks can be nested. diff --git a/include/ur_client_library/control/script_reader.h b/include/ur_client_library/control/script_reader.h new file mode 100644 index 000000000..6720aff3e --- /dev/null +++ b/include/ur_client_library/control/script_reader.h @@ -0,0 +1,128 @@ +// -- BEGIN LICENSE BLOCK ---------------------------------------------- +// Copyright 2025 Universal Robots A/S +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the {copyright_holder} nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// -- END LICENSE BLOCK ------------------------------------------------ + +#pragma once +#include +#include +#include +#include + +#include +#include + +namespace urcl +{ +namespace control +{ +/*! + * \brief This class handles reading script files parsing special instructions that will get replaced. + * + * When parsing the script code, it is supported to have + * - Variable replacements using `{{ VARIABLE_NAME }}` + * - Including other files using `{% include %}`. + * The filename has to be relative to the root script file's folder + * - Conditionals using + * + * {% if %} + * ... + * {% else %} + * ... + * {% endif %} + * + * + * Those directives use Jinja2 notation. + */ +class ScriptReader +{ +public: + using DataVariant = std::variant; + using DataDict = std::unordered_map; + + ScriptReader() = default; + + /*! + * \brief Reads a script file and applies variable replacements, includes, and conditionals. + * \param file_path Path of the script file to be loaded. + * \param data Data dictionary used for variable replacements and expression evaluation. + * \return The Script code with all replacements, includes and conditionals applied. + */ + std::string readScriptFile(const std::string& file_path, const DataDict& data = DataDict()); + + /*! + * \brief Evaluate a boolean expression + * \param expression The boolean expression to be evaluated. + * \param data A data dictionary that will be used when evaluating the expressions + * \return The result of evaluating the boolean expression + */ + static bool evaluateExpression(const std::string& expression, const DataDict& data); + +private: + enum BlockType + { + IF, + ELIF, + ELSE + }; + struct BlockState + { + BlockType type; + bool condition_matched; // Has any previous condition in this block matched? + bool should_render; // Should this block render? + bool parent_render; // Is the parent block rendering? + }; + + std::filesystem::path script_path_; + + static std::string readFileContent(const std::string& file_path); + void replaceIncludes(std::string& script_code, const DataDict& data); + static void replaceVariables(std::string& script_code, const DataDict& data); + static void replaceConditionals(std::string& script_code, const DataDict& data); +}; + +bool operator<(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs); +bool operator>(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs); +bool operator==(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs); + +inline bool operator!=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) +{ + return !(lhs == rhs); +} +inline bool operator<=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) +{ + return (lhs < rhs || lhs == rhs); +} +inline bool operator>=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) +{ + return (lhs > rhs || lhs == rhs); +} +} // namespace control +} // namespace urcl diff --git a/include/ur_client_library/exceptions.h b/include/ur_client_library/exceptions.h index 69157f954..b50ba8f5d 100644 --- a/include/ur_client_library/exceptions.h +++ b/include/ur_client_library/exceptions.h @@ -213,5 +213,17 @@ class UnsupportedMotionType : public UrException { } }; + +class UnknownVariable : public UrException +{ +private: + std::string text_; + +public: + explicit UnknownVariable() = delete; + explicit UnknownVariable(const std::string& variable_name) : std::runtime_error("Unknown variable: " + variable_name) + { + } +}; } // namespace urcl -#endif // ifndef UR_CLIENT_LIBRARY_EXCEPTIONS_H_INCLUDED +#endif // ifndef UR_CLIENT_LIBRARY_EXCEPTIONS_H_INCLUDED \ No newline at end of file diff --git a/include/ur_client_library/helpers.h b/include/ur_client_library/helpers.h index 08b6d0b47..2b898cdfa 100644 --- a/include/ur_client_library/helpers.h +++ b/include/ur_client_library/helpers.h @@ -29,6 +29,7 @@ #ifndef UR_CLIENT_LIBRARY_HELPERS_H_INCLUDED #define UR_CLIENT_LIBRARY_HELPERS_H_INCLUDED +#include #include #include #ifdef _WIN32 @@ -78,5 +79,25 @@ bool setFiFoScheduling(pthread_t& thread, const int priority); */ void waitFor(std::function condition, const std::chrono::milliseconds timeout, const std::chrono::milliseconds check_interval = std::chrono::milliseconds(50)); + +/*! + * \brief Parses a boolean value from a string. + * + * The string can be one of + * - true, True, TRUE + * - on, On, ON + * - yes, Yes, YES + * - 1 + * - false, False, FALSE + * - off, Off, OFF + * - no, No, NO + * - 0 + * + * \param str string to be parsed + * \throws urcl::UrException If the string doesn't match one of the options + * \return The boolean representation of the string + */ +bool parseBoolean(const std::string& str); + } // namespace urcl #endif // ifndef UR_CLIENT_LIBRARY_HELPERS_H_INCLUDED diff --git a/include/ur_client_library/ur/datatypes.h b/include/ur_client_library/ur/datatypes.h index c547a9288..38edf1a24 100644 --- a/include/ur_client_library/ur/datatypes.h +++ b/include/ur_client_library/ur/datatypes.h @@ -30,6 +30,7 @@ #include #include "ur_client_library/log.h" +#include namespace urcl { diff --git a/include/ur_client_library/ur/ur_driver.h b/include/ur_client_library/ur/ur_driver.h index e0c334f22..49951940d 100644 --- a/include/ur_client_library/ur/ur_driver.h +++ b/include/ur_client_library/ur/ur_driver.h @@ -36,6 +36,7 @@ #include "ur_client_library/control/reverse_interface.h" #include "ur_client_library/control/trajectory_point_interface.h" #include "ur_client_library/control/script_command_interface.h" +#include "ur_client_library/control/script_reader.h" #include "ur_client_library/control/script_sender.h" #include "ur_client_library/ur/tool_communication.h" #include "ur_client_library/ur/version_information.h" @@ -891,6 +892,20 @@ class UrDriver trajectory_interface_->registerDisconnectionCallback(fun); } + /*! + * \brief Reads a script file and returns its content. + * + * This doesn't perform any substitutions on the file contents, but simply reads the file into a string. + * + * \deprecated This function isn't used. Please use the ScriptReader class instead. This function + * will be removed in May 2027. + * + * \param filename The name of the script file to read. + * + * \returns The content of the script file as a string. + */ + [[deprecated("This function isn't used. Please use the ScriptReader class instead. This function will be removed in " + "May 2027.")]] static std::string readScriptFile(const std::string& filename); bool isReverseInterfaceConnected() const @@ -921,6 +936,7 @@ class UrDriver std::unique_ptr trajectory_interface_; std::unique_ptr script_command_interface_; std::unique_ptr script_sender_; + std::unique_ptr script_reader_; size_t socket_connection_attempts_ = 0; std::chrono::milliseconds socket_reconnection_timeout_ = std::chrono::milliseconds(10000); diff --git a/include/ur_client_library/ur/version_information.h b/include/ur_client_library/ur/version_information.h index 55dc636dc..784cdc22e 100644 --- a/include/ur_client_library/ur/version_information.h +++ b/include/ur_client_library/ur/version_information.h @@ -54,6 +54,11 @@ class VersionInformation */ static VersionInformation fromString(const std::string& str); + /*! + * \brief Generates a string representation of the version information such as "5.12.0.1101319" + */ + std::string toString() const; + bool isESeries() const; friend bool operator==(const VersionInformation& v1, const VersionInformation& v2); @@ -77,4 +82,4 @@ class VersionInformation std::vector splitString(std::string input, const std::string& delimiter = "."); } // namespace urcl -#endif // ifndef UR_CLIENT_LIBRARY_UR_VERSION_INFORMATION_H_INCLUDED +#endif // ifndef UR_CLIENT_LIBRARY_UR_VERSION_INFORMATION_H_INCLUDED \ No newline at end of file diff --git a/src/control/script_reader.cpp b/src/control/script_reader.cpp new file mode 100644 index 000000000..9b41641d4 --- /dev/null +++ b/src/control/script_reader.cpp @@ -0,0 +1,403 @@ +// -- BEGIN LICENSE BLOCK ---------------------------------------------- +// Copyright 2025 Universal Robots A/S +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the {copyright_holder} nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// -- END LICENSE BLOCK ------------------------------------------------ + +#include +#include +#include +#include + +#include +#include +#include + +namespace urcl +{ +namespace control +{ +constexpr double ZERO_EPSILON = 0.000001; + +bool operator<(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) +{ + if (std::holds_alternative(lhs)) + { + if (std::holds_alternative(rhs)) + { + return std::get(lhs) < std::get(rhs); + } + else if (std::holds_alternative(rhs)) + { + return std::get(lhs) < static_cast(std::get(rhs)); + } + } + if (std::holds_alternative(lhs)) + { + if (std::holds_alternative(rhs)) + { + return static_cast(std::get(lhs)) < std::get(rhs); + } + else if (std::holds_alternative(rhs)) + { + return std::get(lhs) < std::get(rhs); + } + } + if (std::holds_alternative(lhs)) + { + return std::get(lhs) < std::get(rhs); + } + throw std::invalid_argument("The comparison operator is only allowed for numeric values."); +} + +bool operator>(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) +{ + return !(lhs < rhs || lhs == rhs); +} + +bool operator==(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs) +{ + if (lhs.index() != rhs.index()) + { + // Allow comparison between int and double + if ((std::holds_alternative(lhs) && std::holds_alternative(rhs))) + { + return static_cast(std::get(lhs)) == std::get(rhs); + } + if ((std::holds_alternative(lhs) && std::holds_alternative(rhs))) + { + return std::get(lhs) == static_cast(std::get(rhs)); + } + // Allow comparison between bool and int/double + if (std::holds_alternative(lhs)) + { + if (std::holds_alternative(rhs)) + { + return std::abs((static_cast(std::get(lhs)) - std::get(rhs))) < ZERO_EPSILON; + } + if (std::holds_alternative(rhs)) + { + return std::abs((static_cast(std::get(lhs)) - std::get(rhs))) == 0; + } + } + throw std::invalid_argument( + "Checking equality of types is not allowed: " + + std::string(lhs.index() == 0 ? "string" : (lhs.index() == 1 ? "double" : (lhs.index() == 2 ? "int" : "bool"))) + + " with " + + std::string(rhs.index() == 0 ? "string" : (rhs.index() == 1 ? "double" : (rhs.index() == 2 ? "int" : "bool")))); + } + if (std::holds_alternative(lhs)) + { + return std::get(lhs) == std::get(rhs); + } + if (std::holds_alternative(lhs)) + { + return std::get(lhs) == std::get(rhs); + } + if (std::holds_alternative(lhs)) + { + return std::get(lhs) == std::get(rhs); + } + if (std::holds_alternative(lhs)) + { + return std::get(lhs) == std::get(rhs); + } + if (std::holds_alternative(lhs)) + { + return std::get(lhs) == std::get(rhs); + } + throw std::runtime_error("Unknown variant type passed to equality check. Please contact the developers."); +} + +std::string ScriptReader::readScriptFile(const std::string& filename, const DataDict& data) +{ + script_path_ = filename; + std::string script_code = readFileContent(filename); + + replaceVariables(script_code, data); + replaceConditionals(script_code, data); + replaceIncludes(script_code, data); + + return script_code; +} + +std::string ScriptReader::readFileContent(const std::string& file_path) +{ + std::ifstream ifs; + ifs.open(file_path); + std::string content; + if (ifs) + { + content = std::string((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); + ifs.close(); + } + else + { + std::stringstream ss; + ss << "Could not open script file '" << file_path << "'. Please check if the file exists and is readable."; + throw UrException(ss.str().c_str()); + } + + return content; +} + +void ScriptReader::replaceIncludes(std::string& script, const DataDict& data) +{ + std::regex include_pattern(R"(\{\%\s*include\s*['|"]([^'"]+)['|"]\s*\%\})"); + + std::smatch match; + + // Replace all include patterns in the line + while (std::regex_search(script, match, include_pattern)) + { + std::filesystem::path relative_file_path(match[1].str()); + std::string file_content = + readScriptFile((script_path_.parent_path() / relative_file_path.string()).string(), data); + script.replace(match.position(0), match.length(0), file_content); + } +} + +void ScriptReader::replaceVariables(std::string& script_code, const DataDict& data) +{ + std::regex pattern(R"(\{\{\s*([\w-]+)\s*\}\})"); + std::smatch match; + while (std::regex_search(script_code, match, pattern)) + { + std::string key = match[1]; + if (data.find(key) == data.end()) + { + std::stringstream ss; + ss << "Variable '" << key << "' not found in data."; + URCL_LOG_ERROR(ss.str().c_str()); + throw UnknownVariable(ss.str().c_str()); + } + std::string replaced_value; + + if (std::holds_alternative(data.at(key))) + { + replaced_value = std::get(data.at(key)); + } + else if (std::holds_alternative(data.at(key))) + { + replaced_value = std::to_string(std::get(data.at(key))); + } + else if (std::holds_alternative(data.at(key))) + { + replaced_value = std::to_string(std::get(data.at(key))); + } + else if (std::holds_alternative(data.at(key))) + { + std::get(data.at(key)) ? replaced_value = "True" : replaced_value = "False"; + } + else if (std::holds_alternative(data.at(key))) + { + replaced_value = std::get(data.at(key)).toString(); + } + else + { + // This is more of a reminder if we add types to the variant and forget to add it here. + std::stringstream ss; + ss << "Unsupported type for variable '" << key << "'."; + URCL_LOG_ERROR(ss.str().c_str()); + throw UrException(ss.str().c_str()); + } + script_code.replace(match.position(0), match.length(0), replaced_value); + } +} + +void ScriptReader::replaceConditionals(std::string& script_code, const DataDict& data) +{ + std::istringstream stream(script_code); + std::ostringstream output; + std::string line; + std::stack block_stack; + + std::regex if_pattern(R"(\{\%\s*if\s+([^\s].*[^\s])\s+\%\})"); + std::regex elif_pattern(R"(\{\%\s*elif\s+([^\s].*[^\s])\s+\%\})"); + std::regex else_pattern(R"(\{\%\s*else\s*\%\})"); + std::regex endif_pattern(R"(\{\%\s*endif\s*\%\})"); + std::smatch match; + + bool first_line = true; + while (std::getline(stream, line)) + { + if (std::regex_search(line, match, if_pattern)) + { + std::string condition = match[1]; + bool result = evaluateExpression(condition, data); + bool parent_render = block_stack.empty() ? true : block_stack.top().should_render; + block_stack.push({ IF, result, parent_render && result, parent_render }); + } + else if (std::regex_search(line, match, elif_pattern)) + { + if (!block_stack.empty()) + { + BlockState& top = block_stack.top(); + if (top.type == ELSE) + continue; + std::string condition = match[1]; + bool result = evaluateExpression(condition, data); + top.type = ELIF; + if (!top.condition_matched && result) + { + top.condition_matched = true; + top.should_render = top.parent_render; + } + else + { + top.should_render = false; + } + } + } + else if (std::regex_search(line, match, else_pattern)) + { + if (!block_stack.empty()) + { + BlockState& top = block_stack.top(); + top.type = ELSE; + top.should_render = top.parent_render && !top.condition_matched; + } + } + else if (std::regex_search(line, match, endif_pattern)) + { + if (!block_stack.empty()) + { + block_stack.pop(); + } + } + else + { + if (block_stack.empty() || block_stack.top().should_render) + { + if (!first_line) + { + output << "\n"; + } + output << line; + first_line = false; + } + } + } + + script_code = output.str(); +} + +bool ScriptReader::evaluateExpression(const std::string& expression, const DataDict& data) +{ + const std::string trimmed = std::regex_replace(expression, std::regex("^\\s+|\\s+$"), ""); + std::regex expression_pattern(R"(([a-zA-Z_][a-zA-Z0-9_]*)\s*([!=<>]=?)\s*(["']?[^'"]*["']?))"); + + std::smatch match; + if (std::regex_search(trimmed, match, expression_pattern)) + { + std::string variable_name = match[1]; + std::string comp_operator = match[2]; + std::string value_str = match[3]; + DataVariant value = value_str; + + if (!data.count(variable_name)) + { + throw UnknownVariable(variable_name); + } + + std::stringstream ss; + ss << "Evaluating trimmed: " << variable_name << " " << comp_operator << " " << value_str << std::endl; + URCL_LOG_DEBUG(ss.str().c_str()); + + // Is the value a string, a number or a variable? + std::regex string_pattern(R"(^['"]([^'"]+)?['"]$)"); + std::regex number_pattern(R"(^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$)"); + std::regex boolean_pattern(R"(^(true|false|yes|no|on|off)$)", std::regex::icase); + std::regex version_pattern(R"(^v(\d+\.\d+(\.\d+)?(-\d+)?)$)"); + if (std::regex_search(value_str, match, string_pattern)) + { + value = match[1]; // Extract the string content without quotes + } + else if (std::regex_search(value_str, match, number_pattern)) + { + value = std::stod(value_str); + } + else if (std::regex_search(value_str, match, boolean_pattern)) + { + value = parseBoolean(value_str); + } + else if (std::regex_search(value_str, match, version_pattern)) + { + value = VersionInformation::fromString(match[1]); + } + else if (data.count(value_str)) + { + value = data.at(value_str); + } + else + { + throw UnknownVariable(value_str); + } + + if (comp_operator == "==") + { + return data.at(variable_name) == value; + } + else if (comp_operator == "!=") + { + return data.at(variable_name) != value; + } + else if (comp_operator == "<") + { + return data.at(variable_name) < value; + } + else if (comp_operator == ">") + { + return data.at(variable_name) > value; + } + else if (comp_operator == "<=") + { + return data.at(variable_name) <= value; + } + else if (comp_operator == ">=") + { + return data.at(variable_name) >= value; + } + } + else if (std::regex_match(trimmed, std::regex(R"([a-zA-Z_][a-zA-Z0-9_]*)"))) + { + if (!data.count(trimmed)) + { + throw UnknownVariable(trimmed); + } + if (std::holds_alternative(data.at(trimmed))) + { + return std::get(data.at(trimmed)); + } + } + + throw std::runtime_error("trimmed evaluation failed: `" + trimmed + + "`. Expected a boolean value, but got a different type."); +} + +} // namespace control +} // namespace urcl \ No newline at end of file diff --git a/src/control/script_sender.cpp b/src/control/script_sender.cpp index 92a3023a6..1afd0d591 100644 --- a/src/control/script_sender.cpp +++ b/src/control/script_sender.cpp @@ -62,8 +62,12 @@ void ScriptSender::messageCallback(const socket_t filedescriptor, char* buffer) void ScriptSender::sendProgram(const socket_t filedescriptor) { - size_t len = program_.size(); - const uint8_t* data = reinterpret_cast(program_.c_str()); + // urscripts (snippets) must end with a newline, or otherwise the controller's runtime will + // not execute them. To avoid problems, we always just append a newline here, even if + // there may already be one. + const std::string send_string = program_ + "\n"; + size_t len = send_string.size(); + const uint8_t* data = reinterpret_cast(send_string.c_str()); size_t written; if (server_.write(filedescriptor, data, len, written)) @@ -77,4 +81,4 @@ void ScriptSender::sendProgram(const socket_t filedescriptor) } } // namespace control -} // namespace urcl +} // namespace urcl \ No newline at end of file diff --git a/src/helpers.cpp b/src/helpers.cpp index b80f772b4..5e5daf7c4 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -117,4 +118,26 @@ void waitFor(std::function condition, const std::chrono::milliseconds ti } throw urcl::TimeoutException("Timeout while waiting for condition to be met", timeout); } + +bool parseBoolean(const std::string& str) +{ + std::string lower = str; + std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); }); + + if (lower == "true" || lower == "1" || lower == "yes" || lower == "on") + { + return true; + } + else if (lower == "false" || lower == "0" || lower == "no" || lower == "off") + { + return false; + } + else + { + std::stringstream ss; + ss << "Invalid boolean value: '" << str << "'. Expected 'true', 'false', '1', '0', 'yes', 'no', 'on', or 'off'."; + URCL_LOG_ERROR(ss.str().c_str()); + throw UrException(ss.str().c_str()); + } +} } // namespace urcl diff --git a/src/ur/ur_driver.cpp b/src/ur/ur_driver.cpp index 47705b3fa..6ba6aded2 100644 --- a/src/ur/ur_driver.cpp +++ b/src/ur/ur_driver.cpp @@ -32,7 +32,9 @@ //---------------------------------------------------------------------- #include "ur_client_library/ur/ur_driver.h" +#include "ur_client_library/control/script_reader.h" #include "ur_client_library/exceptions.h" +#include "ur_client_library/helpers.h" #include "ur_client_library/primary/primary_parser.h" #include #include @@ -41,16 +43,16 @@ namespace urcl { -static const std::string BEGIN_REPLACE("{{BEGIN_REPLACE}}"); -static const std::string JOINT_STATE_REPLACE("{{JOINT_STATE_REPLACE}}"); -static const std::string TIME_REPLACE("{{TIME_REPLACE}}"); -static const std::string SERVO_J_REPLACE("{{SERVO_J_REPLACE}}"); -static const std::string SERVER_IP_REPLACE("{{SERVER_IP_REPLACE}}"); -static const std::string SERVER_PORT_REPLACE("{{SERVER_PORT_REPLACE}}"); -static const std::string TRAJECTORY_PORT_REPLACE("{{TRAJECTORY_SERVER_PORT_REPLACE}}"); -static const std::string SCRIPT_COMMAND_PORT_REPLACE("{{SCRIPT_COMMAND_SERVER_PORT_REPLACE}}"); -static const std::string FORCE_MODE_SET_DAMPING_REPLACE("{{FORCE_MODE_SET_DAMPING_REPLACE}}"); -static const std::string FORCE_MODE_SET_GAIN_SCALING_REPLACE("{{FORCE_MODE_SET_GAIN_SCALING_REPLACE}}"); +static const std::string BEGIN_REPLACE("BEGIN_REPLACE"); +static const std::string JOINT_STATE_REPLACE("JOINT_STATE_REPLACE"); +static const std::string TIME_REPLACE("TIME_REPLACE"); +static const std::string SERVO_J_REPLACE("SERVO_J_REPLACE"); +static const std::string SERVER_IP_REPLACE("SERVER_IP_REPLACE"); +static const std::string SERVER_PORT_REPLACE("SERVER_PORT_REPLACE"); +static const std::string TRAJECTORY_PORT_REPLACE("TRAJECTORY_SERVER_PORT_REPLACE"); +static const std::string SCRIPT_COMMAND_PORT_REPLACE("SCRIPT_COMMAND_SERVER_PORT_REPLACE"); +static const std::string FORCE_MODE_SET_DAMPING_REPLACE("FORCE_MODE_SET_DAMPING_REPLACE"); +static const std::string FORCE_MODE_SET_GAIN_SCALING_REPLACE("FORCE_MODE_SET_GAIN_SCALING_REPLACE"); UrDriver::~UrDriver() { @@ -86,46 +88,21 @@ void UrDriver::init(const UrDriverConfiguration& config) // Figure out the ip automatically if the user didn't provide it std::string local_ip = config.reverse_ip.empty() ? rtde_client_->getIP() : config.reverse_ip; - std::string prog = readScriptFile(config.script_file); - while (prog.find(JOINT_STATE_REPLACE) != std::string::npos) - { - prog.replace(prog.find(JOINT_STATE_REPLACE), JOINT_STATE_REPLACE.length(), - std::to_string(control::ReverseInterface::MULT_JOINTSTATE)); - } - while (prog.find(TIME_REPLACE) != std::string::npos) - { - prog.replace(prog.find(TIME_REPLACE), TIME_REPLACE.length(), - std::to_string(control::TrajectoryPointInterface::MULT_TIME)); - } + trajectory_interface_.reset(new control::TrajectoryPointInterface(config.trajectory_port)); + script_command_interface_.reset(new control::ScriptCommandInterface(config.script_command_port)); + startPrimaryClientCommunication(); + + control::ScriptReader::DataDict data; + data[JOINT_STATE_REPLACE] = std::to_string(control::ReverseInterface::MULT_JOINTSTATE); + data[TIME_REPLACE] = std::to_string(control::TrajectoryPointInterface::MULT_TIME); std::ostringstream out; out << "lookahead_time=" << servoj_lookahead_time_ << ", gain=" << servoj_gain_; - while (prog.find(SERVO_J_REPLACE) != std::string::npos) - { - prog.replace(prog.find(SERVO_J_REPLACE), SERVO_J_REPLACE.length(), out.str()); - } - - while (prog.find(SERVER_IP_REPLACE) != std::string::npos) - { - prog.replace(prog.find(SERVER_IP_REPLACE), SERVER_IP_REPLACE.length(), local_ip); - } - - while (prog.find(SERVER_PORT_REPLACE) != std::string::npos) - { - prog.replace(prog.find(SERVER_PORT_REPLACE), SERVER_PORT_REPLACE.length(), std::to_string(config.reverse_port)); - } - - while (prog.find(TRAJECTORY_PORT_REPLACE) != std::string::npos) - { - prog.replace(prog.find(TRAJECTORY_PORT_REPLACE), TRAJECTORY_PORT_REPLACE.length(), - std::to_string(config.trajectory_port)); - } - - while (prog.find(SCRIPT_COMMAND_PORT_REPLACE) != std::string::npos) - { - prog.replace(prog.find(SCRIPT_COMMAND_PORT_REPLACE), SCRIPT_COMMAND_PORT_REPLACE.length(), - std::to_string(config.script_command_port)); - } + data[SERVO_J_REPLACE] = out.str(); + data[SERVER_IP_REPLACE] = local_ip; + data[SERVER_PORT_REPLACE] = std::to_string(config.reverse_port); + data[TRAJECTORY_PORT_REPLACE] = std::to_string(config.trajectory_port); + data[SCRIPT_COMMAND_PORT_REPLACE] = std::to_string(config.script_command_port); robot_version_ = rtde_client_->getVersion(); @@ -146,12 +123,10 @@ void UrDriver::init(const UrDriverConfiguration& config) << config.tool_comm_setup->getStopBits() << ", " << config.tool_comm_setup->getRxIdleChars() << ", " << config.tool_comm_setup->getTxIdleChars() << ")"; } - prog.replace(prog.find(BEGIN_REPLACE), BEGIN_REPLACE.length(), begin_replace.str()); - - trajectory_interface_.reset(new control::TrajectoryPointInterface(config.trajectory_port)); - script_command_interface_.reset(new control::ScriptCommandInterface(config.script_command_port)); + data[BEGIN_REPLACE] = begin_replace.str(); + script_reader_.reset(new control::ScriptReader()); + std::string prog = script_reader_->readScriptFile(config.script_file, data); - startPrimaryClientCommunication(); if (in_headless_mode_) { full_robot_program_ = "stop program\n"; @@ -659,4 +634,4 @@ std::deque UrDriver::getErrorCodes() { return primary_client_->getErrorCodes(); } -} // namespace urcl +} // namespace urcl \ No newline at end of file diff --git a/src/ur/version_information.cpp b/src/ur/version_information.cpp index 1bc5cf456..be0b13954 100644 --- a/src/ur/version_information.cpp +++ b/src/ur/version_information.cpp @@ -84,6 +84,11 @@ VersionInformation VersionInformation::fromString(const std::string& str) return info; } +std::string VersionInformation::toString() const +{ + return std::to_string(this->major) + "." + std::to_string(this->minor) + "." + std::to_string(this->bugfix) + "." + + std::to_string(this->build); +} bool VersionInformation::isESeries() const { @@ -144,4 +149,4 @@ bool operator>=(const VersionInformation& v1, const VersionInformation& v2) { return !(v1 < v2); } -} // namespace urcl +} // namespace urcl \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 84ae499a0..67d2b159d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -131,6 +131,13 @@ target_link_libraries(script_sender_tests PRIVATE ur_client_library::urcl GTest: gtest_add_tests(TARGET script_sender_tests ) +add_executable(script_reader_tests test_script_reader.cpp) +target_link_libraries(script_reader_tests PRIVATE ur_client_library::urcl GTest::gtest_main) +gtest_add_tests( + TARGET script_reader_tests + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + add_executable(reverse_interface_tests test_reverse_interface.cpp) target_link_libraries(reverse_interface_tests PRIVATE ur_client_library::urcl GTest::gtest_main) gtest_add_tests(TARGET reverse_interface_tests @@ -225,3 +232,8 @@ add_executable(control_mode_tests test_control_mode.cpp) target_link_libraries(control_mode_tests PRIVATE ur_client_library::urcl GTest::gtest_main) gtest_add_tests(TARGET control_mode_tests ) + +add_executable(helpers_tests test_helpers.cpp) +target_link_libraries(helpers_tests PRIVATE ur_client_library::urcl GTest::gtest_main) +gtest_add_tests(TARGET helpers_tests +) \ No newline at end of file diff --git a/tests/resources/example_urscript_feature.urscript b/tests/resources/example_urscript_feature.urscript new file mode 100644 index 000000000..7f1278a5f --- /dev/null +++ b/tests/resources/example_urscript_feature.urscript @@ -0,0 +1 @@ +textmsg("{{ feature_name }} is a very cool feature!") \ No newline at end of file diff --git a/tests/resources/example_urscript_main.urscript b/tests/resources/example_urscript_main.urscript new file mode 100644 index 000000000..3e290fe19 --- /dev/null +++ b/tests/resources/example_urscript_main.urscript @@ -0,0 +1,5 @@ +{% if SOFTWARE_VERSION >= v5.23.0 %} + {% include "example_urscript_feature.urscript" %} +{% else %} + popup("The cool new feature is not supported on Software version 5.23.0") +{% endif %} diff --git a/tests/test_helpers.cpp b/tests/test_helpers.cpp new file mode 100644 index 000000000..dfae50647 --- /dev/null +++ b/tests/test_helpers.cpp @@ -0,0 +1,61 @@ +// -- BEGIN LICENSE BLOCK ---------------------------------------------- +// Copyright 2025 Universal Robots A/S +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the {copyright_holder} nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// -- END LICENSE BLOCK ------------------------------------------------ + +#include + +#include +#include + +using namespace urcl; + +TEST(TestHelpers, test_parse_boolean) +{ + EXPECT_TRUE(parseBoolean("true")); + EXPECT_TRUE(parseBoolean("True")); + EXPECT_TRUE(parseBoolean("TRUE")); + EXPECT_TRUE(parseBoolean("on")); + EXPECT_TRUE(parseBoolean("On")); + EXPECT_TRUE(parseBoolean("ON")); + EXPECT_TRUE(parseBoolean("yes")); + EXPECT_TRUE(parseBoolean("Yes")); + EXPECT_TRUE(parseBoolean("YES")); + EXPECT_TRUE(parseBoolean("1")); + EXPECT_FALSE(parseBoolean("false")); + EXPECT_FALSE(parseBoolean("False")); + EXPECT_FALSE(parseBoolean("FALSE")); + EXPECT_FALSE(parseBoolean("off")); + EXPECT_FALSE(parseBoolean("Off")); + EXPECT_FALSE(parseBoolean("OFF")); + EXPECT_FALSE(parseBoolean("no")); + EXPECT_FALSE(parseBoolean("No")); + EXPECT_FALSE(parseBoolean("NO")); + EXPECT_FALSE(parseBoolean("0")); + EXPECT_THROW(parseBoolean("notabool"), urcl::UrException); +} diff --git a/tests/test_script_reader.cpp b/tests/test_script_reader.cpp new file mode 100644 index 000000000..ad499ac82 --- /dev/null +++ b/tests/test_script_reader.cpp @@ -0,0 +1,449 @@ +// -- BEGIN LICENSE BLOCK ---------------------------------------------- +// Copyright 2025 Universal Robots A/S +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the {copyright_holder} nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// -- END LICENSE BLOCK ------------------------------------------------ + +#include "ur_client_library/exceptions.h" + +#include +#include "ur_client_library/control/script_reader.h" +#include "ur_client_library/ur/version_information.h" + +#include + +#ifdef _WIN32 +# define mkstemp _mktemp_s +#endif + +using namespace urcl::control; + +class ScriptReaderTest : public ::testing::Test +{ +protected: + std::string valid_script_path_; + std::string invalid_script_path_; + std::string empty_script_path_; + + std::stringstream simple_script_; + + void SetUp() override + { + urcl::setLogLevel(urcl::LogLevel::INFO); + invalid_script_path_ = "test_resources/non_existent_script.urscript"; + empty_script_path_ = "resources/empty.txt"; + char existing_script_file[] = "urscript.XXXXXX"; + std::ignore = mkstemp(existing_script_file); + std::ofstream ofs(existing_script_file); + if (ofs.bad()) + { + std::cout << "Failed to create temporary files" << std::endl; + GTEST_FAIL(); + } + ofs.close(); + + valid_script_path_ = existing_script_file; + + simple_script_ << "movej([0,0,0,0,0,0])"; + + // Create test resources + std::ofstream valid_script(valid_script_path_); + valid_script << simple_script_.str(); + valid_script.close(); + } + + void TearDown() override + { + std::remove(valid_script_path_.c_str()); + } +}; + +TEST_F(ScriptReaderTest, ReadValidScript) +{ + ScriptReader reader; + std::string content = reader.readScriptFile(valid_script_path_); + EXPECT_EQ(content, simple_script_.str()); +} + +TEST_F(ScriptReaderTest, ReadEmptyScript) +{ + ScriptReader reader; + std::string content = reader.readScriptFile(empty_script_path_); + EXPECT_EQ(content, ""); +} + +TEST_F(ScriptReaderTest, ReadNonExistentScript) +{ + ScriptReader reader; + EXPECT_THROW(reader.readScriptFile(invalid_script_path_), std::runtime_error); +} + +TEST_F(ScriptReaderTest, ReplaceIncludes) +{ + ScriptReader reader; + + char existing_script_file[] = "main_script.XXXXXX"; + std::ignore = mkstemp(existing_script_file); + char existing_include_file[] = "included_script.XXXXXX"; + std::ignore = mkstemp(existing_include_file); + std::ofstream ofs(existing_script_file); + if (ofs.bad()) + { + std::cout << "Failed to create temporary files" << std::endl; + GTEST_FAIL(); + } + std::string script_with_include = "{% include '" + std::string(existing_include_file) + "' %}"; + ofs << script_with_include; + ofs.close(); + + // Create a temporary included script + std::ofstream ofs_included(existing_include_file); + if (ofs_included.bad()) + { + std::cout << "Failed to create temporary files" << std::endl; + GTEST_FAIL(); + } + ofs_included << "movej([1,2,3,4,5,6])"; + ofs_included.close(); + + std::string processed_script = reader.readScriptFile(existing_script_file); + EXPECT_EQ(processed_script, "movej([1,2,3,4,5,6])"); + + std::remove(existing_script_file); + std::remove(existing_include_file); +} + +TEST_F(ScriptReaderTest, ReplaceVariables) +{ + char existing_script_file[] = "main_script.XXXXXX"; + std::ignore = mkstemp(existing_script_file); + std::ofstream ofs(existing_script_file); + if (ofs.bad()) + { + std::cout << "Failed to create temporary files" << std::endl; + GTEST_FAIL(); + } + ofs << "movej([{{VAR1}}, {{VAR2}}, {{VAR3}}, 0, 0, 0])" << std::endl; + ofs << "local is_true = {{VAR4}}" << std::endl; + ofs << "local is_false = {{VAR5}}" << std::endl; + ofs << "This is just a line without any replacement" << std::endl; + ofs.close(); + + ScriptReader reader; + ScriptReader::DataDict data; + data["VAR1"] = "value1"; + data["VAR2"] = 42; + data["VAR3"] = 6.28; + data["VAR4"] = true; + data["VAR5"] = false; + std::string script = reader.readScriptFile(existing_script_file, data); + + // By default std::to_string will convert double to 6 decimal places + EXPECT_EQ(script, "movej([value1, 42, 6.280000, 0, 0, 0])\nlocal is_true = True\nlocal is_false = False\nThis is " + "just a line without any replacement"); + std::remove(existing_script_file); +} + +TEST_F(ScriptReaderTest, VariableNotInDictThrowsError) +{ + char existing_script_file[] = "main_script.XXXXXX"; + std::ignore = mkstemp(existing_script_file); + std::ofstream ofs(existing_script_file); + if (ofs.bad()) + { + std::cout << "Failed to create temporary files" << std::endl; + GTEST_FAIL(); + } + ofs << "movej([{{VAR1}}, {{VAR2}}, {{VAR3}}, 0, 0, 0])" << std::endl; + ofs << "This is just a line without any replacement" << std::endl; + ofs.close(); + + ScriptReader reader; + ScriptReader::DataDict data; + data["VAR1"] = "value1"; + + EXPECT_THROW(reader.readScriptFile(existing_script_file, data), urcl::UnknownVariable); + std::remove(existing_script_file); +} + +TEST_F(ScriptReaderTest, ReplaceConditionals) +{ + char existing_script_file[] = "main_script.XXXXXX"; + std::ignore = mkstemp(existing_script_file); + std::ofstream ofs(existing_script_file); + if (ofs.bad()) + { + std::cout << "Failed to create temporary files" << std::endl; + GTEST_FAIL(); + } + ofs << + R"({% if is_logged_in %} +Welcome back, {{ username }}! +{% elif is_guest %} + {% if username != "" %} +Welcome, {{ username }}! + {%else %} +Welcome, guest! + {% endif %} +{% else %} +Please log in. +{% endif %} +)"; + ofs.close(); + + ScriptReader reader; + ScriptReader::DataDict data; + data["is_logged_in"] = true; + data["is_guest"] = false; + data["username"] = "test_user"; + + std::string script = reader.readScriptFile(existing_script_file, data); + + EXPECT_EQ(script, "Welcome back, test_user!"); + + data["is_logged_in"] = false; + data["is_guest"] = true; + script = reader.readScriptFile(existing_script_file, data); + EXPECT_EQ(script, "Welcome, test_user!"); + + data["username"] = ""; + script = reader.readScriptFile(existing_script_file, data); + EXPECT_EQ(script, "Welcome, guest!"); + + data["is_guest"] = false; + script = reader.readScriptFile(existing_script_file, data); + EXPECT_EQ(script, "Please log in."); + std::remove(existing_script_file); +} + +TEST_F(ScriptReaderTest, TestNestedConditionals) +{ + char existing_script_file[] = "main_script.XXXXXX"; + std::ignore = mkstemp(existing_script_file); + std::ofstream ofs(existing_script_file); + if (ofs.bad()) + { + std::cout << "Failed to create temporary files" << std::endl; + GTEST_FAIL(); + } + ofs << + R"({% if PI < THE_ANSWER_TO_EVERYTHING %} + {% if TAU < PI %} +It's a strange universe you live in... + {%elif pie_tastes == "great" %} +What's better than a pie? 2 pies! + {% else %} +You don't like pie? + {% endif %} +{% else %} +How can something be greater than the answer to everything? +{% endif %} +)"; + ofs.close(); + + ScriptReader reader; + ScriptReader::DataDict data; + data["PI"] = 3.14; + data["TAU"] = 6.28; + data["THE_ANSWER_TO_EVERYTHING"] = 42; + + data["pie_tastes"] = "great"; + std::string script = reader.readScriptFile(existing_script_file, data); + EXPECT_EQ(script, "What's better than a pie? 2 pies!"); + + data["pie_tastes"] = "aweful"; + script = reader.readScriptFile(existing_script_file, data); + EXPECT_EQ(script, "You don't like pie?"); + + std::remove(existing_script_file); +} + +TEST_F(ScriptReaderTest, CheckCondition) +{ + ScriptReader reader; + ScriptReader::DataDict data; + data["A"] = true; + data["B"] = false; + data["X"] = 5; + data["Y"] = 10; + data["PI"] = 3.14159; + data["S"] = "hello"; + data["T"] = "world"; + + // True/False + EXPECT_TRUE(reader.evaluateExpression(" A", data)); + EXPECT_FALSE(reader.evaluateExpression("B", data)); + + // Equality + EXPECT_TRUE(reader.evaluateExpression("X == 5", data)); + EXPECT_FALSE(reader.evaluateExpression("X == 6", data)); + EXPECT_TRUE(reader.evaluateExpression("S == \"hello\"", data)); + EXPECT_TRUE(reader.evaluateExpression("S == 'hello'", data)); + EXPECT_FALSE(reader.evaluateExpression("S == \"world\"", data)); + EXPECT_FALSE(reader.evaluateExpression("S == 'world'", data)); + EXPECT_FALSE(reader.evaluateExpression("S == T", data)); + EXPECT_TRUE(reader.evaluateExpression("S == S", data)); + EXPECT_TRUE(reader.evaluateExpression("A == true", data)); + EXPECT_TRUE(reader.evaluateExpression("A == True", data)); + EXPECT_TRUE(reader.evaluateExpression("A == TRUE", data)); + EXPECT_TRUE(reader.evaluateExpression("A == 1", data)); + EXPECT_TRUE(reader.evaluateExpression("A == on", data)); + EXPECT_TRUE(reader.evaluateExpression("A == On", data)); + EXPECT_TRUE(reader.evaluateExpression("A == ON", data)); + EXPECT_FALSE(reader.evaluateExpression("B == true", data)); + EXPECT_FALSE(reader.evaluateExpression("B == True", data)); + EXPECT_FALSE(reader.evaluateExpression("B == TRUE", data)); + EXPECT_FALSE(reader.evaluateExpression("B == 1", data)); + EXPECT_FALSE(reader.evaluateExpression("B == on", data)); + EXPECT_FALSE(reader.evaluateExpression("B == On", data)); + EXPECT_FALSE(reader.evaluateExpression("B == ON", data)); + EXPECT_FALSE(reader.evaluateExpression("A == B", data)); + + // Inequality + EXPECT_TRUE(reader.evaluateExpression("X != 6", data)); + EXPECT_FALSE(reader.evaluateExpression("X != 5", data)); + EXPECT_TRUE(reader.evaluateExpression("A != B", data)); + + // Greater/Less + EXPECT_TRUE(reader.evaluateExpression("Y > X", data)); + EXPECT_FALSE(reader.evaluateExpression("X > Y", data)); + EXPECT_TRUE(reader.evaluateExpression("X < Y", data)); + EXPECT_FALSE(reader.evaluateExpression("Y < X", data)); + EXPECT_TRUE(reader.evaluateExpression("PI > 3", data)); + EXPECT_FALSE(reader.evaluateExpression("PI < 3", data)); + EXPECT_TRUE(reader.evaluateExpression("PI >= 3.14159", data)); + EXPECT_FALSE(reader.evaluateExpression("PI < 3.14159", data)); + EXPECT_TRUE(reader.evaluateExpression("PI < X", data)); + + // String not empty + EXPECT_TRUE(reader.evaluateExpression("S != ''", data)); + EXPECT_TRUE(reader.evaluateExpression("S != \"\"", data)); + EXPECT_FALSE(reader.evaluateExpression("S == \"\"", data)); + + // Provoke errors + // Non-existing operator + EXPECT_THROW(reader.evaluateExpression("X ~= 5", data), std::runtime_error); + EXPECT_THROW(reader.evaluateExpression("This is not an expression at all", data), std::runtime_error); + EXPECT_TRUE(reader.evaluateExpression("S != \"This is not an expression at all\"", data)); + // Non-existing variable + EXPECT_THROW(reader.evaluateExpression("non_existing == 5", data), urcl::UnknownVariable); + EXPECT_THROW(reader.evaluateExpression("A == non_existing", data), urcl::UnknownVariable); + EXPECT_THROW(reader.evaluateExpression("IDONTEXIST", data), urcl::UnknownVariable); + // <, >, <=, >= is only available for numeric types + EXPECT_THROW(reader.evaluateExpression("A < 5", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("S < T", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("X < True", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("A > 5", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("S > T", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("X > True", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("A <= 5", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("S <= T", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("X <= True", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("A >= 5", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("S >= T", data), std::invalid_argument); + EXPECT_THROW(reader.evaluateExpression("X >= True", data), std::invalid_argument); +} + +TEST_F(ScriptReaderTest, DataVariantOperators) +{ + ScriptReader::DataDict data; + data["int1"] = 5; + data["int2"] = 10; + data["double1"] = 3.14; + data["double2"] = 3.14; + data["str1"] = "foo"; + data["str2"] = "bar"; + data["bool1"] = true; + data["bool2"] = false; + data["version1"] = urcl::VersionInformation::fromString("10.7.0"); + data["version2"] = urcl::VersionInformation::fromString("5.22.1"); + + // Equality + EXPECT_TRUE(data["int1"] == 5); + EXPECT_FALSE(data["int1"] == 6); + EXPECT_TRUE(data["double1"] == 3.14); + EXPECT_FALSE(data["double1"] == data["int1"]); + EXPECT_TRUE(data["str1"] == std::string("foo")); + EXPECT_FALSE(data["str1"] == std::string("bar")); + EXPECT_TRUE(data["bool1"] == true); + EXPECT_FALSE(data["bool2"] == true); + EXPECT_TRUE(data["bool1"] == 1); + EXPECT_TRUE(data["bool1"] == 1.0); + EXPECT_FALSE(data["bool1"] == 0.0); + EXPECT_FALSE(data["bool1"] == 3.14); + EXPECT_FALSE(data["bool1"] == 42); + EXPECT_TRUE(data["version1"] == urcl::VersionInformation::fromString("10.7.0")); + EXPECT_FALSE(data["version2"] == urcl::VersionInformation::fromString("10.7.0")); + EXPECT_FALSE(data["version2"] == data["version1"]); + + // Inequality + EXPECT_TRUE(data["int1"] != 6); + EXPECT_FALSE(data["int1"] != 5); + EXPECT_TRUE(data["str1"] != std::string("bar")); + EXPECT_FALSE(data["str1"] != std::string("foo")); + EXPECT_TRUE(data["version1"] != urcl::VersionInformation::fromString("1.2.3")); + + // Less, Greater, etc. (numeric only) + EXPECT_TRUE(data["int1"] < data["int2"]); + EXPECT_TRUE(data["int2"] > data["int1"]); + EXPECT_TRUE(data["int1"] <= data["int2"]); + EXPECT_TRUE(data["int2"] >= data["int1"]); + EXPECT_TRUE(data["double1"] <= data["double2"]); + EXPECT_TRUE(data["double1"] >= data["double2"]); + EXPECT_FALSE(data["int1"] < data["double1"]); + EXPECT_TRUE(data["version1"] > data["version2"]); + EXPECT_TRUE(data["version1"] >= data["version1"]); + EXPECT_TRUE(data["version1"] <= data["version1"]); + EXPECT_TRUE(data["version2"] < data["version1"]); + + // Invalid comparisons (should throw) + EXPECT_THROW((void)(data["str1"] < data["str2"]), std::invalid_argument); + EXPECT_THROW((void)(data["bool1"] < data["bool2"]), std::invalid_argument); + EXPECT_THROW((void)(data["str1"] > data["str2"]), std::invalid_argument); + EXPECT_THROW((void)(data["bool1"] > data["bool2"]), std::invalid_argument); + EXPECT_THROW(data["str1"] == data["bool1"], std::invalid_argument); + EXPECT_THROW(data["double1"] == data["str1"], std::invalid_argument); +} + +TEST_F(ScriptReaderTest, Example) +{ + std::string existing_script_file = "resources/example_urscript_main.urscript"; + + ScriptReader reader; + ScriptReader::DataDict data; + data["SOFTWARE_VERSION"] = urcl::VersionInformation::fromString("5.9"); + data["feature_name"] = "torque control"; + + std::string processed_script = reader.readScriptFile(existing_script_file, data); + std::string expected_script = " popup(\"The cool new feature is not supported on Software version 5.23.0\")"; + EXPECT_EQ(processed_script, expected_script); + + data["SOFTWARE_VERSION"] = urcl::VersionInformation::fromString("5.23.0"); + processed_script = reader.readScriptFile(existing_script_file, data); + expected_script = " textmsg(\"torque control is a very cool feature!\")"; + EXPECT_EQ(processed_script, expected_script); +} \ No newline at end of file diff --git a/tests/test_ur_driver.cpp b/tests/test_ur_driver.cpp index acb4c9a9b..807131578 100644 --- a/tests/test_ur_driver.cpp +++ b/tests/test_ur_driver.cpp @@ -81,7 +81,10 @@ class UrDriverTest : public ::testing::Test TEST_F(UrDriverTest, read_non_existing_script_file) { const std::string non_existing_script_file = ""; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" EXPECT_THROW(UrDriver::readScriptFile(non_existing_script_file), UrException); +#pragma GCC diagnostic pop } TEST_F(UrDriverTest, read_existing_script_file) @@ -98,7 +101,10 @@ TEST_F(UrDriverTest, read_existing_script_file) std::cout << "Failed to create temporary files" << std::endl; GTEST_FAIL(); } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" EXPECT_NO_THROW(UrDriver::readScriptFile(existing_script_file)); +#pragma GCC diagnostic pop // clean up ofs.close(); @@ -326,4 +332,4 @@ int main(int argc, char* argv[]) } return RUN_ALL_TESTS(); -} +} \ No newline at end of file diff --git a/tests/test_version_information.cpp b/tests/test_version_information.cpp index 99ed70daf..df12dedf8 100644 --- a/tests/test_version_information.cpp +++ b/tests/test_version_information.cpp @@ -106,9 +106,17 @@ TEST(version_information, test_is_e_series) EXPECT_FALSE(v2.isESeries()); } +TEST(version_information, test_to_string) +{ + std::string version_string = "5.5.0.1101319"; + auto v1 = VersionInformation::fromString(version_string); + + EXPECT_EQ(v1.toString(), version_string); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); -} \ No newline at end of file +}