Property system
openDAQ™ provides a Property system that allows for custom Properties with metadata descriptions to be added to data acquisition components represented in its architecture. It allows for Properties such as a Device's sampling rate or an FFT Function Block's line count to be customized by users. Any Property on a component can be read/written (if the Property is not read-only) by openDAQ™ applications. The Properties are the main building blocks of any user interface implementation that wishes to interact with openDAQ™ devices.
Simple Property Object example
Property Objects are containers of Properties. Each Property Object holds the metadata describing a Property as well as its value. As each Component is also a Property Object, it inherits all its features and uses.
Let’s start with a simple Property Object example.
-
Cpp
-
Python
#include <coreobjects/Property_object_factory.h>
#include <coreobjects/Property_factory.h>
#include <iostream>
using namespace daq;
PropertyObjectPtr createSimplePropertyObject()
{
auto propObj = PropertyObject();
propObj.addProperty(StringProperty("MyString", "foo"));
propObj.addProperty(IntProperty("MyInteger", 0));
return propObj;
}
int main()
{
auto propObj = createSimplePropertyObject();
std::cout << propObj.getPropertyValue("MyString") << std::endl;
std::cout << propObj.getPropertyValue("MyInteger") << std::endl;
propObj.setPropertyValue("MyString", "bar");
std::cout << propObj.getPropertyValue("MyString") << std::endl;
}
prop_obj = opendaq.PropertyObject()
prop_obj.add_property(opendaq.StringProperty(opendaq.String(
'MyString'), opendaq.String('foo'), opendaq.Boolean(True)))
prop_obj.add_property(opendaq.IntProperty(opendaq.String(
'MyInteger'), opendaq.Integer(0), opendaq.Boolean(True)))
prop_obj.set_property_value('MyString', opendaq.String('bar'))
print(prop_obj.get_property_value('MyString'))
The above snippet creates a simple Property Object with two Properties:
-
"MyString"
with a default value"foo"
-
"MyInteger"
with a default value0
Within our main
function, we print out the two default values, change the string Property to
"bar"
, and retrieve and print the new value. With this, we have created a simple Property
object with two Properties and default values.
Looking back at the statement that each openDAQ™ component is a Property Object, the same Property Object methods are also available on any Device, Function Block, Channel… we might encounter in the SDK.
Property
The above example was a crude and simple example of a Property Object. However, the Property system allows for the description of far more complex and intricate structures than the one created above. This is accomplished through the use of Properties (the metadata structures that describe what a Property can contain and how it should be interpreted) in combination with Property Objects that act as containers for Properties and their associated values.
A Property always describes the value located in a Property Object associated with the key equal to the name of the Property. In other words, a Property’s metadata describes the Property Value in an object with the key that is the same as the name of the Property.
Non-builder factories for basic Properties of each type exist, but they only allow users to specify a minimal set of metadata fields. |
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(StringProperty("MyString", "foo"));
propObj.setPropertyValue("MyString", "bar");
std::cout << propObj.getPropertyValue("MyString") << std::endl;
prop_obj = opendaq.PropertyObject()
prop_obj.add_property(opendaq.StringProperty(opendaq.String(
'MyString'), opendaq.String('foo'), opendaq.Boolean(True)))
prop_obj.set_property_value('MyString', opendaq.String('bar'))
print(prop_obj.get_property_value('MyString'))
In the above example, a Property Value of "bar" is set to the key "MyString"
. The contents
of the value are described by the String Property "MyString"
that was added to the Property
Object. In the case above, the Property defines two metadata fields:
-
Value type: Defined by the factory (
StringProperty
) to be a string. This means that any value written to the key"MyString"
needs to be of a string type. -
Default value: the 2nd argument of the constructor defines a default value of the Property. This defines the value that is read if no Property Value under the key
"MyString"
is present in the PropertyObject.
Property metadata fields
There are several metadata fields available for Properties. They range from simple ones such as Visible and Read-only, to significantly more complex ones such as Referenced Property or Selection values. Depending on different combinations of Properties set, a Property is interpreted differently. The table below provides a brief description of each field, while the next section lists and describes different Property types that can be created depending on what fields are configured.
The Property metadata fields can be configured by using the Property Builder object. The Property
Builder contains setter methods for each of the fields, and the build
method that allows
us to build the Property with the configured metadata.
Name |
The name of the Property. Within a Property Object, no two Properties can have the same name. A Property describes the metadata of a Property Value with a key equal to the Property’s name. |
Description |
A short string description of the Property. |
Value type |
The type of the corresponding Property Value stored in a Property Object. |
Item type |
If the Property has a list or dictionary Value type, the Item type field specifies the types of values stored in the container. The Item type is deduced automatically from the Default value of the Property. |
Key type |
If the Property has a dictionary Value type, the Key type field specifies the types of keys present in the dictionary. The key type is deduced automatically from the Default value of the Property. |
Default value |
The default value of the Property. If no Property Value with the corresponding key is set on the Property Object, the Property Value getter will return the default value. |
Read-only |
Property Values of Properties with Read-only set to true cannot be changed. This can be circumvented by using a protected value write function. |
Visible |
Properties that aren’t visible will not appear when listing all visible Properties of a Property Object. Invisible Properties still exist, but should be hidden from the user interface that displays Properties. |
Unit |
The Property’s unit. E.g., second, meter, volt. The Unit metadata field uses the openDAQ™ Unit type. |
Min value |
The minimum value of the Property’s corresponding value. Available only for numerical Properties. |
Max value |
The maximum value of the Property’s corresponding value. Available only for numerical Properties. |
Suggested values |
A list of suggested values for the Property. The list allows a user to see what values are expected for the Property. Those values, however, are not enforced. The Suggested values field is only available for numerical Properties. |
Selection values |
A list or dictionary of selection values. If the Selection values field is configured, the value of the Property must be an integer that is used to index into the list/dictionary of selection values. The Value type field corresponds to the type of values in the list/dictionary of selection values. Available for selection Properties. |
Referenced Property |
Reference to another Property on the Property Object. When the Referenced Property field is set, all getter/setter methods except for those referencing the Name, Referenced Property, Value type, Item type, and Key type fields will be invoked on the referenced Property instead. This field contains an |
Is referenced |
If true, the Property is referenced by another. Properties, where the Is referenced field is true, are visible only through the Property referencing them and will not be included in the list of visible Properties available on the Property Object. |
Validator |
Validator object that contains an |
Coercer |
Coercer object that contains an |
Callable info |
Contains information about the parameter and return types of the function / procedure stored as the Property Value. Available only for function- and procedure-type Properties. |
On Property Value write |
Event triggered when the corresponding Property Value is written to. Contains a reference to the Property Object and allows for overriding the written value. |
On Property Value read |
Event triggered when the corresponding Property Value is read. Contains a reference to the Property Object and allows for overriding the read value. |
Types of Properties
A Property can be split into several different kinds of Property types. In openDAQ™, we provide a separate factory method for each Property type that automatically pre-configures the minimal set of required fields for a said type. In this section, we list and describe the available type, referencing the relevant Property fields outlined in the above table.
Numerical Properties
Numerical Properties represent numbers. Their values can be either Integers or Floating point numbers, depending on the configured Value type. Numerical Properties can have the Min and Max value fields configured to limit the range of values accepted.
Additionally, Numerical Properties can have a list of Suggested values, indicating the expected values for the Property. Note that the Property system does not enforce that a Property value matches a number in the list of Suggested values.
Numerical Properties must have a default value.
-
Cpp
-
Python
auto propObj = PropertyObject();
auto intProp = IntPropertyBuilder("Integer", 10).setMinValue(0).setMaxValue(15);
propObj.addProperty(intProp.build()));
auto floatProp = FloatPropertyBuilder("Float", 3.21).setSuggestedValues(List<IFloat>(1.23, 3.21, 5.67));
propObj.addProperty(floatProp.build());
// "Integer" is set to 15 due to the max value
propObj.setPropertyValue("Integer", 20);
std::cout << propObj.getPropertyValue("Integer") << std::endl;
// "Float" is set to 2.34 in despite the suggested values not containing the value 2.34
propObj.setPropertyValue("Float", 2.34);
std::cout << propObj.getPropertyValue("Float") << std::endl;
prop_obj = opendaq.PropertyObject()
int_prop = opendaq.IntPropertyBuilder(opendaq.String('Integer'), opendaq.Integer(10))
# Unsupported in the current version
# int_prop.min_value = 0
# int_prop.max_value = 15
prop_obj.add_property(int_prop.build())
float_prop = opendaq.FloatPropertyBuilder(opendaq.String('Float'), opendaq.Float(3.21))
suggested_values = opendaq.List()
suggested_values.push_back(opendaq.Float(1.23))
suggested_values.push_back(opendaq.Float(3.21))
suggested_values.push_back(opendaq.Float(5.67))
float_prop.suggested_values = suggested_values
prop_obj.add_property(float_prop.build())
# "Integer" is set to 15 due to the max value
prop_obj.set_property_value('Integer', opendaq.Integer(20))
print(prop_obj.get_property_value('Integer'))
# "Float" is set to 2.34 in despite the suggested values not containing the value 2.34
prop_obj.set_property_value('Float', opendaq.Float(2.34))
print(prop_obj.get_property_value('Float'))
Selection Properties
Selection Properties are those that have the Selection values field configured with either a list, or dictionary, and its Value type must be Integer. The values of the list / dictionary match the Item type of the Property, while the keys of the dictionary must be integers. (matching the Value type).
The Property Value of a selection Property represents the index or key used to retrieve the Selection value from the list / dictionary. As such, the values written to corresponding Property Values are always integers, but the selected value can be of any type.
To obtain the selected value, we get the corresponding Property Value, and use it as the index / key to obtain the value from our list / dictionary of selection values. Alternatively, the Property Object provides a Selection Property getter method that automatically performs the above steps.
Selection Properties must have a default value assigned.
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(SelectionProperty("ListSelection", List<IString>("Apple", "Banana", "Kiwi"), 1));
auto dict = Dict<Int, IString>();
dict.set(0, "foo");
dict.set(10, "bar");
propObj.addProperty(SparseSelectionProperty("DictSelection", dict, 10));
// Prints "1"
std::cout << propObj.getPropertyValue("ListSelection") << std::endl;
// Prints "Banana"
std::cout << propObj.getPropertySelectionValue("ListSelection") << std::endl;
// Selects "Kiwi"
propObj.setPropertyValue("ListSelection", 2);
// Prints "bar"
std::cout << propObj.getPropertySelectionValue("DictSelection") << std::endl;
// Selects "foo"
propObj.setPropertyValue("DictSelection", 0);
prop_obj = opendaq.PropertyObject()
list = opendaq.List()
list.push_back(opendaq.String('Apple'))
list.push_back(opendaq.String('Banana'))
list.push_back(opendaq.String('Kiwi'))
prop_obj.add_property(opendaq.SelectionProperty(opendaq.String(
'ListSelection'), list, opendaq.Integer(1), opendaq.Boolean(True)))
dict = opendaq.Dict()
dict[0] = opendaq.String('foo')
dict[10] = opendaq.String('bar')
prop_obj.add_property(opendaq.SparseSelectionProperty(opendaq.String(
'DictSelection'), dict, opendaq.Integer(10), opendaq.Boolean(True)))
# Prints "1"
print(prop_obj.get_property_value('ListSelection'))
# Prints "Banana"
print(prop_obj.get_property_selection_value('ListSelection'))
prop_obj.set_property_value('ListSelection', opendaq.Integer(2))
# Prints "bar"
print(prop_obj.get_property_selection_value('DictSelection'))
# Selects "foo"
prop_obj.set_property_value('DictSelection', opendaq.Integer(0))
Object Properties
Object type Properties have the Value type Object. These kinds of Properties allow for Properties to be grouped and represented in a hierarchy of nested Property Objects. A value of an object-type Property can only be a base Property Object. Objects such as Devices or Function blocks that are descendants of the Property Object Class cannot be set as the Property Value.
Accessing the Properties of an object Property can be done from its parent using "dot" notation as shown in the example below.
The Selection Property getter cannot use the "dot" notation at this moment. |
Object type Properties can only have their Name, Description, Visible, Read-only and Default value configured, where the Default value is mandatory.
Object properties, as all other Property types get frozen once added to a Property Object. The notable exception is that locally, the object (default value) is cloned and cached. When the Property value of the Object-type Property is read, the cloned object is returned instead of the default value. This cloned object is not frozen, allowing for the any Properties of the child Property Object to be modified. The same behaviour is applied when a Property Object is created from a Property Object Class - all Object-type properties of the class are cloned. |
Notably, a object-type property cannot be replaced via set property value
(unless using set protected property value
), but calling
clear property value
will reset all of its properties to their default values. clear property value
cannot be called of the object-type property is read-only.
-
Cpp
-
Python
auto propObj = PropertyObject();
auto child1 = PropertyObject();
auto child2 = PropertyObject();
// The order below is important, as "child1" and "child2" are frozen once
// used as default property values.
child2.addProperty(StringProperty("String", "foo"));
child1.addProperty(ObjectProperty("Child", child2));
propObj.addProperty(ObjectProperty("Child", child1));
// Prints out the value of the "String" Property of child2
std::cout << propObj.getPropertyValue("Child.Child.String") << std::endl;
prop_obj = opendaq.PropertyObject()
child1 = opendaq.PropertyObject()
child2 = opendaq.PropertyObject()
# The order below is important, as "child1" and "child2" are frozen once
# used as default property values.
child2.add_property(opendaq.StringProperty(opendaq.String(
'String'), opendaq.String('foo'), opendaq.Boolean(True)))
child1.add_property(opendaq.ObjectProperty(
opendaq.String('Child'), child2))
prop_obj.add_property(opendaq.ObjectProperty(
opendaq.String('Child'), child1))
# Prints out the value of the "String" Property of child2
print(prop_obj.get_property_value('Child.Child.String'))
Container Properties
Container type Properties have the Value type List or Dictionary and must be homogenous - they can only have the keys and values of the same type. Their Key and Item types are configured to match that of the Property’s Default value. Any new Property Value must adhere to the original key and item type.
Containers can’t contain Object-type values, Container-type values (List, Dictionary), or Function-type values. The same applies for the Key type of dictionary objects.
Container-type Properties cannot have empty default values as of now. If the default values are empty, the Key and Item type deduction will not work properly, evaluating the types to be undefined. |
Container Properties must have a default value.
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(ListProperty("List", List<IString>("Banana", "Apple", "Kiwi")));
auto dict = Dict<Int, IString>();
dict.set(0, "foo");
dict.set(10, "bar");
propObj.addProperty(DictProperty("Dict", dict));
// Prints out "Banana"
std::cout << propObj.getPropertyValue("List").asPtr<IList>()[0] << std::endl;
// Prints out "bar"
std::cout << propObj.getPropertyValue("Dict").asPtr<IDict>().get(10) << std::endl;
// Sets a new value for the List Property
propObj.setPropertyValue("List", List<IString>("Pear", "Strawberry"));
prop_obj = opendaq.PropertyObject()
list = opendaq.List()
list.push_back(opendaq.String('Banana'))
list.push_back(opendaq.String('Apple'))
list.push_back(opendaq.String('Kiwi'))
prop_obj.add_property(opendaq.ListProperty(
opendaq.String('List'), list, opendaq.Boolean(True)))
dict = opendaq.Dict()
dict[0] = opendaq.String('foo')
dict[10] = opendaq.String('bar')
prop_obj.add_property(opendaq.DictProperty(
opendaq.String('Dict'), dict, opendaq.Boolean(True)))
print(prop_obj.get_property_value('List')[0])
print(prop_obj.get_property_value('Dict')[10])
list1 = opendaq.List()
list1.push_back(opendaq.String('Pear'))
list1.push_back(opendaq.String('Strawberry'))
prop_obj.set_property_value('List', list1)
Reference Properties
Reference Properties have the Referenced Property field configured. The Referenced Property contains a pointer to another Property that is part of the same Property Object. On such Properties, all Property field getters except for the Name, Is referenced, Referenced Property, Value type, Key type, and Item type return the metadata fields of the referenced Property. Similarly, the Property Object value getters and setters get/set the value of the referenced Property.
The Referenced Property field is configured with an EvalValue that most often switches
between different Properties depending on the value of another Property. For example, the
EvalValue
string "switch($switchProp, 0, %prop1, 1, %prop2)"
reads the value of the
Property named "switchProp"
and references the Property named "prop1"
if the value is 0
. If
the value is 1
, it references "prop2"
instead.
A Property can be referenced by only one Property within a Property Object. |
For more information on the interaction between EvalValue
and Properties see the section on
Properties and EvalValues below.
Reference Properties can only have the Name and Referenced Property fields configured. Their Value type is always undefined.
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(IntProperty("Integer", 0));
propObj.addProperty(StringProperty("Prop1", "foo"));
propObj.addProperty(StringProperty("Prop2", "bar"));
propObj.addProperty(ReferenceProperty("RefProp", EvalValue("switch($Integer, 0, %Prop1, 1, %Prop2)")));
// Prints "foo"
std::cout << propObj.getPropertyValue("RefProp") << std::endl;
propObj.setPropertyValue("Integer", 1);
// Prints "bar"
std::cout << propObj.getPropertyValue("RefProp") << std::endl;
prop_obj = opendaq.PropertyObject()
prop_obj.add_property(opendaq.IntProperty(opendaq.String(
'Integer'), opendaq.Integer(0), opendaq.Boolean(True)))
prop_obj.add_property(opendaq.StringProperty(opendaq.String(
'Prop1'), opendaq.String('foo'), opendaq.Boolean(True)))
prop_obj.add_property(opendaq.StringProperty(opendaq.String(
'Prop2'), opendaq.String('bar'), opendaq.Boolean(True)))
prop_obj.add_property(opendaq.ReferenceProperty(opendaq.String(
'RefProp'), opendaq.EvalValue(opendaq.String('switch($Integer, 0, %Prop1, 1, %Prop2)'))))
# Prints "foo"
print(prop_obj.get_property_value('RefProp'))
prop_obj.set_property_value('Integer', opendaq.Integer(1))
# Prints "bar"
print(prop_obj.get_property_value('RefProp'))
Function / Procedure Properties
Function Properties have the Value type of Function or Procedure. Functions are callable methods that have an optional return type, while procedures don’t return anything. The Property value of a Function / Procedure Property is a callable object.
To determine the parameter count and types, as well as the return type, the Callable info field must be configured. Callable info contains a list of argument types that need to be passed as arguments when invoking the callable object. If the Property is a Function, the Callable info field also contains the type of the variable returned by the function.
Function and Procedure type Properties are currently not accessible through the OPC UA layer. Thus, they will not appear on connected-to devices. |
Function and Procedure type Properties can’t have a default value.
auto propObj = PropertyObject();
auto arguments = List<IArgumentInfo>(ArgumentInfo("Val1", ctInt), ArgumentInfo("Val2", ctInt));
propObj.addProperty(FunctionProperty("SumFunction", FunctionInfo(ctInt, arguments)));
auto func = Function([](IntegerPtr val1, IntegerPtr val2)
{
return val1 + val2;
});
propObj.setPropertyValue("SumFunction", func);
FunctionPtr sumFunc = propObj.getPropertyValue("SumFunction");
// Prints out 42
std::cout << sumFunc(12, 30) << std::endl;
Struct Properties
TODO
Struct Properties can currently not be transmitted over OPC UA unless their Struct type name matches a Structure type and key names/value types in the imported OPC UA nodesets. |
Remaining Property types
We’ve now covered all special kinds of Properties and are left with three remaining ones that have no special fields / behavior. Those are:
-
String Property: Its associated Property Value must be a String
-
Ratio Property: Its associated Property Value must be a Ratio
-
Bool Property: Its associated Property Value must be a Boolean
All of these must have a default value configured.
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(StringProperty("String", "foo"));
propObj.addProperty(RatioProperty("Ratio", Ratio(1, 10)));
propObj.addProperty(BoolProperty("Bool", true));
prop_obj = opendaq.PropertyObject()
prop_obj.add_property(opendaq.StringProperty(opendaq.String(
'String'), opendaq.String('foo'), opendaq.Boolean(True)))
prop_obj.add_property(opendaq.RatioProperty(opendaq.String(
'Ratio'), opendaq.Ratio(1, 10), opendaq.Boolean(True)))
prop_obj.add_property(opendaq.BoolProperty(opendaq.String(
'Bool'), opendaq.Boolean(True), opendaq.Boolean(True)))
Creating and configuring a Property
Properties follow a builder pattern pervasive across the SDK. When constructing a new factory,
the obtained object is a Builder
-type object. Builder
objects contain setter methods in addition
to a build
function. The build
function validates the current configuration of the Property
and returns the built Property with the configured fields.
Both Builder and non-Builder factories for Properties exist, but the non-Builder ones provide only the basic metadata field parameters, while the Builder ones allow for more customization. Note that the metadata fields of a Property cannot be changed once built.
In the example below, we create a Float Property, configuring some of its metadata fields, and freeze it by adding it to a Property Object.
-
Cpp
-
Python
auto propObj = PropertyObject();
PropertyBuilderPtr floatProp = FloatPropertyBuilder("MyFloat", 1.123).setMinValue(0.0).setMaxValue(10.0);
propObj.addProperty(floatProp.build());
prop_obj = opendaq.PropertyObject()
float_prop = opendaq.FloatPropertyBuilder(opendaq.String('MyFloat'), opendaq.Float(1.123))
# Unsupported in the current version
# float_prop.min_value = 0.0
# float_prop.max_value = 10.0
propObj.add_property(float_prop.build())
Properties and EvalValue
We’ve now seen the different kinds of Properties supported in openDAQ™, as well as short
descriptions of the metadata fields available to each of them. As of now, we’re able to describe
simple Properties of which Property Values can be read or changed at will. Where this system
truly comes to life, however, is when EvalValue
objects are part of the equation. We’ve seen
a small example of EvalValue
usage above in the Reference Properties example
where it was used to determine which Property is referenced, depending on the value of another
Property.
EvalValue
objects allow us to define Property metadata fields that are dependent on the
state / value of another Property within the same Property Object. They’re expressions that are
evaluated every time their corresponding metadata field is read, returning the evaluated value.
In this article, we’ll focus on how EvalValue
objects are used to set up
a Property Object describing a configuration of dependent Properties. Below, we see an example
of a simulated channel that can output either a "Sine" or "Counter" signal.
PropertyObjectPtr simulatedChannel = PropertyObject();
simulatedChannel.addProperty(SelectionProperty("Waveform", List<IString>("Sine", "Counter"), 0));
simulatedChannel.addProperty(ReferenceProperty("Settings", EvalValue("if($Waveform == 0, %SineSettings, %CounterSettings)")));
PropertyBuilderPtr freqProp = FloatProperty("Frequency", 10.0)
.setUnit(Unit("Hz"))
.setMinValue(0.1)
.setMaxValue(1000.0)
.setSuggestedValues(List<IFloat>(0.1, 10.0, 100.0, 1000.0));
simulatedChannel.addProperty(freqProp.build());
// Sine settings
PropertyObjectPtr sineSettings = PropertyObject();
sineSettings.addProperty(SelectionProperty("AmplitudeUnit", List<IString>("V", "mV"), 0));
PropertyBuilderPtr amplitudeProp = FloatProperty("Amplitude", 5).setUnit(EvalValue("Unit(%AmplitudeUnit:SelectedValue)"));
sineSettings.addProperty(amplitudeProp.build());
sineSettings.addProperty(BoolProperty("EnableScaling", false));
PropertyBuilderPtr scalingFactor = FloatProperty("ScalingFactor", 1.0).setVisible(EvalValue("$EnableScaling"));
sineSettings.addProperty(scalingFactor.build());
simulatedChannel.addProperty(ObjectProperty("SineSettings", sineSettings));
// Counter settings
PropertyObjectPtr counterSettings = PropertyObject();
counterSettings.addProperty(IntProperty("Increment", 1));
counterSettings.addProperty(SelectionProperty("Mode", List<IString>("Infinite", "Loop"), 0));
PropertyBuilderPtr loopThreshold = IntProperty("LoopThreshold", 100).setMinValue(1).setVisible(EvalValue("$Mode == 1"));
counterSettings.addProperty(loopThreshold.build());
PropertyBuilderPtr resetProp = FunctionProperty("Reset", ProcedureInfo()).setReadOnly(true).setVisible(EvalValue("$Mode == 0"));
counterSettings.addProperty(resetProp.build());
counterSettings.asPtr<IPropertyObjectProtected>().setProtectedPropertyValue("Reset", Procedure([](){ this->reset(); }));
simulatedChannel.addProperty(ObjectProperty("CounterSettings", counterSettings));
Referencing another Property
When a Property is added to a Property Object, it gains access to the metadata fields and
values of other Properties that are part of the object. EvalValue
objects are created with an evaluation
expression that evaluates to an openDAQ™ object when read. As part of the evaluation expression
of a Property metadata field, we can reference either another Property (with the symbol %
), or
the value associated with that Property (with the symbol $
). By doing so, we can create dependent
Property metadata that differs depending on the state of the Property Object. To illustrate this behavior,
let’s refer to the above Simulated Channel Property Object example.
-
Cpp
-
Python
PropertyObjectPtr simulatedChannel = PropertyObject();
simulatedChannel.addProperty(SelectionProperty("Waveform", List<IString>("Sine", "Counter"), 0));
simulatedChannel.addProperty(ReferenceProperty("Settings", EvalValue("if($Waveform == 0, %SineSettings, %CounterSettings)")));
...
simulatedChannel.addProperty(ObjectProperty("SineSettings", sineSettings));
...
simulatedChannel.addProperty(ObjectProperty("CounterSettings", counterSettings));
simulated_channel = opendaq.PropertyObject()
list = opendaq.List()
list.push_back(opendaq.String('Sine'))
list.push_back(opendaq.String('Counter'))
simulated_channel.add_property(opendaq.SelectionProperty(opendaq.String(
'Waveform'), list, opendaq.Integer(0), opendaq.Boolean(True)))
simulated_channel.add_property(opendaq.ReferenceProperty(opendaq.String(
'Settings'), opendaq.EvalValue(opendaq.String('if($Waveform == 0, %SineSettings, %CounterSettings)'))))
...
simulated_channel.add_property(opendaq.ObjectProperty(
opendaq.String('SineSettings'), sineSettings))
...
simulated_channel.add_property(opendaq.ObjectProperty(
opendaq.String('CounterSettings'), counterSettings))
The core of our Simulated channel configuration is formed by the "Waveform"
Selection Property. It
provides the option of choosing between the "Sine"
and "Counter"
modes. Both of the modes have different
settings available. To hide / show different settings, we add a Reference Property "Settings"
that
references either the Sine or Counter settings Object Property. By changing the "Waveform"
Property Value,
the EvalValue
if
check evaluates to a different Property:
-
Cpp
-
Python
// If the value of the "Waveform" Property equals 0, the EvalValue evaluates to the
// "SineSettings" Property. If not, it evaluates to the "CounterSettings" Property.
ReferenceProperty("Settings", EvalValue("if($Waveform == 0, %SineSettings, %CounterSettings)"));
# If the value of the "Waveform" Property equals 0, the EvalValue evaluates to the
# "SineSettings" Property. If not, it evaluates to the "CounterSettings" Property.
opendaq.ReferenceProperty(opendaq.String('Settings'), opendaq.EvalValue(opendaq.String('if($Waveform == 0, %SineSettings, %CounterSettings)')))
Reference Properties, however, aren’t the only use case for EvalValue
expressions. Any configurable
Property metadata fields except for Name, Description, and Value type can make use of the EvalValue
system. In the example above, Properties are hidden depending on the state of other Property Values:
-
Cpp
-
Python
...
// The ScalingFactor Property is shown if EnableScaling is true.
scalingFactor.setVisible(EvalValue("$EnableScaling"));
...
// The LoopThreshold Property is shown if the "Mode" Property is set to 1.
loopThreshold.setVisible(EvalValue("$Mode == 1"));
...
# The ScalingFactor Property is shown if EnableScaling is true.
# Unsupported in the current version
scaling_factor.visible = opendaq.EvalValue(opendaq.String('$EnableScaling'))
...
# The LoopThreshold Property is shown if the "Mode" Property is set to 1.
# Unsupported in the current version
loop_threshold.visible = opendaq.EvalValue(opendaq.String('$Mode == 1'))
We configure the unit of the "Amplitude"
Property based on the "AmplitudeUnit"
selected value, using
the Unit
token in our expression to indicate we’re creating a Unit, and accessing the currently
selected value of our "AmplitudeUnit"
Selection Property.
The %Prop:Value notation is equivalent to $Prop , while %Prop:SelectedValue uses the
value of the Property to retrieve the corresponding Selection value from the list / dictionary
of Selection values.
|
-
Cpp
-
Python
sineSettings.addProperty(SelectionProperty("AmplitudeUnit", List<IString>("V", "mV"), 0));
....
amplitudeProp.setUnit(EvalValue("Unit(%AmplitudeUnit:SelectedValue)"));
list = opendaq.List()
list.push_back(opendaq.String('V'))
list.push_back(opendaq.String('mV'))
sine_settings.add_property(opendaq.SelectionProperty(opendaq.String(
'AmplitudeUnit'), list, opendaq.Integer(0), opendaq.Boolean(True)))
...
# Unsupported in the current version
amplitude_prop.unit = opendaq.EvalValue(opendaq.String('Unit(%AmplitudeUnit:SelectedValue)'))
As showcased above, the combination of the EvalValue
system with the option of
referencing other Properties of a Property Object offers a powerful tool for describing the
configuration of a system.
Validation and Coercion
EvalValue
objects can also be used to define coercion/validation expressions.
Each Property has a Validator and Coercer field. The Validator checks whether a Property Value
written to the object is valid, and produces a validation error when it is not. The Coercer checks
whether the value is valid, and if not, coerces the value to adhere to the requirements of the
coercion expression.
The Validator / Coercer uses an evaluation expression where each instance of the token Value
in the expression is replaced by the written value. For example the validation expression
"Value < 10"
will check whether the written value is smaller than 10
. The coercion
expression "if(Value < 10, Value, 10)"
will first check whether the value is smaller than 10
,
and change it to 10
if it is not.
The Validator / Coercer fields are most often configured for numerical Properties but can be used for most standard Property types.
-
Cpp
-
Python
auto propObj = PropertyObject();
auto coercedProp = IntProperty("CoercedProp", 5).setCoercer(Coercer("if(Value < 10, Value, 10)"));
propObj.addProperty(coercedProp.build());
auto validatedProp = IntProperty("ValidatedProp", 5).setValidator(Validator("Value < 10"));
propObj.addProperty(validatedProp.build());
// Sets the value to 10
propObj.setPropertyValue("CoercedProp", 15);
// Throws a validation error
propObj.setPropertyValue("ValidatedProp", 15);
prop_obj = opendaq.PropertyObject()
coerced_prop = opendaq.IntPropertyBuilder(opendaq.String('CoercedProp'), opendaq.Integer(5))
coerced_prop.coercer = opendaq.Coercer(opendaq.String('if(Value < 10, Value, 10)'))
prop_obj.add_property(coerced_prop.build())
validated_prop = opendaq.IntPropertyBuilder(opendaq.String('ValidatedProp'), opendaq.Integer(5))
validated_prop.validator = opendaq.Validator(opendaq.String('Value < 10'))
prop_obj.add_property(validated_prop.build())
# Sets the value to 10
prop_obj.set_property_value('CoercedProp', opendaq.Integer(15))
# Throws a validation error
prop_obj.set_property_value('ValidatedProp', opendaq.Integer(15))
Property Object
Having tackled all the different kinds of available Properties, we now take a look at the Property Object - the container of Properties and their corresponding Property Values.
We’ve often referred to Property Objects while describing the Properties, as they’re fundamental
to the usage of EvalValue
expressions and writing / reading values of Properties. In essence,
a Property Object has a dictionary of Property names as keys and the Properties themselves as
values. A Property Object can have at most one Property with any given name, and will not allow
for duplicates. For each Property, a separate dictionary of names and Property Values is maintained
in a Property Object. When setting a new value, the old value is overridden, and when reading the
value, that dictionary is queried (if no value is present, the Property’s Default value is read
instead).
Adding/Removing Properties
Adding or removing Properties in a Property Object is simple. We add them via the add
method,
and remove them via remove
. As mentioned above, when a Property is added to a Property Object
it is frozen and can no longer be configured.
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(StringProperty("foo", "bar"));
propObj.removeProperty("foo");
// Retrieves the String Property "foo" added in the 2nd line
auto fooProp = propObj.getProperty("foo");
prop_obj = opendaq.PropertyObject()
prop_obj.add_property(opendaq.StringProperty(opendaq.String(
'foo'), opendaq.String('bar'), opendaq.Boolean(True)))
prop_obj.remove_property('foo')
foo_prop = prop_obj.get_property('foo') # Throws runtime error
Listing Properties
Property Objects allow for listing all Properties, or all visible Properties. As one might
imagine, the list of visible Properties contains a subset of Properties that are visible.
A Property is visible if its Visible metadata field is set to true
, and its IsReferenced field
is false
.
Remember that IsReferenced is true for Properties that are referenced by another Reference Property. |
Properties are listed in the order they’re added to the Property Object if no custom order is specified. At any given point, a new order can be specified by providing an ordered list of Property names.
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(StringProperty("String", "foo"));
propObj.addProperty(IntProperty("Int", 10, false));
propObj.addProperty(FloatProperty("Float", 15.0));
propObj.addProperty(ReferenceProperty("FloatRef", EvalValue("%Float")));
// Contains the Properties "String", "Int", "Float", "FloatRef"
auto allProps = propObj.getAllProperties();
// Contains the Properties "String", "FloatRef"
auto visibleProps = propObj.getVisibleProperties();
auto order = List<IString>("FloatRef", "Float", "Int", "String");
propObj.setPropertyOrder(order);
// Contains the Properties in the order "FloatRef", "Float", "Int", String"
auto allPropsReverseOrder = propObj.getAllProperties();
prop_obj = opendaq.PropertyObject()
prop_obj.add_property(opendaq.StringProperty(opendaq.String(
'String'), opendaq.String('foo'), opendaq.Boolean(True)))
prop_obj.add_property(opendaq.IntProperty(opendaq.String(
'Int'), opendaq.Integer(10), opendaq.Boolean(False)))
prop_obj.add_property(opendaq.FloatProperty(opendaq.String(
'Float'), opendaq.Float(15.0), opendaq.Boolean(True)))
prop_obj.add_property(opendaq.ReferenceProperty(opendaq.String(
'FloatRef'), opendaq.EvalValue(opendaq.String('%Float'))))
all_props = prop_obj.all_properties
visible_props = prop_obj.visible_properties
# Properties orderings are not supported in the current version
Reading / Writing Property Values
Reading and writing Property Values is done by the value getter/setter methods that take the Property name as the first input argument.
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(StringProperty("String", "foo"));
// Prints "foo"
std::cout << propObj.getPropertyValue("String") << std::endl;
propObj.setPropertyValue("String", "bar");
// Prints "bar"
std::cout << propObj.getPropertyValue("String") << std::endl;
prop_obj = opendaq.PropertyObject()
prop_obj.add_property(opendaq.StringProperty(opendaq.String(
'String'), opendaq.String('foo'), opendaq.Boolean(True)))
# Prints "foo"
print(prop_obj.get_property_value('String'))
prop_obj.set_property_value('String', opendaq.String('bar'))
# Prints "bar"
print(prop_obj.get_property_value('String'))
Nested Property Objects
When accessing Object-type Properties, "dot" notation can be used, where the Object Property’s name is followed by a dot and the name of the accessed Property.
-
Cpp
-
Python
auto propObj = PropertyObject();
auto child1 = PropertyObject();
auto child2 = PropertyObject();
child2.addProperty(StringProperty("String", "foo"));
child1.addProperty(ObjectProperty("Child", child2));
propObj.addProperty(ObjectProperty("Child", child1));
propObj.setPropertyValue("Child.Child.String", "bar");
// Prints "bar"
std::cout << propObj.getPropertyValue("Child.Child.String") << std::endl;
prop_obj = opendaq.PropertyObject()
child1 = opendaq.PropertyObject()
child2 = opendaq.PropertyObject()
child2.add_property(opendaq.StringProperty(opendaq.String(
'String'), opendaq.String('foo'), opendaq.Boolean(True)))
child1.add_property(opendaq.ObjectProperty(
opendaq.String('Child'), child2))
prop_obj.add_property(opendaq.ObjectProperty(
opendaq.String('Child'), child1))
prop_obj.set_property_value(
'Child.Child.String', opendaq.String('bar'))
# Prints "bar"
print(prop_obj.get_property_value('Child.Child.String'))
Selection Properties
Selection Properties always have a Value type of Integer. As such, the value getter will always return the integer key/index into the list/dictionary of Selection values. To directly obtain the selected value, a Selection value getter is available.
The "dot" notation used to get/set values of nested objects does not work on the Selection value getter function. |
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(SelectionProperty("Selection", List<IString>("Banana", "Kiwi"), 1));
// Prints "Kiwi"
std::cout << propObj.getPropertySelectionValue("Selection") << std::endl;
prop_obj = opendaq.PropertyObject()
list = opendaq.List()
list.push_back(opendaq.String('Banana'))
list.push_back(opendaq.String('Kiwi'))
prop_obj.add_property(opendaq.SelectionProperty(opendaq.String(
'Selection'), list, opendaq.Integer(1), opendaq.Boolean(True)))
# Prints "Kiwi"
print(prop_obj.get_property_selection_value('Selection'))
List Properties
When reading List Properties, the getter string can already contain a subscript token in the form
of "propName[index]"
.
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(ListProperty("List", List<IString>("Banana", "Kiwi")));
// Prints "Banana"
std::cout << propObj.getPropertyValue("List[0]") << std::endl;
// Sets a new value to the List Property.
propObj.setPropertyValue("List", List<IString>("Pear", "Strawberry"));
// Prints "Pear" and "Strawberry"
std::cout << list.toString() << std::endl;
prop_obj = opendaq.PropertyObject()
list = opendaq.List()
list.push_back(opendaq.String('Banana'))
list.push_back(opendaq.String('Kiwi'))
prop_obj.add_property(opendaq.ListProperty(
opendaq.String('List'), list, opendaq.Boolean(True)))
# Prints "Banana"
print(prop_obj.get_property_value('List[0]'))
When retrieving a list-type property value, the list object is cloned, and can be modified without side effects to the Property object. As such, we can extend the Property defined in the above code snippet as follows:
-
Cpp
-
Python
ListPtr<IString> list = propObj.getPropertyValue("List");
list.pushBack("Blueberry");
propObj.setPropertyValue("List", list);
// Prints "Pear", "Strawberry", and "Blueberry"
std::cout << propObj.getPropertyValue("List").toString() << std::endl;
list = prop_obj.get_property_value("List")
list.push_back('Blueberry')
prop_obj.set_property_value('List', list)
# Prints "Pear", "Strawberry", and "Blueberry"
print(prop_obj.get_property_value('List'))
Dictionary properties
As with list-type properties, when retrieveing a dictionary property value, the dictionary object is cloned, and can be modified without side effects to the Property object:
-
Cpp
-
Python
auto propObj = PropertyObject();
propObj.addProperty(DictProperty("Dict", Dict<IInteger, IString>({{1, "Banana"}, {2, "Kiwi"}})));
DictPtr<IInteger, IString> dict = propObj.getPropertyValue("Dict");
dict.set(3, "Blueberry");
// The "Dict" property now contains {1 : "Banana"}, {2 : "Kiwi"}, {3 : "Blueberry"}
propObj.setPropertyValue("Dict", dict);
dict = opendaq.Dict()
dict[1] = 'Banana'
dict[2] = 'Kiwi'
property_object.add_property(opendaq.DictProperty(
opendaq.String('Dict'), dict, opendaq.Boolean(True)))
dict = property_object.get_property_value("Dict")
dict[3] = 'Blueberry'
# The "Dict" property now contains {1 : "Banana"}, {2 : "Kiwi"}, {3 : "Blueberry"}
property_object.set_property_value('Dict', dict)
Read/Write Events
Property Objects and Properties trigger events when reading/writing a Property Value. This allows custom callback functions to be implemented that react to the value being written/read. The events can be obtained on the Property Object via the appropriate getters.
The event calls all functions of subscribers when triggered with a reference to the Property Object
and an Event arguments
object as function arguments. The Event arguments allow for overriding the
read/written value.
auto propObj = PropertyObject();
propObj.addProperty(IntProperty("IntReadCount", 0));
propObj.addProperty(IntProperty("Int", 10));
// Coerce the value of "Int" to a maximum of 20.
propObj.getOnPropertyValueWrite("Int") +=
[](PropertyObjectPtr& sender, PropertyValueEventArgsPtr& args)
{
Int writtenValue = args.getValue();
if (writtenValue > 20)
{
args.setValue(20);
}
};
// Increment IntReadCount whenever the "Int" Property Value is read.
propObj.getOnPropertyValueRead("Int") +=
[](PropertyObjectPtr& sender, PropertyValueEventArgsPtr& args)
{
IntegerPtr readCount = sender.getPropertyValue("IntReadCount");
sender.setPropertyValue("IntReadCount", readCount + 1);
};
propObj.setPropertyValue("Int", 30);
// Prints out 20
std::cout << propObj.getPropertyValue("Int") << std::endl;
// Prints out 1
std::cout << propObj.getPropertyValue("IntReadCount") << std::endl;
Keen-eyed readers might observe that the same getters/metadata fields also exist on Properties. The events on Properties function the exact same as on Property Objects but should be used with care when a Property instance is used on multiple different Property Objects. An event of a Property will trigger for every value write/read of all Property Objects the Property is part of.
A Property can be part of multiple Property Objects if both objects are instantiated from a Property Object Class.
Property Object Class
To round out the openDAQ™ Property system we introduce the Property Object Classes. Property Object Classes allow us to specify a set of Properties without values that represent a commonly occurring set of Properties from which multiple Property Objects can be instantiated. Each Property Object created with a specific Class name will inherit all the Properties of the class with that name.
Classes are created in a manner similar to Property Objects with the difference that they cannot
contain Property Values. Property Object Classes follow the same Builder
pattern as Properties,
providing all methods of modifying the class on the Builder
interface.
-
Cpp
-
Python
PropertyObjectClassBuilderPtr propClass = PropertyObjectClassBuilder("MyClass")
.addProperty(IntProperty("Integer", 10))
.addProperty(SelectionProperty("Selection", List<IString>("Banana", "Apple", "Kiwi"), 1));
prop_class = opendaq.PropertyObjectClassBuilder(opendaq.String('MyClass'))
prop_class.add_property(opendaq.IntProperty(opendaq.String(
'Integer'), opendaq.Integer(10), opendaq.Boolean(True)))
list = opendaq.List()
list.push_back(opendaq.String('Banana'))
list.push_back(opendaq.String('Apple'))
list.push_back(opendaq.String('Kiwi'))
prop_class.add_property(opendaq.SelectionProperty(opendaq.String('Selection'), list, opendaq.Integer(1), opendaq.Boolean(True)))
Manager
To add then instantiate a Property Object with a given Property Object Class name, the class must be added to a Type manager instance. Most often within a given openDAQ™ instance, a single manager will exist that will contain a collection of all registered Classes (and other types). Once the Class is registered with the manager, any openDAQ™ component with access to the manager can create a Property Object with the given class.
A Property Object Class is frozen once it’s added to the manager, preventing any new Properties from being added to it, or any old ones removed.
-
Cpp
-
Python
TypeManagerPtr manager = TypeManager();
PropertyObjectClassBuilderPtr propClass = PropertyObjectClassBuilder("MyClass")
.addProperty(IntProperty("Integer", 10))
.addProperty(SelectionProperty("Selection", List<IString>("Banana", "Apple", "Kiwi"), 1));
manager.addClass(propClass.build());
PropertyObjectPtr propObj = PropertyObject(manager, "MyClass");
// Prints "Apple"
std::cout << propObj.getPropertySelectionValue("Selection") << std::endl;
manager = opendaq.TypeManager()
prop_class = opendaq.PropertyObjectClassBuilder(opendaq.String('MyClass'))
prop_class.add_property(opendaq.IntProperty(opendaq.String('Integer'), opendaq.Integer(10), opendaq.Boolean(True)))
list = opendaq.List()
list.push_back(opendaq.String('Banana'))
list.push_back(opendaq.String('Apple'))
list.push_back(opendaq.String('Kiwi'))
prop_class.add_property(opendaq.SelectionProperty(opendaq.String('Selection'), list, opendaq.Integer(1), opendaq.Boolean(True)))
manager.add_class(prop_class.build())
prop_obj = opendaq.PropertyObjectWithClassAndManager(manager, opendaq.String('MyClass'))
# Prints "Apple"
print(prop_obj.get_property_selection_value('Selection'))
Class inheritance
As with normal OOP
classes, Property Object Classes also allow for inheritance. A
Property Object Class can inherit another that is registered within the manager. A Property Object
created with such a Class contains both the Class’s Properties, as well as all the Properties of
its inherited Classes.
-
Cpp
-
Python
TypeManagerPtr manager = TypeManager();
PropertyObjectClassBuilderPtr propClass1 = PropertyObjectClassBuilder(manager, "InheritedClass").addProperty(StringProperty("InheritedProp", "foo"));
manager.addClass(propClass1.build());
PropertyObjectClassBuilderPtr propClass2 = PropertyObjectClassBuilder(manager, "MyClass")
.addProperty(StringProperty("OwnProp", "bar"))
.setParentName("InheritedClass");
manager.addClass(propClass2.build());
auto propObj = PropertyObject(manager, "MyClass");
// Prints "foo"
std::cout << propObj.getPropertyValue("InheritedProp") << std::endl;
// Prints "bar"
std::cout << propObj.getPropertyValue("OwnProp") << std::endl;
manager = opendaq.TypeManager()
prop_class1 = opendaq.PropertyObjectClassBuilderWithManager(manager, opendaq.String('InheritedClass'))
prop_class1.add_property(opendaq.StringProperty(opendaq.String('InheritedProp'), opendaq.String('foo'), opendaq.Boolean(True)))
manager.add_class(prop_class1.build())
prop_class2 = opendaq.PropertyObjectClassBuilderWithManager(manager, opendaq.String('MyClass'))
prop_class2.add_property(opendaq.StringProperty(opendaq.String('OwnProp'), opendaq.String('bar'), opendaq.Boolean(True)))
prop_class2.parent_name = 'InheritedClass'
manager.add_class(prop_class2.build())
prop_obj = opendaq.PropertyObjectWithClassAndManager(manager, opendaq.String('MyClass'))
# Prints "foo"
print(prop_obj.get_property_value('InheritedProp'))
# Prints "bar"
print(prop_obj.get_property_value('OwnProp'))
Property events on classes
As stated in the events section, users should be careful when adding events to Properties that are part of classes, as the same callback will be invoked independently of the Property object that triggered the event.
The example below illustrates a possible case of error-prone behaviour.
int readCount = 0;
auto intProp = IntProperty("ReadCount", 0);
intProp.getOnPropertyValueRead() +=
[&](PropertyObjectPtr& sender, PropertyValueEventArgsPtr& args)
{
readCount++;
sender.asPtr<IPropertyObjectProtected>().setProtectedPropertyValue("ReadCount", readCount + 1);
args.setValue(readCount);
};
TypeManagerPtr manager = TypeManager();
PropertyObjectClassBuilderPtr propClass = PropertyObjectClassBuilder(manager, "MyClass").addProperty(intProp);
manager.addClass(propClass.build());
auto propObj1 = PropertyObject(manager, "MyClass");
auto propObj2 = PropertyObject(manager, "MyClass");
// Prints out 1
std::cout << propObj1.getPropertyValue("ReadCount") << std::endl;
// Prints out 2
std::cout << propObj2.getPropertyValue("ReadCount") << std::endl;