Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d17a43b
Use a separate class to read script code from a file
urfeex Jun 6, 2025
4e96383
WIP: Add parsing include statements
urfeex Jun 6, 2025
3b387b5
Use replacement dictionary for replacing variables
urfeex Jun 6, 2025
bef8761
Add parsing of simple conditionals
urfeex Jun 10, 2025
a552f62
Implemented parsing conditional expressions
urfeex Jun 11, 2025
a00ede6
Add a test for conditionally including a file
urfeex Jun 11, 2025
3b14693
WIP: Support VersionInformation in ScriptDreader DataVariant
urfeex Jun 11, 2025
f520cdf
Doxygen documentation of code
urfeex Jun 12, 2025
0a6bfa6
Add sphinx documentation and an example for the ScriptReader
urfeex Jun 12, 2025
72a9eab
Make sure sent script has an empty line
urfeex Jun 12, 2025
dc1c17e
Add missing algorithm include
urfeex Jun 12, 2025
e6f99c9
Explicitly convert match to string
urfeex Jun 12, 2025
8ba6e2b
More windows fixes
urfeex Jun 12, 2025
de3e099
Add missing test
urfeex Jun 12, 2025
33adccb
Add test for VersionInformation::toString()
urfeex Jun 13, 2025
af08325
Add changes from code review
urfeex Jun 13, 2025
76f8d01
Add documentation about variable substitutions
urfeex Jun 13, 2025
5fdc8d0
Apply suggestions from code review
urfeex Jun 13, 2025
c1c5e3a
Fix parsing elifs
urfeex Jun 13, 2025
97f4d26
Re-sort variable declaration in tests
urfeex Jun 13, 2025
1d04975
Revert and deprecate changing the UrDriver's readScriptFile method
urfeex Jun 13, 2025
b366542
Add a time frame to deprecation notice
urfeex Jun 16, 2025
fc04b1f
Also test yes/no in parseBoolean test
urfeex Jun 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions doc/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
156 changes: 156 additions & 0 deletions doc/architecture/script_reader.rst
Original file line number Diff line number Diff line change
@@ -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() <https://en.cppreference.com/w/cpp/string/basic_string/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.
128 changes: 128 additions & 0 deletions include/ur_client_library/control/script_reader.h
Original file line number Diff line number Diff line change
@@ -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 <filesystem>
#include <string>
#include <unordered_map>
#include <variant>

#include <ur_client_library/ur/datatypes.h>
#include <ur_client_library/ur/version_information.h>

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 <filename> %}`.
* The filename has to be relative to the root script file's folder
* - Conditionals using
*
* {% if <condition %}
* ...
* {% elif <condition> %}
* ...
* {% else %}
* ...
* {% endif %}
*
*
* Those directives use Jinja2 notation.
*/
class ScriptReader
{
public:
using DataVariant = std::variant<std::string, double, int, bool, VersionInformation>;
using DataDict = std::unordered_map<std::string, DataVariant>;

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
14 changes: 13 additions & 1 deletion include/ur_client_library/exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 21 additions & 0 deletions include/ur_client_library/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#ifndef UR_CLIENT_LIBRARY_HELPERS_H_INCLUDED
#define UR_CLIENT_LIBRARY_HELPERS_H_INCLUDED

#include <string>
#include <chrono>
#include <functional>
#ifdef _WIN32
Expand Down Expand Up @@ -78,5 +79,25 @@ bool setFiFoScheduling(pthread_t& thread, const int priority);
*/
void waitFor(std::function<bool()> 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
1 change: 1 addition & 0 deletions include/ur_client_library/ur/datatypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include <ur_client_library/types.h>
#include "ur_client_library/log.h"
#include <sstream>

namespace urcl
{
Expand Down
Loading
Loading