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: .. literalinclude:: ../examples/empty.py This example is already a working (empty) device. It is possible to run it without a database using the `tango.test_context` module: .. sourcecode:: console $ 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: .. literalinclude:: ../examples/on.py :pyobject: 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. .. literalinclude:: ../examples/counter1.py :pyobject: Counter1 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: .. literalinclude:: ../examples/counter2.py :pyobject: Counter2 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: .. literalinclude:: ../examples/counter3.py :pyobject: Counter3 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: .. literalinclude:: ../examples/division1.py :pyobject: Division1 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 : 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: .. literalinclude:: ../examples/division2.py :pyobject: Division2 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: .. sourcecode:: console $ 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: .. literalinclude:: ../examples/average.py :pyobject: Average 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: .. literalinclude:: ../examples/commands.py :pyobject: Commands 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.