Open Test Framework  
OTX-Mapping Scripting Support

Although OTX is Turing-complete, practical experience shows that not every task should actually be implemented in OTX. In particular, for computationally intensive algorithms or functions that lie outside the actual diagnostic processes, the use of a specialized scripting language is often significantly more efficient without compromising process reliability.

Introduction

Within an OTX procedure, an arbitrary script can be invoked. The invocation is performed using ExecuteDeviceService, which can also be used to configure and evaluate the input and output parameters.

The following scripting languages are supported:

  1. Lua
    When compactness and performance are important, especially when using OTX in embedded systems, Lua is the best choice. Since the OTX runtime already uses Lua, no additional system requirements are necessary.
  2. Python
    Due to the large ecosystem of libraries, complex calculations can be implemented efficiently in Python.

Note: The script files and all their dependencies are stored directly within the PTX, so that no additional dependencies are required.

To use a script file in an OTX project, the following steps must be performed:

  1. Add the script file inside the Mapping Editor
  2. Bind the script parameters to a DeviceServiceSignature
  3. Invoke the script, including parameter handling, in the ExecuteDeviceService inside an OTX procedure
  4. Export a PTX including the script files
  5. Execute the PTX or start the procedure in OTF

Requirements

  1. Lua
    No installation is required, as it is already included in the OTX Runtime.
  2. Python
    • Install Python, either 32-bit or 64-bit, corresponding to the OTX Runtime version being used.
    • Add the Python installation directory to the PATH environment variable, for example: C:\Users\<user>\AppData\Local\Programs\Python\Python313-32\.
    • Set the PYTHONHOME environment variable to the Python installation directory above, for example: PYTHONHOME=C:\Users\<user>\AppData\Local\Programs\Python\Python313-32

Integration of script files

The script files must be added to the project, see project settings and the script in- and out parameter must be bound to a DeviceServiceSignature inside the OTX mapping editor, see the figure below.

Important: The functions to be used in the script file must be marked using comments, see Lua Scripting Example and Python Scripting Example.

Parameter mapping for Lua

For Lua, only functions marked with comments can be detected and used for mapping. To allow a script method’s parameters to be detected, they must be annotated using special comments as follows:

The following comment describes the parameters of the QuickSort function:

