Skip to content
Snippets Groups Projects
Commit b5f60640 authored by Jan David Mol's avatar Jan David Mol
Browse files

Merge branch 'L2SS-622-store-attribute-io-per-device' into 'master'

L2SS-622: Add an explicit per-device store for each attribute

Closes L2SS-622

See merge request !378
parents 06e86986 3ea7bb29
No related branches found
No related tags found
1 merge request!378L2SS-622: Add an explicit per-device store for each attribute
Showing
with 102 additions and 126 deletions
......@@ -17,10 +17,7 @@ Inside lofar/tango/tangostationcontrol/tangostationcontrol/devices/lofar_device.
**kwargs: any other non attribute_wrapper arguments.
NOTE: the `__init__` function contains wrappers for the unassigned read/write functions. In previous versions the read function of an RW attribute used to return the last value it had written *to* the client instead of the value from the client. This has since been changed.
`initial_value`:
This function fills the attribute with a default value of all zero's with the proper dimensions and type if None is specified.
`Set_comm_client`:
`set_comm_client`:
This function can be called to assign a read and write function to the attribute using the data accessor or client given to this function. The attribute wrapper assumes the client is running and has a function called ‘setup_attribute’ which will provide it with a valid read/write function.
`async_set_comm_client`:
......@@ -29,14 +26,6 @@ This function can be called to assign a read and write function to the attribute
`set_pass_func`:
Can be called to assign a 'fake' read/write function. This is useful as a fallback option while development is still ongoing.
`_decorate_read_function`:
Wrap an attribute read function to annotate its exceptions with our comms_annotation to be able to identify which attribute triggered the error.
`_decorate_write_function`:
Wrap an attribute write function to annotate its exceptions with our comms_annotation to be able to identify which attribute triggered the error.
......
from tango.server import attribute
from tango import AttrWriteType
import numpy
from tangostationcontrol.devices.device_decorators import fault_on_error
import logging
......@@ -8,12 +7,46 @@ import logging
logger = logging.getLogger()
class attribute_io(object):
""" Holds the I/O functionality for an attribute for a specific device. """
def __init__(self, device, attribute_wrapper):
# Link to the associated device
self.device = device
# Link to the associated attribute wrapper
self.attribute_wrapper = attribute_wrapper
# Link to last (written) value
self.cached_value = None
# Specific read and write functions for this attribute on this device
self.read_function = lambda: None
self.write_function = lambda value: None
def cached_read_function(self):
""" Return the last (written) value, if available. Otherwise, read
from the device. """
if self.cached_value is not None:
return self.cached_value
self.cached_value = self.read_function()
return self.cached_value
def cached_write_function(self, value):
""" Writes the given value to the device, and updates the cache. """
self.write_function(value)
self.cached_value = value
class attribute_wrapper(attribute):
"""
Wraps all the attributes in a wrapper class to manage most of the redundant code behind the scenes
"""
def __init__(self, comms_id=None, comms_annotation=None, datatype=None, dims=(1,), access=AttrWriteType.READ, init_value=None, **kwargs):
def __init__(self, comms_id=None, comms_annotation=None, datatype=None, dims=(1,), access=AttrWriteType.READ, **kwargs):
"""
wraps around the tango Attribute class. Provides an easier interface for 1d or 2d arrays. Also provides a way to abstract
managing the communications interface.
......@@ -34,43 +67,43 @@ class attribute_wrapper(attribute):
self.comms_id = comms_id # store data that can be used to identify the comms interface to use. not used by the wrapper itself
self.comms_annotation = comms_annotation # store data that can be used by the comms interface. not used by the wrapper itself
self.init_value = init_value
is_scalar = dims == (1,)
self.datatype = datatype
self.numpy_type = datatype # tango changes our attribute to their representation (E.g numpy.int64 becomes "DevLong64")
# check if not scalar
if is_scalar:
# scalar, just set the single dimension.
if dims == (1,):
# Tango defines a scalar as having dimensions (1,0), see https://pytango.readthedocs.io/en/stable/server_api/attribute.html
max_dim_x = 1
max_dim_y = 0
else:
# get first dimension
dtype = datatype
elif len(dims) == 1:
max_dim_x = dims[0]
max_dim_y = 0
dtype = (datatype,)
elif len(dims) == 2:
max_dim_x = dims[0]
# single dimension/spectrum requires the datatype to be wrapped in a tuple
datatype = (datatype,)
if len(dims) == 2:
# get second dimension
max_dim_y = dims[1]
# wrap the datatype tuple in another tuple for 2d arrays/images
datatype = (datatype,)
dtype = ((datatype,),)
else:
max_dim_y = 0
raise ValueError(f"Only up to 2D arrays are supported. Supplied dimensions: {dims}")
if access == AttrWriteType.READ_WRITE:
""" If the attribute is of READ_WRITE type, assign the write and read functions to it"""
# we return the last written value, as we are the only ones in control,
# and the hardware does not necessarily return what we've written
# (see L2SDP-725).
@fault_on_error()
def write_func_wrapper(device, value):
"""
write_func_wrapper writes a value to this attribute
"""
self.write_function(value)
device.value_dict[self] = value
try:
io = self.get_attribute_io(device)
return io.cached_write_function(value)
except Exception as e:
raise e.__class__(f"Could not write attribute {comms_annotation}") from e
@fault_on_error()
def read_func_wrapper(device):
......@@ -78,16 +111,9 @@ class attribute_wrapper(attribute):
read_func_wrapper reads the attribute value, stores it and returns it"
"""
try:
# we return the last written value, as we are the only ones in control,
# and the hardware does not necessarily return what we've written
# (see L2SDP-725).
if self in device.value_dict:
return device.value_dict[self]
io = self.get_attribute_io(device)
# value was never written, so obtain current one and cache that instead
value = self.read_function()
device.value_dict[self] = value
return value
return io.cached_read_function()
except Exception as e:
raise e.__class__(f"Could not read attribute {comms_annotation}") from e
......@@ -102,7 +128,9 @@ class attribute_wrapper(attribute):
read_func_wrapper reads the attribute value, stores it and returns it"
"""
try:
return self.read_function()
io = self.get_attribute_io(device)
return io.read_function()
except Exception as e:
raise e.__class__(f"Could not read attribute {comms_annotation}") from e
......@@ -114,60 +142,20 @@ class attribute_wrapper(attribute):
#
# NOTE: fisallowed=<callable> does not work: https://gitlab.com/tango-controls/pytango/-/issues/435
# So we have to use fisallowed=<str> here, which causes the function device.<str> to be called.
super().__init__(dtype=datatype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, access=access, fisallowed="is_attribute_access_allowed", **kwargs)
super().__init__(dtype=dtype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, access=access, fisallowed="is_attribute_access_allowed", **kwargs)
def initial_value(self):
def get_attribute_io(self, device):
"""
returns a numpy array filled with zeroes fit to the size of the attribute. Or if init_value is not the default None, return that value
returns the attribute I/O functions from a certain device, or registers it if not present
"""
if self.init_value is not None:
return self.init_value
if self.dim_y > 1:
dims = (self.dim_x, self.dim_y)
else:
dims = (self.dim_x,)
# x and y are swapped for numpy and Tango. to maintain tango conventions, x and y are swapped for numpy
if len(dims) == 2:
numpy_dims = tuple((dims[1], dims[0]))
else:
numpy_dims = dims
if self.dim_x == 1:
if self.numpy_type == str:
value = ''
else:
value = self.numpy_type(0)
else:
value = numpy.zeros(numpy_dims, dtype=self.numpy_type)
return value
def _decorate_read_function(self, read_attr_func):
""" Wrap an attribute read function to annotate its exceptions with our
comms_annotation to be able to identify which attribute triggered the error. """
def wrapper():
try:
return read_attr_func()
except Exception as e:
raise Exception(f"Failed to read attribute {self.comms_annotation}") from e
return wrapper
def _decorate_write_function(self, write_attr_func):
""" Wrap an attribute write function to annotate its exceptions with our
comms_annotation to be able to identify which attribute triggered the error. """
def wrapper(value):
try:
write_attr_func(value)
except Exception as e:
raise Exception(f"Failed to write attribute {self.comms_annotation}") from e
return device._attribute_wrapper_io[self]
except KeyError:
device._attribute_wrapper_io[self] = attribute_io(device, self)
return device._attribute_wrapper_io[self]
return wrapper
def set_comm_client(self, client):
def set_comm_client(self, device, client):
"""
takes a communications client as input arguments This client should be of a class containing a "get_mapping" function
and return a read and write function that the wrapper will use to get/set data.
......@@ -175,29 +163,28 @@ class attribute_wrapper(attribute):
try:
read_attr_func, write_attr_func = client.setup_attribute(self.comms_annotation, self)
self.read_function = self._decorate_read_function(read_attr_func)
self.write_function = self._decorate_write_function(write_attr_func)
io = self.get_attribute_io(device)
io.read_function = read_attr_func
io.write_function = write_attr_func
except Exception as e:
raise Exception(f"Exception while setting {client.__class__.__name__} attribute with annotation: '{self.comms_annotation}'") from e
async def async_set_comm_client(self, client):
async def async_set_comm_client(self, device, client):
"""
Asynchronous version of set_comm_client.
"""
try:
read_attr_func, write_attr_func = await client.setup_attribute(self.comms_annotation, self)
self.read_function = self._decorate_read_function(read_attr_func)
self.write_function = self._decorate_write_function(write_attr_func)
io = self.get_attribute_io(device)
io.read_function = read_attr_func
io.write_function = write_attr_func
except Exception as e:
raise Exception(f"Exception while setting {client.__class__.__name__} attribute with annotation: '{self.comms_annotation}'") from e
def set_pass_func(self):
def pass_func(value=None):
pass
def set_pass_func(self, device):
logger.debug("using pass function for attribute with annotation: {}".format(self.comms_annotation))
self.read_function = pass_func
self.write_function = pass_func
io = self.get_attribute_io(device)
io.read_function = lambda: None
io.write_function = lambda value: None
......@@ -175,7 +175,7 @@ class OPCUAConnection(AsyncCommClient):
# get all the necessary data to set up the read/write functions from the attribute_wrapper
dim_x = attribute.dim_x
dim_y = attribute.dim_y
ua_type = numpy_to_OPCua_dict[attribute.numpy_type] # convert the numpy type to a corresponding UA type
ua_type = numpy_to_OPCua_dict[attribute.datatype] # convert the numpy type to a corresponding UA type
# configure and return the read/write functions
prot_attr = ProtocolAttribute(node, dim_x, dim_y, ua_type)
......
......@@ -94,7 +94,7 @@ class SNMP_client(CommClient):
dim_x = attribute.dim_x
dim_y = attribute.dim_y
dtype = attribute.numpy_type
dtype = attribute.datatype
return dim_x, dim_y, dtype
......
......@@ -116,7 +116,7 @@ class Docker(lofar_device):
async def _connect_docker(self):
# tie attributes to client
for i in self.attr_list():
await i.async_set_comm_client(self.docker_client)
await i.async_set_comm_client(self, self.docker_client)
await self.docker_client.start()
......
......@@ -88,10 +88,10 @@ class lofar_device(Device, metaclass=DeviceMeta):
""" Return a list of all the attribute_wrapper members of this class. """
return [v for k, v in cls.__dict__.items() if type(v) == attribute_wrapper]
def setup_value_dict(self):
""" set the initial value for all the attribute wrapper objects"""
def setup_attribute_wrapper(self):
""" prepare the caches for attribute wrapper objects"""
self.value_dict = {i: i.initial_value() for i in self.attr_list()}
self._attribute_wrapper_io = {}
def is_attribute_access_allowed(self, req_type):
""" Returns whether an attribute wrapped by the attribute_wrapper be accessed. """
......@@ -153,7 +153,7 @@ class lofar_device(Device, metaclass=DeviceMeta):
self.set_state(DevState.INIT)
self.set_status("Device is in the INIT state.")
self.setup_value_dict()
self.setup_attribute_wrapper()
# reload our class & device properties from the Tango database
self.get_device_properties()
......
......@@ -105,10 +105,10 @@ class opcua_device(lofar_device):
for i in self.attr_list():
try:
if not i.comms_id or i.comms_id == OPCUAConnection:
await i.async_set_comm_client(self.opcua_connection)
await i.async_set_comm_client(self, self.opcua_connection)
except Exception as e:
# use the pass function instead of setting read/write fails
i.set_pass_func()
i.set_pass_func(self)
self.opcua_missing_attributes.append(",".join(self.opcua_connection.get_node_path(i.comms_annotation)))
logger.warning(f"Error while setting the attribute {i.comms_annotation} read/write function.", exc_info=True)
......
......@@ -100,10 +100,10 @@ class PSOC(lofar_device):
# map an access helper class
for i in self.attr_list():
try:
i.set_comm_client(self.snmp_manager)
i.set_comm_client(self, self.snmp_manager)
except Exception as e:
# use the pass function instead of setting read/write fails
i.set_pass_func()
i.set_pass_func(self)
logger.warning("error while setting the SNMP attribute {} read/write function. {}".format(i, e))
self.snmp_manager.start()
......
......@@ -133,10 +133,10 @@ class Statistics(opcua_device):
for i in self.attr_list():
try:
if i.comms_id == StatisticsClient:
await i.async_set_comm_client(self.statistics_client)
await i.async_set_comm_client(self, self.statistics_client)
except Exception as e:
# use the pass function instead of setting read/write fails
i.set_pass_func()
i.set_pass_func(self)
logger.warning("error while setting the sst attribute {} read/write function. {}. using pass function instead".format(i, e))
await self.statistics_client.start()
......
......@@ -88,7 +88,7 @@ class TestClientServer(base.IntegrationAsyncTestCase):
class attribute(object):
dim_x = 1
dim_y = 0
numpy_type = numpy.double
datatype = numpy.double
prot_attr = await test_client.setup_protocol_attribute(["double_R"], attribute())
......@@ -108,7 +108,7 @@ class TestClientServer(base.IntegrationAsyncTestCase):
class attribute(object):
dim_x = 1
dim_y = 0
numpy_type = numpy.double
datatype = numpy.double
prot_attr = await test_client.setup_protocol_attribute(["double_RW"], attribute())
......
......@@ -36,7 +36,7 @@ def dev_init(device):
device.set_state(DevState.INIT)
device.test_client = test_client(device.Fault)
for i in device.attr_list():
asyncio.run(i.async_set_comm_client(device.test_client))
asyncio.run(i.async_set_comm_client(device, device.test_client))
device.test_client.start()
......
......@@ -65,7 +65,7 @@ class test_client(CommClient):
else:
dims = (attribute.dim_x,)
dtype = attribute.numpy_type
dtype = attribute.datatype
return dims, dtype
......
......@@ -93,7 +93,7 @@ class TestOPCua(base.AsyncTestCase):
for i in ATTR_TEST_TYPES:
class mock_attr:
def __init__(self, dtype, x, y):
self.numpy_type = dtype
self.datatype = dtype
self.dim_x = x
self.dim_y = y
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment