Tutorial
This tutorial goes through most of the library features by presenting several facade devices with increasing complexity.
Creating and running a facade device
A facade device is an enhanced pytango HLAPI device. It provides the same methods and supports the same pytango object (device properties, attributes, commands, etc.). In order to create a new facade device class, simply inherit from the Facade base class:
from facadedevice import Facade
class Empty(Facade):
pass
if __name__ == "__main__":
Empty.run_server()
This example is already a working (empty) device. It is possible to run it without a database using the tango.test_context module:
$ cd examples/
$ python -m tango.test_context examples.empty.Empty --debug=3
Ready to accept request
Empty started on port 8888 with properties {}
Device access: tango://hostname:8888/test/nodb/empty#dbase=no
Server access: tango://hostname:8888/dserver/Empty/empty#dbase=no
It is now accessible through itango:
In [1]: d = Device('tango://hostname:8888/test/nodb/empty#dbase=no')
In [2]: d.state()
Out[2]: tango._tango.DevState.UNKNOWN
Adding extra logic at initialization
The default state is UNKNOWN. Since facade devices are regular devices, we can change it using the set_state method. However, the init_device method shouldn’t be overridden because it performs specific exception handling. Instead, override safe_init_device if you have to add some extra logic. Don’t forget to call the parent method since it performs other useful steps:
class On(Facade):
def safe_init_device(self):
super().safe_init_device()
self.set_state(DevState.ON)
Now let’s check the state:
In [2]: d.state()
Out[2]: tango._tango.DevState.ON
Local attributes
Good, but a static state is not really useful. Instead we’d like it to react to the values of other attributes. Let’s create a device with a local counter using the local_attribute object.
class Counter1(Facade):
@local_attribute(
dtype=int,
access=AttrWriteType.READ_WRITE,
)
def count(self):
return 0
Note that local_attribute can be used as a decorator to set a default value for the attribute, although it is not mandatory. Also, local_attribute (and other facade-specific attributes) supports all the arguments of the standard pytango attribute object (e.g. access and dtype in the example above). Now let’s try our counter:
In [2]: d.count
Out[2]: 0
In [3]: d.count += 1
In [4]: d.count
Out[4]: 1
See how the count attribute has been incremented successfully. Also note that the facade devices have a full support for events, meaning a change event has been pushed when the count value has been updated (no polling is required on the attribute).
Data model
Now, instead of a writable attribute, we’d like to use a command to increment the value of count. But first, we need to learn about the data model that allows reactivity and the propagation of changes. Every facade device instance has a graph of nodes that represents the different values that the device has to manage. For instance, every local attribute has a corresponding node that can be accessed through self.graph[attr_name]. A node can contain either:
nothing
a triplet result (value, stamp, quality)
an exception
Accessing the node state is done through the following methods:
node.result() == None if the node contains nothing
value, stamp, quality = node.result() if the node contains a result
node.exception() == None if the node doesn’t contain an exception
exc = node.exception() if the node contains an exception
Also note that calling node.result() on a node containing an exception will raise the corresponding exception. The node state is set using the following methods:
node.set_result(None)
node.set_result(triplet(value, stamp, quality))
node.set_exception(exc)
Note that stamp and quality are optional. They respectively default to the current time and the VALID quality. The increment tango command can now be implemented:
class Counter2(Facade):
@local_attribute(dtype=int)
def count(self):
return 0
@command
def increment(self):
node = self.graph["count"]
value, stamp, quality = node.result()
new_result = triplet(value + 1)
node.set_result(new_result)
Let’s give it a try:
In [2]: d.count
Out[2]: 0
In [3]: d.increment()
In [4]: d.count
Out[4]: 1
State attribute
Now, we’d like to have the state react to the value of count. This can be achieved using the state_attribute facade object. It is used as a decorator and takes the list of the nodes to bind to as an argument:
class Counter3(Facade):
@local_attribute(dtype=int)
def count(self):
return 0
@command
def increment(self):
node = self.graph["count"]
value, stamp, quality = node.result()
new_result = triplet(value + 1)
node.set_result(new_result)
@state_attribute(bind=["count"])
def state_and_status(self, count):
if count == 0:
return DevState.OFF, "The count is 0"
return DevState.ON, f"The count is {count}"
Note that it’s possible to return the status along with the state, although it is not mandatory. Let’s run the counter:
In [2]: d.state()
Out[2]: tango._tango.DevState.OFF
In [3]: d.status()
Out[3]: 'The count is 0'
In [4]: d.increment()
In [5]: d.state()
Out[5]: tango._tango.DevState.ON
In [6]: d.status()
Out[6]: 'The count is 1'
See how the state is updated automatically. Remember that there is no polling or periodic update involved: the changes are simply propagated through the device graph.
Logical attributes
State and Status are not the only attributes that can react to changes. It is possible to declare logical attributes using the same binding approach. Let’s write a device that performs a division:
class Division1(Facade):
A = local_attribute(dtype=float, access=AttrWriteType.READ_WRITE)
B = local_attribute(dtype=float, access=AttrWriteType.READ_WRITE)
@logical_attribute(dtype=float, bind=["A", "B"])
def C(self, a, b):
return a / b
Here we defined the relationship C = A / B. Note how the arguments of the method C are simply the value A and B. Let’s give it a try:
In [2]: d.A = 1
In [3]: d.B = 4
In [4]: d.C
Out[4]: 0.25
In [5]: d.B = 0
In [6]: d.C
PyDs_PythonError: Exception while updating node <C>:
float division by zero
Remember that the computation of C does not happen when the attribute C is being read but when the values of A and B are changing. For instance, the zero division exception has been set to the node C right after we set B to zero.
They are special rules about aggregation depending on the state of the different input nodes:
if node A or node B is empty, node C is empty too
if node A or node B contains an exception, it’s propagated to C
if the quality of A or the quality of B is invalid, the quality of C is invalid
otherwise, the C method is executed and the return value is used as a result
Note that the return value of the C method can be:
a single value (timestamp and quality are computed from the input nodes)
a triplet result, in order to set the timestamp and/or the quality
The triplet structure
The triplet is a named tuple provided by the facade device. All the node results are guaranteed to be a triplet when they exist. This is how it is used:
from time import time
from tango import AttrQuality
from facadedevice import triplet
# A triplet from a single value
result = triplet(1.)
# A triplet from a value and a stamp
result = triplet(1., stamp=time())
# A triplet from a value and a quality
result = triplet(1, quality=AttrQuality.ATTR_ALARM)
# A triplet from value, a stamp and a quality
result = triplet(1, time(), AttrQuality.ATTR_CHANGING)
# Triplets can be unpacked
value, stamp, quality = result
# The values can be accessed through attributes
result.value, result.stamp, result.quality
The default quality is VALID and the default stamp is the time at the triplet creation. It has another interesting property: a None value will cause the quality to be INVALID and an INVALID quality will cause the value to be None. This is enforced at triplet creation.
Warning
An empty node and a none (invalid) triplet can easily be confused! They are however very different:
node.set_result(None) empty the node
node.set_result(triplet(None)) set an INVALID result with a timestamp
The both behave differently when reading the corresponding attribute or when used as an input node to propagate changes.
Proxy attribute
The division device is working nicely but it doesn’t really communicate with the outside world. More precisely, the A and B might come from another device. In this case, we can simply replace the local attributes with proxy attributes:
class Division2(Facade):
A = proxy_attribute(dtype=float, property_name="AAttribute")
B = proxy_attribute(dtype=float, property_name="BAttribute")
@logical_attribute(dtype=float, bind=["A", "B"])
def C(self, a, b):
return a / b
The only special argument we need to provide a proxy attribute with is property_name: its the name of the device property that will contain the access to the remote attribute. In this case, the device properties could be:
AAttribute: some/device/somewhere/x
BAttribute: some/other/device/y
Those remote attributes are expected to push either change or periodic events. Facade devices have an expert command called GetInfo that provides extra information about the event subscription, e.g:
In [2]: print(d.getinfo())
The device is currently connected.
It subscribed to event channel of the following attribute(s):
- some/device/somewhere/x (CHANGE_EVENT)
- some/other/device/y (PERIODIC_EVENT)
-----
No errors in history since Tue Apr 25 18:26:47 2017 (last initialization).
Once properly set up, any event comming from those remote attributes will cause A (or B) and C to be updated. Note that facade devices can easily be chained together since they both publish and subscribe.
It is also possible to apply a conversion to the input data by using proxy_attribute as a decorator:
@proxy_attribute(
dtype=float,
property_name='AAttribute')
def A(self, a):
return a * 10
Here, the data coming from the event channel is multiplied by 10. Note that the device property can also be a value if the remote attribute doesn’t exist:
$ python -m tango.test_context --prop "{'AAttribute': 1.0, 'BAttribute': 4.0}" \
division2.Division2
Ready to accept request
Division2 started on port 8888 with properties {'AAttribute': 1.0, 'BAttribute': 4.0}
Device access: tango://vinmic-t440p:8888/test/nodb/division2#dbase=no
Server access: tango://vinmic-t440p:8888/dserver/Division2/division2#dbase=no
Let’s check the values:
In [2]: d.A = 1
In [3]: d.B = 4
In [4]: d.C
Out[4]: 0.25
Combined attributes
In some cases, it is interesting to access remote attributes in a more dynamic way. The facadedevice library does not support dymanic attributes directly, but it provides a combined_attributes object that can be used for similar purposes. Let’s say we’d like to compute the average of the values of an arbitrary list of attributes:
class Average(Facade):
@combined_attribute(dtype=float, property_name="AttributesToAverage")
def average(self, *args):
return sum(args) / len(args)
Here, the AttributesToAverage device property is simply the list of all the attributes that should be used for the computation. The attributes may come from the same device, or different devices. If that device property is a single line, it’s used a pattern for listing the attributes. For instance, the pattern a/b/*/x[12] might yield:
a/b/c/x1
a/b/c/x2
a/b/whatever/x1
a/b/whatever/x2
etc.
It includes all the attributes called x1 or x2 from any device starting with a/b/. Note that the aggregation works the same as for logical attributes.
Timestamps for events and attribute readings
Tango provides a timestamp for the value within each event, and when reading attributes. What times will the facade device use? In general, the timestamps come from the triplet’s stamp field. This is set when the node is updated. If you don’t provide a time, the current time is used. When events are emitted, they use the node’s timestamp.
For local_attribute, the timestamp of the event is the time the attribute was written to. However, when reading the attribute, the timestamp will be the current time (by default).
Continuing the example from the Local Attribute section:
In [5]: t1 = d.read_attribute("count").time.totime()
In [6]: t2 = d.read_attribute("count").time.totime()
In [7]: assert t2 > t1
This default behaviour can be changed by creating the local_attribute with use_current_time=False.
State and Status in Tango don’t maintain a timestamp, so state_attribute will use the current time.
For a logical_attribute, the timestamp will be based on the nodes that it aggregates. Note that the aggregation is only done when a dependent node is updated (not on read). With standard aggregation, the latest timestamp of the dependent nodes is used. With custom aggregation, it is up to you. If you provide a triplet, then that time is used. If you return a simple value, then the current time will be used. If a dependent node changes, an event is emitted with time determined as above. Reads of the attribute will not redo the aggregation of value or of quality, however the timestamp is special. If any of the dependent nodes use the current time (e.g., default for local_attribute), then the logical_attribute reading will always report the current time. If none of the dependent nodes use the current time, then the reading will report the time of the last update.
For a proxy_attribute, events are pushed when an event is received from the proxied attribute (i.e., the underlying attribute on a different Tango device that we subscribe to). Such an event will reuse the timestamp of the event pushed from the proxied attribute. Reading the attribute will also report this time, i.e., the timestamp contained in the most recent event received.
For a combined_attribute the aggregation is the same as a logical_attribute, but we are combining multiple proxied attributes. With the standard aggregation, the event timestamp will be the latest timestamp received from the proxied attribute events. Reading the attribute will also report this time, i.e., the most recent timestamp of the events pushed from the proxied attributes.
Why is it useful to use the current time when reading a local_attribute or a logical_attribute that depends on local_attribute?
Control system users like to see time-series graphs with regular updates, even if values haven’t changed for weeks. These graphs typically get their values from an archiving system, which gets timestamps from the Tango device. The archiver relies on periodic updates from the Tango server (reading the attribute via polling). If we keep using the old update time then the archiver cannot store the new data points since they are duplicates.
Static configuration parameters are typically implemented using local_attribute, so they rarely change, thus we want to use the current time for reads. For proxy_attribute and combined_attribute we get updates on events from those devices and they typically represent measurements which change more often due to noise. It is important to keep the original timestamps for these.
Proxy commands
The library also provides an interface for proxy attributes, although it doesn’t use of the concepts explained earlier (graph, node, triplets, etc.). It’s simply a helper to bind a tango command to a command on a remote device. Consider the following example:
class Commands(Facade):
reset = proxy_command(property_name="ResetCommand")
echo = proxy_command(dtype_in=str, dtype_out=str, property_name="EchoCommand")
set_level = proxy_command(
dtype_in=float, property_name="LevelAttribute", write_attribute=True
)
@proxy_command(dtype_in=int, dtype_out=int, property_name="EchoCommand")
def identity(self, subcommand, arg):
return int(subcommand(str(arg)))
The reset command here simply delegates to the ResetCommand provided in the device properties. It has no input argument, no return value, and the remote command is expected to have the same interface.
The echo command delegates to the EchoCommand provided in the device properties by passing the input string argument to the remote command and returning its return value. Again, both interfaces are expected to match (otherwise an exception will be raised at runtime).
It is also possible to write a remote attribute instead of running a remote command. The set_level command does exactly that by setting write_attribute=True. Note that value to write is directly given by the float input argument.
In some cases, we need a finer control over the command behavior. For instance, we might need to apply some conversion before or after running the remote command. It is then possible to use proxy_command as a decorator of a method implementing this extra bit of logic.
The identity command in the code above is one example of that: the remote command can only handle string, while we’d like our command to work with integers. See how the identity method receives the remote command and the input argument, and how it converts the different values to make the types match.