--[[OtxDeviceService: in List<Integer> arr, in Integer left, in Integer right, out List<Integer>]]
function QuickSort(arr, left, right)
if left >= right then
return sortedList
end
...
return sortedList
end
Comment Description
--[[OtxDeviceService Bound to DeviceServiceSignature
in List<Integer> arr Input parameter named arr of type List<Integer>
in Integer left Input parameter named left of type Integer
in Integer right Input parameter named right of type Integer
out List<Integer> Return value of type List<Integer>. The output parameter names are optional.

The following comment describes the parameters of the TryParseInt function:

--[[OtxDeviceService: in String str, out Integer num, out Boolean]]
function OtxRuntimeUnitTestMapping.TryParseInt(str)
local n = tonumber(str)
if n ~= nil and math.floor(n) == n then
return n, true
else
return 0, false
end
end
Comment Description
--[[OtxDeviceService Bound to DeviceServiceSignature
in String str Input parameter named str of type String
out Integer num Output parameter named num of type Integer
out Boolean Output parameter of type Integer. The output parameter names are optional; however, their order must match the order of the return values defined in the code.

Parameter mapping for Python

For Python, functions do not need to be marked. All functions and their parameters can be detected because Python supports strict parameter type. However, functions must declare return type annotations if they exist.

The following example shows QuickSort function:

def QuickSort(self, arr: List[int], left: int, right: int) -> List[int]:
if left >= right:
return arr
...
return arr
def try_parse_int(s: str) -> tuple[int, bool]:
try:
n = float(s)
if n.is_integer():
return int(n), True
else:
return 0, False
except (ValueError, TypeError):
return 0, False
@code{.py}
@subsection OtfOtxMappingScriptingSupport_SupportedDataTypes Supported Data Types
All @ref OtfOtxMappingDataTypeConversions_SupportedOTXDataTypes "OTX mapping data types" and their @ref OtfOtxMappingDataTypeConversions_AutomaticConversion "automatic conversions" are supported, except for @ref OpenTestSystem.Otx.Extensions.DataType.DataTypes.Enumeration "Enumeration".
> **Note**: Enumerations are not supported!
A structure is a complex data type that contains other elements. In Lua, a table is used to represent a structure, while in Python, a class is used for the same purpose. It must be marked with a comment to be detected.Structure is a complex datatype and contains other elements inside it. In Lua, Table is used to map with Structure and in Python a Class is used to map with Structure. It needs to be marked with comment to be detected.
@subsubsection OtfOtxMappingScriptingSupport_SupportedDataTypes_Structure_LuaSupport Lua support for Structure
Structures are marked in Lua as follows:
@code{.lua}
--[[OtxStructure: String Form, Map<String, Float> Props, List<Position> Positions]]
local Entity = {
Form = "",
Props = {},
Positions = {}
}
--[[OtxStructure: Integer Y, Integer Y]]
local Position = {
X = 10,
Y = 20
}
Comment Description
--[[OtxStructure Bound to Structure
String Form Structure element named Form of type String
Map<String, Float> Props Structure element named Props of type Map<String, Float>
List<Position> Positions Structure element named Positions of type List<Position>
Integer X Structure element named X of type Integer
Integer Y Structure element named Y of type Integer

Python support for Structure

Python uses strict parameter data types; therefore, data types do not need to be declared in comments. However, property getters must explicitly declare return type annotations.

Structures in Python are declared as follows:

@OtxStructure
class Entity:
# constructor
def __init__(self, data=None):
self._Form: str = ""
self._Props: Dict[str, float] = {}
self._Positions: List[Position] = []
if data:
self._Form = data.get("Form", "")
self._Props = data.get("Props", {})
self._Position = data.get("Positions", [])
@property
@OtxStructureElement
def Form(self) -> str:
return self._Form
@Form.setter
def Form(self, value: str):
self._Form = value
@property
@OtxStructureElement
def Props(self) -> Dict[str, float]:
return self._Props
@Props.setter
def Props(self, value: Dict[str, float]):
self._Props = value
@property
@OtxStructureElement
def Positions(self) -> List[Position]:
return self._Positions
@Props.setter
def Positions(self, value: List[Position]):
self._Positions = value
@OtxStructure
class Position:
# constructor
def __init__(self, data=None):
self._X: int = 10
self._Y: int = 20
if data:
self._X = data.get("X", 10)
self._Y = data.get("Y", 20)
@property
@OtxStructureElement
def X(self) -> int:
return self._X
@X.setter
def X(self, value: int):
self._X = value
@property
@OtxStructureElement
def Y(self) -> int:
return self._Y
@Y.setter
def Y(self, value: int):
self._Y = value

The following table lists all existing OTX elements and their counterparts defined decorators.

Comment Description
@OtxStructure Bound to Structure
@OtxStructureElement This is a structure element, bound to StructureElement

OTL code example for Structure

namespace OtxScriptingSupportExamplePackage1
{
package DataType.StructureSignature Entity(String Form, Map<String, Float> Props, List<Position> Positions);
package DataType.StructureSignature Position(Integer X, Integer Y);
}

Export a PTX including script files

To include the scripts in the PTX, only the checkbox "Runtime files" must be selected in the PTX export dialog. During export, all detected dependencies are copied into the PTX.

Note: The script files are stored in the output directory together with the generated output files.

Dependency Resolution

When exporting a PTX that contains scripts, the following dependencies are resolved recursively to arbitrary depth and copied into the PTX file.

Important: Only relative dependencies located below the script file containing the dependency are detected. During export, the dependency structure is restored in the output directory.

Note: If a dependency cannot be found, an error message is displayed and the export process is aborted. Details can be found in the output window.

Lua Dependency Resolution

  • Basic Imports
    require("core.util") -- dependency: "core.util"
    dofile("main.lua") -- dependency: "main.lua"
    loadfile("core.lua") -- dependency: "core.lua"
    package.loadlib("core.dll", "luaopen_mylib") -- dependency: "core.dll"
    loadlib("core.dll", "luaopen_mylib") -- dependency: "core.dll"
  • Scope handling (block scope, variable shadowing)
    local name = "a"
    do
    local name = "b"
    end
    require(name) -- dependency: "a"
  • Alias resolution (require, dofile, loadfile, loadlib)
    local r = require
    r("network") -- dependency: "network"
    local d = dofile
    d("script.lua") -- dependency: "script.lua"
  • Table reference resolution (including nested tables and alias objects)
    local t = {}
    t.a = {}
    t.a.v = "core.util"
    local t2 = t
    require(t2.a.v) -- dependency: "core.util"
  • String resolution (direct value and concatenation)
    local name = "sys" .. ".core"
    require(name) -- dependency: "sys.core"
  • Conditional and function scope analysis
    function load()
    if cond then
    require("a") -- dependency: "a"
    else
    require("b") -- dependency: "b"
    end
    end
  • Recursive dependency analysis (cross-file dependency tracking)
    -- main.lua
    require("a")
    -- a.lua
    dofile("b.lua")
    -- b.lua
    loadfile("c.lua")
    -- dependency: "a.lua", "b.lua", "c.lua"
  • Circular dependency handling
    -- a.lua
    dofile("b.lua")
    -- b.lua
    dofile("a.lua")
    -- dependency: "a.lua", "b.lua"
  • False-positive filtering (comments, string literals)
    -- require("fake")
    local s = "require('fake')"
    Result: 0 dependencies detected

Important : Please note that only the constructs listed above can be detected. If necessary, rewrite the Lua file accordingly.

Lua Dependency Resolution Limitations

The following limitations apply when resolving dependencies. They currently cannot be detected. Please rewrite the file if necessary.

  • Runtime or dynamic value
    local name = os.getenv("SCRIPT")
    require(name) -- cannot dectect module name
  • Function return
    local function f()
    return "core.util"
    end
    require(f()) -- cannot dectect module name
  • load or loadstring
    local string_code = [[
    require("core.util") -- cannot detect module name (dynamic code)
    print("Hello from string")
    ]]
    local func, err = load(string_code)
    if func then func() else print(err) end

Python Dependency Resolution

  • Basic Imports
    import pandas # dependency: pandas
    import numpy, matplotlib, loguru as lr # dependency: "numpy", "matplotlib", "loguru"
    from pydantic import BaseModel, Field # dependency: "pydantic"
    from requests.exceptions import HTTPError as ConnectionFailedException # dependency: "requests.exceptions"
  • Scope handling (block scope, variable shadowing)
    name = "a"
    def f():
    name = "b"
    pass
    importlib.import_module(name) # dependency: "a"
  • Alias resolution (require, dofile, loadfile, loadlib)
    r = importlib.import_module
    r("network") # dependency: "network"
    rp = runpy.run_path
    rp("script.py") # dependency: "script.py"
  • Table reference resolution (including nested tables and alias objects)
    import importlib
    t = {}
    t["a"] = {}
    t["a"]["v"] = "core.util"
    t2 = t
    importlib.import_module(t2.a.v) # dependency: "core.util"
  • String resolution (direct value and concatenation)
    local name = "sys" + ".core"
    importlib.import_module(name) # dependency: "sys.core"
  • Conditional and function scope analysis
    function load()
    if cond then
    import a # dependency: "a"
    else
    import b # dependency: "b"
    end
    end
  • Recursive dependency analysis (cross-file dependency tracking)
    # main.py
    import scripts.a
    # a.py
    import scripts.b
    # b.py
    import scripts.c
    # dependency: "scripts.a", "scripts.b", "scripts.c"
  • Circular dependency handling
    # a.py
    import scripts.b
    # b.py
    import scripts.a
    # dependency: "scripts.a", "scripts.b"
  • False-positive filtering (comments, string literals)
    # from fake
    local s = "from fake"
    Result: 0 dependencies detected

Important : Please note that only the constructs listed above can be detected. If necessary, rewrite the Python file accordingly.

Python Dependency Resolution Limitations

The following limitations apply when resolving dependencies. They currently cannot be detected. Please rewrite the file if necessary.

  • Runtime or dynamic value
    name = os.getenv("SCRIPT")
    importlib.import_module(name) # cannot dectect module name
  • Function return
    def f()
    return "core.util"
    end
    importlib.import_module(f()) # cannot dectect module name

Lua Scripting Example

The following examples demonstrates the execution of a Lua function inside OTX. The executable PTX file can be downloaded:

Executable example Lua PTX

Lua code example

local OtxScriptingSupportExample = {}
--[[OtxDeviceService: in Integer length, out List<Integer>]]
function OtxScriptingSupportExample.RandomList(n)
local t = {}
for i = 1, n do
t[i] = math.random(1, 1000000)
end
return t
end
--[[OtxDeviceService: in List<String> arr, out String]]
function OtxScriptingSupportExample.StringConcat(arr)
return table.concat(arr, "")
end
--[[OtxDeviceService: in List<Integer> arr, in Integer left, in Integer right, out List<Integer>]]
function OtxScriptingSupportExample.QuickSort(arr, left, right)
left = left or 1
right = right or #arr
if left == 0 then
left = 1
right = right + 1
end
if left >= right then
return arr
end
local pivot = arr[math.floor((left + right) / 2)]
local i, j = left, right
while i <= j do
while arr[i] < pivot do i = i + 1 end
while arr[j] > pivot do j = j - 1 end
if i <= j then
arr[i], arr[j] = arr[j], arr[i]
i = i + 1
j = j - 1
end
end
if left < j then
OtxScriptingSupportExample.QuickSort(arr, left, j)
end
if i < right then
OtxScriptingSupportExample.QuickSort(arr, i, right)
end
return arr
end
--[[OtxStructure: String Type, Map<String, Float> Props]]
local Shape = {
Type = "",
Props = {}
}
-- constructor
function Shape:new(o)
o = o or {}
local obj = {}
-- copy default values if not provided
for k,v in pairs(self) do
-- skip functions
if type(v) ~= "function" then
obj[k] = o[k] or v
end
end
obj.Props = o.Props or {}
setmetatable(obj, { __index = self })
return obj
end
--[[OtxDeviceService: in Float width, in Float height, out Shape]]
function OtxScriptingSupportExample.CreateRectangle(width, height)
local r = Shape:new({
Type = "Rectangle",
Props = {
Width = width or 0.0,
Height = height or 0.0
}
})
return r
end
--[[OtxDeviceService: in Shape shape, out Float]]
function OtxScriptingSupportExample.ComputeArea(shape)
if shape.Type == "Circle" then
return 3.14 * shape.Props.Radius * shape.Props.Radius
elseif shape.Type == "Rectangle" then
return shape.Props.Width * shape.Props.Height
else
error("Unknown shape type")
end
end
--[[OtxDeviceService: in Float radius, out BlackBox]]
function OtxScriptingSupportExample.CreateCircle(radius)
local c = Shape:new({
Type = "Circle",
Props = {
Radius = radius or 0.0
}
})
return c
end
--[[OtxDeviceService: in BlackBox shape, out Float]]
function OtxScriptingSupportExample.ComputePerimeter(shape)
if shape.Type == "Circle" then
-- Perimeter = 2 * π * r
return 2 * 3.14 * shape.Props.Radius
elseif shape.Type == "Rectangle" then
-- Perimeter = 2 * (w + h)
return 2 * (shape.Props.Width + shape.Props.Height)
else
error("Unknown shape type: " .. tostring(shape.Type))
end
end
--[[OtxDeviceService: in Map<String, Float> map, out Map<String, Shape>]]
function OtxScriptingSupportExample.CreateShapes(map)
local result = {}
for key, value in pairs(map) do
if type(value) ~= "number" then
error("Value for key '" .. tostring(key) .. "' must be float")
end
if key == "Circle" then
result[key] = Shape:new({
Type = "Circle",
Props = {
Radius = value or 0.0
}
})
elseif key == "Rectangle" then
result[key] = Shape:new({
Type = "Rectangle",
Props = {
Width = value or 0.0,
Height = value or 0.0
}
})
else
error("Shape '" .. tostring(key) .. "' not supported")
end
end
return result
end
--[[OtxDeviceService: in String versionString, out Integer major, out Integer minor, out Integer patch, out Integer build]]
function OtxScriptingSupportExample.ParseVersion(versionString)
local result = {}
for num in versionString:gmatch("%d+") do
table.insert(result, tonumber(num))
end
for i = #result + 1, 4 do
result[i] = 0
end
return result[1], result[2], result[3], result[4]
end
return OtxScriptingSupportExample

OTX/OTL code example which calls functions inside the Lua code

namespace OtxScriptingSupportExamplePackage1
{
package DataType.StructureSignature Shape(String Type, Map<String, Float> Props);
package Measure.DeviceSignature OtxScriptingSupportExample
{
Measure.DeviceServiceSignature RandomList(in Integer length, out List<Integer> Result);
Measure.DeviceServiceSignature QuickSort(
in List<Integer> arr,
in Integer left,
in Integer right,
out List<Integer> Result
);
Measure.DeviceServiceSignature CreateRectangle(in Float width, in Float height, out Shape Result);
Measure.DeviceServiceSignature ComputeArea(in Shape shape, out Float Result);
Measure.DeviceServiceSignature ParseVersion(
in String versionString,
out Integer major,
out Integer minor,
out Integer patch,
out Integer build
);
}
public procedure main(in Integer listLength = 1000)
{
List<Integer> randomList;
List<Integer> sortedList;
Integer time;
Measure.ExecuteDeviceService(OtxScriptingSupportExample, RandomList, {length = listLength, Result = randomList}, false, false);
time = DateTime.GetTimestamp();
Measure.ExecuteDeviceService(OtxScriptingSupportExample, QuickSort, {arr = randomList, left = 1, right = ListGetLength(randomList), Result = sortedList}, false, false);
time = DateTime.GetTimestamp() - time;
HMI.ConfirmDialog(ToString(sortedList), StringUtil.StringConcatenate({"List sorted via Lua in ", ToString(time), " ms"}));
time = DateTime.GetTimestamp();
QuickSort(ref randomList, 0, ListGetLength(randomList) - 1);
time = DateTime.GetTimestamp() - time;
HMI.ConfirmDialog(ToString(sortedList), StringUtil.StringConcatenate({"List sorted via OTX in ", ToString(time), " ms"}));
}
private procedure QuickSort(ref List<Integer> arr, in Integer left, in Integer right)
{
Integer pivot;
Integer i;
Integer j;
Integer mem;
pivot = arr[(left + right) / 2];
i = left;
j = right;
while (i <= j) : w1
{
while (arr[i] < pivot) : w2
{
i = i + 1;
}
while (arr[j] > pivot) : w3
{
j = j - 1;
}
if (i <= j)
{
mem = arr[j];
arr[j] = arr[i];
arr[i] = mem;
i = i + 1;
j = j - 1;
}
}
if (left < j)
{
QuickSort(ref arr, left, j);
}
if (i < right)
{
QuickSort(ref arr, i, right);
}
}
public procedure ParseVersion(in String VersionString, out Integer major, out Integer minor, out Integer patch, out Integer buid)
{
Measure.ExecuteDeviceService(OtxScriptingSupportExample, ParseVersion, {versionString = VersionString, major = major, minor = minor, patch = patch, build = buid}, false, false);
}
}

Python Scripting Example

The following examples demonstrates the execution of a Python function inside OTX. The executable PTX file can be downloaded:

Executable example Python PTX

Python code example

from typing import List, Dict, Optional
import random
import re # regex engine
def OtxStructure(cls):
return cls
def OtxStructureElement(func):
return func
def OtxException(exceptionType):
def decorator(func):
return func
return decorator
@OtxStructure
class Shape:
# constructor
def __init__(self, data=None):
self.Type: str = ""
self.Props: Dict[str, float] = {}
if data:
self._Type = data.get("Type", "")
self._Props = data.get("Props", False)
@property
@OtxStructureElement
def Type(self) -> str:
return self._Type
@Type.setter
def Type(self, value: str):
self._Type = value
@property
@OtxStructureElement
def Props(self) -> Dict[str, float]:
return self._Props
@Props.setter
def Props(self, value: Dict[str, float]):
self._Props = value
class OtxScriptingSupportExample:
def RandomList(self, length: int) -> List[int]:
if length < 0:
return []
return [random.randint(0, 1000000) for _ in range(length)]
def StringConcat(self, arr: List[str]) -> str:
return "".join(arr)
def QuickSort(self, arr: List[int], left: int, right: int) -> List[int]:
if arr is None or len(arr) == 0:
return arr
if left is None:
left = 0
if right is None:
right = len(arr) - 1
def partition(a, l, r):
pivot = a[r]
i = l - 1
for j in range(l, r):
if a[j] <= pivot:
i += 1
a[i], a[j] = a[j], a[i]
a[i + 1], a[r] = a[r], a[i + 1]
return i + 1
if left < right:
pi = partition(arr, left, right)
self.QuickSort(arr, left, pi - 1)
self.QuickSort(arr, pi + 1, right)
return arr
def CreateShapes(self, map: Dict[str, float]) -> Dict[str, Shape]:
result: Dict[str, Shape] = {}
for key, value in map.items():
if not isinstance(value, float):
raise TypeError(f"Value for key '{key}' must be float")
if key == "Circle":
result[key] = Shape({
"Type": "Circle",
"Props": {
"Radius": value or 0.0
}
})
elif key == "Rectangle":
result[key] = Shape({
"Type": "Rectangle",
"Props": {
"Width": value or 0.0,
"Height": value or 0.0
}
})
else:
raise TypeError(f"Shape '{key}' not supported")
return result
def ComputeArea(self, shape: Shape) -> float:
if shape.Type == "Circle":
r = shape.Props.get("Radius", 0.0)
return 3.14 * r * r
elif shape.Type == "Rectangle":
w = shape.Props.get("Width", 0.0)
h = shape.Props.get("Height", 0.0)
return w * h
raise ValueError(f"Unknown shape type: {shape.Type}")
def ParseVersion(self, versionString: str) -> tuple[int, int, int, int]:
nums = [int(n) for n in re.findall(r"\d+", versionString)]
nums += [0] * (4 - len(nums))
return nums[0], nums[1], nums[2], nums[3]

OTX/OTL code example which calls functions inside the Python code

namespace OtxScriptingSupportExamplePackage1
{
package DataType.StructureSignature Shape(String Type, Map<String, Float> Props);
package Measure.DeviceSignature OtxScriptingSupportExample
{
Measure.DeviceServiceSignature RandomList(in Integer length, out List<Integer> Result);
Measure.DeviceServiceSignature QuickSort(
in List<Integer> arr,
in Integer left,
in Integer right,
out List<Integer> Result
);
Measure.DeviceServiceSignature CreateRectangle(in Float width, in Float height, out Shape Result);
Measure.DeviceServiceSignature ComputeArea(in Shape shape, out Float Result);
Measure.DeviceServiceSignature ParseVersion(
in String versionString,
out Integer major,
out Integer minor,
out Integer patch,
out Integer build
);
}
public procedure main(in Integer listLength = 1000)
{
List<Integer> randomList;
List<Integer> sortedList;
Integer time;
Measure.ExecuteDeviceService(OtxScriptingSupportExample, RandomList, {length = listLength, Result = randomList}, false, false);
time = DateTime.GetTimestamp();
Measure.ExecuteDeviceService(OtxScriptingSupportExample, QuickSort, {arr = randomList, left = 0, right = ListGetLength(randomList) - 1, Result = sortedList}, false, false);
time = DateTime.GetTimestamp() - time;
HMI.ConfirmDialog(ToString(sortedList), StringUtil.StringConcatenate({"List sorted via Python in ", ToString(time), " ms"}));
time = DateTime.GetTimestamp();
QuickSort(ref randomList, 0, ListGetLength(randomList) - 1);
time = DateTime.GetTimestamp() - time;
HMI.ConfirmDialog(ToString(sortedList), StringUtil.StringConcatenate({"List sorted via OTX in ", ToString(time), " ms"}));
}
private procedure QuickSort(ref List<Integer> arr, in Integer left, in Integer right)
{
Integer pivot;
Integer i;
Integer j;
Integer mem;
pivot = arr[(left + right) / 2];
i = left;
j = right;
while (i <= j) : w1
{
while (arr[i] < pivot) : w2
{
i = i + 1;
}
while (arr[j] > pivot) : w3
{
j = j - 1;
}
if (i <= j)
{
mem = arr[j];
arr[j] = arr[i];
arr[i] = mem;
i = i + 1;
j = j - 1;
}
}
if (left < j)
{
QuickSort(ref arr, left, j);
}
if (i < right)
{
QuickSort(ref arr, i, right);
}
}
public procedure ParseVersion(in String VersionString, out Integer major, out Integer minor, out Integer patch, out Integer buid)
{
Measure.ExecuteDeviceService(OtxScriptingSupportExample, ParseVersion, {versionString = VersionString, major = major, minor = minor, patch = patch, build = buid}, false, false);
}
}

OpenTestSystem::Otx::Extensions::DataType::DataTypes::Enumeration