diff --git a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py index ae00635acf68e059c62faf7a5b8d27a2ca435eec..ee898ff561ea916431a7100f098069c68b4e5a13 100644 --- a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py +++ b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py @@ -1,6 +1,7 @@ from operator import mul from functools import reduce +import numpy from tango.server import attribute from tango import AttrWriteType, AttReqType @@ -40,6 +41,21 @@ class attribute_io(object): def cached_write_function(self, value): """ Writes the given value to the device, and updates the cache. """ + # flexible array sizes are not supported by all clients. make sure we only write arrays of maximum size. + if self.attribute_wrapper.shape != (): + if isinstance(value, numpy.ndarray): + value_shape = value.shape + else: + if len(value) > 0 and isinstance(value[0], list): + # nested list + value_shape = (len(value), len(value[0])) + else: + # straight list + value_shape = (len(value),) + + if value_shape != self.attribute_wrapper.shape: + raise ValueError(f"Tried writing an array of shape {value_shape} into an attribute of shape {self.attribute_wrapper.shape}") + self.write_function(value) self.cached_value = value @@ -77,20 +93,27 @@ class attribute_wrapper(attribute): max_dim_x = 1 max_dim_y = 0 dtype = datatype + shape = () elif len(dims) == 1: max_dim_x = dims[0] max_dim_y = 0 dtype = (datatype,) + shape = (max_dim_x,) elif len(dims) == 2: max_dim_x = dims[1] max_dim_y = dims[0] dtype = ((datatype,),) + shape = (max_dim_y, max_dim_x) else: # >2D arrays collapse into the X and Y dimensions. The Y (major) dimension mirrors the first dimension given, the # rest collapses into the X (minor) dimension. max_dim_x = reduce(mul, dims[1:]) max_dim_y = dims[0] dtype = ((datatype,),) + shape = (max_dim_y, max_dim_x) + + # actual shape of the data as it is read/written + self.shape = shape if access == AttrWriteType.READ_WRITE: """ If the attribute is of READ_WRITE type, assign the write and read functions to it""" diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py index cfc90c25c7b06e61d7e0dca9f67cc0a46daac281..6381db2d7c4cc6e02cf891b761addacfe37064f1 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py @@ -39,7 +39,6 @@ def dev_init(device): asyncio.run(i.async_set_comm_client(device, device.test_client)) device.test_client.start() - class TestAttributeTypes(base.TestCase): def setUp(self): # Avoid the device trying to access itself as a client @@ -491,7 +490,6 @@ class TestAttributeTypes(base.TestCase): info = "Test failure in {} {} readback test \n\tW: {} \n\tRW: {} \n\tR: {}".format(test_type, dtype, val, result_RW, result_R) raise Exception(info) from e - """ List of different types to be used with attributes testing, using any other might have unexpected results. Each type is bound to a device scalar, @@ -629,6 +627,9 @@ class TestAttributeAccess(base.TestCase): scalar_R = attribute_wrapper(comms_annotation="float32_scalar_R", datatype=numpy.float32) scalar_RW = attribute_wrapper(comms_annotation="float32_scalar_RW", datatype=numpy.float32, access=AttrWriteType.READ_WRITE) + spectrum_RW = attribute_wrapper(comms_annotation="spectrum_RW", dims=(3,), datatype=numpy.float32, access=AttrWriteType.READ_WRITE) + image_RW = attribute_wrapper(comms_annotation="image_RW", dims=(3,2), datatype=numpy.float32, access=AttrWriteType.READ_WRITE) + def configure_for_initialise(self): dev_init(self) self.set_state(DevState.STANDBY) @@ -643,3 +644,30 @@ class TestAttributeAccess(base.TestCase): proxy.initialise() _ = proxy.scalar_R + + def test_write_correct_dims_spectrum(self): + with DeviceTestContext(self.float32_scalar_device, process=True) as proxy: + proxy.initialise() + + proxy.spectrum_RW = [1.0, 2.0, 3.0] + + def test_write_wrong_dims_spectrum(self): + with DeviceTestContext(self.float32_scalar_device, process=True) as proxy: + proxy.initialise() + + with self.assertRaises(DevFailed): + proxy.spectrum_RW = [1.0] + + def test_write_correct_dims_image(self): + with DeviceTestContext(self.float32_scalar_device, process=True) as proxy: + proxy.initialise() + + proxy.image_RW = [[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]] + + def test_write_wrong_dims_image(self): + with DeviceTestContext(self.float32_scalar_device, process=True) as proxy: + proxy.initialise() + + with self.assertRaises(DevFailed): + proxy.image_RW = [[1.0, 2.0], [2.0, 3.0]] + diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_client.py b/tangostationcontrol/tangostationcontrol/test/clients/test_client.py index 245e03eff42a5592fa3a36673a2d1e01405e680f..4f3a0c5d2eda256bc089ebd58c4dbc587beb08cd 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_client.py +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_client.py @@ -23,6 +23,9 @@ class test_client(CommClient): """ super().__init__(fault_func, try_interval) + # holder for the values of all attributes + self.values = {} + # Explicitly connect self.connect() @@ -69,26 +72,32 @@ class test_client(CommClient): return dims, dtype - def _setup_mapping(self, dims, dtype): + def _setup_mapping(self, annotation, dims, dtype): """ takes all gathered data to configure and return the correct read and write functions """ + + # we emulate that values written to annotations ending in _RW show up in their corresponding _R + # point as well + if annotation.endswith("_RW"): + annotation = annotation[:-1] + if dtype == str and dims == (1,): - self.value = '' + self.values[annotation] = '' elif dims == (1,): - self.value = dtype(0) + self.values[annotation] = dtype(0) else: - self.value = numpy.zeros(dims, dtype) + self.values[annotation] = numpy.zeros(dims, dtype) + def read_function(): - logger.debug("from read_function, reading {} array of type {}".format(dims, dtype)) - return self.value + logger.debug("from read_function, reading {}: {} array of type {} == {}".format(annotation, dims, dtype, self.values[annotation])) + return self.values[annotation] def write_function(write_value): - logger.debug("from write_function, writing {} array of type {}".format(dims, dtype)) + logger.debug("from write_function, writing {}: {} array of type {}".format(annotation, dims, dtype)) - self.value = write_value - return + self.values[annotation] = write_value logger.debug("created and bound example_client read/write functions to attribute_wrapper object") return read_function, write_function @@ -106,7 +115,7 @@ class test_client(CommClient): dims, dtype = self._setup_value_conversion(attribute) # configure and return the read/write functions - read_function, write_function = self._setup_mapping(dims, dtype) + read_function, write_function = self._setup_mapping(annotation, dims, dtype) # return the read/write functions return read_function, write_function