Skip to content

Commit f0ee8ca

Browse files
urfeexurmahp
andauthored
Add ScriptReader for script template parsing (#343)
This PR adds a script parser that extends the templating capabilities when loading script code. In particular that allows - Variable substitution (exists already, but this was refactored) - Conditionals - Includes With this we can add script code that only compiles on later software versions only if the required version is used. This way, older software versions can still be used, the new feature will simply not be present there. So far, when using new script code functions, the code would not have compiled on newer versions effectively increasing the minimal required software version. --------- Co-authored-by: Mads Holm Peters <[email protected]>
1 parent 6825b64 commit f0ee8ca

21 files changed

+1354
-61
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ add_library(urcl
1818
src/comm/tcp_socket.cpp
1919
src/comm/tcp_server.cpp
2020
src/control/reverse_interface.cpp
21+
src/control/script_reader.cpp
2122
src/control/script_sender.cpp
2223
src/control/trajectory_point_interface.cpp
2324
src/control/script_command_interface.cpp

doc/architecture.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ well as a couple of standalone modules to directly use subsets of the library's
1313
architecture/reverse_interface
1414
architecture/rtde_client
1515
architecture/script_command_interface
16+
architecture/script_reader
1617
architecture/script_sender
1718
architecture/trajectory_point_interface
1819
architecture/ur_driver

doc/architecture/script_reader.rst

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
:github_url: https://github.com/UniversalRobots/Universal_Robots_Client_Library/blob/master/doc/architecture/script_reader.rst
2+
3+
.. _script_reader:
4+
5+
ScriptReader
6+
============
7+
8+
Script code used by the :ref:`script_sender` is read from a file. That script code might have to be
9+
dynamically modified based on some configuration input. For example, if the script code contains
10+
connections to a remote PC, that PC's IP address might be configured by the user. Another example
11+
would be to include certain parts of the script code only if the robot's software version supports
12+
that.
13+
14+
For that purpose the ``ScriptReader`` class is provided. It reads the script code from a file and
15+
performs the following substitutions:
16+
17+
- Replaces variables in the form of ``{{variable_name}}`` with the value of the variable
18+
from a provided dictionary.
19+
- Includes other script files using the directive ``{% include file_name %}``. The included file is
20+
read from the same directory as the main script file. Nested includes are also possible.
21+
- Use conditionals in order to add certain parts of the script code only if a condition matches.
22+
23+
The supported substitutions use a basic implementation of the `Jinja2 templating engine` syntax.
24+
25+
.. note::
26+
One special literal is defined for **version information**. Use a software version prefixed with a
27+
``v`` character, e.g. ``v10.8.0`` to encode a software version. Version information entries can be
28+
compared with each other.
29+
30+
**Do not** wrap version information into quotes, as this will be interpreted as a string.
31+
32+
Example
33+
-------
34+
35+
Given two script files:
36+
37+
.. literalinclude:: ../../tests/resources/example_urscript_main.urscript
38+
:caption: tests/resources/example_urscript_main.urscript
39+
:linenos:
40+
:lineno-match:
41+
42+
.. literalinclude:: ../../tests/resources/example_urscript_feature.urscript
43+
:language: python
44+
:caption: tests/resources/example_urscript_feature.urscript
45+
:linenos:
46+
:lineno-match:
47+
48+
The dictionary entry for ``feature_name`` is "torque control".
49+
50+
Depending on the ``SOFTWARE_VERSION`` entry in the dictionary passed to the
51+
``ScriptReader``, the script code will be read as follows:
52+
53+
Given ``SOFTWARE_VERSION = v5.21.0``, the script code will be:
54+
55+
.. code-block:: python
56+
57+
popup("The cool new feature is not supported on Software version 5.23.0")
58+
59+
60+
Given ``SOFTWARE_VERSION = v5.23.0``, the script code will be:
61+
62+
.. code-block:: python
63+
64+
textmsg("torque control is a very cool feature!")
65+
66+
Supported Data
67+
--------------
68+
69+
Data dictionary (C++ side)
70+
~~~~~~~~~~~~~~~~~~~~~~~~~~
71+
72+
The data dictionary supports the following types
73+
74+
- ``str``: A string value, e.g. "Hello World"
75+
- ``int``: An integer value, e.g. 42
76+
- ``double``: A floating point value, e.g. 3.14
77+
- ``bool``: A boolean value, e.g. ``true`` or ``false``
78+
- ``VersionInformation``: A version information value, e.g. ``VersionInformation::fromString("10.8.0")``
79+
80+
Script code side
81+
~~~~~~~~~~~~~~~~
82+
83+
Variable replacements
84+
^^^^^^^^^^^^^^^^^^^^^
85+
86+
Variable replacements in the script code are done using the syntax ``{{ variable_name }}``. For
87+
this to work, the variable ``variable_name`` has to be defined in the data dictionary passed to the
88+
``ScriptReader``.
89+
90+
The expression ``{{ variable_name }}`` will be replaced with the string representation of the
91+
variable's content.
92+
93+
- If the variable is a string, it has to be wrapped into quotes in the script
94+
code. e.g.
95+
96+
.. code-block:: python
97+
98+
textmsg("{{ log_message }}")
99+
100+
101+
- Boolean variables will be replaced with the string ``True`` or ``False``.
102+
- Numeric variables (integer and floating point) will be replaced with the string representation generated by
103+
`std::to_string() <https://en.cppreference.com/w/cpp/string/basic_string/to_string>`_.
104+
- Version information variables will be replaced with the string representation similar to
105+
``10.7.0.0``
106+
107+
Boolean expressions
108+
^^^^^^^^^^^^^^^^^^^
109+
110+
Boolean expressions have to follow one of two possible syntax variations
111+
112+
- Direct evaluation of a boolean variable from the data dictionary:
113+
114+
.. code-block::
115+
116+
boolean_variable_name
117+
118+
- Comparison of a variable with a value using an operator.
119+
120+
.. code-block::
121+
122+
variable_name operator value
123+
124+
The operator has to be one of ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``. On the lefthand side
125+
of the operator there has to be a variable from the data dictionary. The right hand side can be
126+
either a variable name or a value. If the right hand side is a variable name, it has to be
127+
defined in the data dictionary as well. Values will be parsed as follows:
128+
129+
- Strings: Wrapped in quotes, e.g. ``"Hello World"`` or ``'Universal Robots'``.
130+
- Numerical values such as ``42``, ``3.14``, ``-1``, ``1e-12``.
131+
- Boolean values: See below.
132+
- Version information: Prefixed with a ``v`` character, e.g. ``v10.8.0``, ``v5.23.0``.
133+
134+
Boolean values parsing
135+
^^^^^^^^^^^^^^^^^^^^^^
136+
137+
Boolean values can be parsed from the following strings:
138+
139+
- ``true``, ``True``, ``TRUE``
140+
- ``on``, ``On``, ``ON``
141+
- ``yes``, ``Yes``, ``YES``
142+
- ``1``
143+
- ``false``, ``False``, ``FALSE``
144+
- ``off``, ``Off``, ``OFF``
145+
- ``no``, ``No``, ``NO``
146+
- ``0``
147+
148+
Conditional blocks
149+
^^^^^^^^^^^^^^^^^^
150+
151+
Conditional blocks have to be started with a ``{% if condition %}`` directive and closed with a
152+
``{% endif %}`` directive. The condition can be any boolean expression as described above.
153+
154+
The ``{% elif condition %}`` and ``{% else %}`` directives can be used to add alternative paths.
155+
156+
Conditional blocks can be nested.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// -- BEGIN LICENSE BLOCK ----------------------------------------------
2+
// Copyright 2025 Universal Robots A/S
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are met:
6+
//
7+
// * Redistributions of source code must retain the above copyright
8+
// notice, this list of conditions and the following disclaimer.
9+
//
10+
// * Redistributions in binary form must reproduce the above copyright
11+
// notice, this list of conditions and the following disclaimer in the
12+
// documentation and/or other materials provided with the distribution.
13+
//
14+
// * Neither the name of the {copyright_holder} nor the names of its
15+
// contributors may be used to endorse or promote products derived from
16+
// this software without specific prior written permission.
17+
//
18+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
// POSSIBILITY OF SUCH DAMAGE.
29+
// -- END LICENSE BLOCK ------------------------------------------------
30+
31+
#pragma once
32+
#include <filesystem>
33+
#include <string>
34+
#include <unordered_map>
35+
#include <variant>
36+
37+
#include <ur_client_library/ur/datatypes.h>
38+
#include <ur_client_library/ur/version_information.h>
39+
40+
namespace urcl
41+
{
42+
namespace control
43+
{
44+
/*!
45+
* \brief This class handles reading script files parsing special instructions that will get replaced.
46+
*
47+
* When parsing the script code, it is supported to have
48+
* - Variable replacements using `{{ VARIABLE_NAME }}`
49+
* - Including other files using `{% include <filename> %}`.
50+
* The filename has to be relative to the root script file's folder
51+
* - Conditionals using
52+
*
53+
* {% if <condition %}
54+
* ...
55+
* {% elif <condition> %}
56+
* ...
57+
* {% else %}
58+
* ...
59+
* {% endif %}
60+
*
61+
*
62+
* Those directives use Jinja2 notation.
63+
*/
64+
class ScriptReader
65+
{
66+
public:
67+
using DataVariant = std::variant<std::string, double, int, bool, VersionInformation>;
68+
using DataDict = std::unordered_map<std::string, DataVariant>;
69+
70+
ScriptReader() = default;
71+
72+
/*!
73+
* \brief Reads a script file and applies variable replacements, includes, and conditionals.
74+
* \param file_path Path of the script file to be loaded.
75+
* \param data Data dictionary used for variable replacements and expression evaluation.
76+
* \return The Script code with all replacements, includes and conditionals applied.
77+
*/
78+
std::string readScriptFile(const std::string& file_path, const DataDict& data = DataDict());
79+
80+
/*!
81+
* \brief Evaluate a boolean expression
82+
* \param expression The boolean expression to be evaluated.
83+
* \param data A data dictionary that will be used when evaluating the expressions
84+
* \return The result of evaluating the boolean expression
85+
*/
86+
static bool evaluateExpression(const std::string& expression, const DataDict& data);
87+
88+
private:
89+
enum BlockType
90+
{
91+
IF,
92+
ELIF,
93+
ELSE
94+
};
95+
struct BlockState
96+
{
97+
BlockType type;
98+
bool condition_matched; // Has any previous condition in this block matched?
99+
bool should_render; // Should this block render?
100+
bool parent_render; // Is the parent block rendering?
101+
};
102+
103+
std::filesystem::path script_path_;
104+
105+
static std::string readFileContent(const std::string& file_path);
106+
void replaceIncludes(std::string& script_code, const DataDict& data);
107+
static void replaceVariables(std::string& script_code, const DataDict& data);
108+
static void replaceConditionals(std::string& script_code, const DataDict& data);
109+
};
110+
111+
bool operator<(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs);
112+
bool operator>(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs);
113+
bool operator==(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs);
114+
115+
inline bool operator!=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs)
116+
{
117+
return !(lhs == rhs);
118+
}
119+
inline bool operator<=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs)
120+
{
121+
return (lhs < rhs || lhs == rhs);
122+
}
123+
inline bool operator>=(const ScriptReader::DataVariant& lhs, const ScriptReader::DataVariant& rhs)
124+
{
125+
return (lhs > rhs || lhs == rhs);
126+
}
127+
} // namespace control
128+
} // namespace urcl

