diff --git a/tangostationcontrol/tangostationcontrol/clients/README.md b/tangostationcontrol/tangostationcontrol/clients/README.md index c7085de860b20ade0e8d759f3bbd72d211e452df..9a0c749575c32ad6d582427f154562d97a7fc8a6 100644 --- a/tangostationcontrol/tangostationcontrol/clients/README.md +++ b/tangostationcontrol/tangostationcontrol/clients/README.md @@ -16,11 +16,8 @@ Inside lofar/tango/tangostationcontrol/tangostationcontrol/devices/lofar_device. Init_value: Initialisation value. If none is presents, fills the attribute with zero data of the correct type and dimension **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. - diff --git a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py index 5d9f5bdc8cd123e0ffd28c962548fc94bdf82572..9375b6d5d2b75c5ebfd66b3cab2318e586227b65 100644 --- a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py +++ b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py @@ -1,6 +1,5 @@ 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.numpy_type = datatype # tango changes our attribute to their representation (E.g numpy.int64 becomes "DevLong64") + self.datatype = datatype - # 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] - - # 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,) - else: - max_dim_y = 0 + max_dim_y = 0 + dtype = (datatype,) + elif len(dims) == 2: + max_dim_x = dims[0] + max_dim_y = dims[1] + dtype = ((datatype,),) + else: + 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] - - # value was never written, so obtain current one and cache that instead - value = self.read_function() - device.value_dict[self] = value - return value + io = self.get_attribute_io(device) + + 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 wrapper + try: + return device._attribute_wrapper_io[self] + except KeyError: + device._attribute_wrapper_io[self] = attribute_io(device, self) + return device._attribute_wrapper_io[self] - 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 diff --git a/tangostationcontrol/tangostationcontrol/clients/opcua_client.py b/tangostationcontrol/tangostationcontrol/clients/opcua_client.py index 448b4fc5f1754cb54273140aa240256f4d6ff58d..53211e52fea7472390d82c450543eb8351ca165f 100644 --- a/tangostationcontrol/tangostationcontrol/clients/opcua_client.py +++ b/tangostationcontrol/tangostationcontrol/clients/opcua_client.py @@ -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) diff --git a/tangostationcontrol/tangostationcontrol/clients/snmp_client.py b/tangostationcontrol/tangostationcontrol/clients/snmp_client.py index 4776bd15f922714739c05f8fbb316606e923877f..f75b6bcf4c4f8a7fd26fd85b6d0a4973a4d8d0dc 100644 --- a/tangostationcontrol/tangostationcontrol/clients/snmp_client.py +++ b/tangostationcontrol/tangostationcontrol/clients/snmp_client.py @@ -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 diff --git a/tangostationcontrol/tangostationcontrol/devices/docker_device.py b/tangostationcontrol/tangostationcontrol/devices/docker_device.py index 5066e9305014bb6ce3e98ba69491c01e5e6e8821..b98bf53e95c65a12c5cdbc224f43d4acd674f4de 100644 --- a/tangostationcontrol/tangostationcontrol/devices/docker_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/docker_device.py @@ -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() diff --git a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py index a005ab7aca27f1288b514ca1e677fa64a1bd868b..cba1a6578894cf88de1a6e6fad8b308515187ea1 100644 --- a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py @@ -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() diff --git a/tangostationcontrol/tangostationcontrol/devices/opcua_device.py b/tangostationcontrol/tangostationcontrol/devices/opcua_device.py index eb2e508c35cf6923fb9d121f729465a4eca621e3..c5fa4d1a791bc2511ae93f9e30091c0c1beba2c8 100644 --- a/tangostationcontrol/tangostationcontrol/devices/opcua_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/opcua_device.py @@ -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) diff --git a/tangostationcontrol/tangostationcontrol/devices/psoc.py b/tangostationcontrol/tangostationcontrol/devices/psoc.py index e599563fd4f1ad3120d86da937aac31f27dcc53e..643617cfba8853168cef0ac59911fc744571c2e9 100644 --- a/tangostationcontrol/tangostationcontrol/devices/psoc.py +++ b/tangostationcontrol/tangostationcontrol/devices/psoc.py @@ -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() diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py index 642b1f7bb6ca9e8c873c430d0c11ec65b683d6b2..c9c3d76338d1b8b7da039a4571355c975dbc6f1e 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py @@ -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() diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_opcua_client_against_server.py b/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_opcua_client_against_server.py index 3ce57a0ae8660ac040a3cff0f5ef8680c5909ebc..e16ac79e1f991737910d689b6fdc406dce2a0e3d 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_opcua_client_against_server.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_opcua_client_against_server.py @@ -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()) diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py index fd45244f870ccf0da97f378891638a17447bb28b..485250131cc8221f4881cec6a158816265ad60a4 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py @@ -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() diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_client.py b/tangostationcontrol/tangostationcontrol/test/clients/test_client.py index 577bab69e469fb26af2252790b22f7f92d2c0ade..245e03eff42a5592fa3a36673a2d1e01405e680f 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_client.py +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_client.py @@ -65,7 +65,7 @@ class test_client(CommClient): else: dims = (attribute.dim_x,) - dtype = attribute.numpy_type + dtype = attribute.datatype return dims, dtype diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py b/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py index ab20b238297af55adef923c7417273dc07e57dc6..63daef88819ad97ebc7b0cb6ddf2c7bda6c86a75 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py @@ -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