Attributes#
Running out of coffee is dangerous! You need to monitor the level of beans. And to get the perfect cup, you’ll tweak the brewing temperature and coarseness of the grind. In Tango, reading and writing a value like that is done with an attribute.
Attributes have a name and a data type. They can be read-only, read/write or write-only. Similar to commands, there is a pre-defined list of data types, so arbitrarily complex types are not supported.
from enum import IntEnum
from tango import AttrWriteType
from tango.server import Device, attribute
class GrinderSetting(IntEnum):
FINE = 0
MEDIUM = 1
COARSE = 2
class MegaCoffee3k(Device):
def init_device(self):
super().init_device()
self._water_level = 54.2
self._bean_levels = [82.5, 100.0]
self._brewing_temperature = 94.4
self._grind = GrinderSetting.FINE
# decorator-style attributes -------------
@attribute
def waterLevel(self) -> float:
print("reading water level")
return self._water_level
@attribute(max_dim_x=2)
def beanLevels(self) -> list[float]:
print("reading bean levels")
return self._bean_levels
@attribute(
dtype=[float], max_dim_x=2, doc="How full is each bean dispenser", unit="%"
)
def beanLevelsDoc(self):
return self._bean_levels
@attribute
def grind(self) -> GrinderSetting:
"""Setting for the coffee bean grinder"""
print("reading grinder setting")
return self._grind
@grind.setter
def grind(self, value: int):
self._grind = GrinderSetting(value)
print(f"Set grinder to {self._grind}")
# non-decorator style attributes -------------
brewingTemperature = attribute(access=AttrWriteType.READ_WRITE, doc="Temperature to brew coffee at [deg C]")
def read_brewingTemperature(self) -> float:
return self._brewing_temperature
def write_brewingTemperature(self, temperature: float):
self._brewing_temperature = temperature
if __name__ == "__main__":
MegaCoffee3k.run_server()
Sorry, still TODO!
Sorry, still TODO!
In Python, you need to import attribute
and then use that to decorate a method on the Device
, or use it to create objects (non-decorator syntax), and link those to methods. In other languages, it is a little more complicated.
You have the following attributes:
waterLevel
a read-only float. It is a scalar value (in other words, zero-dimensional, or 0-D).beanLevels
is also read-only, but returns a list of up to two floats. In Tango 1-D arrays are called spectrum attributes, and 2-D arrays are image attributes.beanLevelsDoc
shows how documentation and the units can be defined.grind
is a read-write, scalar attribute using an enumeration data type. There is one method handling reading, and one handling writing. The docstring for the read method is an alternative to thedoc
keyword argument.brewingTemperature
is defined using assignment rather than decorator syntax. It is a read-write scalar float. The pattern for naming the methods is critical:read_
andwrite_
, followed by the attribute name.
The attribute names use capitalisation as per the Tango Naming Rules.
Tip
Some names are bad choice for attributes:
Init
,State
, andStatus
already exist as commands.Methods that already exist on the
DeviceProxy
class, including:alias
,connect
,description
,info
,lock
,name
,ping
,reconnect
,unlock
,get_...
,set_...
,is_...
,put_
,read_...
,write_...
, etc.Anything starting with an underscore,
_
.
Run this example, and in a second terminal, use the device proxy client to check if it is working :
>>> dp.waterLevel
54.2
>>> dp.beanLevels
array([ 82.5, 100. ])
>>> dp.grind
<grind.FINE: 0>
>>> dp.grind = "MEDIUM"
>>> dp.grind
<grind.MEDIUM: 1>
>>> dp.brewingTemperature
94.4
>>> dp.brewingTemperature = 95.1
>>> dp.brewingTemperature
95.1
Getting the value by reading the attribute name directly on the DeviceProxy
object is a convenience. There is actually a lot more information available when an attribute is read. You can also use the more low-level read_attribute()
and write_attribute()
methods to see the full data structure:
>>> reading = dp.read_attribute("waterLevel")
>>> print(reading)
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.SCALAR
dim_x = 1
dim_y = 0
has_failed = False
is_empty = False
name = "waterLevel"
nb_read = 1
nb_written = 0
quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension[
dim_x = 1
dim_y = 0
]
time = TimeVal(tv_nsec = 0, tv_sec = 1743749828, tv_usec = 308868)
type = tango._tango.CmdArgType.DevDouble
value = 54.2
w_dim_x = 0
w_dim_y = 0
w_dimension = AttributeDimension[
dim_x = 0
dim_y = 0
]
w_value = None
]
>>> reading.value
54.2
>>> dp.write_attribute("brewingTemperature", 94.9)
>>> dp.brewingTemperature
94.9
As a high-level convenience, you can also get the struct using indexed access in Python:
>>> reading = dp["waterLevel"]
>>> print(reading)
DeviceAttribute[
data_format = tango._tango.AttrDataFormat.SCALAR
dim_x = 1
dim_y = 0
has_failed = False
is_empty = False
name = "waterLevel"
nb_read = 1
nb_written = 0
quality = tango._tango.AttrQuality.ATTR_VALID
r_dimension = AttributeDimension[
dim_x = 1
dim_y = 0
]
time = TimeVal(tv_nsec = 0, tv_sec = 1743749877, tv_usec = 460608)
type = tango._tango.CmdArgType.DevDouble
value = 54.2
w_dim_x = 0
w_dim_y = 0
w_dimension = AttributeDimension[
dim_x = 0
dim_y = 0
]
w_value = None
]
Tango is case insensitive when accessing attributes by name, so all of the following calls access the same attribute:
>>> dp.brewingTemperature
95.1
>>> dp.BREWINGTemperature
95.1
>>> dp.brewingtemperature
95.1
>>> dp.read_attribute("BreWingTemPeraTure").value
95.1
>>> dp["BreWingTemPeraTure"].value
95.1
The documentation for each attribute is available to the client:
>>> dp.get_attribute_config("beanLevels").description
'No description'
>>> dp.get_attribute_config("beanLevelsDoc").description
'How full is each bean dispenser'
>>> dp.get_attribute_config("grind").description
'Setting for the coffee bean grinder'
The get_attribute_config()
method provides all the details about an attribute:
>>> config = dp.get_attribute_config("brewingTemperature")
>>> print(config)
AttributeInfoEx[
alarms = AttributeAlarmInfo[
delta_t = "Not specified"
delta_val = "Not specified"
extensions = []
max_alarm = "Not specified"
max_warning = "Not specified"
min_alarm = "Not specified"
min_warning = "Not specified"
]
data_format = tango._tango.AttrDataFormat.SCALAR
data_type = tango._tango.CmdArgType.DevDouble
description = "Temperature to brew coffee at [deg C]"
disp_level = tango._tango.DispLevel.OPERATOR
display_unit = "No display unit"
enum_labels = []
events = AttributeEventInfo[
arch_event = ArchiveEventInfo[
archive_abs_change = "Not specified"
archive_period = "Not specified"
archive_rel_change = "Not specified"
extensions = []
]
ch_event = ChangeEventInfo[
abs_change = "Not specified"
extensions = []
rel_change = "Not specified"
]
per_event = PeriodicEventInfo[
extensions = []
period = "1000"
]
]
extensions = []
format = "%6.2f"
label = "brewingTemperature"
max_alarm = "Not specified"
max_dim_x = 1
max_dim_y = 0
max_value = "Not specified"
memorized = tango._tango.AttrMemorizedType.NONE
min_alarm = "Not specified"
min_value = "Not specified"
name = "brewingTemperature"
root_attr_name = "Not specified"
standard_unit = "No standard unit"
sys_extensions = []
unit = ""
writable = tango._tango.AttrWriteType.READ_WRITE
writable_attr_name = "brewingTemperature"
]
To simplify the implementation of all clients and servers, the data types available to attributes are limited:
simple types: integer, float, string, boolean, enum
1-D lists of simple types (spectrum)
2-D lists of simple types (image)
special structures:
an encoded byte array, with string indicating the format:
DevEncoded
Tip
For more examples of using Python type hints for declaring attributes see device server type hints.
Tip
For more complicated input and output data structures, it is common to use a string that is serialised and de-serialised using JSON. This allows structures like dicts to be passed between client and server. The downside is that the schema of those dicts is not obvious.
Tip
You can easily get a list of all the attributes a Tango device offers:
>>> dp.get_attribute_list()
['waterLevel', 'beanLevels', 'beanLevelsDoc', 'grind', 'brewingTemperature', 'State', 'Status']
State and Status are special, and show up as commands and attributes. Normally we access them as commands.
Note
Python limits/simplifies the data types that can be used, compared to C++ and Java. For PyTango, here is the full list of the data types.
You’ve already learned quite a lot of the basics! Next up is a way to configure persistent settings.