include/ur_client_library/exceptions.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,17 @@ class UnsupportedMotionType : public UrException
213213
{
214214
}
215215
};
216+
217+
class UnknownVariable : public UrException
218+
{
219+
private:
220+
std::string text_;
221+
222+
public:
223+
explicit UnknownVariable() = delete;
224+
explicit UnknownVariable(const std::string& variable_name) : std::runtime_error("Unknown variable: " + variable_name)
225+
{
226+
}
227+
};
216228
} // namespace urcl
217-
#endif // ifndef UR_CLIENT_LIBRARY_EXCEPTIONS_H_INCLUDED
229+
#endif // ifndef UR_CLIENT_LIBRARY_EXCEPTIONS_H_INCLUDED

include/ur_client_library/helpers.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#ifndef UR_CLIENT_LIBRARY_HELPERS_H_INCLUDED
3030
#define UR_CLIENT_LIBRARY_HELPERS_H_INCLUDED
3131

32+
#include <string>
3233
#include <chrono>
3334
#include <functional>
3435
#ifdef _WIN32
@@ -78,5 +79,25 @@ bool setFiFoScheduling(pthread_t& thread, const int priority);
7879
*/
7980
void waitFor(std::function<bool()> condition, const std::chrono::milliseconds timeout,
8081
const std::chrono::milliseconds check_interval = std::chrono::milliseconds(50));
82+
83+
/*!
84+
* \brief Parses a boolean value from a string.
85+
*
86+
* The string can be one of
87+
* - true, True, TRUE
88+
* - on, On, ON
89+
* - yes, Yes, YES
90+
* - 1
91+
* - false, False, FALSE
92+
* - off, Off, OFF
93+
* - no, No, NO
94+
* - 0
95+
*
96+
* \param str string to be parsed
97+
* \throws urcl::UrException If the string doesn't match one of the options
98+
* \return The boolean representation of the string
99+
*/
100+
bool parseBoolean(const std::string& str);
101+
81102
} // namespace urcl
82103
#endif // ifndef UR_CLIENT_LIBRARY_HELPERS_H_INCLUDED

include/ur_client_library/ur/datatypes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
#include <ur_client_library/types.h>
3232
#include "ur_client_library/log.h"
33+
#include <sstream>
3334

3435
namespace urcl
3536
{

0 commit comments

Comments
 (0)